CSharpGL(11)用C#直接編寫GLSL程序


CSharpGL(11)用C#直接編寫GLSL程序

+BIT祝威+悄悄在此留下版了個權的信息說:

2016-08-13

由於CSharpGL一直在更新,現在這個教程已經不適用最新的代碼了。CSharpGL源碼中包含10多個獨立的Demo,更適合入門參考。

為了盡可能提升渲染效率,CSharpGL是面向Shader的,因此稍有難度。

由來

本項目的目的:使開發者可以直接用C#書寫GLSL代碼。

現在(2016年1月30日)編寫GLSL的shader程序時,並沒有什么好的開發環境。智能提示、代碼補全、自動排版都沒有。基本上我是用notepad++之類的編輯器寫的。

很苦惱,一度導致我對shader有偏見。

GLSL是類似C語言的。我發現幾乎所有的GLSL里出現的語法形式都可以用C#以相同的方式寫出來。那么用C#來寫"GLSL代碼",之后再自動轉換為純粹的GLSL代碼,豈非一大快事?!

在本項目定義的類型基礎上,你就可以直接用C#來寫GLSL代碼了。(只有很少的幾點不同,到時候你會立即明白的。)

C#版的GLSL,以后就稱為CSSL(C# Shader Language)。

下載

這個項目是CSharpGL的一部分,CSharpGL已在GitHub開源,歡迎對OpenGL有興趣的同學加入(https://github.com/bitzhuwei/CSharpGL

+BIT祝威+悄悄在此留下版了個權的信息說:

示例

從一個簡單的例子來抽象出整個項目的設計方案來。

Vertex shader(GLSL)

這是一個典型的vertex shader。

 1 #version 150 core
 2 
 3 in vec3 in_Position;
 4 in vec2 in_UV;  
 5 out vec2 pass_UV;
 6 
 7 uniform mat4 projectionMatrix;
 8 uniform mat4 viewMatrix;
 9 uniform mat4 modelMatrix;
10 
11 void main(void) 
12 {
13     gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(in_Position, 1.0);
14 
15     pass_UV = in_UV;
16 }

對應的C#寫法(CSSL)

我用如下的C#代碼與之對應,並期望將來能夠將其自動轉化為上文的vertex shader。

 1     class DemoVert
 2 {
 3 vec4 gl_Position;
 4 
 5         [In]
 6         vec3 in_Position;
 7         [In]
 8         vec2 in_UV;
 9         [Out]
10         vec2 pass_UV;
11 
12         [Uniform]
13         mat4 projectionMatrix;
14         [Uniform]
15         mat4 viewMatrix;
16         [Uniform]
17         mat4 modelMatrix;
18 
19         void main()
20         {
21             gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(in_Position, 1.0);
22             pass_UV = in_UV;
23         }
24     }

 

Fragment shader(GLSL)

這是一個典型的fragment shader。與上文的vertex shader可以組成一個shader program。

 1 #version 150 core
 2 
 3 in vec2 pass_UV;
 4 out vec4 out_Color;
 5 uniform sampler2D texture1;
 6 uniform sampler2D texture2;
 7 uniform float percent;
 8 
 9 void main(void) 
10 {
11     vec4 color = texture(texture1, pass_UV) * percent + texture(texture2, pass_UV) * (1.0 - percent);
12     out_Color = color;
13     //out_Color = texture(texture2, pass_UV);
14     //out_Color = texture(texture1, pass_UV);
15 }

對應的C#寫法(CSSL)

我用如下的C#代碼與之對應,並期望將來能夠將其自動轉化為上文的fragment shader。

 1     class DemoFrag
 2     {
 3         [In]
 4         vec2 pass_UV;
 5         [Out]
 6         vec4 out_Color;
 7 
 8         [Uniform]
 9         sampler2D texture1;
10         [Uniform]
11         sampler2D texture2;
12         [Uniform]
13         float percent;
14         void main()
15         {
16             vec4 color = texture(texture1, pass_UV) * percent + texture(texture2, pass_UV) * (1.0f - percent);
17             out_Color = color;
18             //out_Color = texture(texture2, pass_UV);
19             //out_Color = texture(texture1, pass_UV);
20         }
21 
22         private vec4 texture(sampler2D texture1, vec2 pass_UV)
23         {
24             throw new NotImplementedException();
25         }
26 
27     }
+BIT祝威+悄悄在此留下版了個權的信息說:

設計

大體思路就如上面的例子。頂點屬性、uniform變量都可以用C#字段表示。main函數、內置函數、內置變量都可以用C#相應的函數和類型表示。

稍微有所不同的是,'in','out','uniform'等這些qualifier只好用Attribute代表了。

子函數尚未涉及,到時候再說。

不同類型的shader(vertex、fragment、geometry、tessellation等)都有些相同的內置函數,也都有各自獨特的內置變量,這就是本項目的類庫設計要描述的對象。

對於用戶來說,用戶只需寫出CSSL的代碼,即可一鍵自動獲取GLSL的代碼。

CSSL寫好了,當然應該自動地轉換為GLSL。否則還有什么意義。

CSSL

將C#代碼轉換為另一種形式,無非是反射+字符串解析拼接之類的東西。

設計方案很簡單。包含CSSL的*.cs文件作為輸入,對應的GLSL文件(*.vert或*.frag)作為輸出。用反射獲取in、out、uniform這些變量,用正則表達式獲取main函數代碼。最后用字符串拼接起來就是。Shader有多種,所以要有一個抽象和繼承關系。

上圖是對CSSL代碼的分析和設計圖。注意,這里的CSSL代碼對我這個開發者而言,只是一堆存儲在*.cs文件里的字符串。雖然其內容是C#代碼,但其本質仍然是字符串,只不過這個字符串的內容是一些C#代碼。可不要繞暈了。

語義化的Shader

獲取語義化的shader,就是從字符串形式的CSSL到內存中的數據結構這樣一個過程。這實際上是一個極其簡陋的編譯器做的事。

導出GLSL

獲取字段的過程用反射就可以實現。

 1         private void Parse()
 2         {
 3             FieldInfo[] fields = this.shaderCode.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic);
 4             foreach (var field in fields)
 5             {
 6                 if (field.GetCustomAttribute<InAttribute>() != null)
 7                 {
 8                     this.fields.Add(new FieldTemplate(FieldQualifier.In, field.FieldType, field.Name));
 9                 }
10                 else if (field.GetCustomAttribute<OutAttribute>() != null)
11                 {
12                     this.fields.Add(new FieldTemplate(FieldQualifier.Out, field.FieldType, field.Name));
13                 }
14                 else if (field.GetCustomAttribute<UniformAttribute>() != null)
15                 {
16                     this.fields.Add(new FieldTemplate(FieldQualifier.Uniform, field.FieldType, field.Name));
17                 }
18             }
19 
20             this.mainFunction = SearchMainFunction(this.fullname);
21         }

找到主函數代碼就得用正則表達式了。

 1         protected override string SearchMainFunction(string fullname)
 2         {
 3             string content = File.ReadAllText(fullname);
 4             // class XxxVertexShader : VertexShaderCode
 5             Match match = Regex.Match(content, @"class\s+" + this.shaderCode.GetType().Name + @"\s*:");
 6             int classStart = match.Index + match.Length;
 7             // public override void main() { ... }
 8             match = Regex.Match(content.Substring(classStart),
 9                 @"public\s+override\s+void\s+main\s*\(\s*\)\s*\{");
10             // 自行找到main(){}函數的‘}’
11             int firstLeftBrace = classStart + match.Index + match.Length - 1;
12             int left = 1;
13             int lastRightBrace = -1;
14             for (int i = firstLeftBrace + 1; i < content.Length; i++)
15             {
16                 char c = content[i];
17                 if (c == '\"')
18                 {
19                     for (int j = i + 1; j < content.Length; j++)
20                     {
21                         char tmp = content[j];
22                         if (tmp == '\"')
23                         {
24                             i = j;
25                             break;
26                         }
27                     }
28                 }
29                 else if (c == '\'')
30                 {
31                     i = i + 2;
32                 }
33                 else if (c == '{')
34                 {
35                     left++;
36                 }
37                 else if (c == '}')
38                 {
39                     left--;
40                     if (left == 0)
41                     {
42                         lastRightBrace = i;
43                         break;
44                     }
45                 }
46             }
47 
48             StringBuilder mainBuilder = new StringBuilder();
49             mainBuilder.AppendLine("void main(void)");
50             mainBuilder.AppendLine("{");
51             string[] parts = content.Substring(firstLeftBrace + 1, lastRightBrace - (firstLeftBrace - 1))
52                 .Split(separator, StringSplitOptions.RemoveEmptyEntries);
53             int preEmptyCount = 0;
54             {
55                 string line = Regex.Replace(parts[parts.Length - 1], "\t", "    ");
56                 preEmptyCount = Regex.Match(line, @" *").Length;
57             }
58             foreach (var item in parts)
59             {
60                 string line = Regex.Replace(item, "\t", "    ");
61 
62                 if (Regex.Match(line, @"[\t ]*").Length >= preEmptyCount)
63                 {
64                     line = line.Substring(preEmptyCount);
65                 }
66                 mainBuilder.AppendLine(line);
67             }
68             return mainBuilder.ToString();
69         }
SearchMainFunction
+BIT祝威+悄悄在此留下版了個權的信息說:

使用

學習上手

為了方便教學使用,我制作了一個GUI程序。

你可以在這里找到他。

工程實際

每次用GUI都手動加載一遍在長期的工程實踐中也是很煩人的。所以我提供一個Console程序,可以用腳本、VS生成事件等方式自動調用。這樣,每次編譯整個項目時,就可以順帶更新GLSL代碼了。

How to do

我以下面這個項目為例說明,如何借助VS自帶的生成事件來使用這個Console。

首先如上圖所示添加CSharpShaderLanguage.dll和CSharpGL.CSSL2GLSL.exe兩個文件,並設置其屬性為"如果較新則復制"。

 

然后,如下圖所示,添加兩個CSharp文件,並編寫CSSL代碼。這里就體現出了使用本項目的好處之一:編寫CSSL的過程本質是在VS下編寫C#代碼,你可以盡情享用VS提供的便利!

然后設置項目屬性如下。參數..\..\表示CSSL2GLSL.exe要向上查找2個層級的文件夾。沒有參數時則表示此CSSL2GLSL.exe所在的文件夾。

一切就緒,只欠F6。按F6生成,VS會自動調用CSSL2GLSL.exe。

如果你修改了CSSL代碼,那么就會收到這樣的提示:

這說明CSSL2GLSL.exe被VS自動調用,更新了你的GLSL代碼!

所以,你得再按一次F6,到不再出現上面的提示為止。

編譯完成后CSSL2GLSL.exe會自動打開log文件和文件夾,方便你查看編譯的結果。

這樣一來,我們的GLSL代碼也就有了編譯時的語法檢查了。這是應用本項目的另一個好處。

+BIT祝威+悄悄在此留下版了個權的信息說:

2016-02-16

手動執行CSSL2GLSL

經過一段時間的使用,我發現上面的自動調用CSSL2GLSL也很煩人。常常在沒有更改cssl代碼時,CSSL2GLSL.exe也會執行,而且執行速度也不夠快。這導致F6編譯項目時等待時間增長了幾倍,無法忍受。所以現在我不再使用上面的自動執行的方式,改為將CSSL2GLSL.exe放到solution文件夾下,需要時自己手動執行。

獨立的擴展名

現在我規定CSSL代碼文件的擴展名必須是*.cssl.cs。這樣方便System.IO.Directory.GetFiles()識別,也就避免了處理大量無關代碼的情形。

精簡版log

一般cssl.cs文件不會出什么問題,所以看那個冗長的完整版log也很浪費時間和心情。所以現在在生成完整版log時也會生成一個精簡版log,例如。

 1 Directory: C:\Users\威\Documents\GitHub\CSharpGL
 2 Found 20 CSSL shaders, and 0 of them are dumped to GLSL as needed.
 3     --> Translating C:\Users\威\Documents\GitHub\CSharpGL\Demos\CSharpGL.LightEffects\DiffuseReflectionDirectionalLight.cssl.cs
 4     2 CSSL shaders:
 5         Not need to dump [DiffuseReflectionDirectionalLightVert] to [DiffuseReflectionDirectionalLight.vert] OK!
 6         Not need to dump [DiffuseReflectionDirectionalLightFrag] to [DiffuseReflectionDirectionalLight.frag] OK!
 7     --> Translating C:\Users\威\Documents\GitHub\CSharpGL\Demos\CSharpGL.LightEffects\DiffuseReflectionPointLight.cssl.cs
 8     2 CSSL shaders:
 9         Not need to dump [DiffuseReflectionPointLightVert] to [DiffuseReflectionPointLight.vert] OK!
10         Not need to dump [DiffuseReflectionPointLightFrag] to [DiffuseReflectionPointLight.frag] OK!
11     --> Translating C:\Users\威\Documents\GitHub\CSharpGL\Demos\CSharpGL.LightEffects\PhongPointLight.cssl.cs
12     2 CSSL shaders:
13         Not need to dump [PhongPointLightVert] to [PhongPointLight.vert] OK!
14         Not need to dump [PhongPointLightFrag] to [PhongPointLight.frag] OK!
15     --> Translating C:\Users\威\Documents\GitHub\CSharpGL\Demos\CSharpGL.Objects.Demos\NormalLine.cssl.cs
16     3 CSSL shaders:
17         Not need to dump [NormalLineVert] to [NormalLine.vert] OK!
18         Not need to dump [NormalLineFrag] to [NormalLine.frag] OK!
19         Not need to dump [NormalLineGeom] to [NormalLine.geom] OK!
20     --> Translating C:\Users\威\Documents\GitHub\CSharpGL\Demos\CSharpGL.Objects.Demos\Simple.cssl.cs
21     3 CSSL shaders:
22         Not need to dump [SimpleVert] to [Simple.vert] OK!
23         Not need to dump [SimpleFrag] to [Simple.frag] OK!
24         Not need to dump [SimpleGeom] to [Simple.geom] OK!
25     --> Translating C:\Users\威\Documents\GitHub\CSharpGL\Functions\CSharpGL.Objects.Common\AxisElement.cssl.cs
26     2 CSSL shaders:
27         Not need to dump [AxisElementVert] to [AxisElement.vert] OK!
28         Not need to dump [AxisElementFrag] to [AxisElement.frag] OK!
29     --> Translating C:\Users\威\Documents\GitHub\CSharpGL\Tools\CSharpGL.ObjViewer\ObjFile.cssl.cs
30     3 CSSL shaders:
31         Not need to dump [ObjFileVert] to [ObjFile.vert] OK!
32         Not need to dump [ObjFileGeom] to [ObjFile.geom] OK!
33         Not need to dump [ObjFileFrag] to [ObjFile.frag] OK!
34     --> Translating C:\Users\威\Documents\GitHub\CSharpGL\Tools\ShaderLab\Some.cssl.cs
35     3 CSSL shaders:
36         Not need to dump [SomeVert] to [Some.vert] OK!
37         Not need to dump [SomeFrag] to [Some.frag] OK!
38         Not need to dump [SomeGeom] to [Some.geom] OK!
39 Translation all done!
CSSL2GLSLDump20160216-125347.simple.log

而在有了cssl.cs擴展名這個規定后,我發現完整版log常常和精簡版log的內容完全相同。這也成了一種快速推測cssl是否有問題的方式。

支持geometry shader

現在的CSSL支持geometry shader的編寫和代碼生成。並且,代碼生成過程中也會自動解析用戶自定義的結構類型,例如下面這樣的:

1 in VS_GS_VERTEX
2 {
3     vec3 normal;
4 } vertex_in[];

還有下面這樣的,都支持。

1 out GS_FS_VERTEX
2 {
3     vec3 color;
4 } vertex_out;
對應上面的兩個GLSL類型,C#中的CSSL寫法是這樣的:
1         class VS_GS_VERTEX
2  { 3 public vec3 normal;//必須是public的字段 4  } 5  [In] 6 VS_GS_VERTEX[] vertex_in;
1         class GS_FS_VERTEX
2  { 3 public vec3 color;//必須是public的字段 4  } 5  [Out] 6 GS_FS_VERTEX vertex_out;

初始值的自動轉化

此外,像下面這樣的C#中的初始值也支持自動轉化到GLSL。
1         [Uniform]
2         float normalLength = 0.5f;

這個會自動轉換為GLSL中的:

1 uniform float normalLength = 0.5;

總結

目前的CSSL並未完全覆蓋GLSL的功能。因為我原本就沒有多少寫GLSL的經歷。等我慢慢用GLSL的情形多了,再逐步補充CSSL吧。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM