基于正二十面体球面网格的三角形四叉树星球 LOD 地形系统设计与实现(二)

0 阅读25分钟

三、创建基础三角形网格

接着上篇文章,我们成功计算出了20面体球的网格顶点的位置,接下来的问题就是,如何使用基础的一个mesh,拼接成一个完整的球体。

首先是创建三角形mesh,三角形的mesh的每个顶点必须知道自身的重心坐标,为了区分不同尺度下的重心坐标,我们不妨称三角形mesh的顶点的整数重心坐标为“网格重心坐标”、不同LOD层级下一个patch能够读取的采样点(所有LOD层级都一样)的内部的坐标称为“相对重心坐标”,20面体的一个面的所有采样点的坐标称为“绝对重心坐标”,如下图所示:

image.png

如图,一个mesh的网格顶点根据LOD层级的不同有三种情况,网格顶点和采样点一样密、比采样点更密、比采样点稀疏,由此我们就有了三种坐标,三种坐标也是可以自由地互相转换的。

在下面的创建三角形网格的代码中,我在mesh的uv2通道保存了每个顶点的重心(u,w)坐标信息,这样在顶点着色器中就能够拿到“网格重心坐标”。

/// <summary>
/// 创建每条边为2ⁿ段分割的三角形Mesh
/// </summary>
/// <param name="resolution_exp">分割幂次:每条边的段数 = 2ⁿ</param>
/// <param name="size">三角形的边长(基础大小)</param>
/// <returns>生成的三角形Mesh</returns>
public static Mesh CreateTriangleMesh(int resolution_exp, float size = 2f)
{
    // 1. 校验参数:n≥0,避免2ⁿ为0或负数
    if (resolution_exp < 0)
    {
        Debug.LogError("n必须≥0(2ⁿ段数需为正整数)");
        return null;
    }

    // 2. 计算每条边的分割段数(2ⁿ)
    // int segments = (int)Mathf.Pow(2, n);
    int n = 1 << resolution_exp;

    // 3. 定义基础三角形的3个顶点(正三角形,中心在原点)
    Vector3 A = new Vector3(-size / 2, 0, -size / (2 * Mathf.Sqrt(3))); // 左下
    Vector3 B = new Vector3(size / 2, 0, -size / (2 * Mathf.Sqrt(3)));  // 右下
    Vector3 C = new Vector3(0, 0, size / Mathf.Sqrt(3));                // 顶部

    // 4. 生成所有顶点(边分割点 + 内部顶点)
    List<Vector3> vertices = new List<Vector3>();
    //改成从底边开始,逐行生成,按照整数重心坐标的遍历顺序,也就是从(0,n,0)开始,逐行遍历到(0,0,n)
    //6. TEST:往uv2中填充数据,分别代表着三角形顶点重心坐标的u和v
    List<Vector2> uv2 = new List<Vector2>();
    for(int w=0; w<=n; w++)
    {
        // 当前行的进度:,0=底边AB,1=顶部C
        float t = (float)w / n;
        // 当前行的起始点(A到C的插值)
        Vector3 startPoint = Vector3.Lerp(A, C, t);
        // 当前行的结束点(B到C的插值)
        Vector3 endPoint = Vector3.Lerp(B, C, t);
        //当前行被分割成多少条边
        float rowSegments = n-w;
        for(int u=0; u<=n-w; u++)
        {
            int v = n - u - w;
            uv2.Add(new Vector2(u, w));//将u存进x分量,w存进y分量
            float colT;
            if (rowSegments == 0){
                // 顶部只有1个点,避免除0,直接给出顶点
                Vector3 vertex = C;
                vertices.Add(vertex);
            }
            else{
                colT = (float)u / rowSegments;
                Vector3 vertex = Vector3.Lerp(startPoint, endPoint, colT);
                vertices.Add(vertex);
            }
        }
    }

    // 5. 生成三角面索引(顺时针顺序,保证正面渲染)
    List<int> triangles = new List<int>();
    // 逐行生成三角面
    //改为从底边开始,按照重心坐标遍历顺序
    int vertexIndex;
    for (int w=0; w<=n-1; w++)
    {
        for (int u=0; u<=n-w-1; u++)
        {
            int v = n - u - w;
            vertexIndex = IntCentroidCoordTo1DIdx(u, v, w, n);
            
            if(u==0)
            {//u=0只有1个三角形
                //当前点 → 下一行同列 → 当前行下一列
                triangles.Add(vertexIndex);
                triangles.Add(vertexIndex + SumOfCurrentRow(w, n));
                triangles.Add(vertexIndex + 1);
            }else{
                //u=1到n-w-1都有2个三角形
                //第一个三角面:当前点 → 下一行同列 → 当前行下一列
                triangles.Add(vertexIndex);
                triangles.Add(vertexIndex + SumOfCurrentRow(w, n));
                triangles.Add(vertexIndex + 1);

                // 第二个三角面:当前点 → 下一行上一列 → 下一行同列
                triangles.Add(vertexIndex);
                triangles.Add(vertexIndex + SumOfCurrentRow(w, n) - 1);
                triangles.Add(vertexIndex + SumOfCurrentRow(w, n));
            }
            
        }
    }

    // 7. 创建Mesh并赋值数据
    Mesh mesh = new Mesh();
    mesh.name = $"Triangle_2^{resolution_exp}Segments";
    mesh.vertices = vertices.ToArray();
    mesh.triangles = triangles.ToArray();
    mesh.uv2 = uv2.ToArray();

    // 7. 补充Mesh必要数据(保证渲染正常)
    mesh.RecalculateNormals(); // 重新计算法线
    mesh.RecalculateBounds();  // 重新计算包围盒
    return mesh;
}

四、重心坐标转换验证

为了实现远处的地形的高效渲染,必须使用GPU instancing,而GPU instancing要求这一批次的所有网格使用相同的mesh相同的材质,那么为了实现地形的拼接,就只能想办法把预计算得到的地形网格顶点的Object Space的坐标通过texture2d或者StructuredBuffer的方式输入到GPU,在顶点着色器中,通过读取每个patch的信息来让顶点偏移到正确的位置。之所以预计算使用Object Space,是为了以后实现整个星系的原点偏移,可以自由地旋转和移动根物体。(经过测试不能使用Texture2D Array,似乎是因为unity不支持在顶点阶段采样Texture2DArray)

考虑到纹理的大小是根据GPU的不同有硬性限制的,最终决定采用StructuredBuffer来保存顶点的Position信息。因此,我们还需要把三角形的采样点信息放到一个一维的buffer中,让材质在顶点阶段通过网格重心坐标计算出相对重心坐标,最后得到绝对重心坐标

首先使用非GPU instancing版本的shader来验证一维索引的正确性,可以看到GPU随着target_idx的增大,三角形的顶点确实按照重心坐标的一般遍历顺序依次亮起。

Awaken_Mono_ECS_Minimum - SolarSystemGenerateTestScene - Windows, Mac, Linux - Unity 2022.3.55f1c1_ DX11 2026-03-08 06-21-15.gif

using UnityEngine;

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class TriangleGenerateAnd1DBufferTest : MonoBehaviour
{
    [Header("分割参数")]
    public int resolution_exp = 3; // 每条边的段数 = 2ⁿ(n=3 → 8段)
    public float triangleSize = 2f; // 三角形边长

    [Range(0, 100)]
    public int target_idx = 0;

    struct VertexData
    {
        public Vector3 positionWS;
    }

    Material mat;
    ComputeBuffer buffer;
    void Start()
    {
        // 1. 获取MeshFilter和MeshRenderer
        MeshFilter meshFilter = GetComponent<MeshFilter>();
        MeshRenderer meshRenderer = GetComponent<MeshRenderer>();

        // 2. 生成三角形Mesh
        Mesh triangleMesh = TerrainGenUtil.CreateTriangleMesh(resolution_exp, triangleSize);
        if (triangleMesh == null) return;

        // 3. 赋值Mesh并设置材质(默认白色材质)
        meshFilter.mesh = triangleMesh;
        mat = new Material(Shader.Find("Awaken/CentroidUVAnd1DBufferShow"));
        mat.SetColor("_BaseColor", Color.white);
        mat.SetInt("_N", 1<<resolution_exp);
        mat.SetInt("_TargetIdx", 0);

        buffer = new ComputeBuffer(
            triangleMesh.vertexCount,
            sizeof(float) * 3,
            ComputeBufferType.Structured
        );
        var vertexPositions = new VertexData[buffer.count];
        vertexPositions[0].positionWS = Vector3.up;
        buffer.SetData(vertexPositions);
        mat.SetBuffer("_VertexBuffer", buffer);


        meshRenderer.material = mat;
    }

    // // 【可选】运行时修改n,重新生成Mesh(结合之前的ContextMenu)
    // [ContextMenu("Regenerate Triangle Mesh")]
    // public void RegenerateMesh()
    // {
    //     if (!Application.isPlaying) return;
    //     MeshFilter meshFilter = GetComponent<MeshFilter>();
    //     meshFilter.mesh = TerrainGenUtil.CreateTriangleMesh(resolution_exp, triangleSize);
    // }

    int last_target_idx = 0;
    void Update()
    {
        // mat.SetInt("_TargetIdx", (int)(Time.time) % TerrainGenUtil.GetTriangleVertCountByN(1 << resolution_exp)-1);
        if(last_target_idx != target_idx)
        {
            last_target_idx = target_idx;
            mat.SetInt("_TargetIdx", target_idx);
            
            var vertexPositions = new VertexData[buffer.count];
            if (target_idx < buffer.count)
            {
                vertexPositions[target_idx].positionWS = Vector3.up;                
            }
            buffer.SetData(vertexPositions);
        }
    }

    private void OnDestroy()
    {
        buffer?.Dispose();
    }
}
Shader "Awaken/CentroidUVAnd1DBufferShow"
{
    Properties
    {
        _BaseColor ("Main Color", Color) = (1,1,1,1)
        [Range(0, 10)]_N ("N", Int) = 0
        [Range(0, 1000)]_TargetIdx ("Target Index", Int) = 0
        [Range(0,10)]_TargetU ("Target U", Int) = 0
        [Range(0,10)]_TargetW ("Target W", Int) = 0
    }

    SubShader
    {
        Tags 
        { 
            "RenderPipeline"="UniversalPipeline"
            "RenderType"="Opaque"
            "Queue"="Geometry"
        }
        //LOD 100
        Pass
        {
            Tags
          {
             "LightMode"="UniversalForward"
          }

            //Geometry
            ZWrite On
            ZTest LEqual
            Cull Back

            HLSLPROGRAM
            // #pragma target 3.5
            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

            #include "../Includes/TerrainUtils.cginc"

            ////接收阴影关键字
            ////Receive shadow keywords
            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS
            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE
            #pragma multi_compile _ _SHADOWS_SOFT

            
            struct Attributes
            {
                float4 positionOS : POSITION;
                float3 normalOS : NORMAL;
                float2 uv : TEXCOORD0;

                float2 uv2 : TEXCOORD2;
                uint vertexId : SV_VertexID;
            };

            struct Varings
            {
                float4 positionCS : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 positionWS : TEXCOORD1;
                float3 normalWS : TEXCOORD2;
                float3 viewDirWS : TEXCOORD3;

                float4 color : TEXCOORD4;
            };

            // 材质属性
            CBUFFER_START(UnityPerMaterial)
                float4 _BaseColor;
                uint _N;
                uint _TargetIdx;
                uint _TargetU;
                uint _TargetW;
            CBUFFER_END

            struct vertex_data
            {
                float3 positionWS;
            };
            StructuredBuffer<vertex_data> _VertexBuffer;


            Varings vert (Attributes IN)
            {
                Varings OUT;
                
                VertexPositionInputs positionInputs = GetVertexPositionInputs(IN.positionOS.xyz);
                VertexNormalInputs normalInputs = GetVertexNormalInputs(IN.normalOS.xyz);
                //OUT.positionCS = TransformObjectToHClip(IN.positionOS);
                // OUT.positionCS = positionInputs.positionCS;
                OUT.positionWS = positionInputs.positionWS;
                OUT.viewDirWS = GetCameraPositionWS() - positionInputs.positionWS;
                OUT.normalWS = normalInputs.normalWS;
                // OUT.uv = TRANSFORM_TEX(IN.uv, _MainTex);

                //----------短路-------
                // OUT.color=float4(1,0,0,1);
                // OUT.positionCS = TransformWorldToHClip(OUT.positionWS);
                // return OUT;
                //-----------
                
                uint u = (uint)round(IN.uv2.x);
                uint w = (uint)round(IN.uv2.y);
                uint v = _N - u - w;
                //计算idx的几种尝试,三种都不管用,整数计算不可靠(26年3月,后来又发现可以了,之前可能忽略了什么,比如那个target 3.5)
                //(1)
                // uint idx = IntCentroidCoordTo1DIdx(u, v, w, _N);
                //(2)
                int two = 2;
                int three = 3;
                int idx = two * _N + three;
                idx = idx-w;
                idx = idx*w;
                idx = idx/2;
                idx = idx+u;
                //(3)
                // float u_float = u;
                // float w_float = w;
                // float temp = w_float * (2 * _N - w_float +3)/2.0;
                // float idx_float = temp + u_float;
                // uint idx = (uint)round(idx_float);

                // OUT.positionWS += _VertexBuffer[IN.vertexId].positionWS;
                OUT.positionCS = TransformWorldToHClip(OUT.positionWS);

                // if(idx_float > _TargetIdx)
                // {
                //     OUT.color = float4(1,0,0,1);
                // }
                // else
                // {
                //     OUT.color = float4(0,1,0,1);
                // }
                
                if(idx == _TargetIdx)
                {
                    OUT.color = float4(0,1,0,1);
                }
                else
                {
                    OUT.color = float4(1,0,0,1);
                }

                // if(IN.vertexId == _TargetIdx)
                // {
                //     OUT.color = float4(0,1,0,1);
                // }
                // else
                // {
                //     OUT.color = float4(1,0,0,1);
                // }

                // if(idx > _TargetIdx)
                // {
                //     OUT.color = float4(1,0,0,1);
                // }
                // else
                // {
                //     OUT.color = float4(0,1,0,1);
                // }

                // if(w == _TargetW)
                // {
                //     OUT.color = float4(0,1,0,1);
                // }else
                // {
                //     OUT.color = float4(0,0,0,1);
                // }

                // if (u == _TargetU)
                // {
                //     OUT.color = float4(0,0,1,1);
                // }
                // else
                // {
                //     OUT.color = float4(0,0,0,1);
                // }

                // OUT.color = float4(
                //     frac(IN.uv2.x),
                //     frac(IN.uv2.y),
                //     0,
                //     1
                // );
                return OUT;
            }

            half4 frag (Varings IN) : SV_Target
            {
                // return float4(1,1,1,1);
                return IN.color;
            }
            ENDHLSL
        }

        //以下是对应的三个官方pass,自定义Shader不需要这么多变体,最好自己找地方再写一次
        //Here are the corresponding three official passes. Custom Shaders do not require so many variations, it is best to find a place to write them again
        UsePass "Universal Render Pipeline/Lit/ShadowCaster"
        UsePass "Universal Render Pipeline/Lit/depthOnly"
        UsePass "Universal Render Pipeline/Lit/DepthNormals"
    }
    //使用官方的Diffuse作为FallBack会增加大量变体,可以考虑自定义
    //FallBack "Diffuse"
}

接下来,在C#中验证网格重心坐标->相对重心坐标->绝对重心坐标的这一链条的计算方法是否正确。

public class TriangleGenerateAnd2DBufferTest : MonoBehaviour
{
    public float triangleSize = 2f; // 三角形边长
    [Range(0, 100)]
    public int target_idx = 0;
    public int patchLODLayer;
    [Header("分割参数")]
    public int lod0_map_exp = 3; // 高度图的一块LOD0的区域内,每条边的段数 = 2^exp(exp=3 → 8段)
    [Range(0,4)]
    public int LOD0MeshSize_delta_exp = 0; // mesh的每条边的段数 = 2^exp(exp=3 → 8段),这里的exp=lod0_map_exp + LOD0MeshSize_delta_exp,当为0的时候mesh和lod0_map的分辨率相同
    public Vector2Int PatchPos = new Vector2Int(0, 0);
    public bool isTopVertexUp = true;
    public int maxLOD = 0; //最大的LOD层级,最大的lod层级所对应的N_lod就是高度图的N
    
    Material mat;
    Texture2D tex;
    private NativeArray<Vector4> data;
    void Start()
    {
        var n_map_lod0 = 1 << lod0_map_exp;
        var n_map_global = n_map_lod0 * (1 << maxLOD);
        var n_mesh = n_map_lod0 * (1<<LOD0MeshSize_delta_exp);
        var size_map = n_map_global + 1;
        data = new NativeArray<Vector4>(size_map*size_map, Allocator.Persistent);
        
        // 1. 获取MeshFilter和MeshRenderer
        MeshFilter meshFilter = GetComponent<MeshFilter>();
        MeshRenderer meshRenderer = GetComponent<MeshRenderer>();
        
        // 2. 生成三角形Mesh
        Mesh triangleMesh = TerrainGenUtil.CreateTriangleMesh(lod0_map_exp + LOD0MeshSize_delta_exp, triangleSize);
        if (triangleMesh == null) return;
        meshFilter.mesh = triangleMesh;

        mat = new Material(Shader.Find("Awaken/CentroidUVAnd2DBufferShow"));
        mat.SetColor("_BaseColor", Color.white);
        mat.SetInteger(Shader.PropertyToID("_N_lod0_map"), n_map_lod0);
        mat.SetInteger(Shader.PropertyToID("_LOD0MeshSize_delta_exp"), LOD0MeshSize_delta_exp);
        
        //----------由全局idx计算target_idx_mesh-----------------
        // //把target_idx转化为绝对u、w坐标
        // int u_absolute,v_absolute,w_absolute;
        // (u_absolute,v_absolute,w_absolute) = TerrainGenUtil.TriVertIdxToIntCentroidCoord(target_idx, n_map_global);
        // //然后把绝对u、w坐标转化为相对u、w坐标
        // int u_relative,v_relative,w_relative;
        // (u_relative,v_relative,w_relative) = TerrainGenUtil.TriVertIntCentroidCoordToRelative(
        //     u_absolute,v_absolute,w_absolute,
        //     true,
        //     PatchPos,patchLODLayer,n_map_lod0
        // );
        // //把相对u、w坐标转化为mesh的u、w坐标
        // (int u_mesh,int v_mesh,int w_mesh) = TerrainGenUtil.TriVertIntCentroidCoordRelativeToMesh(
        //     u_relative,v_relative,w_relative,
        //     n_mesh,LOD0MeshSize_delta_exp
        // );
        // //最后把mesh的u、w坐标转化为target_idx_mesh
        // int target_idx_mesh = TerrainGenUtil.IntCentroidCoordTo1DIdx(u_mesh,v_mesh,w_mesh,n_mesh);
        int target_idx_mesh = TerrainGenUtil.TriVertIntIdxToMeshIdx(
            target_idx,n_map_global,n_map_lod0,
            isTopVertexUp,PatchPos,patchLODLayer,
            LOD0MeshSize_delta_exp
        );
        //---------------计算结束-----------------

        mat.SetInteger(Shader.PropertyToID("_TargetIdx_mesh"), target_idx_mesh);
        mat.SetInteger(Shader.PropertyToID("_LODLayer"), patchLODLayer);
        mat.SetInteger(Shader.PropertyToID("_MaxLOD"), maxLOD);
        mat.SetVector(Shader.PropertyToID("_PatchPos"), new Vector4(PatchPos.x, PatchPos.y, 0, 0));
        mat.SetInteger(Shader.PropertyToID("_IsTopVertexUp"), isTopVertexUp? 1 : 0);
        
        //-----------给数据贴图填充数据-----------------
        // //(1)按照target_idx指定的位置填充数据
        // //初始化位置数据贴图
        // var vertices = new List<Vector3>();
        // for (int w_ = 0; w_ <= n_map_global; w_++)
        // {
        //     for (int u_ = 0; u_ <= n_map_global - w_; u_++)
        //     {
        //         vertices.Add(new Vector3(0, 0, 0));
        //     }
        // }
        // var(_u_absolute,_v_absolute,_w_absolute) = TerrainGenUtil.TriVertIdxToIntCentroidCoord(target_idx, n_map_global);
        // vertices[_w_absolute * size_map + _u_absolute] = Vector3.up;
        //(2)填充一个patch所占的所有位置的数据(根据lod层级、patch位置、是否主顶点朝上)
        //初始化位置数据贴图
        var vertices = new List<Vector3>();
        var pivot = PatchPos;
        var(u_limit, _, w_limit) = TerrainGenUtil.TriVertIntCentroidCoordToAbsolute(
            0,n_map_lod0,0,
            isTopVertexUp,
            pivot,
            patchLODLayer,
            n_map_global
        );
        var(_, v_limit, _) = TerrainGenUtil.TriVertIntCentroidCoordToAbsolute(
            n_map_lod0, 0, 0,
            isTopVertexUp,
            pivot,
            patchLODLayer,
            n_map_global
        );

        for (int w_ = 0; w_ <= n_map_global; w_++)
        {
            for (int u_ = 0; u_ <= n_map_global - w_; u_++)
            {
                var v_ = n_map_global - u_ - w_;
                if(isTopVertexUp)
                {//如果朝上
                    if(u_ >= u_limit && v_ >= v_limit && w_ >= w_limit)
                    {//在当前lod的这个patch的范围内
                        if(u_ == u_limit || v_ == v_limit || w_ == w_limit){
                            vertices.Add(Vector3.down);
                        }else{
                            vertices.Add(Vector3.up);
                        }
                        
                    }
                    else
                    {//不在范围内
                        vertices.Add(new Vector3(0, 0, 0));
                    }
                }
                else
                {//如果朝下
                    if(u_ <= u_limit && v_ <= v_limit && w_ <= w_limit)
                    {//在当前lod的这个patch的范围内
                        if(u_ == u_limit || v_ == v_limit || w_ == w_limit){
                            vertices.Add(Vector3.down);
                        }else{
                            vertices.Add(Vector3.up);
                        }
                    }
                    else
                    {//不在范围内
                        vertices.Add(new Vector3(0, 0, 0));
                    }
                }
            }
        }

        tex = CreatePositionTexture(vertices,n_map_global);
        mat.SetTexture("_PositionTex", tex);
        
        
        meshRenderer.material = mat;
    }

    
    int _last_target_idx = 0;
    void Update()
    {
        if (_last_target_idx != target_idx)
        {
            _last_target_idx = target_idx;
            var vertices = new List<Vector3>();
            //更新tex
            var n_map_lod0 = 1<<lod0_map_exp;
            var n_map_global = n_map_lod0 * (1 << maxLOD);
            int size_map = n_map_global + 1;//三角形一条边上的顶点数量

            var(_u_absolute,_v_absolute,_w_absolute) = TerrainGenUtil.TriVertIdxToIntCentroidCoord(target_idx, n_map_global);
            
            for (int i = 0; i < data.Length; i++)
            {
                data[i] = Vector4.zero;
            }
            data[_w_absolute * size_map + _u_absolute] = new Vector4(0,1,0,0);
            tex.SetPixelData(data, 0);
            tex.Apply(false, false);
            mat.SetInteger(Shader.PropertyToID("_TargetIdx"), target_idx);
            // mat.SetTexture("_PositionTex", tex);
        }
    }
    
    Texture2D CreatePositionTexture(
        List<Vector3> vertices,
        int n
    )
    {
        int size = n + 1;

        Texture2D tex = new Texture2D(
            size,
            size,
            TextureFormat.RGBAFloat,
            false,
            true // linear
        );

        tex.wrapMode = TextureWrapMode.Clamp; //uv、采样超出范围则截断,采样到边缘像素
        tex.filterMode = FilterMode.Point; // 非常重要,防止插值,blocky up close意思是近距离看一块一块的

        Color[] pixels = new Color[size * size];

        // 初始化为 0(未使用区域)
        for (int i = 0; i < pixels.Length; i++)
            pixels[i] = Color.clear;

        int idx = 0;

        for (int w = 0; w <= n; w++)
        {
            for (int u = 0; u <= n - w; u++)
            {
                Vector3 pos = vertices[TerrainGenUtil.IntCentroidCoordTo1DIdx(u, n - u - w, w, n)];

                data[w * size + u] = new Vector4(
                    pos.x,
                    pos.y,
                    pos.z,
                    1.0f
                );
            }
        }

        // tex.SetPixels(pixels);
        // tex.Apply(false, true);
        tex.SetPixelData(data, 0);
        tex.Apply(false, false);

        return tex;
    }

    private void OnDestroy()
    {
        data.Dispose();
    }
}
Shader "Awaken/CentroidUVAnd2DBufferShow"
{
    Properties
    {
        _BaseColor ("Main Color", Color) = (1,1,1,1)
//        [Range(0, 10)]_N ("N", Int) = 0
//        [Range(0, 1000)]_TargetIdx ("Target Index", Int) = 0
//        [Range(0,10)]_TargetU ("Target U", Int) = 0
//        [Range(0,10)]_TargetW ("Target W", Int) = 0
    }

    SubShader
    {
        Tags 
        { 
            "RenderPipeline"="UniversalPipeline"
            "RenderType"="Opaque"
            "Queue"="Geometry"
        }
        //LOD 100
        Pass
        {
            Tags
          {
             "LightMode"="UniversalForward"
          }

            //Geometry
            ZWrite On
            ZTest LEqual
            Cull Back

            HLSLPROGRAM
            // #pragma target 3.5
            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

            #include "../Includes/TerrainUtils.cginc"

            ////接收阴影关键字
            ////Receive shadow keywords
            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS
            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE
            #pragma multi_compile _ _SHADOWS_SOFT

            
            struct Attributes
            {
                float4 positionOS : POSITION;
                float3 normalOS : NORMAL;
                float2 uv : TEXCOORD0;

                float2 uv2 : TEXCOORD2;
                uint vertexId : SV_VertexID;
            };

            struct Varings
            {
                float4 positionCS : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 positionWS : TEXCOORD1;
                float3 normalWS : TEXCOORD2;
                float3 viewDirWS : TEXCOORD3;

                float4 color : TEXCOORD4;

                float3 uvw_mesh : TEXCOORD5;

                float3 uvw_absolute : TEXCOORD6; //调试用
            };

            // 材质属性
            CBUFFER_START(UnityPerMaterial)
                float4 _BaseColor;
                
                uint _TargetIdx_mesh;
                uint _TargetU_mesh;
                uint _TargetW_mesh;
                // uint _N_lod0_map; //三角形mesh的一条边被顶点分割为几份
            CBUFFER_END
            

            struct vertex_data
            {
                float3 positionWS;
            };
            // StructuredBuffer<VertexData> _VertexBuffer;
            TEXTURE2D(_PositionTex);
            SAMPLER(sampler_PositionTex);

            //-------用来表示当前patch的位置和lod相关信息的数据结构-----,
            //定义当前patch所处的lod层级
            uint _LODLayer;
            //定义LOD0的patch宽度,其他层级可以推断出宽度,比如lod0宽度为4,则lod1为8,lod2为16,以此类推
            uint _N_lod0_map; //lod0时高度图的一个patch的一条边被高度图的采样点分割成几份
            uint _LOD0MeshSize_delta_exp; //三角形mesh的一条边被顶点分割为几份,_N_mesh一定是_N_lod0_map的2^若干次方倍
            uint _MaxLOD; //最大的lod层级,最大的lod层级所对应的N_lod就是高度图的N
            //定义当前patch的位置,用一个(u,w)表示
            uint2 _PatchPos; //三角形在全局高度图坐标系中的位置
            //定义当前三角形的patch的上顶点是否朝上,1为朝上,0为朝下
            int _IsTopVertexUp;
            
            

            Varings vert (Attributes IN)
            {
                Varings OUT;
                
                //---基础变换操作-----
                VertexPositionInputs positionInputs = GetVertexPositionInputs(IN.positionOS.xyz);
                VertexNormalInputs normalInputs = GetVertexNormalInputs(IN.normalOS.xyz);
                //OUT.positionCS = TransformObjectToHClip(IN.positionOS);
                // OUT.positionCS = positionInputs.positionCS;
                OUT.positionWS = positionInputs.positionWS;
                OUT.viewDirWS = GetCameraPositionWS() - positionInputs.positionWS;
                OUT.normalWS = normalInputs.normalWS;

 
                uint u_mesh = (uint)round(IN.uv2.x);
                uint w_mesh = (uint)round(IN.uv2.y);
                uint n_mesh = _N_lod0_map * (1 << _LOD0MeshSize_delta_exp);
                uint v_mesh = n_mesh - u_mesh - w_mesh;
                OUT.uvw_mesh = float3(u_mesh,v_mesh,w_mesh);
                //从这里开始relative坐标比mesh坐标要更细,所以会出现小数,得换成float类型
                float scale_mesh = (float)(1<<_LOD0MeshSize_delta_exp);
                float u_relative = u_mesh /scale_mesh;
                float v_relative = v_mesh /scale_mesh;
                float w_relative = w_mesh /scale_mesh;
                

                //计算idx的几种尝试,三种都不管用,整数计算不可靠
                // //(1)
                // uint idx = IntCentroidCoordTo1DIdx(u_relative, v_relative, w_relative, _N_lod0_map);
                // //(2)
                // int two = 2;
                // int three = 3;
                // int idx = two * _N_lod0_map + three;
                // idx = idx-w_relative;
                // idx = idx*w_relative;
                // idx = idx/2;
                // idx = idx+u_relative;
                // //(3)
                // float u_float = u_relative;
                // float w_float = w_relative;
                // float temp = w_float * (2 * _N_lod0_map - w_float +3)/2.0;
                // float idx_float = temp + u_float;
                // uint idx = (uint)round(idx_float);

                // if(idx_float > _TargetIdx_mesh)
                // {
                //     OUT.color = float4(1,0,0,1);
                // }
                // else
                // {
                //     OUT.color = float4(0,1,0,1);
                // }
                
                // if(idx == _TargetIdx_mesh)
                // {
                //     OUT.color = float4(0,1,0,1);
                // }
                // else
                // {
                //     OUT.color = float4(1,0,0,1);
                // }

                if(IN.vertexId == _TargetIdx_mesh)//顶点的自己的vertexId绝对可靠
                {
                    OUT.color = float4(0,1,0,1);
                }
                else
                {
                    OUT.color = float4(1,0,0,1);
                }

                // if(idx > _TargetIdx_mesh)
                // {
                //     OUT.color = float4(1,0,0,1);
                // }
                // else
                // {
                //     OUT.color = float4(0,1,0,1);
                // }

                // if(w == _TargetW)
                // {
                //     OUT.color = float4(0,1,0,1);
                // }else
                // {
                //     OUT.color = float4(0,0,0,1);
                // }

                // if (u == _TargetU)
                // {
                //     OUT.color = float4(0,0,1,1);
                // }
                // else
                // {
                //     OUT.color = float4(0,0,0,1);
                // }

                // OUT.color = float4(
                //     frac(IN.uv2.x),
                //     frac(IN.uv2.y),
                //     0,
                //     1
                // );

                uint n_maxLOD = (_N_lod0_map << _MaxLOD);
                float2 uw_relative,uw_absolute;
                uw_relative = float2(u_relative,w_relative);
                float scale_lod = 1<<_LODLayer;
                if(_IsTopVertexUp != 0)
                {//朝上,方向和绝对重心坐标一样
                    uw_absolute = uw_relative * scale_lod + float2(_PatchPos.x, _PatchPos.y);
                }
                else{
                    //朝下,方向和绝对重心坐标相反
                    uw_absolute = -uw_relative * scale_lod + float2(_PatchPos.x, _PatchPos.y);
                }
                uw_absolute = max(0, uw_absolute);//防止浮点误差导致floor操作导致这个坐标小于0
                float v_absolute = (float)n_maxLOD - uw_absolute.x - uw_absolute.y; OUT.uvw_absolute = float3(uw_absolute.x,v_absolute, uw_absolute.y);
                // float3 uvw_absolute = float3(uw_absolute.x, (float)n_maxLOD - uw_absolute.x - uw_absolute.y, uw_absolute.y);
                float2 uw_left_down = floor(uw_absolute);
                float v_left_down = n_maxLOD - uw_left_down.x - uw_left_down.y;
                //判断要采样的网格点在左下还是右上三角形中,v_left_down-1即可得到左下和右上的分界线,用v_left_down-1<0判断边界情况
                

                //---------采样三个点,如下图所示-----------------
                //   /----/
                //  /  \  /
                // /----/
                //当点在左下三角形以内时为第一种情况,在右上三角形以内时为第二种情况
                float3 posWS;
                if(v_absolute >= v_left_down-1) //在左下三角形中,
                {//采样三个点,分别是uw_left_down,uw_left_down+float2(1,0),uw_left_down+float2(0,1)
                    float2 sample0, sample1, sample2;
                    sample0= uw_left_down;
                    if(uw_left_down.x + uw_left_down.y >= n_maxLOD)
                    {//这种情况正好在右边界的网格点上,如果加上float2(1,0)和float2(0,1)会导致采样点出界,因此令三个采样点都相同
                        sample1=sample0;
                        sample2=sample0;
                    }else
                    {
                        sample1 = uw_left_down + float2(1,0);
                        sample2 = uw_left_down + float2(0,1);
                    }
                    
                    float3 posV = SAMPLE_TEXTURE2D_LOD(
                        _PositionTex,
                        sampler_PositionTex,
                        sample0 / (float)(n_maxLOD),
                        0
                    ).xyz;
                    float3 posU = SAMPLE_TEXTURE2D_LOD(
                        _PositionTex,
                        sampler_PositionTex,
                        sample1 / (float)(n_maxLOD),
                        0
                    ).xyz;
                    float3 posW = SAMPLE_TEXTURE2D_LOD(
                        _PositionTex,
                        sampler_PositionTex,
                        sample2 / (float)(n_maxLOD),
                        0
                    ).xyz;
                    float u_smooth,v_smooth,w_smooth;
                    u_smooth = uw_absolute.x-floor(uw_absolute.x);
                    w_smooth = uw_absolute.y-floor(uw_absolute.y);
                    v_smooth = 1 - u_smooth - w_smooth;
                    posWS = posV * v_smooth + posU * u_smooth + posW * w_smooth;
                }
                else
                {//在右上三角形中,采样三个点,分别是uw_left_down+float2(1,0),uw_left_down+float2(0,1),uw_left_down+float2(1,1)
                    float2 sample0 = uw_left_down + float2(1,0);
                    float2 sample1 = uw_left_down + float2(0,1);
                    float2 sample2 = uw_left_down + float2(1,1);
                    float3 posW = SAMPLE_TEXTURE2D_LOD(
                        _PositionTex,
                        sampler_PositionTex,
                        sample0 / (float)(n_maxLOD),
                        0
                    ).xyz;
                    float3 posU = SAMPLE_TEXTURE2D_LOD(
                        _PositionTex,
                        sampler_PositionTex,
                        sample1 / (float)(n_maxLOD),
                        0
                    ).xyz;
                    float3 posV = SAMPLE_TEXTURE2D_LOD(
                        _PositionTex,
                        sampler_PositionTex,
                        sample2 / (float)(n_maxLOD),
                        0
                    ).xyz;
                    float u_smooth,v_smooth,w_smooth;
                    u_smooth = ceil(uw_absolute.x)-uw_absolute.x;
                    w_smooth = ceil(uw_absolute.y)-uw_absolute.y;
                    v_smooth = 1 - u_smooth - w_smooth;
                    posWS = posW * w_smooth + posU * u_smooth + posV * v_smooth;
                }

                //旧的采样方法,只采样一个点,容易在边界处产生不连续的问题
                // uw_absolute /= (float)(n_maxLOD);
                // float3 posWS = SAMPLE_TEXTURE2D_LOD(
                //     _PositionTex,
                //     sampler_PositionTex,
                //     uw_absolute,
                //     0
                // ).xyz;
                

                OUT.positionWS += posWS;
                OUT.positionCS = TransformWorldToHClip(OUT.positionWS);

                // if(_IsTopVertexUp != 0)
                // {
                //     //朝上,方向和绝对重心坐标一样
                //     OUT.color = float4(0,0,1,1);
                // }
                    
                return OUT;
            }

            half4 frag (Varings IN) : SV_Target
            {
                // return 1;
                //------显示三角形内平滑权重场---------
                //(1)左下三角形
                float u = IN.uvw_absolute.x-floor(IN.uvw_absolute.x);
                float w = IN.uvw_absolute.z-floor(IN.uvw_absolute.z);
                float v = 1 - u - w;
                // return u;
                // return w;
                // return v;
                //(2)右上三角形
                float u_ = ceil(IN.uvw_absolute.x)-IN.uvw_absolute.x;
                float w_ = ceil(IN.uvw_absolute.z)-IN.uvw_absolute.z;
                float v_ = 1 - u_ - w_;
                // return u;
                // return w;
                // return v_;
                //------显示结束-----------------
                
                
                //根据uv离整数的远近判断是否是整数点
                float3 uvw = IN.uvw_mesh;
                float3 similarity = abs(frac(uvw)-0.5);//到整数线的近似度 [0,0.5]
                // return similarity;
                float3 d = 1 - similarity * 2.0;//转换成[0,1]的距离
                // return d.x;

                //---------显示顶点高光-------------
                float dist_point = sqrt(d.x*d.x + d.z*d.z);//距离最近顶点距离,距离越小,越接近整数,采用欧氏距离
                // return dist_point;
                //阈值
                float threshold_point = 0.2;
                float highlight_point = smoothstep(threshold_point, 0.0, dist_point);
                // return highlight_point;
                
                //--------显示网格线高光-----------
                float dist_line = min(d.y,min(d.x, d.z));//距离最近网格线距离,距离越小,越接近整数,采用曼哈顿距离
                // return dist_line;
                //阈值
                float threshold_line = 0.02;
                float highlight_line = smoothstep(threshold_line, 0.0, dist_line);
                // return highlight_line;


                float highlight = max(highlight_point,highlight_line);
                float3 finalColor = lerp(IN.color.rgb, float3(1,1,1), highlight);
                // return float4(IN.color.rgb, 1);

                return float4(finalColor, 1);
            }
            ENDHLSL
        }

        //以下是对应的三个官方pass,自定义Shader不需要这么多变体,最好自己找地方再写一次
        //Here are the corresponding three official passes. Custom Shaders do not require so many variations, it is best to find a place to write them again
        UsePass "Universal Render Pipeline/Lit/ShadowCaster"
        // UsePass "Universal Render Pipeline/Lit/depthOnly"
        // UsePass "Universal Render Pipeline/Lit/DepthNormals"
    }
    //使用官方的Diffuse作为FallBack会增加大量变体,可以考虑自定义
    //FallBack "Diffuse"
}

如何验证算法的正确性呢?比如我想要让下图所指的那个点凸起,其mesh的1维索引id为41,在inspector中输入对应的信息:其基准点为(2,6)、方向朝下、lod层级为0,最大lod层级为2、其相对重心坐标(0,0),可以看到该点确实凸起了,算法正确。

image.png

image.png

image.png

五、四叉树分割过程补充说明

如图,从lodmax往下分割,可以看到分成了四个三角形,其中中心的三角形的方向从朝上变为了朝下,计算其绝对重心坐标时需要考虑其朝向。每个patch都有一个基准点V,当三角形朝上时,基准点在左下方(或者说和绝对重心坐标系一个方向),当三角形朝下时,基准点在右上方。

考虑一下如何唯一地表示LOD中的任何一个patch。

patch,就是lod地形中的最基础的一个mesh在不同lod层级,不同位置的实例化,lod0时这个mesh最小,lod1时尺度放大两倍,lod2时放大四倍,以此类推,放大过程中网格顶点数量总是不变的。因此patch的一条边的分割数也必须是2的若干次方,才能保证网格的位置精确。

我们要唯一地表示一个patch,需要的信息有: (1)lod层级 (2)基准点的绝对重心坐标 (3)布尔值IsTopVertexUp,表示三角形当前是一个朝上的三角形还是朝下的三角形 (4)属于20面体的第几个面。

image.png

那么四叉树细分的伪代码如下:


consumeList=[]
appendList=[]
finalList=[]

//在consumeList中填入20面体的0-19的面的根节点
for(int i=0;i<20;i++)
{
    consumeList.Append({
        lod=maxLod,
        mainVert = (0,0),
        IsTopVertexUp=true,
        treeId=i,
    })
}

currLod=maxLod;
while(currLod--)
{
    temp = consumeList.pop();
    if(evaluate(temp)){//判断是否继续分割
        childTriN = n_map_facet >> (maxLOD-temp.lod+1);
        child0={
            lod=temp.lod-1,
            mainVert = temp.mainVert+(temp.IsTopVertexUp?childTriN:-childTriN,0),
            IsTopVertexUp=temp.IsTopVertexUp,
            treeId=i,
        }
        child1={
            lod=temp.lod-1,
            mainVert = temp.mainVert,
            IsTopVertexUp=temp.IsTopVertexUp,
            treeId=i,
        }
        child2={
            lod=temp.lod-1,
            mainVert = temp.mainVert+(0,temp.IsTopVertexUp?childTriN:-childTriN),
            IsTopVertexUp=temp.IsTopVertexUp,
            treeId=i,
        }
        child3={
            lod=temp.lod-1,
            mainVert = temp.mainVert+(temp.IsTopVertexUp?childTriN:-childTriN,temp.IsTopVertexUp?childTriN:-childTriN),
            IsTopVertexUp=!temp.IsTopVertexUp,
            treeId=i,
        }
        appendList.Append(child0);
        appendList.Append(child1);
        appendList.Append(child2);
        appendList.Append(child3);
    }else{//不继续分割
        finalList.Append(temp);
    }
    交换appendList、consumeList
}
consumeList剩余的节点加入finalList
使用finalList的信息进行渲染

六、在CPU中验证四叉树分割

using System.Collections;
using System.Collections.Generic;
using Unity.Collections;
using UnityEngine;
using Unity.VisualScripting;
using UnityEngine.Rendering;
using System;
using System.Runtime.InteropServices;
using Unity.Mathematics;
using UnityEditor;
using UnityEngine.UIElements;

// struct patch_data_struct
// {
//     //--------树内属性
//     uint _LODLayer;//定义当前patch所处的lod层级
//     bool _IsTopVertexUp;//定义当前三角形的patch的上顶点是否朝上,1为朝上,0为朝下
//     uint2 _PatchPos; //定义当前patch的位置,用一个(u,w)表示,三角形在全局高度图坐标系中的位置
//     //-------树外属性
//     uint treeId; //第几棵四叉树,用于索引Texture2DArray,没有这个属性就无法知道自己的位置
// };
[StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct PatchDataStruct_1DBuffer
{
    public uint _LODLayer;
    public uint _IsTopVertexUp; //1代表朝上
    public uint _PatchPosU;         // 拆分uint2的u分量
    public uint _PatchPosW;         // 拆分uint2的w分量
    public uint treeId;
}

public class GPUInstancingTest_1DBuffer : MonoBehaviour
{
    public Color _BaseColor = Color.white;
    public float triangleSize = 2f; // 三角形边长
    [Header("分割参数")]
    public int lod0_map_exp = 3; // 高度图的一块LOD0的区域内,每条边的段数 = 2^exp(exp=3 → 8段)

    // public int lodMaxCountExpInOneFace=0; //20面体的一个面能放下的lodMax区块个数=4^lodMaxCountExpInOneFace,注意是4不是2。不需要了,这个逻辑太冗余
    [Range(0,4)]
    public int LOD0MeshSize_delta_exp = 0; // mesh的每条边的段数 = 2^exp(exp=3 → 8段),这里的exp=lod0_map_exp + LOD0MeshSize_delta_exp,当为0的时候mesh和lod0_map的分辨率相同
    public int maxLOD = 0; //最大的LOD层级,最大的lod层级所对应的N_lod就是高度图的N
    public float planetRadius=20f;

    Material mat;
    Texture2D tex;
    // private NativeArray<Vector4> triangleData;
    private NativeArray<Vector3> allTriangleData;
    // private Texture2D posTex;
    private ComputeBuffer positionBuffer;
    private ComputeBuffer normalBuffer;
    Mesh _triangleMesh;
    RenderParams renderParams;
    MaterialPropertyBlock matPropBlock;
    GraphicsBuffer commandBuffer;
    private GraphicsBuffer.IndirectDrawIndexedArgs[] commandData;
    
    private readonly int ICO_FACE_COUNT = 20;
    // Start is called before the first frame update
    void Start()
    {
        Debug.Log("sizeof(PatchDataStruct_2DBuffer)"+Marshal.SizeOf<PatchDataStruct_2DBuffer>().ToString());
        //1.初始属性设置
        var n_lod0_map = 1 << lod0_map_exp;
        var n_map_facet = 1<<(lod0_map_exp + maxLOD);
        var n_mesh = n_lod0_map * (1<<LOD0MeshSize_delta_exp);
        var size_map = n_map_facet + 1;
        
        Debug.Log("Sizeof Vector4:"+Marshal.SizeOf(typeof(Vector4)));
        // triangleData = new NativeArray<Vector4>(size_map * size_map, Allocator.Persistent);
        // Debug.Log("triangleData.Length:"+triangleData.Length);
        allTriangleData = new NativeArray<Vector3>(ICO_FACE_COUNT * size_map * size_map, Allocator.Persistent);
        Debug.Log("allTriangleData.Length:"+allTriangleData.Length);
        mat = new Material(Shader.Find("Awaken/Patch_1DBuffer"));
        
        // 2. 生成三角形Mesh
        _triangleMesh = TerrainGenUtil.CreateTriangleMesh(lod0_map_exp + LOD0MeshSize_delta_exp, triangleSize);
        if (_triangleMesh == null) return;

        //3.生成三角形的数据()
        var _terrainDataRuntime = this.GetOrAddComponent<CelestialBodyTerrainDataRuntime>();
        _terrainDataRuntime.InitTerrainData(planetRadius, 0.2f, 1f, lod0_map_exp, maxLOD, LOD0MeshSize_delta_exp, 1, ShapeType.EarthLike);
        _terrainDataRuntime.CalculateHeights();
        Vector3[] SphereVertices = _terrainDataRuntime.UnitSphereVertices;
        // Vector3[] SphereNormals = _terrainDataRuntime.Normals;
        float[] heights = _terrainDataRuntime.Heights;
        Debug.Log("heights占用的存储空间为"+heights.Length*4+"byte");
        //对unitSphereVertices进行缩放
        for(int i=0;i<SphereVertices.Length;i++)
        {
            SphereVertices[i] *= heights[i];
        }
        // int singleTriangleVertCount = TerrainGenUtil.GetTriangleVertCountByN(1 << (lod0_map_exp + LOD0MeshSize_delta_exp));

        // //3.1 用第一个三角形的数据生成高度图,也就是[0,singleTriangleVertCount)
        // //复制数据到Vector3[]
        // Vector3[] firstTriangleVertices = new Vector3[singleTriangleVertCount];
        // Array.Copy(unitSphereVertices, 0, firstTriangleVertices, 0, singleTriangleVertCount);
        // posTex = this.CreatePositionTexture(firstTriangleVertices, n_map_facet);
        //3.1,用所有数据生成一个高度图数组Texture2DArray
        // var posTexArray = this.CreatePositionTexture2DArray(unitSphereVertices, n_map_facet, face_count);
        // Texture2D posTex = this.CreatePositionTexture(unitSphereVertices, n_map_facet, PATCH_X_STACK_COUNT, PATCH_Y_STACK_COUNT);
        //3.1,用所有数据生成一个高度缓冲ComputeBuffer
        positionBuffer = this.CreateSparseDataBuffer(SphereVertices, n_map_facet);
        // normalBuffer = this.CreateSparseDataBuffer(SphereNormals, n_map_facet);
        //4.GPU instancing相关
        //填充patch数据
        int maxDiverseCount = ICO_FACE_COUNT*(1 << maxLOD)*(1 << maxLOD);//可能细分的最大patch数量
        ComputeBuffer patchBuffer = new ComputeBuffer(maxDiverseCount, Marshal.SizeOf(typeof(PatchDataStruct_1DBuffer)));
        PatchDataStruct_1DBuffer[] patchData = new PatchDataStruct_1DBuffer[maxDiverseCount];
        List<PatchDataStruct_1DBuffer> segmentConsumeList = new List<PatchDataStruct_1DBuffer>();//用于存储细分的根节点
        List<PatchDataStruct_1DBuffer> segmentResultList = new List<PatchDataStruct_1DBuffer>();
        List<PatchDataStruct_1DBuffer> finalResultList = new List<PatchDataStruct_1DBuffer>();
        // segmentConsumeList.Add(new PatchDataStruct_1DBuffer{
        //     _LODLayer = (uint)maxLOD,
        //     _IsTopVertexUp = 1,
        //     _PatchPosU = 0,
        //     _PatchPosW = 0,
        //     treeId=0,
        // });
        for (int i = 0; i < ICO_FACE_COUNT; i++)
        {
            segmentConsumeList.Add(new PatchDataStruct_1DBuffer{
                _LODLayer = (uint)maxLOD,
                _IsTopVertexUp = 1,
                _PatchPosU = 0,
                _PatchPosW = 0,
                treeId = (uint)i,
            });
        }
        int currLod = maxLOD;
        //先尝试用cpu细分
        while(currLod>0) 
        {
            for(int i=0; i<segmentConsumeList.Count; i++)
            {
                PatchDataStruct_1DBuffer segment = segmentConsumeList[i];
                //评价函数,判断是否需要细分,此时测试,默认为true
                if(true){//需要细分
                    int childTriN = n_map_facet >> (maxLOD-(int)(segment._LODLayer)+1);
                    PatchDataStruct_1DBuffer child0,child1,child2,child3;

                    //子节点0、子节点1、子节点2、子节点3分别是父节点的U子三角形、V子三角形、W子三角形、中心子三角形,U、V、W方向和父节点相同,中心子三角形与父节点方向相反
                    child0 = new PatchDataStruct_1DBuffer{//U子三角形
                        _LODLayer = segment._LODLayer - 1,
                        _IsTopVertexUp = segment._IsTopVertexUp,
                        _PatchPosU = segment._PatchPosU + (uint)(segment._IsTopVertexUp == 1u ? childTriN : (-childTriN)),
                        _PatchPosW = segment._PatchPosW,
                        treeId = segment.treeId,
                    };
                    child1 = new PatchDataStruct_1DBuffer{//V子三角形
                        _LODLayer = segment._LODLayer - 1,
                        _IsTopVertexUp = segment._IsTopVertexUp,
                        _PatchPosU = segment._PatchPosU,
                        _PatchPosW = segment._PatchPosW,
                        treeId = segment.treeId,
                    };
                    child2 = new PatchDataStruct_1DBuffer{//W子三角形
                        _LODLayer = segment._LODLayer - 1,
                        _IsTopVertexUp = segment._IsTopVertexUp,
                        _PatchPosU = segment._PatchPosU,
                        _PatchPosW = segment._PatchPosW + (uint)(segment._IsTopVertexUp == 1u ? childTriN : (-childTriN)),
                        treeId = segment.treeId,
                    };
                    child3 = new PatchDataStruct_1DBuffer{//中心子三角形
                        _LODLayer = segment._LODLayer - 1,
                        _IsTopVertexUp = segment._IsTopVertexUp==1u?0u:1u,
                        _PatchPosU = segment._PatchPosU + (uint)(segment._IsTopVertexUp == 1u ? childTriN : (-childTriN)),
                        _PatchPosW = segment._PatchPosW + (uint)(segment._IsTopVertexUp == 1u ? childTriN : (-childTriN)),
                        treeId = segment.treeId,
                    };     
                    
                    
                    segmentResultList.Add(child0);
                    segmentResultList.Add(child1);
                    segmentResultList.Add(child2);
                    segmentResultList.Add(child3);
                }else{//不需要细分
                    finalResultList.Add(segment);
                }
            }

            //清空consumeList
            segmentConsumeList.Clear();
            //交换consumeList和resultList
            (segmentConsumeList,segmentResultList) = (segmentResultList,segmentConsumeList);

            currLod--;
        }
        //consumeList剩下的加入finalResultList
        finalResultList.AddRange(segmentConsumeList);
        //复制finalResultList到patchData
        finalResultList.CopyTo(patchData);
        patchBuffer.SetData(patchData);
        
        
        // MaterialPropertyBlock matPropBlock = new MaterialPropertyBlock();
        // matPropBlock.SetTexture("_PositionTexArray", posTexArray);
        //(1)所有实例相同,不变的属性
        mat.SetColor(Shader.PropertyToID("_BaseColor"), _BaseColor);
        mat.SetFloat(Shader.PropertyToID("_Radius"), planetRadius);//这个参数不直接用来控制网格位置,只是用于着色辅助计算
        mat.SetInteger(Shader.PropertyToID("_N_lod0_map"), n_lod0_map);
        mat.SetInteger(Shader.PropertyToID("_LOD0MeshSize_delta_exp"), LOD0MeshSize_delta_exp);
        mat.SetInteger(Shader.PropertyToID("_MaxLOD"), maxLOD);
        //(2)所有实例相同,经常变的属性
        mat.SetMatrix(Shader.PropertyToID("_ObjectToWorld"), this.transform.localToWorldMatrix);
        //(3)一个lodMax块三角形内共享,但20个面不同的属性
        mat.SetBuffer(Shader.PropertyToID("_VertexBuffer"), positionBuffer);
        // mat.SetBuffer(Shader.PropertyToID("_NormalBuffer"), normalBuffer);
        //(4)所有实例不同的属性
        mat.SetBuffer(Shader.PropertyToID("_patchBuffer"), patchBuffer);
        
        commandBuffer = new GraphicsBuffer(GraphicsBuffer.Target.IndirectArguments, 1, GraphicsBuffer.IndirectDrawIndexedArgs.size);
        commandData = new GraphicsBuffer.IndirectDrawIndexedArgs[1];
        commandData[0].indexCountPerInstance = _triangleMesh.GetIndexCount(0);
        commandData[0].instanceCount = (uint)(finalResultList.Count);Debug.Log("待设置实例数量:" + (uint)finalResultList.Count);Debug.Log("结构体赋值后:" + commandData[0].instanceCount);
        // commandData[0].instanceCount = 1;
        commandBuffer.SetData(commandData);
        
        renderParams = new RenderParams(mat); //新建这个struct必须传入mat,经测试不传入的话instancing会失败
        // renderParams.camera = Camera.main;
        renderParams.camera = SceneView.lastActiveSceneView.camera;
        renderParams.shadowCastingMode = ShadowCastingMode.On;
        renderParams.worldBounds = new Bounds(Vector3.zero, 10000*Vector3.one);
        renderParams.receiveShadows = true;
        // renderParams.material = mat; //这个似乎不起作用
        // renderParams.camera = Camera.main;
        // renderParams.matProps = matPropBlock;

    }



    // Update is called once per frame
    void Update()
    {
        mat.SetMatrix(Shader.PropertyToID("_ObjectToWorld"), this.transform.localToWorldMatrix);
        Graphics.RenderMeshIndirect(renderParams, _triangleMesh, commandBuffer, 1);
        
    }


    /// <summary>
    /// 根据x/y方向堆叠的数量来创建贴图
    /// </summary>
    /// <param name="vertices"></param>
    /// <param name="n"></param>
    /// <param name="xStackCount"></param>
    /// <param name="yStackCount"></param>
    /// <returns></returns>
    // Texture2D CreatePositionTexture(
    //         Vector3[] vertices,
    //         int n, int xStackCount, int yStackCount
    //     )
    // {
    //     int sizeOfFacet = n + 1;

    //     Texture2D newTex = new Texture2D(
    //         sizeOfFacet * xStackCount,
    //         sizeOfFacet * yStackCount,
    //         TextureFormat.RGBAFloat,
    //         false,
    //         true // linear
    //     );

    //     newTex.wrapMode = TextureWrapMode.Clamp; //uv、采样超出范围则截断,采样到边缘像素
    //     newTex.filterMode = FilterMode.Point; // 非常重要,防止插值,blocky up close意思是近距离看一块一块的

    //     // Color[] pixels = new Color[sizeOfFacet * sizeOfFacet];
    //     //
    //     // // 初始化为 0(未使用区域)
    //     // for (int i = 0; i < pixels.Length; i++)
    //     //     pixels[i] = Color.clear;

    //     for (int y = 0; y < yStackCount; y++)
    //     {
    //         for (int x = 0; x < xStackCount; x++)
    //         {
    //             int faceId = x + y * xStackCount;
    //             for (int w = 0; w <= n; w++)
    //             {
    //                 for (int u = 0; u <= n - w; u++)
    //                 {
    //                     var vertCountPerLodMax = TerrainGenUtil.GetTriangleVertCountByN(n);
    //                     Vector3 pos = vertices[faceId * vertCountPerLodMax + TerrainGenUtil.IntCentroidCoordTo1DIdx(u, n - u - w, w, n)];

    //                     int y_global = w + y*sizeOfFacet;
    //                     int x_global = u + x*sizeOfFacet;
    //                     int id_global = y_global * xStackCount * sizeOfFacet + x_global;
    //                     allTriangleData[id_global] = new Vector4(
    //                         pos.x,
    //                         pos.y,
    //                         pos.z,
    //                         1.0f
    //                     );
    //                 }
    //             }
    //         }
    //     }
        

    //     // tex.SetPixels(pixels);
    //     // tex.Apply(false, true);
    //     newTex.SetPixelData(allTriangleData, 0);
    //     newTex.Apply(false, false);

    //     return newTex;


    //     // Texture2DArray test = new Texture2DArray(,);
    //     // test.SetPixelData();
    // }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="vertices">所有顶点位置数据</param>
    /// <param name="n">一个lodMax块的全局n</param>
    /// <param name="faceCount">lodMax块的数量</param>
    /// <returns></returns>
    // Texture2DArray CreatePositionTexture2DArray(
    //     Vector3[] vertices,
    //     int n, int faceCount
    // )
    // {
    //     int size = n + 1;

    //     Texture2DArray texArray = new Texture2DArray(
    //         size, size, faceCount, TextureFormat.RGBAFloat, false, true
    //     );

    //     texArray.wrapMode = TextureWrapMode.Clamp; //uv、采样超出范围则截断,采样到边缘像素
    //     texArray.filterMode = FilterMode.Point; // 非常重要,防止插值,blocky up close意思是近距离看一块一块的
        
    //     var vertCountPerLodMax = TerrainGenUtil.GetTriangleVertCountByN(n);
    //     for (int faceId = 0; faceId < faceCount; faceId++)
    //     {
    //         for (int w = 0; w <= n; w++)
    //         {
    //             for (int u = 0; u <= n - w; u++)
    //             {
    //                 Vector3 pos = vertices[faceId * vertCountPerLodMax + TerrainGenUtil.IntCentroidCoordTo1DIdx(u, n - u - w, w, n)];

    //                 triangleData[w * size + u] = new Vector4(
    //                     pos.x,
    //                     pos.y,
    //                     pos.z,
    //                     1.0f
    //                 );
    //             }
    //         }
            
    //         texArray.SetPixelData(triangleData, 0, faceId);
    //     }

    //     return texArray;
    // }


    ComputeBuffer CreateSparseDataBuffer(Vector3[] vertices,
        int n)
    {
        int size = n + 1;

        ComputeBuffer newBuffer = new ComputeBuffer(ICO_FACE_COUNT * size * size, Marshal.SizeOf(typeof(Vector3)));
        var vertCountPerLodMax = TerrainGenUtil.GetTriangleVertCountByN(n);
        for (int faceId = 0; faceId < ICO_FACE_COUNT; faceId++)
        {
            for (int w = 0; w <= n; w++)
            {
                for (int u = 0; u <= n - w; u++)
                {
                    Vector3 data = vertices[faceId * vertCountPerLodMax + TerrainGenUtil.IntCentroidCoordTo1DIdx(u, n - u - w, w, n)];

                    allTriangleData[faceId * size * size + w * size + u] = data;
                }
            }
            
        }
        //复制allTriangleData到newBuffer
        newBuffer.SetData(allTriangleData);
        return newBuffer;
    }
    
    private void OnDestroy()
    {
        allTriangleData.Dispose();
        commandBuffer?.Dispose();
        positionBuffer?.Dispose();
    }
}

成功在CPU实现四叉树细分,除了让评价函数默认返回true,其他功能都有了。接下来只需要在GPU实现相同的功能

image.png

未完待续

完整代码

IcoSphereTerrainLODSystem: 实现了基于20面体球面网格的三角形四叉树细分星球LOD地形系统。

参考:

GPU驱动的四叉树地形以及参考了这个文章的代码

  1. (49 封私信 / 70 条消息) 大世界GPU Driven地形入门 - 知乎

地形的噪声生成、海洋和大气层渲染参考了这个仓库的代码

  1. SebLague/Solar-System at Episode_02 (MIT)