前言
前面两篇文章说的都是对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站庄懂的技术美术入门课,感谢。
梦想养活不起你的时候,
你得养着梦想啊。
——小林一茶
评论
还没有任何评论,你来说两句吧!