OpenGL.Shader:6-glDrawArraysInstanced / 内置变量gl_VertexID

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);
    }
};

开始重点分析顶点着色器程序,首先先来顶点着色器

扫描二维码关注公众号,回复: 12021819 查看本文章

#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

猜你喜欢

转载自blog.csdn.net/a360940265a/article/details/90183347