CSharpGL(14)用geometry shader渲染模型的法线(normal)
2016-08-13
由于CSharpGL一直在更新,现在这个教程已经不适用最新的代码了。CSharpGL源码中包含10多个独立的Demo,更适合入门参考。
为了尽可能提升渲染效率,CSharpGL是面向Shader的,因此稍有难度。
问题
在处理光照效果等问题时,模型的顶点的法线是必不可少的数据。但是法线并不直接显示在模型上,也没有别的办法可以直观地看到。如果法线计算错了,那是非常难以排查的。所以我就想用geometry shader来渲染出模型的法线。如下图的白色针状部分,就是这个teapot的法线。为了便于区分,针尖部分是顶点位置,较粗的针头部分则是法线的方向。从这个图中可以看到,我做的这个teapot的法线是很有问题的。怪不得之前拿它试验光照效果时会有一些诡异的现象。
下载
这个示例是CSharpGL的一部分,CSharpGL已在GitHub开源,欢迎对OpenGL有兴趣的同学加入()
原理
Geometry shader的执行,在vertex shader之后,在fragment shader之前。Geometry shader的输入数据是一个primitive(points、lines、triangles等),输出可以是0或多个primitive(points、line_strip或triangle_strip)。Geometry shader的作用,就是能增加新的图元。本篇就利用这个功能,将模型顶点的法线作为新增的图元渲染出来。
Geometry shader
Geometry shader代码如下,含义参考注释即可。如果要用此shader,最好删掉中文注释。因为有的显卡可能不支持中文,会造成无法编译通过的情况。
1 #version 410 core 2 3 //输入类型为三角形 4 layout (triangles) in; 5 //输出的是三角形带 6 layout (triangle_strip, max_vertices = 11) out; 7 8 uniform mat4 modelMatrix; 9 uniform mat4 viewMatrix;10 uniform mat4 projectionMatrix;11 12 uniform float normalLength = 0.5f;13 14 in VS_GS_VERTEX15 {16 vec3 normal;17 } vertex_in[];18 19 out GS_FS_VERTEX20 {21 vec3 color;22 } vertex_out;23 24 void main(void)25 {26 int i;27 //先输出模型本身28 for (i = 0; i < gl_in.length(); i++) {29 vertex_out.color = vertex_in[i].normal;30 vec4 position = gl_in[i].gl_Position;31 gl_Position = projectionMatrix * viewMatrix * (modelMatrix * position);32 EmitVertex();33 }34 EndPrimitive();35 36 //生成顶点的法线(一个法线用一个三棱柱表示)37 for (i = 0; i < gl_in.length(); i++) { //我的理解:此处gl_in.length()为338 //法线颜色为白色39 vertex_out.color = vec3(1, 1, 1);40 41 //获取模型的顶点位置(针尖)42 vec4 position = gl_in[i].gl_Position;43 //获取模型的法线(针头)位置44 vec4 target = position + vertex_in[i].normal * normalLength;45 {46 vec4 v0 = position;47 gl_Position = projectionMatrix * viewMatrix * (modelMatrix * v0);48 EmitVertex();//生成一个三棱柱顶点49 50 vec4 v1 = target;51 if (target.x > position.x) { v1.x += normalLength / 30.0f; }52 else { v1.x -= normalLength / 10.0f; }53 gl_Position = projectionMatrix * viewMatrix * (modelMatrix * v1);54 EmitVertex();//生成一个三棱柱顶点55 56 vec4 v2 = position;57 gl_Position = projectionMatrix * viewMatrix * (modelMatrix * v2);58 EmitVertex();//生成一个三棱柱顶点59 60 vec4 v3 = target;61 if (target.y > position.y) { v3.y += normalLength / 30.0f; }62 else { v3.y -= normalLength / 10.0f; }63 gl_Position = projectionMatrix * viewMatrix * (modelMatrix * v3);64 EmitVertex();//生成一个三棱柱顶点65 66 vec4 v4 = position;67 gl_Position = projectionMatrix * viewMatrix * (modelMatrix * v4);68 EmitVertex();//生成一个三棱柱顶点69 70 vec4 v5 = target;71 if (target.z > position.z) { v5.z += normalLength / 30.0f; }72 else { v5.z -= normalLength / 10.0f; }73 gl_Position = projectionMatrix * viewMatrix * (modelMatrix * v5);74 EmitVertex();//生成一个三棱柱顶点75 76 vec4 v6 = position;77 gl_Position = projectionMatrix * viewMatrix * (modelMatrix * v6);78 EmitVertex();//生成一个三棱柱顶点79 80 vec4 v7 = target;81 if (target.x > position.x) { v7.x += normalLength / 30.0f; }82 else { v7.x -= normalLength / 10.0f; }83 gl_Position = projectionMatrix * viewMatrix * (modelMatrix * v7);84 EmitVertex();//生成一个三棱柱顶点85 86 }87 88 EndPrimitive();//依据上面的8个顶点,为此顶点的法线生成一个三棱柱89 }90 }
输入\输出类型
Geometry shader允许的输入输出类型,可以参考()
GS input | OpenGL primitives | vertex count | |
points | GL_POINTS | point_mode | 1 |
lines | GL_LINES, GL_LINE_STRIP, GL_LINE_LIST | isolines | 2 |
lines_adjacency | GL_LINES_ADJACENCY, GL_LINE_STRIP_ADJACENCY | N/A | 4 |
triangles | GL_TRIANGLES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN | triangles, quads | 3 |
triangles_adjacency | GL_TRIANGLES_ADJACENCY, GL_TRIANGLE_STRIP_ADJACENCY | N/A | 6 |
输出类型只能是points、line_strip或triangle_strip。
Geometry shader的一个特点,就是从vertex shader传过来的顶点,在经过geometry shader后就消失了,不会再传到fragment shader。所以为了保证仍旧渲染模型本身,输入、输出类型必须一致。于是我只能选triangles\triangle_strip。
当然,用一套shader program渲染模型,再用另一套来渲染法线,那也是可以的。我不这么做一是因为懒,二是希望内存中只有一套数据,三是希望不去更改目前的CSSL+Renderer框架。
uniform
Geometry shader中的uniform变量与vertex shader、fragment shader中的相同。
in\out类型变量
例如上面的
1 in VS_GS_VERTEX2 {3 vec3 normal;4 } vertex_in[];
必须用数组的形式。这符合输入数据会有多个顶点的实际。
而out类型的变量,例如上面的
1 out GS_FS_VERTEX2 {3 vec3 color;4 } vertex_out;
与vertex shader、fragment shader中的形式相同。但是它可以被多次使用,在使用后,每调用一次 EmitVertex(); 就会生效一次(产生一个顶点,此顶点在fragment shader中的GS_FS_VERTEX.color字段就是刚刚设置的值)。
可以说,geometry shader的工作方式是:根据输入的图元的各个顶点信息,根据业务需要,设置好你需要的新顶点的各项信息,然后用EmitVertex();,就会生成一个顶点,并传给fragment shader。而调用 EndPrimitive(); 时,就会结束一个图元的生成过程(同时开始下一个图元的生成过程)。
gl_Position
geometry shader中是可以设置顶点的位置的。因此就不宜直接在vertex shader中设置了。
填坑
坑人的是geometry shader的layout in\out类型不支持quads。而我之前做的球体、立方体模型用的都是quad_strip类型的索引。试验证明这样的模型是用不了geometry shader的。所以我只好做一套用triangles或triangle_strip做索引的球体了。
坑填完后,效果如下图。
我制作的球体模型:
我意外制作的“冰激凌”模型:
我制作的立方体模型:
总结
实际上,目前的法线会出现不稳定且无限延长的现象,这个问题待解决。
2016-02-16
解决法线不稳定的问题
这个不稳定问题在我整理了CSSL2GLSL后消失了。原因也未知,可能与下面这个问题相关。
下图中的法线没有完全显示。而是每个面只显示了一个。这是因为在geometry shader中的 layout (triangle_strip, max_vertices = 11) out; 最大顶点数太小了,改到27个顶点(=1个三角形的3个顶点+3个三棱柱的8个顶点)即可。
正常显示的立方体法线图:
CSSL支持geometry shader
现在的CSSL支持geometry shader的编写和代码生成。并且,代码生成过程中也会自动解析用户自定义的结构类型,例如下面这样的:
1 in VS_GS_VERTEX2 {3 vec3 normal;4 } vertex_in[];
还有下面这样的,都支持。
1 out GS_FS_VERTEX2 {3 vec3 color;4 } vertex_out;
1 class VS_GS_VERTEX2 {3 public vec3 normal;//必须是public的字段4 }5 [In]6 VS_GS_VERTEX[] vertex_in;
1 class GS_FS_VERTEX2 {3 public vec3 color;//必须是public的字段4 }5 [Out]6 GS_FS_VERTEX vertex_out;
初始值的自动转化
1 [Uniform]2 float normalLength = 0.5f;
这个会自动转换为GLSL中的:
1 uniform float normalLength = 0.5;