Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OMI_collider and OMI_physics_body extension validation #2

Draft
wants to merge 2 commits into
base: omi
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions ISSUES.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,12 @@
|NON_OBJECT_EXTRAS|Prefer JSON Objects for extras.|Information|
|NON_RELATIVE_URI|Non-relative URI found: '`%1`'.|Warning|
|NON_REQUIRED_EXTENSION|Extension '`%1`' cannot be optional.|Error|
|OMI_COLLIDER_INVALID_CAPSULE_HEIGHT|The capsule height must be at least twice the radius.|Error|
|OMI_COLLIDER_TRIMESH_TRIGGER|This collider is both a trimesh and a trigger. This is valid but not recommended since trimeshes do not have an interior volume and the trigger may not work as expected.|Information|
|OMI_PHYSICS_BODY_INVALID_INERTIA_TENSOR|This physics body has an invalid inertia tensor. The inertia tensor must be a symmetric 3x3 matrix.|Error|
|OMI_PHYSICS_BODY_MISSING_COLLIDER|This physics body does not have any colliders. This is valid but body will not collide with anything.|Information|
|ROTATION_NON_UNIT|Rotation quaternion must be normalized.|Error|
|SHARES_NODE_WITH|`%1` must be on its own glTF node, it cannot be on the same glTF node as `%2`. Move the `%3` to a child node.|Error|
|SKIN_NO_COMMON_ROOT|Joints do not have a common root.|Error|
|SKIN_SKELETON_INVALID|Skeleton node is not a common root.|Error|
|UNKNOWN_ASSET_MAJOR_VERSION|Unknown glTF major asset version: `%1`.|Error|
Expand Down
35 changes: 35 additions & 0 deletions lib/src/errors.dart
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,13 @@ class SemanticError extends IssueType {
(args) => 'Prefer JSON Objects for extras.',
Severity.Information);

static final SemanticError sharesNodeWith = SemanticError._(
'SHARES_NODE_WITH',
(args) => '${args[0]} must be on its own glTF node, '
'it cannot be on the same glTF node as ${args[1]}.'
'${args.length > 2 ? ' Move the ${args[2]} to a child node.' : ''}',
Severity.Error);

static final SemanticError extraProperty = SemanticError._(
'EXTRA_PROPERTY',
(args) => 'This property should not be defined as it will not be used.',
Expand Down Expand Up @@ -533,6 +540,34 @@ class SemanticError extends IssueType {
'minimum is equal to the thickness maximum.',
Severity.Information);

static final SemanticError omiColliderInvalidCapsuleHeight =
SemanticError._(
'OMI_COLLIDER_INVALID_CAPSULE_HEIGHT',
(args) => 'The capsule height must be at least twice the radius.',
Severity.Error);

static final SemanticError omiColliderTrimeshTrigger =
SemanticError._(
'OMI_COLLIDER_TRIMESH_TRIGGER',
(args) => 'This collider is both a trimesh and a trigger. This '
'is valid but not recommended since trimeshes do not have an '
'interior volume and the trigger may not work as expected.',
Severity.Information);

static final SemanticError omiPhysicsBodyMissingCollider =
SemanticError._(
'OMI_PHYSICS_BODY_MISSING_COLLIDER',
(args) => 'This physics body does not have any colliders. '
'This is valid but body will not collide with anything.',
Severity.Information);

static final SemanticError omiPhysicsBodyInvalidInertiaTensor =
SemanticError._(
'OMI_PHYSICS_BODY_INVALID_INERTIA_TENSOR',
(args) => 'This physics body has an invalid inertia tensor. '
'The inertia tensor must be a symmetric 3x3 matrix.',
Severity.Error);

SemanticError._(String type, ErrorFunction message,
[Severity severity = Severity.Error])
: super(type, message, severity);
Expand Down
255 changes: 255 additions & 0 deletions lib/src/ext/OMI_collider/omi_collider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
// Copyright 2022 The Khronos Group Inc.
//
// SPDX-License-Identifier: Apache-2.0

library gltf.extensions.omi_collider;

import 'package:gltf/src/base/gltf_property.dart';
import 'package:gltf/src/ext/extensions.dart';

const String OMI_COLLIDER = 'OMI_collider';

const String IS_TRIGGER = 'isTrigger';
const String SIZE = 'size';
const String RADIUS = 'radius';
const String HEIGHT = 'height';
const String MESH = 'mesh';

const String BOX = 'box';
const String SPHERE = 'sphere';
const String CAPSULE = 'capsule';
const String CYLINDER = 'cylinder';
const String HULL = 'hull';
const String TRIMESH = 'trimesh';

const String COLLIDER = 'collider';
const String COLLIDERS = 'colliders';

const List<String> OMI_COLLIDER_GLTF_MEMBERS = <String>[COLLIDERS];
const List<String> OMI_COLLIDER_NODE_MEMBERS = <String>[COLLIDER];

const List<String> OMI_COLLIDER_COLLIDER_MEMBERS = <String>[
TYPE,
IS_TRIGGER,
SIZE,
RADIUS,
HEIGHT,
MESH
];

const List<String> OMI_COLLIDER_COLLIDER_TYPES = <String>[
BOX,
SPHERE,
CAPSULE,
CYLINDER,
HULL,
TRIMESH
];

const Extension omiColliderExtension =
Extension(OMI_COLLIDER, <Type, ExtensionDescriptor>{
Gltf: ExtensionDescriptor(OmiColliderGltf.fromMap),
Node: ExtensionDescriptor(OmiColliderNode.fromMap)
});

// The document-level extension that holds a list of colliders.
class OmiColliderGltf extends GltfProperty {
final SafeList<OmiColliderCollider> colliders;

OmiColliderGltf._(
this.colliders, Map<String, Object> extensions, Object extras)
: super(extensions, extras);

static OmiColliderGltf fromMap(
Map<String, Object> map, Context context) {
if (context.validate) {
checkMembers(map, OMI_COLLIDER_GLTF_MEMBERS, context);
}

SafeList<OmiColliderCollider> colliders;
final colliderMaps = getMapList(map, COLLIDERS, context);
if (colliderMaps != null) {
colliders = SafeList<OmiColliderCollider>(colliderMaps.length, COLLIDERS);
context.path.add(COLLIDERS);
for (var i = 0; i < colliderMaps.length; i++) {
final colliderMap = colliderMaps[i];
context.path.add(i.toString());
colliders[i] = OmiColliderCollider.fromMap(colliderMap, context);
context.path.removeLast();
}
context.path.removeLast();
} else {
colliders = SafeList<OmiColliderCollider>.empty(COLLIDERS);
}

return OmiColliderGltf._(
colliders,
getExtensions(map, OmiColliderGltf, context),
getExtras(map, context));
}

@override
void link(Gltf gltf, Context context) {
if (colliders != null) {
context.path.add(COLLIDERS);
final extCollectionList = context.path.toList(growable: false);
context.extensionCollections[colliders] = extCollectionList;
colliders.forEachWithIndices((i, collider) {
context.path.add(i.toString());
collider.link(gltf, context);
context.path.removeLast();
});
context.path.removeLast();
}
}
}

// The main data structure that stores collider data.
class OmiColliderCollider extends GltfChildOfRootProperty {
final String type;
final bool isTrigger;
final List<double> size;
final double radius;
final double height;
final int mesh;

OmiColliderCollider._(this.type, this.isTrigger, this.size, this.radius,
this.height, this.mesh, String name,
Map<String, Object> extensions, Object extras)
: super(name, extensions, extras);

static OmiColliderCollider fromMap(
Map<String, Object> map, Context context) {
if (context.validate) {
checkMembers(map, OMI_COLLIDER_COLLIDER_MEMBERS, context);
}

final type = getString(map, TYPE, context,
list: OMI_COLLIDER_COLLIDER_TYPES, req: true);

final isTrigger = getBool(map, IS_TRIGGER, context);

final size = getFloatList(map, SIZE, context,
lengthsList: const [3], min: 0, def: const [1.0, 1.0, 1.0]);

final radius = getFloat(map, RADIUS, context, min: 0, def: 0.5);
final height = getFloat(map, HEIGHT, context, min: 0, def: 2);

final mesh = getIndex(map, MESH, context, req: false);

if (context.validate) {
if (map.containsKey(SIZE) && type != BOX) {
context.addIssue(SemanticError.extraProperty, name: SIZE);
}
if (type != CAPSULE && type != CYLINDER) {
if (map.containsKey(HEIGHT)) {
context.addIssue(SemanticError.extraProperty, name: HEIGHT);
}
if (type != SPHERE) {
if (map.containsKey(RADIUS)) {
context.addIssue(SemanticError.extraProperty, name: RADIUS);
}
}
}
if (type == CAPSULE && height < radius * 2) {
context.addIssue(SemanticError.omiColliderInvalidCapsuleHeight,
name: HEIGHT);
}
if (map.containsKey(MESH) && type != TRIMESH && type != HULL) {
context.addIssue(SemanticError.extraProperty, name: MESH);
}
if (type == TRIMESH && isTrigger) {
context.addIssue(SemanticError.omiColliderTrimeshTrigger,
name: IS_TRIGGER);
}
}

return OmiColliderCollider._(
type,
isTrigger,
size,
radius,
height,
mesh,
getName(map, context),
getExtensions(map, OmiColliderCollider, context),
getExtras(map, context));
}
}

// The node-level extension that references a document-level collider by index.
class OmiColliderNode extends GltfProperty {
final int _colliderIndex;

OmiColliderCollider _collider;

OmiColliderNode._(
this._colliderIndex, Map<String, Object> extensions, Object extras)
: super(extensions, extras);

static OmiColliderNode fromMap(
Map<String, Object> map, Context context) {
if (context.validate) {
checkMembers(map, OMI_COLLIDER_NODE_MEMBERS, context);
}

return OmiColliderNode._(
getIndex(map, COLLIDER, context),
getExtensions(map, OmiColliderNode, context),
getExtras(map, context));
}

@override
void link(Gltf gltf, Context context) {
if (!context.validate) {
return;
}
final collidersExtension = gltf.extensions[omiColliderExtension.name];
if (collidersExtension is OmiColliderGltf) {
// Mark the collider that this node references as used.
if (_colliderIndex != -1) {
_collider = collidersExtension.colliders[_colliderIndex];
if (_collider == null) {
context.addIssue(LinkError.unresolvedReference,
name: COLLIDER, args: [_colliderIndex]);
} else {
_collider.markAsUsed();
// If this is a trimesh, we also need to mark the mesh as used.
final meshIndex = _collider.mesh;
if (meshIndex != -1) {
if (meshIndex >= gltf.meshes.length) {
context.addIssue(LinkError.unresolvedReference,
name: MESH, args: [meshIndex]);
} else {
gltf.meshes[meshIndex].markAsUsed();
}
}
}
}
// Get the glTF node that this collider is attached to.
final path = context.path;
if (path.length < 2 || path[0] != 'nodes') {
return;
}
final nodeIndex = int.tryParse(path[1]);
if (nodeIndex == null) {
return;
}
final node = gltf.nodes[nodeIndex];
// Ensure that the collider is not on the same node as a mesh or camera.
if (node.mesh != null) {
context.addIssue(SemanticError.sharesNodeWith,
args: ['A collider', 'a mesh']);
}
if (node.camera != null) {
context.addIssue(SemanticError.sharesNodeWith,
args: ['A collider', 'a camera']);
}
} else {
context.addIssue(SchemaError.unsatisfiedDependency,
args: ['/$EXTENSIONS/${omiColliderExtension.name}']);
}
}

OmiColliderCollider get collider => _collider;
}
Loading