一,序列帧
// 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 Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#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.zw
o.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 animation
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 "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 fixed
float3 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 fixed
normalDir.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 front
float3 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 quad
float3 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通过法线→向上→右向的顺序构建坐标系,其核心价值在于:
- 数值稳定性:智能选择避免零向量
- 正交性保证:两次叉乘确保坐标系正交
- 视角自适应:始终面向摄像机
这是实现广告牌效果的标准数学流程,广泛应用于粒子系统、特效渲染等领域。
再模型空间下进行的空间动画,批处理往往会破坏这种动画效果