UNPKG

@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.

370 lines • 14.5 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import { Group, Mesh, Vector3 } from "three"; import { isDevEnvironment } from "../engine/debug/index.js"; import { addComponent } from "../engine/engine_components.js"; import { Gizmos } from "../engine/engine_gizmos.js"; import { serializable } from "../engine/engine_serialization_decorator.js"; import { getBoundingBox } from "../engine/engine_three_utils.js"; import { validate } from "../engine/engine_util_decorator.js"; import { getParam, unwatchWrite, watchWrite } from "../engine/engine_utils.js"; import { NEEDLE_progressive } from "../engine/extensions/NEEDLE_progressive.js"; import { Behaviour } from "./Component.js"; import { Rigidbody } from "./RigidBody.js"; /** * Collider is the base class for all colliders. A collider is a physical shape that is used to detect collisions with other objects in the scene. * Colliders are used in combination with a {@link Rigidbody} to create physical interactions between objects. * Colliders are registered with the physics engine when they are enabled and removed when they are disabled. * @category Physics * @group Components */ export class Collider extends Behaviour { /** * Identifies this component as a collider. * @internal */ get isCollider() { return true; } /** * The {@link Rigidbody} that this collider is attached to. This handles the physics simulation for this collider. */ attachedRigidbody = null; /** * When `true` the collider will not be used for collision detection but will still trigger events. * Trigger colliders can trigger events when other colliders enter their space, without creating a physical response/collision. */ isTrigger = false; /** * The physics material that defines physical properties of the collider such as friction and bounciness. */ sharedMaterial; /** * The layers that this collider belongs to. Used for filtering collision detection. * @default [0] */ membership = [0]; /** * The layers that this collider will interact with. Used for filtering collision detection. */ filter; /** @internal */ awake() { super.awake(); if (!this.attachedRigidbody) this.attachedRigidbody = this.gameObject.getComponentInParent(Rigidbody); } /** @internal */ start() { if (!this.attachedRigidbody) this.attachedRigidbody = this.gameObject.getComponentInParent(Rigidbody); } /** @internal */ onEnable() { // a rigidbody is not assigned if we export an asset if (!this.attachedRigidbody) this.attachedRigidbody = this.gameObject.getComponentInParent(Rigidbody); } /** @internal */ onDisable() { this.context.physics.engine?.removeBody(this); } /** * Returns the underlying physics body from the physics engine. * Only available if the component is enabled and active in the scene. */ get body() { return this.context.physics.engine?.getBody(this); } /** * Updates the collider's properties in the physics engine. * Use this when you've changed collider properties and need to sync with the physics engine. */ updateProperties = () => { this.context.physics.engine?.updateProperties(this); }; /** * Updates the physics material in the physics engine. * Call this after changing the sharedMaterial property. */ updatePhysicsMaterial() { this.context.physics.engine?.updatePhysicsMaterial(this); } } __decorate([ serializable(Rigidbody) ], Collider.prototype, "attachedRigidbody", void 0); __decorate([ serializable() ], Collider.prototype, "isTrigger", void 0); __decorate([ serializable() ], Collider.prototype, "sharedMaterial", void 0); __decorate([ serializable() ], Collider.prototype, "membership", void 0); __decorate([ serializable() ], Collider.prototype, "filter", void 0); /** * SphereCollider represents a sphere-shaped collision volume. * Useful for objects that are roughly spherical in shape or need a simple collision boundary. * @category Physics * @group Components */ export class SphereCollider extends Collider { /** * The radius of the sphere collider. */ radius = .5; /** * The center position of the sphere collider relative to the transform's position. */ center = new Vector3(0, 0, 0); /** * Registers the sphere collider with the physics engine and sets up scale change monitoring. */ onEnable() { super.onEnable(); this.context.physics.engine?.addSphereCollider(this); watchWrite(this.gameObject.scale, this.updateProperties); } /** * Removes scale change monitoring when the collider is disabled. */ onDisable() { super.onDisable(); unwatchWrite(this.gameObject.scale, this.updateProperties); } /** * Updates collider properties when validated in the editor or inspector. */ onValidate() { this.updateProperties(); } } __decorate([ validate(), serializable() ], SphereCollider.prototype, "radius", void 0); __decorate([ serializable(Vector3) ], SphereCollider.prototype, "center", void 0); /** * BoxCollider represents a box-shaped collision volume. * Ideal for rectangular objects or objects that need a simple cuboid collision boundary. * @category Physics * @group Components */ export class BoxCollider extends Collider { /** * Creates and adds a BoxCollider to the given object. * @param obj The object to add the collider to * @param opts Configuration options for the collider and optional rigidbody * @returns The newly created BoxCollider */ static add(obj, opts) { const collider = addComponent(obj, BoxCollider); collider.autoFit(); if (opts?.rigidbody === true) { addComponent(obj, Rigidbody, { isKinematic: false }); } return collider; } /** * The size of the box collider along each axis. */ size = new Vector3(1, 1, 1); /** * The center position of the box collider relative to the transform's position. */ center = new Vector3(0, 0, 0); /** * Registers the box collider with the physics engine and sets up scale change monitoring. * @internal */ onEnable() { super.onEnable(); this.context.physics.engine?.addBoxCollider(this, this.size); watchWrite(this.gameObject.scale, this.updateProperties); } /** * Removes scale change monitoring when the collider is disabled. * @internal */ onDisable() { super.onDisable(); unwatchWrite(this.gameObject.scale, this.updateProperties); } /** * Updates collider properties when validated in the editor or inspector. * @internal */ onValidate() { this.updateProperties(); } /** * Automatically fits the collider to the geometry of the object. * Sets the size and center based on the object's bounding box. * @param opts Options object with a debug flag to visualize the bounding box */ autoFit(opts) { const obj = this.gameObject; // we need to transform the object into identity // because the physics collider will correctly apple the object's transform again // if we don't do it here we will have the transform applied twice const originalPosition = obj.position.clone(); const originalQuaternion = obj.quaternion.clone(); const originalScale = obj.scale.clone(); const originalParent = obj.parent; obj.position.set(0, 0, 0); obj.quaternion.set(0, 0, 0, 1); obj.scale.set(1, 1, 1); obj.parent = null; obj.updateMatrix(); const bb = getBoundingBox([obj]); obj.position.copy(originalPosition); obj.quaternion.copy(originalQuaternion); obj.scale.copy(originalScale); obj.parent = originalParent; if (opts?.debug === true) Gizmos.DrawWireBox3(bb, 0xffdd00, 20); // if (!obj.geometry.boundingBox) obj.geometry.computeBoundingBox(); // const bb = obj.geometry.boundingBox!; this.size = bb.getSize(new Vector3()) || new Vector3(1, 1, 1); this.center = bb.getCenter(new Vector3()) || new Vector3(0, 0, 0); if (this.size.length() <= 0) { this.size.set(0.01, 0.01, 0.01); } } } __decorate([ validate(), serializable(Vector3) ], BoxCollider.prototype, "size", void 0); __decorate([ serializable(Vector3) ], BoxCollider.prototype, "center", void 0); /** * MeshCollider creates a collision shape from a mesh geometry. * Allows for complex collision shapes that match the exact geometry of an object. * @category Physics * @group Components */ export class MeshCollider extends Collider { /** * The mesh that is used to create the collision shape. * If not set, the collider will try to use the mesh of the object it's attached to. */ sharedMesh; /** * When `true` the collider is treated as a solid object without holes. * Set to `false` if you want this mesh collider to be able to contain other objects. */ convex = false; /** * Creates and registers the mesh collider with the physics engine. * Handles both individual meshes and mesh groups. */ onEnable() { super.onEnable(); if (!this.context.physics.engine) return; if (!this.sharedMesh?.isMesh) { // HACK using the renderer mesh if (this.gameObject instanceof Mesh || this.gameObject instanceof Group) { // We're passing a group in here as well, the code below handles that correctly this.sharedMesh = this.gameObject; } } const LOD = 0; if (this.sharedMesh?.isMesh) { this.context.physics.engine.addMeshCollider(this, this.sharedMesh, this.convex); NEEDLE_progressive.assignMeshLOD(this.sharedMesh, LOD).then(res => { if (res && this.activeAndEnabled && this.context.physics.engine && this.sharedMesh) { this.context.physics.engine.removeBody(this); this.sharedMesh.geometry = res; this.context.physics.engine.addMeshCollider(this, this.sharedMesh, this.convex); } }); } else { const group = this.sharedMesh; if (group?.isGroup) { console.warn(`MeshCollider mesh is a group \"${this.sharedMesh?.name || this.gameObject.name}\", adding all children as colliders. This is currently not fully supported (colliders can not be removed from world again)`, this); const promises = new Array(); for (const ch in group.children) { const child = group.children[ch]; if (child.isMesh) { this.context.physics.engine.addMeshCollider(this, child, this.convex); promises.push(NEEDLE_progressive.assignMeshLOD(child, LOD)); } } Promise.all(promises).then(res => { if (res.some(r => r) == false) return; this.context.physics.engine?.removeBody(this); const mesh = new Mesh(); for (const r of res) { if (r && this.activeAndEnabled) { mesh.geometry = r; this.context.physics.engine?.addMeshCollider(this, mesh, this.convex); } } }); } else { if (isDevEnvironment() || getParam("showcolliders")) { console.warn(`[MeshCollider] A MeshCollider mesh is assigned to an unknown object on \"${this.gameObject.name}\", but it's neither a Mesh nor a Group. Please double check that you attached the collider component to the right object and report a bug otherwise!`, this); } } } } } __decorate([ serializable(Mesh) ], MeshCollider.prototype, "sharedMesh", void 0); __decorate([ serializable() ], MeshCollider.prototype, "convex", void 0); /** * CapsuleCollider represents a capsule-shaped collision volume (cylinder with hemispherical ends). * Ideal for character controllers and objects that need a rounded collision shape. * @category Physics * @group Components */ export class CapsuleCollider extends Collider { /** * The center position of the capsule collider relative to the transform's position. */ center = new Vector3(0, 0, 0); /** * The radius of the capsule's cylindrical body and hemispherical ends. */ radius = .5; /** * The total height of the capsule including both hemispherical ends. */ height = 2; /** * Registers the capsule collider with the physics engine. */ onEnable() { super.onEnable(); this.context.physics.engine?.addCapsuleCollider(this, this.height, this.radius); } } __decorate([ serializable(Vector3) ], CapsuleCollider.prototype, "center", void 0); __decorate([ serializable() ], CapsuleCollider.prototype, "radius", void 0); __decorate([ serializable() ], CapsuleCollider.prototype, "height", void 0); //# sourceMappingURL=Collider.js.map