--- title: 正交投影混合 date: 2026-05-03 00:00:00 excerpt: OrthoBlend 透视校正技术,实现动画风格的透视扁平化效果 tags: - ARC - Rendering - VertexFactory - Toon rating: ⭐ --- # 正交投影混合 返回 [[Rendering]] ## 概述 正交投影混合(OrthoBlend)是 ARC 引擎实现**动画风格透视扁平化**的核心技术。在动画/格斗游戏中,角色需要减少透视畸变以保持美术控制的外观,这通过在透视投影和正交投影之间进行混合来实现。 引擎提供了两个版本: - **V1** (`USES_ORTHO_BLEND_POSITION`):简单的 X 轴正交混合 - **V2** (`USES_ORTHO_BLEND_POSITION2`):基于物体中心距离的全透视校正("Purse correction") ## 实现细节 ### V1:X 轴正交混合 在 `Common.ush` 中定义: ```hlsl float4 GetOrthoBlendPosition( float4 WorldPosition, float4x4 ViewProjectionMatrix, float weight) { float4 ScreenPosition = mul(WorldPosition, ViewProjectionMatrix); // 计算正交投影的 X 坐标 float OrthoScreenPositionX = dot( ResolvedView.OrthoViewProjectionX, WorldPosition); // 在透视和正交之间混合 X 轴 ScreenPosition.x = lerp( ScreenPosition.x, OrthoScreenPositionX * ScreenPosition.w, weight * ResolvedView.OrthoBlendParameter); return ScreenPosition; } ``` `OrthoBlendParameter` 从 C++ 端通过 `ViewUniformShaderParameters` 传入,全局控制混合强度。`weight` 来自材质属性 `MP_OrthoBlendWeight`,允许逐材质控制。 ### V2:全透视校正 ```hlsl float4 GetOrthoBlendPosition2( float4 WorldPosition, float4x4 ViewProjectionMatrix, float weight, float3 ObjWorldPosition) { // 计算顶点相对于物体中心的偏移 float3 offset = WorldPosition.xyz - ObjWorldPosition.xyz; // 物体中心到相机的距离(沿视线方向) float dist = abs(dot( ObjWorldPosition.xyz - ResolvedView.WorldCameraOrigin.xyz, ResolvedView.ViewForward.xyz)); // 将顶点"拍平"到与物体中心相同距离的平面 float3 origin = ResolvedView.WorldCameraOrigin.xyz + ResolvedView.ViewForward.xyz * dist; WorldPosition.xy = origin.xy + offset.xy; // ... 后续投影变换 } ``` V2 的原理:将所有顶点投影到与物体中心等距的平面上,消除了前后肢体因透视导致的大小差异。 ### 全 VertexFactory 覆盖 两个版本均在所有顶点着色器中实现: | 顶点着色器 | 文件 | |-----------|------| | BasePass | `BasePassVertexShader.usf` | | DepthOnly | `DepthOnlyVertexShader.usf` | | PositionOnlyDepth | `PositionOnlyDepthVertexShader.usf` | | HitProxy | `HitProxyVertexShader.usf` | | DebugViewMode | `DebugViewModeVertexShader.usf` | 覆盖的 VertexFactory: - `LocalVertexFactory.ush` - `GpuSkinVertexFactory.ush` - `LandscapeVertexFactory.ush` - `MeshParticleVertexFactory.ush` - 所有 Particle VertexFactory ## 材质属性 通过 `MP_OrthoBlendWeight` 材质属性控制每个材质的混合权重: ```cpp // MaterialTemplate.ush float GetMaterialOrthoBlendWeight(FMaterialVertexParameters Parameters) { %s; // 由材质图生成 } ``` ## C++ 端支持 - `SceneRendering.cpp`:计算 `OrthoBlendParameter` 并写入 ViewUniformBuffer - `SceneTypes.h`:`MP_OrthoBlendWeight` 枚举值 - `HLSLMaterialTranslator.cpp`:材质编译支持 ## 关联文档 - [[自定义材质属性]] — `MP_OrthoBlendWeight` 的定义 - [[屏幕对齐网格]] — 另一种屏幕空间变换方案 - [[局部位置缩放]] — 局部空间的顶点变换 ## 完整代码解析 ### Common.ush — V1:X轴正交混合 ```hlsl // ASW Change : 2016/03/29 11:30:55 Takuro.K // V1:X轴正交混合 // 在透视投影的X轴坐标和正交投影的X轴坐标之间做 lerp // weight 来自材质属性 MP_OrthoBlendWeight // ResolvedView.OrthoBlendParameter 是全局混合强度 float4 GetOrthoBlendPosition( float4 WorldPosition, float4x4 ViewProjectionMatrix, float weight ) { #if USES_ORTHO_BLEND_POSITION float4 ScreenPosition = mul(WorldPosition, ViewProjectionMatrix); // 用正交投影矩阵的X行点乘世界坐标,得到正交投影下的X坐标 float OrthoScreenPositionX = dot(ResolvedView.OrthoViewProjectionX, WorldPosition); // 在透视X和正交X之间混合 // 注意乘以 ScreenPosition.w 是为了从 NDC 空间转回裁剪空间 ScreenPosition.x = lerp( ScreenPosition.x, // 透视X OrthoScreenPositionX * ScreenPosition.w, // 正交X(转回裁剪空间) weight * ResolvedView.OrthoBlendParameter ); // 混合权重 return ScreenPosition; #else return mul(WorldPosition, ViewProjectionMatrix); #endif } ``` ### Common.ush — V2:全透视校正("Purse correction") ```hlsl // ASW Change : 2019/01/11 13:53:00 Takeshi.N // V2:全透视校正("Purse correction") // 原理:将所有顶点投影到与物体中心等距的平面上 // 消除前后肢体因透视导致的大小差异 float4 GetOrthoBlendPosition2( float4 WorldPosition, float4x4 ViewProjectionMatrix, float weight, float3 ObjWorldPosition ) { #if USES_ORTHO_BLEND_POSITION2 #if PARTICLE_FACTORY && !PARTICLE_MESH_FACTORY // 非网格粒子不做透视校正,直接返回标准投影 return mul(WorldPosition, ViewProjectionMatrix); #else // 保存原始裁剪空间位置(用于最终 lerp) float4 ClipSpacePositionOrigin = mul(WorldPosition, ViewProjectionMatrix); // 计算顶点相对于物体中心的偏移 float3 offset = WorldPosition.xyz - ObjWorldPosition.xyz; // 物体中心到相机的距离(沿视线方向的投影距离) float dist = abs(dot( ObjWorldPosition.xyz - ResolvedView.WorldCameraOrigin.xyz, ResolvedView.ViewForward.xyz)); // 在视线方向上,距离相机 dist 处的点 float3 origin = ResolvedView.WorldCameraOrigin.xyz + ResolvedView.ViewForward.xyz * dist; // 关键步骤:将顶点XY"拍平"到与物体中心等距的平面 // 这样所有顶点在相机方向上的距离相同,消除透视缩放差异 WorldPosition.xy = origin.xy + offset.xy; float4 ClipSpacePosition = mul(WorldPosition, ViewProjectionMatrix); // X轴补偿:物体中心可能不在视线正前方 // 需要补偿因"拍平"导致的X轴偏移 float4x1 VPM_X = float4x1( ViewProjectionMatrix._11_21_31_41 ); float x1 = mul(float4(origin, 1), VPM_X); // "拍平"参考点的X float x2 = mul(float4(ObjWorldPosition, 1), VPM_X); // 物体中心的X ClipSpacePosition.x += ClipSpacePosition.w * (x2 - x1) / dist; // 根据全局参数和材质权重,在原始位置和校正位置之间混合 // step(0.01, weight) 确保 weight 接近0时完全不应用校正 return lerp( ClipSpacePositionOrigin, ClipSpacePosition, ResolvedView.OrthoBlendParameter * step(0.01, weight) ); #endif #else return mul(WorldPosition, ViewProjectionMatrix); #endif } ``` ### BasePassVertexShader.usf — 调用逻辑 ```hlsl // BasePassVertexShader.usf 中的调用逻辑 // 按优先级选择:V2 > V1 > 原始投影 #if USES_ORTHO_BLEND_POSITION2 // V2 透视校正:传入物体世界坐标作为参考点 ClipSpacePosition = GetOrthoBlendPosition2( RasterizedWorldPosition, ResolvedView.TranslatedWorldToClip, GetMaterialOrthoBlendWeight( VertexParameters ), // 材质图中设置的权重 GetActorWorldPosition(VertexParameters.PrimitiveId) // 物体世界坐标 ); #elif USES_ORTHO_BLEND_POSITION // V1 简单X轴混合 ClipSpacePosition = GetOrthoBlendPosition( RasterizedWorldPosition, ResolvedView.TranslatedWorldToClip, GetMaterialOrthoBlendWeight( VertexParameters ) ); #else // 原始透视投影 ClipSpacePosition = INVARIANT(mul(RasterizedWorldPosition, ResolvedView.TranslatedWorldToClip)); #endif ``` ### C++ 端实现 ```cpp // SceneRendering.cpp — 正交投影混合参数计算 // ASW Change : 2016/03/29 Takuro.K // 计算正交投影矩阵的 X 行(用于 Shader 中 dot 计算正交 X 坐标) static FVector4 CalcOrthoBlendParameter( const FViewMatrices& ViewMatrices, const FMatrix& EffectiveTranslatedViewMatrix) { const FVector2D FOVTheta = ViewMatrices.ComputeHalfFieldOfViewPerAxis(); float distance = ViewMatrices.GetOrthoBlendBaseDistance(); float w = distance * FMath::Tan(FOVTheta.X); float h = distance * FMath::Tan(FOVTheta.Y); // 构建正交投影矩阵,提取 X 行 const FMatrix orthoMat = FOrthoMatrix(w, h, 0, 1); const FMatrix ViewOrthoMatrix = EffectiveTranslatedViewMatrix * orthoMat; return FVector4( ViewOrthoMatrix.M[0][0], ViewOrthoMatrix.M[1][0], ViewOrthoMatrix.M[2][0], ViewOrthoMatrix.M[3][0]); } // 计算混合权重(基于相机朝向的衰减) static float CalcOrthoBlendWeight( const FViewMatrices& ViewMatrices, const FMatrix& EffectiveTranslatedViewMatrix) { float weight = ViewMatrices.GetOrthoBlendWeight(); // 相机正面朝向时权重最大,侧面时衰减 float face = ViewMatrices.GetOrthoBlendBaseRot() .RotateVector(FVector(1, 0, 0)).X; if (weight < 1.0f) { static const float THRESHOLD_MIN = 0.995f; static const float RANGE = 1.0f - THRESHOLD_MIN; face = FMath::Max((face - THRESHOLD_MIN) / RANGE, 0.0f); weight *= face * face; // 平方衰减 } return weight; } ``` ```cpp // SceneRendering.cpp — 写入 ViewUniformShaderParameters ViewUniformShaderParameters.OrthoViewProjectionX = CalcOrthoBlendParameter(ViewMatrices, TranslatedViewMatrix); ViewUniformShaderParameters.OrthoBlendParameter = CalcOrthoBlendWeight(ViewMatrices, TranslatedViewMatrix); ``` ## 代码修改情况 | 文件路径 | 行号 | 修改类型 | 修改内容 | |---------|------|---------|---------| | `Shaders/Private/Common.ush` | L1739~L1758 | 新增 | `GetOrthoBlendPosition` V1 X轴正交混合函数 | | `Shaders/Private/Common.ush` | L1761~L1800 | 新增 | `GetOrthoBlendPosition2` V2 全透视校正函数 | | `Shaders/Private/BasePassVertexShader.usf` | L94~L114 | 新增 | V2/V1 投影分支调用逻辑 | | `Shaders/Private/DepthOnlyVertexShader.usf` | L152~L220 | 新增 | 同上(DepthOnly Pass) | | `Shaders/Private/PositionOnlyDepthVertexShader.usf` | L49~L59 | 新增 | 同上(PositionOnlyDepth) | | `Shaders/Private/HitProxyVertexShader.usf` | L147~L169 | 新增 | 同上(HitProxy Pass) | | `Shaders/Private/DebugViewModeVertexShader.usf` | L162~L219 | 新增 | 同上(DebugViewMode Pass) | | `Shaders/Private/MaterialTemplate.ush` | L2235~L2240 | 新增 | `GetMaterialOrthoBlendWeight` 材质访问函数 | | `Source/Runtime/Renderer/Private/SceneRendering.cpp` | L1014~L1067 | 新增 | `CalcOrthoBlendParameter` / `CalcOrthoBlendWeight` 函数 | | `Source/Runtime/Renderer/Private/SceneRendering.cpp` | L1717~L1758 | 新增 | 写入 `ViewUniformShaderParameters` OrthoBlend 参数 |