前言
这几天下来总算是整理到Shader了,先说结论,Godot的Shader非常非常好用。理由:
1.集成好;2.易编写;3.见效快(均是相对于Unity)。
基础探索
一定要夸的是,Godot引擎真的非常轻便,甚至是Shader这种东西都能很轻易的实现集成,而且还集成得非常好。
不像那个逆天UnityShader,找个智能提示都费劲!Godot里直接就集成了,虽然不算很智能。但是写起来确实方便,不用打开其他编辑器的感觉确实爽,我开始有学习GDScript的冲动了,因为我已经懒到连VScode都懒得打开。
在提一嘴,如果用Godot的蓝色主题用得不爽的话,可以在编辑器设置里调整,我现在就是跟着Windows系统的配色方案进行调整的,非常美观,感觉像是换成了一个很高级的引擎,真是“人靠衣著”啊。
Godot已经将所有所有关于Shader的东西放在手册里了,什么内置函数和变量,注意事项等等,所以真的不懂就去看手册绝对够用了,当然可能是因为我已经有事先学习过Vulkan和UnityShader的经验了,所以才觉得简单。
倘若是完全新手的话,应该先从最基础的学起,Godot的手册只负责告诉我们有什么,而非怎么做。
这里说几个我目前比较常用的点。
常用技巧(也许)
shader类型
着色器类型决定了你可以使用的内置函数,变量,渲染模式等,每个模式有染但不完全相同。所以写shader前要先确定好这玩意是用来干什么的,比如像我这种2D人用的是canva_item类型的shader,那么我翻阅手册的时候就要按照它来查东西。
不同的着色器阶段(shader state/stage)也有自己的内置变量等等,所以Godot把整个着色器开发封装得很好。
uniform 统一
这个也是元老级的关键字了,应该也是从GLSL继承而来,还记得在Vulkan中就是通过它们传递CPU数据的,换句话说,在Godot里也一样,这是为Shader建立与内存数据沟通的桥梁。它跟另一个类型varying不同,varying用于着色器内部的顶点到片元阶段之间的数据传递,而uniform类型适用于整个shader甚至shader之外。
对于Godot编辑器而言,它等价于shader们的“Export”标签,通过它可以快速实现效果测试与修改。
对于uniform还应当注意到它的hints,用来指示该成员的具体用法,有些hint呢用于方便inspector调整,还有些呢很关键,比如filter,repeat,screen_texture,depth_texture等等那些hint,可以影响到成员的实际表达。
比如想要循环噪声,应当设置为repeat enable,这样在进行采样的时候才可以实现无限循环。
render_mode 渲染模式
这些个渲染模式都是Godot渲染内置的,虽然不一定要指定,因为GodotShader编辑器为我们自动配置好了很多东西。但是有一些非常好玩,所以我想记录一下:
unshaded 无阴影
这个翻译不好,因为无阴影同时也意味着“无光照”,所以可以理解为“无光影”可能好点。开了这个模式可以忽略光照的影响,将albedo完全反馈到我们眼前(albedo指反射率,其实就是漫反射的颜色)。
light_only 仅光照
这个是我觉得最好玩的一个,也是潜力最大的一个。这个模式下只有在接收到光照的条件下才开始绘制物体,所以想想就能实现很多效果,感觉真的潜力无限。
TIME 内置时间变量
这是专门用于写时间相关的shader的,单位是秒,默认3600秒一次循环,可以在项目设置里面调整。通过这个可以编写很多好玩的shader哈,比如通过波函数实现水体等等都是人尽皆知的了。
TIME属于全局内置,所以可以在顶点和片元等等不同阶段使用,每个阶段都有属于自己的内置,比如VERTEX,COLOR那些常见的,最好自己按需看表,因地制宜。
优点展现
相对于Unity,Godot的Shader简直可以说是简单了,UnityShader不仅结构复杂,写起来也复杂,不过因为更加贴近于GLSL原生,所以“可能”上限会更高。不过既然都选择了Godot,肯定是相信自己的手艺能突破引擎限制吧(doge)。
总之GodotShader跟UnityShader之间的差异简直就是它们两者本体之间的差异的缩影。
实践环节
简单描边
以下shader实现了一个简单的描边outline效果(2D):
首先是开头指定一下shader类型,这是个用于2D的shader,所以canvas_item。
然后uniform两个变量,一个作描边颜色,一个作描边大小。
然后在片元着色器中对源纹理(TEXTURE)进行上下左右的偏移采样。什么意思呢,拿color_up为例,UV + vec2(0, size * TEXTURE_PIXEL_SIZE.y)得到的是偏移之后的UV值,也就是原本在UV位置的点取值取到UV + vec2(0, size * TEXTURE_PIXEL_SIZE.y)去了,所以相当颜色向上偏移,最终造成图像整体上移。
注意一些事实,Godot中的UV呢是左上角为原点,右下角为终点的(就和它的2D世界坐标一样)。所以UV增加,取值向下偏移,原本在该点的颜色取到了在该点之下的颜色,导致图像整体上移。
还有就是时刻提醒自己,COLOR跟UV都是各个片元自己的,这种基本常识还是要有,片元着色器是对每个片元的遍历操作。片元可以理解为具有渲染信息的像素点,它是光栅化过程的中间产物,并最终参与形成最终像素点(只有颜色的那个)。
这里的TEXTURE_PIXEL_SIZE是内置变量,表示图像尺寸的倒数。所以size * TEXTURE_PIXEL_SIZE,就相当于希望偏移 size 个像素点。或者你将其理解为对UV值的标准化也可以,因为UV的取值是0到1。比如:图像尺寸32×32,某点UV为(0.5,0.5),采样就采到(16,16)。当UV偏移了1/32,即UV(0.53125,0.53125),采样得0.53125 × 32 = 17,而且是正好等于17,所以其实还是偏移了一个像素。
最后最后还要注意,这个图像尺寸是整个图像的,换句话说,图集图像(Altas)中某个位置的图像用到的UV也是整个图集的,你可以试着去移动图集图像中某个小图的UV,然后你会发现可以看到其他图集中的图像。事实上很多基于UV动画就是这样做的。
所以这整个shader其实就是把整个图像上下左右偏移了size个像素,然后我们再基于原本的图像对四个偏移出来的图像进行修改,毕竟需求是描边,所以不能覆盖原本的图像,这里我使用的是一个判断语句,意思是如果原本图像的那个位置的颜色本来其实是透明的,那么它就可以是描边颜色,反之就是不变。这里用绝对值判断小于某个0.0001是因为不这样干Godot会警告你可能会丢精度。
最后其实可以用注释掉的那个语句替换,如果你不想用判断语句的话,因为在shader中用判断语句太多不太好。mix其实就是shader的Lerp,懂了吧,想要干的事和判断语句是一样的,不过直接用计算取代了判断。最终效果也是一模一样的,放心。
shader_type canvas_item;
uniform vec4 color : source_color = vec4(0,0,0,1);
uniform float size = 1.0f;
void fragment()
{
vec4 color_origin = texture(TEXTURE,UV);
vec4 color_up =
texture(TEXTURE, UV + vec2(0,size * TEXTURE_PIXEL_SIZE.y));
vec4 color_down =
texture(TEXTURE, UV + vec2(0,size * -TEXTURE_PIXEL_SIZE.y));
vec4 color_left =
texture(TEXTURE, UV + vec2(size * -TEXTURE_PIXEL_SIZE.x,0));
vec4 color_right =
texture(TEXTURE, UV + vec2(size * TEXTURE_PIXEL_SIZE.x,0));
vec4 outline = color_up + color_down + color_left + color_right;
outline.rgb = color.rgb;
COLOR = abs(color_origin.a) < 0.0001 ? outline : COLOR;
// COLOR = mix(outline, color_origin, color_origin.a);
}
简单用途
懒得演示了,反正可以用来丰富游戏画面,做一些简单的选择高亮效果还是很容易的。
注意事项
运行时设置shader参数
(Material as ShaderMaterial).SetShaderParameter(ShaderParamName, Value);
就一句话的事,但是这里的类型转换是必须的(对于C#)。
资源唯一性
和Unity一样,shader作为一个“程序”,实际参与游戏渲染需要媒介,也就是所谓的“材质”(Material),就是一堆用来渲染的数据的集合。
那么就要注意了,假设有两个角色,你想实现鼠标移入就高亮的效果,你很开心地给两位都加上了带着高亮shader的材质,结果发现当移入一个角色时,两个角色同时高亮了。
这就是资源唯一性的问题,因为两者本质上在内存中共享同一个材质,当你设置这个材质的参数时,理所当然它们都会变化。
在Godot中,资源类型呢都有一个叫"Local to Scene"的功能,也就是可以为每个用到该资源的场景本地实例化一个资源。以此可以解决上述问题的发生,就在inspector中勾选一下就可以了。
所以在开发中要搞清楚什么资源是唯一的,什么是共享的,比如Material可以每个实例一个,但是这些Material用到的shader却还是同一个。
还有一点是,Godot的资源是可以虽然保存为tres这种文件,意味着每个运行时的资源实例都是由该文件加载读取生成的,不论有多少个引用,都只会基于一个tres文件生成一个实例。除非像上述一样主动实例化其它的实例。
.tres译为“Text Resource”文本资源,和tscn同门。
结语
写得有点多了,本来不想说这么多的。本来不想碰shader的,但是还是抵挡不住美术的诱惑,后来才发现以前被Unity坑了,觉得写Shader是种折磨。GodotShader真的很好写。
2DShader几乎用不到顶点着色器,因为顶点实在太少了,大多数就一张图四个点。所以围绕着片元着色器是2D永恒的主题,还真的要感谢Vulkan,折磨了我这么久,终于让我在写Shader的时候高兴一会了。
后来我才发现,我一直花很多时间学习怎么去做游戏才能做的好,却没花过多少时间做游戏。写Shader时候侥幸的愉悦方才让我回想起来刚开始做游戏的时候是因为什么,而它们绝非是为了写框架,写策划。