CSharpGL(33)使用uniform块来优化对uniform变量的读写
+BIT祝威+悄悄在此留下版了个权的信息说:
如果shader程序变得比较复杂,那么其中用到的uniform变量数量也会上升。通常会在多个shader程序中用到同一个uniform变量。而uniform buffer object就是一种优化uniform变量访问,以及在不同的shader程序间共享uniform数据的方法。
首先了解一下uniform块的写法。
1 uniform b { // ‘b’ 对应于外部访问时的名称
2 vec4 v1;// 块中的变量列表
3 bool v2;// …
4 }; // 访问成员时使用v1、v2
或者
1 uniform b { // ‘b’ 对应于外部访问时的名称
2 vec4 v1;// 块中的变量列表
3 bool v2;// …
4 } name; // 访问成员时使用name.v1、name.v2
注意,shader程序中的数据类型有两种:不透明的和透明的;其中不透明的包括sampler、image和atomic counter。一个uniform块中只能包含透明类型的变量。
另外,在同一个shader程序里的两个uniform块,里面的变量名都不能相同。
下面我们以具体例子的编写过程来说明在如何使用uniform块,顺便了解一下CSharpGL是如何简化对uniform块的使用的。
+BIT祝威+悄悄在此留下版了个权的信息说:
我认为用Modern OpenGL渲染,首先要写shader。我们先看一个简单的vertex shader。
1 #version 330 core
2
3 uniform mat4 projectionMatrix;
4 uniform mat4 viewMatrix;
5 uniform mat4 modelMatrix;
6
7 in vec3 vPos;
8 in vec3 vColor;
9 out vec3 fColor;
10
11 void main(void) {
12
13 gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(vPos, 1.0);
14
15 fColor = vColor;
16 }
我们就把这里面的uniform变量换作块,如下,只是把原来的uniform变量包了起来,并命名为“Uniforms”。
1 #version 330 core
2
3 uniform Uniforms {
4 mat4 projectionMatrix;
5 mat4 viewMatrix;
6 mat4 modelMatrix;
7 };
8
9 in vec3 vPos;
10 in vec3 vColor;
11 out vec3 fColor;
12
13 void main(void) {
14
15 gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(vPos, 1.0);
16
17 fColor = vColor;
18 }
而fragment shader则更简单:
1 #version 330 core
2
3 in vec3 fColor;
4
5 out vec4 out_Color;
6
7 void main(void) {
8 out_Color = vec4(fColor, 1.0f);
9 }
+BIT祝威+悄悄在此留下版了个权的信息说:
Uniform块,实际上对应一个在应用程序客户端的struct类型。对于示例中的‘Uniforms’块,我们可以定义如下的结构体。为了方便对照,我们也用‘Uniforms’作为 struct
1 struct Uniforms : IEquatable<Uniforms>
2 {
3 public mat4 projection;
4 public mat4 view;
5 public mat4 model;
6
7 public Uniforms(mat4 projection, mat4 view, mat4 model)
8 {
9 this.projection = projection;
10 this.view = view;
11 this.model = model;
12 }
13
14 public bool Equals(Uniforms other)
15 {
16 return this.projection == other.projection
17 && this.view == other.view
18 && this.model == other.model;
19 }
20 }
今后我们就将数据准备好后保存到一个 Uniforms
这里是重点了。传送float类型的uniform变量,我们有 UniformFloat ;传送vec3类型的uniform变量,我们有 UniformVec3 。但是uniform块传送的是一个个可以任意自定义的不同的结构体(例如上面的struct Uniforms),因此最好用一个泛型的 UniformBlock<T>
1 public class UniformBlock<T> : UniformSingleVariableBase where T : struct, IEquatable<T>
2 {
3 protected T value;
4
5 public T Value
6 {
7 get { return this.value; }
8 set
9 {
10 if (!value.Equals(this.value))
11 {
12 this.value = value;
13 this.Updated = true;
14 }
15 }
16 }
17
18 public UniformBlock(string blockName) : base(blockName) { }
19
20 public UniformBlock(string blockName, T value) : base(blockName) { this.Value = value; }
21
22 protected override void DoSetUniform(ShaderProgram program)
23 {
24 // ...
25 }
26 }
UniformBlock<T>
1 protected override void DoSetUniform(ShaderProgram program)
2 {
3 if (uniformBufferPtr == null)
4 {
5 uniformBufferPtr = Initialize(program);
6 }
7 else
8 {
9 IntPtr pointer = uniformBufferPtr.MapBuffer(MapBufferAccess.WriteOnly, bind: true);
10 unsafe
11 {
12 var array = (byte*)pointer.ToPointer();
13 byte[] bytes = this.value.ToBytes();
14 for (int i = 0; i < bytes.Length; i++)
15 {
16 array[i] = bytes[i];
17 }
18 }
19 uniformBufferPtr.UnmapBuffer(unbind: true);
20 }
21
22 this.Updated = false;
23 }
24
25 /// <summary>
26 /// Initialize and setup uniform block's value.
27 /// </summary>
28 /// <param name="program"></param>
29 /// <returns></returns>
30 private UniformBufferPtr Initialize(ShaderProgram program)
31 {
32 uint uboIndex = glGetUniformBlockIndex(program.ProgramId, this.VarName);
33 var uboSize = new uint[1];
34 glGetActiveUniformBlockiv(program.ProgramId, uboIndex, OpenGL.GL_UNIFORM_BLOCK_DATA_SIZE, uboSize);
35 UniformBufferPtr result = null;
36 using (var buffer = new UniformBuffer<byte>(BufferUsage.StaticDraw, noDataCopyed: false))
37 {
38 byte[] bytes = this.value.ToBytes();
39 buffer.Create(bytes.Length);
40 unsafe
41 {
42 var array = (byte*)buffer.Header.ToPointer();
43 for (int i = 0; i < bytes.Length; i++)
44 {
45 array[i] = bytes[i];
46 }
47 }
48
49 result = buffer.GetBufferPtr() as UniformBufferPtr;
50 }
51
52 // 将此uniform块与此uniform buffer object绑定。
53 glBindBufferBase(OpenGL.GL_UNIFORM_BUFFER, uboIndex, result.BufferId);
54
55 return result;
56 }
57
58 private UniformBufferPtr uniformBufferPtr = null;
DoSetUniform
对于普通的uniform变量,CSharpGL用 Renderer.SetUniform(string varName, T value) where T : struct
+BIT祝威+悄悄在此留下版了个权的信息说:
有了上述准备,我们就可以使用uniform块了。
按照CSharpGL的传统,下面来创建一个UniformBlockRenderer,负责加载shader、模型数据和渲染工作。
1 class UniformBlockRenderer : Renderer
2 {
3 public static UniformBlockRenderer Create()
4 {
5 var model = new Teapot();// model
6 var shaderCodes = new ShaderCode[2];// shaders
7 shaderCodes[0] = new ShaderCode(File.ReadAllText(@"shaders \UniformBlock.vert"), ShaderType.VertexShader);
8 shaderCodes[1] = new ShaderCode(File.ReadAllText(@"shaders \UniformBlock.frag"), ShaderType.FragmentShader);
9 var map = new AttributeNameMap();// mapping relation between model and shaders
10 map.Add("vPos", Teapot.strPosition);
11 map.Add("vColor", Teapot.strColor);
12 var renderer = new UniformBlockRenderer(model, shaderCodes, map);// renderer
13
14 return renderer;
15 }
16 }
就像普通的uniform变量一样,我们也在 Renderer.DoRender()
1 protected override void DoRender(RenderEventArgs arg)
2 {
3 mat4 projection = arg.Camera.GetProjectionMatrix();
4 mat4 view = arg.Camera.GetViewMatrix();
5 mat4 model = this.GetModelMatrix();
6 // 设置uniform块,只需这一行。
7 this.SetUniform("Uniforms", new Uniforms(projection, view, model));
8
9 base.DoRender(arg);
10 }
完成的效果如图,能够正常渲染,说明我们成功地更新了uniform块里的数据。
+BIT祝威+悄悄在此留下版了个权的信息说:
借助C#的struct与byte[]的相互转换,加上CSharpGL对Modern Rendering的封装,实际上我们不需要调用 glGetUniformIndices 、 glGetActiveUniformsiv
当shader中写了一个uniform块时,你只需在应用程序客户端也写一个对应的 struct ,然后用 Renderer.SetUniform(blockName, structObj);
PS:测试过程中发现对于vec3结果正常,但是vec4却有诡异的情况,后来想到应用程序客户端里的vec4与shader里的vec4的xyzw布局不一样,可能因此导致原本属于w的数据最终传送到了shader里的y上。于是在调整了CSharpGL里的vec4的字段顺序后就一切正常了。为了避免以后我或者其他人在忘记\不知情的情况下擅自改动了vec4的字段顺序,我用 [FieldOffset(...)]
。