AndroidAndroid开发经验谈程序员

Android OpenGL ES从白痴到入门(五):妖艳的着色

2017-09-08  本文已影响2016人  云华兄

注:小菜鸡谈技术,不要全信,否则后果自负!

这部分主要是了解一下概念,认识着色器是什么东西,着色器程序是如何运行的,它的输入输出是什么,还有就是基本的语法掌握一下,具体细节可以一知半解。看一遍不懂那就再看一遍,还不懂就看下一节,有需要的时候再回头来看就行。

硬件

首先我们先看一下我们电脑中CPU和显卡(GPU)的关系:

老式电脑一台

这是一台远古时代的电脑配置(酷睿2+8800GTX 512m显存+2G内存),但结构大体是一致的。OpenGL主程序在CPU上执行,主程序(CPU)向显存输入顶点等数据,启动渲染,着色器程序开始在GPU上运行。
早期GPU使用固定管线(pipeline)就像CPU中的加法器、乘法器、浮点运算单元一般,每个模块固定处理特定事情,就像工厂的流水线一般,所以才叫管线吧。与CPU不同,GPU更多的是要求并行执行(百万级的线程),因为同一个基元(图形)的每个像素点都需要做同样的处理(输入不同),所以高并发是GPU的基本特点了。
随着技术的发展,固定管线逐渐被可编程管线取代,也就是着色器,着色器处理事情根据实际需要进行编程可大大提高处理单元的利用率(如果是固定管线,那么不做某些操作那部分处理单元就被闲置了)。

图形绘制过程

图形处理的一般流程

绘制图形一般是我们拥有一些图形的顶点,放置到坐标系中,使用绘制函数进行绘制(顶点着色器),分块填充颜色(片元着色器)。这样我们的图形就显示到屏幕上了。

可编程管线

固定管线部分仅作为理解用,现在已经很少用了(OpenGL4.0已经不支持固定管线),我们来看一下OpenGL ES 2.0的可编程管线:

OpenGL ES 2.0的可编程管线

简单讲,渲染流程如下:顶点数据(Vertices) > 顶点着色器(Vertex Shader) > 图元装配(Assembly) > 几何着色器(Geometry Shader) > 光栅化(Rasterization) > 片元着色器(Fragment Shader) > 逐片元处理(Per-Fragment Operations) > 帧缓冲(FrameBuffer)。再经过双缓冲的交换(SwapBuffer),渲染内容就显示到了屏幕上。
图元装配:往土讲就是把图形放置到坐标系中。
光栅化:将图形投影到屏幕上,把图形栅格化成一个个的像素点。一个像素点也就是一个片元。
逐片元处理:填充颜色,在渲染的时候是基于像素分片处理的,会涉及到纹理和光影处理等
在这里我们只要关注顶点着色器(绘图)和片元着色器(上色),其它部分可以暂不关心。

着色器

我们知道,着色器的代码是运行在GPU上面的,和我们一般运行在CPU上的程序肯定会有些不同(毕竟硬件架构不一样),GPU更注重不同的输入数据执行相同的运算。

着色器的输入输出

通过之前的Demo代码我们可以知道着色器是分别编译后链接成一个program的,那么对于CPU这边两个着色器是一体的,输入有Attribute、Unifroms和采样器(纹理/Textures),输出就是Framebuff,但这里我们关注一下gl_Position(位置)和gl_fragColor(颜色),因为这两个是两个着色器的主要输出(在什么位置显示什么颜色)。

Attribute:顶点属性,也就是存储顶点的坐标信息(对于着色器只读)
Unifroms:常量,一般存放矩阵变换或者光照之类(对于着色器只读)
Varying:顶点着色器和片元着色器间的通信接口变量,在顶点着色器中写入值(如颜色、纹理坐标等)在片元着色器中读出(对于片元着色器只读)
gl_Position、gl_PointSize、gl_FrontFacing、gl_FragColor、gl_FragCoord、gl_FrontFacing和gl_PointCoord:这些都是着色器内置的特殊变量,编译器已经定义好的,用到的时候再详细说明。

这里再强调一下,着色器代码处理的是一个片元,也就是一个像素点,千千万万个点通过同一个着色器处理后组成的才是我们的图像,而不是着色器代码执行一遍图像就出来了。

OpenGL ES着色器语言

接下来来点枯燥的东西,该来的跑不了。
基于CPU的编程语音我们有很多,如汇编、VB、C/C++、Java等等,除了汇编以外在高级语言中都有很大的共性,着色器语言也不例外,只是引入了向量和矩阵的概念以及一些细节上的改变。同样有变量、常量、运算、函数、预处理等。
数据类型

类别 类型 描述
标量 float, int, bool 标量数据类型浮点数、整形数、布尔值
浮点向量 float, vec2, vec3, vec4 浮点型矢量,1、2、3、4 维
整型向量 int, ivec2, ivec3, ivec4 整型矢量,1、2、3、4 维
布尔型向量 int, ivec2, ivec3, ivec4 布尔型矢量,1、2、3、4 维
矩阵 mat2, mat3, mat4 浮点类型矩阵 2×2,3×3,4×4

结构体
像C 语言一样,可以集合几种变量成为结构体。定义一个结构体类型后一个同名的构造函数也被定义,可以使用构造函数进行初始化,参数是每个成员变量。访问结构体中的元素和 C 语言相同。

// 定义结构体类型 FogStruct,和变量fogVar
struct FogStruct { 
      vec4 color;  
      float start;  
      float end;  
}fogVar;
// 初始化
fogVar = FogStruct(vec4(0.0, 1.0, 0.0, 0.0),        // color  
                   0.5,                             // start  
                   2.0);                            // end

数组
语法和C语言非常类似,索引以0开始,但索引只能是常量表达式不能是变量(可以是uniform变量,硬件限制)。不支持创建时初始化,所以也不支持const修饰。数组元素需要一对一的初始化。
变量的定义与赋值
矢量元素可以通过“.”运算符或者数组下标(vec3[1])获取。使用“.”运算符的可以选择{x, y, z, w}(顶点坐标),{r, g, b, a}(颜色),或{s, t, r, q}(贴图坐标)中的任意一组命名表表示。使用“.”运算符时可以重新排列一个向量。

float   myFloat;          // 定义一个浮点标量
bool    myBool = true;    // 定义时进行初始化
int     myInt = 0; 
vec3    myVec3;           // 定义一个三维矢量
vec4    myVec4;           // 定义一个四维矢量
mat4    myMat4;           // 定义一个4x4矩阵
float   myArray[4];       // 定义一个数组

myFloat = float(myBool);  // 使用构造函数构造一个浮点数值
myFloat = float(myInt);   // 使用构造函数构造一个浮点数值
myBool = bool(myInt);     // 使用构造函数构造一个布尔值

// 矢量赋值,矢量赋值是相当灵活的,如果只传一个标量,则赋值给矢量的所有成员
// 如果传多个标量或矢量,那从左到右依次赋值,多余部分被截断
myVec3 = vec3(1.0);          // myVec3 = {1.0, 1.0, 1.0}  
myVec3 = vec3(0.0,1.0,2.0);  // myVec3 = {0.0, 1.0, 2.0}
vec3 temp = vec3(myVec3);    // temp = myVec3 
vec2 myVec2 = vec2(myVec3);  // myVec2 = {myVec3.x, myVec3.y}  
myVec4 = vec4(myVec2, temp); // myVec4 = {myVec2.x, myVec2.y,temp.x, temp.y}
// 使用“.”运算符
temp = myVec3.xyz;            // temp = {0.0, 1.0, 2.0}
temp = myVec3.zyx;            // temp = {2.0, 1.0, 0.0}
temp = myVec3.xxx;            // temp = {0.0, 0.0, 0.0}  

// 矩阵赋值
mat3 myMat3 = mat3(1.0,2.0,3.0,  //  第一列 
                   4.0,5.0,6.0,  //  第二列 
                   7.0,8.0,9.0); //  第三列
myMat3 = mat3(1.0);              // myMat3 = {1.0,0.0,0.0,
                                 //           0.0,1.0,0.0,
                                 //           0.0,0.0,1.0}
myMat3 = mat3(myVec3,myVec3,myVec3); 
myMat3 = mat3(myVec2,1.0
              myVec2,2.0,
              myVec2,3.0);
// 从矩阵取值
myVec3 = myMat3[0];
myFloat = myMat3[1][1];
myFloat = myMat3[2].z;

变量修饰符
和C语音类似,变量有修饰符,用来表示变量的类别,如attribute、uniform、varying和const。(invariant?)

// 顶点着色器
uniform mat4 viewMatrix;    // 从CPU传进来的常量属性,一般放矩阵(矩阵变换-平移、旋转、缩放...) 、光照参数或者颜色
attribute vec4 a_position;  // 顶点属性(位置),顶点着色器特有的(位置、法线、贴图坐标和颜色数据)
attribute vec2 a_texCoord0; // 顶点属性(贴图坐标)
varying vec2 v_texCoord;    // 变体(随便翻译的),顶点着色器和片元着色器公有,用于着色器间交互(如传递纹理坐标)
const float myFloat;        // 常量,和C语音一样,固定一个值不可变

void main(void) { 
      gl_Position = u_matViewProjection * a_position; 
      v_texCoord = a_texCoord0;  
} 

// 片元着色器
precision mediump float;       // 此句是声明float的默认精度是中等(片元着色器没默认值,所以需要声明,顶点着色器默认中等)
varying vec2 v_texCoord;       // 和顶点着色器做同样的声明
uniform sampler2D s_baseMap;   // 2D贴图采样器,基础贴图(暂不关心)
uniform sampler2D s_lightMap;  // 2D贴图采样器,光照贴图(暂不关心)                                              
void main(){                                                                                                       
      vec4 base Color;                                                                       
      vec4 lightColor;                                                                     
                                                                                                         
      baseColor = texture2D(s_baseMap, v_texCoord);       
      lightColor = texture2D(s_lightMap, v_texCoord);   
      gl_FragColor = baseColor * (lightColor + 0.25);       
}

运算符

优先级 运算符 操作对象 描述
1 () - 对操作进行聚组
2 [] 数组 数组下标
3 f() 函数 函数调用和构造器
4 . 结构 结构字段或方法访问
5 ++ -- int float vec* mat* 后缀的自增或自减
6 ++ -- int float vec* mat* 前缀的自增或自减
7 + - ! int float vec* mat* 正、负、求反
8 * / int float vec* mat* 乘除操作
9 + - int float vec* mat* 加减操作
10 <> <= >= int float vec* mat* 关系操作
11 == != int float vec* mat* 相等测试做操
12 && bool 逻辑与操作
13 ^^ bool 逻辑异或操作
14 II bool 逻辑或操作
15 a?b:c bool int float vec* mat* 条件操作
16 = int、float、vec,mat 赋值
17 += -= *=/= int、float、vec,mat 算数赋值
18 , - 操作序列

至于向量的运算这个有点复杂后面再补上
if语句
着色器支持if语句,条件表达式的结果必须是布尔值。

if(条件表达式){
    [语句组]
}else{
    [语句组]
}

循环语句
理论上着色器是不支持循环的,这里的循环在编译的时候会被展开,所以会有很多限制。
使用循环时应该非常小心,基本的限制有:必须有循环迭代变量,它的值必须是增加或减小使用简单的表达式(i++, i -- , i+=constant, i-=constant),停止条件必须匹配循环索引,并且是常量表达式。在循环内部不能改变迭代的值。

// 错误范例
float myArr[4]; 
for(int i = 0; i < 3; i++) { 
      sum += myArr[i]; // 变量不能作为数组的下标
}

uniform int loopIter; 
// loopIter必须是常量(uniform变量也不行)
for(int i = 0; i < loopIter; i++) { 
      sum += i;   
} 

函数
和C语言一样,函数使用前必须定义。在参数使用上,提供变量限定词,指示变量是否能够被函数修改。函数不能递归(硬件限制)。
支持内联函数

限定词 描述
In(默认值) 指示参数是传值使用,不能被函数修改
Out 说明变量值不是函数输入,它的值在函数返回时被修改。
Inout 说明变量是通过引用使用,它的值可以被修改,函数引用后值将改变。
vec4 myFunc(inout float myFloat, out vec4 myVec4, mat4 myMat4){
    ...
}

预处理(宏)

预处理 描述
#define #undef #if #ifdef #ifndef #else #elif #endif 和标准 C++ 预处理一样,只是着色器的宏不能带参数定义( 如:#defne fun(x,y) (x+y) )
#error 着色器编译时将产生的编译错误记录到日志中
#version 设置着色器语音版本,如 #version 100表示V1.00
#extension 扩展列表:要求 因为GPU存在着非标准支持,如GL_OES_texture_3D,这里就可以声明是否需要支持某些扩展

扩展(我也很蒙逼)

扩展要求 描述
Require 需要全部扩展,如果有扩展不被支持预编译将产生一个错误
Enable 需要部分扩展,部分不支持产生警告,全部不支持产生错误
Warn 使用扩展功能时产生警告
Disable 禁用扩展,使用扩展时产生错误

例如, 3D 贴图扩展不被支持,你想让预编译产生警告(如果被支持,着色器将正常进行)。你在你的着色器顶上加上:
#extension GL_OES_texture_3D : enable
不变性
本菜鸟觉得作为白痴教程不需要涉及这么复杂的问题(旁白:不懂还要装),来一段译文表示敬意:
在OpenGL ES着色器编程语言里,in variant 是被用于任何顶点着色器的变量输出的关键字。它意味着什么和为什么需要他呢。着色器被编译时可能进行优化,一些指令被重新整理。指令重新整理意味着两个着色器之间平等的计算不保证产生相同的结果。这是个问题在多通道着色器特殊情况下,依稀物体使用透明混合来绘制时。如果精度被使用来计算输出位置是不准确一样,精度的不同将导致 artifacts。这在Z fighting情况下经常发生。每个像素很小的 Z 精度不同引起了不同的反光。

顶点属性

顶点是OpenGL绘图的基础,比如说你要画一个三角形,那么你首先就需要三个顶点然后依次连接起来最后填充上颜色。那么一个顶点需要有什么样的属性呢?
顶点一般有位置(position)、法线(normal)、颜色(color)和纹理坐标(texture coordinates),但这些并非是必须的。顶点属性存储方式有两种:1)结构数组,指把每个顶点的属性放在一起,多个顶点组成数组;2)数组结构,指把每个顶点的同一属性抽出来组成数组,多个属性就有多个数组。这两种方式据说是第一种效率比较高,个人感觉是这种结构比较容易看懂调试,我们下面就来看看这种结构:

结构数组

图中一个顶点包含一个位置坐标、一个法线向量和两个纹理坐标,也许你会问,为什么是两个贴图坐标?如果你做过3D游戏或者玩过游戏引擎你应该知道,一个模型建完后需要加贴图(也就是纹理,或者叫材质),除了基本的色彩贴图一般还有高光和漫反射多张贴图(这里写两个更多的是因为在OpenGL ES 1.1固定管线最多支持两个纹理坐标转换)。
贴图(纹理)不仅仅是颜色,可以说是一个存储数据的二维矩阵,贴图坐标(s、t)就是用来取这个矩阵中的值,这个值怎么用就看你着色器怎么写了。换句话说这里的坐标就是拿来和位置坐标对齐用的。毕竟一张图贴墙上你可以选正着贴倒着贴斜着贴是吧,这里也一样。
位置坐标相信我是不用多说了,那么法线是什么鬼?我们知道法线是垂直于一个面的,那一个点怎么会有法线?首先问一个问题,如果你要画一个弧面怎么画?做无限切割?如果你这么想那就too young too simple了(你真以为你传多少个顶点着色器就运行多少次?着色器运行的次数远比你想象的多),在光栅化阶段GPU会进行插补,在顶点之间分割成一小片一小片,然后每一片过一边片元着色器,此时输入的坐标就是插补得来的。换个方向想,一个面趋近于无穷小那是不是就是一个点?懂了吗,下面来看一下法线如何来插补:

上图是侧着看的,可以看到有三个顶点、三条法线和两个面(侧看为两条线),可以看出三条法线都不垂直于顶点构成的两个面,而是垂直于红色线条的曲面,红色线条则为实际插补后实际显示的曲面。

坐标系统

在本节结束前再重申一下坐标系统,因为这个对概念的理解很重要。
在OpenGL中有三种重要的坐标,即顶点的位置坐标、纹理坐标和屏幕坐标。
位置坐标标识了图元(绘制的物体)放置在哪里;纹理坐标是图元上如何贴图,比如说我们一张方形桌子(图元)上面要盖一张方形的台布(纹理),那么你是不是要把桌面四个角对应到台布的四个角,其中桌面四个角的坐标就是位置坐标而台布的四个角就是纹理坐标,四个角都对上了那台布就盖上去了,我们的贴图也就贴到了物体上面了;屏幕坐标就是这个物体映射到屏幕上的位置,因为屏幕显示是二维的,你可以理解成将桌子拍扁在底板上的样子。
这三个坐标系统的定义有些不同,位置坐标是中间为(0,0,0),往X(右)Y(上)Z(外)三个方向延伸,最大为1最小为-1的浮点坐标;纹理坐标为左下角为坐标原点(0,0),沿X(右)Y(上)方向延伸,最大为1的浮点坐标;屏幕坐标则为左上角为坐标原点(0,0),沿X(右)Y(下)方向延伸,最大无限制(看具体视口设置的大小,最大视口应该在10万像素以下,太大似乎显示异常)。

上图为一个贴图贴在一个二分之一坐标系大小的正方形上面在屏幕的显示效果(暂忽略坐标转置,相机之类的因素)。

结束

本节主要讲了着色器的概念,为了后面着色器编程打基础,如果不是很理解可以先不深究,只要有个大体的概念就行,但我认为学一个新知识时概念是非常重要的所以此节虽然没有干货但是是很有必要的。下一节我们将讲如何实际的写一个顶点着色器。
更新时间未知,如果觉得对你有用,那就敬请期待吧!

上一篇下一篇

猜你喜欢

热点阅读