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,其伪代码如下:

 partial class SoftGLRenderContext:
{
private float lineWidth;
// .. other fields. public static void glLineWidth(float width)
{
SoftGLRenderContext obj = SoftGLRenderContext .GetCurrentContext();
if (obj != null) { obj.LineWidth(width); }
} private void LineWidth(float width)
{
this.lineWidth = width;
} // .. other OpenGL functions.
}

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的对应关系。

伪代码如下:

 partial class SoftGLRenderContext
{
// Thread -> Binding Render Context Object.
static Dictionary<Thread, SoftGLRenderContext> threadContextDict = new Dictionary<Thread, SoftGLRenderContext>(); // Make specified renderContext the current one of current thread.
public static void MakeCurrent(IntPtr deviceContext, IntPtr renderContext)
{
var threadContextDict = SoftGLRenderContext.threadContextDict;
if (renderContext == IntPtr.Zero) // cancel current render context to current thread.
{
SoftGLRenderContext context = null; Thread thread = System.Threading.Thread.CurrentThread;
if (threadContextDict.TryGetValue(thread, out context))
{
threadContextDict.Remove(thread);
}
}
else // change current render context to current thread.
{
SoftGLRenderContext context = GetContextObj(renderContext);
if (context != null)
{
SoftGLRenderContext oldContext = GetCurrentContextObj();
if (oldContext != context)
{
Thread thread = Thread.CurrentThread;
if (oldContext != null) { threadContextDict.Remove(thread); }
threadContextDict.Add(thread, context);
context.DeviceContextHandle = deviceContext;
}
}
}
}
}

获取OpenGL函数指针

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

 public partial class WinGL : CSharpGL.GL
{
public override Delegate GetDelegateFor(string functionName, Type functionDeclaration)
{
Delegate del = null;
if (!extensionFunctions.TryGetValue(functionName, out del))
{
IntPtr proc = Win32.wglGetProcAddress(name);
if (proc != IntPtr.Zero)
{
// Get the delegate for the function pointer.
del = Marshal.GetDelegateForFunctionPointer(proc, functionDeclaration); // Add to the dictionary.
extensionFunctions.Add(functionName, del);
}
} return del;
} // Gets a proc address.
[DllImport("opengl32.dll", SetLastError = true)]
internal static extern IntPtr wglGetProcAddress(string name); // The set of extension functions.
static Dictionary<string, Delegate> extensionFunctions = new Dictionary<string, Delegate>();
}

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

 partial class WinSoftGL : CSharpGL.GL
{
private static readonly Type thisType = typeof(SoftOpengl32.StaticCalls);
public override Delegate GetDelegateFor(string functionName, Type functionDeclaration)
{
Delegate result = null;
if (!extensionFunctions.TryGetValue(functionName, out result))
{
MethodInfo methodInfo = thisType.GetMethod(functionName, BindingFlags.Static | BindingFlags.Public);
if (methodInfo != null)
{
result = System.Delegate.CreateDelegate(functionDeclaration, methodInfo);
} if (result != null)
{
// Add to the dictionary.
extensionFunctions.Add(functionName, result);
}
} return result;
} // The set of extension functions.
static Dictionary<string, Delegate> extensionFunctions = new Dictionary<string, Delegate>();
}

可见只需通过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对象的方式,伪代码如下:

 partial class SoftGLRenderContext
{
private uint nextShaderProgramName = ; // name -> ShaderProgram object
Dictionary<uint, ShaderProgram> nameShaderProgramDict = new Dictionary<uint, ShaderProgram>(); private ShaderProgram currentShaderProgram = null; public static uint glCreateProgram() // OpenGL functions.
{
uint id = ;
SoftGLRenderContext context = ContextManager.GetCurrentContextObj();
if (context != null)
{
id = context.CreateProgram();
} return id;
} private uint CreateProgram()
{
uint name = nextShaderProgramName;
var program = new ShaderProgram(name); //create object.
this.nameShaderProgramDict.Add(name, program); // bind name and object.
nextShaderProgramName++; // prepare for next name. return name;
} public static void glDeleteProgram(uint program)
{
SoftGLRenderContext context = ContextManager.GetCurrentContextObj();
if (context != null)
{
context.DeleteProgram(program);
}
} private void DeleteProgram(uint program)
{
Dictionary<uint, ShaderProgram> dict = this.nameShaderProgramDict;
if (!dict.ContainsKey(program)) { SetLastError(ErrorCode.InvalidValue); return; } dict.Remove(program);
}
}

创建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。

 interface IAttachable
{
uint Format { get; } // buffer’s format
int Width { get; } // buffer’s width.
int Height { get; } // buffer’s height.
byte[] DataStore { get; } // buffer data.
}

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

 byte[] bytes = ...
this.pin = GCHandle.Alloc(bytes, GCHandleType.Pinned);
IntPtr pointer = this.pin.AddrOfPinnedObject();
var array = (vec3*)pointer.ToPointer();
for (in i = ; i< ...; i++) {
array[i] = ...
}

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

glGetIntegerv(uint target, int[] values)

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

设置Viewport

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

更新uniform变量的值

glDrawElements(..)

总结

CSharpGL(49)试水OpenGL软实现的更多相关文章

  1. phaser2->3:来个打地鼠试水

    本文中phaser具体版本 phaser2:2.8.1 phaser3:3.17.0 一.实现效果二.实现细节三.项目总结四.参考文档 一.实现效果 源码地址(phaser2&phaser3) ...

  2. CSharpGL(27)讲讲清楚OpenGL坐标变换

    CSharpGL(27)讲讲清楚OpenGL坐标变换 在理解OpenGL的坐标变换问题的路上,有好几个难点和易错点.且OpenGL秉持着程序难以调试.难点互相纠缠的特色,更让人迷惑.本文依序整理出关于 ...

  3. POJ 2502 - Subway Dijkstra堆优化试水

    做这道题的动机就是想练习一下堆的应用,顺便补一下好久没看的图论算法. Dijkstra算法概述 //从0出发的单源最短路 dis[][] = {INF} ReadMap(dis); for i = 0 ...

  4. 大众点评试水O2O新模式:实体店试穿,扫描二维码付款 现场取货

    在餐饮美食行业取得不错的成绩之后,大众点评将触角延伸到了线下的传统商铺,开始涉足线下商品的 O2O 团购.和传统的线上下单,线下消费的 O2O 模式不同.大众点评的 O2O 团购用户,可在店内试穿后通 ...

  5. Json.Net6.0入门学习试水篇

    原文:Json.Net6.0入门学习试水篇 前言 JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式.简单地说,JSON 可以将 JavaScript 对象中 ...

  6. 第一回:Scrapy的试水

    前言:今天算是见到Scrapy的第二天,之前只是偶尔查了查,对于这个框架的各种解释,我-----都-----看------不------懂----,没办法,见面就是刚. 目的:如题,试水 目标:< ...

  7. UITableView(自定义cell)试水心得

    初次试水自定义cell的UITableView 实现目标      最终实现结果   界面复原度:98% 未能完全复刻的地方:下半部分的tableview与头部的控件间距上的误差 原因:在做table ...

  8. 微博试水卖车社交电商怎样令4S“颤抖”?

        微博对社交电商的探索一直在深入,年初.微博上线了"支付"产品.从而使社交产业链实现了闭环,随后,微博又尝试售卖多种商品,不断扩大移动电商的试水范围,近期微博大规模汽车销售收 ...

  9. mysql练习题目试水50题,附建库sql代码

    如果你没试过水的话,那一题一题地每一题都敲一遍吧.不管它们对你看来有多么简单.  建库代码 部分题目答案在末尾,可用ctrl f  搜索题号. 作业练习——学生-选课 表结构 学生表: Student ...

随机推荐

  1. nginx安装配置+集群tomcat:Centos和windows环境

    版本:nginx-1.8.0.tar.gz 官网:http://nginx.org/en/download.html         版本:apache-tomcat-6.0.44.tar.gz  官 ...

  2. python爬虫入门(二)Opener和Requests

    Handler和Opener Handler处理器和自定义Opener opener是urllib2.OpenerDirector的实例,我们之前一直在使用urlopen,它是一个特殊的opener( ...

  3. Flask入门之自定义过滤器(匹配器)

    1.  动态路由的匹配器? 不知道这种叫啥名,啥用法,暂且叫做匹配器吧. Flask自带的匹配器可以说有四种吧(保守数字,就我学到的) 动态路由本身,可以传任何参数字符串或者数字,如:<user ...

  4. Robot framework(RF) 用户关键字

    3.6  用户关键字 在Robot Framework 中关键字的创建分两种:系统关键字和用户关键字. 系统关键字是需要通过脚本开发相应的类和方法,从而实现某一逻辑功能. 用户关键字是根据业务的需求利 ...

  5. 《与C语言相恋》

    第一章 <与C语言相恋> 目录: 1.1 C语言的诞生 1.2 相恋C语言的理由 1.3 相恋C语言的7个步骤 1.4 目标代码文件,可执行文件和库 1.5 本章小结 C语言的诞生 197 ...

  6. bootstrap datepicker 属性设置 以及方法和事件

    DatePicker支持鼠标点选日期,同时还可以通过键盘控制选择: page up/down - 上一月.下一月 ctrl+page up/down - 上一年.下一年 ctrl+home - 当前月 ...

  7. SSM-SpringMVC-33:SpringMVC中拦截器Interceptor讲解

     ------------吾亦无他,唯手熟尔,谦卑若愚,好学若饥------------- 拦截器Interceptor: 对处理方法进行双向的拦截,可以对其做日志记录等 我选择的是实现Handler ...

  8. [ 搭建Redis本地服务器实践系列三 ] :图解Redis客户端工具连接Redis服务器

    上一章 [ 搭建Redis本地服务器实践系列二 ] :图解CentOS7配置Redis  介绍了Redis的初始化脚本文件及启动配置文件,并图解如何以服务的形式来启动.终止Redis服务,可以说我们的 ...

  9. Base64 image

    [前端攻略]:玩转图片Base64编码 什么是 base64 编码? 我不是来讲概念的,直接切入正题,图片的 base64 编码就是可以将一副图片数据编码成一串字符串,使用该字符串代替图像地址. 这样 ...

  10. adb常用操作命令

    1.adb简介:    adb,即 Android Debug Bridge.通过这个工具和android进行交互操作 2.adb命令格式:    adb [-d|-e|-s <serialNu ...