GLSL Shader特效教程1:遮罩特效
在游戏开发中,许多特效可借助美工等工具来实现。本系列教程将聚焦于GPU的shader特效,让CPU得以“小憩”。
教程概述
作为系列的首个教程,我们将教授如何编写遮罩特效,此内容相对容易上手。在高级阶段,会深入讲解如何在shader中实现复杂的3D特效,涵盖光线追踪和体积渲染。若你对OpenGL、ES缺乏基础,可暂时搁置,只需具备C语言基础即可。
GLSL与C语言颇为相似,语法较为简单。不过,GPU的shader编程对数学和物理知识要求较高,特别是立体几何和线性代数。若你已遗忘立体几何知识,后续可进行补充,当下需熟悉平面几何中的向量、矩阵以及坐标系的变换。虽然本教程暂未涉及矩阵和坐标系变换,但倘若你确实遗忘,还是要抽出时间复习。
GPU与CPU的区别
在开始编写shader之前,我们先来了解一下GPU和CPU的区别。
CPU
CPU虽然有多核,但数量一般不多于10个,常见的有2核、4核、8核,多于16核的在普通场景中较少使用。在CPU编程里,我们可以假设“邻居”数据来完成一些复杂的算法运算。然而,在CPU的多核和多线程、多进程并行编程中,并非真正严格意义上的并行。因为一旦涉及数据共享,就会引发资源竞争。为保证数据的一致性和安全性,必须采取加锁(如读写锁、自旋锁、RCU等)或线程等待机制。这些机制会使某些进程、线程被迫等待,被内核调度器挂起(或主动放弃CPU),放入运行等待队列,等待其他线程唤醒或再次被内核调度器调度,就像茅坑被占后,后续的人都得排队,所以这是真正并行和串行的折中处理方式。
GPU
GPU采用的是真正的并行设计,拥有多个(例如512个)逻辑运算单元,但单个逻辑运算单元的计算能力不如CPU。在GPU编程过程中,不能假设数据的先后顺序,也不允许访问其他线程的数据。例如,在GLSL中就找不到类似sum的求和函数。GPU擅长向量的点积和叉积、矩阵变换、插值处理等操作。在编写shader时,应尽量避免以下情况:
- 避免让GPU进行已知固定值的运算,尤其是浮点运算,例如 float TWOPI = 3.14*2.0;应改为float TWOPI = 6.28;。
- 尽量避免使用除法,例如 float radius = 10.0/2.0;应改为float radius = 5.0;,float dist = radius / 2.0;应改为float dist = radius * 0.5;。
- 向量之间相乘时,可使用点积来替代,这是GPU的强项。
遮罩特效实现
现有方法的局限性
Cocos2d-x提供的clipnode可用于实现遮罩特效,但其基于OPENGL的模板测试,测试结果只有通过和不通过两种状态,不存在中间状态,因此实现的遮罩效果没有渐变过渡带,与现实中明暗之间的过渡情况不符。虽然可以通过clipnode + blend混合来实现渐变效果,但过程较为复杂。我们可以使用shader来实现遮罩特效,先来看一下效果图:
- 正常
- 反向50%
- 反向100%
该shader遮罩可以控制大小、位置、遮罩的颜色、遮罩渐变大小、快慢以及明暗程度。
伪代码实现
// 获取遮罩值的函数
float get_mask() {
// 计算当前纹理坐标到中心的距离
if (uv_2_centre_distance < radius) {
// 计算距离与半径的比值
float dd = uv_2_centre_distance / radius;
// 根据渐变参数和亮度参数计算遮罩值
return smoothstep(0.0, gradient, pow(dd, brightness));
}
return 0.0;
}
void main() {
// 计算纹理坐标
vec2 uv = gl_FragCoord.xy / resolution.xy;
// 获取纹理颜色
vec4 tc = texture2D();
// 获取遮罩值
float mask = get_mask();
if (!inverted) {
// 正向遮罩,计算最终颜色
gl_FragColor = vec4(tc * mask * color);
} else {
// 反向遮罩,计算最终颜色
gl_FragColor = vec4(tc * (1.0 - inverted * mask * color));
}
}
参数说明
- radius:遮罩的半径大小。
- gradient:遮罩的渐变速度。
- brightness:遮罩的明暗程度。
- color:是否改变被遮罩texture的颜色。
- inverted:是否反向,以及反向的百分比。
具体的实现细节可参考源代码。通过上述shader代码,我们可以实现一个灵活可控的遮罩特效。
