diff --git a/build/SharpGLTF.CodeGen/Constants.cs b/build/SharpGLTF.CodeGen/Constants.cs index 335e7510..3e21fe18 100644 --- a/build/SharpGLTF.CodeGen/Constants.cs +++ b/build/SharpGLTF.CodeGen/Constants.cs @@ -10,10 +10,16 @@ static class Constants public static string RemoteSchemaRepo = "https://github.com/KhronosGroup/glTF.git"; + /// + /// Program directory + /// + public static string ProgramDirectory => System.IO.Path.GetDirectoryName(typeof(Program).Assembly.Location); + /// /// Directory where the schema is downloaded and used as source /// - public static string LocalRepoDirectory => System.IO.Path.Combine(System.IO.Path.GetDirectoryName(typeof(Program).Assembly.Location), "glTF"); + public static string LocalRepoDirectory => System.IO.Path.Combine(ProgramDirectory, "glTF"); + #endregion @@ -51,6 +57,12 @@ internal static string VendorExtensionPath(string ext, string json) return System.IO.Path.Combine(VendorSchemaDir, ext, "schema", json); } + internal static string CustomExtensionsPath(string ext, string json) + { + return System.IO.Path.Combine(ProgramDirectory, "Schemas", ext, "schema", json); + } + + #endregion #region code generation output paths diff --git a/build/SharpGLTF.CodeGen/Ext.EXT_MeshFeatures.cs b/build/SharpGLTF.CodeGen/Ext.EXT_MeshFeatures.cs new file mode 100644 index 00000000..b0a9221c --- /dev/null +++ b/build/SharpGLTF.CodeGen/Ext.EXT_MeshFeatures.cs @@ -0,0 +1,45 @@ +using SharpGLTF.CodeGen; +using SharpGLTF.SchemaReflection; +using System; +using System.Collections.Generic; + +namespace SharpGLTF +{ + class ExtMeshFeaturesExtension : SchemaProcessor + { + public override string GetTargetProject() { return Constants.CesiumProjectDirectory; } + + private static string RootSchemaUri => Constants.CustomExtensionsPath("EXT_mesh_features", "mesh.primitive.EXT_mesh_features.schema.json"); + + const string ExtensionFeatureIdTextureName = "Feature ID Texture in EXT_mesh_features"; + + public override void PrepareTypes(CSharpEmitter newEmitter, SchemaType.Context ctx) + { + newEmitter.SetRuntimeName("EXT_mesh_features glTF Mesh Primitive extension", "MeshExtMeshFeatures"); + newEmitter.SetRuntimeName("Feature ID in EXT_mesh_features", "MeshExtMeshFeatureID"); + newEmitter.SetRuntimeName(ExtensionFeatureIdTextureName, "MeshExtMeshFeatureIDTexture"); + } + + public override IEnumerable<(string TargetFileName, SchemaType.Context Schema)> Process() + { + yield return ("Ext.CESIUM_ext_mesh_features.g", ProcessNode()); + } + + private static SchemaType.Context ProcessNode() + { + var ctx = SchemaProcessing.LoadSchemaContext(RootSchemaUri); + ctx.IgnoredByCodeEmitter("glTF Property"); + ctx.IgnoredByCodeEmitter("glTF Child of Root Property"); + ctx.IgnoredByCodeEmitter("Texture Info"); + + var fld = ctx.FindClass(ExtensionFeatureIdTextureName) + .GetField("channels"); + + // for now we simply remove the default value, it can be set + // in the constructor or on demand when the APIs are Called. + fld.RemoveDefaultValue(); + + return ctx; + } + } +} diff --git a/build/SharpGLTF.CodeGen/Program.cs b/build/SharpGLTF.CodeGen/Program.cs index 0722c9bb..b9278453 100644 --- a/build/SharpGLTF.CodeGen/Program.cs +++ b/build/SharpGLTF.CodeGen/Program.cs @@ -1,4 +1,6 @@ -using System; +using SharpGLTF.CodeGen; +using SharpGLTF.SchemaReflection; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -56,19 +58,21 @@ static void Main(string[] args) // other processors.Add(new XmpJsonLdExtension()); + processors.Add(new ExtMeshFeaturesExtension()); + // ---------------------------------------------- process all files foreach (var processor in processors) { - foreach(var (targetFileName, schema) in processor.Process()) + foreach (var (targetFileName, schema) in processor.Process()) { System.Console.WriteLine($"Emitting {targetFileName}..."); SchemaProcessing.EmitCodeFromSchema(processor.GetTargetProject(), targetFileName, schema, processors); } - } + } } #endregion - } -} + } +} \ No newline at end of file diff --git a/build/SharpGLTF.CodeGen/Schemas/EXT_mesh_features/README.md b/build/SharpGLTF.CodeGen/Schemas/EXT_mesh_features/README.md new file mode 100644 index 00000000..3d9b9c95 --- /dev/null +++ b/build/SharpGLTF.CodeGen/Schemas/EXT_mesh_features/README.md @@ -0,0 +1,10 @@ +# EXT_mesh_features + +This directory contains schema's for Cesium extension +EXT_mesh_features + +PR at Khronos: https://github.com/KhronosGroup/glTF/pull/2082 + +Schema's are copied from https://github.com/CesiumGS/glTF/tree/proposal-EXT_mesh_features/extensions/2.0/Vendor/EXT_mesh_features/schema + + diff --git a/build/SharpGLTF.CodeGen/Schemas/EXT_mesh_features/schema/featureId.schema.json b/build/SharpGLTF.CodeGen/Schemas/EXT_mesh_features/schema/featureId.schema.json new file mode 100644 index 00000000..c07de0b0 --- /dev/null +++ b/build/SharpGLTF.CodeGen/Schemas/EXT_mesh_features/schema/featureId.schema.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "featureId.schema.json", + "title": "Feature ID in EXT_mesh_features", + "type": "object", + "description": "Feature IDs stored in an attribute or texture.", + "allOf": [ + { + "$ref": "glTFProperty.schema.json" + } + ], + "properties": { + "featureCount": { + "type": "integer", + "minimum": 1, + "description": "The number of unique features in the attribute or texture." + }, + "nullFeatureId": { + "type": "integer", + "minimum": 0, + "description": "A value that indicates that no feature is associated with this vertex or texel." + }, + "label": { + "type": "string", + "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$", + "description": "A label assigned to this feature ID set. Labels must be alphanumeric identifiers matching the regular expression `^[a-zA-Z_][a-zA-Z0-9_]*$`." + }, + "attribute": { + "description": "An attribute containing feature IDs. When `attribute` and `texture` are omitted the feature IDs are assigned to vertices by their index.", + "$ref": "featureIdAttribute.schema.json" + }, + "texture": { + "description": "A texture containing feature IDs.", + "$ref": "featureIdTexture.schema.json" + }, + "propertyTable": { + "type": "integer", + "minimum": 0, + "description": "The index of the property table containing per-feature property values. Only applicable when using the `EXT_structural_metadata` extension." + }, + "extensions": {}, + "extras": {} + }, + "required": [ + "featureCount" + ] +} \ No newline at end of file diff --git a/build/SharpGLTF.CodeGen/Schemas/EXT_mesh_features/schema/featureIdAttribute.schema.json b/build/SharpGLTF.CodeGen/Schemas/EXT_mesh_features/schema/featureIdAttribute.schema.json new file mode 100644 index 00000000..ddb0373b --- /dev/null +++ b/build/SharpGLTF.CodeGen/Schemas/EXT_mesh_features/schema/featureIdAttribute.schema.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "featureIdAttribute.schema.json", + "title": "Feature ID Attribute in EXT_mesh_features", + "type": "integer", + "minimum": 0, + "description": "An integer value used to construct a string in the format `_FEATURE_ID_` which is a reference to a key in `mesh.primitives.attributes` (e.g. a value of `0` corresponds to `_FEATURE_ID_0`)." +} \ No newline at end of file diff --git a/build/SharpGLTF.CodeGen/Schemas/EXT_mesh_features/schema/featureIdTexture.schema.json b/build/SharpGLTF.CodeGen/Schemas/EXT_mesh_features/schema/featureIdTexture.schema.json new file mode 100644 index 00000000..abe4278e --- /dev/null +++ b/build/SharpGLTF.CodeGen/Schemas/EXT_mesh_features/schema/featureIdTexture.schema.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "featureIdTexture.schema.json", + "title": "Feature ID Texture in EXT_mesh_features", + "type": "object", + "description": "A texture containing feature IDs", + "allOf": [ + { + "$ref": "textureInfo.schema.json" + } + ], + "properties": { + "channels": { + "type": "array", + "items": { + "type": "integer", + "minimum": 0 + }, + "minItems": 1, + "description": "Texture channels containing feature IDs, identified by index. Feature IDs may be packed into multiple channels if a single channel does not have sufficient bit depth to represent all feature ID values. The values are packed in little-endian order.", + "default": [ + 0 + ] + }, + "index": {}, + "texCoord": {}, + "extensions": {}, + "extras": {} + } +} \ No newline at end of file diff --git a/build/SharpGLTF.CodeGen/Schemas/EXT_mesh_features/schema/mesh.primitive.EXT_mesh_features.schema.json b/build/SharpGLTF.CodeGen/Schemas/EXT_mesh_features/schema/mesh.primitive.EXT_mesh_features.schema.json new file mode 100644 index 00000000..a8d1e052 --- /dev/null +++ b/build/SharpGLTF.CodeGen/Schemas/EXT_mesh_features/schema/mesh.primitive.EXT_mesh_features.schema.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "mesh.primitive.EXT_mesh_features.schema.json", + "title": "EXT_mesh_features glTF Mesh Primitive extension", + "type": "object", + "description": "An object describing feature IDs for a mesh primitive.", + "allOf": [ + { + "$ref": "glTFProperty.schema.json" + } + ], + "properties": { + "featureIds": { + "type": "array", + "description": "An array of feature ID sets.", + "minItems": 1, + "items": { + "$ref": "featureId.schema.json" + } + }, + "extensions": {}, + "extras": {} + }, + "required": [ + "featureIds" + ] +} \ No newline at end of file diff --git a/build/SharpGLTF.CodeGen/SharpGLTF.CodeGen.csproj b/build/SharpGLTF.CodeGen/SharpGLTF.CodeGen.csproj index 54a037ab..a75da28e 100644 --- a/build/SharpGLTF.CodeGen/SharpGLTF.CodeGen.csproj +++ b/build/SharpGLTF.CodeGen/SharpGLTF.CodeGen.csproj @@ -11,4 +11,23 @@ + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + diff --git a/src/SharpGLTF.Cesium/Schema2/CesiumExtensions.cs b/src/SharpGLTF.Cesium/Schema2/CesiumExtensions.cs index c013bf5f..6a22f1cc 100644 --- a/src/SharpGLTF.Cesium/Schema2/CesiumExtensions.cs +++ b/src/SharpGLTF.Cesium/Schema2/CesiumExtensions.cs @@ -21,6 +21,8 @@ public static void RegisterExtensions() _CesiumRegistered = true; ExtensionsFactory.RegisterExtension("CESIUM_primitive_outline"); + ExtensionsFactory.RegisterExtension("EXT_mesh_features"); + } } } diff --git a/src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_mesh_features.g.cs b/src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_mesh_features.g.cs new file mode 100644 index 00000000..7ae6772e --- /dev/null +++ b/src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_mesh_features.g.cs @@ -0,0 +1,144 @@ +// + +//------------------------------------------------------------------------------------------------ +// This file has been programatically generated; DON´T EDIT! +//------------------------------------------------------------------------------------------------ + +#pragma warning disable SA1001 +#pragma warning disable SA1027 +#pragma warning disable SA1028 +#pragma warning disable SA1121 +#pragma warning disable SA1205 +#pragma warning disable SA1309 +#pragma warning disable SA1402 +#pragma warning disable SA1505 +#pragma warning disable SA1507 +#pragma warning disable SA1508 +#pragma warning disable SA1652 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Numerics; +using System.Text.Json; + +namespace SharpGLTF.Schema2 +{ + using Collections; + + /// + /// A texture containing feature IDs + /// +#if NET6_0_OR_GREATER + [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("SharpGLTF.CodeGen", "1.0.0.0")] + partial class MeshExtMeshFeatureIDTexture : TextureInfo + { + + private const int _channelsMinItems = 1; + private List _channels; + + + protected override void SerializeProperties(Utf8JsonWriter writer) + { + base.SerializeProperties(writer); + SerializeProperty(writer, "channels", _channels, _channelsMinItems); + } + + protected override void DeserializeProperty(string jsonPropertyName, ref Utf8JsonReader reader) + { + switch (jsonPropertyName) + { + case "channels": DeserializePropertyList(ref reader, _channels); break; + default: base.DeserializeProperty(jsonPropertyName,ref reader); break; + } + } + } + + /// + /// Feature IDs stored in an attribute or texture. + /// +#if NET6_0_OR_GREATER + [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("SharpGLTF.CodeGen", "1.0.0.0")] + partial class MeshExtMeshFeatureID : ExtraProperties + { + + private Int32? _attribute; + + private const Int32 _featureCountMinimum = 1; + private Int32 _featureCount; + + private String _label; + + private const Int32 _nullFeatureIdMinimum = 0; + private Int32? _nullFeatureId; + + private const Int32 _propertyTableMinimum = 0; + private Int32? _propertyTable; + + private MeshExtMeshFeatureIDTexture _texture; + + + protected override void SerializeProperties(Utf8JsonWriter writer) + { + base.SerializeProperties(writer); + SerializeProperty(writer, "attribute", _attribute); + SerializeProperty(writer, "featureCount", _featureCount); + SerializeProperty(writer, "label", _label); + SerializeProperty(writer, "nullFeatureId", _nullFeatureId); + SerializeProperty(writer, "propertyTable", _propertyTable); + SerializePropertyObject(writer, "texture", _texture); + } + + protected override void DeserializeProperty(string jsonPropertyName, ref Utf8JsonReader reader) + { + switch (jsonPropertyName) + { + case "attribute": _attribute = DeserializePropertyValue(ref reader); break; + case "featureCount": _featureCount = DeserializePropertyValue(ref reader); break; + case "label": _label = DeserializePropertyValue(ref reader); break; + case "nullFeatureId": _nullFeatureId = DeserializePropertyValue(ref reader); break; + case "propertyTable": _propertyTable = DeserializePropertyValue(ref reader); break; + case "texture": _texture = DeserializePropertyValue(ref reader); break; + default: base.DeserializeProperty(jsonPropertyName,ref reader); break; + } + } + + } + + /// + /// An object describing feature IDs for a mesh primitive. + /// + #if NET6_0_OR_GREATER + [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] + #endif + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("SharpGLTF.CodeGen", "1.0.0.0")] + partial class MeshExtMeshFeatures : ExtraProperties + { + + private const int _featureIdsMinItems = 1; + private List _featureIds; + + + protected override void SerializeProperties(Utf8JsonWriter writer) + { + base.SerializeProperties(writer); + SerializeProperty(writer, "featureIds", _featureIds, _featureIdsMinItems); + } + + protected override void DeserializeProperty(string jsonPropertyName, ref Utf8JsonReader reader) + { + switch (jsonPropertyName) + { + case "featureIds": DeserializePropertyList(ref reader, _featureIds); break; + default: base.DeserializeProperty(jsonPropertyName,ref reader); break; + } + } + + } + +} diff --git a/src/SharpGLTF.Cesium/Schema2/MeshExtMeshFeatures.cs b/src/SharpGLTF.Cesium/Schema2/MeshExtMeshFeatures.cs new file mode 100644 index 00000000..693b6164 --- /dev/null +++ b/src/SharpGLTF.Cesium/Schema2/MeshExtMeshFeatures.cs @@ -0,0 +1,156 @@ +using SharpGLTF.Validation; +using System.Collections.Generic; +using System.Linq; + +namespace SharpGLTF.Schema2 +{ + partial class MeshExtMeshFeatures + { + private MeshPrimitive _meshPrimitive; + + internal MeshExtMeshFeatures(MeshPrimitive meshPrimitive) + { + _meshPrimitive = meshPrimitive; + _featureIds = new List(); + } + + public List FeatureIds + { + get => _featureIds; + set + { + if (value == null) { _featureIds = null; return; } + _featureIds = value; + } + } + + protected override void OnValidateContent(ValidationContext validate) + { + var extMeshFeatures = (MeshExtMeshFeatures)_meshPrimitive.Extensions.FirstOrDefault(); + + validate.NotNull(nameof(FeatureIds), extMeshFeatures.FeatureIds); + validate.IsTrue(nameof(FeatureIds), extMeshFeatures.FeatureIds.Count > 0, "FeatureIds has items"); + + base.OnValidateContent(validate); + } + } + + public partial class MeshExtMeshFeatureIDTexture + { + public MeshExtMeshFeatureIDTexture(List channels, int? index = null, int? texCoord = null) + { + Guard.NotNullOrEmpty(channels, nameof(channels)); + Guard.MustBeGreaterThanOrEqualTo((int)index, 0, nameof(index)); + Guard.MustBeGreaterThanOrEqualTo((int)texCoord, 0, nameof(index)); + + _channels = channels; + if (index.HasValue) _LogicalTextureIndex = (int)index; + if (texCoord.HasValue) TextureCoordinate = (int)texCoord; + } + + public int Index { get => _LogicalTextureIndex; } + } + + public partial class MeshExtMeshFeatureID + { + public MeshExtMeshFeatureID(int featureCount, int? attribute = null, int? propertyTable = null, string label = null, int? nullFeatureId = null, MeshExtMeshFeatureIDTexture texture = null) + { + _featureCount = featureCount; + _attribute = attribute; + _label = label; + _propertyTable = propertyTable; + _nullFeatureId = nullFeatureId; + _texture = texture; + } + + /// + /// The number of unique features in the attribute or texture. + /// + public int FeatureCount { get => _featureCount; } + + /// + /// A value that indicates that no feature is associated with this vertex or texel. + /// + public int? NullFeatureId { get => _nullFeatureId; } + + /// + /// An attribute containing feature IDs. When `attribute` and `texture` are omitted the + /// feature IDs are assigned to vertices by their index. + /// + public int? Attribute { get => _attribute; } + + /// + /// A label assigned to this feature ID set. Labels must be alphanumeric identifiers + /// matching the regular expression `^[a-zA-Z_][a-zA-Z0-9_]*$`. + /// + public string Label { get => _label; } + + /// + /// A texture containing feature IDs. + /// + public MeshExtMeshFeatureIDTexture Texture { get => _texture; } + + /// + /// The index of the property table containing per-feature property values. Only applicable when using the `EXT_structural_metadata` extension. + /// + public int? PropertyTable { get => _propertyTable; } + } + + public static class ExtMeshFeatures + { + /// + /// Set the FeatureIds for a MeshPrimitive + /// + /// + /// + public static void SetFeatureIds(this MeshPrimitive primitive, List featureIds) + { + if (featureIds == null) { primitive.RemoveExtensions(); return; } + + Guard.NotNullOrEmpty(featureIds, nameof(featureIds)); + + foreach (var featureId in featureIds) + { + ValidateFeature(primitive, featureId); + }; + + var ext = primitive.UseExtension(); + ext.FeatureIds = featureIds; + } + + private static void ValidateFeature(MeshPrimitive primitive, MeshExtMeshFeatureID item) + { + Guard.MustBeGreaterThanOrEqualTo((int)item.FeatureCount, 1, nameof(item.FeatureCount)); + + if (item.NullFeatureId.HasValue) + { + Guard.MustBeGreaterThanOrEqualTo((int)item.NullFeatureId, 0, nameof(item.NullFeatureId)); + } + if (item.Label != null) + { + var regex = "^[a-zA-Z_][a-zA-Z0-9_]*$"; + Guard.IsTrue(System.Text.RegularExpressions.Regex.IsMatch(item.Label, regex), nameof(item.Label)); + } + if (item.Attribute.HasValue) + { + Guard.MustBeGreaterThanOrEqualTo((int)item.Attribute, 0, nameof(item.Attribute)); + // Guard that the custom vertex attribute (_FEATURE_ID_{attribute}) exists when FeatureID has attribute set + var expectedVertexAttribute = $"_FEATURE_ID_{item.Attribute}"; + Guard.NotNull(primitive.GetVertexAccessor(expectedVertexAttribute), expectedVertexAttribute); + } + if (item.PropertyTable.HasValue) + { + Guard.MustBeGreaterThanOrEqualTo((int)item.PropertyTable, 0, nameof(item.PropertyTable)); + } + if (item.Texture != null) + { + Guard.MustBeGreaterThanOrEqualTo((int)item.Texture.TextureCoordinate, 0, nameof(item.Texture.TextureCoordinate)); + var expectedTexCoordAttribute = $"TEXCOORD_{item.Texture.TextureCoordinate}"; + Guard.NotNull(primitive.GetVertexAccessor(expectedTexCoordAttribute), expectedTexCoordAttribute); + + var image = primitive.LogicalParent.LogicalParent.LogicalImages[item.Texture.Index]; + Guard.NotNull(image, "Texture " + nameof(item.Texture.Index)); + } + } + } +} \ No newline at end of file diff --git a/src/SharpGLTF.Cesium/Validator.cs b/src/SharpGLTF.Cesium/Validator.cs new file mode 100644 index 00000000..91d0cd9e --- /dev/null +++ b/src/SharpGLTF.Cesium/Validator.cs @@ -0,0 +1,16 @@ +using SharpGLTF.Schema2; + +namespace SharpGLTF +{ + internal static class Validator + { + internal static void ValidateAccessor(ModelRoot model, Accessor accessor) + { + Guard.NotNull(accessor, nameof(accessor)); + Guard.MustShareLogicalParent(model, "this", accessor, nameof(accessor)); + Guard.IsTrue(accessor.Encoding == EncodingType.UNSIGNED_INT, nameof(accessor)); + Guard.IsTrue(accessor.Dimensions == DimensionType.SCALAR, nameof(accessor)); + Guard.IsFalse(accessor.Normalized, nameof(accessor)); + } + } +} diff --git a/src/SharpGLTF.Core/Schema2/gltf.TextureInfo.cs b/src/SharpGLTF.Core/Schema2/gltf.TextureInfo.cs index 330cf31a..92c542f1 100644 --- a/src/SharpGLTF.Core/Schema2/gltf.TextureInfo.cs +++ b/src/SharpGLTF.Core/Schema2/gltf.TextureInfo.cs @@ -4,11 +4,11 @@ namespace SharpGLTF.Schema2 { [System.Diagnostics.DebuggerDisplay("LogicalTexture[{_LogicalTextureIndex}]")] - internal partial class TextureInfo + public partial class TextureInfo { #region properties - internal int _LogicalTextureIndex + public int _LogicalTextureIndex { get => _index; set => _index = value; diff --git a/tests/SharpGLTF.Cesium.Tests/ExtMeshFeaturesTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtMeshFeaturesTests.cs new file mode 100644 index 00000000..66b8ad0b --- /dev/null +++ b/tests/SharpGLTF.Cesium.Tests/ExtMeshFeaturesTests.cs @@ -0,0 +1,145 @@ +using NUnit.Framework; +using SharpGLTF.Geometry; +using SharpGLTF.Geometry.VertexTypes; +using SharpGLTF.Materials; +using SharpGLTF.Scenes; +using SharpGLTF.Schema2; +using SharpGLTF.Validation; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace SharpGLTF.Cesium +{ + using VBTexture1 = VertexBuilder; + + + [Category("Toolkit.Scenes")] + public class ExtMeshFeaturesTests + { + [SetUp] + public void SetUp() + { + CesiumExtensions.RegisterExtensions(); + } + + [Test(Description = "Test for settting the FeatureIds with vertex attributes. See sample https://github.com/CesiumGS/3d-tiles-samples/blob/main/glTF/EXT_mesh_features/FeatureIdAttribute")] + public void FeaturesIdAttributeTest() + { + TestContext.CurrentContext.AttachGltfValidatorLinks(); + + // Create a triangle with feature ID custom vertex attribute + var featureId = 1; + var material = MaterialBuilder.CreateDefault().WithDoubleSide(true); + + var mesh = new MeshBuilder("mesh"); + var prim = mesh.UsePrimitive(material); + + // All the vertices in the triangle have the same feature ID + var vt0 = GetVertexBuilderWithFeatureId(new Vector3(-10, 0, 0), new Vector3(0, 0, 1), featureId); + var vt1 = GetVertexBuilderWithFeatureId(new Vector3(10, 0, 0), new Vector3(0, 0, 1), featureId); + var vt2 = GetVertexBuilderWithFeatureId(new Vector3(0, 10, 0), new Vector3(0, 0, 1), featureId); + + prim.AddTriangle(vt0, vt1, vt2); + var scene = new SceneBuilder(); + scene.AddRigidMesh(mesh, Matrix4x4.Identity); + var model = scene.ToGltf2(); + + var featureIdAttribute = new MeshExtMeshFeatureID(1, 0); + + // Set the FeatureIds + var featureIds = new List() { featureIdAttribute }; + model.LogicalMeshes[0].Primitives[0].SetFeatureIds(featureIds); + + // Validate the FeatureIds + var cesiumExtMeshFeaturesExtension = (MeshExtMeshFeatures)model.LogicalMeshes[0].Primitives[0].Extensions.FirstOrDefault(); + Assert.NotNull(cesiumExtMeshFeaturesExtension.FeatureIds); + + Assert.IsTrue(cesiumExtMeshFeaturesExtension.FeatureIds.Equals(featureIds)); + + // Check there should be a custom vertex attribute with name _FEATURE_ID_{attribute} + var attribute = cesiumExtMeshFeaturesExtension.FeatureIds[0].Attribute; + Assert.IsTrue(attribute == 0); + var primitive = model.LogicalMeshes[0].Primitives[0]; + var featureIdVertexAccessor = primitive.GetVertexAccessor($"_FEATURE_ID_{attribute}"); + Assert.NotNull(featureIdVertexAccessor); + var items = featureIdVertexAccessor.AsScalarArray(); + Assert.AreEqual(items, new List { featureId, featureId, featureId }); + + var ctx = new ValidationResult(model, ValidationMode.Strict, true); + + model.ValidateContent(ctx.GetContext()); + scene.AttachToCurrentTest("cesium_ext_mesh_features_feature_id_attribute.glb"); + scene.AttachToCurrentTest("cesium_ext_mesh_features_feature_id_attribute.gltf"); + scene.AttachToCurrentTest("cesium_ext_mesh_features_feature_id_attribute.plotly"); + } + + [Test(Description = "Test for settting the FeatureIds with a texture. See sample https://github.com/CesiumGS/3d-tiles-samples/blob/main/glTF/EXT_mesh_features/FeatureIdTexture")] + public void FeaturesIdTextureTest() + { + TestContext.CurrentContext.AttachGltfValidatorLinks(); + + // Bitmap of 16*16 pixels, containing FeatureID's (0, 1, 2, 3) in the red channel + var img0 = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAJElEQVR42mNgYmBgoAQzDLwBgwcwY8FDzIDBDRiR8KgBNDAAAOKBAKByX2jMAAAAAElFTkSuQmCC"; + var imageBytes = Convert.FromBase64String(img0); + var imageBuilder = ImageBuilder.From(imageBytes); + + var material = MaterialBuilder + .CreateDefault() + .WithMetallicRoughnessShader() + .WithBaseColor(imageBuilder, new Vector4(1, 1, 1, 1)) + .WithDoubleSide(true) + .WithAlpha(Materials.AlphaMode.OPAQUE) + .WithMetallicRoughness(0, 1); + + var mesh = VBTexture1.CreateCompatibleMesh("mesh"); + var prim = mesh.UsePrimitive(material); + prim.AddTriangle( + new VBTexture1(new VertexPosition(0, 0, 0), new Vector2(0, 1)), + new VBTexture1(new VertexPosition(1, 0, 0), new Vector2(1, 1)), + new VBTexture1(new VertexPosition(0, 1, 0), new Vector2(0, 0))); + + prim.AddTriangle( + new VBTexture1(new VertexPosition(1, 0, 0), new Vector2(1, 1)), + new VBTexture1(new VertexPosition(1, 1, 0), new Vector2(1, 0)), + new VBTexture1(new VertexPosition(0, 1, 0), new Vector2(0, 0))); + + var scene = new SceneBuilder(); + scene.AddRigidMesh(mesh, Matrix4x4.Identity); + var model = scene.ToGltf2(); + + // Set the FeatureIds, pointing to the red channel of the texture + var texture = new MeshExtMeshFeatureIDTexture(new List() { 0 }, 0, 0); + var featureIdTexture = new MeshExtMeshFeatureID(4, texture: texture); + var featureIds = new List() { featureIdTexture }; + var primitive = model.LogicalMeshes[0].Primitives[0]; + primitive.SetFeatureIds(featureIds); + + var cesiumExtMeshFeaturesExtension = (MeshExtMeshFeatures)primitive.Extensions.FirstOrDefault(); + Assert.NotNull(cesiumExtMeshFeaturesExtension.FeatureIds); + + Assert.IsTrue(cesiumExtMeshFeaturesExtension.FeatureIds.Equals(featureIds)); + var featureId = cesiumExtMeshFeaturesExtension.FeatureIds[0]; + var texCoord = featureId.Texture.TextureCoordinate; + + var textureIdVertexAccessor = primitive.GetVertexAccessor($"TEXCOORD_{texCoord}"); + Assert.NotNull(textureIdVertexAccessor); + Assert.IsTrue(textureIdVertexAccessor.AsVector2Array().Count == 4); + + var ctx = new ValidationResult(model, ValidationMode.Strict, true); + + model.ValidateContent(ctx.GetContext()); + scene.AttachToCurrentTest("cesium_ext_mesh_features_feature_id_texture.glb"); + scene.AttachToCurrentTest("cesium_ext_mesh_features_feature_id_texture.gltf"); + scene.AttachToCurrentTest("cesium_ext_mesh_features_feature_id_texture.plotly"); + } + + private static VertexBuilder GetVertexBuilderWithFeatureId(Vector3 position, Vector3 normal, int featureid) + { + var vp0 = new VertexPositionNormal(position, normal); + var vb0 = new VertexBuilder(vp0, featureid); + return vb0; + } + } +} diff --git a/tests/SharpGLTF.Cesium.Tests/VertexWithFeatureId.cs b/tests/SharpGLTF.Cesium.Tests/VertexWithFeatureId.cs new file mode 100644 index 00000000..f0b1763b --- /dev/null +++ b/tests/SharpGLTF.Cesium.Tests/VertexWithFeatureId.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Numerics; + +using SharpGLTF.Geometry.VertexTypes; +using SharpGLTF.Schema2; + +namespace SharpGLTF +{ + [System.Diagnostics.DebuggerDisplay("𝐂:{Color} 𝐔𝐕:{TexCoord}")] + public struct VertexWithFeatureId : IVertexCustom + { + public static implicit operator VertexWithFeatureId(float batchId) + { + return new VertexWithFeatureId(batchId); + } + + public VertexWithFeatureId(float batchId) + { + BatchId = batchId; + } + + public const string CUSTOMATTRIBUTENAME = "_FEATURE_ID_0"; + + [VertexAttribute(CUSTOMATTRIBUTENAME, EncodingType.FLOAT, false)] + public float BatchId; + + public int MaxColors => 0; + + public int MaxTextCoords => 0; + + public IEnumerable CustomAttributes => throw new NotImplementedException(); + + public void SetColor(int setIndex, Vector4 color) { } + + public void SetTexCoord(int setIndex, Vector2 coord) { } + + public Vector4 GetColor(int index) { throw new ArgumentOutOfRangeException(nameof(index)); } + + public Vector2 GetTexCoord(int index) { throw new ArgumentOutOfRangeException(nameof(index)); } + + public void Validate() { } + + public object GetCustomAttribute(string attributeName) + { + return attributeName == CUSTOMATTRIBUTENAME ? (Object)BatchId : null; + } + + public bool TryGetCustomAttribute(string attribute, out object value) + { + if (attribute != CUSTOMATTRIBUTENAME) { value = null; return false; } + value = BatchId; return true; + } + + public void SetCustomAttribute(string attributeName, object value) + { + throw new NotImplementedException(); + } + + public VertexMaterialDelta Subtract(IVertexMaterial baseValue) + { + throw new NotImplementedException(); + } + + public void Add(in VertexMaterialDelta delta) + { + throw new NotImplementedException(); + } + } +}