1. 程式人生 > >CSharpGL(49)試水OpenGL軟實現

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有興趣的同學加入(

https://github.com/bitzhuwei/SoftGL

從使用者的角度開始

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(..)

 

總結