CSharpGL(34)以从零编写一个KleinBottle渲染器为例学习如何使用CSharpGL

+BIT祝威+悄悄在此留下版了个权的信息说:

本文用step by step的方式,讲述如何使用CSharpGL渲染一个Klein Bottle,从而得到下图所示的图形。你会看到这并不困难。

 CSharpGL(34)以从零编写一个KleinBottle渲染器为例学习如何使用CSharpGL

 

+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是个著名的三维模型,可以用一个公式来计算它的每个顶点。

 CSharpGL(34)以从零编写一个KleinBottle渲染器为例学习如何使用CSharpGL

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

相关文章: