# Needle Engine — Core API Reference ## Table of Contents - [Lifecycle Methods](#lifecycle-methods-complete) - [Decorators](#decorators) - [Context API](#context-api-thiscontext) - [GameObject Utilities](#gameobject-utilities) - [Finding Objects](#finding-objects) - [Coroutines](#coroutines) - [Asset Loading at Runtime](#asset-loading-at-runtime) - [Renderer and Materials](#renderer-and-materials) - [Object3D Extensions](#object3d-extensions) - [Utilities](#utilities) - [Vite Plugin Options](#vite-plugin-options) --- ## Lifecycle Methods (complete) All methods are optional — only implement what you need. ```ts class MyComponent extends Behaviour { // Initialization awake() // first, before Start, even if disabled onEnable() // whenever component/GO becomes active start() // once, on first enabled frame // Per-frame earlyUpdate() // every frame, before update() update() // every frame lateUpdate() // every frame, after all update() runs onBeforeRender(frame: XRFrame | null) // just before Three.js renders onAfterRender() // just after Three.js renders // Deactivation / cleanup onDisable() // when component/GO becomes inactive onDestroy() // called by destroy(obj) — NOT by removeComponent() // Pointer events (requires an EventSystem + Raycaster in the scene) onPointerEnter?(args: PointerEventData) // pointer enters this object onPointerMove?(args: PointerEventData) // pointer moves over this object onPointerExit?(args: PointerEventData) // pointer leaves this object onPointerDown?(args: PointerEventData) // pointer button pressed on this object onPointerUp?(args: PointerEventData) // pointer button released onPointerClick?(args: PointerEventData) // full click on this object // XR events supportsXR?(mode: XRSessionMode): boolean // filter which XR modes this component handles onBeforeXR?(mode: XRSessionMode, args: XRSessionInit) // modify session init params onEnterXR?(args: NeedleXREventArgs) // joined an XR session onUpdateXR?(args: NeedleXREventArgs) // per-frame during XR onLeaveXR?(args: NeedleXREventArgs) // left the XR session onXRControllerAdded?(args: NeedleXRControllerEventArgs) // controller connected onXRControllerRemoved?(args: NeedleXRControllerEventArgs) // controller disconnected // Physics (requires Needle Collider component on same GameObject) onCollisionEnter(col: Collision) onCollisionStay(col: Collision) onCollisionExit(col: Collision) onTriggerEnter(col: Collision) onTriggerStay(col: Collision) onTriggerExit(col: Collision) } ``` --- ## Decorators | Decorator | Purpose | |---|---| | `@registerType` | Required on every component — registers the class for GLB deserialization | | `@serializable()` | Serialize/deserialize a primitive (number, string, boolean) | | `@serializable(Type)` | Serialize/deserialize a typed field (Object3D, Texture, Color, etc.) | | `@syncField()` | Auto-sync field over the network in a SyncedRoom | | `@syncField(onChange)` | Sync + call a callback when value changes remotely | ### Serializable Types ```ts // Primitives — no type argument needed @serializable() myNumber!: number; @serializable() myString!: string; @serializable() myBool!: boolean; // Complex types — pass the constructor import { RGBAColor, AssetReference } from "@needle-tools/engine"; import { Object3D, Texture, Vector2, Vector3, Color } from "three"; @serializable(Object3D) myRef!: Object3D; @serializable(Texture) tex!: Texture; @serializable(RGBAColor) col!: RGBAColor; @serializable(AssetReference) asset!: AssetReference; @serializable(Vector3) pos!: Vector3; ``` --- ## Context API (`this.context`) ```ts this.context.scene // THREE.Scene this.context.mainCamera // THREE.Camera (currently active) this.context.renderer // THREE.WebGLRenderer this.context.domElement // HTML element // Time this.context.time.frame // frame counter (number) this.context.time.deltaTime // seconds since last frame (affected by timeScale) this.context.time.time // total elapsed seconds this.context.time.realtimeSinceStartup this.context.time.timeScale // default 1; affects deltaTime, animation, and audio // Input — polling API (check in update()) this.context.input.getPointerDown(index) // pointer just pressed this frame this.context.input.getPointerUp(index) // pointer just released this frame this.context.input.getPointerPressed(index) // pointer currently held this.context.input.getPointerPosition(index) // {x, y} in screen pixels this.context.input.getPointerPositionDelta(index) // movement since last frame this.context.input.getPointerPressedCount() // how many pointers are pressed this.context.input.mousePosition // shortcut for pointer 0 position this.context.input.getKeyDown(key) // "Space", "ArrowLeft", "a", etc. this.context.input.getKeyUp(key) this.context.input.getKeyPressed(key) // Input — event-based API (subscribe/unsubscribe) this.context.input.addEventListener("pointerdown", (evt) => { /* NEPointerEvent */ }); this.context.input.addEventListener("pointerup", callback); this.context.input.addEventListener("pointermove", callback); this.context.input.addEventListener("keydown", callback); this.context.input.removeEventListener("pointerdown", callback); // Component pointer callbacks (require EventSystem + Raycaster in the scene): // onPointerEnter, onPointerMove, onPointerExit, onPointerDown, onPointerUp, onPointerClick // These fire on the specific object the pointer interacts with (see Lifecycle Methods) // Physics — two raycast systems for different purposes: // 1. Visual raycast: hits rendered geometry (no collider needed) // Automatically builds MeshBVH (three-mesh-bvh) on web workers — falls back to standard // three.js raycasting until BVH is ready. Works with procedural geometry too. // Use for: UI interaction, picking visible objects, click detection // Simplest usage — uses current pointer position, works in pointer event handlers: const hits = this.context.physics.raycast(); // With options: this.context.physics.raycast({ maxDistance: 100, layerMask: 0xff, ignore: [this.gameObject] }) // From a specific pixel position (e.g. in a raw pointerdown handler): // IMPORTANT: screenPoint is in normalized device coordinates (-1 to 1), NOT pixels! const hits = this.context.physics.raycast({ screenPoint: new Vector2( (e.clientX / window.innerWidth) * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1 ), }); // DO NOT pass raw pixel coords as screenPoint — this is wrong: // ctx.physics.raycast({ screenPoint: new Vector2(e.clientX, e.clientY) }) // WRONG! // 2. Physics engine raycast: hits Rapier colliders only // Use for: ground detection, line-of-sight, physics-based queries this.context.physics.engine?.raycast(origin, direction, { maxDistance, solid }) this.context.physics.engine?.raycastAndGetNormal(origin, direction) this.context.physics.engine?.sphereOverlap(position, radius) // Access Rapier world directly for advanced queries: this.context.physics.engine.world // underlying Rapier world // Network this.context.connection // core networking manager (usable directly or via SyncedRoom) ``` --- ## GameObject Utilities ```ts import { instantiate, destroy, GameObject } from "@needle-tools/engine"; // Component access go.getComponent(Type) go.getComponentInChildren(Type) go.getComponentInParent(Type) go.getComponents(Type) // all matching on same GO go.getComponentsInChildren(Type) // Lifecycle instantiate(source, opts?) // preferred — clone; opts: { position, rotation, parent } destroy(obj) // destroys GO + calls onDestroy on components obj.removeComponent(comp) // removes without calling onDestroy // Active state go.visible = false // hides in scene (still ticks) GameObject.setActive(go, false) // disables lifecycle callbacks // Hierarchy go.contains(otherObj) // true if otherObj is a descendant (Needle extension on Object3D) // World-space properties (Needle extensions on Object3D) go.worldPosition // get/set world position (Vector3) go.worldQuaternion // get/set world rotation (Quaternion) go.worldScale // get/set world scale (Vector3) go.worldForward // forward direction in world space (Vector3) go.worldRight // right direction in world space (Vector3) go.worldUp // up direction in world space (Vector3) // Tag / name go.name // string go.userData.tags // string[] (set from Unity via Tag component) ``` --- ## Finding Objects ```ts import { findObjectOfType, findObjectsOfType } from "@needle-tools/engine"; findObjectOfType(MyComponent, ctx) // first match in scene findObjectsOfType(MyComponent, ctx) // all matches ctx.scene.getObjectByName("Player") // by name (Three.js built-in) ``` --- ## Coroutines Generator functions that can yield across frames: ```ts import { WaitForSeconds, WaitForFrames, delayForFrames } from "@needle-tools/engine"; start() { this.startCoroutine(this.flashLight()); } *flashLight() { while (true) { this.light.visible = !this.light.visible; yield WaitForSeconds(0.5); // wait 0.5 seconds // yield; // wait exactly one frame // yield WaitForFrames(10); // wait N frames } } // Stop all coroutines on this component: this.stopAllCoroutines(); // Async alternative (returns a Promise): await delayForFrames(5); ``` --- ## Asset Loading at Runtime ```ts import { AssetReference } from "@needle-tools/engine"; // Declare in component (set in Unity Inspector) @serializable(AssetReference) prefab!: AssetReference; async start() { // Load and instantiate const instance = await this.prefab.instantiate({ parent: this.gameObject }); // Or just load the GLTF (no instantiate) const gltf = await this.prefab.loadAssetAsync(); } ``` Load a GLB by URL at runtime: ```ts import { AssetReference } from "@needle-tools/engine"; const ref = AssetReference.getOrCreate(this.context.domElement.baseURI, "assets/extra.glb"); const instance = await ref.instantiate({ parent: this.gameObject }); ``` Load any asset directly (without AssetReference): ```ts import { loadAsset } from "@needle-tools/engine"; const model = await loadAsset("assets/model.glb"); const obj = model.scene; // ← Object3D is on .scene, not the return value itself obj.traverse(n => { /* ... */ }); ``` > **`loadAsset()` returns a model wrapper** (with `.scene`, `.animations`, etc.) — not an Object3D directly. The wrapper type is universal regardless of format (GLB, FBX, OBJ, USDZ). Use `model.scene` to get the root Object3D. > **Caching:** `AssetReference.getOrCreate()` caches by URL and returns the **same Object3D** on repeated calls. Adding a cached object to the scene again just moves it. Use `.instantiate()` for independent copies. > **Note:** Needle Engine automatically handles KTX, Draco, and meshopt decompression — no loader setup needed. --- ## Renderer and Materials ### Accessing meshes and materials The `Renderer` component wraps Three.js meshes/materials. It's present on objects exported from Unity/Blender, but not automatically created for code-only objects — add it manually with `addComponent(Renderer)`, or access materials directly via Three.js (`(obj as Mesh).material`). ```ts import { Renderer } from "@needle-tools/engine"; const renderer = this.gameObject.getComponent(Renderer); // Materials renderer.sharedMaterial // first material (read/write) renderer.sharedMaterials // all materials (array, index-assignable) renderer.sharedMaterials[0] = mat; // replace a material by index // Meshes renderer.sharedMesh // first Mesh/SkinnedMesh Object3D renderer.sharedMeshes // all mesh Object3Ds (for multi-material groups) // GPU Instancing — draws identical meshes in a single draw call for performance // In Unity/Blender: enable on the material or via the Needle UI on the object // In code: Renderer.setInstanced(obj, true); // enable instancing (also creates Renderer if missing) // Visibility (without affecting hierarchy or component state) Renderer.setVisible(obj, false); ``` ### MaterialPropertyBlock — per-object material overrides Overrides material properties (color, texture, roughness, etc.) on a **per-object** basis without creating new material instances. Multiple objects can share the same material but look different. Overrides are applied in `onBeforeRender` and restored in `onAfterRender`. ```ts import { MaterialPropertyBlock } from "@needle-tools/engine"; import { Color, Texture } from "three"; // Get or create a property block for an object (never use the constructor directly) const block = MaterialPropertyBlock.get(myMesh); // Override properties block.setOverride("color", new Color(1, 0, 0)); block.setOverride("roughness", 0.2); block.setOverride("map", myTexture); // Override with UV transform (e.g. for lightmaps) block.setOverride("lightMap", lightmapTex, { offset: new Vector2(0.5, 0.5), repeat: new Vector2(2, 2) }); // Read back const color = block.getOverride("color")?.value; // Remove overrides block.removeOveride("color"); // remove one block.clearAllOverrides(); // remove all block.dispose(); // remove the entire property block // Check if an object has overrides MaterialPropertyBlock.hasOverrides(myMesh); ``` Overrides are registered on the **Object3D**, not on the material — if you swap the material, overrides still apply to the new one. Use `dispose()` or `clearAllOverrides()` to remove them. Common use cases: per-object colors/tinting, lightmaps, reflection probes, see-through/x-ray effects. --- ## Object3D Extensions Needle Engine patches Three.js `Object3D.prototype` with convenience properties. These work on **any** Object3D in the scene. ### World transforms (getter + setter) ```ts // GET — returns a temporary Vector3/Quaternion (don't store references, copy if needed) obj.worldPosition // Vector3 — world-space position obj.worldQuaternion // Quaternion — world-space rotation obj.worldRotation // Vector3 — world-space euler (degrees) obj.worldScale // Vector3 — world-space scale // SET — must assign to apply (mutating the returned vector won't update the transform) obj.worldPosition = new Vector3(1, 2, 3); // sets world position obj.worldQuaternion = myQuat; // sets world rotation obj.worldScale = new Vector3(2, 2, 2); // sets world scale // Direction vectors (read-only) obj.worldForward // Vector3 — forward direction in world space (0,0,1 rotated) obj.worldRight // Vector3 — right direction obj.worldUp // Vector3 — up direction // worldForward also has a setter — point an object in a direction: obj.worldForward = targetDirection; ``` The getters return **temporary vectors** from an internal pool — they're overwritten on the next call. You can read and re-assign them directly (`obj.worldPosition = other.worldPosition`). For temporary math use `getTempVector()`. Only use `.clone()` when you must store a value across frames — never in per-frame code. ### Component access ```ts obj.getComponent(MyComponent) // first component of type obj.getComponentInChildren(MyComponent) // search children recursively obj.getComponentInParent(MyComponent) // search parents recursively obj.getComponents(MyComponent) // all of type on this object obj.getComponentsInChildren(MyComponent) obj.addComponent(MyComponent) // add a new component ``` ### Other extensions ```ts obj.guid // get/set — unique identifier for networking (string | undefined) obj.contains(otherObj) // true if otherObj is a descendant obj.activeSelf // get/set active state (same as GameObject.setActive) ``` `guid` is used by the networking system to identify objects across clients. Objects exported from Unity/Blender have guids automatically. For runtime-created objects, set `obj.guid = "my-id"` if they need to participate in networking (e.g. `syncInstantiate`, `SyncedTransform`). ### Bounding box and fitting ```ts import { getBoundingBox, fitObjectIntoVolume } from "@needle-tools/engine"; // Get the bounding box of one or more objects const box = getBoundingBox(myObject); // single object const box = getBoundingBox([obj1, obj2, obj3]); // multiple objects const box = getBoundingBox(myObject, [ignoreThisChild]); // with objects to ignore const box = getBoundingBox(myObject, undefined, camera.layers); // filter by layer const size = box.getSize(new Vector3()); const center = box.getCenter(new Vector3()); // Fit an object into a target volume (scale + position) fitObjectIntoVolume(myObject, targetVolume); ``` --- ## Async Modules (`NEEDLE_ENGINE_MODULES`) Heavy dependencies (physics, postprocessing, etc.) are loaded on demand, not bundled into the main entry point. **You do NOT need to call these for normal usage** — physics and postprocessing initialize automatically when their components are used. These are for advanced use cases like accessing the raw Rapier API or pmndrs postprocessing module directly. ```ts import { NEEDLE_ENGINE_MODULES } from "@needle-tools/engine"; // Available modules: NEEDLE_ENGINE_MODULES.RAPIER_PHYSICS // Rapier physics (WASM) NEEDLE_ENGINE_MODULES.POSTPROCESSING // pmndrs postprocessing NEEDLE_ENGINE_MODULES.POSTPROCESSING_AO // N8AO ambient occlusion NEEDLE_ENGINE_MODULES.MaterialX // MaterialX materials (WASM) NEEDLE_ENGINE_MODULES.PEERJS // PeerJS for networking // Each module has: await module.load(); // trigger load + wait for it await module.ready(); // wait for load (doesn't trigger one) module.MODULE // the loaded module (undefined until loaded) module.MAYBEMODULE // null until loaded, then same as MODULE ``` --- ## Utilities All imported from `@needle-tools/engine`. ### Math (`Mathf`) ```ts import { Mathf } from "@needle-tools/engine"; Mathf.lerp(a, b, t) // linear interpolation Mathf.clamp(value, min, max) // clamp to range Mathf.clamp01(value) // clamp to [0, 1] Mathf.remap(value, inMin, inMax, outMin, outMax) // remap between ranges Mathf.moveTowards(current, target, step) // step toward target Mathf.inverseLerp(a, b, value) // find t given value Mathf.toDegrees(radians) Mathf.toRadians(degrees) Mathf.random(min, max) // random in range (or random from array) Mathf.easeInOutCubic(t) // easing function ``` ### Temporary objects (avoid per-frame allocations) ```ts import { getTempVector, getTempQuaternion } from "@needle-tools/engine"; // Returns reusable objects from a circular buffer — no GC pressure const v = getTempVector(1, 0, 0); // temporary Vector3 const q = getTempQuaternion(); // temporary Quaternion // Don't store references — they get reused. Clone if you need to keep them. ``` ### Device detection (`DeviceUtilities`) ```ts import { DeviceUtilities } from "@needle-tools/engine"; DeviceUtilities.isDesktop() // Windows/Mac (not headsets) DeviceUtilities.isMobileDevice() // phone or tablet DeviceUtilities.isiOS() // iPhone, iPad, Vision Pro DeviceUtilities.isAndroidDevice() DeviceUtilities.isQuest() // Meta Quest DeviceUtilities.isVisionOS() // Apple Vision Pro DeviceUtilities.isSafari() DeviceUtilities.supportsQuickLookAR() // USDZ/QuickLook support ``` ### Timing and delays ```ts import { delay, delayForFrames, WaitForSeconds, WaitForFrames, WaitForPromise } from "@needle-tools/engine"; // Async await delay(1000); // wait 1 second await delayForFrames(5); // wait 5 frames // In coroutines yield WaitForSeconds(0.5); yield WaitForFrames(10); yield WaitForPromise(fetch("/api")); // wait for a promise to resolve ``` ### User interaction ```ts import { awaitInputAsync } from "@needle-tools/engine"; // Wait for the first user interaction (useful for audio autoplay policy) await awaitInputAsync(); audioSource.play(); ``` ### URL parameters ```ts import { getParam, setParamWithoutReload } from "@needle-tools/engine"; const room = getParam("room"); // read ?room=xyz from URL setParamWithoutReload("room", "my-room"); // update URL without page reload ``` ### Debug messages (on-screen balloon) ```ts import { showBalloonMessage, showBalloonWarning, showBalloonError } from "@needle-tools/engine"; showBalloonMessage("Hello!"); // info message on screen showBalloonWarning("Watch out!"); // warning (yellow) showBalloonError("Something broke!"); // error (red) ``` ### Debug console Append `?console` to the URL to show an on-screen debug console (uses vConsole). Useful for debugging on mobile devices where dev tools aren't available. ### Screenshots ```ts import { screenshot2, saveImage } from "@needle-tools/engine"; // Simple screenshot (returns data URL) const dataUrl = screenshot2({ width: 1920, height: 1080 }); saveImage(dataUrl, "screenshot.png"); // Screenshot as texture (apply to a material) const tex = screenshot2({ type: "texture", width: 512, height: 512 }); // Screenshot as blob const blob = await screenshot2({ type: "blob" }); // Share via Web Share API await screenshot2({ type: "share", title: "My Scene" }); // Transparent background screenshot2({ transparent: true, trim: true }); // XR screenshot (composites 3D scene over camera feed — requires "camera-access" feature) // Works in AR sessions when camera-access has been requested via onBeforeXR const xrScreenshot = screenshot2({ width: 1080, height: 1920 }); ``` ### QR Code ```ts import { generateQRCode } from "@needle-tools/engine"; const qr = generateQRCode({ text: "https://mysite.com" }); ``` --- ## Vite Plugin Options The `needlePlugins` function accepts user settings as the third argument. These control build behavior, optimization, and features. ```ts import { defineConfig } from "vite"; import { needlePlugins } from "@needle-tools/engine/vite"; export default defineConfig(async ({ command }) => ({ plugins: [ ...(await needlePlugins(command, {}, { // Key options: // Make all external CDN URLs local for offline/self-contained deployments makeFilesLocal: true, // download everything // or: makeFilesLocal: "auto", // auto-detect which features to include // or: makeFilesLocal: { enabled: true, features: ["draco", "ktx2"] }, // PWA support (also install vite-plugin-pwa) pwa: true, // enable with defaults // or: pwa: { /* VitePWAOptions */ }, // Physics engine — set to false to tree-shake Rapier and reduce bundle size useRapier: false, // Build pipeline — compression and optimization of glTF files noBuildPipeline: false, // default: runs optimization buildPipeline: { accessToken: process.env.NEEDLE_CLOUD_TOKEN, // use Needle Cloud for compression }, // Other options: // noAsap: true, // disable glTF preload links // noPoster: true, // disable poster image generation // openBrowser: true, // auto-open browser on local network IP })), ], })); ``` ### `makeFilesLocal` features Downloads external CDN URLs at build time for fully self-contained deployments. Available features: `draco`, `ktx2`, `materialx`, `xr`, `skybox`, `fonts`, `needle-fonts`, `needle-models`, `needle-avatars`, `polyhaven`, `cdn-scripts`, `github-content`, `threejs-models`, `needle-uploads`.