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布局也是有所了解。
。
文章列表