From fdb068037d5462600685ff7a4a1bd2a1206cef1f Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Wed, 29 Nov 2023 22:50:56 +0100 Subject: [PATCH 01/57] initial coding ext_structural_metadata --- .../Ext.EXT_Structural_Metadata.cs | 34 + build/SharpGLTF.CodeGen/Program.cs | 5 +- .../Schemas/EXT_structural_metadata/README.md | 578 +++++ .../figures/property-attribute.png | Bin 0 -> 97434 bytes .../figures/property-attribute.svg | 474 ++++ .../figures/property-table.png | Bin 0 -> 78190 bytes .../figures/property-table.svg | 1501 +++++++++++++ .../figures/property-texture.png | Bin 0 -> 88625 bytes .../figures/property-texture.svg | 1987 +++++++++++++++++ .../EXT_structural_metadata.schema.json | 26 + .../schema/class.property.schema.json | 182 ++ .../schema/class.schema.json | 34 + .../schema/definitions.schema.json | 119 + .../schema/enum.schema.json | 70 + .../schema/enum.value.schema.json | 34 + .../glTF.EXT_structural_metadata.schema.json | 71 + ...mitive.EXT_structural_metadata.schema.json | 32 + .../propertyAttribute.property.schema.json | 55 + .../schema/propertyAttribute.schema.json | 36 + .../schema/propertyTable.property.schema.json | 117 + .../schema/propertyTable.schema.json | 42 + .../propertyTexture.property.schema.json | 62 + .../schema/propertyTexture.schema.json | 36 + .../schema/schema.schema.json | 55 + .../SharpGLTF.CodeGen.csproj | 45 + .../Ext.CESIUM_ext_structural_metadata.g.cs | 66 + 26 files changed, 5660 insertions(+), 1 deletion(-) create mode 100644 build/SharpGLTF.CodeGen/Ext.EXT_Structural_Metadata.cs create mode 100644 build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/README.md create mode 100644 build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/figures/property-attribute.png create mode 100644 build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/figures/property-attribute.svg create mode 100644 build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/figures/property-table.png create mode 100644 build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/figures/property-table.svg create mode 100644 build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/figures/property-texture.png create mode 100644 build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/figures/property-texture.svg create mode 100644 build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/EXT_structural_metadata.schema.json create mode 100644 build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/class.property.schema.json create mode 100644 build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/class.schema.json create mode 100644 build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/definitions.schema.json create mode 100644 build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/enum.schema.json create mode 100644 build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/enum.value.schema.json create mode 100644 build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/glTF.EXT_structural_metadata.schema.json create mode 100644 build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/mesh.primitive.EXT_structural_metadata.schema.json create mode 100644 build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/propertyAttribute.property.schema.json create mode 100644 build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/propertyAttribute.schema.json create mode 100644 build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/propertyTable.property.schema.json create mode 100644 build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/propertyTable.schema.json create mode 100644 build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/propertyTexture.property.schema.json create mode 100644 build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/propertyTexture.schema.json create mode 100644 build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/schema.schema.json create mode 100644 src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata.g.cs diff --git a/build/SharpGLTF.CodeGen/Ext.EXT_Structural_Metadata.cs b/build/SharpGLTF.CodeGen/Ext.EXT_Structural_Metadata.cs new file mode 100644 index 00000000..ecff1d81 --- /dev/null +++ b/build/SharpGLTF.CodeGen/Ext.EXT_Structural_Metadata.cs @@ -0,0 +1,34 @@ +using SharpGLTF.CodeGen; +using SharpGLTF.SchemaReflection; +using System.Collections.Generic; + +namespace SharpGLTF +{ + class ExtStructuralMetadataExtension : SchemaProcessor + { + public override string GetTargetProject() { return Constants.CesiumProjectDirectory; } + + private static string RootSchemaUri => Constants.CustomExtensionsPath("EXT_structural_metadata", "EXT_structural_metadata.schema.json"); + + 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_structural_metadata.g", ProcessRoot()); + } + + private static SchemaType.Context ProcessRoot() + { + var ctx = SchemaProcessing.LoadSchemaContext(RootSchemaUri); + ctx.IgnoredByCodeEmitter("glTF Property"); + ctx.IgnoredByCodeEmitter("glTF Child of Root Property"); + ctx.IgnoredByCodeEmitter("Texture Info"); + return ctx; + } + } +} diff --git a/build/SharpGLTF.CodeGen/Program.cs b/build/SharpGLTF.CodeGen/Program.cs index b9278453..d3099ca8 100644 --- a/build/SharpGLTF.CodeGen/Program.cs +++ b/build/SharpGLTF.CodeGen/Program.cs @@ -15,7 +15,7 @@ partial class Program static void Main(string[] args) { - SchemaDownload.Syncronize(Constants.RemoteSchemaRepo, Constants.LocalRepoDirectory); + // SchemaDownload.Syncronize(Constants.RemoteSchemaRepo, Constants.LocalRepoDirectory); var processors = new List(); @@ -60,6 +60,9 @@ static void Main(string[] args) processors.Add(new ExtMeshFeaturesExtension()); + processors.Add(new ExtStructuralMetadataExtension()); + + // ---------------------------------------------- process all files foreach (var processor in processors) diff --git a/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/README.md b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/README.md new file mode 100644 index 00000000..c5f53d99 --- /dev/null +++ b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/README.md @@ -0,0 +1,578 @@ + + +# EXT_structural_metadata + +PR at Khronos: https://github.com/KhronosGroup/glTF/pull/2151 + +This directory contains schema's for Cesium extension EXT_structural_metadata + +Copied from https://github.com/CesiumGS/glTF/tree/proposal-EXT_structural_metadata + + +## Contributors + +* Peter Gagliardi, Cesium +* Sean Lilley, Cesium +* Sam Suhag, Cesium +* Don McCurdy, Independent +* Marco Hutter, Cesium +* Bao Tran, Cesium +* Samuel Vargas, Cesium +* Patrick Cozzi, Cesium + + +## Status + +Draft + + +## Dependencies + +Written against the glTF 2.0 specification. + + +## Table of Contents + +- [Overview](#overview) +- [Schema Definitions](#schema-definitions) + - [Overview](#overview-1) + - [Schema](#schema) + - [Class](#class) + - [Class Property](#class-property) + - [Enum](#enum) + - [Enum Value](#enum-value) +- [Metadata Storage](#metadata-storage) + - [Property Tables](#property-tables) + - [Property Attributes](#property-attributes) + - [Property Textures](#property-textures) + - [Binary Data Storage](#binary-data-storage) +- [Assigning Metadata](#assigning-metadata) +- [Optional vs. Required](#optional-vs-required) +- [Schema](#schema-1) +- [Revision History](#revision-history) + +## Overview + +This extension defines a means of storing structured metadata within a glTF 2.0 asset. The key concepts of this extension are the definition of a schema that describes the structure of the metadata, and methods for storing and associating metadata with different entities within the asset. + +The metadata schema definition includes a JSON representation of the schema structure, suitable for being stored inside a glTF asset. The storage formats that are defined in this extension allow storing metadata in standard glTF vertex attributes, or in the channels of a texture. Both representations are compact binary storage formats that are appropriate for efficiently transmitting large quantities of metadata. + +The schema definition and the storage formats in this extension are implementations of the [3D Metadata Specification](https://github.com/CesiumGS/3d-tiles/tree/main/specification/Metadata). This specification should be considered a normative reference for definitions and requirements. This document provides inline definitions of terms where appropriate. + +> **Disambiguation:** glTF has other methods of storing details that could similarly be described as metadata or properties, including [`KHR_xmp_json_ld`](../../Khronos/KHR_xmp_json_ld), Extras, and Extensions. While those methods associate data with discrete objects in a glTF asset — nodes, materials, etc. — `EXT_structural_metadata` is uniquely suited for properties of more granular conceptual features in subregions composed of vertices or texels. + +## Schema Definitions + +#### Overview + +Data types and meanings of properties are provided by a schema, as defined in the [3D Metadata Specification](https://github.com/CesiumGS/3d-tiles/tree/main/specification/Metadata/) and summarized below. + +#### Schema + +*Defined in [schema.schema.json](./schema/schema.schema.json).* + +Top-level definitions for the structure and data types of properties. The schema provides a set of [classes](#class) and [enums](#enum) the asset can reference. + +A schema may be embedded in the extension directly or referenced externally with the `schemaUri` property. Multiple glTF assets may refer to the same external schema to avoid duplication. A schema is defined by an `EXT_structural_metadata` extension attached to the glTF root object. + +> **Example:** A simple schema defining enums and classes. +> +> ```jsonc +> { +> "extensions": { +> "EXT_structural_metadata": { +> "schema": { +> "id": "schema_001", +> "name": "Schema 001", +> "description": "An example schema.", +> "version": "3.5.1", +> "enums": { ... }, +> "classes": { ... } +> } +> } +> } +> } +> ``` + +#### Class + +*Defined in [class.schema.json](./schema/class.schema.json).* + +Template for metadata entities. Classes provide a list of property definitions. Instances of a class can be created from property values that conform to the class's property definitions. + +Classes are defined as entries in the `schema.classes` dictionary, indexed by class ID. Class IDs must be alphanumeric identifiers matching the regular expression `^[a-zA-Z_][a-zA-Z0-9_]*$`. + +> **Example:** A "Tree" class, which might describe a table of tree measurements taken in a park. Property definitions are abbreviated here, and introduced in the next section. +> +> ```jsonc +> { +> "extensions": { +> "EXT_structural_metadata": { +> "schema": { +> "classes": { +> "tree": { +> "name": "Tree", +> "description": "Woody, perennial plant.", +> "properties": { +> "species": { ... }, +> "age": { ... }, +> "height": { ... }, +> "diameter": { ... } +> } +> } +> } +> } +> } +> } +> } +> ``` + +#### Class Property + +*Defined in [class.property.schema.json](./schema/class.property.schema.json).* + +Class properties are defined abstractly in a class. The class is instantiated with specific values conforming to these properties. Class properties support a richer variety of data types than glTF accessors or GPU shading languages allow. Details about the supported types can be found in the [3D Metadata Specification](https://github.com/CesiumGS/3d-tiles/tree/main/specification/Metadata#property). + +Class properties are defined as entries in the `class.properties` dictionary, indexed by property ID. Property IDs must be alphanumeric identifiers matching the regular expression `^[a-zA-Z_][a-zA-Z0-9_]*$`. + +> **Example:** A "Tree" class, which might describe a table of tree measurements taken in a park. Properties include species, height, and diameter of each tree, as well as the number of birds observed in its branches. +> +> ```jsonc +> { +> "extensions": { +> "EXT_structural_metadata": { +> "schema": { +> "classes": { +> "tree": { +> "name": "Tree", +> "description": "Woody, perennial plant.", +> "properties": { +> "species": { +> "description": "Type of tree.", +> "type": "ENUM", +> "enumType": "speciesEnum", +> "required": true +> }, +> "age": { +> "description": "The age of the tree, in years", +> "type": "SCALAR", +> "componentType": "UINT8", +> "required": true +> }, +> "height": { +> "description": "Height of tree measured from ground level, in meters.", +> "type": "SCALAR", +> "componentType": "FLOAT32" +> }, +> "diameter": { +> "description": "Diameter at trunk base, in meters.", +> "type": "SCALAR", +> "componentType": "FLOAT32" +> } +> } +> } +> } +> } +> } +> } +> } +> ``` + +#### Enum + +*Defined in [enum.schema.json](./schema/enum.schema.json).* + +Set of categorical types, defined as `(name, value)` pairs. Enum properties use an enum as their type. + +Enums are defined as entries in the `schema.enums` dictionary, indexed by an enum ID. Enum IDs must be alphanumeric identifiers matching the regular expression `^[a-zA-Z_][a-zA-Z0-9_]*$`. + +> **Example:** A "Species" enum defining types of trees. An "Unspecified" enum value is optional, but when provided as the `noData` value for a property (see: [3D Metadata → No Data Values](https://github.com/CesiumGS/3d-tiles/tree/main/specification/Metadata#required-properties-no-data-values-and-default-values)) may be helpful to identify missing data. +> +> ```jsonc +> { +> "extensions": { +> "EXT_structural_metadata": { +> "schema": { +> "enums": { +> "speciesEnum": { +> "name": "Species", +> "description": "An example enum for tree species.", +> "values": [ +> {"name": "Unspecified", "value": 0}, +> {"name": "Oak", "value": 1}, +> {"name": "Pine", "value": 2}, +> {"name": "Maple", "value": 3} +> ] +> } +> } +> } +> } +> } +> } +> ``` + +#### Enum Value + +*Defined in [enum.value.schema.json](./schema/enum.value.schema.json).* + +Pairs of `(name, value)` entries representing possible values of an enum property. + +Enum values are defined as entries in the `enum.values` array. Duplicate names or duplicate integer values are not allowed. + + +## Metadata Storage + +The classes defined in the schema are templates describing the data types and meanings of properties. An instance of such a metadata class is referred to as a _metadata entity_, and can be created from a set of values that conform to the structure of the class. This extension defines different ways of storing large amounts of property values inside a glTF asset, in compact binary forms: + +- **Property Tables** store property values as parallel arrays in a column-based binary layout, using standard glTF buffer views. These tables can be accessed with a row index, and allow associating complex, structured metadata with arbitrary types with entities of a glTF asset on different levels of granularity. +- **Property Attributes** associate vertex attributes of a mesh primitive with a particular metadata class. +- **Property Textures** store property values in channels of a texture, suitable for very high-frequency data mapped to less-detailed 3D surfaces. + +Each storage type refers to a metadata class, and contains a dictionary of `properties`. Each of these properties corresponds to one property of the metadata class and defines how the actual property data is stored. These property storage definitions may override the [`minimum` and `maximum` values](https://github.com/CesiumGS/3d-tiles/tree/main/specification/Metadata#minimum-and-maximum-values) and the [`offset` and `scale`](https://github.com/CesiumGS/3d-tiles/tree/main/specification/Metadata#offset-and-scale) from the property definition in the class, to account for the actual range of values that is stored for each property. + +The following sections describe these storage formats in more detail. + +### Property Tables + +*Defined in [propertyTable.schema.json](./schema/propertyTable.schema.json).* + +Each property table defines a specified number (`count`) of metadata entities conforming to a particular class (`class`), with property values stored as parallel arrays in a column-based binary layout. Property tables support a richer variety of data types than glTF accessors or GPU shading languages allow, and are suitable for datasets that can be expressed in a tabular layout. + +Property tables are defined as entries in the `propertyTables` array of the root-level `EXT_structural_metadata` extension, and may be referenced by [assigning metadata](#assigning-metadata) to glTF elements, or by other extensions. + +The property table may provide value arrays for only a subset of the properties of its class, but class properties marked `required: true` must not be omitted. Each property value array given by the property table must be defined by a class property with the same alphanumeric property ID, with values matching the data type of the class property. + +> **Example:** A `tree_survey_2021-09-29` property table, implementing the `tree` class defined in earlier examples. The table contains observations for 10 trees, with each property defined by a buffer view containing 10 values. +> +> The glTF asset contains multiple objects (trees) that are associated with unique identifiers. These identifiers can be assigned to the objects in different ways. For example, using the [`EXT_mesh_features`](../EXT_mesh_features) extension, the [`EXT_instance_features`](../EXT_instance_features) extension, or an application-specific mechanism for identifying the objects. The identifiers then serve as an index into the property table row that stores the property values for the properties that are defined in the `tree` class. +> +> > ![Property Table](figures/property-table.png) +> +> +> ```jsonc +> { +> "extensions": { +> "EXT_structural_metadata": { +> "schema": { ... }, +> "propertyTables": [{ +> "name": "tree_survey_2021-09-29", +> "class": "tree", +> "count": 10, +> "properties": { +> "species": { +> "values": 0, +> }, +> "age": { +> "values": 1 +> }, +> "height": { +> "values": 2 +> }, +> // "diameter" is not required and has been omitted from this table. +> } +> }] +> } +> } +> } +> ``` + +Property arrays are stored in glTF buffer views and use the binary encoding defined in the [Binary Table Format](https://github.com/CesiumGS/3d-tiles/tree/main/specification/Metadata#binary-table-format) section of the [3D Metadata Specification](https://github.com/CesiumGS/3d-tiles/tree/main/specification/Metadata). + +As in the core glTF specification, values of `NaN`, `+Infinity`, and `-Infinity` are never allowed. + +Each buffer view `byteOffset` must be aligned to a multiple of its component size. + +> **Implementation note:** Authoring tools may choose to align all buffer views to 8-byte boundaries for consistency, but client implementations should only depend on 8-byte alignment for buffer views containing 64-bit component types. + +### Property Attributes + +*Defined in [propertyAttribute.schema.json](./schema/propertyAttribute.schema.json).* + +Property attributes associate vertex attributes of a mesh primitive with a metadata class. They refer to a certain class from the schema definition via their `class` property, and contain a `properties` dictionary that defines a set of properties that conform to this class. Each property refers to an attribute that may be stored in a mesh primitive. + +The property types that are supported via property attributes are therefore restricted to the types that are supported by standard glTF accessors. These types are a strict subset of the types that are supported with the schema definitions in this extension. + +> **Example:** +> +> An example of a property attribute that represents information about the movement of points in a point cloud. +> +> The schema defines a class called `movement`. It has a `direction` property that is a normalized 3D float vector for the movement direction, and a `magnitude` property that describes the magnitude of the movement. +> +> The top-level `propertyAttributes` array contains a property attribute that refers to this class. The `movement` and `direction` properties of the class are associated with attributes called `_DIRECTION` and `_MAGNITUDE`. +> +> The mesh primitive defines (non-indexed) vertices with primitive mode 0, and thus, represents a simple point cloud, with the positions of the points being stored in the `POSITION` attribute. Additionally, it defines vertex attributes `_DIRECTION` and `_MAGNITUDE`, which contain the data for the properties from the property attribute. +> +> ![Property Attribute](figures/property-attribute.png) +> +> _Top-level extension object:_ +> ```jsonc +> { +> "extensions": { +> "EXT_structural_metadata": { +> "schema": { +> "classes": { +> "movement": { +> "name": "movement", +> "description": "The movement of points in a point cloud", +> "properties": { +> "direction": { +> "description": "The movement direction, as a normalized 3D vector", +> "type": "VEC3", +> "componentType": "FLOAT32", +> "required": true +> }, +> "magnitude": { +> "description": "The magnitude of the movement", +> "type": "SCALAR", +> "componentType": "FLOAT32", +> "required": true +> } +> } +> } +> } +> }, +> "propertyAttributes": [{ +> "class": "movement", +> "properties": { +> "direction": { +> "attribute": "_DIRECTION", +> }, +> "magnitude": { +> "attribute": "_MAGNITUDE", +> } +> } +> }] +> } +> } +> } +> ``` +> _Primitive_ +> +> ```jsonc +> { +> "primitives": [ +> { +> "mode:": 0, +> "attributes": { +> "POSITION": 0, +> "_DIRECTION": 1, +> "_MAGNITUDE": 2, +> }, +> "extensions": { +> "EXT_structural_metadata": { +> "propertyAttributes": [0] +> } +> } +> } +> ] +> } +> ``` + + +### Property Textures + +*Defined in [propertyTexture.schema.json](./schema/propertyTexture.schema.json).* + +Property textures use texture channels to store property values conforming to a particular class (identified by ID `class`), with those values accessed directly by texture coordinates. Property textures are especially useful when texture mapping high frequency data to less detailed 3D surfaces. Unlike textures used in glTF materials, property textures are not necessarily visible in a rendered scene. Like property tables, property textures are implementations of the [3D Metadata Specification](https://github.com/CesiumGS/3d-tiles/tree/main/specification/Metadata). + +Property textures are defined as entries in the `propertyTextures` array of the root-level `EXT_structural_metadata` extension, and may be referenced by extensions on primitive objects. + +A property texture may provide channels for only a subset of the properties of its class, but class properties marked `required: true` must not be omitted. + +> **Example:** Property texture implementing a "wall" class, with property values stored in a glTF texture at index 0 and indexed by `TEXCOORD_0`. Each property of the `"wall"` class is stored in one channel of the texture. +> +> ![Property Texture](figures/property-texture.png) +> +> _Class and property texture_ +> +> ```jsonc +> { +> "extensions": { +> "EXT_structural_metadata": { +> "schema": { +> "classes": { +> "wall": { +> "name": "Wall Temperature vs. Insulation", +> "properties": { +> "insideTemperature": { +> "name": "Inside Temperature", +> "type": "SCALAR", +> "componentType": "UINT8" +> }, +> "outsideTemperature": { +> "name": "Outside Temperature", +> "type": "SCALAR", +> "componentType": "UINT8" +> }, +> "insulation": { +> "name": "Insulation Thickness", +> "type": "SCALAR", +> "componentType": "UINT8", +> "normalized": true +> } +> } +> } +> } +> }, +> "propertyTextures": [ +> { +> "class": "wall", +> "properties": { +> "insideTemperature": { +> "index": 0, +> "texCoord": 0, +> "channels": [0] +> }, +> "outsideTemperature": { +> "index": 0, +> "texCoord": 0, +> "channels": [1] +> }, +> "insulation": { +> "index": 0, +> "texCoord": 0, +> "channels": [2] +> } +> } +> } +> ] +> } +> } +> } +> ``` +> +> _Primitive_ +> +> ```jsonc +> { +> "primitives": [ +> { +> "attributes": { +> "POSITION": 0, +> "TEXCOORD_0": 1 +> }, +> "indices": 2, +> "material": 0, +> "extensions": { +> "EXT_structural_metadata": { +> "propertyTextures": [0] +> } +> } +> } +> ] +> } +> ``` + +Each property that is defined in the `propertyTexture` object extends the glTF [`textureInfo`](../../../../specification/2.0/schema/textureInfo.schema.json) object. The `texCoord` specifies a texture coordinate set in the referring primitive. The `index` is the index of the glTF texture object that stores the actual data. Additionally, each property specifies an array of `channels`, which are the indices of the texture channels providing data for the respective property. Channels of an `RGBA` texture are numbered 0–3 respectively. + +Texture filtering must be `9728` (NEAREST), `9729` (LINEAR), or undefined, for any texture object referenced as a property texture. Texture values must be encoded with a linear transfer function. + +> **Example:** A property texture for 2D wind velocity samples. The "speed" property values are stored in the red channel. The "direction" property values are stored as a unit-length vector, with X/Y components in the green and blue channels. Both properties are indexed by UV coordinates in a `TEXCOORD_0` attribute. +> +> ```jsonc +> // Root EXT_structural_metadata extension: +> { +> "propertyTextures": [ +> { +> "class": "wind", +> "properties": { +> "speed": { +> "index": 0, +> "texCoord": 0, +> "channels": [0] +> }, +> "direction": { +> "index": 0, +> "texCoord": 0, +> "channels": [1, 2] +> } +> } +> } +> ] +> } + + +#### Property Texture Data Storage + +Multiple channels of a property texture can be used to represent individual bytes of larger data types. The values from the selected channels represent the bytes of the actual property value, in little-endian order. + +> **Implementation note:** Specialized texture formats may allow additional channels, or channels with a higher number of bits per channel. The usage of such texture formats for property textures has to be defined by additional extensions. + +Certain property types cannot be encoded in property textures. For example, variable-length arrays or strings are not supported. Enum values may be encoded as integer values according to their enum value type (see [Enum](#enum)). For other property types, the number of channels that are selected must match the number of bytes of the property type. + +> **Example:** +> +> If a property is defined to store (single) `FLOAT32` components, then these values can be stored in the 4 channels of a property texture. The raw bits of the property value can be computed as +> ``` +> vec4 rgba = texture(sampler, coordinates); +> uint8 byte0 = rgba[channels[0]]; +> uint8 byte1 = rgba[channels[1]]; +> uint8 byte2 = rgba[channels[2]]; +> uint8 byte3 = rgba[channels[3]]; +> uint32 rawBits = byte0 | (byte1 << 8) | (byte2 << 16) | (byte3 << 24); +> float32 value = uintBitsToFloat(rawBits); +> ``` +> +> If a property has the type `VEC2` with `UIN16` components, or an array with a fixed length of 2 and `UINT16` components, then the respective property can be represented with 4 channels as well: +> ``` +> vec4 rgba = texture(sampler, coordinates); +> uint8 byte0 = rgba[channels[0]]; +> uint8 byte1 = rgba[channels[1]]; +> uint8 byte2 = rgba[channels[2]]; +> uint8 byte3 = rgba[channels[3]]; +> value[0] = byte0 | (byte1 << 8); +> value[1] = byte2 | (byte3 << 8); +> ``` + +### Binary Data Storage + +Property values are stored in a compact binary tabular format described in the [3D Metadata Specification](https://github.com/CesiumGS/3d-tiles/tree/main/specification/Metadata), with each property table array occupying a glTF buffer view. `EXT_structural_metadata` imposes 8-byte binary data alignment requirements on an asset, allowing support for 64-bit data types while remaining compatible with the 4-byte alignments in the core glTF specification: + +- GLB-stored `JSON` chunk must be padded with trailing `Space` characters (`0x20`) to 8-byte boundary. +- GLB-stored `BIN` chunk must be padded with trailing zeroes (`0x00`) to 8-byte boundary. + +As a result, byte length of the `BIN` chunk may be up to 7 bytes larger than JSON-defined `buffer.byteLength` to satisfy alignment requirements. + +## Assigning Metadata + +*Defined in [EXT_structural_metadata.schema.json](./schema/EXT_structural_metadata.schema.json).* + +When property values are stored in a [Property Table](#property-tables), then the entries of this table may be referenced from within the glTF asset: Each `node` of the glTF asset can contain an `EXT_structural_metadata` object that defines the source of the metadata values for this node. It contains the `propertyTable`, which is the index of the property table in the array of property tables of the root-level `EXT_structural_metadata` extension object, and the `index`, which is the index of the row in this table that contains the metadata values for the respective node. + +> **Example:** +> +> An example of metadata that is assigned to a node. It associates the given node with the metadata values that are stored in row 4 of the property table with index 1, referring to the array of property tables that are defined in the top-level `EXT_structural_metadata` extension object. +> +> ```jsonc +> { +> "extensions": { +> "EXT_structural_metadata": { +> "schema": { ... }, +> "propertyTables": [ ... ] +> } +> }, +> "nodes": [ +> ... +> { +> "name": "A node with metadata", +> "EXT_structural_metadata": { +> "propertyTable": 1, +> "index": 4 +> } +> } +> ] +> } +> ``` + +## Optional vs. Required + +This extension is optional, meaning it should be placed in the `extensionsUsed` list, but not in the `extensionsRequired` list. + +## Schema + +* [glTF.EXT_structural_metadata.schema.json](./schema/glTF.EXT_structural_metadata.schema.json) +* [mesh.primitive.EXT_structural_metadata.schema.json](./schema/mesh.primitive.EXT_structural_metadata.schema.json) + +## Revision History + +The revision history of this extension can be found in the [common revision history of the 3D Tiles Next extensions](https://github.com/CesiumGS/3d-tiles/blob/main/next/REVISION_HISTORY.md). diff --git a/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/figures/property-attribute.png b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/figures/property-attribute.png new file mode 100644 index 0000000000000000000000000000000000000000..47de4b0abb615d4b7c42613202fff49d29a3695e GIT binary patch literal 97434 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
IuQ!8KqVy~lD<>>^7NZ26^*Zj2)Z8o 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 DvG|fz literal 0 HcmV?d00001 diff --git a/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/figures/property-texture.svg b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/figures/property-texture.svg new file mode 100644 index 00000000..85f39e6d --- /dev/null +++ b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/figures/property-texture.svg @@ -0,0 +1,1987 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Property texture + Property texture channelsRed: Inside TemperatureGreen: Outside TemperatureBlue: Insulation Thickness + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Inside Temperature:Outside Temperature:Insulatation Thickness: + 22180.2 + + + + + diff --git a/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/EXT_structural_metadata.schema.json b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/EXT_structural_metadata.schema.json new file mode 100644 index 00000000..27726eeb --- /dev/null +++ b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/EXT_structural_metadata.schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "EXT_structural_metadata.schema.json", + "title": "EXT_structural_metadata glTF extension", + "type": "object", + "description": "Structural metadata about a glTF element.", + "allOf": [ + { + "$ref": "glTFProperty.schema.json" + } + ], + "properties": { + "propertyTable": { + "type": "integer", + "minimum": 0, + "description": "The index of the property table containing per-feature property values." + }, + "index": { + "type": "integer", + "minimum": 0, + "description": "The feature index (row index) used for looking up property values for this element." + }, + "extensions": {}, + "extras": {} + } +} \ No newline at end of file diff --git a/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/class.property.schema.json b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/class.property.schema.json new file mode 100644 index 00000000..aea17c8e --- /dev/null +++ b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/class.property.schema.json @@ -0,0 +1,182 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "class.property.schema.json", + "title": "Class Property in EXT_structural_metadata", + "type": "object", + "description": "A class property.", + "allOf": [ + { + "$ref": "glTFProperty.schema.json" + } + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "description": "The name of the property, e.g. for display purposes." + }, + "description": { + "type": "string", + "minLength": 1, + "description": "The description of the property." + }, + "type": { + "description": "The element type.", + "anyOf": [ + { + "const": "SCALAR" + }, + { + "const": "VEC2" + }, + { + "const": "VEC3" + }, + { + "const": "VEC4" + }, + { + "const": "MAT2" + }, + { + "const": "MAT3" + }, + { + "const": "MAT4" + }, + { + "const": "STRING" + }, + { + "const": "BOOLEAN" + }, + { + "const": "ENUM" + }, + { + "type": "string" + } + ] + }, + "componentType": { + "description": "The datatype of the element's components. Only applicable to `SCALAR`, `VECN`, and `MATN` types.", + "anyOf": [ + { + "const": "INT8" + }, + { + "const": "UINT8" + }, + { + "const": "INT16" + }, + { + "const": "UINT16" + }, + { + "const": "INT32" + }, + { + "const": "UINT32" + }, + { + "const": "INT64" + }, + { + "const": "UINT64" + }, + { + "const": "FLOAT32" + }, + { + "const": "FLOAT64" + }, + { + "type": "string" + } + ] + }, + "enumType": { + "type": "string", + "description": "Enum ID as declared in the `enums` dictionary. Required when `type` is `ENUM`." + }, + "array": { + "type": "boolean", + "default": false, + "description": "Whether the property is an array. When `count` is defined the property is a fixed-length array. Otherwise the property is a variable-length array." + }, + "count": { + "type": "integer", + "minimum": 2, + "description": "The number of array elements. May only be defined when `array` is true." + }, + "normalized": { + "type": "boolean", + "description": "Specifies whether integer values are normalized. Only applicable to `SCALAR`, `VECN`, and `MATN` types with integer component types. For unsigned integer component types, values are normalized between `[0.0, 1.0]`. For signed integer component types, values are normalized between `[-1.0, 1.0]`. For all other component types, this property must be false.", + "default": false + }, + "offset": { + "allOf": [ + { + "$ref": "definitions.schema.json#/definitions/numericValue" + } + ], + "description": "An offset to apply to property values. Only applicable to `SCALAR`, `VECN`, and `MATN` types when the component type is `FLOAT32` or `FLOAT64`, or when the property is `normalized`." + }, + "scale": { + "allOf": [ + { + "$ref": "definitions.schema.json#/definitions/numericValue" + } + ], + "description": "A scale to apply to property values. Only applicable to `SCALAR`, `VECN`, and `MATN` types when the component type is `FLOAT32` or `FLOAT64`, or when the property is `normalized`." + }, + "max": { + "allOf": [ + { + "$ref": "definitions.schema.json#/definitions/numericValue" + } + ], + "description": "Maximum allowed value for the property. Only applicable to `SCALAR`, `VECN`, and `MATN` types. This is the maximum of all property values, after the transforms based on the `normalized`, `offset`, and `scale` properties have been applied." + }, + "min": { + "allOf": [ + { + "$ref": "definitions.schema.json#/definitions/numericValue" + } + ], + "description": "Minimum allowed value for the property. Only applicable to `SCALAR`, `VECN`, and `MATN` types. This is the minimum of all property values, after the transforms based on the `normalized`, `offset`, and `scale` properties have been applied." + }, + "required": { + "type": "boolean", + "description": "If required, the property must be present in every entity conforming to the class. If not required, individual entities may include `noData` values, or the entire property may be omitted. As a result, `noData` has no effect on a required property. Client implementations may use required properties to make performance optimizations.", + "default": false + }, + "noData": { + "allOf": [ + { + "$ref": "definitions.schema.json#/definitions/noDataValue" + } + ], + "description": "A `noData` value represents missing data — also known as a sentinel value — wherever it appears. `BOOLEAN` properties may not specify `noData` values. This is given as the plain property value, without the transforms from the `normalized`, `offset`, and `scale` properties. Must not be defined if `required` is true." + }, + "default": { + "allOf": [ + { + "$ref": "definitions.schema.json#/definitions/anyValue" + } + ], + "description": "A default value to use when encountering a `noData` value or an omitted property. The value is given in its final form, taking the effect of `normalized`, `offset`, and `scale` properties into account. Must not be defined if `required` is true." + }, + "semantic": { + "type": "string", + "minLength": 1, + "description": "An identifier that describes how this property should be interpreted. The semantic cannot be used by other properties in the class." + }, + "extensions": {}, + "extras": {} + }, + "required": [ + "type" + ] +} \ No newline at end of file diff --git a/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/class.schema.json b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/class.schema.json new file mode 100644 index 00000000..a7c98cdc --- /dev/null +++ b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/class.schema.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "class.schema.json", + "title": "Class in EXT_structural_metadata", + "type": "object", + "description": "A class containing a set of properties.", + "allOf": [ + { + "$ref": "glTFProperty.schema.json" + } + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "description": "The name of the class, e.g. for display purposes." + }, + "description": { + "type": "string", + "minLength": 1, + "description": "The description of the class." + }, + "properties": { + "type": "object", + "description": "A dictionary, where each key is a property ID and each value is an object defining the property. Property IDs must be alphanumeric identifiers matching the regular expression `^[a-zA-Z_][a-zA-Z0-9_]*$`.", + "minProperties": 1, + "additionalProperties": { + "$ref": "class.property.schema.json" + } + }, + "extensions": {}, + "extras": {} + } +} \ No newline at end of file diff --git a/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/definitions.schema.json b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/definitions.schema.json new file mode 100644 index 00000000..63a74b8f --- /dev/null +++ b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/definitions.schema.json @@ -0,0 +1,119 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "definitions.schema.json", + "title": "Definitions", + "description": "Common definitions used in schema files.", + "definitions": { + "numericValue": { + "title": "Numeric Value", + "oneOf": [ + { + "type": "number" + }, + { + "type": "array", + "items": { + "type": "number" + }, + "minItems": 1 + }, + { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "number" + }, + "minItems": 1 + }, + "minItems": 1 + } + ], + "description": "For `SCALAR` this is a number. For `VECN` this is an array of `N` numbers. For `MATN` this is an array of `N²` numbers. For fixed-length arrays this is an array of `count` elements of the given `type`." + }, + "noDataValue": { + "title": "No Data Value", + "oneOf": [ + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "number" + }, + "minItems": 1 + }, + { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + }, + { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "number" + }, + "minItems": 1 + }, + "minItems": 1 + } + ], + "description": "For `SCALAR` this is a number. For `STRING` this is a string. For `ENUM` this is a string that must be a valid enum `name`, not an integer value. For `VECN` this is an array of `N` numbers. For `MATN` this is an array of `N²` numbers. For fixed-length arrays this is an array of `count` elements of the given `type`." + }, + "anyValue": { + "title": "Any Value", + "oneOf": [ + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "type": "number" + }, + "minItems": 1 + }, + { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + }, + { + "type": "array", + "items": { + "type": "boolean" + }, + "minItems": 1 + }, + { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "number" + }, + "minItems": 1 + }, + "minItems": 1 + } + ], + "description": "For `SCALAR` this is a number. For `STRING` this is a string. For `ENUM` this is a string that must be a valid enum `name`, not an integer value. For `BOOLEAN` this is a boolean. For `VECN` this is an array of `N` numbers. For `MATN` this is an array of `N²` numbers. For fixed-length array this is an array of `count` elements of the given `type`. For variable-length arrays this is an array of any length of the given `type`." + } + } +} \ No newline at end of file diff --git a/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/enum.schema.json b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/enum.schema.json new file mode 100644 index 00000000..5f78f75d --- /dev/null +++ b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/enum.schema.json @@ -0,0 +1,70 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "enum.schema.json", + "title": "Enum in EXT_structural_metadata", + "type": "object", + "description": "An object defining the values of an enum.", + "allOf": [ + { + "$ref": "glTFProperty.schema.json" + } + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "description": "The name of the enum, e.g. for display purposes." + }, + "description": { + "type": "string", + "minLength": 1, + "description": "The description of the enum." + }, + "valueType": { + "default": "UINT16", + "description": "The type of the integer enum value.", + "anyOf": [ + { + "const": "INT8" + }, + { + "const": "UINT8" + }, + { + "const": "INT16" + }, + { + "const": "UINT16" + }, + { + "const": "INT32" + }, + { + "const": "UINT32" + }, + { + "const": "INT64" + }, + { + "const": "UINT64" + }, + { + "type": "string" + } + ] + }, + "values": { + "type": "array", + "description": "An array of enum values. Duplicate names or duplicate integer values are not allowed.", + "items": { + "$ref": "enum.value.schema.json" + }, + "minItems": 1 + }, + "extensions": {}, + "extras": {} + }, + "required": [ + "values" + ] +} \ No newline at end of file diff --git a/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/enum.value.schema.json b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/enum.value.schema.json new file mode 100644 index 00000000..b84c5f2b --- /dev/null +++ b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/enum.value.schema.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "enum.value.schema.json", + "title": "Enum Value in EXT_structural_metadata", + "type": "object", + "description": "An enum value.", + "allOf": [ + { + "$ref": "glTFProperty.schema.json" + } + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "description": "The name of the enum value." + }, + "description": { + "type": "string", + "minLength": 1, + "description": "The description of the enum value." + }, + "value": { + "type": "integer", + "description": "The integer enum value." + }, + "extensions": {}, + "extras": {} + }, + "required": [ + "name", + "value" + ] +} \ No newline at end of file diff --git a/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/glTF.EXT_structural_metadata.schema.json b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/glTF.EXT_structural_metadata.schema.json new file mode 100644 index 00000000..2a90187c --- /dev/null +++ b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/glTF.EXT_structural_metadata.schema.json @@ -0,0 +1,71 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "glTF.EXT_structural_metadata.schema.json", + "title": "EXT_structural_metadata glTF extension", + "type": "object", + "description": "glTF extension that provides structural metadata about vertices, texels, and features in a glTF asset.", + "allOf": [ + { + "$ref": "glTFProperty.schema.json" + } + ], + "properties": { + "schema": { + "allOf": [ + { + "$ref": "schema.schema.json" + } + ], + "description": "An object defining classes and enums." + }, + "schemaUri": { + "type": "string", + "description": "The URI (or IRI) of the external schema file.", + "format": "iri-reference" + }, + "propertyTables": { + "type": "array", + "description": "An array of property table definitions, which may be referenced by index.", + "minItems": 1, + "items": { + "$ref": "propertyTable.schema.json" + } + }, + "propertyTextures": { + "type": "array", + "description": "An array of property texture definitions, which may be referenced by index.", + "minItems": 1, + "items": { + "$ref": "propertyTexture.schema.json" + } + }, + "propertyAttributes": { + "type": "array", + "description": "An array of property attribute definitions, which may be referenced by index.", + "minItems": 1, + "items": { + "$ref": "propertyAttribute.schema.json" + } + }, + "extensions": {}, + "extras": {} + }, + "oneOf": [ + { + "description": "External schema, if any.", + "not": { + "required": [ + "schema" + ] + } + }, + { + "description": "Internal schema, if any.", + "not": { + "required": [ + "schemaUri" + ] + } + } + ] +} \ No newline at end of file diff --git a/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/mesh.primitive.EXT_structural_metadata.schema.json b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/mesh.primitive.EXT_structural_metadata.schema.json new file mode 100644 index 00000000..9c7ca53b --- /dev/null +++ b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/mesh.primitive.EXT_structural_metadata.schema.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "mesh.primitive.EXT_structural_metadata.schema.json", + "title": "EXT_structural_metadata glTF Mesh Primitive extension", + "type": "object", + "description": "Structural metadata about a glTF primitive.", + "allOf": [ + { + "$ref": "glTFProperty.schema.json" + } + ], + "properties": { + "propertyTextures": { + "type": "array", + "description": "An array of indexes of property textures in the root `EXT_structural_metadata` object.", + "minItems": 1, + "items": { + "type": "integer" + } + }, + "propertyAttributes": { + "type": "array", + "description": "An array of indexes of property attributes in the root `EXT_structural_metadata` object.", + "minItems": 1, + "items": { + "type": "integer" + } + }, + "extensions": {}, + "extras": {} + } +} \ No newline at end of file diff --git a/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/propertyAttribute.property.schema.json b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/propertyAttribute.property.schema.json new file mode 100644 index 00000000..418963f3 --- /dev/null +++ b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/propertyAttribute.property.schema.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "propertyAttribute.property.schema.json", + "title": "Property Attribute Property in EXT_structural_metadata", + "type": "object", + "description": "An attribute containing property values.", + "allOf": [ + { + "$ref": "glTFProperty.schema.json" + } + ], + "properties": { + "attribute": { + "type": "string", + "description": "The name of the attribute containing property values." + }, + "offset": { + "allOf": [ + { + "$ref": "definitions.schema.json#/definitions/numericValue" + } + ], + "description": "An offset to apply to property values. Only applicable when the component type is `FLOAT32` or `FLOAT64`, or when the property is `normalized`. Overrides the class property's `offset` if both are defined." + }, + "scale": { + "allOf": [ + { + "$ref": "definitions.schema.json#/definitions/numericValue" + } + ], + "description": "A scale to apply to property values. Only applicable when the component type is `FLOAT32` or `FLOAT64`, or when the property is `normalized`. Overrides the class property's `scale` if both are defined." + }, + "max": { + "allOf": [ + { + "$ref": "definitions.schema.json#/definitions/numericValue" + } + ], + "description": "Maximum value present in the property values. Only applicable to `SCALAR`, `VECN`, and `MATN` types. This is the maximum of all property values, after the transforms based on the `normalized`, `offset`, and `scale` properties have been applied." + }, + "min": { + "allOf": [ + { + "$ref": "definitions.schema.json#/definitions/numericValue" + } + ], + "description": "Minimum value present in the property values. Only applicable to `SCALAR`, `VECN`, and `MATN` types. This is the minimum of all property values, after the transforms based on the `normalized`, `offset`, and `scale` properties have been applied." + }, + "extensions": {}, + "extras": {} + }, + "required": [ + "attribute" + ] +} \ No newline at end of file diff --git a/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/propertyAttribute.schema.json b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/propertyAttribute.schema.json new file mode 100644 index 00000000..2ce472d8 --- /dev/null +++ b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/propertyAttribute.schema.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "propertyAttribute.schema.json", + "title": "Property Attribute in EXT_structural_metadata", + "type": "object", + "description": "Properties conforming to a class, organized as property values stored in attributes.", + "allOf": [ + { + "$ref": "glTFProperty.schema.json" + } + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "description": "The name of the property attribute, e.g. for display purposes." + }, + "class": { + "type": "string", + "description": "The class that property values conform to. The value must be a class ID declared in the `classes` dictionary." + }, + "properties": { + "type": "object", + "description": "A dictionary, where each key corresponds to a property ID in the class' `properties` dictionary and each value is an object describing where property values are stored. Required properties must be included in this dictionary.", + "minProperties": 1, + "additionalProperties": { + "$ref": "propertyAttribute.property.schema.json" + } + }, + "extensions": {}, + "extras": {} + }, + "required": [ + "class" + ] +} \ No newline at end of file diff --git a/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/propertyTable.property.schema.json b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/propertyTable.property.schema.json new file mode 100644 index 00000000..ad13da27 --- /dev/null +++ b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/propertyTable.property.schema.json @@ -0,0 +1,117 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "propertyTable.property.schema.json", + "title": "Property Table Property in EXT_structural_metadata", + "type": "object", + "description": "An array of binary property values.", + "allOf": [ + { + "$ref": "glTFProperty.schema.json" + } + ], + "properties": { + "values": { + "allOf": [ + { + "$ref": "glTFid.schema.json" + } + ], + "description": "The index of the buffer view containing property values. The data type of property values is determined by the property definition: When `type` is `BOOLEAN` values are packed into a bitstream. When `type` is `STRING` values are stored as byte sequences and decoded as UTF-8 strings. When `type` is `SCALAR`, `VECN`, or `MATN` the values are stored as the provided `componentType` and the buffer view `byteOffset` must be aligned to a multiple of the `componentType` size. When `type` is `ENUM` values are stored as the enum's `valueType` and the buffer view `byteOffset` must be aligned to a multiple of the `valueType` size. Each enum value in the array must match one of the allowed values in the enum definition. `arrayOffsets` is required for variable-length arrays and `stringOffsets` is required for strings (for variable-length arrays of strings, both are required)." + }, + "arrayOffsets": { + "allOf": [ + { + "$ref": "glTFid.schema.json" + } + ], + "description": "The index of the buffer view containing offsets for variable-length arrays. The number of offsets is equal to the property table `count` plus one. The offsets represent the start positions of each array, with the last offset representing the position after the last array. The array length is computed using the difference between the subsequent offset and the current offset. If `type` is `STRING` the offsets index into the string offsets array (stored in `stringOffsets`), otherwise they index into the property array (stored in `values`). The data type of these offsets is determined by `arrayOffsetType`. The buffer view `byteOffset` must be aligned to a multiple of the `arrayOffsetType` size." + }, + "stringOffsets": { + "allOf": [ + { + "$ref": "glTFid.schema.json" + } + ], + "description": "The index of the buffer view containing offsets for strings. The number of offsets is equal to the number of string elements plus one. The offsets represent the byte offsets of each string in the property array (stored in `values`), with the last offset representing the byte offset after the last string. The string byte length is computed using the difference between the subsequent offset and the current offset. The data type of these offsets is determined by `stringOffsetType`. The buffer view `byteOffset` must be aligned to a multiple of the `stringOffsetType` size." + }, + "arrayOffsetType": { + "description": "The type of values in `arrayOffsets`.", + "default": "UINT32", + "anyOf": [ + { + "const": "UINT8" + }, + { + "const": "UINT16" + }, + { + "const": "UINT32" + }, + { + "const": "UINT64" + }, + { + "type": "string" + } + ] + }, + "stringOffsetType": { + "description": "The type of values in `stringOffsets`.", + "default": "UINT32", + "anyOf": [ + { + "const": "UINT8" + }, + { + "const": "UINT16" + }, + { + "const": "UINT32" + }, + { + "const": "UINT64" + }, + { + "type": "string" + } + ] + }, + "offset": { + "allOf": [ + { + "$ref": "definitions.schema.json#/definitions/numericValue" + } + ], + "description": "An offset to apply to property values. Only applicable when the component type is `FLOAT32` or `FLOAT64`, or when the property is `normalized`. Overrides the class property's `offset` if both are defined." + }, + "scale": { + "allOf": [ + { + "$ref": "definitions.schema.json#/definitions/numericValue" + } + ], + "description": "A scale to apply to property values. Only applicable when the component type is `FLOAT32` or `FLOAT64`, or when the property is `normalized`. Overrides the class property's `scale` if both are defined." + }, + "max": { + "allOf": [ + { + "$ref": "definitions.schema.json#/definitions/numericValue" + } + ], + "description": "Maximum value present in the property values. Only applicable to `SCALAR`, `VECN`, and `MATN` types. This is the maximum of all property values, after the transforms based on the `normalized`, `offset`, and `scale` properties have been applied." + }, + "min": { + "allOf": [ + { + "$ref": "definitions.schema.json#/definitions/numericValue" + } + ], + "description": "Minimum value present in the property values. Only applicable to `SCALAR`, `VECN`, and `MATN` types. This is the minimum of all property values, after the transforms based on the `normalized`, `offset`, and `scale` properties have been applied." + }, + "extensions": {}, + "extras": {} + }, + "required": [ + "values" + ] +} \ No newline at end of file diff --git a/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/propertyTable.schema.json b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/propertyTable.schema.json new file mode 100644 index 00000000..e4a8e0d2 --- /dev/null +++ b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/propertyTable.schema.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "propertyTable.schema.json", + "title": "Property Table in EXT_structural_metadata", + "type": "object", + "description": "Properties conforming to a class, organized as property values stored in binary columnar arrays.", + "allOf": [ + { + "$ref": "glTFProperty.schema.json" + } + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "description": "The name of the property table, e.g. for display purposes." + }, + "class": { + "type": "string", + "description": "The class that property values conform to. The value must be a class ID declared in the `classes` dictionary." + }, + "count": { + "type": "integer", + "minimum": 1, + "description": "The number of elements in each property array." + }, + "properties": { + "type": "object", + "description": "A dictionary, where each key corresponds to a property ID in the class' `properties` dictionary and each value is an object describing where property values are stored. Required properties must be included in this dictionary.", + "minProperties": 1, + "additionalProperties": { + "$ref": "propertyTable.property.schema.json" + } + }, + "extensions": {}, + "extras": {} + }, + "required": [ + "class", + "count" + ] +} \ No newline at end of file diff --git a/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/propertyTexture.property.schema.json b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/propertyTexture.property.schema.json new file mode 100644 index 00000000..45af22d2 --- /dev/null +++ b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/propertyTexture.property.schema.json @@ -0,0 +1,62 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "propertyTexture.schema.json", + "title": "Property Texture Property in EXT_structural_metadata", + "type": "object", + "description": "A texture containing property values.", + "allOf": [ + { + "$ref": "textureInfo.schema.json" + } + ], + "properties": { + "channels": { + "type": "array", + "items": { + "type": "integer", + "minimum": 0 + }, + "minItems": 1, + "description": "Texture channels containing property values, identified by index. The values may be packed into multiple channels if a single channel does not have sufficient bit depth. The values are packed in little-endian order.", + "default": [ + 0 + ] + }, + "offset": { + "allOf": [ + { + "$ref": "definitions.schema.json#/definitions/numericValue" + } + ], + "description": "An offset to apply to property values. Only applicable when the component type is `FLOAT32` or `FLOAT64`, or when the property is `normalized`. Overrides the class property's `offset` if both are defined." + }, + "scale": { + "allOf": [ + { + "$ref": "definitions.schema.json#/definitions/numericValue" + } + ], + "description": "A scale to apply to property values. Only applicable when the component type is `FLOAT32` or `FLOAT64`, or when the property is `normalized`. Overrides the class property's `scale` if both are defined." + }, + "max": { + "allOf": [ + { + "$ref": "definitions.schema.json#/definitions/numericValue" + } + ], + "description": "Maximum value present in the property values. Only applicable to `SCALAR`, `VECN`, and `MATN` types. This is the maximum of all property values, after the transforms based on the `normalized`, `offset`, and `scale` properties have been applied." + }, + "min": { + "allOf": [ + { + "$ref": "definitions.schema.json#/definitions/numericValue" + } + ], + "description": "Minimum value present in the property values. Only applicable to `SCALAR`, `VECN`, and `MATN` types. This is the minimum of all property values, after the transforms based on the `normalized`, `offset`, and `scale` properties have been applied." + }, + "index": {}, + "texCoord": {}, + "extensions": {}, + "extras": {} + } +} \ No newline at end of file diff --git a/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/propertyTexture.schema.json b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/propertyTexture.schema.json new file mode 100644 index 00000000..21e260d9 --- /dev/null +++ b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/propertyTexture.schema.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "propertyTexture.schema.json", + "title": "Property Texture in EXT_structural_metadata", + "type": "object", + "description": "Properties conforming to a class, organized as property values stored in textures.", + "allOf": [ + { + "$ref": "glTFProperty.schema.json" + } + ], + "properties": { + "name": { + "type": "string", + "minLength": 1, + "description": "The name of the property texture, e.g. for display purposes." + }, + "class": { + "type": "string", + "description": "The class that property values conform to. The value must be a class ID declared in the `classes` dictionary." + }, + "properties": { + "type": "object", + "description": "A dictionary, where each key corresponds to a property ID in the class' `properties` dictionary and each value is an object describing where property values are stored. Required properties must be included in this dictionary.", + "minProperties": 1, + "additionalProperties": { + "$ref": "propertyTexture.property.schema.json" + } + }, + "extensions": {}, + "extras": {} + }, + "required": [ + "class" + ] +} \ No newline at end of file diff --git a/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/schema.schema.json b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/schema.schema.json new file mode 100644 index 00000000..6c615803 --- /dev/null +++ b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/schema.schema.json @@ -0,0 +1,55 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "schema.schema.json", + "title": "Schema in EXT_structural_metadata", + "type": "object", + "description": "An object defining classes and enums.", + "allOf": [ + { + "$ref": "glTFProperty.schema.json" + } + ], + "properties": { + "id": { + "type": "string", + "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$", + "description": "Unique identifier for the schema. Schema IDs must be alphanumeric identifiers matching the regular expression `^[a-zA-Z_][a-zA-Z0-9_]*$`." + }, + "name": { + "type": "string", + "minLength": 1, + "description": "The name of the schema, e.g. for display purposes." + }, + "description": { + "type": "string", + "minLength": 1, + "description": "The description of the schema." + }, + "version": { + "type": "string", + "minLength": 1, + "description": "Application-specific version of the schema." + }, + "classes": { + "type": "object", + "description": "A dictionary, where each key is a class ID and each value is an object defining the class. Class IDs must be alphanumeric identifiers matching the regular expression `^[a-zA-Z_][a-zA-Z0-9_]*$`.", + "minProperties": 1, + "additionalProperties": { + "$ref": "class.schema.json" + } + }, + "enums": { + "type": "object", + "description": "A dictionary, where each key is an enum ID and each value is an object defining the values for the enum. Enum IDs must be alphanumeric identifiers matching the regular expression `^[a-zA-Z_][a-zA-Z0-9_]*$`.", + "minProperties": 1, + "additionalProperties": { + "$ref": "enum.schema.json" + } + }, + "extensions": {}, + "extras": {} + }, + "required": [ + "id" + ] +} \ No newline at end of file diff --git a/build/SharpGLTF.CodeGen/SharpGLTF.CodeGen.csproj b/build/SharpGLTF.CodeGen/SharpGLTF.CodeGen.csproj index a75da28e..1a70d02b 100644 --- a/build/SharpGLTF.CodeGen/SharpGLTF.CodeGen.csproj +++ b/build/SharpGLTF.CodeGen/SharpGLTF.CodeGen.csproj @@ -28,6 +28,51 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + diff --git a/src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata.g.cs b/src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata.g.cs new file mode 100644 index 00000000..0b50a437 --- /dev/null +++ b/src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata.g.cs @@ -0,0 +1,66 @@ +// + +//------------------------------------------------------------------------------------------------ +// 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; + + /// + /// Structural metadata about a glTF element. + /// + #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 EXT_structural_metadataglTFextension : ExtraProperties + { + + private const Int32 _indexMinimum = 0; + private Int32? _index; + + private const Int32 _propertyTableMinimum = 0; + private Int32? _propertyTable; + + + protected override void SerializeProperties(Utf8JsonWriter writer) + { + base.SerializeProperties(writer); + SerializeProperty(writer, "index", _index); + SerializeProperty(writer, "propertyTable", _propertyTable); + } + + protected override void DeserializeProperty(string jsonPropertyName, ref Utf8JsonReader reader) + { + switch (jsonPropertyName) + { + case "index": _index = DeserializePropertyValue(ref reader); break; + case "propertyTable": _propertyTable = DeserializePropertyValue(ref reader); break; + default: base.DeserializeProperty(jsonPropertyName,ref reader); break; + } + } + + } + +} From 8611594a988e6c7fb79bd12f64f613999d444039 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Wed, 29 Nov 2023 23:46:58 +0100 Subject: [PATCH 02/57] got ext_structural_ematdata code generated --- .../Ext.EXT_Structural_Metadata.cs | 45 +- build/SharpGLTF.CodeGen/Program.cs | 2 +- build/SharpGLTF.CodeGen/SchemaProcessing.cs | 1 + .../glTF.EXT_structural_metadata.schema.json | 20 +- ...UM_ext_structural_metadata_primitive.g.cs} | 20 +- ...t.CESIUM_ext_structural_metadata_root.g.cs | 683 ++++++++++++++++++ 6 files changed, 736 insertions(+), 35 deletions(-) rename src/SharpGLTF.Cesium/Schema2/Generated/{Ext.CESIUM_ext_structural_metadata.g.cs => Ext.CESIUM_ext_structural_metadata_primitive.g.cs} (68%) create mode 100644 src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata_root.g.cs diff --git a/build/SharpGLTF.CodeGen/Ext.EXT_Structural_Metadata.cs b/build/SharpGLTF.CodeGen/Ext.EXT_Structural_Metadata.cs index ecff1d81..32d6180e 100644 --- a/build/SharpGLTF.CodeGen/Ext.EXT_Structural_Metadata.cs +++ b/build/SharpGLTF.CodeGen/Ext.EXT_Structural_Metadata.cs @@ -8,18 +8,35 @@ class ExtStructuralMetadataExtension : SchemaProcessor { public override string GetTargetProject() { return Constants.CesiumProjectDirectory; } - private static string RootSchemaUri => Constants.CustomExtensionsPath("EXT_structural_metadata", "EXT_structural_metadata.schema.json"); + const string ExtensionPropertyTexturePropertyName = "Property Texture Property in EXT_structural_metadata"; + + private static string RootSchemaUri => Constants.CustomExtensionsPath("EXT_structural_metadata", "glTF.EXT_structural_metadata.schema.json"); + private static string MeshPrimitiveSchemaUri => Constants.CustomExtensionsPath("EXT_structural_metadata", "mesh.primitive.EXT_structural_metadata.schema.json"); 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"); + newEmitter.SetRuntimeName("EXT_structural_metadata glTF extension", "EXTStructuralMetaData"); + newEmitter.SetRuntimeName("Property Table in EXT_structural_metadata", "PropertyTable"); + newEmitter.SetRuntimeName("Schema in EXT_structural_metadata", "StructuralMetadataSchema"); + newEmitter.SetRuntimeName("Property Table Property in EXT_structural_metadata", "PropertyTableProperty"); + newEmitter.SetRuntimeName("Property Texture in EXT_structural_metadata", "PropertyTexture"); + newEmitter.SetRuntimeName("Property Texture Property in EXT_structural_metadata", "PropertyTextureProperty"); + newEmitter.SetRuntimeName("Property Attribute Property in EXT_structural_metadata", "PropertyAttributeProperty"); + newEmitter.SetRuntimeName("Class Property in EXT_structural_metadata", "ClassProperty"); + newEmitter.SetRuntimeName("Class in EXT_structural_metadata", "StructuralMetadataClass"); + newEmitter.SetRuntimeName("Enum Value in EXT_structural_metadata", "EnumValue"); + newEmitter.SetRuntimeName("Enum in EXT_structural_metadata", "StructuralMetadataEnum"); + + newEmitter.SetRuntimeName("BOOLEAN-ENUM-MAT2-MAT3-MAT4-SCALAR-STRING-VEC2-VEC3-VEC4", "ElementType"); + newEmitter.SetRuntimeName("FLOAT32-FLOAT64-INT16-INT32-INT64-INT8-UINT16-UINT32-UINT64-UINT8", "DataType"); + newEmitter.SetRuntimeName("INT16-INT32-INT64-INT8-UINT16-UINT32-UINT64-UINT8", "IntegerType"); + newEmitter.SetRuntimeName("UINT16-UINT32-UINT64-UINT8", "StringOffsets"); } public override IEnumerable<(string TargetFileName, SchemaType.Context Schema)> Process() { - yield return ("Ext.CESIUM_ext_structural_metadata.g", ProcessRoot()); + yield return ("Ext.CESIUM_ext_structural_metadata_root.g", ProcessRoot()); + yield return ("Ext.CESIUM_ext_structural_metadata_primitive.g", ProcessMeshPrimitive()); } private static SchemaType.Context ProcessRoot() @@ -28,7 +45,25 @@ private static SchemaType.Context ProcessRoot() ctx.IgnoredByCodeEmitter("glTF Property"); ctx.IgnoredByCodeEmitter("glTF Child of Root Property"); ctx.IgnoredByCodeEmitter("Texture Info"); + var fld = ctx.FindClass(ExtensionPropertyTexturePropertyName).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; + } + + private static SchemaType.Context ProcessMeshPrimitive() + { + var ctx = SchemaProcessing.LoadSchemaContext(MeshPrimitiveSchemaUri); + ctx.IgnoredByCodeEmitter("glTF Property"); + ctx.IgnoredByCodeEmitter("glTF Child of Root Property"); + ctx.IgnoredByCodeEmitter("Texture Info"); return ctx; } + + } } diff --git a/build/SharpGLTF.CodeGen/Program.cs b/build/SharpGLTF.CodeGen/Program.cs index d3099ca8..aab2274a 100644 --- a/build/SharpGLTF.CodeGen/Program.cs +++ b/build/SharpGLTF.CodeGen/Program.cs @@ -15,7 +15,7 @@ partial class Program static void Main(string[] args) { - // SchemaDownload.Syncronize(Constants.RemoteSchemaRepo, Constants.LocalRepoDirectory); + SchemaDownload.Syncronize(Constants.RemoteSchemaRepo, Constants.LocalRepoDirectory); var processors = new List(); diff --git a/build/SharpGLTF.CodeGen/SchemaProcessing.cs b/build/SharpGLTF.CodeGen/SchemaProcessing.cs index 56f95de8..26df6440 100644 --- a/build/SharpGLTF.CodeGen/SchemaProcessing.cs +++ b/build/SharpGLTF.CodeGen/SchemaProcessing.cs @@ -24,6 +24,7 @@ public static SchemaType.Context LoadSchemaContext(string srcSchema) var settings = new NJsonSchema.CodeGeneration.CSharp.CSharpGeneratorSettings { + Namespace = "glTf.POCO", ClassStyle = NJsonSchema.CodeGeneration.CSharp.CSharpClassStyle.Poco }; diff --git a/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/glTF.EXT_structural_metadata.schema.json b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/glTF.EXT_structural_metadata.schema.json index 2a90187c..c849417a 100644 --- a/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/glTF.EXT_structural_metadata.schema.json +++ b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/schema/glTF.EXT_structural_metadata.schema.json @@ -49,23 +49,5 @@ }, "extensions": {}, "extras": {} - }, - "oneOf": [ - { - "description": "External schema, if any.", - "not": { - "required": [ - "schema" - ] - } - }, - { - "description": "Internal schema, if any.", - "not": { - "required": [ - "schemaUri" - ] - } - } - ] + } } \ No newline at end of file diff --git a/src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata.g.cs b/src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata_primitive.g.cs similarity index 68% rename from src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata.g.cs rename to src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata_primitive.g.cs index 0b50a437..5f802291 100644 --- a/src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata.g.cs +++ b/src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata_primitive.g.cs @@ -28,35 +28,35 @@ namespace SharpGLTF.Schema2 using Collections; /// - /// Structural metadata about a glTF element. + /// Structural metadata about a glTF 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 EXT_structural_metadataglTFextension : ExtraProperties + partial class EXT_structural_metadataglTFMeshPrimitiveextension : ExtraProperties { - private const Int32 _indexMinimum = 0; - private Int32? _index; + private const int _propertyAttributesMinItems = 1; + private List _propertyAttributes; - private const Int32 _propertyTableMinimum = 0; - private Int32? _propertyTable; + private const int _propertyTexturesMinItems = 1; + private List _propertyTextures; protected override void SerializeProperties(Utf8JsonWriter writer) { base.SerializeProperties(writer); - SerializeProperty(writer, "index", _index); - SerializeProperty(writer, "propertyTable", _propertyTable); + SerializeProperty(writer, "propertyAttributes", _propertyAttributes, _propertyAttributesMinItems); + SerializeProperty(writer, "propertyTextures", _propertyTextures, _propertyTexturesMinItems); } protected override void DeserializeProperty(string jsonPropertyName, ref Utf8JsonReader reader) { switch (jsonPropertyName) { - case "index": _index = DeserializePropertyValue(ref reader); break; - case "propertyTable": _propertyTable = DeserializePropertyValue(ref reader); break; + case "propertyAttributes": DeserializePropertyList(ref reader, _propertyAttributes); break; + case "propertyTextures": DeserializePropertyList(ref reader, _propertyTextures); break; default: base.DeserializeProperty(jsonPropertyName,ref reader); break; } } diff --git a/src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata_root.g.cs b/src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata_root.g.cs new file mode 100644 index 00000000..42ad8440 --- /dev/null +++ b/src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata_root.g.cs @@ -0,0 +1,683 @@ +// + +//------------------------------------------------------------------------------------------------ +// 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; + + /// + /// The element type. + /// + public enum ElementType + { + SCALAR, + VEC2, + VEC3, + VEC4, + MAT2, + MAT3, + MAT4, + STRING, + BOOLEAN, + ENUM, + } + + + /// + /// The datatype of the element's components. Only applicable to `SCALAR`, `VECN`, and `MATN` types. + /// + public enum DataType + { + INT8, + UINT8, + INT16, + UINT16, + INT32, + UINT32, + INT64, + UINT64, + FLOAT32, + FLOAT64, + } + + + /// + /// The type of the integer enum value. + /// + public enum IntegerType + { + INT8, + UINT8, + INT16, + UINT16, + INT32, + UINT32, + INT64, + UINT64, + } + + + /// + /// The type of values in `stringOffsets`. + /// + public enum StringOffsets + { + UINT8, + UINT16, + UINT32, + UINT64, + } + + + /// + /// A class property. + /// + #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 ClassProperty : ExtraProperties + { + + private static readonly Boolean _arrayDefault = false; + private Boolean? _array = _arrayDefault; + + private DataType? _componentType; + + private const Int32 _countMinimum = 2; + private Int32? _count; + + private System.Text.Json.Nodes.JsonNode _default; + + private String _description; + + private String _enumType; + + private System.Text.Json.Nodes.JsonNode _max; + + private System.Text.Json.Nodes.JsonNode _min; + + private String _name; + + private System.Text.Json.Nodes.JsonNode _noData; + + private static readonly Boolean _normalizedDefault = false; + private Boolean? _normalized = _normalizedDefault; + + private System.Text.Json.Nodes.JsonNode _offset; + + private static readonly Boolean _requiredDefault = false; + private Boolean? _required = _requiredDefault; + + private System.Text.Json.Nodes.JsonNode _scale; + + private String _semantic; + + private ElementType _type; + + + protected override void SerializeProperties(Utf8JsonWriter writer) + { + base.SerializeProperties(writer); + SerializeProperty(writer, "array", _array, _arrayDefault); + SerializePropertyEnumSymbol(writer, "componentType", _componentType); + SerializeProperty(writer, "count", _count); + SerializeProperty(writer, "default", _default); + SerializeProperty(writer, "description", _description); + SerializeProperty(writer, "enumType", _enumType); + SerializeProperty(writer, "max", _max); + SerializeProperty(writer, "min", _min); + SerializeProperty(writer, "name", _name); + SerializeProperty(writer, "noData", _noData); + SerializeProperty(writer, "normalized", _normalized, _normalizedDefault); + SerializeProperty(writer, "offset", _offset); + SerializeProperty(writer, "required", _required, _requiredDefault); + SerializeProperty(writer, "scale", _scale); + SerializeProperty(writer, "semantic", _semantic); + SerializePropertyEnumSymbol(writer, "type", _type); + } + + protected override void DeserializeProperty(string jsonPropertyName, ref Utf8JsonReader reader) + { + switch (jsonPropertyName) + { + case "array": _array = DeserializePropertyValue(ref reader); break; + case "componentType": _componentType = DeserializePropertyValue(ref reader); break; + case "count": _count = DeserializePropertyValue(ref reader); break; + case "default": _default = DeserializePropertyValue(ref reader); break; + case "description": _description = DeserializePropertyValue(ref reader); break; + case "enumType": _enumType = DeserializePropertyValue(ref reader); break; + case "max": _max = DeserializePropertyValue(ref reader); break; + case "min": _min = DeserializePropertyValue(ref reader); break; + case "name": _name = DeserializePropertyValue(ref reader); break; + case "noData": _noData = DeserializePropertyValue(ref reader); break; + case "normalized": _normalized = DeserializePropertyValue(ref reader); break; + case "offset": _offset = DeserializePropertyValue(ref reader); break; + case "required": _required = DeserializePropertyValue(ref reader); break; + case "scale": _scale = DeserializePropertyValue(ref reader); break; + case "semantic": _semantic = DeserializePropertyValue(ref reader); break; + case "type": _type = DeserializePropertyValue(ref reader); break; + default: base.DeserializeProperty(jsonPropertyName,ref reader); break; + } + } + + } + + /// + /// A class containing a set of properties. + /// + #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 StructuralMetadataClass : ExtraProperties + { + + private String _description; + + private String _name; + + private Dictionary _properties; + + + protected override void SerializeProperties(Utf8JsonWriter writer) + { + base.SerializeProperties(writer); + SerializeProperty(writer, "description", _description); + SerializeProperty(writer, "name", _name); + SerializeProperty(writer, "properties", _properties); + } + + protected override void DeserializeProperty(string jsonPropertyName, ref Utf8JsonReader reader) + { + switch (jsonPropertyName) + { + case "description": _description = DeserializePropertyValue(ref reader); break; + case "name": _name = DeserializePropertyValue(ref reader); break; + case "properties": DeserializePropertyDictionary(ref reader, _properties); break; + default: base.DeserializeProperty(jsonPropertyName,ref reader); break; + } + } + + } + + /// + /// An enum value. + /// + #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 EnumValue : ExtraProperties + { + + private String _description; + + private String _name; + + private Int32 _value; + + + protected override void SerializeProperties(Utf8JsonWriter writer) + { + base.SerializeProperties(writer); + SerializeProperty(writer, "description", _description); + SerializeProperty(writer, "name", _name); + SerializeProperty(writer, "value", _value); + } + + protected override void DeserializeProperty(string jsonPropertyName, ref Utf8JsonReader reader) + { + switch (jsonPropertyName) + { + case "description": _description = DeserializePropertyValue(ref reader); break; + case "name": _name = DeserializePropertyValue(ref reader); break; + case "value": _value = DeserializePropertyValue(ref reader); break; + default: base.DeserializeProperty(jsonPropertyName,ref reader); break; + } + } + + } + + /// + /// An object defining the values of an enum. + /// + #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 StructuralMetadataEnum : ExtraProperties + { + + private String _description; + + private String _name; + + private const IntegerType _valueTypeDefault = IntegerType.UINT16; + private IntegerType? _valueType = _valueTypeDefault; + + private const int _valuesMinItems = 1; + private List _values; + + + protected override void SerializeProperties(Utf8JsonWriter writer) + { + base.SerializeProperties(writer); + SerializeProperty(writer, "description", _description); + SerializeProperty(writer, "name", _name); + SerializePropertyEnumSymbol(writer, "valueType", _valueType, _valueTypeDefault); + SerializeProperty(writer, "values", _values, _valuesMinItems); + } + + protected override void DeserializeProperty(string jsonPropertyName, ref Utf8JsonReader reader) + { + switch (jsonPropertyName) + { + case "description": _description = DeserializePropertyValue(ref reader); break; + case "name": _name = DeserializePropertyValue(ref reader); break; + case "valueType": _valueType = DeserializePropertyValue(ref reader); break; + case "values": DeserializePropertyList(ref reader, _values); break; + default: base.DeserializeProperty(jsonPropertyName,ref reader); break; + } + } + + } + + /// + /// An object defining classes and enums. + /// + #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 StructuralMetadataSchema : ExtraProperties + { + + private Dictionary _classes; + + private String _description; + + private Dictionary _enums; + + private String _id; + + private String _name; + + private String _version; + + + protected override void SerializeProperties(Utf8JsonWriter writer) + { + base.SerializeProperties(writer); + SerializeProperty(writer, "classes", _classes); + SerializeProperty(writer, "description", _description); + SerializeProperty(writer, "enums", _enums); + SerializeProperty(writer, "id", _id); + SerializeProperty(writer, "name", _name); + SerializeProperty(writer, "version", _version); + } + + protected override void DeserializeProperty(string jsonPropertyName, ref Utf8JsonReader reader) + { + switch (jsonPropertyName) + { + case "classes": DeserializePropertyDictionary(ref reader, _classes); break; + case "description": _description = DeserializePropertyValue(ref reader); break; + case "enums": DeserializePropertyDictionary(ref reader, _enums); break; + case "id": _id = DeserializePropertyValue(ref reader); break; + case "name": _name = DeserializePropertyValue(ref reader); break; + case "version": _version = DeserializePropertyValue(ref reader); break; + default: base.DeserializeProperty(jsonPropertyName,ref reader); break; + } + } + + } + + /// + /// An array of binary property values. + /// + #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 PropertyTableProperty : ExtraProperties + { + + private const StringOffsets _arrayOffsetTypeDefault = StringOffsets.UINT32; + private StringOffsets? _arrayOffsetType = _arrayOffsetTypeDefault; + + private Int32? _arrayOffsets; + + private System.Text.Json.Nodes.JsonNode _max; + + private System.Text.Json.Nodes.JsonNode _min; + + private System.Text.Json.Nodes.JsonNode _offset; + + private System.Text.Json.Nodes.JsonNode _scale; + + private const StringOffsets _stringOffsetTypeDefault = StringOffsets.UINT32; + private StringOffsets? _stringOffsetType = _stringOffsetTypeDefault; + + private Int32? _stringOffsets; + + private Int32 _values; + + + protected override void SerializeProperties(Utf8JsonWriter writer) + { + base.SerializeProperties(writer); + SerializePropertyEnumSymbol(writer, "arrayOffsetType", _arrayOffsetType, _arrayOffsetTypeDefault); + SerializeProperty(writer, "arrayOffsets", _arrayOffsets); + SerializeProperty(writer, "max", _max); + SerializeProperty(writer, "min", _min); + SerializeProperty(writer, "offset", _offset); + SerializeProperty(writer, "scale", _scale); + SerializePropertyEnumSymbol(writer, "stringOffsetType", _stringOffsetType, _stringOffsetTypeDefault); + SerializeProperty(writer, "stringOffsets", _stringOffsets); + SerializeProperty(writer, "values", _values); + } + + protected override void DeserializeProperty(string jsonPropertyName, ref Utf8JsonReader reader) + { + switch (jsonPropertyName) + { + case "arrayOffsetType": _arrayOffsetType = DeserializePropertyValue(ref reader); break; + case "arrayOffsets": _arrayOffsets = DeserializePropertyValue(ref reader); break; + case "max": _max = DeserializePropertyValue(ref reader); break; + case "min": _min = DeserializePropertyValue(ref reader); break; + case "offset": _offset = DeserializePropertyValue(ref reader); break; + case "scale": _scale = DeserializePropertyValue(ref reader); break; + case "stringOffsetType": _stringOffsetType = DeserializePropertyValue(ref reader); break; + case "stringOffsets": _stringOffsets = DeserializePropertyValue(ref reader); break; + case "values": _values = DeserializePropertyValue(ref reader); break; + default: base.DeserializeProperty(jsonPropertyName,ref reader); break; + } + } + + } + + /// + /// Properties conforming to a class, organized as property values stored in binary columnar arrays. + /// + #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 PropertyTable : ExtraProperties + { + + private String _class; + + private const Int32 _countMinimum = 1; + private Int32 _count; + + private String _name; + + private Dictionary _properties; + + + protected override void SerializeProperties(Utf8JsonWriter writer) + { + base.SerializeProperties(writer); + SerializeProperty(writer, "class", _class); + SerializeProperty(writer, "count", _count); + SerializeProperty(writer, "name", _name); + SerializeProperty(writer, "properties", _properties); + } + + protected override void DeserializeProperty(string jsonPropertyName, ref Utf8JsonReader reader) + { + switch (jsonPropertyName) + { + case "class": _class = DeserializePropertyValue(ref reader); break; + case "count": _count = DeserializePropertyValue(ref reader); break; + case "name": _name = DeserializePropertyValue(ref reader); break; + case "properties": DeserializePropertyDictionary(ref reader, _properties); break; + default: base.DeserializeProperty(jsonPropertyName,ref reader); break; + } + } + + } + + /// + /// A texture containing property values. + /// + #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 PropertyTextureProperty : TextureInfo + { + + private const int _channelsMinItems = 1; + private List _channels; + + private System.Text.Json.Nodes.JsonNode _max; + + private System.Text.Json.Nodes.JsonNode _min; + + private System.Text.Json.Nodes.JsonNode _offset; + + private System.Text.Json.Nodes.JsonNode _scale; + + + protected override void SerializeProperties(Utf8JsonWriter writer) + { + base.SerializeProperties(writer); + SerializeProperty(writer, "channels", _channels, _channelsMinItems); + SerializeProperty(writer, "max", _max); + SerializeProperty(writer, "min", _min); + SerializeProperty(writer, "offset", _offset); + SerializeProperty(writer, "scale", _scale); + } + + protected override void DeserializeProperty(string jsonPropertyName, ref Utf8JsonReader reader) + { + switch (jsonPropertyName) + { + case "channels": DeserializePropertyList(ref reader, _channels); break; + case "max": _max = DeserializePropertyValue(ref reader); break; + case "min": _min = DeserializePropertyValue(ref reader); break; + case "offset": _offset = DeserializePropertyValue(ref reader); break; + case "scale": _scale = DeserializePropertyValue(ref reader); break; + default: base.DeserializeProperty(jsonPropertyName,ref reader); break; + } + } + + } + + /// + /// Properties conforming to a class, organized as property values stored in textures. + /// + #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 PropertyTexture : ExtraProperties + { + + private String _class; + + private String _name; + + private Dictionary _properties; + + + protected override void SerializeProperties(Utf8JsonWriter writer) + { + base.SerializeProperties(writer); + SerializeProperty(writer, "class", _class); + SerializeProperty(writer, "name", _name); + SerializeProperty(writer, "properties", _properties); + } + + protected override void DeserializeProperty(string jsonPropertyName, ref Utf8JsonReader reader) + { + switch (jsonPropertyName) + { + case "class": _class = DeserializePropertyValue(ref reader); break; + case "name": _name = DeserializePropertyValue(ref reader); break; + case "properties": DeserializePropertyDictionary(ref reader, _properties); break; + default: base.DeserializeProperty(jsonPropertyName,ref reader); break; + } + } + + } + + /// + /// An attribute containing property values. + /// + #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 PropertyAttributeProperty : ExtraProperties + { + + private String _attribute; + + private System.Text.Json.Nodes.JsonNode _max; + + private System.Text.Json.Nodes.JsonNode _min; + + private System.Text.Json.Nodes.JsonNode _offset; + + private System.Text.Json.Nodes.JsonNode _scale; + + + protected override void SerializeProperties(Utf8JsonWriter writer) + { + base.SerializeProperties(writer); + SerializeProperty(writer, "attribute", _attribute); + SerializeProperty(writer, "max", _max); + SerializeProperty(writer, "min", _min); + SerializeProperty(writer, "offset", _offset); + SerializeProperty(writer, "scale", _scale); + } + + protected override void DeserializeProperty(string jsonPropertyName, ref Utf8JsonReader reader) + { + switch (jsonPropertyName) + { + case "attribute": _attribute = DeserializePropertyValue(ref reader); break; + case "max": _max = DeserializePropertyValue(ref reader); break; + case "min": _min = DeserializePropertyValue(ref reader); break; + case "offset": _offset = DeserializePropertyValue(ref reader); break; + case "scale": _scale = DeserializePropertyValue(ref reader); break; + default: base.DeserializeProperty(jsonPropertyName,ref reader); break; + } + } + + } + + /// + /// Properties conforming to a class, organized as property values stored in attributes. + /// + #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 PropertyAttributeinEXT_structural_metadata : ExtraProperties + { + + private String _class; + + private String _name; + + private Dictionary _properties; + + + protected override void SerializeProperties(Utf8JsonWriter writer) + { + base.SerializeProperties(writer); + SerializeProperty(writer, "class", _class); + SerializeProperty(writer, "name", _name); + SerializeProperty(writer, "properties", _properties); + } + + protected override void DeserializeProperty(string jsonPropertyName, ref Utf8JsonReader reader) + { + switch (jsonPropertyName) + { + case "class": _class = DeserializePropertyValue(ref reader); break; + case "name": _name = DeserializePropertyValue(ref reader); break; + case "properties": DeserializePropertyDictionary(ref reader, _properties); break; + default: base.DeserializeProperty(jsonPropertyName,ref reader); break; + } + } + + } + + /// + /// glTF extension that provides structural metadata about vertices, texels, and features in a glTF asset. + /// + #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 EXTStructuralMetaData : ExtraProperties + { + + private const int _propertyAttributesMinItems = 1; + private List _propertyAttributes; + + private const int _propertyTablesMinItems = 1; + private List _propertyTables; + + private const int _propertyTexturesMinItems = 1; + private List _propertyTextures; + + private StructuralMetadataSchema _schema; + + private String _schemaUri; + + + protected override void SerializeProperties(Utf8JsonWriter writer) + { + base.SerializeProperties(writer); + SerializeProperty(writer, "propertyAttributes", _propertyAttributes, _propertyAttributesMinItems); + SerializeProperty(writer, "propertyTables", _propertyTables, _propertyTablesMinItems); + SerializeProperty(writer, "propertyTextures", _propertyTextures, _propertyTexturesMinItems); + SerializePropertyObject(writer, "schema", _schema); + SerializeProperty(writer, "schemaUri", _schemaUri); + } + + protected override void DeserializeProperty(string jsonPropertyName, ref Utf8JsonReader reader) + { + switch (jsonPropertyName) + { + case "propertyAttributes": DeserializePropertyList(ref reader, _propertyAttributes); break; + case "propertyTables": DeserializePropertyList(ref reader, _propertyTables); break; + case "propertyTextures": DeserializePropertyList(ref reader, _propertyTextures); break; + case "schema": _schema = DeserializePropertyValue(ref reader); break; + case "schemaUri": _schemaUri = DeserializePropertyValue(ref reader); break; + default: base.DeserializeProperty(jsonPropertyName,ref reader); break; + } + } + + } + +} From fbd3d068dffdbf258275b180b3b2b28a3e9bee43 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Thu, 30 Nov 2023 15:12:48 +0100 Subject: [PATCH 03/57] start testing ext_structural_metadata --- .../Schema2/CesiumExtensions.cs | 8 +- .../Schema2/EXTStructuralMetaDataRoot.cs | 76 +++++++++++++++++++ ...t.CESIUM_ext_structural_metadata_root.g.cs | 2 +- .../ExtStructuralMetadataTests.cs | 18 +++++ 4 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs create mode 100644 tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs diff --git a/src/SharpGLTF.Cesium/Schema2/CesiumExtensions.cs b/src/SharpGLTF.Cesium/Schema2/CesiumExtensions.cs index ca70a156..2fd0ff6e 100644 --- a/src/SharpGLTF.Cesium/Schema2/CesiumExtensions.cs +++ b/src/SharpGLTF.Cesium/Schema2/CesiumExtensions.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace SharpGLTF.Schema2 +namespace SharpGLTF.Schema2 { /// /// Extension methods for Cesium glTF Extensions @@ -23,7 +19,7 @@ public static void RegisterExtensions() ExtensionsFactory.RegisterExtension("CESIUM_primitive_outline"); ExtensionsFactory.RegisterExtension("EXT_instance_features"); ExtensionsFactory.RegisterExtension("EXT_mesh_features"); - + ExtensionsFactory.RegisterExtension("EXT_structural_metadata"); } } } diff --git a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs new file mode 100644 index 00000000..50ca8dc6 --- /dev/null +++ b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs @@ -0,0 +1,76 @@ +using SharpGLTF.Validation; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace SharpGLTF.Schema2 +{ + public partial class EXTStructuralMetaDataRoot + { + internal EXTStructuralMetaDataRoot(ModelRoot modelRoot) + { + this.modelRoot = modelRoot; + } + + private ModelRoot modelRoot; + + + internal List PropertyTables + { + get { return _propertyTables; } + set { if (value == null) { _propertyTables = null; return; } _propertyTables = value; } + } + + internal StructuralMetadataSchema Schema + { + get { return _schema; } + set { if (value == null) { _schema = null; return; } _schema = value; } + } + + + protected override void OnValidateContent(ValidationContext validate) + { + } + } + + partial class StructuralMetadataSchema + { + public StructuralMetadataSchema() + { + _classes = new Dictionary(); + } + + } + + partial class PropertyTable + { + public PropertyTable() + { + _properties = new Dictionary(); + } + + } + + partial class PropertyTableProperty + { + } + + partial class StructuralMetadataClass + { + public StructuralMetadataClass() + { + _properties = new Dictionary(); + } + } + + partial class ClassProperty + { + } + + public static class ExtStructuralMetadata + { + public static void AddMetadata(this ModelRoot modelRoot, string fieldname, List values) + { + } + } +} \ No newline at end of file diff --git a/src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata_root.g.cs b/src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata_root.g.cs index 42ad8440..3ef66c7a 100644 --- a/src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata_root.g.cs +++ b/src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata_root.g.cs @@ -638,7 +638,7 @@ protected override void DeserializeProperty(string jsonPropertyName, ref Utf8Jso [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 EXTStructuralMetaData : ExtraProperties + partial class EXTStructuralMetaDataRoot : ExtraProperties { private const int _propertyAttributesMinItems = 1; diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs new file mode 100644 index 00000000..7c4d9c59 --- /dev/null +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -0,0 +1,18 @@ +using NUnit.Framework; +using SharpGLTF.Schema2; + +namespace SharpGLTF +{ + [Category("Toolkit.Scenes")] + public class ExtStructuralMetadataTests + { + [SetUp] + public void SetUp() + { + CesiumExtensions.RegisterExtensions(); + } + + + + } +} From cac94fea8357893fd577f1697b6b179b9d61f9f5 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Thu, 30 Nov 2023 20:13:15 +0100 Subject: [PATCH 04/57] adding first ext_structural_metadata test --- .../Schema2/EXTStructuralMetaDataRoot.cs | 39 ++++++++++++--- .../Schema2/MeshExtMeshFeatures.cs | 1 - .../ExtMeshFeaturesTests.cs | 13 ++--- .../ExtStructuralMetadataTests.cs | 47 ++++++++++++++++++- .../SharpGLTF.Cesium.Tests.csproj | 2 +- tests/SharpGLTF.Cesium.Tests/VertexBuilder.cs | 17 +++++++ 6 files changed, 99 insertions(+), 20 deletions(-) create mode 100644 tests/SharpGLTF.Cesium.Tests/VertexBuilder.cs diff --git a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs index 50ca8dc6..b98bf228 100644 --- a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs +++ b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs @@ -1,20 +1,17 @@ using SharpGLTF.Validation; -using System; using System.Collections.Generic; -using System.Linq; namespace SharpGLTF.Schema2 { public partial class EXTStructuralMetaDataRoot { + private ModelRoot modelRoot; + internal EXTStructuralMetaDataRoot(ModelRoot modelRoot) { this.modelRoot = modelRoot; } - private ModelRoot modelRoot; - - internal List PropertyTables { get { return _propertyTables; } @@ -40,15 +37,17 @@ public StructuralMetadataSchema() _classes = new Dictionary(); } + public Dictionary Classes { get; set; } } partial class PropertyTable { - public PropertyTable() + public PropertyTable(string PropertyTableName, int numberOfFeatures) { + _class = PropertyTableName; + _count = numberOfFeatures; _properties = new Dictionary(); } - } partial class PropertyTableProperty @@ -69,8 +68,34 @@ partial class ClassProperty public static class ExtStructuralMetadata { + // Creates EXTStructuralMetaData with Schema and 1 PropertyTable + public static void InitializeMetadataExtension(this ModelRoot modelRoot, string propertyTableName, int numberOfFeatures) + { + if (propertyTableName == null) { modelRoot.RemoveExtensions(); return; } + + var ext = modelRoot.UseExtension(); + + var schema = GetInitialSchema(propertyTableName); + ext.Schema = schema; + var propertyTable = new PropertyTable(propertyTableName, numberOfFeatures); + ext.PropertyTables = new List() { propertyTable }; + } + public static void AddMetadata(this ModelRoot modelRoot, string fieldname, List values) { } + + private static StructuralMetadataSchema GetInitialSchema(string schemaName) + { + var structuralMetadataSchema = new StructuralMetadataSchema(); + var structuralMetadataClass = new StructuralMetadataClass(); + + structuralMetadataSchema.Classes = new Dictionary + { + { schemaName , structuralMetadataClass } + }; + + return structuralMetadataSchema; + } } } \ No newline at end of file diff --git a/src/SharpGLTF.Cesium/Schema2/MeshExtMeshFeatures.cs b/src/SharpGLTF.Cesium/Schema2/MeshExtMeshFeatures.cs index a20c7ce3..d349072b 100644 --- a/src/SharpGLTF.Cesium/Schema2/MeshExtMeshFeatures.cs +++ b/src/SharpGLTF.Cesium/Schema2/MeshExtMeshFeatures.cs @@ -1,7 +1,6 @@ using SharpGLTF.Validation; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; namespace SharpGLTF.Schema2 { diff --git a/tests/SharpGLTF.Cesium.Tests/ExtMeshFeaturesTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtMeshFeaturesTests.cs index e8e59a31..21583a92 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtMeshFeaturesTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtMeshFeaturesTests.cs @@ -14,7 +14,6 @@ namespace SharpGLTF.Cesium { using VBTexture1 = VertexBuilder; - [Category("Toolkit.Scenes")] public class ExtMeshFeaturesTests { @@ -37,9 +36,9 @@ public void FeaturesIdAttributeTest() 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); + var vt0 = VertexBuilder.GetVertexWithFeatureId(new Vector3(-10, 0, 0), new Vector3(0, 0, 1), featureId); + var vt1 = VertexBuilder.GetVertexWithFeatureId(new Vector3(10, 0, 0), new Vector3(0, 0, 1), featureId); + var vt2 = VertexBuilder.GetVertexWithFeatureId(new Vector3(0, 10, 0), new Vector3(0, 0, 1), featureId); prim.AddTriangle(vt0, vt1, vt2); var scene = new SceneBuilder(); @@ -135,11 +134,5 @@ public void FeaturesIdTextureTest() 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/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index 7c4d9c59..34047142 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -1,7 +1,14 @@ using NUnit.Framework; +using SharpGLTF.Geometry; +using SharpGLTF.Geometry.VertexTypes; +using SharpGLTF.Materials; +using SharpGLTF.Scenes; using SharpGLTF.Schema2; +using SharpGLTF.Validation; +using System.Collections.Generic; +using System.Numerics; -namespace SharpGLTF +namespace SharpGLTF.Cesium { [Category("Toolkit.Scenes")] public class ExtStructuralMetadataTests @@ -12,7 +19,45 @@ public void SetUp() CesiumExtensions.RegisterExtensions(); } + [Test(Description ="First test with ext_structural_metadata")] + public void TriangleWithMetadataTest() + { + 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 = VertexBuilder.GetVertexWithFeatureId(new Vector3(-10, 0, 0), new Vector3(0, 0, 1), featureId); + var vt1 = VertexBuilder.GetVertexWithFeatureId(new Vector3(10, 0, 0), new Vector3(0, 0, 1), featureId); + var vt2 = VertexBuilder.GetVertexWithFeatureId(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); + model.InitializeMetadataExtension("propertyTable", 1); + + // todo add metadata + + var ctx = new ValidationResult(model, ValidationMode.Strict, true); + model.ValidateContent(ctx.GetContext()); + + model.AttachToCurrentTest("cesium_ext_structural_metadata_basic_triangle.glb"); + model.AttachToCurrentTest("cesium_ext_structural_metadata_basic_triangle.gltf"); + model.AttachToCurrentTest("cesium_ext_structural_metadata_basic_triangle.plotly"); + + } } } diff --git a/tests/SharpGLTF.Cesium.Tests/SharpGLTF.Cesium.Tests.csproj b/tests/SharpGLTF.Cesium.Tests/SharpGLTF.Cesium.Tests.csproj index b7f52163..b45f4a6c 100644 --- a/tests/SharpGLTF.Cesium.Tests/SharpGLTF.Cesium.Tests.csproj +++ b/tests/SharpGLTF.Cesium.Tests/SharpGLTF.Cesium.Tests.csproj @@ -1,7 +1,7 @@  - net471;net6.0-windows;net8.0-windows + net471;net8.0-windows false SharpGLTF latest diff --git a/tests/SharpGLTF.Cesium.Tests/VertexBuilder.cs b/tests/SharpGLTF.Cesium.Tests/VertexBuilder.cs new file mode 100644 index 00000000..28a17184 --- /dev/null +++ b/tests/SharpGLTF.Cesium.Tests/VertexBuilder.cs @@ -0,0 +1,17 @@ +using SharpGLTF.Geometry; +using SharpGLTF.Geometry.VertexTypes; +using System.Numerics; + +namespace SharpGLTF +{ + public static class VertexBuilder + { + internal static VertexBuilder GetVertexWithFeatureId(Vector3 position, Vector3 normal, int featureid) + { + var vp0 = new VertexPositionNormal(position, normal); + var vb0 = new VertexBuilder(vp0, featureid); + return vb0; + } + + } +} From dd29197dbd6ee6bfe242b7d5e278d2f5d1588bf8 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Tue, 5 Dec 2023 10:46:58 +0100 Subject: [PATCH 05/57] code cleanup in EXT_Instance_Features --- src/SharpGLTF.Cesium/Schema2/MeshExtInstanceFeatures.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/SharpGLTF.Cesium/Schema2/MeshExtInstanceFeatures.cs b/src/SharpGLTF.Cesium/Schema2/MeshExtInstanceFeatures.cs index 12c36ee1..9f4d4401 100644 --- a/src/SharpGLTF.Cesium/Schema2/MeshExtInstanceFeatures.cs +++ b/src/SharpGLTF.Cesium/Schema2/MeshExtInstanceFeatures.cs @@ -30,14 +30,13 @@ public List FeatureIds protected override void OnValidateContent(ValidationContext validate) { - var extInstanceFeatures = _node.Extensions.Where(item => item is MeshExtInstanceFeatures).FirstOrDefault(); + var extInstanceFeatures = _node.GetExtension(); validate.NotNull(nameof(extInstanceFeatures), extInstanceFeatures); - var ext = (MeshExtInstanceFeatures)extInstanceFeatures; - var extMeshGpInstancing = _node.Extensions.Where(item => item is MeshGpuInstancing).FirstOrDefault(); + var extMeshGpInstancing = _node.GetExtension(); validate.NotNull(nameof(extMeshGpInstancing), extMeshGpInstancing); - validate.NotNull(nameof(FeatureIds), ext.FeatureIds); - validate.IsTrue(nameof(FeatureIds), ext.FeatureIds.Count > 0, "Instance FeatureIds has items"); + validate.NotNull(nameof(FeatureIds), extInstanceFeatures.FeatureIds); + validate.IsTrue(nameof(FeatureIds), extInstanceFeatures.FeatureIds.Count > 0, "Instance FeatureIds has items"); base.OnValidateContent(validate); } From 4b4a9746ef7e2d673a2bf50605a3ada5f8c83e63 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Tue, 5 Dec 2023 20:49:33 +0100 Subject: [PATCH 06/57] got the ext_structural_metada - propertyTable working --- src/SharpGLTF.Cesium/BinaryTable.cs | 56 +++++++++++++ .../Schema2/EXTStructuralMetaDataRoot.cs | 82 ++++++------------- .../Schema2/MeshExtInstanceFeatures.cs | 2 - .../Schema2/MeshExtMeshFeatures.cs | 3 + .../ExtStructuralMetadataTests.cs | 28 +++---- 5 files changed, 92 insertions(+), 79 deletions(-) create mode 100644 src/SharpGLTF.Cesium/BinaryTable.cs diff --git a/src/SharpGLTF.Cesium/BinaryTable.cs b/src/SharpGLTF.Cesium/BinaryTable.cs new file mode 100644 index 00000000..4d225344 --- /dev/null +++ b/src/SharpGLTF.Cesium/BinaryTable.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace SharpGLTF +{ + public static class BinaryTable + { + public static byte[] GetOffsetBuffer(IReadOnlyList strings) + { + var offsetBuffer = GetOffsets(strings); + var offsetBytes = GetBytes(offsetBuffer); + return offsetBytes; + } + + public static byte[] GetBytes(IReadOnlyList values) + { + var type = typeof(T); + int size = 0; + if (type == typeof(float)) + { + size = sizeof(float); + } + else if (type == typeof(int)) + { + size = sizeof(int); + } + else if (type == typeof(uint)) + { + size = sizeof(uint); + } + + var result = new byte[values.Count * size]; + System.Buffer.BlockCopy(values.ToArray(), 0, result, 0, result.Length); + return result; + } + + private static List GetOffsets(IReadOnlyList strings) + { + var offsets = new List() { 0 }; + foreach (string s in strings) + { + var length = (uint)Encoding.UTF8.GetByteCount(s); + + offsets.Add(offsets.Last() + length); + } + return offsets; + } + + public static byte[] GetStringsAsBytes(IReadOnlyList values) + { + var res = string.Join("", values); + return Encoding.UTF8.GetBytes(res); + } + } +} diff --git a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs index b98bf228..40ebb684 100644 --- a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs +++ b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs @@ -10,92 +10,56 @@ public partial class EXTStructuralMetaDataRoot internal EXTStructuralMetaDataRoot(ModelRoot modelRoot) { this.modelRoot = modelRoot; + _propertyTables = new List(); } - internal List PropertyTables + public List PropertyTables { get { return _propertyTables; } - set { if (value == null) { _propertyTables = null; return; } _propertyTables = value; } + set { _propertyTables = value; } } - internal StructuralMetadataSchema Schema - { - get { return _schema; } - set { if (value == null) { _schema = null; return; } _schema = value; } - } - - protected override void OnValidateContent(ValidationContext validate) { } } - partial class StructuralMetadataSchema + public partial class PropertyTable { - public StructuralMetadataSchema() + public PropertyTable() { - _classes = new Dictionary(); + _properties = new Dictionary(); } - - public Dictionary Classes { get; set; } - } - - partial class PropertyTable - { - public PropertyTable(string PropertyTableName, int numberOfFeatures) + public PropertyTable(string PropertyTableName, int NumberOfFeatures): this() { _class = PropertyTableName; - _count = numberOfFeatures; - _properties = new Dictionary(); + _count = NumberOfFeatures; } - } - - partial class PropertyTableProperty - { - } - partial class StructuralMetadataClass - { - public StructuralMetadataClass() + public string PropertyTableName { - _properties = new Dictionary(); + get { return _class; } + set { _class = value; } } - } - partial class ClassProperty - { - } - - public static class ExtStructuralMetadata - { - // Creates EXTStructuralMetaData with Schema and 1 PropertyTable - public static void InitializeMetadataExtension(this ModelRoot modelRoot, string propertyTableName, int numberOfFeatures) + public int NumberOfFeatures { - if (propertyTableName == null) { modelRoot.RemoveExtensions(); return; } - - var ext = modelRoot.UseExtension(); - - var schema = GetInitialSchema(propertyTableName); - ext.Schema = schema; - var propertyTable = new PropertyTable(propertyTableName, numberOfFeatures); - ext.PropertyTables = new List() { propertyTable }; + get { return _count; } + set { _count = value; } } - public static void AddMetadata(this ModelRoot modelRoot, string fieldname, List values) + public Dictionary Properties { + get { return _properties; } + set { _properties = value; } } + } - private static StructuralMetadataSchema GetInitialSchema(string schemaName) - { - var structuralMetadataSchema = new StructuralMetadataSchema(); - var structuralMetadataClass = new StructuralMetadataClass(); - - structuralMetadataSchema.Classes = new Dictionary - { - { schemaName , structuralMetadataClass } - }; - - return structuralMetadataSchema; + public partial class PropertyTableProperty + { + public int Values { + get { return _values; } + set { _values = value; } } } } \ No newline at end of file diff --git a/src/SharpGLTF.Cesium/Schema2/MeshExtInstanceFeatures.cs b/src/SharpGLTF.Cesium/Schema2/MeshExtInstanceFeatures.cs index 9f4d4401..86bc94d6 100644 --- a/src/SharpGLTF.Cesium/Schema2/MeshExtInstanceFeatures.cs +++ b/src/SharpGLTF.Cesium/Schema2/MeshExtInstanceFeatures.cs @@ -1,8 +1,6 @@ using SharpGLTF.Validation; using System.Collections.Generic; using System.Linq; -using System.Xml.Linq; - namespace SharpGLTF.Schema2 { public partial class MeshExtInstanceFeatures diff --git a/src/SharpGLTF.Cesium/Schema2/MeshExtMeshFeatures.cs b/src/SharpGLTF.Cesium/Schema2/MeshExtMeshFeatures.cs index d349072b..9bd9b43e 100644 --- a/src/SharpGLTF.Cesium/Schema2/MeshExtMeshFeatures.cs +++ b/src/SharpGLTF.Cesium/Schema2/MeshExtMeshFeatures.cs @@ -54,6 +54,9 @@ public MeshExtMeshFeatureIDTexture(List channels, int? index = null, int? t public partial class MeshExtMeshFeatureID { + public MeshExtMeshFeatureID() + { + } public MeshExtMeshFeatureID(int featureCount, int? attribute = null, int? propertyTable = null, string label = null, int? nullFeatureId = null, MeshExtMeshFeatureIDTexture texture = null) { _featureCount = featureCount; diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index 34047142..fef27007 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -24,40 +24,32 @@ public void TriangleWithMetadataTest() { 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 mesh = new MeshBuilder("mesh"); var prim = mesh.UsePrimitive(material); - // All the vertices in the triangle have the same feature ID - var vt0 = VertexBuilder.GetVertexWithFeatureId(new Vector3(-10, 0, 0), new Vector3(0, 0, 1), featureId); - var vt1 = VertexBuilder.GetVertexWithFeatureId(new Vector3(10, 0, 0), new Vector3(0, 0, 1), featureId); - var vt2 = VertexBuilder.GetVertexWithFeatureId(new Vector3(0, 10, 0), new Vector3(0, 0, 1), featureId); + prim.AddTriangle(new VertexPosition(-10, 0, 0), new VertexPosition(10, 0, 0), new VertexPosition(0, 10, 0)); - prim.AddTriangle(vt0, vt1, vt2); var scene = new SceneBuilder(); scene.AddRigidMesh(mesh, Matrix4x4.Identity); var model = scene.ToGltf2(); - var featureIdAttribute = new MeshExtMeshFeatureID(1, 0); + var bytes = BinaryTable.GetBytes(new List() { 100 }); + var bufferView = model.UseBufferView(bytes); - // Set the FeatureIds - var featureIds = new List() { featureIdAttribute }; - model.LogicalMeshes[0].Primitives[0].SetFeatureIds(featureIds); + var ext = model.UseExtension(); - model.InitializeMetadataExtension("propertyTable", 1); - - // todo add metadata + var propertyTableProperty = new PropertyTableProperty(); + propertyTableProperty.Values = bufferView.LogicalIndex; + var propertyTable = new PropertyTable("propertyTable", 1); + propertyTable.Properties["id1"] = propertyTableProperty; + ext.PropertyTables.Add( propertyTable); var ctx = new ValidationResult(model, ValidationMode.Strict, true); - model.ValidateContent(ctx.GetContext()); - model.AttachToCurrentTest("cesium_ext_structural_metadata_basic_triangle.glb"); model.AttachToCurrentTest("cesium_ext_structural_metadata_basic_triangle.gltf"); model.AttachToCurrentTest("cesium_ext_structural_metadata_basic_triangle.plotly"); - } } } From 8cef54a968864af852d9352ba32d3def977f3845 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Wed, 6 Dec 2023 12:52:34 +0100 Subject: [PATCH 07/57] add binarytable tests --- src/SharpGLTF.Cesium/BinaryTable.cs | 104 +++++++++++++++--- src/SharpGLTF.Cesium/Validator.cs | 16 --- .../BinaryTableTests.cs | 74 +++++++++++++ .../ExtInstanceFeaturesTests.cs | 1 - ...umTests.cs => ExtPrimitiveOutlineTests.cs} | 8 +- 5 files changed, 167 insertions(+), 36 deletions(-) delete mode 100644 src/SharpGLTF.Cesium/Validator.cs create mode 100644 tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs rename tests/SharpGLTF.Cesium.Tests/{CesiumTests.cs => ExtPrimitiveOutlineTests.cs} (94%) diff --git a/src/SharpGLTF.Cesium/BinaryTable.cs b/src/SharpGLTF.Cesium/BinaryTable.cs index 4d225344..b7cc8510 100644 --- a/src/SharpGLTF.Cesium/BinaryTable.cs +++ b/src/SharpGLTF.Cesium/BinaryTable.cs @@ -1,11 +1,57 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Text; namespace SharpGLTF { + /// + /// Function for converting data into binary buffers + /// Specs see https://github.com/CesiumGS/3d-tiles/tree/main/specification/Metadata#binary-table-format + /// public static class BinaryTable { + /// + /// Converts a list of primitive types into a byte array + /// + /// + /// + /// byte array + public static byte[] GetBytes(IReadOnlyList values) + { + Guard.IsTrue(values.Count > 0, nameof(values), "values must have at least one element"); + + if (typeof(T) == typeof(string)) + { + return GetStringsAsBytes(values.Cast().ToArray()); + } + else if (typeof(T).IsPrimitive) + { + if(typeof(T) == typeof(bool)) + { + // when implementing bool, create a bitstream + // see https://github.com/CesiumGS/3d-tiles/tree/main/specification/Metadata#booleans + throw new NotImplementedException(); + } + var size = GetSize(); + var result = new byte[values.Count * size]; + Buffer.BlockCopy(values.ToArray(), 0, result, 0, result.Length); + return result; + } + else + { + // other types (like enum, mat2, mat3, mat4, vec2, vec3, vec4, array (fixed length, variable length)) are not implemented + // see https://github.com/CesiumGS/3d-tiles/tree/main/specification/Metadata#binary-table-format + throw new NotImplementedException(); + } + } + + /// + /// Creates a list of offsets for a list of strings + /// see https://github.com/CesiumGS/3d-tiles/tree/main/specification/Metadata#strings + /// + /// + /// public static byte[] GetOffsetBuffer(IReadOnlyList strings) { var offsetBuffer = GetOffsets(strings); @@ -13,13 +59,28 @@ public static byte[] GetOffsetBuffer(IReadOnlyList strings) return offsetBytes; } - public static byte[] GetBytes(IReadOnlyList values) + public static int GetSize() { + var isValueType = typeof(T).IsValueType; + Guard.IsTrue(isValueType, nameof(T), "T must be a value type"); + var type = typeof(T); int size = 0; - if (type == typeof(float)) + if (type == typeof(sbyte)) { - size = sizeof(float); + size = sizeof(sbyte); + } + else if (type == typeof(byte)) + { + size = sizeof(byte); + } + else if (type == typeof(short)) + { + size = sizeof(short); + } + else if (type == typeof(ushort)) + { + size = sizeof(ushort); } else if (type == typeof(int)) { @@ -29,10 +90,33 @@ public static byte[] GetBytes(IReadOnlyList values) { size = sizeof(uint); } + else if (type == typeof(long)) + { + size = sizeof(long); + } + else if (type == typeof(ulong)) + { + size = sizeof(ulong); + } + else if (type == typeof(float)) + { + size = sizeof(float); + } + else if (type == typeof(double)) + { + size = sizeof(double); + } + else if (type == typeof(bool)) + { + size = sizeof(bool); + } + return size; + } - var result = new byte[values.Count * size]; - System.Buffer.BlockCopy(values.ToArray(), 0, result, 0, result.Length); - return result; + private static byte[] GetStringsAsBytes(IReadOnlyList values) + { + var res = string.Join("", values); + return Encoding.UTF8.GetBytes(res); } private static List GetOffsets(IReadOnlyList strings) @@ -46,11 +130,5 @@ private static List GetOffsets(IReadOnlyList strings) } return offsets; } - - public static byte[] GetStringsAsBytes(IReadOnlyList values) - { - var res = string.Join("", values); - return Encoding.UTF8.GetBytes(res); - } } } diff --git a/src/SharpGLTF.Cesium/Validator.cs b/src/SharpGLTF.Cesium/Validator.cs deleted file mode 100644 index 91d0cd9e..00000000 --- a/src/SharpGLTF.Cesium/Validator.cs +++ /dev/null @@ -1,16 +0,0 @@ -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/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs b/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs new file mode 100644 index 00000000..7a370658 --- /dev/null +++ b/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs @@ -0,0 +1,74 @@ +using NUnit.Framework; +using System; +using System.Collections.Generic; + +namespace SharpGLTF +{ + public class BinaryTableTests + { + [Test] + public void TestBinaryConversion() + { + var bytes = BinaryTable.GetBytes(GetTestArray()); + Assert.That(bytes.Length, Is.EqualTo(BinaryTable.GetSize() * 2)); + + bytes = BinaryTable.GetBytes(GetTestArray()); + Assert.That(bytes.Length, Is.EqualTo(BinaryTable.GetSize() * 2)); + + bytes = BinaryTable.GetBytes(GetTestArray()); + Assert.That(bytes.Length, Is.EqualTo(BinaryTable.GetSize() * 2)); + + bytes = BinaryTable.GetBytes(GetTestArray()); + Assert.That(bytes.Length, Is.EqualTo(BinaryTable.GetSize() * 2)); + + bytes = BinaryTable.GetBytes(GetTestArray()); + Assert.That(bytes.Length, Is.EqualTo(BinaryTable.GetSize() * 2)); + + bytes = BinaryTable.GetBytes(GetTestArray()); + Assert.That(bytes.Length, Is.EqualTo(BinaryTable.GetSize() * 2)); + + bytes = BinaryTable.GetBytes(GetTestArray()); + Assert.That(bytes.Length, Is.EqualTo(BinaryTable.GetSize() * 2)); + + bytes = BinaryTable.GetBytes(GetTestArray()); + Assert.That(bytes.Length, Is.EqualTo(BinaryTable.GetSize() * 2)); + + bytes = BinaryTable.GetBytes(GetTestArray()); + Assert.That(bytes.Length, Is.EqualTo(BinaryTable.GetSize() * 2)); + + bytes = BinaryTable.GetBytes(GetTestArray()); + Assert.That(bytes.Length, Is.EqualTo(BinaryTable.GetSize() * 2)); + + bytes = BinaryTable.GetBytes(GetTestArray()); + Assert.That(bytes.Length, Is.EqualTo(BinaryTable.GetSize() * 2)); + + bytes = BinaryTable.GetBytes(new List() { "a", "b" }); + Assert.That(bytes.Length, Is.EqualTo(2)); + + Assert.Throws(() => BinaryTable.GetBytes(new List() { true, false })); + var ints = new List>(); + ints.Add(new List() { 0, 1 }); + Assert.Throws(() => BinaryTable.GetBytes(ints)); + } + + [Test] + public void TestOffsetBufferStrings() + { + var strings = new List { "hello, ", "world" }; + var offsetBytes = BinaryTable.GetOffsetBuffer(strings); + Assert.That(offsetBytes.Length, Is.EqualTo(12)); + Assert.That(BitConverter.ToInt32(offsetBytes, 0), Is.EqualTo(0)); + Assert.That(BitConverter.ToInt32(offsetBytes, 4), Is.EqualTo(strings[0].Length)); + Assert.That(BitConverter.ToInt32(offsetBytes, 8), Is.EqualTo(strings[0].Length + strings[1].Length)); + } + + private List GetTestArray() + { + var l = new List(); + l.Add((T)Convert.ChangeType(0, typeof(T))); + l.Add((T)Convert.ChangeType(1, typeof(T))); + return l; + } + + } +} diff --git a/tests/SharpGLTF.Cesium.Tests/ExtInstanceFeaturesTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtInstanceFeaturesTests.cs index e735b8bc..10929cbc 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtInstanceFeaturesTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtInstanceFeaturesTests.cs @@ -4,7 +4,6 @@ using SharpGLTF.Transforms; using SharpGLTF.Validation; using System.Collections.Generic; -using System.Linq; using System.Numerics; using System.Text.Json.Nodes; diff --git a/tests/SharpGLTF.Cesium.Tests/CesiumTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtPrimitiveOutlineTests.cs similarity index 94% rename from tests/SharpGLTF.Cesium.Tests/CesiumTests.cs rename to tests/SharpGLTF.Cesium.Tests/ExtPrimitiveOutlineTests.cs index a7091638..e4a6842b 100644 --- a/tests/SharpGLTF.Cesium.Tests/CesiumTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtPrimitiveOutlineTests.cs @@ -1,10 +1,6 @@ -using System; -using System.Linq; +using System.Linq; using System.Numerics; -using System.Collections.Generic; - using NUnit.Framework; - using SharpGLTF.Schema2; using SharpGLTF.Geometry; using SharpGLTF.Geometry.VertexTypes; @@ -16,7 +12,7 @@ namespace SharpGLTF { [Category("Cesium")] - public partial class CesiumTests + public partial class ExtPrimitiveOutlineTests { [SetUp] public void SetUp() From 22c9b41bb693a96a09b87724928439ffaeeafd92 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Wed, 6 Dec 2023 14:31:04 +0100 Subject: [PATCH 08/57] add StructuralMetadataSchema --- .../Schema2/EXTStructuralMetaDataRoot.cs | 158 +++++++++++++++++- .../ExtStructuralMetadataTests.cs | 39 +++++ 2 files changed, 194 insertions(+), 3 deletions(-) diff --git a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs index 40ebb684..60af7b50 100644 --- a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs +++ b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs @@ -19,18 +19,168 @@ public List PropertyTables set { _propertyTables = value; } } + public StructuralMetadataSchema Schema + { + get { return _schema; } + set { _schema = value; } + } + + protected override void OnValidateContent(ValidationContext validate) { } } + public partial class StructuralMetadataSchema + { + public StructuralMetadataSchema() + { + _classes = new Dictionary(); + _enums = new Dictionary(); + } + + public Dictionary Classes + { + get { return _classes; } + set { _classes = value; } + } + + public string Id + { + get { return _id; } + set { _id = value; } + } + + public string Version + { + get { return _version; } + set { _version = value; } + } + + public string Name + { + get { return _name; } + set { _name = value; } + } + + public string Description + { + get { return _description; } + set { _description = value; } + } + + public Dictionary Enums + { + get { return _enums; } + set { _enums = value; } + } + } + + public partial class StructuralMetadataEnum + { + public StructuralMetadataEnum() + { + _values = new List(); + } + public string Name + { + get { return _name; } + set { _name = value; } + } + public string Description + { + get { return _description; } + set { _description = value; } + } + public List Values + { + get { return _values; } + set { _values = value; } + } + } + + public partial class EnumValue + { + public string Name + { + get { return _name; } + set { _name = value; } + } + public int Value + { + get { return _value; } + set { _value = value; } + } + } + + public partial class StructuralMetadataClass + { + public StructuralMetadataClass() + { + _properties = new Dictionary(); + } + + public Dictionary Properties + { + get { return _properties; } + set { _properties = value; } + } + + public string Name + { + get { return _name; } + set { _name = value; } + } + + public string Description + { + get { return _description; } + set { _description = value; } + } + + } + + public partial class ClassProperty + { + public string Description + { + get { return _description; } + set { _description = value; } + } + + public ElementType Type + { + get { return _type; } + set { _type = value; } + } + + public string EnumType + { + get { return _enumType; } + set { _enumType = value; } + } + + public DataType? ComponentType + { + get { return _componentType; } + set { _componentType = value; } + } + + // required property + public bool? Required + { + get { return _required; } + set { _required = value; } + } + } + public partial class PropertyTable { public PropertyTable() { _properties = new Dictionary(); } - public PropertyTable(string PropertyTableName, int NumberOfFeatures): this() + public PropertyTable(string PropertyTableName, int NumberOfFeatures) : this() { _class = PropertyTableName; _count = NumberOfFeatures; @@ -57,9 +207,11 @@ public Dictionary Properties public partial class PropertyTableProperty { - public int Values { + public int Values + { get { return _values; } set { _values = value; } } } -} \ No newline at end of file +} + diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index fef27007..ee3fe317 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -46,6 +46,45 @@ public void TriangleWithMetadataTest() propertyTable.Properties["id1"] = propertyTableProperty; ext.PropertyTables.Add( propertyTable); + var schema = new StructuralMetadataSchema(); + schema.Id = "schema_001"; + schema.Name = "schema 001"; + schema.Description = "an example schema"; + schema.Version = "3.5.1"; + var classes = new Dictionary(); + var treeClass = new StructuralMetadataClass(); + classes["tree"] = treeClass; + treeClass.Name = "Tree"; + treeClass.Description = "Woody, perennial plant."; + + var speciesProperty = new ClassProperty(); + speciesProperty.Description = "Type of tree"; + speciesProperty.Type = ElementType.ENUM; + speciesProperty.EnumType = "speciesEnum"; + speciesProperty.Required = true; + + treeClass.Properties.Add("species", speciesProperty); + + var ageProperty = new ClassProperty(); + ageProperty.Description = "The age of the tree, in years"; + ageProperty.Type = ElementType.SCALAR; + ageProperty.ComponentType = DataType.UINT8; + ageProperty.Required = true; + + treeClass.Properties.Add("age", ageProperty); + + var speciesEnum = new StructuralMetadataEnum(); + schema.Enums["speciesEnum"] = speciesEnum; + speciesEnum.Name = "Species"; + speciesEnum.Description = "An example enum for tree species."; + speciesEnum.Values.Add(new EnumValue() { Name = "Unpsecified", Value = 0 }); + speciesEnum.Values.Add(new EnumValue() { Name = "Oak", Value = 1 }); + speciesEnum.Values.Add(new EnumValue() { Name = "Pine", Value = 2 }); + speciesEnum.Values.Add(new EnumValue() { Name = "Maple", Value = 3 }); + + schema.Classes = classes; + ext.Schema = schema; + var ctx = new ValidationResult(model, ValidationMode.Strict, true); model.AttachToCurrentTest("cesium_ext_structural_metadata_basic_triangle.glb"); model.AttachToCurrentTest("cesium_ext_structural_metadata_basic_triangle.gltf"); From 02f51f3bbab85549f2040543bf62b3dc2b9dd579 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Thu, 7 Dec 2023 17:44:47 +0100 Subject: [PATCH 09/57] add more metadata testing --- .../Ext.EXT_Structural_Metadata.cs | 5 +- .../Schema2/EXTStructuralMetaDataRoot.cs | 167 +++++++++++++++++- ...t.CESIUM_ext_structural_metadata_root.g.cs | 6 +- .../ExtStructuralMetadataTests.cs | 87 +++++---- 4 files changed, 214 insertions(+), 51 deletions(-) diff --git a/build/SharpGLTF.CodeGen/Ext.EXT_Structural_Metadata.cs b/build/SharpGLTF.CodeGen/Ext.EXT_Structural_Metadata.cs index 32d6180e..2443ec2f 100644 --- a/build/SharpGLTF.CodeGen/Ext.EXT_Structural_Metadata.cs +++ b/build/SharpGLTF.CodeGen/Ext.EXT_Structural_Metadata.cs @@ -15,7 +15,7 @@ class ExtStructuralMetadataExtension : SchemaProcessor public override void PrepareTypes(CSharpEmitter newEmitter, SchemaType.Context ctx) { - newEmitter.SetRuntimeName("EXT_structural_metadata glTF extension", "EXTStructuralMetaData"); + newEmitter.SetRuntimeName("EXT_structural_metadata glTF extension", "EXTStructuralMetaDataRoot"); newEmitter.SetRuntimeName("Property Table in EXT_structural_metadata", "PropertyTable"); newEmitter.SetRuntimeName("Schema in EXT_structural_metadata", "StructuralMetadataSchema"); newEmitter.SetRuntimeName("Property Table Property in EXT_structural_metadata", "PropertyTableProperty"); @@ -26,7 +26,7 @@ public override void PrepareTypes(CSharpEmitter newEmitter, SchemaType.Context c newEmitter.SetRuntimeName("Class in EXT_structural_metadata", "StructuralMetadataClass"); newEmitter.SetRuntimeName("Enum Value in EXT_structural_metadata", "EnumValue"); newEmitter.SetRuntimeName("Enum in EXT_structural_metadata", "StructuralMetadataEnum"); - + newEmitter.SetRuntimeName("Property Attribute in EXT_structural_metadata", "PropertyAttribute"); newEmitter.SetRuntimeName("BOOLEAN-ENUM-MAT2-MAT3-MAT4-SCALAR-STRING-VEC2-VEC3-VEC4", "ElementType"); newEmitter.SetRuntimeName("FLOAT32-FLOAT64-INT16-INT32-INT64-INT8-UINT16-UINT32-UINT64-UINT8", "DataType"); newEmitter.SetRuntimeName("INT16-INT32-INT64-INT8-UINT16-UINT32-UINT64-UINT8", "IntegerType"); @@ -51,7 +51,6 @@ private static SchemaType.Context ProcessRoot() // in the constructor or on demand when the APIs are Called. fld.RemoveDefaultValue(); - return ctx; } diff --git a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs index 60af7b50..064d22d4 100644 --- a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs +++ b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs @@ -1,8 +1,83 @@ using SharpGLTF.Validation; using System.Collections.Generic; +using System.Linq; namespace SharpGLTF.Schema2 { + public static class ExtStructuralMetadata + { + // sample see https://github.com/CesiumGS/3d-tiles-samples/blob/main/glTF/EXT_structural_metadata/SimplePropertyTexture/SimplePropertyTexture.gltf + public static void SetPropertyTexture( + this ModelRoot modelRoot, + StructuralMetadataSchema schema, + PropertyTexture propertyTexture) + { + if (schema == null || propertyTexture == null) { modelRoot.RemoveExtensions(); return; } + + var ext = modelRoot.UseExtension(); + ext.Schema = schema; + ext.PropertyTextures.Clear(); + ext.PropertyTextures.Add(propertyTexture); + } + + public static void SetPropertyAttribute( + this ModelRoot modelRoot, + PropertyAttribute propertyAttribute) + { + if (propertyAttribute == null) { modelRoot.RemoveExtensions(); return; } + + var ext = modelRoot.UseExtension(); + ext.PropertyAttributes.Clear(); + ext.PropertyAttributes.Add(propertyAttribute); + } + + public static void SetPropertyTable( + this ModelRoot modelRoot, + StructuralMetadataSchema schema, + Dictionary> attributes + ) + { + if (schema == null || attributes == null) { modelRoot.RemoveExtensions(); return; } + + var ext = modelRoot.UseExtension(); + ext.Schema = schema; + ext.PropertyTables.Clear(); + ext.PropertyTables.Add(GetPropertyTable(modelRoot, schema, attributes)); + } + + + private static PropertyTable GetPropertyTable( + ModelRoot modelRoot, + StructuralMetadataSchema schema, + Dictionary> attributes, + string name = "PropertyTable") + { + var propertyTable = new PropertyTable(name, attributes.FirstOrDefault().Value.Count); + + var firstClass = schema.Classes.FirstOrDefault().Value; + + foreach (var property in firstClass.Properties) + { + var id = property.Key; + var type = property.Value.Type; + + // Todo check type, for example string + var attribute = attributes[id]; + var list = attribute.ConvertAll(x => (int)x); + + byte[] bytes = BinaryTable.GetBytes(list); + var bufferView = modelRoot.UseBufferView(bytes); + int logicalIndex = bufferView.LogicalIndex; + var propertyTableProperty = new PropertyTableProperty(); + propertyTableProperty.Values = logicalIndex; + propertyTable.Properties[id] = propertyTableProperty; + } + + return propertyTable; + } + + } + public partial class EXTStructuralMetaDataRoot { private ModelRoot modelRoot; @@ -11,26 +86,97 @@ internal EXTStructuralMetaDataRoot(ModelRoot modelRoot) { this.modelRoot = modelRoot; _propertyTables = new List(); + _propertyAttributes = new List(); + _propertyTextures = new List(); } - public List PropertyTables + internal List PropertyTables { get { return _propertyTables; } set { _propertyTables = value; } } - public StructuralMetadataSchema Schema + internal List PropertyAttributes + { + get { return _propertyAttributes; } + set { _propertyAttributes = value; } + } + + internal StructuralMetadataSchema Schema { get { return _schema; } set { _schema = value; } } + internal List PropertyTextures + { + get { return _propertyTextures; } + set { _propertyTextures = value; } + } protected override void OnValidateContent(ValidationContext validate) { } } + public partial class PropertyTexture + { + public PropertyTexture() + { + _properties = new Dictionary(); + } + + public string Class + { + get { return _class; } + set { _class = value; } + } + + public Dictionary Properties + { + get { return _properties; } + set { _properties = value; } + } + } + + public partial class PropertyTextureProperty + { + //public int Index + //{ + // get { return _index; } + // set { _index = value; } + //} + } + + public partial class PropertyAttribute + { + public PropertyAttribute() + { + _properties = new Dictionary(); + } + public string Class + { + get { return _class; } + set { _class = value; } + } + + public Dictionary Properties + { + get { return _properties; } + set { _properties = value; } + } + + } + + public partial class PropertyAttributeProperty + { + public string Attribute + { + get { return _attribute; } + set { _attribute = value; } + } + } + public partial class StructuralMetadataSchema { public StructuralMetadataSchema() @@ -166,12 +312,17 @@ public DataType? ComponentType set { _componentType = value; } } - // required property public bool? Required { get { return _required; } set { _required = value; } } + + public bool? Normalized + { + get { return _normalized; } + set { _normalized = value; } + } } public partial class PropertyTable @@ -180,19 +331,19 @@ public PropertyTable() { _properties = new Dictionary(); } - public PropertyTable(string PropertyTableName, int NumberOfFeatures) : this() + public PropertyTable(string Class, int Count) : this() { - _class = PropertyTableName; - _count = NumberOfFeatures; + _class = Class; + _count = Count; } - public string PropertyTableName + public string Class { get { return _class; } set { _class = value; } } - public int NumberOfFeatures + public int Count { get { return _count; } set { _count = value; } diff --git a/src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata_root.g.cs b/src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata_root.g.cs index 3ef66c7a..5f378d81 100644 --- a/src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata_root.g.cs +++ b/src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata_root.g.cs @@ -600,7 +600,7 @@ protected override void DeserializeProperty(string jsonPropertyName, ref Utf8Jso [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 PropertyAttributeinEXT_structural_metadata : ExtraProperties + partial class PropertyAttribute : ExtraProperties { private String _class; @@ -642,7 +642,7 @@ partial class EXTStructuralMetaDataRoot : ExtraProperties { private const int _propertyAttributesMinItems = 1; - private List _propertyAttributes; + private List _propertyAttributes; private const int _propertyTablesMinItems = 1; private List _propertyTables; @@ -669,7 +669,7 @@ protected override void DeserializeProperty(string jsonPropertyName, ref Utf8Jso { switch (jsonPropertyName) { - case "propertyAttributes": DeserializePropertyList(ref reader, _propertyAttributes); break; + case "propertyAttributes": DeserializePropertyList(ref reader, _propertyAttributes); break; case "propertyTables": DeserializePropertyList(ref reader, _propertyTables); break; case "propertyTextures": DeserializePropertyList(ref reader, _propertyTextures); break; case "schema": _schema = DeserializePropertyValue(ref reader); break; diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index ee3fe317..fbf90301 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -19,8 +19,51 @@ public void SetUp() CesiumExtensions.RegisterExtensions(); } + /// + /// Sample see https://github.com/CesiumGS/3d-tiles-samples/blob/main/glTF/EXT_structural_metadata/PropertyAttributesPointCloud/PropertyAttributesPointCloudHouse.gltf + /// + [Test(Description = "ext_structural_metadata with PropertyAttributes")] + public void TriangleWithPropertyAttributes() + { + // todo: create a model with custom vertex attributes "_INTENSITY" and "_CLASSIFICATION" + var model = GetTriangleModel(); + var propertyAttribute = new Schema2.PropertyAttribute(); + propertyAttribute.Class = "exampleMetadataClass"; + var intensityProperty = new PropertyAttributeProperty(); + intensityProperty.Attribute = "_INTENSITY"; + var classificationProperty = new PropertyAttributeProperty(); + classificationProperty.Attribute = "_CLASSIFICATION"; + propertyAttribute.Properties["intensity"] = intensityProperty; + propertyAttribute.Properties["classification"] = classificationProperty; + + model.SetPropertyAttribute(propertyAttribute); + + var ctx = new ValidationResult(model, ValidationMode.Strict, true); + model.AttachToCurrentTest("cesium_ext_structural_metadata_with_property_attribute.glb"); + model.AttachToCurrentTest("cesium_ext_structural_metadata_with_property_attribute.gltf"); + model.AttachToCurrentTest("cesium_ext_structural_metadata_with_property_attribute.plotly"); + } + [Test(Description ="First test with ext_structural_metadata")] public void TriangleWithMetadataTest() + { + var model = GetTriangleModel(); + + var schema = GetSampleSchema(); + + var attribute = new List() { 100 }; + var dict = new Dictionary>(); + dict["age"] = attribute; + model.SetPropertyTable(schema, dict); + + // create files + var ctx = new ValidationResult(model, ValidationMode.Strict, true); + model.AttachToCurrentTest("cesium_ext_structural_metadata_basic_triangle.glb"); + model.AttachToCurrentTest("cesium_ext_structural_metadata_basic_triangle.gltf"); + model.AttachToCurrentTest("cesium_ext_structural_metadata_basic_triangle.plotly"); + } + + private static ModelRoot GetTriangleModel() { TestContext.CurrentContext.AttachGltfValidatorLinks(); @@ -34,18 +77,11 @@ public void TriangleWithMetadataTest() var scene = new SceneBuilder(); scene.AddRigidMesh(mesh, Matrix4x4.Identity); var model = scene.ToGltf2(); + return model; + } - var bytes = BinaryTable.GetBytes(new List() { 100 }); - var bufferView = model.UseBufferView(bytes); - - var ext = model.UseExtension(); - - var propertyTableProperty = new PropertyTableProperty(); - propertyTableProperty.Values = bufferView.LogicalIndex; - var propertyTable = new PropertyTable("propertyTable", 1); - propertyTable.Properties["id1"] = propertyTableProperty; - ext.PropertyTables.Add( propertyTable); - + private static StructuralMetadataSchema GetSampleSchema() + { var schema = new StructuralMetadataSchema(); schema.Id = "schema_001"; schema.Name = "schema 001"; @@ -53,42 +89,19 @@ public void TriangleWithMetadataTest() schema.Version = "3.5.1"; var classes = new Dictionary(); var treeClass = new StructuralMetadataClass(); - classes["tree"] = treeClass; treeClass.Name = "Tree"; treeClass.Description = "Woody, perennial plant."; - - var speciesProperty = new ClassProperty(); - speciesProperty.Description = "Type of tree"; - speciesProperty.Type = ElementType.ENUM; - speciesProperty.EnumType = "speciesEnum"; - speciesProperty.Required = true; - - treeClass.Properties.Add("species", speciesProperty); - + classes["tree"] = treeClass; var ageProperty = new ClassProperty(); ageProperty.Description = "The age of the tree, in years"; ageProperty.Type = ElementType.SCALAR; - ageProperty.ComponentType = DataType.UINT8; + ageProperty.ComponentType = DataType.UINT32; ageProperty.Required = true; treeClass.Properties.Add("age", ageProperty); - var speciesEnum = new StructuralMetadataEnum(); - schema.Enums["speciesEnum"] = speciesEnum; - speciesEnum.Name = "Species"; - speciesEnum.Description = "An example enum for tree species."; - speciesEnum.Values.Add(new EnumValue() { Name = "Unpsecified", Value = 0 }); - speciesEnum.Values.Add(new EnumValue() { Name = "Oak", Value = 1 }); - speciesEnum.Values.Add(new EnumValue() { Name = "Pine", Value = 2 }); - speciesEnum.Values.Add(new EnumValue() { Name = "Maple", Value = 3 }); - schema.Classes = classes; - ext.Schema = schema; - - var ctx = new ValidationResult(model, ValidationMode.Strict, true); - model.AttachToCurrentTest("cesium_ext_structural_metadata_basic_triangle.glb"); - model.AttachToCurrentTest("cesium_ext_structural_metadata_basic_triangle.gltf"); - model.AttachToCurrentTest("cesium_ext_structural_metadata_basic_triangle.plotly"); + return schema; } } } From cf54bdf89b3bc3f19655d6edd04dcbe40f19bd97 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Fri, 8 Dec 2023 10:41:01 +0100 Subject: [PATCH 10/57] code cleanup --- src/SharpGLTF.Cesium/BinaryTable.cs | 47 +------------------ .../BinaryTableTests.cs | 30 +----------- 2 files changed, 3 insertions(+), 74 deletions(-) diff --git a/src/SharpGLTF.Cesium/BinaryTable.cs b/src/SharpGLTF.Cesium/BinaryTable.cs index b7cc8510..a8c41b1e 100644 --- a/src/SharpGLTF.Cesium/BinaryTable.cs +++ b/src/SharpGLTF.Cesium/BinaryTable.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices; using System.Text; namespace SharpGLTF @@ -65,51 +66,7 @@ public static int GetSize() Guard.IsTrue(isValueType, nameof(T), "T must be a value type"); var type = typeof(T); - int size = 0; - if (type == typeof(sbyte)) - { - size = sizeof(sbyte); - } - else if (type == typeof(byte)) - { - size = sizeof(byte); - } - else if (type == typeof(short)) - { - size = sizeof(short); - } - else if (type == typeof(ushort)) - { - size = sizeof(ushort); - } - else if (type == typeof(int)) - { - size = sizeof(int); - } - else if (type == typeof(uint)) - { - size = sizeof(uint); - } - else if (type == typeof(long)) - { - size = sizeof(long); - } - else if (type == typeof(ulong)) - { - size = sizeof(ulong); - } - else if (type == typeof(float)) - { - size = sizeof(float); - } - else if (type == typeof(double)) - { - size = sizeof(double); - } - else if (type == typeof(bool)) - { - size = sizeof(bool); - } + int size = Marshal.SizeOf(Activator.CreateInstance(type)); return size; } diff --git a/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs b/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs index 7a370658..e699a504 100644 --- a/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs @@ -9,39 +9,11 @@ public class BinaryTableTests [Test] public void TestBinaryConversion() { - var bytes = BinaryTable.GetBytes(GetTestArray()); - Assert.That(bytes.Length, Is.EqualTo(BinaryTable.GetSize() * 2)); - - bytes = BinaryTable.GetBytes(GetTestArray()); + var bytes = BinaryTable.GetBytes(GetTestArray()); Assert.That(bytes.Length, Is.EqualTo(BinaryTable.GetSize() * 2)); - bytes = BinaryTable.GetBytes(GetTestArray()); - Assert.That(bytes.Length, Is.EqualTo(BinaryTable.GetSize() * 2)); - - bytes = BinaryTable.GetBytes(GetTestArray()); - Assert.That(bytes.Length, Is.EqualTo(BinaryTable.GetSize() * 2)); - bytes = BinaryTable.GetBytes(GetTestArray()); Assert.That(bytes.Length, Is.EqualTo(BinaryTable.GetSize() * 2)); - - bytes = BinaryTable.GetBytes(GetTestArray()); - Assert.That(bytes.Length, Is.EqualTo(BinaryTable.GetSize() * 2)); - - bytes = BinaryTable.GetBytes(GetTestArray()); - Assert.That(bytes.Length, Is.EqualTo(BinaryTable.GetSize() * 2)); - - bytes = BinaryTable.GetBytes(GetTestArray()); - Assert.That(bytes.Length, Is.EqualTo(BinaryTable.GetSize() * 2)); - - bytes = BinaryTable.GetBytes(GetTestArray()); - Assert.That(bytes.Length, Is.EqualTo(BinaryTable.GetSize() * 2)); - - bytes = BinaryTable.GetBytes(GetTestArray()); - Assert.That(bytes.Length, Is.EqualTo(BinaryTable.GetSize() * 2)); - - bytes = BinaryTable.GetBytes(GetTestArray()); - Assert.That(bytes.Length, Is.EqualTo(BinaryTable.GetSize() * 2)); - bytes = BinaryTable.GetBytes(new List() { "a", "b" }); Assert.That(bytes.Length, Is.EqualTo(2)); From 64c378290d5f22de01c6fd76185cb13b2fc3a53e Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Tue, 12 Dec 2023 22:51:56 +0100 Subject: [PATCH 11/57] add pointcloud test --- .../ExtStructuralMetadataTests.cs | 34 ++++++++ tests/SharpGLTF.Cesium.Tests/VertexBuilder.cs | 8 ++ .../VertexPointcloud.cs | 80 +++++++++++++++++++ 3 files changed, 122 insertions(+) create mode 100644 tests/SharpGLTF.Cesium.Tests/VertexPointcloud.cs diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index fbf90301..70264115 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -19,6 +19,40 @@ public void SetUp() CesiumExtensions.RegisterExtensions(); } + [Test(Description = "ext_structural_metadata with pointcloud and custom attributes")] + + public void CreatePointCloudWithCustomAttributesTest() + { + var material = new MaterialBuilder("material1").WithUnlitShader(); + var mesh = new MeshBuilder("mesh"); + var pointCloud = mesh.UsePrimitive(material, 1); + + for (var x = -10; x < 10; x++) + { + for (var y = -10; y < 10; y++) + { + for (var z = -10; z < 10; z++) + { + var vt0 = VertexBuilder.GetVertexPointcloud(new Vector3(x, y, z), 199, 4); + + pointCloud.AddPoint(vt0); + } + } + } + var model = ModelRoot.CreateModel(); + model.CreateMeshes(mesh); + + // create a scene, a node, and assign the first mesh (the terrain) + model.UseScene("Default") + .CreateNode().WithMesh(model.LogicalMeshes[0]); + + var ctx = new ValidationResult(model, ValidationMode.Strict, true); + model.AttachToCurrentTest("cesium_ext_structural_metadata_with_pointcloud_attributes.glb"); + model.AttachToCurrentTest("cesium_ext_structural_metadata_with_prointcloud_attributes.gltf"); + model.AttachToCurrentTest("cesium_ext_structural_metadata_with_prointcloud_attributes.plotly"); + } + + /// /// Sample see https://github.com/CesiumGS/3d-tiles-samples/blob/main/glTF/EXT_structural_metadata/PropertyAttributesPointCloud/PropertyAttributesPointCloudHouse.gltf /// diff --git a/tests/SharpGLTF.Cesium.Tests/VertexBuilder.cs b/tests/SharpGLTF.Cesium.Tests/VertexBuilder.cs index 28a17184..5a8c78f7 100644 --- a/tests/SharpGLTF.Cesium.Tests/VertexBuilder.cs +++ b/tests/SharpGLTF.Cesium.Tests/VertexBuilder.cs @@ -13,5 +13,13 @@ internal static VertexBuilder GetVertexPointcloud(Vector3 position, float intensity, float classification) + { + var vertexPointcloud = new VertexPointcloud(intensity, classification); + vertexPointcloud.SetColor(0, new Vector4(1, 0, 0, 0)); + var vp0 = new VertexPosition(position); + var vb0 = new VertexBuilder(vp0, vertexPointcloud); + return vb0; + } } } diff --git a/tests/SharpGLTF.Cesium.Tests/VertexPointcloud.cs b/tests/SharpGLTF.Cesium.Tests/VertexPointcloud.cs new file mode 100644 index 00000000..5519371c --- /dev/null +++ b/tests/SharpGLTF.Cesium.Tests/VertexPointcloud.cs @@ -0,0 +1,80 @@ +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 VertexPointcloud : IVertexCustom + { + public VertexPointcloud(float intensity, float classification) + { + Intensity = intensity; + Classification = classification; + } + + public const string INTENSITYATTRIBUTENAME = "_INTENSITY"; + public const string CLASSIFICATIONATTRIBUTENAME = "_CLASSIFICATION"; + + [VertexAttribute(INTENSITYATTRIBUTENAME, EncodingType.FLOAT, false)] + public float Intensity; + + [VertexAttribute(CLASSIFICATIONATTRIBUTENAME, EncodingType.FLOAT, false)] + public float Classification; + + 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) + { + throw new NotImplementedException(); + } + + public bool TryGetCustomAttribute(string attribute, out object value) + { + if (attribute == INTENSITYATTRIBUTENAME) { + value = Intensity; return true; + } + else if(attribute == CLASSIFICATIONATTRIBUTENAME) + { + value = Classification; return true; + } + else + { + value = null; return false; + } + } + + 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(); + } + } +} From d58067b1c1ae24e8f51a2090671e7eead7aaff81 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Wed, 13 Dec 2023 14:16:32 +0100 Subject: [PATCH 12/57] trying to add color --- .../ExtStructuralMetadataTests.cs | 8 ++++---- tests/SharpGLTF.Cesium.Tests/VertexBuilder.cs | 5 ++--- tests/SharpGLTF.Cesium.Tests/VertexPointcloud.cs | 10 ++++++++-- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index 70264115..b7af68c6 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -26,6 +26,7 @@ public void CreatePointCloudWithCustomAttributesTest() var material = new MaterialBuilder("material1").WithUnlitShader(); var mesh = new MeshBuilder("mesh"); var pointCloud = mesh.UsePrimitive(material, 1); + var greenColor = new Vector4(0f, 1f, 0f, 1f); for (var x = -10; x < 10; x++) { @@ -33,7 +34,7 @@ public void CreatePointCloudWithCustomAttributesTest() { for (var z = -10; z < 10; z++) { - var vt0 = VertexBuilder.GetVertexPointcloud(new Vector3(x, y, z), 199, 4); + var vt0 = VertexBuilder.GetVertexPointcloud(new Vector3(x, y, z), greenColor, 199, 4); pointCloud.AddPoint(vt0); } @@ -45,11 +46,10 @@ public void CreatePointCloudWithCustomAttributesTest() // create a scene, a node, and assign the first mesh (the terrain) model.UseScene("Default") .CreateNode().WithMesh(model.LogicalMeshes[0]); - var ctx = new ValidationResult(model, ValidationMode.Strict, true); model.AttachToCurrentTest("cesium_ext_structural_metadata_with_pointcloud_attributes.glb"); - model.AttachToCurrentTest("cesium_ext_structural_metadata_with_prointcloud_attributes.gltf"); - model.AttachToCurrentTest("cesium_ext_structural_metadata_with_prointcloud_attributes.plotly"); + model.AttachToCurrentTest("cesium_ext_structural_metadata_with_pointcloud_attributes.gltf"); + model.AttachToCurrentTest("cesium_ext_structural_metadata_with_pointcloud_attributes.plotly"); } diff --git a/tests/SharpGLTF.Cesium.Tests/VertexBuilder.cs b/tests/SharpGLTF.Cesium.Tests/VertexBuilder.cs index 5a8c78f7..715b5bf9 100644 --- a/tests/SharpGLTF.Cesium.Tests/VertexBuilder.cs +++ b/tests/SharpGLTF.Cesium.Tests/VertexBuilder.cs @@ -13,10 +13,9 @@ internal static VertexBuilder GetVertexPointcloud(Vector3 position, float intensity, float classification) + internal static VertexBuilder GetVertexPointcloud(Vector3 position, Vector4 color, float intensity, float classification) { - var vertexPointcloud = new VertexPointcloud(intensity, classification); - vertexPointcloud.SetColor(0, new Vector4(1, 0, 0, 0)); + var vertexPointcloud = new VertexPointcloud(color, intensity, classification); var vp0 = new VertexPosition(position); var vb0 = new VertexBuilder(vp0, vertexPointcloud); return vb0; diff --git a/tests/SharpGLTF.Cesium.Tests/VertexPointcloud.cs b/tests/SharpGLTF.Cesium.Tests/VertexPointcloud.cs index 5519371c..5ca13f86 100644 --- a/tests/SharpGLTF.Cesium.Tests/VertexPointcloud.cs +++ b/tests/SharpGLTF.Cesium.Tests/VertexPointcloud.cs @@ -10,8 +10,10 @@ namespace SharpGLTF [System.Diagnostics.DebuggerDisplay("𝐂:{Color} 𝐔𝐕:{TexCoord}")] public struct VertexPointcloud : IVertexCustom { - public VertexPointcloud(float intensity, float classification) + public VertexPointcloud(Vector4 color, float intensity, float classification) { + // Q: How to set the color?? + Color = color; Intensity = intensity; Classification = classification; } @@ -19,6 +21,9 @@ public VertexPointcloud(float intensity, float classification) public const string INTENSITYATTRIBUTENAME = "_INTENSITY"; public const string CLASSIFICATIONATTRIBUTENAME = "_CLASSIFICATION"; + [VertexAttribute("COLOR_0", EncodingType.UNSIGNED_BYTE, true)] + public Vector4 Color; + [VertexAttribute(INTENSITYATTRIBUTENAME, EncodingType.FLOAT, false)] public float Intensity; @@ -31,7 +36,8 @@ public VertexPointcloud(float intensity, float classification) public IEnumerable CustomAttributes => throw new NotImplementedException(); - public void SetColor(int setIndex, Vector4 color) { + void IVertexMaterial.SetColor(int setIndex, Vector4 color) { + if (setIndex == 0) this.Color = color; } public void SetTexCoord(int setIndex, Vector2 coord) { } From 1e3de303b4be23c8bd053b7d3e5eb8675652fde5 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Wed, 13 Dec 2023 14:55:42 +0100 Subject: [PATCH 13/57] implement setColor and MaxColors to 1 --- .../SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs | 6 +++--- tests/SharpGLTF.Cesium.Tests/VertexPointcloud.cs | 9 +++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index b7af68c6..df34d0e8 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -23,10 +23,10 @@ public void SetUp() public void CreatePointCloudWithCustomAttributesTest() { - var material = new MaterialBuilder("material1").WithUnlitShader(); + var material = new MaterialBuilder("material1"); var mesh = new MeshBuilder("mesh"); var pointCloud = mesh.UsePrimitive(material, 1); - var greenColor = new Vector4(0f, 1f, 0f, 1f); + var redColor = new Vector4(1f, 0f, 0f, 1f); for (var x = -10; x < 10; x++) { @@ -34,7 +34,7 @@ public void CreatePointCloudWithCustomAttributesTest() { for (var z = -10; z < 10; z++) { - var vt0 = VertexBuilder.GetVertexPointcloud(new Vector3(x, y, z), greenColor, 199, 4); + var vt0 = VertexBuilder.GetVertexPointcloud(new Vector3(x, y, z), redColor, 199, 4); pointCloud.AddPoint(vt0); } diff --git a/tests/SharpGLTF.Cesium.Tests/VertexPointcloud.cs b/tests/SharpGLTF.Cesium.Tests/VertexPointcloud.cs index 5ca13f86..50e827e7 100644 --- a/tests/SharpGLTF.Cesium.Tests/VertexPointcloud.cs +++ b/tests/SharpGLTF.Cesium.Tests/VertexPointcloud.cs @@ -12,7 +12,6 @@ public struct VertexPointcloud : IVertexCustom { public VertexPointcloud(Vector4 color, float intensity, float classification) { - // Q: How to set the color?? Color = color; Intensity = intensity; Classification = classification; @@ -30,19 +29,21 @@ public VertexPointcloud(Vector4 color, float intensity, float classification) [VertexAttribute(CLASSIFICATIONATTRIBUTENAME, EncodingType.FLOAT, false)] public float Classification; - public int MaxColors => 0; + public int MaxColors => 1; public int MaxTextCoords => 0; public IEnumerable CustomAttributes => throw new NotImplementedException(); void IVertexMaterial.SetColor(int setIndex, Vector4 color) { - if (setIndex == 0) this.Color = color; + if (setIndex == 0) Color = color; } public void SetTexCoord(int setIndex, Vector2 coord) { } - public Vector4 GetColor(int index) { throw new ArgumentOutOfRangeException(nameof(index)); } + public Vector4 GetColor(int index) { + return Color; + } public Vector2 GetTexCoord(int index) { throw new ArgumentOutOfRangeException(nameof(index)); } From 02780d7880ac6d2e75a7003632660c466c7fb125 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Wed, 13 Dec 2023 15:08:11 +0100 Subject: [PATCH 14/57] fix the pointcloud colors --- tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index df34d0e8..2aec1168 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -23,7 +23,7 @@ public void SetUp() public void CreatePointCloudWithCustomAttributesTest() { - var material = new MaterialBuilder("material1"); + var material = new MaterialBuilder("material1").WithUnlitShader(); var mesh = new MeshBuilder("mesh"); var pointCloud = mesh.UsePrimitive(material, 1); var redColor = new Vector4(1f, 0f, 0f, 1f); @@ -34,7 +34,8 @@ public void CreatePointCloudWithCustomAttributesTest() { for (var z = -10; z < 10; z++) { - var vt0 = VertexBuilder.GetVertexPointcloud(new Vector3(x, y, z), redColor, 199, 4); + // intensity values on x-axis, classification on z-axis + var vt0 = VertexBuilder.GetVertexPointcloud(new Vector3(x, y, z), redColor, x, z); pointCloud.AddPoint(vt0); } @@ -46,6 +47,7 @@ public void CreatePointCloudWithCustomAttributesTest() // create a scene, a node, and assign the first mesh (the terrain) model.UseScene("Default") .CreateNode().WithMesh(model.LogicalMeshes[0]); + var ctx = new ValidationResult(model, ValidationMode.Strict, true); model.AttachToCurrentTest("cesium_ext_structural_metadata_with_pointcloud_attributes.glb"); model.AttachToCurrentTest("cesium_ext_structural_metadata_with_pointcloud_attributes.gltf"); From 1691d5d7839c48af9db234ba648bdb3b705b4438 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Wed, 13 Dec 2023 17:20:36 +0100 Subject: [PATCH 15/57] get the pointcloud demo with custom attributes working --- .../Schema2/EXTStructuralMetaDataRoot.cs | 10 ++++++ .../ExtStructuralMetadataTests.cs | 32 ++++++------------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs index 064d22d4..6a151055 100644 --- a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs +++ b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs @@ -27,6 +27,10 @@ public static void SetPropertyAttribute( if (propertyAttribute == null) { modelRoot.RemoveExtensions(); return; } var ext = modelRoot.UseExtension(); + // Todo: Check if this is correct, it's a kinda workaround + // for the OneOf issue. https://github.com/CesiumGS/glTF/blob/proposal-EXT_structural_metadata/extensions/2.0/Vendor/EXT_structural_metadata/schema/glTF.EXT_structural_metadata.schema.json#L53 + // Here we use schemaUri but not schema + ext.SchemaUri = "MetadataSchema.json"; ext.PropertyAttributes.Clear(); ext.PropertyAttributes.Add(propertyAttribute); } @@ -96,6 +100,12 @@ internal List PropertyTables set { _propertyTables = value; } } + internal string SchemaUri + { + get { return _schemaUri; } + set { _schemaUri = value; } + } + internal List PropertyAttributes { get { return _propertyAttributes; } diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index 2aec1168..c8399ac5 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -5,6 +5,7 @@ using SharpGLTF.Scenes; using SharpGLTF.Schema2; using SharpGLTF.Validation; +using System; using System.Collections.Generic; using System.Numerics; @@ -20,6 +21,7 @@ public void SetUp() } [Test(Description = "ext_structural_metadata with pointcloud and custom attributes")] + /// Sample see https://github.com/CesiumGS/3d-tiles-samples/blob/main/glTF/EXT_structural_metadata/PropertyAttributesPointCloud/PropertyAttributesPointCloudHouse.gltf public void CreatePointCloudWithCustomAttributesTest() { @@ -27,15 +29,16 @@ public void CreatePointCloudWithCustomAttributesTest() var mesh = new MeshBuilder("mesh"); var pointCloud = mesh.UsePrimitive(material, 1); var redColor = new Vector4(1f, 0f, 0f, 1f); - + var rand = new Random(); for (var x = -10; x < 10; x++) { for (var y = -10; y < 10; y++) { for (var z = -10; z < 10; z++) { - // intensity values on x-axis, classification on z-axis - var vt0 = VertexBuilder.GetVertexPointcloud(new Vector3(x, y, z), redColor, x, z); + // intensity values is based on x-axis values + // classification of points is 0 or 1 (random) + var vt0 = VertexBuilder.GetVertexPointcloud(new Vector3(x, y, z), redColor, x, rand.Next(0,2)); pointCloud.AddPoint(vt0); } @@ -48,21 +51,6 @@ public void CreatePointCloudWithCustomAttributesTest() model.UseScene("Default") .CreateNode().WithMesh(model.LogicalMeshes[0]); - var ctx = new ValidationResult(model, ValidationMode.Strict, true); - model.AttachToCurrentTest("cesium_ext_structural_metadata_with_pointcloud_attributes.glb"); - model.AttachToCurrentTest("cesium_ext_structural_metadata_with_pointcloud_attributes.gltf"); - model.AttachToCurrentTest("cesium_ext_structural_metadata_with_pointcloud_attributes.plotly"); - } - - - /// - /// Sample see https://github.com/CesiumGS/3d-tiles-samples/blob/main/glTF/EXT_structural_metadata/PropertyAttributesPointCloud/PropertyAttributesPointCloudHouse.gltf - /// - [Test(Description = "ext_structural_metadata with PropertyAttributes")] - public void TriangleWithPropertyAttributes() - { - // todo: create a model with custom vertex attributes "_INTENSITY" and "_CLASSIFICATION" - var model = GetTriangleModel(); var propertyAttribute = new Schema2.PropertyAttribute(); propertyAttribute.Class = "exampleMetadataClass"; var intensityProperty = new PropertyAttributeProperty(); @@ -73,13 +61,13 @@ public void TriangleWithPropertyAttributes() propertyAttribute.Properties["classification"] = classificationProperty; model.SetPropertyAttribute(propertyAttribute); - var ctx = new ValidationResult(model, ValidationMode.Strict, true); - model.AttachToCurrentTest("cesium_ext_structural_metadata_with_property_attribute.glb"); - model.AttachToCurrentTest("cesium_ext_structural_metadata_with_property_attribute.gltf"); - model.AttachToCurrentTest("cesium_ext_structural_metadata_with_property_attribute.plotly"); + model.AttachToCurrentTest("cesium_ext_structural_metadata_with_pointcloud_attributes.glb"); + model.AttachToCurrentTest("cesium_ext_structural_metadata_with_pointcloud_attributes.gltf"); + model.AttachToCurrentTest("cesium_ext_structural_metadata_with_pointcloud_attributes.plotly"); } + [Test(Description ="First test with ext_structural_metadata")] public void TriangleWithMetadataTest() { From 9f865490f5f6e4b9f2b2d76582ec74275fee363a Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Wed, 13 Dec 2023 20:23:41 +0100 Subject: [PATCH 16/57] use schema or schemaUri --- .../Schema2/EXTStructuralMetaDataRoot.cs | 51 ++++++++++++------- .../ExtStructuralMetadataTests.cs | 12 ++--- 2 files changed, 40 insertions(+), 23 deletions(-) diff --git a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs index 6a151055..9c321a7e 100644 --- a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs +++ b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs @@ -1,4 +1,5 @@ using SharpGLTF.Validation; +using System; using System.Collections.Generic; using System.Linq; @@ -9,52 +10,57 @@ public static class ExtStructuralMetadata // sample see https://github.com/CesiumGS/3d-tiles-samples/blob/main/glTF/EXT_structural_metadata/SimplePropertyTexture/SimplePropertyTexture.gltf public static void SetPropertyTexture( this ModelRoot modelRoot, - StructuralMetadataSchema schema, - PropertyTexture propertyTexture) + PropertyTexture propertyTexture, + StructuralMetadataSchema schema = null, + Uri schemaUri = null + ) { - if (schema == null || propertyTexture == null) { modelRoot.RemoveExtensions(); return; } + if (propertyTexture == null) { modelRoot.RemoveExtensions(); return; } var ext = modelRoot.UseExtension(); - ext.Schema = schema; ext.PropertyTextures.Clear(); ext.PropertyTextures.Add(propertyTexture); + ext.AddSchema(schema, schemaUri); } public static void SetPropertyAttribute( this ModelRoot modelRoot, - PropertyAttribute propertyAttribute) + PropertyAttribute propertyAttribute, + StructuralMetadataSchema schema = null, + Uri schemaUri = null) { if (propertyAttribute == null) { modelRoot.RemoveExtensions(); return; } var ext = modelRoot.UseExtension(); - // Todo: Check if this is correct, it's a kinda workaround - // for the OneOf issue. https://github.com/CesiumGS/glTF/blob/proposal-EXT_structural_metadata/extensions/2.0/Vendor/EXT_structural_metadata/schema/glTF.EXT_structural_metadata.schema.json#L53 - // Here we use schemaUri but not schema - ext.SchemaUri = "MetadataSchema.json"; ext.PropertyAttributes.Clear(); ext.PropertyAttributes.Add(propertyAttribute); + ext.AddSchema(schema, schemaUri); } public static void SetPropertyTable( this ModelRoot modelRoot, - StructuralMetadataSchema schema, - Dictionary> attributes + Dictionary> attributes, + string name = "PropertyTable", + StructuralMetadataSchema schema = null, + Uri schemaUri = null ) { - if (schema == null || attributes == null) { modelRoot.RemoveExtensions(); return; } + if (attributes == null) { modelRoot.RemoveExtensions(); return; } var ext = modelRoot.UseExtension(); - ext.Schema = schema; ext.PropertyTables.Clear(); - ext.PropertyTables.Add(GetPropertyTable(modelRoot, schema, attributes)); + ext.PropertyTables.Add(GetPropertyTable(modelRoot, attributes, name)); + ext.AddSchema(schema, schemaUri); } private static PropertyTable GetPropertyTable( ModelRoot modelRoot, - StructuralMetadataSchema schema, Dictionary> attributes, - string name = "PropertyTable") + string name = "PropertyTable", + StructuralMetadataSchema schema = null + // todo: use? Uri schemaUri = null + ) { var propertyTable = new PropertyTable(name, attributes.FirstOrDefault().Value.Count); @@ -79,7 +85,6 @@ private static PropertyTable GetPropertyTable( return propertyTable; } - } public partial class EXTStructuralMetaDataRoot @@ -94,6 +99,18 @@ internal EXTStructuralMetaDataRoot(ModelRoot modelRoot) _propertyTextures = new List(); } + internal void AddSchema(StructuralMetadataSchema schema, Uri schemaUri) + { + if (schema != null) + { + Schema = schema; + } + if (schemaUri != null) + { + SchemaUri = schemaUri.ToString(); + } + } + internal List PropertyTables { get { return _propertyTables; } diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index c8399ac5..9a2e866d 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -38,7 +38,7 @@ public void CreatePointCloudWithCustomAttributesTest() { // intensity values is based on x-axis values // classification of points is 0 or 1 (random) - var vt0 = VertexBuilder.GetVertexPointcloud(new Vector3(x, y, z), redColor, x, rand.Next(0,2)); + var vt0 = VertexBuilder.GetVertexPointcloud(new Vector3(x, y, z), redColor, x, rand.Next(0, 2)); pointCloud.AddPoint(vt0); } @@ -60,7 +60,7 @@ public void CreatePointCloudWithCustomAttributesTest() propertyAttribute.Properties["intensity"] = intensityProperty; propertyAttribute.Properties["classification"] = classificationProperty; - model.SetPropertyAttribute(propertyAttribute); + model.SetPropertyAttribute(propertyAttribute, schemaUri: new Uri("MetadataSchema.json", UriKind.Relative)); var ctx = new ValidationResult(model, ValidationMode.Strict, true); model.AttachToCurrentTest("cesium_ext_structural_metadata_with_pointcloud_attributes.glb"); model.AttachToCurrentTest("cesium_ext_structural_metadata_with_pointcloud_attributes.gltf"); @@ -68,18 +68,18 @@ public void CreatePointCloudWithCustomAttributesTest() } - [Test(Description ="First test with ext_structural_metadata")] + [Test(Description = "First test with ext_structural_metadata")] public void TriangleWithMetadataTest() { - var model = GetTriangleModel(); + var model = GetTriangleModel(); var schema = GetSampleSchema(); var attribute = new List() { 100 }; var dict = new Dictionary>(); dict["age"] = attribute; - model.SetPropertyTable(schema, dict); - + model.SetPropertyTable(dict, schema: schema); + // create files var ctx = new ValidationResult(model, ValidationMode.Strict, true); model.AttachToCurrentTest("cesium_ext_structural_metadata_basic_triangle.glb"); From 98b56c49559f27417511bb1a762f307e93937e26 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Wed, 13 Dec 2023 20:56:38 +0100 Subject: [PATCH 17/57] start multiple classes test --- .../Schema2/EXTStructuralMetaDataRoot.cs | 9 ++- .../ExtStructuralMetadataTests.cs | 30 ++++++- tests/SharpGLTF.Cesium.Tests/VertexBuilder.cs | 9 +++ .../VertexWithFeatureIds.cs | 81 +++++++++++++++++++ 4 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 tests/SharpGLTF.Cesium.Tests/VertexWithFeatureIds.cs diff --git a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs index 9c321a7e..1d718e77 100644 --- a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs +++ b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs @@ -49,7 +49,7 @@ public static void SetPropertyTable( var ext = modelRoot.UseExtension(); ext.PropertyTables.Clear(); - ext.PropertyTables.Add(GetPropertyTable(modelRoot, attributes, name)); + ext.PropertyTables.Add(GetPropertyTable(modelRoot, attributes, name, schema)); ext.AddSchema(schema, schemaUri); } @@ -141,8 +141,13 @@ internal List PropertyTextures set { _propertyTextures = value; } } - protected override void OnValidateContent(ValidationContext validate) + protected override void OnValidateContent(ValidationContext result) { + // Schema or SchemaUri must be defined + Guard.IsTrue(Schema != null || SchemaUri != null, "Schema/SchemaUri", "Schema or SchemaUri must be defined"); + + base.OnValidateContent(result); + } } diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index 9a2e866d..818dd08b 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -20,8 +20,36 @@ public void SetUp() CesiumExtensions.RegisterExtensions(); } + [Test(Description = "ext_structural_metadata with multiple classes")] + // Sample see https://github.com/CesiumGS/3d-tiles-samples/blob/main/glTF/EXT_structural_metadata/MultipleClasses/MultipleClasses.gltf + public void MultipleClassesTest() + { + 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 = VertexBuilder.GetVertexWithFeatureIds(new Vector3(-10, 0, 0), new Vector3(0, 0, 1), 0, 100); + var vt1 = VertexBuilder.GetVertexWithFeatureIds(new Vector3(10, 0, 0), new Vector3(0, 0, 1), 0, 100); + var vt2 = VertexBuilder.GetVertexWithFeatureIds(new Vector3(0, 10, 0), new Vector3(0, 0, 1), 0, 100); + + prim.AddTriangle(vt0, vt1, vt2); + var scene = new SceneBuilder(); + scene.AddRigidMesh(mesh, Matrix4x4.Identity); + var model = scene.ToGltf2(); + + // todo: add metadata + + var ctx = new ValidationResult(model, ValidationMode.Strict, true); + model.AttachToCurrentTest("cesium_ext_structural_metadata_multiple_classes.glb"); + model.AttachToCurrentTest("cesium_ext_structural_metadata_multiple_classes.gltf"); + model.AttachToCurrentTest("cesium_ext_structural_metadata_multiple_classes.plotly"); + } + + [Test(Description = "ext_structural_metadata with pointcloud and custom attributes")] - /// Sample see https://github.com/CesiumGS/3d-tiles-samples/blob/main/glTF/EXT_structural_metadata/PropertyAttributesPointCloud/PropertyAttributesPointCloudHouse.gltf + // Sample see https://github.com/CesiumGS/3d-tiles-samples/blob/main/glTF/EXT_structural_metadata/PropertyAttributesPointCloud/PropertyAttributesPointCloudHouse.gltf public void CreatePointCloudWithCustomAttributesTest() { diff --git a/tests/SharpGLTF.Cesium.Tests/VertexBuilder.cs b/tests/SharpGLTF.Cesium.Tests/VertexBuilder.cs index 715b5bf9..cdf1e743 100644 --- a/tests/SharpGLTF.Cesium.Tests/VertexBuilder.cs +++ b/tests/SharpGLTF.Cesium.Tests/VertexBuilder.cs @@ -6,6 +6,15 @@ namespace SharpGLTF { public static class VertexBuilder { + internal static VertexBuilder GetVertexWithFeatureIds(Vector3 position, Vector3 normal, int featureId0, int featureId1) + { + var vertexWithFeatureIds = new VertexWithFeatureIds(featureId0, featureId1); + + var vp0 = new VertexPositionNormal(position, normal); + var vb0 = new VertexBuilder(vp0, vertexWithFeatureIds); + return vb0; + } + internal static VertexBuilder GetVertexWithFeatureId(Vector3 position, Vector3 normal, int featureid) { var vp0 = new VertexPositionNormal(position, normal); diff --git a/tests/SharpGLTF.Cesium.Tests/VertexWithFeatureIds.cs b/tests/SharpGLTF.Cesium.Tests/VertexWithFeatureIds.cs new file mode 100644 index 00000000..a7c5712f --- /dev/null +++ b/tests/SharpGLTF.Cesium.Tests/VertexWithFeatureIds.cs @@ -0,0 +1,81 @@ +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 VertexWithFeatureIds : IVertexCustom + { + public VertexWithFeatureIds(float featureId0, float featureId1) + { + FeatureId0 = featureId0; + FeatureId1 = featureId1; + } + + public const string FEATUREID0ATTRIBUTENAME = "_FEATURE_ID_0"; + public const string FEATUREID1ATTRIBUTENAME = "_FEATURE_ID_1"; + + [VertexAttribute(FEATUREID0ATTRIBUTENAME, EncodingType.FLOAT, false)] + public float FeatureId0; + + [VertexAttribute(FEATUREID1ATTRIBUTENAME, EncodingType.FLOAT, false)] + public float FeatureId1; + + 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) + { + throw new NotImplementedException(); + } + + public bool TryGetCustomAttribute(string attribute, out object value) + { + if (attribute == FEATUREID0ATTRIBUTENAME) + { + value = FeatureId0; return true; + } + else if (attribute == FEATUREID1ATTRIBUTENAME) + { + value = FeatureId1; return true; + } + else + { + value = null; return false; + } + + } + + 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(); + } + } +} From 06b5950d4dbc8f301c8afc76d02e40e561175e3a Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Wed, 13 Dec 2023 21:09:38 +0100 Subject: [PATCH 18/57] change null check --- .../Schema2/EXTStructuralMetaDataRoot.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs index 1d718e77..3e26fe0b 100644 --- a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs +++ b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs @@ -101,14 +101,8 @@ internal EXTStructuralMetaDataRoot(ModelRoot modelRoot) internal void AddSchema(StructuralMetadataSchema schema, Uri schemaUri) { - if (schema != null) - { - Schema = schema; - } - if (schemaUri != null) - { - SchemaUri = schemaUri.ToString(); - } + Schema ??= schema; + SchemaUri ??= schemaUri.ToString(); } internal List PropertyTables From 1702dd7f6f3e4d7ab62266291676a61744726ba0 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Wed, 13 Dec 2023 23:12:52 +0100 Subject: [PATCH 19/57] add more to MultipleClasses test --- .../Schema2/EXTStructuralMetaDataRoot.cs | 17 +++- .../ExtStructuralMetadataTests.cs | 79 +++++++++++++++++++ 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs index 3e26fe0b..56b97c45 100644 --- a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs +++ b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs @@ -101,8 +101,15 @@ internal EXTStructuralMetaDataRoot(ModelRoot modelRoot) internal void AddSchema(StructuralMetadataSchema schema, Uri schemaUri) { - Schema ??= schema; - SchemaUri ??= schemaUri.ToString(); + if (schema != null) + { + Schema = schema; + } + + if (schemaUri != null) + { + SchemaUri = schemaUri.ToString(); + } } internal List PropertyTables @@ -314,6 +321,12 @@ public string Description public partial class ClassProperty { + public string Name + { + get { return _name; } + set { _name = value; } + } + public string Description { get { return _description; } diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index 818dd08b..0d7e27ce 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -39,7 +39,38 @@ public void MultipleClassesTest() scene.AddRigidMesh(mesh, Matrix4x4.Identity); var model = scene.ToGltf2(); + // FeatureID 0: featureCount=1, attribute=0, porpertyTable=0 + var featureId0Attribute = new MeshExtMeshFeatureID(1, 0, 0); + // FeatureID 1: featureCount=1, attribute=1, porpertyTable=1 + var featureId1Attribute = new MeshExtMeshFeatureID(1, 1, 1); + + // Set the FeatureIds + var featureIds = new List() { featureId0Attribute, featureId1Attribute }; + model.LogicalMeshes[0].Primitives[0].SetFeatureIds(featureIds); + + var schema = new StructuralMetadataSchema(); + schema.Id = "MultipleClassesSchema"; + + var classes = new Dictionary(); + classes["exampleMetadataClassA"] = GetExampleClassA(); + classes["exampleMetadataClassB"] = GetExampleClassB(); + + schema.Classes = classes; + var exampleEnum = new StructuralMetadataEnum(); + exampleEnum.Values.Add(new EnumValue() { Name= "ExampleEnumValueA", Value = 0 }); + exampleEnum.Values.Add(new EnumValue() { Name = "ExampleEnumValueB", Value = 1 }); + exampleEnum.Values.Add(new EnumValue() { Name = "ExampleEnumValueC", Value = 2 }); + + schema.Enums.Add("exampleEnumType", exampleEnum); + + // Todo: create the property tables (First example property table, Second example property table) + + + // model.SetPropertyAttribute(propertyAttribute, schema); + + // todo: add metadata + model.SaveGLTF(@"d:\aaa\bert111.gltf"); var ctx = new ValidationResult(model, ValidationMode.Strict, true); model.AttachToCurrentTest("cesium_ext_structural_metadata_multiple_classes.glb"); @@ -47,6 +78,54 @@ public void MultipleClassesTest() model.AttachToCurrentTest("cesium_ext_structural_metadata_multiple_classes.plotly"); } + private static StructuralMetadataClass GetExampleClassB() + { + var classB = new StructuralMetadataClass(); + classB.Name = "Example metadata class B"; + classB.Description = "Second example metadata class"; + + var uint16Property = new ClassProperty(); + uint16Property.Name = "Example UINT16 property"; + uint16Property.Description = "An example property, with component type UINT16"; + uint16Property.Type = ElementType.SCALAR; + uint16Property.ComponentType = DataType.UINT16; + + classB.Properties.Add("example_UINT16", uint16Property); + + var float64Property = new ClassProperty(); + float64Property.Name = "Example FLOAT64 property"; + float64Property.Description = "An example property, with component type FLOAT64"; + float64Property.Type = ElementType.SCALAR; + float64Property.ComponentType = DataType.FLOAT64; + + classB.Properties.Add("example_FLOAT64", float64Property); + return classB; + } + + + private static StructuralMetadataClass GetExampleClassA() + { + var classA = new StructuralMetadataClass(); + classA.Name = "Example metadata class A"; + classA.Description = "First example metadata class"; + + var float32Property = new ClassProperty(); + float32Property.Name = "Example FLOAT32 property"; + float32Property.Description = "An example property, with component type FLOAT32"; + float32Property.Type = ElementType.SCALAR; + float32Property.ComponentType = DataType.FLOAT32; + + classA.Properties.Add("example_FLOAT32", float32Property); + + var int64Property = new ClassProperty(); + int64Property.Name = "Example INT64 property"; + int64Property.Description = "An example property, with component type INT64"; + int64Property.Type = ElementType.SCALAR; + int64Property.ComponentType = DataType.INT64; + + classA.Properties.Add("example_INT64", int64Property); + return classA; + } [Test(Description = "ext_structural_metadata with pointcloud and custom attributes")] // Sample see https://github.com/CesiumGS/3d-tiles-samples/blob/main/glTF/EXT_structural_metadata/PropertyAttributesPointCloud/PropertyAttributesPointCloudHouse.gltf From efb9534da55e1a060ed6e17b2fafa16b347ff37c Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Wed, 13 Dec 2023 23:17:55 +0100 Subject: [PATCH 20/57] fix the test --- tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index 0d7e27ce..4791982c 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -70,8 +70,6 @@ public void MultipleClassesTest() // todo: add metadata - model.SaveGLTF(@"d:\aaa\bert111.gltf"); - var ctx = new ValidationResult(model, ValidationMode.Strict, true); model.AttachToCurrentTest("cesium_ext_structural_metadata_multiple_classes.glb"); model.AttachToCurrentTest("cesium_ext_structural_metadata_multiple_classes.gltf"); From f62c3efd7a136e12c373a1fe2998a49fdbd012ea Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Thu, 14 Dec 2023 09:50:03 +0100 Subject: [PATCH 21/57] using OneOf --- .../Schema2/EXTStructuralMetaDataRoot.cs | 47 +++++++++---------- src/SharpGLTF.Cesium/SharpGLTF.Cesium.csproj | 4 ++ .../ExtStructuralMetadataTests.cs | 3 +- 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs index 56b97c45..ebb3ad65 100644 --- a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs +++ b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs @@ -1,4 +1,5 @@ -using SharpGLTF.Validation; +using OneOf; +using SharpGLTF.Validation; using System; using System.Collections.Generic; using System.Linq; @@ -11,8 +12,7 @@ public static class ExtStructuralMetadata public static void SetPropertyTexture( this ModelRoot modelRoot, PropertyTexture propertyTexture, - StructuralMetadataSchema schema = null, - Uri schemaUri = null + OneOf schema ) { if (propertyTexture == null) { modelRoot.RemoveExtensions(); return; } @@ -20,50 +20,52 @@ public static void SetPropertyTexture( var ext = modelRoot.UseExtension(); ext.PropertyTextures.Clear(); ext.PropertyTextures.Add(propertyTexture); - ext.AddSchema(schema, schemaUri); + ext.AddSchema(schema); } public static void SetPropertyAttribute( this ModelRoot modelRoot, PropertyAttribute propertyAttribute, - StructuralMetadataSchema schema = null, - Uri schemaUri = null) + OneOf schema) { if (propertyAttribute == null) { modelRoot.RemoveExtensions(); return; } var ext = modelRoot.UseExtension(); ext.PropertyAttributes.Clear(); ext.PropertyAttributes.Add(propertyAttribute); - ext.AddSchema(schema, schemaUri); + ext.AddSchema(schema); } public static void SetPropertyTable( this ModelRoot modelRoot, Dictionary> attributes, - string name = "PropertyTable", - StructuralMetadataSchema schema = null, - Uri schemaUri = null + OneOf schema, + string name = "PropertyTable" ) { if (attributes == null) { modelRoot.RemoveExtensions(); return; } var ext = modelRoot.UseExtension(); ext.PropertyTables.Clear(); - ext.PropertyTables.Add(GetPropertyTable(modelRoot, attributes, name, schema)); - ext.AddSchema(schema, schemaUri); + var propertyTable = GetPropertyTable(modelRoot, attributes, schema, name); + ext.PropertyTables.Add(propertyTable); + ext.AddSchema(schema); } private static PropertyTable GetPropertyTable( ModelRoot modelRoot, Dictionary> attributes, - string name = "PropertyTable", - StructuralMetadataSchema schema = null - // todo: use? Uri schemaUri = null + OneOf schema1, + string name = "PropertyTable" ) { var propertyTable = new PropertyTable(name, attributes.FirstOrDefault().Value.Count); + // todo: what if schema is Uri? + + var structuralMetadataSchema = schema1.TryPickT0(out var schema, out var remainder); + var firstClass = schema.Classes.FirstOrDefault().Value; foreach (var property in firstClass.Properties) @@ -99,17 +101,12 @@ internal EXTStructuralMetaDataRoot(ModelRoot modelRoot) _propertyTextures = new List(); } - internal void AddSchema(StructuralMetadataSchema schema, Uri schemaUri) + internal void AddSchema(OneOf schema) { - if (schema != null) - { - Schema = schema; - } - - if (schemaUri != null) - { - SchemaUri = schemaUri.ToString(); - } + schema.Switch( + StructuralMetadataSchema => _schema = StructuralMetadataSchema, + Uri => this.SchemaUri = Uri.ToString() + ); } internal List PropertyTables diff --git a/src/SharpGLTF.Cesium/SharpGLTF.Cesium.csproj b/src/SharpGLTF.Cesium/SharpGLTF.Cesium.csproj index 88dfa0f5..7101463d 100644 --- a/src/SharpGLTF.Cesium/SharpGLTF.Cesium.csproj +++ b/src/SharpGLTF.Cesium/SharpGLTF.Cesium.csproj @@ -20,4 +20,8 @@ + + + + diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index 4791982c..980d66f9 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -165,7 +165,8 @@ public void CreatePointCloudWithCustomAttributesTest() propertyAttribute.Properties["intensity"] = intensityProperty; propertyAttribute.Properties["classification"] = classificationProperty; - model.SetPropertyAttribute(propertyAttribute, schemaUri: new Uri("MetadataSchema.json", UriKind.Relative)); + var schemaUri = new Uri("MetadataSchema.json", UriKind.Relative); + model.SetPropertyAttribute(propertyAttribute, schemaUri ); var ctx = new ValidationResult(model, ValidationMode.Strict, true); model.AttachToCurrentTest("cesium_ext_structural_metadata_with_pointcloud_attributes.glb"); model.AttachToCurrentTest("cesium_ext_structural_metadata_with_pointcloud_attributes.gltf"); From dbbf9897d666c2a24da16f30a51d1697581205b5 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Thu, 14 Dec 2023 09:59:06 +0100 Subject: [PATCH 22/57] improve check one of schema versus schemaUri --- src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs index ebb3ad65..201e310c 100644 --- a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs +++ b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs @@ -141,11 +141,11 @@ internal List PropertyTextures protected override void OnValidateContent(ValidationContext result) { - // Schema or SchemaUri must be defined - Guard.IsTrue(Schema != null || SchemaUri != null, "Schema/SchemaUri", "Schema or SchemaUri must be defined"); + // Check one of schema or schemaUri is defined, but not both + Guard.IsFalse(Schema != null && SchemaUri != null, "Schema/SchemaUri", "Schema and SchemaUri cannot both be defined"); + Guard.IsFalse(Schema == null && SchemaUri == null, "Schema/SchemaUri", "One of Schema and SchemaUri must be defined"); base.OnValidateContent(result); - } } From 0d4d8ddf543d1042ff310d842c2c56fb8b08f4e2 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Thu, 14 Dec 2023 12:44:35 +0100 Subject: [PATCH 23/57] refactor propertyTables --- .../Schema2/EXTStructuralMetaDataRoot.cs | 73 +++++++++---------- .../ExtStructuralMetadataTests.cs | 72 +++++++++--------- 2 files changed, 73 insertions(+), 72 deletions(-) diff --git a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs index 201e310c..43cc09d7 100644 --- a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs +++ b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs @@ -38,55 +38,43 @@ public static void SetPropertyAttribute( public static void SetPropertyTable( this ModelRoot modelRoot, - Dictionary> attributes, - OneOf schema, - string name = "PropertyTable" - ) + PropertyTable propertyTable, + OneOf schema) { - if (attributes == null) { modelRoot.RemoveExtensions(); return; } + if (propertyTable == null) { modelRoot.RemoveExtensions(); return; } + SetPropertyTables(modelRoot, new List() { propertyTable }, schema); + } + + public static void SetPropertyTables( + this ModelRoot modelRoot, + List propertyTables, + OneOf schema) + { + if (propertyTables == null) { modelRoot.RemoveExtensions(); return; } var ext = modelRoot.UseExtension(); - ext.PropertyTables.Clear(); - var propertyTable = GetPropertyTable(modelRoot, attributes, schema, name); - ext.PropertyTables.Add(propertyTable); + ext.PropertyTables = propertyTables; ext.AddSchema(schema); } - - private static PropertyTable GetPropertyTable( - ModelRoot modelRoot, - Dictionary> attributes, - OneOf schema1, - string name = "PropertyTable" - ) + public static PropertyTableProperty GetPropertyTableProperty(this ModelRoot model, IReadOnlyList values) { - var propertyTable = new PropertyTable(name, attributes.FirstOrDefault().Value.Count); - - // todo: what if schema is Uri? - - var structuralMetadataSchema = schema1.TryPickT0(out var schema, out var remainder); - - var firstClass = schema.Classes.FirstOrDefault().Value; + var propertyTableProperty = new PropertyTableProperty(); + int logicalIndex = GetBufferView(model, values); + propertyTableProperty.Values = logicalIndex; + return propertyTableProperty; + } - foreach (var property in firstClass.Properties) - { - var id = property.Key; - var type = property.Value.Type; + private static int GetBufferView(this ModelRoot model, IReadOnlyList values) + { + var bytesFloat32 = BinaryTable.GetBytes(values); + var bufferView = model.UseBufferView(bytesFloat32); + int logicalIndex = bufferView.LogicalIndex; + return logicalIndex; + } - // Todo check type, for example string - var attribute = attributes[id]; - var list = attribute.ConvertAll(x => (int)x); - byte[] bytes = BinaryTable.GetBytes(list); - var bufferView = modelRoot.UseBufferView(bytes); - int logicalIndex = bufferView.LogicalIndex; - var propertyTableProperty = new PropertyTableProperty(); - propertyTableProperty.Values = logicalIndex; - propertyTable.Properties[id] = propertyTableProperty; - } - return propertyTable; - } } public partial class EXTStructuralMetaDataRoot @@ -367,10 +355,17 @@ public PropertyTable() { _properties = new Dictionary(); } - public PropertyTable(string Class, int Count) : this() + public PropertyTable(string Class, int Count, string Name = "") : this() { _class = Class; _count = Count; + _name = Name; + } + + public string Name + { + get { return _name; } + set { _name = value; } } public string Class diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index 980d66f9..5f5a4859 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -57,25 +57,44 @@ public void MultipleClassesTest() schema.Classes = classes; var exampleEnum = new StructuralMetadataEnum(); - exampleEnum.Values.Add(new EnumValue() { Name= "ExampleEnumValueA", Value = 0 }); + exampleEnum.Values.Add(new EnumValue() { Name = "ExampleEnumValueA", Value = 0 }); exampleEnum.Values.Add(new EnumValue() { Name = "ExampleEnumValueB", Value = 1 }); exampleEnum.Values.Add(new EnumValue() { Name = "ExampleEnumValueC", Value = 2 }); schema.Enums.Add("exampleEnumType", exampleEnum); - // Todo: create the property tables (First example property table, Second example property table) + var firstPropertyTable = GetFirstPropertyTable(model); + var secondPropertyTable = GetSecondPropertyTable(model); - - // model.SetPropertyAttribute(propertyAttribute, schema); - - - // todo: add metadata + var propertyTables = new List() { firstPropertyTable, secondPropertyTable }; + model.SetPropertyTables(propertyTables, schema); + model.SaveGLTF(@"d:\aaa\bertclass.gltf"); var ctx = new ValidationResult(model, ValidationMode.Strict, true); model.AttachToCurrentTest("cesium_ext_structural_metadata_multiple_classes.glb"); model.AttachToCurrentTest("cesium_ext_structural_metadata_multiple_classes.gltf"); model.AttachToCurrentTest("cesium_ext_structural_metadata_multiple_classes.plotly"); } + private static PropertyTable GetFirstPropertyTable(ModelRoot model) + { + var firstPropertyTable = new PropertyTable("exampleMetadataClassA", 1, "First example property table"); + var float32Property = model.GetPropertyTableProperty(new List() { 100 }); + firstPropertyTable.Properties.Add("example_FLOAT32", float32Property); + var int64Property = model.GetPropertyTableProperty(new List() { 101 }); + firstPropertyTable.Properties.Add("example_INT64", int64Property); + return firstPropertyTable; + } + + private static PropertyTable GetSecondPropertyTable(ModelRoot model) + { + var secondPropertyTable = new PropertyTable("exampleMetadataClassB", 1, "First example property table"); + var uint16Property = model.GetPropertyTableProperty(new List() { 102 }); + secondPropertyTable.Properties.Add("example_UINT16", uint16Property); + var float64Property = model.GetPropertyTableProperty(new List() { 103 }); + secondPropertyTable.Properties.Add("example_FLOAT64", float64Property); + return secondPropertyTable; + } + private static StructuralMetadataClass GetExampleClassB() { var classB = new StructuralMetadataClass(); @@ -176,29 +195,9 @@ public void CreatePointCloudWithCustomAttributesTest() [Test(Description = "First test with ext_structural_metadata")] public void TriangleWithMetadataTest() - { - var model = GetTriangleModel(); - - var schema = GetSampleSchema(); - - var attribute = new List() { 100 }; - var dict = new Dictionary>(); - dict["age"] = attribute; - model.SetPropertyTable(dict, schema: schema); - - // create files - var ctx = new ValidationResult(model, ValidationMode.Strict, true); - model.AttachToCurrentTest("cesium_ext_structural_metadata_basic_triangle.glb"); - model.AttachToCurrentTest("cesium_ext_structural_metadata_basic_triangle.gltf"); - model.AttachToCurrentTest("cesium_ext_structural_metadata_basic_triangle.plotly"); - } - - private static ModelRoot GetTriangleModel() { TestContext.CurrentContext.AttachGltfValidatorLinks(); - var material = MaterialBuilder.CreateDefault().WithDoubleSide(true); - var mesh = new MeshBuilder("mesh"); var prim = mesh.UsePrimitive(material); @@ -207,11 +206,7 @@ private static ModelRoot GetTriangleModel() var scene = new SceneBuilder(); scene.AddRigidMesh(mesh, Matrix4x4.Identity); var model = scene.ToGltf2(); - return model; - } - - private static StructuralMetadataSchema GetSampleSchema() - { + var schema = new StructuralMetadataSchema(); schema.Id = "schema_001"; schema.Name = "schema 001"; @@ -231,7 +226,18 @@ private static StructuralMetadataSchema GetSampleSchema() treeClass.Properties.Add("age", ageProperty); schema.Classes = classes; - return schema; + + var propertyTable = new PropertyTable("tree", 1, "PropertyTable"); + var agePropertyTableProperty = model.GetPropertyTableProperty(new List() { 100 }); + propertyTable.Properties.Add("age", agePropertyTableProperty); + + model.SetPropertyTable(propertyTable, schema); + + // create files + var ctx = new ValidationResult(model, ValidationMode.Strict, true); + model.AttachToCurrentTest("cesium_ext_structural_metadata_basic_triangle.glb"); + model.AttachToCurrentTest("cesium_ext_structural_metadata_basic_triangle.gltf"); + model.AttachToCurrentTest("cesium_ext_structural_metadata_basic_triangle.plotly"); } } } From 14958a62c84cef3aa79d688a2fc8f8d6f5e8565b Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Thu, 14 Dec 2023 12:44:56 +0100 Subject: [PATCH 24/57] fix tests --- tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index 5f5a4859..a09cd93e 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -68,7 +68,6 @@ public void MultipleClassesTest() var propertyTables = new List() { firstPropertyTable, secondPropertyTable }; model.SetPropertyTables(propertyTables, schema); - model.SaveGLTF(@"d:\aaa\bertclass.gltf"); var ctx = new ValidationResult(model, ValidationMode.Strict, true); model.AttachToCurrentTest("cesium_ext_structural_metadata_multiple_classes.glb"); model.AttachToCurrentTest("cesium_ext_structural_metadata_multiple_classes.gltf"); From dc2ea46a9050b5bd63943843dbd2eb2643f8b626 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Thu, 14 Dec 2023 14:44:53 +0100 Subject: [PATCH 25/57] finsih MultipleTables test --- src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs | 3 +-- .../SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs | 8 +++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs index 43cc09d7..b54ddcac 100644 --- a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs +++ b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs @@ -41,7 +41,6 @@ public static void SetPropertyTable( PropertyTable propertyTable, OneOf schema) { - if (propertyTable == null) { modelRoot.RemoveExtensions(); return; } SetPropertyTables(modelRoot, new List() { propertyTable }, schema); } @@ -50,7 +49,7 @@ public static void SetPropertyTables( List propertyTables, OneOf schema) { - if (propertyTables == null) { modelRoot.RemoveExtensions(); return; } + if (propertyTables == null || propertyTables.Count == 0) { modelRoot.RemoveExtensions(); return; } var ext = modelRoot.UseExtension(); ext.PropertyTables = propertyTables; diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index a09cd93e..42aa2a7a 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -30,9 +30,9 @@ public void MultipleClassesTest() var prim = mesh.UsePrimitive(material); // All the vertices in the triangle have the same feature ID - var vt0 = VertexBuilder.GetVertexWithFeatureIds(new Vector3(-10, 0, 0), new Vector3(0, 0, 1), 0, 100); - var vt1 = VertexBuilder.GetVertexWithFeatureIds(new Vector3(10, 0, 0), new Vector3(0, 0, 1), 0, 100); - var vt2 = VertexBuilder.GetVertexWithFeatureIds(new Vector3(0, 10, 0), new Vector3(0, 0, 1), 0, 100); + var vt0 = VertexBuilder.GetVertexWithFeatureIds(new Vector3(-10, 0, 0), new Vector3(0, 0, 1), 0, 0); + var vt1 = VertexBuilder.GetVertexWithFeatureIds(new Vector3(10, 0, 0), new Vector3(0, 0, 1), 0, 0); + var vt2 = VertexBuilder.GetVertexWithFeatureIds(new Vector3(0, 10, 0), new Vector3(0, 0, 1), 0, 0); prim.AddTriangle(vt0, vt1, vt2); var scene = new SceneBuilder(); @@ -68,6 +68,8 @@ public void MultipleClassesTest() var propertyTables = new List() { firstPropertyTable, secondPropertyTable }; model.SetPropertyTables(propertyTables, schema); + + model.SaveGLTF(@"D:\dev\github.com\bertt\cesium_3dtiles_samples\samples\1.1\EXT_Structural_Metadata\MultipleClasses\MultipleClasses1.gltf"); var ctx = new ValidationResult(model, ValidationMode.Strict, true); model.AttachToCurrentTest("cesium_ext_structural_metadata_multiple_classes.glb"); model.AttachToCurrentTest("cesium_ext_structural_metadata_multiple_classes.gltf"); From 84c7a40f323f09637ec9751fb4c8b3ea634732c7 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Thu, 14 Dec 2023 15:47:51 +0100 Subject: [PATCH 26/57] starting with complextypes test --- .../Schema2/EXTStructuralMetaDataRoot.cs | 37 +++++++++++- .../Schema2/MeshExtMeshFeatures.cs | 9 ++- .../ExtStructuralMetadataTests.cs | 58 +++++++++++++++++++ 3 files changed, 100 insertions(+), 4 deletions(-) diff --git a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs index b54ddcac..0b1c0efe 100644 --- a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs +++ b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs @@ -2,7 +2,6 @@ using SharpGLTF.Validation; using System; using System.Collections.Generic; -using System.Linq; namespace SharpGLTF.Schema2 { @@ -51,11 +50,37 @@ public static void SetPropertyTables( { if (propertyTables == null || propertyTables.Count == 0) { modelRoot.RemoveExtensions(); return; } + // todo add check if propertyTable.Class is in schema.Classes + foreach (var propertyTable in propertyTables) + { + Guard.IsTrue(propertyTable.Class != null, nameof(propertyTable.Class), "Class must be defined"); + Guard.IsTrue(propertyTable.Count > 0, nameof(propertyTable.Count), "Count must be greater than 0"); + Guard.IsTrue(propertyTable.Properties.Count > 0, nameof(propertyTable.Properties), "Properties must be defined"); + + schema.Switch( + StructuralMetadataSchema => + CheckConsistency(StructuralMetadataSchema, propertyTable), + Uri => + { + // do not check here, because schema is not loaded + } + ); + } + var ext = modelRoot.UseExtension(); ext.PropertyTables = propertyTables; ext.AddSchema(schema); } + private static void CheckConsistency(StructuralMetadataSchema StructuralMetadataSchema, PropertyTable propertyTable) + { + Guard.IsTrue(StructuralMetadataSchema.Classes.ContainsKey(propertyTable.Class), nameof(propertyTable.Class), $"Class {propertyTable.Class} must be defined in schema"); + foreach (var property in propertyTable.Properties) + { + Guard.IsTrue(StructuralMetadataSchema.Classes[propertyTable.Class].Properties.ContainsKey(property.Key), nameof(propertyTable.Properties), $"Property {property.Key} must be defined in schema"); + } + } + public static PropertyTableProperty GetPropertyTableProperty(this ModelRoot model, IReadOnlyList values) { var propertyTableProperty = new PropertyTableProperty(); @@ -346,6 +371,16 @@ public bool? Normalized get { return _normalized; } set { _normalized = value; } } + + // add array property + public bool? Array + { + get { return _array; } + set { _array = value; } + + } + + } public partial class PropertyTable diff --git a/src/SharpGLTF.Cesium/Schema2/MeshExtMeshFeatures.cs b/src/SharpGLTF.Cesium/Schema2/MeshExtMeshFeatures.cs index 9bd9b43e..b4fecbd8 100644 --- a/src/SharpGLTF.Cesium/Schema2/MeshExtMeshFeatures.cs +++ b/src/SharpGLTF.Cesium/Schema2/MeshExtMeshFeatures.cs @@ -102,6 +102,11 @@ public MeshExtMeshFeatureID(int featureCount, int? attribute = null, int? proper public static class ExtMeshFeatures { + public static void SetFeatureId(this MeshPrimitive primitive, MeshExtMeshFeatureID featureId) + { + primitive.SetFeatureIds(new List() { featureId }); + } + /// /// Set the FeatureIds for a MeshPrimitive /// @@ -109,9 +114,7 @@ public static class ExtMeshFeatures /// public static void SetFeatureIds(this MeshPrimitive primitive, List featureIds) { - if (featureIds == null) { primitive.RemoveExtensions(); return; } - - Guard.NotNullOrEmpty(featureIds, nameof(featureIds)); + if (featureIds == null || featureIds.Count == 0) { primitive.RemoveExtensions(); return; } foreach (var featureId in featureIds) { diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index 42aa2a7a..5344b2d9 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -20,6 +20,64 @@ public void SetUp() CesiumExtensions.RegisterExtensions(); } + [Test(Description = "ext_structural_metadata with complex types")] + // sample see https://github.com/CesiumGS/3d-tiles-samples/blob/main/glTF/EXT_structural_metadata/ComplexTypes/ComplexTypes.gltf + public void ComplexTypesTest() + { + TestContext.CurrentContext.AttachGltfValidatorLinks(); + 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 = VertexBuilder.GetVertexWithFeatureId(new Vector3(-10, 0, 0), new Vector3(0, 0, 1), 0); + var vt1 = VertexBuilder.GetVertexWithFeatureId(new Vector3(10, 0, 0), new Vector3(0, 0, 1), 0); + var vt2 = VertexBuilder.GetVertexWithFeatureId(new Vector3(0, 10, 0), new Vector3(0, 0, 1), 0); + + prim.AddTriangle(vt0, vt1, vt2); + + var scene = new SceneBuilder(); + scene.AddRigidMesh(mesh, Matrix4x4.Identity); + + var model = scene.ToGltf2(); + + var featureId = new MeshExtMeshFeatureID(1, 0, 0); + model.LogicalMeshes[0].Primitives[0].SetFeatureId(featureId); + + var schema = new StructuralMetadataSchema(); + + var exampleMetadataClass = new StructuralMetadataClass(); + exampleMetadataClass.Name = "Example metadata class A"; + exampleMetadataClass.Description = "First example metadata class"; + + var uint8ArrayProperty = new ClassProperty(); + uint8ArrayProperty.Name = "Example variable-length ARRAY normalized INT8 property"; + uint8ArrayProperty.Description = "An example property, with type ARRAY, with component type UINT8, normalized, and variable length"; + uint8ArrayProperty.Type = ElementType.SCALAR; + uint8ArrayProperty.ComponentType = DataType.UINT8; + uint8ArrayProperty.Normalized = true; + uint8ArrayProperty.Array = true; + + exampleMetadataClass.Properties.Add("example_variable_length_ARRAY_normalized_UINT8", uint8ArrayProperty); + + schema.Classes.Add("exampleMetadataClass", exampleMetadataClass); + + var examplePropertyTable = new PropertyTable("exampleMetadataClass", 1, "Example property table"); + // todo add the complex type properties + var float32Property = model.GetPropertyTableProperty(new List() { 100 }); + examplePropertyTable.Properties.Add("example_variable_length_ARRAY_normalized_UINT8", float32Property); + + model.SetPropertyTable(examplePropertyTable, schema); + + // create files + var ctx = new ValidationResult(model, ValidationMode.Strict, true); + model.AttachToCurrentTest("cesium_ext_structural_metadata_complex_types.glb"); + model.AttachToCurrentTest("cesium_ext_structural_metadata_complex_types.gltf"); + model.AttachToCurrentTest("cesium_ext_structural_metadata_complex_types.plotly"); + } + + + [Test(Description = "ext_structural_metadata with multiple classes")] // Sample see https://github.com/CesiumGS/3d-tiles-samples/blob/main/glTF/EXT_structural_metadata/MultipleClasses/MultipleClasses.gltf public void MultipleClassesTest() From 46a033e38c6e311517573e499bb39ba2fd75c963 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Thu, 14 Dec 2023 15:50:40 +0100 Subject: [PATCH 27/57] fix tests --- tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index 5344b2d9..84509e67 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -127,7 +127,6 @@ public void MultipleClassesTest() var propertyTables = new List() { firstPropertyTable, secondPropertyTable }; model.SetPropertyTables(propertyTables, schema); - model.SaveGLTF(@"D:\dev\github.com\bertt\cesium_3dtiles_samples\samples\1.1\EXT_Structural_Metadata\MultipleClasses\MultipleClasses1.gltf"); var ctx = new ValidationResult(model, ValidationMode.Strict, true); model.AttachToCurrentTest("cesium_ext_structural_metadata_multiple_classes.glb"); model.AttachToCurrentTest("cesium_ext_structural_metadata_multiple_classes.gltf"); From 877af017d929887cda1e2931cae9d2ac63e8d4ce Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Fri, 15 Dec 2023 11:44:06 +0100 Subject: [PATCH 28/57] add first complex type array of byte --- src/SharpGLTF.Cesium/BinaryTable.cs | 59 +++++++++++-------- .../Schema2/EXTStructuralMetaDataRoot.cs | 40 +++++++++++-- .../BinaryTableTests.cs | 11 ---- .../ExtStructuralMetadataTests.cs | 23 ++++---- 4 files changed, 80 insertions(+), 53 deletions(-) diff --git a/src/SharpGLTF.Cesium/BinaryTable.cs b/src/SharpGLTF.Cesium/BinaryTable.cs index a8c41b1e..d048c5db 100644 --- a/src/SharpGLTF.Cesium/BinaryTable.cs +++ b/src/SharpGLTF.Cesium/BinaryTable.cs @@ -24,7 +24,8 @@ public static byte[] GetBytes(IReadOnlyList values) if (typeof(T) == typeof(string)) { - return GetStringsAsBytes(values.Cast().ToArray()); + // todo: implement string type + throw new NotImplementedException(); } else if (typeof(T).IsPrimitive) { @@ -47,17 +48,23 @@ public static byte[] GetBytes(IReadOnlyList values) } } - /// - /// Creates a list of offsets for a list of strings - /// see https://github.com/CesiumGS/3d-tiles/tree/main/specification/Metadata#strings - /// - /// - /// - public static byte[] GetOffsetBuffer(IReadOnlyList strings) + //public static byte[] GetOffsetBuffer(List> values) + //{ + // var offsetBuffer = GetOffsets(values); + // var offsetBytes = GetBytes(offsetBuffer); + // return offsetBytes; + //} + + public static List GetOffsets(List> values) { - var offsetBuffer = GetOffsets(strings); - var offsetBytes = GetBytes(offsetBuffer); - return offsetBytes; + var offsets = new List() { 0 }; + foreach (var value in values) + { + var length = GetBytes(value).Length; + + offsets.Add(offsets.Last() + (int)length); + } + return offsets; } public static int GetSize() @@ -70,22 +77,22 @@ public static int GetSize() return size; } - private static byte[] GetStringsAsBytes(IReadOnlyList values) - { - var res = string.Join("", values); - return Encoding.UTF8.GetBytes(res); - } + //private static byte[] GetStringsAsBytes(IReadOnlyList values) + //{ + // var res = string.Join("", values); + // return Encoding.UTF8.GetBytes(res); + //} - private static List GetOffsets(IReadOnlyList strings) - { - var offsets = new List() { 0 }; - foreach (string s in strings) - { - var length = (uint)Encoding.UTF8.GetByteCount(s); + //private static List GetOffsets(IReadOnlyList strings) + //{ + // var offsets = new List() { 0 }; + // foreach (string s in strings) + // { + // var length = (uint)Encoding.UTF8.GetByteCount(s); - offsets.Add(offsets.Last() + length); - } - return offsets; - } + // offsets.Add(offsets.Last() + length); + // } + // return offsets; + //} } } diff --git a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs index 0b1c0efe..b6a64a78 100644 --- a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs +++ b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs @@ -81,7 +81,19 @@ private static void CheckConsistency(StructuralMetadataSchema StructuralMetadata } } - public static PropertyTableProperty GetPropertyTableProperty(this ModelRoot model, IReadOnlyList values) + public static PropertyTableProperty GetPropertyTableProperty(this ModelRoot model, List> values) + { + var propertyTableProperty = new PropertyTableProperty(); + int logicalIndex = GetBufferView(model, values); + propertyTableProperty.Values = logicalIndex; + + var offsets = BinaryTable.GetOffsets(values); + int logicalIndexOffsets = GetBufferView(model, offsets); + propertyTableProperty.ArrayOffsets = logicalIndexOffsets; + return propertyTableProperty; + } + + public static PropertyTableProperty GetPropertyTableProperty(this ModelRoot model, List values) { var propertyTableProperty = new PropertyTableProperty(); int logicalIndex = GetBufferView(model, values); @@ -89,16 +101,26 @@ public static PropertyTableProperty GetPropertyTableProperty(this ModelRoot m return propertyTableProperty; } - private static int GetBufferView(this ModelRoot model, IReadOnlyList values) + private static int GetBufferView(this ModelRoot model, List values) { - var bytesFloat32 = BinaryTable.GetBytes(values); - var bufferView = model.UseBufferView(bytesFloat32); + var bytes = BinaryTable.GetBytes(values); + var bufferView = model.UseBufferView(bytes); int logicalIndex = bufferView.LogicalIndex; return logicalIndex; } - - + private static int GetBufferView(this ModelRoot model, List> values) + { + var bytes = new List(); + foreach (var value in values) + { + var b = BinaryTable.GetBytes(value); + bytes.AddRange(b); + } + var bufferView = model.UseBufferView(bytes.ToArray()); + int logicalIndex = bufferView.LogicalIndex; + return logicalIndex; + } } public partial class EXTStructuralMetaDataRoot @@ -428,6 +450,12 @@ public int Values get { return _values; } set { _values = value; } } + + public int? ArrayOffsets + { + get { return _arrayOffsets; } + set { _arrayOffsets = value; } + } } } diff --git a/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs b/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs index e699a504..582b3eed 100644 --- a/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs @@ -23,17 +23,6 @@ public void TestBinaryConversion() Assert.Throws(() => BinaryTable.GetBytes(ints)); } - [Test] - public void TestOffsetBufferStrings() - { - var strings = new List { "hello, ", "world" }; - var offsetBytes = BinaryTable.GetOffsetBuffer(strings); - Assert.That(offsetBytes.Length, Is.EqualTo(12)); - Assert.That(BitConverter.ToInt32(offsetBytes, 0), Is.EqualTo(0)); - Assert.That(BitConverter.ToInt32(offsetBytes, 4), Is.EqualTo(strings[0].Length)); - Assert.That(BitConverter.ToInt32(offsetBytes, 8), Is.EqualTo(strings[0].Length + strings[1].Length)); - } - private List GetTestArray() { var l = new List(); diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index 84509e67..003fd1de 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -30,9 +30,9 @@ public void ComplexTypesTest() var prim = mesh.UsePrimitive(material); // All the vertices in the triangle have the same feature ID - var vt0 = VertexBuilder.GetVertexWithFeatureId(new Vector3(-10, 0, 0), new Vector3(0, 0, 1), 0); - var vt1 = VertexBuilder.GetVertexWithFeatureId(new Vector3(10, 0, 0), new Vector3(0, 0, 1), 0); - var vt2 = VertexBuilder.GetVertexWithFeatureId(new Vector3(0, 10, 0), new Vector3(0, 0, 1), 0); + var vt0 = VertexBuilder.GetVertexWithFeatureId(new Vector3(-1, 0, 0), new Vector3(0, 0, 1), 0); + var vt1 = VertexBuilder.GetVertexWithFeatureId(new Vector3(1, 0, 0), new Vector3(0, 0, 1), 0); + var vt2 = VertexBuilder.GetVertexWithFeatureId(new Vector3(0, 1, 0), new Vector3(0, 0, 1), 0); prim.AddTriangle(vt0, vt1, vt2); @@ -55,7 +55,7 @@ public void ComplexTypesTest() uint8ArrayProperty.Description = "An example property, with type ARRAY, with component type UINT8, normalized, and variable length"; uint8ArrayProperty.Type = ElementType.SCALAR; uint8ArrayProperty.ComponentType = DataType.UINT8; - uint8ArrayProperty.Normalized = true; + uint8ArrayProperty.Normalized = false; uint8ArrayProperty.Array = true; exampleMetadataClass.Properties.Add("example_variable_length_ARRAY_normalized_UINT8", uint8ArrayProperty); @@ -63,21 +63,24 @@ public void ComplexTypesTest() schema.Classes.Add("exampleMetadataClass", exampleMetadataClass); var examplePropertyTable = new PropertyTable("exampleMetadataClass", 1, "Example property table"); - // todo add the complex type properties - var float32Property = model.GetPropertyTableProperty(new List() { 100 }); - examplePropertyTable.Properties.Add("example_variable_length_ARRAY_normalized_UINT8", float32Property); + var list0 = new List() { 0, 1, 2, 3, 4, 5, 6, 7 }; + var list2 = new List>() { + list0 + }; + + var property = model.GetPropertyTableProperty(list2); + examplePropertyTable.Properties.Add("example_variable_length_ARRAY_normalized_UINT8", property); + + // todo add more complex type properties model.SetPropertyTable(examplePropertyTable, schema); - // create files var ctx = new ValidationResult(model, ValidationMode.Strict, true); model.AttachToCurrentTest("cesium_ext_structural_metadata_complex_types.glb"); model.AttachToCurrentTest("cesium_ext_structural_metadata_complex_types.gltf"); model.AttachToCurrentTest("cesium_ext_structural_metadata_complex_types.plotly"); } - - [Test(Description = "ext_structural_metadata with multiple classes")] // Sample see https://github.com/CesiumGS/3d-tiles-samples/blob/main/glTF/EXT_structural_metadata/MultipleClasses/MultipleClasses.gltf public void MultipleClassesTest() From b0e9423ab06b4fa2a2cd7030bdbe97764bba4701 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Fri, 15 Dec 2023 11:52:20 +0100 Subject: [PATCH 29/57] fix the tests --- tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs b/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs index 582b3eed..0f5807d1 100644 --- a/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs @@ -14,8 +14,9 @@ public void TestBinaryConversion() bytes = BinaryTable.GetBytes(GetTestArray()); Assert.That(bytes.Length, Is.EqualTo(BinaryTable.GetSize() * 2)); - bytes = BinaryTable.GetBytes(new List() { "a", "b" }); - Assert.That(bytes.Length, Is.EqualTo(2)); + + //bytes = BinaryTable.GetBytes(new List() { "a", "b" }); + //Assert.That(bytes.Length, Is.EqualTo(2)); Assert.Throws(() => BinaryTable.GetBytes(new List() { true, false })); var ints = new List>(); From acfe0b1e52076ba5d364e1b089a461cd3383a9d6 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Fri, 15 Dec 2023 21:38:28 +0100 Subject: [PATCH 30/57] add booleans complex types --- src/SharpGLTF.Cesium/BinaryTable.cs | 35 ++++--------------- .../Schema2/EXTStructuralMetaDataRoot.cs | 16 ++++++--- .../BinaryTableTests.cs | 8 ++++- .../ExtStructuralMetadataTests.cs | 19 +++++++++- 4 files changed, 43 insertions(+), 35 deletions(-) diff --git a/src/SharpGLTF.Cesium/BinaryTable.cs b/src/SharpGLTF.Cesium/BinaryTable.cs index d048c5db..c3dd6180 100644 --- a/src/SharpGLTF.Cesium/BinaryTable.cs +++ b/src/SharpGLTF.Cesium/BinaryTable.cs @@ -1,8 +1,8 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; -using System.Text; namespace SharpGLTF { @@ -31,10 +31,12 @@ public static byte[] GetBytes(IReadOnlyList values) { if(typeof(T) == typeof(bool)) { - // when implementing bool, create a bitstream - // see https://github.com/CesiumGS/3d-tiles/tree/main/specification/Metadata#booleans - throw new NotImplementedException(); + var bits = new BitArray(values.Cast().ToArray()); + byte[] ret = new byte[(bits.Length - 1) / 8 + 1]; + bits.CopyTo(ret, 0); + return ret; } + var size = GetSize(); var result = new byte[values.Count * size]; Buffer.BlockCopy(values.ToArray(), 0, result, 0, result.Length); @@ -48,13 +50,6 @@ public static byte[] GetBytes(IReadOnlyList values) } } - //public static byte[] GetOffsetBuffer(List> values) - //{ - // var offsetBuffer = GetOffsets(values); - // var offsetBytes = GetBytes(offsetBuffer); - // return offsetBytes; - //} - public static List GetOffsets(List> values) { var offsets = new List() { 0 }; @@ -76,23 +71,5 @@ public static int GetSize() int size = Marshal.SizeOf(Activator.CreateInstance(type)); return size; } - - //private static byte[] GetStringsAsBytes(IReadOnlyList values) - //{ - // var res = string.Join("", values); - // return Encoding.UTF8.GetBytes(res); - //} - - //private static List GetOffsets(IReadOnlyList strings) - //{ - // var offsets = new List() { 0 }; - // foreach (string s in strings) - // { - // var length = (uint)Encoding.UTF8.GetByteCount(s); - - // offsets.Add(offsets.Last() + length); - // } - // return offsets; - //} } } diff --git a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs index b6a64a78..c28f5b60 100644 --- a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs +++ b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs @@ -81,15 +81,18 @@ private static void CheckConsistency(StructuralMetadataSchema StructuralMetadata } } - public static PropertyTableProperty GetPropertyTableProperty(this ModelRoot model, List> values) + public static PropertyTableProperty GetArrayPropertyTableProperty(this ModelRoot model, List> values, bool CreateArrayOffsets = true) { var propertyTableProperty = new PropertyTableProperty(); int logicalIndex = GetBufferView(model, values); propertyTableProperty.Values = logicalIndex; - var offsets = BinaryTable.GetOffsets(values); - int logicalIndexOffsets = GetBufferView(model, offsets); - propertyTableProperty.ArrayOffsets = logicalIndexOffsets; + if (CreateArrayOffsets) + { + var offsets = BinaryTable.GetOffsets(values); + int logicalIndexOffsets = GetBufferView(model, offsets); + propertyTableProperty.ArrayOffsets = logicalIndexOffsets; + } return propertyTableProperty; } @@ -402,6 +405,11 @@ public bool? Array } + public int? Count + { + get { return _count; } + set { _count = value; } + } } diff --git a/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs b/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs index 0f5807d1..8a39d16f 100644 --- a/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs @@ -18,7 +18,13 @@ public void TestBinaryConversion() //bytes = BinaryTable.GetBytes(new List() { "a", "b" }); //Assert.That(bytes.Length, Is.EqualTo(2)); - Assert.Throws(() => BinaryTable.GetBytes(new List() { true, false })); + bytes = BinaryTable.GetBytes(new List() { true, false }); + Assert.That(bytes.Length, Is.EqualTo(1)); + // create a bit arrat frin the byte array + var bits = new System.Collections.BitArray(bytes); + Assert.That(bits[0] == true); + Assert.That(bits[1] == false); + var ints = new List>(); ints.Add(new List() { 0, 1 }); Assert.Throws(() => BinaryTable.GetBytes(ints)); diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index 003fd1de..43406d60 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -60,6 +60,15 @@ public void ComplexTypesTest() exampleMetadataClass.Properties.Add("example_variable_length_ARRAY_normalized_UINT8", uint8ArrayProperty); + var fixedLengthBooleanProperty = new ClassProperty(); + fixedLengthBooleanProperty.Name = "Example fixed-length ARRAY BOOLEAN property"; + fixedLengthBooleanProperty.Description = "An example property, with type ARRAY, with component type BOOLEAN, and fixed length "; + fixedLengthBooleanProperty.Type = ElementType.BOOLEAN; + fixedLengthBooleanProperty.Array = true; + fixedLengthBooleanProperty.Count = 4; + + exampleMetadataClass.Properties.Add("example_fixed_length_ARRAY_BOOLEAN", fixedLengthBooleanProperty); + schema.Classes.Add("exampleMetadataClass", exampleMetadataClass); var examplePropertyTable = new PropertyTable("exampleMetadataClass", 1, "Example property table"); @@ -68,9 +77,17 @@ public void ComplexTypesTest() list0 }; - var property = model.GetPropertyTableProperty(list2); + var property = model.GetArrayPropertyTableProperty(list2); examplePropertyTable.Properties.Add("example_variable_length_ARRAY_normalized_UINT8", property); + var booleans = new List() { true, false, true, false }; + var booleansList = new List>() + { + booleans + }; + var propertyBooleansList = model.GetArrayPropertyTableProperty(booleansList, false); + examplePropertyTable.Properties.Add("example_fixed_length_ARRAY_BOOLEAN", propertyBooleansList); + // todo add more complex type properties model.SetPropertyTable(examplePropertyTable, schema); From 4e2344d6e11472b76055ff888618493b78b59e4d Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Mon, 18 Dec 2023 21:27:51 +0100 Subject: [PATCH 31/57] add array strings complex type --- .../Ext.EXT_Structural_Metadata.cs | 2 +- src/SharpGLTF.Cesium/BinaryTable.cs | 43 ++++++++++++++++--- .../Schema2/EXTStructuralMetaDataRoot.cs | 19 +++++++- ...t.CESIUM_ext_structural_metadata_root.g.cs | 18 ++++---- .../BinaryTableTests.cs | 37 ++++++++++++++-- .../ExtStructuralMetadataTests.cs | 17 ++++++++ .../VertexPointcloud.cs | 6 +-- .../VertexWithFeatureId.cs | 4 +- 8 files changed, 121 insertions(+), 25 deletions(-) diff --git a/build/SharpGLTF.CodeGen/Ext.EXT_Structural_Metadata.cs b/build/SharpGLTF.CodeGen/Ext.EXT_Structural_Metadata.cs index 2443ec2f..0e1c883c 100644 --- a/build/SharpGLTF.CodeGen/Ext.EXT_Structural_Metadata.cs +++ b/build/SharpGLTF.CodeGen/Ext.EXT_Structural_Metadata.cs @@ -30,7 +30,7 @@ public override void PrepareTypes(CSharpEmitter newEmitter, SchemaType.Context c newEmitter.SetRuntimeName("BOOLEAN-ENUM-MAT2-MAT3-MAT4-SCALAR-STRING-VEC2-VEC3-VEC4", "ElementType"); newEmitter.SetRuntimeName("FLOAT32-FLOAT64-INT16-INT32-INT64-INT8-UINT16-UINT32-UINT64-UINT8", "DataType"); newEmitter.SetRuntimeName("INT16-INT32-INT64-INT8-UINT16-UINT32-UINT64-UINT8", "IntegerType"); - newEmitter.SetRuntimeName("UINT16-UINT32-UINT64-UINT8", "StringOffsets"); + newEmitter.SetRuntimeName("UINT16-UINT32-UINT64-UINT8", "ArrayOffsetType"); } public override IEnumerable<(string TargetFileName, SchemaType.Context Schema)> Process() diff --git a/src/SharpGLTF.Cesium/BinaryTable.cs b/src/SharpGLTF.Cesium/BinaryTable.cs index c3dd6180..d5e2ce3f 100644 --- a/src/SharpGLTF.Cesium/BinaryTable.cs +++ b/src/SharpGLTF.Cesium/BinaryTable.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; +using System.Text; namespace SharpGLTF { @@ -24,8 +25,8 @@ public static byte[] GetBytes(IReadOnlyList values) if (typeof(T) == typeof(string)) { - // todo: implement string type - throw new NotImplementedException(); + var res = string.Join("", values); + return Encoding.UTF8.GetBytes(res); } else if (typeof(T).IsPrimitive) { @@ -50,14 +51,46 @@ public static byte[] GetBytes(IReadOnlyList values) } } - public static List GetOffsets(List> values) + public static List GetStringOffsets(List values) + { + var offsets = new List() { 0 }; + foreach (var value in values) + { + var length = Encoding.UTF8.GetBytes(value).Length; + offsets.Add(offsets.Last() + length); + } + + return offsets; + } + + + public static List GetStringOffsets(List> values) + { + var offsets = new List() {}; + foreach (var arr in values) + { + var arrOffsets = GetStringOffsets(arr); + var last = offsets.LastOrDefault(); + foreach (var offset in arrOffsets) + { + if(!offsets.Contains(last + offset)) + { + offsets.Add(last + offset); + } + } + } + + return offsets; + } + + + public static List GetArrayOffsets(List> values) { var offsets = new List() { 0 }; foreach (var value in values) { - var length = GetBytes(value).Length; + offsets.Add(offsets.Last() + value.Count); - offsets.Add(offsets.Last() + (int)length); } return offsets; } diff --git a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs index c28f5b60..799226a4 100644 --- a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs +++ b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs @@ -2,6 +2,7 @@ using SharpGLTF.Validation; using System; using System.Collections.Generic; +using System.Globalization; namespace SharpGLTF.Schema2 { @@ -89,9 +90,17 @@ public static PropertyTableProperty GetArrayPropertyTableProperty(this ModelR if (CreateArrayOffsets) { - var offsets = BinaryTable.GetOffsets(values); - int logicalIndexOffsets = GetBufferView(model, offsets); + var arrayOffsets = BinaryTable.GetArrayOffsets(values); + int logicalIndexOffsets = GetBufferView(model, arrayOffsets); propertyTableProperty.ArrayOffsets = logicalIndexOffsets; + + if(typeof(T) == typeof(string)) + { + var stringValues = values.ConvertAll(x => x.ConvertAll(y => (string)Convert.ChangeType(y, typeof(string),CultureInfo.InvariantCulture))); + var stringOffsets = BinaryTable.GetStringOffsets(stringValues); + int offsets = GetBufferView(model, stringOffsets); + propertyTableProperty.StringOffsets = offsets; + } } return propertyTableProperty; } @@ -464,6 +473,12 @@ public int? ArrayOffsets get { return _arrayOffsets; } set { _arrayOffsets = value; } } + + public int? StringOffsets + { + get { return _stringOffsets; } + set { _stringOffsets = value; } + } } } diff --git a/src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata_root.g.cs b/src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata_root.g.cs index 5f378d81..9ea58f71 100644 --- a/src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata_root.g.cs +++ b/src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata_root.g.cs @@ -82,7 +82,7 @@ public enum IntegerType /// /// The type of values in `stringOffsets`. /// - public enum StringOffsets + public enum ArrayOffsetType { UINT8, UINT16, @@ -365,8 +365,8 @@ protected override void DeserializeProperty(string jsonPropertyName, ref Utf8Jso partial class PropertyTableProperty : ExtraProperties { - private const StringOffsets _arrayOffsetTypeDefault = StringOffsets.UINT32; - private StringOffsets? _arrayOffsetType = _arrayOffsetTypeDefault; + private const ArrayOffsetType _arrayOffsetTypeDefault = ArrayOffsetType.UINT32; + private ArrayOffsetType? _arrayOffsetType = _arrayOffsetTypeDefault; private Int32? _arrayOffsets; @@ -378,8 +378,8 @@ partial class PropertyTableProperty : ExtraProperties private System.Text.Json.Nodes.JsonNode _scale; - private const StringOffsets _stringOffsetTypeDefault = StringOffsets.UINT32; - private StringOffsets? _stringOffsetType = _stringOffsetTypeDefault; + private const ArrayOffsetType _stringOffsetTypeDefault = ArrayOffsetType.UINT32; + private ArrayOffsetType? _stringOffsetType = _stringOffsetTypeDefault; private Int32? _stringOffsets; @@ -389,13 +389,13 @@ partial class PropertyTableProperty : ExtraProperties protected override void SerializeProperties(Utf8JsonWriter writer) { base.SerializeProperties(writer); - SerializePropertyEnumSymbol(writer, "arrayOffsetType", _arrayOffsetType, _arrayOffsetTypeDefault); + SerializePropertyEnumSymbol(writer, "arrayOffsetType", _arrayOffsetType, _arrayOffsetTypeDefault); SerializeProperty(writer, "arrayOffsets", _arrayOffsets); SerializeProperty(writer, "max", _max); SerializeProperty(writer, "min", _min); SerializeProperty(writer, "offset", _offset); SerializeProperty(writer, "scale", _scale); - SerializePropertyEnumSymbol(writer, "stringOffsetType", _stringOffsetType, _stringOffsetTypeDefault); + SerializePropertyEnumSymbol(writer, "stringOffsetType", _stringOffsetType, _stringOffsetTypeDefault); SerializeProperty(writer, "stringOffsets", _stringOffsets); SerializeProperty(writer, "values", _values); } @@ -404,13 +404,13 @@ protected override void DeserializeProperty(string jsonPropertyName, ref Utf8Jso { switch (jsonPropertyName) { - case "arrayOffsetType": _arrayOffsetType = DeserializePropertyValue(ref reader); break; + case "arrayOffsetType": _arrayOffsetType = DeserializePropertyValue(ref reader); break; case "arrayOffsets": _arrayOffsets = DeserializePropertyValue(ref reader); break; case "max": _max = DeserializePropertyValue(ref reader); break; case "min": _min = DeserializePropertyValue(ref reader); break; case "offset": _offset = DeserializePropertyValue(ref reader); break; case "scale": _scale = DeserializePropertyValue(ref reader); break; - case "stringOffsetType": _stringOffsetType = DeserializePropertyValue(ref reader); break; + case "stringOffsetType": _stringOffsetType = DeserializePropertyValue(ref reader); break; case "stringOffsets": _stringOffsets = DeserializePropertyValue(ref reader); break; case "values": _values = DeserializePropertyValue(ref reader); break; default: base.DeserializeProperty(jsonPropertyName,ref reader); break; diff --git a/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs b/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs index 8a39d16f..a29422ba 100644 --- a/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs @@ -1,11 +1,42 @@ using NUnit.Framework; using System; using System.Collections.Generic; +using System.Globalization; namespace SharpGLTF { public class BinaryTableTests { + + [Test] + public void TestGetArrayOffset() + { + // arrange + var list0 = new List(){ "hello", "world!"}; + var list1 = new List(){"test", "testtest"}; + var arrays = new List>() { list0, list1 }; + + // act + var arrayOffsets = BinaryTable.GetArrayOffsets(arrays); + var stringOffsets = BinaryTable.GetStringOffsets(arrays); + + // assert + Assert.That(arrayOffsets.Count, Is.EqualTo(arrays.Count + 1)); + Assert.That(arrayOffsets[0], Is.EqualTo(0)); + var l0 = list0.Count; + var l1 = list1.Count; + + Assert.That(arrayOffsets[1], Is.EqualTo(l0)); + Assert.That(arrayOffsets[2], Is.EqualTo(l0+l1)); + + Assert.That(stringOffsets.Count, Is.EqualTo(list0.Count + list1.Count + 1)); + Assert.That(stringOffsets[0], Is.EqualTo(0)); + Assert.That(stringOffsets[1], Is.EqualTo(list0[0].Length)); + Assert.That(stringOffsets[2], Is.EqualTo(list0[0].Length + list0[1].Length)); + Assert.That(stringOffsets[3], Is.EqualTo(list0[0].Length + list0[1].Length + list1[0].Length)); + Assert.That(stringOffsets[4], Is.EqualTo(list0[0].Length + list0[1].Length + list1[0].Length + list1[1].Length)); + } + [Test] public void TestBinaryConversion() { @@ -30,11 +61,11 @@ public void TestBinaryConversion() Assert.Throws(() => BinaryTable.GetBytes(ints)); } - private List GetTestArray() + private static List GetTestArray() { var l = new List(); - l.Add((T)Convert.ChangeType(0, typeof(T))); - l.Add((T)Convert.ChangeType(1, typeof(T))); + l.Add((T)Convert.ChangeType(0, typeof(T),CultureInfo.InvariantCulture)); + l.Add((T)Convert.ChangeType(1, typeof(T), CultureInfo.InvariantCulture)); return l; } diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index 43406d60..8bdbdd1f 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -69,6 +69,14 @@ public void ComplexTypesTest() exampleMetadataClass.Properties.Add("example_fixed_length_ARRAY_BOOLEAN", fixedLengthBooleanProperty); + + var variableLengthStringArrayProperty = new ClassProperty(); + variableLengthStringArrayProperty.Name = "Example variable-length ARRAY STRING property"; + variableLengthStringArrayProperty.Description = "An example property, with type ARRAY, with component type STRING, and variable length"; + variableLengthStringArrayProperty.Type = ElementType.STRING; + variableLengthStringArrayProperty.Array = true; + exampleMetadataClass.Properties.Add("example_variable_length_ARRAY_STRING", variableLengthStringArrayProperty); + schema.Classes.Add("exampleMetadataClass", exampleMetadataClass); var examplePropertyTable = new PropertyTable("exampleMetadataClass", 1, "Example property table"); @@ -88,6 +96,15 @@ public void ComplexTypesTest() var propertyBooleansList = model.GetArrayPropertyTableProperty(booleansList, false); examplePropertyTable.Properties.Add("example_fixed_length_ARRAY_BOOLEAN", propertyBooleansList); + var strings = new List() { "Example string 1", "Example string 2", "Example string 3" }; + var stringsList = new List>() + { + strings + }; + + var propertyStringsList = model.GetArrayPropertyTableProperty(stringsList); + examplePropertyTable.Properties.Add("example_variable_length_ARRAY_STRING", propertyStringsList); + // todo add more complex type properties model.SetPropertyTable(examplePropertyTable, schema); diff --git a/tests/SharpGLTF.Cesium.Tests/VertexPointcloud.cs b/tests/SharpGLTF.Cesium.Tests/VertexPointcloud.cs index 50e827e7..c206f188 100644 --- a/tests/SharpGLTF.Cesium.Tests/VertexPointcloud.cs +++ b/tests/SharpGLTF.Cesium.Tests/VertexPointcloud.cs @@ -54,12 +54,12 @@ public object GetCustomAttribute(string attributeName) throw new NotImplementedException(); } - public bool TryGetCustomAttribute(string attribute, out object value) + public bool TryGetCustomAttribute(string attributeName, out object value) { - if (attribute == INTENSITYATTRIBUTENAME) { + if (attributeName == INTENSITYATTRIBUTENAME) { value = Intensity; return true; } - else if(attribute == CLASSIFICATIONATTRIBUTENAME) + else if(attributeName == CLASSIFICATIONATTRIBUTENAME) { value = Classification; return true; } diff --git a/tests/SharpGLTF.Cesium.Tests/VertexWithFeatureId.cs b/tests/SharpGLTF.Cesium.Tests/VertexWithFeatureId.cs index f0b1763b..abc17b84 100644 --- a/tests/SharpGLTF.Cesium.Tests/VertexWithFeatureId.cs +++ b/tests/SharpGLTF.Cesium.Tests/VertexWithFeatureId.cs @@ -46,9 +46,9 @@ public object GetCustomAttribute(string attributeName) return attributeName == CUSTOMATTRIBUTENAME ? (Object)BatchId : null; } - public bool TryGetCustomAttribute(string attribute, out object value) + public bool TryGetCustomAttribute(string attributeName, out object value) { - if (attribute != CUSTOMATTRIBUTENAME) { value = null; return false; } + if (attributeName != CUSTOMATTRIBUTENAME) { value = null; return false; } value = BatchId; return true; } From 242ee286457a0105981cfd44d787edc62c0fb9a0 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Tue, 19 Dec 2023 10:13:48 +0100 Subject: [PATCH 32/57] add trimming attribute to custom vertices --- tests/SharpGLTF.Cesium.Tests/VertexPointcloud.cs | 5 +++++ tests/SharpGLTF.Cesium.Tests/VertexWithFeatureIds.cs | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/tests/SharpGLTF.Cesium.Tests/VertexPointcloud.cs b/tests/SharpGLTF.Cesium.Tests/VertexPointcloud.cs index c206f188..04f0bd85 100644 --- a/tests/SharpGLTF.Cesium.Tests/VertexPointcloud.cs +++ b/tests/SharpGLTF.Cesium.Tests/VertexPointcloud.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Numerics; using SharpGLTF.Geometry.VertexTypes; @@ -7,6 +8,10 @@ namespace SharpGLTF { + #if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] + #endif + [System.Diagnostics.DebuggerDisplay("𝐂:{Color} 𝐔𝐕:{TexCoord}")] public struct VertexPointcloud : IVertexCustom { diff --git a/tests/SharpGLTF.Cesium.Tests/VertexWithFeatureIds.cs b/tests/SharpGLTF.Cesium.Tests/VertexWithFeatureIds.cs index a7c5712f..0b11772b 100644 --- a/tests/SharpGLTF.Cesium.Tests/VertexWithFeatureIds.cs +++ b/tests/SharpGLTF.Cesium.Tests/VertexWithFeatureIds.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Numerics; using SharpGLTF.Geometry.VertexTypes; @@ -7,6 +8,10 @@ namespace SharpGLTF { + #if NET6_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] + #endif + [System.Diagnostics.DebuggerDisplay("𝐂:{Color} 𝐔𝐕:{TexCoord}")] public struct VertexWithFeatureIds : IVertexCustom { From 099ea51bcd9e292d147facb3f8d069a3924c0034 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Tue, 19 Dec 2023 11:05:07 +0100 Subject: [PATCH 33/57] add enum complex type --- .../Schema2/EXTStructuralMetaDataRoot.cs | 38 ++++++++++++++--- .../ExtStructuralMetadataTests.cs | 41 +++++++++++++++---- 2 files changed, 66 insertions(+), 13 deletions(-) diff --git a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs index 799226a4..19d02c1b 100644 --- a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs +++ b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs @@ -51,6 +51,15 @@ public static void SetPropertyTables( { if (propertyTables == null || propertyTables.Count == 0) { modelRoot.RemoveExtensions(); return; } + schema.Switch( + metadataschema => + CheckSchema(metadataschema), + Uri => + { + // do not check here, because schema is not loaded + } + ); + // todo add check if propertyTable.Class is in schema.Classes foreach (var propertyTable in propertyTables) { @@ -59,8 +68,8 @@ public static void SetPropertyTables( Guard.IsTrue(propertyTable.Properties.Count > 0, nameof(propertyTable.Properties), "Properties must be defined"); schema.Switch( - StructuralMetadataSchema => - CheckConsistency(StructuralMetadataSchema, propertyTable), + metadataschema => + CheckConsistency(metadataschema, propertyTable), Uri => { // do not check here, because schema is not loaded @@ -73,13 +82,32 @@ public static void SetPropertyTables( ext.AddSchema(schema); } - private static void CheckConsistency(StructuralMetadataSchema StructuralMetadataSchema, PropertyTable propertyTable) + private static void CheckSchema(StructuralMetadataSchema schema) + { + // check if schema class property has type of enum, then the schema enum based on enumtype must be defined + foreach(var @class in schema.Classes) + { + foreach(var property in @class.Value.Properties) + { + if(property.Value.Type == ElementType.ENUM) + { + Guard.IsTrue(schema.Enums.ContainsKey(property.Value.EnumType), nameof(property.Value.EnumType), $"Enum {property.Value.EnumType} must be defined in schema"); + } + } + } + } + + private static void CheckConsistency(StructuralMetadataSchema schema, PropertyTable propertyTable) { - Guard.IsTrue(StructuralMetadataSchema.Classes.ContainsKey(propertyTable.Class), nameof(propertyTable.Class), $"Class {propertyTable.Class} must be defined in schema"); + Guard.IsTrue(schema.Classes.ContainsKey(propertyTable.Class), nameof(propertyTable.Class), $"Class {propertyTable.Class} must be defined in schema"); foreach (var property in propertyTable.Properties) { - Guard.IsTrue(StructuralMetadataSchema.Classes[propertyTable.Class].Properties.ContainsKey(property.Key), nameof(propertyTable.Properties), $"Property {property.Key} must be defined in schema"); + Guard.IsTrue(schema.Classes[propertyTable.Class].Properties.ContainsKey(property.Key), nameof(propertyTable.Properties), $"Property {property.Key} must be defined in schema"); } + + // check if schema class property has type of enum, then the schema enum based on enumtype must be defined + // foreach(var ) + } public static PropertyTableProperty GetArrayPropertyTableProperty(this ModelRoot model, List> values, bool CreateArrayOffsets = true) diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index 8bdbdd1f..548cfe93 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -50,6 +50,8 @@ public void ComplexTypesTest() exampleMetadataClass.Name = "Example metadata class A"; exampleMetadataClass.Description = "First example metadata class"; + // class properties + var uint8ArrayProperty = new ClassProperty(); uint8ArrayProperty.Name = "Example variable-length ARRAY normalized INT8 property"; uint8ArrayProperty.Description = "An example property, with type ARRAY, with component type UINT8, normalized, and variable length"; @@ -69,7 +71,6 @@ public void ComplexTypesTest() exampleMetadataClass.Properties.Add("example_fixed_length_ARRAY_BOOLEAN", fixedLengthBooleanProperty); - var variableLengthStringArrayProperty = new ClassProperty(); variableLengthStringArrayProperty.Name = "Example variable-length ARRAY STRING property"; variableLengthStringArrayProperty.Description = "An example property, with type ARRAY, with component type STRING, and variable length"; @@ -77,35 +78,59 @@ public void ComplexTypesTest() variableLengthStringArrayProperty.Array = true; exampleMetadataClass.Properties.Add("example_variable_length_ARRAY_STRING", variableLengthStringArrayProperty); + var fixed_length_ARRAY_ENUM = new ClassProperty(); + fixed_length_ARRAY_ENUM.Name = "Example fixed-length ARRAY ENUM property"; + fixed_length_ARRAY_ENUM.Description = "An example property, with type ARRAY, with component type ENUM, and fixed length"; + fixed_length_ARRAY_ENUM.Type = ElementType.ENUM; + fixed_length_ARRAY_ENUM.Array = true; + fixed_length_ARRAY_ENUM.Count = 2; + fixed_length_ARRAY_ENUM.EnumType = "exampleEnumType"; + + exampleMetadataClass.Properties.Add("example_fixed_length_ARRAY_ENUM", fixed_length_ARRAY_ENUM); + schema.Classes.Add("exampleMetadataClass", exampleMetadataClass); + // enums + + var exampleEnum = new StructuralMetadataEnum(); + exampleEnum.Values.Add(new EnumValue() { Name = "ExampleEnumValueA", Value = 0 }); + exampleEnum.Values.Add(new EnumValue() { Name = "ExampleEnumValueB", Value = 1 }); + exampleEnum.Values.Add(new EnumValue() { Name = "ExampleEnumValueC", Value = 2 }); + + schema.Enums.Add("exampleEnumType1", exampleEnum); + + // property tables + var examplePropertyTable = new PropertyTable("exampleMetadataClass", 1, "Example property table"); - var list0 = new List() { 0, 1, 2, 3, 4, 5, 6, 7 }; var list2 = new List>() { - list0 + new() { 0, 1, 2, 3, 4, 5, 6, 7 } }; var property = model.GetArrayPropertyTableProperty(list2); examplePropertyTable.Properties.Add("example_variable_length_ARRAY_normalized_UINT8", property); - var booleans = new List() { true, false, true, false }; var booleansList = new List>() { - booleans + new() { true, false, true, false } }; var propertyBooleansList = model.GetArrayPropertyTableProperty(booleansList, false); examplePropertyTable.Properties.Add("example_fixed_length_ARRAY_BOOLEAN", propertyBooleansList); - var strings = new List() { "Example string 1", "Example string 2", "Example string 3" }; var stringsList = new List>() { - strings + new() { "Example string 1", "Example string 2", "Example string 3" } }; var propertyStringsList = model.GetArrayPropertyTableProperty(stringsList); examplePropertyTable.Properties.Add("example_variable_length_ARRAY_STRING", propertyStringsList); - // todo add more complex type properties + var enumsList = new List>() + { + new() { 0, 1 } + }; + + var enumsProperty = model.GetArrayPropertyTableProperty(enumsList, false); + examplePropertyTable.Properties.Add("example_fixed_length_ARRAY_ENUM", enumsProperty); model.SetPropertyTable(examplePropertyTable, schema); From 0feba609ed469319ead05700c51ea906b8dda376 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Tue, 19 Dec 2023 11:52:32 +0100 Subject: [PATCH 34/57] add vector2/vector3/vector4 attributes --- src/SharpGLTF.Cesium/BinaryTable.cs | 58 ++++++- .../Schema2/EXTStructuralMetaDataRoot.cs | 6 - .../BinaryTableTests.cs | 29 ++++ .../ExtStructuralMetadataTests.cs | 159 ++++++++++++------ 4 files changed, 192 insertions(+), 60 deletions(-) diff --git a/src/SharpGLTF.Cesium/BinaryTable.cs b/src/SharpGLTF.Cesium/BinaryTable.cs index d5e2ce3f..3e049389 100644 --- a/src/SharpGLTF.Cesium/BinaryTable.cs +++ b/src/SharpGLTF.Cesium/BinaryTable.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Numerics; using System.Runtime.InteropServices; using System.Text; @@ -22,15 +23,27 @@ public static class BinaryTable public static byte[] GetBytes(IReadOnlyList values) { Guard.IsTrue(values.Count > 0, nameof(values), "values must have at least one element"); - + if (typeof(T) == typeof(string)) { var res = string.Join("", values); return Encoding.UTF8.GetBytes(res); } + else if (typeof(T) == typeof(Vector2)) + { + return Vector2ToBytes(values); + } + else if (typeof(T) == typeof(Vector3)) + { + return Vector3ToBytes(values); + } + else if (typeof(T) == typeof(Vector4)) + { + return Vector4ToBytes(values); + } else if (typeof(T).IsPrimitive) { - if(typeof(T) == typeof(bool)) + if (typeof(T) == typeof(bool)) { var bits = new BitArray(values.Cast().ToArray()); byte[] ret = new byte[(bits.Length - 1) / 8 + 1]; @@ -45,12 +58,51 @@ public static byte[] GetBytes(IReadOnlyList values) } else { - // other types (like enum, mat2, mat3, mat4, vec2, vec3, vec4, array (fixed length, variable length)) are not implemented + // other types (like mat2, mat3, mat4) are not implemented // see https://github.com/CesiumGS/3d-tiles/tree/main/specification/Metadata#binary-table-format throw new NotImplementedException(); } } + private static byte[] Vector2ToBytes(IReadOnlyList values) + { + var result = new byte[values.Count * 8]; + for (int i = 0; i < values.Count; i++) + { + var vec = (Vector2)(object)values[i]; + Buffer.BlockCopy(BitConverter.GetBytes(vec.X), 0, result, i * 8, 4); + Buffer.BlockCopy(BitConverter.GetBytes(vec.Y), 0, result, i * 8 + 4, 4); + } + return result; + } + + private static byte[] Vector3ToBytes(IReadOnlyList values) + { + var result = new byte[values.Count * 12]; + for (int i = 0; i < values.Count; i++) + { + var vec = (Vector3)(object)values[i]; + Buffer.BlockCopy(BitConverter.GetBytes(vec.X), 0, result, i * 12, 4); + Buffer.BlockCopy(BitConverter.GetBytes(vec.Y), 0, result, i * 12 + 4, 4); + Buffer.BlockCopy(BitConverter.GetBytes(vec.Z), 0, result, i * 12 + 8, 4); + } + return result; + } + + private static byte[] Vector4ToBytes(IReadOnlyList values) + { + var result = new byte[values.Count * 16]; + for (int i = 0; i < values.Count; i++) + { + var vec = (Vector4)(object)values[i]; + Buffer.BlockCopy(BitConverter.GetBytes(vec.X), 0, result, i * 16, 4); + Buffer.BlockCopy(BitConverter.GetBytes(vec.Y), 0, result, i * 16 + 4, 4); + Buffer.BlockCopy(BitConverter.GetBytes(vec.Z), 0, result, i * 16 + 8, 4); + Buffer.BlockCopy(BitConverter.GetBytes(vec.W), 0, result, i * 16 + 12, 4); + } + return result; + } + public static List GetStringOffsets(List values) { var offsets = new List() { 0 }; diff --git a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs index 19d02c1b..1c9b7f9e 100644 --- a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs +++ b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs @@ -104,10 +104,6 @@ private static void CheckConsistency(StructuralMetadataSchema schema, PropertyTa { Guard.IsTrue(schema.Classes[propertyTable.Class].Properties.ContainsKey(property.Key), nameof(propertyTable.Properties), $"Property {property.Key} must be defined in schema"); } - - // check if schema class property has type of enum, then the schema enum based on enumtype must be defined - // foreach(var ) - } public static PropertyTableProperty GetArrayPropertyTableProperty(this ModelRoot model, List> values, bool CreateArrayOffsets = true) @@ -434,7 +430,6 @@ public bool? Normalized set { _normalized = value; } } - // add array property public bool? Array { get { return _array; } @@ -447,7 +442,6 @@ public int? Count get { return _count; } set { _count = value; } } - } public partial class PropertyTable diff --git a/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs b/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs index a29422ba..f67c87cd 100644 --- a/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs @@ -7,6 +7,35 @@ namespace SharpGLTF { public class BinaryTableTests { + [Test] + public void ConvertVector2ToBytes() + { + var values = new List(); + values.Add(new System.Numerics.Vector2(0, 1)); + values.Add(new System.Numerics.Vector2(2, 3)); + var bytes = BinaryTable.GetBytes(values); + Assert.That(bytes.Length, Is.EqualTo(BinaryTable.GetSize() * 2)); + } + + [Test] + public void ConvertVector3ToBytes() + { + var values = new List(); + values.Add(new System.Numerics.Vector3(0, 1, 2)); + values.Add(new System.Numerics.Vector3(3, 4, 5)); + var bytes = BinaryTable.GetBytes(values); + Assert.That(bytes.Length, Is.EqualTo(BinaryTable.GetSize() * 2)); + } + + [Test] + public void ConvertVector4ToBytes() + { + var values = new List(); + values.Add(new System.Numerics.Vector4(0, 1, 2, 3)); + values.Add(new System.Numerics.Vector4(4, 5, 6, 7)); + var bytes = BinaryTable.GetBytes(values); + Assert.That(bytes.Length, Is.EqualTo(BinaryTable.GetSize() * 2)); + } [Test] public void TestGetArrayOffset() diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index 548cfe93..fd6c2837 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -20,8 +20,65 @@ public void SetUp() CesiumExtensions.RegisterExtensions(); } + [Test(Description = "ext_structural_metadata with FeatureIdAttributeAndPropertyTable")] + // sample see https://github.com/CesiumGS/3d-tiles-samples/tree/main/glTF/EXT_structural_metadata/FeatureIdAttributeAndPropertyTable + public void FeatureIdAndPropertyTableTest() + { + TestContext.CurrentContext.AttachGltfValidatorLinks(); + 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 = VertexBuilder.GetVertexWithFeatureId(new Vector3(-1, 0, 0), new Vector3(0, 0, 1), 0); + var vt1 = VertexBuilder.GetVertexWithFeatureId(new Vector3(1, 0, 0), new Vector3(0, 0, 1), 0); + var vt2 = VertexBuilder.GetVertexWithFeatureId(new Vector3(0, 1, 0), new Vector3(0, 0, 1), 0); + + prim.AddTriangle(vt0, vt1, vt2); + + var scene = new SceneBuilder(); + scene.AddRigidMesh(mesh, Matrix4x4.Identity); + + var model = scene.ToGltf2(); + + var featureId = new MeshExtMeshFeatureID(1, 0, 0); + model.LogicalMeshes[0].Primitives[0].SetFeatureId(featureId); + + var schema = new StructuralMetadataSchema(); + schema.Id = "FeatureIdAttributeAndPropertyTableSchema"; + + var exampleMetadataClass = new StructuralMetadataClass(); + exampleMetadataClass.Name = "Example metadata class"; + exampleMetadataClass.Description = "An example metadata class"; + + var vector3Property = new ClassProperty(); + vector3Property.Name = "Example VEC3 FLOAT32 property"; + vector3Property.Description = "An example property, with type VEC3, with component type FLOAT32"; + vector3Property.Type = ElementType.VEC3; + vector3Property.ComponentType = DataType.FLOAT32; + + exampleMetadataClass.Properties.Add("example_VEC3_FLOAT32", vector3Property); + + schema.Classes.Add("exampleMetadataClass", exampleMetadataClass); + + var vector3List = new List() { new Vector3(3, 3.0999999046325684f, 3.200000047683716f) }; + + var vector3PropertyTableProperty = model.GetPropertyTableProperty(vector3List); + + var examplePropertyTable = new PropertyTable("exampleMetadataClass", 1, "Example property table"); + + examplePropertyTable.Properties.Add("example_VEC3_FLOAT32", vector3PropertyTableProperty); + + model.SetPropertyTable(examplePropertyTable, schema); + + var ctx = new ValidationResult(model, ValidationMode.Strict, true); + model.AttachToCurrentTest("cesium_ext_structural_metadata_featureid_attribute_and_property_table.glb"); + model.AttachToCurrentTest("cesium_ext_structural_metadata_featureid_attribute_and_property_table.gltf"); + model.AttachToCurrentTest("cesium_ext_structural_metadata_featureid_attribute_and_property_table.plotly"); + } + [Test(Description = "ext_structural_metadata with complex types")] - // sample see https://github.com/CesiumGS/3d-tiles-samples/blob/main/glTF/EXT_structural_metadata/ComplexTypes/ComplexTypes.gltf + // sample see https://github.com/CesiumGS/3d-tiles-samples/blob/main/glTF/EXT_structural_metadata/ComplexTypes/ public void ComplexTypesTest() { TestContext.CurrentContext.AttachGltfValidatorLinks(); @@ -141,7 +198,7 @@ public void ComplexTypesTest() } [Test(Description = "ext_structural_metadata with multiple classes")] - // Sample see https://github.com/CesiumGS/3d-tiles-samples/blob/main/glTF/EXT_structural_metadata/MultipleClasses/MultipleClasses.gltf + // Sample see https://github.com/CesiumGS/3d-tiles-samples/blob/main/glTF/EXT_structural_metadata/MultipleClasses/ public void MultipleClassesTest() { var material = MaterialBuilder.CreateDefault().WithDoubleSide(true); @@ -195,6 +252,55 @@ public void MultipleClassesTest() model.AttachToCurrentTest("cesium_ext_structural_metadata_multiple_classes.plotly"); } + [Test(Description = "ext_structural_metadata with pointcloud and custom attributes")] + // Sample see https://github.com/CesiumGS/3d-tiles-samples/blob/main/glTF/EXT_structural_metadata/PropertyAttributesPointCloud/ + + public void CreatePointCloudWithCustomAttributesTest() + { + var material = new MaterialBuilder("material1").WithUnlitShader(); + var mesh = new MeshBuilder("mesh"); + var pointCloud = mesh.UsePrimitive(material, 1); + var redColor = new Vector4(1f, 0f, 0f, 1f); + var rand = new Random(); + for (var x = -10; x < 10; x++) + { + for (var y = -10; y < 10; y++) + { + for (var z = -10; z < 10; z++) + { + // intensity values is based on x-axis values + // classification of points is 0 or 1 (random) + var vt0 = VertexBuilder.GetVertexPointcloud(new Vector3(x, y, z), redColor, x, rand.Next(0, 2)); + + pointCloud.AddPoint(vt0); + } + } + } + var model = ModelRoot.CreateModel(); + model.CreateMeshes(mesh); + + // create a scene, a node, and assign the first mesh (the terrain) + model.UseScene("Default") + .CreateNode().WithMesh(model.LogicalMeshes[0]); + + var propertyAttribute = new Schema2.PropertyAttribute(); + propertyAttribute.Class = "exampleMetadataClass"; + var intensityProperty = new PropertyAttributeProperty(); + intensityProperty.Attribute = "_INTENSITY"; + var classificationProperty = new PropertyAttributeProperty(); + classificationProperty.Attribute = "_CLASSIFICATION"; + propertyAttribute.Properties["intensity"] = intensityProperty; + propertyAttribute.Properties["classification"] = classificationProperty; + + var schemaUri = new Uri("MetadataSchema.json", UriKind.Relative); + model.SetPropertyAttribute(propertyAttribute, schemaUri); + var ctx = new ValidationResult(model, ValidationMode.Strict, true); + model.AttachToCurrentTest("cesium_ext_structural_metadata_with_pointcloud_attributes.glb"); + model.AttachToCurrentTest("cesium_ext_structural_metadata_with_pointcloud_attributes.gltf"); + model.AttachToCurrentTest("cesium_ext_structural_metadata_with_pointcloud_attributes.plotly"); + } + + private static PropertyTable GetFirstPropertyTable(ModelRoot model) { var firstPropertyTable = new PropertyTable("exampleMetadataClassA", 1, "First example property table"); @@ -264,55 +370,6 @@ private static StructuralMetadataClass GetExampleClassA() return classA; } - [Test(Description = "ext_structural_metadata with pointcloud and custom attributes")] - // Sample see https://github.com/CesiumGS/3d-tiles-samples/blob/main/glTF/EXT_structural_metadata/PropertyAttributesPointCloud/PropertyAttributesPointCloudHouse.gltf - - public void CreatePointCloudWithCustomAttributesTest() - { - var material = new MaterialBuilder("material1").WithUnlitShader(); - var mesh = new MeshBuilder("mesh"); - var pointCloud = mesh.UsePrimitive(material, 1); - var redColor = new Vector4(1f, 0f, 0f, 1f); - var rand = new Random(); - for (var x = -10; x < 10; x++) - { - for (var y = -10; y < 10; y++) - { - for (var z = -10; z < 10; z++) - { - // intensity values is based on x-axis values - // classification of points is 0 or 1 (random) - var vt0 = VertexBuilder.GetVertexPointcloud(new Vector3(x, y, z), redColor, x, rand.Next(0, 2)); - - pointCloud.AddPoint(vt0); - } - } - } - var model = ModelRoot.CreateModel(); - model.CreateMeshes(mesh); - - // create a scene, a node, and assign the first mesh (the terrain) - model.UseScene("Default") - .CreateNode().WithMesh(model.LogicalMeshes[0]); - - var propertyAttribute = new Schema2.PropertyAttribute(); - propertyAttribute.Class = "exampleMetadataClass"; - var intensityProperty = new PropertyAttributeProperty(); - intensityProperty.Attribute = "_INTENSITY"; - var classificationProperty = new PropertyAttributeProperty(); - classificationProperty.Attribute = "_CLASSIFICATION"; - propertyAttribute.Properties["intensity"] = intensityProperty; - propertyAttribute.Properties["classification"] = classificationProperty; - - var schemaUri = new Uri("MetadataSchema.json", UriKind.Relative); - model.SetPropertyAttribute(propertyAttribute, schemaUri ); - var ctx = new ValidationResult(model, ValidationMode.Strict, true); - model.AttachToCurrentTest("cesium_ext_structural_metadata_with_pointcloud_attributes.glb"); - model.AttachToCurrentTest("cesium_ext_structural_metadata_with_pointcloud_attributes.gltf"); - model.AttachToCurrentTest("cesium_ext_structural_metadata_with_pointcloud_attributes.plotly"); - } - - [Test(Description = "First test with ext_structural_metadata")] public void TriangleWithMetadataTest() { From c24b072f430df378473f34e91fe73ef46603b86a Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Tue, 19 Dec 2023 12:10:28 +0100 Subject: [PATCH 35/57] add Matrix4x4 as attribute --- src/SharpGLTF.Cesium/BinaryTable.cs | 37 ++++++++++++++++++- .../BinaryTableTests.cs | 15 ++++++++ .../ExtStructuralMetadataTests.cs | 15 ++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/SharpGLTF.Cesium/BinaryTable.cs b/src/SharpGLTF.Cesium/BinaryTable.cs index 3e049389..72de66d2 100644 --- a/src/SharpGLTF.Cesium/BinaryTable.cs +++ b/src/SharpGLTF.Cesium/BinaryTable.cs @@ -41,6 +41,10 @@ public static byte[] GetBytes(IReadOnlyList values) { return Vector4ToBytes(values); } + else if(typeof(T) == typeof(Matrix4x4)) + { + return Matrix4x4ToBytes(values); + } else if (typeof(T).IsPrimitive) { if (typeof(T) == typeof(bool)) @@ -58,12 +62,43 @@ public static byte[] GetBytes(IReadOnlyList values) } else { - // other types (like mat2, mat3, mat4) are not implemented + // other types (like datetime, mat2, mat3) are not implemented // see https://github.com/CesiumGS/3d-tiles/tree/main/specification/Metadata#binary-table-format throw new NotImplementedException(); } } + private static byte[] Matrix4x4ToBytes(IReadOnlyList values) + { + var result = new byte[values.Count * 64]; + + for (int i = 0; i < values.Count; i++) + { + var mat = (Matrix4x4)(object)values[i]; + Buffer.BlockCopy(BitConverter.GetBytes(mat.M11), 0, result, i * 64, 4); + Buffer.BlockCopy(BitConverter.GetBytes(mat.M12), 0, result, i * 64 + 4, 4); + Buffer.BlockCopy(BitConverter.GetBytes(mat.M13), 0, result, i * 64 + 8, 4); + Buffer.BlockCopy(BitConverter.GetBytes(mat.M14), 0, result, i * 64 + 12, 4); + + Buffer.BlockCopy(BitConverter.GetBytes(mat.M21), 0, result, i * 64 + 16, 4); + Buffer.BlockCopy(BitConverter.GetBytes(mat.M22), 0, result, i * 64 + 20, 4); + Buffer.BlockCopy(BitConverter.GetBytes(mat.M23), 0, result, i * 64 + 24, 4); + Buffer.BlockCopy(BitConverter.GetBytes(mat.M24), 0, result, i * 64 + 28, 4); + + Buffer.BlockCopy(BitConverter.GetBytes(mat.M31), 0, result, i * 64 + 32, 4); + Buffer.BlockCopy(BitConverter.GetBytes(mat.M32), 0, result, i * 64 + 36, 4); + Buffer.BlockCopy(BitConverter.GetBytes(mat.M33), 0, result, i * 64 + 40, 4); + Buffer.BlockCopy(BitConverter.GetBytes(mat.M34), 0, result, i * 64 + 44, 4); + + Buffer.BlockCopy(BitConverter.GetBytes(mat.M41), 0, result, i * 64 + 48, 4); + Buffer.BlockCopy(BitConverter.GetBytes(mat.M42), 0, result, i * 64 + 52, 4); + Buffer.BlockCopy(BitConverter.GetBytes(mat.M43), 0, result, i * 64 + 56, 4); + Buffer.BlockCopy(BitConverter.GetBytes(mat.M44), 0, result, i * 64 + 60, 4); + } + + return result; + } + private static byte[] Vector2ToBytes(IReadOnlyList values) { var result = new byte[values.Count * 8]; diff --git a/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs b/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs index f67c87cd..795921f8 100644 --- a/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs @@ -37,6 +37,21 @@ public void ConvertVector4ToBytes() Assert.That(bytes.Length, Is.EqualTo(BinaryTable.GetSize() * 2)); } + [Test] + public void ConvertMatrix4x4ToBytes() + { + // create list of identity matrices + var values = new List(); + values.Add(System.Numerics.Matrix4x4.Identity); + + // convert to bytes + var bytes = BinaryTable.GetBytes(values); + + // check size + Assert.That(bytes.Length, Is.EqualTo(BinaryTable.GetSize())); + + } + [Test] public void TestGetArrayOffset() { diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index fd6c2837..332ff5f2 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -2,6 +2,7 @@ using SharpGLTF.Geometry; using SharpGLTF.Geometry.VertexTypes; using SharpGLTF.Materials; +using SharpGLTF.Memory; using SharpGLTF.Scenes; using SharpGLTF.Schema2; using SharpGLTF.Validation; @@ -59,6 +60,14 @@ public void FeatureIdAndPropertyTableTest() exampleMetadataClass.Properties.Add("example_VEC3_FLOAT32", vector3Property); + var matrix4x4Property = new ClassProperty(); + matrix4x4Property.Name = "Example MAT4 FLOAT32 property"; + matrix4x4Property.Description = "An example property, with type MAT4, with component type FLOAT32"; + matrix4x4Property.Type = ElementType.MAT4; + matrix4x4Property.ComponentType = DataType.FLOAT32; + + exampleMetadataClass.Properties.Add("example_MAT4_FLOAT32", matrix4x4Property); + schema.Classes.Add("exampleMetadataClass", exampleMetadataClass); var vector3List = new List() { new Vector3(3, 3.0999999046325684f, 3.200000047683716f) }; @@ -69,6 +78,12 @@ public void FeatureIdAndPropertyTableTest() examplePropertyTable.Properties.Add("example_VEC3_FLOAT32", vector3PropertyTableProperty); + var matrix4x4List = new List() { Matrix4x4.Identity }; + + var matrix4x4PropertyTableProperty = model.GetPropertyTableProperty(matrix4x4List); + + examplePropertyTable.Properties.Add("example_MAT4_FLOAT32", matrix4x4PropertyTableProperty); + model.SetPropertyTable(examplePropertyTable, schema); var ctx = new ValidationResult(model, ValidationMode.Strict, true); From 09f3ffd6d20173190be7b25e66c0a51f979f3275 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Tue, 19 Dec 2023 12:29:47 +0100 Subject: [PATCH 36/57] add some tests for binary conversion --- src/SharpGLTF.Cesium/BinaryTable.cs | 13 +++++++++++ .../Schema2/EXTStructuralMetaDataRoot.cs | 7 +----- .../BinaryTableTests.cs | 23 ++++++++++++++----- .../ExtStructuralMetadataTests.cs | 2 +- 4 files changed, 32 insertions(+), 13 deletions(-) diff --git a/src/SharpGLTF.Cesium/BinaryTable.cs b/src/SharpGLTF.Cesium/BinaryTable.cs index 72de66d2..86e7805e 100644 --- a/src/SharpGLTF.Cesium/BinaryTable.cs +++ b/src/SharpGLTF.Cesium/BinaryTable.cs @@ -14,6 +14,19 @@ namespace SharpGLTF /// public static class BinaryTable { + public static List GetBytesForArray(List> values) + { + var bytes = new List(); + foreach (var value in values) + { + var b = GetBytes(value); + bytes.AddRange(b); + } + + return bytes; + } + + /// /// Converts a list of primitive types into a byte array /// diff --git a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs index 1c9b7f9e..e8e40fe2 100644 --- a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs +++ b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs @@ -147,12 +147,7 @@ private static int GetBufferView(this ModelRoot model, List values) private static int GetBufferView(this ModelRoot model, List> values) { - var bytes = new List(); - foreach (var value in values) - { - var b = BinaryTable.GetBytes(value); - bytes.AddRange(b); - } + List bytes = BinaryTable.GetBytesForArray(values); var bufferView = model.UseBufferView(bytes.ToArray()); int logicalIndex = bufferView.LogicalIndex; return logicalIndex; diff --git a/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs b/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs index 795921f8..845bcf93 100644 --- a/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs @@ -7,6 +7,16 @@ namespace SharpGLTF { public class BinaryTableTests { + [Test] + public void ConvertListofListofIntToBytes() + { + var values = new List>(); + values.Add(new List() { 0, 1 }); + values.Add(new List() { 2, 3 }); + var bytes = BinaryTable.GetBytesForArray(values); + Assert.That(bytes.Count, Is.EqualTo(BinaryTable.GetSize() * 4)); + } + [Test] public void ConvertVector2ToBytes() { @@ -90,19 +100,20 @@ public void TestBinaryConversion() bytes = BinaryTable.GetBytes(GetTestArray()); Assert.That(bytes.Length, Is.EqualTo(BinaryTable.GetSize() * 2)); - //bytes = BinaryTable.GetBytes(new List() { "a", "b" }); - //Assert.That(bytes.Length, Is.EqualTo(2)); + bytes = BinaryTable.GetBytes(new List() { "a", "b" }); + Assert.That(bytes.Length, Is.EqualTo(2)); bytes = BinaryTable.GetBytes(new List() { true, false }); Assert.That(bytes.Length, Is.EqualTo(1)); - // create a bit arrat frin the byte array + var bits = new System.Collections.BitArray(bytes); Assert.That(bits[0] == true); Assert.That(bits[1] == false); - var ints = new List>(); - ints.Add(new List() { 0, 1 }); - Assert.Throws(() => BinaryTable.GetBytes(ints)); + // test exception for datetime + var dates = new List(); + dates.Add(new DateTime()); + Assert.Throws(() => BinaryTable.GetBytes(dates)); } private static List GetTestArray() diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index 332ff5f2..07a42cc7 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -169,7 +169,7 @@ public void ComplexTypesTest() exampleEnum.Values.Add(new EnumValue() { Name = "ExampleEnumValueB", Value = 1 }); exampleEnum.Values.Add(new EnumValue() { Name = "ExampleEnumValueC", Value = 2 }); - schema.Enums.Add("exampleEnumType1", exampleEnum); + schema.Enums.Add("exampleEnumType", exampleEnum); // property tables From 4459b77f29985c648d726c80706d834d57712c8b Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Tue, 19 Dec 2023 15:10:05 +0100 Subject: [PATCH 37/57] add test with multiple feature ids and properties --- .../Schema2/EXTStructuralMetaDataRoot.cs | 18 +++- .../ExtStructuralMetadataTests.cs | 90 ++++++++++++++++++- 2 files changed, 103 insertions(+), 5 deletions(-) diff --git a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs index e8e40fe2..4ca55873 100644 --- a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs +++ b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs @@ -84,8 +84,15 @@ public static void SetPropertyTables( private static void CheckSchema(StructuralMetadataSchema schema) { + // check schema id is defined and valid + if (!String.IsNullOrEmpty(schema.Id)) + { + var regex = "^[a-zA-Z_][a-zA-Z0-9_]*$"; + Guard.IsTrue(System.Text.RegularExpressions.Regex.IsMatch(schema.Id, regex), nameof(schema.Id)); + } + // check if schema class property has type of enum, then the schema enum based on enumtype must be defined - foreach(var @class in schema.Classes) + foreach (var @class in schema.Classes) { foreach(var property in @class.Value.Properties) { @@ -134,6 +141,15 @@ public static PropertyTableProperty GetPropertyTableProperty(this ModelRoot m var propertyTableProperty = new PropertyTableProperty(); int logicalIndex = GetBufferView(model, values); propertyTableProperty.Values = logicalIndex; + + if (typeof(T) == typeof(string)) + { + var stringvalues = values.ConvertAll(x => (string)Convert.ChangeType(x, typeof(string), CultureInfo.InvariantCulture)); + var stringOffsets = BinaryTable.GetStringOffsets(stringvalues); + int offsets = GetBufferView(model, stringOffsets); + propertyTableProperty.StringOffsets = offsets; + } + return propertyTableProperty; } diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index 07a42cc7..ee4a7f67 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -2,7 +2,6 @@ using SharpGLTF.Geometry; using SharpGLTF.Geometry.VertexTypes; using SharpGLTF.Materials; -using SharpGLTF.Memory; using SharpGLTF.Scenes; using SharpGLTF.Schema2; using SharpGLTF.Validation; @@ -21,6 +20,88 @@ public void SetUp() CesiumExtensions.RegisterExtensions(); } + [Test(Description = "ext_structural_metadata with Multiple Feature IDs and Properties")] + // sample see https://github.com/CesiumGS/3d-tiles-samples/tree/main/glTF/EXT_structural_metadata/MultipleFeatureIdsAndProperties + public void MultipleFeatureIdsandPropertiesTest() + { + TestContext.CurrentContext.AttachGltfValidatorLinks(); + 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 = VertexBuilder.GetVertexWithFeatureIds(new Vector3(0, 0, 0), new Vector3(0, 0, 1), 0, 1); + var vt1 = VertexBuilder.GetVertexWithFeatureIds(new Vector3(1, 0, 0), new Vector3(0, 0, 1), 0, 1); + var vt2 = VertexBuilder.GetVertexWithFeatureIds(new Vector3(0, 1, 0), new Vector3(0, 0, 1), 0, 1); + + var vt3 = VertexBuilder.GetVertexWithFeatureIds(new Vector3(1, 1, 0), new Vector3(0, 0, 1), 1, 0); + var vt4 = VertexBuilder.GetVertexWithFeatureIds(new Vector3(0, 0, 0), new Vector3(0, 0, 1), 1, 0); + var vt5 = VertexBuilder.GetVertexWithFeatureIds(new Vector3(1, 0, 0), new Vector3(0, 0, 1), 1, 0); + + prim.AddTriangle(vt0, vt1, vt2); + prim.AddTriangle(vt3, vt4, vt5); + + var scene = new SceneBuilder(); + scene.AddRigidMesh(mesh, Matrix4x4.Identity); + + var model = scene.ToGltf2(); + + var featureId0 = new MeshExtMeshFeatureID(2, 0, 0); + var featureId1 = new MeshExtMeshFeatureID(2, 1, 0); + var featureIds = new List() { featureId0, featureId1 }; + + model.LogicalMeshes[0].Primitives[0].SetFeatureIds(featureIds); + + var schema = new StructuralMetadataSchema(); + schema.Id = "MultipleFeatureIdsAndPropertiesSchema"; + + var exampleMetadataClass = new StructuralMetadataClass(); + exampleMetadataClass.Name = "Example metadata class"; + exampleMetadataClass.Description = "An example metadata class"; + + var vector3Property = new ClassProperty(); + vector3Property.Name = "Example VEC3 FLOAT32 property"; + vector3Property.Description = "An example property, with type VEC3, with component type FLOAT32"; + vector3Property.Type = ElementType.VEC3; + vector3Property.ComponentType = DataType.FLOAT32; + + exampleMetadataClass.Properties.Add("example_VEC3_FLOAT32", vector3Property); + + var stringProperty = new ClassProperty(); + stringProperty.Name = "Example STRING property"; + stringProperty.Description = "An example property, with type STRING"; + stringProperty.Type = ElementType.STRING; + + exampleMetadataClass.Properties.Add("example_STRING", stringProperty); + + schema.Classes.Add("exampleMetadataClass", exampleMetadataClass); + + var vector3List = new List() { + new Vector3(3, 3.0999999046325684f, 3.200000047683716f), + new Vector3(103, 103.0999999046325684f, 103.200000047683716f) + + }; + + var vector3PropertyTableProperty = model.GetPropertyTableProperty(vector3List); + + var examplePropertyTable = new PropertyTable("exampleMetadataClass", 2, "Example property table"); + + examplePropertyTable.Properties.Add("example_VEC3_FLOAT32", vector3PropertyTableProperty); + + var stringList = new List() { "Rain 🌧", "Thunder ⛈" }; + + var stringPropertyTableProperty = model.GetPropertyTableProperty(stringList); + + examplePropertyTable.Properties.Add("example_STRING", stringPropertyTableProperty); + + model.SetPropertyTable(examplePropertyTable, schema); + + var ctx = new ValidationResult(model, ValidationMode.Strict, true); + model.AttachToCurrentTest("cesium_ext_structural_metadata_featureid_attribute_and_property_table.glb"); + model.AttachToCurrentTest("cesium_ext_structural_metadata_featureid_attribute_and_property_table.gltf"); + model.AttachToCurrentTest("cesium_ext_structural_metadata_featureid_attribute_and_property_table.plotly"); + } + [Test(Description = "ext_structural_metadata with FeatureIdAttributeAndPropertyTable")] // sample see https://github.com/CesiumGS/3d-tiles-samples/tree/main/glTF/EXT_structural_metadata/FeatureIdAttributeAndPropertyTable public void FeatureIdAndPropertyTableTest() @@ -87,9 +168,9 @@ public void FeatureIdAndPropertyTableTest() model.SetPropertyTable(examplePropertyTable, schema); var ctx = new ValidationResult(model, ValidationMode.Strict, true); - model.AttachToCurrentTest("cesium_ext_structural_metadata_featureid_attribute_and_property_table.glb"); - model.AttachToCurrentTest("cesium_ext_structural_metadata_featureid_attribute_and_property_table.gltf"); - model.AttachToCurrentTest("cesium_ext_structural_metadata_featureid_attribute_and_property_table.plotly"); + model.AttachToCurrentTest("cesium_ext_structural_metadata_multiple_featureids_and_properties.glb"); + model.AttachToCurrentTest("cesium_ext_structural_metadata_multiple_featureids_and_properties.gltf"); + model.AttachToCurrentTest("cesium_ext_structural_metadata_multiple_featureids_and_properties.plotly"); } [Test(Description = "ext_structural_metadata with complex types")] @@ -205,6 +286,7 @@ public void ComplexTypesTest() examplePropertyTable.Properties.Add("example_fixed_length_ARRAY_ENUM", enumsProperty); model.SetPropertyTable(examplePropertyTable, schema); + model.SaveGLTF(@"D:\dev\github.com\bertt\cesium_3dtiles_samples\samples\1.1\EXT_Structural_Metadata\MultipleFeatureIdsAndProperties\MultipleFeatureIdsAndProperties1.gltf"); var ctx = new ValidationResult(model, ValidationMode.Strict, true); model.AttachToCurrentTest("cesium_ext_structural_metadata_complex_types.glb"); From 815e7c7b9efae81d9390b12f7139826ef8637fe2 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Tue, 19 Dec 2023 15:47:28 +0100 Subject: [PATCH 38/57] fix tests --- .../Schema2/EXTStructuralMetaDataRoot.cs | 10 ++- .../ExtStructuralMetadataTests.cs | 78 ++++++++++++++++++- 2 files changed, 82 insertions(+), 6 deletions(-) diff --git a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs index 4ca55873..f6c7614d 100644 --- a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs +++ b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs @@ -94,9 +94,9 @@ private static void CheckSchema(StructuralMetadataSchema schema) // check if schema class property has type of enum, then the schema enum based on enumtype must be defined foreach (var @class in schema.Classes) { - foreach(var property in @class.Value.Properties) + foreach (var property in @class.Value.Properties) { - if(property.Value.Type == ElementType.ENUM) + if (property.Value.Type == ElementType.ENUM) { Guard.IsTrue(schema.Enums.ContainsKey(property.Value.EnumType), nameof(property.Value.EnumType), $"Enum {property.Value.EnumType} must be defined in schema"); } @@ -125,9 +125,9 @@ public static PropertyTableProperty GetArrayPropertyTableProperty(this ModelR int logicalIndexOffsets = GetBufferView(model, arrayOffsets); propertyTableProperty.ArrayOffsets = logicalIndexOffsets; - if(typeof(T) == typeof(string)) + if (typeof(T) == typeof(string)) { - var stringValues = values.ConvertAll(x => x.ConvertAll(y => (string)Convert.ChangeType(y, typeof(string),CultureInfo.InvariantCulture))); + var stringValues = values.ConvertAll(x => x.ConvertAll(y => (string)Convert.ChangeType(y, typeof(string), CultureInfo.InvariantCulture))); var stringOffsets = BinaryTable.GetStringOffsets(stringValues); int offsets = GetBufferView(model, stringOffsets); propertyTableProperty.StringOffsets = offsets; @@ -252,6 +252,8 @@ public Dictionary Properties public partial class PropertyTextureProperty { + // todo: how to set index? + //public int Index //{ // get { return _index; } diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index ee4a7f67..c451e888 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -11,6 +11,8 @@ namespace SharpGLTF.Cesium { + using VBTexture1 = VertexBuilder; + [Category("Toolkit.Scenes")] public class ExtStructuralMetadataTests { @@ -20,6 +22,78 @@ public void SetUp() CesiumExtensions.RegisterExtensions(); } + [Test(Description = "ext_structural_metadata with simple property texture")] + // sample see https://github.com/CesiumGS/3d-tiles-samples/tree/main/glTF/EXT_structural_metadata/SimplePropertyTexture + public void SimplePropertyTextureTest() + { + 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(); + + model.SaveGLTF(@"D:\dev\github.com\bertt\cesium_3dtiles_samples\samples\1.1\EXT_Structural_Metadata\SimplePropertyTexture\SimplePropertyTexture1.gltf"); + + var schema = new StructuralMetadataSchema(); + + schema.Id = "SimplePropertyTextureSchema"; + + var exampleMetadataClass = new StructuralMetadataClass(); + exampleMetadataClass.Name = "Building properties"; + + var insideTemperatureProperty = new ClassProperty(); + insideTemperatureProperty.Name = "Inside temperature"; + insideTemperatureProperty.Type = ElementType.SCALAR; + insideTemperatureProperty.ComponentType = DataType.UINT8; + + var outsideTemperatureProperty = new ClassProperty(); + outsideTemperatureProperty.Name = "Outside temperature"; + outsideTemperatureProperty.Type = ElementType.SCALAR; + outsideTemperatureProperty.ComponentType = DataType.UINT8; + + var insulationProperty = new ClassProperty(); + insulationProperty.Name = "Insulation Thickness"; + insulationProperty.Type = ElementType.SCALAR; + insulationProperty.ComponentType = DataType.UINT8; + insideTemperatureProperty.Normalized = true; + + exampleMetadataClass.Properties.Add("inside_temperature", insideTemperatureProperty); + exampleMetadataClass.Properties.Add("outside_temperature", outsideTemperatureProperty); + exampleMetadataClass.Properties.Add("insulation_thickness", insulationProperty); + + // todo set propertytexture + // var insideTemperaturePropertyTexture = new PropertyTexture(); + // insideTemperaturePropertyTexture.index = 1 + + var propertyTextures = new List() {}; + } + + [Test(Description = "ext_structural_metadata with Multiple Feature IDs and Properties")] // sample see https://github.com/CesiumGS/3d-tiles-samples/tree/main/glTF/EXT_structural_metadata/MultipleFeatureIdsAndProperties public void MultipleFeatureIdsandPropertiesTest() @@ -29,11 +103,12 @@ public void MultipleFeatureIdsandPropertiesTest() var mesh = new MeshBuilder("mesh"); var prim = mesh.UsePrimitive(material); - // All the vertices in the triangle have the same feature ID + // first triangle has _feature_id_0 = 0 and _feature_id_1 = 1 var vt0 = VertexBuilder.GetVertexWithFeatureIds(new Vector3(0, 0, 0), new Vector3(0, 0, 1), 0, 1); var vt1 = VertexBuilder.GetVertexWithFeatureIds(new Vector3(1, 0, 0), new Vector3(0, 0, 1), 0, 1); var vt2 = VertexBuilder.GetVertexWithFeatureIds(new Vector3(0, 1, 0), new Vector3(0, 0, 1), 0, 1); + // second triangle has _feature_id_0 = 1 and _feature_id_1 = 0 var vt3 = VertexBuilder.GetVertexWithFeatureIds(new Vector3(1, 1, 0), new Vector3(0, 0, 1), 1, 0); var vt4 = VertexBuilder.GetVertexWithFeatureIds(new Vector3(0, 0, 0), new Vector3(0, 0, 1), 1, 0); var vt5 = VertexBuilder.GetVertexWithFeatureIds(new Vector3(1, 0, 0), new Vector3(0, 0, 1), 1, 0); @@ -286,7 +361,6 @@ public void ComplexTypesTest() examplePropertyTable.Properties.Add("example_fixed_length_ARRAY_ENUM", enumsProperty); model.SetPropertyTable(examplePropertyTable, schema); - model.SaveGLTF(@"D:\dev\github.com\bertt\cesium_3dtiles_samples\samples\1.1\EXT_Structural_Metadata\MultipleFeatureIdsAndProperties\MultipleFeatureIdsAndProperties1.gltf"); var ctx = new ValidationResult(model, ValidationMode.Strict, true); model.AttachToCurrentTest("cesium_ext_structural_metadata_complex_types.glb"); From 965d2a0ba4e512a78df2facc5a5273ebaaac1f0a Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Tue, 19 Dec 2023 15:55:35 +0100 Subject: [PATCH 39/57] fix test 2 --- tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index c451e888..669faf1f 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -57,7 +57,7 @@ public void SimplePropertyTextureTest() scene.AddRigidMesh(mesh, Matrix4x4.Identity); var model = scene.ToGltf2(); - model.SaveGLTF(@"D:\dev\github.com\bertt\cesium_3dtiles_samples\samples\1.1\EXT_Structural_Metadata\SimplePropertyTexture\SimplePropertyTexture1.gltf"); + // model.SaveGLTF(@"D:\dev\github.com\bertt\cesium_3dtiles_samples\samples\1.1\EXT_Structural_Metadata\SimplePropertyTexture\SimplePropertyTexture1.gltf"); var schema = new StructuralMetadataSchema(); From c16d6e64cf23f87d6e246a6f47702bf30537cc56 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Wed, 20 Dec 2023 13:10:44 +0100 Subject: [PATCH 40/57] add textures and propertytable test --- .../Ext.EXT_Structural_Metadata.cs | 3 +- .../Schema2/CesiumExtensions.cs | 5 +- .../Schema2/EXTStructuralMetaDataRoot.cs | 93 ++++++++---- ...IUM_ext_structural_metadata_primitive.g.cs | 2 +- ...t.CESIUM_ext_structural_metadata_root.g.cs | 2 +- .../Schema2/MeshExtMeshFeatures.cs | 5 + .../ExtStructuralMetadataTests.cs | 138 ++++++++++++++++-- 7 files changed, 202 insertions(+), 46 deletions(-) diff --git a/build/SharpGLTF.CodeGen/Ext.EXT_Structural_Metadata.cs b/build/SharpGLTF.CodeGen/Ext.EXT_Structural_Metadata.cs index 0e1c883c..107c4f7c 100644 --- a/build/SharpGLTF.CodeGen/Ext.EXT_Structural_Metadata.cs +++ b/build/SharpGLTF.CodeGen/Ext.EXT_Structural_Metadata.cs @@ -15,7 +15,8 @@ class ExtStructuralMetadataExtension : SchemaProcessor public override void PrepareTypes(CSharpEmitter newEmitter, SchemaType.Context ctx) { - newEmitter.SetRuntimeName("EXT_structural_metadata glTF extension", "EXTStructuralMetaDataRoot"); + newEmitter.SetRuntimeName("EXT_structural_metadata glTF Mesh Primitive extension", "ExtStructuralMetadataMeshPrimitive"); + newEmitter.SetRuntimeName("EXT_structural_metadata glTF extension", "EXTStructuralMetadataRoot"); newEmitter.SetRuntimeName("Property Table in EXT_structural_metadata", "PropertyTable"); newEmitter.SetRuntimeName("Schema in EXT_structural_metadata", "StructuralMetadataSchema"); newEmitter.SetRuntimeName("Property Table Property in EXT_structural_metadata", "PropertyTableProperty"); diff --git a/src/SharpGLTF.Cesium/Schema2/CesiumExtensions.cs b/src/SharpGLTF.Cesium/Schema2/CesiumExtensions.cs index 2fd0ff6e..5249821d 100644 --- a/src/SharpGLTF.Cesium/Schema2/CesiumExtensions.cs +++ b/src/SharpGLTF.Cesium/Schema2/CesiumExtensions.cs @@ -19,7 +19,10 @@ public static void RegisterExtensions() ExtensionsFactory.RegisterExtension("CESIUM_primitive_outline"); ExtensionsFactory.RegisterExtension("EXT_instance_features"); ExtensionsFactory.RegisterExtension("EXT_mesh_features"); - ExtensionsFactory.RegisterExtension("EXT_structural_metadata"); + ExtensionsFactory.RegisterExtension("EXT_structural_metadata"); + + // todo: register the rest of the extensions + // ExtensionsFactory.RegisterExtension("EXT_structural_metadata"); } } } diff --git a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs index f6c7614d..82f25e48 100644 --- a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs +++ b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs @@ -6,33 +6,67 @@ namespace SharpGLTF.Schema2 { - public static class ExtStructuralMetadata + public static class ExtStructuralMetadataRoot { - // sample see https://github.com/CesiumGS/3d-tiles-samples/blob/main/glTF/EXT_structural_metadata/SimplePropertyTexture/SimplePropertyTexture.gltf - public static void SetPropertyTexture( + public static void SetPropertyAttribute( this ModelRoot modelRoot, - PropertyTexture propertyTexture, - OneOf schema - ) + PropertyAttribute propertyAttribute, + OneOf schema) + { + SetPropertyAttributes(modelRoot, new List() { propertyAttribute }, schema); + } + + public static void SetPropertyAttributes( +this ModelRoot modelRoot, +List propertyAttributes, +OneOf schema) { - if (propertyTexture == null) { modelRoot.RemoveExtensions(); return; } + if (propertyAttributes == null || propertyAttributes.Count == 0) { modelRoot.RemoveExtensions(); return; } + + schema.Switch( + metadataschema => + CheckSchema(metadataschema), + Uri => + { + // do not check here, because schema is not loaded + } + ); - var ext = modelRoot.UseExtension(); - ext.PropertyTextures.Clear(); - ext.PropertyTextures.Add(propertyTexture); + // todo add check propertyAttribute + var ext = modelRoot.UseExtension(); + ext.PropertyAttributes = propertyAttributes; ext.AddSchema(schema); } - public static void SetPropertyAttribute( - this ModelRoot modelRoot, - PropertyAttribute propertyAttribute, - OneOf schema) + + public static void SetPropertyTexture( + this ModelRoot modelRoot, + PropertyTexture propertyTexture, + OneOf schema) + { + SetPropertyTextures(modelRoot, new List() { propertyTexture }, schema); + } + + + public static void SetPropertyTextures( + this ModelRoot modelRoot, + List propertyTextures, + OneOf schema) { - if (propertyAttribute == null) { modelRoot.RemoveExtensions(); return; } + if (propertyTextures == null || propertyTextures.Count == 0) { modelRoot.RemoveExtensions(); return; } - var ext = modelRoot.UseExtension(); - ext.PropertyAttributes.Clear(); - ext.PropertyAttributes.Add(propertyAttribute); + schema.Switch( + metadataschema => + CheckSchema(metadataschema), + Uri => + { + // do not check here, because schema is not loaded + } + ); + + // todo add check propertyTexture + var ext = modelRoot.UseExtension(); + ext.PropertyTextures = propertyTextures; ext.AddSchema(schema); } @@ -49,7 +83,7 @@ public static void SetPropertyTables( List propertyTables, OneOf schema) { - if (propertyTables == null || propertyTables.Count == 0) { modelRoot.RemoveExtensions(); return; } + if (propertyTables == null || propertyTables.Count == 0) { modelRoot.RemoveExtensions(); return; } schema.Switch( metadataschema => @@ -77,7 +111,7 @@ public static void SetPropertyTables( ); } - var ext = modelRoot.UseExtension(); + var ext = modelRoot.UseExtension(); ext.PropertyTables = propertyTables; ext.AddSchema(schema); } @@ -170,11 +204,11 @@ private static int GetBufferView(this ModelRoot model, List> values) } } - public partial class EXTStructuralMetaDataRoot + public partial class EXTStructuralMetadataRoot { private ModelRoot modelRoot; - internal EXTStructuralMetaDataRoot(ModelRoot modelRoot) + internal EXTStructuralMetadataRoot(ModelRoot modelRoot) { this.modelRoot = modelRoot; _propertyTables = new List(); @@ -252,13 +286,16 @@ public Dictionary Properties public partial class PropertyTextureProperty { - // todo: how to set index? + public PropertyTextureProperty() + { + _channels = new List(); + } - //public int Index - //{ - // get { return _index; } - // set { _index = value; } - //} + public List Channels + { + get { return _channels; } + set { _channels = value; } + } } public partial class PropertyAttribute diff --git a/src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata_primitive.g.cs b/src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata_primitive.g.cs index 5f802291..3ffe9782 100644 --- a/src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata_primitive.g.cs +++ b/src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata_primitive.g.cs @@ -34,7 +34,7 @@ namespace SharpGLTF.Schema2 [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 EXT_structural_metadataglTFMeshPrimitiveextension : ExtraProperties + partial class ExtStructuralMetadataMeshPrimitive : ExtraProperties { private const int _propertyAttributesMinItems = 1; diff --git a/src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata_root.g.cs b/src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata_root.g.cs index 9ea58f71..c085400b 100644 --- a/src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata_root.g.cs +++ b/src/SharpGLTF.Cesium/Schema2/Generated/Ext.CESIUM_ext_structural_metadata_root.g.cs @@ -638,7 +638,7 @@ protected override void DeserializeProperty(string jsonPropertyName, ref Utf8Jso [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 EXTStructuralMetaDataRoot : ExtraProperties + partial class EXTStructuralMetadataRoot : ExtraProperties { private const int _propertyAttributesMinItems = 1; diff --git a/src/SharpGLTF.Cesium/Schema2/MeshExtMeshFeatures.cs b/src/SharpGLTF.Cesium/Schema2/MeshExtMeshFeatures.cs index b4fecbd8..29b5c348 100644 --- a/src/SharpGLTF.Cesium/Schema2/MeshExtMeshFeatures.cs +++ b/src/SharpGLTF.Cesium/Schema2/MeshExtMeshFeatures.cs @@ -38,6 +38,11 @@ protected override void OnValidateContent(ValidationContext validate) public partial class MeshExtMeshFeatureIDTexture { + public MeshExtMeshFeatureIDTexture() + { + _channels = new List(); + } + public MeshExtMeshFeatureIDTexture(List channels, int? index = null, int? texCoord = null) { Guard.NotNullOrEmpty(channels, nameof(channels)); diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index 669faf1f..8c2766f1 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -22,6 +22,82 @@ public void SetUp() CesiumExtensions.RegisterExtensions(); } + [Test(Description = "ext_structural_metadata with FeatureId Texture and Property Table")] + // sample see https://github.com/CesiumGS/3d-tiles-samples/tree/main/glTF/EXT_structural_metadata/FeatureIdTextureAndPropertyTable + public void FeatureIdTextureAndPropertytableTest() + { + TestContext.CurrentContext.AttachGltfValidatorLinks(); + + var img0 = "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"; + var imageBytes0 = Convert.FromBase64String(img0); + var imageBuilder0 = ImageBuilder.From(imageBytes0); + + var img1 = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAJElEQVR42mNgYmBgoAQzDLwBgwcwY8FDzIDBDRiR8KgBNDAAAOKBAKByX2jMAAAAAElFTkSuQmCC"; + var imageBytes1 = Convert.FromBase64String(img1); + var imageBuilder1 = ImageBuilder.From(imageBytes1); + + var material = MaterialBuilder + .CreateDefault() + .WithMetallicRoughnessShader() + .WithBaseColor(imageBuilder0, new Vector4(1, 1, 1, 1)) + .WithDoubleSide(true) + .WithAlpha(Materials.AlphaMode.OPAQUE) + .WithMetallicRoughness(0, 1) + .WithSpecularFactor(imageBuilder1, 0); + ; + + 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(); + + var schema = new StructuralMetadataSchema(); + schema.Id = "FeatureIdTextureAndPropertyTableSchema"; + + var buildingComponentsClass = new StructuralMetadataClass(); + buildingComponentsClass.Name = "Building components"; + buildingComponentsClass.Properties.Add("component", new ClassProperty() { Name = "Component", Type = ElementType.STRING }); + buildingComponentsClass.Properties.Add("yearBuilt", new ClassProperty() { Name = "Year built", Type = ElementType.SCALAR, ComponentType = DataType.INT16 }); + schema.Classes.Add ("buildingComponents", buildingComponentsClass); + + var propertyTable = new PropertyTable(); + propertyTable.Name = "Example property table"; + propertyTable.Class = "buildingComponents"; + propertyTable.Count = 4; + + var componentProperty = model.GetPropertyTableProperty(new List() { "Wall", "Door", "Roof", "Window" }); + var yearBuiltProperty = model.GetPropertyTableProperty(new List() { 1960, 1996, 1985, 2002}); + propertyTable.Properties.Add("component", componentProperty); + propertyTable.Properties.Add("yearBuilt", yearBuiltProperty); + + model.SetPropertyTable(propertyTable, schema); + + // Set the FeatureIds, pointing to the red channel of the texture + var texture = new MeshExtMeshFeatureIDTexture(new List() { 0 }, 1, 0); + var featureIdTexture = new MeshExtMeshFeatureID(4, texture: texture, propertyTable: 0); + var featureIds = new List() { featureIdTexture }; + + var primitive = model.LogicalMeshes[0].Primitives[0]; + primitive.SetFeatureIds(featureIds); + + var ctx = new ValidationResult(model, ValidationMode.Strict, true); + model.AttachToCurrentTest("cesium_ext_structural_metadata_featureid_texture_and_property_table.glb"); + model.AttachToCurrentTest("cesium_ext_structural_metadata_featureid_texture_and_property_table.gltf"); + model.AttachToCurrentTest("cesium_ext_structural_metadata_featureid_texture_and_property_table.plotly"); + } + + [Test(Description = "ext_structural_metadata with simple property texture")] // sample see https://github.com/CesiumGS/3d-tiles-samples/tree/main/glTF/EXT_structural_metadata/SimplePropertyTexture public void SimplePropertyTextureTest() @@ -29,14 +105,19 @@ public void SimplePropertyTextureTest() 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 img0 = "AAABAAIAAQADAAIAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAACAPwAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAgD8AAIA/AACAPwAAAAAAAAAAAACAPwAAAAA="; + var img0 = "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"; + var imageBytes0 = Convert.FromBase64String(img0); + var imageBuilder0 = ImageBuilder.From(imageBytes0); + + var img1 = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABLUlEQVR42mVSSxbDIAh0GzUxKZrmCF3n/oerIx9pupgHIswAGtblE7bIKN0vqSOyXSOjPLAtktv9sCFxmcXj7EgsFj8zN00yYxrBZZJBRYk2LdC4WCDUfAdab7bpDm1lCyBW+7lpDnyNS34gcTQRltTPbAeEdFjcSQ0X9EOhGPYjhgLA7xh3kjxEEpMj1qQj7iAzAYoPELzYtuwK02M06WywAFDfX1MdJEoOtSZ7Allz1mYmWZDNL0pNF6ezu9jsQJUcNK7qzbWvMdSYQ8Jo7KKK8/uo4dxreHe0/HgF2/IqBen/za+Di69Sf8cZz5jmk+hcuhdd2tWLz8IE5MbFnRWT+yyU5vZJRtAOqlvq6MDeOrstu0UidsoO0Ak9xGwE+67+34salNEBSCxX7Bexg0rbq6TFvwAAAABJRU5ErkJggg=="; + var imageBytes1 = Convert.FromBase64String(img1); + var imageBuilder1 = ImageBuilder.From(imageBytes1); var material = MaterialBuilder .CreateDefault() .WithMetallicRoughnessShader() - .WithBaseColor(imageBuilder, new Vector4(1, 1, 1, 1)) + .WithBaseColor(imageBuilder0, new Vector4(1, 1, 1, 1)) .WithDoubleSide(true) .WithAlpha(Materials.AlphaMode.OPAQUE) .WithMetallicRoughness(0, 1); @@ -56,8 +137,7 @@ public void SimplePropertyTextureTest() var scene = new SceneBuilder(); scene.AddRigidMesh(mesh, Matrix4x4.Identity); var model = scene.ToGltf2(); - - // model.SaveGLTF(@"D:\dev\github.com\bertt\cesium_3dtiles_samples\samples\1.1\EXT_Structural_Metadata\SimplePropertyTexture\SimplePropertyTexture1.gltf"); + model.UseImage(imageBuilder1.Content); var schema = new StructuralMetadataSchema(); @@ -82,17 +162,45 @@ public void SimplePropertyTextureTest() insulationProperty.ComponentType = DataType.UINT8; insideTemperatureProperty.Normalized = true; - exampleMetadataClass.Properties.Add("inside_temperature", insideTemperatureProperty); - exampleMetadataClass.Properties.Add("outside_temperature", outsideTemperatureProperty); - exampleMetadataClass.Properties.Add("insulation_thickness", insulationProperty); + exampleMetadataClass.Properties.Add("insideTemperature", insideTemperatureProperty); + exampleMetadataClass.Properties.Add("outsideTemperature", outsideTemperatureProperty); + exampleMetadataClass.Properties.Add("insulation", insulationProperty); - // todo set propertytexture - // var insideTemperaturePropertyTexture = new PropertyTexture(); - // insideTemperaturePropertyTexture.index = 1 + schema.Classes.Add("buildingComponents", exampleMetadataClass); - var propertyTextures = new List() {}; - } + var buildingPropertyTexture = new PropertyTexture(); + buildingPropertyTexture.Class = "buildingComponents"; + + var insideTemperatureTextureProperty = new PropertyTextureProperty(); + insideTemperatureTextureProperty._LogicalTextureIndex = 1; + insideTemperatureTextureProperty.TextureCoordinate = 0; + insideTemperatureTextureProperty.Channels = new List() { 0 }; + + buildingPropertyTexture.Properties.Add("insideTemperature", insideTemperatureTextureProperty); + + var outsideTemperatureTextureProperty = new PropertyTextureProperty(); + outsideTemperatureTextureProperty._LogicalTextureIndex = 1; + outsideTemperatureTextureProperty.TextureCoordinate = 0; + outsideTemperatureTextureProperty.Channels = new List() { 1 }; + + buildingPropertyTexture.Properties.Add("outsideTemperature", outsideTemperatureTextureProperty); + var insulationTextureProperty = new PropertyTextureProperty(); + insulationTextureProperty._LogicalTextureIndex = 1; + insulationTextureProperty.TextureCoordinate = 0; + insulationTextureProperty.Channels = new List() { 2 }; + + buildingPropertyTexture.Properties.Add("insulation", insulationTextureProperty); + + model.SetPropertyTexture(buildingPropertyTexture, schema); + // todo: set the textures on the primitive + // model.LogicalMeshes[0].Primitives[0].SetPropertyTextures(new List() { 0 }); + + var ctx = new ValidationResult(model, ValidationMode.Strict, true); + model.AttachToCurrentTest("cesium_ext_structural_metadata_simple_property_texture.glb"); + model.AttachToCurrentTest("cesium_ext_structural_metadata_simple_property_texture.gltf"); + model.AttachToCurrentTest("cesium_ext_structural_metadata_simple_property_texture.plotly"); + } [Test(Description = "ext_structural_metadata with Multiple Feature IDs and Properties")] // sample see https://github.com/CesiumGS/3d-tiles-samples/tree/main/glTF/EXT_structural_metadata/MultipleFeatureIdsAndProperties @@ -242,6 +350,8 @@ public void FeatureIdAndPropertyTableTest() model.SetPropertyTable(examplePropertyTable, schema); + model.SaveGLTF(@"D:\dev\github.com\bertt\cesium_3dtiles_samples\samples\1.1\EXT_Structural_Metadata\MultipleFeatureIdsAndProperties\MultipleFeatureIdsAndProperties1.gltf"); + var ctx = new ValidationResult(model, ValidationMode.Strict, true); model.AttachToCurrentTest("cesium_ext_structural_metadata_multiple_featureids_and_properties.glb"); model.AttachToCurrentTest("cesium_ext_structural_metadata_multiple_featureids_and_properties.gltf"); From b9a8e7ff3d861cbc343d3fdd4444d9aa6c6374e0 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Wed, 20 Dec 2023 13:17:59 +0100 Subject: [PATCH 41/57] fix the test --- tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index 8c2766f1..e96d2f73 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -350,8 +350,6 @@ public void FeatureIdAndPropertyTableTest() model.SetPropertyTable(examplePropertyTable, schema); - model.SaveGLTF(@"D:\dev\github.com\bertt\cesium_3dtiles_samples\samples\1.1\EXT_Structural_Metadata\MultipleFeatureIdsAndProperties\MultipleFeatureIdsAndProperties1.gltf"); - var ctx = new ValidationResult(model, ValidationMode.Strict, true); model.AttachToCurrentTest("cesium_ext_structural_metadata_multiple_featureids_and_properties.glb"); model.AttachToCurrentTest("cesium_ext_structural_metadata_multiple_featureids_and_properties.gltf"); From 3dab41c1ab5d0f88c7639111508125417a7b8e57 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Wed, 20 Dec 2023 23:06:47 +0100 Subject: [PATCH 42/57] add checks for textures --- .../Schema2/EXTStructuralMetaDataRoot.cs | 20 ++++++++++++++++++- .../Schema2/MeshExtMeshFeatures.cs | 10 ++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs index 82f25e48..dfc16290 100644 --- a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs +++ b/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs @@ -64,7 +64,25 @@ public static void SetPropertyTextures( } ); - // todo add check propertyTexture + foreach(var propertyTexture in propertyTextures) + { + foreach(var propertyTextureProperty in propertyTexture.Properties) + { + var texCoord = propertyTextureProperty.Value.TextureCoordinate; + var channels = propertyTextureProperty.Value.Channels; + var index = propertyTextureProperty.Value._LogicalTextureIndex; + Guard.MustBeGreaterThanOrEqualTo(texCoord, 0, nameof(texCoord)); + Guard.IsTrue(channels.Count > 0, nameof(channels), "Channels must be defined"); + try + { + var texture = modelRoot.LogicalTextures[index]; + } + catch (ArgumentOutOfRangeException) + { + throw new ArgumentOutOfRangeException($"Texture index {index} does not exist"); + } + } + } var ext = modelRoot.UseExtension(); ext.PropertyTextures = propertyTextures; ext.AddSchema(schema); diff --git a/src/SharpGLTF.Cesium/Schema2/MeshExtMeshFeatures.cs b/src/SharpGLTF.Cesium/Schema2/MeshExtMeshFeatures.cs index 29b5c348..318887f4 100644 --- a/src/SharpGLTF.Cesium/Schema2/MeshExtMeshFeatures.cs +++ b/src/SharpGLTF.Cesium/Schema2/MeshExtMeshFeatures.cs @@ -160,8 +160,14 @@ private static void ValidateFeature(MeshPrimitive primitive, MeshExtMeshFeatureI 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)); + try + { + var texture = primitive.LogicalParent.LogicalParent.LogicalTextures[item.Texture.Index]; + } + catch (System.ArgumentOutOfRangeException) + { + throw new System.Exception($"Texture index {item.Texture.Index} does not exist"); + } } } } From bce7499b4c17d7a0c015ee19744d60e7cc54fe0c Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Wed, 20 Dec 2023 23:25:01 +0100 Subject: [PATCH 43/57] fix simplepropertytexture --- tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index e96d2f73..b0f1f7cb 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -120,7 +120,8 @@ public void SimplePropertyTextureTest() .WithBaseColor(imageBuilder0, new Vector4(1, 1, 1, 1)) .WithDoubleSide(true) .WithAlpha(Materials.AlphaMode.OPAQUE) - .WithMetallicRoughness(0, 1); + .WithMetallicRoughness(0, 1) + .WithSpecularFactor(imageBuilder1, 0); var mesh = VBTexture1.CreateCompatibleMesh("mesh"); var prim = mesh.UsePrimitive(material); @@ -193,8 +194,6 @@ public void SimplePropertyTextureTest() buildingPropertyTexture.Properties.Add("insulation", insulationTextureProperty); model.SetPropertyTexture(buildingPropertyTexture, schema); - // todo: set the textures on the primitive - // model.LogicalMeshes[0].Primitives[0].SetPropertyTextures(new List() { 0 }); var ctx = new ValidationResult(model, ValidationMode.Strict, true); model.AttachToCurrentTest("cesium_ext_structural_metadata_simple_property_texture.glb"); From 2afa25e606cd316c852b80a84658d5ce5a565075 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Thu, 21 Dec 2023 14:39:28 +0100 Subject: [PATCH 44/57] add StructuralMetadataPrimitive --- .../Schema2/CesiumExtensions.cs | 2 +- ...nceFeatures.cs => Ext.InstanceFeatures.cs} | 0 ...ExtMeshFeatures.cs => Ext.MeshFeatures.cs} | 13 ++-- .../Ext.StructuralMetadataPrimitive.cs | 60 +++++++++++++++++++ ...aRoot.cs => Ext.StructuralMetadataRoot.cs} | 18 +++--- .../ExtStructuralMetadataTests.cs | 8 ++- 6 files changed, 81 insertions(+), 20 deletions(-) rename src/SharpGLTF.Cesium/Schema2/{MeshExtInstanceFeatures.cs => Ext.InstanceFeatures.cs} (100%) rename src/SharpGLTF.Cesium/Schema2/{MeshExtMeshFeatures.cs => Ext.MeshFeatures.cs} (90%) create mode 100644 src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataPrimitive.cs rename src/SharpGLTF.Cesium/Schema2/{EXTStructuralMetaDataRoot.cs => Ext.StructuralMetadataRoot.cs} (97%) diff --git a/src/SharpGLTF.Cesium/Schema2/CesiumExtensions.cs b/src/SharpGLTF.Cesium/Schema2/CesiumExtensions.cs index 5249821d..10e0478a 100644 --- a/src/SharpGLTF.Cesium/Schema2/CesiumExtensions.cs +++ b/src/SharpGLTF.Cesium/Schema2/CesiumExtensions.cs @@ -20,9 +20,9 @@ public static void RegisterExtensions() ExtensionsFactory.RegisterExtension("EXT_instance_features"); ExtensionsFactory.RegisterExtension("EXT_mesh_features"); ExtensionsFactory.RegisterExtension("EXT_structural_metadata"); + ExtensionsFactory.RegisterExtension("EXT_structural_metadata"); // todo: register the rest of the extensions - // ExtensionsFactory.RegisterExtension("EXT_structural_metadata"); } } } diff --git a/src/SharpGLTF.Cesium/Schema2/MeshExtInstanceFeatures.cs b/src/SharpGLTF.Cesium/Schema2/Ext.InstanceFeatures.cs similarity index 100% rename from src/SharpGLTF.Cesium/Schema2/MeshExtInstanceFeatures.cs rename to src/SharpGLTF.Cesium/Schema2/Ext.InstanceFeatures.cs diff --git a/src/SharpGLTF.Cesium/Schema2/MeshExtMeshFeatures.cs b/src/SharpGLTF.Cesium/Schema2/Ext.MeshFeatures.cs similarity index 90% rename from src/SharpGLTF.Cesium/Schema2/MeshExtMeshFeatures.cs rename to src/SharpGLTF.Cesium/Schema2/Ext.MeshFeatures.cs index 318887f4..1529e78d 100644 --- a/src/SharpGLTF.Cesium/Schema2/MeshExtMeshFeatures.cs +++ b/src/SharpGLTF.Cesium/Schema2/Ext.MeshFeatures.cs @@ -153,21 +153,16 @@ private static void ValidateFeature(MeshPrimitive primitive, MeshExtMeshFeatureI if (item.PropertyTable.HasValue) { Guard.MustBeGreaterThanOrEqualTo((int)item.PropertyTable, 0, nameof(item.PropertyTable)); + var metadataExtension = primitive.LogicalParent.LogicalParent.GetExtension(); + Guard.NotNull(metadataExtension, nameof(metadataExtension), "EXT_Structural_Meatdata extension is not found."); + Guard.NotNull(metadataExtension.PropertyTables[item.PropertyTable.Value], nameof(item.PropertyTable), $"Property table index {item.PropertyTable.Value} does not exist"); } 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); - - try - { - var texture = primitive.LogicalParent.LogicalParent.LogicalTextures[item.Texture.Index]; - } - catch (System.ArgumentOutOfRangeException) - { - throw new System.Exception($"Texture index {item.Texture.Index} does not exist"); - } + Guard.NotNull(primitive.LogicalParent.LogicalParent.LogicalTextures[item.Texture.Index], nameof(primitive.LogicalParent.LogicalParent.LogicalTextures), $"Texture {item.Texture.Index} does not exist"); } } } diff --git a/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataPrimitive.cs b/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataPrimitive.cs new file mode 100644 index 00000000..13bfa0f8 --- /dev/null +++ b/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataPrimitive.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; + +namespace SharpGLTF.Schema2 +{ + partial class ExtStructuralMetadataMeshPrimitive + { + internal ExtStructuralMetadataMeshPrimitive(MeshPrimitive meshPrimitive) + { + this.meshPrimitive = meshPrimitive; + } + + private MeshPrimitive meshPrimitive; + + public List PropertyTextures + { + get + { + return _propertyTextures; + } + set + { + if (value == null) { _propertyTextures = null; return; } + _propertyTextures = value; + } + } + + public List PropertyAttributes + { + get + { + return _propertyAttributes; + } + set + { + if (value == null) { _propertyAttributes = null; return; } + _propertyAttributes = value; + } + } + + } + + partial class CesiumExtensions + { + public static void SetPropertyTextures(this MeshPrimitive primitive, List propertyTextures) + { + if (propertyTextures == null) { primitive.RemoveExtensions(); return; } + + var ext = primitive.UseExtension(); + ext.PropertyTextures = propertyTextures; + } + + public static void SetPropertyAttributes(this MeshPrimitive primitive, List propertyAttributes) + { + if (propertyAttributes == null) { primitive.RemoveExtensions(); return; } + + var ext = primitive.UseExtension(); + ext.PropertyAttributes = propertyAttributes; + } + } +} \ No newline at end of file diff --git a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs b/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataRoot.cs similarity index 97% rename from src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs rename to src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataRoot.cs index dfc16290..b665d58a 100644 --- a/src/SharpGLTF.Cesium/Schema2/EXTStructuralMetaDataRoot.cs +++ b/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataRoot.cs @@ -73,14 +73,8 @@ public static void SetPropertyTextures( var index = propertyTextureProperty.Value._LogicalTextureIndex; Guard.MustBeGreaterThanOrEqualTo(texCoord, 0, nameof(texCoord)); Guard.IsTrue(channels.Count > 0, nameof(channels), "Channels must be defined"); - try - { - var texture = modelRoot.LogicalTextures[index]; - } - catch (ArgumentOutOfRangeException) - { - throw new ArgumentOutOfRangeException($"Texture index {index} does not exist"); - } + Guard.IsTrue(index >= 0, nameof(index), "Index must be defined"); + Guard.NotNull(modelRoot.LogicalTextures[index], nameof(index), $"Texture {index} must be defined"); } } var ext = modelRoot.UseExtension(); @@ -278,6 +272,14 @@ protected override void OnValidateContent(ValidationContext result) Guard.IsFalse(Schema != null && SchemaUri != null, "Schema/SchemaUri", "Schema and SchemaUri cannot both be defined"); Guard.IsFalse(Schema == null && SchemaUri == null, "Schema/SchemaUri", "One of Schema and SchemaUri must be defined"); + + // check if the propertyTable id is set, then the propertyTable must be defined + + // loop through all the primitive and check if propertyTable is defined, then the propertyTable must be defined + + + + base.OnValidateContent(result); } } diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index b0f1f7cb..4c914e35 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -110,6 +110,7 @@ public void SimplePropertyTextureTest() var imageBytes0 = Convert.FromBase64String(img0); var imageBuilder0 = ImageBuilder.From(imageBytes0); + // var img1 = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABLUlEQVR42mVSSxbDIAh0GzUxKZrmCF3n/oerIx9pupgHIswAGtblE7bIKN0vqSOyXSOjPLAtktv9sCFxmcXj7EgsFj8zN00yYxrBZZJBRYk2LdC4WCDUfAdab7bpDm1lCyBW+7lpDnyNS34gcTQRltTPbAeEdFjcSQ0X9EOhGPYjhgLA7xh3kjxEEpMj1qQj7iAzAYoPELzYtuwK02M06WywAFDfX1MdJEoOtSZ7Allz1mYmWZDNL0pNF6ezu9jsQJUcNK7qzbWvMdSYQ8Jo7KKK8/uo4dxreHe0/HgF2/IqBen/za+Di69Sf8cZz5jmk+hcuhdd2tWLz8IE5MbFnRWT+yyU5vZJRtAOqlvq6MDeOrstu0UidsoO0Ak9xGwE+67+34salNEBSCxX7Bexg0rbq6TFvwAAAABJRU5ErkJggg=="; var img1 = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABLUlEQVR42mVSSxbDIAh0GzUxKZrmCF3n/oerIx9pupgHIswAGtblE7bIKN0vqSOyXSOjPLAtktv9sCFxmcXj7EgsFj8zN00yYxrBZZJBRYk2LdC4WCDUfAdab7bpDm1lCyBW+7lpDnyNS34gcTQRltTPbAeEdFjcSQ0X9EOhGPYjhgLA7xh3kjxEEpMj1qQj7iAzAYoPELzYtuwK02M06WywAFDfX1MdJEoOtSZ7Allz1mYmWZDNL0pNF6ezu9jsQJUcNK7qzbWvMdSYQ8Jo7KKK8/uo4dxreHe0/HgF2/IqBen/za+Di69Sf8cZz5jmk+hcuhdd2tWLz8IE5MbFnRWT+yyU5vZJRtAOqlvq6MDeOrstu0UidsoO0Ak9xGwE+67+34salNEBSCxX7Bexg0rbq6TFvwAAAABJRU5ErkJggg=="; var imageBytes1 = Convert.FromBase64String(img1); var imageBuilder1 = ImageBuilder.From(imageBytes1); @@ -138,7 +139,6 @@ public void SimplePropertyTextureTest() var scene = new SceneBuilder(); scene.AddRigidMesh(mesh, Matrix4x4.Identity); var model = scene.ToGltf2(); - model.UseImage(imageBuilder1.Content); var schema = new StructuralMetadataSchema(); @@ -161,7 +161,7 @@ public void SimplePropertyTextureTest() insulationProperty.Name = "Insulation Thickness"; insulationProperty.Type = ElementType.SCALAR; insulationProperty.ComponentType = DataType.UINT8; - insideTemperatureProperty.Normalized = true; + insulationProperty.Normalized = true; exampleMetadataClass.Properties.Add("insideTemperature", insideTemperatureProperty); exampleMetadataClass.Properties.Add("outsideTemperature", outsideTemperatureProperty); @@ -195,6 +195,10 @@ public void SimplePropertyTextureTest() model.SetPropertyTexture(buildingPropertyTexture, schema); + var primitive = model.LogicalMeshes[0].Primitives[0]; + var propertyTextures = new List { 0 }; + primitive.SetPropertyTextures(propertyTextures); + var ctx = new ValidationResult(model, ValidationMode.Strict, true); model.AttachToCurrentTest("cesium_ext_structural_metadata_simple_property_texture.glb"); model.AttachToCurrentTest("cesium_ext_structural_metadata_simple_property_texture.gltf"); From 9d857386e24ea541dfdb0ca0be378602f8ef80cc Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Thu, 21 Dec 2023 14:56:19 +0100 Subject: [PATCH 45/57] fix the tests --- .../Schema2/Ext.MeshFeatures.cs | 2 +- .../Ext.StructuralMetadataPrimitive.cs | 2 ++ .../ExtStructuralMetadataTests.cs | 29 ++++++++++--------- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/SharpGLTF.Cesium/Schema2/Ext.MeshFeatures.cs b/src/SharpGLTF.Cesium/Schema2/Ext.MeshFeatures.cs index 1529e78d..14f667a3 100644 --- a/src/SharpGLTF.Cesium/Schema2/Ext.MeshFeatures.cs +++ b/src/SharpGLTF.Cesium/Schema2/Ext.MeshFeatures.cs @@ -154,7 +154,7 @@ private static void ValidateFeature(MeshPrimitive primitive, MeshExtMeshFeatureI { Guard.MustBeGreaterThanOrEqualTo((int)item.PropertyTable, 0, nameof(item.PropertyTable)); var metadataExtension = primitive.LogicalParent.LogicalParent.GetExtension(); - Guard.NotNull(metadataExtension, nameof(metadataExtension), "EXT_Structural_Meatdata extension is not found."); + Guard.NotNull(metadataExtension, nameof(metadataExtension), "EXT_Structural_Metadata extension is not found."); Guard.NotNull(metadataExtension.PropertyTables[item.PropertyTable.Value], nameof(item.PropertyTable), $"Property table index {item.PropertyTable.Value} does not exist"); } if (item.Texture != null) diff --git a/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataPrimitive.cs b/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataPrimitive.cs index 13bfa0f8..923878be 100644 --- a/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataPrimitive.cs +++ b/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataPrimitive.cs @@ -7,6 +7,8 @@ partial class ExtStructuralMetadataMeshPrimitive internal ExtStructuralMetadataMeshPrimitive(MeshPrimitive meshPrimitive) { this.meshPrimitive = meshPrimitive; + _propertyTextures = new List(); + _propertyAttributes = new List(); } private MeshPrimitive meshPrimitive; diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index 4c914e35..f73a9b95 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -232,12 +232,6 @@ public void MultipleFeatureIdsandPropertiesTest() var model = scene.ToGltf2(); - var featureId0 = new MeshExtMeshFeatureID(2, 0, 0); - var featureId1 = new MeshExtMeshFeatureID(2, 1, 0); - var featureIds = new List() { featureId0, featureId1 }; - - model.LogicalMeshes[0].Primitives[0].SetFeatureIds(featureIds); - var schema = new StructuralMetadataSchema(); schema.Id = "MultipleFeatureIdsAndPropertiesSchema"; @@ -282,6 +276,12 @@ public void MultipleFeatureIdsandPropertiesTest() model.SetPropertyTable(examplePropertyTable, schema); + var featureId0 = new MeshExtMeshFeatureID(2, 0, 0); + var featureId1 = new MeshExtMeshFeatureID(2, 1, 0); + var featureIds = new List() { featureId0, featureId1 }; + + model.LogicalMeshes[0].Primitives[0].SetFeatureIds(featureIds); + var ctx = new ValidationResult(model, ValidationMode.Strict, true); model.AttachToCurrentTest("cesium_ext_structural_metadata_featureid_attribute_and_property_table.glb"); model.AttachToCurrentTest("cesium_ext_structural_metadata_featureid_attribute_and_property_table.gltf"); @@ -309,9 +309,6 @@ public void FeatureIdAndPropertyTableTest() var model = scene.ToGltf2(); - var featureId = new MeshExtMeshFeatureID(1, 0, 0); - model.LogicalMeshes[0].Primitives[0].SetFeatureId(featureId); - var schema = new StructuralMetadataSchema(); schema.Id = "FeatureIdAttributeAndPropertyTableSchema"; @@ -353,6 +350,9 @@ public void FeatureIdAndPropertyTableTest() model.SetPropertyTable(examplePropertyTable, schema); + var featureId = new MeshExtMeshFeatureID(1, 0, 0); + model.LogicalMeshes[0].Primitives[0].SetFeatureId(featureId); + var ctx = new ValidationResult(model, ValidationMode.Strict, true); model.AttachToCurrentTest("cesium_ext_structural_metadata_multiple_featureids_and_properties.glb"); model.AttachToCurrentTest("cesium_ext_structural_metadata_multiple_featureids_and_properties.gltf"); @@ -380,9 +380,6 @@ public void ComplexTypesTest() var model = scene.ToGltf2(); - var featureId = new MeshExtMeshFeatureID(1, 0, 0); - model.LogicalMeshes[0].Primitives[0].SetFeatureId(featureId); - var schema = new StructuralMetadataSchema(); var exampleMetadataClass = new StructuralMetadataClass(); @@ -473,6 +470,9 @@ public void ComplexTypesTest() model.SetPropertyTable(examplePropertyTable, schema); + var featureId = new MeshExtMeshFeatureID(1, 0, 0); + model.LogicalMeshes[0].Primitives[0].SetFeatureId(featureId); + var ctx = new ValidationResult(model, ValidationMode.Strict, true); model.AttachToCurrentTest("cesium_ext_structural_metadata_complex_types.glb"); model.AttachToCurrentTest("cesium_ext_structural_metadata_complex_types.gltf"); @@ -504,8 +504,6 @@ public void MultipleClassesTest() var featureId1Attribute = new MeshExtMeshFeatureID(1, 1, 1); // Set the FeatureIds - var featureIds = new List() { featureId0Attribute, featureId1Attribute }; - model.LogicalMeshes[0].Primitives[0].SetFeatureIds(featureIds); var schema = new StructuralMetadataSchema(); schema.Id = "MultipleClassesSchema"; @@ -528,6 +526,9 @@ public void MultipleClassesTest() var propertyTables = new List() { firstPropertyTable, secondPropertyTable }; model.SetPropertyTables(propertyTables, schema); + var featureIds = new List() { featureId0Attribute, featureId1Attribute }; + model.LogicalMeshes[0].Primitives[0].SetFeatureIds(featureIds); + var ctx = new ValidationResult(model, ValidationMode.Strict, true); model.AttachToCurrentTest("cesium_ext_structural_metadata_multiple_classes.glb"); model.AttachToCurrentTest("cesium_ext_structural_metadata_multiple_classes.gltf"); From bd754e3d16de74fdd3d292873453020e009614e3 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Thu, 21 Dec 2023 17:51:02 +0100 Subject: [PATCH 46/57] working on validation --- .../Ext.StructuralMetadataPrimitive.cs | 24 ++- .../Schema2/Ext.StructuralMetadataRoot.cs | 139 +++++++----------- .../ExtStructuralMetadataTests.cs | 3 - 3 files changed, 73 insertions(+), 93 deletions(-) diff --git a/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataPrimitive.cs b/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataPrimitive.cs index 923878be..b073b9af 100644 --- a/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataPrimitive.cs +++ b/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataPrimitive.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using SharpGLTF.Validation; +using System.Collections.Generic; namespace SharpGLTF.Schema2 { @@ -39,6 +40,27 @@ public List PropertyAttributes } } + protected override void OnValidateReferences(ValidationContext validate) + { + foreach (var propertyTexture in PropertyTextures) + { + var propertyTextures = meshPrimitive.LogicalParent.LogicalParent.GetExtension().PropertyTextures; + validate.IsNullOrIndex(nameof(propertyTexture), propertyTexture, propertyTextures); + } + + foreach (var propertyAttribute in PropertyAttributes) + { + var propertyAttributes = meshPrimitive.LogicalParent.LogicalParent.GetExtension().PropertyAttributes; + validate.IsNullOrIndex(nameof(propertyAttribute), propertyAttribute, propertyAttributes); + } + + base.OnValidateReferences(validate); + } + + protected override void OnValidateContent(ValidationContext result) + { + base.OnValidateContent(result); + } } partial class CesiumExtensions diff --git a/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataRoot.cs b/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataRoot.cs index b665d58a..79703583 100644 --- a/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataRoot.cs +++ b/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataRoot.cs @@ -23,16 +23,6 @@ public static void SetPropertyAttributes( { if (propertyAttributes == null || propertyAttributes.Count == 0) { modelRoot.RemoveExtensions(); return; } - schema.Switch( - metadataschema => - CheckSchema(metadataschema), - Uri => - { - // do not check here, because schema is not loaded - } - ); - - // todo add check propertyAttribute var ext = modelRoot.UseExtension(); ext.PropertyAttributes = propertyAttributes; ext.AddSchema(schema); @@ -55,28 +45,6 @@ public static void SetPropertyTextures( { if (propertyTextures == null || propertyTextures.Count == 0) { modelRoot.RemoveExtensions(); return; } - schema.Switch( - metadataschema => - CheckSchema(metadataschema), - Uri => - { - // do not check here, because schema is not loaded - } - ); - - foreach(var propertyTexture in propertyTextures) - { - foreach(var propertyTextureProperty in propertyTexture.Properties) - { - var texCoord = propertyTextureProperty.Value.TextureCoordinate; - var channels = propertyTextureProperty.Value.Channels; - var index = propertyTextureProperty.Value._LogicalTextureIndex; - Guard.MustBeGreaterThanOrEqualTo(texCoord, 0, nameof(texCoord)); - Guard.IsTrue(channels.Count > 0, nameof(channels), "Channels must be defined"); - Guard.IsTrue(index >= 0, nameof(index), "Index must be defined"); - Guard.NotNull(modelRoot.LogicalTextures[index], nameof(index), $"Texture {index} must be defined"); - } - } var ext = modelRoot.UseExtension(); ext.PropertyTextures = propertyTextures; ext.AddSchema(schema); @@ -97,68 +65,11 @@ public static void SetPropertyTables( { if (propertyTables == null || propertyTables.Count == 0) { modelRoot.RemoveExtensions(); return; } - schema.Switch( - metadataschema => - CheckSchema(metadataschema), - Uri => - { - // do not check here, because schema is not loaded - } - ); - - // todo add check if propertyTable.Class is in schema.Classes - foreach (var propertyTable in propertyTables) - { - Guard.IsTrue(propertyTable.Class != null, nameof(propertyTable.Class), "Class must be defined"); - Guard.IsTrue(propertyTable.Count > 0, nameof(propertyTable.Count), "Count must be greater than 0"); - Guard.IsTrue(propertyTable.Properties.Count > 0, nameof(propertyTable.Properties), "Properties must be defined"); - - schema.Switch( - metadataschema => - CheckConsistency(metadataschema, propertyTable), - Uri => - { - // do not check here, because schema is not loaded - } - ); - } - var ext = modelRoot.UseExtension(); ext.PropertyTables = propertyTables; ext.AddSchema(schema); } - private static void CheckSchema(StructuralMetadataSchema schema) - { - // check schema id is defined and valid - if (!String.IsNullOrEmpty(schema.Id)) - { - var regex = "^[a-zA-Z_][a-zA-Z0-9_]*$"; - Guard.IsTrue(System.Text.RegularExpressions.Regex.IsMatch(schema.Id, regex), nameof(schema.Id)); - } - - // check if schema class property has type of enum, then the schema enum based on enumtype must be defined - foreach (var @class in schema.Classes) - { - foreach (var property in @class.Value.Properties) - { - if (property.Value.Type == ElementType.ENUM) - { - Guard.IsTrue(schema.Enums.ContainsKey(property.Value.EnumType), nameof(property.Value.EnumType), $"Enum {property.Value.EnumType} must be defined in schema"); - } - } - } - } - - private static void CheckConsistency(StructuralMetadataSchema schema, PropertyTable propertyTable) - { - Guard.IsTrue(schema.Classes.ContainsKey(propertyTable.Class), nameof(propertyTable.Class), $"Class {propertyTable.Class} must be defined in schema"); - foreach (var property in propertyTable.Properties) - { - Guard.IsTrue(schema.Classes[propertyTable.Class].Properties.ContainsKey(property.Key), nameof(propertyTable.Properties), $"Property {property.Key} must be defined in schema"); - } - } - public static PropertyTableProperty GetArrayPropertyTableProperty(this ModelRoot model, List> values, bool CreateArrayOffsets = true) { var propertyTableProperty = new PropertyTableProperty(); @@ -266,8 +177,58 @@ internal List PropertyTextures set { _propertyTextures = value; } } + protected override void OnValidateReferences(ValidationContext validate) + { + foreach (var propertyTexture in PropertyTextures) + { + foreach(var propertyTextureProperty in propertyTexture.Properties) + { + var textureId = propertyTextureProperty.Value._LogicalTextureIndex; + validate.IsNullOrIndex(nameof(propertyTexture), textureId, modelRoot.LogicalTextures); + } + } + + foreach (var propertyTable in PropertyTables) + { + foreach (var property in propertyTable.Properties) + { + Guard.IsTrue(Schema.Classes[propertyTable.Class].Properties.ContainsKey(property.Key), nameof(propertyTable.Properties), $"Property {property.Key} must be defined in schema"); + } + } + + + base.OnValidateReferences(validate); + } + protected override void OnValidateContent(ValidationContext result) { + // check schema id is defined and valid + if (Schema!=null && !String.IsNullOrEmpty(Schema.Id)) + { + var regex = "^[a-zA-Z_][a-zA-Z0-9_]*$"; + Guard.IsTrue(System.Text.RegularExpressions.Regex.IsMatch(Schema.Id, regex), nameof(Schema.Id)); + } + + foreach (var propertyTexture in PropertyTextures) + { + foreach (var propertyTextureProperty in propertyTexture.Properties) + { + var texCoord = propertyTextureProperty.Value.TextureCoordinate; + var channels = propertyTextureProperty.Value.Channels; + var index = propertyTextureProperty.Value._LogicalTextureIndex; + Guard.MustBeGreaterThanOrEqualTo(texCoord, 0, nameof(texCoord)); + Guard.IsTrue(channels.Count > 0, nameof(channels), "Channels must be defined"); + Guard.IsTrue(index >= 0, nameof(index), "Index must be defined"); + } + } + + foreach (var propertyTable in PropertyTables) + { + Guard.IsTrue(propertyTable.Class != null, nameof(propertyTable.Class), "Class must be defined"); + Guard.IsTrue(propertyTable.Count > 0, nameof(propertyTable.Count), "Count must be greater than 0"); + Guard.IsTrue(propertyTable.Properties.Count > 0, nameof(propertyTable.Properties), "Properties must be defined"); + } + // Check one of schema or schemaUri is defined, but not both Guard.IsFalse(Schema != null && SchemaUri != null, "Schema/SchemaUri", "Schema and SchemaUri cannot both be defined"); Guard.IsFalse(Schema == null && SchemaUri == null, "Schema/SchemaUri", "One of Schema and SchemaUri must be defined"); diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index f73a9b95..54cf43dd 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -104,13 +104,10 @@ public void SimplePropertyTextureTest() { TestContext.CurrentContext.AttachGltfValidatorLinks(); - // Bitmap of 16*16 pixels, containing FeatureID's (0, 1, 2, 3) in the red channel - // var img0 = "AAABAAIAAQADAAIAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAACAPwAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAgD8AAIA/AACAPwAAAAAAAAAAAACAPwAAAAA="; var img0 = "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"; var imageBytes0 = Convert.FromBase64String(img0); var imageBuilder0 = ImageBuilder.From(imageBytes0); - // var img1 = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABLUlEQVR42mVSSxbDIAh0GzUxKZrmCF3n/oerIx9pupgHIswAGtblE7bIKN0vqSOyXSOjPLAtktv9sCFxmcXj7EgsFj8zN00yYxrBZZJBRYk2LdC4WCDUfAdab7bpDm1lCyBW+7lpDnyNS34gcTQRltTPbAeEdFjcSQ0X9EOhGPYjhgLA7xh3kjxEEpMj1qQj7iAzAYoPELzYtuwK02M06WywAFDfX1MdJEoOtSZ7Allz1mYmWZDNL0pNF6ezu9jsQJUcNK7qzbWvMdSYQ8Jo7KKK8/uo4dxreHe0/HgF2/IqBen/za+Di69Sf8cZz5jmk+hcuhdd2tWLz8IE5MbFnRWT+yyU5vZJRtAOqlvq6MDeOrstu0UidsoO0Ak9xGwE+67+34salNEBSCxX7Bexg0rbq6TFvwAAAABJRU5ErkJggg=="; var img1 = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABLUlEQVR42mVSSxbDIAh0GzUxKZrmCF3n/oerIx9pupgHIswAGtblE7bIKN0vqSOyXSOjPLAtktv9sCFxmcXj7EgsFj8zN00yYxrBZZJBRYk2LdC4WCDUfAdab7bpDm1lCyBW+7lpDnyNS34gcTQRltTPbAeEdFjcSQ0X9EOhGPYjhgLA7xh3kjxEEpMj1qQj7iAzAYoPELzYtuwK02M06WywAFDfX1MdJEoOtSZ7Allz1mYmWZDNL0pNF6ezu9jsQJUcNK7qzbWvMdSYQ8Jo7KKK8/uo4dxreHe0/HgF2/IqBen/za+Di69Sf8cZz5jmk+hcuhdd2tWLz8IE5MbFnRWT+yyU5vZJRtAOqlvq6MDeOrstu0UidsoO0Ak9xGwE+67+34salNEBSCxX7Bexg0rbq6TFvwAAAABJRU5ErkJggg=="; var imageBytes1 = Convert.FromBase64String(img1); var imageBuilder1 = ImageBuilder.From(imageBytes1); From 72a56bedeec431d3e926c558c12072cb7a954696 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Thu, 21 Dec 2023 21:07:38 +0100 Subject: [PATCH 47/57] refactor validation --- .../Schema2/Ext.InstanceFeatures.cs | 10 +- .../Schema2/Ext.MeshFeatures.cs | 102 ++++++++++-------- .../Schema2/Ext.StructuralMetadataRoot.cs | 40 +++++-- 3 files changed, 97 insertions(+), 55 deletions(-) diff --git a/src/SharpGLTF.Cesium/Schema2/Ext.InstanceFeatures.cs b/src/SharpGLTF.Cesium/Schema2/Ext.InstanceFeatures.cs index 86bc94d6..b28f7116 100644 --- a/src/SharpGLTF.Cesium/Schema2/Ext.InstanceFeatures.cs +++ b/src/SharpGLTF.Cesium/Schema2/Ext.InstanceFeatures.cs @@ -26,13 +26,19 @@ public List FeatureIds } } - protected override void OnValidateContent(ValidationContext validate) + protected override void OnValidateReferences(ValidationContext validate) { var extInstanceFeatures = _node.GetExtension(); validate.NotNull(nameof(extInstanceFeatures), extInstanceFeatures); var extMeshGpInstancing = _node.GetExtension(); validate.NotNull(nameof(extMeshGpInstancing), extMeshGpInstancing); + base.OnValidateReferences(validate); + } + + protected override void OnValidateContent(ValidationContext validate) + { + var extInstanceFeatures = _node.GetExtension(); validate.NotNull(nameof(FeatureIds), extInstanceFeatures.FeatureIds); validate.IsTrue(nameof(FeatureIds), extInstanceFeatures.FeatureIds.Count > 0, "Instance FeatureIds has items"); @@ -97,6 +103,8 @@ public static void SetFeatureIds(this Node node, List var extMeshGpInstancing = node.Extensions.Where(item => item is MeshGpuInstancing).FirstOrDefault(); Guard.NotNull(extMeshGpInstancing, nameof(extMeshGpInstancing)); + // todo move validate in the validation function + foreach (var instanceFeatureId in instanceFeatureIds) { ValidateInstanceFeatureId(node, instanceFeatureId); diff --git a/src/SharpGLTF.Cesium/Schema2/Ext.MeshFeatures.cs b/src/SharpGLTF.Cesium/Schema2/Ext.MeshFeatures.cs index 14f667a3..a76f7c4f 100644 --- a/src/SharpGLTF.Cesium/Schema2/Ext.MeshFeatures.cs +++ b/src/SharpGLTF.Cesium/Schema2/Ext.MeshFeatures.cs @@ -24,15 +24,67 @@ public List FeatureIds } } + protected override void OnValidateReferences(ValidationContext validate) + { + foreach (var featureId in _featureIds) + { + if (featureId.Attribute.HasValue) + { + var expectedVertexAttribute = $"_FEATURE_ID_{featureId.Attribute}"; + Guard.NotNull(_meshPrimitive.GetVertexAccessor(expectedVertexAttribute), expectedVertexAttribute); + } + if (featureId.PropertyTable.HasValue) + { + var metadataExtension = _meshPrimitive.LogicalParent.LogicalParent.GetExtension(); + Guard.NotNull(metadataExtension, nameof(metadataExtension), "EXT_Structural_Metadata extension is not found."); + Guard.NotNull(metadataExtension.PropertyTables[featureId.PropertyTable.Value], nameof(featureId.PropertyTable), $"Property table index {featureId.PropertyTable.Value} does not exist"); + } + if (featureId.Texture != null) + { + var expectedTexCoordAttribute = $"TEXCOORD_{featureId.Texture.TextureCoordinate}"; + Guard.NotNull(_meshPrimitive.GetVertexAccessor(expectedTexCoordAttribute), expectedTexCoordAttribute); + + var modelRoot = _meshPrimitive.LogicalParent.LogicalParent; + validate.IsNullOrIndex(nameof(featureId.Texture), featureId.Texture.TextureCoordinate, modelRoot.LogicalTextures); + } + } + + base.OnValidateReferences(validate); + } + + protected override void OnValidateContent(ValidationContext validate) { 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"); + validate.NotNull(nameof(FeatureIds), _featureIds); + validate.IsTrue(nameof(FeatureIds), _featureIds.Count > 0, "FeatureIds has items"); - base.OnValidateContent(validate); + foreach (var featureId in _featureIds) + { + if (featureId.NullFeatureId.HasValue) + { + Guard.MustBeGreaterThanOrEqualTo((int)featureId.NullFeatureId, 0, nameof(featureId.NullFeatureId)); + } + if (featureId.Label != null) + { + var regex = "^[a-zA-Z_][a-zA-Z0-9_]*$"; + Guard.IsTrue(System.Text.RegularExpressions.Regex.IsMatch(featureId.Label, regex), nameof(featureId.Label)); + } + if (featureId.Attribute.HasValue) + { + Guard.MustBeGreaterThanOrEqualTo((int)featureId.Attribute, 0, nameof(featureId.Attribute)); + } + if (featureId.PropertyTable.HasValue) + { + Guard.MustBeGreaterThanOrEqualTo((int)featureId.PropertyTable, 0, nameof(featureId.PropertyTable)); + } + if (featureId.Texture != null) + { + Guard.MustBeGreaterThanOrEqualTo(featureId.Texture.TextureCoordinate, 0, nameof(featureId.Texture.TextureCoordinate)); + } + base.OnValidateContent(validate); + } } } @@ -121,49 +173,9 @@ public static void SetFeatureIds(this MeshPrimitive primitive, List(); return; } - 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)); - var metadataExtension = primitive.LogicalParent.LogicalParent.GetExtension(); - Guard.NotNull(metadataExtension, nameof(metadataExtension), "EXT_Structural_Metadata extension is not found."); - Guard.NotNull(metadataExtension.PropertyTables[item.PropertyTable.Value], nameof(item.PropertyTable), $"Property table index {item.PropertyTable.Value} does not exist"); - } - 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); - Guard.NotNull(primitive.LogicalParent.LogicalParent.LogicalTextures[item.Texture.Index], nameof(primitive.LogicalParent.LogicalParent.LogicalTextures), $"Texture {item.Texture.Index} does not exist"); - } - } } -} \ No newline at end of file +} diff --git a/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataRoot.cs b/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataRoot.cs index 79703583..eed25868 100644 --- a/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataRoot.cs +++ b/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataRoot.cs @@ -190,12 +190,42 @@ protected override void OnValidateReferences(ValidationContext validate) foreach (var propertyTable in PropertyTables) { + Guard.NotNull(Schema.Classes[propertyTable.Class], nameof(propertyTable.Class), $"Schema must have class {propertyTable.Class}"); + foreach (var property in propertyTable.Properties) { - Guard.IsTrue(Schema.Classes[propertyTable.Class].Properties.ContainsKey(property.Key), nameof(propertyTable.Properties), $"Property {property.Key} must be defined in schema"); + Guard.NotNull(Schema.Classes[propertyTable.Class].Properties[property.Key], nameof(property.Key), $"Schema must have property {property.Key}"); + + var values = property.Value.Values; + validate.IsNullOrIndex(nameof(propertyTable), values, modelRoot.LogicalBufferViews); + + if (property.Value.ArrayOffsets.HasValue) + { + var arrayOffsets = property.Value.ArrayOffsets.Value; + validate.IsNullOrIndex(nameof(propertyTable), arrayOffsets, modelRoot.LogicalBufferViews); + } + + if (property.Value.StringOffsets.HasValue) + { + var stringOffsets = property.Value.StringOffsets.Value; + validate.IsNullOrIndex(nameof(propertyTable), stringOffsets, modelRoot.LogicalBufferViews); + } } } + if(Schema!= null) + { + foreach (var @class in Schema.Classes) + { + foreach (var property in @class.Value.Properties) + { + if (property.Value.Type == ElementType.ENUM) + { + Guard.IsTrue(Schema.Enums.ContainsKey(property.Value.EnumType), nameof(property.Value.EnumType), $"Enum {property.Value.EnumType} must be defined in schema"); + } + } + } + } base.OnValidateReferences(validate); } @@ -233,14 +263,6 @@ protected override void OnValidateContent(ValidationContext result) Guard.IsFalse(Schema != null && SchemaUri != null, "Schema/SchemaUri", "Schema and SchemaUri cannot both be defined"); Guard.IsFalse(Schema == null && SchemaUri == null, "Schema/SchemaUri", "One of Schema and SchemaUri must be defined"); - - // check if the propertyTable id is set, then the propertyTable must be defined - - // loop through all the primitive and check if propertyTable is defined, then the propertyTable must be defined - - - - base.OnValidateContent(result); } } From 41a265bafaa22ba2ab1b57da08bac4f12add0276 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Fri, 22 Dec 2023 11:01:33 +0100 Subject: [PATCH 48/57] refactor validation --- .../Schema2/Ext.InstanceFeatures.cs | 81 ++++---- .../Schema2/Ext.MeshFeatures.cs | 1 - .../Schema2/Ext.StructuralMetadataRoot.cs | 195 ++++++++++++++---- .../ExtInstanceFeaturesTests.cs | 4 +- 4 files changed, 206 insertions(+), 75 deletions(-) diff --git a/src/SharpGLTF.Cesium/Schema2/Ext.InstanceFeatures.cs b/src/SharpGLTF.Cesium/Schema2/Ext.InstanceFeatures.cs index b28f7116..337d25c2 100644 --- a/src/SharpGLTF.Cesium/Schema2/Ext.InstanceFeatures.cs +++ b/src/SharpGLTF.Cesium/Schema2/Ext.InstanceFeatures.cs @@ -1,6 +1,5 @@ using SharpGLTF.Validation; using System.Collections.Generic; -using System.Linq; namespace SharpGLTF.Schema2 { public partial class MeshExtInstanceFeatures @@ -33,6 +32,25 @@ protected override void OnValidateReferences(ValidationContext validate) var extMeshGpInstancing = _node.GetExtension(); validate.NotNull(nameof(extMeshGpInstancing), extMeshGpInstancing); + foreach (var instanceFeatureId in FeatureIds) + { + if (instanceFeatureId.Attribute.HasValue) + { + var expectedVertexAttribute = $"_FEATURE_ID_{instanceFeatureId.Attribute}"; + var gpuInstancing = _node.GetGpuInstancing(); + var featureIdAccessors = gpuInstancing.GetAccessor(expectedVertexAttribute); + Guard.NotNull(featureIdAccessors, expectedVertexAttribute); + + if (instanceFeatureId.PropertyTable.HasValue) + { + var metadataExtension = _node.LogicalParent.GetExtension(); + Guard.NotNull(metadataExtension, nameof(metadataExtension), "EXT_Structural_Metadata extension is not found."); + Guard.NotNull(metadataExtension.PropertyTables[instanceFeatureId.PropertyTable.Value], nameof(instanceFeatureId.PropertyTable), $"Property table index {instanceFeatureId.PropertyTable.Value} does not exist"); + + } + } + } + base.OnValidateReferences(validate); } @@ -42,6 +60,31 @@ protected override void OnValidateContent(ValidationContext validate) validate.NotNull(nameof(FeatureIds), extInstanceFeatures.FeatureIds); validate.IsTrue(nameof(FeatureIds), extInstanceFeatures.FeatureIds.Count > 0, "Instance FeatureIds has items"); + + foreach (var instanceFeatureId in FeatureIds) + { + 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)); + } + if (instanceFeatureId.PropertyTable.HasValue) + { + Guard.MustBeGreaterThanOrEqualTo((int)instanceFeatureId.PropertyTable, 0, nameof(instanceFeatureId.PropertyTable)); + } + } + base.OnValidateContent(validate); } } @@ -100,45 +143,11 @@ public static void SetFeatureIds(this Node node, List Guard.NotNullOrEmpty(instanceFeatureIds, nameof(instanceFeatureIds)); - var extMeshGpInstancing = node.Extensions.Where(item => item is MeshGpuInstancing).FirstOrDefault(); + var extMeshGpInstancing = node.GetExtension(); Guard.NotNull(extMeshGpInstancing, nameof(extMeshGpInstancing)); - // todo move validate in the validation function - - foreach (var instanceFeatureId in instanceFeatureIds) - { - ValidateInstanceFeatureId(node, instanceFeatureId); - }; - var ext = node.UseExtension(); 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)); - } - } } } diff --git a/src/SharpGLTF.Cesium/Schema2/Ext.MeshFeatures.cs b/src/SharpGLTF.Cesium/Schema2/Ext.MeshFeatures.cs index a76f7c4f..c1a7c521 100644 --- a/src/SharpGLTF.Cesium/Schema2/Ext.MeshFeatures.cs +++ b/src/SharpGLTF.Cesium/Schema2/Ext.MeshFeatures.cs @@ -176,6 +176,5 @@ public static void SetFeatureIds(this MeshPrimitive primitive, List(); ext.FeatureIds = featureIds; } - } } diff --git a/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataRoot.cs b/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataRoot.cs index eed25868..62defca1 100644 --- a/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataRoot.cs +++ b/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataRoot.cs @@ -181,7 +181,7 @@ protected override void OnValidateReferences(ValidationContext validate) { foreach (var propertyTexture in PropertyTextures) { - foreach(var propertyTextureProperty in propertyTexture.Properties) + foreach (var propertyTextureProperty in propertyTexture.Properties) { var textureId = propertyTextureProperty.Value._LogicalTextureIndex; validate.IsNullOrIndex(nameof(propertyTexture), textureId, modelRoot.LogicalTextures); @@ -213,7 +213,7 @@ protected override void OnValidateReferences(ValidationContext validate) } } - if(Schema!= null) + if (Schema != null) { foreach (var @class in Schema.Classes) { @@ -233,7 +233,7 @@ protected override void OnValidateReferences(ValidationContext validate) protected override void OnValidateContent(ValidationContext result) { // check schema id is defined and valid - if (Schema!=null && !String.IsNullOrEmpty(Schema.Id)) + if (Schema != null && !String.IsNullOrEmpty(Schema.Id)) { var regex = "^[a-zA-Z_][a-zA-Z0-9_]*$"; Guard.IsTrue(System.Text.RegularExpressions.Regex.IsMatch(Schema.Id, regex), nameof(Schema.Id)); @@ -258,7 +258,7 @@ protected override void OnValidateContent(ValidationContext result) Guard.IsTrue(propertyTable.Count > 0, nameof(propertyTable.Count), "Count must be greater than 0"); Guard.IsTrue(propertyTable.Properties.Count > 0, nameof(propertyTable.Properties), "Properties must be defined"); } - + // Check one of schema or schemaUri is defined, but not both Guard.IsFalse(Schema != null && SchemaUri != null, "Schema/SchemaUri", "Schema and SchemaUri cannot both be defined"); Guard.IsFalse(Schema == null && SchemaUri == null, "Schema/SchemaUri", "One of Schema and SchemaUri must be defined"); @@ -310,13 +310,21 @@ public PropertyAttribute() public string Class { get { return _class; } - set { _class = value; } + set + { + if (value == null) { _class = null; return; } + _class = value; + } } public Dictionary Properties { get { return _properties; } - set { _properties = value; } + set + { + if (value == null) { _properties = null; return; } + _properties = value; + } } } @@ -326,7 +334,12 @@ public partial class PropertyAttributeProperty public string Attribute { get { return _attribute; } - set { _attribute = value; } + set + { + + if (value == null) { _attribute = null; return; } + _attribute = value; + } } } @@ -341,37 +354,61 @@ public StructuralMetadataSchema() public Dictionary Classes { get { return _classes; } - set { _classes = value; } + set + { + if (value == null) { _classes = null; return; } + _classes = value; + } } public string Id { get { return _id; } - set { _id = value; } + set + { + if (value == null) { _id = null; return; } + _id = value; + } } public string Version { get { return _version; } - set { _version = value; } + set + { + if (value == null) { _version = null; return; } + _version = value; + } } public string Name { get { return _name; } - set { _name = value; } + set + { + if (value == null) { _name = null; return; } + _name = value; + } } public string Description { get { return _description; } - set { _description = value; } + set + { + if (value == null) { _description = null; return; } + _description = value; + } } public Dictionary Enums { get { return _enums; } - set { _enums = value; } + set + { + if (value == null) { _enums = null; return; } + _enums = value; + } } } @@ -384,17 +421,29 @@ public StructuralMetadataEnum() public string Name { get { return _name; } - set { _name = value; } + set + { + if (value == null) { _name = null; return; } + _name = value; + } } public string Description { get { return _description; } - set { _description = value; } + set + { + if (value == null) { _description = null; return; } + _description = value; + } } public List Values { get { return _values; } - set { _values = value; } + set + { + if (value == null) { _values = null; return; } + _values = value; + } } } @@ -403,12 +452,19 @@ public partial class EnumValue public string Name { get { return _name; } - set { _name = value; } + set + { + if (value == null) { _name = null; return; } + _name = value; + } } public int Value { get { return _value; } - set { _value = value; } + set + { + _value = value; + } } } @@ -422,19 +478,31 @@ public StructuralMetadataClass() public Dictionary Properties { get { return _properties; } - set { _properties = value; } + set + { + if (value == null) { _properties = null; return; } + _properties = value; + } } public string Name { get { return _name; } - set { _name = value; } + set + { + if (value == null) { _name = null; return; } + _name = value; + } } public string Description { get { return _description; } - set { _description = value; } + set + { + if (value == null) { _description = null; return; } + _description = value; + } } } @@ -444,56 +512,92 @@ public partial class ClassProperty public string Name { get { return _name; } - set { _name = value; } + set + { + if (value == null) { _name = null; return; } + _name = value; + } } public string Description { get { return _description; } - set { _description = value; } + set + { + if (value == null) { _description = null; return; } + _description = value; + } } public ElementType Type { get { return _type; } - set { _type = value; } + set + { + _type = value; + } } public string EnumType { get { return _enumType; } - set { _enumType = value; } + set + { + if (value == null) { _enumType = null; return; } + _enumType = value; + } } public DataType? ComponentType { get { return _componentType; } - set { _componentType = value; } + set + { + if (value == null) { _componentType = null; return; } + _componentType = value; + } } public bool? Required { get { return _required; } - set { _required = value; } + set + { + + if (value == null) { _required = null; return; } + _required = value; + } } public bool? Normalized { get { return _normalized; } - set { _normalized = value; } + set + { + if (value == null) { _normalized = null; return; } + _normalized = value; + } } public bool? Array { get { return _array; } - set { _array = value; } + set + { + if (value == null) { _array = null; return; } + _array = value; + } } public int? Count { get { return _count; } - set { _count = value; } + set + { + if (value == null) { _count = null; return; } + _count = value; + } } } @@ -513,25 +617,38 @@ public PropertyTable(string Class, int Count, string Name = "") : this() public string Name { get { return _name; } - set { _name = value; } + set + { + if (value == null) { _name = null; return; } + _name = value; + } } public string Class { get { return _class; } - set { _class = value; } + set + { + if (value == null) { _class = null; return; } + _class = value; + } } public int Count { get { return _count; } - set { _count = value; } + set + { + _count = value; + } } public Dictionary Properties { get { return _properties; } - set { _properties = value; } + set { + if (value == null) { _properties = null; return; } + _properties = value; } } } @@ -546,13 +663,19 @@ public int Values public int? ArrayOffsets { get { return _arrayOffsets; } - set { _arrayOffsets = value; } + set { + + if (value == null) { _arrayOffsets= null; return; } + _arrayOffsets = value; } } public int? StringOffsets { get { return _stringOffsets; } - set { _stringOffsets = value; } + set { + + if (value == null) { _stringOffsets = null; return; } + _stringOffsets = value; } } } } diff --git a/tests/SharpGLTF.Cesium.Tests/ExtInstanceFeaturesTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtInstanceFeaturesTests.cs index 10929cbc..f24c1c1b 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtInstanceFeaturesTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtInstanceFeaturesTests.cs @@ -38,8 +38,8 @@ public void AddExtGpuInstanceFeatures() 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 featureId0 = new MeshExtInstanceFeatureID(2, 0, label: "Forests"); + var featureId1 = new MeshExtInstanceFeatureID(9, label: "Trees"); var featureIds = new List() { featureId0, featureId1 }; From 0183497a2844c5fa2abcc7531b93e5764c3c9850 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Fri, 22 Dec 2023 11:32:54 +0100 Subject: [PATCH 49/57] use WithMetallicRoughness(imageBuilder) --- .../ExtStructuralMetadataTests.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs index 54cf43dd..b5882cea 100644 --- a/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/ExtStructuralMetadataTests.cs @@ -43,7 +43,7 @@ public void FeatureIdTextureAndPropertytableTest() .WithDoubleSide(true) .WithAlpha(Materials.AlphaMode.OPAQUE) .WithMetallicRoughness(0, 1) - .WithSpecularFactor(imageBuilder1, 0); + .WithMetallicRoughness(imageBuilder1) ; var mesh = VBTexture1.CreateCompatibleMesh("mesh"); @@ -69,7 +69,7 @@ public void FeatureIdTextureAndPropertytableTest() buildingComponentsClass.Name = "Building components"; buildingComponentsClass.Properties.Add("component", new ClassProperty() { Name = "Component", Type = ElementType.STRING }); buildingComponentsClass.Properties.Add("yearBuilt", new ClassProperty() { Name = "Year built", Type = ElementType.SCALAR, ComponentType = DataType.INT16 }); - schema.Classes.Add ("buildingComponents", buildingComponentsClass); + schema.Classes.Add("buildingComponents", buildingComponentsClass); var propertyTable = new PropertyTable(); propertyTable.Name = "Example property table"; @@ -77,7 +77,7 @@ public void FeatureIdTextureAndPropertytableTest() propertyTable.Count = 4; var componentProperty = model.GetPropertyTableProperty(new List() { "Wall", "Door", "Roof", "Window" }); - var yearBuiltProperty = model.GetPropertyTableProperty(new List() { 1960, 1996, 1985, 2002}); + var yearBuiltProperty = model.GetPropertyTableProperty(new List() { 1960, 1996, 1985, 2002 }); propertyTable.Properties.Add("component", componentProperty); propertyTable.Properties.Add("yearBuilt", yearBuiltProperty); @@ -87,11 +87,12 @@ public void FeatureIdTextureAndPropertytableTest() var texture = new MeshExtMeshFeatureIDTexture(new List() { 0 }, 1, 0); var featureIdTexture = new MeshExtMeshFeatureID(4, texture: texture, propertyTable: 0); var featureIds = new List() { featureIdTexture }; - + var primitive = model.LogicalMeshes[0].Primitives[0]; primitive.SetFeatureIds(featureIds); var ctx = new ValidationResult(model, ValidationMode.Strict, true); + model.AttachToCurrentTest("cesium_ext_structural_metadata_featureid_texture_and_property_table.glb"); model.AttachToCurrentTest("cesium_ext_structural_metadata_featureid_texture_and_property_table.gltf"); model.AttachToCurrentTest("cesium_ext_structural_metadata_featureid_texture_and_property_table.plotly"); @@ -119,7 +120,7 @@ public void SimplePropertyTextureTest() .WithDoubleSide(true) .WithAlpha(Materials.AlphaMode.OPAQUE) .WithMetallicRoughness(0, 1) - .WithSpecularFactor(imageBuilder1, 0); + .WithMetallicRoughness(imageBuilder1); var mesh = VBTexture1.CreateCompatibleMesh("mesh"); var prim = mesh.UsePrimitive(material); @@ -253,7 +254,7 @@ public void MultipleFeatureIdsandPropertiesTest() schema.Classes.Add("exampleMetadataClass", exampleMetadataClass); - var vector3List = new List() { + var vector3List = new List() { new Vector3(3, 3.0999999046325684f, 3.200000047683716f), new Vector3(103, 103.0999999046325684f, 103.200000047683716f) @@ -344,7 +345,7 @@ public void FeatureIdAndPropertyTableTest() var matrix4x4PropertyTableProperty = model.GetPropertyTableProperty(matrix4x4List); examplePropertyTable.Properties.Add("example_MAT4_FLOAT32", matrix4x4PropertyTableProperty); - + model.SetPropertyTable(examplePropertyTable, schema); var featureId = new MeshExtMeshFeatureID(1, 0, 0); @@ -384,7 +385,7 @@ public void ComplexTypesTest() exampleMetadataClass.Description = "First example metadata class"; // class properties - + var uint8ArrayProperty = new ClassProperty(); uint8ArrayProperty.Name = "Example variable-length ARRAY normalized INT8 property"; uint8ArrayProperty.Description = "An example property, with type ARRAY, with component type UINT8, normalized, and variable length"; @@ -663,7 +664,7 @@ public void TriangleWithMetadataTest() var scene = new SceneBuilder(); scene.AddRigidMesh(mesh, Matrix4x4.Identity); var model = scene.ToGltf2(); - + var schema = new StructuralMetadataSchema(); schema.Id = "schema_001"; schema.Name = "schema 001"; From 5bd1e1af1d4c542abf2299a93dc7e1cec83f9528 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Fri, 22 Dec 2023 11:44:58 +0100 Subject: [PATCH 50/57] fixed null checks --- .../Schema2/Ext.InstanceFeatures.cs | 3 -- .../Schema2/Ext.MeshFeatures.cs | 1 - .../Ext.StructuralMetadataPrimitive.cs | 2 - .../Schema2/Ext.StructuralMetadataRoot.cs | 50 +++++-------------- 4 files changed, 12 insertions(+), 44 deletions(-) diff --git a/src/SharpGLTF.Cesium/Schema2/Ext.InstanceFeatures.cs b/src/SharpGLTF.Cesium/Schema2/Ext.InstanceFeatures.cs index 337d25c2..654225cc 100644 --- a/src/SharpGLTF.Cesium/Schema2/Ext.InstanceFeatures.cs +++ b/src/SharpGLTF.Cesium/Schema2/Ext.InstanceFeatures.cs @@ -19,8 +19,6 @@ public List FeatureIds } set { - if (value == null) { _featureIds = null; return; } - _featureIds = value; } } @@ -46,7 +44,6 @@ protected override void OnValidateReferences(ValidationContext validate) var metadataExtension = _node.LogicalParent.GetExtension(); Guard.NotNull(metadataExtension, nameof(metadataExtension), "EXT_Structural_Metadata extension is not found."); Guard.NotNull(metadataExtension.PropertyTables[instanceFeatureId.PropertyTable.Value], nameof(instanceFeatureId.PropertyTable), $"Property table index {instanceFeatureId.PropertyTable.Value} does not exist"); - } } } diff --git a/src/SharpGLTF.Cesium/Schema2/Ext.MeshFeatures.cs b/src/SharpGLTF.Cesium/Schema2/Ext.MeshFeatures.cs index c1a7c521..cfc397dd 100644 --- a/src/SharpGLTF.Cesium/Schema2/Ext.MeshFeatures.cs +++ b/src/SharpGLTF.Cesium/Schema2/Ext.MeshFeatures.cs @@ -19,7 +19,6 @@ public List FeatureIds get => _featureIds; set { - if (value == null) { _featureIds = null; return; } _featureIds = value; } } diff --git a/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataPrimitive.cs b/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataPrimitive.cs index b073b9af..12919175 100644 --- a/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataPrimitive.cs +++ b/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataPrimitive.cs @@ -22,7 +22,6 @@ public List PropertyTextures } set { - if (value == null) { _propertyTextures = null; return; } _propertyTextures = value; } } @@ -35,7 +34,6 @@ public List PropertyAttributes } set { - if (value == null) { _propertyAttributes = null; return; } _propertyAttributes = value; } } diff --git a/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataRoot.cs b/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataRoot.cs index 62defca1..b6f4965a 100644 --- a/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataRoot.cs +++ b/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataRoot.cs @@ -312,7 +312,6 @@ public string Class get { return _class; } set { - if (value == null) { _class = null; return; } _class = value; } } @@ -322,7 +321,6 @@ public Dictionary Properties get { return _properties; } set { - if (value == null) { _properties = null; return; } _properties = value; } } @@ -336,8 +334,6 @@ public string Attribute get { return _attribute; } set { - - if (value == null) { _attribute = null; return; } _attribute = value; } } @@ -356,7 +352,6 @@ public Dictionary Classes get { return _classes; } set { - if (value == null) { _classes = null; return; } _classes = value; } } @@ -366,7 +361,6 @@ public string Id get { return _id; } set { - if (value == null) { _id = null; return; } _id = value; } } @@ -376,7 +370,6 @@ public string Version get { return _version; } set { - if (value == null) { _version = null; return; } _version = value; } } @@ -386,7 +379,6 @@ public string Name get { return _name; } set { - if (value == null) { _name = null; return; } _name = value; } } @@ -396,7 +388,6 @@ public string Description get { return _description; } set { - if (value == null) { _description = null; return; } _description = value; } } @@ -406,7 +397,6 @@ public Dictionary Enums get { return _enums; } set { - if (value == null) { _enums = null; return; } _enums = value; } } @@ -423,7 +413,6 @@ public string Name get { return _name; } set { - if (value == null) { _name = null; return; } _name = value; } } @@ -432,7 +421,6 @@ public string Description get { return _description; } set { - if (value == null) { _description = null; return; } _description = value; } } @@ -441,7 +429,6 @@ public List Values get { return _values; } set { - if (value == null) { _values = null; return; } _values = value; } } @@ -454,7 +441,6 @@ public string Name get { return _name; } set { - if (value == null) { _name = null; return; } _name = value; } } @@ -480,7 +466,6 @@ public Dictionary Properties get { return _properties; } set { - if (value == null) { _properties = null; return; } _properties = value; } } @@ -500,7 +485,6 @@ public string Description get { return _description; } set { - if (value == null) { _description = null; return; } _description = value; } } @@ -514,7 +498,6 @@ public string Name get { return _name; } set { - if (value == null) { _name = null; return; } _name = value; } } @@ -524,7 +507,6 @@ public string Description get { return _description; } set { - if (value == null) { _description = null; return; } _description = value; } } @@ -543,7 +525,6 @@ public string EnumType get { return _enumType; } set { - if (value == null) { _enumType = null; return; } _enumType = value; } } @@ -553,7 +534,6 @@ public DataType? ComponentType get { return _componentType; } set { - if (value == null) { _componentType = null; return; } _componentType = value; } } @@ -563,8 +543,6 @@ public bool? Required get { return _required; } set { - - if (value == null) { _required = null; return; } _required = value; } } @@ -574,7 +552,6 @@ public bool? Normalized get { return _normalized; } set { - if (value == null) { _normalized = null; return; } _normalized = value; } } @@ -584,7 +561,6 @@ public bool? Array get { return _array; } set { - if (value == null) { _array = null; return; } _array = value; } @@ -595,7 +571,6 @@ public int? Count get { return _count; } set { - if (value == null) { _count = null; return; } _count = value; } } @@ -619,7 +594,6 @@ public string Name get { return _name; } set { - if (value == null) { _name = null; return; } _name = value; } } @@ -629,7 +603,6 @@ public string Class get { return _class; } set { - if (value == null) { _class = null; return; } _class = value; } } @@ -646,9 +619,10 @@ public int Count public Dictionary Properties { get { return _properties; } - set { - if (value == null) { _properties = null; return; } - _properties = value; } + set + { + _properties = value; + } } } @@ -663,19 +637,19 @@ public int Values public int? ArrayOffsets { get { return _arrayOffsets; } - set { - - if (value == null) { _arrayOffsets= null; return; } - _arrayOffsets = value; } + set + { + _arrayOffsets = value; + } } public int? StringOffsets { get { return _stringOffsets; } - set { - - if (value == null) { _stringOffsets = null; return; } - _stringOffsets = value; } + set + { + _stringOffsets = value; + } } } } From 67694cc2e910f635a849417d6e37a547049bcab2 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Fri, 22 Dec 2023 13:14:13 +0100 Subject: [PATCH 51/57] add validation on count --- src/SharpGLTF.Cesium/Schema2/CesiumExtensions.cs | 2 -- .../Schema2/Ext.StructuralMetadataRoot.cs | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/SharpGLTF.Cesium/Schema2/CesiumExtensions.cs b/src/SharpGLTF.Cesium/Schema2/CesiumExtensions.cs index 10e0478a..9c49edfa 100644 --- a/src/SharpGLTF.Cesium/Schema2/CesiumExtensions.cs +++ b/src/SharpGLTF.Cesium/Schema2/CesiumExtensions.cs @@ -21,8 +21,6 @@ public static void RegisterExtensions() ExtensionsFactory.RegisterExtension("EXT_mesh_features"); ExtensionsFactory.RegisterExtension("EXT_structural_metadata"); ExtensionsFactory.RegisterExtension("EXT_structural_metadata"); - - // todo: register the rest of the extensions } } } diff --git a/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataRoot.cs b/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataRoot.cs index b6f4965a..238544c5 100644 --- a/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataRoot.cs +++ b/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataRoot.cs @@ -237,6 +237,20 @@ protected override void OnValidateContent(ValidationContext result) { var regex = "^[a-zA-Z_][a-zA-Z0-9_]*$"; Guard.IsTrue(System.Text.RegularExpressions.Regex.IsMatch(Schema.Id, regex), nameof(Schema.Id)); + + + foreach(var _class in Schema.Classes) + { + Guard.IsTrue(System.Text.RegularExpressions.Regex.IsMatch(_class.Key, regex), nameof(_class.Key)); + + foreach(var property in _class.Value.Properties) + { + if (property.Value.Count.HasValue) + { + Guard.MustBeGreaterThanOrEqualTo(property.Value.Count.Value, 2, nameof(property.Value.Count)); + } + } + } } foreach (var propertyTexture in PropertyTextures) From c570bd551a1a9184bd667227a201b079e00eaeb9 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Fri, 22 Dec 2023 14:49:14 +0100 Subject: [PATCH 52/57] fix references check in ext_instance_features --- src/SharpGLTF.Cesium/Schema2/Ext.InstanceFeatures.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/SharpGLTF.Cesium/Schema2/Ext.InstanceFeatures.cs b/src/SharpGLTF.Cesium/Schema2/Ext.InstanceFeatures.cs index 654225cc..b3c7b6b6 100644 --- a/src/SharpGLTF.Cesium/Schema2/Ext.InstanceFeatures.cs +++ b/src/SharpGLTF.Cesium/Schema2/Ext.InstanceFeatures.cs @@ -38,13 +38,13 @@ protected override void OnValidateReferences(ValidationContext validate) var gpuInstancing = _node.GetGpuInstancing(); var featureIdAccessors = gpuInstancing.GetAccessor(expectedVertexAttribute); Guard.NotNull(featureIdAccessors, expectedVertexAttribute); + } - if (instanceFeatureId.PropertyTable.HasValue) - { - var metadataExtension = _node.LogicalParent.GetExtension(); - Guard.NotNull(metadataExtension, nameof(metadataExtension), "EXT_Structural_Metadata extension is not found."); - Guard.NotNull(metadataExtension.PropertyTables[instanceFeatureId.PropertyTable.Value], nameof(instanceFeatureId.PropertyTable), $"Property table index {instanceFeatureId.PropertyTable.Value} does not exist"); - } + if (instanceFeatureId.PropertyTable.HasValue) + { + var metadataExtension = _node.LogicalParent.GetExtension(); + Guard.NotNull(metadataExtension, nameof(metadataExtension), "EXT_Structural_Metadata extension is not found."); + Guard.NotNull(metadataExtension.PropertyTables[instanceFeatureId.PropertyTable.Value], nameof(instanceFeatureId.PropertyTable), $"Property table index {instanceFeatureId.PropertyTable.Value} does not exist"); } } From 13fd9d7cadba6b7a25dcdfd05da18e9ee7037248 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Wed, 27 Dec 2023 10:06:45 +0100 Subject: [PATCH 53/57] fix verbose codegen project --- .../SharpGLTF.CodeGen.csproj | 74 +------------------ 1 file changed, 1 insertion(+), 73 deletions(-) diff --git a/build/SharpGLTF.CodeGen/SharpGLTF.CodeGen.csproj b/build/SharpGLTF.CodeGen/SharpGLTF.CodeGen.csproj index 8b7f524c..4b519628 100644 --- a/build/SharpGLTF.CodeGen/SharpGLTF.CodeGen.csproj +++ b/build/SharpGLTF.CodeGen/SharpGLTF.CodeGen.csproj @@ -13,78 +13,6 @@ + - - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - From 187e405b3745ab3b32c2e3b280843ce6b2a2d5ed Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Wed, 27 Dec 2023 10:18:27 +0100 Subject: [PATCH 54/57] remove Activator.CreateInstance to get size --- src/SharpGLTF.Cesium/BinaryTable.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SharpGLTF.Cesium/BinaryTable.cs b/src/SharpGLTF.Cesium/BinaryTable.cs index 86e7805e..c823a3de 100644 --- a/src/SharpGLTF.Cesium/BinaryTable.cs +++ b/src/SharpGLTF.Cesium/BinaryTable.cs @@ -201,7 +201,7 @@ public static int GetSize() Guard.IsTrue(isValueType, nameof(T), "T must be a value type"); var type = typeof(T); - int size = Marshal.SizeOf(Activator.CreateInstance(type)); + var size = Marshal.SizeOf(); return size; } } From 6dfc9a257e614530686b1525ec0331c4dca025d0 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Wed, 27 Dec 2023 10:51:42 +0100 Subject: [PATCH 55/57] use RuntimeHelpers.IsReferenceOrContainsReferences --- src/SharpGLTF.Cesium/BinaryTable.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/SharpGLTF.Cesium/BinaryTable.cs b/src/SharpGLTF.Cesium/BinaryTable.cs index c823a3de..ea177cf1 100644 --- a/src/SharpGLTF.Cesium/BinaryTable.cs +++ b/src/SharpGLTF.Cesium/BinaryTable.cs @@ -197,7 +197,11 @@ public static List GetArrayOffsets(List> values) public static int GetSize() { - var isValueType = typeof(T).IsValueType; + #if NETSTANDARD2_1_OR_GREATER + var isValueType = !System.Runtime.CompilerServices.RuntimeHelpers.IsReferenceOrContainsReferences(); + #else + var isValueType = typeof(T).IsValueType; + #endif Guard.IsTrue(isValueType, nameof(T), "T must be a value type"); var type = typeof(T); From e75dd9d40b55401462a9c2f5562d8d2d7df5824a Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Wed, 27 Dec 2023 11:15:33 +0100 Subject: [PATCH 56/57] reverse target framework netstandard20 check --- src/SharpGLTF.Cesium/BinaryTable.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/SharpGLTF.Cesium/BinaryTable.cs b/src/SharpGLTF.Cesium/BinaryTable.cs index ea177cf1..259e7b8e 100644 --- a/src/SharpGLTF.Cesium/BinaryTable.cs +++ b/src/SharpGLTF.Cesium/BinaryTable.cs @@ -197,11 +197,12 @@ public static List GetArrayOffsets(List> values) public static int GetSize() { - #if NETSTANDARD2_1_OR_GREATER - var isValueType = !System.Runtime.CompilerServices.RuntimeHelpers.IsReferenceOrContainsReferences(); - #else + #if NETSTANDARD2_0 var isValueType = typeof(T).IsValueType; + #else + var isValueType = !System.Runtime.CompilerServices.RuntimeHelpers.IsReferenceOrContainsReferences(); #endif + Guard.IsTrue(isValueType, nameof(T), "T must be a value type"); var type = typeof(T); From a9af095d2c03a2d16275d9f283e46ab4980fd4d6 Mon Sep 17 00:00:00 2001 From: Bert Temme Date: Wed, 27 Dec 2023 12:17:19 +0100 Subject: [PATCH 57/57] move BinaryTable class to Memory workspace --- .../{ => Memory}/BinaryTable.cs | 18 +++++++++--------- .../Schema2/Ext.StructuralMetadataRoot.cs | 1 + .../{ => Memory}/BinaryTableTests.cs | 18 +++++++++--------- 3 files changed, 19 insertions(+), 18 deletions(-) rename src/SharpGLTF.Cesium/{ => Memory}/BinaryTable.cs (95%) rename tests/SharpGLTF.Cesium.Tests/{ => Memory}/BinaryTableTests.cs (91%) diff --git a/src/SharpGLTF.Cesium/BinaryTable.cs b/src/SharpGLTF.Cesium/Memory/BinaryTable.cs similarity index 95% rename from src/SharpGLTF.Cesium/BinaryTable.cs rename to src/SharpGLTF.Cesium/Memory/BinaryTable.cs index 259e7b8e..a8795b02 100644 --- a/src/SharpGLTF.Cesium/BinaryTable.cs +++ b/src/SharpGLTF.Cesium/Memory/BinaryTable.cs @@ -6,7 +6,7 @@ using System.Runtime.InteropServices; using System.Text; -namespace SharpGLTF +namespace SharpGLTF.Memory { /// /// Function for converting data into binary buffers @@ -54,7 +54,7 @@ public static byte[] GetBytes(IReadOnlyList values) { return Vector4ToBytes(values); } - else if(typeof(T) == typeof(Matrix4x4)) + else if (typeof(T) == typeof(Matrix4x4)) { return Matrix4x4ToBytes(values); } @@ -166,14 +166,14 @@ public static List GetStringOffsets(List values) public static List GetStringOffsets(List> values) { - var offsets = new List() {}; + var offsets = new List() { }; foreach (var arr in values) { var arrOffsets = GetStringOffsets(arr); var last = offsets.LastOrDefault(); foreach (var offset in arrOffsets) { - if(!offsets.Contains(last + offset)) + if (!offsets.Contains(last + offset)) { offsets.Add(last + offset); } @@ -197,12 +197,12 @@ public static List GetArrayOffsets(List> values) public static int GetSize() { - #if NETSTANDARD2_0 +#if NETSTANDARD2_0 var isValueType = typeof(T).IsValueType; - #else - var isValueType = !System.Runtime.CompilerServices.RuntimeHelpers.IsReferenceOrContainsReferences(); - #endif - +#else + var isValueType = !System.Runtime.CompilerServices.RuntimeHelpers.IsReferenceOrContainsReferences(); +#endif + Guard.IsTrue(isValueType, nameof(T), "T must be a value type"); var type = typeof(T); diff --git a/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataRoot.cs b/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataRoot.cs index 238544c5..af298deb 100644 --- a/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataRoot.cs +++ b/src/SharpGLTF.Cesium/Schema2/Ext.StructuralMetadataRoot.cs @@ -1,4 +1,5 @@ using OneOf; +using SharpGLTF.Memory; using SharpGLTF.Validation; using System; using System.Collections.Generic; diff --git a/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs b/tests/SharpGLTF.Cesium.Tests/Memory/BinaryTableTests.cs similarity index 91% rename from tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs rename to tests/SharpGLTF.Cesium.Tests/Memory/BinaryTableTests.cs index 845bcf93..e18b99d5 100644 --- a/tests/SharpGLTF.Cesium.Tests/BinaryTableTests.cs +++ b/tests/SharpGLTF.Cesium.Tests/Memory/BinaryTableTests.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Globalization; -namespace SharpGLTF +namespace SharpGLTF.Memory { public class BinaryTableTests { @@ -66,9 +66,9 @@ public void ConvertMatrix4x4ToBytes() public void TestGetArrayOffset() { // arrange - var list0 = new List(){ "hello", "world!"}; - var list1 = new List(){"test", "testtest"}; - var arrays = new List>() { list0, list1 }; + var list0 = new List() { "hello", "world!" }; + var list1 = new List() { "test", "testtest" }; + var arrays = new List>() { list0, list1 }; // act var arrayOffsets = BinaryTable.GetArrayOffsets(arrays); @@ -81,7 +81,7 @@ public void TestGetArrayOffset() var l1 = list1.Count; Assert.That(arrayOffsets[1], Is.EqualTo(l0)); - Assert.That(arrayOffsets[2], Is.EqualTo(l0+l1)); + Assert.That(arrayOffsets[2], Is.EqualTo(l0 + l1)); Assert.That(stringOffsets.Count, Is.EqualTo(list0.Count + list1.Count + 1)); Assert.That(stringOffsets[0], Is.EqualTo(0)); @@ -99,13 +99,13 @@ public void TestBinaryConversion() bytes = BinaryTable.GetBytes(GetTestArray()); Assert.That(bytes.Length, Is.EqualTo(BinaryTable.GetSize() * 2)); - + bytes = BinaryTable.GetBytes(new List() { "a", "b" }); Assert.That(bytes.Length, Is.EqualTo(2)); - bytes = BinaryTable.GetBytes(new List() { true, false }); + bytes = BinaryTable.GetBytes(new List() { true, false }); Assert.That(bytes.Length, Is.EqualTo(1)); - + var bits = new System.Collections.BitArray(bytes); Assert.That(bits[0] == true); Assert.That(bits[1] == false); @@ -119,7 +119,7 @@ public void TestBinaryConversion() private static List GetTestArray() { var l = new List(); - l.Add((T)Convert.ChangeType(0, typeof(T),CultureInfo.InvariantCulture)); + l.Add((T)Convert.ChangeType(0, typeof(T), CultureInfo.InvariantCulture)); l.Add((T)Convert.ChangeType(1, typeof(T), CultureInfo.InvariantCulture)); return l; }

E_ke*hH?~wr?}nYcm#q?cCY^OPXRqwu?xUUU$f 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 literal 0 HcmV?d00001 diff --git a/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/figures/property-attribute.svg b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/figures/property-attribute.svg new file mode 100644 index 00000000..24f94036 --- /dev/null +++ b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/figures/property-attribute.svg @@ -0,0 +1,474 @@ + + + + + + + + + + POSITION + _DIRECTION + _MAGNITUDE + + + + + ( 0.59,-0.16, -0.80),( 0.30, 0.69, 0.66),(0.98, 0.14, -0.13), ... + + + + + 0.538, 2.011, 1.319, ... + Magnitude + + + + + (1.3, 8.3, 2.4 ),(5.5, 4.2, 6.0 ),(2.1, 1.1, 9.8 ), ... + Metadata stored for each point: + Metadata storage in vertex attribute accessors: + Point cloud: + + + + + + + 0 + + + 1 + + + + + + ... + + ... + + ... + + ... + 2 + + + + + + Position + Direction + + diff --git a/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/figures/property-table.png b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/figures/property-table.png new file mode 100644 index 0000000000000000000000000000000000000000..f0dc4de4c295ecc0fd2fc2800e7e6bfd3ac7ee22 GIT binary patch literal 78190 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 c^89~QX@gll+K + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ID: + 0 + 1 + 2 + 3 + 4 + 5 + ... + + + + + + + + 0 + 1 + 2 + + + ID + + + + "Oak" + + species + "Pine" + "Maple" + age + + + + + height + + + + + diameter + 8.1 + - + - + - + 19 + 34 + 22 + 11.7 + 9.3 + + + + + + + 3 + 4 + 5 + + + + "Pine" + "Pine" + "Oak" + + ... + + ... + + ... + + + + + ... + + + + + 7.4 + - + - + - + - + 38 + 29 + 43 + 9.1 + 13.3 + + + + diff --git a/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/figures/property-texture.png b/build/SharpGLTF.CodeGen/Schemas/EXT_structural_metadata/figures/property-texture.png new file mode 100644 index 0000000000000000000000000000000000000000..db8958212bdfbde98c5897983dae965f6815f1c7 GIT binary patch literal 88625 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