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.

408 lines • 16.7 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 { Matrix4 } from "three"; import * as ThreeMeshUI from 'three-mesh-ui'; import { Mathf } from "../../engine/engine_math.js"; import { serializable } from "../../engine/engine_serialization_decorator.js"; import { FrameEvent } from "../../engine/engine_setup.js"; import { delayForFrames, getParam } from "../../engine/engine_utils.js"; import { Camera } from "../Camera.js"; import { GameObject } from "../Component.js"; import { BaseUIComponent, UIRootComponent } from "./BaseUIComponent.js"; import { EventSystem } from "./EventSystem.js"; import { LayoutGroup } from "./Layout.js"; import { RectTransform } from "./RectTransform.js"; import { updateRenderSettings as updateRenderSettingsRecursive } from "./Utils.js"; export var RenderMode; (function (RenderMode) { RenderMode[RenderMode["ScreenSpaceOverlay"] = 0] = "ScreenSpaceOverlay"; RenderMode[RenderMode["ScreenSpaceCamera"] = 1] = "ScreenSpaceCamera"; RenderMode[RenderMode["WorldSpace"] = 2] = "WorldSpace"; RenderMode[RenderMode["Undefined"] = -1] = "Undefined"; })(RenderMode || (RenderMode = {})); const debugLayout = getParam("debuguilayout"); /** * @category User Interface * @group Components */ export class Canvas extends UIRootComponent { get isCanvas() { return true; } get screenspace() { return this.renderMode !== RenderMode.WorldSpace; } set renderOnTop(val) { if (val === this._renderOnTop) { return; } this._renderOnTop = val; this.onRenderSettingsChanged(); } get renderOnTop() { if (this._renderOnTop !== undefined) return this._renderOnTop; if (this.screenspace) { // Render ScreenSpaceOverlay always on top if (this._renderMode === RenderMode.ScreenSpaceOverlay) return true; } return false; } _renderOnTop; set depthWrite(val) { if (this._depthWrite === val) return; this._depthWrite = val; this.onRenderSettingsChanged(); } get depthWrite() { return this._depthWrite; } _depthWrite = false; set doubleSided(val) { if (this._doubleSided === val) return; this._doubleSided = val; this.onRenderSettingsChanged(); } get doubleSided() { return this._doubleSided; } _doubleSided = true; set castShadows(val) { if (this._castShadows === val) return; this._castShadows = val; this.onRenderSettingsChanged(); } get castShadows() { return this._castShadows; } _castShadows = false; set receiveShadows(val) { if (this._receiveShadows === val) return; this._receiveShadows = val; this.onRenderSettingsChanged(); } get receiveShadows() { return this._receiveShadows; } _receiveShadows = false; get renderMode() { return this._renderMode; } set renderMode(val) { if (this._renderMode === val) return; this._renderMode = val; this.onRenderSettingsChanged(); } _renderMode = RenderMode.Undefined; _rootCanvas; set rootCanvas(val) { if (this._rootCanvas instanceof Canvas) return; this._rootCanvas = val; } get rootCanvas() { return this._rootCanvas; } _scaleFactor = 1; get scaleFactor() { return this._scaleFactor; } set scaleFactor(val) { this._scaleFactor = val; } worldCamera; planeDistance = -1; awake() { //@ts-ignore this.shadowComponent = this.gameObject; this.previousParent = this.gameObject.parent; if (debugLayout) console.log("Canvas.Awake()", this.previousParent?.name + "/" + this.gameObject.name); super.awake(); } start() { this.applyRenderSettings(); } onEnable() { super.onEnable(); this._updateRenderSettingsRoutine = undefined; this._lastMatrixWorld = new Matrix4(); this.applyRenderSettings(); document.addEventListener("resize", this._boundRenderSettingsChanged); // We want to run AFTER all regular onBeforeRender callbacks this.context.pre_render_callbacks.push(this.onBeforeRenderRoutine); this.context.post_render_callbacks.push(this.onAfterRenderRoutine); } onDisable() { super.onDisable(); document.removeEventListener("resize", this._boundRenderSettingsChanged); // Remove callbacks const preRenderIndex = this.context.pre_render_callbacks.indexOf(this.onBeforeRenderRoutine); if (preRenderIndex !== -1) { this.context.pre_render_callbacks.splice(preRenderIndex, 1); } const postRenderIndex = this.context.post_render_callbacks.indexOf(this.onAfterRenderRoutine); if (postRenderIndex !== -1) { this.context.post_render_callbacks.splice(postRenderIndex, 1); } } _boundRenderSettingsChanged = this.onRenderSettingsChanged.bind(this); previousParent = null; _lastMatrixWorld = null; _rectTransforms = []; registerTransform(rt) { this._rectTransforms.push(rt); } unregisterTransform(rt) { const index = this._rectTransforms.indexOf(rt); if (index !== -1) { this._rectTransforms.splice(index, 1); } } _layoutGroups = new Map(); registerLayoutGroup(group) { const obj = group.gameObject; this._layoutGroups.set(obj, group); } unregisterLayoutGroup(group) { const obj = group.gameObject; this._layoutGroups.delete(obj); } _receivers = []; registerEventReceiver(receiver) { this._receivers.push(receiver); } unregisterEventReceiver(receiver) { const index = this._receivers.indexOf(receiver); if (index !== -1) { this._receivers.splice(index, 1); } } async onEnterXR(args) { // workaround for https://linear.app/needle/issue/NE-4114 if (this.screenspace) { if (args.xr.isVR || args.xr.isPassThrough) { this.gameObject.visible = false; } } else { this.gameObject.visible = false; await delayForFrames(1).then(() => { this.gameObject.visible = true; }); } } onLeaveXR(args) { if (this.screenspace) { if (args.xr.isVR || args.xr.isPassThrough) { this.gameObject.visible = true; } } } onBeforeRenderRoutine = () => { this.previousParent = this.gameObject.parent; if ((this.context.xr?.isVR || this.context.xr?.isPassThrough) && this.screenspace) { // see https://linear.app/needle/issue/NE-4114 this.gameObject.visible = false; this.gameObject.removeFromParent(); return; } // console.log(this.previousParent?.name + "/" + this.gameObject.name); if (this.renderOnTop || this.screenspace) { // TODO: Ideally all screenspace canvases should be combined in one render pass this.gameObject.removeFromParent(); } else { this.onUpdateRenderMode(); this.handleLayoutUpdates(); // TODO: we might need to optimize this. This is here to make sure the TMUI text clipping matrices are correct. Ideally the text does use onBeforeRender and apply the clipping matrix there so we dont have to force update all the matrices here this.shadowComponent?.updateMatrixWorld(true); this.shadowComponent?.updateWorldMatrix(true, true); this.invokeBeforeRenderEvents(); EventSystem.ensureUpdateMeshUI(ThreeMeshUI, this.context); } }; onAfterRenderRoutine = () => { if ((this.context.xr?.isVR || this.context.xr?.isPassThrough) && this.screenspace) { this.previousParent?.add(this.gameObject); // this is currently causing an error during XR (https://linear.app/needle/issue/NE-4114) // this.gameObject.visible = true; return; } if ((this.screenspace || this.renderOnTop) && this.previousParent && this.context.mainCamera) { if (this.screenspace) { const camObj = this.context.mainCamera; camObj?.add(this.gameObject); } else { this.previousParent.add(this.gameObject); } const prevAutoClearDepth = this.context.renderer.autoClear; const prevAutoClearColor = this.context.renderer.autoClearColor; this.context.renderer.autoClear = false; this.context.renderer.autoClearColor = false; this.context.renderer.clearDepth(); this.onUpdateRenderMode(true); this.handleLayoutUpdates(); this.shadowComponent?.updateMatrixWorld(true); // this.handleLayoutUpdates(); this.invokeBeforeRenderEvents(); EventSystem.ensureUpdateMeshUI(ThreeMeshUI, this.context, true); this.context.renderer.render(this.gameObject, this.context.mainCamera); this.context.renderer.autoClear = prevAutoClearDepth; this.context.renderer.autoClearColor = prevAutoClearColor; this.previousParent.add(this.gameObject); } this._lastMatrixWorld?.copy(this.gameObject.matrixWorld); }; invokeBeforeRenderEvents() { for (const receiver of this._receivers) { receiver.onBeforeCanvasRender?.(this); } } handleLayoutUpdates() { if (this._lastMatrixWorld === null) { this._lastMatrixWorld = new Matrix4(); } const matrixWorldChanged = !this._lastMatrixWorld.equals(this.gameObject.matrixWorld); if (debugLayout && matrixWorldChanged) console.log("Canvas Layout changed", this.context.time.frameCount, this.name); // TODO: optimize this, we should only need to update a subhierarchy of the parts where layout has changed const didLog = false; for (const ch of this._rectTransforms) { if (matrixWorldChanged) ch.markDirty(); let layout = this._layoutGroups.get(ch.gameObject); if (ch.isDirty && !layout) { layout = ch.gameObject.getComponentInParent(LayoutGroup); } if (ch.isDirty || layout?.isDirty) { if (debugLayout && !didLog) { console.log("CANVAS UPDATE ### " + ch.name + " ##################################### " + this.context.time.frame); // didLog = true; } layout?.updateLayout(); ch.updateTransform(); } } } applyRenderSettings() { this.onRenderSettingsChanged(); } _updateRenderSettingsRoutine; onRenderSettingsChanged() { if (this._updateRenderSettingsRoutine) return; this._updateRenderSettingsRoutine = this.startCoroutine(this._updateRenderSettingsDelayed(), FrameEvent.OnBeforeRender); } *_updateRenderSettingsDelayed() { yield; this._updateRenderSettingsRoutine = undefined; if (this.shadowComponent) { this.onUpdateRenderMode(); // this.onWillUpdateRenderSettings(); updateRenderSettingsRecursive(this.shadowComponent, this); for (const ch of GameObject.getComponentsInChildren(this.gameObject, BaseUIComponent)) { updateRenderSettingsRecursive(ch.shadowComponent, this); } } } _activeRenderMode = -1; _lastWidth = -1; _lastHeight = -1; onUpdateRenderMode(force = false) { if (!force) { if (this._renderMode === this._activeRenderMode && this._lastWidth === this.context.domWidth && this._lastHeight === this.context.domHeight) { return; } } this._activeRenderMode = this._renderMode; let camera = this.context.mainCameraComponent; let planeDistance = 10; if (camera && camera.nearClipPlane > 0 && camera.farClipPlane > 0) { // TODO: this is a hack/workaround for event system currently only passing events to the nearest object so we move the canvas close to the nearplane planeDistance = Mathf.lerp(camera.nearClipPlane, camera.farClipPlane, .01); } if (this._renderMode === RenderMode.ScreenSpaceCamera) { if (this.worldCamera) camera = this.worldCamera; if (this.planeDistance > 0) planeDistance = this.planeDistance; } switch (this._renderMode) { case RenderMode.ScreenSpaceOverlay: case RenderMode.ScreenSpaceCamera: this._lastWidth = this.context.domWidth; this._lastHeight = this.context.domHeight; // showBalloonWarning("Screenspace Canvas is not supported yet. Please use worldspace"); if (!camera) return; // we move the plane SLIGHTLY closer to be sure not to cull the canvas const plane = planeDistance + .01; this.gameObject.position.x = 0; this.gameObject.position.y = 0; this.gameObject.position.z = -plane; this.gameObject.quaternion.identity(); const rect = this.gameObject.getComponent(RectTransform); let hasChanged = false; if (rect.sizeDelta.x !== this.context.domWidth) { hasChanged = true; } if (rect.sizeDelta.y !== this.context.domHeight) { hasChanged = true; } const vFOV = camera.fieldOfView * Math.PI / 180; const h = 2 * Math.tan(vFOV / 2) * Math.abs(plane); this.gameObject.scale.x = h / this.context.domHeight; this.gameObject.scale.y = h / this.context.domHeight; // Set scale.z, otherwise small offsets in screenspace mode have different visual results based on export scale and other settings this.gameObject.scale.z = .01; if (hasChanged) { rect.sizeDelta.x = this.context.domWidth; rect.sizeDelta.y = this.context.domHeight; rect?.markDirty(); } // this.context.scene.add(this.gameObject) // this.gameObject.scale.multiplyScalar(.01); // this.gameObject.position.set(0,0,0); break; case RenderMode.WorldSpace: this._lastWidth = -1; this._lastHeight = -1; break; } } } __decorate([ serializable() ], Canvas.prototype, "renderOnTop", null); __decorate([ serializable() ], Canvas.prototype, "depthWrite", null); __decorate([ serializable() ], Canvas.prototype, "doubleSided", null); __decorate([ serializable() ], Canvas.prototype, "castShadows", null); __decorate([ serializable() ], Canvas.prototype, "receiveShadows", null); __decorate([ serializable() ], Canvas.prototype, "renderMode", null); __decorate([ serializable(Canvas) ], Canvas.prototype, "rootCanvas", null); __decorate([ serializable() ], Canvas.prototype, "scaleFactor", null); __decorate([ serializable(Camera) ], Canvas.prototype, "worldCamera", void 0); __decorate([ serializable() ], Canvas.prototype, "planeDistance", void 0); //# sourceMappingURL=Canvas.js.map