3D点转屏幕坐标时出现双镜像投影问题求助
点云投影MVP矩阵异常:点沿所有轴镜像重复且移动方向相反
问题现象
- 3D点云通过MVP矩阵投影到屏幕时,所有点沿轴镜像成两份,移动方向完全相反
- 调试验证:固定在世界空间
(0, 0, 0)的点仍出现两次;相机位于(0, 1, 0)时,该点同时出现在相机上下方 - 已排除项:点生成逻辑(固定原点仍复现)、屏幕坐标转换(无扭曲/倒置)、多种MVP矩阵构造方式均无效
相关代码
C# 脚本
using UnityEngine; public class MultiPass : MonoBehaviour { public Texture2D input; public ComputeShader computeShader; public Vector2Int projectionResolution; public RenderTexture transformed; private Camera mainCamera; private int project, projectGroupsX, projectGroupsY; private void Start() { mainCamera = GetComponent<Camera>(); project = computeShader.FindKernel("Project"); computeShader.GetKernelThreadGroupSizes(project, out uint threadGroupSizeX, out uint threadGroupSizeY, out _); projectGroupsX = projectionResolution.x / (int)threadGroupSizeX; projectGroupsY = projectionResolution.y / (int)threadGroupSizeY; transformed = new RenderTexture(projectionResolution.x, projectionResolution.y, 0); transformed.enableRandomWrite = true; transformed.format = RenderTextureFormat.RGFloat; computeShader.SetFloat("PI", Mathf.PI); computeShader.SetFloat("PI2", Mathf.PI * 2); computeShader.SetVector("INPUT_RESOLUTION", new Vector2(input.width, input.height)); computeShader.SetVector("PROJECTION_RESOLUTION", new Vector2(projectionResolution.x, projectionResolution.y)); computeShader.SetTexture(project, "Input", input); computeShader.SetTexture(project, "Transformed", transformed); } private void Update() { RenderTexture rt = RenderTexture.active; RenderTexture.active = transformed; GL.Clear(false, true, Color.clear); RenderTexture.active = rt; Matrix4x4 MVP = GL.GetGPUProjectionMatrix(mainCamera.projectionMatrix, true) * mainCamera.worldToCameraMatrix; computeShader.SetMatrix("MVP", MVP); computeShader.Dispatch(project, projectGroupsX, projectGroupsY, 1); } private void OnRenderImage(RenderTexture source, RenderTexture destination) { Graphics.Blit(transformed, destination); } }
HLSL 着色器
#define NCLIP 1 #define FCLIP 30 #define MAP_TO_RANGE(tc, targetRange) \ ((tc) * ((targetRange) - 1)) #define NORMALIZE_RANGE(tc, sourceRange) \ ((tc) / ((sourceRange) - 1)) Texture2D<float4> Input; RWTexture2D<float2> Transformed; float2 INPUT_RESOLUTION, PROJECTION_RESOLUTION; float PI, PI2; float4x4 MVP; #pragma kernel Project [numthreads(8, 8, 1)] void Project(uint3 id : SV_DispatchThreadID) { // 1. calculate some points (the depth based point cloud or just a sphere) float2 uv = NORMALIZE_RANGE(id.xy, PROJECTION_RESOLUTION); float d = Input[MAP_TO_RANGE(uv, INPUT_RESOLUTION)].a; float2 ll1 = uv.yx; ll1.x *= PI; ll1.y *= PI2; ll1.y += PI; float CP = d * (FCLIP - NCLIP) + NCLIP; // override point cloud with sphere // CP = FCLIP; float3 P = float3( CP * sin(ll1.y) * sin(ll1.x), CP * cos(ll1.x), CP * cos(ll1.y) * sin(ll1.x)); // for only transforming one point /* if (!any(id.xy)) P = float3(0, 0, 0); else return; */ // 2. the actual matrix multiplication float4 pos4 = float4(P, 1); float4 transformedPosition = mul(MVP, pos4); float2 screenCoords; screenCoords = (transformedPosition.xy / transformedPosition.w) * 0.5 + 0.5; screenCoords = MAP_TO_RANGE(screenCoords, PROJECTION_RESOLUTION); Transformed[screenCoords] = float2(1, 1); }
排查思路与问题分析
核心排查步骤
验证MVP矩阵的行/列主序匹配
Unity使用列主序矩阵,HLSL的mul(矩阵, 向量)是标准列主序乘法,但可尝试以下验证:- 在C#中对MVP矩阵执行转置后传递给着色器,同时修改HLSL乘法为
mul(pos4, MVP)(行主序向量左乘) - 在着色器中输出
transformedPosition的原始值,检查裁剪空间坐标是否符合预期
- 在C#中对MVP矩阵执行转置后传递给着色器,同时修改HLSL乘法为
修正投影矩阵Y轴翻转设置
GL.GetGPUProjectionMatrix的第二个参数invertY=true会强制翻转Y轴,但RenderTexture的坐标系已与GPU一致,无需额外翻转。将参数改为false后测试:Matrix4x4 MVP = GL.GetGPUProjectionMatrix(mainCamera.projectionMatrix, false) * mainCamera.worldToCameraMatrix;添加裁剪空间可见性判断
着色器未做裁剪判断,裁剪空间外的点(transformedPosition.w <= 0)会导致透视除法后坐标翻转,添加判断过滤无效点:if (transformedPosition.w > 0 && abs(transformedPosition.x) <= transformedPosition.w && abs(transformedPosition.y) <= transformedPosition.w) { float2 screenCoords; screenCoords = (transformedPosition.xy / transformedPosition.w) * 0.5 + 0.5; screenCoords = MAP_TO_RANGE(screenCoords, PROJECTION_RESOLUTION); uint2 screenUV = uint2(round(screenCoords)); if (screenUV.x < PROJECTION_RESOLUTION.x && screenUV.y < PROJECTION_RESOLUTION.y) Transformed[screenUV] = float2(1, 1); }检查线程调度与纹理写入精度
将浮点屏幕坐标转为uint后再写入纹理,避免浮点索引导致的意外写入;同时验证projectGroupsX/Y计算是否正确(确保分辨率能被线程组大小整除,或补全剩余线程)
可能的根本原因
最可能的问题是投影矩阵Y轴翻转重复应用:GL.GetGPUProjectionMatrix(invertY=true)与RenderTexture的坐标系冲突,导致Y轴镜像;其次是矩阵主序不匹配,Unity传递的列主序矩阵在HLSL中被错误地按行主序处理。
内容的提问来源于stack exchange,提问作者Wanja Wischmeier
相关产品推荐
相关产品推荐

