解惑
我现在学习的这门课程是在 2020 年开始录制的,使用的是 Unity 2019,有些年代久远了。课程作者是一位资深 TA ,他也在 B 站个人简介标出:此课程已过赏味期 。
在4年后的今天,Shader Forge 的版本也定格在了 2019 ,有更多方便好用的工具可做为连连看替代。我在听过一点课之后,某次和朋友交流时,不经意间看到他在用另一款既美观又看起来很吊的插件(忘记名字了),当时有深深地怀疑过,自己在 2024 年去听这样一门课疑似”过时“的课是否值得。
技术美术最终是要落实到 Shader 代码的。当我接触到这门课程的代码部分时,之前的疑问全都烟消云散了。
以 Shader Forge 作为 Unity Shader 的开始仍是有意义的。Shader Forge 程序化生成的代码很简洁,可以很容易地删减出 Unity Shader 版的 HelloWorld ,辅助思路实现的同时也很好拿来做写代码的参考。Shader 代码的部分是相通的,会写 Shader 之后,我们可以把 SD 、 SP 里的 Shader 拿出来参考,把它们移植到 Unity 里或者别的地方,也可以比葫芦画瓢把写好的 Unity Shader 移植到其他地方。
连连看终究是一个辅助,不是最终答案,最终还是要丢掉的。目前 Unity 和 UE 有连连看,然而到一些陌生的平台,有没有这样的连连看工具还难说,也不会有人为了你想向这个平台移植 Shader ,专门为你开发一款工具。学会 Shader 是一个学会 “渔” 的过程。
综上, Shader Forge 只是一个工具,很适合拿来入门,等到熟练之后要脱离它。很多情况下写代码比连连看更容易实现效果,现在因为纠结工具问题而犹犹豫豫就输了。
PS:不同的 Unity 版本使用起来还是有区别的,比如某个变量在 2019 版里还叫这个名字,有可能之后某个版本就改了。同时有一些旧版本官方文档推荐的做法,在之后的新版本就不推荐了。还是要多查官方文档,这也是一名技美必备的基本素养。
HelloWorld ( FlatCol )
我们在 Shader Forge 中新建一个 Unlit Shader ,自带一个颜色连到了 Emission 上,这就是一个最简单的 Shader 。
这个颜色我们可以任意更改,比如我们可以把它改成绿色。效果:
我们用 VSCode 打开这个 Shader ,删除掉 Shader Forge 自带的内容,最大程度上地精简它,得到一个 Unity Shader 版的 Hello World —— FlatCol ( Flat Color ,最简的 Shader )
Shader "VS/FlatCol" {
Properties {
}
SubShader {
Tags {
"RenderType"="Opaque"
}
Pass {
Name "FORWARD"
Tags {
"LightMode"="ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#pragma multi_compile_fwdbase_fullshadows
#pragma target 3.0
// 输入结构
struct VertexInput {
float4 vertex : POSITION; // 将模型的顶点信息输入进来
};
// 输出结构
struct VertexOutput {
float4 pos : SV_POSITION; // 由模型顶点信息换算而来的顶点屏幕位置
};
// 输入结构>>>顶点Shader>>>输出结构
VertexOutput vert (VertexInput v) {
VertexOutput o = (VertexOutput)0; // 新建一个输出结构
o.pos = UnityObjectToClipPos( v.vertex ); // 变换顶点信息 并将其塞给输出结构
return o; // 将输出结构 输出
}
// 输出结构>>>像素
float4 frag(VertexOutput i) : COLOR {
return float4(0.0, 1.0, 0.0, 1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
我们目前需要关注的只有以下这些部分,其他部分可以当做“无用”的形式段。知道含义自然更好,不知道也无所谓,就把它当做一种仪式感。可以把它们当做发动魔术之前吟唱的咒文。
// 材质面板上暴露出的参数
Properties {
}
// CGPROGRAM 前面的部分,目前是空白,这个位置用来为材质面板上的参数声明变量
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// #include 用来引用库文件
#include "UnityCG.cginc"
// 输入结构
struct VertexInput {
float4 vertex : POSITION; // 将模型的顶点信息输入进来
};
// 输出结构
struct VertexOutput {
float4 pos : SV_POSITION; // 由模型顶点信息换算而来的顶点屏幕位置
};
// 输入结构>>>顶点Shader>>>输出结构
VertexOutput vert (VertexInput v) {
VertexOutput o = (VertexOutput)0; // 新建一个输出结构
o.pos = UnityObjectToClipPos( v.vertex ); // 变换顶点信息 并将其塞给输出结构
return o; // 将输出结构 输出
}
以及最重要的也是我们主要写的部分——像素 Shader
// 输出结构>>>像素
float4 frag(VertexOutput i) : COLOR {
return float4(0.0, 1.0, 0.0, 1.0);
}
需要注意的地方
- 每个 Shader 都应该有一个独一无二的路径名,位置在开头的
Shader "VS/FlatCol"
,引号内的就是路径名。Shader Forge 中的路径在左栏的 Shader Settings >> Path 。 - 只有从
CGPROGRAM
到ENDCG
之间的内容,每行末尾需要加;
,其余的地方是不用加分号的,相当于是在 Shader 代码中插入了一段 cg 代码。 - 注意半角符号!
- 最后 return 的值必须是一个四维的(有时会自动转换)
- 在写 Shader 时数字最好保留一位小数,如 0.0 , 1.0 这种,虽说引擎可能自动转换,但是在某些情况下转换又会很邪门地出问题,尤其在一些老设备上,所以最好养成这个习惯。
- 打开 Unity 的 Console 放在窗口下部,这样每次 Shader 报错出现 bug 红 的时候,可以看到报错信息。
当我们精简好之后,我们用 FlatCol 创建一个材质附给一个模型,不出意外的话,会和 Shadow Forge 中连出来的效果一样。如果有些地方不慎写错,会出现以下的 bug 红 和 Console 报错:
代码 Lambert
有了 Hello World ( FlatCol ),我们已经可以输出一个纯色了
我们离写出人生中写的第一个 Shader 只有一步之遥了。
我们先观察在 Shader Forge 连出来的 Lambert 节点。
FlatCol 中已经有了顶点信息。
我们在 Lambert 又用到了 法线信息
,因此在 输入结构
、 输出结构
、以及 输入结构 >>顶点 Shader >> 输出结构
这三个部分内追加法线信息。
看不懂是正常现象,直接从 Shader Forge 那边先复制过来,之后用的多了慢慢就会懂了。
// 输入结构
struct VertexInput {
float4 vertex : POSITION; // 输入模型的顶点信息
float3 normal : NORMAL; // 输入模型的法线信息
};
// 输出结构
struct VertexOutput {
float4 pos : SV_POSITION; // 由模型顶点信息换算而来的顶点屏幕位置
float3 nDirWS : TEXCOORD0; // 由模型法线信息换算而来的世界空间法线信息
};
// 输入结构>>>顶点Shader>>>输出结构
VertexOutput vert (VertexInput v) {
VertexOutput o = (VertexOutput)0; //新建一个输出结构
o.pos = UnityObjectToClipPos( v.vertex ); //变换顶点信息,并将它塞给数据结构
o.nDirWS = UnityObjectToWorldNormal( v.normal ); //变换法线信息,并将它塞给数据结构
return o; //输出数据结构
}
然后我们准备向量 nDIr 和 lDir ,可以先不用理解,依旧直接从 Shader Forge 连好的代码里粘贴过来。
由于 nDIr 和 lDir 都是三维的,我们用 float3 。
点积的结果是一个一维的量,用 float ,然后截断负值。
我们把计算结果补充成 float4 ( RGBA ),最后输出。
// 输出结构>>>像素Shader
float4 frag(VertexOutput i) : COLOR {
float3 nDir = i.nDirWS; //获取nDir
float3 lDir = _WorldSpaceLightPos0.xyz; //获取lDir
float nDotl = dot(nDir,lDir); //nDir点积lDir
float Lambert = max(0.0,nDotl); //截断负值
return float4(Lambert,Lambert,Lambert,1.0); //最终输出颜色,只能是float4,float等也可以只不过会自行转换
}
展开查看完整代码
Shader "VS/Lambert"
{
Properties {
}
SubShader {
Tags {
"RenderType"="Opaque"
}
Pass {
Name "FORWARD"
Tags {
"LightMode"="ForwardBase"
}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#pragma multi_compile_fwdbase_fullshadows
#pragma target 3.0
// 输入结构
struct VertexInput {
float4 vertex : POSITION; // 输入模型的顶点信息
float3 normal : NORMAL; // 输入模型的法线信息
};
// 输出结构
struct VertexOutput {
float4 pos : SV_POSITION; // 由模型顶点信息换算而来的顶点屏幕位置
float3 nDirWS : TEXCOORD0; // 由模型法线信息换算而来的世界空间法线信息
};
// 输入结构>>>顶点Shader>>>输出结构
VertexOutput vert (VertexInput v) {
VertexOutput o = (VertexOutput)0; //新建一个输出结构
o.pos = UnityObjectToClipPos( v.vertex ); //变换顶点信息,并将它塞给数据结构
o.nDirWS = UnityObjectToWorldNormal( v.normal ); //变换法线信息,并将它塞给数据结构
return o; //输出数据结构
}
// 输出结构>>>像素Shader
float4 frag(VertexOutput i) : COLOR { //只能是float4或half4
float3 nDir = i.nDirWS; //获取nDir
float3 lDir = _WorldSpaceLightPos0.xyz; //获取lDir
float nDotl = dot(nDir,lDir); //nDir点积lDir
float Lambert = max(0.0,nDotl); //截断负值
return float4(Lambert,Lambert,Lambert,1.0); //最终输出颜色,只能是float4,float等也可以只不过会自行转换
}
ENDCG
}
}
FallBack "Diffuse"
}
代码 HalfLambert
我们只需要在 像素Shader 部分,对 Lambert 的结果做一下处理即可。
// 输出结构>>>像素Shader
float4 frag(VertexOutput i) : COLOR { //只能是float4或half4
float3 nDir = i.nDirWS; //获取nDir
float3 lDir = _WorldSpaceLightPos0.xyz; //获取lDir
float nDotl = dot( nDir, lDir ); //nDir点积lDir
float HalfLambert = max( 0.0, nDotl * 0.5 + 0.5 ); //转换为半兰伯特并截断负值
return float4( HalfLambert, HalfLambert, HalfLambert, 1.0 ); //最终输出颜色,只能是float4,float等也可以只不过会自行转换
}
委托任务
如 Gif 图创建一个材质,面板上提供一个 Slider 可以平滑调整猴子的明暗调子;