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.

217 lines (186 loc) • 7.62 kB
import { AxesHelper, Camera, Color, DirectionalLight, Fog, GridHelper, Mesh, MeshBasicMaterial, MeshStandardMaterial, PerspectiveCamera, PointLight, Scene, WebGLRenderer } from "three"; import { ObjectUtils, PrimitiveType } from "../engine_create_objects.js"; import { Mathf } from "../engine_math.js"; import { delay } from "../engine_utils.js"; export declare type SessionInfo = { session: XRSession, mode: XRSessionMode, init: XRSessionInit }; /** Create with static `start`- used to start an XR session while waiting for session granted */ export class TemporaryXRContext { private static _active: TemporaryXRContext | null = null; static get active() { return this._active; } private static _requestInFlight = false; static async start(mode: XRSessionMode, init: XRSessionInit) { if (this._active) { console.error("Cannot start a new XR session while one is already active"); return null; } if (this._requestInFlight) { console.error("Cannot start a new XR session while a request is already in flight"); return null; } if ('xr' in navigator && navigator.xr) { if (!init) { console.error("XRSessionInit must be provided"); return null; } this._requestInFlight = true; const session = await navigator.xr.requestSession(mode, init); session.addEventListener("end", () => { this._active = null; }); if (!this._requestInFlight) { session.end(); return null; } this._requestInFlight = false; this._active = new TemporaryXRContext(mode, init, session); return this._active; } return null; } static async handoff(): Promise<SessionInfo | null> { if (this._active) { return this._active.handoff(); } return null; } static async stop() { this._requestInFlight = false; if (this._active) { await this._active.end(); await delay(100); } this._active = null; } private readonly _session: XRSession | null; private readonly _mode: XRSessionMode; private readonly _init: XRSessionInit; get isAR() { return this._mode === "immersive-ar"; } private readonly _renderer: WebGLRenderer; private readonly _camera: Camera; private readonly _scene: Scene; private constructor(mode: XRSessionMode, init: XRSessionInit, session: XRSession) { this._mode = mode; this._init = init; this._session = session; this._session.addEventListener("end", this.onEnd); this._renderer = new WebGLRenderer({ alpha: true }); this._renderer.setAnimationLoop(this.onFrame); this._renderer.xr.setSession(session); this._renderer.xr.enabled = true; this._camera = new PerspectiveCamera(); this._scene = new Scene(); this._scene.fog = new Fog(0x444444, 10, 250); this._scene.add(this._camera); this.setupScene(); } end() { if (!this._session) return Promise.resolve(); return this._session.end(); } /** returns the session and session info and stops the temporary rendering */ async handoff() { if (!this._session) throw new Error("Cannot handoff a session that has already ended"); const info: SessionInfo = { session: this._session, mode: this._mode, init: this._init }; await this.onBeforeHandoff(); // calling onEnd here directly because we dont end the session this.onEnd(); // set the session to null because we dont want this class to accidentaly end the session //@ts-ignore this._session = null; return info; } private onEnd = () => { this._session?.removeEventListener("end", this.onEnd); this._renderer.setAnimationLoop(null); this._renderer.dispose(); this._scene.clear(); } private _lastTime = 0; private onFrame = (time: DOMHighResTimeStamp, _frame: XRFrame) => { const dt = time - this._lastTime; this.update(time, dt); if (this._camera.parent !== this._scene) { this._scene.add(this._camera); } this._renderer.render(this._scene, this._camera); } /** can be used to prepare the user or fade to black */ private async onBeforeHandoff() { // for(const sphere of this._spheres) { // sphere.removeFromParent(); // await delay(10); // } // const obj = ObjectUtils.createPrimitive(PrimitiveType.Cube); // obj.position.z = -3; // obj.position.y = .5; // this._scene.add(obj); await delay(1000); this._scene.clear(); // await delay(100); } private _objects: Mesh[] = []; private setupScene() { this._scene.background = new Color(0x000000); this._scene.add(new GridHelper(5, 10, 0x111111, 0x111111)); const light = new DirectionalLight(0xffffff, 1); light.position.set(0, 20, 0); light.castShadow = false; this._scene.add(light); const light2 = new DirectionalLight(0xffffff, 1); light2.position.set(0, -1, 0); light2.castShadow = false; this._scene.add(light2); const light3 = new PointLight(0xffffff, 1, 100, 1); light3.position.set(0, 2, 0); light3.castShadow = false; light3.distance = 200; this._scene.add(light3); const range = 50; for (let i = 0; i < 100; i++) { const material = new MeshStandardMaterial({ color: 0x222222, metalness: 1, roughness: .8, }); // if we're in passthrough if (this.isAR) { material.emissive = new Color(Math.random(), Math.random(), Math.random()); material.emissiveIntensity = Math.random(); } const type = Mathf.random(0, 1) > .5 ? PrimitiveType.Sphere : PrimitiveType.Cube; const obj = ObjectUtils.createPrimitive(type, { material }); obj.position.x = Mathf.random(-range, range); obj.position.y = Mathf.random(-2, range); obj.position.z = Mathf.random(-range, range); // random rotation obj.rotation.x = Mathf.random(0, Math.PI * 2); obj.rotation.y = Mathf.random(0, Math.PI * 2); obj.rotation.z = Mathf.random(0, Math.PI * 2); obj.scale.multiplyScalar(.5 + Math.random() * 10); const dist = obj.position.distanceTo(this._camera.position) - obj.scale.x; if (dist < 1) { obj.position.multiplyScalar(1 + 1 / dist); } this._objects.push(obj); this._scene.add(obj); } } private update(time: number, _deltaTime: number) { const speed = time * .0004; for (let i = 0; i < this._objects.length; i++) { const obj = this._objects[i]; obj.position.y += Math.sin(speed + i * .5) * 0.005; obj.rotateY(.002); } } }