Browse Source

Merge pull request #201 from bertt/add_cesium_ext_instance_features

add Cesium ext_instance_features
Vicente Penades 2 years ago
parent
commit
363bc729a5

+ 0 - 1
build/SharpGLTF.CodeGen/Constants.cs

@@ -20,7 +20,6 @@ namespace SharpGLTF
         /// </summary>
         public static string LocalRepoDirectory => System.IO.Path.Combine(ProgramDirectory, "glTF");
 
-
         #endregion
 
         #region main schema paths

+ 33 - 0
build/SharpGLTF.CodeGen/Ext.EXT_InstanceFeatures.cs

@@ -0,0 +1,33 @@
+using SharpGLTF.CodeGen;
+using SharpGLTF.SchemaReflection;
+using System.Collections.Generic;
+
+namespace SharpGLTF
+{
+    class ExtInstanceFeaturesExtension : SchemaProcessor
+    {
+        public override string GetTargetProject() { return Constants.CesiumProjectDirectory; }
+
+        private static string NodeSchemaUri => Constants.CustomExtensionsPath("EXT_instance_features", "node.EXT_instance_features.schema.json");
+
+        public override void PrepareTypes(CSharpEmitter newEmitter, SchemaType.Context ctx)
+        {
+            newEmitter.SetRuntimeName("EXT_instance_features glTF Node extension", "MeshExtInstanceFeatures");
+            newEmitter.SetRuntimeName("Feature ID in EXT_instance_features", "MeshExtInstanceFeatureID");
+        }
+
+        public override IEnumerable<(string TargetFileName, SchemaType.Context Schema)> Process()
+        {
+            yield return ("Ext.CESIUM_ext_instance_features.g", ProcessNode());
+        }
+
+        private static SchemaType.Context ProcessNode()
+        {
+            var ctx = SchemaProcessing.LoadSchemaContext(NodeSchemaUri);
+            ctx.IgnoredByCodeEmitter("glTF Property");
+            ctx.IgnoredByCodeEmitter("glTF Child of Root Property");
+            ctx.IgnoredByCodeEmitter("Texture Info");
+            return ctx;
+        }
+    }
+}

+ 2 - 0
build/SharpGLTF.CodeGen/Program.cs

@@ -60,6 +60,8 @@ namespace SharpGLTF
 
             processors.Add(new ExtMeshFeaturesExtension());
 
+            processors.Add(new ExtInstanceFeaturesExtension());
+
             // ----------------------------------------------  process all files
 
             foreach (var processor in processors)

+ 8 - 0
build/SharpGLTF.CodeGen/Schemas/EXT_instance_features/README.md

@@ -0,0 +1,8 @@
+# EXT_instance_features
+
+This directory contains schema's for Cesium extension 
+EXT_instance_features
+
+Specs: https://github.com/CesiumGS/glTF/tree/3d-tiles-next/extensions/2.0/Vendor/EXT_instance_features
+
+Schema's are copied from https://github.com/CesiumGS/glTF/tree/3d-tiles-next/extensions/2.0/Vendor/EXT_instance_features/schema

+ 43 - 0
build/SharpGLTF.CodeGen/Schemas/EXT_instance_features/schema/featureId.schema.json

@@ -0,0 +1,43 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "featureId.schema.json",
+  "title": "Feature ID in EXT_instance_features",
+  "type": "object",
+  "description": "Feature IDs stored in a GPU mesh instancing attribute",
+  "allOf": [
+    {
+      "$ref": "glTFProperty.schema.json"
+    }
+  ],
+  "properties": {
+    "featureCount": {
+      "type": "integer",
+      "minimum": 1,
+      "description": "The number of unique features in the attribute."
+    },
+    "nullFeatureId": {
+      "type": "integer",
+      "minimum": 0,
+      "description": "A value that indicates that no feature is associated with this instance."
+    },
+    "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 this is omitted, then the feature IDs are assigned to the GPU instances by their index.",
+      "$ref": "featureIdAttribute.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"
+  ]
+}

+ 8 - 0
build/SharpGLTF.CodeGen/Schemas/EXT_instance_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_instance_features",
+  "type": "integer",
+  "minimum": 0,
+  "description": "An integer value used to construct a string in the format `_FEATURE_ID_<set index>` which is a reference to a key in `EXT_mesh_gpu_instancing.attributes` dictionary (e.g. a value of `0` corresponds to `_FEATURE_ID_0`)."
+}

+ 27 - 0
build/SharpGLTF.CodeGen/Schemas/EXT_instance_features/schema/node.EXT_instance_features.schema.json

@@ -0,0 +1,27 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "node.EXT_instance_features.schema.json",
+  "title": "EXT_instance_features glTF Node extension",
+  "type": "object",
+  "description": "An object describing per-instance feature IDs.",
+  "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"
+  ]
+}

+ 12 - 0
build/SharpGLTF.CodeGen/SharpGLTF.CodeGen.csproj

@@ -30,4 +30,16 @@
     </None>
   </ItemGroup>
 
+  <ItemGroup>
+    <None Update="Schemas\EXT_instance_features\schema\featureId.schema.json">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </None>
+    <None Update="Schemas\EXT_instance_features\schema\featureIdAttribute.schema.json">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </None>
+    <None Update="Schemas\EXT_instance_features\schema\node.EXT_instance_features.schema.json">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </None>
+  </ItemGroup>
+
 </Project>

+ 1 - 0
src/SharpGLTF.Cesium/Schema2/CesiumExtensions.cs

@@ -21,6 +21,7 @@ namespace SharpGLTF.Schema2
             _CesiumRegistered = true;
 
             ExtensionsFactory.RegisterExtension<MeshPrimitive, CesiumPrimitiveOutline>("CESIUM_primitive_outline");
+            ExtensionsFactory.RegisterExtension<Node, MeshExtInstanceFeatures>("EXT_instance_features");
             ExtensionsFactory.RegisterExtension<MeshPrimitive, MeshExtMeshFeatures>("EXT_mesh_features");
 
         }

+ 110 - 0
src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_instance_features.g.cs

@@ -0,0 +1,110 @@
+// <auto-generated/>
+
+//------------------------------------------------------------------------------------------------
+//      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;
+
+	/// <summary>
+	/// Feature IDs stored in a GPU mesh instancing attribute
+	/// </summary>
+	#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 MeshExtInstanceFeatureID : 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;
+		
+	
+		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);
+		}
+	
+		protected override void DeserializeProperty(string jsonPropertyName, ref Utf8JsonReader reader)
+		{
+			switch (jsonPropertyName)
+			{
+				case "attribute": _attribute = DeserializePropertyValue<Int32?>(ref reader); break;
+				case "featureCount": _featureCount = DeserializePropertyValue<Int32>(ref reader); break;
+				case "label": _label = DeserializePropertyValue<String>(ref reader); break;
+				case "nullFeatureId": _nullFeatureId = DeserializePropertyValue<Int32?>(ref reader); break;
+				case "propertyTable": _propertyTable = DeserializePropertyValue<Int32?>(ref reader); break;
+				default: base.DeserializeProperty(jsonPropertyName,ref reader); break;
+			}
+		}
+	
+	}
+
+	/// <summary>
+	/// An object describing per-instance feature IDs.
+	/// </summary>
+	#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 MeshExtInstanceFeatures : ExtraProperties
+	{
+	
+		private const int _featureIdsMinItems = 1;
+		private List<MeshExtInstanceFeatureID> _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<MeshExtInstanceFeatureID>(ref reader, _featureIds); break;
+				default: base.DeserializeProperty(jsonPropertyName,ref reader); break;
+			}
+		}
+	
+	}
+
+}

+ 139 - 0
src/SharpGLTF.Cesium/Schema2/MeshExtInstanceFeatures.cs

@@ -0,0 +1,139 @@
+using SharpGLTF.Validation;
+using System.Collections.Generic;
+using System.Linq;
+using System.Xml.Linq;
+
+namespace SharpGLTF.Schema2
+{
+    public partial class MeshExtInstanceFeatures
+    {
+        private Node _node;
+        internal MeshExtInstanceFeatures(Node node)
+        {
+            _node = node;
+            _featureIds = new List<MeshExtInstanceFeatureID>();
+        }
+
+        public List<MeshExtInstanceFeatureID> FeatureIds
+        {
+            get
+            {
+                return _featureIds;
+            }
+            set
+            {
+                if (value == null) { _featureIds = null; return; }
+
+                _featureIds = value;
+            }
+        }
+
+        protected override void OnValidateContent(ValidationContext validate)
+        {
+            var extInstanceFeatures = _node.Extensions.Where(item => item is MeshExtInstanceFeatures).FirstOrDefault();
+            validate.NotNull(nameof(extInstanceFeatures), extInstanceFeatures);
+            var ext = (MeshExtInstanceFeatures)extInstanceFeatures;
+            var extMeshGpInstancing = _node.Extensions.Where(item => item is MeshGpuInstancing).FirstOrDefault();
+            validate.NotNull(nameof(extMeshGpInstancing), extMeshGpInstancing);
+
+            validate.NotNull(nameof(FeatureIds), ext.FeatureIds);
+            validate.IsTrue(nameof(FeatureIds), ext.FeatureIds.Count > 0, "Instance FeatureIds has items");
+
+            base.OnValidateContent(validate);
+        }
+    }
+
+    public partial class MeshExtInstanceFeatureID
+    {
+        public MeshExtInstanceFeatureID()
+        {
+        }
+
+        public MeshExtInstanceFeatureID(int featureCount, int? attribute = null, int? propertyTable = null, string label = null, int? nullFeatureId = null)
+        {
+            _featureCount = featureCount;
+            _attribute = attribute;
+            _label = label;
+            _propertyTable = propertyTable;
+            _nullFeatureId = nullFeatureId;
+        }
+
+        /// <summary>
+        /// The number of unique features in the attribute
+        /// </summary>
+        public int FeatureCount { get => _featureCount; }
+
+        /// <summary>
+        /// An attribute containing feature IDs. When this is omitted, then the feature IDs are assigned to the GPU instances by their index.
+        /// </summary>
+        public int? Attribute { get => _attribute; }
+
+        /// <summary>
+        /// A label assigned to this feature ID set
+        /// </summary>
+        public string Label { get => _label; }
+
+        /// <summary>
+        /// The index of the property table containing per-feature property values. Only applicable when using the `EXT_structural_metadata` extension.
+        /// </summary>
+        public int? PropertyTable { get => _propertyTable; }
+
+        /// <summary>
+        /// A value that indicates that no feature is associated with this instance
+        /// </summary>
+        public int? NullFeatureId { get => _nullFeatureId; }
+    }
+
+    public static class ExtInstanceFeatures
+    {
+        /// <summary>
+        /// Set the instance feature ids for this node.
+        /// </summary>
+        /// <param name="node"></param>
+        /// <param name="instanceFeatureIds"></param>
+        public static void SetFeatureIds(this Node node, List<MeshExtInstanceFeatureID> instanceFeatureIds)
+        {
+            if (instanceFeatureIds == null) { node.RemoveExtensions<MeshExtInstanceFeatures>(); return; }
+
+            Guard.NotNullOrEmpty(instanceFeatureIds, nameof(instanceFeatureIds));
+
+            var extMeshGpInstancing = node.Extensions.Where(item => item is MeshGpuInstancing).FirstOrDefault();
+            Guard.NotNull(extMeshGpInstancing, nameof(extMeshGpInstancing));
+
+            foreach (var instanceFeatureId in instanceFeatureIds)
+            {
+                ValidateInstanceFeatureId(node, instanceFeatureId);
+            };
+
+            var ext = node.UseExtension<MeshExtInstanceFeatures>();
+            ext.FeatureIds = instanceFeatureIds;
+        }
+
+        private static void ValidateInstanceFeatureId(Node node, MeshExtInstanceFeatureID instanceFeatureId)
+        {
+            Guard.MustBeGreaterThanOrEqualTo((int)instanceFeatureId.FeatureCount, 1, nameof(instanceFeatureId.FeatureCount));
+
+            if (instanceFeatureId.NullFeatureId.HasValue)
+            {
+                Guard.MustBeGreaterThanOrEqualTo((int)instanceFeatureId.NullFeatureId, 0, nameof(instanceFeatureId.NullFeatureId));
+            }
+            if (instanceFeatureId.Label != null)
+            {
+                var regex = "^[a-zA-Z_][a-zA-Z0-9_]*$";
+                Guard.IsTrue(System.Text.RegularExpressions.Regex.IsMatch(instanceFeatureId.Label, regex), nameof(instanceFeatureId.Label));
+            }
+            if (instanceFeatureId.Attribute.HasValue)
+            {
+                Guard.MustBeGreaterThanOrEqualTo((int)instanceFeatureId.Attribute, 0, nameof(instanceFeatureId.Attribute));
+                var expectedVertexAttribute = $"_FEATURE_ID_{instanceFeatureId.Attribute}";
+                var gpuInstancing = node.GetGpuInstancing();
+                var featureIdAccessors = gpuInstancing.GetAccessor(expectedVertexAttribute);
+                Guard.NotNull(featureIdAccessors, expectedVertexAttribute);
+            }
+            if (instanceFeatureId.PropertyTable.HasValue)
+            {
+                Guard.MustBeGreaterThanOrEqualTo((int)instanceFeatureId.PropertyTable, 0, nameof(instanceFeatureId.PropertyTable));
+            }
+        }
+    }
+}

+ 6 - 4
src/SharpGLTF.Cesium/Schema2/MeshExtMeshFeatures.cs

@@ -1,6 +1,7 @@
 using SharpGLTF.Validation;
 using System.Collections.Generic;
 using System.Linq;
+using System.Xml.Linq;
 
 namespace SharpGLTF.Schema2
 {
@@ -26,10 +27,11 @@ namespace SharpGLTF.Schema2
 
         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");
+            var extMeshFeatures = _meshPrimitive.Extensions.Where(item => item is MeshExtMeshFeatures).FirstOrDefault();
+            validate.NotNull(nameof(extMeshFeatures), extMeshFeatures);
+            var ext = (MeshExtMeshFeatures)extMeshFeatures;
+            validate.NotNull(nameof(FeatureIds), ext.FeatureIds);
+            validate.IsTrue(nameof(FeatureIds), ext.FeatureIds.Count > 0, "FeatureIds has items");
 
             base.OnValidateContent(validate);
         }

+ 65 - 0
tests/SharpGLTF.Cesium.Tests/ExtInstanceFeaturesTests.cs

@@ -0,0 +1,65 @@
+using NUnit.Framework;
+using SharpGLTF.Scenes;
+using SharpGLTF.Schema2;
+using SharpGLTF.Transforms;
+using SharpGLTF.Validation;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Text.Json.Nodes;
+
+namespace SharpGLTF.Cesium
+{
+    [Category("Toolkit.Scenes")]
+    public class ExtInstanceFeaturesTests
+    {
+        [SetUp]
+        public void SetUp()
+        {
+            CesiumExtensions.RegisterExtensions();
+        }
+
+        [Test(Description = "Creates a gpu_instancing glTF from a tree with Cesium EXT_Instance_Features")]
+        public void AddExtGpuInstanceFeatures()
+        {
+            var settings = SceneBuilderSchema2Settings.WithGpuInstancing;
+            settings.GpuMeshInstancingMinCount = 0;
+
+            var modelRoot = ModelRoot.Load(ResourceInfo.From("tree.glb"));
+            var meshBuilder = modelRoot.LogicalMeshes.First().ToMeshBuilder();
+            var sceneBuilder = new SceneBuilder();
+            var quaternion = Quaternion.CreateFromYawPitchRoll(0, 0, 0);
+            var scale = Vector3.One;
+
+            sceneBuilder.
+                    AddRigidMesh(meshBuilder, new AffineTransform(scale, quaternion, new Vector3(-10, 0, 10))).
+                    WithExtras(JsonNode.Parse("{\"_FEATURE_ID_0\":0}"));
+            sceneBuilder.
+                    AddRigidMesh(meshBuilder, new AffineTransform(scale, quaternion, new Vector3(0, 0, 0))).
+                    WithExtras(JsonNode.Parse("{\"_FEATURE_ID_0\":1}"));
+
+
+            var featureId0 = new MeshExtInstanceFeatureID(2, 0, 0, "Forests", 2);
+            var featureId1 = new MeshExtInstanceFeatureID(9, propertyTable: 1, label: "Trees");
+
+            var featureIds = new List<MeshExtInstanceFeatureID>() { featureId0, featureId1 };
+
+            var model = sceneBuilder.ToGltf2(settings);
+            model.LogicalNodes[0].SetFeatureIds(featureIds);
+
+            var cesiumExtInstanceFeaturesExtension = (MeshExtInstanceFeatures)model.LogicalNodes[0].Extensions.Where(item => item is MeshExtInstanceFeatures).FirstOrDefault();
+
+            Assert.That(cesiumExtInstanceFeaturesExtension.FeatureIds, Is.Not.Null);
+            Assert.That(cesiumExtInstanceFeaturesExtension.FeatureIds.Equals(featureIds));
+            Assert.That(cesiumExtInstanceFeaturesExtension.FeatureIds[0].Equals(featureId0));
+            Assert.That(cesiumExtInstanceFeaturesExtension.FeatureIds[1].Equals(featureId1));
+
+            var ctx = new ValidationResult(model, ValidationMode.Strict, true);
+            model.ValidateContent(ctx.GetContext());
+
+            model.AttachToCurrentTest("cesium_ext_instance_features.glb");
+            model.AttachToCurrentTest("cesium_ext_instance_features.gltf");
+            model.AttachToCurrentTest("cesium_ext_instance_features.plotly");
+        }
+    }
+}

+ 8 - 2
tests/SharpGLTF.Cesium.Tests/SharpGLTF.Cesium.Tests.csproj

@@ -1,7 +1,7 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFrameworks>net471;net8.0-windows</TargetFrameworks>
+    <TargetFrameworks>net471;net6.0-windows;net8.0-windows</TargetFrameworks>
     <IsPackable>false</IsPackable>
     <RootNamespace>SharpGLTF</RootNamespace>
     <LangVersion>latest</LangVersion>
@@ -20,4 +20,10 @@
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
   </ItemGroup>
 
+  <ItemGroup>
+    <None Update="tree.glb">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </None>
+  </ItemGroup>
+
 </Project>

BIN
tests/SharpGLTF.Cesium.Tests/tree.glb