@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
text/typescript
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);
}
}
}