找回密码
 立即注册
首页 业界区 业界 纯C#软实现openGL(V0.1),黑盒变白盒

纯C#软实现openGL(V0.1),黑盒变白盒

僚娥 2025-6-21 15:54:54
纯C#软实现openGL(V0.1),黑盒变白盒

为了彻底掌握openGL,做一个openGL的软实现(命名为SoftGLImpl)是必要的。(而非仅仅调用opengl32.dll)
openGL API的每个函数,都是在按下述方式执行3D渲染算法:
  1. using GLsizei = System.Int32;
  2. using GLuint = System.UInt32;
  3. using GLenum = System.UInt32;
  4. public unsafe partial class SoftGL { // class for openGL API
  5.     // 以glGenBuffers为例
  6.     static void glGenBuffers(GLsizei n, GLuint[] names) {
  7.         RenderContext? context = SoftGLImpl.SoftGL.GetCurrentContextObj();
  8.         if (context == null) { return; }// 若不先创建openGL上下文,则直接调用此API是无效的。
  9.         context.GenBuffers(n, names);// 安排数据,保存到openGL上下文中。
  10.     }
  11.     // 以glDrawElements为例
  12.     public static void glDrawElements(GLenum mode, GLsizei count, GLenum type, IntPtr indices) {
  13.         RenderContext? context = SoftGLImpl.SoftGL.GetCurrentContextObj();
  14.         if (context == null) { return; }// 若不先创建openGL上下文,则直接调用此API是无效的。
  15.         context.DrawElements(mode, count, type, indices);// 执行渲染过程,结果将被保存到openGL上下文中。
  16.     }
  17. }
复制代码
这些函数的作用,要么是安排buffer/shader/texture数据、各种选项,要么是执行渲染过程。整个渲染过程(pipeline)如下图所示:

实现openGL就是要实现图中的全部过程。本文重点介绍下列问题:

  • 如何让shader运行起来
  • 如何实现pipeline各步骤中的算法
  • 如何提升纯C#软实现的效率
如何让shader运行起来

所谓modern openGL,其核心特点是利用shader并行计算的办法来提高3D图形渲染的效率和效果。shader是一段GLSL代码,类似C语言,所以我必须做一个简单的编译器。https://www.cnblogs.com/bitzhuwei/p/18631231是我做好的GLSL编译器前端。这样,我就可以用下面的思路解决“如何让shader运行起来”的问题:
  1. // 1. 解析shader字符串,得到其语义信息
  2. var parser = new ShaderParser();
  3. string source = File.ReadAllText("blinnphong.vert");
  4. List<Token> tokens = parser.Analyze(source);
  5. SyntaxTree tree = parser.Parse(tokens);
  6. var translation_unit = parser.Extract(tree, tokens, source);
  7. // 2. 将语义信息转换为C#代码
  8. var builder = new StringBuilder();
  9. using (var writer = new StringWriter(builder)) {
  10.     var config = new BlankConfig(inlineBlank: 0, forceNewline: false);
  11.     var context = new FormatContext(tabUnit: 4, tabCount: 0, tokens);
  12.     translation_unit.Transform(config, writer, context);
  13. }
  14. var csCode = builder.ToString();
  15. // 3. 动态编译C#代码并调用,即模拟GPU运行shader的过程
  16. var script = CSharpScript.Create<VertexShaderCode>(csCode);
  17. var result = await script.RunAsync();
复制代码
主要的工作量在于:

  • 编写GLSL编译器前端
    详情可参考https://www.cnblogs.com/bitzhuwei/p/18683262/my-own-parsers。
  • 根据语法树生成C#版的shader代码。由于GLSL和C#的语法书写差异,需要处理下列差异:
点击查看代码
  1. // from GLSL
  2. struct { mat4 model; mat4 view; mat4 projection; }  va[3], vb;
  3. // to C#
  4. struct struct0 { mat4 model; mat4 view; mat4 projection; }
  5. struct0[] va = new struct0[3]; struct0 vb;
  6. // from GLSL
  7. layout(origin_upper_left, pixel_center_integer) in vec4 gl_FragCoord;
  8. // to C#
  9. [layout(values = [origin_upper_left, pixel_center_integer])]
  10. [In]
  11. vec4 gl_FragCoord;
  12. // from GLSL
  13. layout(std140, binding = 2) uniform Camera {
  14.     mat4 view;
  15.     mat4 projection;
  16. };
  17. // to C#
  18. [layout(binding = 2, values = [std140])]
  19. [uniform]
  20. struct Camera字 {
  21.     mat4 view;
  22.     mat4 projection;
  23. }
  24. Camera字 Camera;
复制代码

  • 为C#版的shader代码提供底层支持,这包括实现GLSL内置的变量(gl_VertexID、gl_FragCoord等)、类型(vec4、mat4等)、函数(max、pow、mix)。
pipeline各步骤中的算法

pipeline很长,里面涉及的算法很多。每次调用glDrawElements()都是在执行pipeline。下面我们详细解释这里的各个步骤。
  1. public static void glDrawElements(GLenum mode, GLsizei count, GLenum type, IntPtr indices) {
  2.     var context = SoftGL.GetCurrentContextObj();
  3.     if (context == null) { return; }
  4.     if (!Enum.IsDefined(typeof(DrawTarget), mode) || !Enum.IsDefined(typeof(DrawElementsType), type))
  5.     { context.lastErrorCode = (uint)(ErrorCode.InvalidEnum); return; }
  6.     if (count < 0) { context.lastErrorCode = (uint)(ErrorCode.InvalidValue); return; }
  7.     // TODO: GL_INVALID_OPERATION is generated if a geometry shader is active
  8.     // and mode is incompatible with the input primitive type of the geometry shader in the currently installed program object.
  9.     // TODO: GL_INVALID_OPERATION is generated
  10.     // if a non-zero buffer object name is bound to an enabled array
  11.     // or the element array and the buffer object's data store is currently mapped.
  12.     var vao = context.currentVertexArrayObject; // data structure.
  13.     if (vao == null) { return; }
  14.     var program = context.currentShaderProgram; // algorithm.
  15.     if (program == null) { return; }
  16.     var indexBuffer = context.target2CurrentBuffer[(GLenum)BindBufferTarget.ElementArrayBuffer];
  17.     if (indexBuffer == null) { return; }
  18.     // execute vertex shader for each vertex!
  19.     Dictionary<uint, VertexCodeBase> vertexID2Shader = VertexShaderStage(
  20.         count, (DrawElementsType)type, indices, vao, program, indexBuffer);
  21.     var framebuffer = context.target2CurrentFramebuffer[(GLenum)BindFramebufferTarget.Framebuffer];
  22.     ClipSpace2NormalDeviceSpace(vertexID2Shader);
  23.     // linear interpolation.
  24.     ConcurrentBag<Fragment> fragmentList = LinearInterpolation(
  25.         context, (DrawTarget)mode, count, (DrawElementsType)type, indices, vao, program, indexBuffer, vertexID2Shader);
  26.     // execute fargment shader for each fragment!
  27.     FragmentShaderStage(program, fragmentList);
  28.     // TODO: Scissor test
  29.     // TODO: Multisampel fragment operations
  30.     // TODO: Stencil test
  31.     DepthTest(context, fragmentList);
  32.     // TODO: Blending
  33.     // TODO: Dithering
  34.     // TODO: Logical operations
  35.     // write fragments to framebuffer's colorbuffer attachment(s).
  36.     WriteFragments2Framebuffer(context, framebuffer, fragmentList);
  37. }
  38. private static void WriteFragments2Framebuffer(RenderContext context, GLFramebuffer framebuffer, ConcurrentBag<Fragment> fragmentList) {
  39.     if (framebuffer.ColorbufferAttachments == null) { return; }
  40.     uint[] drawBufferIndexes = framebuffer.DrawBuffers.ToArray();
  41.     Func<int, IntPtr, Fragment, bool> hasValidDepth;
  42.     var depthBuffer = framebuffer.DepthbufferAttachment;
  43.     GCHandle pin = new GCHandle(); IntPtr pDepthBuffer = IntPtr.Zero;
  44.     if (depthBuffer == null) { hasValidDepth = alwaysHasValidDepth; }
  45.     else {
  46.         switch (depthBuffer.Format) {
  47.         case GL.GL_DEPTH_COMPONENT: hasValidDepth = hasValidDepth32float; break;
  48.         case GL.GL_DEPTH_COMPONENT24: hasValidDepth = hasValidDepth24uint; break;
  49.         case GL.GL_DEPTH_COMPONENT32: hasValidDepth = hasValidDepth32float; break;
  50.         default: throw new Exception("bug, fix this!");
  51.         }
  52.         pin = GCHandle.Alloc(depthBuffer.DataStore, GCHandleType.Pinned);
  53.         pDepthBuffer = pin.AddrOfPinnedObject();
  54.     }
  55.     int width = context.viewport.w;
  56.     foreach (var fragment in fragmentList) {
  57.         if (fragment.discard) { continue; }
  58.         if (fragment.outVariables == null) { continue; }
  59.         if (!hasValidDepth(width, pDepthBuffer, fragment)) { continue; }
  60.         for (int i = 0; i < fragment.outVariables.Length && i < drawBufferIndexes.Length; i++) {
  61.             PassBuffer outVar = fragment.outVariables[i];
  62.             var attachment = framebuffer.ColorbufferAttachments[drawBufferIndexes[i].ToIndex()];
  63.             if (attachment != null) {
  64.                 attachment.Set((int)fragment.gl_FragCoord.x, (int)fragment.gl_FragCoord.y, outVar);
  65.             }
  66.         }
  67.     }
  68. }
  69. private static int ByteLength(DrawElementsType type) {
  70.     int result = 0;
  71.     switch (type) {
  72.     case DrawElementsType.UnsignedByte: result = sizeof(byte); break;
  73.     case DrawElementsType.UnsignedShort: result = sizeof(ushort); break;
  74.     case DrawElementsType.UnsignedInt: result = sizeof(uint); break;
  75.     default: throw new NotDealWithNewEnumItemException(typeof(DrawElementsType));
  76.     }
  77.     return result;
  78. }
复制代码
vertex processing

这一步的任务:
①预备:找到要处理的vertex,为其挨个编号;
②计算:为每个vertex分别调用vertex shader的main()函数;
③持有:保存②的计算结果,供下一步使用。
点击查看代码 vertex processing
  1. // vertex processing
  2. private static unsafe Dictionary<uint, VertexCodeBase> VertexShaderStage(
  3.     int count, // how many elements to be rendered
  4.     DrawElementsType type,
  5.     IntPtr indices, // an offset of the first index in the buffer currently bound to GL_ELEMENT_ARRAY_BUFFER
  6.     GLVertexArrayObject vao, GLProgram program, GLBuffer indexBuffer) {
  7.     var vs = program.VertexShader;
  8.     uint vertexCount = GetVertexCount(vao, indexBuffer, type);
  9.     // gl_VertexID -> shader object
  10.     var vertexID2Shader = new Dictionary<uint, VertexCodeBase>((int)vertexCount);
  11.     // execute vertex shader for each vertex.
  12.     byte[] indexData = indexBuffer.Data;
  13.     int indexLength = indexData.Length / ByteLength(type);
  14.     GCHandle pin = GCHandle.Alloc(indexData, GCHandleType.Pinned);
  15.     IntPtr pointer = pin.AddrOfPinnedObject();
  16.     int indexID = indices.ToInt32() / ByteLength(type);
  17.     for (var c = 0; c < count && indexID < indexLength; indexID++, c++) {
  18.         uint gl_VertexID = GetVertexID(pointer, type, indexID);
  19.         var instance = vs.CreateCodeInstance() as VertexCodeBase; // an executable vertex shader.
  20.         vertexID2Shader.Add(gl_VertexID, instance);
  21.         instance.gl_VertexID = (int)gl_VertexID; // setup gl_VertexID.
  22.         // setup "in SomeType varName;" vertex attributes.
  23.         Dictionary<uint, VertexAttribDesc> locVertexAttribDict = vao.LocVertexAttribDict;
  24.         foreach (PassVariable inVar in vs.name2inVar.Values) {
  25.             if (locVertexAttribDict.TryGetValue(inVar.location, out var desc)) {
  26.                 byte[] dataStore = desc.vbo.Data;
  27.                 int byteIndex = desc.GetDataIndex(gl_VertexID);
  28.                 VertexAttribType vertexAttribType = (VertexAttribType)desc.dataType;
  29.                 var value = dataStore.ToStruct(inVar.fieldInfo.FieldType, byteIndex);
  30.                 inVar.fieldInfo.SetValue(instance, value);
  31.             }
  32.         }
  33.         // setup "uniform SomeType varName;" in vertex shader.
  34.         Dictionary<string, UniformValue> nameUniformDict = program.name2Uniform;
  35.         foreach (UniformVariable uniformVar in vs.Name2uniformVar.Values) {
  36.             string name = uniformVar.fieldInfo.Name;
  37.             if (nameUniformDict.TryGetValue(name, out var obj)) {
  38.                 if (obj.value != null) {
  39.                     uniformVar.fieldInfo.SetValue(instance, obj.value);
  40.                 }
  41.             }
  42.         }
  43.         instance.main(); // execute vertex shader code.
  44.     }
  45.     pin.Free();
  46.     return vertexID2Shader;
  47. }
  48. // Get the vertex id at specified <paramref name="indexID"/> of the array represented by <paramref name="pointer"/>.
  49. // The <paramref name="type"/> indicates the type of the array(byte[], ushort[] or uint[]).
  50. private static unsafe uint GetVertexID(IntPtr pointer, DrawElementsType type, int indexID) {
  51.     uint gl_VertexID = uint.MaxValue;
  52.     switch (type) {
  53.     case DrawElementsType.UnsignedByte: {
  54.         byte* array = (byte*)pointer;
  55.         gl_VertexID = array[indexID];
  56.     }
  57.     break;
  58.     case DrawElementsType.UnsignedShort: {
  59.         ushort* array = (ushort*)pointer;
  60.         gl_VertexID = array[indexID];
  61.     }
  62.     break;
  63.     case DrawElementsType.UnsignedInt: {
  64.         uint* array = (uint*)pointer;
  65.         gl_VertexID = array[indexID];
  66.     }
  67.     break;
  68.     default: throw new NotDealWithNewEnumItemException(typeof(DrawElementsType));
  69.     }
  70.     return gl_VertexID;
  71. }
  72. // How many vertexIDs are there in the specified <paramref name="byteArray"/>.
  73. private static uint GetVertexIDCount(byte[] byteArray, DrawElementsType type) {
  74.     uint result = 0;
  75.     uint byteLength = (uint)byteArray.Length;
  76.     switch (type) {
  77.     case DrawElementsType.UnsignedByte: result = byteLength; break;
  78.     case DrawElementsType.UnsignedShort: result = byteLength / 2; break;
  79.     case DrawElementsType.UnsignedInt: result = byteLength / 4; break;
  80.     default: throw new NotDealWithNewEnumItemException(typeof(DrawElementsType));
  81.     }
  82.     return result;
  83. }
  84. // Gets the maximum vertexID in the specified <paramref name="byteArray"/>.
  85. private static unsafe uint GetMaxVertexID(byte[] byteArray, DrawElementsType type) {
  86.     int byteLength = byteArray.Length;
  87.     GCHandle pin = GCHandle.Alloc(byteArray, GCHandleType.Pinned);
  88.     IntPtr pointer = pin.AddrOfPinnedObject();
  89.     uint gl_VertexID = 0;
  90.     switch (type) {
  91.     case DrawElementsType.UnsignedByte: {
  92.         byte* array = (byte*)pointer;
  93.         for (int i = 0; i < byteLength; i++) {
  94.             if (gl_VertexID < array[i]) { gl_VertexID = array[i]; }
  95.         }
  96.     }
  97.     break;
  98.     case DrawElementsType.UnsignedShort: {
  99.         ushort* array = (ushort*)pointer;
  100.         int length = byteLength / 2;
  101.         for (int i = 0; i < length; i++) {
  102.             if (gl_VertexID < array[i]) { gl_VertexID = array[i]; }
  103.         }
  104.     }
  105.     break;
  106.     case DrawElementsType.UnsignedInt: {
  107.         uint* array = (uint*)pointer;
  108.         int length = byteLength / 4;
  109.         for (int i = 0; i < length; i++) {
  110.             if (gl_VertexID < array[i]) { gl_VertexID = array[i]; }
  111.         }
  112.     }
  113.     break;
  114.     default: throw new NotDealWithNewEnumItemException(typeof(DrawElementsType));
  115.     }
  116.     pin.Free();
  117.     return gl_VertexID;
  118. }
  119. private static uint GetVertexCount(GLVertexArrayObject vao, GLBuffer indexBuffer, DrawElementsType type) {
  120.     uint vertexCount = 0;
  121.     VertexAttribDesc[] descs = vao.LocVertexAttribDict.Values.ToArray();
  122.     if (descs.Length > 0) {
  123.         int c = descs[0].GetVertexCount();
  124.         if (c >= 0) { vertexCount = (uint)c; }
  125.     }
  126.     else {
  127.         uint maxvertexID = GetMaxVertexID(indexBuffer.Data, type);
  128.         uint vertexIDCount = GetVertexIDCount(indexBuffer.Data, type);
  129.         vertexCount = Math.Min(maxvertexID, vertexIDCount);
  130.     }
  131.     return vertexCount;
  132. }
复制代码
tessellation

primitive processing

transform feedback processing

rasterization

上一步得到了用vertex和其他参数描述的POINTS、LINES、TRIANGLES、QUADS,这一步要计算出它们会出现在哪些像素点上。
POINTS的光栅化

POINTS的光栅化过程最简单,因为不需要插值。
  1. private static unsafe ConcurrentBag<Fragment> LinearInterpolationPoints(RenderContext context, int count, DrawElementsType type, IntPtr indices, GLVertexArrayObject vao, GLProgram program, GLBuffer indexBuffer, Dictionary<uint, VertexCodeBase> vertexID2Shader) {
  2.     var result = new System.Collections.Concurrent.ConcurrentBag<Fragment>();
  3.     byte[] indexData = indexBuffer.Data;
  4.     int indexLength = indexData.Length / ByteLength(type);
  5.     GCHandle pin = GCHandle.Alloc(indexData, GCHandleType.Pinned);
  6.     IntPtr pointer = pin.AddrOfPinnedObject();
  7.     ivec4 viewport = context.viewport;
  8.     int indexID = indices.ToInt32() / ByteLength(type);
  9.     for (var c = 0; c < count && indexID < indexLength; indexID++, c++) {
  10.         uint gl_VertexID = GetVertexID(pointer, type, indexID);
  11.         var shaderObj = vertexID2Shader[gl_VertexID];
  12.         var fragCoord = new vec3(
  13.             (shaderObj.gl_Position.x + 1) / 2.0f * viewport.z + viewport.x,
  14.             (shaderObj.gl_Position.y + 1) / 2.0f * viewport.w + viewport.y,
  15.             (shaderObj.gl_Position.z + 1) / 2.0f * (float)(context.depthRangeFar - context.depthRangeNear)
  16.                 + (float)context.depthRangeNear);
  17.         var fragment = new Fragment(fragCoord, shaderObj);
  18.         result.Add(fragment);
  19.     }
  20.     pin.Free();
  21.     return result;
  22. }
复制代码
LINES的光栅化

上一步得到的是LINE的两个端点的位置,现在需要通过插值确定LINE会出现在哪些像素Fragment上。
这用到了Bresenham算法。本项目提供了一个展示此算法效果的demo,如下图所示:
2.png

点击查看代码 Bresenham算法对LINE插值
  1. private static void FindPixelsAtLine(vec3 start, vec3 end, List<Pixel3> pixels) {
  2.     if (start.x < end.x) { DoFindPixelsAtLine(start, end, pixels); }
  3.     else { DoFindPixelsAtLine(end, start, pixels); }
  4. }
  5. // from left(start) to right(end)
  6. private static void DoFindPixelsAtLine(vec3 start, vec3 end, List<Pixel3> pixels) {
  7.     // now start.X <= end.X
  8.     if (start.y < end.y) { FindPixelsAtLine1(start, end, pixels); }
  9.     else { FindPixelsAtLine2(start, end, pixels); }
  10. }
  11. // from (0, height - 1)(start) to (width - 1, 0)(end)
  12. private static void FindPixelsAtLine2(vec3 start, vec3 end, List<Pixel3> pixels) {
  13.     var x0 = (int)start.x; var y0 = (int)start.y;
  14.     var x1 = (int)end.x; var y1 = (int)end.y;
  15.     float dx = x1 - x0, dy = y0 - y1;
  16.     if (dx >= dy) {
  17.         float p = dy + dy - dx;
  18.         for (; x0 <= x1; x0++) {
  19.             var a = (x0 + 0.5f - start.x) / (end.x - start.x);
  20.             if (x0 == x1) { y0 = y1; }
  21.             pixels.Add(new Pixel3(x0, y0, start.z + a * (end.z - start.z)));
  22.             if (p > 0) {
  23.                 y0 -= 1;
  24.                 p = p + dy + dy - dx - dx;
  25.             }
  26.             else {
  27.                 p = p + dy + dy;
  28.             }
  29.         }
  30.     }
  31.     else {
  32.         float p = dx + dx - dy;
  33.         for (; y0 >= y1; y0--) {
  34.             var a = (y0 + 0.5f - end.y) / (start.y - end.y);
  35.             if (y0 == y1) { x0 = x1; }
  36.             pixels.Add(new Pixel3(x0, y0, end.z + a * (start.z - end.z)));
  37.             if (p >= 0) {
  38.                 x0 += 1;
  39.                 p = p + dx + dx - dy - dy;
  40.             }
  41.             else {
  42.                 p = p + dx + dx;
  43.             }
  44.         }
  45.     }
  46. }
  47. // from (0, 0)(start) to (width - 1, height - 1)(end)
  48. private static void FindPixelsAtLine1(vec3 start, vec3 end, List<Pixel3> pixels) {
  49.     var x0 = (int)start.x; var y0 = (int)start.y;
  50.     var x1 = (int)end.x; var y1 = (int)end.y;
  51.     float dx = x1 - x0, dy = y1 - y0;
  52.     if (dx >= dy) {
  53.         float p = dy + dy - dx;
  54.         for (; x0 <= x1; x0++) {
  55.             var a = (x0 + 0.5f - start.x) / (end.x - start.x);
  56.             if (x0 == x1) { y0 = y1; }
  57.             pixels.Add(new Pixel3(x0, y0, start.z + a * (end.z - start.z)));
  58.             if (p >= 0) {
  59.                 y0 += 1;
  60.                 p = p + dy + dy - dx - dx;
  61.             }
  62.             else {
  63.                 p = p + dy + dy;
  64.             }
  65.         }
  66.     }
  67.     else {
  68.         float p = dx + dx - dy;
  69.         for (; y0 <= y1; y0++) {
  70.             var a = (y0 + 0.5f - start.y) / (end.y - start.y);
  71.             if (y0 == y1) { x0 = x1; }// the last pixel
  72.             pixels.Add(new Pixel3(x0, y0, start.z + a * (end.z - start.z)));
  73.             if (p >= 0) {
  74.                 x0 += 1;
  75.                 p = p + dx + dx - dy - dy;
  76.             }
  77.             else {
  78.                 p = p + dx + dx;
  79.             }
  80.         }
  81.     }
  82. }
复制代码
TRIANGLES的光栅化

上一步得到的是TRIANGLE的三个端点的位置,现在需要通过插值确定TRIANGLE会出现在哪些像素Fragment上。
用Bresenham算法确定每一列里最上一个和最下一个像素的位置,就可以逐列确定全部Fragment,且便于并行计算。当然,也可以逐行扫描。
本项目提供了一个展示此算法效果的demo,如下图所示:
3.png

点击查看代码 借助Bresenham算法逐列扫描Fragment
  1. unsafe private static void FindFragmentsInTriangle(
  2.     vec3 fragCoord0, vec3 fragCoord1, vec3 fragCoord2,
  3.     VertexCodeBase endpoints0, VertexCodeBase endpoints1, VertexCodeBase endpoints2,
  4.     ConcurrentBag<Fragment> result) {
  5.     int left = (int)fragCoord0.x, right = left;
  6.     if (left > (int)fragCoord1.x) { left = (int)fragCoord1.x; }
  7.     if (left > (int)fragCoord2.x) { left = (int)fragCoord2.x; }
  8.     if (right < (int)fragCoord1.x) { right = (int)fragCoord1.x; }
  9.     if (right < (int)fragCoord2.x) { right = (int)fragCoord2.x; }
  10.     var scanlines = new Scanline[right - left + 1];// we'll find the vertial scanlines
  11.     LocateScanlines(fragCoord0, fragCoord1, left, scanlines);
  12.     LocateScanlines(fragCoord1, fragCoord2, left, scanlines);
  13.     LocateScanlines(fragCoord2, fragCoord0, left, scanlines);
  14.     var matrix = new mat3(fragCoord0, fragCoord1, fragCoord2);
  15.     var inverseMat = CodeBase.inverse(matrix);
  16.     // way #1
  17.     for (int i = 0; i < scanlines.Length; i++) {
  18.         var scanline = scanlines[i];
  19.         var min = scanline.start; var max = scanline.end;
  20.         for (int y = min.y; y <= max.y; y++) {
  21.             float a = (min.y != max.y) ? (y + 0.5f - min.y) / (max.y - min.y) : (0);
  22.             float z = min.depth + a * (max.depth - min.depth);
  23.             var pixel = new vec3(min.x + 0.5f, y + 0.5f, z);// pixel.x += 0.5f; pixel.y += 0.5f;
  24.             vec3 p012 = inverseMat * pixel;
  25.             var fragment = new Fragment(pixel, endpoints0, endpoints1, endpoints2, p012.x, p012.y, p012.z);
  26.             result.Add(fragment);
  27.         }
  28.     }
  29. }
  30. private static void LocateScanlines(vec3 start, vec3 end,
  31.     int left, Scanline[] scanlines) {
  32.     if (start.x < end.x) { DoLocateScanlines(start, end, left, scanlines); }
  33.     else { DoLocateScanlines(end, start, left, scanlines); }
  34. }
  35. private static void DoLocateScanlines(vec3 start, vec3 end, int left, Scanline[] scanlines) {
  36.     // now start.x <= end.x
  37.     if (start.y < end.y) { LocateScanlines1(start, end, left, scanlines); }
  38.     else { LocateScanlines2(start, end, left, scanlines); }
  39. }
  40. // from (0, height - 1)(start) to (width - 1, 0)(end)
  41. private static void LocateScanlines2(vec3 start, vec3 end, int left, Scanline[] scanlines) {
  42.     var x0 = (int)start.x; var y0 = (int)start.y;
  43.     var x1 = (int)end.x; var y1 = (int)end.y;
  44.     float dx = x1 - x0, dy = y0 - y1;
  45.     if (dx >= dy) {
  46.         float p = dy + dy - dx;
  47.         for (; x0 <= x1; x0++) {
  48.             var a = (x0 + 0.5f - start.x) / (end.x - start.x);
  49.             if (x0 == x1) { y0 = y1; }
  50.             {
  51.                 var index = x0 - left;
  52.                 scanlines[index].TryExtend(x0, y0, start.z + a * (end.z - start.z));
  53.             }
  54.             if (p > 0) {
  55.                 y0 -= 1;
  56.                 p = p + dy + dy - dx - dx;
  57.             }
  58.             else {
  59.                 p = p + dy + dy;
  60.             }
  61.         }
  62.     }
  63.     else {
  64.         float p = dx + dx - dy;
  65.         for (; y0 >= y1; y0--) {
  66.             var a = (y0 + 0.5f - end.y) / (start.y - end.y);
  67.             if (y0 == y1) { x0 = x1; }
  68.             {
  69.                 var index = x0 - left;
  70.                 scanlines[index].TryExtend(x0, y0, end.z + a * (start.z - end.z));
  71.             }
  72.             if (p >= 0) {
  73.                 x0 += 1;
  74.                 p = p + dx + dx - dy - dy;
  75.             }
  76.             else {
  77.                 p = p + dx + dx;
  78.             }
  79.         }
  80.     }
  81. }
  82. // from (0, 0)(start) to (width - 1, height - 1)(end)
  83. private static void LocateScanlines1(vec3 start, vec3 end, int left, Scanline[] scanlines) {
  84.     var x0 = (int)start.x; var y0 = (int)start.y;
  85.     var x1 = (int)end.x; var y1 = (int)end.y;
  86.     float dx = x1 - x0, dy = y1 - y0;
  87.     if (dx >= dy) {
  88.         float p = dy + dy - dx;
  89.         for (; x0 <= x1; x0++) {
  90.             var a = (x0 + 0.5f - start.x) / (end.x - start.x);
  91.             if (x0 == x1) { y0 = y1; }
  92.             {
  93.                 var index = x0 - left;
  94.                 scanlines[index].TryExtend(x0, y0, start.z + a * (end.z - start.z));
  95.             }
  96.             if (p >= 0) {
  97.                 y0 += 1;
  98.                 p = p + dy + dy - dx - dx;
  99.             }
  100.             else {
  101.                 p = p + dy + dy;
  102.             }
  103.         }
  104.     }
  105.     else {
  106.         float p = dx + dx - dy;
  107.         for (; y0 <= y1; y0++) {
  108.             var a = (y0 + 0.5f - start.y) / (end.y - start.y);
  109.             if (y0 == y1) { x0 = x1; }// the last pixel
  110.             {
  111.                 var index = x0 - left;
  112.                 scanlines[index].TryExtend(x0, y0, start.z + a * (end.z - start.z));
  113.             }
  114.             if (p >= 0) {
  115.                 x0 += 1;
  116.                 p = p + dx + dx - dy - dy;
  117.             }
  118.             else {
  119.                 p = p + dx + dx;
  120.             }
  121.         }
  122.     }
  123. }
复制代码
fragment processing

这一步为每个Fragment分别调用fragment shader的main()函数。在此之前,要将每个Fragment的数据传递给fragment shader的各个in变量。在此之后,要将fragment shader的各个out变量(通常只有1个)传递给Fragment。通过反射机制,这很好实现。
点击查看代码 为每个Fragment执行fragment shader
  1. private static unsafe void FragmentShaderStage(GLProgram program, ConcurrentBag<Fragment> fragmentList) {
  2.     var fs = program.FragmentShader;
  3.     const BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
  4.     var inFieldInfos = (from item in fs.codeType.GetFields(flags)
  5.                         where item.IsDefined(typeof(InAttribute), true)
  6.                         select item).ToArray();
  7.     var name2fielfInfo = new Dictionary<string, FieldInfo>();
  8.     foreach (var item in fragmentList.ElementAt(0).endpoints0.GetType().GetFields(flags)) {
  9.         name2fielfInfo.Add(item.Name, item);
  10.     }
  11.     foreach (var fragment in fragmentList) {
  12.         var instance = fs.CreateCodeInstance() as FragmentCodeBase; // an executable fragment shader.
  13.         Debug.Assert(instance != null);
  14.         instance.gl_FragCoord = fragment.gl_FragCoord; // setup fragment coordinate in window/screen space.
  15.         // setup "in SomeType varName;" vertex attributes.
  16.         foreach (var field in inFieldInfos) {
  17.             if (name2fielfInfo.TryGetValue(field.Name, out var endpointField)) {
  18.                 var type = endpointField.FieldType; object value;
  19.                 if (false) { }
  20.                 else if (type == typeof(float)) { value = fragment.GetValue(endpointField, PassType.Float); }
  21.                 else if (type == typeof(vec2)) { value = fragment.GetValue(endpointField, PassType.Vec2); }
  22.                 else if (type == typeof(vec3)) { value = fragment.GetValue(endpointField, PassType.Vec3); }
  23.                 else if (type == typeof(vec4)) { value = fragment.GetValue(endpointField, PassType.Vec4); }
  24.                 else if (type == typeof(mat2)) { value = fragment.GetValue(endpointField, PassType.Mat2); }
  25.                 else if (type == typeof(mat3)) { value = fragment.GetValue(endpointField, PassType.Mat3); }
  26.                 else if (type == typeof(mat4)) { value = fragment.GetValue(endpointField, PassType.Mat4); }
  27.                 else { throw new NotDealWithNewEnumItemException(type); }
  28.                 field.SetValue(instance, value);
  29.             }
  30.         }
  31.         // setup "uniform SomeType varName;" in fragment shader.
  32.         Dictionary<string, UniformValue> nameUniformDict = program.name2Uniform;
  33.         foreach (UniformVariable uniformVar in fs.Name2uniformVar.Values) {
  34.             string name = uniformVar.fieldInfo.Name;
  35.             if (nameUniformDict.TryGetValue(name, out var obj)) {
  36.                 uniformVar.fieldInfo.SetValue(instance, obj.value);
  37.             }
  38.         }
  39.         instance.main(); // execute fragment shader code.
  40.         fragment.discard = instance.discard;
  41.         if (!instance.discard) {// if this fragment is not discarded.
  42.             PassVariable[] outVariables = fs.name2outVar.Values.ToArray();
  43.             var outBuffers = new PassBuffer[outVariables.Length];
  44.             for (int index = 0; index < outVariables.Length; index++) {
  45.                 PassVariable outVar = outVariables[index];
  46.                 var outBuffer = new PassBuffer(outVar.fieldInfo.FieldType.GetPassType(), 1);
  47.                 var pointer = outBuffer.Mapbuffer();
  48.                 var value = outVar.fieldInfo.GetValue(instance);
  49.                 Debug.Assert(value != null);
  50.                 switch (outBuffer.elementType) {
  51.                 case PassType.Float: {// make sure no negtive values
  52.                     var v = (float)value;
  53.                     if (v < 0) { v = 0; } else if (v > 1) { v = 1; }
  54.                     ((float*)pointer)[0] = v >= 0 ? v : 0;
  55.                 }
  56.                 break;
  57.                 case PassType.Vec2: {// make sure no negtive values
  58.                     var v = (vec2)value;
  59.                     if (v.x < 0) { v.x = 0; } else if (v.x > 1) { v.x = 1; }
  60.                     if (v.y < 0) { v.y = 0; } else if (v.y > 1) { v.y = 1; }
  61.                     ((vec2*)pointer)[0] = v;
  62.                 }
  63.                 break;
  64.                 case PassType.Vec3: {// make sure no negtive values
  65.                     var v = (vec3)value;
  66.                     if (v.x < 0) { v.x = 0; } else if (v.x > 1) { v.x = 1; }
  67.                     if (v.y < 0) { v.y = 0; } else if (v.y > 1) { v.y = 1; }
  68.                     if (v.z < 0) { v.z = 0; } else if (v.z > 1) { v.z = 1; }
  69.                     ((vec3*)pointer)[0] = v;
  70.                 }
  71.                 break;
  72.                 case PassType.Vec4: {// make sure no negtive values
  73.                     var v = (vec4)value;
  74.                     if (v.x < 0) { v.x = 0; } else if (v.x > 1) { v.x = 1; }
  75.                     if (v.y < 0) { v.y = 0; } else if (v.y > 1) { v.y = 1; }
  76.                     if (v.z < 0) { v.z = 0; } else if (v.z > 1) { v.z = 1; }
  77.                     if (v.w < 0) { v.w = 0; } else if (v.w > 1) { v.w = 1; }
  78.                     ((vec4*)pointer)[0] = v;
  79.                 }
  80.                 break;
  81.                 case PassType.Mat2: ((mat2*)pointer)[0] = (mat2)value; break;
  82.                 case PassType.Mat3: ((mat3*)pointer)[0] = (mat3)value; break;
  83.                 case PassType.Mat4: ((mat4*)pointer)[0] = (mat4)value; break;
  84.                 default: throw new NotDealWithNewEnumItemException(typeof(PassType));
  85.                 }
  86.                 outBuffer.Unmapbuffer();
  87.                 outBuffers[index] = outBuffer;
  88.             }
  89.             fragment.outVariables = outBuffers;
  90.         }
  91.     }
  92. }
复制代码
pixel processing

暂时只实现了深度测试功能。
点击查看代码 DepthTest
  1. private static void DepthTest(RenderContext context, ConcurrentBag<Fragment> fragmentList) {
  2.     var framebuffer = context.target2CurrentFramebuffer[(GLenum)BindFramebufferTarget.DrawFramebuffer];
  3.     var depthBuffer = framebuffer.DepthbufferAttachment;
  4.     ivec4 viewport = context.viewport;
  5.     switch (depthBuffer.Format) {
  6.     case GL.GL_DEPTH_COMPONENT: {// 32 bit -> float
  7.         DepthTest32float(viewport.w, depthBuffer, fragmentList);
  8.     }
  9.     break; // TODO: what should this be? ok, uint it is.
  10.     case GL.GL_DEPTH_COMPONENT24: {// 24 bit -> uint
  11.         DepthTest24uint(viewport.w, depthBuffer, fragmentList);
  12.     }
  13.     break;
  14.     case GL.GL_DEPTH_COMPONENT32: {// 32 bit -> float
  15.         DepthTest32float(viewport.w, depthBuffer, fragmentList);
  16.     }
  17.     break;
  18.     default: throw new Exception("invalid depth format!");
  19.     }
  20. }
  21. private static void DepthTest24uint(int width, IGLAttachable depthBuffer, ConcurrentBag<Fragment> fragmentList) {
  22.     GCHandle pin = GCHandle.Alloc(depthBuffer.DataStore, GCHandleType.Pinned);
  23.     IntPtr pointer = pin.AddrOfPinnedObject();
  24.     var depthTestPlatform = (byte*)pointer;// [viewport.z, viewport.w];
  25.     foreach (var post in fragmentList) {
  26.         var x = (int)post.gl_FragCoord.x;
  27.         var y = (int)post.gl_FragCoord.y;
  28.         var coord = (y * width + x) * 3;
  29.         uint preDepth = 0;
  30.         for (int i = 0; i < 3; i++) { preDepth += (uint)(depthTestPlatform[coord + i] << i); }
  31.         var postDepth = (uint)post.gl_FragCoord.z * (1 << 24);
  32.         // TODO: switch (depthfunc(..)) { .. }
  33.         if (postDepth < preDepth) {// fragment is nearer.
  34.             for (int i = 0; i < 3; i++) {
  35.                 depthTestPlatform[coord + i] = (byte)(postDepth >> i);
  36.             }
  37.         }
  38.     }
  39.     pin.Free();
  40. }
  41. private static void DepthTest32float(int width, IGLAttachable depthBuffer, ConcurrentBag<Fragment> fragmentList) {
  42.     GCHandle pin = GCHandle.Alloc(depthBuffer.DataStore, GCHandleType.Pinned);
  43.     IntPtr pointer = pin.AddrOfPinnedObject();
  44.     var depthTestPlatform = (float*)pointer;// [viewport.z, viewport.w];
  45.     foreach (var post in fragmentList) {
  46.         var x = (int)post.gl_FragCoord.x;
  47.         var y = (int)post.gl_FragCoord.y;
  48.         var coord = y * width + x;
  49.         var preDepth = depthTestPlatform[coord];
  50.         // TODO: switch (depthfunc(..)) { .. }
  51.         if (post.gl_FragCoord.z < preDepth) {// fragment is nearer.
  52.             depthTestPlatform[coord] = post.gl_FragCoord.z;
  53.         }
  54.     }
  55.     pin.Free();
  56. }
复制代码
如何提升纯C#软实现的效率

软实现本来就不把效率放在第一位,但完全忽视效率也是不行的。下面是我为提升SoftGLImpl运行效率采取的一些措施。
高效的IntPtr GetProcAddress(string procName)

对应Windows的wglGetProcAddress和GetProcAddress,SoftGLImpl也要实现一个IntPtr GetProcAddress(string procName)函数供openGL程序员获取openGL函数指针。下面利用反射机制获取openGL全部函数指针并缓存之,避免了:
①使用笨重的Delegate委托;
②重复创建函数指针;
③way #2中千百次设置造成的冗长代码;
④更新openGL版本时遗忘了更新way #2中的缓存。
这里的“高效”,包含着使用者高速(①②)、代码量低(③)、开发者省事(④)三个意思。
  1. public unsafe partial class SoftGL {
  2.     // 缓存全部openGL函数指针
  3.     private static readonly Dictionary<string, IntPtr> procName2Address = new();
  4.     public static IntPtr GetProcAddress(string procName) {
  5.         if (SoftGL.procName2Address.TryGetValue(procName, out var address)) {
  6.             return address;
  7.         }
  8.         else { return IntPtr.Zero; }
  9.     }
  10.     static SoftGL() { // 在SoftGL加载时,初始化openGL函数指针的缓存dict
  11.         // way #1 - 利用反射机制
  12.         Type type = typeof(SoftGLImpl.SoftGL);
  13.         var methodInfos = type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
  14.         foreach (var methodInfo in methodInfos) {
  15.             var procName = methodInfo.Name;
  16.             if (procName.StartsWith("gl")) { // SoftGL中以gl开头的static函数
  17.                 var pointer = methodInfo.MethodHandle.GetFunctionPointer();
  18.                 procName2Address.Add(procName, pointer);
  19.             }
  20.         }
  21.         // way #2 - 一个一个设置
  22.         //var pglGenBuffers = (delegate* managed<GLsizei, GLuint[], void>)(&SoftGLImpl.SoftGL.glGenBuffers);
  23.         //procName2Address.Add("glGenBuffers", (IntPtr)pglGenBuffers);
  24.         //var pglDrawElements = (delegate* managed<GLenum, GLsizei, GLenum, IntPtr>)(&SoftGLImpl.SoftGL.glDrawElements);
  25.         //procName2Address.Add("glDrawElements", (IntPtr)pglDrawElements);
  26.         // other function pointers ...
  27.     }
  28. }
复制代码
运用C#中的并行计算

pipeline中的某些环节可以用Parallel或ThreadPool施展并行计算,以提高软实现的运行效率。
例如,在渲染GL_TRIANGLES时,可以并行处理各个TRIANGLE。
  1. private static unsafe ConcurrentBag<Fragment> LinearInterpolationTriangles(RenderContext context, int count, DrawElementsType type, IntPtr indices, GLVertexArrayObject vao, GLProgram program, GLBuffer indexBuffer, Dictionary<uint, VertexCodeBase> vertexID2Shader) {
  2.     var result = new System.Collections.Concurrent.ConcurrentBag<Fragment>();
  3.     byte[] indexData = indexBuffer.Data; int elementBytes = ByteLength(type);
  4.     int indexLength = indexData.Length / elementBytes;
  5.     GCHandle pin = GCHandle.Alloc(indexData, GCHandleType.Pinned);
  6.     IntPtr pointer = pin.AddrOfPinnedObject();
  7.     ivec4 viewport = context.viewport;  // ivec4(x, y, width, height)
  8.     count = (count - count % 3);
  9.     const int fromInclusive = 0; int toExclusive = count / 3;
  10.     int start = indices.ToInt32() / elementBytes;
  11.     int to2 = (indexLength - start) / 3;
  12.     if (to2 < toExclusive) { toExclusive = to2; }
  13.     Parallel.For(fromInclusive, toExclusive, t => {
  14.         int indexID = t * 3 + start;
  15.         var endpoints = new VertexCodeBase[3];
  16.         var fragCoords = stackalloc vec3[3];
  17.         for (int i = 0; i < 3; i++) {
  18.             uint gl_VertexID = GetVertexID(pointer, type, indexID + i);
  19.             System.Diagnostics.Debug.Assert(vertexID2Shader.ContainsKey(gl_VertexID));
  20.             var shaderObj = vertexID2Shader[gl_VertexID];
  21.             endpoints[i] = shaderObj;
  22.             vec4 gl_Position = shaderObj.gl_Position;
  23.             vec3 fragCoord = new vec3((gl_Position.x + 1) / 2.0f * viewport.z + viewport.x,
  24.                 (gl_Position.y + 1) / 2.0f * viewport.w + viewport.y,
  25.                 (gl_Position.z + 1) / 2.0f * (float)(context.depthRangeFar - context.depthRangeNear) + (float)context.depthRangeNear);
  26.             fragCoords[i] = fragCoord;
  27.         }
  28.         FindFragmentsInTriangle(
  29.             fragCoords[0], fragCoords[1], fragCoords[2],
  30.             endpoints[0], endpoints[1], endpoints[2], result);
  31.     });
  32.     pin.Free();
  33.     return result;
  34. }
复制代码
其他可以并行计算的过程包括:

  • 并行执行vertex shader。
  • 在两个顶点之间进行线性插值。
  • 并行执行fragment shader。
开源地址

开源地址在https://gitee.com/bitzhuwei/glTF2。github我已无法登陆了,它新要求的什么狗屁验证措施,让我很不安。我已经把github上我的所有项目都转移到gitee了。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册