@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.
375 lines (334 loc) • 13.7 kB
text/typescript
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.
*/
(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.
*/
()
isTrigger: boolean = false;
/**
* The physics material that defines physical properties of the collider such as friction and bounciness.
*/
()
sharedMaterial?: PhysicsMaterial;
/**
* The layers that this collider belongs to. Used for filtering collision detection.
* @default [0]
*/
()
membership: number[] = [0];
/**
* The layers that this collider will interact with. Used for filtering collision detection.
*/
()
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.
*/
()
()
radius: number = .5;
/**
* The center position of the sphere collider relative to the transform's position.
*/
(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.
*/
()
(Vector3)
size: Vector3 = new Vector3(1, 1, 1);
/**
* The center position of the box collider relative to the transform's position.
*/
(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.
*/
(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.
*/
()
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<Promise<BufferGeometry | null>>();
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.
*/
(Vector3)
center: Vector3 = new Vector3(0, 0, 0);
/**
* The radius of the capsule's cylindrical body and hemispherical ends.
*/
()
radius: number = .5;
/**
* The total height of the capsule including both hemispherical ends.
*/
()
height: number = 2;
/**
* Registers the capsule collider with the physics engine.
*/
onEnable() {
super.onEnable();
this.context.physics.engine?.addCapsuleCollider(this, this.height, this.radius);
}
}