diff --git a/src/Editor/Ghost.Editor.Core/Assets/MeshAssetHandler.cs b/src/Editor/Ghost.Editor.Core/Assets/MeshAssetHandler.cs index 3c32cd5..81a09cf 100644 --- a/src/Editor/Ghost.Editor.Core/Assets/MeshAssetHandler.cs +++ b/src/Editor/Ghost.Editor.Core/Assets/MeshAssetHandler.cs @@ -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 Children { get; set; } + public IReadOnlyCollection Children + { + get; set; + } = Array.Empty(); + ~MeshNode() { Dispose(false); @@ -45,15 +46,36 @@ public class MeshNode : IDisposable child.Dispose(); } + Parent = null; + Children = Array.Empty(); + Dispose(true); GC.SuppressFinalize(this); } } +/// +/// Describes one material partition within a unified vertex/index buffer. +/// +public struct MaterialPartInfo +{ + /// The material slot index (from ufbx face_material). + public int materialIndex; + /// Byte offset into the unified index buffer. + public int indexStart; + /// Number of indices belonging to this part. + public int indexCount; + /// Byte offset into the unified vertex buffer. + public int vertexStart; + /// Number of unique vertices belonging to this part. + public int vertexCount; +} + public class GeometryMeshNode : MeshNode { private UnsafeList _vertices; private UnsafeList _indices; + private UnsafeArray _materialParts; public UnsafeList Vertices { @@ -75,15 +97,21 @@ public class GeometryMeshNode : MeshNode } } - public int MaterialIndex + public UnsafeArray MaterialParts { - get; set; + get => _materialParts; + set + { + _materialParts.Dispose(); + _materialParts = value; + } } protected override void Dispose(bool disposing) { _vertices.Dispose(); _indices.Dispose(); + _materialParts.Dispose(); } } diff --git a/src/Editor/Ghost.Editor.Core/Assets/MeshProcessor.Import.cs b/src/Editor/Ghost.Editor.Core/Assets/MeshProcessor.Import.cs index 32c5b79..98f6b0a 100644 --- a/src/Editor/Ghost.Editor.Core/Assets/MeshProcessor.Import.cs +++ b/src/Editor/Ghost.Editor.Core/Assets/MeshProcessor.Import.cs @@ -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 vertices; + public UnsafeList 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> _taskCompletionSource; - public UnsafeList vertices; - public UnsafeList indices; - - public Task> Task => _taskCompletionSource.Task; + public readonly Task> 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 ParseGeometry(ufbx_mesh* pMesh) + private GeometryMeshNode? ParseGeometry(ufbx_mesh* pMesh) { - var resultNodes = new List(); - if (pMesh->num_faces == 0) { - return resultNodes; + return null; } var numMaterials = pMesh->materials.count > 0 ? (int)pMesh->materials.count : 1; - var materialBuckets = new UnsafeList[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>(numMaterials, AllocationHandle.FreeList); + using var missingNormalsBucket = new UnsafeArray(numMaterials, AllocationHandle.FreeList); + using var missingTangentsBucket = new UnsafeArray(numMaterials, AllocationHandle.FreeList); + + for (var i = 0; i < numMaterials; i++) { materialBuckets[i] = new UnsafeList(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(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((int)numUniqueVertices, _allocationHandle); - var nodeIndices = new UnsafeList((int)numIndices, _allocationHandle); + // Allocate temporary per-part buffers (will be merged then disposed) + var partVertices = new UnsafeList((int)numUniqueVertices, AllocationHandle.FreeList); + var partIndices = new UnsafeList((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(), - 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(totalVertexCount, _allocationHandle); + var mergedIndices = new UnsafeList(totalIndexCount, _allocationHandle); + var materialParts = new UnsafeArray(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.Success(rootNode)); + _taskCompletionSource.SetResult(Result.Success(rootNode)); } } diff --git a/src/Editor/Ghost.Editor.Core/Assets/MeshProcessor.Meshlet.cs b/src/Editor/Ghost.Editor.Core/Assets/MeshProcessor.Meshlet.cs index 47c6572..1d701a4 100644 --- a/src/Editor/Ghost.Editor.Core/Assets/MeshProcessor.Meshlet.cs +++ b/src/Editor/Ghost.Editor.Core/Assets/MeshProcessor.Meshlet.cs @@ -701,70 +701,6 @@ public static unsafe partial class MeshProcessor public int materialIndex; } - public static void BuildMeshlets(MeshletMeshData* pMeshletData, ReadOnlyUnsafeCollection vertices, ReadOnlyUnsafeCollection 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 clusters) { var context = (MeshletContext*)contextPtr; @@ -829,6 +765,91 @@ public static unsafe partial class MeshProcessor return 0; } + /// + /// Builds meshlets for a unified multi-material mesh. + /// Each describes a material partition's index range within the unified buffer. + /// Meshlets are built per-part and tagged with the corresponding localMaterialIndex. + /// + public static void BuildMeshlets(MeshletMeshData* pMeshletData, ReadOnlyUnsafeCollection vertices, ReadOnlyUnsafeCollection indices, ReadOnlySpan 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. diff --git a/src/Editor/Ghost.Editor.Core/Assets/TextureProcessor.Compress.cs b/src/Editor/Ghost.Editor.Core/Assets/TextureProcessor.Compress.cs index aa405eb..b350322 100644 --- a/src/Editor/Ghost.Editor.Core/Assets/TextureProcessor.Compress.cs +++ b/src/Editor/Ghost.Editor.Core/Assets/TextureProcessor.Compress.cs @@ -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.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.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(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 baseCubeData; unsafe { using var cubeSurface0 = new DisposablePtr(NvttCubeSurface.Create()); using var mip0Surf = new DisposablePtr(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(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(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); diff --git a/src/Editor/Ghost.Editor.Core/Assets/TextureProcessor.GGXMip.cs b/src/Editor/Ghost.Editor.Core/Assets/TextureProcessor.GGXMip.cs index 8ef420a..ff9f534 100644 --- a/src/Editor/Ghost.Editor.Core/Assets/TextureProcessor.GGXMip.cs +++ b/src/Editor/Ghost.Editor.Core/Assets/TextureProcessor.GGXMip.cs @@ -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 DirToEquirectangularUV(Vector3 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(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 SampleEquirectangularMap(float* img, int w, int h, int c, Vector3 dir) + private static Vector3 SampleCubemap(float* img, int edge, int c, Vector3 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(); - var py = (uv.y * (h - 1.0f)).Cast(); + 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(); + + 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(); + var py = (v * (edge - 1.0f)).Cast(); + + 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(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 mipLevels) + private static JobHandle GenerateMipHDRI(JobScheduler scheduler, TextureAssetHandler.TextureInfo textureInfo, UnsafeArray baseCubeData, int edge, int totalMipLevels, out UnsafeArray 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(w * h * textureInfo.colorComponents, AllocationHandle.FreeList), + data = new UnsafeArray(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> { - 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> { - 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, }; diff --git a/src/Runtime/Ghost.Core/Ghost.Core.csproj b/src/Runtime/Ghost.Core/Ghost.Core.csproj index 192f2f4..897cc11 100644 --- a/src/Runtime/Ghost.Core/Ghost.Core.csproj +++ b/src/Runtime/Ghost.Core/Ghost.Core.csproj @@ -8,12 +8,13 @@ - $(DefineConstants);MHP_ENABLE_SAFETY_CHECKS + $(DefineConstants);MHP_ENABLE_SAFETY_CHECKS;MHP_ENABLE_MIMALLOC True True + $(DefineConstants);MHP_ENABLE_MIMALLOC True True @@ -28,6 +29,7 @@ + diff --git a/src/Runtime/Ghost.Graphics/Utilities/MeshBuilder.cs b/src/Runtime/Ghost.Graphics/Utilities/MeshBuilder.cs index 98a9c52..2de25be 100644 --- a/src/Runtime/Ghost.Graphics/Utilities/MeshBuilder.cs +++ b/src/Runtime/Ghost.Graphics/Utilities/MeshBuilder.cs @@ -190,19 +190,18 @@ public static unsafe class MeshBuilder /// /// The vertex list. /// The index list. - public static void ComputeNormal(UnsafeList vertices, UnsafeList indices) + public static void ComputeNormal(Span vertices, Span 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 /// /// The vertex list. /// The index list. - public static void ComputeTangents(UnsafeList vertices, UnsafeList indices) + public static void ComputeTangents(Span vertices, Span indices) { using var scope = AllocationManager.CreateStackScope(); - var bitangents = new UnsafeArray(vertices.Count, scope.AllocationHandle, AllocationOption.Clear); + var bitangents = new UnsafeArray(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;