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.

103 lines (83 loc) 3.6 kB
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[] = []; @serializable() 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); } }