前言

前面两篇文章说的都是对UV进行修改实现shader的动态效果,这一篇来说一下通过顶点的偏移实现动态shader。

对于顶点常用的偏移操作有移动、旋转、缩放这三种,下面分别来介绍。

 

顶点移动

顶点的移动比较简单,就是在顶点shader中,直接对顶点位置进行偏移,下面的代码是一个y轴上下移动的shader。

// shader的路径和名称
Shader "Shader Forge/VertixTranslation" {
    // 暴露属性到面板
    Properties {
        _MainTex("RGB: Color A:Alpha", 2d) = "gray"{}
        _Opacity("Opacity", range(0.0, 1.0)) = 1.0
        _MoveRange("Move Range", range(0.0, 2.0)) = 1.0
        _MoveSpeed("Move Speed", range(0.0, 3.0)) = 1.0
    }
    SubShader {
        Tags {
            "Queue"="Transparent"                //调整渲染顺序
            "RenderType"="Transparent"           //改为对应的
            "ForceNoShadowCasting"="True"        //关闭阴影投射
            "IgnoreProjector"="True"             //不响应投射器
        }
        Pass {
            Name "FORWARD"
            Tags {
                "LightMode"="ForwardBase"
            }
            Blend One OneMinusSrcAlpha          //修改混合模式
            
            Cull Off         //双面显示
            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; //支持UV TillingOffset
            uniform half _Opacity;
            uniform half _MoveRange;
            uniform half _MoveSpeed;
            
            // 输入结构
            struct VertexInput {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };
            
            // 输出结构
            struct VertexOutput {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            // 声明常量
            #define TwoPi 6.283185

            // 顶点动画方法
            void Translation(inout float3 vertex){ //inout表示在函数内直接修改此传入变量
                vertex.y += _MoveRange * sin(frac(_Time.z * _MoveSpeed) * TwoPi); //sin()用于产生周期波动,sin()的完整周期是0到2π,frac()取余的值域是0到1,因此需要乘TwoPi
            }
            
            // 顶点shader
            VertexOutput vert (VertexInput v) {
                VertexOutput o = (VertexOutput)0;
                Translation(v.vertex.xyz);
                o.pos = UnityObjectToClipPos( v.vertex );
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }
            
            // 像素shader
            float4 frag(VertexOutput i) : COLOR {
                half4 var_MainTex = tex2D(_MainTex, i.uv);   
                half finalOpacity = var_MainTex.a * _Opacity;  
                return half4(var_MainTex.rgb * finalOpacity,finalOpacity);
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

注意sin()的完整周期是2π,因此如果不乘2π会造成动画卡顿闪烁。

效果如下

 

顶点缩放

顶点的缩放和顶点的移动是相同的原理,代码也类似。

// shader的路径和名称
Shader "Shader Forge/VertexScale" {
    // 暴露属性到面板
    Properties {
        _MainTex("RGB: Color A:Alpha", 2d) = "gray"{}
        _Opacity("Opacity", range(0.0, 1.0)) = 1.0
        _ScaleRange("Scale Range", range(0.0, 2.0)) = 1.0
        _ScaleSpeed("Scale Speed", range(0.0, 3.0)) = 1.0
    }
    SubShader {
        Tags {
            "Queue"="Transparent"                //调整渲染顺序
            "RenderType"="Transparent"           //改为对应的
            "ForceNoShadowCasting"="True"        //关闭阴影投射
            "IgnoreProjector"="True"             //不响应投射器
        }
        Pass {
            Name "FORWARD"
            Tags {
                "LightMode"="ForwardBase"
            }
            Blend One OneMinusSrcAlpha          //修改混合模式
            
            Cull Off         //双面显示
            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; //支持UV TillingOffset
            uniform half _Opacity;
            uniform half _ScaleRange;
            uniform half _ScaleSpeed;
            
            // 输入结构
            struct VertexInput {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };
            
            // 输出结构
            struct VertexOutput {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            // 声明常量
            #define TwoPi 6.283185

            // 顶点动画方法
            void Scaling(inout float3 vertex){ //inout表示在函数内直接修改此传入变量
                vertex *= 1.0 + _ScaleRange * sin(frac(_Time.z * _ScaleSpeed) * TwoPi); //sin()用于产生周期波动,sin()的完整周期是0到2π,frac()取余的值域是0到1,因此需要乘TwoPi
            }
            
            // 顶点shader
            VertexOutput vert (VertexInput v) {
                VertexOutput o = (VertexOutput)0;
                Scaling(v.vertex.xyz);
                o.pos = UnityObjectToClipPos( v.vertex );
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }
            
            // 像素shader
            float4 frag(VertexOutput i) : COLOR {
                half4 var_MainTex = tex2D(_MainTex, i.uv);   
                half finalOpacity = var_MainTex.a * _Opacity;  
                return half4(var_MainTex.rgb * finalOpacity,finalOpacity);
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

区别就是+=换成*=,修改的轴向由单个y轴改成xyz三轴整体,效果如下。

 

顶点旋转

顶点的旋转相对于移动和缩放要复杂一些,需要了解向量的旋转原理。

首先将问题简化一下,顶点绕Y轴旋转时,此时y坐标不变,可以视为[x, y]二维向量绕原点旋转,求旋转后的红色箭头处的XZ坐标,下面的图展示了这种方式的计算方法。

通过三角函数配合向量的加减运算即可得到旋转后的坐标,具体代码如下:


// shader的路径和名称
Shader "Shader Forge/VertexRotate" {
    // 暴露属性到面板
    Properties {
        _MainTex("RGB: Color A:Alpha", 2d) = "gray"{}
        _Opacity("Opacity", range(0.0, 1.0)) = 1.0
        _RotateRange("Rotate Range", range(0.0, 90.0)) = 30.0
        _RotateSpeed("Rotate Speed", range(0.0, 3.0)) = 1.0
    }
    SubShader {
        Tags {
            "Queue"="Transparent"                //调整渲染顺序
            "RenderType"="Transparent"           //改为对应的
            "ForceNoShadowCasting"="True"        //关闭阴影投射
            "IgnoreProjector"="True"             //不响应投射器
        }
        Pass {
            Name "FORWARD"
            Tags {
                "LightMode"="ForwardBase"
            }
            Blend One OneMinusSrcAlpha          //修改混合模式
            
            Cull Off         //双面显示
            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; //支持UV TillingOffset
            uniform half _Opacity;
            uniform half _RotateRange;
            uniform half _RotateSpeed;
            
            // 输入结构
            struct VertexInput {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };
            
            // 输出结构
            struct VertexOutput {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            // 声明常量
            #define TwoPi 6.283185

            // 顶点动画方法
            void Rotation(inout float3 vertex){ //inout表示在函数内直接修改此传入变量
                float angelY = _RotateRange * sin(frac(_Time.z * _RotateSpeed) * TwoPi); //旋转范围
                float radY = radians(angelY); //计算弧度
                float sinY, cosY = 0;
                sincos(radY, sinY, cosY); //根据弧度计算sin和cos 
                vertex.xz = float2(vertex.x * cosY - vertex.z * sinY, vertex.x * sinY + vertex.z * cosY); //计算偏移量
            }
            
            // 顶点shader
            VertexOutput vert (VertexInput v) {
                VertexOutput o = (VertexOutput)0;
                Rotation(v.vertex.xyz);
                o.pos = UnityObjectToClipPos( v.vertex );
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }
            
            // 像素shader
            float4 frag(VertexOutput i) : COLOR {
                half4 var_MainTex = tex2D(_MainTex, i.uv);   
                half finalOpacity = var_MainTex.a * _Opacity;  
                return half4(var_MainTex.rgb * finalOpacity,finalOpacity);
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

效果如下

 

注意

注意下图中,当选择三个应用了不同定点动画的模型时,可以发现三个物体实际仍在原来的位置,也就是说顶点动画可以使用静态物体实现动态的效果,在大量物体进行随机运动的情况下,可以避免由CPU处理这些transform信息,减少DrawCall压力,也就是所谓的静态合批。

顶点动画由于实现简单、效果容易控制、节省CPU资源等优点,因此在草丛、建筑等大量随机散布物体,特别是移动平台上被大量使用。

目前动画虽然已经实现,但默认的阴影由于调用的是原始的顶点位置进行计算,会导致有顶点动画的物体投影不正确,为了修复这个问题,需要自定义一个ShadowCaster Pass,在这个Pass中,进行同样的顶点变换过程。这一部分之后会单独开篇介绍。

 

本文同样参考自B站庄懂的技术美术入门课,感谢。


梦想养活不起你的时候,
你得养着梦想啊。

——小林一茶