在游戏中有许多特效可以通过美工等工具来实现。在这个系列教程里,我们讲一讲GPU的shader特效,让你的CPU打个盹吧。
作为该系列的第一个教程,教大家如何让写一个遮罩特效。这样比较容易上手。在高级阶段教大家如何在shader中实现复杂的3D特效,包括光线追踪和体积渲染。如果你对opengl、es没有基础,可以先放开不理会。你只需要了解C语言。
GLSL和C语音很相似,语法也比较简单。GPU的shader编程对数学和物理的要求比较高,特别是立体几何和线性代数。如果你已经把立体几何忘光了,后面可以补,现在要熟悉平面几何里的向量和矩阵、坐标系的变换。
作为第一个教程,还没涉及矩阵和坐标系变换。但如何你真的忘记了,要花点时间来复习下了,时间挤挤都会有的。
在开始之前先普及下GPU和CPU的区别:
两者的主要区别在于GPU是完全并行的,具有多个(例如512)逻辑运算单元,但计算能力不如CPU的逻辑运算单元;CPU虽然有多核的,但一般不多于10 。一般都是 2、4、8核,多于16核的一般人都用不到。在CPU的编程中你可以假设“邻居”数据而完成一些复杂的算法运算。但在GPU中数据是完全并行的,你不能假设其他数据,这也是为什么在GPU中你永远看不到“求和”函数的存在,例如在GLSL中,你找不到类似sum的求和函数。
在CPU的多核和多线程、多进程的并行编程中,不是真正严格意义上的并行。因为只要涉及到数据的共享就会导致资源的竞争,为了数据的一致性和安全,就必须采取加锁(读写锁、自旋锁、RCU等)或者线程等待,这些机制都将会导致某些进程、线程被迫等待而被内核调度器挂起(或者主动放弃CPU),放在运行等待队列中,等待其他线程的唤起或者再次被内核调度器调度。好比,茅坑被占了,即使后面有再多的人(线程或者进程)都必须要排队。因此,这就不是真正的并行了。可以说这是真正并行和串行产生的一个折中的处理机制。
GPU的设计是真正的并行设计。因此在编程的过程中你不能假设数据的先后顺序,也不允许访问其他线程的数据。例如sum求和就是一个最明显的例子。GPU擅长向量的点积和叉积、矩阵变换、插值处理等。在编写shader的时候,尽量避免使用:
float TWOPI = 3.14*2.0;//应该使用float TWOPI = 6.28;不要让GPU帮你这种已经知道固定值是多少的运算,尤其是浮点运算;
float radius = 10.0/2.0; //应该使用 float radius = 5.0;
float dist = radius / 2.0; //应该使用float dist = radius * 0.5; 尽量避免使用除法;
向量之间相乘,可以使用点积来代替,这是GPU的强项。
好的,开始我们的第一个shader特效教程-遮罩特效。
Cocos2d-x提供的clipnode可以用来实现遮罩特效。但是clipnode是使用OPENGL的模板测试来实现的。因此测试的结果只存在2种,即通过和不通过。不存在中间状态。这就导致实现出来的遮罩效果不存在渐变的过度带。这和我们现实中看到明暗的不一致,因为明暗之间必定有过度带。好比晚上看到的影子,有明到暗的渐变过程。当然你可以使用clipnode+blend混合来达到渐变的效果。但这样显得过于复杂了。我们可以使用shader来实现,先看效果图:
正常
反向50%
反向100%
该shader遮罩可以控制:大小、位置、遮罩的颜色、遮罩渐变大小、快慢、明暗程度。
下面是伪代码:
get_mask()
{
if (uv_2_centre_distance < radius) {
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:是否反向,反向的百分比是多少。
具体实现见源代码。
红色遮罩