Unity 6相机相对平滑逐像素运动向量计算抖动问题求助
Unity 6 平滑非归一化逐像素运动向量实现方案
我已搜索过相关问题但未找到有效解决方案,若有遗漏烦请告知。我在Unity 6的Shader中计算网格相对相机的逐像素运动向量,需求是相机移动时静止网格也能被检测到运动。我认为计算逻辑正确,但始终无法消除抖动,导致运动向量无法满足使用需求;归一化处理会让向量失去价值。我尝试过Unity自带的运动向量纹理,抖动情况和我的实现一致。希望能得到实现平滑、非归一化逐像素运动向量的思路或解决方法。
当前实现代码
Shader代码
Shader "Unlit/OCFShader_minimalWorkingExample" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_fog #include "UnityCG.cginc" float4x4 _CurrentVP; float4x4 _PreviousVP; float3 calculateVelocity(float4 worldPosition) { float4 currClip = mul(_CurrentVP, worldPosition); float4 prevClip = mul(_PreviousVP, worldPosition); float3 currNDC = currClip.xyz / currClip.w; float3 prevNDC = prevClip.xyz / prevClip.w; return currNDC - prevNDC; } struct appdata { float4 vertex : POSITION; }; struct v2f { UNITY_FOG_COORDS(1) float4 vertex : SV_POSITION; float3 worldPos : TEXCOORD4; }; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; UNITY_TRANSFER_FOG(o, o.vertex); return o; } float4 frag (v2f i) : SV_Target { float3 velocity = 20.0 * calculateVelocity(float4(i.worldPos, 1.0)); UNITY_APPLY_FOG(i.fogCoord, float4(velocity,1)); return float4(velocity.xyz, 1.0); } ENDCG } } }
C#脚本代码
using UnityEngine; [ExecuteAlways] public class CameraPropertiesShaderFeed : MonoBehaviour { private Camera cam; private Matrix4x4 prevVP; private static readonly int PrevVPID = Shader.PropertyToID("_PreviousVP"); private static readonly int CurrVPID = Shader.PropertyToID("_CurrentVP"); void OnEnable() { cam = GetComponent<Camera>(); if (!cam) cam = Camera.main; prevVP = cam.nonJitteredProjectionMatrix * cam.worldToCameraMatrix; } void LateUpdate() { var currVP = cam.nonJitteredProjectionMatrix * cam.worldToCameraMatrix; Shader.SetGlobalMatrix(CurrVPID, currVP); Shader.SetGlobalMatrix(PrevVPID, prevVP); prevVP = currVP; } }
抖动原因分析
- 浮点精度损失:世界坐标转NDC时的除法运算(
/ w)会引入精度误差,远离相机的区域w值极大,小数部分精度不足,导致前后帧NDC差值出现抖动。 - 插值误差:顶点传递的世界坐标在像素阶段是插值得到的,插值过程的精度损失会放大抖动。
- 矩阵时序不匹配:LateUpdate中更新矩阵可能和渲染管线的帧时序不完全对应,导致前后帧VP矩阵并非严格相邻的相机状态。
解决思路与优化方案
1. 顶点阶段预计算运动向量(减少插值误差)
将运动向量计算移至顶点阶段,直接输出NDC差值,像素阶段直接使用插值后的结果,避免像素阶段重新计算世界坐标除法带来的精度问题:
// 修改v2f结构 struct v2f { UNITY_FOG_COORDS(1) float4 vertex : SV_POSITION; float3 velocity : TEXCOORD4; }; // 修改顶点函数 v2f vert (appdata v) { v2f o; float4 worldPos = mul(unity_ObjectToWorld, v.vertex); o.vertex = UnityObjectToClipPos(v.vertex); // 顶点阶段计算运动向量 float4 currClip = mul(_CurrentVP, worldPos); float4 prevClip = mul(_PreviousVP, worldPos); float3 currNDC = currClip.xyz / currClip.w; float3 prevNDC = prevClip.xyz / prevClip.w; o.velocity = currNDC - prevNDC; UNITY_TRANSFER_FOG(o, o.vertex); return o; } // 修改片元函数 float4 frag (v2f i) : SV_Target { float3 velocity = 20.0 * i.velocity; UNITY_APPLY_FOG(i.fogCoord, float4(velocity,1)); return float4(velocity.xyz, 1.0); }
2. 优化矩阵更新时机与精度
- 调整矩阵更新时机:改用
OnPreRender确保矩阵和渲染帧严格对应,避免LateUpdate的时序偏差:
void OnPreRender() { var currVP = cam.nonJitteredProjectionMatrix * cam.worldToCameraMatrix; Shader.SetGlobalMatrix(CurrVPID, currVP); Shader.SetGlobalMatrix(PrevVPID, prevVP); prevVP = currVP; }
- 使用高精度矩阵暂存:用double类型存储矩阵,减少累积精度损失:
private Matrix4x4d prevVPd; void OnEnable() { cam = GetComponent<Camera>(); if (!cam) cam = Camera.main; prevVPd = (Matrix4x4d)cam.nonJitteredProjectionMatrix * (Matrix4x4d)cam.worldToCameraMatrix; prevVP = (Matrix4x4)prevVPd; } void OnPreRender() { Matrix4x4d currVPd = (Matrix4x4d)cam.nonJitteredProjectionMatrix * (Matrix4x4d)cam.worldToCameraMatrix; Shader.SetGlobalMatrix(CurrVPID, (Matrix4x4)currVPd); Shader.SetGlobalMatrix(PrevVPID, prevVP); prevVP = (Matrix4x4)currVPd; prevVPd = currVPd; }
3. 拆分VP矩阵计算(降低乘法精度损失)
单独传递视图矩阵和投影矩阵,分开计算视空间到NDC的转换,减少矩阵乘法的精度损失:
- C#中新增传递上一帧的视图矩阵和投影矩阵:
private static readonly int PrevVMatrixID = Shader.PropertyToID("_PreviousV"); private static readonly int PrevPMatrixID = Shader.PropertyToID("_PreviousP"); private Matrix4x4d prevVd, prevPd; void OnEnable() { cam = GetComponent<Camera>(); if (!cam) cam = Camera.main; prevVd = (Matrix4x4d)cam.worldToCameraMatrix; prevPd = (Matrix4x4d)cam.nonJitteredProjectionMatrix; Shader.SetGlobalMatrix(PrevVMatrixID, (Matrix4x4)prevVd); Shader.SetGlobalMatrix(PrevPMatrixID, (Matrix4x4)prevPd); prevVP = cam.nonJitteredProjectionMatrix * cam.worldToCameraMatrix; } void OnPreRender() { Matrix4x4 currV = cam.worldToCameraMatrix; Matrix4x4 currP = cam.nonJitteredProjectionMatrix; Matrix4x4 currVP = currP * currV; Shader.SetGlobalMatrix(CurrVPID, currVP); Shader.SetGlobalMatrix(PrevVPID, prevVP); Shader.SetGlobalMatrix(PrevVMatrixID, (Matrix4x4)prevVd); Shader.SetGlobalMatrix(PrevPMatrixID, (Matrix4x4)prevPd); prevVP = currVP; prevVd = (Matrix4x4d)currV; prevPd = (Matrix4x4d)currP; }
- Shader中修改计算逻辑:
float4x4 _PreviousV; float4x4 _PreviousP; float3 calculateVelocity(float4 worldPosition) { float4 currView = mul(UNITY_MATRIX_V, worldPosition); float4 prevView = mul(_PreviousV, worldPosition); float4 currClip = mul(UNITY_MATRIX_P, currView); float4 prevClip = mul(_PreviousP, prevView); float3 currNDC = currClip.xyz / currClip.w; float3 prevNDC = prevClip.xyz / prevClip.w; return currNDC - prevNDC; }
4. 可选:后处理平滑滤波
如果仍有轻微抖动,可添加后处理Pass对运动向量纹理进行高斯模糊或双边滤波,仅对相似向量进行加权平均,避免破坏向量精度。
内容的提问来源于stack exchange,提问作者Luca Hilbrich
相关产品推荐
相关产品推荐

