一看就懂的OpenGL ES教程——渲染渐变色及光栅化插值原理

2,634 阅读13分钟

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

系列文章目录

体系化学习系列博文,请看音视频系统学习总目录

实践项目: 介绍一个自己刚出炉的安卓音视频播放录制开源项目 欢迎各位来star~

相关专栏:

C/C++基础与进阶之路

音视频理论基础系列专栏

音视频开发实战系列专栏

一看就懂的OpenGL es教程

通过阅读本文,你将获得以下收获:

1.客户端程序使用uniform变量指定颜色值
2.客户端程序使用顶点属性数组指定图形的渲染颜色
3.OpenGL es光栅化的插值现象

天青色等烟雨,
而我在等你

月色被打捞起,
晕开了结局

OpenGL ES绘制三角形的博文已经到了第五篇了,今天也正是要揭开结局——绘制完成之前指定的目标三角形。

上篇回顾

上一篇文章 已经详细展示了OpenGL如何绘制各种的基本图元。之前定的绘制三角形任务已经完成了绘制,不过万事俱备只欠东风,上一篇里三角形用的是单一颜色,而我们在一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(一)指定的三角形终极任务是这样的:

image.png

所以我们还差给三角形上这种渐变色效果,一旦加上渐变色效果,那么这个绘制三角形的小系列就剧终了,入门OpenGL es我觉得至少算成功一半了吧。

4b92c9eb51bacdac1d9fb219756944f4.jpeg

客户端程序指定渲染颜色

之前的例子片段着色器都是将颜色写固定在代码中的:

FragColor = vec4(1.0,0.5,0.5,1.0);

这样的话客户端就无权控制绘制的颜色了,显然不符合广大人民群众的需要,从客户端传颜色到着色器中就成了必需项。

这么传呢?

使用uniform变量传单一颜色值

之前文章对GLSL的叙述中,我们已经接触过了out、in修饰的变量,那么今天要再认识一个变量修饰符了,名曰:uniform

“uniform”的英文单词有一致的,统一的意思,在这里也算是描述很精准,它表示在一次渲染中不会被改变的变量,类似Java中的final修饰符。所以相对于之前的out、in修饰的变量,它比较牛逼,它是一个渲染管线中的全局常量。一般用于储存变换矩阵、光参数和颜色等。

那么怎么理解渲染管线中的全局常量呢?

这里的全局是相对一个渲染阶段的一次着色器执行周期而言的。比如在先前文章的例子中,我们传了三个顶点坐标给着色器的vertex变量,会导致执行三次顶点着色器代码,三次执行中的vertex变量值都是不一样的,所以vertex变量就不是一个全局变量。

但是对于用uniform修饰的全局常量来说,在一次渲染中,不管执行多少次着色器代码,它都是同一个值,而且是在整个渲染管线内保持同一个值的。

之所以uniform全局常量有这种功能,是因为它是保存在着色器程序对象中的,在硬件上体现在全局数据区中

uniform全局常量代码实例

顶点着色器不用改动:

        #version 300 es
        layout (location = 0) 
        in vec4 aPosition;//输入的顶点坐标,会在程序指定layout将数据输入到该字段

        void main() {
           //直接把传入的坐标值作为传入渲染管线。gl_Position是OpenGL内置的表示坐标的变量
            gl_Position = aPosition;   
           
        }"

对于片段着色器来说,新增一个uniform变量uTextColor,类型为vec4,用来接收客户端程序传入的颜色值:

        #version 300 es
        precision mediump float;

        uniform vec4 uTextColor;//输出的颜色
        out vec4 FragColor;

        void main() {
            FragColor = uTextColor;
        };

对了,因为uniform独特的全局地位,所以客户端程序传值的时候并不用从渲染管线头部开始传,而是直接一步到位将数据丢给uniform变量,所谓可以直捣黄龙:

static float color[] = {
        //表示RGBA
        0.0f, 1.0f, 0.0f,1.0f
};

//获取到uTextColor在着色器程序中的location
int colorLocation = glGetUniformLocation(program, "uTextColor");
//通过location去传入一个color向量
glUniform4fv(colorLocation,1, color);

这里首先还是熟悉的老套路,欲得到一个变量的修改权,必先拿到它的location,location就是它的引用,这也是OpenGL惯用的套路,拿到引用后,再通过修改方法就可以给其赋值。

这里通过glGetUniformLocation方法拿到location,然后通过glUniform4fv方法来修改。

glUniform4fv是OpenGL中对uniform变量赋值全家桶的其中一个方法,最后的v表示uniform变量类型为向量vec,对于全家桶中修改向量的方法系列,其基本格式为:

glUniform + n维向量 + 向量元素数据类型 + v

(glUniform*系列方法详细可见官方文档:registry.khronos.org/OpenGL-Refp…)

着色器中的vec变量在客户端程序可以通过数组来表示,所以这里传入color数组即可,color数组就是一个rgba的颜色值

运行一下,果然是一个绿油油的三角形:

Screenshot_20220917-185012.jpg

使用顶点属性传颜色值

之前强调过,我们传给顶点着色器的叫做顶点属性数组,而不是顶点数组,因为顶点坐标是一个属性,那么我们是不是可以添加一个颜色属性呢?

3307c199e280e6026f9e72fe5484b565.jpeg

怎么添加颜色属性呢?

第一种方式是增加一个颜色的数组,这样就有两个数组:

static float triangleVer[] = {
        0.0f, 0.8f, 0.0f,//顶点
        -0.8f, -0.8f, 0.0f,//顶点
        0.8f, -0.8f, 0.0f,//顶点
};

static float colors[] = {
        1.0, 0.0, 0.0,//颜色
        0.0, 1.0, 0.0,//颜色
        0.0, 0.0, 1.0,//颜色
};

然后调用2次glVertexAttribPointer方法分别解析这2个数组:

//解析顶点坐标数据
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 12, triangleVer);
//解析颜色数据
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 12, colors);
//分别打开着色器中layout为0,1的变量开关
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);

此时顶点着色器相应地增加一个输入变量aColor来接受颜色属性,并且增加一个输出给后面阶段着色器的out变量vTextColor

        #version 300 es
        layout (location = 0) 
        in vec4 aPosition;//输入的顶点坐标,会在程序指定layout将数据输入到该字段
        
        layout (location = 1)  
        in vec4 aColor;//输入的顶点的颜色
        
        out vec4 vTextColor;//输出的颜色

        void main() {
           //直接把传入的坐标值作为传入渲染管线。gl_Position是OpenGL内置的表示坐标的变量
            gl_Position = aPosition;   
            //颜色传给下一个阶段
            vTextColor = aColor;

        }

片段着色器代码如下所示,关键是定义一个接受上个阶段输入的颜色的in修饰的上个阶段着色器同名的变量vTextColor

       #version 300 es
       precision mediump float;
       out vec4 FragColor;
       
       in vec4 vTextColor;//从上个阶段输入的颜色

        void main() {
           //输入的颜色给当前片段赋颜色值
           FragColor = vTextColor;

        }

简单来说,就是在流水线上,前面工序的工人吼一声,后面拥有vTextColor盒子(变量)的工序的人记得接住我这里的vTextColor物料(变量值)

这种方式没问题,但是每次增加一个属性就要增加一个数组,显示不符合资深程序员的身份。于是乎第二种方式更加科学一些,这个方法可以叫做见缝插针法,在每个顶点坐标中间插一个rgb的颜色值

static float triangleVerWithColor[] = {
        0.0f, 0.8f, 0.0f,//顶点
        1.0, 0.0, 0.0,//颜色
        
        -0.8f, -0.8f, 0.0f,//顶点
        0.0, 1.0, 0.0,//颜色
        
        0.8f, -0.8f, 0.0f,//顶点
        0.0, 0.0, 1.0,//颜色
};

这样子依旧是三个顶点,但是每个顶点分别有两个属性:坐标和颜色,这样子的好处是无论多少个属性,都可以用一个顶点属性数组解决

解析逻辑要怎么调整呢?

//解析顶点坐标数据
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 24, triangleVerWithColor);
//解析颜色数据
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 24, triangleVerWithColor + 3);
//分别打开着色器中layout为0,1的变量开关
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);

一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(二) 中的传递数据给着色器章节曾分析过glVertexAttribPointer方法是如何解析顶点属性数组的,那么现在多了一个颜色属性,解析顶点属性就变成:

顶点属性数组每3个元素为一个顶点坐标,从数组的第0个元素开始取,每间隔4*6(一个float4个字节,一个顶点开头到下一个顶点开头距离6个浮点数)取一次。 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 24, triangleVerWithColor);

这是原来只有坐标属性的顶点属性数组:

image.png

那么接下来取颜色属性就依葫芦画瓢即可:

顶点属性数组每3个元素为一个顶点颜色值,从数组的第4个元素(注意数组地址偏移+3)开始取,每间隔4*6(一个float4个字节,一个顶点开头到下一个顶点开头距离6个浮点数)取一次。 glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 24, triangleVerWithColor + 3);

如今添加了颜色属性,于是数组摇身一变:

image.png

客户端程序,就是要把这个结构清晰地告诉给OpenGL,而具体的表达方法,就是通过glVertexAttribPointer方法。总之,你要玩几个数组随你,但是要明确告诉OpenGL怎么解析即可~

5d9418443cd8d537baa5e718ce35719c.jpeg

一切貌似很顺利,不过聪明的你可能已经觉察到了一丝不对劲……

af7385527c183b6c0f4c92d96084867b.jpeg

按照如上做法,也不过每个顶点传了一个颜色属性,一个三角形像素那么多,你只给顶点传了颜色,其他点咋整啊?

管那么多,先运行看看效果~

image.png

这不就是一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(一) 里面提到的绘制三角形的任务的终极目标么。

55e0423de180540df69d81d639750630.gif

但是这种有点酷毙的渐变颜色是怎么形成的呢?

光栅化的插值现象

不知各位记得不,在 一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(三) 中,我曾埋过一个小小的彩蛋:

每个光栅化产生的片段,会携带位置信息,以及顶点着色器产生的数据的插值信息

这里的插值信息如何理解?

我们在看下这张经典的图形渲染管线图:

image.png

在光栅化中,光栅化器将装配好的图元切成一个个片段,但是我们在外部传入的是顶点属性,但是这些属性往往不是只有顶点才需要的,而是每个片段都需要的,那么光栅化器是如何把这些属性带给每个片段呢?答案便是:线性插值大法

线段的线性插值

让我们再回到线段的绘制,只是这次改用类似上面的方式给线段2个顶点传入不同的颜色值,比如左边红色(1.0, 0.0, 0.0),右边为蓝色(0.0, 0.0, 1.0),绘制出来的线段如下图所示:

Screenshot_20220917-214229.jpg

通过线段,能够比较明显看出颜色渐变的规律了。

左端最红,然后从左往右逐步蓝色化,直到最右最蓝。

聪明的你可能可以看出是呈现线性变化的,那么用数学语言表示即为:

假如线段上的某个点距离左端点的距离占线段长度比例为a,距离右端点的距离占线段长度比例为b,且左端点的属性为r,右端点属性为g,则该点上的属性c为:

c = r*a + g*b

在上面的例子中,对于线段中点来说,它的颜色便是:

(1.0, 0.0, 0.0)*0.5 + (0.0, 0.0, 1.0)*0.5 = (0.5,0.0,0.5)

三角形的线性插值

对于三角形来说,同理,只是它不再是看距离,而是看面积占比

假如当前点如图所示,Aa,Ab,Ac分别为该点和其他另外2个点连成的三角形面积,则比例系数分别为:

image.png

则对于该点来说,它的某个属性值为三个顶点该属性的线性插值:

image.png

按照这个公式,则我们上面绘制的三角形内部的中心点颜色值为:

(1.0, 0.0, 0.0)*0.5 + (0.0, 1.0, 0.0)*0.5 + (0.0, 0.0, 1.0)*0.5 = (0.5,0.5,0.5)

a0cd92c82eabe807a2720c58a96e4ede.jpeg

加餐:如何证明片段着色器每个片段执行一次?

这个结论其实在之前的博文已经讲过,不过如何去证明呢?

经过上文描述的线性插值,我们直到了所有顶点属性都会在光栅化阶段被线性插值处理,那么定带你坐标自然也会被处理,处理之后的结果是每个片段上的获取到的坐标就是它的中心点的坐标。所以将顶点坐标作为一个out类型变量传给片段着色器,那么片段片段着色器拿到的就是当前片段的坐标点

那么如果这里突发奇想,将坐标值作为颜色值赋值给片段颜色,那么如果显示的颜色和对应坐标的一一对应的,就能说明片段着色器是每个片段执行一次

修改顶点着色器代码如下所示:

顶点着色器:

         #version 300 es
                layout (location = 0) 
                in vec4 aPosition;//输入的顶点坐标,会在程序指定将数据输入到该字段
       
                out vec4 vTextColor;//输出的颜色

                out vec4 vPosition;//输出的坐标
        
                void main() {
                   //直接把传入的坐标值作为传入渲染管线。gl_Position是OpenGL内置的
                    gl_Position = aPosition;
                    //把坐标输出给片段着色器
                    vPosition = aPosition;
                 
               };
  #version 300 es
  precision  mediump float;

  in vec4 vTextColor;//输入的颜色
  out vec4 FragColor;//最终的片段颜色

  in vec4 vPosition;//输入的坐标,注意这里得到的是顶点的坐标插值的结果,即当前片段的坐标值

  void main() {
       //确定当前片段颜色,这里设置为传入的坐标值
       FragColor = vec4(vPosition.x ,vPosition.y ,vPosition.z,1.0);
  };

因为是作为颜色值,所以就都传入正数的坐标,为了控制变量使得观察方便,所以蓝色分量设置为0:

static float triangleVer[] = {
       0.8f, 0.0f, 0.0f,
       0.0f, 0.0f, 0.0f,
       0.0f, 0.8f, 0.0f,

};

再次敲黑板,这里最需要注意的点,就是顶点着色器传过来的vPosition变量是经过光栅化线性插值过的,所以拿到的是当前片段的坐标,而不是三个顶点的坐标(当然三个顶点所在的片段拿到的就是对应顶点的坐标)。

运行结果:

Screenshot_20220918-114020.jpg

又是一个渐变色的三角形映入眼帘。我们细细观察一下,可以看到越接近(0,0)点的颜色越接近黑色,越接近顶部的点越绿色,因为此时横坐标越接近0,纵坐标越大,所以红色分量越少,绿色分量越大。反之,越靠右的点越红

所以可以证明片段着色器每个片段执行一次!

dbf5075cc02fcc5442a492c08babfca4.jpeg

总结

本文主要讲uniform类型变量的特点和使用,以及讲解了OpenGL一个很重要的特性,即光栅化中的线性差值效应,本文终于完成了渐变色三角形的绘制,下一篇文章讲将关注新的内容:一看就懂的OpenGL ES教程——缓冲对象优化程序(一)

代码地址

opengl-es-study-demo (不断更新中)

参考

《OpenGL超级宝典》第五版
你好,三角形
Uniform (GLSL)

原创不易,如果觉得本文对自己有帮助,别忘了随手点赞和关注,这也是我创作的最大动力~