一、写在前面
你没看错,这篇文章重新发了。
因为C姐说,之前的版本没头没尾。
现在想想,确实有点突兀了。
可编程管线基础在后面的学习中至关重要,所以我觉得有必要认认真真优化一次。
昨天朋友跟我说:好汉不提当年勇,何况当初那么菜鸡,你写公众号里干啥?
我回答说:菜鸡怎么了?菜鸡就不是鸡了?
每当写这个系列文章的时候,脑海中就有一种奇特的画面。
但具体是什么,我抓不住,一种似曾相识却又恍如隔世的感觉。
我总是忍不住要把自己当初学Shader时候的窘迫分享出来。
目的是为了激励大家:没有人生来就会,但不去学,就永远都不会。
二、最简单的shader
在上一篇《Cocos Shader入门基础二:初识Cocos Shader》中我提到说,以后的Cocos Shader将在builtin-unlit.effect基础上逐步添加内容,渐进式学习。
但有不少C友反馈:
麒麟子,这就是你说的教程?只是粗浅的介绍了各个部分的含义,Google翻译也能做啊。
麒麟子,能不能讲讲基础知识啊,你这么一讲,还是啥也不懂啊。
麒麟子,照顾一下新人呗,麻烦从零开始啦。
于是我做了一个大胆的决定,删除不必要的东西,从最最最最简单的effect开始,简单到缺少任何一行代码,就跑不起来的地步。
我把它命名为cocos-shader-helloworld.effect,它将渲染出下面的效果。
它目前很简单,但随着教程的进行,它将变得越来越胡里花哨。
为了使大家更好地理解各个细节,麒麟子在shader中加了注释,请看下图:
三、可编程管线浅析
麒麟子一直在想,3D可编程管线到底应该以什么样的方式,什么样的程度讲给大家听。
看到上面的截图时我灵机一动。
我们不如就从上到下,以讲解Cocos Shader的角度,对涉及到的3D管线知识进行讲解。
3.1、定义部分
在cocos shader effect中,可以定义多个technique,每一个technique主要的属性就是name和pass。
在上面的cocos-shaer-helloworld.effect中我们定义了一个叫opaque的technique,且这个technique只有一个pass。
如果你不喜欢,可以改掉technique的名字,这不会对效果造成任何影响。
3.2、vert函数(顶点着色器)
在本案例中,顶点着色器的入口函数是 vert()。
在pass中,通过 vert:unlit-vs:vert 引用。
unlit-vs 就是CCProgram的名字,在一个effect文件中,我们可以定义多个CCProgram,每一个CCProgram中可以有多个函数,然后根据配置来决定入口函数。
在本文所示例的vert函数中,我们只做了最基本的位置信息输出:顶点坐标信息在经历了世界变换,摄像机变换,投影变换后,作为vert函数的返回值。
3.3、顶点坐标从本地到屏幕的经过
很多朋友误以为经过投影变换后的坐标就是屏幕坐标,那是不对的。
vert函数输出的坐标并不是屏幕坐标,在有些书上把这个叫 裁剪坐标。不管叫什么,大家记住它就是投影后的坐标就行。
这个投影后的坐标,为了适应不同的显示设备,会做一次规范化设备坐标系(NDC)处理。
NDC处理过后,会进行视口映射。
视口映射结束后,才是显示到窗口上。
麒麟子用拙劣的绘图能力给大家绘制了下面这张变换图。可以清晰的看到顶点坐标需要经历的变换步骤。
注:投影变换之后,坐标信息就不受Shader控制了。
3.4、光栅化
顶点着色器之后,并不会直接传递给像素着色器,而是会先把顶点着色器输出的东西进行插值、像素化。
这个过程有一个术语叫:光栅化
如下图所示,三角形经过光栅化后,变成了一个个像素。
除了顶点位置信息,顶点法线、颜色、纹理坐标等都会先经过光栅化,再传递给像素着色器。
由于所有vert输出的值都会被光栅化,所以顶点着色器传递到像素着色器的法线向量,在使用的时候,记得先normalize,否则会有意想不到的效果。
关于光栅化的内容,建议大家多在网上搜索资料看看,有一个更深入的了解。
3.5、frag函数 (像素着色器)
光栅化之后的顶点信息会被传递给像素着色器。
后期的教程中,我们为了实现一些高级效果,其实大部分情况下是对frag函数的增强。在本文的示例中,我们为了尽可能减少大家的理解成本,麒麟子连color都没有从外部传递过来,直接在代码中定义了颜色。
大家可以修改frag()函数中的颜色值来查看像素变化
这里顺便说一下,像素着色器(Pixel Shader)和片元着色器(Fragment Shader)是一个东西。前者来自于Direct3D圈,后者来自于OpenGL圈。
3.6、像素的一生
像素着色器处理之后,像素还会进行一系列的测试和操作,只有测试都通过的像素,才会被写入到目标缓冲区中。如下图所示:
3.6.1、模板测试(Stencil Test)
模板测试会根据预先设置好的模板测试参数进行工作,并决定是否要丢弃像素。
在本文里,麒麟子不打算进一步讲模板测试的细节。
如果后面的章节有用到模板测试的地方,会进行详细说明。
如果现在就想了解模板测试的朋友,请自行搜索。
3.6.2、深度测试(Depth Test)
深度测试需要深度缓冲区的配合,请先查看本文6.4小节中的深度缓冲区概念。
深度测试提供了 > >= == < <= != 总是,从不等比较运算符,默认是 <=。
当一个像素进入深度测试环节时,会进行如下操作。
3.6.3、融合 (Alpha Blend)
如果一个像素以上所有测试都通过了,则会进入融合处理阶段。
融合就是我们经常说的透明混合。
它会将当前像素的值按照我们设置好的混合方式,与目标颜色缓冲区的值进行融合。
如果没有开启Alpha Blend开关,则这阶段自动跳过。
我们常见的alpha混合因子如src_alpha、one_minus_src_alpha等就是用在这个操作上面的。
关于alpha blend参数细节和使用技巧,我们会在后面的讲解中深入解释。
3.6.4、写入帧缓冲区
最终,像素会被写入缓冲区中。帧缓冲区有三个:颜色缓冲区、深度缓冲区、模板缓冲区。
颜色缓冲区(Color Buffer):
颜色缓冲区,故明思意,存储颜色的缓冲区。这个缓冲区就是我们屏幕上能看到的缓冲区。
深度缓冲区(Depth Buffer):
是一个看不见的缓冲区。
深度缓冲区存储的是一个与顶点z值相关的值(这个z值是摄影后的z值,由于它处于摄像机空间,由近即远,所以我们称它为深度)。
这个缓冲区使我们在渲染非透明物体的时候,不用管先后顺序,也能保证结果的正确性。
大大提高了绘制效率。
眼尖的朋友就会问,透明物体怎么办呢?
透明物体是需要从远到近进行渲染的,可以搜索“油画家算法”作进一步研究。
当然,cocos引擎已经做了这个排序的事情,不必过于担忧。
如果渲染状态 深度写未开启,则不会进行深度缓冲区写入。
模板缓冲区(Stencil Buffer):
这个缓冲区可以理解为一个标记缓冲区,他提供了一些比较运算操作,用于实现一些特殊效果。
如果渲染状态开关 模板缓冲未开启,则不会进行模板缓冲区写入
模板缓冲区的背景知识,建议大家多看其他资料。
这里只是简单介绍了颜色缓冲区、深度缓冲区、模板缓冲区的基本概念,如果要展开来讲各个细节,可能够写好几篇文章了。
想要了解更多细节的朋友,请自行搜索关键字。
四、总结
根据C友们的反馈,麒麟子在本文中直接将Cocos Shader的学习拉回到了原点,真正的从零开始。
虽然本文的cocos-shader-helloworld非常简单,但随着教程的进行,它会越来越丰富。
它越是华丽,代表你Shader的学习越有进步。
本文也对3D可编程管线做了一个简要的梳理。
然而由于篇幅有限,不可能详尽地讲每一个知识点。
好在网络上已经有很多相关优质的文章。
希望大家下来后,以本文为题纲,针对文中提到的各种术语和关键字进行搜索,去拓展自己的相关知识。
编程这个事情,只要下功夫多练,收获不会差的。
借卖油翁的一句话:
我亦无它,但手熟尔!
五、预告
下一篇,我们将在cocos-shader-helloworld的基础上做如下操作
为shader添加外部变量
外部变量在Inspector面板上的控制
外部变量在代码中如何控制
敬请关注!