iOS --- OpenGLES之简单的图形绘制

本文详细介绍使用OpenGLES绘制三角形与矩形的过程,包括Shader脚本编写、顶点数据处理及VBO使用等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在上一篇博客 iOS — OpenGLES之着色器(shader)的编译、链接及使用 中,简要介绍着色器(shader)的编译、链接及使用。本文将在之前一系列OpenGLES相关博客的基础上,使用OpenGLES绘制基本的图形。
以下两个例子中,对于Shader的编译使用等过程基本一致。

绘制三角形

Shader脚本

Vertex Shader如下:

attribute vec4 Position; // variable passed into

void main(void) {
    gl_Position = Position; // gl_Position is built-in pass-out variable. Must config for in vertex shader
}

Fragment Shader如下:

precision mediump float;

void main(void) {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // must set gl_FragColor for fragment shader
}

在这里,我们要绘制一个红色的三角形,因此只需要传递Position参数即可。
颜色我们在Fragment Shader中指定即可 gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);

关联Shader参数

关于Shader的编译,链接及使用的更多内容, 请参考之前的博客:
iOS — OpenGLES之着色器[shader]的编译、链接及使用

_glProgram = [ShaderOperations compileShaders:@"DemoShaderVertex" shaderFragment:@"DemoShaderFragment"];

glUseProgram(_glProgram);
_positionSlot = glGetAttribLocation(_glProgram, "Position");

// 设置viewport
glViewport(0, 0, self.view.frame.size.width, self.view.frame.size.height);

以上代码使用已编译好的_glProgram着色器程序, 将Shader脚本的Position参数, 与_positionSlot插槽绑定起来.
则后续赋值给_positionSlot的顶点数据, 会直接传递至Shader中的Position参数.
这样, 就完成了Shader脚本的链接及使用步骤.

而glViewPort方法用于设置OpenGLES显示的区域.

仅使用顶点数据绘制三角形

// 设置顶点数组
const GLfloat vertices[] = {
    0.0f,  0.5f, 0.0f,
    -0.5f, -0.5f, 0.0f,
    0.5f,  -0.5f, 0.0f };

// 给_positionSlot传递vertices数据
glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, vertices);
glEnableVertexAttribArray(_positionSlot);

// Draw triangle
glDrawArrays(GL_TRIANGLES, 0, 3);

[_eaglContext presentRenderbuffer:GL_RENDERBUFFER];

绘制三角形的步骤也很简单:
1. 准备好顶点数据.
2. 将顶点数据传递给_positionSlot插槽, 即传递至Shader脚本中.
3. 使用glDrawArrays绘制图形, 使用presentRenderbuffer:将其呈现到屏幕上.

使用VBO绘制三角形

Vertex Buffer Object(VBO)是GPU存储空间里的一块缓冲区, 可用于存储顶点的所有信息. OpenGL在GPU中记录着VBO与对应的GPU内存地址.
如果不使用VBO, 则每次直接从CPU内存中传递顶点数据到GPU内存中进行运算和渲染.
使用VBO就可将顶点数据存于GPU内存中, 而不需要每次都在CPU和GPU之间进行传递, 效率大大提升.
VBO适用于频繁读取顶点数据的场景, 在这里绘制三角形仅需三个顶点, 且不会重复使用. 因此不是VBO的典型使用场景. 这里的目的仅为了简单介绍VBO.

const GLfloat vertices[] = {
    0.0f,  0.5f, 0.0f,
    -0.5f, -0.5f, 0.0f,
    0.5f,  -0.5f, 0.0f };

GLuint vertexBuffer;
glGenBuffers(1, &vertexBuffer);
// 绑定vertexBuffer到GL_ARRAY_BUFFER目标
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
// 为VBO申请空间,初始化并传递数据
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

// 使用VBO时,最后一个参数0为要获取参数在GL_ARRAY_BUFFER中的偏移量
glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(_positionSlot);

glDrawArrays(GL_TRIANGLES, 0, 3);

由上边代码可看出, VBO的使用方式比较固定.
一旦将VBO对象与GL_ARRAY_BUFFER绑定起来, 并将顶点数据传递过去, 则后续的顶点数据默认都从GL_ARRAY_BUFFER中取得.
glVertexAttribPointer方法的最后一个参数要注意:
当不使用VBO时, 要传递顶点数组的指针, 如

glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, vertices);

而当使用VBO时, 传递顶点数据在GL_ARRAY_BUFFER中的偏离量, 这里是0.

glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, 0);

结果如图:
绘制红色的三角形

绘制矩形

Shader脚本

Vertex Shader如下:

// variable pass into
attribute vec4 Position;    // position of vertex
attribute vec4 SourceColor; // color of vertex

// variable pass out into fragment shader
// varying means that calculate the color of every pixel between two vertex linearly(smoothly) according to the 2 vertex's color
varying vec4 DestinationColor;

void main(void) {
    DestinationColor = SourceColor;
    // gl_Position is built-in pass-out variable. Must config for in vertex shader
    gl_Position = Position;
}

Vertex Shader接收如下两个参数:
Position: 直接赋值给gl_Position变量, 用于指定顶点坐标.
SourceColor: 传递给Fragment Shader中的DestinationColor参数, 指定顶点颜色.

Fragment Shader如下:

varying lowp vec4 DestinationColor;

void main(void) {
    // must set gl_FragColor for fragment shader
    gl_FragColor = DestinationColor;
}

将DestinationColor传递给gl_FragColor变量, 用于指定顶点的颜色.
我们即将绘制的矩形不再是纯色,而是通过shader传递颜色给每一个像素。所以SourceColor参数至关重要。

关联Shader参数

glUseProgram(_glProgram);
_positionSlot = glGetAttribLocation(_glProgram, "Position");
_colorSlot = glGetAttribLocation(_glProgram, "SourceColor");

glViewport(0, 0, self.view.frame.size.width, self.view.frame.size.height);

仅使用顶点数据绘制矩形

// 顶点数组
const GLfloat Vertices[] = {
    -1,-1,0,// 左下,黑色
    1,-1,0, // 右下,红色
    -1,1,0, // 左上,蓝色

    1,-1,0, // 右下,红色
    -1,1,0, // 左上,蓝色
    1,1,0,  // 右上,绿色
};

// 颜色数组
const GLfloat Colors[] = {
    0,0,0,1, // 左下,黑色
    1,0,0,1, // 右下,红色
    0,0,1,1, // 左上,蓝色

    1,0,0,1, // 右下,红色
    0,0,1,1, // 左上,蓝色
    0,1,0,1, // 右上,绿色
};

// 纯粹使用顶点的方式,颜色与顶点要一一对应。
// 在shader中DestinationColor为最终要传递给OpenGLES的颜色,要使用varying,即两个顶点之间颜色平滑渐变
// 若不使用varying,则完全花掉。

// 取出Vertices数组中的坐标点值,赋给_positionSlot
glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, Vertices);
glEnableVertexAttribArray(_positionSlot);

// 取出Colors数组中的每个坐标点的颜色值,赋给_colorSlot
glVertexAttribPointer(_colorSlot, 4, GL_FLOAT, GL_FALSE, 0, Colors);
glEnableVertexAttribArray(_colorSlot);

// 以上两个slot分别于着色器脚本中的Positon,SourceColor两个参数

// 绘制两个三角形,不复用顶点,因此需要6个顶点坐标。
// V0-V1-V2, V3-V4-V5

/**
 *  参数1:三角形组合方式
 *  参数2:从顶点数组的哪个offset开始
 *  参数3:顶点个数6个
 */
glDrawArrays(GL_TRIANGLES, 0, 6);

使用顶点数组绘制矩形时, Colors数组用于指定每个顶点的颜色, 与顶点数组Vertices中的顶点一一对应.
注意, 此时可留意Shader中的DestinationColor之前的varying关键字, 作用是使两个顶点之间的颜色平滑渐变.

关于三角形绘制方式, 还可以使用如下两种方式:

// 绘制两个三角形,复用两个顶点,因此只需要四个顶点坐标
// V0-V1-V2, V1-V2-V3
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

// 绘制两个三角形,复用两个顶点,因此只需要四个顶点坐标
// V0-V1-V2, V0-V2-V3
glDrawArrays(GL_TRIANGLE_FAN, 0, 4);

三种方式的绘制效果是一样的.

使用顶点索引数组

// 顶点数组
const GLfloat Vertices[] = {
    -1,-1,0,// 左下,黑色
    1,-1,0, // 右下,红色
    -1,1,0, // 左上,蓝色
    1,1,0,  // 右上,绿色
};

// 颜色数组
const GLfloat Colors[] = {
    0,0,0,1, // 左下,黑色
    1,0,0,1, // 右下,红色
    0,0,1,1, // 左上,蓝色
    0,1,0,1, // 右上,绿色
};

// 索引数组,指定好了绘制三角形的方式
// 与glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);一样。
const GLubyte Indices[] = {
    0,1,2, // 三角形0
    1,2,3  // 三角形1
};

// 取出Vertices数组中的坐标点值,赋给_positionSlot
glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, Vertices);
glEnableVertexAttribArray(_positionSlot);

// 注意,未使用VBO时,glVertexAttribPointer的最后一个参数是指向对应数组的指针。
// 取出Colors数组中的每个坐标点的颜色值,赋给_colorSlot
glVertexAttribPointer(_colorSlot, 4, GL_FLOAT, GL_FALSE, 0, Colors);
glEnableVertexAttribArray(_colorSlot);

/**
 *  参数1:三角形组合方式
 *  参数2:索引数组中的元素个数,即6个元素,才能绘制矩形
 *  参数3:索引数组中的元素类型
 *  参数4:索引数组
 */
// 注意,未使用VBO时,glDrawElements的最后一个参数是指向对应索引数组的指针。
glDrawElements(GL_TRIANGLES, sizeof(Indices)/sizeof(Indices[0]), GL_UNSIGNED_BYTE, Indices);

/**
 *  结论:
 *  不管使用哪种方式,顶点和颜色两个数组一定要一一对应。
 *  glDrawArrays:
 *  glDrawElements: 引入了索引,则很方便地实现顶点的复用。
 *
 *  在每个vertex上调用我们的vertex shader,以及每个像素调用fragment shader
 *  相比glDrawArray, 使用顶点索引数组可减少存储和绘制重复顶点的资源消耗
 */
 ```
indices是顶点索引数组, 指定绘制图形时的顶点顺序.
使用glDrawElements即可按照顶点索引数组指定的顺序进行绘制.

### 使用VBO

// 定义一个Vertex结构, 其中包含了坐标和颜色
typedef struct {
float Position[3];
float Color[4];
} Vertex;

// 顶点数组
const Vertex Vertices[] = {
{{-1,-1,0}, {0,0,0,1}},// 左下,黑色
{{1,-1,0}, {1,0,0,1}}, // 右下,红色
{{-1,1,0}, {0,0,1,1}}, // 左上,蓝色
{{1,1,0}, {0,1,0,1}}, // 右上,绿色
};

// 索引数组
const GLubyte Indices[] = {
0,1,2, // 三角形0
1,2,3 // 三角形1
};

// setup VBOs

// GL_ARRAY_BUFFER用于顶点数组
GLuint vertexBuffer;
glGenBuffers(1, &vertexBuffer);
// 绑定vertexBuffer到GL_ARRAY_BUFFER,
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
// 给VBO传递数据
glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices), Vertices, GL_STATIC_DRAW);

// GL_ELEMENT_ARRAY_BUFFER用于顶点数组对应的Indices,即索引数组
GLuint indexBuffer;
glGenBuffers(1, &indexBuffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(Indices), Indices, GL_STATIC_DRAW);

// 注意,未使用VBO时,glVertexAttribPointer的最后一个参数是指向对应数组的指针。
// 但是,当使用VBO时,glVertexAttribPointer的最后一个参数是要获取的参数在GL_ARRAY_BUFFER(每一个Vertex)的偏移量
// 取出Vertex结构体的Position,赋给_positionSlot
glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), 0);
glEnableVertexAttribArray(_positionSlot);

// 注意,未使用VBO时,glVertexAttribPointer的最后一个参数是指向对应数组的指针。
// 但是,当使用VBO时,glVertexAttribPointer的最后一个参数是要获取的参数在GL_ARRAY_BUFFER(每一个Vertex)的偏移量
// Vertex结构体,偏移3个float的位置,即是Color值
glVertexAttribPointer(_colorSlot, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), (GLvoid )(sizeof(float) 3));
glEnableVertexAttribArray(_colorSlot);

// 使用glDrawArrays也可绘制,此时仅从GL_ARRAY_BUFFER中取出顶点数据,
// 而索引数组就可以不要了,即GL_ELEMENT_ARRAY_BUFFER实际上没有用到。
// glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

// 而使用glDrawElements的方式:本身就用到了索引,即GL_ELEMENT_ARRAY_BUFFER。
// 所以,GL_ARRAY_BUFFER和GL_ELEMENT_ARRAY_BUFFER两个都需要。

/**
* 参数1:三角形组合方式
* 参数2:索引数组中的元素个数,即6个元素,才能绘制矩形
* 参数3:索引数组中的元素类型
* 参数4:索引数组在GL_ELEMENT_ARRAY_BUFFER(索引数组)中的偏移量
*/
// 注意,未使用VBO时,glDrawElements的最后一个参数是指向对应索引数组的指针。
// 但是,当使用VBO时,参数4表示索引数据在VBO(GL_ELEMENT_ARRAY_BUFFER)中的偏移量
glDrawElements(GL_TRIANGLE_STRIP, sizeof(Indices)/sizeof(Indices[0]), GL_UNSIGNED_BYTE, 0);
“`

其中,Vertices数组中包含了每个顶点的位置信息及颜色。Indices数组是绘制矩形过程中使用的顶点索引数组。
使用顶点索引数组结合glDrawElements(),可减少存储和绘制重复顶点的资源消耗。
使用VBO可大大提高顶点数据在内存中的传递效率.

结果如图:
绘制矩形

Demo

请参考: Demo

参考资料

OpenGL Tutorial for iOS: OpenGL ES 2.0
iOS — OpenGLES之着色器(shader)的编译、链接及使用
OpenGL ES渲染管线与着色器

另外,绘制该矩形的时候,用到了Vertex Buffer Object(VBO),是用来提升OpenGLES绘制效率的,不是本文的重点所在,将在下一篇博客中再做介绍。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值