Unity Shader学习01--------Shader

发布时间 2023-03-23 15:17:02作者: 打工仔-也想飞
  Shader比较学术的百科回答就是用来实现图像渲染的,用来替代固定渲染管线的可编辑程序。其中Vertex Shader(顶点着色器)主要负责顶点的几何关系等的运算,Pixel Shader(像素着色器)主要负责片元颜色等的计算。 
  那说人话就是可以把渲染流程看作美食的制作流程,一般来说制作美食需要选食材,切菜,炒菜,加调料上色,出锅等流程。我们可以把模型的Mesh看作食材,而Shader在这个流程中主要负责切菜和上色的步骤,切菜控制食材的形态,这就是顶点Shader负责的,加调料上色可以让美食变得更加秀色可餐,这也就是像素Shader负责的。

 

 

Standard Surface Shader 
标准表面着色器,它是一种基于物理的着色系统,可以理解为 它是通过对物理现象的简单模拟,可以实现生活中各种物品的效果,比如石头、木材、玻璃、塑料和金属等等。
 
Unlit Shader 
它是最简单的着色器,与 Standard Surface Shader 相比,它去除了冗长的光照公式以及阴影解算,因此得名 Unlit,翻译过来就是无光照,也正因如此,它只由最基础的 Vertex Shader 和 Fragment Shader 组成,最为基础易懂。
 
Image Effect Shader 
它其实也是也是顶点片元着色器,不过它主要针对实现各种屏幕后处理效果,那后处理是什么呢?一般像是泛光、调色、景深、模糊等基于最终整个屏幕画面而进行再次处理的就是后处理,这里做个简单的了解即可。
  
Compute Shader 
计算着色器,它是在GPU中运行的一段程序,独立于常规渲染管线之外的,它可以直接将GPU作为并行处理器加以利用,从而使GPU不仅具有3D渲染能力,还具有其他的运算能力。一般会在需要大量并行计算的时候使用。
 
Ray Tracing Shader 
光线追踪着色器,光线追踪是指从摄像机出发的若干条光线,每条光线会和场景里的物体求交,根据交点位置获取表面的材质、纹理等信息,并结合光源信息计算光照。相对于传统的光栅化渲染,光线追踪可以轻松模拟各种光学效果,如反射、折射、散射、色散等。但由于在进行求交计算时需要知道整个场景的信息,它的计算成本也是非常高的。
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
ShaderLab 
  Unity为我们封装的着色器语言,而目前主流的着色器语言有3种,基于OpenGL的GLSL / 基于DX的HLSL / NVIDIA公司的CG
   GLSL与HLSL分别是基于OpenGL和Direct3D的接口,两者不能混用。而CG则是为了使图形硬件的编程变得和 C语言编程一样方便自由,它本身基于C语言。如果你之前使用过C系语言其中的任意一个,那CG的语法也是比较容易掌握的。但其实由于Microsoft和NVIDIA的相互协作,他们在标准硬件光照语言的语法和语义上达成了一致,所以HLSL和Cg其实可以看为是同一种语言。
   ShaderLab则是Unity在HLSL和CG的基础之上封装的只属于Unity的着色器语言,它的灵活性更高,而且不再需要将 Shader 的配置 硬写在引擎代码中,本质是在底层着色语言的基础上,额外提供了声明信息,以数据驱动的方式使我们在渲染管线内自由发挥。
 
 ShaderLab语法详细解析
 
// Shader 的路径名称  默认为文件名,也可以与文件名不同
Shader "Unlit/HiShader"
{
    // 属性 
    // Material Inspector显示的所有参数都在需要在这里进行声明
    Properties
    {
        // 通常所有属性名都以下划线字符开头 _MainTex
        _MainTex("Texture", 2D) = "white" {}

        // 比较常见的属性类型
        // ————————————————————————————————————————————————
        _Integer("整数(新版)", Integer) = 1
        _Int("整数(旧版)", Int) = 1
        _Float("浮点数", Float) = 0.5
        _FloatRange("浮点数滑动条", Range(0.0, 1.0)) = 0.5
        // Unity包含以下内置纹理, 可以直接填充
        // “white”(RGBA:1,1,1,1)
        // “black”(RGBA:0,0,0,1)
        // “gray”(RGBA:0.5,0.5,0.5,1)
        // “bump”(RGBA:0.5,0.5,1,0.5)
        // “red”(RGBA:1,0,0,1)
        _Texture2D("2D纹理贴图", 2D) = "red" {}
        // 字符串留空或输入无效值,则它默认为 “gray”
        _DefaultTexture2D("2D纹理贴图", 2D) = "" {}
        // 默认值为 “gray”(RGBA:0.5,0.5,0.5,1)
        _Texture3D("3D纹理贴图", 3D) = "" {}
        _Cubemap("立方体贴图", Cube) = "" {}
        // Inspector会显示四个单独的浮点数字段
        _Vector("Example vector", Vector) = (0.25, 0.5, 0.5, 1)
        // Inspector会显示拾色器拾取色彩RGBA值
        _Color("色彩", Color) = (0.25, 0.5, 0.5, 1)
        // ————————————————————————————————————————————————

        // 除此之外 属性声明还可以具有一个可选特性 用来告知Unity如何处理它们
        // HDR可以使色彩亮度的值超过1
        [HDR]_HDRColor("HDR色彩", Color) = (1,1,1,1)
        // Inspector隐藏此属性
        [HideInInspector]_Hide("看不见我~", Color) = (1,1,1,1)
        // Inspector隐藏此纹理属性的Scale Offset字段
        [NoScaleOffset]_HideScaleOffset("隐藏ScaleOffset", 2D) = "" {}
        // 指示纹理属性为法线贴图,如果分配了不兼容的纹理,编辑器则会显示警告。
        [Normal]_Normal("法线贴图", 2D) = "" {}
    }

    // 子着色器 
    // 一个Shader至少有一个或者多个子着色器SubShader,这些子着色器互不干扰,且只有一个会运行
    // 在加载shader时Unity会遍历所有SubShader列表,并最终选择用户机器支持的第一个
    SubShader
    {
        // 可以通过Tags来向子着色器分配标签
        // 只可以写在SubShader语块内,不可写在Pass内
        /* 以键值对的形式存在,可以出现多个键值对
        Tags {
            "TagName1" = "Value1"
            "TagName2" = "Value2"
            "TagName3" = "Value3"
            ...
            }
        */

        // RenderPipeline: 声明子着色器是否与通用渲染管线 (URP) 或高清渲染管线 (HDRP) 兼容
        // 仅与 URP 兼容
        // Tags { "RenderPipeline"="UniversalRenderPipeline" }
        // 仅与 HDRP 兼容
        // Tags { "RenderPipeline"="HighDefinitionRenderPipeline" }
        // RenderPipeline不声明或任何其他值表示与 URP 和 HDRP 不兼容
        // ————————————————————————————————————————————————

        // Queue: 声明渲染队列
        // Tags { "Queue"="Background" } // 最早被调用的渲染,用来渲染天空盒或者背景
        // Tags { "Queue"="Geometry" }   // 这是默认值,用来渲染非透明物体(普通情况下,场景中的绝大多数物体应该是非透明的)
        // Tags { "Queue"="AlphaTest" }  // 用来渲染经过Alpha Test的像素,单独为AlphaTest设定一个Queue是出于对效率的考虑
        // Tags { "Queue"="Transparent" }// 以从后往前的顺序渲染透明物体
        // Tags { "Queue"="Overlay" }    // 用来渲染叠加的效果,是渲染的最后阶段(比如镜头光晕等特效)
        // ————————————————————————————————————————————————

        // RenderType: 用来区别这个Shader要渲染的对象是属于什么类别的。
        // 设置渲染类型 用一种称为着色器替换的技术在运行时交换子着色器,用来区别这个Shader要渲染的对象是属于什么类别的
        // 这里表示非透明物体渲染
        Tags { "RenderType" = "Opaque" }
        // 更多详细内容可参考官网文档 https://docs.unity.cn/cn/2021.3/Manual/SL-SubShaderTags.html

        // LOD (Level of Detail)
        LOD 100

        // 每个子着色器由多个通道组成,许多简单的着色器只使用一个通道,但想要一些更复杂的效果,着色器可能需要更多通道
        // 一个Pass就是一次绘制,可以看成是一个Draw Call而Pass的意义在于多次渲染,
        // 如果你有一个Pass,那么着色器只会被调用一次,如果你有多个Pass的话,
        // 那么就相当于执行多次SubShader了,这就叫双通道或者多通道。

        // Draw Call:其实就是CPU调用图像编程接口的渲染命令,CPU每次调用DrawCall,都需要向GPU发送许多数据啊、渲染状态等等,
        // 一旦CPU执行完应用阶段,GPU就会开始执行这次的渲染流程。而GPU渲染的速度比CPU提交命令的速度要快的多,
        // 所以如果DrawCall数量过多的情况下,CPU需要进行大量的计算,进而就会导致CPU过载,影响游戏的运行效率。
        Pass
        {
            CGPROGRAM
            // 声明顶点着色器
            #pragma vertex vert
            // 声明像素着色器
            #pragma fragment frag
            // 使雾生效
            #pragma multi_compile_fog

            // 引用CG的核心代码库
            #include "UnityCG.cginc"

            // 应用程序阶段结构体
            struct appdata
            {
                // 参考:https://docs.microsoft.com/zh-cn/windows/win32/direct3dhlsl/dx-graphics-hlsl-semantics
                // POSITION 着色器语言的语义,用来限定着色器的输入输出值的类型
                // 模型空间的顶点坐标
                float4 vertex : POSITION;
                // 模型的第一套UV坐标
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                // UV
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                // SV_POSITION 当这个值需要作为输出值输出给系统用的时候 前面需要加SV_前缀
                // 当然因为有向下兼容的机制 不加也没啥太大问题
                float4 vertex : SV_POSITION;
            };

            // 在Properties中声明的参数要在这里相对应的定义后才可以使用
            sampler2D _MainTex;
            float4 _MainTex_ST;

            // 定义顶点着色器函数 函数名要与声明顶点着色器名称相同
            v2f vert(appdata v)
            {
                v2f o;
                // 将顶点坐标从模型空间变换到裁剪空间
                o.vertex = UnityObjectToClipPos(v.vertex);
                // Transforms 2D UV by scale/bias property
                // #define TRANSFORM_TEX(tex,name) (tex.xy * name##_ST.xy + name##_ST.zw)
                // 等价于v.uv.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                // 简单来说,TRANSFORM_TEX主要作用是拿顶点的uv去和材质球的tiling和offset作运算,
                // 确保材质球里的缩放和偏移设置是正确的
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            // SV_Target可以视为COLOR ,虽说他也是作为输出值输出给系统的
            // 但它其实是告诉系统把输出的颜色值存储到RenderTarget中
            // 所以这里我们用SV_Target
            fixed4 frag(v2f i) : SV_Target
            {
                // 采样2D纹理贴图
                fixed4 col = tex2D(_MainTex, i.uv);
                // 应用雾
                UNITY_APPLY_FOG(i.fogCoord, col);
                // 返回经过处理后的最终色彩
            return col;
            }
        ENDCG
        }
    }
}
 
实战案例
VertexShader 
找到 VertexShader,这里将通过调整顶点的 Y 轴位置实现一个简单的压扁效果
 
            // 定义顶点着色器函数 函数名要与声明顶点着色器名称相同
            v2f vert(appdata v)
            {
                v2f o;
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                // 模型空间转到世界空间
                float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                // 压Y轴位置 这里把世界空间下顶点的y减去最低部y的值乘上一个系数
                // 然后再用y去减去这个值,就可以通过这个系数来控制兔子被压扁的程度
                float y = worldPos.y - (worldPos.y - _Bottom) * _Value;
                // 最终世界空间位置
                float3 tempWorld = float3(worldPos.x, y, worldPos.z);
                // 世界空间转裁剪空间
                o.vertex = UnityWorldToClipPos(tempWorld);
                return o;
            }

记得把 _Bottom 和 _Value 公开定义出来方便调整

 Properties
 {
     _Value ("压扁系数",Range(0, 1)) = 0
    _Bottom ("底部", float) = 0
 }
                                        
float _Value;
float _Bottom;
 
最后拖拽压扁系数条就可以看到效果啦~
  
 

 

 

 

Pixel Shader 
这里创建一个 Cube,相比之下,虽然它们都是三维模型,但这只兔子看起来就跟纸片一样,完全没有立体感。
 
 
 
这是因为 Unlit 默认是不受光材质,纹理什么颜色它就直接显示出来了,但通常来说,光照是三维世界不可或缺的部分,现实世界中,当光照射到物体表面时, 物体对光会发生反射、透射、吸收、折射等被物体反射的光进入视觉系统,使看见物体的表面有明暗之分,为了模拟这一现象,科研家建立了一些数学模型来替代复杂的物理模型,统称为光照模型。 
 
 
比较常见的光照模型有漫反射的 Half Lambert 模型 ,以及镜面反射的 Blinn-Phong 模型。 Half Lambert 能够较好地表现粗糙表面上的光照现象,像如石灰墙,纸张等等,但是在渲染金属材质制成的物体时,则会显得呆板,表现不出光泽。主要原因是其没有考虑到镜面反射效果,所以 Blinn-Phong 对其进行了很好的补充。 
 
 
这里我们将通过Pixel Shader实现最简单的 Half Lambert ,首先拿到计算光照需要的模型法线和世界坐标,在v2f的结构体里将它们进行定义,
 
struct v2f
{
float2 uv : TEXCOORD0;                      
// 计算光照需要用到法线和世界位置
// 通常使用TEXCOORDn语义来修饰float2, float3, float4类型
float3 worldNormal: TEXCOORD1;
float3 worldPos:TEXCOORD2;                    
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
};

在顶点着色器中将世界坐标和法线进行处理,传递给接下来的像素着色器

 
 v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
// 模型空间转到世界空间
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
// 法线向量归一化
o.worldNormal = normalize(UnityObjectToWorldNormal(v.normal));
return o;
}

在像素着色器中,首先通过世界坐标拿到光照的方向,也就是所谓的入射光,用法线点乘入射光,就可以得到入射光与模型表面的夹角,由于当入射光和法线夹角的余弦值为负数的时候,所得到的结果始终都是零,就会导致照不到的地方一片漆黑,所以这里需要乘0.5再加0.5 ,这样就可以把原本-1到1的取值范围变为0-1的取值范围。最后把求出来的光照亮度叠加到最终的像素色彩值就可以啦

 fixed4 frag(v2f i) : SV_Target
{
      // sample the texture
      fixed4 col = tex2D(_MainTex, i.uv);
      // 得到光照方向
      float3 worldLightDir = UnityWorldSpaceLightDir(i.worldPos);
      // NoL代表表面接受的能量大小
      float NoL = dot(i.worldNormal, worldLightDir);
      // 计算half-lambert亮度值
      float halfLambert = NoL * 0.5 + 0.5;
      // apply fog
      UNITY_APPLY_FOG(i.fogCoord, col);
      return col * halfLambert;
}

  这样最终的效果就会更符合我们的视觉观感。