当前位置: 首页 > news >正文

【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
  • 目的:消除浮点误差导致的非正交问题

三、技术实现验证

场景测试
场景normalDirupDir选择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

  1. 叉乘性质

    • un不平行,则r = u × n非零
    • un平行,需调整u方向
  2. 正交性验证
    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通过法线→向上→右向的顺序构建坐标系,其核心价值在于:

  1. 数值稳定性:智能选择避免零向量
  2. 正交性保证:两次叉乘确保坐标系正交
  3. 视角自适应:始终面向摄像机

这是实现广告牌效果的标准数学流程,广泛应用于粒子系统、特效渲染等领域。
再模型空间下进行的空间动画,批处理往往会破坏这种动画效果

相关文章:

  • JVM虚拟机:内存结构、垃圾回收、性能优化
  • 深入理解Java单例模式:确保类只有一个实例
  • Java-IO流之打印流详解
  • MySQL基础(四)DML、数据表操作DDL操作
  • MAX3490
  • 关于双网卡优先级:有效跃点数的解析(设置值×2)
  • Levenberg-Marquardt算法详解和C++代码示例
  • 代驾数据库
  • Java逻辑运算符常见错误分析与规避指南
  • 使用 Python 和 HuggingFace Transformers 进行对象检测
  • 位运算(Bitwise Operations)深度解析
  • 基于J2EE架构的在线考试系统设计与实现【源码+文档】
  • 机器学习算法时间复杂度解析:为什么它如此重要?
  • C/C++ 中附加包含目录、附加库目录与附加依赖项详解
  • 波士顿房价预测(线性回归模型)
  • c++重点知识总结
  • VMware 安装 CentOS8详细教程 (附步骤截图)附连接公网、虚拟机yum源等系统配置
  • SQLAlchemy 中的 func 函数使用指南
  • CVAT标注服务
  • Python训练营---Day46
  • 网站留言板块怎么做/深圳网络营销推广中心
  • 苏州建设局网站实名制/有网站模板怎么建站
  • 做最好的言情网站/军事新闻最新24小时
  • 大连网站建设怎么做/seo搜索排名影响因素主要有
  • 在家可以加工的小工厂/怎么优化整站
  • 中国企业500强中国铁建/太原高级seo主管