SRPCore Motion Vector流程以及材质管理
序言
原本一开始是想着写个简单点的SSR的,看到HDRP SSR利用Motion Vector重新投影能够用历史帧实现异步,就肘不动道了。
于是乎就想通过这篇文章记录关于unity实现Motion Vector输出的一些坑点(材质Pass Enable/模版值管理)。
Motion Vector Pass
Motion Vector记录的是当前帧像素的运动矢量,也就是当前像素在上一帧时的屏幕空间坐标与当前的屏幕空间坐标之差。
而Motion Vector最主要的用途是用当前帧像素的屏幕空间坐标重新投影得到上一帧的屏幕空间坐标,从而复用上一帧的渲染结果。
减少当前帧资源的依赖最大的好处也就是,如果平台支持多线程渲染的话,可以通过AsyncCompute进一步提高GPU的利用率。
\(curPosNDC-perPosNDC\)
Motion Vector计算一共分为两种:
Skinned Mesh Renderer,Alembic(Precomputed Motion Vector)等动态物体输出的Per Object Motion Vector,
以及Camera Motion Vector。
需要注意的是:Skinned Mesh Renderer的输出勾选上Skinned Motion vector(需要把全部材质的MotionVector关闭,达成取消勾选不输出MotionVector),GPU蒙皮插值计算会用到两个缓冲区,一个是当前帧,一个是前一帧,Motion Vector就等当前帧的位置减去上一帧的位置。
可以看到Graphics内存分析里面确实多了一倍(用的是23版本的MemoryProfile)
Unity GPU蒙皮计算完之后,我们在Shader里面的PositionOS就是插值过后的结果。
可以看到勾选了Skinned Motion vector之后如果要读取上一帧的PositionOS就需要从TEXCOORD4中读取。
如果Skinned Mesh Renderer没有开启Skinned Motion vector就不输出Object Motion Vector,由Camera Motion Vector的计算结果代替/或者使用当前帧的PositionOS当做上一帧的PositionOS。
具体要怎么在SRP中实现呢?
Object Motion Vector:
CommandBuffer调用绘制RendererList指令绘制Motion Vector pass,输出到Motion Vector RT(R16G16_SFloat)上。
为了减少DrawCall,原本Depth Normal Pre Pass以及Motion vector Pass可以合并在一起,在输出Motion Vector的同时顺便把Normal(Smoothness)输出。
计算Object Motion Vector需要当前帧以及上一帧的M矩阵,当前帧以及上一帧的PositionOS。
然后就能计算出上一帧以及当前帧的PositionCS(没有透除的Clip Spacce,不要与SV_POSITION搞混)
片元中再做透视除法得到CurPositionCS以及PrePositionCS,做差得到MotionVector(in Clip Space (-1,1))。
MotionVector*0.5转换到NDC Space
最后再把MotionVector Encode输出到Motion Vector RT上。
Camera Motion Vector:
Camera Motion Vector就没有那么麻烦,只需要管线中记录(UNITY_MATRIX_PREV_VP / UNITY_MATRIX_VP)上一帧以及当前帧的VP矩阵,
先用深度求PositionWS。
然后再用PositionWS以及VP矩阵就能计算出上一帧的PositionCS以及当前的帧PositionCS。
后续的计算也就跟Object Motion Vector类似就不细说了。(透除,Encode)
Camera记录上一帧VP矩阵
上一帧的VP矩阵,unity默认并没有提供给Unity Pre Draw Constant Buffer之中,需要我们自己记录上一帧的VP矩阵,并提交到Constant Buffer中
记录上一帧的VP矩阵的实现方式很多,记录在哪一个实例身上就有讲究。
一般来说,用additional Camera Data去处理就可以了。
但是通常为了避免渲染线程内对逻辑线程的脚本的对象进行TryGetComponent类似的操作,
还是会在管线渲染线程这边创建自定义的相机对象(例如HDRP的HDCamera,RenderLoop的时候通过Camera,HDCamera字典映射对象),在渲染线程只对自定义的相机对象操作。
internal bool isFirstFrame { get; private set; }
internal HDCamera(Camera cam)
{
//Reset()
isFirstFrame = true;
}
void UpdateViewConstants(ref ViewConstants viewConstants, Matrix4x4 projMatrix, Matrix4x4 viewMatrix, Vector3 cameraPosition, bool jitterProjectionMatrix, bool updatePreviousFrameConstants)
{
// If TAA is enabled projMatrix will hold a jittered projection matrix. The original,
// non-jittered projection matrix can be accessed via nonJitteredProjMatrix.
var nonJitteredCameraProj = projMatrix;
var cameraProj = jitterProjectionMatrix
? GetJitteredProjectionMatrix(nonJitteredCameraProj)
: nonJitteredCameraProj;
// The actual projection matrix used in shaders is actually massaged a bit to work across all platforms
// (different Z value ranges etc.)
var gpuProj = GL.GetGPUProjectionMatrix(cameraProj, true); // Had to change this from 'false'
var gpuView = viewMatrix;
var gpuNonJitteredProj = GL.GetGPUProjectionMatrix(nonJitteredCameraProj, true);
if (ShaderConfig.s_CameraRelativeRendering != 0)
{
// Zero out the translation component.
gpuView.SetColumn(3, new Vector4(0, 0, 0, 1));
}
var gpuVP = gpuNonJitteredProj * gpuView;
Matrix4x4 noTransViewMatrix = gpuView;
if (ShaderConfig.s_CameraRelativeRendering == 0)
{
// In case we are not camera relative, gpuView contains the camera translation component at this stage, so we need to remove it.
noTransViewMatrix.SetColumn(3, new Vector4(0, 0, 0, 1));
}
var gpuVPNoTrans = gpuNonJitteredProj * noTransViewMatrix;
// A camera can be rendered multiple times in a single frame with different resolution/fov that would change the projection matrix
// In this case we need to update previous rendering information.
// We need to make sure that this code is not called more than once for one camera rendering (not frame! multiple renderings can happen in one frame) otherwise we'd overwrite previous rendering info
// Note: if your first rendered view during the frame is not the Game view, everything breaks.
if (updatePreviousFrameConstants)
{
if (isFirstFrame)
{
viewConstants.prevWorldSpaceCameraPos = cameraPosition;
viewConstants.prevViewMatrix = gpuView;
//上一帧的VP矩阵
viewConstants.prevViewProjMatrix = gpuVP;
viewConstants.prevInvViewProjMatrix = viewConstants.prevViewProjMatrix.inverse;
viewConstants.prevViewProjMatrixNoCameraTrans = gpuVPNoTrans;
}
else
{
//不是第一帧就用没更新之前的的矩阵值
viewConstants.prevWorldSpaceCameraPos = viewConstants.worldSpaceCameraPos;
viewConstants.prevViewMatrix = viewConstants.viewMatrix;
viewConstants.prevViewProjMatrix = viewConstants.nonJitteredViewProjMatrix;
viewConstants.prevViewProjMatrixNoCameraTrans = viewConstants.viewProjectionNoCameraTrans;
}
}
viewConstants.viewMatrix = gpuView;
viewConstants.invViewMatrix = gpuView.inverse;
viewConstants.projMatrix = gpuProj;
viewConstants.invProjMatrix = gpuProj.inverse;
viewConstants.viewProjMatrix = gpuProj * gpuView;
viewConstants.invViewProjMatrix = viewConstants.viewProjMatrix.inverse;
viewConstants.nonJitteredViewProjMatrix = gpuNonJitteredProj * gpuView;
viewConstants.worldSpaceCameraPos = cameraPosition;
viewConstants.worldSpaceCameraPosViewOffset = Vector3.zero;
viewConstants.viewProjectionNoCameraTrans = gpuVPNoTrans;
...
}
//
internal void Update(FrameSettings currentFrameSettings, HDRenderPipeline hdrp, XRPass xrPass, bool allocateHistoryBuffers = true)
{
...
//Update第一帧相关的ViewConstant之后立马将isFirstFrame设为false
UpdateViewConstants();
isFirstFrame = false;
cameraFrameCount++;
}
//更新GlobalConstantBuffer的值
unsafe internal void UpdateShaderVariablesGlobalCB(ref ShaderVariablesGlobal cb, int frameCount)
{
bool taaEnabled = frameSettings.IsEnabled(FrameSettingsField.Postprocess)
&& antialiasing == AntialiasingMode.TemporalAntialiasing
&& camera.cameraType == CameraType.Game;
cb._ViewMatrix = mainViewConstants.viewMatrix;
cb._CameraViewMatrix = mainViewConstants.viewMatrix;
cb._InvViewMatrix = mainViewConstants.invViewMatrix;
cb._ProjMatrix = mainViewConstants.projMatrix;
cb._InvProjMatrix = mainViewConstants.invProjMatrix;
cb._ViewProjMatrix = mainViewConstants.viewProjMatrix;
cb._CameraViewProjMatrix = mainViewConstants.viewProjMatrix;
cb._InvViewProjMatrix = mainViewConstants.invViewProjMatrix;
cb._NonJitteredViewProjMatrix = mainViewConstants.nonJitteredViewProjMatrix;
cb._PrevViewProjMatrix = mainViewConstants.prevViewProjMatrix;
cb._PrevInvViewProjMatrix = mainViewConstants.prevInvViewProjMatrix;
cb._WorldSpaceCameraPos_Internal = mainViewConstants.worldSpaceCameraPos;
cb._PrevCamPosRWS_Internal = mainViewConstants.prevWorldSpaceCameraPos;
...
}
Per Object Motion
Depth Pre Pass
调用Motion Vector,需要留意的是,为了节省DrawCall,在输出Motion Vector的同时,也要顺便输出DepthOnly/DepthNormal相关RT上。
所以Depth Pre Pass的时候 DrawRendererList需要过滤掉输出Object Motion Vector的Skinned Mesh Renderer。(RendererListDesc.excludeObjectMotionVectors设置为true)
bool RenderDepthPrepass(RenderGraph renderGraph, CullingResults cull, HDCamera hdCamera, ref PrepassOutput output, out TextureHandle decalBuffer)
{
if (!hdCamera.frameSettings.IsEnabled(FrameSettingsField.OpaqueObjects))
{
decalBuffer = renderGraph.defaultResources.blackTextureXR;
output.depthAsColor = CreateDepthAsColorBuffer(renderGraph, hdCamera.msaaSamples);
output.normalBuffer = CreateNormalBuffer(renderGraph, hdCamera, hdCamera.msaaSamples);
return false;
}
bool msaa = hdCamera.msaaEnabled;
bool decalLayersEnabled = hdCamera.frameSettings.IsEnabled(FrameSettingsField.DecalLayers);
bool decalsEnabled = hdCamera.frameSettings.IsEnabled(FrameSettingsField.Decals);
bool fullDeferredPrepass = hdCamera.frameSettings.IsEnabled(FrameSettingsField.DepthPrepassWithDeferredRendering);
// To avoid rendering objects twice (once in the depth pre-pass and once in the motion vector pass when the motion vector pass is enabled) we exclude the objects that have motion vectors.
bool objectMotionEnabled = hdCamera.frameSettings.IsEnabled(FrameSettingsField.ObjectMotionVectors);
bool excludeMotion = fullDeferredPrepass ? objectMotionEnabled : false;
bool shouldRenderMotionVectorAfterGBuffer = (hdCamera.frameSettings.litShaderMode == LitShaderMode.Deferred) && !fullDeferredPrepass;
// Needs to be created ahead because it's used in both pre-passes.
if (decalLayersEnabled)
decalBuffer = CreateDecalPrepassBuffer(renderGraph, hdCamera.msaaSamples);
else
decalBuffer = renderGraph.defaultResources.blackTextureXR;
string forwardPassName = hdCamera.frameSettings.litShaderMode == LitShaderMode.Deferred ? "Forward Depth Prepass (Deferred ForwardOnly)" : "Forward Depth Prepass";
// Then prepass for forward materials.
using (var builder = renderGraph.AddRenderPass<DrawRendererListPassData>(forwardPassName, out var passData, ProfilingSampler.Get(HDProfileId.ForwardDepthPrepass)))
{
passData.frameSettings = hdCamera.frameSettings;
output.depthBuffer = builder.UseDepthBuffer(output.depthBuffer, DepthAccess.ReadWrite);
int mrtIndex = 0;
if (msaa)
output.depthAsColor = builder.UseColorBuffer(CreateDepthAsColorBuffer(renderGraph, hdCamera.msaaSamples), mrtIndex++);
output.normalBuffer = builder.UseColorBuffer(CreateNormalBuffer(renderGraph, hdCamera, hdCamera.msaaSamples), mrtIndex++);
if (decalLayersEnabled)
decalBuffer = builder.UseColorBuffer(decalBuffer, mrtIndex++);
if (hdCamera.frameSettings.litShaderMode == LitShaderMode.Forward)
{
RenderStateBlock? stateBlock = null;
if (!hdCamera.frameSettings.IsEnabled(FrameSettingsField.AlphaToMask))
stateBlock = m_AlphaToMaskBlock;
//excludeObjectMotionVectors 排除掉输出ObjectMotionVector的SkinnedMeshRenderer
passData.rendererList = builder.UseRendererList(renderGraph.CreateRendererList(
CreateOpaqueRendererListDesc(cull, hdCamera.camera, m_DepthOnlyAndDepthForwardOnlyPassNames, stateBlock: stateBlock, excludeObjectMotionVectors: objectMotionEnabled)));
}
else if (hdCamera.frameSettings.litShaderMode == LitShaderMode.Deferred)
{
// Forward only material that output normal buffer
passData.rendererList = builder.UseRendererList(renderGraph.CreateRendererList(
CreateOpaqueRendererListDesc(cull, hdCamera.camera, m_DepthForwardOnlyPassNames, stateBlock: m_AlphaToMaskBlock, excludeObjectMotionVectors: excludeMotion)));
}
builder.SetRenderFunc(
(DrawRendererListPassData data, RenderGraphContext context) =>
{
DrawOpaqueRendererList(context.renderContext, context.cmd, data.frameSettings, data.rendererList);
});
}
return shouldRenderMotionVectorAfterGBuffer;
}
关闭所有材质的Motion Vectors Pass
另外在Object Motion Vector Pass调用DrawRendererList之前,需要把所有的材质MOTIONVECTOR pass禁用掉。
管线c++端绘制时就不会把其他Renderer(包含有MotionVectors LightMode 相关材质的MeshRenderer/没有勾选Skinned Motion vector的SkinnedMeshRenderer)也加入渲染中。
//Lit.Shader
Pass
{
Name "MotionVectors"
Tags{ "LightMode" = "MotionVectors" } // Caution, this need to be call like this to setup the correct parameters by C++ (legacy Unity)
// If velocity pass (motion vectors) is enabled we tag the stencil so it don't perform CameraMotionVelocity
Stencil
{
WriteMask [_StencilWriteMaskMV]
Ref [_StencilRefMV]
Comp Always
Pass Replace
}
...
}
禁用所有的材质Motion Vector pass,HDRP是借由ShaderGUI中ValidateMaterial接口调用静态函数控制Motion Vector pass开关。
之所以用静态函数,是因为想让当前加载进来的Material都能够跟着代码改动同步修改关键字设置以及Pass的开关。
//UnlitGUI.cs
public override void ValidateMaterial(Material material) => SetupUnlitKeywordsAndPass(material);
// All Setup Keyword functions must be static. It allow to create script to automatically update the shaders with a script if code change
public static void SetupUnlitKeywordsAndPass(Material material)
{
material.SetupBaseUnlitKeywords();
material.SetupBaseUnlitPass();
....
}
// All Setup Keyword functions must be static. It allow to create script to automatically update the shaders with a script if code change
static public void SetupBaseUnlitPass(this Material material)
{
...
// Shader graphs materials have their own management of motion vector pass in the material inspector
// (see DrawMotionVectorToggle())
if (!material.IsShaderGraph())
{
//In the case of additional velocity data we will enable the motion vector pass.
bool addPrecomputedVelocity = false;
if (material.HasProperty(kAddPrecomputedVelocity))
{
addPrecomputedVelocity = material.GetInt(kAddPrecomputedVelocity) != 0;
}
// We don't have any vertex animation for lit/unlit vector, so we
// setup motion vector pass to false. Remind that in HDRP this
// doesn't disable motion vector, it just mean that the material
// don't do any vertex deformation but we can still have
// skinning / morph target
material.SetShaderPassEnabled(HDShaderPassNames.s_MotionVectorsStr, addPrecomputedVelocity);
}
}
设置Object Motion Vector模版值
在输出Object Motion Vector像素写入模版值,让后续的Camera Motion Vector Pass只在模版测试通过(Object Motion Vector的空白区域)的像素上面计算,
从而Camera Motion Vector Pass减少计算的像素。
而设置模版值也是通过上面提及过的ShaderGUI中ValidateMaterial接口进行设置。
值得留意的一点是,Stencil模板值的管理不推荐写hard core,最好的方式还是写成Enum的形式,方便调整。
//这里为了提前减少不接受SSR物体表面的计算,同样也是用到了模版 stencilRefMV |= (int)StencilUsage.TraceReflectionRay;
//
static public void ComputeStencilProperties(bool receivesSSR, bool useSplitLighting, out int stencilRef, out int stencilWriteMask,
out int stencilRefDepth, out int stencilWriteMaskDepth, out int stencilRefGBuffer, out int stencilWriteMaskGBuffer,
out int stencilRefMV, out int stencilWriteMaskMV)
{
// Stencil usage rules:
// TraceReflectionRay need to be tagged during depth prepass
// RequiresDeferredLighting need to be tagged during GBuffer
// SubsurfaceScattering need to be tagged during either GBuffer or Forward pass
// ObjectMotionVectors need to be tagged in velocity pass.
// As motion vectors pass can be use as a replacement of depth prepass it also need to have TraceReflectionRay
// As GBuffer pass can have no depth prepass, it also need to have TraceReflectionRay
// Object motion vectors is always render after a full depth buffer (if there is no depth prepass for GBuffer all object motion vectors are render after GBuffer)
// so we have a guarantee than when we write object motion vectors no other object will be draw on top (and so would have require to overwrite motion vectors).
// Final combination is:
// Prepass: TraceReflectionRay
// Motion vectors: TraceReflectionRay, ObjectVelocity
// GBuffer: LightingMask, ObjectVelocity
// Forward: LightingMask
stencilRef = (int)StencilUsage.Clear; // Forward case
stencilWriteMask = (int)StencilUsage.RequiresDeferredLighting | (int)StencilUsage.SubsurfaceScattering;
stencilRefDepth = 0;
stencilWriteMaskDepth = 0;
stencilRefGBuffer = (int)StencilUsage.RequiresDeferredLighting;
stencilWriteMaskGBuffer = (int)StencilUsage.RequiresDeferredLighting | (int)StencilUsage.SubsurfaceScattering;
stencilRefMV = (int)StencilUsage.ObjectMotionVector;
stencilWriteMaskMV = (int)StencilUsage.ObjectMotionVector;
if (useSplitLighting)
{
stencilRefGBuffer |= (int)StencilUsage.SubsurfaceScattering;
stencilRef |= (int)StencilUsage.SubsurfaceScattering;
}
if (receivesSSR)
{
stencilRefDepth |= (int)StencilUsage.TraceReflectionRay;
stencilRefGBuffer |= (int)StencilUsage.TraceReflectionRay;
stencilRefMV |= (int)StencilUsage.TraceReflectionRay;
}
stencilWriteMaskDepth |= (int)StencilUsage.TraceReflectionRay;
stencilWriteMaskGBuffer |= (int)StencilUsage.TraceReflectionRay;
stencilWriteMaskMV |= (int)StencilUsage.TraceReflectionRay;
}
static public void SetupStencil(Material material, bool receivesSSR, bool useSplitLighting)
{
ComputeStencilProperties(receivesSSR, useSplitLighting, out int stencilRef, out int stencilWriteMask,
out int stencilRefDepth, out int stencilWriteMaskDepth, out int stencilRefGBuffer, out int stencilWriteMaskGBuffer,
out int stencilRefMV, out int stencilWriteMaskMV
);
// As we tag both during motion vector pass and Gbuffer pass we need a separate state and we need to use the write mask
if (material.HasProperty(kStencilRef))
{
material.SetInt(kStencilRef, stencilRef);
material.SetInt(kStencilWriteMask, stencilWriteMask);
}
if (material.HasProperty(kStencilRefDepth))
{
material.SetInt(kStencilRefDepth, stencilRefDepth);
material.SetInt(kStencilWriteMaskDepth, stencilWriteMaskDepth);
}
if (material.HasProperty(kStencilRefGBuffer))
{
material.SetInt(kStencilRefGBuffer, stencilRefGBuffer);
material.SetInt(kStencilWriteMaskGBuffer, stencilWriteMaskGBuffer);
}
if (material.HasProperty(kStencilRefDistortionVec))
{
material.SetInt(kStencilRefDistortionVec, (int)StencilUsage.DistortionVectors);
material.SetInt(kStencilWriteMaskDistortionVec, (int)StencilUsage.DistortionVectors);
}
if (material.HasProperty(kStencilRefMV))
{
material.SetInt(kStencilRefMV, stencilRefMV);
material.SetInt(kStencilWriteMaskMV, stencilWriteMaskMV);
}
}
DrawRendererList
调用绘制就比较简单,需要注意的是PerObjectData以及camera.depthTextureMode的设置
void RenderObjectsMotionVectors(RenderGraph renderGraph, CullingResults cull, HDCamera hdCamera, TextureHandle decalBuffer, in PrepassOutput output)
{
if (!hdCamera.frameSettings.IsEnabled(FrameSettingsField.ObjectMotionVectors) ||
!hdCamera.frameSettings.IsEnabled(FrameSettingsField.OpaqueObjects))
return;
using (var builder = renderGraph.AddRenderPass<DrawRendererListPassData>("Objects Motion Vectors Rendering", out var passData, ProfilingSampler.Get(HDProfileId.ObjectsMotionVector)))
{
// These flags are still required in SRP or the engine won't compute previous model matrices...
// If the flag hasn't been set yet on this camera, motion vectors will skip a frame.
hdCamera.camera.depthTextureMode |= DepthTextureMode.MotionVectors | DepthTextureMode.Depth;
passData.frameSettings = hdCamera.frameSettings;
builder.UseDepthBuffer(output.depthBuffer, DepthAccess.ReadWrite);
BindMotionVectorPassColorBuffers(builder, output, decalBuffer, hdCamera);
RenderStateBlock? stateBlock = null;
if (hdCamera.frameSettings.litShaderMode == LitShaderMode.Deferred || !hdCamera.frameSettings.IsEnabled(FrameSettingsField.AlphaToMask))
stateBlock = m_AlphaToMaskBlock;
passData.rendererList = builder.UseRendererList(
renderGraph.CreateRendererList(CreateOpaqueRendererListDesc(cull, hdCamera.camera, HDShaderPassNames.s_MotionVectorsName, PerObjectData.MotionVectors, stateBlock: stateBlock)));
builder.SetRenderFunc(
(DrawRendererListPassData data, RenderGraphContext context) =>
{
DrawOpaqueRendererList(context, data.frameSettings, data.rendererList);
});
}
}
UnityPerDraw ConstantBuffer
PerObjectData设置为PerObjectData.MotionVectors我猜测就是让UnityPerDraw中增加unity_MatrixPreviousM,unity_MatrixPreviousMI以及unity_MotionVectorsParams参数
unity_MatrixPreviousM也就是上一帧的M矩阵。对于SkinnedMeshRenderer来说这个矩阵跟unity_ObjectToWorld类似,也就是在上一帧中Skinned Mesh Renderer中轴点的Transform位置对应的Matrix。
unity_MotionVectorsParams主要是用来标志Renderer上面的Motion Vector模式。
unity_MotionVectorsParams.x就是判断当前的Renderer是否读取上一帧的Position(只有Skinned Mesh Renderer)
unity_MotionVectorsParams.y Renderer是否强制不输出Motion vector。
由于和Depth Pre Pass合并了,深度z偏移值unity_MotionVectorsParams.z值也就没有用了。
unity_MotionVectorsParams.w Renderer使用CameraMotionVector作为代替。
CBUFFER_START(UnityPerDraw)
float4x4 unity_ObjectToWorld;
float4x4 unity_WorldToObject;
float4 unity_LODFade; // x is the fade value ranging within [0,1]. y is x quantized into 16 levels
float4 unity_WorldTransformParams; // w is usually 1.0, or -1.0 for odd-negative scale transforms
float4 unity_RenderingLayer;
float4 unity_LightmapST;
float4 unity_DynamicLightmapST;
// SH lighting environment
float4 unity_SHAr;
float4 unity_SHAg;
float4 unity_SHAb;
float4 unity_SHBr;
float4 unity_SHBg;
float4 unity_SHBb;
float4 unity_SHC;
// x = Disabled(0)/Enabled(1)
// y = Computation are done in global space(0) or local space(1)
// z = Texel size on U texture coordinate
float4 unity_ProbeVolumeParams;
float4x4 unity_ProbeVolumeWorldToObject;
float4 unity_ProbeVolumeSizeInv; // Note: This variable is float4 and not float3 (compare to builtin unity) to be compatible with SRP batcher
float4 unity_ProbeVolumeMin; // Note: This variable is float4 and not float3 (compare to builtin unity) to be compatible with SRP batcher
// This contain occlusion factor from 0 to 1 for dynamic objects (no SH here)
float4 unity_ProbesOcclusion;
// Velocity
float4x4 unity_MatrixPreviousM;
float4x4 unity_MatrixPreviousMI;
//X : Use last frame positions (right now skinned meshes are the only objects that use this
//Y : Force No Motion
//Z : Z bias value
//W : Camera only
float4 unity_MotionVectorsParams;
CBUFFER_END
Motion Vector Shader
Vertex Shader
需要注意的点:Skinned Mesh Renderer可以从TEXCOORD4读取上一帧的PositionOS。
计算的当前帧以及上一帧PositionCS的不需要透除
//读取上一帧的PositionOS需要通过TEXCOORD4
// Available semantic start from TEXCOORD4
struct AttributesPass
{
float3 previousPositionOS : TEXCOORD4; // Contain previous transform position (in case of skinning for example)
#if defined (_ADD_PRECOMPUTED_VELOCITY)
float3 precomputedVelocity : TEXCOORD5; // Add Precomputed Velocity (Alembic computes velocities on runtime side).
#endif
};
struct Attributes
{
float3 positionOS : POSITION;
#ifdef ATTRIBUTES_NEED_NORMAL
float3 normalOS : NORMAL;
#endif
#ifdef ATTRIBUTES_NEED_TANGENT
float4 tangentOS : TANGENT; // Store sign in w
#endif
#ifdef ATTRIBUTES_NEED_TEXCOORD0
float2 uv0 : TEXCOORD0;
#endif
#ifdef ATTRIBUTES_NEED_TEXCOORD1
float2 uv1 : TEXCOORD1;
#endif
#ifdef ATTRIBUTES_NEED_TEXCOORD2
float2 uv2 : TEXCOORD2;
#endif
#ifdef ATTRIBUTES_NEED_TEXCOORD3
float2 uv3 : TEXCOORD3;
#endif
#ifdef ATTRIBUTES_NEED_COLOR
float4 vertexColor : COLOR;
#endif
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct VaryingsMotionVectors
{
float2 uv0 : TEXCOORD0;
float4 positionCS : SV_POSITION;
float3 normalWS : TEXCOORD1;
#ifdef _NORMALMAP
float4 tangentWS : TEXCOORD2;
#endif
#ifdef ATTRIBUTES_NEED_TEXCOORD3
float2 uv3 : TEXCOORD4;
#endif
float4 currentPositionCS:TEXCOORD5;
float4 previousPositionCS:TEXCOORD6;
UNITY_VERTEX_INPUT_INSTANCE_ID
UNITY_VERTEX_OUTPUT_STEREO
#if SHADER_STAGE_FRAGMENT
//D3D11 SV_IsFrontFace
FRONT_FACE_TYPE cullFace : FRONT_FACE_SEMANTIC;
#endif
};
VaryingsMotionVectors LitMotionVectorPassVertex(Attributes input, AttributesPass inputPass)
{
VaryingsMotionVectors output;
ZERO_INITIALIZE(VaryingsMotionVectors, output);
UNITY_SETUP_INSTANCE_ID(input);
UNITY_TRANSFER_INSTANCE_ID(input, output);
float3 positionWS = TransformObjectToWorld(input.positionOS.xyz);
float3 effectivePositionOS = (float3)0.0f;
bool hasDeformation = unity_MotionVectorsParams.x > 0.0; // Skin or morph target
effectivePositionOS = (hasDeformation ? inputPass.previousPositionOS : input.positionOS);
float3 previousPositionRWS = TransformPreviousObjectToWorld(effectivePositionOS);
output.positionCS = TransformWorldToHClip(positionWS);
//用VP矩阵计算ClipSpace,不需要透除
output.currentPositionCS = mul(UNITY_MATRIX_VP, float4(positionWS, 1.0));
output.previousPositionCS = mul(UNITY_MATRIX_PREV_VP, float4(previousPositionRWS, 1.0));
...
}
Fragment Shader
计算Motion Vector需要留意的是透除完positionCS之后就变成[-1,1],输出到RT上需要变换到NDC空间上[0,1]。
由于\(((positionCS * 0.5 + 0.5) - (previousPositionCS * 0.5 + 0.5)) = (motionVector * 0.5)\),所以Encode之前只需要乘0.5
struct MotionVectorPassOutput
{
#if defined(WRITE_MSAA_DEPTH)
float depthColor : SV_TARGET_DEPTH_COLOR;
#endif
#if defined(WRITE_NORMAL_BUFFER)
float4 outNormalBuffer : SV_TARGET_NORMAL;
#endif
#if defined(WRITE_DECAL_BUFFER)
float4 outDecalBuffer : SV_TARGET_DECAL;
#endif
#if defined(WRITE_MOTION_VEC_BUFFER)
float4 outMotionVecBuffer : SV_TARGET_MOTION_VEC;
#endif
};
void InitializeMotionVectorPassOutput(inout MotionVectorPassOutput output)
{
output = ZERO_INITIALIZE(MotionVectorPassOutput, output);
}
void EncodeMotionVector(float2 motionVector, out float4 outBuffer)
{
// RT - 16:16 float
outBuffer = float4(motionVector.xy, 0.0, 0.0);
}
//用顶点着色阶段计算的Clip计算 Motion Vector
// Calculate motion vector in Clip space [-1..1]
float2 CalculateMotionVector(float4 positionCS, float4 previousPositionCS)
{
// This test on define is required to remove warning of divide by 0 when initializing empty struct
// TODO: Add forward opaque MRT case...
#if (SHADERPASS == SHADERPASS_MOTION_VECTORS) || defined(_WRITE_TRANSPARENT_MOTION_VECTOR)
// Encode motion vector
positionCS.xy = positionCS.xy / positionCS.w;
previousPositionCS.xy = previousPositionCS.xy / previousPositionCS.w;
float2 motionVec = (positionCS.xy - previousPositionCS.xy);
#ifdef KILL_MICRO_MOVEMENT
motionVec.x = abs(motionVec.x) < MICRO_MOVEMENT_THRESHOLD.x ? 0 : motionVec.x;
motionVec.y = abs(motionVec.y) < MICRO_MOVEMENT_THRESHOLD.y ? 0 : motionVec.y;
#endif
motionVec = clamp(motionVec, -1.0f + MICRO_MOVEMENT_THRESHOLD, 1.0f - MICRO_MOVEMENT_THRESHOLD);
#if UNITY_UV_STARTS_AT_TOP
motionVec.y = -motionVec.y;
#endif
return motionVec;
#else
return float2(0.0, 0.0);
#endif
}
void EncodeMotionVector(float2 motionVector, out float4 outBuffer)
{
// RT - 16:16 float
outBuffer = float4(motionVector.xy, 0.0, 0.0);
}
MotionVectorPassOutput LitMotionVectorPassFragment(VaryingsMotionVectors input) : SV_Target
{
MotionVectorPassOutput output;
ZERO_INITIALIZE(MotionVectorPassOutput, output);
UNITY_SETUP_INSTANCE_ID(input);
//FragInput
FragInput fragInput = VaryingsToFragInput(input);
//SurfaceData 采样贴图信息
SurfaceData surfaceData;
ZERO_INITIALIZE(SurfaceData, surfaceData);
float3 normalTS;
float alpha = CalcSurfaceAndInputMotionVector(input, fragInput, normalTS, surfaceData);
InputData inputData;
InitializeInputData(input, fragInput, normalTS, inputData);
//输出法线
#if defined(WRITE_NORMAL_BUFFER)
NormalData normalData;
ZERO_INITIALIZE(NormalData, normalData);
normalData.normalWS = inputData.normalWS;
normalData.perceptualRoughness = PerceptualSmoothnessToPerceptualRoughness(surfaceData.perceptualSmoothness);
EncodeIntoNormalBuffer(normalData, output.outNormalBuffer);
#endif
//输出Motion Vector
#if defined(WRITE_MOTION_VEC_BUFFER)
float2 motionVector = CalculateMotionVector(input.currentPositionCS, input.previousPositionCS);
// Convert motionVector from Clip space (-1..1) to NDC 0..1 space
// Note it doesn't mean we don't have negative value, we store negative or positive offset in NDC space.
// Note: ((positionCS * 0.5 + 0.5) - (previousPositionCS * 0.5 + 0.5)) = (motionVector * 0.5)
EncodeMotionVector(motionVector * 0.5, output.outMotionVecBuffer);
//Mesh Renderer关闭Motion Vector的输出
// Note: unity_MotionVectorsParams.y is 0 is forceNoMotion is enabled
bool forceNoMotion = unity_MotionVectorsParams.y == 0.0;
// Setting the motionVector to a value more than 2 set as a flag for "force no motion". This is valid because, given that the velocities are in NDC,
// a value of >1 can never happen naturally, unless explicitely set.
if (forceNoMotion)
output.outMotionVecBuffer = float4(2.0, 0.0, 0.0, 0.0);
#endif
return output;
}
Camera Motion Vector
管线绘制调用
Camera Motion Vector管线这边调用就比较简单了只需要读取深度图做一次全屏后处理绘制就行。
//HDRenderPipeline.Prepass.cs
class CameraMotionVectorsPassData
{
public Material cameraMotionVectorsMaterial;
public TextureHandle motionVectorsBuffer;
public TextureHandle depthBuffer;
}
void RenderCameraMotionVectors(RenderGraph renderGraph, HDCamera hdCamera, TextureHandle depthBuffer, TextureHandle motionVectorsBuffer)
{
if (!hdCamera.frameSettings.IsEnabled(FrameSettingsField.MotionVectors))
return;
using (var builder = renderGraph.AddRenderPass<CameraMotionVectorsPassData>("Camera Motion Vectors Rendering", out var passData, ProfilingSampler.Get(HDProfileId.CameraMotionVectors)))
{
// These flags are still required in SRP or the engine won't compute previous model matrices...
// If the flag hasn't been set yet on this camera, motion vectors will skip a frame.
hdCamera.camera.depthTextureMode |= DepthTextureMode.MotionVectors | DepthTextureMode.Depth;
passData.cameraMotionVectorsMaterial = m_CameraMotionVectorsMaterial;
passData.depthBuffer = builder.UseDepthBuffer(depthBuffer, DepthAccess.ReadWrite);
passData.motionVectorsBuffer = builder.WriteTexture(motionVectorsBuffer);
builder.SetRenderFunc(
(CameraMotionVectorsPassData data, RenderGraphContext context) =>
{
//用模版测试减少计算
data.cameraMotionVectorsMaterial.SetInt(HDShaderIDs._StencilMask, (int)StencilUsage.ObjectMotionVector);
data.cameraMotionVectorsMaterial.SetInt(HDShaderIDs._StencilRef, (int)StencilUsage.ObjectMotionVector);
HDUtils.DrawFullScreen(context.cmd, data.cameraMotionVectorsMaterial, data.motionVectorsBuffer, data.depthBuffer, null, 0);
});
}
}
Shader
这里计算需要用到之前在自定义Camera对象中记录的UNITY_MATRIX_PREV_VP,计算当前像素上一帧的ClipPos。
后续的流程跟Object Motion Vector类似。
注意模版值的比对Operation是NotEqual,需要在没有ObjectMotionVector的地方上面计算Camera Motion Vetor
//CameraMotionVector.Shader
HLSLINCLUDE
...
half4 Frag(Varyings input) : SV_Target
{
half4 outColor = 0;
half2 uv = input.texcoord.xy;
//用Clipspace.xy配合对应的深度,求posInput.positionWS
float depth = LOAD_TEXTURE2D(_Source, input.positionCS.xy);
PositionInputs posInput = GetPositionInput(input.positionCS.xy, _ScreenSize.zw, depth, UNITY_MATRIX_I_VP, UNITY_MATRIX_V);
float4 worldPos = float4(posInput.positionWS, 1.0);
float4 prevPos = worldPos;
//管线中记录的UNITY_MATRIX_PREV_VP以及UNITY_MATRIX_VP
float4 prevClipPos = mul(UNITY_MATRIX_PREV_VP, prevPos);
float4 curClipPos = mul(UNITY_MATRIX_VP, worldPos);
float2 previousPositionCS = prevClipPos.xy / prevClipPos.w;
float2 positionCS = curClipPos.xy / curClipPos.w;
//这里还在ClipSpace中
float2 motionVector = (positionCS - previousPositionCS);
//细微的Motion Vector直接设置为0
#ifdef KILL_MICRO_MOVEMENT
motionVector.x = abs(motionVector.x) < MICRO_MOVEMENT_THRESHOLD.x ? 0 : motionVector.x;
motionVector.y = abs(motionVector.y) < MICRO_MOVEMENT_THRESHOLD.y ? 0 : motionVector.y;
#endif
motionVector = clamp(motionVector, -1.0f + MICRO_MOVEMENT_THRESHOLD, 1.0f - MICRO_MOVEMENT_THRESHOLD);
#if UNITY_UV_STARTS_AT_TOP
motionVector.y = -motionVector.y;
#endif
// Convert motionVector from Clip space (-1..1) to NDC 0..1 space
// Note it doesn't mean we don't have negative value, we store negative or positive offset in NDC space.
// Note: ((positionCS * 0.5 + 0.5) - (previousPositionCS * 0.5 + 0.5)) = (motionVector * 0.5)
EncodeMotionVector(motionVector * 0.5, outColor);
return outColor;
}
ENDHLSL
Pass
{
ZWrite Off ZTest Always Blend Off Cull Off
// We will perform camera motion vector only where there is no object motion vectors
Stencil
{
//利用模版值减少运算
WriteMask [_CameraMVStencilMask]
ReadMask [_CameraMVStencilMask]
Ref [_CameraMVStencilRef]
Comp NotEqual
// This line is intentionally commented, we keep the objectmotionvector information
// as it is used to do history rejection for numerous temporal accumulation based effects.
// Fail Zero // We won't need the bit anymore.
}
HLSLPROGRAM
#pragma editor_sync_compilation
#pragma target 4.5
#pragma only_renderers d3d11 playstation xboxone xboxseries vulkan metal switch
#pragma vertex Vert
#pragma fragment Frag
ENDHLSL
}
ValidateMaterial隐患
在上面提及了HDRP用ShaderGUI的ValidateMaterial接口关闭了所有材质的Motion Vector pass。
但是这个接口也仅仅对加载进场景的材质起到作用(这里读者可以自行Debug测试),对于是不是项目里每个材质是不是都跟ValidateMaterial中的Keyword和Pass以及模板值的设置一致都是个大问题。
因为实际情况有可能因为多人协作的原因,代码版本不一致导致材质渲染出现bug,产生垃圾关键字(影响合批)等问题。
所以,对所有材质做批量清理,减少不必要的材质字段(减少材质引用贴图资源)是很有必要的流程(无论是打包,还是美术提交材质修改)。
批量处理清理材质
主要的思路是:
1.通过调用AssetDatabase.FindAssets("t:Material" 过滤得到路径下的所有Material,获取 材质对应的shader的KeyWord以及Property并用字典缓存结果。
(ShaderUtil.GetShaderGlobalKeywords/ShaderUtil.GetShaderLocalKeywords,属于Editor Internal方法可以用之前新建Package的方式调用或者是用反射)
2.将材质转换成SerializedObject,找到 SavedProperties.TexEnvs/Floats/Colors所有序列化字段,并将其与刚刚找到 Shader的Property进行比对,如果 没有与之对应的字段则将其从TexEnvs/Floats/Colors移除,最后ApplyModifiedProperties
3.重新设置KeyWord需要 获取到shader的自定义Editor Type,通过反射, 调用(SetupxxxShaderState)静态方法 重新设置KeyWord(在这之前记得清空KeyWord)
4.处理完所有的材质AssetDatabase.SaveAssets
调用ShaderUtil Internal函数
public static class CustomShaderUtils
{
public static string[] GetShaderGlobalKeywords(Shader shader)
{
return ShaderUtil.GetShaderGlobalKeywords(shader);
}
public static string[] GetShaderLocalKeywords(Shader shader)
{
return ShaderUtil.GetShaderLocalKeywords(shader);
}
public static string GetMaterialCustomEditor(Shader shader)
{
return ShaderUtil.GetCurrentCustomEditor(shader);
}
}
ClearMaterial
public class ClearMaterialBlock : UIBlock, IDisposable
{
private static class Styles
{
internal static GUIContent bindMaterial = new GUIContent("Bind Material");
internal static GUIContent bindShaderPropertyStr = new GUIContent("Bind Property String");
}
protected override string Title
{
get => "ClearMaterial";
}
protected override string SecondaryTitle { get; }
private ReplaceMaterialKeywordPanel panel;
private Dictionary<Shader, (HashSet<string>, HashSet<string>)> shaderToKeyWordAndPropertyDic;
public bool soloClear;
public Shader clearShaderTarget;
private string relatedShaderProperty;
private float propertyVal;
private string sourceKeyWord;
private string targetKeyWord;
private void DrawBtn(Rect rect)
{
}
public ClearMaterialBlock(ReplaceMaterialKeywordPanel panel)
{
this.buttonCallbackFunc += DrawBtn;
this.panel = panel;
this.shaderToKeyWordAndPropertyDic = new Dictionary<Shader, (HashSet<string>, HashSet<string>)>();
}
protected override bool Validate()
{
return true;
}
public bool GetShaderKeywords(Shader target, out HashSet<string> hashKeyWordSet, out HashSet<string> hashPropertySet)
{
try
{
if (!this.shaderToKeyWordAndPropertyDic.TryGetValue(target, out var value))
{
var global = CustomShaderUtils.GetShaderGlobalKeywords(target);
var local = CustomShaderUtils.GetShaderLocalKeywords(target);
HashSet<string> keywordSet = new HashSet<string>();
foreach (var g in global)
{
keywordSet.Add(g);
}
foreach (var l in local)
{
keywordSet.Add(l);
}
HashSet<string> property = new HashSet<string>();
int count = target.GetPropertyCount();
for (int i = 0; i < count; i++)
{
property.Add(target.GetPropertyName(i));
}
hashKeyWordSet = keywordSet;
hashPropertySet = property;
this.shaderToKeyWordAndPropertyDic.Add(target, (hashKeyWordSet, hashPropertySet));
return true;
}
hashKeyWordSet = value.Item1;
hashPropertySet = value.Item2;
return true;
}
catch
{
hashKeyWordSet = null;
hashPropertySet = null;
return false;
}
}
public void ClearMaterial(Material m)
{
if (!GetShaderKeywords(m.shader, out var keywordSet, out var hashPropertySet))
{
return;
}
if (soloClear && m.shader != clearShaderTarget)
return;
SerializedObject o = new SerializedObject(m);
SerializedProperty disabledShaderPasses = o.FindProperty("disabledShaderPasses");
SerializedProperty SavedProperties = o.FindProperty("m_SavedProperties");
SerializedProperty TexEnvs = SavedProperties.FindPropertyRelative("m_TexEnvs");
SerializedProperty Floats = SavedProperties.FindPropertyRelative("m_Floats");
SerializedProperty Colors = SavedProperties.FindPropertyRelative("m_Colors");
//重置keywords
m.shaderKeywords = null;
disabledShaderPasses.ClearArray();
//对比属性删除残留的属性
for (int i = TexEnvs.arraySize - 1; i >= 0; i--)
{
if (!hashPropertySet.Contains(TexEnvs.GetArrayElementAtIndex(i).displayName))
{
TexEnvs.DeleteArrayElementAtIndex(i);
}
}
for (int i = Floats.arraySize - 1; i >= 0; i--)
{
if (!hashPropertySet.Contains(Floats.GetArrayElementAtIndex(i).displayName))
{
Floats.DeleteArrayElementAtIndex(i);
}
}
for (int i = Colors.arraySize - 1; i >= 0; i--)
{
if (!hashPropertySet.Contains(Colors.GetArrayElementAtIndex(i).displayName))
{
Colors.DeleteArrayElementAtIndex(i);
}
}
o.ApplyModifiedProperties();
//重新设置Keyword
string editorName = CustomShaderUtils.GetMaterialCustomEditor(m.shader);
Type editorClass = Type.GetType($"{editorName},com.xfrp.materialgui");
if (editorClass != null)
{
var methods = editorClass.GetMethods(BindingFlags.Static | BindingFlags.Public);
foreach (var method in methods)
{
if (method.Name.Contains("Setup") && method.Name.Contains("ShaderState"))
{
method.Invoke(null, new object[] {m});
}
}
}
}
protected override void OnContentGUI()
{
using (var vs = new EditorGUILayout.VerticalScope())
{
soloClear = EditorGUILayout.Toggle("Solo Clear", soloClear);
this.clearShaderTarget = (Shader) EditorGUILayout.ObjectField("检测目标shader", this.clearShaderTarget, typeof(Shader), false);
if (GUILayout.Button("Clear Material"))
{
var materialGUIDs = AssetDatabase.FindAssets("t:Material", new string[] {this.panel.selectPath});
foreach (var guid in materialGUIDs)
{
var mat = AssetDatabase.LoadAssetAtPath<Material>(AssetDatabase.GUIDToAssetPath(guid));
ClearMaterial(mat);
}
materialGUIDs = null;
AssetDatabase.SaveAssets();
}
}
}
public void Dispose()
{
foreach (var pair in shaderToKeyWordAndPropertyDic)
{
pair.Value.Item1.Clear();
pair.Value.Item2.Clear();
}
this.shaderToKeyWordAndPropertyDic.Clear();
this.shaderToKeyWordAndPropertyDic = null;
}
}