写在前面:

这个系列来源于书籍冯乐乐老师的《unity shader 入门精要》,
参考的代码库“https://github.com/candycat1992/Unity_Shaders_Book”
我自己跟着写了一遍代码,并在代码块里给shader代码加上了比较详细的注释
更为仔细的解释和unity shader原理和知识书里都有,本blog不做详细解释,推荐买书来看。
本项目的所有代码在https://github.com/takashiwangbh/Unity-shader-effect-reproduction/tree/main

介绍

噪声就是在规则的事物里添加一些“杂乱无章的效果”,所以噪声常常用来做一些神奇的特效,这里使用噪声模拟消融效果,水面波纹和全局雾效的随机波动。值得注意的是,我们的噪声一般是使用噪声纹理,而不是纯代码模拟,所以不管是哪一个模拟中,都会应用对应的噪声纹理。详情可以看代码里是注释。

消融效果

消融效果大概就是噪声纹理+透明度测试,使用对噪声纹理采样的结果和控制消融程度的阈值比较,如果小于阈值,就用cilp函数将对应的函数裁掉。然后使用一个改变阈值的脚本让阈值一直往上加,那么就能够看起来像是消融的效果

shader code

/*
功能说明:
1. **溶解控制**:
   - 通过 `_BurnAmount` 参数控制溶解的程度。
   - 使用 `_BurnMap` 纹理定义溶解效果的图案。

2. **燃烧线条**:
   - 燃烧边界的宽度由 `_LineWidth` 决定。
   - 燃烧线条颜色通过 `_BurnFirstColor` 和 `_BurnSecondColor` 进行渐变。

3. **光照支持**:
   - 包括环境光和漫反射光的计算。
   - 支持 Unity 的前向光照模型和阴影投射。
*/

Shader "Unity Shaders Book/Chapter 15/Dissolve" {
    Properties {
        _BurnAmount ("Burn Amount", Range(0.0, 1.0)) = 0.0 // 溶解程度控制,从0到1,1表示完全溶解。
        _LineWidth ("Burn Line Width", Range(0.0, 0.2)) = 0.1 // 燃烧线条的宽度。
        _MainTex ("Base (RGB)", 2D) = "white" {} // 物体的基础纹理。
        _BumpMap ("Normal Map", 2D) = "bump" {} // 法线贴图,用于增强表面细节的光照效果。
        _BurnFirstColor ("Burn First Color", Color) = (1, 0, 0, 1) // 燃烧线条颜色的起始颜色。
        _BurnSecondColor ("Burn Second Color", Color) = (1, 0, 0, 1) // 燃烧线条颜色的结束颜色。
        _BurnMap ("Burn Map", 2D) = "white" {} // 定义溶解图案的纹理。
    }
    SubShader {
        Tags { "RenderType"="Opaque" "Queue"="Geometry"} // 定义渲染类型和队列。

        Pass {
            Tags { "LightMode"="ForwardBase" } // 支持 Unity 的前向光照模型。
            Cull Off // 关闭剔除,渲染物体的正面和背面。

            CGPROGRAM

            #include "Lighting.cginc" // 包含 Unity 的光照函数库。
            #include "AutoLight.cginc" // 包含 Unity 自动光照处理的相关函数。

            #pragma multi_compile_fwdbase // 支持基础光照模式的多重编译。
            #pragma vertex vert // 指定顶点着色器。
            #pragma fragment frag // 指定片段着色器。

            fixed _BurnAmount; // 溶解进度控制。
            fixed _LineWidth; // 燃烧线条宽度控制。
            sampler2D _MainTex; // 基础纹理。
            sampler2D _BumpMap; // 法线贴图。
            fixed4 _BurnFirstColor; // 燃烧线条起始颜色。
            fixed4 _BurnSecondColor; // 燃烧线条结束颜色。
            sampler2D _BurnMap; // 溶解纹理。

            float4 _MainTex_ST; // 基础纹理的 UV 变换。
            float4 _BumpMap_ST; // 法线贴图的 UV 变换。
            float4 _BurnMap_ST; // 溶解纹理的 UV 变换。

            struct a2v {
                float4 vertex : POSITION; // 顶点位置。
                float3 normal : NORMAL; // 顶点法线。
                float4 tangent : TANGENT; // 顶点切线,用于法线贴图。
                float4 texcoord : TEXCOORD0; // 顶点的 UV 坐标。
            };

            struct v2f {
                float4 pos : SV_POSITION; // 顶点投影后的屏幕位置。
                float2 uvMainTex : TEXCOORD0; // 基础纹理的 UV 坐标。
                float2 uvBumpMap : TEXCOORD1; // 法线贴图的 UV 坐标。
                float2 uvBurnMap : TEXCOORD2; // 溶解纹理的 UV 坐标。
                float3 lightDir : TEXCOORD3; // 光源方向。
                float3 worldPos : TEXCOORD4; // 世界空间的顶点位置。
                SHADOW_COORDS(5) // 阴影坐标。
            };

            // 顶点着色器:计算各项数据并传递给片段着色器。
            v2f vert(a2v v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex); // 转换到投影空间。

                o.uvMainTex = TRANSFORM_TEX(v.texcoord, _MainTex); // 计算基础纹理的 UV。
                o.uvBumpMap = TRANSFORM_TEX(v.texcoord, _BumpMap); // 计算法线贴图的 UV。
                o.uvBurnMap = TRANSFORM_TEX(v.texcoord, _BurnMap); // 计算溶解纹理的 UV。

                TANGENT_SPACE_ROTATION; // 计算切线空间的旋转矩阵。
                o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz; // 计算切线空间的光照方向。

                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; // 转换到世界空间。

                TRANSFER_SHADOW(o); // 计算阴影信息。

                return o;
            }

            // 片段着色器:实现溶解效果的颜色计算。
            fixed4 frag(v2f i) : SV_Target {
                fixed3 burn = tex2D(_BurnMap, i.uvBurnMap).rgb; // 从溶解纹理中取样。

                clip(burn.r - _BurnAmount); // 根据 Burn Map 和 Burn Amount 控制像素是否渲染。

                float3 tangentLightDir = normalize(i.lightDir); // 归一化切线空间的光照方向。
                fixed3 tangentNormal = UnpackNormal(tex2D(_BumpMap, i.uvBumpMap)); // 解码法线。

                fixed3 albedo = tex2D(_MainTex, i.uvMainTex).rgb; // 从基础纹理中取样颜色。

                // 环境光计算
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

                // 漫反射光计算
                fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));

                // 燃烧线条颜色计算
                fixed t = 1 - smoothstep(0.0, _LineWidth, burn.r - _BurnAmount);
                fixed3 burnColor = lerp(_BurnFirstColor, _BurnSecondColor, t); // 线条颜色渐变。
                burnColor = pow(burnColor, 5); // 加强颜色对比度。

                UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos); // 光照衰减。
                fixed3 finalColor = lerp(ambient + diffuse * atten, burnColor, t * step(0.0001, _BurnAmount));

                return fixed4(finalColor, 1); // 返回最终颜色。
            }

            ENDCG
        }
    }
    FallBack "Diffuse" // 当不支持此 Shader 时,使用 Diffuse 着色器。
}

scipt code

/*
脚本名称: BurnHelper (溶解效果辅助器)
用途: 此脚本用于动态控制材质的溶解效果。通过修改 `_BurnAmount` 属性,实现物体从完整到逐渐溶解的动画。
*/

using UnityEngine;
using System.Collections;

public class BurnHelper : MonoBehaviour {

    // 需要应用溶解效果的材质
    public Material material;

    // 控制溶解速度,范围 [0.01, 1.0]
    [Range(0.01f, 1.0f)]
    public float burnSpeed = 0.3f;

    // 当前溶解程度,范围 [0.0, 1.0]
    private float burnAmount = 0.0f;

    // 初始化逻辑
    void Start () {
        // 如果没有手动指定材质,尝试从当前物体或其子物体的 Renderer 中获取材质
        if (material == null) {
            Renderer renderer = gameObject.GetComponentInChildren<Renderer>();
            if (renderer != null) {
                material = renderer.material; // 获取 Renderer 的材质
            }
        }

        // 如果未能获取材质,则禁用该脚本
        if (material == null) {
            this.enabled = false; // 禁用脚本,防止报错
        } else {
            material.SetFloat("_BurnAmount", 0.0f); // 初始化溶解程度为 0
        }
    }
    
    // 每帧更新逻辑
    void Update () {
        // 使用 Time.time 和 burnSpeed 动态计算当前的溶解程度
        burnAmount = Mathf.Repeat(Time.time * burnSpeed, 1.0f);
        // 更新材质的 _BurnAmount 属性
        material.SetFloat("_BurnAmount", burnAmount);
    }
}

效果展示

请添加图片描述

水波效果

同样也可以使用噪声纹理来模拟实时水面,噪声纹理会用作一个高度图,不断修改水面法线方向,使用和时间相关的变量对噪声纹理进行采样,得到法线信息以后进行反射+折射计算得到水面波纹效果

shader code

/*
功能说明:
1. **水波扰动**:
   - 使用噪声纹理控制水面的动态法线变化,实现水波效果。
   - 控制波动的速度和幅度。

2. **折射效果**:
   - 使用屏幕抓取纹理模拟光线穿过水面的折射现象。
   - 根据法线扰动计算水面折射后的图像偏移。

3. **反射效果**:
   - 使用环境立方体贴图模拟水面的反射。
   - 结合法线和视角方向计算反射颜色。

4. **菲涅尔效应**:
   - 根据观察角度混合反射和折射,实现真实的水面外观。

*/

Shader "Noise-WaterWave" {
	Properties {
		_Color ("Main Color", Color) = (0, 0.15, 0.115, 1) // 主颜色,用于整体的颜色调节
		_MainTex ("Base (RGB)", 2D) = "white" {} // 基础颜色纹理
		_WaveMap ("Wave Map", 2D) = "bump" {} // 水波扰动的法线贴图
		_Cubemap ("Environment Cubemap", Cube) = "_Skybox" {} // 环境立方体贴图
		_WaveXSpeed ("Wave Horizontal Speed", Range(-0.1, 0.1)) = 0.01 // 水波横向速度
		_WaveYSpeed ("Wave Vertical Speed", Range(-0.1, 0.1)) = 0.01 // 水波纵向速度
		_Distortion ("Distortion", Range(0, 100)) = 10 // 水波扰动强度
	}
	SubShader {
		// 定义渲染顺序为透明队列,其他物体需先绘制
		Tags { "Queue"="Transparent" "RenderType"="Opaque" }
		
		// 抓取屏幕作为折射纹理
		GrabPass { "_RefractionTex" }
		
		Pass {
			Tags { "LightMode"="ForwardBase" }
			
			CGPROGRAM
			
			#include "UnityCG.cginc"
			#include "Lighting.cginc"
			
			#pragma multi_compile_fwdbase
			#pragma vertex vert
			#pragma fragment frag
			
			// 参数声明
			fixed4 _Color;
			sampler2D _MainTex;
			float4 _MainTex_ST;
			sampler2D _WaveMap;
			float4 _WaveMap_ST;
			samplerCUBE _Cubemap;
			fixed _WaveXSpeed;
			fixed _WaveYSpeed;
			float _Distortion;	
			sampler2D _RefractionTex;
			float4 _RefractionTex_TexelSize;
			
			// 顶点着色器
			struct a2v {
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float4 tangent : TANGENT; 
				float4 texcoord : TEXCOORD0;
			};
			
			struct v2f {
				float4 pos : SV_POSITION; // 顶点位置
				float4 scrPos : TEXCOORD0; // 屏幕空间坐标
				float4 uv : TEXCOORD1; // UV 坐标
				float4 TtoW0 : TEXCOORD2;  
				float4 TtoW1 : TEXCOORD3;  
				float4 TtoW2 : TEXCOORD4; 
			};
			
			v2f vert(a2v v) {
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex); // 转换顶点位置到裁剪空间
				o.scrPos = ComputeGrabScreenPos(o.pos); // 计算屏幕空间坐标
				o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex); // 主纹理 UV
				o.uv.zw = TRANSFORM_TEX(v.texcoord, _WaveMap); // 波纹法线贴图 UV
				
				// 计算世界空间的切线、法线和副法线
				float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;  
				fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);  
				fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);  
				fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w; 
				
				// 存储切线到世界的变换矩阵
				o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);  
				o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);  
				o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);  
				
				return o;
			}
			
			// 像素着色器
			fixed4 frag(v2f i) : SV_Target {
				// 计算视线方向
				float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
				fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
				
				// 计算水波扰动
				float2 speed = _Time.y * float2(_WaveXSpeed, _WaveYSpeed);
				fixed3 bump1 = UnpackNormal(tex2D(_WaveMap, i.uv.zw + speed)).rgb;
				fixed3 bump2 = UnpackNormal(tex2D(_WaveMap, i.uv.zw - speed)).rgb;
				fixed3 bump = normalize(bump1 + bump2); // 组合扰动法线
				
				// 根据扰动计算 UV 偏移
				float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy;
				i.scrPos.xy = offset * i.scrPos.z + i.scrPos.xy;
				fixed3 refrCol = tex2D(_RefractionTex, i.scrPos.xy/i.scrPos.w).rgb;
				
				// 计算反射方向
				bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));
				fixed4 texColor = tex2D(_MainTex, i.uv.xy + speed);
				fixed3 reflDir = reflect(-viewDir, bump);
				fixed3 reflCol = texCUBE(_Cubemap, reflDir).rgb * texColor.rgb * _Color.rgb;
				
				// 菲涅尔效应
				fixed fresnel = pow(1 - saturate(dot(viewDir, bump)), 4);
				fixed3 finalColor = reflCol * fresnel + refrCol * (1 - fresnel);
				
				return fixed4(finalColor, 1);
			}
			
			ENDCG
		}
	}
	// 不渲染阴影
	FallBack Off
}

效果展示

图像太大了传不上来,可以在github仓库里查看或者clone我的仓库在unity运行也能查看

全局雾效

之前实现过雾效的效果,现在是实现一个基于深度和噪声的全局雾效。通过结合相机的深度纹理和噪声纹理,动态计算雾气的浓度和变化,最终生成带有随机性的自然雾效。当然也包括一些属性,包括雾的密度、颜色、起始和结束距离,以及噪声的影响。

shader code


Shader "Noise - Global fog effect" {
    Properties {
        _MainTex ("Base (RGB)", 2D) = "white" {} // 场景基础纹理
        _FogDensity ("Fog Density", Float) = 1.0 // 雾气浓度
        _FogColor ("Fog Color", Color) = (1, 1, 1, 1) // 雾的颜色
        _FogStart ("Fog Start", Float) = 0.0 // 雾效开始的距离
        _FogEnd ("Fog End", Float) = 1.0 // 雾效结束的距离
        _NoiseTex ("Noise Texture", 2D) = "white" {} // 噪声纹理,用于动态变化
        _FogXSpeed ("Fog Horizontal Speed", Float) = 0.1 // 雾气水平流动速度
        _FogYSpeed ("Fog Vertical Speed", Float) = 0.1 // 雾气垂直流动速度
        _NoiseAmount ("Noise Amount", Float) = 1 // 噪声对雾气的影响强度
    }
    SubShader {
        CGINCLUDE
        
        #include "UnityCG.cginc"

        // 定义全局矩阵和纹理采样器
        float4x4 _FrustumCornersRay; // 用于计算屏幕空间到世界空间的光线方向

        sampler2D _MainTex; // 场景基础纹理
        half4 _MainTex_TexelSize; // 主纹理的像素大小
        sampler2D _CameraDepthTexture; // 相机的深度纹理
        half _FogDensity; // 雾气浓度
        fixed4 _FogColor; // 雾的颜色
        float _FogStart; // 雾效开始的距离
        float _FogEnd; // 雾效结束的距离
        sampler2D _NoiseTex; // 噪声纹理
        half _FogXSpeed; // 雾气水平移动速度
        half _FogYSpeed; // 雾气垂直移动速度
        half _NoiseAmount; // 噪声对雾气影响的强度

        // 定义顶点与片元之间的结构体
        struct v2f {
            float4 pos : SV_POSITION; // 顶点的屏幕坐标
            float2 uv : TEXCOORD0; // 主纹理的UV坐标
            float2 uv_depth : TEXCOORD1; // 深度纹理的UV坐标
            float4 interpolatedRay : TEXCOORD2; // 从屏幕空间到世界空间的光线
        };

        // 顶点着色器,用于计算光线方向和UV坐标
        v2f vert(appdata_img v) {
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex); // 转换为屏幕空间坐标
            
            // 计算纹理的UV坐标
            o.uv = v.texcoord;
            o.uv_depth = v.texcoord;

            #if UNITY_UV_STARTS_AT_TOP
            if (_MainTex_TexelSize.y < 0)
                o.uv_depth.y = 1 - o.uv_depth.y; // 修正UV坐标
            #endif
            
            // 根据顶点的位置,选择插值光线方向
            int index = 0;
            if (v.texcoord.x < 0.5 && v.texcoord.y < 0.5) {
                index = 0;
            } else if (v.texcoord.x > 0.5 && v.texcoord.y < 0.5) {
                index = 1;
            } else if (v.texcoord.x > 0.5 && v.texcoord.y > 0.5) {
                index = 2;
            } else {
                index = 3;
            }
            #if UNITY_UV_STARTS_AT_TOP
            if (_MainTex_TexelSize.y < 0)
                index = 3 - index; // 修正索引
            #endif
            
            o.interpolatedRay = _FrustumCornersRay[index]; // 获取光线方向
                 
            return o;
        }

        // 片元着色器,用于计算最终的雾效颜色
        fixed4 frag(v2f i) : SV_Target {
            // 线性深度,转换屏幕深度值为世界深度值
            float linearDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth));
            float3 worldPos = _WorldSpaceCameraPos + linearDepth * i.interpolatedRay.xyz; // 世界空间位置
            
            // 计算噪声影响
            float2 speed = _Time.y * float2(_FogXSpeed, _FogYSpeed); // 动态噪声移动速度
            float noise = (tex2D(_NoiseTex, i.uv + speed).r - 0.5) * _NoiseAmount; // 获取噪声值
            
            // 雾气浓度,根据深度和噪声调整
            float fogDensity = (_FogEnd - worldPos.y) / (_FogEnd - _FogStart); 
            fogDensity = saturate(fogDensity * _FogDensity * (1 + noise)); // 最终浓度
            
            // 混合雾的颜色与场景颜色
            fixed4 finalColor = tex2D(_MainTex, i.uv); // 场景颜色
            finalColor.rgb = lerp(finalColor.rgb, _FogColor.rgb, fogDensity); // 混合结果
            
            return finalColor; // 返回最终颜色
        }
        
        ENDCG
        
        // 渲染通道
        Pass {              
            CGPROGRAM  
            
            #pragma vertex vert  
            #pragma fragment frag  
              
            ENDCG
        }
    } 
    FallBack Off // 无需降级到默认Shader
}
   

请添加图片描述

题外话

至此,对于冯乐乐老师的《unity shader 入门精要》的书籍的效果演示以及代码解释已经全部完了,具体是书的前15章。后面还有几章拓展的就不做效果展示了,此外我将所有的效果也在个人网页和github仓库展示,并且github仓库直接就可以clone下来用unity运行就能够看到所有的效果了,当然,这本书籍也有自己的代码仓库,也是有所有的效果的。做整理的过程本身也就是在学习,对于这种比较入门且经典的书籍,这个过程是值得的,并且也让我确实懂了不少shader的知识,如果在这上面每一块上都去拓展学习,那会是一个很有趣且丰富的过程。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐