using System; using System.Collections; using cgltf; using GlitchyEngine.Math; using GlitchyEngine.Renderer; using GlitchyEngine.Renderer.Animation; using GlitchyEngine.World; using System.IO; namespace GlitchyEngine.Content { public static class ModelLoader { static readonly Matrix RightToLeftHand = .Scaling(1, 1, -1); public static Result GetMeshNames(String filename, List meshNames) { CGLTF.Options options = .(); CGLTF.Data* data; CGLTF.Result result = CGLTF.ParseFile(options, filename, out data); if (!(result case .Success)) return .Err; for (var mesh in data.Meshes) { meshNames.Add(new String(mesh.Name)); } CGLTF.Free(data); return .Ok; } public static GeometryBinding LoadMesh(StringView fileName, StringView meshName, int primitiveIndex) { // TODO: add a context to remember which buffers were loaded before so that we don't load the same data multiple times. char8* scopedFileName = fileName.ToScopeCStr!(); CGLTF.Options options = .(); CGLTF.Data* data; CGLTF.Result result = CGLTF.ParseFile(options, scopedFileName, out data); if (!(result case .Success)) return null; result = CGLTF.LoadBuffers(options, data, scopedFileName); GeometryBinding geoBinding = null; for (var mesh in data.Meshes) { var name = StringView(mesh.Name); if (name == meshName) { Log.EngineLogger.AssertDebug(primitiveIndex >= 0 && primitiveIndex < mesh.Primitives.Length); geoBinding = PrimitiveToGeoBinding(mesh.Primitives[primitiveIndex]); break; } } CGLTF.Free(data); return geoBinding; } public static GeometryBinding LoadMesh(Stream data, StringView meshName, int primitiveIndex) { // TODO: add a context to remember which buffers were loaded before so that we don't load the same data multiple times. uint8[] rawData = new:ScopedAlloc! uint8[data.Length]; var dataReadResult = data.TryRead(rawData); if (dataReadResult case .Err(let err)) { Log.EngineLogger.Error($"Failed to read data from stream. Error: {err}"); } CGLTF.Options options = .(); CGLTF.Data* modelData; CGLTF.Result result = CGLTF.Parse(options, (Span)rawData, out modelData); if (!(result case .Success)) return null; // TODO: one buffer can be used by multiple primitives, the content manager could manage the buffers // TODO: load with content manager result = CGLTF.LoadBuffers(options, modelData, (char8*)null); //result = LoadBuffersWithContentManager(options, modelData, meshName, Application.Get().ContentManager); GeometryBinding geoBinding = null; for (var mesh in modelData.Meshes) { var name = StringView(mesh.Name); //if (name == meshName) { Log.EngineLogger.AssertDebug(primitiveIndex >= 0 && primitiveIndex < mesh.Primitives.Length); geoBinding = PrimitiveToGeoBinding(mesh.Primitives[primitiveIndex]); break; } } CGLTF.Free(modelData); return geoBinding; } private static CGLTF.Result LoadBuffersWithContentManager(CGLTF.Options options, CGLTF.Data* data, StringView fileName, IContentManager contentManager) { if (data.Buffers.Length > 0 && data.Buffers[0].Data == null && data.Buffers[0].Uri == null && !data.Bin.IsEmpty) { if ((uint)data.Bin.Length < data.Buffers[0].Size) return .DataTooShort; data.Buffers[0].Data = data.Bin.Ptr; data.Buffers[0].DataFreeMethod = .None; } for (ref CGLTF.Buffer buffer in ref data.Buffers) { if (buffer.Data != null) continue; if (buffer.Uri == null) continue; StringView uri = StringView(buffer.Uri); if (uri.StartsWith("data:")) { int commaIndex = uri.IndexOf(','); //char* comma = strchr(uri, ','); if (commaIndex == -1 || commaIndex >= 7 || uri.StartsWith(";base64")) return .UnknownFormat; StringView dataView = uri.Substring(commaIndex + 1); #unwarn CGLTF.Result loadBufferResult = CGLTF.LoadBuffersBase64(&options, buffer.Size, dataView.Ptr, &buffer.Data); buffer.DataFreeMethod = .MemoryFree; return loadBufferResult; } else { Runtime.NotImplemented(); // TODO: Request Buffer from Content Manager //int index = uri.IndexOf("://"); //if (index == -1) // return .UnknownFormat; // TODO: load buffer file... //CGLTF.Result res = //cgltf_load_buffer_file(options, data->buffers[i].size, uri, gltf_path, &data->buffers[i].data); //buffer.DataFreeMethod = cgltf_data_free_method_file_release; /*if (res != cgltf_result_success) { return res; }*/ } } /* for (cgltf_size i = 0; i < data->buffers_count; ++i) { if (data->buffers[i].data) { continue; } const char* uri = data->buffers[i].uri; if (uri == NULL) { continue; } if (strncmp(uri, "data:", 5) == 0) { const char* comma = strchr(uri, ','); if (comma && comma - uri >= 7 && strncmp(comma - 7, ";base64", 7) == 0) { cgltf_result res = cgltf_load_buffer_base64(options, data->buffers[i].size, comma + 1, &data->buffers[i].data); data->buffers[i].data_free_method = cgltf_data_free_method_memory_free; if (res != cgltf_result_success) { return res; } } else { return cgltf_result_unknown_format; } } else if (strstr(uri, "://") == NULL && gltf_path) { cgltf_result res = cgltf_load_buffer_file(options, data->buffers[i].size, uri, gltf_path, &data->buffers[i].data); data->buffers[i].data_free_method = cgltf_data_free_method_file_release; if (res != cgltf_result_success) { return res; } } else { return cgltf_result_unknown_format; } } */ return .Success; } public static GeometryBinding PrimitiveToGeoBinding(CGLTF.Primitive primitive) { GeometryBinding binding = new GeometryBinding(); // primitive topology switch(primitive.Type) { case .Points: binding.SetPrimitiveTopology(.PointList); case .Lines: binding.SetPrimitiveTopology(.LineList); case .LineStrip: binding.SetPrimitiveTopology(.LineStrip); case .Triangles: binding.SetPrimitiveTopology(.TriangleList); case .TriangleStrip: binding.SetPrimitiveTopology(.TriangleStrip); default: Log.EngineLogger.Assert(false, scope $"{primitive.Type} not supported."); } // indices { CGLTF.Accessor* indices = primitive.Indices; if(indices != null) { bool is16bit = indices.ComponentType == .R_16u || indices.ComponentType == .R_16; IndexBuffer ib = new IndexBuffer((.)indices.Count, .Immutable, .None, is16bit ? .Index16Bit : .Index32Bit); uint8* bufferData = (uint8*)indices.BufferView.Buffer.Data; bufferData += indices.BufferView.Offset; // TODO: this is a mess ib.SetData(bufferData, (.)indices.BufferView.Size); binding.SetIndexBuffer(ib); ib.ReleaseRef(); } } bool hasNormals = false; bool hasTangents = false; // vertices { List elements = scope .(primitive.Attributes.Length); Dictionary buffers = scope .(); List bindings = scope .(); for(var attribute in primitive.Attributes) { { StringView attributeName = StringView(attribute.Name); if(attributeName.Equals("NORMAL")) { hasNormals = true; } else if(attributeName.Equals("TANGENT")) { hasTangents = true; } } // Get Input Element format Format format = FormatFromVectorComponent(attribute.Data.Type, attribute.Data.ComponentType); Log.EngineLogger.AssertDebug(format != .Unknown, "Vertex element format must not be \"Unknown.\""); VertexBuffer vertexBuffer = null; binding.VertexCount = (uint32)attribute.Data.Count; // Get vertex buffer { CGLTF.BufferView* bufferView = attribute.Data.BufferView; // if buffer doesn't exist -> create if(!buffers.TryGetValue(bufferView, out vertexBuffer)) { vertexBuffer = new VertexBuffer(1, (uint32)bufferView.Size, .Immutable)..ReleaseRefNoDelete(); uint8* bufferData = (uint8*)bufferView.Buffer.Data; bufferData += bufferView.Offset; vertexBuffer.SetData(bufferData, (.)bufferView.Size); buffers.Add(attribute.Data.BufferView, vertexBuffer); } Log.EngineLogger.AssertDebug(vertexBuffer != null); } VertexBufferBinding bufferBinding = .(vertexBuffer, (.)attribute.Data.Stride, (.)attribute.Data.Offset); // get slot of bufferBinding int bindingSlot = bindings.IndexOf(bufferBinding); // binding has no slot -> add to list if(bindingSlot == -1) { bindingSlot = bindings.Count; bindings.Add(bufferBinding); binding.SetVertexBufferSlot(bufferBinding, (.)bindingSlot); } StringView strView = .(attribute.Name); // Remove number from end of name while((*(strView.EndPtr - 1)).IsDigit || (*(strView.EndPtr - 1)) == '_') { strView.Length--; } VertexElement element = .(format, new String(strView), true, (.)attribute.Index, (.)bindingSlot); elements.Add(element); } // Generate normals if missing if(!hasNormals) { CGLTF.Accessor* positions = null; // Find position accessor for(var attribute in primitive.Attributes) { if(StringView(attribute.Name).Equals("POSITION")) { positions = attribute.Data; break; } } Log.EngineLogger.AssertDebug(positions != null, "The model appears to have no position data?!"); float3[] normals = new float3[positions.Count]; if(primitive.Indices != null) GenerateNormals(primitive.Indices, positions, normals); else GenerateNormals(positions, normals); VertexBuffer vertexBuffer = new VertexBuffer((uint32)sizeof(float3), (uint32)normals.Count, .Immutable); vertexBuffer.SetData(normals); bindings.Add(vertexBuffer.Binding); uint32 bindingSlot = (.)bindings.Count - 1; binding.SetVertexBufferSlot(vertexBuffer.Binding, (.)bindingSlot); VertexElement element = .(.R32G32B32_Float, "NORMAL", false, 0, bindingSlot); elements.Add(element); } // TODO: validate vertex layout somewhere else VertexElement[] vertexElements = new VertexElement[elements.Count]; for(int i < elements.Count) { vertexElements[i] = elements[i]; } VertexLayout layout = new VertexLayout(vertexElements, true); binding.SetVertexLayout(layout..ReleaseRefNoDelete()); } // TODO: calculate tangents if missing (MikkTSpace algorithm) // Note: Bitangent = cross(normal, tangent.xyz) * tangent.w return binding; } /// Generates the normals for one triangle static mixin GenerateTriangleNormals(int index0, int index1, int index2, CGLTF.Accessor* positions, float3[] normals) { float3 position0 = GetEntry(positions, (.)index0); float3 position1 = GetEntry(positions, (.)index1); float3 position2 = GetEntry(positions, (.)index2); ref float3 normal0 = ref normals[(.)index0]; ref float3 normal1 = ref normals[(.)index1]; ref float3 normal2 = ref normals[(.)index2]; float3 e0 = position1 - position0; float3 e1 = position2 - position0; float3 normal = cross(e0, e1); normal0 += normal; normal1 += normal; normal2 += normal; } [Inline] static void NormalizeNormals(float3[] normals) { for(int i < normals.Count) { normalize(normals[i]); } } /// Generates normals for the given model using indexed geometry static void GenerateNormals(CGLTF.Accessor* indices, CGLTF.Accessor* positions, float3[] normals) { /// Enumerate triangle-wise for(uint t = 0; t < indices.Count; t += 3) { int index0 = (.)CGLTF.AccessorReadIndex(indices, t); int index1 = (.)CGLTF.AccessorReadIndex(indices, t + 1); int index2 = (.)CGLTF.AccessorReadIndex(indices, t + 2); GenerateTriangleNormals!(index0, index1, index2, positions, normals); } NormalizeNormals(normals); } /// Generates normals for the given model using nonindexed geometry static void GenerateNormals(CGLTF.Accessor* positions, float3[] normals) { /// Enumerate triangle-wise for(int i = 0; i < (.)positions.Count; i += 3) { GenerateTriangleNormals!(i, i + 1, i + 2, positions, normals); } NormalizeNormals(normals); } static Skeleton ExtractSkeleton(CGLTF.Skin* skin) { Skeleton skeleton = new Skeleton(); skeleton.Joints = new Joint[skin.Joints.Length]; for(int i < skin.Joints.Length) { ref Joint joint = ref skeleton.Joints[i]; joint.InverseBindPose = GetEntry(skin.InverseBindMatrices, i); if (skin.Joints[i].Name != null) joint.Name = new String(skin.Joints[i].Name); int parentId = skin.Joints.IndexOf(skin.Joints[i].Parent); Log.EngineLogger.AssertDebug(parentId < uint8.MaxValue, scope $"A skeleton must not have more than {uint8.MaxValue - 1} bones."); if(parentId == -1) joint.ParentID = uint8.MaxValue; else joint.ParentID = (uint8)parentId; } return skeleton; } static bool AnimationBelongsToSkeleton(CGLTF.Animation animation, CGLTF.Skin* skin) { for(var channel in animation.Channels) { if(skin.Joints.IndexOf(channel.TargetNode) == -1) { return false; } } return true; } static void LoadAnimationClips(CGLTF.Data* data, CGLTF.Skin* skin, Skeleton skeleton, List clips) { for(var animation in data.Animations) { if(!AnimationBelongsToSkeleton(animation, skin)) continue; AnimationClip clip = new AnimationClip(skeleton); clips.Add(clip); clip.IsLooping = true; for(var channel in animation.Channels) { int nodeIndex = skin.Joints.IndexOf(channel.TargetNode); Log.EngineLogger.AssertDebug(nodeIndex != -1); ref JointAnimation jointAnimation = ref clip.JointAnimations[nodeIndex]; if(jointAnimation == null) jointAnimation = new JointAnimation(); Log.EngineLogger.AssertDebug(channel.Sampler.Input.Count == channel.Sampler.Output.Count); int samples = (int)channel.Sampler.Input.Count; Log.EngineLogger.AssertDebug(channel.Sampler.Input.ComponentType == .R_32f); Log.EngineLogger.AssertDebug(channel.Sampler.Input.Type == .Scalar); InterpolationMode interpolationMode; switch(channel.Sampler.Interpolation) { case .Step: interpolationMode = .Step; case .Linear: interpolationMode = .Linear; case .CubicSpline: interpolationMode = .CubicSpline; } switch(channel.TargetPath) { case .Translation: jointAnimation.TranslationChannel = new .(samples, interpolationMode); Log.EngineLogger.AssertDebug(channel.Sampler.Output.ComponentType == .R_32f); Log.EngineLogger.AssertDebug(channel.Sampler.Output.Type == .Vec3); for(int i < samples) { float timeStamp = GetEntry(channel.Sampler.Input, i); float3 sample = GetEntry(channel.Sampler.Output, i); jointAnimation.TranslationChannel.TimeStamps[i] = timeStamp; jointAnimation.TranslationChannel.Values[i] = sample; } case .Rotation: jointAnimation.RotationChannel = new .(samples, interpolationMode); Log.EngineLogger.AssertDebug(channel.Sampler.Output.ComponentType == .R_32f); Log.EngineLogger.AssertDebug(channel.Sampler.Output.Type == .Vec4); for(int i < samples) { float timeStamp = GetEntry(channel.Sampler.Input, i); Quaternion sample = GetEntry(channel.Sampler.Output, i); jointAnimation.RotationChannel.TimeStamps[i] = timeStamp; jointAnimation.RotationChannel.Values[i] = sample; } case .Scale: jointAnimation.ScaleChannel = new .(samples, interpolationMode); Log.EngineLogger.AssertDebug(channel.Sampler.Output.ComponentType == .R_32f); Log.EngineLogger.AssertDebug(channel.Sampler.Output.Type == .Vec3); for(int i < samples) { float timeStamp = GetEntry(channel.Sampler.Input, i); float3 sample = GetEntry(channel.Sampler.Output, i); jointAnimation.ScaleChannel.TimeStamps[i] = timeStamp; jointAnimation.ScaleChannel.Values[i] = sample; } default: Log.EngineLogger.Error($"Unknown channel target path \"{channel.TargetPath}\""); } clip.Duration = Math.Max(clip.Duration, jointAnimation.Duration); } } } static T GetEntry(CGLTF.Accessor* accessor, int index) { Log.EngineLogger.AssertDebug((uint)index < accessor.Count); T result = ?; switch(typeof(T)) { case typeof(float): CGLTF.AccessorReadFloat(accessor, (uint)index, (float*)&result, 1); case typeof(float2): CGLTF.AccessorReadFloat(accessor, (uint)index, (float*)&result, 2); case typeof(float3): CGLTF.AccessorReadFloat(accessor, (uint)index, (float*)&result, 3); case typeof(float4), typeof(Quaternion): CGLTF.AccessorReadFloat(accessor, (uint)index, (float*)&result, 4); case typeof(Matrix): CGLTF.AccessorReadFloat(accessor, (uint)index, (float*)&result, 16); case typeof(uint16): CGLTF.AccessorReadUint(accessor, (uint)index, (uint32*)&result, 2); case default: uint8* data = (uint8*)accessor.BufferView.Buffer.Data; data += accessor.BufferView.Offset; data += accessor.Offset; data += accessor.Stride * (uint)index; result = *(T*)data; } return result; /* uint8* data = (uint8*)accessor.BufferView.Buffer.Data; data += accessor.BufferView.Offset; data += accessor.Offset; data += accessor.Stride * (uint)index; return *(T*)data; */ } /** * Converts the vector and component type to the corresponding Format. */ static Format FormatFromVectorComponent(CGLTF.Type vectorType, CGLTF.ComponentType componentType) { switch((vectorType, componentType)) { case (.Scalar, .R_8): return .R8_SInt; case (.Scalar, .R_8u): return .R8_UInt; case (.Scalar, .R_16): return .R16_SInt; case (.Scalar, .R_16u): return .R16_UInt; case (.Scalar, .R_32u): return .R32_UInt; case (.Scalar, .R_32f): return .R32_Float; case (.Vec2, .R_8): return .R8G8_SInt; case (.Vec2, .R_8u): return .R8G8_UInt; case (.Vec2, .R_16): return .R16G16_SInt; case (.Vec2, .R_16u): return .R16G16_UInt; case (.Vec2, .R_32u): return .R32G32_UInt; case (.Vec2, .R_32f): return .R32G32_Float; //case (.Vec3, .R_8): //case (.Vec3, .R_8u): //case (.Vec3, .R_16): //case (.Vec3, .R_16u): case (.Vec3, .R_32u): return .R32G32B32_UInt; case (.Vec3, .R_32f): return .R32G32B32_Float; case (.Vec4, .R_8): return .R8G8B8A8_SInt; case (.Vec4, .R_8u): return .R8G8B8A8_UInt; case (.Vec4, .R_16): return .R16G16B16A16_SInt; case (.Vec4, .R_16u): return .R16G16B16A16_UInt; case (.Vec4, .R_32u): return .R32G32B32A32_UInt; case (.Vec4, .R_32f): return .R32G32B32A32_Float; case (.Mat4, .R_32f): return .R32G32B32A32_Float; default: Log.EngineLogger.Assert(false, scope $"Unhandled vector - componenttype combination. ({vectorType}, {componentType})"); } return .Unknown; } } }