OpenGL.Shader:6-glDrawArraysInstanced+内置变量gl_VertexID绘制自跟踪的《三块广告牌》
首先可以看看效果。这种效果可以从现在超火的《一起来捉妖》就能观察到。现在我们就来学习其中shader的知识。
首先这次模型和着色器程序的代码组织上和以前稍作一些改变,便于更好的理解学习。我们先来定义那堆绿草的模型代码GreeneryMgr.hpp
#pragma once
#ifndef GREENERY_MGR_HPP
#define GREENERY_MGR_HPP
#include <GLES3/gl3.h>
#include <vector>
#include "../common/CELLMath.hpp"
#include "../common/Camera3D.hpp"
#include "../program/GreeneryShaderProgram.hpp"
class GreeneryMgr {
private:
std::vector<CELL::float3> mGrassesPos;
GLuint mTexGrasses;
GLuint vbo;
GreeneryShaderProgram _program;
public:
GreeneryMgr() {}
~GreeneryMgr() {}
void init(GLuint tex)
{
mTexGrasses = tex;
// 初始化着色器程序
_program.initialize();
// 每一棵小草堆的位置索引
for (float x = -3 ; x < 6 ; x += 3)
{
for (float z = -3 ; z < 6 ; z += 3)
{
if(x==0&&z==0) continue;// 留位置给中间的正方体
mGrassesPos.push_back(CELL::float3(x,-1,z));
}
}
// 把草堆实例的位置索引寄存到vbo
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(mGrassesPos.size()*sizeof(CELL::float3)),
&mGrassesPos.front(), GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER,0);
}
void render(Camera3D& camera)
{
CELL::matrix4 MVP = camera._matProj * camera._matView;
_program.begin();
{
glActiveTexture(GL_TEXTURE0);
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, mTexGrasses );
glUniform1i(_program._texture, 0);
glUniform3f(_program._rightDir,
static_cast<GLfloat>(camera._right.x),
static_cast<GLfloat>(camera._right.y),
static_cast<GLfloat>(camera._right.z));
glUniform3f(_program._upDir, 0,1,0);
//glUniform3f(_program._upDir,
// static_cast<GLfloat>(camera._up.x),
// static_cast<GLfloat>(camera._up.y),
// static_cast<GLfloat>(camera._up.z));
glUniformMatrix4fv(_program._mvp, 1, GL_FALSE, MVP.data());
/// 这个将vbo点数据传递给shader
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glVertexAttribPointer(_program._position, 3, GL_FLOAT, GL_FALSE, sizeof(CELL::float3), 0);
/// 启用_position顶点属性的多实例特性
glVertexAttribDivisor(_program._position, 1);
/// 绘制函数,完成绘制
glDrawArraysInstanced(GL_TRIANGLES, 0, 6, mGrassesPos.size());
/// 禁用_position顶点属性的多实例特性
glVertexAttribDivisor(_program._position, 0);
/// 解绑vbo
glBindBuffer(GL_ARRAY_BUFFER,0);
}
_program.end();
}
};
#endif // GREENERY_MGR_HPP
GreeneryMgr的初始化函数init(GLuint tex)其实只做了一件事,准备好草堆中8个实例(Instance)的位置属性点,其中我们使用float3(x,y,z)来表示一个绿草实例的关键位置点。然后创建VBO把实例的位置关键点数组存入GPU显存缓存起来(有关VBO的知识请参考这里)其次保存外部传入的渲染纹理ID。
然后进入render渲染函数,传入的是上一章介绍的Camera3D摄像头类型的引用,传入纹理ID绑定纹理,把Camera3D的_rightDir和_upDir,以及vbo绑定的8个草堆实例位置关键点,分别传入着色器程序当中。调用新的绘制函数glDrawArraysInstanced,在调用之前,要先启用_position顶点属性的多实例特性glVertexAttribDivisor(_program._position, 1);(1启用0关闭)
这两个函数工作原理是什么,我们先学习完本章的重点内容:着色器程序GreeneryShaderProgram within version 300 es;结合着色器再回头细说。 接下来看看着色器程序GreeneryShaderProgram
class GreeneryShaderProgram : public ShaderProgram
{
public:
GLint _position;
GLint _mvp ;
GLint _rightDir;
GLint _upDir ;
GLint _texture ;
public:
GreeneryShaderProgram() {}
~GreeneryShaderProgram() {}
/// 初始化函数
virtual void initialize()
{
const char * vertexShaderResourceStr =
{
"#version 320 es\n\
uniform vec3 _rightDir; \n\
uniform vec3 _upDir; \n\
uniform mat4 _mvp; \n\
in vec3 _pos; \n\
out vec2 _texcoord;\n\
void main() \n\
{\n\
const vec2 uvData[6] = vec2[6]( \n\
vec2(0.0, 0.0),\n\
vec2(0.0, 1.0),\n\
vec2(1.0, 1.0),\n\
vec2(0.0, 0.0),\n\
vec2(1.0, 1.0),\n\
vec2(1.0, 0.0) );\n\
_texcoord = uvData[gl_VertexID];\n\
float _texcoord_x = _texcoord.x;\n\
float _texcoord_y = _texcoord.y;\n\
vec3 newPs = _pos + ((_texcoord_x - 0.5)* 4.0)* _rightDir + (_texcoord_y * 4.0) * _upDir;\n\
gl_Position = _mvp * vec4(newPs.x,newPs.y,newPs.z,1);\n\
}\n"
};
const char * fragmentShaderResourceStr =
{
"#version 320 es\n\
precision mediump float;\n\
uniform sampler2D _texture;\n\
in vec2 _texcoord;\n\
//use your own output instead of gl_FragColor \n\
out vec4 fragColor;\n\
void main()\n\
{\n\
vec4 color = texture(_texture, vec2(_texcoord.x, 1.0-_texcoord.y)); \n\
if(color.a < 0.2) discard;\n\
fragColor = color;\n\
}\n"
};
programId = ShaderHelper::buildProgram(vertexShaderResourceStr, fragmentShaderResourceStr);
_position = glGetAttribLocation (programId, "_pos");
_mvp = glGetUniformLocation(programId, "_mvp");
_rightDir = glGetUniformLocation(programId, "_rightDir");
_upDir = glGetUniformLocation(programId, "_upDir");
_texture = glGetUniformLocation(programId, "_texture");
}
/**
* 使用程序
*/
virtual void begin()
{
glUseProgram(programId);
glEnableVertexAttribArray(static_cast<GLuint>(_position));
}
/**
* 使用完成
*/
virtual void end()
{
glDisableVertexAttribArray(static_cast<GLuint>(_position));
glUseProgram(0);
}
};
开始重点分析顶点着色器程序,首先先来顶点着色器
#version 320 es
uniform vec3 _rightDir;
uniform vec3 _upDir;
uniform mat4 _mvp;
in vec3 _pos;
out vec2 _texcoord;
void main()
{
const vec2 uvData[6] = vec2[6](
vec2(0.0, 0.0),
vec2(0.0, 1.0),
vec2(1.0, 1.0),
vec2(0.0, 0.0),
vec2(1.0, 1.0),
vec2(1.0, 0.0) );
_texcoord = uvData[gl_VertexID];
float _texcoord_x = _texcoord.x;
float _texcoord_y = _texcoord.y;
vec3 newPs = _pos + ((_texcoord_x - 0.5)* 4.0)* _rightDir + (_texcoord_y * 4.0) * _upDir;
gl_Position = _mvp * vec4(newPs.x,newPs.y,newPs.z,1);
}
1、 第一行代码声明当前着色程序版本为320 es。简单的一句版本声明但却耗费了近2天的时间。因为在传统的资料,版本号一般就是#version 300 普通模式 或者 是 #version 300 core核心模式,然后在我移植到手机的过程中,填写这两个版本编译的时候都会出现:Could not compile vertex shader: ERROR: Invalid #version 最后发现移动设备上的opengles对应的glsl也是带es的,所以版本也就是es模式了。也可以通过如下GLAPI检测当前ShaderLanguage的版本。
const GLubyte * glslVersionStr = glGetString(GL_SHADING_LANGUAGE_VERSION);
LOGW("GL_SHADING_LANGUAGE_VERSION = %s", glslVersionStr);
// 测试机器Nexus5X输出结果:GL_SHADING_LANGUAGE_VERSION = OpenGL ES GLSL ES 3.20
// 既然GLSL使用到320ES了,那我们创建EglCore时升级为3,即new EglCore(NULL, FLAG_TRY_GLES3);
2、在GLSL 3版本,顶点着色器输入变量attribute统一改成in,varying输出改成out,相应的片元着色器的varying就是in了。这一点很好理解。
3、同样在GLSL 3版本当中,可以在main函数里面定义数组了,语法和C是一致的。通过是对数组的使用,结合内置变量,我们可以在gpu上做很多cpu的for循环运算。譬如我们这次使用的gl_VertexID。这也是GLSL 3版本当中新增的内置变量,表示的是绘画纹理的ID。
我们回到上方的render函数,glVertexAttribDivisor启用_position顶点属性的多实例特性,然后调用glDrawArraysInstanced(GL_TRIANGLES, 0, 6, mGrassesPos.size());绘制函数,完成绘制。所传入的参数看着和之前的glDrawArrays(GL_TRIANGLES, 0, 6)没多大区别,但是我们glDrawArrays是针对一个绘制实例的,一次glDrawArrays只画一个对象;glDrawArraysInstanced是一次针对多个(参数四的个数)绘制实例,一次能画多个对象。
那么这个glDrawArraysInstanced和gl_VertexID有什么关联呢?其实不单只是glDrawArraysInstanced,glDrawArrays以及glDrawElements都有关联。在我们以前的学习当中,每绘制一张纹理,都要画2个三角形*3个顶点,之前学习都是在应用层代码准备好纹理顶点数据。这次glDrawArraysInstanced显然并没有预处理这些顶点数据,我们只是传入每个实例的位置,并每个实例触发6次顶点着色器,这6次触发的顶点着色器就是gl_VertexID的值,取值范围就是0~5;glDrawArraysInstanced(GL_TRIANGLES, 1, 6, 20); 那么就是画20个实例,每个实例对应触发6次顶点着色器,每次对应的gl_VertexID取值就是(1~6)
4、明白了render的glDrawArraysInstanced 和 对应的gl_VertexID关联之后,接下来继续看看顶点着色器代码内容。首先定义静态数组,存放6个顶点的纹理坐标。然后结合左下方示意图分析逻辑:红色点代表的是草丛的位置坐标点,黄色的就是草丛图像的四个纹理点。
float _texcoord_x = _texcoord.x;
float _texcoord_y = _texcoord.y;
vec3 newPs = _pos + ((_texcoord_x - 0.5)* 4.0)* _rightDir + (_texcoord_y * 4.0) * _upDir;
gl_Position = _mvp * vec4(newPs.x,newPs.y,newPs.z,1);
以一个红色的位置坐标点为例(-3,0,-3)纹理下方根据gl_VertexID检索uv数组,以右上角(1,1)为例,在原有的_pos(-3,0,-3)沿着Camera3D的_rightDir右方向延申,当Camera3D镜头发生改变时,顶点可以立刻跟随改变;用纹理的横坐标-0.5是因为以原_pos的位置为中间点;4.0这个数组充当于图像的size,是延申的总长度,这个数值可以在应用层定义。在纵方向上同理,沿着Camera3D的_upDir头顶方向延申4.0个长度。
经过这样的运算之后,纹理右上角的坐标点就是(-3,0,-3)+_rightDir*(2)+_upDir*(4),如果_rightDir是沿着xz平面45°,那就是(1,0,1)_upDir是竖直向上(0,1,0)那么结果就是(-3,0,-3)+(2,0,2)+(0,4,0)=(-1,4,-1);在GPU上完美的实现了动态计算顶点坐标。
5、接下来就是分析片元着色器,如下所示。注意到的是内置gl_FragColor被取消,我们需要手动设置输出变量fragColor。纹理抽样函数texture2d改名为texture。为啥纹理坐标y要取反,这个是Android系统导致的,有疑问请参考这里。根据抽样颜色点的透明通道,小于0.2的我们全部过滤不渲染,这样就能看到背后的场景了。
#version 320 es
precision mediump float;
uniform sampler2D _texture;
in vec2 _texcoord;
//use your own output instead of gl_FragColor
out vec4 fragColor;
void main()
{
vec4 color = texture(_texture, vec2(_texcoord.x, 1.0-_texcoord.y));
if(color.a < 0.2) discard;
fragColor = color;
}
最后简单的提下使用方式:
void GL3DRender::surfaceCreated(ANativeWindow *window)
{
// 加载纹理
sprintf(res_name, "%s%s", res_path, "grass.png");
texture_greenery_id = TextureHelper::createTextureFromImage(res_name);
// 初始化构造
greeneryMgr.init(texture_greenery_id);
}
void GL3DRender::renderOnDraw(double elpasedInMilliSec)
{
// 传入Camera3D摄像头进行渲染
greeneryMgr.render(mCamera3D);
mWindowSurface->swapBuffers();
}
CMakeList升级GLES2->GLES3的支持
target_link_libraries(
native-egl
EGL
# GLESv2
GLESv3
android
zip
log)
项目地址:https://github.com/MrZhaozhirong/NativeCppApp 参考GreeneryMgr.hpp & GreeneryShaderProgram.hpp