CSharpGL(11)用C#直接编写GLSL程序
+BIT祝威+悄悄在此留下版了个权的信息说:
由来
本项目的目的:使开发者可以直接用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祝威+悄悄在此留下版了个权的信息说:
总结
目前的CSSL并未完全覆盖GLSL的功能。因为我原本就没有多少写GLSL的经历。等我慢慢用GLSL的情形多了,再逐步补充CSSL吧。