Skip to main content

Zhuangdong_Course_Note_VFXShader

4105 words·20 mins
Table of Contents

13 Four Modes Related to Transparent Effects#

1. Alpha Blend (AB)
#

Usage
#

  • Objects with complex outlines and no clear edges
  • Semi-transparent objects
  • General base for effects

Advantages
#

Good performance on mobile, natural edge effect

Disadvantages
#

Has sorting issues

Key Code
#

Shader "Zhuangdong/AP1/L07/L07_AB" {
    Properties{
        _MainTex ("RGB: Color A: Alpha", 2D) = "white" {}
        _Opacity ("Opacity", range(0,1)) = 0.5
    }
    SubShader{
        Tags {
            "Queue" = "Transparent"
            // Render transparent objects last to prevent objects behind from disappearing
            "RenderType" = "Transparent"
            "ForceNoShadowCasting" = "True" // Disable shadow casting
            "IgnoreProject" = "True" // Does not affect projector
        }
        Pass {
            Name "FORWARD"
            Tags {
                "LightMode" = "ForwardBase"
            }
            Blend One OneMinusSrcAlpha // Declare blend mode
            CGPROGRAM 
            #pragma vertex vert 
            #pragma fragment frag 
            #include "UnityCG.cginc" 
            #pragma multi_compile_fwdbase_fullshadows
            #pragma target 3.0 
            uniform sampler2D _MainTex; uniform float4 _MainTex_ST;
            uniform half _Opacity;
            struct VertexInput { 
                float4 vertex : POSITION;
                float2 uv0 : TEXCOORD0;
            };
            struct VertexOutput { 
                float4 pos : SV_POSITION;
                float2 uv0 : TEXCOORD0;
            };
            VertexOutput vert(VertexInput v) { 
                VertexOutput o = (VertexOutput)0;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv0 = TRANSFORM_TEX(v.uv0, _MainTex);
                return o;
            }
            half4 frag(VertexOutput i) : COLOR{ // Standard writing
                half4 var_MainTex = tex2D(_MainTex, i.uv0);
                half3 finalRGB = var_MainTex.rgb;
                half opacity = var_MainTex.a * _Opacity;
                return half4(finalRGB * opacity, opacity);
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

L13_AB


2. Additive (AD)
#

Usage
#

  • Luminous objects, glow effects
  • Brightening effects

Disadvantages
#

  • Has sorting issues
  • Multiple layers easily cause overdraw
  • For glow effects, usually can be replaced by post-processing

Key Code
#

// Use AB example as template
...
SubShader{
    ...
    pass{
        ...
        Blend One One
        // Just change the BlendMode from the AB example
        ...
    }
    ...
}

L13_AD


3. Alpha CutOut (AC)
#

Usage
#

  • Complex outlines, clear edges (e.g. leaves, hair, metal hollow-outs)
  • Cartoon effects (with anti-aliasing)

Advantages
#

No sorting issues

Disadvantages
#

Edges are harsh, poor performance on mobile

Key Code
#

// Use AB example as template
...
SubShader{
    Tags{
        "RenderType" = "TransparentCutout" // Corresponds to AC
        "ForceNoShadowCasting" = "True"
        "IgnoreProject" = "True"
    }
    pass{
        ...
        half4 frag(VertexOutput i) : COLOR{ 
            half4 var_MainTex = tex2D(_MainTex, i.uv0);
            half opacity = var_MainTex.a;
            clip(opacity - _Cutoff); // Alpha cutout
            return half4(var_MainTex.rgb, 1.0);
        }
    }
    ...
}

L13_AC


4. Custom Blending
#

Usage
#

Custom blend formulas for flexible special effects

Combination
#

Src * SrcFactor op Dst * DstFactor

Elements
#

  • Src: Source, the current shader drawing result
  • Dst: Destination, the background before the current shader draws
  • SrcFactor: Source multiplier
  • DstFactor: Destination multiplier
  • op: Blend operator

The multipliers determine how the elements participate in blending, and the operator determines the form of blending. For details, please refer to the Unity official documentation.

Key Code
#

...
Pass {
    Name "FORWARD"
    Tags {
        "LightMode" = "ForwardBase"
    }
    BlendOp [_BlendOp]
    Blend [_BlendSrc] [_BlendDst]
    // Add the formula after Tags
...

15 Common Issues
#

1. Sorting Issue
#

The front and back relationship is unclear when rendering transparent objects

Solutions
#

  • Detach/Attach (modify model vertex order with DDC software)
  • ZWrite Off (disable depth writing)

2. Premultiplied Texture Issue
#

Some texture assets are premultiplied (BaseColor * Alpha), some are not, so the blend mode needs to be distinguished

  • AB mode: For premultiplied, use One OneMinusSrcAlpha; for non-premultiplied, use SrcAlpha OneMinusSrcAlpha, or multiply in frag
  • AD mode: Premultiplied can have no A channel, non-premultiplied needs to multiply in frag

3. Example: GhostFlow
#

// Use AB as template
 Shader "Zhuangdong/AP1/L08/L08_GhostFlow" {
            Properties{
                _MainTex ("RGB: Color A: Alpha", 2D) = "gray" {}
                _Opacity ("Opacity", range(0,1)) = 0.5
                _WarpTex ("Distortion Map", 2D) = "gray" {}
                _WarpInt ("Distortion Intensity", range(0, 1)) = 0.5
                _NoiseInt("Noise Intensity", range(0, 5)) = 0.5
                _FlowSpeed ("Flow Speed", range(0, 10)) = 5
            }
            SubShader{
                ...
                Pass {
                    ...
                    Blend One OneMinusSrcAlpha
                    ...
                    struct VertexInput { 
                        float4 vertex : POSITION;
                        float2 uv : TEXCOORD0;
                    };
                    struct VertexOutput { 
                        float4 pos : SV_POSITION;
                        float2 uv0 : TEXCOORD0;
                        float2 uv1 : TEXCOORD1;
                    };
                    VertexOutput vert(VertexInput v) { 
                        VertexOutput o = (VertexOutput)0;
                        o.pos = UnityObjectToClipPos(v.vertex); 
                        o.uv0 = v.uv;
                        o.uv1 = TRANSFORM_TEX(v.uv, _WarpTex); // Enable Tiling
                        o.uv1.y = o.uv1.y + frac(_Time.x * _FlowSpeed); // Start V axis flow
                        return o;
                    }
                    half4 frag(VertexOutput i) : COLOR{ 
                        half3 var_WarpTex = tex2D(_WarpTex, i.uv1);
                        half2 uvBias = (var_WarpTex.rg - 0.5) * _WarpInt;
                        half2 uv0 = i.uv0 + uvBias;
                        half4 var_MainTex = tex2D(_MainTex, uv0);

                        half3 finalRGB = var_MainTex.rgb;
                        half noise = lerp(1.0, var_WarpTex.b * 2.0, _NoiseInt);
                        noise = max(0.0, noise);
                        half opacity = var_MainTex.a * _Opacity * noise;
                        return half4(finalRGB * opacity, opacity);
                    }
                    ENDCG
                }
            }
            FallBack "Diffuse"
        }

L15_GhostFlow

Core Idea
#

By using a distortion map and flow speed parameter, achieve dynamic changes in transparent effects

o.uv1.y = o.uv1.y + frac(_Time.x * _FlowSpeed); // V axis flow
half opacity = var_MainTex.a * _Opacity * noise;
  • _Time: A parameter that increases with time, with x, y, z, w components representing different speed levels
  • _FlowSpeed controls the flow speed
  • frac takes the fractional part, used here to prevent _Time from growing indefinitely and causing moiré patterns
  • noise = lerp(1.0, var_WarpTex.b * 2.0, _NoiseInt) ensures brightness does not darken
  • _NoiseInt The higher this value, the clearer the noise; when above 1, noise contrast increases
  • max(0.0, noise) prevents negative values that cause color anomalies

4. Example: GhostWarp
#

// Use GhostFlow as template
...
half3 var_WarpTex = tex2D(_WarpTex, i.uv1);
half2 uvBias = (var_WarpTex.rg - 0.5) * _WarpInt;
half2 uv0 = i.uv0 + uvBias;
half4 var_MainTex = tex2D(_MainTex, uv0);
...

Principle
#

The rg channels of WarpTex control the direction of UV distortion, and -0.5 ensures even distribution of distortion

Note
#

When WarpInt is too large, the distortion will be abrupt. This is caused by large grayscale differences between the bright and dark sides

L15_GhostWarp


14, 16 Flame and Water Ripple Effect Cases
#

Flame Effect
#

Key Code
#

// Use GhostFlow as template
Shader "Zhuangdong/AP1/L09/L09_Fire" {
            Properties{
                _Mask ("R:Outer Flame G:Inner Flame B:Alpha", 2D) = "blue" {}
                _Noise ("R:Noise1 G:Noise2", 2D) = "gray" {}
                _Noise1Params ("Noise1 x:Scale y:Speed z:Intensity", vector) = (1.0, 0.2, 0.2, 1.0)
                _Noise2Params ("Noise2 x:Scale y:Speed z:Intensity", vector) = (1.0, 0.2, 0.2, 1.0)
                _color1 ("Outer Flame Color", Color) = (1,1,1,1)
                _color2 ("Inner Flame Color", Color) = (1,1,1,1)
            }
            SubShader{
                ...
                Pass {
                    ...
                    Blend One OneMinusSrcAlpha
                    ...

                    struct VertexInput { 
                        float4 vertex : POSITION;
                        float2 uv : TEXCOORD0;
                    };
                    struct VertexOutput { 
                        float4 pos : SV_POSITION;
                        float2 uv0 : TEXCOORD0;
                        float2 uv1 : TEXCOORD1;
                        float2 uv2 : TEXCOORD2;
                    };
                    VertexOutput vert(VertexInput v) { 
                        VertexOutput o = (VertexOutput)0;
                        o.pos = UnityObjectToClipPos(v.vertex); 
                        o.uv0 = v.uv;
                        o.uv1 = v.uv * _Noise1Params.x - float2(0.0, frac(_Time.x * _Noise1Params.y));
                        o.uv2 = v.uv * _Noise2Params.x - float2(0.0, frac(_Time.x * _Noise2Params.y));
                        return o;
                    }
                    half4 frag(VertexOutput i) : COLOR{ 
                        half warpMask = tex2D(_Mask, i.uv0).b;
                        half var_Noise1 = tex2D(_Noise, i.uv1).r;
                        half var_Noise2 = tex2D(_Noise, i.uv2).g;
                        half noise = var_Noise1 * _Noise1Params.z + 
                                     var_Noise2 * _Noise2Params.z;
                        float2 warpUV = i.uv0 - float2(0.0, noise) * warpMask;
                        half3 var_Mask = tex2D(_Mask, warpUV);
                        half3 finalRGB = _color1 * var_Mask.r + _color2 * var_Mask.g;
                        half opacity = var_Mask.r + var_Mask.g;
                        return half4(finalRGB, opacity);
                    }
                    ENDCG
                }
            }
            FallBack "Diffuse"
        }

Principle
#

Two layers of noise overlay to control flame dynamics, and use mask to customize the color of inner and outer flames

Key Points
#

  • _NoiseParams.x controls tiling
  • _NoiseParams.y controls speed
  • _NoiseParams.z controls distortion intensity
  • float2(0,0, frac()) If v.uv - frac(), the texture will flow diagonally, because frac is automatically recognized as float2(frac(), frac())

L14_Fire

Water Ripple Effect
#

Key Code
#

// Use Fire as template
Shader "Zhuangdong/AP1/L09/L09_Water" {
    Properties{
        _MainTex ("Color Texture", 2D) = "white" {}
        _WarpTex ("Distortion Map", 2D) = "gray" {}
        _Speed ("x: Speed x y: Speed y", vector) = (1.0, 1.0, 0.5, 1.0)
        _Warp1Params ("Noise1 x: Scale y: Speed x z: Speed y w: Intensity", vector) = (1.0, 0.2, 0.2, 1.0)
        _Warp2Params ("Noise2 x: Scale y: Speed x z: Speed y w: Intensity", vector) = (1.0, 0.2, 0.2, 1.0)
    }
    SubShader{
        Tags {
            "RenderType" = "Opaque"
        }
        Pass {
            Name "FORWARD"
            Tags {
                "LightMode" = "ForwardBase"
            }
            ...
            VertexOutput vert(VertexInput v) {
                VertexOutput o = (VertexOutput)0;
                o.pos = UnityObjectToClipPos(v.vertex); 
                o.uv0 = v.uv - frac(_Time.x * _Speed); // Main texture flow
                o.uv1 = v.uv * _Warp1Params.x - frac(_Time.x * _Warp1Params.yz); // Distortion map 1 flow
                o.uv2 = v.uv * _Warp2Params.x - frac(_Time.x * _Warp2Params.yz); // Distortion map 2 flow
                return o;
            }
            float4 frag(VertexOutput i) : COLOR{ 
                half3 var_Warp1 = tex2D(_WarpTex, i.uv1).rgb; // Distortion value 1
                half3 var_Warp2 = tex2D(_WarpTex, i.uv2).rgb; // Distortion value 2
                half2 warp = (var_Warp1.xy - 0.5) * _Warp1Params.w +
                            (var_Warp2.xy - 0.5) * _Warp2Params.w;
                float2 warpUV = i.uv0 + warp; // Add distortion value
                half4 var_MainTex = tex2D(_MainTex, warpUV);
                return float4(var_MainTex.xyz, 1.0);
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

Principle
#

Main texture UV plus two layers of noise to achieve water surface ripple effect

L16_Water


17 ScreenUV & ScreenWarp
#

ScreenUV
#

By sampling in screen space UV, achieve effects where the texture changes and flows with camera distance.

The key is to use view space coordinates to correct distortion and overlay flow effects.

Key Code
#

// Use AB as template
...
VertexOutput vert(VertexInput v) { 
    VertexOutput o = (VertexOutput)0;
        o.pos = UnityObjectToClipPos(v.vertex); 
        o.uv = v.uv; 
        float3 posVS = UnityObjectToViewPos(v.vertex).xyz; // Vertex position OS to VS
        o.screenUV = posVS.xy / posVS.z; // VS space distortion correction
        float originDist = UnityObjectToViewPos(float3(0.0, 0.0, 0.0)).z; // Origin position
        o.screenUV *= originDist; // o.screenUV * originDist
        o.screenUV = o.screenUV * _ScreenTex_ST.xy - frac(_Time.x * _ScreenTex_ST.zw); // Add tiling, offset, and flow
    return o;
}
half4 frag(VertexOutput i) : COLOR{ 
    half4 var_MainTex = tex2D(_MainTex, i.uv);
    half var_ScreenTex = tex2D(_ScreenTex, i.screenUV).r;
    half3 finalRGB = var_MainTex.rgb;
    half opacity = var_MainTex.a * _Opacity * var_ScreenTex;
    return half4(finalRGB * opacity, opacity);
}
...

Key Points
#

  • o.screenUV view space corrected UV, prevents depth distortion
  • originDist controls texture scaling by origin distance
  • time overlays flow effect

L17_ScreenUV

ScreenWarp
#

Use GrabPass to get the background, and use a channel of the main texture to distort the screen UV, achieving background distortion in semi-transparent areas (similar to Photoshop overlay).

Key Code
#

Shader "Zhuangdong/AP1/L10/L10_ScreenWarp" {
    Properties{
        _MainTex ("RGB: Color A: Alpha", 2D) = "white" {}
        _Opacity ("Opacity", range(0,1)) = 0.5
        _WarpMidVal ("Distortion Mid Value", range(0, 1)) = 0.5
        _WarpInt ("Distortion Intensity", range(0, 3)) = 0.2
    }
    SubShader{
        ...
        GrabPass{
            "_BGTex"
        }
        ...
        uniform sampler2D _BGTex;
        ...
            struct VertexOutput { 
                float4 pos : SV_POSITION;
                float2 uv0 : TEXCOORD0;
                float4 grabPos : TEXCOORD1; // Background sampling coordinates
            };
            VertexOutput vert(VertexInput v) { 
                VertexOutput o = (VertexOutput)0;
                o.pos = UnityObjectToClipPos(v.vertex); 
                o.uv0 = TRANSFORM_TEX(v.uv0, _MainTex);
                o.grabPos = ComputeGrabScreenPos(o.pos); // Background texture sampling coordinates for grabpass
                return o;
            }
            half4 frag(VertexOutput i) : COLOR{ 
                half4 var_MainTex = tex2D(_MainTex, i.uv0);
                i.grabPos.xy += (var_MainTex.b - _WarpMidVal) * _WarpInt * _Opacity;
                half3 var_BGTex = tex2Dproj(_BGTex, i.grabPos);
                // tex2Dproj is a command specifically for sampling this type of texture
                half3 finalRGB = var_MainTex.rgb * var_BGTex;
                half opacity = var_MainTex.a * _Opacity;
                return half4(finalRGB * opacity, opacity);
        ...

Key Points
#

  • GrabPass gets the background and saves it in BGTex
  • WarpMidVal adjusts the UV sampling position
  • _Opacity is bound to i.grabPos, the lower the opacity, the more obvious the distortion

Similar Methods
#

  • GrabPass: Suitable for high quality but high performance cost
  • CommandBuffer: Custom pipeline method before SRP era
  • Lwrp/Urp: Custom pipeline method after SRP era (recommended)

L17_ScreenWarp

18. Sequence Animation (Sequence)
#

Principle of Sequence Animation
#

In effect production, a sequence texture containing multiple frames is often used, with each frame representing a state of the animation. By switching the UV sampling area, the animation playback effect is achieved.

  • The sample texture is 3 rows and 4 columns, with each frame arranged in order.
  • The effect layer floats above the object surface (extruded along the normal direction by the vertex).
  • The UV origin is at the top left, and the initial sampling area needs to be adjusted.

L18_Sequence

Key Code
#

// Add a pass based on the AB example
...
pass{
Name "FORWARD"
Tags {
    "LightMode" = "ForwardBase"
}
Blend One One
...
VertexOutput vert(VertexInput v) { 
    VertexOutput o = (VertexOutput)0;
        // Extrude vertex along normal direction
        v.vertex.xyz += v.normal * 0.01;
        o.pos = UnityObjectToClipPos(v.vertex); 
        o.uv = TRANSFORM_TEX(v.uv, _Sequence);
        // Calculate current frame index
        float id = floor(_Time.z * _Speed);
        float idV = floor(id / _ColCount);
        float idU = id - idV * _ColCount;
        float stepU = 1.0 / _ColCount;
        float stepV = 1.0 / _RowCount;
        // UV scaling and adjust origin to top left
        float2 initUV = o.uv * float2(stepU, stepV) + float2(0.0, stepV * (_RowCount -1));
        o.uv = initUV + float2(idU * stepU, -idV * stepV);
    return o;
}
half4 frag(VertexOutput i) : COLOR{ 
    half4 var_Sequence = tex2D(_Sequence, i.uv);
    half3 finalRGB = var_Sequence.rgb;
    half opacity = var_Sequence.a;
    return half4(finalRGB * opacity, opacity);
}
ENDCG
...

Key Points Summary
#

  • floor rounds down, e.g. 1.9 outputs 1
  • id moves horizontally by 1/_ColCount each time
  • idV = floor(id / _ColCount) when id is an integer multiple of the column count, idV increases by 1
  • idU = id - idV * _ColCount moves vertically by 1/_RowCount for each full row, and idU resets to zero
  • float2(idU * stepU, -idV * stepV) moves by 1/_ColCount each time, when the number of moves equals _ColCount, idU resets to the leftmost, and moves vertically by 1/_RowCount

Note: Effect algorithms should be as simple as possible to avoid overdraw.


Polar Coordinate Animation (PolarCoord)
#

Polar coordinate transformation can achieve radial flow, scanning and other effects. By converting UV from Cartesian coordinates to polar coordinates and overlaying time flow, special animation effects are achieved.

Polar Coordinates
#

Assume an x-axis, connect the origin to point M, the angle between OM and the x-axis is θ, OM length is P, polar coordinates are (θ, P)

L18_PolarC

Key Code
#

half4 frag(VertexOutput i) : COLOR {
    i.uv = i.uv - float2(0.5, 0.5);
    float theta = atan2(i.uv.y, i.uv.x);
    theta = theta / 3.1415926 * 0.5 + 0.5;
    float p = length(i.uv) + frac(_Time.x * 3);
    i.uv = float2(theta, p);

    half4 var_MainTex = tex2D(_MainTex, i.uv);
    half3 finalRGB = (1 - var_MainTex.rgb);
    half opacity = (1 - var_MainTex.r) * _Opacity * i.color.r;
    return half4(finalRGB * opacity, opacity);
}

L18_PolarCoord

Key Points Summary
#

  • atan2 calculates angle θ, result is (-π, π), normalized to [0,1].

L18_Atan2

  • length calculates the distance from point (x, y) to the origin. Since the origin is shifted from (0,0) to (0.5, 0.5), the output will look like this

L18_Length

  • float2(theta, p) maps UV to polar coordinates to achieve radial animation.
  • i.color vertex color is used to soften the edge.

19. Vertex Animation
#

19.1 Translation
#

Use sine function to make the vertex move periodically in the Y axis direction, achieving an overall up and down floating effect.

Key Code
#

// Use AB as template
...
#define TWO_PI 6.283185

void Translation (inout float3 vertex) {
    vertex.y += _MoveRange * sin(frac(_Time.z * _MoveSpeed) * TWO_PI);
}

VertexOutput vert(VertexInput v) { 
    VertexOutput o = (VertexOutput)0;
    Translation(v.vertex.xyz);
    o.pos = UnityObjectToClipPos(v.vertex);
    o.uv0 = v.uv0;
    return o;
}

L19_Translation

Key Points
#

  • frac Fractional part, takes the decimal part, ensures the time parameter cycles in [0,1], prevents overflow.
  • sin sin(0, 2π) is a periodic motion from 0 to 1 and back to 0.

19.2 Scale
#

All vertices are scaled by the same ratio.

Key Code
#

// Use AB as template
void Scaling (inout float3 vertex) {
    vertex.xyz *= 1.0 + _ScaleRange * sin(frac(_Time.z * _ScaleSpeed) * TWO_PI);
}

VertexOutput vert(VertexInput v) {
    VertexOutput o = (VertexOutput)0;
    Scaling(v.vertex.xyz);
    o.pos = UnityObjectToClipPos(v.vertex);
    o.uv0 = TRANSFORM_TEX(v.uv0, _MainTex);
    return o;
}

L19_Scaling

Key Points
#

Ensure the scale value is greater than 0 to avoid reverse scaling.


19.3 Rotation
#

Vertices rotate periodically around the Y axis, achieving effects like head shaking and swinging.

Key Code
#

// Use AB as template
void Rotation (inout float3 vertex) {
    float angleY = _RotateRange * sin(frac(_Time.z * _Rotatepeed) * TWO_PI);
    float radY = radians(angleY);
    float sinY, cosY = 0;
    sincos(radY, sinY, cosY);
    vertex.xz = float2(
        vertex.x * cosY - vertex.z * sinY,
        vertex.x * sinY + vertex.z * cosY
    );
}

L19_Rotation

Key Points
#

  • radians converts degrees to radians, which improves performance
  • sincos(raY, sinY, cosY) is equivalent to sinY = sin(radY), cosY = cos(radY), this writing improves performance.
  • vertex.xz rotation matrix M (x * cosθ - y * sinθ, x * sinθ + y * cosθ)

19.4 Composite Animation (AnimationGhost)
#

Combine scaling, translation, rotation and other animations, and use vertex color to achieve complex movements (such as ghosts, angel rings, etc).

Key Code
#

...
// Use AB as template
void AnimGhost (inout float3 vertex, inout float3 color){
    // 天使圈缩放
    float scale = _ScaleParams.x * color.g * sin(frac(_Time.z * _ScaleParams.y) * TWO_PI);
    vertex.xyz *= 1.0 + scale;
    vertex.y -= _ScaleParams.z * -scale;
    // 幽灵摆动
    float swingX = _SwingXparams.x * sin(frac(_Time.z * _SwingXparams.y + vertex.y * _SwingXparams.z) * TWO_PI);
    float swingZ = _SwingZparams.x * sin(frac(_Time.z * _SwingZparams.y + vertex.y * _SwingZparams.z) * TWO_PI);
    vertex.xz += float2(swingX, swingZ) * color.r;
    // 幽灵摇头
    float radY = radians(_ShakeYparams.x) * (1.0 - color.r) * sin(frac(_Time.z * _ShakeYparams.y - color.g * _ShakeYparams.z) * TWO_PI);
    float sinY, cosY = 0;
    sincos(radY, sinY, cosY);
    vertex.xz = float2(
        vertex.x * cosY - vertex.z * sinY,
        vertex.x * sinY + vertex.z * cosY
    );
    // 幽灵起伏
    float swingY = _SwingYparams.x * sin(frac(_Time.z * _SwingYparams.y - color.g * _SwingYparams.z) * TWO_PI);
    vertex.y += swingY;
    // 处理顶点色
    float lightness = 1.0 + color.g * 1.0 + scale * 2.0;
    color = float3(lightness, lightness, lightness);
}
...

L19_AnimGhost

Key Points
#

  • Use vertex color R, G channels to control different animation areas.
  • vertex.y -= _ScaleParams.z * -scale is to scale around the model origin, control y axis scale amplitude, avoid angel ring leaving origin
  • vertex.y * _SwingXparams.z sin result is affected by vertex.y, achieve S-shaped swing
  • _ShakeYoarams.z _SwingYparams.z make angel ring and other parts of animation, produce time lag
  • lightness brightness changes with time

20. Clock Animation (ClockAnim)
#

20.1 Clock Pointer Animation
#

By C# script obtaining system time, driving Clock pointer rotation in Shader, achieving real clock animation.

Key Code
#

...
void RotateZwithOffset(float angle, float offset, float mask, inout float3 vertex){
    vertex.y -= offset * mask;
    float radZ = radians(angle * mask);
    float sinZ, cosZ = 0;
    sincos(radZ, sinZ, cosZ);
    vertex.xy = float2(
        vertex.x * cosZ - vertex.y * sinZ,
        vertex.x * sinZ + vertex.y * cosZ
    );
    vertex.y += offset * mask;
}

void ClockAnim(float3 color, inout float3 vertex) {
    RotateZwithOffset(_HourHandAngle, _RotateOffset, color.r, vertex);
    RotateZwithOffset(_MinuteHandAngle, _RotateOffset, color.g, vertex);
    RotateZwithOffset(_SecondHandAngle, _RotateOffset, color.b, vertex);
}
VertexOutput vert(VertexInput v) { 
    VertexOutput o = (VertexOutput)0;
        ClockAnim(v.color.rgb, v.vertex.xyz);
...

L20_Clock

Key Points
#

  • angle represents the rotation angle of each time unit
  • offset adjusts the rotation center. Initial center is at model origin, needs adjustment

C# script binding system time
#

using System;
using UnityEngine;

public class HelloWorld : MonoBehaviour
{
    // --------- Public -------
    public Material clockMat;
    // --------- private --------
    private bool valid;
    private int hourAnglePropID;
    private int minuteAnglePropID;
    private int secondAnglePropID;

    // Start is called before the first frame update
    void Start()
    {
        if(clockMat == null) return;
        hourAnglePropID = Shader.PropertyToID("_HourHandAngle");
        minuteAnglePropID = Shader.PropertyToID("_MinuteHandAngle");
        secondAnglePropID = Shader.PropertyToID("_SecondHandAngle");
        if(clockMat.HasProperty(hourAnglePropID) && clockMat.HasProperty(minuteAnglePropID) && clockMat.HasProperty(secondAnglePropID))
            valid = true;

        Debug.Log("hourAnglePropID" + hourAnglePropID);
        Debug.Log("minuteAnglePropID" + minuteAnglePropID);
        Debug.Log("secondAnglePropID" + secondAnglePropID);
        Debug.Log(valid);
    }

    // Update is called once per frame
    void Update()
    {
        if(!valid) return;
        int second = DateTime.Now.Second;
        float secondAngle = second /60.0f * 360.0f;
        clockMat.SetFloat(secondAnglePropID, secondAngle);

        int minute = DateTime.Now.Minute;
        float minuteAngle = minute /60.0f * 360.0f;
        clockMat.SetFloat(minuteAnglePropID, minuteAngle);

        int hour = DateTime.Now.Hour;
        float hourAngle = (hour % 12) / 12.0f * 360.0f + minuteAngle / 360.0f * 30.0f;
        clockMat.SetFloat(hourAnglePropID, hourAngle);
    }
}

Key Points
#

  • % remainder, ensure hour over 12 to 0

20.2 Shadow Projection Pass
#

Key Code
#

Pass {
    Name "ShadowCaster"
    Tags { "LightMode" = "ShadowCaster" }
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    #pragma multi_compile_shadowcaster
    #include "UnityCG.cginc"

    uniform float _HourHandAngle, _MinuteHandAngle, _SecondHandAngle, _RotateOffset;

    struct appdata {
        float4 vertex : POSITION;
        float3 normal : NORMAL;
        float3 color : COLOR;
    };

    struct v2f {
        V2F_SHADOW_CASTER;
    };

    void RotateZwithOffset(float angle, float offset, float mask, inout float3 vertex){
        vertex.y -= offset * mask;
        float radZ = radians(angle * mask);
        float sinZ, cosZ = 0;
        sincos(radZ, sinZ, cosZ);
        vertex.xy = float2(
            vertex.x * cosZ - vertex.y * sinZ,
            vertex.x * sinZ + vertex.y * cosZ
        );
        vertex.y += offset * mask;
    }

    void ClockAnim(float3 color, inout float3 vertex) {
        RotateZwithOffset(_HourHandAngle, _RotateOffset, color.r, vertex);
        RotateZwithOffset(_MinuteHandAngle, _RotateOffset, color.g, vertex);
        RotateZwithOffset(_SecondHandAngle, _RotateOffset, color.b, vertex);
    }

    v2f vert(appdata v) {
        v2f o;
        ClockAnim(v.color.rgb, v.vertex.xyz);
        // 关键修复:计算带法线偏移的阴影坐标
        float3 posWS = mul(unity_ObjectToWorld, v.vertex).xyz;//动画后的顶点转世界空间
        float3 normalWS = UnityObjectToWorldNormal(v.normal);//法线转世界空间
        o.pos = UnityClipSpaceShadowCasterPos(posWS, normalWS);
        TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
        return o;
    }

    float4 frag(v2f i) : SV_Target {
        SHADOW_CASTER_FRAGMENT(i)
    }
    ENDCG
}

Key Points
#

  • ShadowCaster is fixed for projection Pass naming and LightMode

  • UnityClipSpaceShadowCasterPos(posWS, normalWS) normal offset correction, get animation front normal and animation back vertex, calculate animation back normal direction, finally output animation back vertex position and corrected normal in light cone clipping space coordinates

  • V2F_SHADOW_CASTER Unity built-in macro, equivalent to:

    • float4 pos : SV_POSITION
    • float3 vec : TEXCOORD0 (only used when there is point light)
  • TRANSFER_SHADOW_CASTER_NORMALOFFSET(o) input posWS, normalWS, output V2F_SHADOW_CASTER

  • SHADOW_CASTER_FRAGMENT(i) input pos, process projection according to light type

Note: Only need ShadowCaster, V2F_SHADOW_CASTER, TRANSFER_SHADOW_CASTER_NORMALOFFSET(o), SHADOW_CASTER_FRAGMENT(i) 4 elements to get a complete shadow projection Pass


21. Effect Dissolve Animation Case
#

21.1 Gray Scale and Noise Control Dissolve
#

By multi-channel gray scale map, noise map, and vertex color, achieve complex dissolve effects like mesh disappearance, randomness, and light emission.

L21_CyberPunk

Key Structure and Function
#

struct VertexInput { 
    float4 vertex  : POSITION; 
    float2 uv0     : TEXCOORD0; 
    float2 uv1     : TEXCOORD1;
    float4 normal  : NORMAL; 
    float4 tangent : TANGENT;  
    float4 color : COLOR; 
};
struct VertexOutput {
    float4 pos : SV_POSITION; 
    float2 uv0 : TEXCOORD0; 
    float2 uv1 : TEXCOORD1;
    float4 posWS : TEXCOORD2;  
    float3 nDirWS : TEXCOORD3;  
    float3 tDirWS : TEXCOORD4;  
    float3 bDirWS : TEXCOORD5;  
    float4 effectMask : TEXCOORD6; 
    LIGHTING_COORDS(7,8)
};

float4 CyberpunkAnim(float noise, float mask, float3 normal, inout float3 vertex){
    float baseMask = abs(frac(vertex.y * _EffParams.x - _Time.x * _EffParams.y) -0.5) * 2.0;
    baseMask = min(1.0, baseMask * 2.0);
    baseMask += (noise - 0.5) * _EffParams.z;
    float4 effectMask = float4(0.0, 0.0, 0.0, 0.0);
    effectMask.x = smoothstep(0.0, 0.9, baseMask);
    effectMask.y = smoothstep(0.2, 0.7, baseMask);
    effectMask.z = smoothstep(0.4, 0.5, baseMask);
    effectMask.w = mask;
    vertex.xz += normal.xz * (1.0 - effectMask.y) * _EffParams.w * mask;
    return effectMask;
}

Vertex Shader
#

VertexOutput vert(VertexInput v) {
    float noise = tex2Dlod(_EffectMap02, float4(v.uv1, 0.0, 0.0)).r;
    VertexOutput o = (VertexOutput)0;
    o.effectMask = CyberpunkAnim(noise, v.color, v.normal.xyz, v.vertex.xyz);
    o.pos = UnityObjectToClipPos(v.vertex); 
    o.uv0 = v.uv0;
    o.uv1 = v.uv1;
    o.posWS = mul(unity_ObjectToWorld, v.vertex); 
    o.nDirWS = UnityObjectToWorldNormal(v.normal); 
    o.tDirWS = normalize( mul( unity_ObjectToWorld, float4( v.tangent.xyz, 0.0 ) ).xyz );
    o.bDirWS = normalize(cross(o.nDirWS, o.tDirWS) * v.tangent.w);
    TRANSFER_VERTEX_TO_FRAGMENT(o)
    return o;
}

Fragment Shader
#

float4 frag(VertexOutput i) : COLOR{ 
// ...(向量、光照、采样等略)...
//光照模型
float3 baseCol = var_MainTex.rgb * _BaseColor;
float  Lambert = max(0.0, nDotl);//lambert
float specCol = var_SpecTex.rgb;
float specpow = lerp(1, _SpecularPow_Value, var_SpecTex.a);
float   Phong = pow(max(0.0, vDotr), _SpecularPow_Value);
float shadow = LIGHT_ATTENUATION(i);
float3 dirlighting = (baseCol * Lambert + specCol * Phong) * _LightColor0 * shadow; 
float3 EnvCol = TriColAmbient(nDirWS, _TopCol, _MidCol, _BotCol);
float fresnel = pow(1.0 - ndotv, _FresnelPow);
float occlusion = var_MainTex.a; 
float3 envLighting = (baseCol * EnvCol * _EnvDiffInt + var_Cubemap * fresnel* _EnvSpecint * var_SpecTex.a) * occlusion;
//自发光
float3 emission = var_EmitTex * _EmitInt;

float3 _EffectMap01_var = tex2D(_EffectMap01, i.uv1).xyz;
float meshMask = _EffectMap01_var.x;
float faceRandomMask = max(0.001, _EffectMap01_var.y);
float faceSlopMask = max(0.001, _EffectMap01_var.z);

float smallMask = i.effectMask.x;
float midMask = i.effectMask.y;
float bigMask = i.effectMask.z;
float baseMask = i.effectMask.w;

float midOpacity = saturate(floor(min(faceRandomMask, 0.999999) + midMask));
float bigOpacity = saturate(floor(min(faceSlopMask, 0.999999) + bigMask));
float opacity = lerp(1.0, min(bigOpacity, midOpacity), baseMask);

float meshEmitInt = (bigMask - smallMask) * meshMask;//只在半透明的区域有发光效果
meshEmitInt = meshEmitInt * meshEmitInt * 2.0;//进行固定值的 power,让发光区域缩小
emission += _EffCol * meshEmitInt * baseMask;

float3 FinalColor = dirlighting + envLighting + emission;
return float4(FinalColor * opacity, opacity);
}

Key Points
#

  • EffMap01
    • R - WireframeMap records model wire frame
    • G - RandomGrayScale records random gray scale based on surface
    • B - DisappearanceGrayscale records uniform depth based on surface
  • EffMap02
    • 3DPerlinNoise

DisappearanceGrayscale

L21_CyberPunk_Slop

RandomGrayScale

L21_CyberPunk_Ramdon

贴图制作与合并

L21_CyberPunk_SD

  • mask vertex color, animate only on character
  • baseMask = abs(frac(vertex.y * _EffParams.x - _Time.x * _EffParams.y) -0.5) * 2.0;
    • frac(vertex.y) only takes the decimal, visual gradient Linear 1
    • EffectParams.x controls gradient Linear 1 Tiling
    • EffectParams.y controls animation speed and direction
    • abs(frac() -0.5) * 2.0 abs takes absolute value, finally result gradient Linear 1 to gradient Linear 3
  • baseMask = min() increases value range to 1
  • baseMask += (noise - 0.5) * _EffParams.z; makes baseMask value non 1 area change, _EffParams.z controls change intensity
  • smoothstep adjusts specified area waveform
  • vertex.xz += animate transparent area, EffParams.w controls animation intensity
  • floor less than 1 to 0, greater than 1 to 1
  • saturate limits value range to (0, 1)
  • meshEmitInt * meshEmitInt equivalent to Power = value^2
  • faceRandomMask = max(0.001, _EffectMap01_var.y) prevent negative, negative causes display error

L21_CyberPunk_BaseMask

Related

EssenceOfLinearAlgebra - 03
667 words·4 mins
EssenceOfLinearAlgebra - 04
1099 words·6 mins
EssenceOfLinearAlgebra - 02
1458 words·7 mins