Cocos Shader 基础入门(一):WebGL 着色器与 GLSL 着色器语言基础
引言:
Shader(可编程着色器)是用来实现图像渲染的可编辑程序。它并不是简单的计算机应用,要想真正地掌握 Shader,需要对图形学、图形 API、GPU、显卡等都有个宏观概念。而对大多数游戏开发者来说,“快速”入门又是当下的迫切需求。因此,Cocos 布道师放空整理了一个 Cocos Shader 基础系列教程(8期入门+2期扩展),介绍 Shader 的基础知识以及在 Cocos Creator 里的应用,带一些小伙伴快速入门 Shader 编写。
在 Cocos Shader 开始之前,我会先介绍一些 Shader 基础知识,从最浅显的部分开始,揭开 Shader 的神秘面纱。本次讲解采用的渲染引擎是 WebGL,首先来了解一些渲染管线知识。
CPU 与 GPU 的区别
在介绍 WebGL 之前先来了解一些前置知识,也就是 CPU 和 GPU。
CPU 和 GPU 都属于处理单元,但是结构不同。形象来说,CPU 有点像大型的传输管道,等待处理的任务只能依次通过,所以 CPU 处理任务的速度取决于处理单个任务的时间。又由于 CPU 内部结构异常复杂,能够处理大量的数据和逻辑判断,所以处理一些大型任务是足够的。但是处理图像却不在行,因为处理图像的逻辑通常不复杂,但是由于一幅图像是有成千上万的像素点构成,每个像素的处理都是一个任务,如果由 CPU 处理,那简直就是大材小用。因此就需要用到 GPU。GPU 由大量的小型处理单元构成,处理能力没 CPU 强大,但胜在数量多,并且能够并行处理。
渲染管线
在渲染过程中需要 CPU 和 GPU 之间的通力合作。CPU 如同进货的卡车不断地将要处理的数据丢给 GPU,GPU 工厂调动一个个如工人一般的计算单元对这些数据进行简单的处理,最后组装出产品——图像。
什么是 WebGL?
WebGL 是一种 3D 绘图标准,它的本质是 JavaScript 操作 OpenGL 接口,所以 WebGL 是在 OpenGL 的基础上做了一层封装,底层本质还是 OpenGL。利用 WebGL 可以根据你的代码绘制出点、线和三角形。任何复杂的场景都可以组合使用点、线和三角形实现。WebGL 运行在 GPU 中,因此需要使用能够在 GPU 上运行的程序。这样的程序需要成对提供,每对方法中都包含一个顶点着色器和一个片断着色器,并且使用 GLSL 语言(GL 着色语言)编写。每对组合起来称作一个 program(着色程序)。
在 WebGL 中,任何事物都处于 3D 空间中,但最终输出呈现给观众的往往是屏幕或者窗口这种 2D 像素,因此,在渲染引擎底层大部分工作都是把 3D 坐标转变成适应屏幕的 2D 像素。3D 坐标转 2D 的处理过程是由 WebGL 的图形渲染管线处理,它的主要传输流程分为两步:
将 3D 坐标转换成 2D 坐标
把 2D 坐标转变成实际有颜色的像素
这两个流程又被划分为几个阶段处理,每个阶段都会把前一个阶段的输出作为输入。
如上图所见,图形渲染管线包含多个阶段,在转换顶点数据到最终像素的这个过程中,每个阶段都将处理各自的职责部分。接下来,简单介绍管线各个阶段的功能:
顶点数据:顶点数据用来为后面的顶点着色器等阶段提供处理的数据,是渲染管线的数据主要来源。送入到渲染管线的数据包括顶点坐标、纹理坐标、顶点法线和顶点颜色等顶点属性,WebGL 根据渲染指令传递对应的图元信息(常见图元包括点、线和面)。
顶点着色器:顶点着色器的主要功能是坐标转换。把一个单独的顶点作为输入,并对顶点进行从局部坐标到裁剪坐标的变换,其实就是将游戏里操作的 3D 坐标转换成另一种 3D 坐标。
图元装配:图元装配阶段将顶点着色器输出的所有顶点作为输入,根据指定的指令(点、线或面)将所有的点装配成指定图元的形状。例如:提供两个顶点时,是否要将两个顶点连接成一条线段,以及多条线段之间是否需要连接。
光栅化:如何使用顶点和装配的方式将矩形表示在屏幕上,就是光栅化的过程。遍历所有的像素为止,依次判断它们是否落入了组装的图形内,如果在图形内,则对该像素进行下一步操作(着色)。还会对非顶点的位置进行插值处理,赋予每个像素其他的信息。
片段着色器:片段着色器主要是对图形内的片元进行着色处理,这里也是高级效果产生的阶段。通常,片段着色器包含 3D 场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。
测试与混合:在所有对应颜色值确定以后,最终的对象将会被传到最后一个阶段,叫做 Alpha 测试与混合阶段。这个阶段检测片段的对应的深度,用以判断这个像素在其它物体的前面还是后面、决定是否应该丢弃。这个阶段也会检查透明度 alpha 值,并对物体进行混合。所以,即使在片段着色器中计算出了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。
可以看出,图形渲染管线非常复杂,简单总结一下:WebGL 在 GPU 上的工作基本上分为两部分,第一部分是将顶点数据转换到裁剪空间坐标, 第二部分是基于第一部分的结果绘制像素点。接下来,我们一步步实践,加深对这块的理解。
GLSL 语言基础
GLSL 是为图形计算量身定制的,它包含一些针对向量和矩阵操作的特性。一个着色器通常包含输入输出变量、uniform 和 main 函数。每个着色器的入口都是 main 函数,在 main 函数中处理所有输入变量,并将结果输出到输出变量中。
一个常见顶点/片段着色器如下:
// 顶点着色器
// 输入属性 attribute 变量类型 vec4 变量名 a_position
attribute vec4 a_position;
attribute vec2 a_uv;
attribute vec4 a_color;
// 输入输出属性
varying vec4 v_color;
varying vec2 v_uv;
// 每一个着色器都有 main 函数,这是与 WebGL 对接的接口
void main() {
// 基础赋值语句
v_color = a_color;
v_uv = a_uv;
// 内置变量 gl_Position
gl_Position = a_position;
}
// 片段着色器
// lowp 精度
varying lowp vec4 v_color;
varying highp vec2 v_uv;
uniform sampler2D mainTexture;
void main(void) {
vec4 o = texture2D(mainTexture, v_uv);
o *= v_color
gl_FragColor = o;
}
此处会列举一些变量、修饰符和常见用法,更多使用方式可以参考下方的 GLSL 详解(基础篇),里面的内容写的还是很详细的,当然,有条件的同学可以直接上 GLSL 官网文档查看。
1
变量和变量类型
常用的几种使用方式如下:
// 标量
float myFloat = 1.0;
bool myBool = bool(myFloat); // float -> bool
// 向量
vec4 myVec4 = vec4(1.0); // myVec4 = {1.0, 1.0, 1.0, 1.0}
vec2 myVec2 = vec2(1.0, 0.5); // myVec2 = {1.0, 0.5}
vec2 temp = vec2(myVec2); // temp = {1.0, 0.5}
myVec4 = vec4(myVec2, temp, 0.0);
// 向量和矩阵的计算是逐分量进行的,因此,也可以采用下面这两个个方法
vec3 myVec3a = myVec2.xyx; // 通过分量访问符 myVec3a = {1.0, 0.5, 1.0}
vec3 myVec3b = vec3(myVec2[0], myVec2[1], myVec2[0]); // 通过数组 myVec3b = {1.0, 0.5, 1.0}
// 矩阵
mat3 myMat3 = mat3(1.0, 0.0, 0.0, // 第一列
0.0, 1.0, 0.0, // 第二列
0.0, 1.0, 1.0); // 第三列
mat4 myMat4 = mat4(1.0) // myMat4 = {1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0}
// 向量、标量和矩阵的交互使用
vec3 v, u;
float f;
v = u + f;
// 等价于 v.x = u.x + f;
// v.y = u.y + f;
// v.z = u.z + f;
mat3 m;
u = v * m;
// 等价于 u.x = m[0].x * v.x + m[1].x * v.y + m[2].x * v.z;
// u.y = m[0].y * v.x + m[1].y * v.y + m[2].y * v.z;
// u.z = m[0].z * v.x + m[1].z * v.y + m[2].z * v.z;
2
限定符
2.1 存储限定符
使用方法:
attribute vec4 a_position;
varying vec4 v_color;
2.2 参数限定符
vec4 myFunc(inout float myFloat, // 输入输出参数
out vec4 myVec4, // 输出
mat4 myMat4); // 输入参数
2.3 精度限定符
使用方法:
// 预先声明
precision highp float;
precision mediump int;
// 指定变量声明
varying lowp vec4 v_color;
状态机
在 WebGL 上,大多数元素都可以用状态来描述,比如:是否启用了光照、是否启用了纹理、是否启用了混合、是否启用了深度测试等。通常 WebGL 会执行默认状态,除非我们调用相关接口改变它,比如:gl.Enablexxx 或者 gl.Disablexxx。
上下文(Context)
WebGL 需要依赖 canvas 这个载体获取对应的绘图上下文,通过绘图上下文调用相对应的绘图 API,包括上面提到的各种状态切换。每一个对象的绘制,都需要先设置一系列状态值,然后通过调用 "gl.drawArrays" 或 "gl.drawElements" 运行一个着色方法对,使得你的着色器对能够在 GPU 上运行。WebGL 渲染上下文创建如下:
// 定义一个 canvas 元素
var gl = canvas.getContext("webgl");
if(!gl){
// 你不能使用 WebGL
}
本章到此主要介绍一些 WebGL 的基础知识,包括渲染管线流程、渲染使用语言等。下一章开始介绍绘制流程,重点内容包括顶点着色器和片元着色器的作用。
内容参考:
1. WebGL 基础:
https://webglfundamentals.org/webgl/lessons/zh_cn/webgl-fundamentals.html
2. WebGL API 对照表:
https://www.khronos.org/files/webgl/webgl-reference-card-1_0.pdf
3. OpenGL 中文文档:
https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/
4. GLSL 详解:
https://colin1994.github.io/2017/11/11/OpenGLES-Lesson04/
5. 细说图形渲染管线:
https://zhuanlan.zhihu.com/p/79183044
往期精彩