OpenGL ES教程——GLSL

OpenGL程序总是离不开glsl语言的,各种着色器都是用glsl写的,能够说,某种程度上,glsl是OpenGL的根底,内功,只需根底深沉,才或许达到至高境界。

1、GLSL简介

着色器是运用一种叫GLSL的类C语言写成的。GLSL是为图形核算量身定制的,它包括一些针对向量和矩阵操作的有用特性。

着色器的最初总是要声明版别,接着是输入和输出变量、uniform和main函数。每个着色器的入口点都是main函数,在这个函数中咱们处理所有的输入变量,并将成果输出到输出变量中。

一个典型的着色器有下面的结构:

#version version_number 
in type in_variable_name; 
in type in_variable_name; 
out type out_variable_name; 
uniform type uniform_name; 
int main() { // 处理输入并进行一些图形操作 ... 
    // 输出处理过的成果到输出变量 
    out_variable_name = weird_stuff_we_processed;
}

前几篇文章中咱们制作了三角形、图片、yuv视频,也用glsl写过一些着色器代码了。目前来看,极点着色器中一定要回来内置变量:gl_Position。而片段着色器傍边,一定要回来一个色彩值:fragColor = outColor

别的,极点着色器中,每个输入变量也叫极点特色,咱们能声明的极点特色是有限的,一般最多16个。它能够经过如下代码获取数值:

int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes); 
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;

2、GLSL数据类型

和其他编程语言一样,GLSL有数据类型能够来指定变量的种类。GLSL中包括C等其它语言大部分的默许根底数据类型:intfloatdoubleuintbool。GLSL也有两种容器类型,它们会在这个教程中运用许多,分别是向量(Vector)和矩阵(Matrix),其中矩阵咱们会在之后的教程里再评论。

向量

GLSL中的向量是一个能够包括有2、3或许4个重量的容器,重量的类型能够是前面默许根底类型的恣意一个。它们能够是下面的办法(n代表重量的数量):

类型 意义
vecn 包括n个float重量的默许向量
bvecn 包括n个bool重量的向量
ivecn 包括n个int重量的向量
uvecn 包括n个unsigned int重量的向量
dvecn 包括n个double重量的向量

大多数时分咱们运用vecn,由于float满足满足大多数要求了。

一个向量的重量能够经过vec.x这种办法获取,这里x是指这个向量的第一个重量。你能够分别运用.x.y.z.w来获取它们的第1、2、3、4个重量。GLSL也答应你对色彩运用rgba,或是对纹路坐标运用stpq拜访相同的重量。

向量这一数据类型也答应一些风趣而灵活的重量挑选办法,叫做重组(Swizzling)。重组答应这样的语法:

vec2 someVec; 
vec4 differentVec = someVec.xyxx; 
vec3 anotherVec = differentVec.zyw; 
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
vec2 vect = vec2(0.5, 0.7); 
vec4 result = vec4(vect, 0.0, 0.0); 
vec4 otherResult = vec4(result.xyz, 1.0);

3、输入与输出

GLSL界说了inout关键字来声明输入变量和输出变量。每个着色器运用这两个关键字设定输入和输出,只需一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去。但在极点和片段着色器中会有点不同。

注意,上一着色器输出变量与下一着色器输入变量匹配,数据就会传递下去,意味着极点着色器如果有数据要输出到片段着色器中,这两个变量名有必要是一样的

极点着色器的输入较为特别,现在一般用layout (location = 0)来标识变量,然后在传递数据时只需location和变量声明的一样,就能传递数据了,简化了代码写法。

片段着色器有必要要输出一个vec4的色彩值,如果没有,体系会默许把图形制作成黑色或白色。

4、Uniform

Uniform是一种从CPU中的应用向GPU中的着色器发送数据的办法,但uniform和极点特色有些不同。首要,uniform是大局的(Global)。大局意味着uniform变量有必要在每个着色器程序目标中都是独一无二的,而且它能够被着色器程序的恣意着色器在恣意阶段拜访。第二,不管你把uniform值设置成什么,uniform会一向保存它们的数据,直到它们被重置或更新。

uniform最大的特色便是可在恣意阶段拜访并修正参数。

咱们来一个小比如,来学习Uniform,运用它来完结一个三角形色彩突变

首要,极点着色器和制作三角形时一致,不赘述了。片段着色器稍有不一样:

#version 300 es
out vec4 fragColor;
uniform vec4 outColor;
void main(){
    fragColor = outColor;
}

三角形的制作咱们也很纯熟于心了,但想要让三角形色彩改动,咱们就得时时刻刻改动片段着色器的输出色彩,如上所示,咱们把色彩界说为一个uniform参数,在制作的时分指定它的色彩,然后不停地重新制作,是不是就会实现效果呢?

glBindVertexArray(mVaoId);
//获取当时时刻
time_t t = time(NULL);
//依据时刻值,调用sin办法,核算得到greenValue值
float greenValue = sin(t) / 2.0f + 0.5f;
//获取outColor色彩,然后指定此色彩
int vertexColorLocation = glGetUniformLocation(m_ProgramObj, "outColor");
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(GL_NONE);
glUseProgram(GL_NONE);

那怎么让页面不停地制作呢?GlSurfaceView,能够指定它的renderMode,不指定,默许便是会不停地重绘,即上述代码会不停地履行,所以效果就显示出来了。

  • GlSurfaceView.renderMode,能够让页面不停地重绘或许按需求重绘
  • glSwapBuffer,在一次制作中,如果有多帧页面,有必要调用此办法,不然第二帧出不来,由于GlSurfaceView运用双缓冲机制,准备好一帧则应该送入体系,让体系显示新帧

uniform关于设置一个在渲染迭代中会改动的特色是一个十分有用的工具,它也是一个在程序和着色器间数据交互的很好工具

最终,由于glsl是类c语言,它不支持办法重载,如果设置不同的参数,glUniform办法后边就会加不同的标志位。

后缀 意义
f 函数需求一个float作为它的值
i 函数需求一个int作为它的值
ui 函数需求一个unsigned int作为它的值
3f 函数需求3个float作为它的值
fv 函数需求一个float向量/数组作为它的值

每逢你计划配置一个OpenGL的选项时就能够简单地依据这些规矩挑选适合你的数据类型的重载函数。在咱们的比如里,咱们期望分别设定uniform的4个float值,所以咱们经过glUniform4f传递咱们的数据(注意,咱们也能够运用fv版别

5、两个纹路堆叠

为了习惯自己的代码结构,我用了一个新的办法来获取图片。 首要,咱们得有自己有JNIEnv,不或许需求用它的时分有必要到jni中拿,那怎样办呢?保存一个JavaVM指针,需求的时分自己取

JavaVM* s_jni_vm = nullptr;
void InitializeJniHelper(JavaVM* vm) {
    s_jni_vm = vm;
}
JNIEnv* GetJniEnv(){
    JNIEnv* env = nullptr;
    JavaVMAttachArgs args;
    args.version = JNI_VERSION_1_4;
    args.name = "pthread-test";
    args.group = NULL;
    s_jni_vm->AttachCurrentThread(&env, &args);
    s_jni_vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_4);
    return env;
}

别的,我怎样拿到图片呢,从c++端经过反射获取图片是个不错的主见:

companion object {
    val instance: NativeContext by lazy { NativeContext() }
    //有必要要加这个注解,不然会crash,反射会找不到这个办法
    @JvmStatic
    fun getAssetBitmap(path: String): Bitmap {
        val ins = App.context.assets.open(path)
        val bitmap = BitmapFactory.decodeStream(ins)
        return bitmap
    }
}

那c++中怎么反射调用java,拿到图片呢?首要咱们要先得到jclass目标,jmethod目标,这种一般只需求获取一次就能够了,即能够在初始化的时分获取:

jmethodID g_method_get_bitmap = nullptr;
jclass g_NativeContext_clazz = nullptr;
MyGlRenderContext::MyGlRenderContext(): mSample(nullptr) {
    auto env = GetJniEnv();
    g_NativeContext_clazz = static_cast<jclass>(env->NewGlobalRef(
            env->FindClass("com/ou/demo/nativecontext/NativeContext")));
    if (g_NativeContext_clazz) {
        g_method_get_bitmap = env->GetStaticMethodID(g_NativeContext_clazz, "getAssetBitmap",
                                                     "(Ljava/lang/String;)Landroid/graphics/Bitmap;");
    }
    LOGI("g_method_get_bitmap = %d,  clazz = %d", g_method_get_bitmap, g_NativeContext_clazz);
}

注意,jclass有必要运用大局引证,不然接下来运用会报错。

反射java办法获取图片并获取它的像素值:

//想要初始化指针,就要传指针的指针,不然相当于传值,得到的指针便是个空值,报SEGV_ACCERR异常
void MyGlRenderContext::getBitmap(const char* path, void** data, int &width, int &height) {
    if (g_method_get_bitmap) {
        auto env = GetJniEnv();
        auto jPath = env->NewStringUTF(path);
        auto bitmap = env->CallStaticObjectMethod(g_NativeContext_clazz, g_method_get_bitmap, jPath);
        AndroidBitmapInfo info;
        if (AndroidBitmap_getInfo(env, bitmap, &info) < 0) {
            LOGI("get bitmap info failed");
            return;
        }
        AndroidBitmap_lockPixels(env, bitmap, data);
        width = info.width;
        height = info.height;
        LOGI("bitmap width = %d, height = %d", info.width, info.height);
        AndroidBitmap_unlockPixels(env, bitmap);
    }
}

注意,这里的data参数,如果咱们用一维指针,那最终只能得到一个空值,由于这里是要初始化此指针,那就肯定要用二维指针,即指针的指针。

图片像素获取结束之后,就简单了,首要界说片段着色器,肯定要界说两个2D纹路:

#version 300 es
precision mediump float;
in vec2 v_texCoord;
out vec4 outColor;
uniform sampler2D lyfId;
uniform sampler2D nsId;
void main() {
    //如果第三个值是0.0,它会回来第一个输入;
    //如果是1.0,会回来第二个输入值。
    //0.2会回来80%的第一个输入色彩和20%的第二个输入色彩,即回来两个纹路的混合色。
    outColor = mix(texture(lyfId, v_texCoord), texture(nsId, v_texCoord), 0.2);
}

然后,初始化纹路,指定纹路的特色,给片段着色器里的纹路变量赋值。

void TwoTextureSample::prepareTexture() {
    void* lyfPixel;
    int lyfWidth, lyfHeiht;
    MyGlRenderContext::getInstance()->getBitmap("res/lyf.png", &lyfPixel, lyfWidth, lyfHeiht);
    LOGI("lyfWidth = %d, lyfHeiht = %d", lyfWidth, lyfHeiht);
    void* nsPixel;
    int nsWidth, nsHeiht;
    MyGlRenderContext::getInstance()->getBitmap("res/ns.png", &nsPixel, nsWidth, nsHeiht);
    LOGI("nsWidth = %d, nsHeiht = %d", nsWidth, nsHeiht);
    glActiveTexture(GL_TEXTURE0);
    glGenTextures(1, &mLyfTextureId);
    glBindTexture(GL_TEXTURE_2D, mLyfTextureId);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    LOGI("run this line lyfPixel = %p", lyfPixel);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, lyfWidth, lyfHeiht,
                 0, GL_RGBA, GL_UNSIGNED_BYTE, lyfPixel);
    glBindTexture(GL_TEXTURE_2D, GL_NONE);
    glActiveTexture(GL_TEXTURE1);
    glGenTextures(1, &mNsTextureId);
    glBindTexture(GL_TEXTURE_2D, mNsTextureId);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, nsWidth, nsHeiht,
                 0, GL_RGBA, GL_UNSIGNED_BYTE, nsPixel);
    glBindTexture(GL_TEXTURE_2D, GL_NONE);
}

最终制作:

void TwoTextureSample::draw() {
    if (m_ProgramObj == GL_NONE) {
        return;
    }
    glClearColor(1.0f, 1.0f, 1.0f, 1);
    glClear(GL_COLOR_BUFFER_BIT);
    glUseProgram(m_ProgramObj);
    glBindVertexArray(mVaoId);
    //指定片段着色器中名为lyfId的变量,运用序号为0的纹路
    GLUtils::setUniformValue1i(m_ProgramObj, "lyfId", 0);
    //指定片段着色器中名为nsId的变量,运用序号为1的纹路
    GLUtils::setUniformValue1i(m_ProgramObj, "nsId", 1);
    //激活编号为0的纹路,此纹路运用mLyfTextureId的数据。
    //要把纹路编号指定运用什么数据,也要指定片段着色器中的变量是运用哪个编号的纹路,一个程序中或许有许多个纹路
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, mLyfTextureId);
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, mNsTextureId);
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, (const void*)0);
}

效果如下:

OpenGL ES教程——GLSL

6、问题记录

  • 读图片数据时,要传二维指针,不然是传值
  • 读vao时,不要在vao里去绑定空值(即处理vbo结束时,绑定个空的vbo),这样会导致vao中记录空值,之前的事白干了,什么东西也制作不出来
  • 制作时,一定要重新激活纹路编号,设置纹路编号对应的资源,然后把片段着色器上的变量指定为某个具体纹路编号
  • 关于JniEnv相关的处理还需求考虑下,优化下,别的大局引证也要重新处理下,需求让它主动回收