三、创建基础三角形网格
接着上篇文章,我们成功计算出了20面体球的网格顶点的位置,接下来的问题就是,如何使用基础的一个mesh,拼接成一个完整的球体。
首先是创建三角形mesh,三角形的mesh的每个顶点必须知道自身的重心坐标,为了区分不同尺度下的重心坐标,我们不妨称三角形mesh的顶点的整数重心坐标为“网格重心坐标”、不同LOD层级下一个patch能够读取的采样点(所有LOD层级都一样)的内部的坐标称为“相对重心坐标”,20面体的一个面的所有采样点的坐标称为“绝对重心坐标”,如下图所示:
如图,一个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的增大,三角形的顶点确实按照重心坐标的一般遍历顺序依次亮起。
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),可以看到该点确实凸起了,算法正确。
五、四叉树分割过程补充说明
如图,从lodmax往下分割,可以看到分成了四个三角形,其中中心的三角形的方向从朝上变为了朝下,计算其绝对重心坐标时需要考虑其朝向。每个patch都有一个基准点V,当三角形朝上时,基准点在左下方(或者说和绝对重心坐标系一个方向),当三角形朝下时,基准点在右上方。
考虑一下如何唯一地表示LOD中的任何一个patch。
patch,就是lod地形中的最基础的一个mesh在不同lod层级,不同位置的实例化,lod0时这个mesh最小,lod1时尺度放大两倍,lod2时放大四倍,以此类推,放大过程中网格顶点数量总是不变的。因此patch的一条边的分割数也必须是2的若干次方,才能保证网格的位置精确。
我们要唯一地表示一个patch,需要的信息有: (1)lod层级 (2)基准点的绝对重心坐标 (3)布尔值IsTopVertexUp,表示三角形当前是一个朝上的三角形还是朝下的三角形 (4)属于20面体的第几个面。
那么四叉树细分的伪代码如下:
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实现相同的功能
未完待续
完整代码
IcoSphereTerrainLODSystem: 实现了基于20面体球面网格的三角形四叉树细分星球LOD地形系统。
参考:
GPU驱动的四叉树地形以及参考了这个文章的代码
地形的噪声生成、海洋和大气层渲染参考了这个仓库的代码