CSharpGL(34)以从零编写一个KleinBottle渲染器为例学习如何使用CSharpGL 开始 用Modern OpenGL渲染 Shader Klein Bottle模型 渲染器(Renderer) GLCanvas 其他 总结

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

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

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

 CSharpGL(34)以从零编写一个KleinBottle渲染器为例学习如何使用CSharpGL
开始
用Modern OpenGL渲染
Shader
Klein Bottle模型
渲染器(Renderer)
GLCanvas
其他
总结

+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
开始
用Modern OpenGL渲染
Shader
Klein Bottle模型
渲染器(Renderer)
GLCanvas
其他
总结

(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(@"shadersKleinBottle.vert"), ShaderType.VertexShader);
 7             shaderCodes[1] = new ShaderCode(File.ReadAllText(@"shadersKleinBottle.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祝威+悄悄在此留下版了个权的信息说:

拽控件

首先我们在项目中添加一个窗口。

 CSharpGL(34)以从零编写一个KleinBottle渲染器为例学习如何使用CSharpGL
开始
用Modern OpenGL渲染
Shader
Klein Bottle模型
渲染器(Renderer)
GLCanvas
其他
总结

 CSharpGL(34)以从零编写一个KleinBottle渲染器为例学习如何使用CSharpGL
开始
用Modern OpenGL渲染
Shader
Klein Bottle模型
渲染器(Renderer)
GLCanvas
其他
总结

然后拽一个GLCanvas控件进来。

 CSharpGL(34)以从零编写一个KleinBottle渲染器为例学习如何使用CSharpGL
开始
用Modern OpenGL渲染
Shader
Klein Bottle模型
渲染器(Renderer)
GLCanvas
其他
总结

稍微布局一下,好看点。

 CSharpGL(34)以从零编写一个KleinBottle渲染器为例学习如何使用CSharpGL
开始
用Modern OpenGL渲染
Shader
Klein Bottle模型
渲染器(Renderer)
GLCanvas
其他
总结

关闭这个窗口,然后重新打开,你应该能看到下面的景象。立方体不停地旋转,钟表则一直显示当前时间,左下角写着控件全名,左上角是FPS。这表明GLCanvas运转良好。

 CSharpGL(34)以从零编写一个KleinBottle渲染器为例学习如何使用CSharpGL
开始
用Modern OpenGL渲染
Shader
Klein Bottle模型
渲染器(Renderer)
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(34)以从零编写一个KleinBottle渲染器为例学习如何使用CSharpGL
开始
用Modern OpenGL渲染
Shader
Klein Bottle模型
渲染器(Renderer)
GLCanvas
其他
总结

 CSharpGL(34)以从零编写一个KleinBottle渲染器为例学习如何使用CSharpGL
开始
用Modern OpenGL渲染
Shader
Klein Bottle模型
渲染器(Renderer)
GLCanvas
其他
总结

★我将持续更新CSharpGL。虽然不能保证最后能做到多好多强大。。。。。。

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

总结

你可以尝试用`OneIndexBuffer`代替`ZeroIndexBuffer`,从而实现画线、面。`OneIndexBuffer`用的是`glDrawElements()`。