顶点插值片元

版权所有,未经许可请勿转载

2015-10-22

下文只是我自己的理解,我理解有可能有错误,请自行斟酌。

总览

将渲染过程,粗略分成两部分:

  • 数据
  • 程序

所有渲染需要用到的数据,组成一个状态机。程序使用状态机的数据来渲染。

OpenGL 2.0 之前,渲染程序是预先写好,固定不变的。当需要控制输出结果,唯有通过修改数据,间接影响输出。渲染的最终输出就是一个个像素值。

举个例子,假如有一个图片处理程序,预先写好一系列滤镜,每个滤镜分配一个数值。程序可以读取一个配置文件,有个字段叫 filterType。当 filterType 值为 1 时就磨皮,值为 2 时就模糊,值为 3 时就黑白。这样用户可以通过修改配置文件,来间接影响最终图片的输出。

这种配置方式很不灵活。当需要的滤镜效果在程序中没有预先实现,也就没有相应的配置项,就没有办法生成那种滤镜效果了。

OpenGL 2.0 之前的版本也一样,就算它的配置再多,从本质上看都是不灵活,有些效果肯定也没有办法实现。OpenGL 2.0 引入 Shader,是一个质的飞跃。这点,就类似游戏开发中从配置表进化成使用脚本,或者网页中引入 JavaScript。

Shader 是跑在 GPU 上的一个小程序,程序可以由用户编写,千变万化,可以实现的效果理论上是无限的。

OpenGL 当中很多 API,其实仅仅是向 OpenGL 这个状态机传数据或者读数据。当1.x 版本没有 Shader 前,需要配置的数据比较多并且繁琐,相应的 API 也会多。

当 2.0 版本,引入 Shader,很多 API 就简化成向 Shader中传数据,传什么数据由用户自己来定义。理解之后,2.0 的 API 反而比 1.x 更简单。

流水线

将OpenGl当成一个黑盒子:

BlackBox

用户的数据输入,经过 OpenGL 这个盒子,就会输出成像素。用户输入,可以是顶点、纹理、颜色、光照、法线等等。当我们打开 OpenGL 这个盒子,它分成一个个模块。将一些细节砍掉,粗略分为:

                状态数据

输入 -> | 顶点处理 -> 裁剪、图元组装 -> 光栅化 -> 图元处理 | -> 像素

上一个模块的输出,是下一个部件的模块的输入,构成数据流。类似工厂的流水线,每一模块做一点,最后组装出整个渲染过程,所以称呼为渲染流水线。流水线,英文为pipeline,有时也翻译成管线。

每一个模块,都有可能访问一些 OpenGL 的状态数据,或者写入一些状态数据,传输到下一级。在编译器的语言解析中,也可以看到这种数据流的处理方式:

                  符号表

输入:字符流 -> 词法分析 -> 语法分析 -> 输出:语法树

当中每一模块都可能访问或者写入符号表。可以看出,语言解析的过程跟渲染流水线整体模式是很类似的。

Unix 中的管道,也是类似的方式。Unix 的管道用文本流来交换数据,这样不同的小程序可以组装起来。

现实中的程序自然没有分离的这样清楚,也有很多细节需要补充。但这种抽象简化的数据流思想,可以帮助理解。

Shader语言

OpenGL 的模块向可编程方向发展。顶点处理模块和图元处理模块,最先被抽离出来,变成可编程。OpenGL 可以看成 GPU 的一个抽象,Shader 其实就是跑在 GPU 上的一段小程序。

顶点处理模块,执行 Vertex Shader。 图元处理模块,执行 Fragment Shader。

两种 Shader 的语法总体相同,只有些细微的区别。两个 Shader 分别在不同的模块中执行,因此需要分开编译。顶点处理的输出,会最终传到图元处理模块作为输入,因此两个 Shader 需要配合起来,也就需要链接。

其实这里可以有另外一种设计思路,就是两种 Shader 写在一起,这样API 设计应该可以省略掉链接那一步。但这样就需要用些手段来指明那一段代码在顶点处理模块执行,哪一段代码在图元处理模块执行。

Fragment 这个词,这里翻译成图元,另外的文章可能翻译成面片,或者是片段。3D模型(2D可以看成是3D的特列),不管怎么复杂,也可以一直分解,最终分解到一个个三角形,或者是一个个四边形。这种最终分解出来,不可以再拆分的最基础的部分,就叫图元。OpenGL ES 去掉了四边行,只留下三角形作为最基础的图元。三角形有个优点,是三点永远在同一平面上,可以避免很多特殊情况。

三角形由一个个顶点组成,不同的三角形可能共用相同的顶点。先处理顶点,再将顶点组装起来,变成一个个图元,再处理图元。组装之后图元中每个顶点的信息都有了,之后再通过插值的方式,来处理每个图元中内部的像素点。

Vertex Shader,用来处理顶点,每个顶点执行一次。 Fragment Shader,用来处理图元,每个图元中的像素执行一次。(这里先忽略像素裁剪)

同一个渲染过程中,Vertex Shader 和 Fragment Shader 执行的次数并不是一样的。Fragment Shader会执行更多次。

Shader 针对图形学设计,基本的数据类型有。

  • 整形,int
  • 浮点,float
  • 向量,vec4,vec3
  • 矩阵,mat4
  • 纹理单元,sampler2D

Shader 语言语法来自 C 语言,并将 C 语言语法简化,裁剪掉容易混淆的语法。

Shader的修饰符

Shader 每个数据,除了需要指定数据类型,有时候还需要指定修饰符。有三个修饰符:

  • attribute
  • uniform
  • varying

网上可以找到很多资料,告诉你修饰符的作用。比如 attribute 只可以用在 Vertex shader 中,uniform 可以同时用在 Vertex 和 Fragment Shader中,varying 需要分别在 Vertex 和 Frament 中定义。

但是为什么呢?修饰符怎么用可以很容易查到,但是为什么修饰符要这样用呢?

将上面的渲染管线图再简化:

                    状态数据

输入 -> | 顶点处理 -> XXXXXX -> 图元处理 | -> 像素

Vertex Shader对应于顶点处理,Fragment Shader 对应于图元处理,这样变成

                    状态数据

输入 -> | Vertex Shader -> XXXXXX -> Fragment Shader | -> 像素

从Shader的角度来看,有三种数据:

  • Shader 内部用的数据
  • Shader 的输入数据
  • Shader 的输出数据

内部数据是自己用的,也就不用加任何修饰符。

按照管线的设计,只能向 Vertex Shader 传数据,不能直接向 Fragment Shader传数据。而Vertex Shader 可以向 Fragment Shader 传送数据。

另外用户也可以向 OpenGL 的状态机传数据,用户传的状态数据在整个渲染过程,是始终如一,不变的。Vertex Shader 和 Fragment Shader 都可以获取。

Vertex Shader 每个顶点都会被执行一次,因此传向 Vertex Shader 的数据是每个顶点都不同的,用户的输入会表现成一个数组。

经过上面的讨论,再来分析三个修饰符:

  • attribute,为每个顶点的属性数据,每个顶点都不一样。顶点属性也只能在Vertex Shader 中定义,为用户向顶点处理模块传输的数据。

  • uniform,为状态数据,整个渲染过程都是不变的。uniform 这个单词意思就为始终如一。状态数据,两种Shader 中都可以使用,但需要声明一致,不然会链接错误。

  • varying,是 Vertex Shader 向 Fragment Shader 传输的数据,相当于两个shader 之间的数据通道。要通信,两个 Shader 就需要有一致的约定,表现成语言,就变成一致的数据声明。Vertex Shader 只管向 varying 写入顶点处理后的结果。Fragment shader 只管从 varying 中读取已经是经过插值后的结果。

纹理、矩阵是整个渲染过程都不变的(一个渲染过程指调用一次glDrawXXX),通常都是作为 uniform 来传输。

从语法的设计角度来,可能将 attribute 和 varying,换成关键字 in 和 out 会更好些。现在只有 2 个Shader,假如出现 3 个Shader,就变成:

输入 -> shader1 -> shader2 -> shader3 -> 输出

中间的 shader2 中定义varying,也不知道到底是写入还是写出了。我有点好奇OpenGL 升级成有 3 个 Shader 会怎样。只要有需求,OpenGL中其它部件是有可能慢慢变成可编程,也就慢慢出现更多的 Shader。

上面一直说 OpenGL 的输出是像素,其实假如将像素看成内存数组。通常一个像素为4 个byte,这样输出就是一个个 byte,将 byte 看成别的东西,就可以利用OpenGL来计算了。OpenGL 其实也只是 GPU 的抽象,用 OpenGL 计算,也就利用了 GPU 加速计算了。

纹理跟 sampler2D 的关联

初学 OpenGL,我有很长一段时间对纹理不理解。我自然知道纹理是什么,自然也知道怎么去生成纹理,但我不理解纹理是怎么跟 Shader 中的 sampler2D 关联起来的。不理解这个,就不能很自如地去操作多个纹理。

最后我才发现这个问题也挺简单的,只是一直没有意识到纹理单元(Texture Unit)的存在。

OpenGL 所有的资源,都分配了一个数字标示符,之后用标示符来操作资源。

比如应用中有 2 个 Shader 程序(一个程序由 Vertex Shader 和 Fragment Shader 链接而成)有不同的标示符。glUseProgram 传进相应的标示符时就可以切换当前的程序。纹理也一样,每个纹理也有一个标示符,使用 glBindTexture 来切换当前的纹理。

但 Shader 程序跟纹理有点不一样。同一时刻只可以使用一个程序,但同一时刻可以使用多个纹理。

纹理单元用来处理纹理,既然可以同时使用多个纹理,就会有多个纹理单元。

这样处理纹理之前,就需要两个步骤:

  • 选择一个纹理单元,作为当前的纹理单元。对应于接口 glActiveTexture。
  • 选择一个纹理,放进当前的纹理单元中。对应于接口 glBindTexture。

纹理单元也用数字来标示。GL_TEXTURE0 表示纹理单元 0,GL_TEXTURE1 表示纹理单元 1,依次类推。

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, dogTextureId);

glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, catTextureId);

上面代码就表示纹理单元 0,关联了 catTextureId 表示的纹理。纹理单元 1,关联了 catTextureId 表示的纹理。

Shader 中的变量类型 sampler2D,并非表示纹理。而是表示纹理单元。假如 Shader 中定义了:

uniform sampler2D CC_Texture0;
uniform sampler2D CC_Texture1;

而用户代码调用:

GLuint location0 = glGetUniformLocation(programeId, "CC_Texture0");
glUniform1i(location0, 0);

GLuint location1 = glGetUniformLocation(programeId, "CC_Texture1");
glUniform1i(location1, 1);

这样 Shader 中 CC_Texture0 赋值为 0,表示 CC_Texture0 将使用纹理单元 0 中的纹理。CC_Texture1 赋值为 1,将使用纹理单元 1 中的纹理。从而

CC_Texture0 就跟 dogTextureId 的纹理关联起来了。 CC_Texture1 就跟 catTextureId 的纹理关联起来了。

当用户代码修改成

glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, dogTextureId);

这样 CC_Texture1 也就跟 dogTextureId 的纹理关联起来了。纹理单元 0 时默认的,因此当只需要处理一张纹理的时候不用显式调用 glActiveTexture。很多例子代码写成:

glBindTexture(GL_TEXTURE_2D, dogTextureId);
GLuint location0 = glGetUniformLocation(programeId, "CC_Texture0");
glUniform1i(location0, 0);

从中看不出 CC_Texture0 是怎么跟 dogTextureId 关联的。更惨的是 CC_Texture0 的值默认也是0。有些例子更是直接写成:

glBindTexture(GL_TEXTURE_2D, dogTextureId);

之后就可以使用在 Shader 中使用 CC_Texture0 了。这种简化,初学者更加看不明白了。

sampler2D 跟纹理的关联,中间多了纹理单元这个间接层。这个问题现在看来很简单,但我资质有限,当初疑惑了很久。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值