Refactor mesh/texture pipeline for unified buffers & cubemaps

- Switch mesh import to unified vertex/index buffers with multi-material partitioning (`MaterialPartInfo`)
- Update `GeometryMeshNode` and meshlet builder for unified buffer layout
- Refactor cubemap texture pipeline: packed faces, improved GGX mip generation, equirect->cubemap conversion, and cubemap sampling
- Change MeshBuilder normal/tangent utilities to use `Span<T>`
- Add mimalloc allocator dependency and enable in Debug/Release
- Misc bug fixes, resource management, and code cleanup
This commit is contained in:
2026-04-26 21:40:24 +09:00
parent 5903ddda2b
commit e3a02437c3
7 changed files with 398 additions and 238 deletions

View File

@@ -1,6 +1,7 @@
using Ghost.Core;
using Ghost.Engine;
using Ghost.Graphics.RHI;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.Mathematics;
using System.Runtime.InteropServices;
@@ -20,15 +21,15 @@ public class MeshNode : IDisposable
}
public MeshNode? Parent
{
get; init;
}
public required IReadOnlyCollection<MeshNode> Children
{
get; set;
}
public IReadOnlyCollection<MeshNode> Children
{
get; set;
} = Array.Empty<MeshNode>();
~MeshNode()
{
Dispose(false);
@@ -45,15 +46,36 @@ public class MeshNode : IDisposable
child.Dispose();
}
Parent = null;
Children = Array.Empty<MeshNode>();
Dispose(true);
GC.SuppressFinalize(this);
}
}
/// <summary>
/// Describes one material partition within a unified vertex/index buffer.
/// </summary>
public struct MaterialPartInfo
{
/// <summary> The material slot index (from ufbx face_material). </summary>
public int materialIndex;
/// <summary> Byte offset into the unified index buffer. </summary>
public int indexStart;
/// <summary> Number of indices belonging to this part. </summary>
public int indexCount;
/// <summary> Byte offset into the unified vertex buffer. </summary>
public int vertexStart;
/// <summary> Number of unique vertices belonging to this part. </summary>
public int vertexCount;
}
public class GeometryMeshNode : MeshNode
{
private UnsafeList<Vertex> _vertices;
private UnsafeList<uint> _indices;
private UnsafeArray<MaterialPartInfo> _materialParts;
public UnsafeList<Vertex> Vertices
{
@@ -75,15 +97,21 @@ public class GeometryMeshNode : MeshNode
}
}
public int MaterialIndex
public UnsafeArray<MaterialPartInfo> MaterialParts
{
get; set;
get => _materialParts;
set
{
_materialParts.Dispose();
_materialParts = value;
}
}
protected override void Dispose(bool disposing)
{
_vertices.Dispose();
_indices.Dispose();
_materialParts.Dispose();
}
}

View File

@@ -12,21 +12,32 @@ using Misaki.HighPerformance.LowLevel.Utilities;
using Misaki.HighPerformance.Mathematics;
using System.Runtime.CompilerServices;
using System.Text;
using System.Xml.Linq;
namespace Ghost.Editor.Core.Assets;
internal unsafe class MeshParsingWorkItem : IJob
internal readonly unsafe struct MeshParsingWorkItem : IJob
{
private struct GeometryPart : IDisposable
{
public UnsafeList<Vertex> vertices;
public UnsafeList<uint> indices;
public int materialIndex;
public bool missingNormals;
public bool missingTangents;
public void Dispose()
{
vertices.Dispose();
indices.Dispose();
}
}
private readonly string _filePath;
private readonly AllocationHandle _allocationHandle;
private readonly MeshAssetSettings _settings;
private readonly TaskCompletionSource<Result<MeshNode>> _taskCompletionSource;
public UnsafeList<Vertex> vertices;
public UnsafeList<uint> indices;
public Task<Result<MeshNode>> Task => _taskCompletionSource.Task;
public readonly Task<Result<MeshNode>> Task => _taskCompletionSource.Task;
public MeshParsingWorkItem(string filePath, AllocationHandle allocationHandle, MeshAssetSettings settings)
{
@@ -82,8 +93,11 @@ internal unsafe class MeshParsingWorkItem : IJob
if (node->mesh != null)
{
var geoNodes = ParseGeometry(node->mesh);
children.AddRange(geoNodes);
var geoNode = ParseGeometry(node->mesh);
if (geoNode != null)
{
children.Add(geoNode);
}
}
// TODO: Handle lights, cameras, and other node types.
@@ -96,21 +110,22 @@ internal unsafe class MeshParsingWorkItem : IJob
return meshNode;
}
private IReadOnlyCollection<GeometryMeshNode> ParseGeometry(ufbx_mesh* pMesh)
private GeometryMeshNode? ParseGeometry(ufbx_mesh* pMesh)
{
var resultNodes = new List<GeometryMeshNode>();
if (pMesh->num_faces == 0)
{
return resultNodes;
return null;
}
var numMaterials = pMesh->materials.count > 0 ? (int)pMesh->materials.count : 1;
var materialBuckets = new UnsafeList<Vertex>[numMaterials];
var missingNormalsBucket = new bool[numMaterials];
var missingTangentsBucket = new bool[numMaterials];
for (int i = 0; i < numMaterials; i++)
// Bucket faces by material
using var materialBuckets = new UnsafeArray<UnsafeList<Vertex>>(numMaterials, AllocationHandle.FreeList);
using var missingNormalsBucket = new UnsafeArray<bool>(numMaterials, AllocationHandle.FreeList);
using var missingTangentsBucket = new UnsafeArray<bool>(numMaterials, AllocationHandle.FreeList);
for (var i = 0; i < numMaterials; i++)
{
materialBuckets[i] = new UnsafeList<Vertex>(1024, AllocationHandle.FreeList);
}
@@ -176,9 +191,13 @@ internal unsafe class MeshParsingWorkItem : IJob
}
}
for (int m = 0; m < numMaterials; m++)
// Per-material weld + optimize, collect intermediate results
using var partResults = new UnsafeList<GeometryPart>(numMaterials, AllocationHandle.FreeList);
for (var m = 0; m < numMaterials; m++)
{
var flatVertices = materialBuckets[m];
ref var flatVertices = ref materialBuckets[m];
if (flatVertices.Count == 0)
{
flatVertices.Dispose();
@@ -207,60 +226,102 @@ internal unsafe class MeshParsingWorkItem : IJob
MeshOptApi.OptimizeVertexCache((uint*)cachedIndices.GetUnsafePtr(), (uint*)weldedIndices.GetUnsafePtr(), numIndices, numUniqueVertices);
var nodeVertices = new UnsafeList<Vertex>((int)numUniqueVertices, _allocationHandle);
var nodeIndices = new UnsafeList<uint>((int)numIndices, _allocationHandle);
// Allocate temporary per-part buffers (will be merged then disposed)
var partVertices = new UnsafeList<Vertex>((int)numUniqueVertices, AllocationHandle.FreeList);
var partIndices = new UnsafeList<uint>((int)numIndices, AllocationHandle.FreeList);
var finalVertexCount = MeshOptApi.OptimizeVertexFetch(nodeVertices.GetUnsafePtr(), (uint*)cachedIndices.GetUnsafePtr(), numIndices, flatVertices.GetUnsafePtr(), numIndices, (nuint)sizeof(Vertex));
var finalVertexCount = MeshOptApi.OptimizeVertexFetch(partVertices.GetUnsafePtr(), (uint*)cachedIndices.GetUnsafePtr(), numIndices, flatVertices.GetUnsafePtr(), numIndices, (nuint)sizeof(Vertex));
nodeVertices.UnsafeSetCount((int)finalVertexCount);
partVertices.UnsafeSetCount((int)finalVertexCount);
MemoryUtility.MemCpy(nodeIndices.GetUnsafePtr(), cachedIndices.GetUnsafePtr(), numIndices * sizeof(uint));
nodeIndices.UnsafeSetCount((int)numIndices);
MemoryUtility.MemCpy(partIndices.GetUnsafePtr(), cachedIndices.GetUnsafePtr(), numIndices * sizeof(uint));
partIndices.UnsafeSetCount((int)numIndices);
if (_settings.NormalDataSource == VertexDataSource.Computed || (_settings.NormalDataSource == VertexDataSource.ComputedIfMissing && missingNormalsBucket[m]))
var part = new GeometryPart
{
MeshBuilder.ComputeNormal(nodeVertices, nodeIndices);
}
if (_settings.TangentDataSource == VertexDataSource.Computed || (_settings.TangentDataSource == VertexDataSource.ComputedIfMissing && missingTangentsBucket[m]))
{
MeshBuilder.ComputeTangents(nodeVertices, nodeIndices);
}
var meshNodeName = numMaterials > 1 ? $"{pMesh->name.ToString()}_mat{m}" : pMesh->name.ToString();
var meshNode = new GeometryMeshNode
{
Name = meshNodeName,
LocalTransform = new float4x4(new float4(1, 0, 0, 0), new float4(0, 1, 0, 0), new float4(0, 0, 1, 0), new float4(0, 0, 0, 1)),
Children = Array.Empty<MeshNode>(),
Vertices = nodeVertices,
Indices = nodeIndices,
MaterialIndex = m
vertices = partVertices,
indices = partIndices,
materialIndex = m,
missingNormals = missingNormalsBucket[m],
missingTangents = missingTangentsBucket[m]
};
resultNodes.Add(meshNode);
partResults.Add(part);
flatVertices.Dispose();
}
return resultNodes;
if (partResults.Count == 0)
{
return null;
}
// Merge all material parts into one unified vertex/index buffer
var totalVertexCount = 0;
var totalIndexCount = 0;
for (var i = 0; i < partResults.Count; i++)
{
totalVertexCount += partResults[i].vertices.Count;
totalIndexCount += partResults[i].indices.Count;
}
var mergedVertices = new UnsafeList<Vertex>(totalVertexCount, _allocationHandle);
var mergedIndices = new UnsafeList<uint>(totalIndexCount, _allocationHandle);
var materialParts = new UnsafeArray<MaterialPartInfo>(partResults.Count, _allocationHandle);
var vertexOffset = 0;
var indexOffset = 0;
for (var i = 0; i < partResults.Count; i++)
{
ref var part = ref partResults[i];
// Compute normals/tangents per-part before merge (requires local indices)
if (_settings.NormalDataSource == VertexDataSource.Computed || (_settings.NormalDataSource == VertexDataSource.ComputedIfMissing && part.missingNormals))
{
MeshBuilder.ComputeNormal(part.vertices, part.indices);
}
if (_settings.TangentDataSource == VertexDataSource.Computed || (_settings.TangentDataSource == VertexDataSource.ComputedIfMissing && part.missingTangents))
{
MeshBuilder.ComputeTangents(part.vertices, part.indices);
}
materialParts[i] = new MaterialPartInfo
{
materialIndex = part.materialIndex,
vertexStart = vertexOffset,
vertexCount = part.vertices.Count,
indexStart = indexOffset,
indexCount = part.indices.Count,
};
mergedVertices.AddRange(part.vertices.AsSpan());
// Rebase indices to global vertex space
for (var j = 0; j < part.indices.Count; j++)
{
mergedIndices.Add(part.indices[j] + (uint)vertexOffset);
}
vertexOffset += part.vertices.Count;
indexOffset += part.indices.Count;
part.Dispose();
}
return new GeometryMeshNode
{
Name = pMesh->name.ToString(),
LocalTransform = float4x4.identity,
Vertices = mergedVertices,
Indices = mergedIndices,
MaterialParts = materialParts,
};
}
public void Execute(ref readonly JobExecutionContext context)
{
if (!File.Exists(_filePath))
{
_taskCompletionSource.SetResult(Result.Failure("Invalid file path."));
return;
}
if (!Path.GetExtension(_filePath).Equals(".obj", StringComparison.OrdinalIgnoreCase)
&& !Path.GetExtension(_filePath).Equals(".fbx", StringComparison.OrdinalIgnoreCase))
{
_taskCompletionSource.SetResult(Result.Failure("Unsupported file format. Only .obj and .fbx are supported."));
return;
}
var error = new ufbx_error();
var load_Opts = new ufbx_load_opts
{
@@ -299,7 +360,7 @@ internal unsafe class MeshParsingWorkItem : IJob
var rootNode = ParseHierarchy(scene.Get()->root_node);
rootNode.Name = Path.GetFileNameWithoutExtension(_filePath);
_taskCompletionSource.SetResult(Result<MeshNode>.Success(rootNode));
_taskCompletionSource.SetResult(Result.Success(rootNode));
}
}

View File

@@ -701,70 +701,6 @@ public static unsafe partial class MeshProcessor
public int materialIndex;
}
public static void BuildMeshlets(MeshletMeshData* pMeshletData, ReadOnlyUnsafeCollection<Vertex> vertices, ReadOnlyUnsafeCollection<uint> indices, int materialIndex = 0)
{
Logger.DebugAssert(pMeshletData->meshletCount > 0, "Mesh must have vertices to build meshlets.");
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,
};
var clodMesh = new ClodMesh
{
vertexPositions = (float*)Unsafe.AsPointer(in vertices[0].position),
vertexCount = (nuint)vertices.Count,
vertexPositionsStride = (nuint)sizeof(Vertex),
vertexAttributes = (float*)Unsafe.AsPointer(in vertices[0].normal),
vertexAttributesStride = (nuint)sizeof(Vertex),
indices = (uint*)indices.GetUnsafePtr(),
indexCount = (nuint)indices.Count,
attributeProtectMask = 0, // TODO: We need to protect UVs and other vertex attributes to ensure they are not altered during simplification.
};
var context = new MeshletContext
{
data = pMeshletData,
materialIndex = materialIndex
};
Build(in config, in clodMesh, &context, MeshletOutputCallback);
pMeshletData->meshletCount = pMeshletData->meshlets.IsCreated ? pMeshletData->meshlets.Count : 0;
if (pMeshletData->groups.IsCreated && pMeshletData->groups.Count > 0)
{
var maxLodLevel = 0u;
for (var i = 0; i < pMeshletData->groups.Count; i++)
{
maxLodLevel = Math.Max(maxLodLevel, pMeshletData->groups[i].lodLevel);
}
pMeshletData->lodLevelCount = (int)maxLodLevel + 1;
}
pMeshletData->materialSlotCount = Math.Max(pMeshletData->materialSlotCount, materialIndex + 1);
}
private static int MeshletOutputCallback(void* contextPtr, ClodGroup group, ReadOnlyUnsafeCollection<ClodCluster> clusters)
{
var context = (MeshletContext*)contextPtr;
@@ -829,6 +765,91 @@ public static unsafe partial class MeshProcessor
return 0;
}
/// <summary>
/// Builds meshlets for a unified multi-material mesh.
/// Each <see cref="MaterialPartInfo"/> describes a material partition's index range within the unified buffer.
/// Meshlets are built per-part and tagged with the corresponding <c>localMaterialIndex</c>.
/// </summary>
public static void BuildMeshlets(MeshletMeshData* pMeshletData, ReadOnlyUnsafeCollection<Vertex> vertices, ReadOnlyUnsafeCollection<uint> indices, ReadOnlySpan<MaterialPartInfo> parts)
{
Logger.DebugAssert(pMeshletData->meshletCount == 0, "Meshlet data is not empty.");
Logger.DebugAssert(vertices.Count > 0, "Mesh must have vertices to build meshlets.");
Logger.DebugAssert(indices.Count > 0, "Mesh must have indices to build meshlets.");
Logger.DebugAssert(parts.Length > 0, "Must have at least one material part.");
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,
};
for (var i = 0; i < parts.Length; i++)
{
ref readonly var part = ref parts[i];
// Each part references a slice of the global index buffer,
// but vertex positions are the full unified buffer so global indices remain valid.
var clodMesh = new ClodMesh
{
vertexPositions = (float*)Unsafe.AsPointer(in vertices[0].position),
vertexCount = (nuint)vertices.Count,
vertexPositionsStride = (nuint)sizeof(Vertex),
vertexAttributes = (float*)Unsafe.AsPointer(in vertices[0].normal),
vertexAttributesStride = (nuint)sizeof(Vertex),
indices = (uint*)indices.GetUnsafePtr() + part.indexStart,
indexCount = (nuint)part.indexCount,
attributeProtectMask = 0, // TODO: Protect UVs at material boundaries.
};
var context = new MeshletContext
{
data = pMeshletData,
materialIndex = part.materialIndex
};
Build(in config, in clodMesh, &context, MeshletOutputCallback);
}
pMeshletData->meshletCount = pMeshletData->meshlets.IsCreated ? pMeshletData->meshlets.Count : 0;
if (pMeshletData->groups.IsCreated && pMeshletData->groups.Count > 0)
{
var maxLodLevel = 0u;
for (var j = 0; j < pMeshletData->groups.Count; j++)
{
maxLodLevel = Math.Max(maxLodLevel, pMeshletData->groups[j].lodLevel);
}
pMeshletData->lodLevelCount = (int)maxLodLevel + 1;
}
var maxMaterialSlot = 0;
for (var j = 0; j < parts.Length; j++)
{
maxMaterialSlot = Math.Max(maxMaterialSlot, parts[j].materialIndex);
}
pMeshletData->materialSlotCount = maxMaterialSlot + 1;
}
public static void BuildClusterLodHierarchy()
{
// TODO: Implement a function that builds a cluster LOD hierarchy for a mesh, which can be used for efficient rendering of large meshes with varying levels of detail.

View File

@@ -2,6 +2,7 @@ using Ghost.Core;
using Ghost.Engine;
using Ghost.Nvtt;
using Misaki.HighPerformance.LowLevel;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using System.IO.Hashing;
using System.Runtime.CompilerServices;
@@ -188,58 +189,36 @@ internal static partial class TextureProcessor
pCtx.Get()->SetCudaAcceleration(NvttApi.IsCudaSupported());
int maxCubeMips = _mipLevels.Length;
var cubeSurfaces = new IntPtr[maxCubeMips];
try
var maxCubeMips = _mipLevels.Length;
var w0 = _mipLevels[0].width;
if (!pCtx.Get()->OutputHeaderData(NvttTextureType.NVTT_TextureType_Cube, w0, w0, 1, maxCubeMips, false, pCompOpts.Get(), pOutOpts.Get()))
{
return Result.Failure("Failed to output header for cube map.");
}
for (var face = 0; face < 6; face++)
{
for (var level = 0; level < maxCubeMips; level++)
{
var cubeSurf = NvttCubeSurface.Create();
cubeSurfaces[level] = (IntPtr)cubeSurf;
using var faceSurf = new DisposablePtr<NvttSurface>(NvttSurface.Create());
var w = _mipLevels[level].width;
var faceSize = w * w * _textureInfo.colorComponents;
var pSrcData = (float*)_mipLevels[level].data.GetUnsafePtr() + face * faceSize;
using var mipSurf = new DisposablePtr<NvttSurface>(NvttSurface.Create());
if (!mipSurf.Get()->SetImageData(NvttInputFormat.NVTT_InputFormat_RGBA_32F, _mipLevels[level].width, _mipLevels[level].height, 1, _mipLevels[level].data.GetUnsafePtr(), false, null))
if (!faceSurf.Get()->SetImageData(NvttInputFormat.NVTT_InputFormat_RGBA_32F, w, w, 1, pSrcData, false, null))
{
return Result.Failure("Failed to set image data for NVTT compression.");
}
if (_settings.Basic.IsSRGB)
{
mipSurf.Get()->ToSrgb(null);
faceSurf.Get()->ToSrgb(null);
}
cubeSurf->Fold(mipSurf.Get(), NvttCubeLayout.NVTT_CubeLayout_LatitudeLongitude);
}
var firstCube = (NvttCubeSurface*)cubeSurfaces[0];
if (!pCtx.Get()->OutputHeaderCube(firstCube, maxCubeMips, pCompOpts.Get(), pOutOpts.Get()))
{
return Result.Failure("Failed to output header for cube map.");
}
for (var face = 0; face < 6; face++)
{
for (var level = 0; level < maxCubeMips; level++)
if (!pCtx.Get()->Compress(faceSurf.Get(), face, level, pCompOpts.Get(), pOutOpts.Get()))
{
var cubeSurf = (NvttCubeSurface*)cubeSurfaces[level];
using var faceSurf = new DisposablePtr<NvttSurface>(cubeSurf->Face(face));
if (!pCtx.Get()->Compress(faceSurf.Get(), face, level, pCompOpts.Get(), pOutOpts.Get()))
{
return Result.Failure("Failed to compress cube map face.");
}
}
}
}
finally
{
for (var level = 0; level < maxCubeMips; level++)
{
if (cubeSurfaces[level] != IntPtr.Zero)
{
var cubeSurf = (NvttCubeSurface*)cubeSurfaces[level];
cubeSurf->Dispose();
return Result.Failure("Failed to compress cube map face.");
}
}
}
@@ -332,17 +311,53 @@ internal static partial class TextureProcessor
if (settings.Basic.TextureShape == TextureShape.TextureCube)
{
int maxCubeMips;
int edge;
UnsafeArray<float> baseCubeData;
unsafe
{
using var cubeSurface0 = new DisposablePtr<NvttCubeSurface>(NvttCubeSurface.Create());
using var mip0Surf = new DisposablePtr<NvttSurface>(NvttSurface.Create());
mip0Surf.Get()->SetImageData(NvttInputFormat.NVTT_InputFormat_RGBA_32F, textureInfo.width, textureInfo.height, 1, (void*)textureInfo.pixelData, false, null);
if (!mip0Surf.Get()->SetImageData(NvttInputFormat.NVTT_InputFormat_RGBA_32F, textureInfo.width, textureInfo.height, 1, (void*)textureInfo.pixelData, false, null))
{
return Result.Failure("Failed to set image data for cube map.");
}
cubeSurface0.Get()->Fold(mip0Surf.Get(), NvttCubeLayout.NVTT_CubeLayout_LatitudeLongitude);
maxCubeMips = (int)Math.Floor(Math.Log2(cubeSurface0.Get()->EdgeLength())) + 1;
edge = cubeSurface0.Get()->EdgeLength();
maxCubeMips = (int)Math.Floor(Math.Log2(edge)) + 1;
var pixelsPerFace = edge * edge;
var faceSize = pixelsPerFace * textureInfo.colorComponents;
baseCubeData = new UnsafeArray<float>(faceSize * 6, AllocationHandle.FreeList);
var channels = textureInfo.colorComponents;
var channelPtrs = stackalloc float*[channels];
for (var face = 0; face < 6; face++)
{
using var faceSurf = new DisposablePtr<NvttSurface>(cubeSurface0.Get()->Face(face));
// NVTT stores data in planar format: [RRRR...][GGGG...][BBBB...][AAAA...]
// We need to interleave into RGBARGBA... for our sampling code.
var pDst = (float*)baseCubeData.GetUnsafePtr() + face * faceSize;
for (var ch = 0; ch < channels; ch++)
{
channelPtrs[ch] = faceSurf.Get()->Channel(ch);
}
for (var p = 0; p < pixelsPerFace; p++)
{
for (var ch = 0; ch < channels; ch++)
{
pDst[p * channels + ch] = channelPtrs[ch][p];
}
}
}
}
var handle = GenerateMipHDRI(scheduler, textureInfo, maxCubeMips, out mipLevels);
var handle = GenerateMipHDRI(scheduler, textureInfo, baseCubeData, edge, maxCubeMips, out mipLevels);
await scheduler.WaitAsync(handle, cancellationToken);
baseCubeData.Dispose();
}
var workItem = new NvttPipelineTask(cachePath, textureInfo, settings, mipLevels);

View File

@@ -2,6 +2,7 @@ using Ghost.Core;
using Misaki.HighPerformance.Jobs;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.Mathematics;
using Misaki.HighPerformance.Mathematics.SPMD;
using System.Runtime.CompilerServices;
using static Misaki.HighPerformance.Mathematics.math;
@@ -72,34 +73,70 @@ internal static partial class TextureProcessor
return MathV.Normalize(sampleVec);
}
// Maps a 3D direction vector to 2D equirectangular UVs
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector2<TFloat, float> DirToEquirectangularUV(Vector3<TFloat, float> dir)
private static float3 CubemapUVToDir(int face, float u, float v)
{
var u = TFloat.Atan2(dir.z, dir.x);
var v = TFloat.Asin(dir.y);
var sc = 2.0f * u - 1.0f;
var tc = 1.0f - 2.0f * v;
u = u / (2.0f * PI) + 0.5f;
v = v / PI + 0.5f;
return MathV.Create<TFloat, float>(u, v);
float x = 0, y = 0, z = 0;
switch (face)
{
case 0: x = 1.0f; y = tc; z = -sc; break;
case 1: x = -1.0f; y = tc; z = sc; break;
case 2: x = sc; y = 1.0f; z = -tc; break;
case 3: x = sc; y = -1.0f; z = tc; break;
case 4: x = sc; y = tc; z = 1.0f; break;
case 5: x = -sc; y = tc; z = -1.0f; break;
}
return normalize(float3(x, y, z));
}
// Samples the source HDR image using bilinear interpolation (simplified to nearest neighbor for brevity here)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector3<TFloat, float> SampleEquirectangularMap(float* img, int w, int h, int c, Vector3<TFloat, float> dir)
private static Vector3<TFloat, float> SampleCubemap(float* img, int edge, int c, Vector3<TFloat, float> dir)
{
var uv = DirToEquirectangularUV(dir);
var absX = TFloat.Abs(dir.x);
var absY = TFloat.Abs(dir.y);
var absZ = TFloat.Abs(dir.z);
// Nearest neighbor pixel coordinates
var px = (uv.x * (w - 1.0f)).Cast<TInt, int>();
var py = (uv.y * (h - 1.0f)).Cast<TInt, int>();
var isXPos = dir.x >= TFloat.Zero;
var isYPos = dir.y >= TFloat.Zero;
var isZPos = dir.z >= TFloat.Zero;
// Clamp
px = TInt.Clamp(px, TInt.Zero, w - 1);
py = TInt.Clamp(py, TInt.Zero, h - 1);
var maxAxis = TFloat.Max(TFloat.Max(absX, absY), absZ);
// Assuming float RGB array format
var idx = (py * w + px) * c;
var faceIndexF = TFloat.Select(maxAxis == absX,
TFloat.Select(isXPos, 0.0f, 1.0f),
TFloat.Select(maxAxis == absY,
TFloat.Select(isYPos, 2.0f, 3.0f),
TFloat.Select(isZPos, 4.0f, 5.0f)));
var faceIndex = faceIndexF.Cast<TInt, int>();
var sc = TFloat.Select(maxAxis == absX,
TFloat.Select(isXPos, -dir.z, dir.z),
TFloat.Select(maxAxis == absY,
dir.x,
TFloat.Select(isZPos, dir.x, -dir.x)));
var tc = TFloat.Select(maxAxis == absX,
dir.y,
TFloat.Select(maxAxis == absY,
TFloat.Select(isYPos, -dir.z, dir.z),
dir.y));
var u = 0.5f * (sc / maxAxis + 1.0f);
var v = 0.5f * (1.0f - tc / maxAxis);
var px = (u * (edge - 1.0f)).Cast<TInt, int>();
var py = (v * (edge - 1.0f)).Cast<TInt, int>();
px = TInt.Clamp(px, TInt.Zero, edge - 1);
py = TInt.Clamp(py, TInt.Zero, edge - 1);
var faceOffset = faceIndex * (edge * edge);
var idx = (faceOffset + py * edge + px) * c;
return MathV.GatherVector3<TFloat, float>(img, idx.GetUnsafePtr(), 1);
}
@@ -115,22 +152,20 @@ internal static partial class TextureProcessor
var pLevel = &pMipLevels[m];
var w = pLevel->width;
var h = pLevel->height;
var pData = pLevel->data;
var data = pLevel->data;
var local_i = loopIndex - pLevel->offset;
var x = local_i % w;
var y = local_i / w;
var u = (float)x / (w - 1);
var v = (float)y / (h - 1);
var phi = (u - 0.5f) * 2.0f * PI;
var theta = (v - 0.5f) * PI;
var faceArea = w * w;
var face = local_i / faceArea;
var face_local_i = local_i % faceArea;
var x = face_local_i % w;
var y = face_local_i / w;
sincos(theta, out var sinTheta, out var cosTheta);
sincos(phi, out var sinPhi, out var cosPhi);
var N = float3(cosTheta * cosPhi, sinTheta, cosTheta * sinPhi);
N = normalize(N);
var u = (x + 0.5f) / w;
var v = (y + 0.5f) / w;
var N = CubemapUVToDir(face, u, v);
// For split-sum, we assume View and Reflection directions equal the Normal
var V = N;
@@ -173,7 +208,7 @@ internal static partial class TextureProcessor
L = MathV.Normalize(L);
var NdotL = TFloat.Max(MathV.Dot(vN, L), TFloat.Zero);
var sampleColor = SampleEquirectangularMap(pImage, imageWidth, imageHeight, channelCount, L);
var sampleColor = SampleCubemap(pImage, imageWidth, channelCount, L);
NdotL &= validLaneMask;
@@ -208,13 +243,13 @@ internal static partial class TextureProcessor
}
// Write to output mip array
var out_idx = (y * w + x) * channelCount;
pData[out_idx] = prefilteredColor.x;
pData[out_idx + 1] = prefilteredColor.y;
pData[out_idx + 2] = prefilteredColor.z;
var out_idx = (face * (w * w) + y * w + x) * channelCount;
data[out_idx] = prefilteredColor.x;
data[out_idx + 1] = prefilteredColor.y;
data[out_idx + 2] = prefilteredColor.z;
if (channelCount == 4)
{
pData[out_idx + 3] = 1.0f;
data[out_idx + 3] = 1.0f;
}
}
}
@@ -255,7 +290,7 @@ internal static partial class TextureProcessor
return bits * 2.3283064365386963e-10f; // bits / 0x100000000
}
private static JobHandle GenerateMipHDRI(JobScheduler scheduler, TextureAssetHandler.TextureInfo textureInfo, int totalMipLevels, out UnsafeArray<MipLevel> mipLevels)
private static JobHandle GenerateMipHDRI(JobScheduler scheduler, TextureAssetHandler.TextureInfo textureInfo, UnsafeArray<float> baseCubeData, int edge, int totalMipLevels, out UnsafeArray<MipLevel> mipLevels)
{
Logger.DebugAssert(textureInfo.isHDR, "GenerateMipHDRI should only be called for HDR textures.");
Logger.DebugAssert(textureInfo.colorComponents >= 3, "Texture must have at least 3 color components for RGB.");
@@ -268,24 +303,23 @@ internal static partial class TextureProcessor
radicalInverse_VdCLut[i] = RadicalInverse_VdC(i);
}
int w, h;
int w;
var totalPixel = 0;
for (var i = 0; i < totalMipLevels; i++)
{
w = Math.Max(1, textureInfo.width >> i);
h = Math.Max(1, textureInfo.height >> i);
w = Math.Max(1, edge >> i);
mipLevels[i] = new MipLevel
{
data = new UnsafeArray<float>(w * h * textureInfo.colorComponents, AllocationHandle.FreeList),
data = new UnsafeArray<float>(w * w * 6 * textureInfo.colorComponents, AllocationHandle.FreeList),
width = w,
height = h,
height = w,
offset = totalPixel,
roughness = (float)i / (totalMipLevels - 1) // Linear roughness from 0 to 1 across mip levels
};
totalPixel += w * h;
totalPixel += w * w * 6;
}
JobHandle handle;
@@ -295,11 +329,11 @@ internal static partial class TextureProcessor
{
var job = new GGXMipGenerationJobSPMD<WideLane<float>, WideLane<int>>
{
pImage = (float*)textureInfo.pixelData,
pImage = (float*)baseCubeData.GetUnsafePtr(),
pMipLevels = (MipLevel*)mipLevels.GetUnsafePtr(),
pRadicalInverse_VdCLut = (float*)radicalInverse_VdCLut.GetUnsafePtr(),
imageWidth = textureInfo.width,
imageHeight = textureInfo.height,
imageWidth = edge,
imageHeight = edge,
numMipLevels = totalMipLevels,
channelCount = textureInfo.colorComponents,
};
@@ -310,11 +344,11 @@ internal static partial class TextureProcessor
{
var job = new GGXMipGenerationJobSPMD<ScalarLane<float>, ScalarLane<int>>
{
pImage = (float*)textureInfo.pixelData,
pImage = (float*)baseCubeData.GetUnsafePtr(),
pMipLevels = (MipLevel*)mipLevels.GetUnsafePtr(),
pRadicalInverse_VdCLut = (float*)radicalInverse_VdCLut.GetUnsafePtr(),
imageWidth = textureInfo.width,
imageHeight = textureInfo.height,
imageWidth = edge,
imageHeight = edge,
numMipLevels = totalMipLevels,
channelCount = textureInfo.colorComponents,
};

View File

@@ -8,12 +8,13 @@
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DefineConstants>$(DefineConstants);MHP_ENABLE_SAFETY_CHECKS</DefineConstants>
<DefineConstants>$(DefineConstants);MHP_ENABLE_SAFETY_CHECKS;MHP_ENABLE_MIMALLOC</DefineConstants>
<IsAotCompatible>True</IsAotCompatible>
<IsTrimmable>True</IsTrimmable>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DefineConstants>$(DefineConstants);MHP_ENABLE_MIMALLOC</DefineConstants>
<IsAotCompatible>True</IsAotCompatible>
<IsTrimmable>True</IsTrimmable>
</PropertyGroup>
@@ -28,6 +29,7 @@
<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.Mimalloc" Version="1.6.7.2" />
<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.26100.6" />
</ItemGroup>

View File

@@ -190,19 +190,18 @@ public static unsafe class MeshBuilder
/// </summary>
/// <param name="vertices">The vertex list.</param>
/// <param name="indices">The index list.</param>
public static void ComputeNormal(UnsafeList<Vertex> vertices, UnsafeList<uint> indices)
public static void ComputeNormal(Span<Vertex> vertices, Span<uint> indices)
{
if (!vertices.IsCreated || vertices.Count < 3
|| !indices.IsCreated || indices.Count < 3)
if (vertices.Length < 3 || indices.Length < 3)
{
return;
}
for (var i = 0; i < indices.Count; i += 3)
for (var i = 0; i < indices.Length; i += 3)
{
var i0 = indices[i];
var i1 = indices[i + 1];
var i2 = indices[i + 2];
var i0 = (int)indices[i];
var i1 = (int)indices[i + 1];
var i2 = (int)indices[i + 2];
var v0 = vertices[i0];
var v1 = vertices[i1];
@@ -217,7 +216,7 @@ public static unsafe class MeshBuilder
vertices[i2].normal.xyz += faceNormal;
}
for (var i = 0; i < vertices.Count; i++)
for (var i = 0; i < vertices.Length; i++)
{
vertices[i].normal = math.normalize(vertices[i].normal);
}
@@ -228,16 +227,16 @@ public static unsafe class MeshBuilder
/// </summary>
/// <param name="vertices">The vertex list.</param>
/// <param name="indices">The index list.</param>
public static void ComputeTangents(UnsafeList<Vertex> vertices, UnsafeList<uint> indices)
public static void ComputeTangents(Span<Vertex> vertices, Span<uint> indices)
{
using var scope = AllocationManager.CreateStackScope();
var bitangents = new UnsafeArray<float3>(vertices.Count, scope.AllocationHandle, AllocationOption.Clear);
var bitangents = new UnsafeArray<float3>(vertices.Length, scope.AllocationHandle, AllocationOption.Clear);
for (var i = 0; i < indices.Count; i += 3)
for (var i = 0; i < indices.Length; i += 3)
{
var i0 = indices[i];
var i1 = indices[i + 1];
var i2 = indices[i + 2];
var i0 = (int)indices[i];
var i1 = (int)indices[i + 1];
var i2 = (int)indices[i + 2];
var v0 = vertices[i0];
var v1 = vertices[i1];
@@ -258,7 +257,7 @@ public static unsafe class MeshBuilder
for (var j = 0; j < 3; j++)
{
var idx = indices[i + j];
var idx = (int)indices[i + j];
var t = vertices[idx].tangent;
vertices[idx].tangent.xyz = t.xyz + tangent.xyz;
@@ -266,7 +265,7 @@ public static unsafe class MeshBuilder
}
}
for (var i = 0; i < vertices.Count; i++)
for (var i = 0; i < vertices.Length; i++)
{
var n = vertices[i].normal;
var t = vertices[i].tangent.xyz;