#nullable enable
using SharpDX.Direct3D11;
using SharpDX.WIC;
using SharpGLTF.Schema2;
using T3.Core.Rendering;
using T3.Core.Rendering.Material;
using T3.Core.Utils;
using T3.Core.Utils.Geometry;
using Scene = SharpGLTF.Schema2.Scene;
namespace Lib.render.scene;
///
/// Uses the GltfSharp library to load a gltf file and convert it into a .
///
[Guid("00618c91-f39a-44ea-b9d8-175c996460dc")]
public class LoadGltfScene : Instance
, IDescriptiveFilename, IStatusProvider
{
[Output(Guid = "C2D28EBE-E235-4234-8234-EB56E87ED9AF")]
public readonly Slot ResultSetup = new();
[Output(Guid = "9891EA8D-7DE7-4DD1-A61E-337ADC569EF3")]
public readonly Slot Mesh = new();
[Output(Guid = "F33EC4C1-F07B-46B3-BEC7-34E91B8312EF")]
public readonly Slot Material = new();
public LoadGltfScene()
{
_resource = new Resource(Path, OnFileChanged);
_resource.AddDependentSlots(ResultSetup, Mesh, Material);
ResultSetup.UpdateAction += Update;
Mesh.UpdateAction += Update;
}
private bool OnFileChanged(FileResource file,
SceneSetup? currentValue,
[NotNullWhen(true)]out SceneSetup? newValue,
[NotNullWhen(false)]out string? failureReason)
{
LoadFile(file.AbsolutePath, 0, true);
// if (BmFontDescription.TryInitializeFromFile(file.AbsolutePath, out newValue))
// {
failureReason = null;
newValue = ResultSetup.Value!;
// return true;
// }
return true;
// failureReason = "Failed to load font from file";
// return false;
}
private void Update(EvaluationContext context)
{
_lastErrorMessage = string.Empty;
var materialNeedsUpdate = OffsetRoughness.DirtyFlag.IsDirty || OffsetMetallic.DirtyFlag.IsDirty;
_offsetRoughness = OffsetRoughness.GetValue(context);
_offsetMetallic = OffsetMetallic.GetValue(context);
var meshChildIndex = MeshChildIndex.GetValue(context);
var sceneSetup = Setup.GetValue(context);
if (sceneSetup == null || Setup.Input.IsDefault)
{
sceneSetup = new SceneSetup();
Setup.SetTypedInputValue(sceneSetup);
}
var filePath = Path.GetValue(context);
_updateTriggered = TriggerUpdate.GetValue(context);
_updateTriggered |= MathUtils.WasChanged(CombineBuffer.GetValue(context), ref _combineBuffer);
TriggerUpdate.SetTypedInputValue(false);
// if (filePath == null || !File.Exists(filePath))
// {
// _lastErrorMessage = $"Gltf File not found: {filePath}";
// return;
// }
LoadFile(filePath, meshChildIndex, materialNeedsUpdate);
}
private void LoadFile(string? filePath, int meshChildIndex, bool materialNeedsUpdate)
{
if (filePath == null)
return;
if (LoadFileIfRequired(filePath, out var newSetup))
{
ResultSetup.Value?.Dispose();
// TODO: Need to clarify of null reference types are allowed for slot values
Setup.SetTypedInputValue(newSetup!); //TODO: this is weird.
ResultSetup.Value = newSetup!;
}
if (ResultSetup?.Value?.Dispatches != null && ResultSetup.Value.Dispatches.Count > 0)
{
var dispatchCount = ResultSetup.Value.Dispatches.Count;
var index = meshChildIndex.Mod(dispatchCount);
Mesh.Value = ResultSetup.Value.Dispatches[index].MeshBuffers;
Material.Value = ResultSetup.Value.Dispatches[index].Material;
}
if (materialNeedsUpdate && ResultSetup?.Value?.Dispatches != null && ResultSetup.Value.Dispatches.Count > 0)
{
foreach (var dispatch in ResultSetup.Value.Dispatches)
{
if (dispatch.Material == null)
continue;
dispatch.Material.Parameters.Roughness = _offsetRoughness;
dispatch.Material.Parameters.Metal = _offsetMetallic;
dispatch.Material.UpdateParameterBuffer();
}
}
}
protected override void Dispose(bool isDisposing)
{
if (!isDisposing)
return;
ResultSetup.Value?.Dispose();
Log.Debug("Destroying LoadGltfScene");
}
private bool LoadFileIfRequired(string path, out SceneSetup? sceneSetup)
{
sceneSetup = null;
if (!_updateTriggered && path == _lastFilePath)
return false;
_lastFilePath = path;
_sceneMaterialsByName.Clear();
_meshBuffersForPrimitives.Clear();
sceneSetup = new SceneSetup();
if (!TryGetFilePath(path, out var fullPath))
{
ShowError($"Gltf File not found: {path}");
return true;
}
try
{
var model = ModelRoot.Load(fullPath);
var rootNode = _combineBuffer
? ConvertToNodeStructureIntoChunks(model.DefaultScene)
: ConvertToNodeStructure(model.DefaultScene);
sceneSetup.RootNodes.Clear();
sceneSetup.RootNodes.Add(rootNode);
sceneSetup.GenerateSceneDrawDispatches();
}
catch (Exception e)
{
ShowError($"Failed to load gltf file: {fullPath} \n{e.Message}");
return false;
}
return true;
}
private SceneSetup.SceneNode ConvertToNodeStructure(Scene modelDefaultScene)
{
var rootNode = new SceneSetup.SceneNode()
{
Name = modelDefaultScene.Name
};
ParseChildren(modelDefaultScene.VisualChildren, rootNode);
return rootNode;
}
// Todo adjust shader implementation new stride
[StructLayout(LayoutKind.Explicit, Size = Stride)]
// ReSharper disable once MemberCanBePrivate.Global
public struct MeshChunkDef
{
[FieldOffset(0)]
public int StartFaceIndex;
[FieldOffset(4)]
public int FaceCount;
[FieldOffset(8)]
public int StartVertexIndex;
[FieldOffset(12)]
public int VertexCount;
internal const int Stride = 16;
}
/** Flatten all meshes into a single mesh buffer seperated into chunks */
private SceneSetup.SceneNode ConvertToNodeStructureIntoChunks(Scene modelDefaultScene)
{
var rootNode = new SceneSetup.SceneNode()
{
Name = modelDefaultScene.Name
};
// Count all vertices and faces
var totalCounts = new MeshDataCounts();
HashSet collectedMeshPrimitives = new ();
_chunkDefIndicesForPrimitives.Clear();
ComputeTotalMeshCounts(modelDefaultScene.VisualChildren, ref totalCounts, ref collectedMeshPrimitives);
var meshDataSet = new MeshDataSet()
{
VertexBufferData = new PbrVertex[totalCounts.VertexCount],
IndexBufferData = new Int3[totalCounts.FaceCount],
ChunksDefs = new List(),
};
var meshBufferReference = new MeshBuffers(); // empty references that will be filled later
// Fill the buffers
var counters = new MeshDataCounts();
ParseChildrenIntoChunks(modelDefaultScene.VisualChildren, rootNode, ref counters, ref meshDataSet, ref meshBufferReference);
// Create actual mesh buffers
var faceCount = meshDataSet.IndexBufferData!.Length;
var indexBufferData = meshDataSet.IndexBufferData;
var verticesCount = meshDataSet.VertexBufferData!.Length;
var vertexBufferData = meshDataSet.VertexBufferData;
var chunkCount = meshDataSet.ChunksDefs!.Count;
var chunkBufferData = meshDataSet.ChunksDefs.ToArray();
ResourceManager.SetupStructuredBuffer(indexBufferData, 3 * 4 * faceCount, 3 * 4, ref meshBufferReference.IndicesBuffer.Buffer);
ResourceManager.CreateStructuredBufferSrv(meshBufferReference.IndicesBuffer.Buffer, ref meshBufferReference.IndicesBuffer.Srv);
ResourceManager.CreateStructuredBufferUav(meshBufferReference.IndicesBuffer.Buffer, UnorderedAccessViewBufferFlags.None,
ref meshBufferReference.IndicesBuffer.Uav);
ResourceManager.SetupStructuredBuffer(vertexBufferData, PbrVertex.Stride * verticesCount, PbrVertex.Stride,
ref meshBufferReference.VertexBuffer.Buffer);
ResourceManager.CreateStructuredBufferSrv(meshBufferReference.VertexBuffer.Buffer, ref meshBufferReference.VertexBuffer.Srv);
ResourceManager.CreateStructuredBufferUav(meshBufferReference.VertexBuffer.Buffer, UnorderedAccessViewBufferFlags.None,
ref meshBufferReference.VertexBuffer.Uav);
ResourceManager.SetupStructuredBuffer(chunkBufferData, MeshChunkDef.Stride * chunkCount, MeshChunkDef.Stride, ref meshBufferReference.ChunkDefsBuffer.Buffer);
ResourceManager.CreateStructuredBufferSrv(meshBufferReference.ChunkDefsBuffer.Buffer, ref meshBufferReference.ChunkDefsBuffer.Srv);
ResourceManager.CreateStructuredBufferUav(meshBufferReference.ChunkDefsBuffer.Buffer, UnorderedAccessViewBufferFlags.None,
ref meshBufferReference.ChunkDefsBuffer.Uav);
// TODO: Create instance points -> See [GetPointsFromSceneDef]
// TODO: Separate this into a separate operator
return rootNode;
}
private sealed class MeshDataSet
{
public PbrVertex[]? VertexBufferData;
public Int3[]? IndexBufferData;
public List? ChunksDefs;
}
private struct MeshDataCounts
{
public int ChunkCount;
public int VertexCount;
public int FaceCount;
}
private void ComputeTotalMeshCounts(IEnumerable visualChildren, ref MeshDataCounts totalCounts, ref HashSet collectedMeshPrimitives)
{
foreach (var child in visualChildren)
{
if (child == null)
continue;
if (child.Mesh != null)
{
foreach (var meshPrimitive in child.Mesh.Primitives)
{
if(!collectedMeshPrimitives.Add(meshPrimitive))
continue;
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if(meshPrimitive == null)
continue;
if (!meshPrimitive.VertexAccessors.TryGetValue("POSITION", out var positionAccessor))
continue;
var triangleCount= meshPrimitive.GetTriangleIndices().Count();
var verticesCount = positionAccessor.Count;
if(triangleCount == 0 || verticesCount == 0)
continue;
totalCounts.ChunkCount++;
totalCounts.VertexCount += verticesCount;
totalCounts.FaceCount += triangleCount;
}
}
ComputeTotalMeshCounts(child.VisualChildren, ref totalCounts, ref collectedMeshPrimitives);
}
}
private static string MatrixToString(Matrix4x4 m)
{
return $"{m.M11:0.00} {m.M12:0.00} {m.M13:0.00} {m.M13:0.00} | " +
$"{m.M21:0.00} {m.M22:0.00} {m.M23:0.00} {m.M23:0.00} | " +
$"{m.M31:0.00} {m.M32:0.00} {m.M33:0.00} {m.M33:0.00} | " +
$"{m.M41:0.00} {m.M42:0.00} {m.M43:0.00} {m.M43:0.00} | ";
}
/**
* TODO: This is work in progress!
* This will only add the mesh data to the meshDataSet, but not generate buffers.
*/
private void ParseChildrenIntoChunks(IEnumerable visualChildren, SceneSetup.SceneNode parentNode, ref MeshDataCounts counters,
ref MeshDataSet meshData, ref MeshBuffers meshBufferReference)
{
foreach (var child in visualChildren)
{
if (child == null || meshData.VertexBufferData == null || meshData.IndexBufferData == null || meshData.ChunksDefs == null)
continue;
var t = child.LocalTransform.GetDecomposed();
var transform = new SceneSetup.Transform
{
Translation = t.Translation,
Scale = t.Scale,
Rotation = t.Rotation,
};
var structureNode = new SceneSetup.SceneNode
{
Name = child.Name,
MeshName = child.Mesh?.Name,
// MeshBuffers = meshBuffers,
Transform = transform,
CombinedTransform = child.WorldMatrix,
};
parentNode.ChildNodes.Add(structureNode);
var useStructureNodeForMesh = true;
if (child.Mesh != null)
{
foreach (var meshPrimitive in child.Mesh.Primitives)
{
if(meshPrimitive == null)
continue;
if(!_chunkDefIndicesForPrimitives.TryGetValue(meshPrimitive, out var chunkDefIndex))
{
if (!GetMeshDataFromPrimitive(meshPrimitive, out var vertexBufferData, out var indexBufferData, out var message))
continue;
Array.Copy(vertexBufferData, 0, meshData.VertexBufferData, counters.VertexCount, vertexBufferData.Length);
for(var faceIndex=0; faceIndex visualChildren, SceneSetup.SceneNode parentNode)
{
foreach (var child in visualChildren)
{
if (child == null)
continue;
var t = child.LocalTransform.GetDecomposed();
var transform = new SceneSetup.Transform
{
Translation = t.Translation,
Scale = t.Scale,
Rotation = t.Rotation,
};
// Pure logic node
var structureNode = new SceneSetup.SceneNode()
{
Name = child.Name,
MeshName = child.Mesh?.Name,
// MeshBuffers = meshBuffers,
Transform = transform,
CombinedTransform = child.WorldMatrix,
};
parentNode.ChildNodes.Add(structureNode);
var useStructureNodeForMesh = true;
if (child.Mesh != null)
{
var meshIndex = 0;
foreach (var meshPrimitive in child.Mesh.Primitives)
{
if(meshPrimitive == null)
continue;
if (!_meshBuffersForPrimitives.TryGetValue(meshPrimitive, out var meshBuffers))
{
meshIndex++;
if (!TryGenerateMeshBuffersFromGltfChild(meshPrimitive, out meshBuffers, out var errorMessage))
{
ShowError(errorMessage);
meshBuffers = null;
}
if (meshBuffers == null)
continue;
_meshBuffersForPrimitives[meshPrimitive] = meshBuffers;
}
Log.Debug($" mesh:{child.Name} {child.Mesh?.Name} {meshIndex}");
var materialDef = GetOrCreateMaterialDefinition(meshPrimitive.Material);
//Log.Debug("Material: " + materialDef);
if (useStructureNodeForMesh)
{
structureNode.MeshBuffers = meshBuffers;
structureNode.Material = materialDef;
useStructureNodeForMesh = false;
continue;
}
var meshNode = new SceneSetup.SceneNode()
{
Name = child.Name,
MeshName = child.Mesh?.Name,
MeshBuffers = meshBuffers,
Transform = transform,
CombinedTransform = child.WorldMatrix,
Material = materialDef,
};
parentNode.ChildNodes.Add(meshNode);
}
}
ParseChildren(child.VisualChildren, structureNode);
}
}
private readonly Resource _resource;
#region Asset extraction
///
/// Extract gltf material definitions so we can later create a PbrMaterial from this data.
///
private SceneSetup.SceneMaterial? GetOrCreateMaterialDefinition(Material? gltfMaterial)
{
if (gltfMaterial == null)
return null;
var name = gltfMaterial.Name;
if (string.IsNullOrEmpty(name))
return null;
if (_sceneMaterialsByName.TryGetValue(name, out var materialDef))
{
return materialDef;
}
var baseColor = new System.Numerics.Vector4(1, 1, 1, 1);
var baseColorSrv = PbrMaterial.DefaultAlbedoColorSrv;
var emissiveColor = Vector4.Zero;
var emissiveSrv = PbrMaterial.DefaultEmissiveColorSrv;
var normalSrv = PbrMaterial.DefaultNormalSrv;
ShaderResourceView? occlusionSrv = null;
ShaderResourceView? metallicRoughnessSrv = null;
Texture2D? metallicRoughnessTexture = null;
Texture2D? occlusionTexture = null;
var roughness = 0.5f;
var metallic = 0f;
foreach (var channel in gltfMaterial.Channels)
{
Log.Debug("channel: " + channel.Key);
switch (channel.Key)
{
case "BaseColor":
{
baseColor = channel.Color;
if (TryCreateTextureFromChannel(gltfMaterial, channel, out var _, out var srv, PbrMaterial.DefaultAlbedoColorSrv))
{
baseColorSrv = srv;
}
break;
}
case "Emissive":
{
emissiveColor = channel.Color;
if (TryCreateTextureFromChannel(gltfMaterial, channel, out var _, out var srv, PbrMaterial.DefaultEmissiveColorSrv))
{
emissiveSrv = srv;
}
break;
}
case "MetallicRoughness":
{
if (TryCreateTextureFromChannel(gltfMaterial, channel, out var texture, out var srv, null))
{
metallicRoughnessTexture = texture;
metallicRoughnessSrv = srv;
}
foreach (var p in channel.Parameters)
{
switch (p.Name)
{
case "MetallicFactor":
metallic = (float)p.Value;
break;
case "RoughnessFactor":
roughness = (float)p.Value;
break;
}
}
break;
}
case "Occlusion":
{
if (TryCreateTextureFromChannel(gltfMaterial, channel, out var texture, out var srv, null))
{
occlusionTexture = texture;
occlusionSrv = srv;
}
break;
}
case "Normal":
{
if (TryCreateTextureFromChannel(gltfMaterial, channel, out var _, out var srv, PbrMaterial.DefaultNormalSrv))
{
normalSrv = srv;
}
break;
}
}
}
TryCreateRoughnessMetallicOcclusionTexture(
metallicRoughnessTexture, metallicRoughnessSrv,
occlusionTexture, occlusionSrv,
out var roughnessMetallicOcclusionSrv);
metallicRoughnessTexture?.Dispose();
metallicRoughnessSrv?.Dispose();
occlusionTexture?.Dispose();
occlusionSrv?.Dispose();
var newMaterialDef = new SceneSetup.SceneMaterial
{
Name = name,
// ColorTexture = baseColorTexture,
// NormalTexture = normalTexture,
PbrParameters = new PbrMaterial.PbrParameters
{
BaseColor = baseColor,
EmissiveColor = emissiveColor,
Roughness = roughness - 1 + _offsetRoughness, // glTF standard uses roughness factor?!
Specular = 1,
Metal = metallic -1 + _offsetMetallic, // glTF standard uses metallic factor?!
}
};
var newPbrMaterial = new PbrMaterial
{
Name = name,
AlbedoMapSrv = baseColorSrv,
EmissiveMapSrv = emissiveSrv,
RoughnessMetallicOcclusionSrv = roughnessMetallicOcclusionSrv,
NormalSrv = normalSrv,
Parameters = newMaterialDef.PbrParameters,
};
newPbrMaterial.UpdateParameterBuffer();
newMaterialDef.PbrMaterial = newPbrMaterial;
_sceneMaterialsByName[name] = newMaterialDef;
return newMaterialDef;
}
///
/// Setup shaders and textures required for combining roughness, metallic and occlusion textures.
///
private static void PrepareCombineShaderResources(Instance instance, bool forceUpdate = false)
{
if (_combineChannelsComputeShaderResource == null)
{
const string sourcePath = "Lib:shaders/cs/CombineGltfChannels-cs.hlsl";
const string entryPoint = "main";
_combineChannelsComputeShaderResource = ResourceManager.CreateShaderResource(sourcePath, instance, () => entryPoint, OnShaderChanged);
OnShaderChanged(_combineChannelsComputeShaderResource.Value);
}
if (!forceUpdate && _combineChannelsComputeShaderResource?.Value != null)
return;
OnShaderChanged(_combineChannelsComputeShaderResource!.Value);
return;
static void OnShaderChanged(ComputeShader? e)
{
if (e == null)
{
Log.Error($"Failed to initialize video conversion shader in {nameof(LoadGltfScene)}");
return;
}
e.Name = "CombineGltfChannels";
var samplerDesc = new SamplerStateDescription
{
Filter = Filter.MinMagMipLinear,
AddressU = TextureAddressMode.Clamp,
AddressV = TextureAddressMode.Clamp,
AddressW = TextureAddressMode.Clamp,
MipLodBias = 0,
MaximumAnisotropy = 1,
ComparisonFunction = Comparison.Never,
MinimumLod = -999999,
MaximumLod = 9999999
};
_combineChannelsSampler?.Dispose();
_combineChannelsSampler = new SamplerState(ResourceManager.Device, samplerDesc);
}
}
///
/// Fall back to default texture if nothing set. Otherwise use shader to create new texture.
///
private void TryCreateRoughnessMetallicOcclusionTexture(Texture2D? metallicRoughnessTexture,
ShaderResourceView? metallicRoughnessSrv,
Texture2D? occlusionTexture,
ShaderResourceView? occlusionSrv,
out ShaderResourceView? resultSrv)
{
if (metallicRoughnessSrv == null && occlusionSrv == null)
{
resultSrv = PbrMaterial.DefaultRoughnessMetallicOcclusionSrv;
return;
}
PrepareCombineShaderResources(this, _updateTriggered);
var device = ResourceManager.Device;
var deviceContext = device.ImmediateContext;
var csStage = deviceContext.ComputeShader;
// Compute max resolution
var width = 1;
var height = 1;
if (metallicRoughnessTexture != null)
{
width = Math.Max(width, metallicRoughnessTexture.Description.Width);
height = Math.Max(height, metallicRoughnessTexture.Description.Width);
}
if (occlusionTexture != null)
{
width = Math.Max(width, occlusionTexture.Description.Width);
height = Math.Max(height, occlusionTexture.Description.Width);
}
metallicRoughnessSrv ??= PbrMaterial.BlackPixelSrv;
occlusionSrv ??= PbrMaterial.WhitePixelSrv;
// TODO: create and test merge compute shader
// Keep previous setup
var prevShader = csStage.Get();
var prevUavs = csStage.GetUnorderedAccessViews(0, 1);
var prevSrvs = csStage.GetShaderResources(0, 1);
var prevSamplers = csStage.GetSamplers(0, 1);
// Set Shader
if (_combineChannelsComputeShaderResource != null)
{
var convertShader = _combineChannelsComputeShaderResource.Value;
csStage.Set(convertShader);
}
var srvs = new[] { metallicRoughnessSrv, occlusionSrv };
csStage.SetShaderResources(0, srvs);
csStage.SetSampler(0, _combineChannelsSampler);
// Create target texture
var resultTextureDescription = new Texture2DDescription
{
BindFlags = BindFlags.UnorderedAccess | BindFlags.RenderTarget | BindFlags.ShaderResource,
Format = SharpDX.DXGI.Format.R8G8B8A8_UNorm,
Width = width,
Height = height,
MipLevels = 1,
SampleDescription = new SampleDescription(1, 0),
Usage = ResourceUsage.Default,
OptionFlags = ResourceOptionFlags.None | ResourceOptionFlags.GenerateMipMaps,
CpuAccessFlags = CpuAccessFlags.None,
ArraySize = 1
};
var resultTexture = Texture2D.CreateTexture2D(resultTextureDescription);
resultSrv = new ShaderResourceView(ResourceManager.Device, resultTexture);
var resultUav = new UnorderedAccessView(ResourceManager.Device, resultTexture);
csStage.SetUnorderedAccessView(0, resultUav, 0);
// Dispatch
const int threadNumX = 16, threadNumY = 16;
var dispatchCountX = (width / threadNumX) + 1;
var dispatchCountY = (height / threadNumY) + 1;
deviceContext.Dispatch(dispatchCountX, dispatchCountY, 1);
ResourceManager.Device.ImmediateContext.GenerateMips(resultSrv);
// Restore prev setup
csStage.SetUnorderedAccessView(0, prevUavs[0]);
csStage.SetShaderResource(0, prevSrvs[0]);
csStage.SetSamplers(0, prevSamplers);
csStage.Set(prevShader);
}
private static Resource? _combineChannelsComputeShaderResource;
///
/// Tries to create a texture from a gltf material channel.
///
private static bool TryCreateTextureFromChannel(Material gltfMaterial, MaterialChannel channel, out Texture2D? texture, out ShaderResourceView? srv,
ShaderResourceView? fallbackSrv)
{
texture = null;
srv = fallbackSrv;
if (channel.Texture == null)
{
return false;
}
var imageContent = channel.Texture.PrimaryImage.Content.Content;
try
{
using var memStream = new MemoryStream(imageContent.ToArray());
memStream.Position = 0;
using var imagingFactory = new ImagingFactory();
using var bitmapDecoder = new BitmapDecoder(imagingFactory, memStream, DecodeOptions.CacheOnDemand);
using var formatConverter = new FormatConverter(imagingFactory);
using var bitmapFrameDecode = bitmapDecoder.GetFrame(0);
formatConverter.Initialize(bitmapFrameDecode, SharpDX.WIC.PixelFormat.Format32bppRGBA, BitmapDitherType.None, null, 0.0,
BitmapPaletteType.Custom);
texture = Texture2D.CreateFromBitmap(ResourceManager.Device, formatConverter);
texture.Name = channel.Key;
Log.Debug($" Created {gltfMaterial.Name}.{channel.Key} with {texture.Description.Width}×{texture.Description.Height}");
// bitmapFrameDecode.Dispose();
// bitmapDecoder.Dispose();
// formatConverter.Dispose();
// imagingFactory.Dispose();
srv = new ShaderResourceView(ResourceManager.Device, texture);
ResourceManager.Device.ImmediateContext.GenerateMips(srv);
return true;
}
catch (Exception e)
{
Log.Error($"Failed to create texture from channel {gltfMaterial.Name}.{channel.Key} : {e.Message}");
return false;
}
}
private static bool TryGenerateMeshBuffersFromGltfChild(MeshPrimitive meshPrimitive, out MeshBuffers newMesh, out string message)
{
// TODO: return cached mesh to reuse buffer
newMesh = new MeshBuffers();
message = string.Empty;
if (!GetMeshDataFromPrimitive(meshPrimitive, out var vertexBufferData, out var indexBufferData, out message))
return false;
try
{
var faceCount = indexBufferData.Length;
var verticesCount = vertexBufferData.Length;
const int stride = 3 * 4;
ResourceManager.SetupStructuredBuffer(indexBufferData, stride * faceCount, stride, ref newMesh.IndicesBuffer.Buffer);
ResourceManager.CreateStructuredBufferSrv(newMesh.IndicesBuffer.Buffer, ref newMesh.IndicesBuffer.Srv);
ResourceManager.CreateStructuredBufferUav(newMesh.IndicesBuffer.Buffer, UnorderedAccessViewBufferFlags.None,
ref newMesh.IndicesBuffer.Uav);
ResourceManager.SetupStructuredBuffer(vertexBufferData, PbrVertex.Stride * verticesCount, PbrVertex.Stride,
ref newMesh.VertexBuffer.Buffer);
ResourceManager.CreateStructuredBufferSrv(newMesh.VertexBuffer.Buffer, ref newMesh.VertexBuffer.Srv);
ResourceManager.CreateStructuredBufferUav(newMesh.VertexBuffer.Buffer, UnorderedAccessViewBufferFlags.None,
ref newMesh.VertexBuffer.Uav);
}
catch (Exception e)
{
message = $"Failed loading gltf: {e.Message}";
return false;
}
return true;
}
private static bool GetMeshDataFromPrimitive(MeshPrimitive meshPrimitive, out PbrVertex[] vertexBufferData, out Int3[] indexBufferData, out string message)
{
vertexBufferData = Array.Empty();
indexBufferData = Array.Empty();
// Convert vertices
{
// TODO: Iterate over all primitives
var vertexAccessors = meshPrimitive.VertexAccessors;
// Collect positions
if (!vertexAccessors.TryGetValue("POSITION", out var positionAccessor))
{
message = "Can't find POSITION attribute in gltf mesh";
return false;
}
var verticesCount = positionAccessor.Count;
if (vertexBufferData.Length != verticesCount)
vertexBufferData = new PbrVertex[verticesCount];
var positions = positionAccessor.AsVector3Array();
// Collect normals
Vector3[]? normals = null;
if (vertexAccessors.TryGetValue("NORMAL", out var normalAccess))
{
normals = normalAccess.AsVector3Array().ToArray();
}
// Collect texture coords
Vector2[]? texCoords = null;
Vector2[]? texCoords2 = null;
if (vertexAccessors.TryGetValue("TEXCOORD_0", out var texAccess))
{
texCoords = texAccess.AsVector2Array().ToArray();
}
if (vertexAccessors.TryGetValue("TEXCOORD_1", out var texAccess2)) // Check for second texture coordinate set
{
texCoords2 = texAccess2.AsVector2Array().ToArray();
}
// Write vertex buffer
for (var vertexIndex = 0; vertexIndex < positions.Count; vertexIndex++)
{
var position = positions[vertexIndex];
vertexBufferData[vertexIndex] = new PbrVertex
{
Position = new Vector3(position.X, position.Y, position.Z),
Normal = normals == null ? VectorT3.Up : normals[vertexIndex],
Tangent = VectorT3.Right,
Bitangent = VectorT3.ForwardLH,
Texcoord = texCoords == null
? Vector2.Zero
: new Vector2(texCoords[vertexIndex].X,
1 - texCoords[vertexIndex].Y),
Texcoord2 = texCoords2 == null
? Vector2.Zero
: new Vector2(texCoords2[vertexIndex].X,
1 - texCoords2[vertexIndex].Y),
ColorRgb = Vector3.One,
Selection = 1,
};
}
if (verticesCount == 0)
{
message = "No vertices found";
return false;
}
}
// Convert indices
{
var indices = meshPrimitive.GetTriangleIndices().ToList();
var faceCount = indices.Count;
if (indexBufferData.Length != faceCount)
indexBufferData = new Int3[faceCount];
var faceIndex = 0;
foreach (var (a, b, c) in indices)
{
indexBufferData[faceIndex] = new Int3(a, b, c);
faceIndex++;
// Calc TBN space
var p1 = vertexBufferData[a].Position;
var p2 = vertexBufferData[b].Position;
var p3 = vertexBufferData[c].Position;
// check for degenerated triangle
if (p1 == p2 || p1 == p3 || p2 == p3) continue;
var uv1 = vertexBufferData[a].Texcoord;
var uv2 = vertexBufferData[b].Texcoord;
var uv3 = vertexBufferData[c].Texcoord;
// check for degenerated triangle
if (uv1 == uv2 || uv1 == uv3 || uv2 == uv3) continue;
var n1 = vertexBufferData[a].Normal;
var n2 = vertexBufferData[b].Normal;
var n3 = vertexBufferData[c].Normal;
// Taken from https://github.com/vpenades/SharpGLTF/blob/master/examples/SharpGLTF.Runtime.MonoGame/NormalTangentFactories.cs
var s = p2 - p1;
var t = p3 - p1;
var sUv = uv2 - uv1;
var tUv = uv3 - uv1;
//var tUv = uv1 - uv3;
//tUv.Y = 1 - tUv.Y;
var sx = sUv.X;
var tx = tUv.X;
var sy = sUv.Y;
var ty = tUv.Y;
var r = 1.0F / ((sx * ty) - (tx * sy));
if (!r._IsFinite()) continue;
var sDir = new Vector3((ty * s.X) - (sy * t.X), (ty * s.Y) - (sy * t.Y), (ty * s.Z) - (sy * t.Z)) * r;
var tDir = new Vector3((sx * t.X) - (tx * s.X), (sx * t.Y) - (tx * s.Y), (sx * t.Z) - (tx * s.Z)) * r;
if (!sDir._IsFinite()) continue;
if (!tDir._IsFinite()) continue;
// Ill-fated attempt with brute force
// sDir = Vector3.Cross(n1, Vector3.UnitY);
// tDir = Vector3.Cross(n1, sDir);
// Todo: Sadly this fill add significant artifacts to complex meshes
vertexBufferData[a].Tangent = Vector3.Normalize(sDir);
vertexBufferData[a].Bitangent = Vector3.Normalize(tDir);
}
if (faceCount == 0)
{
message = "No faces found";
return false;
}
}
message = string.Empty;
return true;
}
#endregion
private readonly Dictionary _sceneMaterialsByName = new();
private readonly Dictionary _meshBuffersForPrimitives = new();
private readonly Dictionary _chunkDefIndicesForPrimitives = new();
private bool _combineBuffer;
private float _offsetRoughness;
private float _offsetMetallic;
private static SamplerState? _combineChannelsSampler;
private bool _updateTriggered;
#region implement graph node interfaces
InputSlot IDescriptiveFilename.SourcePathSlot => Path;
private string _lastFilePath = string.Empty;
IStatusProvider.StatusLevel IStatusProvider.GetStatusLevel()
{
return string.IsNullOrEmpty(_lastErrorMessage) ? IStatusProvider.StatusLevel.Success : IStatusProvider.StatusLevel.Warning;
}
string IStatusProvider.GetStatusMessage()
{
return _lastErrorMessage;
}
private void ShowError(string message)
{
_lastErrorMessage = message;
Log.Warning(_lastErrorMessage, this);
}
private string _lastErrorMessage = string.Empty;
#endregion
[Input(Guid = "292e80cf-ba31-4a50-9bf4-83712430f811")]
public readonly InputSlot Path = new();
[Input(Guid = "D02F41A6-1A6B-4A6E-8D6C-A28873C79F2C")]
public readonly InputSlot Setup = new();
[Input(Guid = "57F129AE-0B2E-465D-8C0E-F6259FDD37CE")]
public readonly InputSlot CombineBuffer = new();
[Input(Guid = "EF7075E9-4BC2-442E-8E0C-E03667FF2E0A")]
public readonly InputSlot TriggerUpdate = new();
[Input(Guid = "D7AE3173-C490-47CF-8D4B-4C25102F6904")]
public readonly InputSlot OffsetRoughness = new();
[Input(Guid = "49237499-9B1E-4371-AFAC-4E3394868370")]
public readonly InputSlot OffsetMetallic = new();
[Input(Guid = "FB325383-754A-4702-AFF2-C19E16363460")]
public readonly InputSlot MeshChildIndex = new();
}