zoukankan      html  css  js  c++  java
  • CSharpGL(34)以从零编写一个KleinBottle渲染器为例学习如何使用CSharpGL

    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(@"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祝威+悄悄在此留下版了个权的信息说:

    拽控件

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

     

     

    然后拽一个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()`。

  • 相关阅读:
    02.jwt单点登录
    04.RBAC
    COM interop
    C++、c#互调用之VC6 调用 VC6 COM
    Type Library Importer (Tlbimp.exe)
    C++、C#互调用之C++ 调用C# dll
    VS tools
    Type Library to Assembly 转换摘要
    7个顶级心理预言
    c++、C#互调用之c# 调用 vc6 COM
  • 原文地址:https://www.cnblogs.com/bitzhuwei/p/CSharpGL-34-get-to-know-CSharpGL-with-a-Klien-Bottle-renderer-from-scratch.html
Copyright © 2011-2022 走看看