欢迎来真孝善网,为您提供真孝善正能量书籍故事!

OpenGL ES 教程(第三部分):绘制三角形

时间:11-07 现代故事 提交错误

这篇文章给大家聊聊关于OpenGL ES 教程(第三部分):绘制三角形,以及对应的知识点,希望对各位有所帮助,不要忘了收藏本站哦。

- (void)setupConfig {

//创建一个新的OpenGLES上下文

self.mContext=[[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];

//VC为GLViewContrlller,storyboard需要修改类型

GLKView* 视图=(GLKView *)self.view;

view.context=self.mContext;

view.drawableColorFormat=GLKViewDrawableColorFormatRGBA8888; //颜色缓冲区格式

[EAGLContext setCurrentContext:self.mContext];

}

2. 顶点数据准备

这里用三个顶点来绘制一个三角形:

//创建顶点数组

GLfloat 顶点[]={

0.0,0.5,0.0, //点1:x,y,z

0.5,-0.5,0.0,//点2:x,y,z

-0.5,-0.5,0.0//点3:x,y,z

};这里需要了解一个概念:标准化设备坐标(NDC);

标准坐标轴(忽略z 轴)如下:

NDC顶点着色器的主要任务是将输入顶点处理为标准坐标。因为这只是一个例子,我们会在后续的顶点着色器中直接使用输入的坐标,所以这里生成的三个顶点都是基于标准坐标的。用坐标轴构造;

3. 传递顶点数据到 GPU

上面的三个顶点是在CPU内存中创建的,渲染管线是在GPU中完成的,所以这些顶点着色器需要传入GPU供后续阶段的着色器使用。

GPU中的内存通常用Buffer来表示。 Buffer有不同类型,常见的Frame Buffer就是其中之一。

现在需要将顶点数组传递给GPU中的Buffer,并且需要创建一个Buffer来接收数据。这就是VBO(顶点缓冲区对象)。 VBO可以一次向显卡发送大量数据。数据发送到显卡内存后,顶点着色器几乎可以立即访问顶点。创建过程如下:

//创建VBO(Vertex Buffer Object)用于将顶点数据从CPU发送到GPU

无符号整型VBO;

//第一个参数是数量?第二个参数是地址,用作id

glGenBuffers(1, VBO);以上就是Buffer的创建。创建完成后,需要绑定类型来告诉GPU这个Buffer是用来做什么的。顶点数据Buffer的类型是GL_ARRAY_BUFFER。绑定代码如下:

//绑定缓冲区类型,顶点缓冲区类型为GL_ARRAY_BUFFER

glBindBuffer(GL_ARRAY_BUFFER, VBO);绑定完成后,下次调用GL_ARRAY_BUFFER类型的缓冲函数就会对这个VBO进行操作。您可以使用glBufferData 将数据从CPU 传输到GPU:

//复制顶点数据到缓冲存储器(CPU-GPU)

glBufferData(GL_ARRAY_BUFFER,sizeof(triangleVertices),triangleVertices,GL_STATIC_DRAW); glBufferData是一个专门用于将用户定义的数据复制到当前绑定缓冲区的函数。其输入参数如下:

第一个参数:目标缓冲区的类型;当顶点缓冲区对象当前绑定到GL_ARRAY_BUFFER 目标时;

第二个参数:指定传输数据的大小(以字节为单位);只需使用简单的sizeof 来计算顶点数据大小。在此演示中,顶点数组直接在同一函数中定义。如果数组作为指针传递,则需要同时传递length,因为sizeof总是计算指针的大小,而不是数组的实际大小;

第三个参数:实际数据;这里可以直接传入顶点数组;

第四个参数:指定我们希望显卡如何管理给定的数据;该参数决定数据写入内存的哪一部分,例如缓存或普通内存。它有三种形式:

GL_STATIC_DRAW:数据从不或很少改变。

GL_DYNAMIC_DRAW:数据会发生很大的变化。

GL_STREAM_DRAW:每次绘制数据都会改变。

三角形的位置数据不会改变,并且在每次渲染调用时保持不变,因此最好使用GL_STATIC_DRAW。例如,如果缓冲区中的数据会频繁更改,那么使用的类型是GL_DYNAMIC_DRAW或GL_STREAM_DRAW,这可以确保显卡将数据放置在可以高速写入的内存部分中。

上述函数调用完成后,顶点数据已经从CPU传输到GPU,可以进行渲染操作了;

虽然在一些移动设备上GPU和CPU共享内存,即GPU的Buffer也位于CPU内存中,但学习时应该区分一下,这样概念会更清晰;

4. 顶点着色器

要使用着色器,您需要了解GLSL(OpenGL 着色语言)。 openGL ES中的着色器语言称为GLSL ES;

着色器本身就是一个微程序,需要使用相应的语言来编写程序。 iOS中使用OpenGL ES 3.0,使用对应的GLSL ES 3.0。 GLSL其实和C语言很相似,语法也很简单。只需要注意一些具体的规则,比如version的声明、in和out指定输入输出参数等,这里不再赘述。具体语法可以参考。官方文档:

https://www.khronos.org/registry/OpenGL/specs/es/3.0/GLSL_ES_Specification_3.00.pdf

顶点传输到GPU后,首先需要被顶点着色器处理成标准坐标轴。为了简单起见,这里我们直接使用输入顶点来输出,所以这个简单的着色器代码如下:

//GLSL ES 版本3.0

#版本300es

//输入顶点是一个向量aPos(变量名),分量为3(vec3)

vec3 aPos 中的布局(位置=0);

无效主(){

//这里输入的顶点数据直接作为输出传递

gl_Position=vec4(aPos.x, aPos.y, aPos.z, 1.0);

//第二种写法:

gl_Position=vec4(aPos,1.0);

上面代码中,第一行首先声明GLSL的版本为ES版本,版本号为300;

在GLSL ES中,需要在第一行使用#version number es来声明版本,并且它必须位于所有预处理命令和注释之前;

如果不使用#version声明版本,则默认使用GLSL ES 1.0版本;

文档如下:

版本向量Vector 是GLSL 中的变量类型。文档如下:

解释向量:

vec + number:表示向量有多少个分量。例如上面的vec3 表示有三个分量。这里用来表示顶点的x,y,z,输出vec4表示有4个分量。这里vec4中的第四个分量是w,不代表空间位置,而是与透视划分有关,暂时设置为1;

type + vec:表示向量中分量的类型。默认为float,所以省略。但如果是其他类型,则需要加上type的前缀。例如ivec2表示具有2个int分量类型的向量;

在+ 位置

in的官方文档如下:

在+ 位置,因为顶点着色器是第一个着色器,直接接受外部数据。其他着色器只能接收来自前一个着色器的输入参数。因此,顶点着色器可以使用位置来指示需要在顶点数组中的何处开始获取数据。在这个例子中,显然第一个顶点是有效数据,所以设置location=0;

in 表示输入顶点属性。顶点属性指的是顶点数据将如何被解析,类似于MVC中数据模型的作用,稍后将讨论。这里,使用in表示输入顶点属性是vec3向量;

至于layout的其他功能,可以自行查看官方文档;

官方文档如下:

Out 与顶点着色器类似。片段着色器是混合之前的最后一个输出,也可以定义一个位置。

另外,out表示输出到下一个shader的数据类型,暂不赘述;

gl_Positiongl_Position表示顶点着色器的输出结果。顶点着色器需要输出一个分量为4的向量,所以这里没有声明out;

至于代码中的两种写法,可以参考GLSL ES的语法标准进行详细了解;

5. 编译顶点着色器

顶点着色器代码写完后,还需要编译,编译时只接受C类型的char字符,所以需要这样转换:

const char *vertexShaderSource="#version 300 esn"

"vec3 aPos 中的布局(位置=0);n"

"无效主() {n"

" gl_Position=vec4(aPos.x, aPos.y, aPos.z, 1.0);n"

"}";接下来,您需要编译着色器。在编译着色器之前,需要创建着色器:

//创建着色器

GLuint 顶点着色器;

vertexShader=glCreateShader(GL_VERTEX_SHADER);创建完成后,可以编译:

/**

* glShaderSource函数将要编译的着色器对象作为第一个参数。

* 第二个参数指定传递的源代码字符串的数量,这里只有一个。

* 第三个参数是顶点着色器的真实源代码

* 我们首先将第四个参数设置为NULL。

*/

glShaderSource(vertexShader, 1, vertexShaderSource, NULL);

//编译着色器

glCompileShader(顶点着色器);编译shader时,可能会因为语法或者版本原因而报错。您可以通过以下方式获取错误信息:

//检查编译是否成功

成功;

字符信息日志[512];

glGetShaderiv(vertexShader, GL_COMPILE_STATUS, 成功);

如果(!成功){

glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);

NSLog(@"着色器编译失败:%s",infoLog);

}

6. 片段着色器

因为系统提供了几何着色器,为了简单起见,我们可以使用系统默认的几何着色器,直接编写片段着色器;

此示例中的着色器非常简单,仅输出蓝色:

#版本300es

布局(位置=0)out lowp vec4 myColor;

无效主(){

myColor=vec4(1.0, 0.5, 0.2, 1.0);

}这里有几点需要注意:

布局+位置上面已经简单讨论过,不再重复;在GLSL 3.0中,对于片段着色器的输出,需要标明精度;如果不标明精度,会报错:

2022-04-20 11:08:45.211535+0800 XKOpenGL[11112:1652554] ERROR: 0:2: "vec4" : 声明必须包含type2D 的精度限定符。对于中等精度,使用lowp:

精度选择

7. 编译片段着色器

编译过程与顶点着色器相同,不再赘述:

const char *fragmentShaderSource="#version 300 esn"

"布局(位置=0)out lowp vec4 myColor;n"

"无效主() {n"

"myColor=vec4(1.0, 0.5, 0.2, 1.0);n"

"}";

无符号整数fragmentShader;

fragmentShader=glCreateShader(GL_FRAGMENT_SHADER);

glShaderSource(fragmentShader,1,fragmentShaderSource,NULL);

glCompileShader(fragmentShader);

int 片段编译成功;

字符fragmentInfoLog[512];

glGetShaderiv(fragmentShader,GL_COMPILE_STATUS,fragmentCompileSuccess);

if(!fragmentCompileSuccess){

glGetShaderInfoLog(fragmentShader, 512, NULL,fragmentInfoLog);

NSLog(@"%s",fragmentInfoLog);

}

8. 链接着色器生成着色器程序

编译好的着色器程序还需要连接到主着色器程序。代码如下:

//创建着色器程序

无符号整型着色器程序;

着色器程序=glCreateProgram();

//添加着色器

glAttachShader(shaderProgram, vertexShader);

glAttachShader(shaderProgram,fragmentShader);

//关联

glLinkProgram(着色器程序);

int 链接成功;

char linkInfoLog[512];

glGetProgramiv(shaderProgram, GL_LINK_STATUS, linkSuccess);

if(!linkSuccess) {

glGetProgramInfoLog(shaderProgram, 512, NULL, linkInfoLog);

NSLog(@"%s",linkInfoLog);

}

9. 激活着色器程序并清理已链接完成的着色器

激活shader主程序后,pipline中会使用shader程序来执行整个pipline;

另外,链接的着色器代码已被复制到着色器主程序中,应将其删除以释放内存;

代码如下:

//激活程序,每次着色器调用和渲染调用都会使用这个程序对象

glUseProgram(着色器程序);

//链接到shader程序后(相当于打包生成可执行程序),原来的两个小shader就可以删除了

glDeleteShader(顶点着色器);

glDeleteShader(fragmentShader);

10. 顶点属性解析

通过以上步骤,我们完成了:

1. 顶点数据已从CPU内存复制到GPU缓存。

2.使用顶点着色器指示GPU如何处理顶点并将其输出到下一个着色器

3. 使用片段着色器来指示GPU生成的像素的颜色值。

4.编译并链接两个shader,生成最终的shader程序

5. 着色器程序被激活,后续管线将使用该着色器程序。

6.删除了两个已链接的着色器

此时我可以调用draw call吗?不,因为OpenGL 还不知道如何解释缓冲区中的顶点数据。

顶点数据传递到GPU后,仍然是一对浮点数组。一个顶点应该包含4 个元素还是3 个元素?价值从哪里开始?第二点从何而来? GPU不知道其他一系列问题,因此它还需要告诉顶点着色器如何解析这些顶点数据:

//根据点解析顶点

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);

//顶点属性默认是禁用的,这里开始顶点数据

glEnableVertexAttribArray(0);第一个参数:指定我们要配置的顶点属性。该参数类似于布局中的location(location=0),告诉着色器应该从顶点数组的哪个元素开始获取数据;

第二个参数:指定顶点属性的大小。这里的大小是指数组中元素的数量。顶点属性是一个vec3,它由标识顶点的3个浮点值组成,因此大小为3。

第三个参数:指定数据类型,这里是GL_FLOAT;

第四个参数:是否要对数据进行归一化(Normalize)。如果我们将其设置为GL_TRUE,则所有数据都将映射在0(-1表示有符号数据)和1之间。我们将其设置为GL_FALSE。

第五个参数:步幅补偿告诉我们连续顶点属性组之间的间隔。由于下一组位置数据是3 个float 后,我们将步长设置为3 * sizeof(float)。

请注意,由于我们知道这个数组是紧密排列的(两个顶点属性之间没有间隙),我们还可以将其设置为0,让OpenGL 决定步长是多少(仅当值紧密排列时才可用)。一旦我们有了更多的顶点属性,我们就必须更加小心地定义每个顶点属性之间的间距,稍后我们将看到更多这样的例子;

第六个参数:缓冲区起始位置的偏移量。参数类型是void*,所以我们需要执行这个奇怪的转换。它表示位置数据在缓冲区中起始位置的偏移量(Offset)。由于位置数据位于数组的开头,因此这里为0。

11. VAO

上面已经将顶点数据传递给了GPU,并且已经告诉着色器如何获取和解析顶点数据。一切都会好起来吗?

此时需要考虑一个场景:重复绘制。假设上图顶点表示的三角形需要绘制多次,那么上面的步骤是不是:

顶点数组的生成;创建缓冲区;将数据从CPU传输到GPU缓冲区;顶点数据属性的设置;

这些步骤需要再次完成。事实上,这种场景并不少见。例如,GLKViewController每秒会调用代理方法-glkView:drawInRect: 60次来进行绘制。如果上面的代码写在代理方法中,上面的4个步骤每秒会重复60次吗?这是个人猜测,实际情况可以使用仪器进行验证;

于是,VAO这个时候登场了。

VAO:Vertex Array Object,顶点数组对象。记录顶点数组数据和顶点数据属性的解析格式;

猜猜VAO应该指向GPU中的内存?这避免了从CPU到GPU的频繁数据传输;

另外,需要说明两点:

OpenGL的核心模式要求我们使用VAO。如果VAO没有绑定或者绑定失败,那么OpenGL将不会进行任何绘制;当调用绘制调用时,将使用当前绑定的VAO中存储的数据进行绘制;总之,你绕不开VAO,所以你最好了解一下它是什么~~~

官方的解释有点虚幻。就人类而言,这种方法会影响以下功能:

glBufferData:从CPU复制数据到GPU glVertexAttribPointer:属性设置相关方法glEnableVertexAttribArray/glDisableVertexAttribArray:打开/关闭顶点数组以上方法调用的结果将存储在VAO中。如果下次需要使用这些顶点以及对应的属性解析格式,则不需要执行上述四个步骤,只需要重新绑定VAO即可。

官方图标如下:

VAO和VBO 因此,VAO相关代码必须写在VBO之前。前面步骤中的代码更新如下:

//创建VAO

无符号整型VAO;

glGenVertexArrays(1, VAO);

//绑定VAO

glBindVertexArray(VAO);

//初始化顶点数组

GLfloat 三角形顶点[]={

-0.5f、-0.5f、0.0f、

0.5f、-0.5f、0.0f、

0.0f、0.5f、0.0f

};

//创建VBO

无符号整型VBO;

glGenBuffers(1, VBO);

glBindBuffer(GL_ARRAY_BUFFER, VBO);

//将数据从CPU传输到GPU

glBufferData(GL_ARRAY_BUFFER,sizeof(triangleVertices),triangleVertices,GL_STATIC_DRAW);

//设置顶点属性

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);

//开始顶点数据

glEnableVertexAttribArray(0);

. 省略shader等代码. 上述代码调用glBindVertexArray后,VAO将作为当前shader的数据源。随后又调用了glBufferData、glVertexAttribPointer、glEnableVertexAttribArray这三个函数。相关数据已经绑定到这个VAO上了,所以可以直接进行draw call~~~

12. draw call

在iOS中,需要在GLKView的代理方法中调用draw call:

- (无效)glkView:(GLKView *)视图drawInRect:(CGRect)矩形{

glClearColor(0.3f, 0.6f, 1.0f, 1.0f);

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

glDrawArrays(GL_TRIANGLES, 0, 3);

}

13. surprise

结果如下:

结果:

14. 多个 VBO 之间的切换

的官网代码演示只显示了1个VAO。这里使用两个VAO来进一步了解VAO的作用和功能;

为了用代码表达显示,首先创建两个VAO:

@property(分配,非原子) GLuint vaoTriangleOne;

@property(分配,非原子) GLuint vaoTriangleTwo;这里我们首先封装一下上面四种可能的重复步骤以及VAO的绑定操作。源码如下:

- (void)setupVertexArrayObject:(GLuint *)vao 顶点:(GLfloat[])顶点长度:(GLuint)长度strideCount:(GLuint)strideCount {

//创建顶点数组对象

glGenVertexArra

ys(1, vao); glBindVertexArray(*vao); // create vertex buffer object GLuint vbo; glGenBuffers(1, &vbo); // bind buffer object glBindBuffer(GL_ARRAY_BUFFER, vbo); // copy data to buffer glBufferData(GL_ARRAY_BUFFER, length *sizeof(GLfloat), vertices, GL_STATIC_DRAW); // attr glVertexAttribPointer(0, strideCount, GL_FLOAT, GL_FALSE, strideCount * sizeof(float), (void*)0); // 顶点属性默认是禁用的,这里启动顶点数据 glEnableVertexAttribArray(0); }此时,就可以创建两个顶点数据来绑定 VAO 了: GLfloat triangleOne[] = { 0,0.5,1.0, 0.5,-0.5,1.0, 0,-0.5,1.0 }; [self setupVertexArrayObject:&_vaoTriangleOne vertices:triangleOne length:9 strideCount:3]; GLfloat triangleTwo[] = { 0,0.5,1.0, -0.5,-0.5,1.0, 0,-0.5,1.0 }; [self setupVertexArrayObject:&_vaoTriangleTwo vertices:triangleTwo length:9 strideCount:3];上述代码调用了两次glBindVertexArray函数,也就是顶点数据在 GPU 中的位置、如何解析顶点属性、顶点数组是否开启,这个结果已经被保存在了两个 VAO 中了,接下来的调用代码可以简化成下面: - (void)glkView:(GLKView *)view drawInRect:(CGRect)rect { glClearColor(0.3f, 0.6f, 1.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); if (self.shouldShowSecond) { glBindVertexArray(self.vaoTriangleTwo); } else { glBindVertexArray(self.vaoTriangleOne); } // shader program if (self.program != 0) { glDrawArrays(GL_TRIANGLES, 0, 3); }

用户评论

你与清晨阳光

终于来了!一直在等著看看怎么用 OpenGL ES 画三角形的教程。

    有19位网友表示赞同!

凉笙墨染

之前学过一点图形学的知识,应该能理解这个教程吧

    有7位网友表示赞同!

轨迹!

感觉画三角形是渲染3D模型的基础啊,要好好掌握一下

    有7位网友表示赞同!

无关风月

我最近在尝试做 AR 应用,这教程是不是很有用?

    有19位网友表示赞同!

熟悉看不清

希望教程讲解简单易懂,方便小白学习

    有10位网友表示赞同!

古巷青灯

看这个标题,应该会介绍 OpenGL ES 的基本用法吧?

    有16位网友表示赞同!

无寒

学习3D游戏开发肯定得了解 OpenGL ES!

    有15位网友表示赞同!

琴断朱弦

做手机游戏需要用到三维图形渲染,真是太厉害了!

    有17位网友表示赞同!

醉红颜

以前用其他的库画过三角形,想看看 OpenGL ES 的做法有什么区别

    有6位网友表示赞同!

此刻不是了i

期待这个教程能教会我如何控制三角形的颜色和大小等属性

    有6位网友表示赞同!

蝶恋花╮

感觉学习这些知识很有挑战性,但也很有趣!

    有9位网友表示赞同!

念旧是个瘾。

如果教程中包含代码示例,那就更棒了!

    有13位网友表示赞同!

冷眼旁观i

我已经订阅了这个博主的其他内容,他写的教程真的很好理解!

    有5位网友表示赞同!

淡抹丶悲伤

想尝试用 OpenGL ES 做个简单的3D动画效果

    有13位网友表示赞同!

凉话刺骨

学习 OpenGL ES 需要哪些基础知识啊?

    有18位网友表示赞同!

莫阑珊

这篇文章能让我初步了解OpenGL ES的用法吗?

    有19位网友表示赞同!

余温散尽ぺ

我最近在学习移动开发,这个教程会不会太深入了?

    有14位网友表示赞同!

失心疯i

希望这个教程能包含更多丰富的应用案例

    有11位网友表示赞同!

殃樾晨

画三角形听起来很简单,但实际上可能有很多细节吧!

    有15位网友表示赞同!

【OpenGL ES 教程(第三部分):绘制三角形】相关文章:

1.蛤蟆讨媳妇【哈尼族民间故事】

2.米颠拜石

3.王羲之临池学书

4.清代敢于创新的“浓墨宰相”——刘墉

5.“巧取豪夺”的由来--米芾逸事

6.荒唐洁癖 惜砚如身(米芾逸事)

7.拜石为兄--米芾逸事

8.郑板桥轶事十则

9.王献之被公主抢亲后的悲惨人生

10.史上真实张三丰:在棺材中竟神奇复活