CSharpGL(49)試水OpenGL軟實現
CSharpGL(49)試水OpenGL軟實現
CSharpGL迎來了第49篇。本篇內容是用C#編寫一個OpenGL的軟實現。暫且將其命名為SoftGL。
目前已經實現了由Vertex Shader和Fragment Shader組成的Pipeline,其效果與顯示卡支援的OpenGL實現幾乎相同。下圖左是常規OpenGL渲染的結果,右是SoftGL渲染的結果。
下載
CSharpGL已在GitHub開源,歡迎對OpenGL有興趣的同學加入(https://github.com/bitzhuwei/CSharpGL)
SoftGL也已在GitHub開源,歡迎對OpenGL有興趣的同學加入(
從使用者的角度開始
OpenGL的使用者就是OpenGL應用程式開發者(Dev)。
下面按其被執行的先後順序陳列OpenGL相關的命令(只陳列最基本的命令):
建立Device Context
用一個System.Windows.Forms.Control型別的物件即可。
最後會發現,這個Device Context的作用之一是為建立Render Context提供引數。目前在SoftGL中不需要這個引數。
建立Render Context
Render Context包含OpenGL的所有狀態欄位。例如,當Dev呼叫glLineWidth(float width);時,Render Context會記下這一width值,從而使之長期有效(直到下次呼叫glLineWidth(float width);來修改它)。
可能同時存在多個Render Context,每個都儲存自己的lineWidth等欄位。當使用靜態的OpenGL函式static void glLineWidth(float width);時,它會首先找到當前的Render Context物件(詳見後面的MakeCurrent(..)),然後讓此物件執行真正的glLineWidth(float width);函式。
可見Render Context就是一個典型的class,其虛擬碼如下:
1 partial class SoftGLRenderContext: 2 { 3 private float lineWidth;4 // .. other fields. 5 6 public static void glLineWidth(float width) 7 { 8 SoftGLRenderContext obj = SoftGLRenderContext .GetCurrentContext(); 9 if (obj != null) { obj.LineWidth(width); } 10 } 11 12 private void LineWidth(float width) 13 { 14 this.lineWidth = width; 15 } 16 17 // .. other OpenGL functions. 18 }
MakeCurrent(IntPtr dc, IntPtr rc);
函式static void MakeCurrent(IntPtr dc, IntPtr rc);不是OpenGL函式。它的作用是指定當前執行緒(Thread)與哪個Render Context對應,即在Dictionary<Thread, RenderContext>這一字典型別中記錄下Thread與Render Context的對應關係。
當然,如果rc為IntPtr.Zero,就是要解除當前Thread與其Render Context的對應關係。
虛擬碼如下:
1 partial class SoftGLRenderContext 2 { 3 // Thread -> Binding Render Context Object. 4 static Dictionary<Thread, SoftGLRenderContext> threadContextDict = new Dictionary<Thread, SoftGLRenderContext>(); 5 6 // Make specified renderContext the current one of current thread. 7 public static void MakeCurrent(IntPtr deviceContext, IntPtr renderContext) 8 { 9 var threadContextDict = SoftGLRenderContext.threadContextDict; 10 if (renderContext == IntPtr.Zero) // cancel current render context to current thread. 11 { 12 SoftGLRenderContext context = null; 13 14 Thread thread = System.Threading.Thread.CurrentThread; 15 if (threadContextDict.TryGetValue(thread, out context)) 16 { 17 threadContextDict.Remove(thread); 18 } 19 } 20 else // change current render context to current thread. 21 { 22 SoftGLRenderContext context = GetContextObj(renderContext); 23 if (context != null) 24 { 25 SoftGLRenderContext oldContext = GetCurrentContextObj(); 26 if (oldContext != context) 27 { 28 Thread thread = Thread.CurrentThread; 29 if (oldContext != null) { threadContextDict.Remove(thread); } 30 threadContextDict.Add(thread, context); 31 context.DeviceContextHandle = deviceContext; 32 } 33 } 34 } 35 } 36 }
獲取OpenGL函式指標
在CSharpGL.Windows專案中,我們可以通過Win32 API找到在opengl32.dll中的OpenGL函式指標,並將其轉換為C#中的函式委託(Delegate),從而可以像使用普通函式一樣使用OpenGL函式。其虛擬碼如下:
1 public partial class WinGL : CSharpGL.GL 2 { 3 public override Delegate GetDelegateFor(string functionName, Type functionDeclaration) 4 { 5 Delegate del = null; 6 if (!extensionFunctions.TryGetValue(functionName, out del)) 7 { 8 IntPtr proc = Win32.wglGetProcAddress(name); 9 if (proc != IntPtr.Zero) 10 { 11 // Get the delegate for the function pointer. 12 del = Marshal.GetDelegateForFunctionPointer(proc, functionDeclaration); 13 14 // Add to the dictionary. 15 extensionFunctions.Add(functionName, del); 16 } 17 } 18 19 return del; 20 } 21 22 // Gets a proc address. 23 [DllImport("opengl32.dll", SetLastError = true)] 24 internal static extern IntPtr wglGetProcAddress(string name); 25 26 // The set of extension functions. 27 static Dictionary<string, Delegate> extensionFunctions = new Dictionary<string, Delegate>(); 28 }
此時我們想使用SoftGL,那麼要相應地為其編寫一個SoftGL.Windows專案。這個專案通過在類似opengl32.dll的SoftOpengl32專案(或者SoftOpengl32.dll)中查詢函式的方式來找到我們自己實現的OpenGL函式。其虛擬碼如下:
1 partial class WinSoftGL : CSharpGL.GL 2 { 3 private static readonly Type thisType = typeof(SoftOpengl32.StaticCalls); 4 public override Delegate GetDelegateFor(string functionName, Type functionDeclaration) 5 { 6 Delegate result = null; 7 if (!extensionFunctions.TryGetValue(functionName, out result)) 8 { 9 MethodInfo methodInfo = thisType.GetMethod(functionName, BindingFlags.Static | BindingFlags.Public); 10 if (methodInfo != null) 11 { 12 result = System.Delegate.CreateDelegate(functionDeclaration, methodInfo); 13 } 14 15 if (result != null) 16 { 17 // Add to the dictionary. 18 extensionFunctions.Add(functionName, result); 19 } 20 } 21 22 return result; 23 } 24 25 // The set of extension functions. 26 static Dictionary<string, Delegate> extensionFunctions = new Dictionary<string, Delegate>(); 27 }
可見只需通過C#和.NET提供的反射機制即可實現。在找到System.Delegate.CreateDelegate(..)這個方法時,我感覺到一種“完美”。
此時,我們應當注意到另一個涉及大局的問題,就是整個SoftGL的框架結構。
SoftGL專案本身的作用與顯示卡驅動中的OpenGL實現相同。作業系統(例如Windows)提供了一個opengl32.dll之類的方式來讓Dev找到OpenGL函式指標,從而使用OpenGL。CSharpGL專案是對OpenGL的封裝,具體地講,是對OpenGL的函式宣告的封裝,它不包含對OpenGL的實現、初始化等功能。這些功能是在CSharpGL.Windows中實現的。Dev通過引用CSharpGL專案和CSharpGL.Windows專案就可以直接使用OpenGL了。
如果不使用顯示卡中的OpenGL實現,而是換做SoftGL,那麼這一切就要相應地變化。SoftOpengl32專案代替作業系統的opengl32.dll。CSharpGL保持不變。SoftGL.Windows代替CSharpGL.Windows。Dev通過引用CSharpGL專案和SoftGL.Windows專案就可以直接使用軟實現的OpenGL了。
最重要的是,這樣保證了應用程式的程式碼不需任何改變,應用程式只需將對CSharpGL.Windows的引用修改為對SoftGL.Windows的引用即可。真的。
建立ShaderProgram和Shader
根據OpenGL命令,可以推測一種可能的建立和刪除ShaderProgram物件的方式,虛擬碼如下:
1 partial class SoftGLRenderContext 2 { 3 private uint nextShaderProgramName = 1; 4 5 // name -> ShaderProgram object 6 Dictionary<uint, ShaderProgram> nameShaderProgramDict = new Dictionary<uint, ShaderProgram>(); 7 8 private ShaderProgram currentShaderProgram = null; 9 10 public static uint glCreateProgram() // OpenGL functions. 11 { 12 uint id = 0; 13 SoftGLRenderContext context = ContextManager.GetCurrentContextObj(); 14 if (context != null) 15 { 16 id = context.CreateProgram(); 17 } 18 19 return id; 20 } 21 22 private uint CreateProgram() 23 { 24 uint name = nextShaderProgramName; 25 var program = new ShaderProgram(name); //create object. 26 this.nameShaderProgramDict.Add(name, program); // bind name and object. 27 nextShaderProgramName++; // prepare for next name. 28 29 return name; 30 } 31 32 public static void glDeleteProgram(uint program) 33 { 34 SoftGLRenderContext context = ContextManager.GetCurrentContextObj(); 35 if (context != null) 36 { 37 context.DeleteProgram(program); 38 } 39 } 40 41 private void DeleteProgram(uint program) 42 { 43 Dictionary<uint, ShaderProgram> dict = this.nameShaderProgramDict; 44 if (!dict.ContainsKey(program)) { SetLastError(ErrorCode.InvalidValue); return; } 45 46 dict.Remove(program); 47 } 48 }
建立ShaderProgram物件的邏輯很簡單,首先找到當前的Render Context物件,然後讓它建立一個ShaderProgram物件,並使之與一個name繫結(記錄到一個Dictionary<uint, ShaderProgram>字典型別的欄位中)。刪除ShaderProgram物件的邏輯也很簡單,首先判斷引數是否合法,然後將字典中的ShaderProgram物件刪除即可。
OpenGL中的很多物件都遵循這樣的建立模式,例如Shader、Buffer、VertexArrayObject、Framebuffer、Renderbuffer、Texture等。
ShaderProgram是一個大塊頭的型別,它要處理很多和GLSL Shader相關的東西。到時候再具體說。
建立VertexBuffer、IndexBuffer和VertexArrayObject
參見建立ShaderProgram物件的方式。要注意的是,這些型別的建立分2步。第一步是呼叫glGen*(int count, uint[] names);,此時只為其分配了name,沒有建立物件。第二步是首次呼叫glBind*(uint target, uint name);,此時才會真正建立物件。我猜這是早期的函式介面,所以用了這麼囉嗦的方式。
對頂點屬性進行設定
一個頂點快取物件(GLBuffer)實際上是一個位元組陣列(byte[])。它裡面儲存的,可能是頂點的位置屬性(vec3[]),可能是頂點的紋理座標屬性(vec2[]),可能是頂點的密度屬性(float[]),可能是頂點的法線屬性(vec3[]),還可能是這些屬性的某種組合(如一個位置屬性+一個紋理座標屬性這樣的輪流出現)。OpenGL函式glVertexAttribPointer(uint index, int size, uint type, bool normalized, int stride, IntPtr pointer)的作用就是描述頂點快取物件儲存的是什麼,是如何儲存的。
glClear(uint mask)
每次渲染場景前,都應清空畫布,即用glClear(uint mask);清空指定的快取。
OpenGL函式glClearColor(float r, float g, float b, float a);用於指定將畫布清空為什麼顏色。這是十分簡單的,只需設定Render Context中的一個欄位即可。
需要清空顏色快取(GL_COLOR_BUFFER_BIT)時,實際上是將當前Framebuffer物件上的顏色快取設定為指定的顏色。需要清空深度快取(GL_DEPTH_BUFFER_BIT)或模板快取(GL_STENCIL_BUFFER_BIT)時,實際上也是將當前Framebuffer物件上的深度快取或模板快取設定為指定的值。
所以,為了實現glClear(uint mask)函式,必須將Framebuffer和各類快取都準備好。
Framebuffer中的各種快取都可以簡單的用一個Renderbuffer物件充當。一個Renderbuffer物件實際上也是一個位元組陣列(byte[]),只不過它還用額外的欄位記錄了自己的資料格式(GL_RGBA等)等資訊。紋理(Texture)物件裡的各個Image也可以充當Framebuffer中的各種快取。所以Image是和Renderbuffer類似的東西,或者說,它們支援同一個介面IAttachable。
1 interface IAttachable 2 { 3 uint Format { get; } // buffer’s format 4 int Width { get; } // buffer’s width. 5 int Height { get; } // buffer’s height. 6 byte[] DataStore { get; } // buffer data. 7 }
這裡就涉及到對與byte[]這樣的陣列與各種其他型別的陣列(例如描述位置的vec3[])相互賦值的問題。一般,可以用下面的方式解決:
1 byte[] bytes = ... 2 this.pin = GCHandle.Alloc(bytes, GCHandleType.Pinned); 3 IntPtr pointer = this.pin.AddrOfPinnedObject(); 4 var array = (vec3*)pointer.ToPointer(); 5 for (in i = 0; i< ...; i++) { 6 array[i] = ... 7 }
只要能將陣列轉換為 void* 型別,就沒有什麼做不到的了。
glGetIntegerv(uint target, int[] values)
這個十分簡單。一個大大的switch語句。
設定Viewport
設定viewport本身是十分簡單的,與設定lineWidth類似。但是,在一個Render Context物件被首次MakeCurrent()到一個執行緒時,要將Device Context的Width和Height賦值給viewport。這個有點麻煩。
更新uniform變數的值
glDrawElements(..)
總結