【Unity Shader编程】之让画面动起来
一,序列帧
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'Shader "Unity Shaders Book/Chapter 11/Image Sequence Animation" {Properties {_Color ("Color Tint", Color) = (1, 1, 1, 1)_MainTex ("Image Sequence", 2D) = "white" {}_HorizontalAmount ("Horizontal Amount", Float) = 4_VerticalAmount ("Vertical Amount", Float) = 4_Speed ("Speed", Range(1, 100)) = 30}SubShader {Tags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}Pass {Tags { "LightMode"="ForwardBase" }ZWrite OffBlend SrcAlpha OneMinusSrcAlphaCGPROGRAM#pragma vertex vert #pragma fragment frag#include "UnityCG.cginc"fixed4 _Color;sampler2D _MainTex;float4 _MainTex_ST;float _HorizontalAmount;float _VerticalAmount;float _Speed;struct a2v { float4 vertex : POSITION; float2 texcoord : TEXCOORD0;}; struct v2f { float4 pos : SV_POSITION;float2 uv : TEXCOORD0;}; v2f vert (a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); return o;} fixed4 frag (v2f i) : SV_Target {float time = floor(_Time.y * _Speed); float row = floor(time / _HorizontalAmount);float column = time - row * _HorizontalAmount;// half2 uv = float2(i.uv.x /_HorizontalAmount, i.uv.y / _VerticalAmount);
// uv.x += column / _HorizontalAmount;
// uv.y -= row / _VerticalAmount;half2 uv = i.uv + half2(column, -row);uv.x /= _HorizontalAmount;uv.y /= _VerticalAmount;fixed4 c = tex2D(_MainTex, uv);c.rgb *= _Color;return c;}ENDCG} }FallBack "Transparent/VertexLit"
}
当time为797的时候,给我详细的各个步骤的值,重头到位时怎么计算的当time为797的时候,给我详细的各个步骤的值,重头到位时怎么计算的
当 time = 797
时,我们可以依照这个 shader 的 fragment 函数逻辑一步步推导出最终用于采样的 uv
值,并解释每一步的计算过程。
✅ 参数回顾:
_HorizontalAmount = 4
_VerticalAmount = 4
_Speed = 30
✅ Step 1: 计算动画帧索引时间
float time = floor(_Time.y * _Speed);
你指定了 time = 797
,这表示 floor(_Time.y * _Speed) = 797
。
因此:
_Time.y = 797 / 30 ≈ 26.5667
所以我们此时正在播放第 797 帧。
✅ Step 2: 计算行列索引
float row = floor(time / _HorizontalAmount);
row = floor(797 / 4) = floor(199.25) = 199
float column = time - row * _HorizontalAmount;
column = 797 - 199 * 4 = 797 - 796 = 1
所以我们现在正在采样第 199 行,第 1 列 的帧图块。
✅ Step 3: 构建最终采样用的 uv
这部分代码:
half2 uv = i.uv + half2(column, -row);
uv.x /= _HorizontalAmount;
uv.y /= _VerticalAmount;
是关键。我们逐项解释:
原始 i.uv
:
这来自于 TRANSFORM_TEX(v.texcoord, _MainTex)
,假设它是默认的 (0~1)
范围,例如我们用中间点 (0.5, 0.5)
来说明。
步骤:
uv = (0.5, 0.5) + (1, -199) = (1.5, -198.5)
然后进行缩放:
uv.x = 1.5 / 4 = 0.375
uv.y = -198.5 / 4 = -49.625
✅ 总结
最终用于 tex2D(_MainTex, uv)
的采样坐标为:
uv = float2(0.375, -49.625)
由于 uv.y
是负值,这意味着 超出了纹理坐标范围,采样时会出现以下几种情况(取决于贴图的 Wrap Mode):
- Clamp:超出部分会重复边界值(边缘拉伸)。
- Repeat:会根据小数部分循环(模 1 处理)。
- Mirror:镜像重复。
🔍 补充建议
此 shader 中的 uv
操作方式虽然可行,但容易导致负坐标。更健壮的方式是:
half2 uv;
uv.x = (i.uv.x + column) / _HorizontalAmount;
uv.y = (i.uv.y + row) / _VerticalAmount;
这样可以避免负数,提升稳定性,特别是搭配 Repeat
模式的纹理。
如需进一步说明 WrapMode
或添加播放循环支持,我可以继续补充。
二,滚动的背景
Shader "Unity Shaders Book/Chapter 11/Scrolling Background" {// 属性定义区:控制材质面板参数Properties {_MainTex ("Base Layer (RGB)", 2D) = "white" {} // 基础层纹理(主纹理)_DetailTex ("2nd Layer (RGB)", 2D) = "white" {} // 二级层纹理(叠加纹理)_ScrollX ("Base layer Scroll Speed", Float) = 1.0 // 基础层X轴滚动速度_Scroll2X ("2nd layer Scroll Speed", Float) = 1.0 // 二级层X轴滚动速度_Multiplier ("Layer Multiplier", Float) = 1 // 层次混合强度}SubShader {Tags { "RenderType"="Opaque" "Queue"="Geometry"} // 渲染类型标记Pass { Tags { "LightMode"="ForwardBase" } // 前向渲染基础通道CGPROGRAM#pragma vertex vert // 指定顶点着色器函数#pragma fragment frag // 指定片元着色器函数#include "UnityCG.cginc" // 引入Unity内置函数库// GPU参数声明sampler2D _MainTex; // 基础纹理采样器sampler2D _DetailTex; // 叠加纹理采样器float4 _MainTex_ST; // 基础纹理缩放平移参数float4 _DetailTex_ST; // 叠加纹理缩放平移参数float _ScrollX; // 基础层滚动速度float _Scroll2X; // 叠加层滚动速度float _Multiplier; // 混合强度// 顶点输入结构体(模型空间)struct a2v {float4 vertex : POSITION; // 顶点位置float4 texcoord : TEXCOORD0; // 纹理坐标};// 顶点输出/片元输入结构体(裁剪空间)struct v2f {float4 pos : SV_POSITION; // 裁剪空间位置float4 uv : TEXCOORD0; // 纹理坐标(4维用于双纹理)};// 顶点着色器核心逻辑v2f vert (a2v v) {v2f o;o.pos = UnityObjectToClipPos(v.vertex); // MVP矩阵变换// 双纹理UV计算(关键帧动画实现)// TRANSFORM_TEX宏展开为:texcoord * _MainTex_ST.xy + _MainTex_ST.zwo.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex) + frac(float2(_ScrollX, 0.0) * _Time.y); // 基础层滚动o.uv.zw = TRANSFORM_TEX(v.texcoord, _DetailTex) + frac(float2(_Scroll2X, 0.0) * _Time.y); // 叠加层滚动return o;}// 片元着色器核心逻辑fixed4 frag (v2f i) : SV_Target {// 双纹理采样(使用4维UV坐标的前后两维)fixed4 firstLayer = tex2D(_MainTex, i.uv.xy); // 基础层采样fixed4 secondLayer = tex2D(_DetailTex, i.uv.zw);// 叠加层采样// 混合模式:根据叠加层Alpha值进行插值fixed4 c = lerp(firstLayer, secondLayer, secondLayer.a);// 强度控制(全局亮度调节)c.rgb *= _Multiplier;return c; // 输出最终颜色}ENDCG}}FallBack "VertexLit" // 降级方案
}
TRANSFORM_TEX(v.texcoord, _MainTex) 实际上等于是:v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw
在 a 和 b 之间线性插值,t 为插值因子。
三. 顶点动画
在这里插入代码片Shader "Unity Shaders Book/Chapter 11/Water" {// 属性定义区:控制材质面板参数Properties {_MainTex ("Main Tex", 2D) = "white" {} // 基础水面纹理_Color ("Color Tint", Color) = (1,1,1,1) // 水面颜色叠加_Magnitude ("Distortion Magnitude", Float) = 1 // 波浪振幅(高度)_Frequency ("Distortion Frequency", Float) = 1 // 波浪频率(密集度)_InvWaveLength ("Distortion Inverse Wave Length", Float) = 10 // 波长倒数(波纹间距)_Speed ("Speed", Float) = 0.5 // 波浪移动速度}SubShader {// 渲染设置Tags {"Queue"="Transparent" // 渲染队列:透明物体后处理"IgnoreProjector"="True" // 忽略投影器影响"RenderType"="Transparent" // 标记为透明材质"DisableBatching"="True" // 禁用批处理(顶点动画需要独立处理)}Pass {Tags { "LightMode"="ForwardBase" } // 前向渲染基础通道// 渲染状态设置ZWrite Off // 关闭深度写入(允许后续物体穿透水面)Blend SrcAlpha OneMinusSrcAlpha // 透明混合模式Cull Off // 关闭背面剔除(显示双面水面)CGPROGRAM #pragma vertex vert // 指定顶点着色器函数#pragma fragment frag // 指定片元着色器函数#include "UnityCG.cginc" // 引入Unity内置函数库// GPU参数声明sampler2D _MainTex; // 基础纹理采样器float4 _MainTex_ST; // 纹理缩放平移参数fixed4 _Color; // 颜色叠加参数float _Magnitude; // 波浪振幅float _Frequency; // 波浪频率float _InvWaveLength; // 波长倒数(1/波长)float _Speed; // 波浪移动速度// 顶点输入结构体(模型空间)struct a2v {float4 vertex : POSITION; // 顶点位置(模型空间)float4 texcoord : TEXCOORD0; // 纹理坐标};// 顶点输出/片元输入结构体(裁剪空间)struct v2f {float4 pos : SV_POSITION; // 裁剪空间位置float2 uv : TEXCOORD0; // 纹理坐标};// 顶点着色器核心逻辑v2f vert(a2v v) {v2f o;// 计算顶点偏移量(关键帧动画)float4 offset;offset.yzw = float3(0.0, 0.0, 0.0); // 仅修改X轴偏移// 正弦波函数生成动态位移offset.x = sin(_Frequency * _Time.y // 时间维度频率+ v.vertex.x * _InvWaveLength // X轴空间相位偏移+ v.vertex.y * _InvWaveLength // Y轴空间相位偏移+ v.vertex.z * _InvWaveLength // Z轴空间相位偏移) * _Magnitude; // 振幅控制// 顶点位置变换(模型→裁剪空间)o.pos = UnityObjectToClipPos(v.vertex + offset);// 纹理坐标计算o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); // 自动处理纹理缩放平移o.uv += float2(0.0, _Time.y * _Speed); // 水平纹理流动效果return o;}// 片元着色器核心逻辑fixed4 frag(v2f i) : SV_Target {fixed4 c = tex2D(_MainTex, i.uv); // 纹理采样c.rgb *= _Color.rgb; // 应用颜色叠加return c; // 输出最终颜色} ENDCG}}FallBack "Transparent/VertexLit" // 降级渲染方案
}
o.pos = UnityObjectToClipPos(v.vertex + offset);
修改o.pos的值就改变了顶点位置
顶点动画之广告牌
// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'Shader "Unity Shaders Book/Chapter 11/Billboard" {Properties {_MainTex ("Main Tex", 2D) = "white" {}_Color ("Color Tint", Color) = (1, 1, 1, 1)_VerticalBillboarding ("Vertical Restraints", Range(0, 1)) = 1 }SubShader {// Need to disable batching because of the vertex animationTags {"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True"}Pass { Tags { "LightMode"="ForwardBase" }ZWrite OffBlend SrcAlpha OneMinusSrcAlphaCull OffCGPROGRAM#pragma vertex vert#pragma fragment frag#include "Lighting.cginc"sampler2D _MainTex;float4 _MainTex_ST;fixed4 _Color;fixed _VerticalBillboarding;struct a2v {float4 vertex : POSITION;float4 texcoord : TEXCOORD0;};struct v2f {float4 pos : SV_POSITION;float2 uv : TEXCOORD0;};v2f vert (a2v v) {v2f o;// Suppose the center in object space is fixedfloat3 normalDir = mul(unity_WorldToObject,float4(_WorldSpaceCameraPos, 1));// float3 normalDir = viewer - center;// If _VerticalBillboarding equals 1, we use the desired view dir as the normal dir// Which means the normal dir is fixed// Or if _VerticalBillboarding equals 0, the y of normal is 0// Which means the up dir is fixednormalDir.y =normalDir.y * _VerticalBillboarding;normalDir = normalize(normalDir);// Get the approximate up dir// If normal dir is already towards up, then the up dir is towards frontfloat3 upDir = abs(normalDir.y) > 0.999 ? float3(0, 0, 1) : float3(0, 1, 0);float3 rightDir = normalize(cross(upDir, normalDir));upDir = normalize(cross(normalDir, rightDir));// Use the three vectors to rotate the quadfloat3 centerOffs = v.vertex.xyz ;float3 localPos = rightDir * centerOffs.x + upDir * centerOffs.y + normalDir * centerOffs.z;o.pos = UnityObjectToClipPos(float4(localPos, 1));o.uv = TRANSFORM_TEX(v.texcoord,_MainTex);return o;}fixed4 frag (v2f i) : SV_Target {fixed4 c = tex2D (_MainTex, i.uv);c.rgb *= _Color.rgb;return c;}ENDCG}} FallBack "Transparent/VertexLit"
}
广告牌Shader中构建坐标系的步骤确实是先确定法线方向,再计算向上方向,最后通过叉积得到右方向。以下是完整的数学推导和实现逻辑:
一、标准实现流程
// 1. 计算法线方向(摄像机到广告牌中心)
float3 normalDir = normalize(viewer - center);// 2. 智能选择向上方向
float3 upDir = abs(normalDir.y) > 0.999 ? float3(0,0,1) : float3(0,1,0);// 3. 叉乘计算右方向
float3 rightDir = normalize(cross(upDir, normalDir));// 4. 重新正交化向上方向
float3 finalUpDir = normalize(cross(normalDir, rightDir));
二、步骤解析
1. 法线方向计算
- 数学本质:摄像机到广告牌中心的向量方向
normalDir = normalize(viewer - center);
- 物理意义:广告牌需要朝向的目标方向
2. 向上方向选择
- 智能判断逻辑:
if (normalDir.y接近1) {upDir = (0,0,1); // 摄像机几乎在正上方时使用世界Z轴 } else {upDir = (0,1,0); // 默认使用世界Y轴 }
- 作用:避免法线与向上方向平行导致叉乘失效
3. 右方向计算
- 叉乘公式:
r ⃗ = u ⃗ × n ⃗ \vec{r} = \vec{u} \times \vec{n} r=u×n - 几何意义:生成垂直于法线和向上方向的右方向
4. 正交化验证
- 二次正交化:
u ⃗ ′ = n ⃗ × r ⃗ \vec{u}' = \vec{n} \times \vec{r} u′=n×r - 目的:消除浮点误差导致的非正交问题
三、技术实现验证
场景测试
场景 | normalDir | upDir选择 | rightDir计算结果 |
---|---|---|---|
摄像机正上方 | (0,1,0) | (0,0,1) | (1,0,0) |
常规视角 | (0.7,0.7,0.7) | (0,1,0) | (0.0, -0.49, 0.7) |
摄像机侧视 | (1,0,0) | (0,1,0) | (0,0,1) |
四、错误场景分析
若省略步骤2的智能判断:
float3 upDir = float3(0,1,0); // 固定选择
float3 rightDir = cross(upDir, normalDir);
- 当normalDir=(0,1,0) 时,
rightDir
为零向量 - 结果:广告牌出现扭曲或翻转
五、数学证明
设广告牌中心为原点,摄像机方向为n
:
-
叉乘性质:
- 若
u
与n
不平行,则r = u × n
非零 - 若
u
与n
平行,需调整u
方向
- 若
-
正交性验证:
r ⃗ ⋅ u ⃗ = ( u ⃗ × n ⃗ ) ⋅ u ⃗ = 0 \vec{r} \cdot \vec{u} = (\vec{u} \times \vec{n}) \cdot \vec{u} = 0 r⋅u=(u×n)⋅u=0
六、扩展应用
1. 动态约束
// 控制垂直方向约束强度
float3 constrainedUp = lerp(float3(0,1,0), float3(0,0,1), _VerticalBillboarding);
2. 物理模拟增强
// 添加风力影响
float3 windDir = normalize(float3(1,0,0));
rightDir += windDir * _WindStrength;
总结
广告牌Shader通过法线→向上→右向的顺序构建坐标系,其核心价值在于:
- 数值稳定性:智能选择避免零向量
- 正交性保证:两次叉乘确保坐标系正交
- 视角自适应:始终面向摄像机
这是实现广告牌效果的标准数学流程,广泛应用于粒子系统、特效渲染等领域。
再模型空间下进行的空间动画,批处理往往会破坏这种动画效果