diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a4c2ef..62ec6e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### New Features +* Added support for the `KHR_node_visibility` extension. + * Added new `NODE_SKINNED_MESH_PARENT_TRANSFORMS` validation warning. ### Changes diff --git a/lib/src/ext/KHR_node_visibility/khr_node_visibility.dart b/lib/src/ext/KHR_node_visibility/khr_node_visibility.dart new file mode 100644 index 0000000..ea886c2 --- /dev/null +++ b/lib/src/ext/KHR_node_visibility/khr_node_visibility.dart @@ -0,0 +1,34 @@ +library gltf.extensions.khr_node_visibility; + +import 'package:gltf/src/base/gltf_property.dart'; +import 'package:gltf/src/ext/extensions.dart'; + +const String KHR_NODE_VISIBILITY = 'KHR_node_visibility'; +const String VISIBLE = 'visible'; + +const List KHR_NODE_VISIBILITY_MEMBERS = [ + VISIBLE, +]; + +class KhrNodeVisibility extends GltfProperty { + final bool visible; + + KhrNodeVisibility._( + this.visible, Map extensions, Object extras) + : super(extensions, extras); + + static KhrNodeVisibility fromMap(Map map, Context context) { + if (context.validate) { + checkMembers(map, KHR_NODE_VISIBILITY_MEMBERS, context); + } + return KhrNodeVisibility._( + getBool(map, VISIBLE, context, def: true), + getExtensions(map, KhrNodeVisibility, context), + getExtras(map, context)); + } +} + +const Extension khrNodeVisibilityExtension = + Extension(KHR_NODE_VISIBILITY, { + Node: ExtensionDescriptor(KhrNodeVisibility.fromMap), +}); diff --git a/lib/src/ext/extensions.dart b/lib/src/ext/extensions.dart index 27e8e33..3e4368e 100644 --- a/lib/src/ext/extensions.dart +++ b/lib/src/ext/extensions.dart @@ -34,6 +34,7 @@ import 'package:gltf/src/ext/KHR_materials_unlit/khr_materials_unlit.dart'; import 'package:gltf/src/ext/KHR_materials_variants/KHR_materials_variants.dart'; import 'package:gltf/src/ext/KHR_materials_volume/khr_materials_volume.dart'; import 'package:gltf/src/ext/KHR_mesh_quantization/khr_mesh_quantization.dart'; +import 'package:gltf/src/ext/KHR_node_visibility/khr_node_visibility.dart'; import 'package:gltf/src/ext/KHR_texture_transform/khr_texture_transform.dart'; import 'package:gltf/src/hash.dart'; import 'package:meta/meta.dart'; @@ -56,6 +57,7 @@ export 'package:gltf/src/ext/KHR_materials_unlit/khr_materials_unlit.dart'; export 'package:gltf/src/ext/KHR_materials_variants/KHR_materials_variants.dart'; export 'package:gltf/src/ext/KHR_materials_volume/khr_materials_volume.dart'; export 'package:gltf/src/ext/KHR_mesh_quantization/khr_mesh_quantization.dart'; +export 'package:gltf/src/ext/KHR_node_visibility/khr_node_visibility.dart'; export 'package:gltf/src/ext/KHR_texture_transform/khr_texture_transform.dart'; class Extension { @@ -120,5 +122,6 @@ const List kDefaultExtensions = [ khrMaterialsVariantsExtension, khrMaterialsVolumeExtension, khrMeshQuantizationExtension, + khrNodeVisibilityExtension, khrTextureTransformExtension ]; diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 3ed0168..92e7f83 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -64,17 +64,22 @@ int getIndex(Map map, String name, Context context, return -1; } -bool getBool(Map map, String name, Context context) { +bool getBool(Map map, String name, Context context, + {bool req = false, bool def = false}) { final value = _getGuarded(map, name, _kBoolean, context); if (value == null) { - return false; + if (!req) { + return def; + } + context.addIssue(SchemaError.undefinedProperty, args: [name]); + return def; } if (value is bool) { return value; } context .addIssue(SchemaError.typeMismatch, name: name, args: [value, _kBoolean]); - return false; + return def; } int getUint(Map map, String name, Context context, diff --git a/test/ext/KHR_node_visibility/assets.json b/test/ext/KHR_node_visibility/assets.json new file mode 100644 index 0000000..f39e663 --- /dev/null +++ b/test/ext/KHR_node_visibility/assets.json @@ -0,0 +1,10 @@ +{ + "node": { + "name": "node.KHR_node_visibility", + "tests": { + "custom_property.gltf": "Custom property", + "unexpected_extension.gltf": "Unexpected extension object location", + "valid.gltf": "Valid" + } + } +} diff --git a/test/ext/KHR_node_visibility/data/node/custom_property.gltf b/test/ext/KHR_node_visibility/data/node/custom_property.gltf new file mode 100644 index 0000000..089783a --- /dev/null +++ b/test/ext/KHR_node_visibility/data/node/custom_property.gltf @@ -0,0 +1,13 @@ +{ + "asset": { "version": "2.0" }, + "extensionsUsed": ["KHR_node_visibility"], + "nodes": [ + { + "extensions": { + "KHR_node_visibility": { + "customProperty": true + } + } + } + ] +} diff --git a/test/ext/KHR_node_visibility/data/node/custom_property.gltf.report.json b/test/ext/KHR_node_visibility/data/node/custom_property.gltf.report.json new file mode 100644 index 0000000..66c13f3 --- /dev/null +++ b/test/ext/KHR_node_visibility/data/node/custom_property.gltf.report.json @@ -0,0 +1,41 @@ +{ + "uri": "test/ext/KHR_node_visibility/data/node/custom_property.gltf", + "mimeType": "model/gltf+json", + "issues": { + "numErrors": 0, + "numWarnings": 1, + "numInfos": 1, + "numHints": 0, + "messages": [ + { + "code": "UNEXPECTED_PROPERTY", + "message": "Unexpected property.", + "severity": 1, + "pointer": "/nodes/0/extensions/KHR_node_visibility/customProperty" + }, + { + "code": "UNUSED_OBJECT", + "message": "This object may be unused.", + "severity": 2, + "pointer": "/nodes/0" + } + ], + "truncated": false + }, + "info": { + "version": "2.0", + "extensionsUsed": ["KHR_node_visibility"], + "animationCount": 0, + "materialCount": 0, + "hasMorphTargets": false, + "hasSkins": false, + "hasTextures": false, + "hasDefaultScene": false, + "drawCallCount": 0, + "totalVertexCount": 0, + "totalTriangleCount": 0, + "maxUVs": 0, + "maxInfluences": 0, + "maxAttributes": 0 + } +} diff --git a/test/ext/KHR_node_visibility/data/node/unexpected_extension.gltf b/test/ext/KHR_node_visibility/data/node/unexpected_extension.gltf new file mode 100644 index 0000000..371bacf --- /dev/null +++ b/test/ext/KHR_node_visibility/data/node/unexpected_extension.gltf @@ -0,0 +1,13 @@ +{ + "asset": { "version": "2.0" }, + "extensionsUsed": ["KHR_node_visibility"], + "materials": [ + { + "extensions": { + "KHR_node_visibility": { + "visible": true + } + } + } + ] +} diff --git a/test/ext/KHR_node_visibility/data/node/unexpected_extension.gltf.report.json b/test/ext/KHR_node_visibility/data/node/unexpected_extension.gltf.report.json new file mode 100644 index 0000000..3a8d95e --- /dev/null +++ b/test/ext/KHR_node_visibility/data/node/unexpected_extension.gltf.report.json @@ -0,0 +1,41 @@ +{ + "uri": "test/ext/KHR_node_visibility/data/node/unexpected_extension.gltf", + "mimeType": "model/gltf+json", + "issues": { + "numErrors": 1, + "numWarnings": 0, + "numInfos": 1, + "numHints": 0, + "messages": [ + { + "code": "UNEXPECTED_EXTENSION_OBJECT", + "message": "Unexpected location for this extension.", + "severity": 0, + "pointer": "/materials/0/extensions/KHR_node_visibility" + }, + { + "code": "UNUSED_OBJECT", + "message": "This object may be unused.", + "severity": 2, + "pointer": "/materials/0" + } + ], + "truncated": false + }, + "info": { + "version": "2.0", + "extensionsUsed": ["KHR_node_visibility"], + "animationCount": 0, + "materialCount": 1, + "hasMorphTargets": false, + "hasSkins": false, + "hasTextures": false, + "hasDefaultScene": false, + "drawCallCount": 0, + "totalVertexCount": 0, + "totalTriangleCount": 0, + "maxUVs": 0, + "maxInfluences": 0, + "maxAttributes": 0 + } +} diff --git a/test/ext/KHR_node_visibility/data/node/valid.gltf b/test/ext/KHR_node_visibility/data/node/valid.gltf new file mode 100644 index 0000000..35ddc5b --- /dev/null +++ b/test/ext/KHR_node_visibility/data/node/valid.gltf @@ -0,0 +1,9 @@ +{ + "asset": { "version": "2.0" }, + "extensionsUsed": ["KHR_node_visibility"], + "nodes": [ + { "extensions": { "KHR_node_visibility": { "visible": false } }, "name": "InvisibleCube" }, + { "extensions": { "KHR_node_visibility": { "visible": true } }, "name": "VisibleCube" }, + { "extensions": { "KHR_node_visibility": {} }, "name": "EmptyExtensionObject" } + ] +} diff --git a/test/ext/KHR_node_visibility/data/node/valid.gltf.report.json b/test/ext/KHR_node_visibility/data/node/valid.gltf.report.json new file mode 100644 index 0000000..348b081 --- /dev/null +++ b/test/ext/KHR_node_visibility/data/node/valid.gltf.report.json @@ -0,0 +1,47 @@ +{ + "uri": "test/ext/KHR_node_visibility/data/node/valid.gltf", + "mimeType": "model/gltf+json", + "issues": { + "numErrors": 0, + "numWarnings": 0, + "numInfos": 3, + "numHints": 0, + "messages": [ + { + "code": "UNUSED_OBJECT", + "message": "This object may be unused.", + "severity": 2, + "pointer": "/nodes/0" + }, + { + "code": "UNUSED_OBJECT", + "message": "This object may be unused.", + "severity": 2, + "pointer": "/nodes/1" + }, + { + "code": "UNUSED_OBJECT", + "message": "This object may be unused.", + "severity": 2, + "pointer": "/nodes/2" + } + ], + "truncated": false + }, + "info": { + "version": "2.0", + "extensionsUsed": ["KHR_node_visibility"], + "animationCount": 0, + "materialCount": 0, + "hasMorphTargets": false, + "hasSkins": false, + "hasTextures": false, + "hasDefaultScene": false, + "drawCallCount": 0, + "totalVertexCount": 0, + "totalTriangleCount": 0, + "maxUVs": 0, + "maxInfluences": 0, + "maxAttributes": 0 + } +} diff --git a/test/ext/KHR_node_visibility/khr_node_visibility_test.dart b/test/ext/KHR_node_visibility/khr_node_visibility_test.dart new file mode 100644 index 0000000..2078772 --- /dev/null +++ b/test/ext/KHR_node_visibility/khr_node_visibility_test.dart @@ -0,0 +1,46 @@ +/* + * # Copyright (c) 2016-2026 The Khronos Group Inc. + * # + * # Licensed under the Apache License, Version 2.0 (the "License"); + * # you may not use this file except in compliance with the License. + * # You may obtain a copy of the License at + * # + * # http://www.apache.org/licenses/LICENSE-2.0 + * # + * # Unless required by applicable law or agreed to in writing, software + * # distributed under the License is distributed on an "AS IS" BASIS, + * # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * # See the License for the specific language governing permissions and + * # limitations under the License. + */ + +import 'dart:async'; + +import 'package:gltf/gltf.dart'; +import 'package:test/test.dart'; + +import '../../utils.dart'; + +Future main() async { + await compareReports('test/ext/KHR_node_visibility'); + + group('Evaluate valid objects', () { + test('node.KHR_node_visibility', () async { + final gltf = (await read('ext/KHR_node_visibility/data/node/valid.gltf', + ignoreUnused: true)) + .gltf; + + final invisibleNodeVisibility = + gltf.nodes[0].extensions['KHR_node_visibility'] as KhrNodeVisibility; + expect(invisibleNodeVisibility.visible, isFalse); + + final visibleNodeVisibility = + gltf.nodes[1].extensions['KHR_node_visibility'] as KhrNodeVisibility; + expect(visibleNodeVisibility.visible, isTrue); + + final emptyExtensionObject = + gltf.nodes[2].extensions['KHR_node_visibility'] as KhrNodeVisibility; + expect(emptyExtensionObject.visible, isTrue); + }); + }); +}