Refactor asset pipeline to use file paths, improve import

- Switched asset handler interfaces and implementations to use file paths instead of FileStreams for all operations.
- Refactored mesh asset structure and parsing, moved meshlet logic to MeshProcessor, and introduced hierarchical MeshNode types.
- Updated texture asset handling: switched to bits-per-channel, improved mipmap/cubemap generation, and SPMD HDRI support.
- Updated shader asset handlers to use file paths and split code compilation logic.
- Improved asset registry: added event debouncing, better path handling, and import time/hash tracking.
- Added source generator for IAssetSettings registration to support polymorphic JSON serialization.
- Updated dependencies and tests; various minor fixes and cleanups.
This commit is contained in:
2026-04-25 18:23:21 +09:00
parent 4757c0c91a
commit 1a91811621
27 changed files with 1523 additions and 748 deletions

View File

@@ -26,6 +26,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Misaki.HighPerformance.Mathematics" Version="1.3.3" />
<PackageReference Include="Misaki.HighPerformance.Mathematics.SPMD" Version="1.3.0" />
<PackageReference Include="System.IO.Hashing" Version="10.0.7" />
<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.26100.6" />
</ItemGroup>

View File

@@ -12,7 +12,7 @@ public struct TextureContentHeader
{
public uint width;
public uint height;
public uint depth;
public uint bpc;
public uint mipLevels;
public uint dimension; // 1 for 1D, 2 for 2D, 3 for 3D
public uint colorComponents;
@@ -48,25 +48,25 @@ internal partial class AssetEntry
};
}
private static TextureFormat GetTextureFormat(uint depth, uint colorComponents)
private static TextureFormat GetTextureFormat(uint bpc, uint colorComponents)
{
return colorComponents switch
{
1 => depth switch
1 => bpc switch
{
8 => TextureFormat.R8_UNorm,
16 => TextureFormat.R16_UNorm,
32 => TextureFormat.R32_UInt,
_ => TextureFormat.Unknown,
},
2 => depth switch
2 => bpc switch
{
8 => TextureFormat.R8G8_UNorm,
16 => TextureFormat.R16G16_UNorm,
32 => TextureFormat.R32G32_Float,
_ => TextureFormat.Unknown,
},
3 or 4 => depth switch
3 or 4 => bpc switch
{
8 => TextureFormat.R8G8B8A8_UNorm,
16 => TextureFormat.R16G16B16A16_Float,
@@ -91,7 +91,7 @@ internal partial class AssetEntry
Height = header.height,
MipLevels = header.mipLevels,
Slice = 1,
Format = GetTextureFormat(header.depth, header.colorComponents),
Format = GetTextureFormat(header.bpc, header.colorComponents),
Dimension = (TextureDimension)header.dimension,
Usage = TextureUsage.ShaderResource,
};

View File

@@ -88,3 +88,73 @@ internal static partial class {registerTypeName}
return null;
}
}
[Generator]
internal class IAssetSettingsRegistrationGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var settingsCandidates = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: (s, _) => s is ClassDeclarationSyntax,
transform: GetAssetSettingsSymbol)
.Where(symbol => symbol != null)
.Collect();
context.RegisterSourceOutput(settingsCandidates, GenerateRegistrationCode);
}
private void GenerateRegistrationCode(SourceProductionContext context, ImmutableArray<INamedTypeSymbol> array)
{
if (array.IsDefaultOrEmpty)
{
return;
}
var sb = new System.Text.StringBuilder();
foreach (var iface in array)
{
sb.AppendLine($" global::Ghost.Editor.Core.AssetHandler.AssetHandlerRegistry.RegisterIAssetSettingsType(typeof({iface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}), \"{iface.Name}\");");
}
var registerTypeName = "g_iassetsettings_registeration";
var code = $@"// <auto-generated />
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
internal static partial class {registerTypeName}
{{
[global::System.Runtime.CompilerServices.ModuleInitializer]
internal static void RegisterIAssetSettingsTypes()
{{
{sb}
}}
}}";
context.AddSource($"{registerTypeName}.gen.cs", code);
}
private INamedTypeSymbol GetAssetSettingsSymbol(GeneratorSyntaxContext context, CancellationToken token)
{
var classSyntax = (ClassDeclarationSyntax)context.Node;
if (context.SemanticModel.GetDeclaredSymbol(classSyntax) is not INamedTypeSymbol symbol)
{
return null;
}
var iSettingsSymbol = context.SemanticModel.Compilation.GetTypeByMetadataName("Ghost.Editor.Core.AssetHandler.IAssetSettings");
if (iSettingsSymbol == null)
{
return null;
}
foreach (var iface in symbol.AllInterfaces)
{
if (SymbolEqualityComparer.Default.Equals(iface, iSettingsSymbol))
{
return symbol;
}
}
return null;
}
}

View File

@@ -6,7 +6,6 @@ using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.Mathematics;
using Misaki.HighPerformance.Mathematics.Geometry;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ghost.Graphics.Core;
@@ -203,158 +202,6 @@ public struct Mesh : IResourceReleasable
_meshletData.Dispose();
}
public unsafe void CookMeshlets()
{
if (_meshletData.meshlets.IsCreated)
{
_meshletData.meshlets.Dispose();
}
if (_meshletData.groups.IsCreated)
{
_meshletData.groups.Dispose();
}
if (_meshletData.hierarchyNodes.IsCreated)
{
_meshletData.hierarchyNodes.Dispose();
}
if (_meshletData.meshletVertices.IsCreated)
{
_meshletData.meshletVertices.Dispose();
}
if (_meshletData.meshletTriangles.IsCreated)
{
_meshletData.meshletTriangles.Dispose();
}
_meshletData.meshletCount = 0;
_meshletData.lodLevelCount = 0;
_meshletData.materialSlotCount = 0;
// 1. Prepare Configuration
var config = new ClodConfig
{
maxVertices = 64,
minTriangles = 32,
maxTriangles = 124,
partitionSpatial = true,
partitionSize = 16,
clusterSpatial = false,
clusterSplitFactor = 2.0f,
optimizeClusters = true,
optimizeClustersLevel = 1,
simplifyRatio = 0.5f,
simplifyThreshold = 0.85f,
simplifyErrorMergePrevious = 1.0f,
simplifyErrorFactorSloppy = 2.0f,
simplifyPermissive = true,
simplifyFallbackPermissive = false,
simplifyFallbackSloppy = true,
};
// 2. Map Mesh to ClodMesh
var clodMesh = new ClodMesh
{
vertexPositions = (float*)Unsafe.AsPointer(ref _vertices[0].position),
vertexCount = (nuint)_vertices.Count,
vertexPositionsStride = (nuint)sizeof(Vertex),
vertexAttributes = (float*)Unsafe.AsPointer(ref _vertices[0].normal),
vertexAttributesStride = (nuint)sizeof(Vertex),
indices = (uint*)_indices.GetUnsafePtr(),
indexCount = (nuint)_indices.Count,
attributeProtectMask = 0,
};
// 3. Build
MeshletUtility.Build(in config, in clodMesh, Unsafe.AsPointer(ref this), MeshletOutputCallback);
_meshletData.meshletCount = _meshletData.meshlets.IsCreated ? _meshletData.meshlets.Count : 0;
if (_meshletData.groups.IsCreated && _meshletData.groups.Count > 0)
{
var maxLodLevel = 0u;
for (var i = 0; i < _meshletData.groups.Count; i++)
{
maxLodLevel = Math.Max(maxLodLevel, _meshletData.groups[i].lodLevel);
}
_meshletData.lodLevelCount = (int)maxLodLevel + 1;
}
_meshletData.materialSlotCount = 1;
}
private static unsafe int MeshletOutputCallback(void* context, ClodGroup group, ReadOnlyUnsafeCollection<ClodCluster> clusters)
{
var mesh = (Mesh*)context;
ref var data = ref mesh->_meshletData;
// Ensure lists are initialized
if (!data.groups.IsCreated) data.groups = new UnsafeList<MeshletGroup>(16, AllocationHandle.Persistent);
if (!data.meshlets.IsCreated) data.meshlets = new UnsafeList<Meshlet>(64, AllocationHandle.Persistent);
if (!data.meshletVertices.IsCreated) data.meshletVertices = new UnsafeList<uint>(128, AllocationHandle.Persistent);
if (!data.meshletTriangles.IsCreated) data.meshletTriangles = new UnsafeList<uint>(128, AllocationHandle.Persistent);
var meshletGroup = new MeshletGroup
{
boundingSphere = new SphereBounds(group.simplified.center, group.simplified.radius),
boundingBox = new AABB(group.simplified.center - group.simplified.radius, group.simplified.center + group.simplified.radius),
parentError = group.simplified.error,
meshletStartIndex = (uint)data.meshlets.Count,
meshletCount = (uint)clusters.Count,
lodLevel = (uint)group.depth
};
data.groups.Add(meshletGroup);
for (var i = 0; i < clusters.Count; i++)
{
var cluster = clusters[i];
var meshlet = new Meshlet
{
boundingSphere = new SphereBounds(cluster.bounds.center, cluster.bounds.radius),
parentBoundingSphere = new SphereBounds(group.simplified.center, group.simplified.radius),
boundingBox = new AABB(cluster.bounds.center - cluster.bounds.radius, cluster.bounds.center + cluster.bounds.radius),
vertexCount = (byte)cluster.vertexCount,
triangleCount = (byte)(cluster.localIndexCount / 3),
vertexOffset = (uint)data.meshletVertices.Count,
triangleOffset = (uint)data.meshletTriangles.Count,
groupIndex = (uint)data.groups.Count - 1,
clusterError = cluster.bounds.error,
parentError = group.simplified.error,
localMaterialIndex = 0, // TODO: support multiple materials
lodLevel = (byte)group.depth,
};
data.meshlets.Add(meshlet);
// Add unique vertices
for (nuint j = 0; j < cluster.vertexCount; j++)
{
data.meshletVertices.Add(cluster.uniqueVertices[j]);
}
// Add local triangles (packed into uints)
var triangleCount = cluster.localIndexCount / 3;
for (nuint j = 0; j < triangleCount; j++)
{
uint i0 = cluster.localIndices[j * 3 + 0];
uint i1 = cluster.localIndices[j * 3 + 1];
uint i2 = cluster.localIndices[j * 3 + 2];
var packedTriangle = i0 | (i1 << 8) | (i2 << 16);
data.meshletTriangles.Add(packedTriangle);
}
}
return 0;
}
public void ReleaseResource(IResourceDatabase database)
{
ReleaseCpuResources();

View File

@@ -130,7 +130,7 @@ public readonly unsafe ref struct RenderContext
if (staticMesh)
{
meshData.CookMeshlets();
//meshData.CookMeshlets();
UploadMeshlets(mesh);
meshData.ReleaseCpuResources();
}

View File

@@ -1,694 +0,0 @@
// Source: https://github.com/zeux/meshoptimizer/blob/master/demo/clusterlod.h
// Translated from C++ to C#.
// TODO: This file should be moved to editor project since there is no reason we need to build meshlets and LOD at runtime.
using Ghost.Core;
using Ghost.MeshOptimizer;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.Mathematics;
using System.Diagnostics;
namespace Ghost.Graphics.Utilities;
internal struct Cluster : IDisposable
{
public UnsafeList<uint> indices;
public UnsafeList<uint> uniqueVertices;
public UnsafeList<byte> localIndices;
public ClodBounds bounds;
public nuint vertices;
public int group;
public int refined;
public void Dispose()
{
indices.Dispose();
uniqueVertices.Dispose();
localIndices.Dispose();
}
}
/// <summary>
/// Represents the bounding sphere and simplification error for a LOD cluster.
/// </summary>
public struct ClodBounds
{
/// <summary> The center of the bounding sphere. </summary>
public float3 center;
/// <summary> The radius of the bounding sphere. </summary>
public float radius;
/// <summary> The simplification error associated with this LOD level. </summary>
public float error;
}
/// <summary>
/// Configuration parameters for the cluster LOD generation pipeline.
/// </summary>
public struct ClodConfig
{
/// <summary> The maximum number of vertices per meshlet. </summary>
public nuint maxVertices;
/// <summary> The minimum number of triangles per meshlet. </summary>
public nuint minTriangles;
/// <summary> The maximum number of triangles per meshlet. </summary>
public nuint maxTriangles;
/// <summary> Whether to use spatial partitioning during meshlet building. </summary>
public bool partitionSpatial;
/// <summary> Whether to sort clusters after partitioning. </summary>
public bool partitionSort;
/// <summary> The target size for partitions. </summary>
public nuint partitionSize;
/// <summary> Whether to cluster meshlets using spatial clustering. </summary>
public bool clusterSpatial;
/// <summary> Weight factor for cluster fill calculation. </summary>
public float clusterFillWeight;
/// <summary> Split factor for flexible clustering. </summary>
public float clusterSplitFactor;
/// <summary> The simplification ratio to achieve per LOD level. </summary>
public float simplifyRatio;
/// <summary> Threshold for stopping simplification. </summary>
public float simplifyThreshold;
/// <summary> Error factor used when merging previous LOD level errors. </summary>
public float simplifyErrorMergePrevious;
/// <summary> Additive error factor when merging LOD levels. </summary>
public float simplifyErrorMergeAdditive;
/// <summary> Error factor for sloppy simplification. </summary>
public float simplifyErrorFactorSloppy;
/// <summary> Edge length limit error factor. </summary>
public float simplifyErrorEdgeLimit;
/// <summary> Whether to allow permissive simplification. </summary>
public bool simplifyPermissive;
/// <summary> Whether to fallback to permissive simplification. </summary>
public bool simplifyFallbackPermissive;
/// <summary> Whether to fallback to sloppy simplification. </summary>
public bool simplifyFallbackSloppy;
/// <summary> Whether to regularize the mesh during simplification. </summary>
public bool simplifyRegularize;
/// <summary> Whether to optimize cluster bounds. </summary>
public bool optimizeBounds;
/// <summary> Whether to optimize clusters post-build. </summary>
public bool optimizeClusters;
/// <summary> Level of cluster optimization. </summary>
public int optimizeClustersLevel;
}
/// <summary>
/// Contains input data for the Cluster LOD generation pipeline.
/// </summary>
public unsafe struct ClodMesh
{
/// <summary> Pointer to vertex position data (float array). </summary>
public float* vertexPositions;
/// <summary> Number of vertices in the mesh. </summary>
public nuint vertexCount;
/// <summary> Stride in bytes for vertex position data. </summary>
public nuint vertexPositionsStride;
/// <summary> Pointer to vertex attribute data (float array). </summary>
public float* vertexAttributes;
/// <summary> Stride in bytes for vertex attribute data. </summary>
public nuint vertexAttributesStride;
/// <summary> Pointer to attribute weights for simplification. </summary>
public float* attributeWeights;
/// <summary> Number of vertex attributes. </summary>
public nuint attributeCount;
/// <summary> Pointer to index data. </summary>
public uint* indices;
/// <summary> Number of indices in the mesh. </summary>
public nuint indexCount;
/// <summary> Pointer to per-vertex lock flags (1 byte per vertex). </summary>
public byte* vertexLock;
/// <summary> Mask indicating which attributes are protected during simplification. </summary>
public uint attributeProtectMask;
}
/// <summary>
/// Defines a group of clusters in the LOD hierarchy.
/// </summary>
public struct ClodGroup
{
/// <summary> LOD hierarchy depth of this group. </summary>
public int depth;
/// <summary> Bounding information for the simplified group. </summary>
public ClodBounds simplified;
}
/// <summary>
/// Represents a cluster of meshlets in the LOD hierarchy.
/// </summary>
public unsafe struct ClodCluster
{
/// <summary> Refinement level of the cluster. </summary>
public int refined;
/// <summary> Bounding info for the cluster. </summary>
public ClodBounds bounds;
/// <summary> Pointer to indices for this cluster. </summary>
public uint* indices;
/// <summary> Number of indices. </summary>
public nuint indexCount;
/// <summary> Pointer to unique vertices for this cluster. </summary>
public uint* uniqueVertices;
/// <summary> Number of unique vertices in the cluster. </summary>
public nuint vertexCount;
/// <summary> Pointer to local triangle indices for this cluster. </summary>
public byte* localIndices;
/// <summary> Number of local indices. </summary>
public nuint localIndexCount;
}
/// <summary>
/// Delegate type for processing generated LOD groups.
/// </summary>
public unsafe delegate int ClodOutputDelegate(void* context, ClodGroup group, ReadOnlyUnsafeCollection<ClodCluster> clusters);
// FIX: UnsafeList and UnsafeArray are not same as std::vector.
public static unsafe class MeshletUtility
{
private static ClodBounds ComputeBounds(ref readonly ClodMesh mesh, UnsafeList<uint> indices, float error)
{
var bounds = MeshOptApi.ComputeClusterBounds((uint*)indices.GetUnsafePtr(), (nuint)indices.Count, mesh.vertexPositions, mesh.vertexCount, mesh.vertexPositionsStride);
return new ClodBounds
{
center = new float3(bounds.center[0], bounds.center[1], bounds.center[2]),
radius = bounds.radius,
error = error
};
}
private static ClodBounds MergeBounds(UnsafeList<Cluster> clusters, UnsafeList<int> group)
{
using var boundsList = new UnsafeArray<ClodBounds>(group.Count, AllocationHandle.FreeList);
for (var j = 0; j < group.Count; j++)
{
boundsList[j] = (clusters[group[j]].bounds);
}
var merged = MeshOptApi.ComputeSphereBounds(
(float*)boundsList.GetUnsafePtr(),
(nuint)group.Count,
(nuint)sizeof(ClodBounds),
(float*)boundsList.GetUnsafePtr() + 3,
(nuint)sizeof(ClodBounds)
);
var maxError = 0.0f;
for (var j = 0; j < group.Count; j++)
{
maxError = Math.Max(maxError, clusters[group[j]].bounds.error);
}
return new ClodBounds
{
center = new float3(merged.center[0], merged.center[1], merged.center[2]),
radius = merged.radius,
error = maxError
};
}
private static UnsafeList<Cluster> Clusterize(ref readonly ClodConfig config, ref readonly ClodMesh mesh, uint* indices, nuint indexCount)
{
var maxMeshlets = MeshOptApi.BuildMeshletsBound(indexCount, config.maxVertices, config.minTriangles);
using var meshlets = new UnsafeArray<meshopt_Meshlet>((int)maxMeshlets, AllocationHandle.FreeList);
using var meshletVertices = new UnsafeArray<uint>((int)indexCount, AllocationHandle.FreeList);
using var meshletTriangles = new UnsafeArray<byte>((int)indexCount, AllocationHandle.FreeList);
var pMeshlets = (meshopt_Meshlet*)meshlets.GetUnsafePtr();
var pMeshletVertices = (uint*)meshletVertices.GetUnsafePtr();
var pMeshletTriangles = (byte*)meshletTriangles.GetUnsafePtr();
nuint meshletCount;
if (config.clusterSpatial)
{
meshletCount = pMeshlets[0].BuildsSpatial(
pMeshletVertices, pMeshletTriangles,
indices, indexCount,
mesh.vertexPositions, mesh.vertexCount, mesh.vertexPositionsStride,
config.maxVertices, config.minTriangles, config.maxTriangles,
config.clusterFillWeight
);
}
else
{
meshletCount = pMeshlets[0].BuildsFlex(
pMeshletVertices, pMeshletTriangles,
indices, indexCount,
mesh.vertexPositions, mesh.vertexCount, mesh.vertexPositionsStride,
config.maxVertices, config.minTriangles, config.maxTriangles,
0.0f, config.clusterSplitFactor
);
}
var clusters = new UnsafeList<Cluster>((int)meshletCount, AllocationHandle.FreeList);
for (nuint i = 0; i < meshletCount; i++)
{
ref var meshlet = ref pMeshlets[i];
if (config.optimizeClusters)
{
MeshOptApi.OptimizeMeshlet(
pMeshletVertices + meshlet.vertex_offset,
pMeshletTriangles + meshlet.triangle_offset,
meshlet.triangle_count,
meshlet.vertex_count
);
}
var cluster = new Cluster
{
vertices = meshlet.vertex_count,
indices = new UnsafeList<uint>((int)(meshlet.triangle_count * 3), AllocationHandle.FreeList),
uniqueVertices = new UnsafeList<uint>((int)meshlet.vertex_count, AllocationHandle.FreeList),
localIndices = new UnsafeList<byte>((int)(meshlet.triangle_count * 3), AllocationHandle.FreeList),
group = -1,
refined = -1
};
for (nuint j = 0; j < meshlet.vertex_count; j++)
{
cluster.uniqueVertices.Add(pMeshletVertices[meshlet.vertex_offset + j]);
}
for (nuint j = 0; j < meshlet.triangle_count * 3; j++)
{
var localIdx = pMeshletTriangles[meshlet.triangle_offset + j];
cluster.localIndices.Add(localIdx);
cluster.indices.Add(pMeshletVertices[meshlet.vertex_offset + localIdx]);
}
clusters.Add(cluster);
}
return clusters;
}
internal static void LockBoundary(UnsafeArray<byte> locks, UnsafeList<UnsafeList<int>> groups, UnsafeList<Cluster> clusters, UnsafeArray<uint> remap, byte* vertexLock)
{
var pLocks = (byte*)locks.GetUnsafePtr();
var pRemap = (uint*)remap.GetUnsafePtr();
for (var i = 0; i < locks.Length; i++)
{
pLocks[i] = unchecked((byte)(pLocks[i] & ~((1 << 0) | (1 << 7))));
}
for (var i = 0; i < groups.Count; i++)
{
for (var j = 0; j < groups[i].Count; j++)
{
var cluster = clusters[groups[i][j]];
for (var k = 0; k < cluster.indices.Count; k++)
{
var r = pRemap[(int)cluster.indices[k]];
pLocks[r] |= (byte)(pLocks[r] >> 7);
}
}
for (var j = 0; j < groups[i].Count; j++)
{
var cluster = clusters[groups[i][j]];
for (var k = 0; k < cluster.indices.Count; k++)
{
var r = pRemap[(int)cluster.indices[k]];
pLocks[r] |= 1 << 7;
}
}
}
for (var i = 0; i < locks.Length; i++)
{
var r = pRemap[i];
pLocks[i] = (byte)((pLocks[r] & 1) | (pLocks[i] & (byte)SimplifyVertexOptions.Protect & 0xFF));
if (vertexLock != null)
{
pLocks[i] |= vertexLock[i];
}
}
}
private static UnsafeList<UnsafeList<int>> Partition(ref readonly ClodConfig config, ref readonly ClodMesh mesh, UnsafeList<Cluster> clusters, UnsafeList<int> pending, UnsafeArray<uint> remap)
{
if (pending.Count <= (int)config.partitionSize)
{
var single = new UnsafeList<UnsafeList<int>>(1, AllocationHandle.FreeList);
var pendingcpy = new UnsafeList<int>(pending.Count, AllocationHandle.FreeList);
pendingcpy.AddRange(pending.AsSpan());
single.Add(pendingcpy);
return single;
}
nuint totalIndexCount = 0;
for (var i = 0; i < pending.Count; i++)
{
totalIndexCount += (nuint)clusters[pending[i]].indices.Count;
}
using var clusterIndices = new UnsafeList<uint>((int)totalIndexCount, AllocationHandle.FreeList);
using var clusterCounts = new UnsafeList<uint>(pending.Count, AllocationHandle.FreeList);
nuint offset = 0;
for (var i = 0; i < pending.Count; i++)
{
var cluster = clusters[pending[i]];
clusterCounts.Add((uint)cluster.indices.Count);
for (var j = 0; j < cluster.indices.Count; j++)
{
clusterIndices.Add(((uint*)remap.GetUnsafePtr())[(int)cluster.indices[j]]);
}
offset += (nuint)cluster.indices.Count;
}
using var clusterPart = new UnsafeArray<uint>(pending.Count, AllocationHandle.FreeList);
var partitionCount = MeshOptApi.PartitionClusters(
(uint*)clusterPart.GetUnsafePtr(),
(uint*)clusterIndices.GetUnsafePtr(),
totalIndexCount,
(uint*)clusterCounts.GetUnsafePtr(),
(nuint)pending.Count,
config.partitionSpatial ? mesh.vertexPositions : null,
(nuint)remap.Length,
mesh.vertexPositionsStride,
config.partitionSize
);
var partitions = new UnsafeList<UnsafeList<int>>((int)partitionCount, AllocationHandle.FreeList);
for (nuint i = 0; i < partitionCount; i++)
{
partitions.Add(new UnsafeList<int>((int)(config.partitionSize + config.partitionSize / 3), AllocationHandle.FreeList));
}
for (var i = 0; i < pending.Count; i++)
{
partitions[(int)clusterPart[i]].Add(pending[i]);
}
return partitions;
}
private static int OutputGroup(ref readonly ClodConfig config, ref readonly ClodMesh mesh, UnsafeList<Cluster> clusters, UnsafeList<int> group, ClodBounds simplified, int depth, void* outputContext, ClodOutputDelegate? outputCallback)
{
using var groupClusters = new UnsafeList<ClodCluster>(group.Count, AllocationHandle.FreeList);
for (var i = 0; i < group.Count; i++)
{
ref var srcCluster = ref clusters[group[i]];
groupClusters.Add(new ClodCluster
{
refined = srcCluster.refined,
bounds = (config.optimizeBounds && srcCluster.refined != -1)
? ComputeBounds(in mesh, srcCluster.indices, srcCluster.bounds.error)
: srcCluster.bounds,
indices = (uint*)srcCluster.indices.GetUnsafePtr(),
indexCount = (nuint)srcCluster.indices.Count,
uniqueVertices = (uint*)srcCluster.uniqueVertices.GetUnsafePtr(),
vertexCount = srcCluster.vertices,
localIndices = (byte*)srcCluster.localIndices.GetUnsafePtr(),
localIndexCount = (nuint)srcCluster.localIndices.Count
});
}
var clodGroup = new ClodGroup { depth = depth, simplified = simplified };
var result = outputCallback != null
? outputCallback(outputContext, clodGroup, groupClusters.AsReadOnly())
: -1;
return result;
}
private struct SloppyVertex
{
public float x, y, z;
public uint id;
}
private static void SimplifyFallback(ref UnsafeArray<uint> lod, ref readonly ClodMesh mesh, ReadOnlyUnsafeCollection<uint> indices, ReadOnlyUnsafeCollection<byte> locks, nuint target_count, float* error)
{
using var subset = new UnsafeArray<SloppyVertex>(indices.Count, AllocationHandle.FreeList);
using var subset_locks = new UnsafeArray<byte>(indices.Count, AllocationHandle.FreeList);
lod.Resize(indices.Count);
var positions_stride = mesh.vertexPositionsStride / sizeof(float);
// deindex the mesh subset to avoid calling simplifySloppy on the entire vertex buffer (which is prohibitively expensive without sparsity)
for (var i = 0; i < indices.Count; ++i)
{
var v = indices[i];
Logger.DebugAssert(v < mesh.vertexCount);
subset[i].x = mesh.vertexPositions[v * positions_stride + 0];
subset[i].y = mesh.vertexPositions[v * positions_stride + 1];
subset[i].z = mesh.vertexPositions[v * positions_stride + 2];
subset[i].id = v;
subset_locks[i] = locks[v];
lod[i] = (uint)i;
}
var newSize = MeshOptApi.SimplifySloppy((uint*)lod.GetUnsafePtr(), (uint*)lod.GetUnsafePtr(), (nuint)lod.Count, (float*)subset.GetUnsafePtr(), (nuint)subset.Count, (nuint)sizeof(SloppyVertex), (byte*)subset_locks.GetUnsafePtr(), target_count, float.MaxValue, error);
lod.Resize((int)newSize);
// convert error to absolute
*error *= MeshOptApi.SimplifyScale((float*)subset.GetUnsafePtr(), (nuint)subset.Count, (nuint)sizeof(SloppyVertex));
// restore original vertex indices
for (var i = 0; i < lod.Count; ++i)
{
lod[i] = subset[lod[i]].id;
}
}
public static UnsafeArray<uint> Simplify(ref readonly ClodConfig config, ref readonly ClodMesh mesh, ReadOnlyUnsafeCollection<uint> indices, ReadOnlyUnsafeCollection<byte> locks, nuint targetCount, float* error)
{
var lod = new UnsafeArray<uint>(indices.Count, AllocationHandle.FreeList);
if (targetCount >= (nuint)indices.Count)
{
lod.CopyFrom(indices.AsSpan());
return lod;
}
var options = SimplifyOptions.Sparse | SimplifyOptions.ErrorAbsolute;
if (config.simplifyPermissive)
{
options |= SimplifyOptions.Permissive;
}
if (config.simplifyRegularize)
{
options |= SimplifyOptions.Regularize;
}
var resultSize = MeshOptApi.SimplifyWithAttributes(
(uint*)lod.GetUnsafePtr(),
(uint*)indices.GetUnsafePtr(),
(nuint)indices.Count,
mesh.vertexPositions,
mesh.vertexCount,
mesh.vertexPositionsStride,
mesh.vertexAttributes,
mesh.vertexAttributesStride,
mesh.attributeWeights,
mesh.attributeCount,
(byte*)locks.GetUnsafePtr(),
targetCount,
float.MaxValue,
options,
error
);
lod.Resize((int)resultSize);
if ((nuint)lod.Length > targetCount && config.simplifyFallbackPermissive && !config.simplifyPermissive)
{
options |= SimplifyOptions.Permissive;
resultSize = MeshOptApi.SimplifyWithAttributes(
(uint*)lod.GetUnsafePtr(),
(uint*)indices.GetUnsafePtr(),
(nuint)indices.Count,
mesh.vertexPositions,
mesh.vertexCount,
mesh.vertexPositionsStride,
mesh.vertexAttributes,
mesh.vertexAttributesStride,
mesh.attributeWeights,
mesh.attributeCount,
(byte*)locks.GetUnsafePtr(),
targetCount,
float.MaxValue,
options,
error
);
lod.Resize((int)resultSize);
}
if ((nuint)lod.Length > targetCount && config.simplifyFallbackSloppy)
{
SimplifyFallback(ref lod, in mesh, indices, locks, targetCount, error);
*error *= config.simplifyErrorFactorSloppy;
}
if (config.simplifyErrorEdgeLimit > 0)
{
float maxEdgeSq = 0;
var pIdx = (uint*)indices.GetUnsafePtr();
var posStride = mesh.vertexPositionsStride / (nuint)sizeof(float);
for (var i = 0; i < indices.Count; i += 3)
{
uint a = pIdx[i], b = pIdx[i + 1], c = pIdx[i + 2];
var va = mesh.vertexPositions + (a * posStride);
var vb = mesh.vertexPositions + (b * posStride);
var vc = mesh.vertexPositions + (c * posStride);
float dx, dy, dz;
dx = va[0] - vb[0]; dy = va[1] - vb[1]; dz = va[2] - vb[2];
var eab = dx * dx + dy * dy + dz * dz;
dx = va[0] - vc[0]; dy = va[1] - vc[1]; dz = va[2] - vc[2];
var eac = dx * dx + dy * dy + dz * dz;
dx = vb[0] - vc[0]; dy = vb[1] - vc[1]; dz = vb[2] - vc[2];
var ebc = dx * dx + dy * dy + dz * dz;
var emax = Math.Max(Math.Max(eab, eac), ebc);
var emin = Math.Min(Math.Min(eab, eac), ebc);
maxEdgeSq = Math.Max(maxEdgeSq, Math.Max(emin, emax / 4));
}
*error = Math.Min(*error, (float)Math.Sqrt(maxEdgeSq) * config.simplifyErrorEdgeLimit);
}
return lod;
}
/// <summary>
/// Builds a cluster LOD hierarchy from the input mesh.
/// </summary>
/// <param name="config">The configuration parameters for the LOD building process.</param>
/// <param name="mesh">The input mesh data.</param>
/// <param name="outputContext">Optional context pointer passed to the output callback.</param>
/// <param name="outputCallback">Delegate invoked for each generated LOD group.</param>
/// <returns>The total count of generated clusters.</returns>
public static nuint Build(ref readonly ClodConfig config, ref readonly ClodMesh mesh, void* outputContext, ClodOutputDelegate? outputCallback)
{
Logger.DebugAssert(mesh.vertexAttributesStride % sizeof(float) == 0, "vertexAttributesStride must be a multiple of sizeof(float)");
using var locks = new UnsafeArray<byte>((int)mesh.vertexCount, AllocationHandle.FreeList, AllocationOption.Clear); ;
using var remap = new UnsafeArray<uint>((int)mesh.vertexCount, AllocationHandle.FreeList);
MeshOptApi.GeneratePositionRemap((uint*)remap.GetUnsafePtr(), mesh.vertexPositions, mesh.vertexCount, mesh.vertexPositionsStride);
if (mesh.attributeProtectMask != 0)
{
var maxAttributes = mesh.vertexAttributesStride / sizeof(float);
for (nuint i = 0; i < mesh.vertexCount; i++)
{
var r = ((uint*)remap.GetUnsafePtr())[(int)i];
for (nuint j = 0; j < maxAttributes; j++)
{
if ((r != i) && ((mesh.attributeProtectMask & (1u << (int)j)) != 0))
{
if (mesh.vertexAttributes[i * maxAttributes + j] != mesh.vertexAttributes[r * maxAttributes + j])
{
((byte*)locks.GetUnsafePtr())[i] |= (byte)SimplifyVertexOptions.Protect & 0xFF;
}
}
}
}
}
using var clusters = Clusterize(in config, in mesh, mesh.indices, mesh.indexCount);
for (var i = 0; i < clusters.Count; i++)
{
clusters[i].bounds = ComputeBounds(in mesh, clusters[i].indices, 0.0f);
}
using var pending = new UnsafeList<int>(clusters.Count, AllocationHandle.FreeList);
for (var i = 0; i < clusters.Count; i++)
{
pending.Add(i);
}
var depth = 0;
while (pending.Count > 1)
{
using var groups = Partition(in config, in mesh, clusters, pending, remap);
pending.Clear();
LockBoundary(locks, groups, clusters, remap, mesh.vertexLock);
for (var i = 0; i < groups.Count; i++)
{
using var merged = new UnsafeList<uint>(groups[i].Count * (int)config.maxTriangles * 3, AllocationHandle.FreeList);
for (var j = 0; j < groups[i].Count; j++)
{
var clusterIndices = clusters[groups[i][j]].indices;
merged.AddRange(clusterIndices.AsSpan());
}
var targetSize = (nuint)(merged.Count / 3 * config.simplifyRatio * 3.0f);
var bounds = MergeBounds(clusters, groups[i]);
var error = 0.0f;
using var simplified = Simplify(in config, in mesh, merged.AsReadOnly(), locks.AsReadOnly(), targetSize, &error);
if ((nuint)simplified.Length > (nuint)(merged.Count * config.simplifyThreshold))
{
bounds.error = float.MaxValue;
OutputGroup(in config, in mesh, clusters, groups[i], bounds, depth, outputContext, outputCallback);
continue;
}
bounds.error = Math.Max(bounds.error * config.simplifyErrorMergePrevious, error) + error * config.simplifyErrorMergeAdditive;
var refined = OutputGroup(in config, in mesh, clusters, groups[i], bounds, depth, outputContext, outputCallback);
for (var j = 0; j < groups[i].Count; j++)
{
clusters[groups[i][j]].Dispose();
}
using var split = Clusterize(in config, in mesh, (uint*)simplified.GetUnsafePtr(), (nuint)simplified.Length);
for (var j = 0; j < split.Count; j++)
{
split[j].refined = refined;
split[j].bounds = bounds;
clusters.Add(split[j]);
pending.Add(clusters.Count - 1);
}
}
for (var i = 0; i < groups.Count; i++)
{
groups[i].Dispose();
}
depth++;
}
if (pending.Count > 0)
{
var bounds = clusters[pending[0]].bounds;
bounds.error = float.MaxValue;
OutputGroup(in config, in mesh, clusters, pending, bounds, depth, outputContext, outputCallback);
}
var finalClusterCount = (nuint)clusters.Count;
for (var i = 0; i < clusters.Count; i++)
{
clusters[i].Dispose();
}
return finalClusterCount;
}
}