@needle-tools/engine
Version:
Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.
133 lines (116 loc) • 8.25 kB
text/typescript
import { Object3D } from "three";
import { BoxCollider, CapsuleCollider, Collider, MeshCollider, SphereCollider } from "../../../../Collider.js";
import { GameObject } from "../../../../Component.js";
import { Rigidbody } from "../../../../RigidBody.js";
import type { IUSDExporterExtension } from "../../Extension.js";
import { USDObject, USDWriter, USDZExporterContext } from "../../ThreeUSDZExporter.js";
export class PhysicsExtension implements IUSDExporterExtension {
get extensionName(): string { return "Physics"; }
onExportObject?(object: Object3D, model : USDObject, _context: USDZExporterContext) {
const rigidBodySources = GameObject.getComponents(object, Rigidbody).filter(c => c.enabled);
// no triggers supported for now; large trigger areas seem to break VisionOS since they're still interactable and block OS UI
const colliderSources = GameObject.getComponents(object, Collider).filter(c => c.enabled && !c.isTrigger);
let rigidBody = rigidBodySources.length > 0 ? rigidBodySources[0] : null;
const colliderSource = colliderSources.length > 0 ? colliderSources[0] : null;
// we still need a RigidBody component for the object to participate in physics;
// with only a Collider component it can only serve as trigger
// see https://developer.apple.com/documentation/realitykit/collisioncomponent#overview
let temporaryRigidbody: Rigidbody | undefined = undefined;
if (colliderSource && !rigidBody) {
rigidBody = new Rigidbody();
rigidBody.isKinematic = true;
temporaryRigidbody = rigidBody;
}
if (rigidBody) {
model.addEventListener('serialize', (writer: USDWriter, _context: USDZExporterContext) => {
if (!rigidBody) return;
writer.appendLine();
writer.beginBlock(`def RealityKitComponent "RigidBody"`, "{", true );
// Gravity is enabled by default
if (!rigidBody.useGravity){
writer.appendLine(`bool gravityEnabled = 0`);
}
writer.appendLine(`uniform token info:id = "RealityKit.RigidBody"`);
if (rigidBody.isKinematic){
writer.appendLine(`token motionType = "Kinematic"`);
}
writer.beginBlock(`def RealityKitStruct "massFrame"`, "{", true );
writer.appendLine(`float m_mass = ${rigidBody.mass}`);
writer.beginBlock(`def RealityKitStruct "m_pose"`, "{", true );
/* TODO: Apple has a concept of center of mass orientation, not sure what that means here or what an equivalent mapping would be */
writer.appendLine(`float3 position = (${rigidBody.centerOfMass.x}, ${rigidBody.centerOfMass.y}, ${rigidBody.centerOfMass.z})`);
writer.closeBlock( "}" );
writer.closeBlock( "}" );
if (colliderSources.length > 0) {
const colliderSource = colliderSources[0];
writer.beginBlock(`def RealityKitStruct "material"`, "{", true );
const mat = colliderSource.sharedMaterial;
if (mat && mat.dynamicFriction !== undefined)
writer.appendLine(`double dynamicFriction = ${colliderSource.sharedMaterial?.dynamicFriction}`);
if (mat && mat.bounciness !== undefined)
writer.appendLine(`double restitution = ${colliderSource.sharedMaterial?.bounciness}`);
// eslint-disable-next-line deprecation/deprecation
if (mat && mat.staticFriction !== undefined)
// eslint-disable-next-line deprecation/deprecation
writer.appendLine(`double staticFriction = ${colliderSource.sharedMaterial?.staticFriction}`);
writer.closeBlock( "}" );
}
writer.closeBlock( "}" );
});
}
if (colliderSource) {
// TODO: Apple only allows one collider, Unity allows many, are many typically used on each object though? What can we do here? combine them? take the first?
model.addEventListener('serialize', (writer: USDWriter, _context: USDZExporterContext) => {
writer.beginBlock(`def RealityKitComponent "Collider"`, "{", true );
// TODO: a group is needed for collisions, not sure what this controls though
writer.appendLine(`uint group = 1`);
writer.appendLine(`uniform token info:id = "RealityKit.Collider"`);
// TODO: a mask is needed for collisions, this is the max value for int, possibly collision flags but i'm not sure what each bit means, or if we have that exported info from unity
// This is coming from RealityComposerPro
writer.appendLine(`uint mask = 4294967295`);
const isTrigger = colliderSource.isTrigger; // not currently used, see comment above
const typeName = isTrigger ? "Trigger" : "Default";
writer.appendLine(`token type = "${typeName}"`);
writer.beginBlock(`def RealityKitStruct "Shape"`, "{", true );
if (colliderSource instanceof SphereCollider){
const sphereCollider = colliderSource as SphereCollider;
writer.appendLine(`token shapeType = "Sphere"`);
writer.appendLine(`float radius = ${sphereCollider.radius}`);
}
else if (colliderSource instanceof BoxCollider){
const boxCollider = colliderSource as BoxCollider;
writer.appendLine(`token shapeType = "Box"`);
writer.appendLine(`float3 extent = (${boxCollider.size.x}, ${boxCollider.size.y}, ${boxCollider.size.z})`);
}
else if (colliderSource instanceof CapsuleCollider){
const capsuleCollider = colliderSource as CapsuleCollider;
writer.appendLine(`token shapeType = "Capsule"`);
writer.appendLine(`float radius = ${capsuleCollider.radius}`);
writer.appendLine(`float height = ${capsuleCollider.height}`);
}
else if (colliderSource instanceof MeshCollider && colliderSource.sharedMesh?.geometry) {
// get the bounds of the mesh
const geo = colliderSource.sharedMesh.geometry;
if (!geo.boundingBox) geo.computeBoundingBox();
const box = colliderSource.sharedMesh.geometry.boundingBox;
if (box) {
writer.appendLine(`token shapeType = "Box"`);
writer.appendLine(`float3 extent = (${box.max.x - box.min.x}, ${box.max.y - box.min.y}, ${box.max.z - box.min.z})`);
console.log("[USDZ] Only Box, Sphere, and Capsule colliders are supported in visionOS/iOS. MeshCollider will be exported as Box", colliderSource);
}
}
else {
console.warn("[USDZ] Only Box, Sphere, and Capsule colliders are supported in visionOS/iOS. Ignoring collider:", colliderSource)
}
writer.beginBlock(`def RealityKitStruct "pose"`, "{", true );
// TODO: this is used for rotating the collider, I think this isn't necessary since all the changes happen in the parent transform from needle. Not positive about this though.
writer.closeBlock( "}" );
writer.closeBlock( "}" );
writer.closeBlock( "}" );
});
if (colliderSources.length > 1) {
console.log("WARNING: Multiple colliders detected. visionOS / iOS can only support objects with a single collider, only exporting the first collider: ", colliderSource)
}
}
}
}