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

579 lines • 23.9 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import { EquirectangularReflectionMapping, Euler, Frustum, Matrix4, OrthographicCamera, PerspectiveCamera, Ray, Vector3 } from "three"; import { showBalloonMessage } from "../engine/debug/index.js"; import { Gizmos } from "../engine/engine_gizmos.js"; import { serializable } from "../engine/engine_serialization_decorator.js"; import { RenderTexture } from "../engine/engine_texture.js"; import { getTempColor, getWorldPosition } from "../engine/engine_three_utils.js"; import { getParam } from "../engine/engine_utils.js"; import { RGBAColor } from "../engine/js-extensions/index.js"; import { Behaviour, GameObject } from "./Component.js"; import { OrbitControls } from "./OrbitControls.js"; /** The ClearFlags enum is used to determine how the camera clears the background */ export var ClearFlags; (function (ClearFlags) { ClearFlags[ClearFlags["None"] = 0] = "None"; /** Clear the background with a skybox */ ClearFlags[ClearFlags["Skybox"] = 1] = "Skybox"; /** Clear the background with a solid color. The alpha channel of the color determines the transparency */ ClearFlags[ClearFlags["SolidColor"] = 2] = "SolidColor"; /** Clear the background with a transparent color */ ClearFlags[ClearFlags["Uninitialized"] = 4] = "Uninitialized"; })(ClearFlags || (ClearFlags = {})); const debug = getParam("debugcam"); const debugscreenpointtoray = getParam("debugscreenpointtoray"); /** * @category Camera Controls * @group Components */ export class Camera extends Behaviour { get isCamera() { return true; } /** The camera's aspect ratio (width divided by height) if it is a perspective camera */ get aspect() { if (this._cam instanceof PerspectiveCamera) return this._cam.aspect; return (this.context.domWidth / this.context.domHeight); } set aspect(value) { if (this._cam instanceof PerspectiveCamera) { if (this._cam.aspect !== value) { this._cam.aspect = value; this._cam.updateProjectionMatrix(); } } } /** The camera's field of view in degrees if it is a perspective camera */ get fieldOfView() { if (this._cam instanceof PerspectiveCamera) { return this._cam.fov; } return this._fov; } set fieldOfView(val) { const changed = this.fieldOfView != val; this._fov = val; if (changed && this._cam) { if (this._cam instanceof PerspectiveCamera) { if (this._fov === undefined) { console.warn("Can not set undefined fov on PerspectiveCamera"); return; } this._cam.fov = this._fov; this._cam.updateProjectionMatrix(); } } } /** The camera's near clipping plane */ get nearClipPlane() { return this._nearClipPlane; } set nearClipPlane(val) { const changed = this._nearClipPlane != val; this._nearClipPlane = val; if (this._cam && (changed || this._cam.near != val)) { this._cam.near = val; this._cam.updateProjectionMatrix(); } } _nearClipPlane = 0.1; applyClippingPlane() { if (this._cam) { this._cam.near = this._nearClipPlane; this._cam.far = this._farClipPlane; this._cam.updateProjectionMatrix(); } } /** The camera's far clipping plane */ get farClipPlane() { return this._farClipPlane; } set farClipPlane(val) { const changed = this._farClipPlane != val; this._farClipPlane = val; if (this._cam && (changed || this._cam.far != val)) { this._cam.far = val; this._cam.updateProjectionMatrix(); } } _farClipPlane = 1000; /** The camera's clear flags - determines if the background is a skybox or a solid color or transparent */ get clearFlags() { return this._clearFlags; } set clearFlags(val) { if (val === this._clearFlags) return; this._clearFlags = val; this.applyClearFlagsIfIsActiveCamera(); } orthographic = false; orthographicSize = 5; ARBackgroundAlpha = 0; /** * The [`mask`](https://threejs.org/docs/#api/en/core/Layers.mask) value of the three camera object layers * If you want to just see objects on one layer (e.g. layer 2) then you can use `cullingLayer = 2` on this camera component instead */ set cullingMask(val) { this._cullingMask = val; if (this._cam) { this._cam.layers.mask = val; } } get cullingMask() { if (this._cam) return this._cam.layers.mask; return this._cullingMask; } _cullingMask = 0xffffffff; /** Set only a specific layer active to be rendered by the camera. * This is equivalent to calling `layers.set(val)` **/ set cullingLayer(val) { this.cullingMask = (1 << val | 0) >>> 0; } /** The blurriness of the background texture (when using a skybox) */ set backgroundBlurriness(val) { if (val === this._backgroundBlurriness) return; if (val === undefined) this._backgroundBlurriness = undefined; else this._backgroundBlurriness = Math.min(Math.max(val, 0), 1); this.applyClearFlagsIfIsActiveCamera(); } get backgroundBlurriness() { return this._backgroundBlurriness; } _backgroundBlurriness = undefined; /** The intensity of the background texture (when using a skybox) */ set backgroundIntensity(val) { if (val === this._backgroundIntensity) return; if (val === undefined) this._backgroundIntensity = undefined; else this._backgroundIntensity = Math.min(Math.max(val, 0), 10); this.applyClearFlagsIfIsActiveCamera(); } get backgroundIntensity() { return this._backgroundIntensity; } _backgroundIntensity = undefined; /** the rotation of the background texture (when using a skybox) */ set backgroundRotation(val) { if (val === this._backgroundRotation) return; if (val === undefined) this._backgroundRotation = undefined; else this._backgroundRotation = val; this.applyClearFlagsIfIsActiveCamera(); } get backgroundRotation() { return this._backgroundRotation; } _backgroundRotation = undefined; /** The intensity of the environment map */ set environmentIntensity(val) { this._environmentIntensity = val; } get environmentIntensity() { return this._environmentIntensity; } _environmentIntensity = undefined; /** The background color of the camera when {@link ClearFlags} are set to `SolidColor` */ get backgroundColor() { return this._backgroundColor ?? null; } set backgroundColor(val) { if (!val) return; if (!this._backgroundColor) { if (!val.clone) return; this._backgroundColor = val.clone(); } else this._backgroundColor.copy(val); // set background color to solid if provided color doesnt have any alpha channel if (val.alpha === undefined) this._backgroundColor.alpha = 1; this.applyClearFlagsIfIsActiveCamera(); } /** The texture that the camera should render to * It can be used to render to a {@link Texture} instead of the screen. */ set targetTexture(rt) { this._targetTexture = rt; } get targetTexture() { return this._targetTexture; } _targetTexture = null; _backgroundColor; _fov; _cam = null; _clearFlags = ClearFlags.SolidColor; _skybox; /** * Get the three.js camera object. This will create a camera if it does not exist yet. * @returns {PerspectiveCamera | OrthographicCamera} the three camera * @deprecated use {@link threeCamera} instead */ get cam() { return this.threeCamera; } /** * Get the three.js camera object. This will create a camera if it does not exist yet. * @returns {PerspectiveCamera | OrthographicCamera} the three camera */ get threeCamera() { if (this.activeAndEnabled) this.buildCamera(); return this._cam; } static _origin = new Vector3(); static _direction = new Vector3(); screenPointToRay(x, y, ray) { const cam = this.threeCamera; const origin = Camera._origin; origin.set(x, y, -1); this.context.input.convertScreenspaceToRaycastSpace(origin); if (debugscreenpointtoray) console.log("screenPointToRay", x.toFixed(2), y.toFixed(2), "now:", origin.x.toFixed(2), origin.y.toFixed(2), "isInXR:" + this.context.isInXR); origin.z = -1; origin.unproject(cam); const dir = Camera._direction.set(origin.x, origin.y, origin.z); const camPosition = getWorldPosition(cam); dir.sub(camPosition); dir.normalize(); if (ray) { ray.set(camPosition, dir); return ray; } else { return new Ray(camPosition.clone(), dir.clone()); } } _frustum; /** * Get a frustum - it will be created the first time this method is called and updated every frame in onBeforeRender when it exists. * You can also manually update it using the updateFrustum method. */ getFrustum() { if (!this._frustum) { this._frustum = new Frustum(); this.updateFrustum(); } return this._frustum; } /** Force frustum update - note that this also happens automatically every frame in onBeforeRender */ updateFrustum() { if (!this._frustum) this._frustum = new Frustum(); this._frustum.setFromProjectionMatrix(this.getProjectionScreenMatrix(this._projScreenMatrix, true), this.context.renderer.coordinateSystem); } /** * @returns {Matrix4} this camera's projection screen matrix. */ getProjectionScreenMatrix(target, forceUpdate) { if (forceUpdate) { this._projScreenMatrix.multiplyMatrices(this.threeCamera.projectionMatrix, this.threeCamera.matrixWorldInverse); } if (target === this._projScreenMatrix) return target; return target.copy(this._projScreenMatrix); } _projScreenMatrix = new Matrix4(); /** @internal */ awake() { if (debugscreenpointtoray) { window.addEventListener("pointerdown", evt => { const px = evt.clientX; const py = evt.clientY; console.log("touch", px.toFixed(2), py.toFixed(2)); const ray = this.screenPointToRay(px, py); const randomHex = "#" + Math.floor(Math.random() * 16777215).toString(16); Gizmos.DrawRay(ray.origin, ray.direction, randomHex, 10); }); } } /** @internal */ onEnable() { if (debug) console.log(`Camera enabled: \"${this.name}\". ClearFlags=${ClearFlags[this._clearFlags]}`, this); this.buildCamera(); if (this.tag == "MainCamera" || !this.context.mainCameraComponent) { this.context.setCurrentCamera(this); handleFreeCam(this); } this.applyClearFlagsIfIsActiveCamera({ applySkybox: true }); } /** @internal */ onDisable() { this.context.removeCamera(this); } /** @internal */ onBeforeRender() { if (this._cam) { if (this._frustum) { this.updateFrustum(); } // because the background color may be animated! if (this._clearFlags === ClearFlags.SolidColor) this.applyClearFlagsIfIsActiveCamera(); if (this._targetTexture) { if (this.context.isManagedExternally) { // TODO: rendering with r3f renderer does throw an shader error for some reason? if (!this["_warnedAboutExternalRenderer"]) { this["_warnedAboutExternalRenderer"] = true; console.warn("Rendering with external renderer is not supported yet. This may not work or throw errors. Please remove the the target texture from your camera: " + this.name, this.targetTexture); } } // TODO: optimize to not render twice if this is already the main camera. In that case we just want to blit const composer = this.context.composer; const useNormalRenderer = true; // this.context.isInXR || !composer; const renderer = useNormalRenderer ? this.context.renderer : composer; if (renderer) { // TODO: we should do this in onBeforeRender for the main camera only const mainCam = this.context.mainCameraComponent; this.applyClearFlags(); this._targetTexture.render(this.context.scene, this._cam, renderer); mainCam?.applyClearFlags(); } } } } /** * Creates a {@link PerspectiveCamera} if it does not exist yet and set the camera's properties. This is internally also called when accessing the {@link cam} property. **/ buildCamera() { if (this._cam) return; const cameraAlreadyCreated = this.gameObject["isCamera"]; // TODO: when exporting from blender we already have a camera in the children let cam = null; if (cameraAlreadyCreated) { cam = this.gameObject; cam?.layers.enableAll(); if (cam instanceof PerspectiveCamera) this._fov = cam.fov; } else cam = this.gameObject.children[0]; if (cam && cam.isCamera) { if (cam instanceof PerspectiveCamera) { if (this._fov) cam.fov = this._fov; cam.near = this._nearClipPlane; cam.far = this._farClipPlane; cam.updateProjectionMatrix(); } } else if (!this.orthographic) { cam = new PerspectiveCamera(this.fieldOfView, window.innerWidth / window.innerHeight, this._nearClipPlane, this._farClipPlane); if (this.fieldOfView) cam.fov = this.fieldOfView; this.gameObject.add(cam); } else { const factor = this.orthographicSize * 100; cam = new OrthographicCamera(window.innerWidth / -factor, window.innerWidth / factor, window.innerHeight / factor, window.innerHeight / -factor, this._nearClipPlane, this._farClipPlane); this.gameObject.add(cam); } this._cam = cam; this._cam.layers.mask = this._cullingMask; if (this.tag == "MainCamera") { this.context.setCurrentCamera(this); } } applyClearFlagsIfIsActiveCamera(opts) { if (this.context.mainCameraComponent === this) { this.applyClearFlags(opts); } } /** Apply this camera's clear flags and related settings to the renderer */ applyClearFlags(opts) { if (!this._cam) { if (debug) console.log("Camera does not exist (apply clear flags)"); return; } // restore previous fov (e.g. when user was in VR or AR and the camera's fov has changed) this.fieldOfView = this._fov; if (debug) { const msg = `Camera \"${this.name}\" clear flags: ${ClearFlags[this._clearFlags]}`; console.debug(msg); } switch (this._clearFlags) { case ClearFlags.None: return; case ClearFlags.Skybox: if (Camera.backgroundShouldBeTransparent(this.context)) { if (!this.ARBackgroundAlpha || this.ARBackgroundAlpha < 0.001) { this.context.scene.background = null; this.context.renderer.setClearColor(0x000000, 0); return; } } // apply the skybox only if it is not already set or if it's the first time (e.g. if the _skybox is not set yet) if (!this.scene.background || !this._skybox || opts?.applySkybox === true) this.applySceneSkybox(); // set background blurriness and intensity if (this._backgroundBlurriness !== undefined) this.context.scene.backgroundBlurriness = this._backgroundBlurriness; else if (debug) console.warn(`Camera \"${this.name}\" has no background blurriness`); if (this._backgroundIntensity !== undefined) this.context.scene.backgroundIntensity = this._backgroundIntensity; if (this._backgroundRotation !== undefined) this.context.scene.backgroundRotation = this._backgroundRotation; else if (debug) console.warn(`Camera \"${this.name}\" has no background intensity`); break; case ClearFlags.SolidColor: if (this._backgroundColor) { let alpha = this._backgroundColor.alpha; // when in WebXR use ar background alpha override or set to 0 if (Camera.backgroundShouldBeTransparent(this.context)) { alpha = this.ARBackgroundAlpha ?? 0; } this.context.scene.background = null; // In WebXR VR the background colorspace is wrong if (this.context.xr?.isVR) { this.context.renderer.setClearColor(getTempColor(this._backgroundColor).convertLinearToSRGB()); } else { this.context.renderer.setClearColor(this._backgroundColor, alpha); } } else { if (debug) console.warn(`Camera \"${this.name}\" has no background color`, this); } break; case ClearFlags.Uninitialized: this.context.scene.background = null; this.context.renderer.setClearColor(0x000000, 0); break; } } /** * Apply the skybox to the scene */ applySceneSkybox() { if (!this._skybox) this._skybox = new CameraSkybox(this); this._skybox.apply(); } /** Used to determine if the background should be transparent when in pass through AR * @returns true when in XR on a pass through device where the background shouldbe invisible **/ static backgroundShouldBeTransparent(context) { const session = context.renderer.xr?.getSession(); if (!session) return false; if (typeof session["_transparent"] === "boolean") { return session["_transparent"]; } const environmentBlendMode = session.environmentBlendMode; if (debug) showBalloonMessage("Environment blend mode: " + environmentBlendMode + " on " + navigator.userAgent); let transparent = environmentBlendMode === 'additive' || environmentBlendMode === 'alpha-blend'; if (context.isInAR) { if (environmentBlendMode === "opaque") { // workaround for Quest 2 returning opaque when it should be alpha-blend // check user agent if this is the Quest browser and return true if so if (navigator.userAgent?.includes("OculusBrowser")) { transparent = true; } // Mozilla WebXR Viewer else if (navigator.userAgent?.includes("Mozilla") && navigator.userAgent?.includes("Mobile WebXRViewer/v2")) { transparent = true; } } } session["_transparent"] = transparent; return transparent; } } __decorate([ serializable() ], Camera.prototype, "aspect", null); __decorate([ serializable() ], Camera.prototype, "fieldOfView", null); __decorate([ serializable() ], Camera.prototype, "nearClipPlane", null); __decorate([ serializable() ], Camera.prototype, "farClipPlane", null); __decorate([ serializable() ], Camera.prototype, "clearFlags", null); __decorate([ serializable() ], Camera.prototype, "orthographic", void 0); __decorate([ serializable() ], Camera.prototype, "orthographicSize", void 0); __decorate([ serializable() ], Camera.prototype, "ARBackgroundAlpha", void 0); __decorate([ serializable() ], Camera.prototype, "cullingMask", null); __decorate([ serializable() ], Camera.prototype, "backgroundBlurriness", null); __decorate([ serializable() ], Camera.prototype, "backgroundIntensity", null); __decorate([ serializable(Euler) ], Camera.prototype, "backgroundRotation", null); __decorate([ serializable() ], Camera.prototype, "environmentIntensity", null); __decorate([ serializable(RGBAColor) ], Camera.prototype, "backgroundColor", null); __decorate([ serializable(RenderTexture) ], Camera.prototype, "targetTexture", null); class CameraSkybox { _camera; _skybox; get context() { return this._camera?.context; } constructor(camera) { this._camera = camera; } apply() { this._skybox = this.context.lightmaps.tryGetSkybox(this._camera.sourceId); if (!this._skybox) { if (!this["_did_log_failed_to_find_skybox"]) { this["_did_log_failed_to_find_skybox"] = true; console.warn(`Camera \"${this._camera.name}\" has no skybox texture. ${this._camera.sourceId}`); } } else if (this.context.scene.background !== this._skybox) { if (debug) console.log(`Camera \"${this._camera.name}\" set skybox`, this._camera, this._skybox); this._skybox.mapping = EquirectangularReflectionMapping; this.context.scene.background = this._skybox; } } } function handleFreeCam(cam) { const isFreecam = getParam("freecam"); if (isFreecam) { if (cam.context.mainCameraComponent === cam) { GameObject.getOrAddComponent(cam.gameObject, OrbitControls); } } } //# sourceMappingURL=Camera.js.map