diff --git a/README.md b/README.md index fef3474..73d2c5d 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,10 @@ The extension has been submitted and merged as an OMI vendor extension. Alternat ## OMI glTF Vendor Extensions ### Stage 0 +- [OMI_vehicle_body](extensions/2.0/OMI_vehicle_body/README.md) +- [OMI_vehicle_hover_thruster](extensions/2.0/OMI_vehicle_hover_thruster/README.md) +- [OMI_vehicle_thruster](extensions/2.0/OMI_vehicle_thruster/README.md) +- [OMI_vehicle_wheel](extensions/2.0/OMI_vehicle_wheel/README.md) ### Stage 1 diff --git a/extensions/2.0/OMI_vehicle_body/README.md b/extensions/2.0/OMI_vehicle_body/README.md new file mode 100644 index 0000000..c7780d1 --- /dev/null +++ b/extensions/2.0/OMI_vehicle_body/README.md @@ -0,0 +1,188 @@ +# OMI_vehicle_body + +## Contributors + +- Aaron Franke, Godot Engine. + +## Status + +Open Metaverse Interoperability Group Draft Proposal + +## Dependencies + +Written against the glTF 2.0 spec. + +Depends on the `OMI_physics_body` spec, and is designed to be used with `OMI_vehicle_wheel` and `OMI_seat`. + +## Overview + +This extension allows for specifying additional specialized details of a vehicle body. + +To use this extension, place `OMI_vehicle_body` on the same glTF node as an `OMI_physics_body`. The `OMI_vehicle_body` extension can be empty if it does not need any of the parameters defined, its existence indicates that the glTF node is a vehicle body. + +The front of the vehicle faces +Z. The pilot seat is expected to face +Z to have the pilot facing towards the front of the vehicle. A stable ground vehicle like a car is expected to have its parts mostly above the center of mass, so that the vehicle is bottom-heavy. + +### Example: + +The file [minimal_vehicle_body.gltf](examples/minimal_vehicle_body.gltf) defines a very minimal vehicle with no wheels, thrusters, or other functional parts. It only has a body, collider, pilot seat, and some gyroscope torque. The vehicle body is defined as a dynamic physics body node with the `OMI_vehicle_body` extension added on the same node: + +```json +{ + "children": [1, 2], + "name": "MinimalVehicle", + "extensions": { + "OMI_physics_body": { + "motion": { + "type": "dynamic", + "mass": 1000 + } + }, + "OMI_vehicle_body": { + "gyroTorque": [1000, 1000, 1000], + "pilotSeat": 1 + } + } +} +``` + +The vehicle body refers to a pilot seat glTF node by index, which is defined using the `OMI_seat` extension on a glTF node: + +```json +{ + "name": "SeatPilot", + "translation": [0.4, 0.4, 0.6], + "extensions": { + "OMI_physics_body": { + "trigger": { + "shape": 0 + } + }, + "OMI_seat": { + "back": [0, 0, -0.3], + "foot": [0, -0.5, 0.3], + "knee": [0, 0, 0.3] + } + } +} +``` + +The trigger shape is not required, but provides a way for players to interact with the seat; if no trigger shape is defined, and the implementation allows for characters to pilot vehicles, then the implementation should provide another way for players to enter the pilot seat, such as interacting with the entire vehicle body. Also note, the pilot seat itself is not required to be used by an implementation; if an implementation does not allow for characters to pilot the vehicle, the pilot seat can be ignored. + +The minimal vehicle in this file is not functional. It exists to showcase the `OMI_vehicle_body` extension by itself, but it has no parts that can move or exert a thrust, except for the built-in gyroscope torque. In order to make the vehicle functional, additional parts would need to be attached, such as a wheel or thruster. See the example files in the `OMI_vehicle_wheel` spec or the `OMI_vehicle_thruster` spec. A vehicle without wheels, thrusters, gyroscope torque, or other functional parts will not have a way to propel itself. + +## glTF Schema Updates + +This extension consists of a new `OMI_vehicle_body` data structure which can be added to the extensions of an `OMI_physics_body` glTF node. + +The extension must also be added to the glTF's `extensionsUsed` array and because it is optional, it does not need to be added to the `extensionsRequired` array. + +The extension is intended to be used together with `OMI_seat`. + +### Property Summary + +| | Type | Description | Default value | +| --------------------- | ----------- | -------------------------------------------------------------------------------- | --------------- | +| **angularActivation** | `number[3]` | The input value controlling the ratio of the vehicle's angular forces. | [0.0, 0.0, 0.0] | +| **linearActivation** | `number[3]` | The input value controlling the ratio of the vehicle's linear forces. | [0.0, 0.0, 0.0] | +| **gyroTorque** | `number[3]` | The gyroscope torque intrinsic to the vehicle, excluding torque from parts. | [0.0, 0.0, 0.0] | +| **maxSpeed** | `number` | The speed at which the vehicle should stop driving acceleration further. | -1.0 | +| **pilotSeat** | `number` | The index of the `OMI_seat` glTF node to use as the pilot / driver seat. | -1 (no seat) | +| **angularDampeners** | `boolean` | If true, the vehicle should slow its rotation when not given angular activation. | true | +| **linearDampeners** | `boolean` | If true, the vehicle should slow down when not given linear activation. | false | +| **useThrottle** | `boolean` | If true, the vehicle should use a throttle for linear movement. | false | + +#### Angular Activation + +The `"angularActivation"` property is an array of three numbers that defines the input value controlling the ratio of the vehicle's angular forces. If not specified, the default value is [0.0, 0.0, 0.0], which means that the vehicle should not drive itself with angular forces. + +The three numbers represent the ratio of activation of the vehicle's angular forces around the X, Y, and Z axes, respectively. The values are expected to be between -1.0 and 1.0. The behavior of values outside of this range is implementation-defined, they may be clamped to this range, or be allowed to go beyond this range for some kind of turbo boost or overdrive. + +For example, a value of [0.0, 1.0, 0.0] would mean that the vehicle should try to spin around its local Y axis to the left at full speed. For a car, this would mean wheels used for steering are turned to the left. For an aircraft or sea ship, this would mean turning the rudder so that the vehicle rotates to the left. For a spacecraft, this may activate gyroscopes or small RCS thrusters to rotate the vehicle to the left. If an angular direction is not applicable to the vehicle, the value should be ignored, such as a car ignoring X and Z angular activations since it can only steer left and right. + +Note that due to glTF's right-handed coordinate system with +Z as forward, positive X rotations rotate down, positive Y rotations rotate to the left, and positive Z rotations rotate clockwise. + +This value is expected to dynamically change at runtime. This is not usually saved in the glTF file, but it is allowed to be. The input source for angular activation is implementation-defined. The control scheme may be keyboard buttons, gamepad joysticks, buttons in the UI, in-game objects such as a steering wheel, or anything else. The input source may be from a player in the pilot seat, such as a user pressing buttons, an NPC controlling the vehicle, or a script such as `KHR_interactivity`. + +#### Linear Activation + +The `"linearActivation"` property is an array of three numbers that defines the input value controlling the ratio of the vehicle's linear forces. If not specified, the default value is [0.0, 0.0, 0.0], which means that the vehicle should not drive itself with linear forces. + +The three numbers represent the ratio of activation of the vehicle's linear forces in the X, Y, and Z directions, respectively. The values are expected to be between -1.0 and 1.0. The behavior of values outside of this range is implementation-defined, they may be clamped to this range, or be allowed to go beyond this range for some kind of turbo boost or overdrive. + +For example, a value of [0.0, 0.0, 1.0] would mean that the vehicle should propel itself forward on its local +Z axis at full speed. For a car, this would mean driving the wheels forward with maximum force. For an aircraft or sea ship, this would mean activating main propellers or turbines at full power. For a spaceship, this would mean firing the rear thrusters at full power. If a linear direction is not applicable to the vehicle, the value should be ignored, such as a car ignoring X and Y linear activations since it can only drive forward and backward. + +This value is expected to dynamically change at runtime. This is not usually saved in the glTF file, but it is allowed to be. The input source for linear activation is implementation-defined. The control scheme may be keyboard buttons, gamepad joysticks, buttons in the UI, in-game objects such as a gas pedal, or anything else. The input source may be from a player in the pilot seat, such as a user pressing buttons, an NPC controlling the vehicle, or a script such as `KHR_interactivity`. + +#### Gyro Torque + +The `"gyroTorque"` property is an array of three numbers that defines the gyroscope torque intrinsic to the vehicle, excluding torque from parts such as thrusters. If not specified, the default value is [0.0, 0.0, 0.0], meaning that the vehicle does not have any gyroscopic torque ability. + +Gyroscope torque is not commonly used for land, sea, and air-based vehicles, but is common for spacecraft. Cars usually rotate themselves by turning their wheels, while sea ships and aircraft usually rotate themselves with control surfaces such as rudders and flaps. Spacecraft need to rotate themselves in space where there is no matter to push against, so they can only rotate themselves using gyroscopes or thrusters. If an implementation does not allow for spacecraft or other vehicles with gyroscope torque, such as a racing game importing cars, this property can be ignored. + +Gyroscope torque is a type of torque, and therefore is measured in Newton-meters per radian (kg⋅m²/s²/rad). The amount specified here is the maximum torque; the actual value applied to the vehicle depends on this value, the `"angularActivation"` input, and the `"linearDampeners"` setting. Also note, the rotational effect of the torque depends on the vehicle's mass and rotational inertia, so this value may need to be increased for large vehicles. The torque should be applied local to the vehicle's own reference frame. + +Some space games such as Kerbal Space Program and Space Engineers have gyroscopes as placeable parts (called SAS in KSP). The net effect of all gyroscopes on the vehicle is the sum of all the gyroscopes' torques. When exporting from those games to OMI_vehicle_body, the gyroscope torque from all of those parts should be summed up and written to this property. Allowing for individual gyroscopes to break off or otherwise become detached from the vehicle, and therefore changing the vehicle's total gyro torque, is not defined by this extension, but another extension may alter this value at runtime. + +#### Pilot Seat + +The `"pilotSeat"` property is an integer that defines the index of the glTF node to use as a pilot seat, also known as driver's seat in the context of cars. The node that `"pilotSeat"` points to should be a seat as defined by the `OMI_seat` extension. If not specified or set to -1, this vehicle does not have a pilot seat. + +A character sitting in the pilot seat is called the pilot. The pilot should be given control of the vehicle through whatever control scheme the implementation desires. The pilot seat is expected to face +Z to have the pilot facing towards the front of the vehicle. If an implementation does not allow for characters to pilot the vehicle, this property can be ignored. + +The pilot seat node is usually a child of the vehicle body node, but it does not have to be. A pilot seat node that is not a descendant of the vehicle body allows for a remote-controlled vehicle, such as a drone. + +#### Max Speed + +The `"maxSpeed"` property is a number that defines the speed in meters per second at which the vehicle should stop driving acceleration further in that direction. If not defined or negative, the vehicle should not have a maximum speed. + +This only affects the maximum speed at which the vehicle can accelerate to under its own power, in all directions. If the vehicle is already moving faster than this speed, the vehicle does not need to use its own thrust to slow down, but it is allowed to do so if given input to thrust in the opposite direction, or if inertia dampeners are enabled. + +Note that this only affects how the vehicle is driven, not how it is simulated. The physics engine should still simulate the vehicle at any speed. For example, if a vehicle is driven off a cliff, it may accelerate due to gravity, unrelated to the maximum speed. + +#### Angular Dampeners + +The `"angularDampeners"` property is a boolean that defines whether the vehicle should slow its rotation down when not given angular activation input for a specific rotation. If not specified, the default value is true. + +When angular inertia dampeners are enabled, and any number inside of `"angularActivation"` is zero, it indicates that the vehicle should attempt to stop rotating in that rotational plane. This can be accomplished by straightening the wheels on a car, turning the rudder back to center on a boat, or using gyroscope torque or thruster gimbal on a spaceship. This value is true by default, because it is almost always desired for vehicles to stop rotating when the pilot stops giving input. + +The reference frame used to determine what "stop" and "not rotating" mean is implementation-defined, and usually determined at runtime. Inertia dampeners may be relative to global coordinates, or relative to some object, such as a planet, moon, space station, or another vehicle. Angular dampeners are called SAS stabilization in Kerbal Space Program, and Rotational Correction in Elite Dangerous, and both of those games allow the dampeners to be relative to different reference frames. + +#### Linear Dampeners + +The `"linearDampeners"` property is a boolean that defines whether the vehicle should slow itself down when not given linear activation input for a specific direction. If not specified, the default value is false. + +When linear inertia dampeners are enabled, and any number inside of `"linearActivation"` is zero, it indicates that the vehicle should attempt to come to a stop in that direction. This may be the brakes on a car, the reverse thrusters on a spaceship, or some other mechanism. For example, a spaceship with inertia dampeners should use its thrusters to slow down and stop moving when the pilot stops giving input, rather than continuing to drift in space forever. + +The reference frame used to determine what "stop" and "not moving" mean is implementation-defined, and usually determined at runtime. Inertia dampeners may be relative to global coordinates, or relative to some object, such as a planet, moon, space station, or another vehicle. Linear dampeners are called Inertia Dampeners in Space Engineers, and Flight Assist in Elite Dangerous, and both of those games allow the dampeners to be relative to different reference frames. + +#### Use Throttle + +The `"useThrottle"` property is a boolean that defines whether the vehicle should use a throttle for forward movement. This controls how input affects the vehicle's linear activation. + +Throttle is a common feature of airplanes and boats, but may be used on other kinds of vehicles. When using throttle, the linear activation is usually between 0.0 and 1.0, where 0.0 is no throttle and 1.0 is full throttle. See the `"linearActivation"` property for more details. + +For example, consider the case where the vehicle is given input to move forward, such as a player pressing the W key, or any other input method. + +- Without a throttle, the vehicle's linear activation Z is set to 1.0, so the vehicle should accelerate as fast as it can in the forward direction, up to the maximum speed; then when W is released, the vehicle's linear activation Z is set to 0.0, the vehicle should stop accelerating forward, either continuing at the same speed, or if inertia dampeners are enabled, slowing down. +- With a throttle, the W key should increase the linear activation Z (gradually rising towards 1.0), and the S key should decrease the linear activation Z (gradually falling towards 0.0), allowing the player to control how fast the vehicle accelerates; when the keys are no longer pressed, the throttle stays at its value. The rate of throttle change is implementation-defined. + +The actual control scheme is implementation-defined. It may be W and S keys, a gamepad joystick, a slider in a UI, or some other method. It is also common to bind a "cut throttle" key, such as X, to set the throttle to 0.0 instantly. Another option useful for VR implementations is to have a 3D model of a throttle lever that the player can grab and move. The input source may be a player pressing buttons, an NPC controlling the vehicle, or a script such as `KHR_interactivity`. + +Aside from `"useThrottle"` determining how input affects activation, there is also the matter of how throttle affects the vehicle's thrust. If `"maxSpeed"` is non-negative, the throttle should be a ratio of that speed, with the thrust adjusting so that the vehicle meets the target speed. Otherwise, the throttle should be a ratio of the thrust power, allowing the throttle to control the ratio of the vehicle's acceleration power. + +### JSON Schema + +See [schema/node.OMI_vehicle_body.schema.json](schema/node.OMI_vehicle_body.schema.json). + +## Known Implementations + +- Godot Engine + +## Resources: + +See the [mapping document](mappings.md) for how this extension maps to other games, game engines, and platforms. + +- Godot VehicleBody3D: https://docs.godotengine.org/en/stable/classes/class_vehiclebody3d.html +- Hyperfy Car: https://madjin.github.io/hyperfy-docs/docs/worlds/apps/objects/#car +- Space Engineers Flight Seat: https://spaceengineers.wiki.gg/wiki/Flight_Seat +- Elite Dangerous Flight Assist: https://elite-dangerous.fandom.com/wiki/Flight_Assist diff --git a/extensions/2.0/OMI_vehicle_body/examples/minimal_vehicle_body.gltf b/extensions/2.0/OMI_vehicle_body/examples/minimal_vehicle_body.gltf new file mode 100644 index 0000000..80c4913 --- /dev/null +++ b/extensions/2.0/OMI_vehicle_body/examples/minimal_vehicle_body.gltf @@ -0,0 +1,67 @@ +{ + "asset": { + "version": "2.0" + }, + "extensionsUsed": ["GODOT_single_root", "OMI_physics_body", "OMI_physics_shape", "OMI_seat", "OMI_vehicle_body"], + "extensions": { + "OMI_physics_shape": { + "shapes": [ + { + "type": "box", + "box": { "size": [0.65, 0.65, 0.65] } + }, + { + "type": "box", + "box": { "size": [1.4, 1.5, 2.2] } + } + ] + } + }, + "nodes": [ + { + "children": [1, 2], + "name": "MinimalVehicle", + "extensions": { + "OMI_physics_body": { + "motion": { + "type": "dynamic", + "mass": 1000 + } + }, + "OMI_vehicle_body": { + "gyroTorque": [1000, 1000, 1000], + "pilotSeat": 1 + } + } + }, + { + "name": "SeatPilot", + "translation": [0.4, 0.4, 0.6], + "extensions": { + "OMI_physics_body": { + "trigger": { + "shape": 0 + } + }, + "OMI_seat": { + "back": [0, 0, -0.3], + "foot": [0, -0.5, 0.3], + "knee": [0, 0, 0.3] + } + } + }, + { + "name": "MinimalCollider", + "translation": [0, 0.6, 0], + "extensions": { + "OMI_physics_body": { + "collider": { + "shape": 1 + } + } + } + } + ], + "scene": 0, + "scenes": [{ "nodes": [0] }] +} diff --git a/extensions/2.0/OMI_vehicle_body/mappings.md b/extensions/2.0/OMI_vehicle_body/mappings.md new file mode 100644 index 0000000..a1e14be --- /dev/null +++ b/extensions/2.0/OMI_vehicle_body/mappings.md @@ -0,0 +1,236 @@ +# OMI_vehicle mappings + +This document describes how the OMI_vehicle extensions map to different games, game engines, and platforms. + +The term "import" refers to bringing the OMI_vehicle extension data into a game or game engine. The term "export" refers to taking objects from a game or game engine and converting them into OMI_vehicle extension data. + +In general, every property in the OMI_vehicle extensions is listed in the tables below, except that the "current" properties are omitted in most cases, because these are usually determined at runtime and do not have any special notes for import/export. + +In alphabetical order: + +- [Empyrion Galactic Survival](#empyrion-galactic-survival) +- [Godot Engine](#godot-engine) +- [Hyperfy](#hyperfy) +- [Jolt Physics](#jolt-physics) +- [Kerbal Space Program](#kerbal-space-program) +- [Space Engineers](#space-engineers) +- [VRChat SaccFlightAndVehicles](#vrchat-saccflightandvehicles) + +## Empyrion Galactic Survival + +Empyrion Galactic Survival has space vessels and hover vessels, and no wheeled vehicles except for the motorbike. + +| OMI_vehicle_body | Empyrion | Additional Notes | +| -------------------- | ------------- | -------------------------------------------------------------------------------------------- | +| **gyroTorque** | RCS block | Comes in various sizes from 50 kNm to 150 MNm. On export, sum the torque of all RCS blocks. | +| **maxSpeed** | 60 to 130 m/s | Depends on type of vessel and the environment (space/atmosphere). | +| **pilotSeat** | Cockpit block | The cockpit block is the pilot seat. | +| **angularDampeners** | Pilot Mode | Enabling Pilot Mode will hold a specific rotational speed. This is the inverse of dampeners. | +| **linearDampeners** | Auto Brake | Hover engines always have dampeners, Auto Brake only controls non-hover thrusters. | +| **useThrottle** | Not available | Empyrion does not have throttle controls. | + +Empyrion also has "Auto Rotate", which is a button that can be held to automatically rotate the vessel to be upright. Neither this nor Pilot Mode are perfect matches for the OMI_vehicle_body `"angularDampeners"` property. However, note that the vehicle can also be powered off to disable all dampeners. + +| OMI_vehicle_thruster | Empyrion | Additional Notes | +| -------------------- | ------------------------ | -------------------------------------------- | +| **maxForce** | Depends on thruster type | Comes in various sizes from 55 kN to 920 MN. | +| **maxGimbal** | Not available | Empyrion thrusters do not gimbal. | + +Empyrion allows toggling thrusters on and off, but does not have an override for the current force and current gimbal of a thruster. + +For hover thrusters, Empyrion's hover engines have a force measured in kN, the same as non-hover thrusters, but are handicapped so that they stop working beyond a certain height above the ground. For OMI_vehicle_hover_thruster, there is no artificial height limit, instead they operate with a force that is proportional to the distance from the ground - the closer to the ground, the more force they provide. The OMI_vehicle_hover_thruster `maxHoverEnergy` property is measured in Newton-meters (N⋅m or kg⋅m²/s² or Joules), so dividing this number by the amount of meters above the ground will give the force in Newtons (N). + +## Godot Engine + +Godot Engine's built-in vehicles are designed to be cars, but its feature set is extended by the Godot implementation of the OMI_vehicle extensions. + +| OMI_vehicle_body | Godot Engine | Additional Notes | +| -------------------- | ----------------- | ---------------------------------------------------------------------------------------- | +| **gyroTorque** | Gyroscope Torque | Part of PilotedVehicleBody3D, provided by the OMI_vehicle_body extension implementation. | +| **maxSpeed** | Max Speed | Part of PilotedVehicleBody3D, provided by the OMI_vehicle_body extension implementation. | +| **pilotSeat** | Pilot Seat Node | Refers to a PilotSeat3D node in the scene, provided by the OMI_vehicle_body extension. | +| **angularDampeners** | Angular Dampeners | Part of PilotedVehicleBody3D, provided by the OMI_vehicle_body extension implementation. | +| **linearDampeners** | Linear Dampeners | Part of PilotedVehicleBody3D, provided by the OMI_vehicle_body extension implementation. | +| **useThrottle** | Use Throttle | Part of PilotedVehicleBody3D, provided by the OMI_vehicle_body extension implementation. | + +Note: Godot's built-in VehicleBody3D system is designed for body-wide motion parameters, but OMI specifies all motion as per-part. As a result, there is not a lot of overlap between VehicleBody3D's built-in parameters and what is actually used by the OMI_vehicle_body extension implementation in Godot. The OMI_vehicle extensions were designed to be very general-purpose for all kinds of vehicles, not just cars. + +| OMI_vehicle_wheel | Godot Engine Wheels | Additional Notes | +| -------------------------------- | -------------------- | ------------------------------------------------------------------------------------------- | +| **maxForce** | Max Force | Part of PilotedVehicleWheel3D, provided by the OMI_vehicle_wheel extension implementation. | +| **maxSteeringAngle** | Max Steering Angle | Part of PilotedVehicleWheel3D, provided by the OMI_vehicle_wheel extension implementation. | +| **physicsMaterial** | Wheel Friction Sleep | Godot only specifies friction, not other material properties. | +| **radius** | Wheel Radius | Vanilla Godot VehicleWheel3D. | +| **suspensionDampingCompression** | Damping Compression | Vanilla Godot VehicleWheel3D. Godot's units are in Mg/s while OMI_vehicle_wheel uses kg/s. | +| **suspensionDampingRebound** | Damping Relaxation | Vanilla Godot VehicleWheel3D. Godot's units are in Mg/s while OMI_vehicle_wheel uses kg/s. | +| **suspensionStiffness** | Suspension Stiffness | Vanilla Godot VehicleWheel3D. Godot is Mg/s² (N/mm), while OMI_vehicle_wheel us kg/s² (N/m) | +| **suspensionTravel** | Suspension Travel | Vanilla Godot VehicleWheel3D. | +| **width** | Not available | Godot Engine wheels are infinitely thin discs. | + +Additionally, the OMI_vehicle_thruster extension has a 1:1 mapping with the VehicleThruster3D node provided by the OMI_vehicle_thruster extension implementation in Godot. Since it is identical, a table is not provided for it. + +## Hyperfy + +Hyperfy vehicles are strictly cars, not aircraft, spacecraft, etc. + +| OMI_vehicle_body | Hyperfy Vehicle Body | Additional Notes | +| -------------------- | -------------------- | ------------------------------------------------------------------------------------------- | +| **gyroTorque** | Anti-roll | Gyroscope torque can be used to prevent rolling. | +| **maxSpeed** | Max Speed | | +| **pilotSeat** | Seat #1 (Driver) | Use glTF nodes for all transforms. Use OMI_seat for seats. | +| **angularDampeners** | Unknown | | +| **linearDampeners** | Brake Force | Linear dampeners stop linear motion, which is the same as braking. | +| **useThrottle** | Not available | Hyperfy does not have throttle controls. | +| Not available | Power Curve | OMI_vehicle_body does not specify acceleration of the vehicle as a whole, only wheel force. | +| Not available | Turn Speed / Curve | OMI_vehicle_body does not specify turning of the vehicle as a whole, only the wheels. | + +| OMI_vehicle_wheel | Hyperfy Vehicle Wheels | Additional Notes | +| -------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------- | +| **maxForce** | Body (Ac/De)celeration | OMI_vehicle_body does not specify acceleration of the vehicle as a whole, only wheel force. | +| **maxSteeringAngle** | Body Turn Angle | OMI_vehicle_body does not specify turning of the vehicle as a whole, only the wheels. | +| **physicsMaterial** | Grip Curve | Very different, but describes the same thing. | +| **radius** | Tire Radius | | +| **suspensionDampingCompression** | Spring Damper | Hyperfy docs do not specify the unit. OMI_vehicle_wheel uses N⋅s/m or kg/s. | +| **suspensionDampingRebound** | Spring Damper | Hyperfy docs do not specify the unit. OMI_vehicle_wheel uses N⋅s/m or kg/s. | +| **suspensionStiffness** | Spring Strength | Hyperfy docs do not specify the unit. OMI_vehicle_wheel uses N/m or kg/s². | +| **suspensionTravel** | Spring Length | | +| **width** | Not available | Hyperfy wheels are infinitely thin discs. | +| glTF node position | Axle Y / Z / Width | Use glTF nodes for all transforms. | +| Per-wheel force settings | Body Mode | Hyperfy has "FWD", "RWD", and "4WD" modes. For glTF, specify this per-wheel. | + +| Other | Hyperfy | Additional Notes | +| ---------------------------------- | ---------------------- | -------------------------------------------------------------------------- | +| OMI_physics_body center of mass | Center Of Mass | Handled by the OMI_physics_body extension. | +| Calculated from shapes | Drag & Angular Drag | Handled by the OMI_physics_body extension. | +| Not available | Engine Sound | Needs discussion for what this will look like in glTF. | +| Not available | Label | Use the glTF node name for naming a vehicle. | +| glTF mesh, shapes, etc. | Model & Size | The appearance and size are determined by the meshes etc in the glTF file. | +| Not available | On Enter & On Exit | Needs to wait for KHR_interactivity. | +| glTF body node position/rotation | Position & Rotation | Use glTF nodes for all transforms. | +| Not available | Prefab | Configure properties of specific parts intead. | +| Not available | Reset Time | Reset functionality is not specified in the OMI extensions. | +| glTF node position of pilot seat | Seat #1 Position X/Y/Z | Use glTF nodes for all transforms. Use OMI_seat for seats. | +| glTF node rotation of pilot seat | Seat #1 Rotation | Use glTF nodes for all transforms. Use OMI_seat for seats. | +| glTF node positions of other seats | Seat #2/3/4 Pos X/Y/Z | Use glTF nodes for all transforms. Use OMI_seat for seats. | +| glTF node rotations of other seats | Seat #2/3/4 Rotation | Use glTF nodes for all transforms. Use OMI_seat for seats. | +| Not available | Seat #1/2/3/4 Enabled | OMI_seat does not have an enabled property. | + +## Jolt Physics + +Jolt is a physics engine which has a built-in vehicle system, designed for cars and other wheeled vehicles. Jolt's vehicle controller is very low-level and does not map to the OMI_vehicle_body properties, so another system would need to be built on top to support OMI_vehicle_body's input and handling properties. + +| OMI_vehicle_wheel | Jolt Physics | Additional Notes | +| -------------------------------- | ----------------------------- | --------------------------------------------------------------------------------- | +| **currentForceRatio** | Unknown | | +| **currentSteeringRatio** | Steer Angle | | +| **maxForce** | Unknown | | +| **maxSteeringAngle** | WheelSettingsWV MaxSteerAngle | Radians. | +| **physicsMaterial** | WheelSettingsWV Friction | Separate LongitudinalFriction and LateralFriction settings in Jolt. | +| **radius** | Radius | Meters. | +| **suspensionDampingCompression** | SpringSettings Damping | | +| **suspensionDampingRebound** | SpringSettings Damping | | +| **suspensionStiffness** | SpringSettings Stiffness | Newtons per meter (N/m) or kilograms per second squared (kg/s²) for both. | +| **suspensionTravel** | Suspension Length | Meters. | +| **width** | Width | Meters. | +| Always down | Suspension Direction | OMI_vehicle_wheel suspension always points down, while Jolt may be any direction. | +| Not available | WheelSettingsWV Inertia | OMI_vehicle_wheel does not define wheel inertia. | + +## Kerbal Space Program + +Kerbal Space Program vehicles can be rockets, planes, rovers, and more. + +| OMI_vehicle_body | Kerbal Space Program | Additional Notes | +| -------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------- | +| **gyroTorque** | SAS Torque | Provided by SAS modules or command pods. On export, sum the torque of all SAS modules and command pods. | +| **maxSpeed** | Infinite | Kerbal Space Program does not have a speed limit. On export, do not set this property. | +| **pilotSeat** | Command pod | Crafts are controlled by Kerbals sitting in command pods, or unmanned command modules such as probe cores. | +| **angularDampeners** | SAS Enabled | SAS is a system that automatically stabilizes the craft. The user controls it with the T key. | +| **linearDampeners** | Brakes | Press B to toggle brakes on rovers and aircraft. Spacecraft behave realistically and do not have dampeners. | +| **useThrottle** | Always enabled | Kerbal Space Program always has throttle controls. | + +| OMI_vehicle_wheel | KSP Wheels | Additional Notes | +| -------------------------------- | --------------------- | --------------------------------------------------------------------------------------------- | +| **currentForceRatio** | Motor settings | Motors can be toggled, and are otherwise controlled by the vehicle-wide linear activation. | +| **currentSteeringRatio** | Steering settings | Steering can be toggled, and are otherwise controlled by the vehicle-wide angular activation. | +| **maxForce** | Depends on wheel type | Kerbal Space Program provides several pre-set wheels. | +| **maxSteeringAngle** | Depends on wheel type | Kerbal Space Program provides several pre-set wheels. | +| **physicsMaterial** | Friction Control | Kerbal Space Program only has wheel friction, not other material properties. | +| **radius** | Depends on wheel type | Kerbal Space Program provides several pre-set wheels. | +| **suspensionDampingCompression** | Damper Strength | Kerbal Space Program wiki does not specify the unit. OMI_vehicle_wheel uses N⋅s/m or kg/s. | +| **suspensionDampingRebound** | Damper Strength | Kerbal Space Program wiki does not specify the unit. OMI_vehicle_wheel uses N⋅s/m or kg/s. | +| **suspensionStiffness** | Spring Strength | Kerbal Space Program wiki does not specify the unit. OMI_vehicle_wheel uses N/m or kg/s². | +| **suspensionTravel** | Depends on wheel type | Kerbal Space Program provides several pre-set wheels. | +| **width** | Depends on wheel type | Kerbal Space Program provides several pre-set wheels. | + +| OMI_vehicle_thruster | KSP Engines | Additional Notes | +| ---------------------- | ------------------------ | ----------------------------------------------------------------------------- | +| **currentForceRatio** | Throttle settings | Engine thrust can be toggled and limited, up to the throttle amount. | +| **currentGimbalRatio** | Gimbal settings | Engines can gimbal like real rocket engines. Gimbal can be limited or locked. | +| **maxForce** | Depends on thruster type | Kerbal Space Program provides many pre-set thrusters. | +| **maxGimbal** | Depends on thruster type | Kerbal Space Program provides many pre-set thrusters. | + +## Space Engineers + +Space Engineers vehicles are general-purpose. They can be spacecraft, cars, and more. + +| OMI_vehicle_body | Space Engineers | Additional Notes | +| -------------------- | -------------------------- | ------------------------------------------------------------------- | +| **gyroTorque** | Gyroscope blocks | On export, sum the torque of all gyroscopes. | +| **maxSpeed** | Constant 100 m/s | Some Space Engineers mods increase this limit. | +| **pilotSeat** | Flight Seat, Cockpit, etc. | If one is set to "Main Cockpit", that one should be the pilot seat. | +| **angularDampeners** | Always enabled | | +| **linearDampeners** | Inertia Dampeners / Brakes | Press Z to toggle in Space Engineers. | +| **useThrottle** | Not available | Space Engineers does not have throttle controls. | + +| OMI_vehicle_wheel | Space Engineers Wheels | Additional Notes | +| -------------------------------- | ---------------------- | --------------------------------------------------------------------------------------------- | +| **currentForceRatio** | Propulsion Override | Space Engineers allows overriding force per-wheel directly by the player. | +| **currentSteeringRatio** | Steer Override | Space Engineers allows overriding steering per-wheel directly by the player. | +| **maxForce** | Power & Propulsion | Each wheel type can go up to a certain force, and the Power setting is a multiplier (0-1). | +| **maxSteeringAngle** | Steering Angle | This is the maximum. | +| **physicsMaterial** | Friction | Space Engineers only specifies friction, not other material properties. | +| **radius** | Depends on wheel type | Space Engineers provides several pre-set wheel sizes. | +| **suspensionDampingCompression** | Not available | Space Engineers does not have a numeric value for wheel suspension damping. | +| **suspensionDampingRebound** | Not available | Space Engineers does not have a numeric value for wheel suspension damping. | +| **suspensionStiffness** | Strength | Space Engineers wiki does not specify the unit. OMI_vehicle_wheel uses N/m or kg/s². | +| **suspensionTravel** | Height offset | Space Engineers uses centimeters, OMI_vehicle_wheel uses meters. | +| **width** | Depends on wheel type | Space Engineers provides several pre-set wheel sizes. | +| OMI_vehicle_body maxSpeed | Speed Limit | Space Engineers has a global speed limit, but also a per-wheel limit, in km/h instead of m/s. | +| Damping and travel settings | AirShock | No perfect mapping, some experimentation may be needed. | + +| OMI_vehicle_thruster | Space Engineers Thrusters | Additional Notes | +| ---------------------- | ------------------------- | -------------------------------------------------------------------------------------------------- | +| **currentForceRatio** | Thrust Override | Space Engineers allows overriding per-thruster thrust directly by the player. | +| **currentGimbalRatio** | Not available | Space Engineers thrusters do not gimbal. | +| **maxForce** | Depends on thruster type | Space Engineers provides several pre-set thrusters (ion: 14.4 kN, 172.8 kN, 345.6 kN, and 4.3 MN). | +| **maxGimbal** | Not available | Space Engineers thrusters do not gimbal. | + +## VRChat SaccFlightAndVehicles + +VRChat does not have built-in vehicles, but there are third-party solutions built in VRChat's Udon scripting language. SaccFlightAndVehicles is one such solution. SaccFlightAndVehicles supports ground vehicles (cars/etc), air vehicles, and sea vehicles. + +| OMI_vehicle_body | SaccFlightAndVehicles | Additional Notes | +| -------------------- | ----------------------- | ---------------------------------------------------------------------------------------- | +| **gyroTorque** | Pitch/Yaw/Roll settings | Sacc air vehicles have an extensive rotation system. | +| **maxSpeed** | Speed settings | Sacc has a speed system with many parameters. | +| **pilotSeat** | Steering controls | Sacc allows controlling steering in many ways including grabbing a steering wheel in VR. | +| **angularDampeners** | Pitch/Yaw/Roll settings | Sacc air vehicles have an extensive rotation system. | +| **linearDampeners** | Brake settings | Sacc has a brake system with many parameters. | +| **useThrottle** | Throttle settings | Sacc has a throttle system with many parameters. | + +| OMI_vehicle_wheel | Sacc Wheels | Additional Notes | +| -------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------ | +| **currentForceRatio** | Throttle controls | Sacc allows controlling throttle in many ways. | +| **currentSteeringRatio** | Steering controls | Sacc allows controlling steering in many ways including grabbing a steering wheel in VR. | +| **maxForce** | Engine Influence | Sacc also requires a wheel to be a drive wheel for it to apply force. | +| **maxSteeringAngle** | Steering Wheel Degrees | Sacc defines this per-body using degrees, OMI_vehicle_wheel uses radians and is per-wheel. | +| **physicsMaterial** | Wheel Slow Down & Grip | Sacc defines "CurrentGrip" and "CurrentWheelSlowDown" instead of friction. | +| **radius** | Wheel Radius | | +| **suspensionDampingCompression** | Damping Force Multi | | +| **suspensionDampingRebound** | Damping Force Multi | | +| **suspensionStiffness** | Max Suspension Force | | +| **suspensionTravel** | Suspension Distance | | +| **width** | Not available | Sacc wheels are infinitely thin discs. | +| glTF node position | Wheel Point | Use glTF nodes for all transforms. | + +Note: SaccFlightAndVehicles has many hundreds more properties that are very specific and don't map to any of the OMI_vehicle extension properties. They have been omitted for brevity. diff --git a/extensions/2.0/OMI_vehicle_body/schema/node.OMI_vehicle_body.schema.json b/extensions/2.0/OMI_vehicle_body/schema/node.OMI_vehicle_body.schema.json new file mode 100644 index 0000000..cf4bc8a --- /dev/null +++ b/extensions/2.0/OMI_vehicle_body/schema/node.OMI_vehicle_body.schema.json @@ -0,0 +1,65 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "node.OMI_vehicle_body.schema.json", + "title": "OMI_vehicle_body glTF Node Extension", + "type": "object", + "properties": { + "angularActivation": { + "type": "array", + "description": "The input value controlling the ratio of the vehicle's angular forces as a Vector3.", + "items": { + "type": "number" + }, + "minItems": 3, + "maxItems": 3, + "default": [0.0, 0.0, 0.0] + }, + "linearActivation": { + "type": "array", + "description": "The input value controlling the ratio of the vehicle's linear forces as a Vector3.", + "items": { + "type": "number" + }, + "minItems": 3, + "maxItems": 3, + "default": [0.0, 0.0, 0.0] + }, + "gyroTorque": { + "type": "array", + "description": "The gyroscope torque intrinsic to the vehicle, measured in Newton-meters per radian (kg⋅m²/s²/rad). Does not include torque from parts such as thrusters.", + "items": { + "type": "number" + }, + "minItems": 3, + "maxItems": 3, + "default": [0.0, 0.0, 0.0] + }, + "maxSpeed": { + "type": "number", + "description": "If non-negative, the speed in meters per second at which the vehicle should stop driving acceleration further.", + "default": -1.0 + }, + "pilotSeat": { + "allOf": [{ "$ref": "glTFid.schema.json" }], + "description": "The index of the OMI_seat glTF node to use as the pilot seat / driver seat.", + "default": -1 + }, + "angularDampeners": { + "type": "boolean", + "description": "If true, the vehicle should slow its rotation down when not given angular activation input for a specific rotation.", + "default": true + }, + "linearDampeners": { + "type": "boolean", + "description": "If true, the vehicle should slow itself down when not given linear activation input for a specific direction.", + "default": false + }, + "useThrottle": { + "type": "boolean", + "description": "If true, the vehicle should use a throttle for forward movement. If maxSpeed is non-negative, the throttle should be a ratio of that speed, otherwise it should be a ratio of thrust power.", + "default": false + }, + "extensions": { }, + "extras": { } + } +} diff --git a/extensions/2.0/OMI_vehicle_hover_thruster/README.md b/extensions/2.0/OMI_vehicle_hover_thruster/README.md new file mode 100644 index 0000000..3e3c02e --- /dev/null +++ b/extensions/2.0/OMI_vehicle_hover_thruster/README.md @@ -0,0 +1,149 @@ +# OMI_vehicle_hover_thruster + +## Contributors + +- Aaron Franke, Godot Engine. + +## Status + +Open Metaverse Interoperability Group Draft Proposal + +## Dependencies + +Written against the glTF 2.0 spec. + +Designed to be used together with the `OMI_vehicle_body` spec. + +## Overview + +This extension allows specifying generic thrusters in glTF scenes. + +Hover thrusters as defined by `OMI_vehicle_hover_thruster` use hover energy for propulsion and can optionally gimbal. The experienced force is proportional to both the hover energy and the distance from the ground - the closer to the ground, the more force they provide. The `"maxHoverEnergy"` property is measured in Newton-meters (N⋅m or kg⋅m²/s²), so dividing this number by the amount of meters above the ground will give the force in Newtons (N). This extension does not define fuel, power, or any other resource that the hover thruster may consume. + +Hover thruster glTF nodes can be added as a descendant of a vehicle body glTF node. A vehicle body is defined as a glTF node with the `OMI_vehicle_body` extension. Then hover thrusters can be added to the vehicle by adding descendant nodes with the `OMI_vehicle_hover_thruster` extension. Hover thrusters must be specified on their own glTF nodes as descendants of a vehicle body, not on the same glTF node as the vehicle; this so that they may have their own transform relative to the vehicle, including their own position, and so that they and may optionally rotate for gimbal. + +A hover thruster's force is applied in the local +Z direction of the glTF node, pushing the vehicle in the hover thruster's local +Z direction, which mimics the "nozzle" pointing in the local -Z direction. The forward direction of a vehicle is the local +Z direction. A hover thruster with no rotation relative to the vehicle will push the vehicle forward in its local +Z direction, as if shooting propellant out the back (local -Z). To apply hover thrust in other directions, the hover thruster must be rotated relative to the vehicle. + +Hover thrusters can optionally gimbal, which means they can rotate to apply hover thrust in different directions. The maximum angle the hover thruster can rotate is defined by the `"maxGimbal"` property. When a hover thruster gimbals, the glTF node MUST rotate to match the gimbal, such that the local +Z direction of the glTF node is always the direction the hover thruster is applying thrust, and the local -Z direction is the direction the "nozzle" is pointing. This rotation means that any child nodes will also rotate with the hover thruster, allowing for visual effects like a rotating hover thruster nozzle mesh. + +Hover thrusters are invisible by default. To give a thruster a visual appearance, you may wish to add another glTF node with a mesh as a child of the thruster. + +### Example: + +The file [hovercraft.gltf](examples/hovercraft.gltf) defines a chrome-colored hovercraft with 4 hover thrusters at the bottom corners of the hovercraft, each a child node of the hovercraft's vehicle body. The hovercraft can rotate itself by gimballing its hover thrusters. The hovercraft also has a pilot seat, allowing a player to sit in it to control the vehicle. This can be imported into a game that supports OMI_vehicle_thruster to have a controllable hovercraft. + +For this example, the only source of thrust of any kind is the hover thrusters, so it will struggle to do things like moving up a steep hill. To enhance the handling of the hovercraft, one could add some gyroscope torque and/or non-hover thrusters. However, a proper implementation of OMI_vehicle_hover_thruster should be able to handle a hovercraft with only hover thrusters, including stable hovering over flat ground, controlled angular movement such as pitch, yaw, and roll, preventing the hovercraft from flipping over, and controlled linear movement forward/back/left/right with a bit of up/down movement. + +The hover thruster settings are defined at the document level: + +```json +{ + "extensions": { + "OMI_vehicle_hover_thruster": { + "hoverThrusters": [ + { + "maxGimbal": 0.25, + "maxHoverEnergy": 30000 + } + ] + } + }, +} +``` + +This is then referenced by index on hover thruster glTF nodes. For example, the front right thruster: + +```json +{ + "name": "HoverThrusterFR", + "rotation": [-0.17670360207558, 0.81949108839035, 0.426600247621536, 0.33944433927536], + "translation": [-1.2, -0.25, 2.1], + "extensions": { + "OMI_vehicle_hover_thruster": { "hoverThruster": 0 } + } +} +``` + +The hover thruster settings at index 0 have a maximum hover energy of 30000 Newton-meters, and a maximum gimbal of 0.25 radians, which allows this hover thruster to rotate up to about 14 degrees. The hovercraft example has 4 thrusters, each using the same thruster settings, so this example file has only one entry in the document-level hover thruster settings array. + +## glTF Schema Updates + +This extension consists of three new data structures for defining hover thruster specifications on the root glTF document and referencing them on a glTF node. The main data structure defines hover thruster parameters and is what most of this document describes. The second data structure uses the key `"OMI_vehicle_hover_thruster"` in the document-level `"extensions"` which contains a list of the main data structures of thruster parameters. The third data structure uses the key `"OMI_vehicle_hover_thruster"` in the node-level `"extensions"` which contains an index of the thruster parameters to use from the document-level thruster list. + +The extension must also be added to the glTF's `extensionsUsed` array and because it is optional, it does not need to be added to the `extensionsRequired` array. + +### Property Summary + +The rest of the document, including this summary, defines the properties for the main data structure. + +| | Type | Description | Default value | +| ---------------------- | ----------- | --------------------------------------------------------------------------- | -------------------- | +| **currentHoverRatio** | `number` | The ratio of the maximum hover energy the thruster is using for propulsion. | 0.0 | +| **currentGimbalRatio** | `number[2]` | The ratios of the maximum gimbal angle the hover thruster is rotated to. | [0.0, 0.0] | +| **maxHoverEnergy** | `number` | The maximum hover energy in Newton-meters (kg⋅m²/s²) of this thruster. | Required, no default | +| **maxGimbal** | `number` | The maximum angle the hover thruster can rotate in radians. | 0.0 | + +#### Current Hover Ratio + +The `"currentHoverRatio"` property is a number that defines the ratio of `"maxHoverEnergy"` the hover thruster is using for propulsion. If not specified, the default value is 0.0, which means the hover thruster is not firing. + +This value is expected to be between 0.0 and 1.0. The behavior of values outside of this range is implementation-defined, it may be clamped to this range, or be allowed to go beyond this range for some kind of turbo boost or overdrive. Negative values should not be used since the hover thrust strength may be different in the opposite direction, so for reverse thrust, a separate hover thruster should be used facing the opposite direction. For example, a hovercraft may have some hover thrusters on the front and some on the rear, with the rear hover thrusters propelling the vehicle forward, and the front hover thrusters propelling the vehicle backward. + +This value is expected to dynamically change at runtime. This is not usually saved in the glTF file, but it is allowed to be. The input used to set this value is implementation-defined. The thruster may be used on its own without a vehicle, but usually it is used for child nodes of a vehicle. In the common case of a vehicle, the hover thruster's force ratio is calculated using the vehicle's `"angularActivation"` and `"linearActivation"` properties, as defined by `OMI_vehicle_body` extension. However, this value may be set by other means, such as a `KHR_interactivity` script. + +#### Current Gimbal Ratio + +The `"currentGimbalRatio"` property is an array of two numbers that defines the ratio of `"maxGimbal"` the thruster is rotated to. If not specified, the default value is [0.0, 0.0], which means the thruster is not gimballing. + +Each number is expected to be between -1.0 and 1.0, and both numbers together should form a vector of length no longer than 1.0. The behavior of values outside of this range is implementation-defined, it may be clamped to this range, or be allowed to go beyond this range for some kind of over-rotation or over-gimbal. + +For a hover thruster in the default orientation (rear hover thruster providing forward +Z hover thrust by "shooting propellant" out the back -Z), due to glTF's right-handed coordinate system, positive X gimbal rotates the thruster's nozzle up (resulting in hover thrust down), and positive Y gimbal rotates the thruster's nozzle to the right (resulting in hover thrust to the left). A hover thruster provides hover thrust in the direction of its local +Z axis after applying gimbal, which is like the hover thruster shooting propellant out the back in its local -Z direction. + +This value is expected to dynamically change at runtime. This is not usually saved in the glTF file, but it is allowed to be. The input used to set this value is implementation-defined. The gimbal may be used on its own without a vehicle, but usually it is used for child nodes of a vehicle. In the common case of a vehicle, the thruster's gimbal ratio is calculating using the vehicle's `"angularActivation"` and `"linearActivation"` properties, as defined by `OMI_vehicle_body` extension. However, this value may be set by other means, such as a `KHR_interactivity` script. + +The thruster's gimbal values can be converted into a rotation Quaternion using the following formula: + +```javascript +function getGimbalRotationQuaternion() { + if (isZeroApprox(currentGimbalRatio) || isZeroApprox(maxGimbal)) { + return Quaternion.IDENTITY; + } + let rotAngles = limitLength(currentGimbalRatio).multiply(maxGimbal); + let angleMag = rotAngles.length(); + let sinNormAngle = Math.sin(angleMag / 2.0) / angleMag; + let cosHalfAngle = Math.cos(angleMag / 2.0); + return new Quaternion(rotAngles.x * sinNormAngle, rotAngles.y * sinNormAngle, 0.0, cosHalfAngle); +} +``` + +Where `isZeroApprox` returns true if the vector or number is approximately zero, and `limitLength` returns the same vector when its length is less than or equal to 1.0, or a normalized version of the vector when its length is greater than 1.0. The variable `rotAngles` is the rotation in radians, and `angleMag` is the total angle in radians. + +#### Max Hover Energy + +The `"maxHoverEnergy"` property is a number that defines the maximum hover energy that the hover thruster can provide, measured in Newton-meters (N⋅m), or kg⋅m²/s² in SI base units, or simply Joules. This property is required and has no default value. + +Valid values are positive numbers. Hover energy is a sci-fi concept, so there is no real-world equivalent. The desired hover energy of a thruster varies a lot depending on the mass of the vehicle, the desired distance above the ground to hover at, and the desired acceleration and handling characteristics. This wide variety means that there is no sane choice for a default value, so there is no default value. A hover thruster with zero hover energy is useless. + +As an example, if a vehicle has a mass of 1000 kg, and is designed for hovering a maximum 4 meters between the thrusters and the ground in 10 m/s² gravity, then the minimum total hover energy required is 1000 kg * 4 m * 10 m/s² = 40000 N⋅m = 40000 J. The [hovercraft.gltf](examples/hovercraft.gltf) example has a mass of 2000 kg and 4 hover thrusters with 30000 N⋅m of hover energy each, which can sustain a hover height of (4 * 30000 N⋅m) / (2000 kg * 10 m/s²) = 6 meters above the ground. Note that the hovercraft may hover lower than this if the hover thrusters are not firing at full power or if the thrusters are not pointing directly down, in practice the example hovercraft hovers at around 4 to 5 meters above the ground. + +The active force of the hover thruster is defined by a combination of this property, the distance between the hover thruster and the ground, and the `"currentForceRatio"` property. In general, hover thrusters should fire when there is input in the same direction the thruster exerts force in, such as rear hover thrusters providing forward hover energy when forward input is given. + +#### Max Gimbal + +The `"maxGimbal"` property is a number that defines the maximum angle in radians that the hover thruster can rotate. If not specified, the default value is 0.0 radians in both axes, which means the hover thruster cannot rotate. + +Valid values are between 0.0 and τ/2 radians (π or 3.14159265... radians, or 180 degrees). Sane values are between 0.0 and 1.0 radians, while realistic values are between 0.0 and 0.2 radians. + +The active gimbal of the hover thruster is defined by a combination of this property and the `"currentGimbalRatio"` property. In general, hover thrusters should gimbal when there is angular input in the same direction the hover thruster gimbals in, such as a rear hover thruster gimballing its nozzle up when up input is given, causing the thrust direction to angle down, causing the vehicle to pitch up. + +### JSON Schema + +See [glTF.OMI_vehicle_hover_thruster.thruster.schema.json](schema/glTF.OMI_vehicle_hover_thruster.thruster.schema.json) for the main thruster parameter schema, [glTF.OMI_vehicle_hover_thruster.schema.json](schema/glTF.OMI_vehicle_hover_thruster.schema.json) for the document-level list of thruster parameters, and [node.OMI_vehicle_hover_thruster.schema.json](schema/node.OMI_vehicle_hover_thruster.schema.json) for the node-level collider selection. + +## Known Implementations + +- Godot Engine + +## Resources: + +- None diff --git a/extensions/2.0/OMI_vehicle_hover_thruster/examples/hovercraft.bin b/extensions/2.0/OMI_vehicle_hover_thruster/examples/hovercraft.bin new file mode 100644 index 0000000..13a73d6 Binary files /dev/null and b/extensions/2.0/OMI_vehicle_hover_thruster/examples/hovercraft.bin differ diff --git a/extensions/2.0/OMI_vehicle_hover_thruster/examples/hovercraft.gltf b/extensions/2.0/OMI_vehicle_hover_thruster/examples/hovercraft.gltf new file mode 100644 index 0000000..0960a31 --- /dev/null +++ b/extensions/2.0/OMI_vehicle_hover_thruster/examples/hovercraft.gltf @@ -0,0 +1,267 @@ +{ + "accessors": [ + { + "bufferView": 0, + "byteOffset": 0, + "componentType": 5126, + "count": 378, + "max": [1.98310995101929, 1.67935001850128, 2.88399004936218], + "min": [-1.98310995101929, -0.93333500623703, -2.9108099937439], + "normalized": false, + "type": "VEC3" + }, + { + "bufferView": 1, + "byteOffset": 0, + "componentType": 5123, + "count": 378, + "max": [377], + "min": [0], + "normalized": false, + "type": "SCALAR" + }, + { + "bufferView": 2, + "byteOffset": 0, + "componentType": 5126, + "count": 3793, + "max": [1.98310720920563, 1.6796053647995, 2.88399434089661], + "min": [-1.98310720920563, -0.93333530426025, -2.91081380844116], + "normalized": false, + "type": "VEC3" + }, + { + "bufferView": 3, + "byteOffset": 0, + "componentType": 5126, + "count": 3793, + "max": [1, 0.999988317489624, 0.996253252029419, 1], + "min": [-1, -0.99999952316284, -0.9926170706749, -1], + "normalized": false, + "type": "VEC4" + }, + { + "bufferView": 4, + "byteOffset": 0, + "componentType": 5126, + "count": 3793, + "max": [1, 0.999989926815033, 0.999467015266418], + "min": [-1, -1, -0.99987590312958], + "normalized": false, + "type": "VEC3" + }, + { + "bufferView": 5, + "byteOffset": 0, + "componentType": 5126, + "count": 3793, + "max": [0.87500011920929, 1], + "min": [0.125, -0.00000011920929], + "normalized": false, + "type": "VEC2" + }, + { + "bufferView": 6, + "byteOffset": 0, + "componentType": 5123, + "count": 6792, + "max": [1594], + "min": [0], + "normalized": false, + "type": "SCALAR" + } + ], + "asset": { + "extensions": { "KHR_xmp_json_ld": { "packet": 0 } }, + "generator": "Godot Engine v4.3.stable.official@77dcf97d82cbfe4e4615475fa52ca03da645dbd8", + "version": "2.0" + }, + "bufferViews": [ + { "buffer": 0, "byteLength": 4536, "byteOffset": 0, "byteStride": 12, "target": 34962 }, + { "buffer": 0, "byteLength": 756, "byteOffset": 4536, "target": 34963 }, + { "buffer": 0, "byteLength": 45516, "byteOffset": 5292, "byteStride": 12, "target": 34962 }, + { "buffer": 0, "byteLength": 60688, "byteOffset": 50808, "byteStride": 16, "target": 34962 }, + { "buffer": 0, "byteLength": 45516, "byteOffset": 111496, "byteStride": 12, "target": 34962 }, + { "buffer": 0, "byteLength": 30344, "byteOffset": 157012, "byteStride": 8, "target": 34962 }, + { "buffer": 0, "byteLength": 13584, "byteOffset": 187356, "target": 34963 } + ], + "buffers": [{ "byteLength": 200940, "uri": "hovercraft.bin" }], + "extensions": { + "KHR_xmp_json_ld": { + "packets": [ + { + "@context": { "dc": "http://purl.org/dc/elements/1.1/" }, + "@id": "", + "dc:creator": { "@list": ["Tameno", "aaronfranke"] }, + "dc:description": "Hovercraft test file for the OMI_vehicle_body and OMI_vehicle_hover_thruster extensions.", + "dc:format": "model/gltf+json", + "dc:rights": "Public domain", + "dc:subject": { "@set": ["Hovercraft"] }, + "dc:title": "Hovercraft", + "dc:type": { "@set": ["Vehicle", "Demo", "Test"] } + } + ] + }, + "OMI_physics_shape": { + "shapes": [ + { "convex": { "mesh": 0 }, "type": "convex" }, + { "box": { "size": [2, 2, 2] }, "type": "box" } + ] + }, + "OMI_vehicle_hover_thruster": { + "hoverThrusters": [ + { + "maxGimbal": 0.25, + "maxHoverEnergy": 30000 + } + ] + } + }, + "extensionsUsed": [ + "GODOT_single_root", + "KHR_xmp_json_ld", + "OMI_physics_body", + "OMI_physics_shape", + "OMI_seat", + "OMI_vehicle_body", + "OMI_vehicle_hover_thruster" + ], + "materials": [ + { + "extensions": {}, + "name": "HovercraftMaterial", + "pbrMetallicRoughness": { + "baseColorFactor": [1, 1, 1, 1], + "metallicFactor": 1, + "roughnessFactor": 0.2 + } + } + ], + "meshes": [ + { + "extras": { "targetNames": [] }, + "primitives": [{ "attributes": { "POSITION": 0 }, "indices": 1, "mode": 4 }] + }, + { + "extras": { "targetNames": [] }, + "primitives": [ + { + "attributes": { "NORMAL": 4, "POSITION": 2, "TANGENT": 3, "TEXCOORD_0": 5 }, + "indices": 6, + "material": 0, + "mode": 4 + } + ] + } + ], + "nodes": [ + { + "children": [1, 2, 3, 4, 5, 6, 7, 9, 11, 13], + "extensions": { + "OMI_physics_body": { "motion": { "mass": 2000, "type": "dynamic" } }, + "OMI_vehicle_body": { "linearDampeners": true, "pilotSeat": 13 } + }, + "name": "HovercraftBody" + }, + { + "extensions": { + "OMI_physics_body": { "collider": { "shape": 0 } } + }, + "name": "HovercraftCollider" + }, + { + "mesh": 1, + "name": "HovercraftMesh" + }, + { + "extensions": { + "OMI_vehicle_hover_thruster": { "hoverThruster": 0 } + }, + "name": "HoverThrusterBL", + "rotation": [-0.42660024762154, -0.33944433927536, -0.17670360207558, 0.81949108839035], + "translation": [1.2, -0.25, -2.1] + }, + { + "extensions": { + "OMI_vehicle_hover_thruster": { "hoverThruster": 0 } + }, + "name": "HoverThrusterBR", + "rotation": [-0.42660024762154, 0.33944433927536, 0.176703602075577, 0.81949108839035], + "translation": [-1.2, -0.25, -2.1] + }, + { + "extensions": { + "OMI_vehicle_hover_thruster": { "hoverThruster": 0 } + }, + "name": "HoverThrusterFL", + "rotation": [0.176703602075577, 0.81949108839035, 0.426600247621536, -0.33944433927536], + "translation": [1.2, -0.25, 2.1] + }, + { + "extensions": { + "OMI_vehicle_hover_thruster": { "hoverThruster": 0 } + }, + "name": "HoverThrusterFR", + "rotation": [-0.17670360207558, 0.81949108839035, 0.426600247621536, 0.33944433927536], + "translation": [-1.2, -0.25, 2.1] + }, + { + "children": [8], + "extensions": { + "OMI_physics_body": { "trigger": { "nodes": [8] } }, + "OMI_seat": { "back": [0, 0, -0.25], "foot": [0, -0.5, 0.25], "knee": [0, 0, 0.25] } + }, + "name": "PassengerSeatFront", + "translation": [-0.5, 0.5, 0] + }, + { + "extensions": { "OMI_physics_body": { "trigger": { "shape": 1 } } }, + "name": "PassengerSeatFrontShape", + "translation": [-0.75, 0, 0.75] + }, + { + "children": [10], + "extensions": { + "OMI_physics_body": { "trigger": { "nodes": [10] } }, + "OMI_seat": { "back": [0, 0, -0.25], "foot": [0, -0.5, 0.25], "knee": [0, 0, 0.25] } + }, + "name": "PassengerSeatRearLeft", + "translation": [0.5, 0.5, -1.125] + }, + { + "extensions": { "OMI_physics_body": { "trigger": { "shape": 1 } } }, + "name": "PassengerSeatRearLeftShape", + "translation": [0.75, 0, -0.75] + }, + { + "children": [12], + "extensions": { + "OMI_physics_body": { "trigger": { "nodes": [12] } }, + "OMI_seat": { "back": [0, 0, -0.25], "foot": [0, -0.5, 0.25], "knee": [0, 0, 0.25] } + }, + "name": "PassengerSeatRearRight", + "translation": [-0.5, 0.5, -1.125] + }, + { + "extensions": { "OMI_physics_body": { "trigger": { "shape": 1 } } }, + "name": "PassengerSeatRearRightShape", + "translation": [-0.75, 0, -0.75] + }, + { + "children": [14], + "extensions": { + "OMI_physics_body": { "trigger": { "nodes": [14] } }, + "OMI_seat": { "back": [0, 0, -0.25], "foot": [0, -0.5, 0.25], "knee": [0, 0, 0.25] } + }, + "name": "PilotSeat", + "translation": [0.5, 0.5, 0] + }, + { + "extensions": { "OMI_physics_body": { "trigger": { "shape": 1 } } }, + "name": "PilotSeatShape", + "translation": [0.75, 0, 0.75] + } + ], + "scene": 0, + "scenes": [{ "nodes": [0] }] +} diff --git a/extensions/2.0/OMI_vehicle_hover_thruster/schema/glTF.OMI_vehicle_hover_thruster.schema.json b/extensions/2.0/OMI_vehicle_hover_thruster/schema/glTF.OMI_vehicle_hover_thruster.schema.json new file mode 100644 index 0000000..46ab0d5 --- /dev/null +++ b/extensions/2.0/OMI_vehicle_hover_thruster/schema/glTF.OMI_vehicle_hover_thruster.schema.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "glTF.OMI_vehicle_hover_thruster.schema.json", + "title": "OMI_vehicle_hover_thruster glTF Document Extension", + "type": "object", + "allOf": [ { "$ref": "glTFProperty.schema.json" } ], + "properties": { + "hoverThrusters": { + "type": "array", + "description": "An array of hover thruster parameters that can be referenced by nodes.", + "items": { + "type": "object", + "$ref": "glTF.OMI_vehicle_hover_thruster.thruster.schema.json" + }, + "minItems": 1 + }, + "extensions": {}, + "extras": {} + }, + "required": [ + "hoverThrusters" + ] +} diff --git a/extensions/2.0/OMI_vehicle_hover_thruster/schema/glTF.OMI_vehicle_hover_thruster.thruster.schema.json b/extensions/2.0/OMI_vehicle_hover_thruster/schema/glTF.OMI_vehicle_hover_thruster.thruster.schema.json new file mode 100644 index 0000000..f99ffca --- /dev/null +++ b/extensions/2.0/OMI_vehicle_hover_thruster/schema/glTF.OMI_vehicle_hover_thruster.thruster.schema.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "glTF.OMI_vehicle_hover_thruster.thruster.schema.json", + "title": "OMI_vehicle_hover_thruster Hover Thruster Parameters", + "type": "object", + "properties": { + "currentForceRatio": { + "type": "number", + "description": "The ratio of the maximum hover energy the hover thruster is using for propulsion.", + "default": 0.0 + }, + "currentGimbalRatio": { + "type": "array", + "description": "The ratio of the maximum gimbal angles the hover thruster is rotated to.", + "items": { + "type": "number" + }, + "minItems": 2, + "maxItems": 2, + "default": [0.0, 0.0] + }, + "maxHoverEnergy": { + "type": "number", + "description": "The maximum hover energy force in Newton-meters (N⋅m or kg⋅m²/s²) that the hover thruster can provide. Required." + }, + "maxGimbal": { + "type": "array", + "description": "The maximum angle the hover thruster can gimbal or rotate in radians.", + "default": 0.0 + }, + "extensions": { }, + "extras": { } + }, + "required": [ + "maxHoverEnergy" + ] +} diff --git a/extensions/2.0/OMI_vehicle_hover_thruster/schema/node.OMI_vehicle_hover_thruster.schema.json b/extensions/2.0/OMI_vehicle_hover_thruster/schema/node.OMI_vehicle_hover_thruster.schema.json new file mode 100644 index 0000000..ea40823 --- /dev/null +++ b/extensions/2.0/OMI_vehicle_hover_thruster/schema/node.OMI_vehicle_hover_thruster.schema.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "node.OMI_vehicle_hover_thruster.schema.json", + "title": "OMI_vehicle_hover_thruster glTF Node Extension", + "type": "object", + "allOf": [ { "$ref": "glTFProperty.schema.json" } ], + "properties": { + "hoverThruster": { + "allOf": [ { "$ref": "glTFid.schema.json" } ], + "description": "The id of the hover thruster parameters referenced by this node." + }, + "extensions": {}, + "extras": {} + }, + "required": [ + "hoverThruster" + ] +} diff --git a/extensions/2.0/OMI_vehicle_thruster/README.md b/extensions/2.0/OMI_vehicle_thruster/README.md new file mode 100644 index 0000000..3f898cb --- /dev/null +++ b/extensions/2.0/OMI_vehicle_thruster/README.md @@ -0,0 +1,155 @@ +# OMI_vehicle_thruster + +## Contributors + +- Aaron Franke, Godot Engine. + +## Status + +Open Metaverse Interoperability Group Draft Proposal + +## Dependencies + +Written against the glTF 2.0 spec. + +Designed to be used together with the `OMI_vehicle_body` spec. + +## Overview + +This extension allows specifying generic thrusters in glTF scenes. + +Thrusters as defined by `OMI_vehicle_thruster` are very general-purpose, they emit a force and can optionally gimbal. `OMI_vehicle_thruster` can be used to define rocket engines, jet engines, control thrusters, or any other kind of thruster. This extension does not define fuel, power, or any other resource that the thruster may consume. + +Thruster glTF nodes can be added as a descendant of a vehicle body glTF node. A vehicle body is defined as a glTF node with the `OMI_vehicle_body` extension. Then thrusters can be added to the vehicle by adding descendant nodes with the `OMI_vehicle_thruster` extension. Thrusters must be specified on their own glTF nodes as descendants of a vehicle body, not on the same glTF node as the vehicle; this so that they may have their own transform relative to the vehicle, including their own position, and so that they and may optionally rotate for gimbal. + +A thruster's force is applied in the local +Z direction of the glTF node, pushing the vehicle in the thruster's local +Z direction, which mimics the "nozzle" pointing in the local -Z direction. The forward direction of a vehicle is the local +Z direction. A thruster with no rotation relative to the vehicle will push the vehicle forward in its local +Z direction, as if shooting propellant out the back (local -Z). To apply thrust in other directions, the thruster must be rotated relative to the vehicle. + +Thrusters can optionally gimbal, which means they can rotate to apply thrust in different directions. The maximum angle the thruster can rotate is defined by the `"maxGimbal"` property. When a thruster gimbals, the glTF node MUST rotate to match the gimbal, such that the local +Z direction of the glTF node is always the direction the thruster is applying thrust, and the local -Z direction is the direction the "nozzle" is pointing. This rotation means that any child nodes will also rotate with the thruster, allowing for visual effects like a rotating thruster nozzle mesh. + +Thrusters are invisible by default. To give a thruster a visual appearance, you may wish to add another glTF node with a mesh as a child of the thruster. + +### Example: + +The file [simple_spaceship.gltf](examples/simple_spaceship.gltf) defines a low-poly space ship with thrusters in all 6 directions, each a child node of the ship's vehicle body. The ship can rotate itself with gyroscope torque or gimbal its thrusters to rotate. The ship also has a pilot seat, allowing a player to sit in it to control the vehicle. This can be imported into a game that supports OMI_vehicle_thruster to have a controllable spaceship. + +The thruster settings are defined at the document level: + +```json +{ + "extensions": { + "OMI_vehicle_thruster": { + "thrusters": [ + { + "maxForce": 1000, + "maxGimbal": 0.25 + }, + { + "maxForce": 2000, + "maxGimbal": 0.2 + }, + { + "maxForce": 5000, + "maxGimbal": 0.25 + } + ] + } + } +} +``` + +This is then referenced by index on thruster glTF nodes. For example, the right main thruster: + +```json +{ + "name": "SimpleSpaceshipMainThrusterRight", + "translation": [-0.5333333, 0, -1.25], + "extensions": { + "OMI_vehicle_thruster": { + "thruster": 2 + } + } +} +``` + +The thruster settings at index 2 have a maximum force of 5000 Newtons, and a maximum gimbal of 0.25 radians, which allows this thruster to rotate up to about 14 degrees. The simple spaceship example has several more thrusters, each using one of the items in the thruster settings array. + +## glTF Schema Updates + +This extension consists of three new data structures for defining thruster specifications on the root glTF document and referencing them on a glTF node. The main data structure defines thruster parameters and is what most of this document describes. The second data structure uses the key `"OMI_vehicle_thruster"` in the document-level `"extensions"` which contains a list of the main data structures of thruster parameters. The third data structure uses the key `"OMI_vehicle_thruster"` in the node-level `"extensions"` which contains an index of the thruster parameters to use from the document-level thruster list. + +The extension must also be added to the glTF's `extensionsUsed` array and because it is optional, it does not need to be added to the `extensionsRequired` array. + +### Property Summary + +The rest of the document, including this summary, defines the properties for the main data structure. + +| | Type | Description | Default value | +| ---------------------- | ----------- | -------------------------------------------------------------------- | -------------------- | +| **currentForceRatio** | `number` | The ratio of the maximum force the thruster is using for propulsion. | 0.0 | +| **currentGimbalRatio** | `number[2]` | The ratios of the maximum gimbal angle the thruster is rotated to. | [0.0, 0.0] | +| **maxForce** | `number` | The maximum thrust force in Newtons (kg⋅m/s²) of this thruster. | Required, no default | +| **maxGimbal** | `number` | The maximum angle the thruster can rotate in radians. | 0.0 | + +#### Current Force Ratio + +The `"currentForceRatio"` property is a number that defines the ratio of `"maxForce"` the thruster is using for propulsion. If not specified, the default value is 0.0, which means the thruster is not firing. + +This value is expected to be between 0.0 and 1.0. The behavior of values outside of this range is implementation-defined, it may be clamped to this range, or be allowed to go beyond this range for some kind of turbo boost or overdrive. Negative values should not be used since the thrust strength may be different in the opposite direction, so for reverse thrust, a separate thruster should be used facing the opposite direction. For example, a rocket engine will only use one thruster, while a jet engine may have two thrusters, one for forward thrust and one for reverse thrust, with the reverse thruster having lower thrust. + +This value is expected to dynamically change at runtime. This is not usually saved in the glTF file, but it is allowed to be. The input used to set this value is implementation-defined. The thruster may be used on its own without a vehicle, but usually it is used for child nodes of a vehicle. In the common case of a vehicle, the thruster's force ratio is set by the vehicle's `"linearActivation"` property in the thrust direction, as defined by `OMI_vehicle_body` extension. However, this value may be set by other means, such as a `KHR_interactivity` script. + +#### Current Gimbal Ratio + +The `"currentGimbalRatio"` property is an array of two numbers that defines the ratio of `"maxGimbal"` the thruster is rotated to. If not specified, the default value is [0.0, 0.0], which means the thruster is not gimballing. + +Each number is expected to be between -1.0 and 1.0, and both numbers together should form a vector of length no longer than 1.0. The behavior of values outside of this range is implementation-defined, it may be clamped to this range, or be allowed to go beyond this range for some kind of over-rotation or over-gimbal. + +For a thruster in the default orientation (rear thruster providing forward +Z thrust by shooting propellant out the back -Z), due to glTF's right-handed coordinate system, positive X gimbal rotates the thruster's nozzle up (resulting in thrust down), and positive Y gimbal rotates the thruster's nozzle to the right (resulting in thrust to the left). A thruster provides thrust in the direction of its local +Z axis after applying gimbal, which is like the thruster shooting propellant out the back in its local -Z direction. + +This value is expected to dynamically change at runtime. This is not usually saved in the glTF file, but it is allowed to be. The input used to set this value is implementation-defined. The gimbal may be used on its own without a vehicle, but usually it is used for child nodes of a vehicle. In the common case of a vehicle, the thruster's gimbal ratio is set by the vehicle's `"angularActivation"` property in the gimbal direction, as defined by `OMI_vehicle_body` extension. However, this value may be set by other means, such as a `KHR_interactivity` script. + +The thruster's gimbal values can be converted into a rotation Quaternion using the following formula: + +```javascript +function getGimbalRotationQuaternion() { + if (isZeroApprox(currentGimbalRatio) || isZeroApprox(maxGimbal)) { + return Quaternion.IDENTITY; + } + let rotAngles = limitLength(currentGimbalRatio).multiply(maxGimbal); + let angleMag = rotAngles.length(); + let sinNormAngle = Math.sin(angleMag / 2.0) / angleMag; + let cosHalfAngle = Math.cos(angleMag / 2.0); + return new Quaternion(rotAngles.x * sinNormAngle, rotAngles.y * sinNormAngle, 0.0, cosHalfAngle); +} +``` + +Where `isZeroApprox` returns true if the vector or number is approximately zero, and `limitLength` returns the same vector when its length is less than or equal to 1.0, or a normalized version of the vector when its length is greater than 1.0. The variable `rotAngles` is the rotation in radians, and `angleMag` is the total angle in radians. + +#### Max Force + +The `"maxForce"` property is a number that defines the maximum thrust force in Newtons (kg⋅m/s²) that the thruster can provide. This property is required and has no default value. + +Valid values are positive numbers. Rocket engines in real life have a wide variety of force amounts. Typical rocket engines are usually between one thousand and one million Newtons, but may be much less in the case of small control thrusters, or much more in the case of large main engines. A thruster with zero force is useless. The desired force of a thruster also varies a lot depending on the mass of the vehicle and the desired acceleration and control over it. This wide variety means that there is no sane choice for a default value, so there is no default value. + +The active force of the thruster is defined by a combination of this property and the `"currentForceRatio"` property. In general, thrusters should fire when there is linear input in the same direction the thruster exerts force in, such as rear thrusters providing a forward force when forward input is given. + +#### Max Gimbal + +The `"maxGimbal"` property is a number that defines the maximum angle in radians that the thruster can rotate. If not specified, the default value is 0.0 radians in both axes, which means the thruster cannot rotate. + +Valid values are between 0.0 and τ/2 radians (π or 3.14159265... radians, or 180 degrees). Sane values are between 0.0 and 1.0 radians, while realistic values are between 0.0 and 0.2 radians. + +The active gimbal of the thruster is defined by a combination of this property and the `"currentGimbalRatio"` property. In general, thrusters should gimbal when there is angular input in the same direction the thruster gimbals in, such as a rear thruster gimballing its nozzle up when up input is given, causing the thrust direction to angle down, causing the vehicle to pitch up. + +### JSON Schema + +See [glTF.OMI_vehicle_thruster.thruster.schema.json](schema/glTF.OMI_vehicle_thruster.thruster.schema.json) for the main thruster parameter schema, [glTF.OMI_vehicle_thruster.schema.json](schema/glTF.OMI_vehicle_thruster.schema.json) for the document-level list of thruster parameters, and [node.OMI_vehicle_thruster.schema.json](schema/node.OMI_vehicle_thruster.schema.json) for the node-level collider selection. + +## Known Implementations + +- + +## Resources: + +- Kerbal Space Program wiki on engines: https://wiki.kerbalspaceprogram.com/wiki/Parts#Engines +- Space Engineers wiki on thrusters: https://spaceengineers.fandom.com/wiki/Thruster diff --git a/extensions/2.0/OMI_vehicle_thruster/examples/rocket_ship.bin b/extensions/2.0/OMI_vehicle_thruster/examples/rocket_ship.bin new file mode 100644 index 0000000..6485c97 Binary files /dev/null and b/extensions/2.0/OMI_vehicle_thruster/examples/rocket_ship.bin differ diff --git a/extensions/2.0/OMI_vehicle_thruster/examples/rocket_ship.gltf b/extensions/2.0/OMI_vehicle_thruster/examples/rocket_ship.gltf new file mode 100644 index 0000000..b97a46e --- /dev/null +++ b/extensions/2.0/OMI_vehicle_thruster/examples/rocket_ship.gltf @@ -0,0 +1,261 @@ +{ + "accessors": [ + { + "bufferView": 0, + "byteOffset": 0, + "componentType": 5126, + "count": 980, + "max": [1.97742998600006, 2.2833399772644, 5.4761004447937], + "min": [-1.97742998600006, -1.49512994289398, -3.59999990463257], + "normalized": false, + "type": "VEC3" + }, + { + "bufferView": 1, + "byteOffset": 0, + "componentType": 5126, + "count": 980, + "max": [0.997367382049561, 0.998750805854797, 0.981513559818268, 1], + "min": [-0.99995929002762, -0.99766159057617, -0.99944770336151, -1], + "normalized": false, + "type": "VEC4" + }, + { + "bufferView": 2, + "byteOffset": 0, + "componentType": 5126, + "count": 980, + "max": [1, 0.999686658382416, 0.998062670230865], + "min": [-1, -0.99944996833801, -0.99985074996948], + "normalized": false, + "type": "VEC3" + }, + { + "bufferView": 3, + "byteOffset": 0, + "componentType": 5126, + "count": 980, + "max": [0.988067448139191, 0.991119265556335], + "min": [0.00279240100644529, 0.0032501716632396], + "normalized": false, + "type": "VEC2" + }, + { + "bufferView": 4, + "byteOffset": 0, + "componentType": 5123, + "count": 1311, + "max": [755], + "min": [0], + "normalized": false, + "type": "SCALAR" + }, + { + "bufferView": 5, + "byteOffset": 0, + "componentType": 5126, + "count": 66, + "max": [0.223891004920006, 2.28324007987976, -0.56534498929977], + "min": [-0.22389100492001, 1.22107005119324, -3.59999990463257], + "normalized": false, + "type": "VEC3" + }, + { + "bufferView": 6, + "byteOffset": 0, + "componentType": 5123, + "count": 66, + "max": [65], + "min": [0], + "normalized": false, + "type": "SCALAR" + }, + { + "bufferView": 7, + "byteOffset": 0, + "componentType": 5126, + "count": 504, + "max": [1.49483001232147, 1.68090999126434, 4.35096979141235], + "min": [-1.49512994289398, -1.49512994289398, -3.08471989631653], + "normalized": false, + "type": "VEC3" + }, + { + "bufferView": 8, + "byteOffset": 0, + "componentType": 5123, + "count": 504, + "max": [503], + "min": [0], + "normalized": false, + "type": "SCALAR" + } + ], + "asset": { + "extensions": { "KHR_xmp_json_ld": { "packet": 0 } }, + "generator": "Godot Engine v4.4.dev.custom_build@04f6321ecc2d24d08cb3368d87f483c6cc308105", + "version": "2.0" + }, + "bufferViews": [ + { "buffer": 0, "byteLength": 11760, "byteOffset": 0, "byteStride": 12, "target": 34962 }, + { "buffer": 0, "byteLength": 15680, "byteOffset": 11760, "byteStride": 16, "target": 34962 }, + { "buffer": 0, "byteLength": 11760, "byteOffset": 27440, "byteStride": 12, "target": 34962 }, + { "buffer": 0, "byteLength": 7840, "byteOffset": 39200, "byteStride": 8, "target": 34962 }, + { "buffer": 0, "byteLength": 2622, "byteOffset": 47040, "target": 34963 }, + { "buffer": 0, "byteLength": 792, "byteOffset": 49664, "byteStride": 12, "target": 34962 }, + { "buffer": 0, "byteLength": 132, "byteOffset": 50456, "target": 34963 }, + { "buffer": 0, "byteLength": 6048, "byteOffset": 50588, "byteStride": 12, "target": 34962 }, + { "buffer": 0, "byteLength": 1008, "byteOffset": 56636, "target": 34963 } + ], + "buffers": [{ "byteLength": 57644, "uri": "rocket_ship.bin" }], + "extensions": { + "KHR_xmp_json_ld": { + "packets": [ + { + "@context": { "dc": "http://purl.org/dc/elements/1.1/" }, + "@id": "", + "dc:creator": { "@list": ["Poly by Google", "aaronfranke"] }, + "dc:description": "Low poly rpcket ship as a test file for the OMI_vehicle_body and OMI_vehicle_thruster extensions.", + "dc:format": "model/gltf+json", + "dc:rights": "CC-BY 3.0 Attribution", + "dc:source": "https://poly.pizza/m/4mPkOKdzAk-", + "dc:subject": { "@set": ["Space", "Spacecraft"] }, + "dc:title": "Low poly rocket ship vehicle", + "dc:type": { "@set": ["Rocket", "Vehicle", "Demo", "Test"] } + } + ] + }, + "OMI_physics_shape": { + "shapes": [ + { "capsule": { "height": 1.5, "radius": 0.14 }, "type": "capsule" }, + { "convex": { "mesh": 1 }, "type": "convex" }, + { "convex": { "mesh": 2 }, "type": "convex" }, + { "box": { "size": [1, 2, 1] }, "type": "box" } + ] + }, + "OMI_vehicle_thruster": { "thrusters": [{ "maxForce": 50000, "maxGimbal": 0.125 }] } + }, + "extensionsUsed": [ + "GODOT_single_root", + "KHR_xmp_json_ld", + "OMI_physics_body", + "OMI_physics_shape", + "OMI_seat", + "OMI_vehicle_body", + "OMI_vehicle_thruster" + ], + "images": [{ "uri": "rocket_ship_base_color.png" }], + "materials": [ + { + "extensions": {}, + "name": "RocketShipMaterial", + "pbrMetallicRoughness": { + "baseColorFactor": [1, 1, 1, 1], + "baseColorTexture": { "index": 0 }, + "metallicFactor": 0, + "roughnessFactor": 1 + } + } + ], + "meshes": [ + { + "extras": { "targetNames": [] }, + "primitives": [ + { + "attributes": { "NORMAL": 2, "POSITION": 0, "TANGENT": 1, "TEXCOORD_0": 3 }, + "indices": 4, + "material": 0, + "mode": 4 + } + ] + }, + { + "extras": { "targetNames": [] }, + "primitives": [{ "attributes": { "POSITION": 5 }, "indices": 6, "mode": 4 }] + }, + { + "extras": { "targetNames": [] }, + "primitives": [{ "attributes": { "POSITION": 7 }, "indices": 8, "mode": 4 }] + } + ], + "nodes": [ + { + "children": [1, 2, 3, 4, 5, 6, 7, 8], + "extensions": { + "OMI_physics_body": { + "motion": { + "mass": 20000, + "type": "dynamic" + } + }, + "OMI_vehicle_body": { + "gyroTorque": [10000, 10000, 10000], + "pilotSeat": 8, + "useThrottle": true + } + }, + "name": "RocketShipBody" + }, + { + "mesh": 0, + "name": "RocketShipMesh" + }, + { + "extensions": { "OMI_physics_body": { "collider": { "shape": 0 } } }, + "name": "RocketShipShapeAntenna", + "rotation": [0.70710676908493, 0, 0, 0.70710676908493], + "translation": [0, 0, 4.71999979019165] + }, + { + "extensions": { "OMI_physics_body": { "collider": { "shape": 1 } } }, + "name": "RocketShipShapeFinLeft", + "rotation": [0, 0, 0.866025447845459, -0.5] + }, + { + "extensions": { "OMI_physics_body": { "collider": { "shape": 1 } } }, + "name": "RocketShipShapeFinRight", + "rotation": [0, 0, 0.866025447845459, 0.5] + }, + { + "extensions": { "OMI_physics_body": { "collider": { "shape": 1 } } }, + "name": "RocketShipShapeFinTop" + }, + { + "extensions": { "OMI_physics_body": { "collider": { "shape": 2 } } }, + "name": "RocketShipShapeMainHull" + }, + { + "extensions": { + "OMI_vehicle_thruster": { + "thruster": 0 + } + }, + "name": "RocketShipThruster", + "translation": [0, 0, -2.8] + }, + { + "children": [9], + "extensions": { + "OMI_physics_body": { + "trigger": { "nodes": [9] } + }, + "OMI_seat": { + "back": [0, 0, -0.25], + "foot": [0, -0.5, 0.25], + "knee": [0, 0, 0.25] + } + }, + "name": "RocketShipPilotSeat3D", + "translation": [0, 0, 2] + }, + { + "extensions": { "OMI_physics_body": { "trigger": { "shape": 3 } } }, + "name": "RocketShipPilotSeatShape", + "translation": [0, 1, 0] + } + ], + "samplers": [{ "magFilter": 9729, "minFilter": 9987, "wrapS": 10497, "wrapT": 10497 }], + "scene": 0, + "scenes": [{ "nodes": [0] }], + "textures": [{ "sampler": 0, "source": 0 }] +} diff --git a/extensions/2.0/OMI_vehicle_thruster/examples/rocket_ship_base_color.png b/extensions/2.0/OMI_vehicle_thruster/examples/rocket_ship_base_color.png new file mode 100644 index 0000000..0c48a7b Binary files /dev/null and b/extensions/2.0/OMI_vehicle_thruster/examples/rocket_ship_base_color.png differ diff --git a/extensions/2.0/OMI_vehicle_thruster/examples/simple_spaceship.bin b/extensions/2.0/OMI_vehicle_thruster/examples/simple_spaceship.bin new file mode 100644 index 0000000..237b530 Binary files /dev/null and b/extensions/2.0/OMI_vehicle_thruster/examples/simple_spaceship.bin differ diff --git a/extensions/2.0/OMI_vehicle_thruster/examples/simple_spaceship.gltf b/extensions/2.0/OMI_vehicle_thruster/examples/simple_spaceship.gltf new file mode 100644 index 0000000..70f36e5 --- /dev/null +++ b/extensions/2.0/OMI_vehicle_thruster/examples/simple_spaceship.gltf @@ -0,0 +1,304 @@ +{ + "asset": { + "extensions": { "KHR_xmp_json_ld": { "packet": 0 } }, + "generator": "Khronos glTF Blender I/O v4.1.62", + "version": "2.0" + }, + "extensionsUsed": [ + "GODOT_single_root", + "KHR_xmp_json_ld", + "OMI_physics_body", + "OMI_physics_shape", + "OMI_seat", + "OMI_vehicle_body", + "OMI_vehicle_thruster" + ], + "extensions": { + "KHR_xmp_json_ld": { + "packets": [ + { + "@context": { "dc": "http://purl.org/dc/elements/1.1/" }, + "@id": "", + "dc:creator": { "@list": ["chrisonciuconcepts", "aaronfranke"] }, + "dc:description": "Low poly space ship as a test file for the OMI_vehicle_body and OMI_vehicle_thruster extensions.", + "dc:format": "model/gltf+json", + "dc:rights": "CC-BY 4.0 Attribution", + "dc:source": "https://sketchfab.com/3d-models/low-poly-space-ship-587941c9c11742c6b82dfb99e7b210b9", + "dc:subject": { "@set": ["Space", "Spacecraft"] }, + "dc:title": "Low poly space ship vehicle", + "dc:type": { "@set": ["Vehicle", "Demo", "Test"] } + } + ] + }, + "OMI_physics_shape": { + "shapes": [ + { + "type": "convex", + "convex": { "mesh": 0 } + }, + { + "type": "box", + "box": { "size": [0.75, 0.75, 1] } + } + ] + }, + "OMI_vehicle_thruster": { + "thrusters": [ + { + "maxForce": 1000, + "maxGimbal": 0.25 + }, + { + "maxForce": 2000, + "maxGimbal": 0.2 + }, + { + "maxForce": 5000, + "maxGimbal": 0.25 + } + ] + } + }, + "scene": 0, + "scenes": [ + { + "name": "SimpleSpaceshipScene", + "nodes": [0] + } + ], + "nodes": [ + { + "children": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + "name": "SimpleSpaceshipBody", + "extensions": { + "OMI_physics_body": { + "motion": { + "type": "dynamic", + "mass": 2000 + } + }, + "OMI_vehicle_body": { + "gyroTorque": [3000, 3000, 3000], + "linearDampeners": true, + "pilotSeat": 6 + } + } + }, + { + "name": "SimpleSpaceshipBottomThruster", + "rotation": [-0.7071068286895752, 0, 0, 0.7071068286895752], + "translation": [0, -0.3, 0], + "extensions": { + "OMI_vehicle_thruster": { + "thruster": 0 + } + } + }, + { + "name": "SimpleSpaceshipConvexHull", + "extensions": { + "OMI_physics_body": { + "collider": { + "shape": 0 + } + } + } + }, + { + "name": "SimpleSpaceshipMainThrusterLeft", + "translation": [0.5333333, 0, -1.25], + "extensions": { + "OMI_vehicle_thruster": { + "thruster": 2 + } + } + }, + { + "name": "SimpleSpaceshipMainThrusterRight", + "translation": [-0.5333333, 0, -1.25], + "extensions": { + "OMI_vehicle_thruster": { + "thruster": 2 + } + } + }, + { + "mesh": 0, + "name": "SimpleSpaceshipMesh" + }, + { + "name": "SimpleSpaceshipPilotSeat", + "translation": [0, 0.2, 1.25], + "extensions": { + "OMI_physics_body": { + "trigger": { + "shape": 1 + } + }, + "OMI_seat": { + "back": [0, -0.2, -0.4], + "foot": [0, -0.4, 0.65], + "knee": [0, -0.2, 0.15] + } + } + }, + { + "name": "SimpleSpaceshipReverseThrusterLeft", + "rotation": [0, -1, 0, 0], + "translation": [0.5333333, 0, 0.4166666865348816], + "extensions": { + "OMI_vehicle_thruster": { + "thruster": 1 + } + } + }, + { + "name": "SimpleSpaceshipReverseThrusterRight", + "rotation": [0, -1, 0, 0], + "translation": [-0.5333333, 0, 0.4166666865348816], + "extensions": { + "OMI_vehicle_thruster": { + "thruster": 1 + } + } + }, + { + "name": "SimpleSpaceshipSideThrusterLeft", + "rotation": [0, -0.7071068286895752, 0, 0.7071068286895752], + "translation": [0.8, 0, 0], + "extensions": { + "OMI_vehicle_thruster": { + "thruster": 0 + } + } + }, + { + "name": "SimpleSpaceshipSideThrusterRight", + "rotation": [0, 0.7071068286895752, 0, 0.7071068286895752], + "translation": [-0.8, 0, 0], + "extensions": { + "OMI_vehicle_thruster": { + "thruster": 0 + } + } + }, + { + "name": "SimpleSpaceshipTopThruster", + "rotation": [0.7071068286895752, 0, 0, 0.7071068286895752], + "translation": [0, 0.5, 0], + "extensions": { + "OMI_vehicle_thruster": { + "thruster": 0 + } + } + } + ], + "materials": [ + { + "doubleSided": true, + "name": "SimpleSpaceshipMaterial", + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 0 + }, + "metallicFactor": 0.75, + "roughnessFactor": 0.25 + } + } + ], + "meshes": [ + { + "name": "SimpleSpaceshipMeshData", + "primitives": [ + { + "attributes": { + "POSITION": 0, + "NORMAL": 1, + "TEXCOORD_0": 2 + }, + "indices": 3, + "material": 0 + } + ] + } + ], + "textures": [ + { + "sampler": 0, + "source": 0 + } + ], + "images": [ + { + "mimeType": "image/png", + "name": "simple_spaceship_color", + "uri": "simple_spaceship_color.png" + } + ], + "accessors": [ + { + "bufferView": 0, + "componentType": 5126, + "count": 832, + "max": [2.4141058921813965, 0.8135462999343872, 2.1287012100219727], + "min": [-2.4141058921813965, -0.5360468626022339, -1.7438040971755981], + "type": "VEC3" + }, + { + "bufferView": 1, + "componentType": 5126, + "count": 832, + "type": "VEC3" + }, + { + "bufferView": 2, + "componentType": 5126, + "count": 832, + "type": "VEC2" + }, + { + "bufferView": 3, + "componentType": 5123, + "count": 1380, + "type": "SCALAR" + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteLength": 9984, + "byteOffset": 0, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 9984, + "byteOffset": 9984, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 6656, + "byteOffset": 19968, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 2760, + "byteOffset": 26624, + "target": 34963 + } + ], + "samplers": [ + { + "magFilter": 9729, + "minFilter": 9987 + } + ], + "buffers": [ + { + "byteLength": 29384, + "uri": "simple_spaceship.bin" + } + ] +} diff --git a/extensions/2.0/OMI_vehicle_thruster/examples/simple_spaceship_color.png b/extensions/2.0/OMI_vehicle_thruster/examples/simple_spaceship_color.png new file mode 100644 index 0000000..2b3c9f1 Binary files /dev/null and b/extensions/2.0/OMI_vehicle_thruster/examples/simple_spaceship_color.png differ diff --git a/extensions/2.0/OMI_vehicle_thruster/schema/glTF.OMI_vehicle_thruster.schema.json b/extensions/2.0/OMI_vehicle_thruster/schema/glTF.OMI_vehicle_thruster.schema.json new file mode 100644 index 0000000..155afa3 --- /dev/null +++ b/extensions/2.0/OMI_vehicle_thruster/schema/glTF.OMI_vehicle_thruster.schema.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "glTF.OMI_vehicle_thruster.schema.json", + "title": "OMI_vehicle_thruster glTF Document Extension", + "type": "object", + "allOf": [ { "$ref": "glTFProperty.schema.json" } ], + "properties": { + "thrusters": { + "type": "array", + "description": "An array of thruster parameters that can be referenced by nodes.", + "items": { + "type": "object", + "$ref": "glTF.OMI_vehicle_thruster.thruster.schema.json" + }, + "minItems": 1 + }, + "extensions": {}, + "extras": {} + }, + "required": [ + "thrusters" + ] +} diff --git a/extensions/2.0/OMI_vehicle_thruster/schema/glTF.OMI_vehicle_thruster.thruster.schema.json b/extensions/2.0/OMI_vehicle_thruster/schema/glTF.OMI_vehicle_thruster.thruster.schema.json new file mode 100644 index 0000000..f00ca5d --- /dev/null +++ b/extensions/2.0/OMI_vehicle_thruster/schema/glTF.OMI_vehicle_thruster.thruster.schema.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "glTF.OMI_vehicle_thruster.thruster.schema.json", + "title": "OMI_vehicle_thruster Thruster Parameters", + "type": "object", + "properties": { + "currentForceRatio": { + "type": "number", + "description": "The ratio of the maximum force the thruster is using for propulsion.", + "default": 0.0 + }, + "currentGimbalRatio": { + "type": "array", + "description": "The ratio of the maximum gimbal angles the thruster is rotated to.", + "items": { + "type": "number" + }, + "minItems": 2, + "maxItems": 2, + "default": [0.0, 0.0] + }, + "maxForce": { + "type": "number", + "description": "The maximum thrust force in Newtons (kg⋅m/s²) that the thruster can provide. Required." + }, + "maxGimbal": { + "type": "array", + "description": "The maximum angle the thruster can gimbal or rotate in radians.", + "default": 0.0 + }, + "extensions": { }, + "extras": { } + }, + "required": [ + "maxForce" + ] +} diff --git a/extensions/2.0/OMI_vehicle_thruster/schema/node.OMI_vehicle_thruster.schema.json b/extensions/2.0/OMI_vehicle_thruster/schema/node.OMI_vehicle_thruster.schema.json new file mode 100644 index 0000000..637b75b --- /dev/null +++ b/extensions/2.0/OMI_vehicle_thruster/schema/node.OMI_vehicle_thruster.schema.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "node.OMI_vehicle_thruster.schema.json", + "title": "OMI_vehicle_thruster glTF Node Extension", + "type": "object", + "allOf": [ { "$ref": "glTFProperty.schema.json" } ], + "properties": { + "thruster": { + "allOf": [ { "$ref": "glTFid.schema.json" } ], + "description": "The id of the thruster parameters referenced by this node." + }, + "extensions": {}, + "extras": {} + }, + "required": [ + "thruster" + ] +} diff --git a/extensions/2.0/OMI_vehicle_wheel/README.md b/extensions/2.0/OMI_vehicle_wheel/README.md new file mode 100644 index 0000000..8462617 --- /dev/null +++ b/extensions/2.0/OMI_vehicle_wheel/README.md @@ -0,0 +1,156 @@ +# OMI_vehicle_wheel + +## Contributors + +- Aaron Franke, Godot Engine. + +## Status + +Open Metaverse Interoperability Group Draft Proposal + +## Dependencies + +Written against the glTF 2.0 spec. + +Designed to be used together with the `OMI_vehicle_body` spec. + +## Overview + +This extension allows specifying vehicle wheels in glTF scenes. + +Wheel glTF nodes can be added as a descendant of a vehicle body glTF node. A vehicle body is defined as a glTF node with the `OMI_vehicle_body` extension. Then wheels can be added to the vehicle by adding descendant nodes with the `OMI_vehicle_wheel` extension. Wheels must be specified on their own glTF nodes as descendants of a vehicle body, not on the same glTF node as the vehicle; this so that they may have their own transform relative to the vehicle, including their own position, and so that they and may optionally rotate for steering. + +Wheels are circles on the local YZ plane, or optionally cylinders if the width property is specified, and are invisible by default. To give a wheel a visual appearance, add another glTF node with a mesh as a child of the wheel. The forward direction of both a vehicle and its wheels is the local +Z direction. + +### Example: + +The file [simple_car.gltf](examples/simple_car.gltf) defines a low-poly simple car with 4 wheels, and 4 seats, one of which is the pilot seat. This example uses `OMI_physics_body`, `OMI_physics_shape`, `OMI_seat`, `OMI_vehicle_body`, and `OMI_vehicle_wheel`. + +The wheel settings are defined at the document level: + +```json +{ + "extensions": { + "OMI_vehicle_wheel": { + "wheels": [ + { + "radius": 0.35, + "maxSteeringAngle": 0.5 + }, + { + "radius": 0.35, + "maxForce": 2000 + } + ] + } + } +} +``` + +This is then referenced by index on wheel glTF nodes. For example, the front right wheel: + +```json +{ + "children": [14], + "name": "WheelFrontRight", + "translation": [-0.8, 0, 1.5], + "extensions": { + "OMI_vehicle_wheel": { + "wheel": 0 + } + } +} +``` + +The wheel settings at index 0 have a radius of 0.35 meters and a maximum steering angle of 0.5 radians, which allows this wheel to rotate up to about 28 degrees around the Y axis (ZX plane). The front left wheel uses the same settings. The rear wheels use settings index 1 instead, which does not have a maximum steering angle, but does have a maximum force of 2000 Newtons, which allows those wheels to provide thrust when in contact with the ground. + +## glTF Schema Updates + +This extension consists of three new data structures for defining wheel specifications on the root glTF document and referencing them on a glTF node. The main data structure defines wheel parameters and is what most of this document describes. The second data structure uses the key `"OMI_vehicle_wheel"` in the document-level `"extensions"` which contains a list of the main data structures of wheel parameters. The third data structure uses the key `"OMI_vehicle_wheel"` in the node-level `"extensions"` which contains an index of the wheel parameters to use from the document-level wheel list. + +The extension must also be added to the glTF's `extensionsUsed` array and because it is optional, it does not need to be added to the `extensionsRequired` array. + +### Property Summary + +The rest of the document, including this summary, defines the properties for the main data structure. + +| | Type | Description | Default value | +| -------------------------------- | -------- | -------------------------------------------------------------------------- | ---------------- | +| **currentForceRatio** | `number` | The ratio of the maximum force the wheel is using for propulsion. | 0.0 | +| **currentSteeringRatio** | `number` | The ratio of the maximum steering angle the wheel is rotated to. | 0.0 | +| **maxForce** | `number` | The maximum thrust force in Newtons (kg⋅m/s²) of this wheel. | 0.0 | +| **maxSteeringAngle** | `number` | The maximum angle in radians that the wheel can steer or rotate. | 0.0 | +| **physicsMaterial** | `number` | The index of the physics material in the top level physicsMaterials array. | -1 (no material) | +| **radius** | `number` | The radius of the wheel in meters. | 0.25 | +| **suspensionDampingCompression** | `number` | The resistance to the velocity of the suspension in kg/s when compressing. | 2000.0 | +| **suspensionDampingRebound** | `number` | The resistance to the velocity of the suspension in kg/s when extending. | 2000.0 | +| **suspensionStiffness** | `number` | The resistance to traveling away from the start point in kg/s². | 20000.0 | +| **suspensionTravel** | `number` | The distance the suspension can move up or down in meters. | 0.25 | +| **width** | `number` | The width of the wheel in meters. | 0.125 | + +#### Current Force Ratio + +The `"currentForceRatio"` property is a number that defines the ratio of the `"maximumForce"` the wheel is using for propulsion. If not specified, the default value is 0.0, which means the wheel is not providing any force. + +This value is expected to be between -1.0 and 1.0. The behavior of values outside of this range is implementation-defined, it may be clamped to this range, or be allowed to go beyond this range for some kind of turbo boost or overdrive. + +This value is expected to dynamically change at runtime. This is not usually saved in the glTF file, but it is allowed to be. The input used to set this value is implementation-defined. The wheel may be used on its own without a vehicle, but usually it is used for child nodes of a vehicle. In the common case of a vehicle, the wheel's force ratio is set by the vehicle's `"linearActivation"` property in the direction the wheel is facing, as defined by `OMI_vehicle_body` extension. However, this value may be set by other means, such as a `KHR_interactivity` script. + +#### Current Steering Ratio + +The `"currentSteeringRatio"` property is a number that defines the ratio of the `"maximumSteeringAngle"` the wheel is rotated to. If not specified, the default value is 0.0, which means the wheel is not rotated. + +This value is expected to be between -1.0 and 1.0. The behavior of values outside of this range is implementation-defined, it may be clamped to this range, or be allowed to go beyond this range for some kind of oversteer. Due to glTF's right-handed coordinate system, positive values should rotate the wheel to the left, and negative values should rotate the wheel to the right. + +This value is expected to dynamically change at runtime. This is not usually saved in the glTF file, but it is allowed to be. The input used to set this value is implementation-defined. The wheel may be used on its own without a vehicle, but usually it is used for child nodes of a vehicle. In the common case of a vehicle, the wheel's steering ratio is set by the vehicle's `"angularActivation"` property in the direction the wheel is facing, as defined by `OMI_vehicle_body` extension. However, this value may be set by other means, such as a `KHR_interactivity` script. The exact calculation is determined by the implementation, it may be as simple as using the distance on the vehicle's local Z axis, or as complex as Ackermann steering geometry. For example, a car with all-wheel steering would have the rear wheels rotate in the opposite direction of the front wheels. + +#### Max Force + +The `"maxForce"` property is a number that defines the maximum thrust force in Newtons (kg⋅m/s²) that the wheel can transfer to the ground. If not specified, the default value is 0.0 Newtons, which means the wheel cannot provide any force. + +For a realistic normal car with rear-wheel drive and two rear wheels, the rear wheels should be given a maximum force value that is half the maximum force of the car's engine. The ideal value varies depending on the mass of the vehicle, the desired acceleration, and the amount of wheels. + +#### Max Steering Angle + +The `"maxSteeringAngle"` property is a number that defines the maximum angle in radians that the wheel can steer. If not specified, the default value is 0.0 radians, which means the wheel cannot steer. + +For a realistic normal car with front-wheel steering, the front wheels should be given a maximum steering angle value up to 0.5 radians, or 28.6478897565... degrees, or a smaller value than this. The ideal value varies depending on the vehicle's desired turning radius, the location of the wheel relative to the center of mass, and the desired handling characteristics. + +#### Physics Material + +The `"physicsMaterial"` property is an integer that defines the index of the physics material used by the wheel, as defined in the top-level physicsMaterials array in the `OMI_physics_body` or `KHR_physics_rigid_bodies` extensions. If not specified or -1, this wheel should use a default physics material defined by the implementation. + +#### Radius + +The `"radius"` property is a number that defines the radius of the wheel in meters. A wheel is a circle centered on the glTF node's origin, with this radius, aligned on the local YZ plane. If not specified, the default radius is 0.25 meters, which is 0.5 meters in diameter. + +#### Suspension Damping Compression + +The `"suspensionDampingCompression"` property is a number that defines the damping of the suspension, the resistance to the velocity of the suspension when compressing. If not specified, the default damping is 2000.0 Newton-seconds per meter, or 2000.0 kg/s in SI base units, which is a sane default that reflects real-world cars. + +#### Suspension Damping Rebound + +The `"suspensionDampingRebound"` property is a number that defines the damping of the suspension, the resistance to the velocity of the suspension when rebounding, also known as extending or relaxing. If not specified, the default damping is 2000.0 Newton-seconds per meter, or 2000.0 kg/s in SI base units, which is a sane default that reflects real-world cars. + +#### Suspension Stiffness + +The `"suspensionStiffness"` property is a number that defines the stiffness of the suspension, the resistance to moving up or down. If not specified, the default stiffness is 20000.0 Newtons per meter, or 20000.0 kg/s² in SI base units (twenty thousand), which is a sane default that reflects real-world cars. + +#### Suspension Travel + +The `"suspensionTravel"` property is a number that defines the distance in meters the wheel can move up or down. If not specified, the default travel is 0.25 meters, which is a sane default that reflects real-world cars. + +### JSON Schema + +See [glTF.OMI_vehicle_wheel.wheel.schema.json](schema/glTF.OMI_vehicle_wheel.wheel.schema.json) for the main wheel parameter schema, [glTF.OMI_vehicle_wheel.schema.json](schema/glTF.OMI_vehicle_wheel.schema.json) for the document-level list of wheel parameters, and [node.OMI_vehicle_wheel.schema.json](schema/node.OMI_vehicle_wheel.schema.json) for the node-level collider selection. + +## Known Implementations + +- Godot Engine: + +## Resources: + +- Godot VehicleWheel3D: https://docs.godotengine.org/en/latest/classes/class_vehiclewheel3d.html +- Hyperfy Car: https://madjin.github.io/hyperfy-docs/docs/worlds/apps/objects/#car +- Space Engineers Wheel Suspension: https://spaceengineers.fandom.com/wiki/Wheel_Suspension_5x5#Wheel_Suspension_Controls +- Wikipedia Ackermann steering geometry: https://en.wikipedia.org/wiki/Ackermann_steering_geometry diff --git a/extensions/2.0/OMI_vehicle_wheel/examples/simple_car.gltf b/extensions/2.0/OMI_vehicle_wheel/examples/simple_car.gltf new file mode 100644 index 0000000..19696c8 --- /dev/null +++ b/extensions/2.0/OMI_vehicle_wheel/examples/simple_car.gltf @@ -0,0 +1,633 @@ +{ + "asset": { + "generator": "Khronos glTF Blender I/O v3.5.30", + "version": "2.0" + }, + "extensionsUsed": [ + "GODOT_single_root", + "KHR_materials_clearcoat", + "OMI_physics_shape", + "OMI_physics_body", + "OMI_seat", + "OMI_vehicle_body", + "OMI_vehicle_wheel" + ], + "extensions": { + "OMI_physics_shape": { + "shapes": [ + { + "type": "box", + "box": { "size": [0.65, 0.65, 0.65] } + }, + { + "type": "box", + "box": { "size": [1.4, 1.3, 2.2] } + }, + { + "type": "box", + "box": { "size": [1.4, 0.6, 1.5] } + } + ] + }, + "OMI_vehicle_wheel": { + "wheels": [ + { + "radius": 0.35, + "maxSteeringAngle": 0.5 + }, + { + "radius": 0.35, + "maxForce": 2000 + } + ] + } + }, + "scene": 0, + "scenes": [ { "nodes": [0] } ], + "nodes": [ + { + "children": [1, 2, 3, 4, 5, 6, 7, 9, 11, 13, 15], + "name": "SimpleCar", + "extensions": { + "OMI_physics_body": { + "motion": { + "type": "dynamic", + "mass": 1000 + } + }, + "OMI_vehicle_body": { + "pilotSeat": 6 + } + } + }, + { + "name": "ColliderBack", + "translation": [0, 0.7, -0.65], + "extensions": { + "OMI_physics_body": { + "collider": { + "shape": 1 + } + } + } + }, + { + "name": "ColliderFront", + "translation": [0, 0.35, 1.2], + "extensions": { + "OMI_physics_body": { + "collider": { + "shape": 2 + } + } + } + }, + { + "name": "SeatBackLeft", + "translation": [0.4, 0.4, -0.8], + "extensions": { + "OMI_physics_body": { + "trigger": { + "shape": 0 + } + }, + "OMI_seat": { + "back": [0, 0, -0.3], + "foot": [0, -0.5, 0.3], + "knee": [0, 0, 0.3] + } + } + }, + { + "name": "SeatBackRight", + "translation": [-0.4, 0.4, -0.8], + "extensions": { + "OMI_physics_body": { + "trigger": { + "shape": 0 + } + }, + "OMI_seat": { + "back": [0, 0, -0.3], + "foot": [0, -0.5, 0.3], + "knee": [0, 0, 0.3] + } + } + }, + { + "name": "SeatPassenger", + "translation": [-0.4, 0.4, 0], + "extensions": { + "OMI_physics_body": { + "trigger": { + "shape": 0 + } + }, + "OMI_seat": { + "back": [0, 0, -0.3], + "foot": [0, -0.5, 0.3], + "knee": [0, 0, 0.3] + } + } + }, + { + "name": "SeatPilot", + "translation": [0.4, 0.4, 0], + "extensions": { + "OMI_physics_body": { + "trigger": { + "shape": 0 + } + }, + "OMI_seat": { + "back": [0, 0, -0.3], + "foot": [0, -0.5, 0.3], + "knee": [0, 0, 0.3] + } + } + }, + { + "mesh": 0, + "name": "SimpleCarMesh" + }, + { + "mesh": 1, + "name": "WheelBackLeftMesh" + }, + { + "children": [8], + "name": "WheelBackLeft", + "translation": [0.8, 0, -1.15], + "extensions": { + "OMI_vehicle_wheel": { + "wheel": 1 + } + } + }, + { + "mesh": 1, + "name": "WheelBackRightMesh" + }, + { + "children": [10], + "name": "WheelBackRight", + "translation": [-0.8, 0, -1.15], + "extensions": { + "OMI_vehicle_wheel": { + "wheel": 1 + } + } + }, + { + "mesh": 1, + "name": "WheelFrontLeftMesh" + }, + { + "children": [12], + "name": "WheelFrontLeft", + "translation": [0.8, 0, 1.5], + "extensions": { + "OMI_vehicle_wheel": { + "wheel": 0 + } + } + }, + { + "mesh": 1, + "name": "WheelFrontRightMesh" + }, + { + "children": [14], + "name": "WheelFrontRight", + "translation": [-0.8, 0, 1.5], + "extensions": { + "OMI_vehicle_wheel": { + "wheel": 0 + } + } + } + ], + "materials": [ + { + "extensions": { + "KHR_materials_clearcoat": { + "clearcoatFactor": 0.8, + "clearcoatRoughnessFactor": 0.05 + } + }, + "name": "car_red", + "pbrMetallicRoughness": { + "baseColorFactor": [ + 0.8, + 0.016, + 0.016, + 1 + ], + "metallicFactor": 0, + "roughnessFactor": 0.6 + } + }, + { + "name": "car_windows", + "pbrMetallicRoughness": { + "baseColorFactor": [ + 0.36724644899368286, + 0.36724644899368286, + 0.36724644899368286, + 1 + ], + "metallicFactor": 0.3, + "roughnessFactor": 0.1 + } + }, + { + "name": "chrome", + "pbrMetallicRoughness": { + "baseColorFactor": [ + 0.6, + 0.6, + 0.6, + 1 + ], + "roughnessFactor": 0.35 + } + }, + { + "emissiveFactor": [ + 1, + 0.8618969917297363, + 0.6978915929794312 + ], + "name": "headlights", + "pbrMetallicRoughness": { + "baseColorFactor": [ + 0.8, + 0.8, + 0.8, + 1 + ], + "metallicFactor": 0, + "roughnessFactor": 0.5 + } + }, + { + "name": "tire", + "pbrMetallicRoughness": { + "baseColorFactor": [ + 0.05354800075292587, + 0.05354800075292587, + 0.05354800075292587, + 1 + ], + "metallicFactor": 0, + "roughnessFactor": 0.8 + } + } + ], + "meshes": [ + { + "name": "SimpleCarMesh", + "primitives": [ + { + "attributes": { + "POSITION": 0, + "NORMAL": 1 + }, + "indices": 2, + "material": 0 + }, + { + "attributes": { + "POSITION": 3, + "NORMAL": 4 + }, + "indices": 5, + "material": 1 + }, + { + "attributes": { + "POSITION": 6, + "NORMAL": 7 + }, + "indices": 8, + "material": 2 + }, + { + "attributes": { + "POSITION": 9, + "NORMAL": 10 + }, + "indices": 11, + "material": 3 + } + ] + }, + { + "name": "WheelMesh", + "primitives": [ + { + "attributes": { + "POSITION": 12, + "NORMAL": 13 + }, + "indices": 14, + "material": 4 + }, + { + "attributes": { + "POSITION": 15, + "NORMAL": 16 + }, + "indices": 17, + "material": 2 + } + ] + } + ], + "accessors": [ + { + "bufferView": 0, + "componentType": 5126, + "count": 157, + "max": [ + 0.9146543741226196, + 1.3307487964630127, + 1.9688242673873901 + ], + "min": [ + -0.916010856628418, + -0.15977101027965546, + -1.7800047397613525 + ], + "type": "VEC3" + }, + { + "bufferView": 1, + "componentType": 5126, + "count": 157, + "type": "VEC3" + }, + { + "bufferView": 2, + "componentType": 5123, + "count": 324, + "type": "SCALAR" + }, + { + "bufferView": 3, + "componentType": 5126, + "count": 24, + "max": [ + 0.7027391195297241, + 1.3307487964630127, + 0.5760020017623901 + ], + "min": [ + -0.7040956020355225, + 0.6703968048095703, + -1.7800047397613525 + ], + "type": "VEC3" + }, + { + "bufferView": 4, + "componentType": 5126, + "count": 24, + "type": "VEC3" + }, + { + "bufferView": 5, + "componentType": 5123, + "count": 48, + "type": "SCALAR" + }, + { + "bufferView": 6, + "componentType": 5126, + "count": 22, + "max": [ + 0.7027391195297241, + 0.5432487726211548, + 1.9688242673873901 + ], + "min": [ + -0.7040956020355225, + -0.15977101027965546, + -1.566460371017456 + ], + "type": "VEC3" + }, + { + "bufferView": 7, + "componentType": 5126, + "count": 22, + "type": "VEC3" + }, + { + "bufferView": 8, + "componentType": 5123, + "count": 36, + "type": "SCALAR" + }, + { + "bufferView": 9, + "componentType": 5126, + "count": 8, + "max": [ + 0.726665198802948, + 0.09771557152271271, + 1.9238450527191162 + ], + "min": [ + -0.7280216217041016, + -0.00012339651584625244, + 1.8910331726074219 + ], + "type": "VEC3" + }, + { + "bufferView": 10, + "componentType": 5126, + "count": 8, + "type": "VEC3" + }, + { + "bufferView": 11, + "componentType": 5123, + "count": 12, + "type": "SCALAR" + }, + { + "bufferView": 12, + "componentType": 5126, + "count": 120, + "max": [ + 0.09073548763990402, + 0.29643508791923523, + 0.29643508791923523 + ], + "min": [ + -0.09073548763990402, + -0.29643508791923523, + -0.29643508791923523 + ], + "type": "VEC3" + }, + { + "bufferView": 13, + "componentType": 5126, + "count": 120, + "type": "VEC3" + }, + { + "bufferView": 14, + "componentType": 5123, + "count": 648, + "type": "SCALAR" + }, + { + "bufferView": 15, + "componentType": 5126, + "count": 26, + "max": [ + 0.049634311348199844, + 0.17193351686000824, + 0.17193353176116943 + ], + "min": [ + -0.049634311348199844, + -0.17193351686000824, + -0.17193347215652466 + ], + "type": "VEC3" + }, + { + "bufferView": 16, + "componentType": 5126, + "count": 26, + "type": "VEC3" + }, + { + "bufferView": 17, + "componentType": 5123, + "count": 72, + "type": "SCALAR" + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteLength": 1884, + "byteOffset": 0, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 1884, + "byteOffset": 1884, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 648, + "byteOffset": 3768, + "target": 34963 + }, + { + "buffer": 0, + "byteLength": 288, + "byteOffset": 4416, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 288, + "byteOffset": 4704, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 96, + "byteOffset": 4992, + "target": 34963 + }, + { + "buffer": 0, + "byteLength": 264, + "byteOffset": 5088, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 264, + "byteOffset": 5352, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 72, + "byteOffset": 5616, + "target": 34963 + }, + { + "buffer": 0, + "byteLength": 96, + "byteOffset": 5688, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 96, + "byteOffset": 5784, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 24, + "byteOffset": 5880, + "target": 34963 + }, + { + "buffer": 0, + "byteLength": 1440, + "byteOffset": 5904, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 1440, + "byteOffset": 7344, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 1296, + "byteOffset": 8784, + "target": 34963 + }, + { + "buffer": 0, + "byteLength": 312, + "byteOffset": 10080, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 312, + "byteOffset": 10392, + "target": 34962 + }, + { + "buffer": 0, + "byteLength": 144, + "byteOffset": 10704, + "target": 34963 + } + ], + "buffers": [ + { + "byteLength": 10848, + "uri": "data:application/octet-stream;base64,tuYzPwObI74y1+O/tuYzPwObI74y1+O/tuYzPwObI74y1+O/GssxugObI74y1+O/GssxugObI74y1+O/GssxugObI77yD/e+GssxuvpVqj/yD/e+GssxugObI75u9uE+ohCyPgObI75vAvw/ohCyPgObI75vAvw/ohCyPgObI75vAvw/YHrRPtBeoj4z79o/YHrRPtBeoj4z79o/YHrRPtBeoj4z79o/Hh0CP/JAqT4AvKU/Hh0CP/JAqT4AvKU/tuYzPyCfKz/yD/e+tuYzP6Lgrj7AVGe/tuYzP6Lgrj7AVGe/tuYzP6Lgrj7AVGe/tuYzPyCfKz9u9uE+tuYzPyCfKz9u9uE+GssxuvpVqj8y1+O/tuYzPwObI75u9uE+tuYzPwObI75u9uE+tuYzPwObI75u9uE+yiZqPwObI77Ggci/yiZqP+LxpD4GXKg/tuYzPwObI75PLjq/tuYzPwObI75PLjq/tuYzPwObI75PLjq/tuYzPwObI75PLjq/yiZqPwObI75u9uE+yiZqP27EqT79R22/yiZqP27EqT79R22/yiZqP27EqT5fC6+/yiZqP27EqT5fC6+/yiZqPwObI75PLjq/yiZqPwObI75PLjq/yiZqPwObI75vAvw/yiZqPxb0qD4o6+A/yiZqPxb0qD4o6+A/nD80vwObI74y1+O/nD80vwObI74y1+O/nD80vwObI74y1+O/bcKyvgObI75vAvw/bcKyvgObI75vAvw/bcKyvgObI75vAvw/KyzSvtBeoj4z79o/KyzSvtBeoj4z79o/KyzSvtBeoj4z79o/A3YCv/JAqT4AvKU/A3YCv/JAqT4AvKU/nD80vyCfKz/yD/e+nD80v6Lgrj7AVGe/nD80v6Lgrj7AVGe/nD80v6Lgrj7AVGe/nD80vyCfKz9u9uE+nD80vyCfKz9u9uE+nD80vwObI75PLjq/nD80vwObI75PLjq/nD80vwObI75PLjq/nD80vwObI75PLjq/nD80vwObI75u9uE+nD80vwObI75u9uE+nD80vwObI75u9uE+sH9qvwObI77Ggci/sH9qv+LxpD4GXKg/sH9qvwObI75u9uE+sH9qv27EqT79R22/sH9qv27EqT79R22/sH9qv27EqT5fC6+/sH9qv27EqT5fC6+/sH9qvwObI75PLjq/sH9qvwObI75PLjq/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HwAVACEAHwAhAAsAdABYAFcAdABXAHMAJwBSAE4AJwBOAAAABAAfAAsABAALAA0AbwBbAF0AbwBdAHAADAAFABwADAAcABcADgAYABsADgAbAAEAcwBXAFwAcwBcAG4AQgBDAC8AQgAvADAAdgBeAFYAdgBWAHcAbQBaAF4AbQBeAHYAVABgAGsAVABrAF8AIABNACIAIAAiABMAXwBrAGEAXwBhAFUAXgBqAGIAXgBiAFYAVQBhAGUAVQBlAFkAdwBWAFgAdwBYAHQAJgAEAA0AJgANAB4AcQBfAFUAcQBVAHUABgAkAA8ABgAPAAoAWQBlAGYAWQBmAFoABQAmAB4ABQAeABwAAABOAE0AAABNACAAVwBjAGgAVwBoAFwAcABdAFQAcABUAHIAFgAHABoAFgAaABIAcgBUAF8AcgBfAHEAdQBVAFkAdQBZAGwACAAGAAoACAAKAB0AAwAZAFEAAwBRAAkAJAARACUAJAAlAA8AbgBcAFsAbgBbAG8AFQAWABIAFQASACEAbABZAFoAbABaAG0AEQAMABcAEQAXACUAAgBMAE8AAgBPACMAMAAvADsAMAA7ADwAKgAsADgAKgA4ADYAMQAoADQAMQA0AD0ALAArADcALAA3ADgALwAxAD0ALwA9ADsALgAyAD4ALgA+ADoAFgAVAEMAFgBDAEIADAARAEEADABBAEAAFQAfAEQAFQBEAEMABAAmAEUABABFAEYACAAHAEcACABHAEgAJgAFAEkAJgBJAEUAEQAkAEoAEQBKAEEABQAMAEAABQBAAEkABwAWAEIABwBCAEcAHwAEAEYAHwBGAEQAJAAGAEsAJABLAEoABgAIAEgABgBIAEsAHAAeAFIAHABSABsAGgAdAFEAGgBRABkAFwAcABsAFwAbABgAJQAXABgAJQAYAFMAEgAaABkAEgAZAFAAIQASAFAAIQBQACIADwAlAFMADwBTAE8AHgANAE4AHgBOAFIACwAhACIACwAiAE0ADQALAE0ADQBNAE4ACgAPAE8ACgBPAEwAHQAKAEwAHQBMAFEAAQAbAFIAAQBSACcAFABQABkAFAAZAAMACQBRAEwACQBMAAIAIwBPAFMAIwBTABAAEwAiAFAAEwBQABQAEABTABgAEAAYAA4AXABoAGcAXABnAFsAVgBiAGQAVgBkAFgAXQBpAGAAXQBgAFQAWABkAGMAWABjAFcAWwBnAGkAWwBpAF0AWgBmAGoAWgBqAF4AFABuAG8AFABvABMADgBsAG0ADgBtABAAEwBvAHAAEwBwACAAAAByAHEAAABxACcACQB0AHMACQBzAAMAJwBxAHUAJwB1AAEAEABtAHYAEAB2ACMAAQB1AGwAAQBsAA4AAwBzAG4AAwBuABQAIABwAHIAIAByAAAAIwB2AHcAIwB3AAIAAgB3AHQAAgB0AAkAWU1LPSzXAD4t1wA+WU1LPW6wPL1YDzA+WU1LPSzXAL4r1wC+WU1LPW6wPD1UDzC+WU1LPW6wPL1UDzC+WU1LPSzXAL4t1wA+WU1LPVcPML50sDw9WU1LPVcPMD5nsDy9WU1LPSzXAD4r1wC+WU1LPVcPMD50sDw9WU1LPVcPML5nsDy9WU1LPW6wPD1YDzA+V01LPWZmprAjIsIyWU1LvSzXAD4t1wA+WU1LvW6wPL1YDzA+WU1LvSzXAL4r1wC+WU1LvW6wPD1UDzC+WU1LvW6wPL1UDzC+WU1LvSzXAL4t1wA+WU1LvVcPML50sDw9WU1LvVcPMD5nsDy9WU1LvSzXAD4r1wC+WU1LvVcPMD50sDw9WU1LvVcPML5nsDy9WU1LvW6wPD1YDzA+V01LvWZmprAjIsIyAACAPwAAAIAAAACAAACAPwAAAAAAAACAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAACAAACAPwAAAAAAAACAAACAPwAAAIAAAACAAACAPwAAAIAAAACAAACAPwAAAIAAAACAAACAPwAAAAAAAACAAACAPwAAAAAAAACAAACAPwAAAAAAAAAAAACAvwAAAIAAAACAAACAvwAAAAAAAACAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAACAAACAvwAAAAAAAACAAACAvwAAAIAAAAAAAACAvwAAAIAAAACAAACAvwAAAIAAAACAAACAvwAAAAAAAAAAAACAvwAAAIAAAACAAACAvwAAAIAAAACACQAAAAwACgACAAwAAQAFAAwAAwAIAAwAAAALAAwABgAKAAwABAADAAwACAAHAAwABQAGAAwAAgAEAAwACwABAAwABwAJAAwAFgAZAA0AFwAZAA8ADgAZABIAEAAZABUADQAZABgAEwAZABcAEQAZABAAFQAZABQAEgAZABMADwAZABEAGAAZAA4AFAAZABYA" + } + ] +} diff --git a/extensions/2.0/OMI_vehicle_wheel/schema/glTF.OMI_vehicle_wheel.schema.json b/extensions/2.0/OMI_vehicle_wheel/schema/glTF.OMI_vehicle_wheel.schema.json new file mode 100644 index 0000000..f3da0d7 --- /dev/null +++ b/extensions/2.0/OMI_vehicle_wheel/schema/glTF.OMI_vehicle_wheel.schema.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "glTF.OMI_vehicle_wheel.schema.json", + "title": "OMI_vehicle_wheel glTF Document Extension", + "type": "object", + "allOf": [ { "$ref": "glTFProperty.schema.json" } ], + "properties": { + "wheels": { + "type": "array", + "description": "An array of wheel parameters that can be referenced by nodes.", + "items": { + "type": "object", + "$ref": "glTF.OMI_vehicle_wheel.wheel.schema.json" + }, + "minItems": 1 + }, + "extensions": {}, + "extras": {} + }, + "required": [ + "wheels" + ] +} diff --git a/extensions/2.0/OMI_vehicle_wheel/schema/glTF.OMI_vehicle_wheel.wheel.schema.json b/extensions/2.0/OMI_vehicle_wheel/schema/glTF.OMI_vehicle_wheel.wheel.schema.json new file mode 100644 index 0000000..45922ea --- /dev/null +++ b/extensions/2.0/OMI_vehicle_wheel/schema/glTF.OMI_vehicle_wheel.wheel.schema.json @@ -0,0 +1,65 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "glTF.OMI_vehicle_wheel.wheel.schema.json", + "title": "OMI_vehicle_wheel Wheel Parameters", + "type": "object", + "properties": { + "currentForceRatio": { + "type": "number", + "description": "The ratio of the maximum force the wheel is using for propulsion.", + "default": 0.0 + }, + "currentSteeringRatio": { + "type": "number", + "description": "The ratio of the maximum steering angle the wheel is rotated to.", + "default": 0.0 + }, + "maxForce": { + "type": "number", + "description": "The maximum force in Newtons (kg⋅m/s²) that the wheel can provide.", + "default": 0.0 + }, + "maxSteeringAngle": { + "type": "number", + "description": "The maximum angle in radians that the wheel can steer.", + "default": 0.0 + }, + "physicsMaterial": { + "allOf": [ { "$ref": "glTFid.schema.json" } ], + "description": "The index of the physics material in the top level physicsMaterials array.", + "default": -1 + }, + "radius": { + "type": "number", + "description": "The radius of the wheel in meters. This is the radius of a circle in the local YZ plane.", + "default": 0.25 + }, + "suspensionDampingCompression": { + "type": "number", + "description": "The damping of the suspension during compression, the resistance to the velocity of the suspension. It is measured in Newton-seconds per meter (N⋅s/m), or kilograms per second (kg/s) in SI base units.", + "default": 2000.0 + }, + "suspensionDampingRebound": { + "type": "number", + "description": "The damping of the suspension during rebound/relaxation, the resistance to the velocity of the suspension. It is measured in Newton-seconds per meter (N⋅s/m), or kilograms per second (kg/s) in SI base units.", + "default": 2000.0 + }, + "suspensionStiffness": { + "type": "number", + "description": "The stiffness of the suspension, the resistance to traveling away from the start point. It is measured in Newtons per meter (N/m), or kilograms per second squared (kg/s²) in SI base units.", + "default": 20000.0 + }, + "suspensionTravel": { + "type": "number", + "description": "The maximum distance the suspension can move up or down in meters.", + "default": 0.25 + }, + "width": { + "type": "number", + "description": "The width of the wheel in meters. This is the width of the wheel in the local X axis.", + "default": 0.125 + }, + "extensions": { }, + "extras": { } + } +} diff --git a/extensions/2.0/OMI_vehicle_wheel/schema/node.OMI_vehicle_wheel.schema.json b/extensions/2.0/OMI_vehicle_wheel/schema/node.OMI_vehicle_wheel.schema.json new file mode 100644 index 0000000..573b0fe --- /dev/null +++ b/extensions/2.0/OMI_vehicle_wheel/schema/node.OMI_vehicle_wheel.schema.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "node.OMI_vehicle_wheel.schema.json", + "title": "OMI_vehicle_wheel glTF Node Extension", + "type": "object", + "allOf": [ { "$ref": "glTFProperty.schema.json" } ], + "properties": { + "wheel": { + "allOf": [ { "$ref": "glTFid.schema.json" } ], + "description": "The id of the wheel parameters referenced by this node." + }, + "extensions": {}, + "extras": {} + }, + "required": [ + "wheel" + ] +}