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:
@@ -159,18 +159,27 @@ public static class DSLShaderCompiler
|
|||||||
public static Result<GraphicsShaderDescriptor> CompileGraphicsShader(Stream stream)
|
public static Result<GraphicsShaderDescriptor> CompileGraphicsShader(Stream stream)
|
||||||
{
|
{
|
||||||
using var reader = new StreamReader(stream);
|
using var reader = new StreamReader(stream);
|
||||||
return CompileGraphicsShader(reader.ReadToEnd());
|
return CompileGraphicsShaderCode(reader.ReadToEnd());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Result<GraphicsShaderDescriptor> CompileGraphicsShader(string shaderPath)
|
public static Result<GraphicsShaderDescriptor> CompileGraphicsShader(string shaderPath)
|
||||||
|
{
|
||||||
|
if (!File.Exists(shaderPath))
|
||||||
|
{
|
||||||
|
return Result.Failure("Shader file not found: " + shaderPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
var code = File.ReadAllText(shaderPath);
|
||||||
|
return CompileGraphicsShaderCode(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Result<GraphicsShaderDescriptor> CompileGraphicsShaderCode(string shaderCode)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var source = File.ReadAllText(shaderPath);
|
|
||||||
|
|
||||||
// Use ANTLR4 parser
|
// Use ANTLR4 parser
|
||||||
var parseErrors = new List<DSLShaderError>();
|
var parseErrors = new List<DSLShaderError>();
|
||||||
var shaderModels = AntlrShaderCompiler.ParseShaders(source, parseErrors);
|
var shaderModels = AntlrShaderCompiler.ParseShaders(shaderCode, parseErrors);
|
||||||
|
|
||||||
if (parseErrors.Count != 0)
|
if (parseErrors.Count != 0)
|
||||||
{
|
{
|
||||||
@@ -219,17 +228,26 @@ public static class DSLShaderCompiler
|
|||||||
public static Result<ComputeShaderDescriptor> CompileComputeShader(Stream stream)
|
public static Result<ComputeShaderDescriptor> CompileComputeShader(Stream stream)
|
||||||
{
|
{
|
||||||
using var reader = new StreamReader(stream);
|
using var reader = new StreamReader(stream);
|
||||||
return CompileComputeShader(reader.ReadToEnd());
|
return CompileComputeShaderCode(reader.ReadToEnd());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Result<ComputeShaderDescriptor> CompileComputeShader(string shaderPath)
|
public static Result<ComputeShaderDescriptor> CompileComputeShader(string shaderPath)
|
||||||
|
{
|
||||||
|
if (!File.Exists(shaderPath))
|
||||||
|
{
|
||||||
|
return Result.Failure("Shader file not found: " + shaderPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
var code = File.ReadAllText(shaderPath);
|
||||||
|
return CompileComputeShaderCode(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Result<ComputeShaderDescriptor> CompileComputeShaderCode(string shaderCode)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var source = File.ReadAllText(shaderPath);
|
|
||||||
|
|
||||||
var parseErrors = new List<DSLShaderError>();
|
var parseErrors = new List<DSLShaderError>();
|
||||||
var shaderModels = AntlrShaderCompiler.ParseComputeShaders(source, parseErrors);
|
var shaderModels = AntlrShaderCompiler.ParseComputeShaders(shaderCode, parseErrors);
|
||||||
|
|
||||||
if (parseErrors.Count != 0)
|
if (parseErrors.Count != 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,380 +0,0 @@
|
|||||||
using Ghost.Core;
|
|
||||||
using Ghost.Engine;
|
|
||||||
using Ghost.Graphics.RHI;
|
|
||||||
using Ghost.Graphics.Utilities;
|
|
||||||
using Ghost.MeshOptimizer;
|
|
||||||
using Ghost.Ufbx;
|
|
||||||
using Misaki.HighPerformance.LowLevel;
|
|
||||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
|
||||||
using Misaki.HighPerformance.LowLevel.Collections;
|
|
||||||
using Misaki.HighPerformance.LowLevel.Utilities;
|
|
||||||
using Misaki.HighPerformance.Mathematics;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.AssetHandler;
|
|
||||||
|
|
||||||
public abstract class MeshAsset : IAsset
|
|
||||||
{
|
|
||||||
|
|
||||||
private UnsafeList<Vertex> _vertices;
|
|
||||||
private UnsafeList<uint> _indices;
|
|
||||||
|
|
||||||
public Guid ID
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IAssetSettings Settings
|
|
||||||
{
|
|
||||||
get;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Guid TypeID => typeof(MeshAsset).GUID;
|
|
||||||
|
|
||||||
public Span<Vertex> Vertices => _vertices.AsSpan();
|
|
||||||
public Span<uint> Indices => _indices.AsSpan();
|
|
||||||
|
|
||||||
internal MeshAsset(ref UnsafeList<Vertex> vertices, ref UnsafeList<uint> indices, Guid id, MeshAssetSettings settings)
|
|
||||||
{
|
|
||||||
_vertices = vertices;
|
|
||||||
_indices = indices;
|
|
||||||
|
|
||||||
ID = id;
|
|
||||||
Settings = settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_vertices.Dispose();
|
|
||||||
_indices.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Guid(GUID)]
|
|
||||||
public partial class FBXAsset : MeshAsset
|
|
||||||
{
|
|
||||||
public const string GUID = "B99CA68E-EE7A-4822-BF1C-AA0A5120C36A";
|
|
||||||
|
|
||||||
internal FBXAsset(ref UnsafeList<Vertex> vertices, ref UnsafeList<uint> indices, Guid id, FbxAssetSettings settings)
|
|
||||||
: base(ref vertices, ref indices, id, settings)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public enum CoordinateAxis
|
|
||||||
{
|
|
||||||
PositiveX,
|
|
||||||
PositiveY,
|
|
||||||
PositiveZ,
|
|
||||||
NegativeX,
|
|
||||||
NegativeY,
|
|
||||||
NegativeZ
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum VertexDataSource
|
|
||||||
{
|
|
||||||
Imported,
|
|
||||||
Computed,
|
|
||||||
ComputedIfMissing
|
|
||||||
}
|
|
||||||
|
|
||||||
public class MeshAssetSettings : IAssetSettings
|
|
||||||
{
|
|
||||||
public VertexDataSource NormalDataSource
|
|
||||||
{
|
|
||||||
get; set;
|
|
||||||
} = VertexDataSource.ComputedIfMissing;
|
|
||||||
|
|
||||||
public VertexDataSource TangentDataSource
|
|
||||||
{
|
|
||||||
get; set;
|
|
||||||
} = VertexDataSource.ComputedIfMissing;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class ObjAssetSettings : MeshAssetSettings
|
|
||||||
{
|
|
||||||
public CoordinateAxis ObjectUpAxis
|
|
||||||
{
|
|
||||||
get; set;
|
|
||||||
} = CoordinateAxis.PositiveY;
|
|
||||||
|
|
||||||
public CoordinateAxis ObjectForwardAxis
|
|
||||||
{
|
|
||||||
get; set;
|
|
||||||
} = CoordinateAxis.NegativeZ;
|
|
||||||
|
|
||||||
public CoordinateAxis ObjectRightAxis
|
|
||||||
{
|
|
||||||
get; set;
|
|
||||||
} = CoordinateAxis.PositiveX;
|
|
||||||
|
|
||||||
public float UnitMeterScale
|
|
||||||
{
|
|
||||||
get; set;
|
|
||||||
} = 1.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class FbxAssetSettings : MeshAssetSettings
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class MeshParsingWorkItem : IThreadPoolWorkItem
|
|
||||||
{
|
|
||||||
private readonly string _filePath;
|
|
||||||
private readonly AllocationHandle _allocationHandle;
|
|
||||||
private readonly MeshAssetSettings _settings;
|
|
||||||
private readonly TaskCompletionSource<Result> _taskCompletionSource;
|
|
||||||
|
|
||||||
public UnsafeList<Vertex> vertices;
|
|
||||||
public UnsafeList<uint> indices;
|
|
||||||
|
|
||||||
public Task<Result> Task => _taskCompletionSource.Task;
|
|
||||||
|
|
||||||
public MeshParsingWorkItem(string filePath, AllocationHandle allocationHandle, MeshAssetSettings settings)
|
|
||||||
{
|
|
||||||
_filePath = filePath;
|
|
||||||
_allocationHandle = allocationHandle;
|
|
||||||
_settings = settings;
|
|
||||||
_taskCompletionSource = new TaskCompletionSource<Result>();
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
private static float4 ComputeTangent(float3 t, float3 n, float3 b)
|
|
||||||
{
|
|
||||||
var proj = n * math.dot(n, t);
|
|
||||||
t = math.normalize(t - proj);
|
|
||||||
var w = math.dot(math.cross(n, t), b) < 0.0f ? -1.0f : 1.0f;
|
|
||||||
return new float4(t.xyz, w);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ufbx_coordinate_axis ToUfbxCoordinateAxis(CoordinateAxis axis)
|
|
||||||
{
|
|
||||||
return axis switch
|
|
||||||
{
|
|
||||||
CoordinateAxis.PositiveX => ufbx_coordinate_axis.UFBX_COORDINATE_AXIS_POSITIVE_X,
|
|
||||||
CoordinateAxis.PositiveY => ufbx_coordinate_axis.UFBX_COORDINATE_AXIS_POSITIVE_Y,
|
|
||||||
CoordinateAxis.PositiveZ => ufbx_coordinate_axis.UFBX_COORDINATE_AXIS_POSITIVE_Z,
|
|
||||||
CoordinateAxis.NegativeX => ufbx_coordinate_axis.UFBX_COORDINATE_AXIS_NEGATIVE_X,
|
|
||||||
CoordinateAxis.NegativeY => ufbx_coordinate_axis.UFBX_COORDINATE_AXIS_NEGATIVE_Y,
|
|
||||||
CoordinateAxis.NegativeZ => ufbx_coordinate_axis.UFBX_COORDINATE_AXIS_NEGATIVE_Z,
|
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(axis), axis, null)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public unsafe void Execute()
|
|
||||||
{
|
|
||||||
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
|
|
||||||
{
|
|
||||||
target_unit_meters = 1.0f,
|
|
||||||
target_axes = ufbx_coordinate_axes.left_handed_y_up,
|
|
||||||
// Force z-axis mirroring to correctly convert handedness to Left-Handed,
|
|
||||||
// while preserving correct left/right orientation when viewed from the front.
|
|
||||||
handedness_conversion_axis = ufbx_mirror_axis.UFBX_MIRROR_AXIS_Z,
|
|
||||||
space_conversion = ufbx_space_conversion.UFBX_SPACE_CONVERSION_MODIFY_GEOMETRY,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (_settings is ObjAssetSettings objSettings)
|
|
||||||
{
|
|
||||||
load_Opts.obj_axes = new ufbx_coordinate_axes
|
|
||||||
{
|
|
||||||
right = ToUfbxCoordinateAxis(objSettings.ObjectRightAxis),
|
|
||||||
up = ToUfbxCoordinateAxis(objSettings.ObjectUpAxis),
|
|
||||||
front = ToUfbxCoordinateAxis(objSettings.ObjectForwardAxis)
|
|
||||||
};
|
|
||||||
|
|
||||||
load_Opts.obj_unit_meters = objSettings.UnitMeterScale;
|
|
||||||
load_Opts.obj_search_mtl_by_filename = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var str = new UnsafeArray<byte>(Encoding.UTF8.GetByteCount(_filePath) + 1, AllocationHandle.FreeList);
|
|
||||||
var count = Encoding.UTF8.GetBytes(_filePath, str.AsSpan());
|
|
||||||
str[count] = 0;
|
|
||||||
|
|
||||||
using var scene = new DisposablePtr<ufbx_scene>(ufbx_scene.LoadFile((sbyte*)str.GetUnsafePtr(), &load_Opts, &error));
|
|
||||||
if (scene.Get() == null)
|
|
||||||
{
|
|
||||||
_taskCompletionSource.SetResult(Result.Failure(error.description.ToString()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
using var flatVertices = new UnsafeList<Vertex>(1024, AllocationHandle.FreeList);
|
|
||||||
|
|
||||||
var missingNormals = false;
|
|
||||||
var missingTangents = false;
|
|
||||||
|
|
||||||
for (var i = 0u; i < scene.Get()->nodes.count; i++)
|
|
||||||
{
|
|
||||||
var data = scene.Get()->nodes.data;
|
|
||||||
var node = scene.Get()->nodes.data[i];
|
|
||||||
if (node->is_root)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node->mesh != null)
|
|
||||||
{
|
|
||||||
var pMesh = node->mesh;
|
|
||||||
if (pMesh->num_faces == 0)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var maxScratchIndices = (int)(pMesh->max_face_triangles * 3u);
|
|
||||||
|
|
||||||
using var triIndicesArray = new UnsafeArray<uint>(maxScratchIndices, AllocationHandle.FreeList);
|
|
||||||
|
|
||||||
for (var j = 0u; j < pMesh->num_faces; j++)
|
|
||||||
{
|
|
||||||
var face = pMesh->faces.data[j];
|
|
||||||
|
|
||||||
var numTris = UfbxApi.TriangulateFace(triIndicesArray.AsSpan(0, maxScratchIndices), pMesh, face);
|
|
||||||
|
|
||||||
var totalIndices = numTris * 3;
|
|
||||||
for (var k = 0; k < totalIndices; k++)
|
|
||||||
{
|
|
||||||
var ufbxTopologyIndex = triIndicesArray[k];
|
|
||||||
|
|
||||||
var posIdx = pMesh->vertex_position.indices.data[ufbxTopologyIndex];
|
|
||||||
var normIdx = pMesh->vertex_normal.exists ? pMesh->vertex_normal.indices.data[ufbxTopologyIndex] : uint.MaxValue;
|
|
||||||
var tanIdx = pMesh->vertex_tangent.exists ? pMesh->vertex_tangent.indices.data[ufbxTopologyIndex] : uint.MaxValue;
|
|
||||||
var uvIdx = pMesh->vertex_uv.exists ? pMesh->vertex_uv.indices.data[ufbxTopologyIndex] : uint.MaxValue;
|
|
||||||
var colIdx = pMesh->vertex_color.exists ? pMesh->vertex_color.indices.data[ufbxTopologyIndex] : uint.MaxValue;
|
|
||||||
var btanIdx = pMesh->vertex_bitangent.exists ? pMesh->vertex_bitangent.indices.data[ufbxTopologyIndex] : uint.MaxValue;
|
|
||||||
|
|
||||||
var position = pMesh->vertex_position.values.data[posIdx];
|
|
||||||
var normal = normIdx != uint.MaxValue ? pMesh->vertex_normal.values.data[normIdx] : default;
|
|
||||||
var uv = uvIdx != uint.MaxValue ? pMesh->vertex_uv.values.data[uvIdx] : default;
|
|
||||||
var color = colIdx != uint.MaxValue ? pMesh->vertex_color.values.data[colIdx] : default;
|
|
||||||
|
|
||||||
var vertex = new Vertex
|
|
||||||
{
|
|
||||||
position = new float3(position.x, position.y, position.z),
|
|
||||||
normal = new float3(normal.x, normal.y, normal.z),
|
|
||||||
uv = new float2(uv.x, uv.y),
|
|
||||||
color = new Color128(color.x, color.y, color.z, color.w)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (tanIdx != uint.MaxValue)
|
|
||||||
{
|
|
||||||
var mt = pMesh->vertex_tangent.values.data[tanIdx];
|
|
||||||
var mb = btanIdx != uint.MaxValue ? pMesh->vertex_bitangent.values.data[btanIdx] : default;
|
|
||||||
|
|
||||||
var t = new float3(mt.x, mt.y, mt.z);
|
|
||||||
var n = vertex.normal;
|
|
||||||
var b = btanIdx != uint.MaxValue ? new float3(mb.x, mb.y, mb.z) : math.cross(n, t);
|
|
||||||
vertex.tangent = ComputeTangent(t, n, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
var newIndex = (uint)flatVertices.Count;
|
|
||||||
|
|
||||||
flatVertices.Add(vertex);
|
|
||||||
|
|
||||||
if (!missingNormals)
|
|
||||||
{
|
|
||||||
missingNormals = normIdx == uint.MaxValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!missingTangents)
|
|
||||||
{
|
|
||||||
missingTangents = tanIdx == uint.MaxValue || btanIdx == uint.MaxValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var numIndices = (uint)flatVertices.Count;
|
|
||||||
|
|
||||||
using var weldedIndices = new UnsafeArray<uint>((int)numIndices, AllocationHandle.FreeList);
|
|
||||||
using var cachedIndices = new UnsafeArray<uint>((int)numIndices, AllocationHandle.FreeList);
|
|
||||||
|
|
||||||
var stream = new ufbx_vertex_stream
|
|
||||||
{
|
|
||||||
data = flatVertices.GetUnsafePtr(),
|
|
||||||
vertex_count = numIndices,
|
|
||||||
vertex_size = (nuint)sizeof(Vertex)
|
|
||||||
};
|
|
||||||
|
|
||||||
var numUniqueVertices = UfbxApi.GenerateIndices([stream], weldedIndices, null, &error);
|
|
||||||
if (numUniqueVertices == 0 && error.type != ufbx_error_type.UFBX_ERROR_NONE)
|
|
||||||
{
|
|
||||||
_taskCompletionSource.SetResult(Result.Failure($"Welding failed: {error.description}"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MeshOptApi.OptimizeVertexCache((uint*)cachedIndices.GetUnsafePtr(), (uint*)weldedIndices.GetUnsafePtr(), numIndices, numUniqueVertices);
|
|
||||||
|
|
||||||
vertices = new UnsafeList<Vertex>((int)numUniqueVertices, _allocationHandle);
|
|
||||||
indices = new UnsafeList<uint>((int)numIndices, _allocationHandle);
|
|
||||||
|
|
||||||
var finalVertexCount = MeshOptApi.OptimizeVertexFetch(vertices.GetUnsafePtr(), (uint*)cachedIndices.GetUnsafePtr(), numIndices, flatVertices.GetUnsafePtr(), numIndices, (nuint)sizeof(Vertex));
|
|
||||||
|
|
||||||
vertices.UnsafeSetCount((int)finalVertexCount);
|
|
||||||
|
|
||||||
MemoryUtility.MemCpy(indices.GetUnsafePtr(), cachedIndices.GetUnsafePtr(), numIndices * sizeof(uint));
|
|
||||||
indices.UnsafeSetCount((int)numIndices);
|
|
||||||
|
|
||||||
if (_settings.NormalDataSource == VertexDataSource.Computed || (_settings.NormalDataSource == VertexDataSource.ComputedIfMissing && missingNormals))
|
|
||||||
{
|
|
||||||
MeshBuilder.ComputeNormal(vertices, indices);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_settings.TangentDataSource == VertexDataSource.Computed || (_settings.TangentDataSource == VertexDataSource.ComputedIfMissing && missingTangents))
|
|
||||||
{
|
|
||||||
MeshBuilder.ComputeTangents(vertices, indices);
|
|
||||||
}
|
|
||||||
|
|
||||||
_taskCompletionSource.SetResult(Result.Success());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class FBXAssetHandler : IImportableAssetHandler
|
|
||||||
{
|
|
||||||
public AssetType RuntimeAssetType => AssetType.Mesh;
|
|
||||||
|
|
||||||
public Guid EditorAssetTypeID => typeof(FBXAsset).GUID;
|
|
||||||
|
|
||||||
public bool CanExport => false;
|
|
||||||
|
|
||||||
public IAssetSettings? CreateDefaultSettings()
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ValueTask<Result<IAsset>> LoadAssetAsync(FileStream assetStream, Guid id, IAssetSettings? settings, CancellationToken token = default)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ValueTask<Result> SaveAssetAsync(FileStream targetStream, IAsset asset, CancellationToken token = default)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ValueTask<Result> ImportAsync(FileStream sourceStream, FileStream targetStream, Guid id, IAssetSettings? settings, CancellationToken token = default)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ValueTask<Result> ExportAsync(FileStream assetStream, FileStream targetStream, IAssetExportOptions? options, CancellationToken token = default)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using Ghost.Core;
|
using Ghost.Core;
|
||||||
using Ghost.Engine;
|
using Ghost.Engine;
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.AssetHandler;
|
namespace Ghost.Editor.Core.Assets;
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Class)]
|
[AttributeUsage(AttributeTargets.Class)]
|
||||||
public sealed class CustomAssetHandlerAttribute : Attribute
|
public sealed class CustomAssetHandlerAttribute : Attribute
|
||||||
@@ -23,7 +23,7 @@ public interface IAsset : IDisposable
|
|||||||
get;
|
get;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IAssetSettings Settings
|
public IAssetSettings? Settings
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
}
|
}
|
||||||
@@ -38,18 +38,18 @@ public interface IAssetHandler
|
|||||||
|
|
||||||
IAssetSettings? CreateDefaultSettings();
|
IAssetSettings? CreateDefaultSettings();
|
||||||
|
|
||||||
ValueTask<Result<IAsset>> LoadAssetAsync(FileStream assetStream, Guid id, IAssetSettings? settings, CancellationToken token = default);
|
ValueTask<Result<IAsset>> LoadAssetAsync(string assetPath, Guid id, IAssetSettings? settings, CancellationToken token = default);
|
||||||
ValueTask<Result> SaveAssetAsync(FileStream targetStream, IAsset asset, CancellationToken token = default);
|
ValueTask<Result> SaveAssetAsync(string targetPath, IAsset asset, CancellationToken token = default);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IImportableAssetHandler : IAssetHandler
|
public interface IImportableAssetHandler : IAssetHandler
|
||||||
{
|
{
|
||||||
bool CanExport { get; }
|
bool CanExport { get; }
|
||||||
ValueTask<Result> ImportAsync(FileStream sourceStream, FileStream targetStream, Guid id, IAssetSettings? settings, CancellationToken token = default);
|
ValueTask<Result> ImportAsync(string sourcePath, string targetPath, Guid id, IAssetSettings? settings, CancellationToken token = default);
|
||||||
ValueTask<Result> ExportAsync(FileStream assetStream, FileStream targetStream, IAssetExportOptions? options, CancellationToken token = default);
|
ValueTask<Result> ExportAsync(string assetPath, string targetPath, IAssetExportOptions? options, CancellationToken token = default);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IPackableAssetHandler : IAssetHandler
|
public interface IPackableAssetHandler : IAssetHandler
|
||||||
{
|
{
|
||||||
ValueTask<Result> PackAsync(FileStream assetStream, Stream targetStream, CancellationToken token = default);
|
ValueTask<Result> PackAsync(string assetPath, MemoryStream targetStream, CancellationToken token = default);
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using Ghost.Engine;
|
using Ghost.Engine;
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.AssetHandler;
|
namespace Ghost.Editor.Core.Assets;
|
||||||
|
|
||||||
public static class AssetHandlerRegistry
|
public static class AssetHandlerRegistry
|
||||||
{
|
{
|
||||||
@@ -9,12 +9,16 @@ public static class AssetHandlerRegistry
|
|||||||
private static readonly Dictionary<Guid, IAssetHandler> s_byTypeId;
|
private static readonly Dictionary<Guid, IAssetHandler> s_byTypeId;
|
||||||
private static readonly Dictionary<Guid, int> s_versionByTypeId;
|
private static readonly Dictionary<Guid, int> s_versionByTypeId;
|
||||||
|
|
||||||
|
private static readonly List<(Type Type, string Name)> s_iAssetSettingsTypes;
|
||||||
|
|
||||||
static AssetHandlerRegistry()
|
static AssetHandlerRegistry()
|
||||||
{
|
{
|
||||||
s_byExtension = new Dictionary<string, IAssetHandler>(StringComparer.OrdinalIgnoreCase);
|
s_byExtension = new Dictionary<string, IAssetHandler>(StringComparer.OrdinalIgnoreCase);
|
||||||
s_typeByExtension = new Dictionary<string, AssetType>(StringComparer.OrdinalIgnoreCase);
|
s_typeByExtension = new Dictionary<string, AssetType>(StringComparer.OrdinalIgnoreCase);
|
||||||
s_byTypeId = new Dictionary<Guid, IAssetHandler>();
|
s_byTypeId = new Dictionary<Guid, IAssetHandler>();
|
||||||
s_versionByTypeId = new Dictionary<Guid, int>();
|
s_versionByTypeId = new Dictionary<Guid, int>();
|
||||||
|
|
||||||
|
s_iAssetSettingsTypes = new List<(Type Type, string Name)>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void RegisterHandler(IAssetHandler handler, Guid assetTypeId, ReadOnlySpan<string> extensions, int version)
|
public static void RegisterHandler(IAssetHandler handler, Guid assetTypeId, ReadOnlySpan<string> extensions, int version)
|
||||||
@@ -30,6 +34,11 @@ public static class AssetHandlerRegistry
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void RegisterIAssetSettingsType(Type type, string name)
|
||||||
|
{
|
||||||
|
s_iAssetSettingsTypes.Add((type, name));
|
||||||
|
}
|
||||||
|
|
||||||
public static IAssetHandler? GetByExtension(string extension)
|
public static IAssetHandler? GetByExtension(string extension)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(extension))
|
if (string.IsNullOrEmpty(extension))
|
||||||
@@ -69,4 +78,9 @@ public static class AssetHandlerRegistry
|
|||||||
var normalized = extension.StartsWith('.') ? extension : "." + extension;
|
var normalized = extension.StartsWith('.') ? extension : "." + extension;
|
||||||
return s_typeByExtension.GetValueOrDefault(normalized, AssetType.Unknown);
|
return s_typeByExtension.GetValueOrDefault(normalized, AssetType.Unknown);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public static IReadOnlyCollection<(Type Type, string Name)> GetIAssetSettingsTypes()
|
||||||
|
{
|
||||||
|
return s_iAssetSettingsTypes;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,16 @@
|
|||||||
using Ghost.Engine;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Text.Json.Serialization.Metadata;
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.AssetHandler;
|
namespace Ghost.Editor.Core.Assets;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Mark IAssetSettings for polymorphic serialization.
|
/// Mark IAssetSettings for polymorphic serialization.
|
||||||
/// Each handler type will register its own derived type.
|
/// Each handler type will register its own derived type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
|
|
||||||
[JsonDerivedType(typeof(DefaultAssetSettings), "Default")]
|
|
||||||
public interface IAssetSettings;
|
public interface IAssetSettings;
|
||||||
|
|
||||||
public sealed class DefaultAssetSettings : IAssetSettings;
|
internal sealed class DefaultAssetSettings : IAssetSettings;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Persisted as a JSON sidecar (.gmeta) next to every source asset.
|
/// Persisted as a JSON sidecar (.gmeta) next to every source asset.
|
||||||
@@ -76,9 +74,36 @@ internal static class AssetMetaIO
|
|||||||
WriteIndented = true,
|
WriteIndented = true,
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||||
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }
|
TypeInfoResolver = new DefaultJsonTypeInfoResolver
|
||||||
|
{
|
||||||
|
Modifiers = { ConfigureAssetSettingsPolymorphism }
|
||||||
|
},
|
||||||
|
Converters =
|
||||||
|
{
|
||||||
|
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase),
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static void ConfigureAssetSettingsPolymorphism(JsonTypeInfo typeInfo)
|
||||||
|
{
|
||||||
|
if (typeInfo.Type != typeof(IAssetSettings))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
typeInfo.PolymorphismOptions = new JsonPolymorphismOptions
|
||||||
|
{
|
||||||
|
TypeDiscriminatorPropertyName = "$type",
|
||||||
|
IgnoreUnrecognizedTypeDiscriminators = true,
|
||||||
|
UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var setting in AssetHandlerRegistry.GetIAssetSettingsTypes())
|
||||||
|
{
|
||||||
|
typeInfo.PolymorphismOptions.DerivedTypes.Add(new JsonDerivedType(setting.Type, setting.Name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static async ValueTask<AssetMeta?> ReadAsync(string metaPath, CancellationToken token = default)
|
public static async ValueTask<AssetMeta?> ReadAsync(string metaPath, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
if (!File.Exists(metaPath))
|
if (!File.Exists(metaPath))
|
||||||
248
src/Editor/Ghost.Editor.Core/Assets/MeshAssetHandler.cs
Normal file
248
src/Editor/Ghost.Editor.Core/Assets/MeshAssetHandler.cs
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
using Ghost.Core;
|
||||||
|
using Ghost.Engine;
|
||||||
|
using Ghost.Graphics.RHI;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Collections;
|
||||||
|
using Misaki.HighPerformance.Mathematics;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Ghost.Editor.Core.Assets;
|
||||||
|
|
||||||
|
public class MeshNode : IDisposable
|
||||||
|
{
|
||||||
|
public required string Name
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float4x4 LocalTransform
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MeshNode? Parent
|
||||||
|
{
|
||||||
|
get; init;
|
||||||
|
}
|
||||||
|
|
||||||
|
public required IReadOnlyCollection<MeshNode> Children
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
~MeshNode()
|
||||||
|
{
|
||||||
|
Dispose(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
foreach (var child in Children)
|
||||||
|
{
|
||||||
|
child.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Dispose(true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GeometryMeshNode : MeshNode
|
||||||
|
{
|
||||||
|
private UnsafeList<Vertex> _vertices;
|
||||||
|
private UnsafeList<uint> _indices;
|
||||||
|
|
||||||
|
public UnsafeList<Vertex> Vertices
|
||||||
|
{
|
||||||
|
get => _vertices;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_vertices.Dispose();
|
||||||
|
_vertices = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public UnsafeList<uint> Indices
|
||||||
|
{
|
||||||
|
get => _indices;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_indices.Dispose();
|
||||||
|
_indices = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int MaterialIndex
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
_vertices.Dispose();
|
||||||
|
_indices.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LightMeshNode : MeshNode
|
||||||
|
{
|
||||||
|
public float3 Color
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float Intensity
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class MeshAsset : IAsset
|
||||||
|
{
|
||||||
|
private MeshNode _root;
|
||||||
|
|
||||||
|
public Guid ID
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAssetSettings Settings
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Guid TypeID => typeof(MeshAsset).GUID;
|
||||||
|
|
||||||
|
public MeshNode Root
|
||||||
|
{
|
||||||
|
get => _root;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_root?.Dispose();
|
||||||
|
_root = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal MeshAsset(MeshNode root, Guid id, MeshAssetSettings settings)
|
||||||
|
{
|
||||||
|
_root = root;
|
||||||
|
|
||||||
|
ID = id;
|
||||||
|
Settings = settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_root?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Guid(GUID)]
|
||||||
|
public partial class FBXAsset : MeshAsset
|
||||||
|
{
|
||||||
|
public const string GUID = "B99CA68E-EE7A-4822-BF1C-AA0A5120C36A";
|
||||||
|
|
||||||
|
internal FBXAsset(MeshNode root, Guid id, FbxAssetSettings settings)
|
||||||
|
: base(root, id, settings)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum CoordinateAxis
|
||||||
|
{
|
||||||
|
PositiveX,
|
||||||
|
PositiveY,
|
||||||
|
PositiveZ,
|
||||||
|
NegativeX,
|
||||||
|
NegativeY,
|
||||||
|
NegativeZ
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum VertexDataSource
|
||||||
|
{
|
||||||
|
Imported,
|
||||||
|
Computed,
|
||||||
|
ComputedIfMissing
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MeshAssetSettings : IAssetSettings
|
||||||
|
{
|
||||||
|
public VertexDataSource NormalDataSource
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
} = VertexDataSource.ComputedIfMissing;
|
||||||
|
|
||||||
|
public VertexDataSource TangentDataSource
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
} = VertexDataSource.ComputedIfMissing;
|
||||||
|
|
||||||
|
public bool BuildMeshlets
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
} = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class ObjAssetSettings : MeshAssetSettings
|
||||||
|
{
|
||||||
|
public CoordinateAxis ObjectUpAxis
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
} = CoordinateAxis.PositiveY;
|
||||||
|
|
||||||
|
public CoordinateAxis ObjectForwardAxis
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
} = CoordinateAxis.NegativeZ;
|
||||||
|
|
||||||
|
public CoordinateAxis ObjectRightAxis
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
} = CoordinateAxis.PositiveX;
|
||||||
|
|
||||||
|
public float UnitMeterScale
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
} = 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class FbxAssetSettings : MeshAssetSettings
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class FBXAssetHandler : IImportableAssetHandler
|
||||||
|
{
|
||||||
|
public AssetType RuntimeAssetType => AssetType.Mesh;
|
||||||
|
|
||||||
|
public Guid EditorAssetTypeID => typeof(FBXAsset).GUID;
|
||||||
|
|
||||||
|
public bool CanExport => false;
|
||||||
|
|
||||||
|
public IAssetSettings? CreateDefaultSettings()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask<Result<IAsset>> LoadAssetAsync(string assetPath, Guid id, IAssetSettings? settings, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask<Result> SaveAssetAsync(string targetPath, IAsset asset, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask<Result> ImportAsync(string sourcePath, string targetPath, Guid id, IAssetSettings? settings, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask<Result> ExportAsync(string assetPath, string targetPath, IAssetExportOptions? options, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
302
src/Editor/Ghost.Editor.Core/Assets/MeshProcessor.Import.cs
Normal file
302
src/Editor/Ghost.Editor.Core/Assets/MeshProcessor.Import.cs
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
using Ghost.Core;
|
||||||
|
using Ghost.Graphics.RHI;
|
||||||
|
using Ghost.Graphics.Utilities;
|
||||||
|
using Ghost.MeshOptimizer;
|
||||||
|
using Ghost.Ufbx;
|
||||||
|
using Misaki.HighPerformance.Jobs;
|
||||||
|
using Misaki.HighPerformance.LowLevel;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Collections;
|
||||||
|
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
|
||||||
|
{
|
||||||
|
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 MeshParsingWorkItem(string filePath, AllocationHandle allocationHandle, MeshAssetSettings settings)
|
||||||
|
{
|
||||||
|
_filePath = filePath;
|
||||||
|
_allocationHandle = allocationHandle;
|
||||||
|
_settings = settings;
|
||||||
|
_taskCompletionSource = new TaskCompletionSource<Result<MeshNode>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static float4 ComputeTangent(float3 t, float3 n, float3 b)
|
||||||
|
{
|
||||||
|
var proj = n * math.dot(n, t);
|
||||||
|
t = math.normalize(t - proj);
|
||||||
|
var w = math.dot(math.cross(n, t), b) < 0.0f ? -1.0f : 1.0f;
|
||||||
|
return new float4(t.xyz, w);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static ufbx_coordinate_axis ToUfbxCoordinateAxis(CoordinateAxis axis)
|
||||||
|
{
|
||||||
|
return axis switch
|
||||||
|
{
|
||||||
|
CoordinateAxis.PositiveX => ufbx_coordinate_axis.UFBX_COORDINATE_AXIS_POSITIVE_X,
|
||||||
|
CoordinateAxis.PositiveY => ufbx_coordinate_axis.UFBX_COORDINATE_AXIS_POSITIVE_Y,
|
||||||
|
CoordinateAxis.PositiveZ => ufbx_coordinate_axis.UFBX_COORDINATE_AXIS_POSITIVE_Z,
|
||||||
|
CoordinateAxis.NegativeX => ufbx_coordinate_axis.UFBX_COORDINATE_AXIS_NEGATIVE_X,
|
||||||
|
CoordinateAxis.NegativeY => ufbx_coordinate_axis.UFBX_COORDINATE_AXIS_NEGATIVE_Y,
|
||||||
|
CoordinateAxis.NegativeZ => ufbx_coordinate_axis.UFBX_COORDINATE_AXIS_NEGATIVE_Z,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(axis), axis, null)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private GeometryMeshNode ParseGeometry(ufbx_mesh* pMesh)
|
||||||
|
{
|
||||||
|
var meshNode = new GeometryMeshNode
|
||||||
|
{
|
||||||
|
Name = pMesh->name.ToString(),
|
||||||
|
Children = Array.Empty<MeshNode>(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pMesh->num_faces == 0)
|
||||||
|
{
|
||||||
|
return meshNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
var missingNormals = false;
|
||||||
|
var missingTangents = false;
|
||||||
|
|
||||||
|
using var flatVertices = new UnsafeList<Vertex>(1024, AllocationHandle.FreeList);
|
||||||
|
|
||||||
|
var maxScratchIndices = (int)(pMesh->max_face_triangles * 3u);
|
||||||
|
|
||||||
|
using var triIndicesArray = new UnsafeArray<uint>(maxScratchIndices, AllocationHandle.FreeList);
|
||||||
|
|
||||||
|
for (var j = 0u; j < pMesh->num_faces; j++)
|
||||||
|
{
|
||||||
|
var face = pMesh->faces.data[j];
|
||||||
|
|
||||||
|
var numTris = UfbxApi.TriangulateFace(triIndicesArray.AsSpan(0, maxScratchIndices), pMesh, face);
|
||||||
|
|
||||||
|
var totalIndices = numTris * 3;
|
||||||
|
for (var k = 0; k < totalIndices; k++)
|
||||||
|
{
|
||||||
|
var ufbxTopologyIndex = triIndicesArray[k];
|
||||||
|
|
||||||
|
var posIdx = pMesh->vertex_position.indices.data[ufbxTopologyIndex];
|
||||||
|
var normIdx = pMesh->vertex_normal.exists ? pMesh->vertex_normal.indices.data[ufbxTopologyIndex] : uint.MaxValue;
|
||||||
|
var tanIdx = pMesh->vertex_tangent.exists ? pMesh->vertex_tangent.indices.data[ufbxTopologyIndex] : uint.MaxValue;
|
||||||
|
var uvIdx = pMesh->vertex_uv.exists ? pMesh->vertex_uv.indices.data[ufbxTopologyIndex] : uint.MaxValue;
|
||||||
|
var colIdx = pMesh->vertex_color.exists ? pMesh->vertex_color.indices.data[ufbxTopologyIndex] : uint.MaxValue;
|
||||||
|
var btanIdx = pMesh->vertex_bitangent.exists ? pMesh->vertex_bitangent.indices.data[ufbxTopologyIndex] : uint.MaxValue;
|
||||||
|
|
||||||
|
var position = pMesh->vertex_position.values.data[posIdx];
|
||||||
|
var normal = normIdx != uint.MaxValue ? pMesh->vertex_normal.values.data[normIdx] : default;
|
||||||
|
var uv = uvIdx != uint.MaxValue ? pMesh->vertex_uv.values.data[uvIdx] : default;
|
||||||
|
var color = colIdx != uint.MaxValue ? pMesh->vertex_color.values.data[colIdx] : default;
|
||||||
|
|
||||||
|
var vertex = new Vertex
|
||||||
|
{
|
||||||
|
position = new float3(position.x, position.y, position.z),
|
||||||
|
normal = new float3(normal.x, normal.y, normal.z),
|
||||||
|
uv = new float2(uv.x, uv.y),
|
||||||
|
color = new Color128(color.x, color.y, color.z, color.w)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tanIdx != uint.MaxValue)
|
||||||
|
{
|
||||||
|
var mt = pMesh->vertex_tangent.values.data[tanIdx];
|
||||||
|
var mb = btanIdx != uint.MaxValue ? pMesh->vertex_bitangent.values.data[btanIdx] : default;
|
||||||
|
|
||||||
|
var t = new float3(mt.x, mt.y, mt.z);
|
||||||
|
var n = vertex.normal;
|
||||||
|
var b = btanIdx != uint.MaxValue ? new float3(mb.x, mb.y, mb.z) : math.cross(n, t);
|
||||||
|
vertex.tangent = ComputeTangent(t, n, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
var newIndex = (uint)flatVertices.Count;
|
||||||
|
|
||||||
|
flatVertices.Add(vertex);
|
||||||
|
|
||||||
|
if (!missingNormals)
|
||||||
|
{
|
||||||
|
missingNormals = normIdx == uint.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!missingTangents)
|
||||||
|
{
|
||||||
|
missingTangents = tanIdx == uint.MaxValue || btanIdx == uint.MaxValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var numIndices = (uint)flatVertices.Count;
|
||||||
|
|
||||||
|
using var weldedIndices = new UnsafeArray<uint>((int)numIndices, AllocationHandle.FreeList);
|
||||||
|
using var cachedIndices = new UnsafeArray<uint>((int)numIndices, AllocationHandle.FreeList);
|
||||||
|
|
||||||
|
var stream = new ufbx_vertex_stream
|
||||||
|
{
|
||||||
|
data = flatVertices.GetUnsafePtr(),
|
||||||
|
vertex_count = numIndices,
|
||||||
|
vertex_size = (nuint)sizeof(Vertex)
|
||||||
|
};
|
||||||
|
|
||||||
|
var error = new ufbx_error();
|
||||||
|
var numUniqueVertices = UfbxApi.GenerateIndices([stream], weldedIndices, null, &error);
|
||||||
|
if (numUniqueVertices == 0 && error.type != ufbx_error_type.UFBX_ERROR_NONE)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MeshOptApi.OptimizeVertexCache((uint*)cachedIndices.GetUnsafePtr(), (uint*)weldedIndices.GetUnsafePtr(), numIndices, numUniqueVertices);
|
||||||
|
|
||||||
|
vertices = new UnsafeList<Vertex>((int)numUniqueVertices, _allocationHandle);
|
||||||
|
indices = new UnsafeList<uint>((int)numIndices, _allocationHandle);
|
||||||
|
|
||||||
|
var finalVertexCount = MeshOptApi.OptimizeVertexFetch(vertices.GetUnsafePtr(), (uint*)cachedIndices.GetUnsafePtr(), numIndices, flatVertices.GetUnsafePtr(), numIndices, (nuint)sizeof(Vertex));
|
||||||
|
|
||||||
|
vertices.UnsafeSetCount((int)finalVertexCount);
|
||||||
|
|
||||||
|
MemoryUtility.MemCpy(indices.GetUnsafePtr(), cachedIndices.GetUnsafePtr(), numIndices * sizeof(uint));
|
||||||
|
indices.UnsafeSetCount((int)numIndices);
|
||||||
|
|
||||||
|
if (_settings.NormalDataSource == VertexDataSource.Computed || (_settings.NormalDataSource == VertexDataSource.ComputedIfMissing && missingNormals))
|
||||||
|
{
|
||||||
|
MeshBuilder.ComputeNormal(vertices, indices);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_settings.TangentDataSource == VertexDataSource.Computed || (_settings.TangentDataSource == VertexDataSource.ComputedIfMissing && missingTangents))
|
||||||
|
{
|
||||||
|
MeshBuilder.ComputeTangents(vertices, indices);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
target_unit_meters = 1.0f,
|
||||||
|
target_axes = ufbx_coordinate_axes.left_handed_y_up,
|
||||||
|
// Force z-axis mirroring to correctly convert handedness to Left-Handed,
|
||||||
|
// while preserving correct left/right orientation when viewed from the front.
|
||||||
|
handedness_conversion_axis = ufbx_mirror_axis.UFBX_MIRROR_AXIS_Z,
|
||||||
|
space_conversion = ufbx_space_conversion.UFBX_SPACE_CONVERSION_MODIFY_GEOMETRY,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (_settings is ObjAssetSettings objSettings)
|
||||||
|
{
|
||||||
|
load_Opts.obj_axes = new ufbx_coordinate_axes
|
||||||
|
{
|
||||||
|
right = ToUfbxCoordinateAxis(objSettings.ObjectRightAxis),
|
||||||
|
up = ToUfbxCoordinateAxis(objSettings.ObjectUpAxis),
|
||||||
|
front = ToUfbxCoordinateAxis(objSettings.ObjectForwardAxis)
|
||||||
|
};
|
||||||
|
|
||||||
|
load_Opts.obj_unit_meters = objSettings.UnitMeterScale;
|
||||||
|
load_Opts.obj_search_mtl_by_filename = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var str = new UnsafeArray<byte>(Encoding.UTF8.GetByteCount(_filePath) + 1, AllocationHandle.FreeList);
|
||||||
|
var count = Encoding.UTF8.GetBytes(_filePath, str.AsSpan());
|
||||||
|
str[count] = 0;
|
||||||
|
|
||||||
|
using var scene = new DisposablePtr<ufbx_scene>(ufbx_scene.LoadFile((sbyte*)str.GetUnsafePtr(), &load_Opts, &error));
|
||||||
|
if (scene.Get() == null)
|
||||||
|
{
|
||||||
|
_taskCompletionSource.SetResult(Result.Failure(error.description.ToString()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var flatVertices = new UnsafeList<Vertex>(1024, AllocationHandle.FreeList);
|
||||||
|
|
||||||
|
var missingNormals = false;
|
||||||
|
var missingTangents = false;
|
||||||
|
|
||||||
|
for (var i = 0u; i < scene.Get()->nodes.count; i++)
|
||||||
|
{
|
||||||
|
var data = scene.Get()->nodes.data;
|
||||||
|
var node = scene.Get()->nodes.data[i];
|
||||||
|
if (node->is_root)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node->mesh != null)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var numIndices = (uint)flatVertices.Count;
|
||||||
|
|
||||||
|
using var weldedIndices = new UnsafeArray<uint>((int)numIndices, AllocationHandle.FreeList);
|
||||||
|
using var cachedIndices = new UnsafeArray<uint>((int)numIndices, AllocationHandle.FreeList);
|
||||||
|
|
||||||
|
var stream = new ufbx_vertex_stream
|
||||||
|
{
|
||||||
|
data = flatVertices.GetUnsafePtr(),
|
||||||
|
vertex_count = numIndices,
|
||||||
|
vertex_size = (nuint)sizeof(Vertex)
|
||||||
|
};
|
||||||
|
|
||||||
|
var numUniqueVertices = UfbxApi.GenerateIndices([stream], weldedIndices, null, &error);
|
||||||
|
if (numUniqueVertices == 0 && error.type != ufbx_error_type.UFBX_ERROR_NONE)
|
||||||
|
{
|
||||||
|
_taskCompletionSource.SetResult(Result.Failure($"Welding failed: {error.description}"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MeshOptApi.OptimizeVertexCache((uint*)cachedIndices.GetUnsafePtr(), (uint*)weldedIndices.GetUnsafePtr(), numIndices, numUniqueVertices);
|
||||||
|
|
||||||
|
vertices = new UnsafeList<Vertex>((int)numUniqueVertices, _allocationHandle);
|
||||||
|
indices = new UnsafeList<uint>((int)numIndices, _allocationHandle);
|
||||||
|
|
||||||
|
var finalVertexCount = MeshOptApi.OptimizeVertexFetch(vertices.GetUnsafePtr(), (uint*)cachedIndices.GetUnsafePtr(), numIndices, flatVertices.GetUnsafePtr(), numIndices, (nuint)sizeof(Vertex));
|
||||||
|
|
||||||
|
vertices.UnsafeSetCount((int)finalVertexCount);
|
||||||
|
|
||||||
|
MemoryUtility.MemCpy(indices.GetUnsafePtr(), cachedIndices.GetUnsafePtr(), numIndices * sizeof(uint));
|
||||||
|
indices.UnsafeSetCount((int)numIndices);
|
||||||
|
|
||||||
|
if (_settings.NormalDataSource == VertexDataSource.Computed || (_settings.NormalDataSource == VertexDataSource.ComputedIfMissing && missingNormals))
|
||||||
|
{
|
||||||
|
MeshBuilder.ComputeNormal(vertices, indices);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_settings.TangentDataSource == VertexDataSource.Computed || (_settings.TangentDataSource == VertexDataSource.ComputedIfMissing && missingTangents))
|
||||||
|
{
|
||||||
|
MeshBuilder.ComputeTangents(vertices, indices);
|
||||||
|
}
|
||||||
|
|
||||||
|
_taskCompletionSource.SetResult(Result.Success());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class MeshProcessor
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -4,13 +4,16 @@
|
|||||||
// TODO: This file should be moved to editor project since there is no reason we need to build meshlets and LOD at runtime.
|
// 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.Core;
|
||||||
|
using Ghost.Graphics.Core;
|
||||||
|
using Ghost.Graphics.RHI;
|
||||||
using Ghost.MeshOptimizer;
|
using Ghost.MeshOptimizer;
|
||||||
using Misaki.HighPerformance.LowLevel.Buffer;
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
using Misaki.HighPerformance.LowLevel.Collections;
|
using Misaki.HighPerformance.LowLevel.Collections;
|
||||||
using Misaki.HighPerformance.Mathematics;
|
using Misaki.HighPerformance.Mathematics;
|
||||||
using System.Diagnostics;
|
using Misaki.HighPerformance.Mathematics.Geometry;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace Ghost.Graphics.Utilities;
|
namespace Ghost.Editor.Core.Assets;
|
||||||
|
|
||||||
internal struct Cluster : IDisposable
|
internal struct Cluster : IDisposable
|
||||||
{
|
{
|
||||||
@@ -164,7 +167,7 @@ public unsafe delegate int ClodOutputDelegate(void* context, ClodGroup group, Re
|
|||||||
|
|
||||||
// FIX: UnsafeList and UnsafeArray are not same as std::vector.
|
// FIX: UnsafeList and UnsafeArray are not same as std::vector.
|
||||||
|
|
||||||
public static unsafe class MeshletUtility
|
public static unsafe partial class MeshProcessor
|
||||||
{
|
{
|
||||||
private static ClodBounds ComputeBounds(ref readonly ClodMesh mesh, UnsafeList<uint> indices, float error)
|
private static ClodBounds ComputeBounds(ref readonly ClodMesh mesh, UnsafeList<uint> indices, float error)
|
||||||
{
|
{
|
||||||
@@ -651,7 +654,7 @@ public static unsafe class MeshletUtility
|
|||||||
bounds.error = Math.Max(bounds.error * config.simplifyErrorMergePrevious, error) + error * config.simplifyErrorMergeAdditive;
|
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);
|
var refined = OutputGroup(in config, in mesh, clusters, groups[i], bounds, depth, outputContext, outputCallback);
|
||||||
|
|
||||||
for (var j = 0; j < groups[i].Count; j++)
|
for (var j = 0; j < groups[i].Count; j++)
|
||||||
{
|
{
|
||||||
clusters[groups[i][j]].Dispose();
|
clusters[groups[i][j]].Dispose();
|
||||||
@@ -691,4 +694,129 @@ public static unsafe class MeshletUtility
|
|||||||
|
|
||||||
return finalClusterCount;
|
return finalClusterCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void BuildMeshlets(MeshletMeshData* pMeshletData, ReadOnlyUnsafeCollection<Vertex> vertices, ReadOnlyUnsafeCollection<uint> indices)
|
||||||
|
{
|
||||||
|
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.
|
||||||
|
};
|
||||||
|
|
||||||
|
Build(in config, in clodMesh, pMeshletData, 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 = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int MeshletOutputCallback(void* context, ClodGroup group, ReadOnlyUnsafeCollection<ClodCluster> clusters)
|
||||||
|
{
|
||||||
|
var pMeshletData = (MeshletMeshData*)context;
|
||||||
|
|
||||||
|
// Ensure lists are initialized
|
||||||
|
if (!pMeshletData->groups.IsCreated) pMeshletData->groups = new UnsafeList<MeshletGroup>(16, AllocationHandle.Persistent);
|
||||||
|
if (!pMeshletData->meshlets.IsCreated) pMeshletData->meshlets = new UnsafeList<Meshlet>(64, AllocationHandle.Persistent);
|
||||||
|
if (!pMeshletData->meshletVertices.IsCreated) pMeshletData->meshletVertices = new UnsafeList<uint>(128, AllocationHandle.Persistent);
|
||||||
|
if (!pMeshletData->meshletTriangles.IsCreated) pMeshletData->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)pMeshletData->meshlets.Count,
|
||||||
|
meshletCount = (uint)clusters.Count,
|
||||||
|
lodLevel = (uint)group.depth
|
||||||
|
};
|
||||||
|
pMeshletData->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)pMeshletData->meshletVertices.Count,
|
||||||
|
triangleOffset = (uint)pMeshletData->meshletTriangles.Count,
|
||||||
|
groupIndex = (uint)pMeshletData->groups.Count - 1,
|
||||||
|
clusterError = cluster.bounds.error,
|
||||||
|
parentError = group.simplified.error,
|
||||||
|
localMaterialIndex = 0, // TODO: support multiple materials
|
||||||
|
lodLevel = (byte)group.depth,
|
||||||
|
};
|
||||||
|
pMeshletData->meshlets.Add(meshlet);
|
||||||
|
|
||||||
|
// Add unique vertices
|
||||||
|
for (nuint j = 0; j < cluster.vertexCount; j++)
|
||||||
|
{
|
||||||
|
pMeshletData->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);
|
||||||
|
pMeshletData->meshletTriangles.Add(packedTriangle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@ using Ghost.DSL.ShaderCompiler;
|
|||||||
using Ghost.Engine;
|
using Ghost.Engine;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.AssetHandler;
|
namespace Ghost.Editor.Core.Assets;
|
||||||
|
|
||||||
[Guid(GUID)]
|
[Guid(GUID)]
|
||||||
public sealed partial class GraphicsShaderAsset : IAsset
|
public sealed partial class GraphicsShaderAsset : IAsset
|
||||||
@@ -16,7 +16,7 @@ public sealed partial class GraphicsShaderAsset : IAsset
|
|||||||
get;
|
get;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IAssetSettings Settings
|
public IAssetSettings? Settings
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
}
|
}
|
||||||
@@ -49,7 +49,7 @@ public sealed partial class ComputeShaderAsset : IAsset
|
|||||||
get;
|
get;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IAssetSettings Settings
|
public IAssetSettings? Settings
|
||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
}
|
}
|
||||||
@@ -84,13 +84,11 @@ internal class GraphicsShaderAssetHandler : IPackableAssetHandler
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<Result<IAsset>> LoadAssetAsync(FileStream assetStream, Guid id, IAssetSettings? settings, CancellationToken token = default)
|
public async ValueTask<Result<IAsset>> LoadAssetAsync(string assetPath, Guid id, IAssetSettings? settings, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var reader = new StreamReader(assetStream);
|
var result = DSLShaderCompiler.CompileGraphicsShader(assetPath);
|
||||||
var shaderCode = await reader.ReadToEndAsync(token);
|
|
||||||
var result = DSLShaderCompiler.CompileGraphicsShader(shaderCode);
|
|
||||||
if (result.IsFailure)
|
if (result.IsFailure)
|
||||||
{
|
{
|
||||||
return Result.Failure(result.Message);
|
return Result.Failure(result.Message);
|
||||||
@@ -104,12 +102,12 @@ internal class GraphicsShaderAssetHandler : IPackableAssetHandler
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValueTask<Result> SaveAssetAsync(FileStream targetStream, IAsset asset, CancellationToken token = default)
|
public ValueTask<Result> SaveAssetAsync(string targetPath, IAsset asset, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
return new ValueTask<Result>(Result.Failure("Saving shader assets is not supported yet as it's read-only. Please edit the shader source file directly if you need to modify it."));
|
return new ValueTask<Result>(Result.Failure("Saving shader assets is not supported yet as it's read-only. Please edit the shader source file directly if you need to modify it."));
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValueTask<Result> PackAsync(FileStream assetStream, Stream targetStream, CancellationToken token = default)
|
public ValueTask<Result> PackAsync(string assetPath, MemoryStream targetStream, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
@@ -126,13 +124,11 @@ internal class ComputeShaderAssetHandler : IPackableAssetHandler
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<Result<IAsset>> LoadAssetAsync(FileStream assetStream, Guid id, IAssetSettings? settings, CancellationToken token = default)
|
public async ValueTask<Result<IAsset>> LoadAssetAsync(string assetPath, Guid id, IAssetSettings? settings, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var reader = new StreamReader(assetStream);
|
var result = DSLShaderCompiler.CompileComputeShaderCode(assetPath);
|
||||||
var shaderCode = await reader.ReadToEndAsync(token);
|
|
||||||
var result = DSLShaderCompiler.CompileComputeShader(shaderCode);
|
|
||||||
if (result.IsFailure)
|
if (result.IsFailure)
|
||||||
{
|
{
|
||||||
return Result.Failure(result.Message);
|
return Result.Failure(result.Message);
|
||||||
@@ -146,12 +142,12 @@ internal class ComputeShaderAssetHandler : IPackableAssetHandler
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValueTask<Result> SaveAssetAsync(FileStream targetStream, IAsset asset, CancellationToken token = default)
|
public ValueTask<Result> SaveAssetAsync(string targetPath, IAsset asset, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
return new ValueTask<Result>(Result.Failure("Saving shader assets is not supported yet as it's read-only. Please edit the shader source file directly if you need to modify it."));
|
return new ValueTask<Result>(Result.Failure("Saving shader assets is not supported yet as it's read-only. Please edit the shader source file directly if you need to modify it."));
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValueTask<Result> PackAsync(FileStream assetStream, Stream targetStream, CancellationToken token = default)
|
public ValueTask<Result> PackAsync(string assetPath, MemoryStream targetStream, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
@@ -6,9 +6,8 @@ using Misaki.HighPerformance.LowLevel;
|
|||||||
using System.IO.MemoryMappedFiles;
|
using System.IO.MemoryMappedFiles;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using TerraFX.Interop.Windows;
|
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.AssetHandler;
|
namespace Ghost.Editor.Core.Assets;
|
||||||
|
|
||||||
public enum TextureType : uint
|
public enum TextureType : uint
|
||||||
{
|
{
|
||||||
@@ -86,7 +85,7 @@ public unsafe class TextureAsset : IAsset
|
|||||||
_textureData = data;
|
_textureData = data;
|
||||||
_width = header.width;
|
_width = header.width;
|
||||||
_height = header.height;
|
_height = header.height;
|
||||||
_depth = header.depth;
|
_depth = header.bpc;
|
||||||
_dimension = header.dimension;
|
_dimension = header.dimension;
|
||||||
_colorComponents = header.colorComponents;
|
_colorComponents = header.colorComponents;
|
||||||
}
|
}
|
||||||
@@ -160,11 +159,6 @@ public class TextureAssetSettings : IAssetSettings
|
|||||||
get; set;
|
get; set;
|
||||||
} = 0; // 0 means generate full mipmap levels.
|
} = 0; // 0 means generate full mipmap levels.
|
||||||
|
|
||||||
public bool GammaCorrection
|
|
||||||
{
|
|
||||||
get; set;
|
|
||||||
} = true;
|
|
||||||
|
|
||||||
public bool PremultiplyAlpha
|
public bool PremultiplyAlpha
|
||||||
{
|
{
|
||||||
get; set;
|
get; set;
|
||||||
@@ -264,6 +258,7 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand
|
|||||||
public int width;
|
public int width;
|
||||||
public int height;
|
public int height;
|
||||||
public int depth;
|
public int depth;
|
||||||
|
public int bitsPerChannel;
|
||||||
public int colorComponents;
|
public int colorComponents;
|
||||||
public bool isHDR;
|
public bool isHDR;
|
||||||
}
|
}
|
||||||
@@ -305,33 +300,27 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand
|
|||||||
return TextureDimension.Texture2D;
|
return TextureDimension.Texture2D;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static unsafe Result<TextureInfo> GetImageInfo(FileStream sourceStream)
|
private static unsafe Result<TextureInfo> GetImageInfo(string sourcePath, TextureAssetSettings settings)
|
||||||
{
|
{
|
||||||
using var mmf = MemoryMappedFile.CreateFromFile(sourceStream, null, 0, MemoryMappedFileAccess.Read, HandleInheritability.None, true);
|
using var mmf = MemoryMappedFile.CreateFromFile(sourcePath, FileMode.Open);
|
||||||
using var accessor = mmf.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read);
|
using var accessor = mmf.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read);
|
||||||
|
|
||||||
byte* ptr = null;
|
byte* ptr = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var ext = Path.GetExtension(sourceStream.Name);
|
var ext = Path.GetExtension(sourcePath);
|
||||||
var isHDR = ext.Equals(".hdr", StringComparison.OrdinalIgnoreCase);
|
var isHDR = ext.Equals(".hdr", StringComparison.OrdinalIgnoreCase) || settings.Basic.TextureShape == TextureShape.TextureCube;
|
||||||
|
|
||||||
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr);
|
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr);
|
||||||
|
|
||||||
int imageWidth, imageHeight, bitsPerChannel, colorComponents;
|
int imageWidth, imageHeight, bitsPerChannel, colorComponents;
|
||||||
|
|
||||||
var bufferSpan = new ReadOnlySpan<byte>(ptr, (int)sourceStream.Length);
|
var bufferSpan = new ReadOnlySpan<byte>(ptr, (int)accessor.Capacity);
|
||||||
var code = StbIApi.InfoFromMemory(bufferSpan, &imageWidth, &imageHeight, &colorComponents);
|
|
||||||
if (code == 0)
|
|
||||||
{
|
|
||||||
return Result.Failure("Failed to read image info from memory.");
|
|
||||||
}
|
|
||||||
|
|
||||||
bitsPerChannel = StbIApi.Is16BitFromMemory(bufferSpan) > 0 ? 16 : 8;
|
bitsPerChannel = StbIApi.Is16BitFromMemory(bufferSpan) > 0 ? 16 : 8;
|
||||||
|
|
||||||
void* pPixels;
|
void* pPixels;
|
||||||
if (bitsPerChannel > 8)
|
if (isHDR || bitsPerChannel > 8)
|
||||||
{
|
{
|
||||||
pPixels = StbIApi.LoadfFromMemory(bufferSpan, &imageWidth, &imageHeight, &colorComponents, 4);
|
pPixels = StbIApi.LoadfFromMemory(bufferSpan, &imageWidth, &imageHeight, &colorComponents, 4);
|
||||||
}
|
}
|
||||||
@@ -345,8 +334,9 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand
|
|||||||
pixelData = (IntPtr)pPixels,
|
pixelData = (IntPtr)pPixels,
|
||||||
width = imageWidth,
|
width = imageWidth,
|
||||||
height = imageHeight,
|
height = imageHeight,
|
||||||
depth = bitsPerChannel,
|
depth = 1,
|
||||||
colorComponents = colorComponents,
|
bitsPerChannel = bitsPerChannel,
|
||||||
|
colorComponents = 4, // We forced req_comp to 4
|
||||||
isHDR = isHDR,
|
isHDR = isHDR,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -363,25 +353,25 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValueTask<Result<IAsset>> LoadAssetAsync(FileStream assetStream, Guid id, IAssetSettings? settings, CancellationToken token = default)
|
public ValueTask<Result<IAsset>> LoadAssetAsync(string assetPath, Guid id, IAssetSettings? settings, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var infoResult = GetImageInfo(assetStream);
|
var textureSettings = settings as TextureAssetSettings ?? new TextureAssetSettings();
|
||||||
|
var infoResult = GetImageInfo(assetPath, textureSettings);
|
||||||
if (infoResult.IsFailure)
|
if (infoResult.IsFailure)
|
||||||
{
|
{
|
||||||
return ValueTask.FromResult(Result<IAsset>.Failure(infoResult.Message));
|
return ValueTask.FromResult(Result<IAsset>.Failure(infoResult.Message));
|
||||||
}
|
}
|
||||||
|
|
||||||
var info = infoResult.Value;
|
var info = infoResult.Value;
|
||||||
var textureSettings = settings as TextureAssetSettings ?? new TextureAssetSettings();
|
|
||||||
var contentHeader = new TextureContentHeader
|
var contentHeader = new TextureContentHeader
|
||||||
{
|
{
|
||||||
width = (uint)info.width,
|
width = (uint)info.width,
|
||||||
height = (uint)info.height,
|
height = (uint)info.height,
|
||||||
depth = (uint)info.depth,
|
bpc = (uint)info.bitsPerChannel,
|
||||||
colorComponents = (uint)info.colorComponents,
|
colorComponents = (uint)info.colorComponents,
|
||||||
dimension = (uint)GetTextureDimension(textureSettings)
|
dimension = (uint)GetTextureDimension(textureSettings),
|
||||||
};
|
};
|
||||||
|
|
||||||
return ValueTask.FromResult(Result.Success<IAsset>(new TextureAsset(info.pixelData, contentHeader, id, textureSettings)));
|
return ValueTask.FromResult(Result.Success<IAsset>(new TextureAsset(info.pixelData, contentHeader, id, textureSettings)));
|
||||||
@@ -393,22 +383,24 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand
|
|||||||
}
|
}
|
||||||
|
|
||||||
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
|
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
|
||||||
private unsafe static void WriteCallback(void* context, void* data, int size)
|
private static unsafe void WriteCallback(void* context, void* data, int size)
|
||||||
{
|
{
|
||||||
var stream = (Stream)GCHandle.FromIntPtr((IntPtr)context).Target!;
|
var stream = (Stream)GCHandle.FromIntPtr((IntPtr)context).Target!;
|
||||||
var buffer = new ReadOnlySpan<byte>(data, size);
|
var buffer = new ReadOnlySpan<byte>(data, size);
|
||||||
stream.Write(buffer);
|
stream.Write(buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<Result> SaveAssetAsync(FileStream targetStream, IAsset asset, CancellationToken token = default)
|
public async ValueTask<Result> SaveAssetAsync(string targetPath, IAsset asset, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
if (asset is not TextureAsset textureAsset)
|
if (asset is not TextureAsset textureAsset)
|
||||||
{
|
{
|
||||||
return Result.Failure("Asset type is not TextureAsset");
|
return Result.Failure("Asset type is not TextureAsset");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await using var targetStream = new FileStream(targetPath, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||||
return await Task.Run(() =>
|
return await Task.Run(() =>
|
||||||
{
|
{
|
||||||
|
// It will be safe here to pass the gc handle to c because c will not use it, c will only pass it back to c# in the callback, and we will free the handle after the write operation is done.
|
||||||
var gcHandle = GCHandle.Alloc(targetStream, GCHandleType.Normal);
|
var gcHandle = GCHandle.Alloc(targetStream, GCHandleType.Normal);
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -447,28 +439,24 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand
|
|||||||
}, token).ConfigureAwait(false);
|
}, token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<Result> ImportAsync(FileStream sourceStream, FileStream targetStream, Guid id, IAssetSettings? settings, CancellationToken token = default)
|
public async ValueTask<Result> ImportAsync(string sourcePath, string targetPath, Guid id, IAssetSettings? settings, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
if (sourceStream.Length > int.MaxValue)
|
if (!File.Exists(sourcePath))
|
||||||
{
|
{
|
||||||
return Result.Failure("Source stream is too large.");
|
return Result.Failure("Source file does not exist.");
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var textureSettings = settings as TextureAssetSettings ?? new TextureAssetSettings();
|
var textureSettings = settings as TextureAssetSettings ?? new TextureAssetSettings();
|
||||||
|
var infoResult = GetImageInfo(sourcePath, textureSettings);
|
||||||
using var mmf = MemoryMappedFile.CreateFromFile(sourceStream, null, 0, MemoryMappedFileAccess.Read, HandleInheritability.None, true);
|
|
||||||
using var accessor = mmf.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read);
|
|
||||||
|
|
||||||
var infoResult = GetImageInfo(sourceStream);
|
|
||||||
if (!infoResult.IsSuccess)
|
if (!infoResult.IsSuccess)
|
||||||
{
|
{
|
||||||
return Result.Failure(infoResult.Message);
|
return Result.Failure(infoResult.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
var info = infoResult.Value;
|
var info = infoResult.Value;
|
||||||
var result = await TextureProcessor.CompressToCacheAsync(EditorApplication.CacheFolderPath, id,
|
var result = await TextureProcessor.GenerateMipAndCompressAsync(EditorApplication.CacheFolderPath, id,
|
||||||
info,
|
info,
|
||||||
textureSettings, token)
|
textureSettings, token)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
@@ -480,13 +468,12 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand
|
|||||||
|
|
||||||
var (cachePath, mip) = result.Value;
|
var (cachePath, mip) = result.Value;
|
||||||
|
|
||||||
targetStream.Seek(0, SeekOrigin.Begin);
|
await using var targetStream = new FileStream(targetPath, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||||
|
|
||||||
var header = new TextureContentHeader
|
var header = new TextureContentHeader
|
||||||
{
|
{
|
||||||
width = (uint)info.width,
|
width = (uint)info.width,
|
||||||
height = (uint)info.height,
|
height = (uint)info.height,
|
||||||
depth = (uint)info.depth,
|
bpc = (uint)info.bitsPerChannel,
|
||||||
colorComponents = (uint)info.colorComponents,
|
colorComponents = (uint)info.colorComponents,
|
||||||
mipLevels = (uint)mip,
|
mipLevels = (uint)mip,
|
||||||
dimension = (uint)GetTextureDimension(textureSettings)
|
dimension = (uint)GetTextureDimension(textureSettings)
|
||||||
@@ -506,12 +493,12 @@ internal class TextureAssetHandler : IImportableAssetHandler, IPackableAssetHand
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValueTask<Result> ExportAsync(FileStream assetStream, FileStream targetStream, IAssetExportOptions? options, CancellationToken token = default)
|
public ValueTask<Result> ExportAsync(string assetPath, string targetPath, IAssetExportOptions? options, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
return ValueTask.FromResult(Result.Failure("Exporting texture assets is not supported yet."));
|
return ValueTask.FromResult(Result.Failure("Exporting texture assets is not supported yet."));
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValueTask<Result> PackAsync(FileStream assetStream, Stream targetStream, CancellationToken token = default)
|
public ValueTask<Result> PackAsync(string assetPath, MemoryStream targetStream, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
@@ -1,46 +1,42 @@
|
|||||||
using Ghost.Core;
|
using Ghost.Core;
|
||||||
|
using Ghost.Engine;
|
||||||
using Ghost.Nvtt;
|
using Ghost.Nvtt;
|
||||||
|
using Misaki.HighPerformance.Jobs;
|
||||||
using Misaki.HighPerformance.LowLevel;
|
using Misaki.HighPerformance.LowLevel;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Collections;
|
||||||
|
using Misaki.HighPerformance.Mathematics.SPMD;
|
||||||
using System.IO.Hashing;
|
using System.IO.Hashing;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.AssetHandler;
|
namespace Ghost.Editor.Core.Assets;
|
||||||
|
|
||||||
/// <summary>
|
internal static partial class TextureProcessor
|
||||||
/// Drives the NVTT compression + mipmap pipeline for a single texture asset.
|
|
||||||
///
|
|
||||||
/// Responsibilities:
|
|
||||||
/// 1. Accept raw decoded pixel bytes + settings.
|
|
||||||
/// 2. Determine the cache file path (<c>CachesFolderPath/TextureCache/<guid>_<hash>.dds</c>).
|
|
||||||
/// 3. If the cache is already valid (hash matches), skip compression.
|
|
||||||
/// 4. Otherwise run the full NVTT pipeline and write the DDS to the cache file.
|
|
||||||
///
|
|
||||||
/// The caller owns opening/closing all streams; this class only takes spans and paths.
|
|
||||||
/// </summary>
|
|
||||||
internal static class TextureProcessor
|
|
||||||
{
|
{
|
||||||
private class NvttPipelineTask : IThreadPoolWorkItem
|
private class NvttPipelineTask : IThreadPoolWorkItem
|
||||||
{
|
{
|
||||||
private readonly string _outputPath;
|
private readonly string _outputPath;
|
||||||
|
|
||||||
private readonly TextureAssetHandler.TextureInfo _textureInfo;
|
private readonly TextureAssetHandler.TextureInfo _textureInfo;
|
||||||
|
|
||||||
private readonly TextureAssetSettings _settings;
|
private readonly TextureAssetSettings _settings;
|
||||||
|
private UnsafeArray<MipLevel> _mipLevels;
|
||||||
|
|
||||||
private readonly TaskCompletionSource<Result<int>> _completionSource;
|
private readonly TaskCompletionSource<Result<int>> _completionSource;
|
||||||
|
|
||||||
public Task<Result<int>> Task => _completionSource.Task;
|
public Task<Result<int>> Task => _completionSource.Task;
|
||||||
|
|
||||||
public NvttPipelineTask(string outputPath, TextureAssetHandler.TextureInfo textureInfo, TextureAssetSettings settings)
|
public NvttPipelineTask(string outputPath, TextureAssetHandler.TextureInfo textureInfo, TextureAssetSettings settings, UnsafeArray<MipLevel> mipLevels)
|
||||||
{
|
{
|
||||||
_outputPath = outputPath;
|
_outputPath = outputPath;
|
||||||
_textureInfo = textureInfo;
|
_textureInfo = textureInfo;
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
|
_mipLevels = mipLevels;
|
||||||
_completionSource = new TaskCompletionSource<Result<int>>();
|
_completionSource = new TaskCompletionSource<Result<int>>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public unsafe void Execute()
|
private unsafe Result<int> RunMipGenCompressionPipeline()
|
||||||
{
|
{
|
||||||
using var pSurface = new DisposablePtr<NvttSurface>(NvttSurface.Create());
|
using var pSurface = new DisposablePtr<NvttSurface>(NvttSurface.Create());
|
||||||
using var pCompOpts = new DisposablePtr<NvttCompressionOptions>(NvttCompressionOptions.Create());
|
using var pCompOpts = new DisposablePtr<NvttCompressionOptions>(NvttCompressionOptions.Create());
|
||||||
@@ -49,20 +45,24 @@ internal static class TextureProcessor
|
|||||||
|
|
||||||
var inputFormat = _textureInfo.colorComponents == 1
|
var inputFormat = _textureInfo.colorComponents == 1
|
||||||
? NvttInputFormat.NVTT_InputFormat_R_32F
|
? NvttInputFormat.NVTT_InputFormat_R_32F
|
||||||
: _textureInfo.depth > 8
|
: _textureInfo.bitsPerChannel > 8
|
||||||
? NvttInputFormat.NVTT_InputFormat_RGBA_32F
|
? NvttInputFormat.NVTT_InputFormat_RGBA_32F
|
||||||
: NvttInputFormat.NVTT_InputFormat_BGRA_8UB; // we'll swizzle RB below
|
: NvttInputFormat.NVTT_InputFormat_BGRA_8UB; // we'll swizzle RB below
|
||||||
|
|
||||||
var needUnsigned = _settings.Basic.TextureType == TextureType.Normal ? NvttBoolean.NVTT_True : NvttBoolean.NVTT_False;
|
var isNormal = _settings.Basic.TextureType == TextureType.Normal;
|
||||||
if (pSurface.Get()->SetImageData(inputFormat, _textureInfo.width, _textureInfo.height, _textureInfo.depth, (void*)_textureInfo.pixelData, needUnsigned, null))
|
if (!pSurface.Get()->SetImageData(inputFormat, _textureInfo.width, _textureInfo.height, _textureInfo.depth, (void*)_textureInfo.pixelData, isNormal, null))
|
||||||
{
|
{
|
||||||
_completionSource.SetResult(Result.Failure("Failed to set image data for NVTT compression."));
|
return Result.Failure<int>("Failed to set image data for NVTT compression.");
|
||||||
return;
|
}
|
||||||
|
|
||||||
|
if (isNormal)
|
||||||
|
{
|
||||||
|
pSurface.Get()->SetNormalMap(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// stb gives us RGBA byte order; NVTT BGRA_8UB reads it as BGRA,
|
// stb gives us RGBA byte order; NVTT BGRA_8UB reads it as BGRA,
|
||||||
// so channels R and B are swapped — fix with swizzle(2,1,0,3).
|
// so channels R and B are swapped — fix with swizzle(2,1,0,3).
|
||||||
if (_textureInfo.colorComponents > 1 && _textureInfo.depth <= 8)
|
if (_textureInfo.colorComponents > 1 && _textureInfo.bitsPerChannel <= 8)
|
||||||
{
|
{
|
||||||
pSurface.Get()->Swizzle(2, 1, 0, 3, null);
|
pSurface.Get()->Swizzle(2, 1, 0, 3, null);
|
||||||
}
|
}
|
||||||
@@ -91,7 +91,7 @@ internal static class TextureProcessor
|
|||||||
pSurface.Get()->SetBorder(0f, 0f, 0f, 0f, null);
|
pSurface.Get()->SetBorder(0f, 0f, 0f, 0f, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_settings.Basic.IsSRGB && _settings.Advanced.GammaCorrection)
|
if (_settings.Basic.IsSRGB)
|
||||||
{
|
{
|
||||||
pSurface.Get()->ToLinearFromSrgb(null);
|
pSurface.Get()->ToLinearFromSrgb(null);
|
||||||
}
|
}
|
||||||
@@ -136,6 +136,10 @@ internal static class TextureProcessor
|
|||||||
pCtx.Get()->OutputHeader(pSurface.Get(), mipmapCount, pCompOpts.Get(), pOutOpts.Get());
|
pCtx.Get()->OutputHeader(pSurface.Get(), mipmapCount, pCompOpts.Get(), pOutOpts.Get());
|
||||||
|
|
||||||
using var pMip = new DisposablePtr<NvttSurface>(pSurface.Get()->Clone());
|
using var pMip = new DisposablePtr<NvttSurface>(pSurface.Get()->Clone());
|
||||||
|
if (pMip.Get() == null)
|
||||||
|
{
|
||||||
|
return Result.Failure("Failed to clone surface for mipmap generation.");
|
||||||
|
}
|
||||||
|
|
||||||
for (var level = 0; level < mipmapCount; level++)
|
for (var level = 0; level < mipmapCount; level++)
|
||||||
{
|
{
|
||||||
@@ -148,19 +152,110 @@ internal static class TextureProcessor
|
|||||||
_settings.Advanced.ScaleAlphaForMipCoverageThreshold / 255f, 3, null);
|
_settings.Advanced.ScaleAlphaForMipCoverageThreshold / 255f, 3, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
pCtx.Get()->Compress(pMip.Get(), 0, level, pCompOpts.Get(), pOutOpts.Get());
|
using var compressMip = new DisposablePtr<NvttSurface>(pMip.Get()->Clone());
|
||||||
|
if (_settings.Basic.IsSRGB)
|
||||||
|
{
|
||||||
|
compressMip.Get()->ToSrgb(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pCtx.Get()->Compress(compressMip.Get(), 0, level, pCompOpts.Get(), pOutOpts.Get()))
|
||||||
|
{
|
||||||
|
return Result.Failure("Failed to compress mipmap.");
|
||||||
|
}
|
||||||
|
|
||||||
if (level + 1 < mipmapCount)
|
if (level + 1 < mipmapCount)
|
||||||
{
|
{
|
||||||
pMip.Get()->BuildNextMipmapDefaults(nvttFilter, 1, null);
|
if (!pMip.Get()->BuildNextMipmapDefaults(nvttFilter, 1, null))
|
||||||
|
{
|
||||||
|
return Result.Failure("Failed to build next mipmap.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_completionSource.SetResult(Result.Success(mipmapCount));
|
return Result.Success(mipmapCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe Result<int> RunCubeMapCompressionPipeline()
|
||||||
|
{
|
||||||
|
using var pCompOpts = new DisposablePtr<NvttCompressionOptions>(NvttCompressionOptions.Create());
|
||||||
|
using var pOutOpts = new DisposablePtr<NvttOutputOptions>(NvttOutputOptions.Create());
|
||||||
|
using var pCtx = new DisposablePtr<NvttContext>(NvttContext.Create());
|
||||||
|
|
||||||
|
pCompOpts.Get()->SetFormat(SelectFormat(_settings, _textureInfo.isHDR));
|
||||||
|
pCompOpts.Get()->SetQuality(SelectQuality(_settings.Advanced.CompressionLevel));
|
||||||
|
|
||||||
|
pOutOpts.Get()->SetOutputHeader(true);
|
||||||
|
pOutOpts.Get()->SetSrgbFlag(_settings.Basic.IsSRGB);
|
||||||
|
pOutOpts.Get()->SetContainer(NvttContainer.NVTT_Container_DDS10);
|
||||||
|
pOutOpts.Get()->SetFileName(Encoding.UTF8.GetBytes(_outputPath));
|
||||||
|
|
||||||
|
pCtx.Get()->SetCudaAcceleration(NvttApi.IsCudaSupported());
|
||||||
|
|
||||||
|
int edgeLength;
|
||||||
|
using (var cubeSurface0 = new DisposablePtr<NvttCubeSurface>(NvttCubeSurface.Create()))
|
||||||
|
using (var mip0Surf = new DisposablePtr<NvttSurface>(NvttSurface.Create()))
|
||||||
|
{
|
||||||
|
if (!mip0Surf.Get()->SetImageData(NvttInputFormat.NVTT_InputFormat_RGBA_32F, _mipLevels[0].width, _mipLevels[0].height, 1, _mipLevels[0].data.GetUnsafePtr(), false, null))
|
||||||
|
{
|
||||||
|
return Result.Failure<int>("Failed to set image data for NVTT compression.");
|
||||||
|
}
|
||||||
|
|
||||||
|
cubeSurface0.Get()->Fold(mip0Surf.Get(), NvttCubeLayout.NVTT_CubeLayout_LatitudeLongitude);
|
||||||
|
edgeLength = cubeSurface0.Get()->EdgeLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
pCtx.Get()->OutputHeaderData(NvttTextureType.NVTT_TextureType_Cube, edgeLength, edgeLength, 1, _mipLevels.Length, false, pCompOpts.Get(), pOutOpts.Get());
|
||||||
|
|
||||||
|
for (var level = 0; level < _mipLevels.Length; level++)
|
||||||
|
{
|
||||||
|
using var cubeSurface = new DisposablePtr<NvttCubeSurface>(NvttCubeSurface.Create());
|
||||||
|
using var mipSurf = new DisposablePtr<NvttSurface>(NvttSurface.Create());
|
||||||
|
|
||||||
|
mipSurf.Get()->SetImageData(NvttInputFormat.NVTT_InputFormat_RGBA_32F, _mipLevels[level].width, _mipLevels[level].height, 1, _mipLevels[level].data.GetUnsafePtr(), false, null);
|
||||||
|
cubeSurface.Get()->Fold(mipSurf.Get(), NvttCubeLayout.NVTT_CubeLayout_LatitudeLongitude);
|
||||||
|
|
||||||
|
for (var face = 0; face < 6; face++)
|
||||||
|
{
|
||||||
|
var faceSurf = cubeSurface.Get()->Face(face);
|
||||||
|
if (_settings.Basic.IsSRGB)
|
||||||
|
{
|
||||||
|
faceSurf->ToSrgb(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pCtx.Get()->Compress(faceSurf, face, level, pCompOpts.Get(), pOutOpts.Get()))
|
||||||
|
{
|
||||||
|
return Result.Failure("Failed to compress mipmap.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.Success(_mipLevels.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Execute()
|
||||||
|
{
|
||||||
|
Result<int> finalResult;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_settings.Basic.TextureShape == TextureShape.TextureCube)
|
||||||
|
{
|
||||||
|
finalResult = RunCubeMapCompressionPipeline();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
finalResult = RunMipGenCompressionPipeline();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
finalResult = Result.Failure($"Compression threw an exception: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
_completionSource.SetResult(finalResult);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async ValueTask<Result<(string cachePath, int mipmapCount)>> CompressToCacheAsync(string cachesFolderPath, Guid assetId,
|
public static async ValueTask<Result<(string cachePath, int mipmapCount)>> GenerateMipAndCompressAsync(string cachesFolderPath, Guid assetId,
|
||||||
TextureAssetHandler.TextureInfo textureInfo,
|
TextureAssetHandler.TextureInfo textureInfo,
|
||||||
TextureAssetSettings settings, CancellationToken cancellationToken)
|
TextureAssetSettings settings, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
@@ -174,46 +269,79 @@ internal static class TextureProcessor
|
|||||||
|
|
||||||
if (File.Exists(cachePath))
|
if (File.Exists(cachePath))
|
||||||
{
|
{
|
||||||
using var fs = new FileStream(cachePath, FileMode.Open, FileAccess.Read);
|
var isValid = false;
|
||||||
using var reader = new BinaryReader(fs);
|
var mipMapCount = 1u;
|
||||||
if (reader.ReadUInt32() != 0x20534444)
|
var hasMipMapFlag = false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var fs = new FileStream(cachePath, FileMode.Open, FileAccess.Read);
|
||||||
|
using var reader = new BinaryReader(fs);
|
||||||
|
if (reader.ReadUInt32() == 0x20534444)
|
||||||
|
{
|
||||||
|
reader.BaseStream.Seek(4, SeekOrigin.Current);
|
||||||
|
var flags = reader.ReadUInt32();
|
||||||
|
hasMipMapFlag = (flags & 0x00020000) != 0;
|
||||||
|
|
||||||
|
reader.BaseStream.Seek(28, SeekOrigin.Begin);
|
||||||
|
mipMapCount = reader.ReadUInt32();
|
||||||
|
isValid = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore read errors and regenerate
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isValid)
|
||||||
|
{
|
||||||
|
return (cachePath, (!hasMipMapFlag || mipMapCount == 0) ? 1 : (int)mipMapCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
File.Delete(cachePath);
|
File.Delete(cachePath);
|
||||||
goto ScheduleWork;
|
|
||||||
}
|
}
|
||||||
|
catch
|
||||||
// Read dwFlags (Offset 8)
|
|
||||||
// Skip dwSize (4 bytes), then read dwFlags (4 bytes)
|
|
||||||
reader.BaseStream.Seek(4, SeekOrigin.Current);
|
|
||||||
var flags = reader.ReadUInt32();
|
|
||||||
|
|
||||||
// The DDSD_MIPMAPCOUNT flag is 0x00020000
|
|
||||||
var hasMipMapFlag = (flags & 0x00020000) != 0;
|
|
||||||
|
|
||||||
// Read dwMipMapCount (Offset 28)
|
|
||||||
reader.BaseStream.Seek(28, SeekOrigin.Begin);
|
|
||||||
var mipMapCount = reader.ReadUInt32();
|
|
||||||
|
|
||||||
// Return the correct count
|
|
||||||
// If the flag is missing, or the count says 0, there is still 1 main image.
|
|
||||||
if (!hasMipMapFlag || mipMapCount == 0)
|
|
||||||
{
|
{
|
||||||
return (cachePath, 1);
|
// Ignore deletion errors, maybe file is still locked or we have no permission.
|
||||||
|
// The pipeline will overwrite it.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UnsafeArray<MipLevel> mipLevels = default;
|
||||||
|
var scheduler = EditorApplication.GetService<EngineCore>().JobScheduler;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (settings.Basic.TextureShape == TextureShape.TextureCube)
|
||||||
|
{
|
||||||
|
var handle = GenerateMipHDRI(scheduler, textureInfo, out mipLevels);
|
||||||
|
await scheduler.WaitAsync(handle, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (cachePath, (int)mipMapCount);
|
var workItem = new NvttPipelineTask(cachePath, textureInfo, settings, mipLevels);
|
||||||
}
|
ThreadPool.UnsafeQueueUserWorkItem(workItem, true);
|
||||||
|
var result = await workItem.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
if (result.IsFailure)
|
||||||
|
{
|
||||||
|
return Result.Failure(result.Message);
|
||||||
|
}
|
||||||
|
|
||||||
ScheduleWork:
|
return (cachePath, result.Value);
|
||||||
var workItem = new NvttPipelineTask(cachePath, textureInfo, settings);
|
}
|
||||||
ThreadPool.UnsafeQueueUserWorkItem(workItem, true);
|
finally
|
||||||
var result = await workItem.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
if (result.IsFailure)
|
|
||||||
{
|
{
|
||||||
return Result.Failure(result.Message);
|
if (mipLevels.IsCreated)
|
||||||
}
|
{
|
||||||
|
var mipDisposeJob = new MipLevelDisposeJob
|
||||||
|
{
|
||||||
|
mipLevels = mipLevels,
|
||||||
|
};
|
||||||
|
|
||||||
return (cachePath, result.Value);
|
scheduler.Schedule(in mipDisposeJob);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static NvttFormat SelectFormat(TextureAssetSettings settings, bool isHDR)
|
private static NvttFormat SelectFormat(TextureAssetSettings settings, bool isHDR)
|
||||||
338
src/Editor/Ghost.Editor.Core/Assets/TextureProcessor.GGXMip.cs
Normal file
338
src/Editor/Ghost.Editor.Core/Assets/TextureProcessor.GGXMip.cs
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
using Ghost.Core;
|
||||||
|
using Misaki.HighPerformance.Jobs;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Buffer;
|
||||||
|
using Misaki.HighPerformance.LowLevel.Collections;
|
||||||
|
using Misaki.HighPerformance.Mathematics.SPMD;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using static Misaki.HighPerformance.Mathematics.math;
|
||||||
|
|
||||||
|
namespace Ghost.Editor.Core.Assets;
|
||||||
|
|
||||||
|
internal static partial class TextureProcessor
|
||||||
|
{
|
||||||
|
private const int _SAMPLE_COUNT = 1024;
|
||||||
|
|
||||||
|
private struct MipLevel
|
||||||
|
{
|
||||||
|
public UnsafeArray<float> data;
|
||||||
|
public int width;
|
||||||
|
public int height;
|
||||||
|
public int offset;
|
||||||
|
public float roughness;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe struct GGXMipGenerationJobSPMD<TFloat, TInt> : IJobParallelFor
|
||||||
|
where TFloat : unmanaged, ISPMDLane<TFloat, float>
|
||||||
|
where TInt : unmanaged, ISPMDLane<TInt, int>
|
||||||
|
{
|
||||||
|
public float* pImage;
|
||||||
|
public MipLevel* pMipLevels;
|
||||||
|
public float* pRadicalInverse_VdCLut;
|
||||||
|
public int imageWidth;
|
||||||
|
public int imageHeight;
|
||||||
|
public int numMipLevels;
|
||||||
|
public int channelCount;
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static Vector2<TFloat, float> Hammersley(TFloat i, int N, float* lut)
|
||||||
|
{
|
||||||
|
var x = i / N;
|
||||||
|
var y = TFloat.Load(lut + (int)i[0]);
|
||||||
|
return MathV.Create<TFloat, float>(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GGX Importance Sampling
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static Vector3<TFloat, float> ImportanceSampleGGX(Vector2<TFloat, float> Xi, Vector3<TFloat, float> N, float roughness)
|
||||||
|
{
|
||||||
|
var a = roughness * roughness; // Disney remap roughness for better visual linearity
|
||||||
|
|
||||||
|
var phi = 2.0f * PI * Xi.x;
|
||||||
|
|
||||||
|
// Clamp the inside of the cosTheta Sqrt to prevent NaN on division precision edges
|
||||||
|
var cosThetaInner = TFloat.Max((1.0f - Xi.y) / (1.0f + (a * a - 1.0f) * Xi.y), TFloat.Zero);
|
||||||
|
var cosTheta = TFloat.Sqrt(cosThetaInner);
|
||||||
|
|
||||||
|
// Clamp the inside of sinTheta to prevent sqrt of negative floating-point errors
|
||||||
|
var sinThetaInner = TFloat.Max(1.0f - cosTheta * cosTheta, TFloat.Zero);
|
||||||
|
var sinTheta = TFloat.Sqrt(sinThetaInner);
|
||||||
|
|
||||||
|
// Spherical to Cartesian coordinates (Halfway vector)
|
||||||
|
var (sinPhi, cosPhi) = TFloat.SinCos(phi);
|
||||||
|
var H = MathV.Create<TFloat, float>(cosPhi * sinTheta, sinPhi * sinTheta, cosTheta);
|
||||||
|
|
||||||
|
// Tangent space to World space
|
||||||
|
var mask = TFloat.Abs(N.z) < 0.999f;
|
||||||
|
var up = MathV.Select(mask, MathV.Create<TFloat, float>(0.0f, 0.0f, 1.0f), MathV.Create<TFloat, float>(1.0f, 0.0f, 0.0f));
|
||||||
|
|
||||||
|
var tangent = MathV.Normalize(MathV.Cross(up, N));
|
||||||
|
var bitangent = MathV.Cross(N, tangent);
|
||||||
|
|
||||||
|
var sampleVec = (tangent * H.x) + (bitangent * H.y) + (N * H.z);
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
var u = TFloat.Atan2(dir.z, dir.x);
|
||||||
|
var v = TFloat.Asin(dir.y);
|
||||||
|
|
||||||
|
u = u / (2.0f * PI) + 0.5f;
|
||||||
|
v = v / PI + 0.5f;
|
||||||
|
return MathV.Create<TFloat, float>(u, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
var uv = DirToEquirectangularUV(dir);
|
||||||
|
|
||||||
|
// Nearest neighbor pixel coordinates
|
||||||
|
var px = (uv.x * (w - 1.0f)).Cast<TInt, int>();
|
||||||
|
var py = (uv.y * (h - 1.0f)).Cast<TInt, int>();
|
||||||
|
|
||||||
|
// Clamp
|
||||||
|
px = TInt.Clamp(px, TInt.Zero, w - 1);
|
||||||
|
py = TInt.Clamp(py, TInt.Zero, h - 1);
|
||||||
|
|
||||||
|
// Assuming float RGB array format
|
||||||
|
var idx = (py * w + px) * c;
|
||||||
|
return MathV.GatherVector3<TFloat, float>(img, idx.GetUnsafePtr(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Execute(int loopIndex, ref readonly JobExecutionContext ctx)
|
||||||
|
{
|
||||||
|
var m = 0;
|
||||||
|
while (m < numMipLevels - 1 && loopIndex >= pMipLevels[m + 1].offset)
|
||||||
|
{
|
||||||
|
m++;
|
||||||
|
}
|
||||||
|
|
||||||
|
var span = new ReadOnlySpan<MipLevel>(pMipLevels, numMipLevels);
|
||||||
|
var pLevel = &pMipLevels[m];
|
||||||
|
|
||||||
|
var w = pLevel->width;
|
||||||
|
var h = pLevel->height;
|
||||||
|
var pData = 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;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// For split-sum, we assume View and Reflection directions equal the Normal
|
||||||
|
var V = N;
|
||||||
|
var R = N;
|
||||||
|
|
||||||
|
var vN = MathV.Create<TFloat, float>(
|
||||||
|
TFloat.Create(N.x),
|
||||||
|
TFloat.Create(N.y),
|
||||||
|
TFloat.Create(N.z)
|
||||||
|
);
|
||||||
|
|
||||||
|
var vV = MathV.Create<TFloat, float>(
|
||||||
|
TFloat.Create(V.x),
|
||||||
|
TFloat.Create(V.y),
|
||||||
|
TFloat.Create(V.z)
|
||||||
|
);
|
||||||
|
|
||||||
|
var vPrefilteredColor = Vector3<TFloat, float>.Zero;
|
||||||
|
var vTotalWeight = TFloat.Zero;
|
||||||
|
|
||||||
|
// Monte Carlo Integration Loop
|
||||||
|
|
||||||
|
var vLuma = MathV.Create<TFloat, float>(0.2126f, 0.7152f, 0.0722f);
|
||||||
|
var dynamicSampleCount = (int)max(1.0f, _SAMPLE_COUNT * pLevel->roughness);
|
||||||
|
var dsc = TFloat.Create(dynamicSampleCount);
|
||||||
|
|
||||||
|
for (var i = 0; i < dynamicSampleCount; i += TFloat.LaneWidth)
|
||||||
|
{
|
||||||
|
var laneIndices = TFloat.Sequence(i, 1.0f);
|
||||||
|
var validLaneMask = laneIndices < dsc;
|
||||||
|
|
||||||
|
// Generate a Hammersley random sequence point
|
||||||
|
var Xi = Hammersley(laneIndices, dynamicSampleCount, pRadicalInverse_VdCLut);
|
||||||
|
|
||||||
|
// Get the halfway vector based on GGX NDF
|
||||||
|
var H = ImportanceSampleGGX(Xi, vN, pLevel->roughness);
|
||||||
|
|
||||||
|
// Calculate Light direction
|
||||||
|
var L = MathV.Reflect(-vV, H);
|
||||||
|
L = MathV.Normalize(L);
|
||||||
|
|
||||||
|
var NdotL = TFloat.Max(MathV.Dot(vN, L), TFloat.Zero);
|
||||||
|
var sampleColor = SampleEquirectangularMap(pImage, imageWidth, imageHeight, channelCount, L);
|
||||||
|
|
||||||
|
NdotL &= validLaneMask;
|
||||||
|
|
||||||
|
// The Karis Average Weight: 1 / (1 + luma)
|
||||||
|
// A normal sky pixel (luma 1.0) gets a weight of 0.5.
|
||||||
|
// A sun pixel (luma 1000.0) gets a tiny weight of ~0.001, naturally suppressing it.
|
||||||
|
// This introduce bias, but significantly reduces fireflies without needing solid angle sampling or cdf inversion.
|
||||||
|
// And since this is a mip generation step, a little bias is acceptable for much better performance and stability.
|
||||||
|
var luma = MathV.Dot(sampleColor, vLuma);
|
||||||
|
var fireflyWeight = TFloat.One / (TFloat.One + luma);
|
||||||
|
var finalWeight = NdotL * fireflyWeight;
|
||||||
|
|
||||||
|
vPrefilteredColor += sampleColor * finalWeight;
|
||||||
|
vTotalWeight += finalWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalWeight = 0.0f;
|
||||||
|
var prefilteredColor = float3(0, 0, 0);
|
||||||
|
|
||||||
|
for (var i = 0; i < TFloat.LaneWidth; i++)
|
||||||
|
{
|
||||||
|
prefilteredColor.x += vPrefilteredColor.x[i];
|
||||||
|
prefilteredColor.y += vPrefilteredColor.y[i];
|
||||||
|
prefilteredColor.z += vPrefilteredColor.z[i];
|
||||||
|
totalWeight += vTotalWeight[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Average the result
|
||||||
|
if (totalWeight > 0.0f)
|
||||||
|
{
|
||||||
|
prefilteredColor *= 1.0f / totalWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct VdCLutDisposeJob : IJob
|
||||||
|
{
|
||||||
|
public UnsafeArray<float> radicalInverse_VdCLut;
|
||||||
|
|
||||||
|
public void Execute(ref readonly JobExecutionContext ctx)
|
||||||
|
{
|
||||||
|
radicalInverse_VdCLut.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct MipLevelDisposeJob : IJob
|
||||||
|
{
|
||||||
|
public UnsafeArray<MipLevel> mipLevels;
|
||||||
|
|
||||||
|
public void Execute(ref readonly JobExecutionContext ctx)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < mipLevels.Length; i++)
|
||||||
|
{
|
||||||
|
mipLevels[i].data.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
mipLevels.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static float RadicalInverse_VdC(uint bits)
|
||||||
|
{
|
||||||
|
bits = (bits << 16) | (bits >> 16);
|
||||||
|
bits = ((bits & 0x55555555u) << 1) | ((bits & 0xAAAAAAAAu) >> 1);
|
||||||
|
bits = ((bits & 0x33333333u) << 2) | ((bits & 0xCCCCCCCCu) >> 2);
|
||||||
|
bits = ((bits & 0x0F0F0F0Fu) << 4) | ((bits & 0xF0F0F0F0u) >> 4);
|
||||||
|
bits = ((bits & 0x00FF00FFu) << 8) | ((bits & 0xFF00FF00u) >> 8);
|
||||||
|
return bits * 2.3283064365386963e-10f; // bits / 0x100000000
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JobHandle GenerateMipHDRI(JobScheduler scheduler, TextureAssetHandler.TextureInfo textureInfo, out UnsafeArray<MipLevel> mipLevels)
|
||||||
|
{
|
||||||
|
Logger.DebugAssert(textureInfo.isHDR, "GenerateMipHDRI should only be called for HDR textures.");
|
||||||
|
|
||||||
|
var totalMipLevels = (int)Math.Floor(Math.Log2(Math.Max(textureInfo.width, textureInfo.height))) + 1;
|
||||||
|
|
||||||
|
mipLevels = new UnsafeArray<MipLevel>(totalMipLevels, AllocationHandle.FreeList);
|
||||||
|
var radicalInverse_VdCLut = new UnsafeArray<float>(_SAMPLE_COUNT, AllocationHandle.FreeList);
|
||||||
|
|
||||||
|
for (var i = 0u; i < _SAMPLE_COUNT; i++)
|
||||||
|
{
|
||||||
|
radicalInverse_VdCLut[i] = RadicalInverse_VdC(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
int w, h;
|
||||||
|
var totalPixel = 0;
|
||||||
|
|
||||||
|
for (var i = 0; i < totalMipLevels; i++)
|
||||||
|
{
|
||||||
|
w = Math.Max(1, textureInfo.width >> i);
|
||||||
|
h = Math.Max(1, textureInfo.height >> i);
|
||||||
|
|
||||||
|
mipLevels[i] = new MipLevel
|
||||||
|
{
|
||||||
|
data = new UnsafeArray<float>(w * h * textureInfo.colorComponents, AllocationHandle.FreeList),
|
||||||
|
width = w,
|
||||||
|
height = h,
|
||||||
|
offset = totalPixel,
|
||||||
|
roughness = (float)i / (totalMipLevels - 1) // Linear roughness from 0 to 1 across mip levels
|
||||||
|
};
|
||||||
|
|
||||||
|
totalPixel += w * h;
|
||||||
|
}
|
||||||
|
|
||||||
|
JobHandle handle;
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
if (WideLane.IsSupported)
|
||||||
|
{
|
||||||
|
var job = new GGXMipGenerationJobSPMD<WideLane<float>, WideLane<int>>
|
||||||
|
{
|
||||||
|
pImage = (float*)textureInfo.pixelData,
|
||||||
|
pMipLevels = (MipLevel*)mipLevels.GetUnsafePtr(),
|
||||||
|
pRadicalInverse_VdCLut = (float*)radicalInverse_VdCLut.GetUnsafePtr(),
|
||||||
|
imageWidth = textureInfo.width,
|
||||||
|
imageHeight = textureInfo.height,
|
||||||
|
numMipLevels = totalMipLevels,
|
||||||
|
channelCount = textureInfo.colorComponents,
|
||||||
|
};
|
||||||
|
|
||||||
|
handle = scheduler.ScheduleParallelFor(in job, totalPixel, 64);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var job = new GGXMipGenerationJobSPMD<ScalarLane<float>, ScalarLane<int>>
|
||||||
|
{
|
||||||
|
pImage = (float*)textureInfo.pixelData,
|
||||||
|
pMipLevels = (MipLevel*)mipLevels.GetUnsafePtr(),
|
||||||
|
pRadicalInverse_VdCLut = (float*)radicalInverse_VdCLut.GetUnsafePtr(),
|
||||||
|
imageWidth = textureInfo.width,
|
||||||
|
imageHeight = textureInfo.height,
|
||||||
|
numMipLevels = totalMipLevels,
|
||||||
|
channelCount = textureInfo.colorComponents,
|
||||||
|
};
|
||||||
|
|
||||||
|
handle = scheduler.ScheduleParallelFor(in job, totalPixel, 64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!handle.IsValid)
|
||||||
|
{
|
||||||
|
return JobHandle.Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
var disposeJob = new VdCLutDisposeJob
|
||||||
|
{
|
||||||
|
radicalInverse_VdCLut = radicalInverse_VdCLut
|
||||||
|
};
|
||||||
|
|
||||||
|
var disposeHandle = scheduler.Schedule(in disposeJob, handle);
|
||||||
|
Logger.DebugAssert(disposeHandle.IsValid, "Dispose job handle is invalid.");
|
||||||
|
|
||||||
|
return disposeHandle;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
using Ghost.Core;
|
using Ghost.Core;
|
||||||
using Ghost.Editor.Core.AssetHandler;
|
using Ghost.Editor.Core.Assets;
|
||||||
using Ghost.Editor.Core.Services;
|
using Ghost.Editor.Core.Services;
|
||||||
using Ghost.Engine.AssetLoader;
|
using Ghost.Engine.AssetLoader;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Ghost.Editor.Core.AssetHandler;
|
using Ghost.Editor.Core.Assets;
|
||||||
using Microsoft.Data.Sqlite;
|
using Microsoft.Data.Sqlite;
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.Services;
|
namespace Ghost.Editor.Core.Services;
|
||||||
@@ -17,9 +17,12 @@ public sealed partial class AssetCatalog : IDisposable
|
|||||||
private readonly SqliteCommand _cmdGetPath;
|
private readonly SqliteCommand _cmdGetPath;
|
||||||
private readonly SqliteCommand _cmdUpsert;
|
private readonly SqliteCommand _cmdUpsert;
|
||||||
private readonly SqliteCommand _cmdDelete;
|
private readonly SqliteCommand _cmdDelete;
|
||||||
|
|
||||||
private readonly SqliteCommand _cmdGetHandlerTypeId;
|
private readonly SqliteCommand _cmdGetHandlerTypeId;
|
||||||
private readonly SqliteCommand _cmdGetReferencers;
|
private readonly SqliteCommand _cmdGetReferencers;
|
||||||
private readonly SqliteCommand _cmdGetDependencies;
|
private readonly SqliteCommand _cmdGetDependencies;
|
||||||
|
private readonly SqliteCommand _cmdGetImportedAt;
|
||||||
|
|
||||||
private readonly SqliteCommand _cmdInsertDep;
|
private readonly SqliteCommand _cmdInsertDep;
|
||||||
private readonly SqliteCommand _cmdClearDeps;
|
private readonly SqliteCommand _cmdClearDeps;
|
||||||
private readonly SqliteCommand _cmdEnumerate;
|
private readonly SqliteCommand _cmdEnumerate;
|
||||||
@@ -48,16 +51,22 @@ public sealed partial class AssetCatalog : IDisposable
|
|||||||
_cmdGetGuid = CreateCommand("SELECT guid FROM assets WHERE source_path = @path");
|
_cmdGetGuid = CreateCommand("SELECT guid FROM assets WHERE source_path = @path");
|
||||||
_cmdGetPath = CreateCommand("SELECT source_path FROM assets WHERE guid = @guid");
|
_cmdGetPath = CreateCommand("SELECT source_path FROM assets WHERE guid = @guid");
|
||||||
_cmdGetHandlerTypeId = CreateCommand("SELECT handler_type_id FROM assets WHERE guid = @guid");
|
_cmdGetHandlerTypeId = CreateCommand("SELECT handler_type_id FROM assets WHERE guid = @guid");
|
||||||
|
_cmdGetImportedAt = CreateCommand("SELECT imported_at_ms FROM assets WHERE guid = @guid");
|
||||||
|
|
||||||
_cmdUpsert = CreateCommand(@"
|
_cmdUpsert = CreateCommand(@"
|
||||||
INSERT INTO assets (guid, source_path, handler_type_id, handler_version)
|
INSERT INTO assets (guid, source_path, handler_type_id, handler_version, content_hash, settings_hash, imported_at_ms)
|
||||||
VALUES (@guid, @path, @handler_id, @version)
|
VALUES (@guid, @path, @handler_id, @version, @content_hash, @settings_hash, @imported_at_ms)
|
||||||
ON CONFLICT(guid) DO UPDATE SET
|
ON CONFLICT(guid) DO UPDATE SET
|
||||||
source_path = excluded.source_path,
|
source_path = excluded.source_path,
|
||||||
handler_type_id = excluded.handler_type_id,
|
handler_type_id = excluded.handler_type_id,
|
||||||
handler_version = excluded.handler_version");
|
handler_version = excluded.handler_version,
|
||||||
|
content_hash = excluded.content_hash,
|
||||||
|
settings_hash = excluded.settings_hash,
|
||||||
|
imported_at_ms = excluded.imported_at_ms");
|
||||||
_cmdDelete = CreateCommand("DELETE FROM assets WHERE guid = @guid");
|
_cmdDelete = CreateCommand("DELETE FROM assets WHERE guid = @guid");
|
||||||
_cmdGetReferencers = CreateCommand("SELECT from_guid FROM dependencies WHERE to_guid = @guid");
|
_cmdGetReferencers = CreateCommand("SELECT from_guid FROM dependencies WHERE to_guid = @guid");
|
||||||
_cmdGetDependencies = CreateCommand("SELECT to_guid FROM dependencies WHERE from_guid = @guid");
|
_cmdGetDependencies = CreateCommand("SELECT to_guid FROM dependencies WHERE from_guid = @guid");
|
||||||
|
|
||||||
_cmdInsertDep = CreateCommand("INSERT INTO dependencies (from_guid, to_guid) VALUES (@from, @to)");
|
_cmdInsertDep = CreateCommand("INSERT INTO dependencies (from_guid, to_guid) VALUES (@from, @to)");
|
||||||
_cmdClearDeps = CreateCommand("DELETE FROM dependencies WHERE from_guid = @guid");
|
_cmdClearDeps = CreateCommand("DELETE FROM dependencies WHERE from_guid = @guid");
|
||||||
_cmdEnumerate = CreateCommand("SELECT guid, source_path FROM assets");
|
_cmdEnumerate = CreateCommand("SELECT guid, source_path FROM assets");
|
||||||
@@ -135,6 +144,9 @@ public sealed partial class AssetCatalog : IDisposable
|
|||||||
_cmdUpsert.Parameters.AddWithValue("@path", ToUniversalPath(sourcePath));
|
_cmdUpsert.Parameters.AddWithValue("@path", ToUniversalPath(sourcePath));
|
||||||
_cmdUpsert.Parameters.AddWithValue("@handler_id", meta.HandlerTypeId?.ToByteArray() ?? (object)DBNull.Value);
|
_cmdUpsert.Parameters.AddWithValue("@handler_id", meta.HandlerTypeId?.ToByteArray() ?? (object)DBNull.Value);
|
||||||
_cmdUpsert.Parameters.AddWithValue("@version", meta.HandlerVersion);
|
_cmdUpsert.Parameters.AddWithValue("@version", meta.HandlerVersion);
|
||||||
|
_cmdUpsert.Parameters.AddWithValue("@content_hash", meta.ContentHash ?? (object)DBNull.Value);
|
||||||
|
_cmdUpsert.Parameters.AddWithValue("@settings_hash", meta.SettingsHash ?? (object)DBNull.Value);
|
||||||
|
_cmdUpsert.Parameters.AddWithValue("@imported_at_ms", meta.LastImportedUtc?.Ticks ?? (object)DBNull.Value);
|
||||||
_cmdUpsert.ExecuteNonQuery();
|
_cmdUpsert.ExecuteNonQuery();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,6 +169,20 @@ public sealed partial class AssetCatalog : IDisposable
|
|||||||
return result is byte[] bytes ? new Guid(bytes) : Guid.Empty;
|
return result is byte[] bytes ? new Guid(bytes) : Guid.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DateTime? GetImportedAt(Guid guid)
|
||||||
|
{
|
||||||
|
_cmdGetImportedAt.Parameters.Clear();
|
||||||
|
_cmdGetImportedAt.Parameters.AddWithValue("@guid", guid.ToByteArray());
|
||||||
|
var result = _cmdGetImportedAt.ExecuteScalar();
|
||||||
|
|
||||||
|
if (result is long ticks)
|
||||||
|
{
|
||||||
|
return new DateTime(ticks, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public void SetDependencies(Guid assetId, ReadOnlySpan<Guid> dependencies)
|
public void SetDependencies(Guid assetId, ReadOnlySpan<Guid> dependencies)
|
||||||
{
|
{
|
||||||
lock (_writeLock)
|
lock (_writeLock)
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
using Ghost.Core;
|
using Ghost.Core;
|
||||||
using Ghost.Core.Utilities;
|
using Ghost.Core.Utilities;
|
||||||
using Ghost.Editor.Core.AssetHandler;
|
using Ghost.Editor.Core.Assets;
|
||||||
using Ghost.Editor.Core.Contracts;
|
using Ghost.Editor.Core.Contracts;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.IO.MemoryMappedFiles;
|
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.Services;
|
namespace Ghost.Editor.Core.Services;
|
||||||
|
|
||||||
@@ -21,6 +20,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
|
|||||||
|
|
||||||
private readonly ConcurrentDictionary<string, bool> _ignoreMetaWrites;
|
private readonly ConcurrentDictionary<string, bool> _ignoreMetaWrites;
|
||||||
private readonly ConcurrentHashSet<Guid> _dirtyAssets;
|
private readonly ConcurrentHashSet<Guid> _dirtyAssets;
|
||||||
|
private readonly ConcurrentDictionary<string, CancellationTokenSource> _eventDebouncers;
|
||||||
|
|
||||||
public event EventHandler<AssetChangedEventArgs>? OnAssetChanged;
|
public event EventHandler<AssetChangedEventArgs>? OnAssetChanged;
|
||||||
public event EventHandler<Guid>? OnAssetImported
|
public event EventHandler<Guid>? OnAssetImported
|
||||||
@@ -41,6 +41,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
|
|||||||
|
|
||||||
_ignoreMetaWrites = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
|
_ignoreMetaWrites = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
|
||||||
_dirtyAssets = new ConcurrentHashSet<Guid>();
|
_dirtyAssets = new ConcurrentHashSet<Guid>();
|
||||||
|
_eventDebouncers = new ConcurrentDictionary<string, CancellationTokenSource>();
|
||||||
|
|
||||||
SyncCatalogWithDisk();
|
SyncCatalogWithDisk();
|
||||||
|
|
||||||
@@ -48,7 +49,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
|
|||||||
{
|
{
|
||||||
IncludeSubdirectories = true,
|
IncludeSubdirectories = true,
|
||||||
EnableRaisingEvents = true,
|
EnableRaisingEvents = true,
|
||||||
NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.DirectoryName
|
NotifyFilter = NotifyFilters.LastWrite
|
||||||
};
|
};
|
||||||
|
|
||||||
_watcher.Created += OnFileSystemEvent;
|
_watcher.Created += OnFileSystemEvent;
|
||||||
@@ -72,7 +73,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
|
|||||||
var meta = AssetMetaIO.ReadAsync(metaPath).AsTask().Result;
|
var meta = AssetMetaIO.ReadAsync(metaPath).AsTask().Result;
|
||||||
if (meta != null)
|
if (meta != null)
|
||||||
{
|
{
|
||||||
var sourceRelative = AssetMetaIO.GetSourcePath(Path.GetRelativePath(EditorApplication.AssetsFolderPath, metaPath));
|
var sourceRelative = AssetMetaIO.GetSourcePath(Path.GetRelativePath(EditorApplication.ProjectPath, metaPath));
|
||||||
_catalog.Upsert(meta, sourceRelative);
|
_catalog.Upsert(meta, sourceRelative);
|
||||||
foundGuids.Add(meta.Guid);
|
foundGuids.Add(meta.Guid);
|
||||||
}
|
}
|
||||||
@@ -90,57 +91,86 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
|
|||||||
private async void OnFileSystemEvent(object sender, FileSystemEventArgs e)
|
private async void OnFileSystemEvent(object sender, FileSystemEventArgs e)
|
||||||
{
|
{
|
||||||
var ext = Path.GetExtension(e.FullPath);
|
var ext = Path.GetExtension(e.FullPath);
|
||||||
var relativePath = Path.GetRelativePath(EditorApplication.AssetsFolderPath, e.FullPath);
|
|
||||||
|
|
||||||
if (_ignoreMetaWrites.TryRemove(e.FullPath, out _))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ext is ".tmp" or ".gtemp")
|
if (ext is ".tmp" or ".gtemp")
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_eventDebouncers.TryGetValue(e.FullPath, out var existingCts))
|
||||||
|
{
|
||||||
|
existingCts.Cancel();
|
||||||
|
existingCts.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
_eventDebouncers[e.FullPath] = cts;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Add a small delay to group rapid sequential triggers together (250ms is usually sufficient)
|
||||||
|
await Task.Delay(250, cts.Token);
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
// A newer event for this file interrupted us; abort this duplicate handling
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_eventDebouncers.TryGetValue(e.FullPath, out var currentCts) && currentCts == cts)
|
||||||
|
{
|
||||||
|
_eventDebouncers.TryRemove(e.FullPath, out _);
|
||||||
|
cts.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_ignoreMetaWrites.TryRemove(e.FullPath, out _))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var relativePath = Path.GetRelativePath(EditorApplication.ProjectPath, e.FullPath);
|
||||||
|
var fileExists = File.Exists(e.FullPath);
|
||||||
|
|
||||||
if (ext == AssetMetaIO.META_EXTENSION)
|
if (ext == AssetMetaIO.META_EXTENSION)
|
||||||
{
|
{
|
||||||
if (e.ChangeType == WatcherChangeTypes.Changed || e.ChangeType == WatcherChangeTypes.Created)
|
if (fileExists)
|
||||||
{
|
{
|
||||||
var meta = AssetMetaIO.ReadAsync(e.FullPath).AsTask().Result;
|
var meta = await AssetMetaIO.ReadAsync(e.FullPath);
|
||||||
if (meta != null)
|
if (meta != null)
|
||||||
{
|
{
|
||||||
_catalog.Upsert(meta, AssetMetaIO.GetSourcePath(relativePath));
|
_catalog.Upsert(meta, AssetMetaIO.GetSourcePath(relativePath));
|
||||||
await _importCoordinator.EnqueueAsync(new ImportJob(meta.Guid, AssetMetaIO.GetSourcePath(relativePath), e.FullPath, ImportReason.SettingsChanged));
|
await _importCoordinator.EnqueueAsync(new ImportJob(meta.Guid, AssetMetaIO.GetSourcePath(relativePath), relativePath, ImportReason.SettingsChanged));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var changeType = AssetChangeType.None;
|
var changeType = AssetChangeType.None;
|
||||||
if (e.ChangeType == WatcherChangeTypes.Created)
|
var guid = _catalog.GetGuid(relativePath);
|
||||||
|
|
||||||
|
if (!fileExists)
|
||||||
{
|
{
|
||||||
await HandleNewSourceFileAsync(relativePath);
|
// The file is no longer on disk. Wait safely completed.
|
||||||
changeType = AssetChangeType.Created;
|
|
||||||
}
|
|
||||||
else if (e.ChangeType == WatcherChangeTypes.Changed)
|
|
||||||
{
|
|
||||||
var guid = _catalog.GetGuid(relativePath);
|
|
||||||
if (guid != Guid.Empty)
|
|
||||||
{
|
|
||||||
await _importCoordinator.EnqueueAsync(new ImportJob(guid, relativePath, AssetMetaIO.GetMetaPath(e.FullPath), ImportReason.SourceChanged));
|
|
||||||
changeType = AssetChangeType.Modified;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (e.ChangeType == WatcherChangeTypes.Deleted)
|
|
||||||
{
|
|
||||||
var guid = _catalog.GetGuid(relativePath);
|
|
||||||
if (guid != Guid.Empty)
|
if (guid != Guid.Empty)
|
||||||
{
|
{
|
||||||
_catalog.Remove(guid);
|
_catalog.Remove(guid);
|
||||||
changeType = AssetChangeType.Deleted;
|
changeType = AssetChangeType.Deleted;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (guid == Guid.Empty)
|
||||||
|
{
|
||||||
|
// The file exists but isn't located inside our catalog yet -> Essentially a Creation
|
||||||
|
await HandleNewSourceFileAsync(relativePath);
|
||||||
|
changeType = AssetChangeType.Created;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// The file exists and is tracked in the catalog, but triggered an event -> Modification
|
||||||
|
await _importCoordinator.EnqueueAsync(new ImportJob(guid, relativePath, AssetMetaIO.GetMetaPath(relativePath), ImportReason.SourceChanged));
|
||||||
|
changeType = AssetChangeType.Modified;
|
||||||
|
}
|
||||||
|
|
||||||
if (changeType != AssetChangeType.None)
|
if (changeType != AssetChangeType.None)
|
||||||
{
|
{
|
||||||
@@ -150,8 +180,8 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
|
|||||||
|
|
||||||
private void OnFileSystemRenameEvent(object sender, RenamedEventArgs e)
|
private void OnFileSystemRenameEvent(object sender, RenamedEventArgs e)
|
||||||
{
|
{
|
||||||
var oldRelative = Path.GetRelativePath(EditorApplication.AssetsFolderPath, e.OldFullPath);
|
var oldRelative = Path.GetRelativePath(EditorApplication.ProjectPath, e.OldFullPath);
|
||||||
var newRelative = Path.GetRelativePath(EditorApplication.AssetsFolderPath, e.FullPath);
|
var newRelative = Path.GetRelativePath(EditorApplication.ProjectPath, e.FullPath);
|
||||||
|
|
||||||
var guid = _catalog.GetGuid(oldRelative);
|
var guid = _catalog.GetGuid(oldRelative);
|
||||||
if (guid != Guid.Empty)
|
if (guid != Guid.Empty)
|
||||||
@@ -276,8 +306,7 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
|
|||||||
return Result.Failure("Meta file does not exist.");
|
return Result.Failure("Meta file does not exist.");
|
||||||
}
|
}
|
||||||
|
|
||||||
await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
return await handler.LoadAssetAsync(path, id, meta.Settings, token);
|
||||||
return await handler.LoadAssetAsync(stream, id, meta.Settings, token);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -305,9 +334,8 @@ internal sealed class AssetRegistry : IAssetRegistry, IDisposable
|
|||||||
return Result.Failure("No Avaliable handler type.");
|
return Result.Failure("No Avaliable handler type.");
|
||||||
}
|
}
|
||||||
|
|
||||||
await using var stream = new FileStream(path, FileMode.Open, FileAccess.Write, FileShare.None);
|
|
||||||
// This will trigger the fsw and reimport automatically.
|
// This will trigger the fsw and reimport automatically.
|
||||||
return await handler.SaveAssetAsync(stream, asset, token);
|
return await handler.SaveAssetAsync(path, asset, token);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using Ghost.Core;
|
using Ghost.Core;
|
||||||
using Ghost.Editor.Core.AssetHandler;
|
using Ghost.Editor.Core.Assets;
|
||||||
using Ghost.Editor.Core.Contracts;
|
using Ghost.Editor.Core.Contracts;
|
||||||
using Ghost.Engine;
|
using Ghost.Engine;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using Ghost.Core;
|
using Ghost.Core;
|
||||||
using Ghost.Editor.Core.AssetHandler;
|
using Ghost.Editor.Core.Assets;
|
||||||
using System.IO.Hashing;
|
using System.IO.Hashing;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@@ -79,13 +79,14 @@ internal sealed partial class ImportCoordinator : IDisposable
|
|||||||
var fileName = $"{assetGuid:N}{IMPORTED_EXTENSION}";
|
var fileName = $"{assetGuid:N}{IMPORTED_EXTENSION}";
|
||||||
var folderName = fileName.Substring(0, 2);
|
var folderName = fileName.Substring(0, 2);
|
||||||
|
|
||||||
var finalPath = Path.Combine(EditorApplication.LibraryImportsFolderPath, folderName, fileName);
|
var importsFolder = Path.Combine(EditorApplication.LibraryImportsFolderPath, folderName);
|
||||||
Directory.CreateDirectory(finalPath);
|
var finalPath = Path.Combine(importsFolder, fileName);
|
||||||
|
Directory.CreateDirectory(importsFolder);
|
||||||
|
|
||||||
return finalPath;
|
return finalPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async ValueTask ProcessImportAsync(ImportJob job, CancellationToken token)
|
private async ValueTask ProcessImportAsync(ImportJob job, CancellationToken token)
|
||||||
{
|
{
|
||||||
var meta = await AssetMetaIO.ReadAsync(job.MetaPath, token);
|
var meta = await AssetMetaIO.ReadAsync(job.MetaPath, token);
|
||||||
if (meta is null)
|
if (meta is null)
|
||||||
@@ -114,11 +115,7 @@ internal sealed partial class ImportCoordinator : IDisposable
|
|||||||
if (handler is IImportableAssetHandler importable)
|
if (handler is IImportableAssetHandler importable)
|
||||||
{
|
{
|
||||||
var targetPath = GetImportedAssetPath(job.AssetGuid);
|
var targetPath = GetImportedAssetPath(job.AssetGuid);
|
||||||
|
importResult = await importable.ImportAsync(job.SourcePath, targetPath, job.AssetGuid, meta.Settings, token);
|
||||||
await using var sourceStream = new FileStream(job.SourcePath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
|
||||||
await using var targetStream = new FileStream(targetPath, FileMode.Create, FileAccess.Write, FileShare.None);
|
|
||||||
|
|
||||||
importResult = await importable.ImportAsync(sourceStream, targetStream, job.AssetGuid, meta.Settings, token);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (importResult.IsSuccess)
|
if (importResult.IsSuccess)
|
||||||
@@ -129,6 +126,8 @@ internal sealed partial class ImportCoordinator : IDisposable
|
|||||||
meta.LastImportedUtc = DateTime.UtcNow;
|
meta.LastImportedUtc = DateTime.UtcNow;
|
||||||
|
|
||||||
await AssetMetaIO.WriteAsync(job.MetaPath, meta, token);
|
await AssetMetaIO.WriteAsync(job.MetaPath, meta, token);
|
||||||
|
|
||||||
|
OnImportCompleted?.Invoke(null, job.AssetGuid);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -66,13 +66,13 @@ internal static class ActivationHandler
|
|||||||
|
|
||||||
AllocationManager.Initialize(opts);
|
AllocationManager.Initialize(opts);
|
||||||
|
|
||||||
var assetRegistry = App.GetService<IAssetRegistry>();
|
//var assetRegistry = App.GetService<IAssetRegistry>();
|
||||||
var engineCore = App.GetService<EngineCore>();
|
//var engineCore = App.GetService<EngineCore>();
|
||||||
|
|
||||||
assetRegistry.OnAssetImported += (sender, e) =>
|
//assetRegistry.OnAssetImported += (sender, e) =>
|
||||||
{
|
//{
|
||||||
engineCore.AssetManager.ReimportAsset(e);
|
// engineCore.AssetManager.ReimportAsset(e);
|
||||||
};
|
//};
|
||||||
|
|
||||||
return ValueTask.CompletedTask;
|
return ValueTask.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ internal partial class ExplorerItem : ObservableObject
|
|||||||
public ExplorerItem(string name, string path, bool isDirectory, AssetType assetType = AssetType.Unknown)
|
public ExplorerItem(string name, string path, bool isDirectory, AssetType assetType = AssetType.Unknown)
|
||||||
{
|
{
|
||||||
Name = name;
|
Name = name;
|
||||||
Path = PathUtility.Normalize(path);
|
Path = path;
|
||||||
IsDirectory = isDirectory;
|
IsDirectory = isDirectory;
|
||||||
AssetType = assetType;
|
AssetType = assetType;
|
||||||
|
|
||||||
|
|||||||
@@ -62,13 +62,13 @@ internal partial class ContentBrowserViewModel : ObservableObject
|
|||||||
|
|
||||||
private void OnAssetChanged(object? sender, AssetChangedEventArgs e)
|
private void OnAssetChanged(object? sender, AssetChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (Path.GetExtension(e.AssetPath) == FileExtensions.META_FILE_EXTENSION)
|
if (e.AssetPath.EndsWith(FileExtensions.META_FILE_EXTENSION))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var fullPath = PathUtility.Normalize(Path.Combine(EditorApplication.AssetsFolderPath, e.AssetPath));
|
var fullPath = PathUtility.Normalize(e.AssetPath);
|
||||||
var dirPath = PathUtility.Normalize(Path.GetDirectoryName(fullPath));
|
var dirPath = Path.GetDirectoryName(fullPath);
|
||||||
|
|
||||||
if (string.Equals(dirPath, CurrentDirectoryPath, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(dirPath, CurrentDirectoryPath, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
@@ -78,7 +78,7 @@ internal partial class ContentBrowserViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
if (e.ChangeType == AssetChangeType.Renamed && e.OldAssetPath != null)
|
if (e.ChangeType == AssetChangeType.Renamed && e.OldAssetPath != null)
|
||||||
{
|
{
|
||||||
var oldFullPath = PathUtility.Normalize(Path.Combine(EditorApplication.AssetsFolderPath, e.OldAssetPath));
|
var oldFullPath = PathUtility.Normalize(e.OldAssetPath);
|
||||||
var oldItem = Files.FirstOrDefault(f => string.Equals(f.Path, oldFullPath, StringComparison.OrdinalIgnoreCase));
|
var oldItem = Files.FirstOrDefault(f => string.Equals(f.Path, oldFullPath, StringComparison.OrdinalIgnoreCase));
|
||||||
if (oldItem != null) Files.Remove(oldItem);
|
if (oldItem != null) Files.Remove(oldItem);
|
||||||
}
|
}
|
||||||
@@ -138,7 +138,7 @@ internal partial class ContentBrowserViewModel : ObservableObject
|
|||||||
|
|
||||||
foreach (var file in Directory.EnumerateFiles(path))
|
foreach (var file in Directory.EnumerateFiles(path))
|
||||||
{
|
{
|
||||||
if (Path.GetExtension(file) == FileExtensions.META_FILE_EXTENSION)
|
if (file.EndsWith(FileExtensions.META_FILE_EXTENSION))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Misaki.HighPerformance.Mathematics" Version="1.3.3" />
|
<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="System.IO.Hashing" Version="10.0.7" />
|
||||||
<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.26100.6" />
|
<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.26100.6" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ public struct TextureContentHeader
|
|||||||
{
|
{
|
||||||
public uint width;
|
public uint width;
|
||||||
public uint height;
|
public uint height;
|
||||||
public uint depth;
|
public uint bpc;
|
||||||
public uint mipLevels;
|
public uint mipLevels;
|
||||||
public uint dimension; // 1 for 1D, 2 for 2D, 3 for 3D
|
public uint dimension; // 1 for 1D, 2 for 2D, 3 for 3D
|
||||||
public uint colorComponents;
|
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
|
return colorComponents switch
|
||||||
{
|
{
|
||||||
1 => depth switch
|
1 => bpc switch
|
||||||
{
|
{
|
||||||
8 => TextureFormat.R8_UNorm,
|
8 => TextureFormat.R8_UNorm,
|
||||||
16 => TextureFormat.R16_UNorm,
|
16 => TextureFormat.R16_UNorm,
|
||||||
32 => TextureFormat.R32_UInt,
|
32 => TextureFormat.R32_UInt,
|
||||||
_ => TextureFormat.Unknown,
|
_ => TextureFormat.Unknown,
|
||||||
},
|
},
|
||||||
2 => depth switch
|
2 => bpc switch
|
||||||
{
|
{
|
||||||
8 => TextureFormat.R8G8_UNorm,
|
8 => TextureFormat.R8G8_UNorm,
|
||||||
16 => TextureFormat.R16G16_UNorm,
|
16 => TextureFormat.R16G16_UNorm,
|
||||||
32 => TextureFormat.R32G32_Float,
|
32 => TextureFormat.R32G32_Float,
|
||||||
_ => TextureFormat.Unknown,
|
_ => TextureFormat.Unknown,
|
||||||
},
|
},
|
||||||
3 or 4 => depth switch
|
3 or 4 => bpc switch
|
||||||
{
|
{
|
||||||
8 => TextureFormat.R8G8B8A8_UNorm,
|
8 => TextureFormat.R8G8B8A8_UNorm,
|
||||||
16 => TextureFormat.R16G16B16A16_Float,
|
16 => TextureFormat.R16G16B16A16_Float,
|
||||||
@@ -91,7 +91,7 @@ internal partial class AssetEntry
|
|||||||
Height = header.height,
|
Height = header.height,
|
||||||
MipLevels = header.mipLevels,
|
MipLevels = header.mipLevels,
|
||||||
Slice = 1,
|
Slice = 1,
|
||||||
Format = GetTextureFormat(header.depth, header.colorComponents),
|
Format = GetTextureFormat(header.bpc, header.colorComponents),
|
||||||
Dimension = (TextureDimension)header.dimension,
|
Dimension = (TextureDimension)header.dimension,
|
||||||
Usage = TextureUsage.ShaderResource,
|
Usage = TextureUsage.ShaderResource,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -88,3 +88,73 @@ internal static partial class {registerTypeName}
|
|||||||
return null;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ using Misaki.HighPerformance.LowLevel.Collections;
|
|||||||
using Misaki.HighPerformance.Mathematics;
|
using Misaki.HighPerformance.Mathematics;
|
||||||
using Misaki.HighPerformance.Mathematics.Geometry;
|
using Misaki.HighPerformance.Mathematics.Geometry;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Ghost.Graphics.Core;
|
namespace Ghost.Graphics.Core;
|
||||||
@@ -203,158 +202,6 @@ public struct Mesh : IResourceReleasable
|
|||||||
_meshletData.Dispose();
|
_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)
|
public void ReleaseResource(IResourceDatabase database)
|
||||||
{
|
{
|
||||||
ReleaseCpuResources();
|
ReleaseCpuResources();
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ public readonly unsafe ref struct RenderContext
|
|||||||
|
|
||||||
if (staticMesh)
|
if (staticMesh)
|
||||||
{
|
{
|
||||||
meshData.CookMeshlets();
|
//meshData.CookMeshlets();
|
||||||
UploadMeshlets(mesh);
|
UploadMeshlets(mesh);
|
||||||
meshData.ReleaseCpuResources();
|
meshData.ReleaseCpuResources();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Ghost.MicroTest;
|
using Ghost.MicroTest;
|
||||||
using Ghost.Test.Core;
|
using Ghost.Test.Core;
|
||||||
|
|
||||||
TestRunner.Run<StbIBindingTest>();
|
TestRunner.Run<NvttBindingTest>();
|
||||||
@@ -7,7 +7,7 @@ using System.Numerics;
|
|||||||
|
|
||||||
//return;
|
//return;
|
||||||
#if true
|
#if true
|
||||||
var result = DSLShaderCompiler.CompileComputeShader("F:\\csharp\\GhostEngine\\src\\Runtime\\Ghost.Graphics\\TestCompute.gcomp");
|
var result = DSLShaderCompiler.CompileComputeShaderCode("F:\\csharp\\GhostEngine\\src\\Runtime\\Ghost.Graphics\\TestCompute.gcomp");
|
||||||
if (result.IsFailure)
|
if (result.IsFailure)
|
||||||
{
|
{
|
||||||
Console.WriteLine(result.Message);
|
Console.WriteLine(result.Message);
|
||||||
|
|||||||
Reference in New Issue
Block a user