CSharpGL(33)使用uniform块来优化对uniform变量的读写
Uniform块
如果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块的使用的。
Shader
我认为用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 }
准备工作
传送一个自定义的struct
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 对象,最终传送到shader。
uniform块结构
这里是重点了。传送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> 更新uniform块的操作比较复杂:它要创建一个uniform buffer object,并与之绑定;以后它只需更新这个buffer里的数据,就可以实现对uniform块的更新。
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;
SetUniform()
对于普通的uniform变量,CSharpGL用 Renderer.SetUniform(string varName, T value) where T : struct 即可(无论是什么类型的uniform都可以处理)。对于Uniform块,也可以用这个方法!
UniformBlockRenderer
有了上述准备,我们就可以使用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块的值
就像普通的uniform变量一样,我们也在 Renderer.DoRender() 方法里更新uniform块。
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块里的数据。
下载
CSharpGL已在GitHub开源,欢迎对OpenGL有兴趣的同学加入(https://github.com/bitzhuwei/CSharpGL)
总结
借助C#的struct与byte[]的相互转换,加上CSharpGL对Modern Rendering的封装,实际上我们不需要调用 glGetUniformIndices 、 glGetActiveUniformsiv (用于获取shader中uniform块里的各个变量的偏移量)这些接口,就可以使用uniform块了。
当shader中写了一个uniform块时,你只需在应用程序客户端也写一个对应的 struct ,然后用 Renderer.SetUniform(blockName, structObj); 一行即可实现对uniform块数据的更新。
PS:测试过程中发现对于vec3结果正常,但是vec4却有诡异的情况,后来想到应用程序客户端里的vec4与shader里的vec4的xyzw布局不一样,可能因此导致原本属于w的数据最终传送到了shader里的y上。于是在调整了CSharpGL里的vec4的字段顺序后就一切正常了。为了避免以后我或者其他人在忘记不知情的情况下擅自改动了vec4的字段顺序,我用 [FieldOffset(...)] 特性标注了vec4的各个字段。这真是一个很深的bug。多亏我对C#的struct布局也是有所了解。
。