You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 03:06:00