运行时Frustum可视化

Number of views 55

这是某天一朋友让帮忙看的一个功能,大概就是想要一个在运行时可以可视化看到模型是否在特定Camera的 Frustum内的逻辑,功能不难,但是有些点需要注意下,整理出来做个简单记录。

听到这个逻辑需求,很容易能想到,不就是类似引擎编辑器相机的可视化需求么?

不过这位朋友不想用特定图标来表示相机,而是希望直接用顶点数据来绘制相机,另外模型是否在Frustum内需要能够清晰的可以让用户看到,所以决定分成两步,先实现类似引擎编辑器相机的可视化功能。

基于上述功能,进行迭代。

image1745566662410.png

这样模型出入Frustum就可以看到了 。

image1745566700081.png

开始功能之前需要了解MVP矩阵的作用是将模型从其本地坐标系变换到裁剪坐标系(Clip Space)中的立方体(范围为[-w, w]³)的过程,其中w值可以理解为是在MV矩阵变换顶点后的z值坐标,顶点到相机的距离越远,w的值就越大。接着通过透视除法操作,即将裁剪空间中的坐标除以w,以得到NDC坐标,这个立方体就称为规范化设备坐标(Normalized Device Coordinates,NDC),在NDC坐标系中,其范围是从-1到1的立方体。

基于上述的理论,我们可以定义一个以[-1, 1]³的立方体为边界的顶点数据,通过乘于VP矩阵的逆矩阵把该顶点数据变换为世界坐标即可绘制Frustum。

我们定义两个部分的顶点数据,一个用来表示相机图标的Gizmo,一个用来描述Frustum:

    void createCameraGizmo()
    {
        // 定义顶点数据
        gizmoVertices = new List<Vector3> {
            new Vector3(-1, -1,  1), //0
            new Vector3( 1, -1,  1),//1
            new Vector3(-1,  1,  1), //2
            new Vector3( 1,  1,  1),//3
            new Vector3(-1, -1,  3),//4
            new Vector3( 1, -1,  3),//5
            new Vector3(-1,  1,  3),//6
            new Vector3( 1,  1,  3),//7
            new Vector3( 0,  0,  1),//8(视锥顶点)
        };

        // 定义索引数据
        gizmoIndices = new List<int> {
            0, 1, 1,
            3, 3, 2,
            2, 0, 4,
            5, 5, 7,
            7, 6, 6,
            4, 0, 4,
            1, 5, 3,
            7, 2, 6,
        };
        // 描述相机Gizmo的朝向
        int numSegments = 6;
        int coneBaseIndex = gizmoVertices.Count * 3 / 3;
        int coneTipIndex = coneBaseIndex - 1;
        for (int i = 0; i < numSegments; ++i)
        {
            float u = (float)i / (float)numSegments;
            float angle = u * Mathf.PI * 2.0f;
            float x = Mathf.Cos(angle);
            float y = Mathf.Sin(angle);
            gizmoVertices.Add(new Vector3(x, y, 0));
            // 从顶点到锥体边缘的连线
            gizmoIndices.Add(coneTipIndex);
            gizmoIndices.Add(coneBaseIndex + i);
            // 锥体底边的连线
            gizmoIndices.Add(coneBaseIndex + i);
            gizmoIndices.Add(coneBaseIndex + (i + 1) % numSegments);
        }
        // 通过获取相机的世界矩阵来确定gizmo的位置,旋转及缩放
        Matrix4x4 worldMatrix = cam.transform.localToWorldMatrix;
        for (int vi = 0; vi < gizmoVertices.Count; vi++)
        {
            // 其中scale可以适当调整gizmo的大小到合适位置
            gizmoVertices[vi] = worldMatrix.MultiplyPoint(gizmoVertices[vi] * scale);
        }
    }
    // 获取目标相机VP的逆矩阵
    Matrix4x4 getProViewInvMat()
    {
        Matrix4x4 projectionMatrix = cam.projectionMatrix;
        Matrix4x4 viewMatrix = cam.worldToCameraMatrix;
        Matrix4x4 vpMatrix = projectionMatrix * viewMatrix;
        Matrix4x4 invVpMatrix = Matrix4x4.Inverse(vpMatrix);
        return invVpMatrix;
    }
    // 通过顶点数据创建Frustum
    void createFrustum()
    {
        // [-1, 1]³范围顶点数据
        frustumVertices = new List<Vector3> {
            new Vector3(-1, -1, -1), //0
            new Vector3(1, -1, -1),//1
            new Vector3(-1,  1, -1), //2
            new Vector3(1,  1, -1),//3
            new Vector3(-1, -1,  1),//4
            new Vector3(1, -1,  1),//5
            new Vector3(-1,  1,  1),//6
            new Vector3(1,  1,  1),//7
        };
        // 顶点索引
        frustumIndices = new List<int> {
            0, 1, 1, 3, 3, 2, 2, 0,
            4, 5, 5, 7, 7, 6, 6, 4,
            0, 4, 1, 5, 3, 7, 2, 6,
        };

        Matrix4x4 invVpMatrix = getProViewInvMat();
        for (int vi = 0; vi < frustumVertices.Count; vi++)
        {
            // 通过VP矩阵的逆矩阵还原为Frustum,不要使用MultiplyVector以及*符号
            Vector4 frustumPos = invVpMatrix.MultiplyPoint(frustumVertices[vi]);
            frustumVertices[vi] = frustumPos;
        }
    }
    // 指定目标gameobject,shader,顶点与索引数据生成mesh与材质
    Material generateMesh(GameObject obj, string shader, List<Vector3> vertices, List<int> indices)
    {
        Mesh gizmoMesh = new Mesh();
        gizmoMesh.vertices = vertices.ToArray();
        gizmoMesh.SetIndices(indices, MeshTopology.Lines, 0);
        MeshFilter gizmoMeshFilter = obj.AddComponent<MeshFilter>();
        gizmoMeshFilter.mesh = gizmoMesh;
        MeshRenderer meshRenderer = obj.AddComponent<MeshRenderer>();
        Material material = new Material(Shader.Find(shader));
        meshRenderer.material = material;
        return material;
    }
    void Start()
    {
        createCameraGizmo();
        createFrustum();
        gizmoMaterial = generateMesh(gameObject, "Unlit/Color", gizmoVertices, gizmoIndices);
        GameObject frustumObj = new GameObject("Frustum");
        frustumObj.transform.parent = transform;
        frustumMaterial = generateMesh(frustumObj, "Unlit/Color", frustumVertices, frustumIndices);
    }

通过该方式运行后发现gizmo的朝向反了:

这是因为我们把gizmo的顶点数据乘上了目标相机的localToWorldMatrix矩阵,而相机的朝向是指向相机自身坐标系的-z轴方向,所以我们需要把这些附带的条件考虑进去,部分代码修改为:

    void createCameraGizmo()
    {
        // ...
        Matrix4x4 viewMatrix = cam.cameraToWorldMatrix;// 或则cam.worldToCameraMatrix.inverse
        for (int vi = 0; vi < gizmoVertices.Count; vi++)
        {
            gizmoVertices[vi] = viewMatrix.MultiplyPoint(gizmoVertices[vi] * scale);
        }
    }

这时候就正确了:

但是发现移动目标相机后线框没有跟着移动,原因是我们只在程序开始的时候设定了顶点数据,所以移动相机后数据还是原来的,不过如果每帧都在cpu内存中更新顶点数据毕竟是负担,所以这一步就交给Shader来完成:

GizmoShader

// 目标相机的cameraToWorldMatrix矩阵
uniform float4x4 targetViewInv;
// gizmo的缩放值
uniform float scale;
v2f vert (appdata v)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(mul(targetViewInv, float4(v.vertex.x * scale, v.vertex.y * scale, v.vertex.z * scale, 1.0)));
    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    UNITY_TRANSFER_FOG(o,o.vertex);
    return o;
}

FrustumShader

// 目标相机的VP矩阵的逆矩阵
uniform float4x4 targetProjViewInv;
v2f vert (appdata v)
{
     v2f o;
     // 目标相机的VP逆矩阵求取世界空间中的坐标
     float4 currVer = mul(targetProjViewInv, float4(v.vertex.x, v.vertex.y, v.vertex.z, 1.0));
     // 需要除以w
     float wNum = 1.0 / currVer.w;
     o.vertex = UnityObjectToClipPos(float3(currVer.x * wNum, currVer.y * wNum, currVer.z * wNum));
     o.uv = TRANSFORM_TEX(v.uv, _MainTex);
     UNITY_TRANSFER_FOG(o,o.vertex);
     return o;
}

然后修改Start函数以及Update函数:

void Start()
{
        createCameraGizmo();
        createFrustum();
        gizmoMaterial = generateMesh(gameObject, "Custom/GizmoShader", gizmoVertices, gizmoIndices);
        // 传递目标相机的View的逆矩阵
        gizmoMaterial.SetMatrix("targetViewInv", cam.worldToCameraMatrix.inverse);
        // 传递scale的值
        gizmoMaterial.SetFloat("scale", scale);
        GameObject frustumObj = new GameObject("Frustum");
        frustumObj.transform.parent = transform;
        frustumMaterial = generateMesh(frustumObj, "Custom/FrustumShader", frustumVertices, frustumIndices);
        // 传递目标相机的ViewProj的逆矩阵
        frustumMaterial.SetMatrix("targetProjViewInv", getProViewInvMat());
}
private void Update()
{
        gizmoMaterial.SetMatrix("targetViewInv", cam.worldToCameraMatrix.inverse);
        gizmoMaterial.SetFloat("scale", scale);
        frustumMaterial.SetMatrix("targetProjViewInv", getProViewInvMat());
}

这样移动相机或调整相机的Fov等参数就可以同步数据了(代码没做优化,可自行调整)。

接下来就把线框改为三角面绘制, 并优化下顶点以及索引数据:

void createCameraGizmo()
{
        // Define the vertex data
        gizmoVertices = new List<Vector3> {
            // Cube Vertices
            new Vector3(-0.5f, -0.5f, 0f), // 0
            new Vector3(0.5f, -0.5f, 0f),  // 1
            new Vector3(-0.5f, 0.5f, 0f),  // 2
            new Vector3(0.5f, 0.5f, 0f),   // 3
            new Vector3(-0.5f, -0.5f, 1.0f),  // 4
            new Vector3(0.5f, -0.5f, 1.0f),   // 5
            new Vector3(-0.5f, 0.5f, 1.0f),   // 6
            new Vector3(0.5f, 0.5f, 1.0f),    // 7

            // Triangle Vertices
            new Vector3(0f, 0f, 0f),        // 8 (Triangle Vertex)
            new Vector3(0f, 0.4f, -0.5f),// 9 (Triangle Vertex)
            new Vector3(0f, -0.4f, -0.5f)  // 10 (Triangle Vertex)
        };

        // Define the index data
        gizmoIndices = new List<int> {
           // Cube Indices
            0, 1, 2, // Front Face
            1, 3, 2,
            4, 6, 5, // Back Face
            5, 6, 7,
            0, 4, 1, // Left Face
            4, 5, 1,
            2, 3, 6, // Right Face
            3, 7, 6,
            0, 2, 4, // Top Face
            2, 6, 4,
            1, 5, 3, // Bottom Face
            5, 7, 3,

            // Triangle Indices
            8, 9, 10,  // Triangle
            10, 9, 8
        };
}

void createFrustum()
{
        frustumVertices = new List<Vector3> {
            // Front Face
            new Vector3(-1, -1, 1),   // 左下
            new Vector3(1, -1, 1),    // 右下
            new Vector3(-1, 1, 1),    // 左上
            new Vector3(1, 1, 1),     // 右上
  
            // Back Face
            new Vector3(-1, -1, -1),  // 左下
            new Vector3(1, -1, -1),   // 右下
            new Vector3(-1, 1, -1),   // 左上
            new Vector3(1, 1, -1),    // 右上
  
            // Left Face
            new Vector3(-1, -1, -1),  // 左下
            new Vector3(-1, -1, 1),   // 右下
            new Vector3(-1, 1, -1),   // 左上
            new Vector3(-1, 1, 1),    // 右上
  
            // Right Face
            new Vector3(1, -1, 1),    // 左下
            new Vector3(1, -1, -1),   // 右下
            new Vector3(1, 1, 1),     // 左上
            new Vector3(1, 1, -1),    // 右上
  
            // Top Face
            new Vector3(-1, 1, 1),    // 左下
            new Vector3(1, 1, 1),     // 右下
            new Vector3(-1, 1, -1),   // 左上
            new Vector3(1, 1, -1),    // 右上
  
            // Bottom Face
            new Vector3(-1, -1, -1),  // 左下
            new Vector3(1, -1, -1),   // 右下
            new Vector3(-1, -1, 1),   // 左上
            new Vector3(1, -1, 1)     // 右上
        };
        frustumIndices = new List<int>{
            // Front Face
            0, 1, 2,
            2, 1, 3,
  
            // Back Face
            4, 5, 6,
            6, 5, 7,
  
            // Left Face
            8, 9, 10,
            10, 9, 11,
  
            // Right Face
            12, 13, 14,
            14, 13, 15,
  
            // Top Face
            16, 17, 18,
            18, 17, 19,
  
            // Bottom Face
            20, 21, 22,
            22, 21, 23
        };
        frustumColors = new List<Color>
        {
            // Front Face (Yellow)
            Color.yellow,
            Color.yellow,
            Color.yellow,
            Color.yellow,
  
            // Back Face (White)
            Color.white,
            Color.white,
            Color.white,
            Color.white,
  
            // Left Face (Red)
            Color.red,
            Color.red,
            Color.red,
            Color.red,
  
            // Right Face (Green)
            Color.green,
            Color.green,
            Color.green,
            Color.green,
  
            // Top Face (Blue)
            Color.blue,
            Color.blue,
            Color.blue,
            Color.blue,
  
            // Bottom Face (White)
            Color.white,
            Color.white,
            Color.white,
            Color.white
        };
}

然后把topology改为MeshTopology.Triangles,运行结果为:

这是代码

0 Answers