CSharpGL(7)对VAO和VBO的封装
CSharpGL(7)对VAO和VBO的封装
2016-08-13
由于CSharpGL一直在更新,现在这个教程已经不适用最新的代码了。CSharpGL源码中包含10多个独立的Demo,更适合入门参考。
为了尽可能提升渲染效率,CSharpGL是面向Shader的,因此稍有难度。
VAO(VBO)
在legacy OpenGL中,渲染图形是用glVertex()之类的方式实现的。
在modern OpenGL中,则是用VAO和VBO来存储图形信息以备渲染的。
VBO(Vertex Buffer Object)是用来存储顶点属性的对象。VBO就像一个定长的数组(SomeType[] vbo = new SomeType[100];),只不过是存贮于GPU内存里。我们在编码时可以通过一个代表它的指针来操作它。
VAO(Vertex Array Object)是用来管理VBO的渲染流程的。简单地说,就是让VAO看一次各个VBO是如何完成一次渲染工作的,VAO就记住了这个过程。以后就可以仅借助VAO来完成渲染过程。这就减少了DrawCall。
如果你没有使用过modern OpenGL,没有使用过VBO,那么现在看下文是无意义的。你可以先下载(https://github.com/bitzhuwei/CSharpGL)和稍微浏览一下其中的代码,之后再看本文是如何封装VAO和VBO的。
OpenGL告诉你应该这样用VBO
首先来仔细观察OpenGL提供的使用VBO的方式。
下图是CSharpGL中的一个渲染四面体的例子。我就以渲染这个四面体为例。

给出顶点属性
我们给出这个四面体的顶点数据。其中每3个连续的顶点代表一个三角形,显然这里有4个三角形。
/// <summary>
/// 金字塔的posotion array.
/// </summary>
static vec3[] positions = new vec3[]
{
new vec3(0.0f, 1.0f, 0.0f),
new vec3(-1.0f, -1.0f, 1.0f),
new vec3(1.0f, -1.0f, 1.0f),
new vec3(0.0f, 1.0f, 0.0f),
new vec3(1.0f, -1.0f, 1.0f),
new vec3(1.0f, -1.0f, -1.0f),
new vec3(0.0f, 1.0f, 0.0f),
new vec3(1.0f, -1.0f, -1.0f),
new vec3(-1.0f, -1.0f, -1.0f),
new vec3(0.0f, 1.0f, 0.0f),
new vec3(-1.0f, -1.0f, -1.0f),
new vec3(-1.0f, -1.0f, 1.0f),
};
有了顶点的位置,下面给出每个顶点的颜色。
/// <summary>
/// 金字塔的color array.
/// </summary>
static vec3[] colors = new vec3[]
{
new vec3(1.0f, 0.0f, 0.0f),
new vec3(0.0f, 1.0f, 0.0f),
new vec3(0.0f, 0.0f, 1.0f),
new vec3(1.0f, 0.0f, 0.0f),
new vec3(0.0f, 0.0f, 1.0f),
new vec3(0.0f, 1.0f, 0.0f),
new vec3(1.0f, 0.0f, 0.0f),
new vec3(0.0f, 1.0f, 0.0f),
new vec3(0.0f, 0.0f, 1.0f),
new vec3(1.0f, 0.0f, 0.0f),
new vec3(0.0f, 0.0f, 1.0f),
new vec3(0.0f, 1.0f, 0.0f),
};
准备shader program
在使用VBO前,需要准备好shader program。你只需知道OpenGL会给我们一个ShaderProgram的uint值代表它。VAO、VBO、texture也都有这样一个uint值。
protected void InitializeShader(out ShaderProgram shaderProgram)
{
var vertexShaderSource = ManifestResourceLoader.LoadTextFile(@"SceneElements.PyramidElement.vert");
var fragmentShaderSource = ManifestResourceLoader.LoadTextFile(@"SceneElements.PyramidElement.frag"); shaderProgram = new ShaderProgram();
shaderProgram.Create(vertexShaderSource, fragmentShaderSource, null); shaderProgram.AssertValid();
}
这里我们给出vertex shader。此shader里有' in_Position'、' in_Color'两个in变量,分别与positions和colors的VBO对应。
#version core in vec3 in_Position;
in vec3 in_Color;
out vec4 pass_Color; uniform mat4 MVP; void main(void)
{
gl_Position = MVP * vec4(in_Position, 1.0); pass_Color = vec4(in_Color, 1.0);
}
初始化VBO
在这个四面体的例子中,我们需要为positions创建一个VBO,然后为colors再创建一个VBO。现在,positions是CPU内存中的数据,我们要把它上传到GPU内存中。
var positionBufferObject = new uint[];
// 创建VBO
GL.GenBuffers(, this.positionBufferObject);
// 绑定VBO(选中此VBO)
GL.BindBuffer(BufferTarget.ArrayBuffer, positionBufferObject[]);
// 将托管的数据转到非托管内存
UnmanagedArray<vec3> positionArray = new UnmanagedArray<vec3>(positions.Length);
for (int i = ; i < positions.Length; i++)
{
positionArray[i] = positions[i];
} // 将position属性上传到GPU内存,即为VBO填入数据
GL.BufferData(BufferTarget.ArrayBuffer, positionArray, BufferUsage.StaticDraw); positionArray.Dispose();
这里的GL.GenBuffers、GL.BindBuffer和GL.BufferData完成了初始化VBO的工作。
初始化colors的VBO的过程同上。
VBO初始化完成后,内存中的数组就可以删掉了。
用VBO进行渲染
渲染时的操作如下:
指定顶点属性
// 绑定VBO(选中此VBO)
GL.BindBuffer(BufferTarget.ArrayBuffer, positionBufferObject[]);
//获取shader中的’in_Position’的指针。
uint positionLocation = shaderProgram.GetAttributeLocation(strin_Position);
// 当前VBO(此时为positions的VBO)对应ShaderProgram里的'in_Position'in变量。每3个float(即一个顶点)为一个数据单元。
GL.VertexAttribPointer(positionLocation, , GL.GL_FLOAT, false, , IntPtr.Zero);
// 启用此顶点属性数组。
GL.EnableVertexAttribArray(positionLocation);
这是指定顶点的position属性的过程,指定顶点的color属性的过程同上。
无索引渲染
各个属性都指定好了,就该开始渲染了:
// 指定要使用哪个VAO。
GL.BindVertexArray(vao[]);
// 依照VBO中的顺序渲染12个顶点(用渲染三角形的方式)。
GL.DrawArrays(GL.GL_TRIANGLES, , );
// 不再指定VAO。
GL.BindVertexArray();
有索引渲染
上面的渲染方式不能重复利用同一个顶点,而下面的用索引进行渲染的方式则可以。
为了使用索引进行渲染,我们先要初始化一个索引的VBO。这与初始化顶点属性VBO是同样的步骤。
var indexBufferObject = new uint[];
GL.GenBuffers(, indexBufferObject);
GL.BindBuffer(BufferTarget.ElementArrayBuffer, indexBufferObject[]);
UnmanagedArray<uint> indexArray = new UnmanagedArray<uint>();
for (int i = ; i < indexArray.Length; i++)
{
indexArray[i] = (uint)i;
}
GL.BufferData(BufferTarget.ElementArrayBuffer, indexArray, BufferUsage.StaticDraw);
indexArray.Dispose();
然后用索引进行渲染:
GL.BindVertexArray(vao[]);
// 依照依照索引VBO中给定的顺序渲染12个顶点(用渲染三角形的方式)
GL.DrawElements(GL.GL_TRIANGLES, , GL.GL_UNSIGNED_INT, IntPtr.Zero);
GL.BindVertexArray();
OpenGL是如何看待VBO的
从上面的初始化和渲染过程来看,OpenGL里有这样一些概念。
顶点属性(vertex property)
在OpenGL中,想渲染一个顶点,至少需要知道它的位置和颜色这两个信息。位置、颜色这样的信息就称为顶点的属性。当然,像法线等也都是属性。你也可以根据业务需求自定义一些属性。总之,一个VBO描述的是一组顶点的某个属性。例如上文我们定义了一个描述顶点position属性的VBO和一个描述顶点color属性的VBO。
但是,描述索引的VBO并不是顶点的属性,证据是,索引VBO可以重复引用同一个顶点。这就提醒我们,每个顶点都有且只有1份的才是顶点属性。描述索引的VBO长度不一定等于描述其他顶点属性的VBO的长度。
所以,VBO有描述顶点属性和描述索引两大种类。
显式索引和隐式索引
声明,显式索引和隐式索引是我自创的两个名词,你没听过并不奇怪。
其含义很简单:
显式索引,就是上文中用GL.DrawElements进行渲染的方式。
隐式索引,就是上文中用GL.DrawArrays进行渲染的方式。这种方式没有显式指明索引,但是隐含着的规则是以顶点在VBO中的顺序为索引进行渲染。
封装VBO
有了上面的分析,对VBO的封装就呼之欲出了。

此类图符合VBO分为描述属性和描述索引两类,索引分为显示和隐式两类。
此类图中的对象是为初始化VBO用的。初始化完成后就可以释放掉了。所以有如下的代码:
|
protected override BufferRenderer CreateRenderer() { uint[] buffers = new uint[1]; GL.GenBuffers(1, buffers); GL.BindBuffer(GL.GL_ARRAY_BUFFER, buffers[0]); GL.BufferData(GL.GL_ARRAY_BUFFER, this.ByteLength, this.Header, (uint)this.Usage); PropertyBufferRenderer renderer = new PropertyBufferRenderer( this.VarNameInVertexShader, buffers[0], this.DataSize, this.DataType); return renderer; } |
上述代码中的BufferRenderer是用来反复执行渲染过程的。其类图如下,与VertexBuffer的继承十分对仗。

封装VAO
一个VAO里有多个VBO,就这么简单。
/// <summary>
/// 一个vertex array object。(即VAO)
/// <para>VAO是用来管理VBO的。可以进一步减少DrawCall。</para>
/// </summary>
public class VertexArrayObject : IDisposable
{
BufferRenderer[] bufferRenderers;
IndexBufferBaseRenderer indexBufferRenderer; /// <summary>
/// 一个vertex array object。(即VAO)
/// <para>VAO是用来管理VBO的。可以进一步减少DrawCall。</para>
/// </summary>
/// <param name="propertyBuffers">给出此VAO要管理的所有VBO。</param>
public VertexArrayObject(params BufferRenderer[] propertyBuffers)
{
this.bufferRenderers = propertyBuffers;
foreach (var item in propertyBuffers)
{
var renderer = item as IndexBufferBaseRenderer;
if (renderer != null)
{
indexBufferRenderer = renderer;
}
}
} private bool disposedValue; /// <summary>
/// 此VAO的ID,由OpenGL给出。
/// </summary>
public uint ID { get; private set; } /// <summary>
/// 在OpenGL中创建VAO。
/// </summary>
/// <param name="e"></param>
/// <param name="shaderProgram"></param>
public void Create(RenderEventArgs e, Shaders.ShaderProgram shaderProgram)
{
uint[] buffers = new uint[];
GL.GenVertexArrays(, buffers); this.ID = buffers[]; this.Bind();
foreach (var item in this.bufferRenderers)
{
item.Render(e, shaderProgram);
}
this.Unbind();
} private void Bind()
{
GL.BindVertexArray(this.ID);
} private void Unbind()
{
GL.BindVertexArray();
} public void Render(RenderEventArgs e, Shaders.ShaderProgram shaderProgram)
{
this.Bind();
this.indexBufferRenderer.Render(e, shaderProgram);
this.Unbind();
} public override string ToString()
{
return string.Format("VAO ID: {0}", this.ID);
} public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
} ~VertexArrayObject()
{
this.Dispose(false);
} protected virtual void Dispose(bool disposing)
{ if (this.disposedValue == false)
{
if (disposing)
{
// Dispose managed resources. } // Dispose unmanaged resources.
foreach (var item in this.bufferRenderers)
{
item.Dispose();
}
GL.DeleteVertexArrays(, new uint[] { this.ID });
} this.disposedValue = true;
} }
VAO
详情参考CSharpGL代码即可。
CSharpGL(7)对VAO和VBO的封装的更多相关文章
- CSharpGL(37)创建和使用VBO的最佳方式
CSharpGL(37)创建和使用VBO的最佳方式 开始 近日在OpenGL红宝书上看到这样的讲解. 其核心意思是,在创建VBO时用 glBufferData(GL_ARRAY_BUFFER, len ...
- 几张图看明白VAO、VBO、EBO的关系和代码顺序
0.详细教程可看https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/ 1.可以简单地认为VAO的 ...
- 【OpenGL】VAO与VBO
1.我们先了解什么是OpenGL对象(OpenGL Object) 根据OpenGL Wiki的定义: An OpenGL Object is an OpenGL construct that con ...
- OpenGL 4.0 GLSL 基础教程概览——VAO和VBO常用操作接口
(一) OpenGL 4.3 最新渲染管线图 从OpenGL 2.0 到 OpenGL 3.0变化非常大,但从OpenGL 3.0 到OpenGL 4.0 变化不是太大. 着色器程序直接运行在GPU ...
- (Python OpenGL)【 0】关于VAO和VBO以及OpenGL新特性
(Python OpenGL)关于新版OpenGL需要了解的: 随着OpenGL状态和固定管线模式的移除,我们不在用任何glEnable函数调用,而且也不会有glVertex.glColor等函数调用 ...
- OpenGL(二十四) VAO、VBO和着色器使用示例
1. 新建一个工程,新建一个头文件Shader.h,内容如下: #ifndef _SHADER_H_ #define _SHADER_H_ #include <vector> #inclu ...
- VAO和VBO
我想大家都已经熟悉VBO了吧.在GL3.0时代的VBO大体还是处于最重要的地位,但是与此同时也出现了不少新的用法和辅助役,其中一个就是VAO.本文大致小记一下这两者的联系,帮助大家理解一下这个角色.— ...
- 图形渲染的大致过程和关于OpenGL渲染管线的一些零碎知识,openglpipeline,vao,vbo,ebo.
重要!!! OpenGL新人一枚,希望可以再此和大家分享有用的知识,少走弯路 文章会定期更新,把前面几段已经整理过的知识更完后,接下来每周至少会更两次. 文章如果有不对的,理解错误的地方,也非常希望在 ...
- BIT祝威博客汇总(Blog Index)
+BIT祝威+悄悄在此留下版了个权的信息说: 关于硬件(Hardware) <穿越计算机的迷雾>笔记 继电器是如何成为CPU的(1) 继电器是如何成为CPU的(2) 关于操作系统(Oper ...
随机推荐
- C语言 · 高精度加法
问题描述 输入两个整数a和b,输出这两个整数的和.a和b都不超过100位. 算法描述 由于a和b都比较大,所以不能直接使用语言中的标准数据类型来存储.对于这种问题,一般使用数组来处理. 定义一个数组A ...
- C语言 · Torry的困惑(基本型)
问题描述 Torry从小喜爱数学.一天,老师告诉他,像2.3.5.7--这样的数叫做质数.Torry突然想到一个问题,前10.100.1000.10000--个质数的乘积是多少呢?他把这个问题告诉老师 ...
- 从备考PMP到与项目经理同呼吸
前言 PMP是什么梗? 项目管理专业人士资格认证.它是由美国项目管理协会(Project Management Institute(PMI)发起的,严格评估项目管理人员知识技能是否具有高品质的资格认证 ...
- OVS 中的各种网络设备 - 每天5分钟玩转 OpenStack(128)
上一节我们启用了 Open vSwitch,本节将查看当前的网络状态并介绍 Open vSwitch 涉及的各种网络设备 初始网络状态 查看一下当前的网络状态. 控制节点 ifconfig 显示控制节 ...
- 算法与数据结构(八) AOV网的关键路径
上篇博客我们介绍了AOV网的拓扑序列,请参考<数据结构(七) AOV网的拓扑排序(Swift面向对象版)>.拓扑序列中包括项目的每个结点,沿着拓扑序列将项目进行下去是肯定可以将项目完成的, ...
- 自定义搭建PHP开发环境
学习了一段时间php了,因为之前是刚接触php,所以用的是集成安装包(wamp).现在想进一步了解apache.mysql.php之间的关系以及提升自己所以进行自定义搭建PHP开发环境.废话不多说,请 ...
- [转]Patch文件结构详解
N久不来 于是不知道扔在哪儿于是放这里先 如果你觉得碍事的话 帮我扔到合适的版块去.. 导读这是一篇说明文 它介绍了标准冒险岛更新文件(*.patch;*.exe)的格式文章的最后附了一段C#的参考代 ...
- 消息队列性能对比——ActiveMQ、RabbitMQ与ZeroMQ(译文)
Dissecting Message Queues 概述: 我花了一些时间解剖各种库执行分布式消息.在这个分析中,我看了几个不同的方面,包括API特性,易于部署和维护,以及性能质量..消息队列已经被分 ...
- Linux实战教学笔记01:计算机硬件组成与基本原理
标签(空格分隔): Linux实战教学笔记 第1章 如何学习Linux 要想学好任何一门学问,不仅要眼睛看,耳朵听,还要动手记,勤思考,多交流甚至尝试着去教会别人. 第2章 服务器 2.1 运维的基本 ...
- BZOJ 1597: [Usaco2008 Mar]土地购买 [斜率优化DP]
1597: [Usaco2008 Mar]土地购买 Time Limit: 10 Sec Memory Limit: 162 MBSubmit: 4026 Solved: 1473[Submit] ...