import { BufferGeometry, Group, Mesh, Object3D, 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 type { PhysicsMaterial } from "../engine/engine_physics.types.js"; import { serializable } from "../engine/engine_serialization_decorator.js"; import { getBoundingBox } from "../engine/engine_three_utils.js"; import type { IBoxCollider, ICollider, ISphereCollider } from "../engine/engine_types.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 implements ICollider { /** * Identifies this component as a collider. * @internal */ get isCollider(): any { return true; } /** * The {@link Rigidbody} that this collider is attached to. This handles the physics simulation for this collider. */ @serializable(Rigidbody) attachedRigidbody: Rigidbody | null = 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. */ @serializable() isTrigger: boolean = false; /** * The physics material that defines physical properties of the collider such as friction and bounciness. */ @serializable() sharedMaterial?: PhysicsMaterial; /** * The layers that this collider belongs to. Used for filtering collision detection. * @default [0] */ @serializable() membership: number[] = [0]; /** * The layers that this collider will interact with. Used for filtering collision detection. */ @serializable() filter?: number[]; /** @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); } } /** * 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 implements ISphereCollider { /** * The radius of the sphere collider. */ @validate() @serializable() radius: number = .5; /** * The center position of the sphere collider relative to the transform's position. */ @serializable(Vector3) center: Vector3 = 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(): void { super.onDisable(); unwatchWrite(this.gameObject.scale, this.updateProperties); } /** * Updates collider properties when validated in the editor or inspector. */ onValidate(): void { this.updateProperties(); } } /** * 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 implements IBoxCollider { /** * 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: Mesh | Object3D, opts?: { rigidbody: boolean, debug?: boolean }) { 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. */ @validate() @serializable(Vector3) size: Vector3 = new Vector3(1, 1, 1); /** * The center position of the box collider relative to the transform's position. */ @serializable(Vector3) center: Vector3 = 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(): void { super.onDisable(); unwatchWrite(this.gameObject.scale, this.updateProperties); } /** * Updates collider properties when validated in the editor or inspector. * @internal */ onValidate(): void { 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?: { debug?: boolean }) { 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); } } } /** * 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. */ @serializable(Mesh) sharedMesh?: Mesh; /** * 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. */ @serializable() convex: boolean = 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 as Mesh; } } 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 as any as Group; 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] as Mesh; 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); } } } } } /** * 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. */ @serializable(Vector3) center: Vector3 = new Vector3(0, 0, 0); /** * The radius of the capsule's cylindrical body and hemispherical ends. */ @serializable() radius: number = .5; /** * The total height of the capsule including both hemispherical ends. */ @serializable() height: number = 2; /** * Registers the capsule collider with the physics engine. */ onEnable() { super.onEnable(); this.context.physics.engine?.addCapsuleCollider(this, this.height, this.radius); } }