CSharpGL(49)试水OpenGL软实现

CSharpGL(49)试水OpenGL软实现

CSharpGL迎来了第49篇。本篇内容是用C#编写一个OpenGL的软实现。暂且将其命名为SoftGL。

目前已经实现了由Vertex Shader和Fragment Shader组成的Pipeline,其效果与显卡支持的OpenGL实现几乎相同。下图左是常规OpenGL渲染的结果,右是SoftGL渲染的结果。

下载

CSharpGL已在GitHub开源,欢迎对OpenGL有兴趣的同学加入(https://github.com/bitzhuwei/CSharpGL)

SoftGL也已在GitHub开源,欢迎对OpenGL有兴趣的同学加入(https://github.com/bitzhuwei/SoftGL)

从使用者的角度开始

OpenGL的使用者就是OpenGL应用程序开发者(Dev)。

下面按其被执行的先后顺序陈列OpenGL相关的命令(只陈列最基本的命令):

创建Device Context

用一个System.Windows.Forms.Control类型的对象即可。

最后会发现,这个Device Context的作用之一是为创建Render Context提供参数。目前在SoftGL中不需要这个参数。

创建Render Context

Render Context包含OpenGL的所有状态字段。例如,当Dev调用glLineWidth(float width);时,Render Context会记下这一width值,从而使之长期有效(直到下次调用glLineWidth(float width);来修改它)。

可能同时存在多个Render Context,每个都保存自己的lineWidth等字段。当使用静态的OpenGL函数static void glLineWidth(float width);时,它会首先找到当前的Render Context对象(详见后面的MakeCurrent(..)),然后让此对象执行真正的glLineWidth(float width);函数。

可见Render Context就是一个典型的class,其伪代码如下:

 1 partial class SoftGLRenderContext:
 2 {
 3     private float lineWidth;
 4     // .. other fields.
 5     
 6     public static void glLineWidth(float width)
 7     {
 8         SoftGLRenderContext obj = SoftGLRenderContext .GetCurrentContext();
 9         if (obj != null) { obj.LineWidth(width); }
10     }
11     
12     private void LineWidth(float width)
13     {
14         this.lineWidth = width;
15     }
16     
17     // .. other OpenGL functions.
18 }

MakeCurrent(IntPtr dc, IntPtr rc);

函数static void MakeCurrent(IntPtr dc, IntPtr rc);不是OpenGL函数。它的作用是指定当前线程(Thread)与哪个Render Context对应,即在Dictionary<Thread, RenderContext>这一字典类型中记录下Thread与Render Context的对应关系。

当然,如果rc为IntPtr.Zero,就是要解除当前Thread与其Render Context的对应关系。

伪代码如下:

 1 partial class SoftGLRenderContext
 2 {
 3     // Thread -> Binding Render Context Object.
 4     static Dictionary<Thread, SoftGLRenderContext> threadContextDict = new Dictionary<Thread, SoftGLRenderContext>();
 5 
 6     // Make specified renderContext the current one of current thread.
 7     public static void MakeCurrent(IntPtr deviceContext, IntPtr renderContext)
 8     {
 9         var threadContextDict = SoftGLRenderContext.threadContextDict;
10         if (renderContext == IntPtr.Zero) // cancel current render context to current thread.
11         {
12             SoftGLRenderContext context = null;
13 
14             Thread thread = System.Threading.Thread.CurrentThread;
15             if (threadContextDict.TryGetValue(thread, out context))
16             {
17                 threadContextDict.Remove(thread);
18             }
19         }
20         else // change current render context to current thread.
21         {
22             SoftGLRenderContext context = GetContextObj(renderContext);
23             if (context != null)
24             {
25                 SoftGLRenderContext oldContext = GetCurrentContextObj();
26                 if (oldContext != context)
27                 {
28                     Thread thread = Thread.CurrentThread;
29                     if (oldContext != null) { threadContextDict.Remove(thread); }
30                     threadContextDict.Add(thread, context);
31                     context.DeviceContextHandle = deviceContext;
32                 }
33             }
34         }
35     }
36 }

获取OpenGL函数指针

在CSharpGL.Windows项目中,我们可以通过Win32 API找到在opengl32.dll中的OpenGL函数指针,并将其转换为C#中的函数委托(Delegate),从而可以像使用普通函数一样使用OpenGL函数。其伪代码如下:

 1 public partial class WinGL : CSharpGL.GL
 2 {
 3     public override Delegate GetDelegateFor(string functionName, Type functionDeclaration)
 4     {
 5         Delegate del = null;
 6         if (!extensionFunctions.TryGetValue(functionName, out del))
 7         {
 8             IntPtr proc = Win32.wglGetProcAddress(name);
 9             if (proc != IntPtr.Zero)
10             {
11                 // Get the delegate for the function pointer.
12                 del = Marshal.GetDelegateForFunctionPointer(proc, functionDeclaration);
13 
14                 // Add to the dictionary.
15                 extensionFunctions.Add(functionName, del);
16             }
17         }
18 
19         return del;
20 }
21 
22     // Gets a proc address.
23     [DllImport("opengl32.dll", SetLastError = true)]
24 internal static extern IntPtr wglGetProcAddress(string name);
25 
26     // The set of extension functions.
27     static Dictionary<string, Delegate> extensionFunctions = new Dictionary<string, Delegate>();
28 }

此时我们想使用SoftGL,那么要相应地为其编写一个SoftGL.Windows项目。这个项目通过在类似opengl32.dll的SoftOpengl32项目(或者SoftOpengl32.dll)中查找函数的方式来找到我们自己实现的OpenGL函数。其伪代码如下:

 1 partial class WinSoftGL : CSharpGL.GL
 2 {
 3     private static readonly Type thisType = typeof(SoftOpengl32.StaticCalls);
 4     public override Delegate GetDelegateFor(string functionName, Type functionDeclaration)
 5     {
 6         Delegate result = null;
 7         if (!extensionFunctions.TryGetValue(functionName, out result))
 8         {
 9             MethodInfo methodInfo = thisType.GetMethod(functionName, BindingFlags.Static | BindingFlags.Public);
10             if (methodInfo != null)
11             {
12                 result = System.Delegate.CreateDelegate(functionDeclaration, methodInfo);
13             }
14 
15             if (result != null)
16             {
17                 //  Add to the dictionary.
18                 extensionFunctions.Add(functionName, result);
19             }
20         }
21 
22         return result;
23     }
24 
25     // The set of extension functions.
26     static Dictionary<string, Delegate> extensionFunctions = new Dictionary<string, Delegate>();
27 }

可见只需通过C#和.NET提供的反射机制即可实现。在找到System.Delegate.CreateDelegate(..)这个方法时,我感觉到一种“完美”。

此时,我们应当注意到另一个涉及大局的问题,就是整个SoftGL的框架结构。

SoftGL项目本身的作用与显卡驱动中的OpenGL实现相同。操作系统(例如Windows)提供了一个opengl32.dll之类的方式来让Dev找到OpenGL函数指针,从而使用OpenGL。CSharpGL项目是对OpenGL的封装,具体地讲,是对OpenGL的函数声明的封装,它不包含对OpenGL的实现、初始化等功能。这些功能是在CSharpGL.Windows中实现的。Dev通过引用CSharpGL项目和CSharpGL.Windows项目就可以直接使用OpenGL了。

如果不使用显卡中的OpenGL实现,而是换做SoftGL,那么这一切就要相应地变化。SoftOpengl32项目代替操作系统的opengl32.dll。CSharpGL保持不变。SoftGL.Windows代替CSharpGL.Windows。Dev通过引用CSharpGL项目和SoftGL.Windows项目就可以直接使用软实现的OpenGL了。

最重要的是,这样保证了应用程序的代码不需任何改变,应用程序只需将对CSharpGL.Windows的引用修改为对SoftGL.Windows的引用即可。真的。

创建ShaderProgram和Shader

根据OpenGL命令,可以推测一种可能的创建和删除ShaderProgram对象的方式,伪代码如下:

 1 partial class SoftGLRenderContext
 2 {
 3     private uint nextShaderProgramName = 1;
 4 
 5     // name -> ShaderProgram object
 6     Dictionary<uint, ShaderProgram> nameShaderProgramDict = new Dictionary<uint, ShaderProgram>();
 7 
 8     private ShaderProgram currentShaderProgram = null;
 9 
10     public static uint glCreateProgram() // OpenGL functions.
11     {
12         uint id = 0;
13         SoftGLRenderContext context = ContextManager.GetCurrentContextObj();
14         if (context != null)
15         {
16             id = context.CreateProgram();
17         }
18 
19         return id;
20     }
21 
22     private uint CreateProgram()
23     {
24         uint name = nextShaderProgramName;
25         var program = new ShaderProgram(name); //create object.
26         this.nameShaderProgramDict.Add(name, program); // bind name and object.
27         nextShaderProgramName++; // prepare for next name.
28 
29         return name;
30 }
31 
32     public static void glDeleteProgram(uint program)
33     {
34         SoftGLRenderContext context = ContextManager.GetCurrentContextObj();
35         if (context != null)
36         {
37             context.DeleteProgram(program);
38         }
39     }
40     
41     private void DeleteProgram(uint program)
42     {
43         Dictionary<uint, ShaderProgram> dict = this.nameShaderProgramDict;
44         if (!dict.ContainsKey(program)) { SetLastError(ErrorCode.InvalidValue); return; }
45 
46         dict.Remove(program);
47     }
48 }

创建ShaderProgram对象的逻辑很简单,首先找到当前的Render Context对象,然后让它创建一个ShaderProgram对象,并使之与一个name绑定(记录到一个Dictionary<uint, ShaderProgram>字典类型的字段中)。删除ShaderProgram对象的逻辑也很简单,首先判断参数是否合法,然后将字典中的ShaderProgram对象删除即可。

OpenGL中的很多对象都遵循这样的创建模式,例如Shader、Buffer、VertexArrayObject、Framebuffer、Renderbuffer、Texture等。

ShaderProgram是一个大块头的类型,它要处理很多和GLSL Shader相关的东西。到时候再具体说。

创建VertexBuffer、IndexBuffer和VertexArrayObject

参见创建ShaderProgram对象的方式。要注意的是,这些类型的创建分2步。第一步是调用glGen*(int count, uint[] names);,此时只为其分配了name,没有创建对象。第二步是首次调用glBind*(uint target, uint name);,此时才会真正创建对象。我猜这是早期的函数接口,所以用了这么啰嗦的方式。

对顶点属性进行设置

一个顶点缓存对象(GLBuffer)实际上是一个字节数组(byte[])。它里面保存的,可能是顶点的位置属性(vec3[]),可能是顶点的纹理坐标属性(vec2[]),可能是顶点的密度属性(float[]),可能是顶点的法线属性(vec3[]),还可能是这些属性的某种组合(如一个位置属性+一个纹理坐标属性这样的轮流出现)。OpenGL函数glVertexAttribPointer(uint index, int size, uint type, bool normalized, int stride, IntPtr pointer)的作用就是描述顶点缓存对象保存的是什么,是如何保存的。

glClear(uint mask)

每次渲染场景前,都应清空画布,即用glClear(uint mask);清空指定的缓存。

OpenGL函数glClearColor(float r, float g, float b, float a);用于指定将画布清空为什么颜色。这是十分简单的,只需设置Render Context中的一个字段即可。

需要清空颜色缓存(GL_COLOR_BUFFER_BIT)时,实际上是将当前Framebuffer对象上的颜色缓存设置为指定的颜色。需要清空深度缓存(GL_DEPTH_BUFFER_BIT)或模板缓存(GL_STENCIL_BUFFER_BIT)时,实际上也是将当前Framebuffer对象上的深度缓存或模板缓存设置为指定的值。

所以,为了实现glClear(uint mask)函数,必须将Framebuffer和各类缓存都准备好。

Framebuffer中的各种缓存都可以简单的用一个Renderbuffer对象充当。一个Renderbuffer对象实际上也是一个字节数组(byte[]),只不过它还用额外的字段记录了自己的数据格式(GL_RGBA等)等信息。纹理(Texture)对象里的各个Image也可以充当Framebuffer中的各种缓存。所以Image是和Renderbuffer类似的东西,或者说,它们支持同一个接口IAttachable。

1 interface IAttachable
2 {
3     uint Format { get; }  // buffer’s format
4     int Width { get; } // buffer’s width.
5     int Height { get; } // buffer’s height.
6     byte[] DataStore { get; } // buffer data.
7 }

这里就涉及到对与byte[]这样的数组与各种其他类型的数组(例如描述位置的vec3[])相互赋值的问题。一般,可以用下面的方式解决:

1 byte[] bytes = ...
2 this.pin = GCHandle.Alloc(bytes, GCHandleType.Pinned);
3 IntPtr pointer = this.pin.AddrOfPinnedObject();
4 var array = (vec3*)pointer.ToPointer();
5 for (in i = 0; i< ...; i++) {
6     array[i] = ...
7 }

只要能将数组转换为 void* 类型,就没有什么做不到的了。

 

glGetIntegerv(uint target, int[] values)

这个十分简单。一个大大的switch语句。

 

设置Viewport

设置viewport本身是十分简单的,与设置lineWidth类似。但是,在一个Render Context对象被首次MakeCurrent()到一个线程时,要将Device Context的Width和Height赋值给viewport。这个有点麻烦。

更新uniform变量的值

 

glDrawElements(..)

 

总结

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/537578.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SonarQube结合IDEA实现代码检测

环境准备 1.SonarQube下载&#xff1a;https://www.sonarqube.org/downloads/ 建议用最新版本&#xff0c;SonarQube与idea的结合 需要SonarQube很多插件&#xff0c;需要借助idea的SonarLint 插件。 不同的SonarQube版本&#xff0c;有不同的插件版本 idea的SonarLint 插件…

二维小波变换_【外文文献速读】实时二维水波模拟

题目&#xff1a;Water surface wavelets 作者&#xff1a;Stefan Jeschke&#xff0c; TomšSkřivan&#xff0c; MatthiasMller-Fischer&#xff0c; Nuttapong Chentanez&#xff0c; Miles Macklin&#xff0c; Chris Wojtan

技术开发(委托)合同怎么写?

一直基于宁波市科技局备案合同模板签订合同&#xff0c;并完成科技局备案工作&#xff0c;成功了N次&#xff0c;直接分享模板&#xff0c;该模板通过了法务审核&#xff0c;财务审核&#xff0c;只需要批示修改相关内容即可&#xff0c;一份技术开发委托合同&#xff0c;十几分…

最常用的15个前端表单验证JS正则表达式

2019独角兽企业重金招聘Python工程师标准>>> 在表单验证中&#xff0c;使用正则表达式来验证正确与否是一个很频繁的操作&#xff0c;本文收集整理了15个常用的JavaScript正则表达式&#xff0c;其中包括用户名、密码强度、整数、数字、电子邮件地址&#xff08;Ema…

程序员个人外包合同怎么写?

分享一份工作上经常用到的个人外包合同协议&#xff0c;该协议通过了法务与财务审核&#xff0c;兼顾甲乙双方利益&#xff0c;程序员接私活必备&#xff01;&#xff01;&#xff01;&#xff01; ---需要电子word版&#xff0c;请关注--------- 回复&#xff1a;个人外包合同…

rocketmq新扩容的broker没有tps_深入研究RocketMQ消费者是如何获取消息的

前言小伙伴们&#xff0c;国庆都过的开心吗&#xff1f;国庆后的第一个工作日是不是很多小伙伴还沉浸在假期的心情中&#xff0c;没有工作状态呢&#xff1f;那王子今天和大家聊一聊RocketMQ的消费者是如何获取消息的&#xff0c;通过学习知识来找回状态吧。废话不多说&#xf…

苏宁 11.11:仓库内多 AGV 协作的全局路径规划算法研究

本文为『InfoQ x 苏宁 2018双十一』技术特别策划系列文章之一。 1. 背景 随着物联网和人工智能的发展&#xff0c;越来越多的任务渐渐的被机器人取代&#xff0c;机器人逐渐在发展中慢慢进入物流领域&#xff0c;“智能叉车”&#xff0c;AGV&#xff08;Automated Guided Vehi…

用python绘制玫瑰花的代码_python也能玩出玫瑰花!程序员的表白代码

有些情侣是异地恋&#xff0c;情人节想送朵玫瑰花给女朋友都困难。别担心&#xff0c;用Python就好了&#xff0c;互联网时代的恋爱神器&#xff01;接下来就让我们一起来看看如何用Python变出玫瑰花的。 1、首先我们导入画图工具turtle&#xff0c;即import turtle 2、导入画图…

Springboot 整合 swagger

版权声明&#xff1a;本文为博主原创文章&#xff0c;未经博主允许不得转载。 https://blog.csdn.net/weixin_40254498/article/details/83622098 swagger 主要是为后端服务的接口文档&#xff0c;懒人必备&#xff0c;swagger就是一款让你更好的书写API文档的框架。 其他的框架…

Project为项目设置预算

假设项目预算10万元&#xff0c;如果项目完成后&#xff0c;花费没有超过10万元&#xff0c;则成本管理是成功的&#xff0c;如果花费了11万&#xff0c;则超过了预算。 预算是10万&#xff0c;一般目标成本设得比预算成本低&#xff0c;比如9.5万。在项目实施过程中&#xff…

activiti7流程设计器_变频空调器通信电路

通信电路由室内机和室外机主板两个部分单元电路组成&#xff0c;并且在实际维修中该电路的故障率比较高&#xff0c;因此单设--节进行详细说明。第三章变频空调器单元电路对比和通信电路第二节通信电路通信电路由室内机和室外机主板两个部分单元电路组成&#xff0c;并且在实际…

PyCharm 中为 Python 项目添加.gitignore文件

文章目录 1.安装.ignore插件 2.在项目中添加.ignore文件 1.安装.ignore插件 在pycharm编译器中&#xff0c;依次点击File->Setting 在跳出Setting的页面中&#xff0c;执行如下操作&#xff1a; 点击左侧的Plugins&#xff0c; 在搜索框中输入.ignore 点击右侧的install 点…

mysql的分页查询

为什么80%的码农都做不了架构师&#xff1f;>>> order by case when 的用法&#xff08;实现特殊情况的排序&#xff0c;如leader1的排最前面&#xff09;&#xff1a; select * from m_worker_project order by CASE WHEN leader 1 THEN 100 ELSE 1000 END 项目中…

.describe() python_python的apply应用:一般性的“拆分-应用-合并”,附加详细讲解

跟aggregate一样&#xff0c;transform也是一个有着严格条件的特殊函数&#xff1a;传入的函数只能产生两种结果&#xff0c;要么产生一个可以传播的标量值(如np.mean)&#xff0c;要么产生一个相同大小的结果数组。最一般化的GroupBy方法是apply&#xff0c;apply会将待处理的…

DNS服务(4)Slave DNS及高级特性

为了简化运维人员的负担&#xff0c;使用Master/Slave DNS架构的情况比较好&#xff0c;现在我们来简单叙述一下Master/Slaver DNS的特点主DNS服务器&#xff1a;维护所负责解析的域内解析库服务器&#xff1b;解析库由管理员维护&#xff1b;从DNS服务器:从主DNS服务器或其它的…

think python下载 中文版开源!这或许是最经典的编程教材

《Think Python》是很多Python初学者的不二入门教材&#xff0c;受到广泛好评。该书原作者是美国Olin工程学院的教授Allen B. Downey&#xff0c;目前该书的原版和中文版本都已免费开源。 中文版本译者是一名自学Python的编程爱好者。选择翻译《Think Python》&#xff0c;一是…

datatable的数据进行组内排序_排序算法学习分享(四)希尔排序

排序&#xff0c;也称为排序算法&#xff0c;可以说是我们学习算法的过程中遇到的第一个门槛&#xff0c;也是实际应用中使用得较为频繁的算法&#xff0c;我将自己对所学的排序算法进行一个归纳总结与分享&#xff0c;如有错误&#xff0c;欢迎指正&#xff01;排序算法学习分…

jupyter notebook 安装代码提示功能

效果 安装成功后&#xff0c;输入部分代码&#xff0c;按 tab 键&#xff0c;会提示代码 安装步骤 1.安装nbextensions 从国内的pip镜像下载快 pip install -i http://pypi.douban.com/simple --trusted-host pypi.douban.com jupyter_contrib_nbextensions jupyter contr…

python需要配置环境变量吗_python为什么会环境变量设置不成功

学习python编程&#xff0c;首先要配置好环境变量。本文主要讲解python的环境变量配置&#xff0c;在不同版本下如何安装 Windows 打开Python官方下载网站 https://www.python.org/downloads/release/python-370/ x86:表示是32位电脑 x86-64:表示是64位电脑 目前Python版本分为…

一维数组、二维数组、三维数组、四维数组、多维数组的理解

以图书馆来举例 一维数组是一条线 二维数组是一页纸 三维数组是一本书 四维数组是书架 五维数组是图书室2201&#xff08;好几个书架&#xff09; 六维数组是图书馆某一层&#xff0c;2楼/3楼&#xff0c;好几个图书室 七维数组是整个图书馆 第N维数组是宇宙..................…