Unity学习之Shader

Shader

是用来实现图像渲染的,用来替代固定渲染管线的可编辑程序。其中Vertex Shader(顶点着色器)主要负责顶点的几何关系等的运算,Pixel Shader(像素着色器)主要负责片元颜色等的计算。顶点Shader负责改变形态,像素Shader负责改变颜色。

Unity中的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。而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
        }
    }
}

基础认知

MeshFilter 网格过滤器 ,可以理解为 写作的素材。

MeshRenderer 网格渲染器,负责把MeshFilter丢过来的Mesh,绘制显示到我们的场景中,可以理解为 把素材整合最终写成一篇文章。

Material 材质球  Material是MeshRenderer中非常重要的角色,它的配置决定了物体表面的外观将以怎样的质地呈现到我们眼前。

Mesh 网格 Mesh指的就是模型的网格,它决定了物体的表面形状是怎样的,一个模型的表面大多是由多个彼此相连的三角面构成,而Mesh则是构成这些三角面所需的信息集合。

三角面所需的信息如下:Mesh data

Vertices 顶点数组 Vector3[] 它存储的是顶点的相关信息,所谓点成线,线成面,可以理解为这里面存储的是构成网格面全部的点.

Topology 拓扑类型 储的就是一个类型信息,可以理解为它是图形表面排列结构的组成方式,Unity给我们提供了5种拓扑类型,三角面、四边形、线条、虚线、点阵,最常用的则是三角面。

Indices 索引数组 int[] 它是每个三角面顶点 的索引,可以理解为他存储了构网格三角面所用到的顶点索引。

Vertex data 顶点数据 包含了顶点的位置、法线、切线、UV等属性.

  1. Normal 法线 Vector3[] 法线就是垂直于该顶点三角面的一条三维向量,它只有方向,没有大小。法线的方向就是顶点三角面朝外的方向。假设我们面前有一面镜子,它的正中心会有一条法线垂直于镜面指向我们,指向我们的面就是正面,相反就是背面
  2. Tangent 切线 Vector3[] 它是垂直于法线的一条向量,而由于垂直于法线的向量有无数条,所以切线最终是由UV坐标来决定朝向的
  3. UV 纹理坐标 Vector2[] U增长的方向就是切线的方向,它和三维空间的X, Y, Z较为类似,它是一个二维的坐标系统,模型网格除了有三维空间的xyz坐标外,还有一个二维的UV坐标,在UV坐标中,U和V分别代表顶点在Texture水平和垂直方向上的采样坐标,这些坐标通常位于(0,0)和(1,1)之间,(0,0)代表最左下角,而(1,1)代表最右上角。这就跟平时装修房子贴墙纸一样,可以理解为它是Texture映射到模型表面的依据,模型顶点 会依据UV坐标对Texture进行采样。
  4. Index data 索引数据 这个数据取决于拓扑类型,如果是三角面他储存的就是[0,1,2],四边形储存的就是[0,1,2,3],这个索引数值对应的就是顶点数组的下标。

渲染管线:渲染管线

渲染管线也称为渲染流水线或像素流水线或像素管线,是显示芯片内部处理图形信号相互独立的的并行处理单元。在某种程度上可以把渲染管线比喻为工厂里面常见的各种生产流水线,工厂里的生产流水线是为了提高产品的生产能力和效率,而渲染管线则是提高显卡的工作能力和效率。

一般这个过程会分为四个主要阶段:应用程序阶段、几何阶段、光栅化阶段、像素处理阶段。而每个阶段 又会分为很多个部分。

应用程序阶段 (The Application Stage) CPU 

它最主要是负责数据的准备,也就是准备后面的阶段 所需的数据,像如模型,贴图,光照,相机位置等信息。 

几何阶段(The Geometry Stage) GPU 

        分为 顶点着色、几何着色、裁剪  

        顶点着色 :可编程部分,它会对逐个顶点相关的信息进行处理,生成图元,计算并传递给接下来的渲染流程。这个阶段就是为了控制图形的形态,它对应的则是这里的VertxShader,它的工作主要是计算顶点的位置、法线、纹理坐标,根据材质、纹理、以及光源属性进行顶点光照的计算,平时常见的顶点动画一般就是在这里实现的。

图元:可以简单理解为它是渲染管线中所有点,线,面的统称。

几何着色: 可选可编程部分,并非所有GPU都支持 ,它可以把简单的图元拓展成更复杂的形式,通常我们认为,这两大着色器共同构成了 几何阶段的可编程部分。

裁剪 : 固定功能硬件实现,对顶点几何两大着色器的输出结果进行处理,它会把完全处于视锥体交界外 以及屏幕窗口外的 几何体部分裁剪掉, 只留下用户能看到的部分,并且对生成的新顶点部分进行插值,输送给接下来的阶段。

光栅化阶段(Rasterization) GPU

屏幕映射:经过裁剪之后,硬件会通过透视除法将物体从 裁剪空间 变换 为 标准化设备坐标也叫NDC,之后GPU会把得到的NDC空间坐标下的顶点,映射到屏幕空间坐标中,进行图元装配,这一步会计算微分、边方程和其他三角形数据,三角形的朝向剔除就是在这个阶段完成的。

NDC:全称Normalized Device Coordinates,一般来说裁剪完成后,会通过透视除法,将物体从裁剪空间 变换为标准化设备坐标NDC,透视除法是将裁剪空间中,顶点的4个分量都除以w分量,从裁剪空间转换到NDC。它是一个长宽高取值范围为[-1,1]的立方体,之所以要转到NDC,是为了方面我们后面进行视口变换把它映射到屏幕空间

图元装配:主要是计算微分(differentials)、边方程(edge equations)和其他三角形数据(顶点属性插值)

光栅化 :它会在每个像素点上生成一个片元,如果开启了多重采样抗锯齿,就会对每个像素进行多次采样,产生多个片元,最终进行混合来达到抗锯齿的效果。

片元:是光栅化之后产生的像素点,因为没有被画到屏幕上,不能被直接称为像素一个像素的最终结果可能是由多个片元来决定的,渲染管线为了细分,就单独创造了片元这个词来描述它,片元只是渲染管线的概念。

像素:则是最后写到图像上的值。

像素处理阶段 ( Pixel Processing )GPU

        像素 (片元) 着色器:可编程部分,它的工作主要是根据顶点的插值属性,进行逐像素计算,因为它需要处理每一个像素,所以这也是最耗时的一个阶段。它的输入输出都是片元数据,输入的数据是 颜色 和纹理坐标,输出的则是计算后所得的每个像素的色彩值,像是逐像素光照、反射、阴影等等更为复杂的效果都是可以在这里实现的。

合并:只可配置不可编程部分,在一系列的测试后会进行合并,所谓的测试则是判断一个像素点最终是否应该被显示在屏幕上,通过测试的颜色会和缓冲区的颜色叠加混合。

坐标空间

模型空间 以物体本身为原点的坐标空间,

世界空间以世界的(0,0)为原点的坐标空间,

视图空间 以相机为原点的坐标空间,描述的物体在相机的哪个位置,

裁剪空间 顶点坐标乘以MVP矩阵之后所在的空间,

屏幕空间 窗口屏幕上的二维像素坐标空间。

实战案例

轮廓描边

渲染轮廓线的方式有很多种, 在这里带大家熟悉其中最简单的一种, 对物体做两次渲染, 第二次渲染时开启正面剔除,将顶点沿法线向外延深一段距离,(放大物体),实现轮廓线,这里就用到我们之前提到的多Pass渲染。

Shader "Unlit/ToonShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _OutlineWidth ("Outline Width", Range(0.01, 1)) = 0.01
    _OutLineColor ("OutLine Color", Color) = (0.5,0.5,0.5,1)
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
        
        Pass
        {	
        // 开启前向剔除 表示剔除前面 只显示背面
            Cull Front
            
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            
            // 线条宽度
            float _OutlineWidth;
            // 线条颜色
            float4 _OutLineColor;

            struct appdata 
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                // 法线
                float3 normal : NORMAL;
            };

            struct v2f
            {
        float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v) 
            {
                v2f o;
        // 顶点沿着法线方向外扩(放大模型)
        float4 newVertex = float4(v.vertex.xyz + v.normal * _OutlineWidth * 0.01 ,1);
        // UnityObjectToClipPos(v.vertex) 将模型空间下的顶点转换到齐次裁剪空间
                o.vertex = UnityObjectToClipPos(newVertex);
                return o;
            }

            half4 frag(v2f i) : SV_TARGET 
            {
            // 返回线条色彩
                return _OutLineColor;
            }
            
            ENDCG
        }
    }
}

色阶

通常来说都是由它来决定画面色彩的丰富度饱满度精细度

// 得到顶点法线
float3 normal = normalize(i.worldNormal);
// 得到光照方向
float3 worldLightDir = UnityWorldSpaceLightDir(i.worldPos);
// NoL代表表面接受的能量大小
float NoL = dot(i.worldNormal, worldLightDir);
// 计算half-lambert亮度值
float halfLambert = NoL * 0.5 + 0.5;

// 通过亮度值计算线性ramp
float ramp = linearstep(_RampStart, _RampStart + _RampSize, halfLambert);
float step = ramp * _RampStep;  // 使每个色阶大小为1, 方便计算
float gridStep = floor(step);   // 得到当前所处的色阶
float smoothStep = smoothstep(gridStep, gridStep + _RampSmooth, step) + gridStep;
ramp = smoothStep / _RampStep;  // 回到原来的空间
// 得到最终的ramp色彩
float3 rampColor = lerp(_DarkColor, _LightColor, ramp);
rampColor *= col;

高光 

用相机的位置减去世界位置得到视向量,也就是当前物体表面指向摄像机的方向,由于反射不太好算,所以这里通过 视向量 和 光照方向 得到角平分线,也就是半程向量。通过 法线方向 点乘 半程向量 就可以得到 法线 和 半程向量 的 夹角,由此就 可以推断出 视向量 和 反射向量 的 接近程度,用 noh 来 计算高光 的 亮度值,而这个参数 SpecPow 则是 控制高光的 光泽度,也就是 高光 亮斑的 范围,和色阶同样,用smoothStep来做个柔边的效果再把高光颜色和强度值加上,最后我们把漫反射和高光混合,就可以来调试效果啦。

// 得到视向量
float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
// 计算half向量, 使用Blinn-phone计算高光
float3 halfDir = normalize(viewDir + worldLightDir);
// 计算NoH用于计算高光
float NoH = dot(normal, halfDir);
// 计算高光亮度值
float blinnPhone = pow(max(0, NoH), _SpecPow * 128.0);
// 计算高光色彩
float3 specularColor = smoothstep(0.7 - _SpecSmooth / 2, 0.7 + _SpecSmooth / 2, blinnPhone) 
                                            * _SpecularColor * _SpecIntensity;       

边缘光

首先我们需要得知哪里是我们看到的边缘,当我们的视向量和法线向量的夹角越接近直角时它就越靠近边缘,先拿到视向量和法向量的夹角,就可以看到,越是接近边缘的地方越暗,但边缘光一般都是越接近边缘越亮,所以给 1- 反转一下,但正常来说阴影部分是不应该有边缘光的,所以要把漫反射加一下,那至此边缘光就正确了

// 计算NoV用于计算边缘光
float NoV = dot(i.worldNormal, viewDir);
// 计算边缘光亮度值
float rim = (1 - max(0, NoV)) * NoL;
// 计算边缘光颜色
float3 rimColor = smoothstep(_RimThreshold - _RimSmooth / 2, _RimThreshold + _RimSmooth / 2, rim) * _RimColor;

最后把各项颜色混合就大功告成

// 混合颜色
float3 finalColor = saturate(rampColor + specularColor + rimColor);
return float4(finalColor,1);

完整代码 

Shader "Custom/ToonShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        
        _OutlineWidth ("Outline Width", Range(0.01, 1)) = 0.01
        _OutLineColor ("OutLine Color", Color) = (0.5,0.5,0.5,1)
        
        _RampStart ("交界起始 RampStart", Range(0.1, 1)) = 0.3
        _RampSize ("交界大小 RampSize", Range(0, 1)) = 0.1
        [IntRange] _RampStep("交界段数 RampStep", Range(1,10)) = 1
        _RampSmooth ("交界柔和度 RampSmooth", Range(0.01, 1)) = 0.1
        _DarkColor ("暗面 DarkColor", Color) = (0.4, 0.4, 0.4, 1)
        _LightColor ("亮面 LightColor", Color) = (0.8, 0.8, 0.8, 1)
        
        _SpecPow("SpecPow 光泽度", Range(0, 1)) = 0.1
        _SpecularColor ("SpecularColor 高光", Color) = (1.0, 1.0, 1.0, 1)
        _SpecIntensity("SpecIntensity 高光强度", Range(0, 1)) = 0
        _SpecSmooth("SpecSmooth 高光柔和度", Range(0, 0.5)) = 0.1
        
        _RimColor ("RimColor 边缘光", Color) = (1.0, 1.0, 1.0, 1)
        _RimThreshold("RimThreshold 边缘光阈值", Range(0, 1)) = 0.45
        _RimSmooth("RimSmooth 边缘光柔和度", Range(0, 0.5)) = 0.1
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float3 normal: NORMAL;  // 计算光照需要用到模型法线
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
                // 计算光照需要用到法线和世界位置
                float3 worldNormal: TEXCOORD1;
                float3 worldPos:TEXCOORD2;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _RampStart;
            float _RampSize;
            float _RampStep;
            float _RampSmooth;
            float3 _DarkColor;
            float3 _LightColor;

            float _SpecPow;
            float3 _SpecularColor;
            float _SpecIntensity;
            float _SpecSmooth;

            float3 _RimColor;
            float _RimThreshold;
            float _RimSmooth;

            float linearstep (float min, float max, float t)
            {
                return saturate((t - min) / (max - min));
            }

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                // 向下传输这些数据
                o.worldNormal = normalize(UnityObjectToWorldNormal(v.normal));
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                
                //------------------------ 漫反射 ------------------------
                // 得到顶点法线
                float3 normal = normalize(i.worldNormal);
                // 得到光照方向
                float3 worldLightDir = UnityWorldSpaceLightDir(i.worldPos);
                // NoL代表表面接受的能量大小
                float NoL = dot(i.worldNormal, worldLightDir);
                // 计算half-lambert亮度值
                float halfLambert = NoL * 0.5 + 0.5;

                //------------------------ 高光 ------------------------
                // 得到视向量
                float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
                // 计算half向量, 使用Blinn-phone计算高光
                float3 halfDir = normalize(viewDir + worldLightDir);
                // 计算NoH用于计算高光
                float NoH = dot(normal, halfDir);
                // 计算高光亮度值
                float blinnPhone = pow(max(0, NoH), _SpecPow * 128.0);
                // 计算高光色彩
                float3 specularColor = smoothstep(0.7 - _SpecSmooth / 2, 0.7 + _SpecSmooth / 2, blinnPhone)
                                        * _SpecularColor * _SpecIntensity;

                //------------------------ 边缘光 ------------------------
                // 计算NoV用于计算边缘光
                float NoV = dot(i.worldNormal, viewDir);
                // 计算边缘光亮度值
                float rim = (1 - max(0, NoV)) * NoL;
                // 计算边缘光颜色
                float3 rimColor = smoothstep(_RimThreshold - _RimSmooth / 2, _RimThreshold + _RimSmooth / 2, rim) * _RimColor;

                //------------------------ 色阶 ------------------------
                // 通过亮度值计算线性ramp
                float ramp = linearstep(_RampStart, _RampStart + _RampSize, halfLambert);
                float step = ramp * _RampStep;  // 使每个色阶大小为1, 方便计算
                float gridStep = floor(step);   // 得到当前所处的色阶
                float smoothStep = smoothstep(gridStep, gridStep + _RampSmooth, step) + gridStep;
                ramp = smoothStep / _RampStep;  // 回到原来的空间
                // 得到最终的ramp色彩
                float3 rampColor = lerp(_DarkColor, _LightColor, ramp);
                rampColor *= col;
                
                // 混合颜色
                float3 finalColor = saturate(rampColor + specularColor + rimColor);
                return float4(finalColor,1);
            }
            ENDCG
        }
        
        Pass
        {
            Cull Front
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                // 法线
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            // 线条宽度
            float _OutlineWidth;
            // 线条颜色
            float4 _OutLineColor;

            v2f vert (appdata v)
            {
                v2f o;
                float4 newVertex = float4(v.vertex.xyz +  normalize(v.normal) * _OutlineWidth * 0.05,1);
                o.vertex = UnityObjectToClipPos(newVertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                return _OutLineColor;
            }
            ENDCG
        }
    }
    fallback"Diffuse"
}

猜你喜欢

转载自blog.csdn.net/liu_xueping/article/details/127909153