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

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);
}

排查思路与问题分析

核心排查步骤

  1. 验证MVP矩阵的行/列主序匹配
    Unity使用列主序矩阵,HLSL的mul(矩阵, 向量)是标准列主序乘法,但可尝试以下验证:

    • 在C#中对MVP矩阵执行转置后传递给着色器,同时修改HLSL乘法为mul(pos4, MVP)(行主序向量左乘)
    • 在着色器中输出transformedPosition的原始值,检查裁剪空间坐标是否符合预期
  2. 修正投影矩阵Y轴翻转设置
    GL.GetGPUProjectionMatrix的第二个参数invertY=true会强制翻转Y轴,但RenderTexture的坐标系已与GPU一致,无需额外翻转。将参数改为false后测试:

    Matrix4x4 MVP = GL.GetGPUProjectionMatrix(mainCamera.projectionMatrix, false) * mainCamera.worldToCameraMatrix;
    
  3. 添加裁剪空间可见性判断
    着色器未做裁剪判断,裁剪空间外的点(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);
    }
    
  4. 检查线程调度与纹理写入精度
    将浮点屏幕坐标转为uint后再写入纹理,避免浮点索引导致的意外写入;同时验证projectGroupsX/Y计算是否正确(确保分辨率能被线程组大小整除,或补全剩余线程)

可能的根本原因

最可能的问题是投影矩阵Y轴翻转重复应用:GL.GetGPUProjectionMatrix(invertY=true)与RenderTexture的坐标系冲突,导致Y轴镜像;其次是矩阵主序不匹配,Unity传递的列主序矩阵在HLSL中被错误地按行主序处理。

内容的提问来源于stack exchange,提问作者Wanja Wischmeier

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 05:55:55