文章出處

CSharpGL(34)以從零編寫一個KleinBottle渲染器為例學習如何使用CSharpGL

+BIT祝威+悄悄在此留下版了個權的信息說:

開始

本文用step by step的方式,講述如何使用CSharpGL渲染一個Klein Bottle,從而得到下圖所示的圖形。你會看到這并不困難。

 

 

+BIT祝威+悄悄在此留下版了個權的信息說:

用Modern OpenGL渲染

在Modern OpenGL中,shader是在GPU上執行的程序,用于計算圖形最終的樣子;模型則提供頂點數據給shader。也就是說,shader是算法,模型是數據結構。渲染器(Renderer)就是將兩者聯合起來,實現渲染的那么一個干活的工人。

比喻來說,模型是白菜豆腐牛羊豬肉這些食材,shader是煎炒烹炸川魯粵蘇這些做法,渲染器(Renderer)就是廚師。

我們要用Modern OpenGL渲染一個Klein Bottle,就得完成shader、模型、渲染器這三項。為了避免可有可無的細節干擾,本文都采用最簡單的方式。

+BIT祝威+悄悄在此留下版了個權的信息說:

Shader

我認為從shader開始是一個好習慣,因為shader里除了算法本身,也定義了數據結構(最底層的形式),在shader、模型、渲染器三者中算得上是最為完整的了。

+BIT祝威+悄悄在此留下版了個權的信息說:

Vertex shader

下面這個vertex shader已經十分簡單了。它的功能就是將Klein Bottle模型的一個頂點從模型空間(Model Space)坐標系變換到裁剪空間(Clip Space)坐標系

 1 #version 150 core
 2 
 3 in vec3 in_Position;// 一個頂點
 4 uniform mat4 projectionMatrix;// 投影矩陣
 5 uniform mat4 viewMatrix;// 視圖矩陣
 6 uniform mat4 modelMatrix;// 模型矩陣
 7 
 8 void main(void) {
 9     // 計算頂點位置
10     gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(in_Position, 1.0);
11 }

 

簡單來說,vertex shader程序會對KleinBottle模型上的每個頂點都執行一次。因此在輸入數據上寫的是`in vec3 in_Position`,而不是`in vec3 in_Positions[]`。由于各個頂點之間互不影響,所以GPU就可以通過并行計算的方式大幅度提高渲染效率。即使有上百萬個頂點,GPU也可以同時計算,這等于用一次執行的時間代替了CPU上的一個大型循環的時間。

而`uniform`修飾的變量則是對每次執行的vertex shader都相同的(即全局變量)。

+BIT祝威+悄悄在此留下版了個權的信息說:

Fragment shader

下面這個fragment shader也是十分簡單的。它的功能就是計算每個頂點的顏色。簡單來說,這個fragment shader程序也會對KleinBottle模型上的每個頂點都執行一次。(這是最簡單的情況,為了不分散精力,現在這樣認為即可)

Fragment shader里的`out_Color`你可以改成其他你喜歡的名字,其效果是一樣的。

1 #version 150 core
2 
3 out vec4 out_Color;// 輸出到屏幕
4 
5 uniform vec3 uniformColor = vec3(1, 1, 1);// 顏色為白色
6 
7 void main(void) {
8     out_Color = vec4(uniformColor, 1.0f);// 輸出指定的顏色
9 }
+BIT祝威+悄悄在此留下版了個權的信息說:

Klein Bottle模型

菜系已然確定,下面就該準備食材(模型數據)了。

下面我們就新建一個KleinBottleModel類。為了融入CSharpGL,讓它實現`IBufferable`接口。這個接口的作用是把各式各樣的模型數據轉化為shader能接受的頂點屬性緩存(Vertex Buffer Object)和索引緩存(Index Buffer Object)。(順帶處理一點其他的小事)

1     class KleinBottleModel : IBufferable
2     {
3     }

 

下面我們來逐步完成這個Model類。

+BIT祝威+悄悄在此留下版了個權的信息說:

公式

Klein Bottle是個著名的三維模型,可以用一個公式來計算它的每個頂點。

 

(0 ≤ u < π and 0 ≤ v < 2π)

這個公式輸入變量是u和v,輸出是(x, y, z)。我們先用程序來描述一下這個公式:

 1         private vec3 GetPosition(double u, double v)
 2         {
 3             double sinU = Math.Sin(u), cosU = Math.Cos(u);
 4             double sinV = Math.Sin(v), cosV = Math.Cos(v);
 5             double x = -2.0 * cosU * (3 * cosV - 30 * sinU + 90 * Math.Pow(cosU, 4) * sinU - 60 * Math.Pow(cosU, 6) * sinU + 5 * cosU * cosV * sinU);
 6             double y = -1.0 * sinU * (3 * cosV - 3 * Math.Pow(cosU, 2) * cosV - 48 * Math.Pow(cosU, 4) * cosV + 48 * Math.Pow(cosU, 6) * cosV - 60 * sinU + 5 * cosU * cosV * sinU - 5 * Math.Pow(cosU, 3) * cosV * sinU - 80 * Math.Pow(cosU, 5) * cosV * sinU + 80 * Math.Pow(cosU, 7) * cosV * sinU);
 7             double z = 2.0 * (3.0 + 5 * cosU * sinU) * sinV;
 8 
 9             return new vec3((float)x, (float)y, (float)z);
10         }

在u、v各自的范圍內,各自采樣的點越多,模型就越細致,那么到底要采樣多少呢?我們就用一個`double interval`來控制。

 1         private double interval;
 2 
 3         private int GetUCount(double interval)
 4         {
 5             int uCount = (int)(Math.PI / interval);
 6             return uCount;
 7         }
 8 
 9         private int GetVCount(double interval)
10         {
11             int vCount = (int)(Math.PI * 2 / interval / 10.0);
12             return vCount;
13         }
14 
15         public KleinBottleModel(double interval = 0.02)
16         {
17             this.interval = interval;
18         }

 

+BIT祝威+悄悄在此留下版了個權的信息說:

實現IBufferable

下面來實現`IBufferable`接口。

 1         public const string strPosition = "position";// buffer name.
 2         private VertexAttributeBufferPtr positionBufferPtr = null;
 3 
 4         /// <summary>
 5         /// 獲取指定的頂點屬性緩存。
 6         /// <para>Gets specified vertex buffer object.</para>
 7         /// </summary>
 8         /// <param name="bufferName">buffer name(Gets this name from 'strPosition' etc.</param>
 9         /// <param name="varNameInShader">name in vertex shader like `in vec3 in_Position;`.</param>
10         /// <returns>Vertex Buffer Object.</returns>
11         VertexAttributeBufferPtr IBufferable.GetVertexAttributeBufferPtr(string bufferName, string varNameInShader)
12         {
13             //
14         }
15 
16         private IndexBufferPtr indexBufferPtr = null;
17 
18 
19         IndexBufferPtr IBufferable.GetIndexBufferPtr()
20         {
21             //
22         }
23 
24         /// <summary>
25         /// Uses <see cref="ZeroIndexBuffer"/> or <see cref="OneIndexBuffer"/>.
26         /// </summary>
27         /// <returns></returns>
28         bool IBufferable.UsesZeroIndexBuffer() { return true; }

 

頂點屬性緩存——位置(Vertex Attribute Buffer – Position)

為了簡單,本例中的Klein Bottle,我們只給它一條頂點屬性,即必不可少的位置。等學會了這個,今后再加其他的屬性(顏色、法線等等)就可以觸類旁通了。

提供頂點屬性緩存的是`IBufferable.GetVertexAttributeBufferPtr (string bufferName, string varNameInShader);`這個方法。根據`bufferName`,這個方法提供用戶需要的緩存對象。下面就是實現這個方法的框架結構。

 1         VertexAttributeBufferPtr IBufferable.GetVertexAttributeBufferPtr(string bufferName, string varNameInShader)
 2         {
 3             if (bufferName == KleinBottleModel.strPosition)
 4             {
 5                 if (this.positionBufferPtr == null)
 6                 {
 7                     this.positionBufferPtr = GetPositionBufferPtr(varNameInShader);
 8                 }
 9                 return this.positionBufferPtr;
10             }
11             else
12             {
13                 throw new ArgumentException();
14             }
15         }

 

具體創建位置緩存的方法如下。

 1         private VertexAttributeBufferPtr GetPositionBufferPtr(string varNameInShader)
 2         {
 3 VertexAttributeBufferPtr positionBufferPtr = null;
 4 // 在CPU端創建緩存buffer,buffer實際上是一個數組,數組元素的類型為vec3。
 5             using (var buffer = new VertexAttributeBuffer<vec3>(
 6                 varNameInShader, VertexAttributeConfig.Vec3, BufferUsage.StaticDraw))
 7             { 
 8                 int uCount = GetUCount(this.interval);
 9                 int vCount = GetVCount(this.interval);             
10                 // 申請非托管數組(長度為uCount * vCount * sizeof(vec3)個字節)。到此才真正得到了一個可能很大的空間。
11   buffer.Create(uCount * vCount);
12                 unsafe
13                 {
14                     int index = 0;
15                     // 用unsafe方式設置數組元素的值。
16                     var array = (vec3*)buffer.Header.ToPointer();
17                     for (int uIndex = 0; uIndex < uCount; uIndex++)
18                     {
19                         for (int vIndex = 0; vIndex < vCount; vIndex++)
20                         {
21                             double u = Math.PI * uIndex / uCount;
22                             double v = Math.PI * 2 * vIndex / vCount;
23                             vec3 position = GetPosition(u, v);
24                             array[index++] = position;
25                         }
26                     }
27                 }
28 
29                 // GetBufferPtr()將CPU端的數組上傳到GPU端,GPU返回此buffer的指針,將此指針及其相關數據封裝起來,就成為了我們需要的位置緩存對象。
30                 positionBufferPtr = buffer.GetBufferPtr();
31             }// using(){} 結束,CPU端的非托管數組空間被釋放。即CPU端不再需要保持buffer了。
32 
33             return positionBufferPtr;
34         }
VertexAttributeBufferPtr GetPositionBufferPtr(string varNameInShader)

 

+BIT祝威+悄悄在此留下版了個權的信息說:

索引屬性緩存

每個渲染器(Renderer)都需要一個索引緩存。索引緩存告訴GPU,頂點屬性緩存里的數據是按怎樣的順序依次渲染的。本例用最簡單的索引緩存`ZeroIndexBuffer`。`ZeroIndexBuffer`用`glDrawArrays()`這個OpenGL指令來渲染。

 1         private IndexBufferPtr indexBufferPtr = null;
 2 
 3         IndexBufferPtr IBufferable.GetIndexBufferPtr()
 4         {
 5             if (indexBufferPtr == null)
 6             {
 7                 int uCount = GetUCount(interval);
 8                 int vCount = GetVCount(interval);
 9                 using (var buffer = new ZeroIndexBuffer(DrawMode.Points, 0, uCount * vCount))
10                 {
11                     indexBufferPtr = buffer.GetBufferPtr();
12                 }
13             }
14 
15             return indexBufferPtr;
16         }
+BIT祝威+悄悄在此留下版了個權的信息說:

渲染器(Renderer)

渲染器要做的已經被`Renderer`類型封裝好,只需繼承之就可以。

+BIT祝威+悄悄在此留下版了個權的信息說:

KleinBottleRenderer

 1     class KleinBottleRenderer : Renderer
 2     {
 3         private KleinBottleRenderer(IBufferable model, ShaderCode[] shaderCodes,
 4             AttributeNameMap attributeNameMap, params GLSwitch[] switches)
 5             : base(model, shaderCodes, attributeNameMap, switches)
 6         {
 7             // 設定點的大小。
 8             this.switchList.Add(new PointSizeSwitch(3));
 9         }
10     }

你注意到這個`KleinBottleRenderer`的構造函數被標記為`private`。這是因為我們不想每次都讓用戶去指定那些參數(又麻煩又困難),我們用一個`static`方法來創建` KleinBottleRenderer `。

 1     class KleinBottleRenderer : Renderer
 2     {
 3         public static KleinBottleRenderer Create(KleinBottleModel model)
 4         {
 5             var shaderCodes = new ShaderCode[2];
 6             shaderCodes[0] = new ShaderCode(File.ReadAllText(@"shaders\KleinBottle.vert"), ShaderType.VertexShader);
 7             shaderCodes[1] = new ShaderCode(File.ReadAllText(@"shaders\KleinBottle.frag"), ShaderType.FragmentShader);
 8             var map = new AttributeNameMap();
 9             map.Add("in_Position", // variable name in vertex shader.
10                 KleinBottleModel.strPosition // buffer name in model.
11                 );
12             var renderer = new KleinBottleRenderer(model, shaderCodes, map);
13 
14             return renderer;
15         }
16     }

 你注意到這里有個`AttributeNameMap`對象,它指定了shader中的in屬性與`IBufferable`模型中的頂點屬性的對應關系。有了這個map,`Renderer`才能把shader和模型關聯起來。

+BIT祝威+悄悄在此留下版了個權的信息說:

Override渲染功能

對于每個具體的Renderer,或多或少都有各自的特殊設定。因此需要override DoRender();方法。此方法完成了真正執行渲染的功能。

 1     class KleinBottleRenderer : Renderer
 2     {
 3         public vec3 UniformColor { get; set; }
 4         
 5         protected override void DoRender(RenderEventArgs arg)
 6         {
 7             mat4 projection = arg.Camera.GetProjectionMatrix();
 8             mat4 view = arg.Camera.GetViewMatrix();
 9             mat4 model = this.GetModelMatrix();
10             this.SetUniform("projectionMatrix", // variable name in shader.
11 projection);
12             this.SetUniform("viewMatrix", // variable name in shader.
13 view);
14             this.SetUniform("modelMatrix", // variable name in shader.
15 model);
16             this.SetUniform("uniformColor", // variable name in shader.
17 this.uniformColor);
18 
19             base.DoRender(arg);
20         }
21     }

 

可見一般都是設定一些uniform變量。

+BIT祝威+悄悄在此留下版了個權的信息說:

Override 初始化功能

對于每個具體的Renderer,或多或少都有各自的特殊項目需要初始化。因此需要override DoInitialize();方法。不過本例實際上并不需要。

1     class KleinBottleRenderer : Renderer
2     {
3         protected override void DoInitialize()
4         {
5             base.DoInitialize();
6         }
7     }

現在渲染功能準備完畢,我們把它放到窗口上,真正畫出來。

+BIT祝威+悄悄在此留下版了個權的信息說:

GLCanvas

+BIT祝威+悄悄在此留下版了個權的信息說:

拽控件

首先我們在項目中添加一個窗口。

 

 

然后拽一個GLCanvas控件進來。

 

稍微布局一下,好看點。

 

關閉這個窗口,然后重新打開,你應該能看到下面的景象。立方體不停地旋轉,鐘表則一直顯示當前時間,左下角寫著控件全名,左上角是FPS。這表明GLCanvas運轉良好。

 

+BIT祝威+悄悄在此留下版了個權的信息說:

場景

控件就準備好了。下面就把一個 KlienBottleRenderer加入此控件。

首先來準備好場景`Scene`,有了場景,就可以添加、管理多個Renderer。當然,本例只需要1個。

 1         private Scene scene;
 2 
 3         private void Form_Load(object sender, EventArgs e)
 4         {
 5             // step 1.
 6             // 創建攝像機。
 7             var camera = new Camera(
 8                 new vec3(3, 4, 5) * 4, new vec3(0, 0, 0), new vec3(0, 1, 0),
 9                 CameraType.Perspecitive, this.glCanvas1.Width, this.glCanvas1.Height);
10             // 指定移動攝像機的方式(讓攝像機像衛星一樣圍繞目標旋轉)。
11             var rotator = new SatelliteManipulater();
12             rotator.Bind(camera, this.glCanvas1);
13             // 創建場景。
14             var scene = new Scene(camera, this.glCanvas1);
15             // 指定背景色。
16             scene.ClearColor = Color.SkyBlue;
17             this.scene = scene;
18             // 指定Resize如何處理。
19             this.glCanvas1.Resize += this.scene.Resize;
20 
21             // step 2.
22             //
23         }

 

+BIT祝威+悄悄在此留下版了個權的信息說:

場景對象

有場景了,該往里面加一些能渲染的對象了。本例就加入一個` KleinBottleRenderer`。

 1 private void Form_Load(object sender, EventArgs e)
 2         {
 3             // step 1.
 4             // 5             // step 2.
 6             // 創建Renderer。
 7             KleinBottleRenderer renderer = KleinBottleRenderer.Create(new KleinBottleModel(interval: 0.2));
 8             // 把renderer封裝為SceneObject。
 9             SceneObject obj = renderer.WrapToSceneObject(generateBoundingBox: true);
10             // 把SceneObject加入場景的對象列表(其實是個樹結構)。
11             this.scene.RootObject.Children.Add(obj);
12         }
+BIT祝威+悄悄在此留下版了個權的信息說:

UI

其實這樣就可以了。不過為了更多地展示Scene的能力,我們再添加一個UI對象——坐標軸到窗口的左下角。

1 private void Form_Load(object sender, EventArgs e)
2         {
3             // step 3.
4             // 創建一個坐標軸對象。
5             var uiAxis = new UIAxis(AnchorStyles.Left | AnchorStyles.Bottom,
6                 new Padding(3, 3, 3, 3), new Size(128, 128));
7             // 坐標軸對象加入到場景里的UI列表(其實是個樹結構)。
8             this.scene.UIRoot.Children.Add(uiAxis);
9         }
+BIT祝威+悄悄在此留下版了個權的信息說:

其他

至此你就可以看到本文開始處渲染出的效果了。

使用CSharpGL,你可以獲得如下好處:

★不必擔心使用OpenGL指令時不小心用錯了各種各樣的target、param等標記。這種易錯又難易排查的問題往往會讓初學者想去自殺。

★CSharpGL會自動釋放那些不需要的CPU端Buffer占用的內存。CSharpGL通過封裝好的Buffer對象的使用方式,保證了不需要的大量空間會被及時釋放。

★CSharpGL封裝了拾取、拖拽模型、UI、文字、場景等常用的功能,你只需繼承這些類型即可使用。CSharpGL對每項功能都提供了Demo,運行這些demo,就可以得知如何使用這些功能。

★可以用PropertyGrid來實時控制渲染效果,這是十分便利的工具。例如本例中,你可以用PointSizeSwitch來控制渲染的頂點的大小。

 

 

★我將持續更新CSharpGL。雖然不能保證最后能做到多好多強大。。。。。。

 

+BIT祝威+悄悄在此留下版了個權的信息說:

總結

你可以嘗試用`OneIndexBuffer`代替`ZeroIndexBuffer`,從而實現畫線、面。`OneIndexBuffer`用的是`glDrawElements()`。

 


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()