Unity Shader 入门笔记vol2--符文·筑基·照

解惑

我现在学习的这门课程是在 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 。

image-20241009100254724

这个颜色我们可以任意更改,比如我们可以把它改成绿色。效果:

image-20241009110821958

image-20241009103255422

我们用 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 。
  • 只有从 CGPROGRAMENDCG 之间的内容,每行末尾需要加 ; ,其余的地方是不用加分号的,相当于是在 Shader 代码中插入了一段 cg 代码。
  • 注意半角符号
  • 最后 return 的值必须是一个四维的(有时会自动转换)
  • 在写 Shader 时数字最好保留一位小数,如 0.0 , 1.0 这种,虽说引擎可能自动转换,但是在某些情况下转换又会很邪门地出问题,尤其在一些老设备上,所以最好养成这个习惯。
  • 打开 Unity 的 Console 放在窗口下部,这样每次 Shader 报错出现 bug 红 的时候,可以看到报错信息。

当我们精简好之后,我们用 FlatCol 创建一个材质附给一个模型,不出意外的话,会和 Shadow Forge 中连出来的效果一样。如果有些地方不慎写错,会出现以下的 bug 红 和 Console 报错:

image-20241009103048045

代码 Lambert

有了 Hello World ( FlatCol ),我们已经可以输出一个纯色了

我们离写出人生中写的第一个 Shader 只有一步之遥了。

image-20241009103514468

我们先观察在 Shader Forge 连出来的 Lambert 节点。

image-20241009104150694

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等也可以只不过会自行转换
}

image-20241009114909119

展开查看完整代码

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"

}

image-20241009105733738

代码 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 可以平滑调整猴子的明暗调子;

20241009112922

归档
arrow_up
theme