@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.
103 lines (83 loc) • 3.6 kB
text/typescript
import { Intersection, Object3D, SkinnedMesh } from "three";
import { type IRaycastOptions, RaycastOptions } from "../../engine/engine_physics.js";
import { serializable } from "../../engine/engine_serialization.js";
import { NeedleXRSession } from "../../engine/engine_xr.js";
import { Behaviour } from "../Component.js";
import { EventSystem } from "./EventSystem.js";
/** Derive from this class to create your own custom Raycaster
* If you override awake, onEnable or onDisable, be sure to call the base class methods
* Implement `performRaycast` to perform your custom raycasting logic
*/
export abstract class Raycaster extends Behaviour {
awake(): void {
EventSystem.createIfNoneExists(this.context);
}
onEnable(): void {
EventSystem.get(this.context)?.register(this);
}
onDisable(): void {
EventSystem.get(this.context)?.unregister(this);
}
abstract performRaycast(_opts?: IRaycastOptions | RaycastOptions | null): Intersection[] | null;
}
export class ObjectRaycaster extends Raycaster {
private targets: Object3D[] | null = null;
private raycastHits: Intersection[] = [];
ignoreSkinnedMeshes = false;
start(): void {
this.targets = [this.gameObject];
}
performRaycast(opts: IRaycastOptions | RaycastOptions | null = null): Intersection[] | null {
if (!this.targets) return null;
opts ??= new RaycastOptions();
opts.targets = this.targets;
opts.results = this.raycastHits;
opts.useAcceleratedRaycast = true;
const orig = opts.testObject;
if (this.ignoreSkinnedMeshes) {
opts.testObject = obj => {
// if we are set to ignore skinned meshes, we return false for them
if (obj instanceof SkinnedMesh) {
return "continue in children";
}
// call the original testObject function
if (orig) return orig(obj);
// otherwise allow raycasting
return true;
};
}
const hits = this.context.physics.raycast(opts);
opts.testObject = orig;
return hits;
}
}
export class GraphicRaycaster extends ObjectRaycaster {
// eventCamera: Camera | null = null;
// ignoreReversedGraphics: boolean = false;
// rootRaycaster: GraphicRaycaster | null = null;
constructor() {
super();
this.ignoreSkinnedMeshes = true;
}
}
export class SpatialGrabRaycaster extends Raycaster {
/**
* Use to disable SpatialGrabRaycaster globally
*/
static allow: boolean = true;
performRaycast(_opts?: IRaycastOptions | RaycastOptions | null): Intersection[] | null {
// ensure we're in XR, otherwise return
if (!NeedleXRSession.active) return null;
if (!SpatialGrabRaycaster.allow) return null;
if (!_opts?.ray) return null;
// TODO this raycast should actually start from gripWorldPosition, not the ray origin, for
// cases like transient-pointer on VisionOS where the ray starts at the head and not the hand
const rayOrigin = _opts.ray.origin;
const radius = 0.015;
// TODO if needed, check if the input source is a XR controller or hand
// draw gizmo around ray origin
// Gizmos.DrawSphere(rayOrigin, radius, 0x00ff0022);
return this.context.physics.sphereOverlap(rayOrigin, radius, false, true);
}
}