UNPKG

@spearwolf/twopoint5d

Version:

Create 2.5D realtime graphics and pixelart with WebGL and three.js

383 lines 12.9 kB
var _a; import { emit, eventize, once } from '@spearwolf/eventize'; import { texture } from 'three/tsl'; import { Color, RenderTarget } from 'three/webgpu'; import { isWebGLRenderer } from '../display/isWebGLRenderer.js'; import { OnAddToParent, OnRemoveFromParent, OnStageAdded, OnStageRemoved, } from '../events.js'; import { RootRenderPipeline } from './RootRenderPipeline.js'; const hasAsPassNode = (s) => typeof s?.asPassNode === 'function'; export class StageRenderer { #parent; #clearColor; get clearColor() { return this.#clearColor; } set clearColor(color) { if (color == null) { this.#clearColor = null; } else { this.#clearColor = color; this.clear = true; } } setClearColor(color, alpha = 1) { this.#clearColor = color; this.clearAlpha = alpha; this.clear = true; return this; } #oldClearColor; #renderOrder; #orderedStages; set renderOrder(order) { order = order || '*'; if (this.#renderOrder !== order) { this.#renderOrder = order; this.#renderOrderArray = undefined; this.#orderedStages = undefined; this.#outputDirty = true; this.onRenderOrderChanged(); } } onRenderOrderChanged() { } get renderOrder() { return this.#renderOrder; } #renderOrderArray; get renderOrderArray() { if (!this.#renderOrderArray) { this.#renderOrderArray = this.renderOrder .split(',') .map((item) => item.trim()) .filter(Boolean); } return this.#renderOrderArray; } get parent() { return this.#parent; } set parent(parent) { if (this.#parent !== parent) { this.#removeFromParent(); this.#parent = parent; if (this.#parent) { this.#addToParent(); } } } #removeFromParent() { if (this.#parent == null) return; emit(this, OnRemoveFromParent); if (this.#parent instanceof _a) { this.#parent.remove(this); } } #addToParent() { if (this.#parent instanceof _a) { this.#parent.add(this); } else { this.#addToHost(this.#parent); } emit(this, OnAddToParent); } #addToHost(host) { once(this, OnRemoveFromParent, host.onResize(({ width, height }) => { this.resize(width, height); })); once(this, OnRemoveFromParent, host.onRenderFrame(({ renderer, now, deltaTime, frameNo }) => { this.updateFrame(now, deltaTime, frameNo); this.renderTo(renderer); })); } constructor(parent) { this.name = 'StageRenderer'; this.width = 0; this.height = 0; this.clear = false; this.#clearColor = null; this.clearAlpha = 1; this.clearColorBuffer = true; this.clearDepthBuffer = true; this.clearStencilBuffer = true; this.#oldClearColor = new Color(0x000000); this.stages = []; this.#renderOrder = '*'; this.#renderOrderArray = []; this.#outputDirty = true; eventize(this); if (parent) { this.parent = parent; } } attach(parent) { this.parent = parent; return this; } detach() { this.parent = undefined; return this; } resize(width, height) { if (this.width === width && this.height === height) return; this.width = width; this.height = height; if (this.#internalRT) this.#internalRT.setSize(Math.max(1, width), Math.max(1, height)); if (this.#asPassNodeRT) this.#asPassNodeRT.setSize(Math.max(1, width), Math.max(1, height)); for (const stage of this.stages) { this.resizeStage(stage, width, height); } } resizeStage(stageItem, width, height) { if (stageItem.width !== width || stageItem.height !== height) { stageItem.width = width; stageItem.height = height; stageItem.stage.resize(width, height); } } updateFrame(now, deltaTime, frameNo) { for (const { stage } of this.orderedStages) { stage.updateFrame(now, deltaTime, frameNo); } } #internalRT; #asPassNodeRT; #outputDirty; invalidateOutputNode() { this.#outputDirty = true; } renderTo(renderer) { if (isWebGLRenderer(renderer)) { throw new TypeError('The WebGLRenderer renderer is not supported anymore'); } if (this.outputRenderTarget) { const prev = renderer.getRenderTarget(); renderer.setRenderTarget(this.outputRenderTarget); try { this.#renderToCurrentTarget(renderer); } finally { renderer.setRenderTarget(prev); } } else { this.#renderToCurrentTarget(renderer); } } #renderToCurrentTarget(renderer) { if (this.pipeline) { if (this.buildOutputNode || this.pipeline instanceof RootRenderPipeline) { this.#renderPipelineComposed(renderer); } else { this.#renderPipelineSimple(renderer); } } else { this.#renderStagesInline(renderer); } } #renderStagesInline(renderer) { const wasPreviouslyAutoClear = renderer.autoClear; if (this.clear) this.#applyClear(renderer); renderer.autoClear = false; for (const stageItem of this.orderedStages) { this.renderStage(stageItem, renderer); } renderer.autoClear = wasPreviouslyAutoClear; } #renderPipelineSimple(renderer) { const rt = this.#ensureInternalRT(renderer); const prev = renderer.getRenderTarget(); renderer.setRenderTarget(rt); try { this.#clearForInternalRT(renderer); const wasPreviouslyAutoClear = renderer.autoClear; renderer.autoClear = false; for (const stageItem of this.orderedStages) this.renderStage(stageItem, renderer); renderer.autoClear = wasPreviouslyAutoClear; } finally { renderer.setRenderTarget(prev); } if (this.#outputDirty) { this.pipeline.outputNode = texture(rt.texture); this.pipeline.needsUpdate = true; this.#outputDirty = false; } if (this.clear) this.#applyClear(renderer); this.pipeline.render(); } #clearForInternalRT(renderer) { if (this.clear) { this.#applyClear(renderer); } else { const oldClearAlpha = renderer.getClearAlpha(); renderer.setClearAlpha(0); renderer.clear(true, true, false); renderer.setClearAlpha(oldClearAlpha); } } #renderPipelineComposed(renderer) { for (const stageItem of this.orderedStages) { const stage = stageItem.stage; if (stage instanceof _a) { const childRT = stage.#ensureAsPassNodeRT(renderer); const prev = renderer.getRenderTarget(); renderer.setRenderTarget(childRT); try { stage.#renderToCurrentTarget(renderer); } finally { renderer.setRenderTarget(prev); } } } if (this.#outputDirty) { const passes = this.orderedStages.map((s) => this.#getStagePass(s, renderer)); const compose = this.buildOutputNode ?? RootRenderPipeline.buildOutputNode; this.pipeline.outputNode = compose(passes); this.pipeline.needsUpdate = true; this.#outputDirty = false; } if (this.clear) this.#applyClear(renderer); this.pipeline.render(); } #getStagePass(stageItem, renderer) { const stage = stageItem.stage; if (!hasAsPassNode(stage)) { throw new TypeError(`StageRenderer.buildOutputNode: stage ${JSON.stringify(stage.name)} does not implement asPassNode() — incompatible with the buildOutputNode composition path`); } return stage.asPassNode(renderer); } asPassNode(renderer) { const rt = this.#ensureAsPassNodeRT(renderer); return texture(rt.texture); } #ensureInternalRT(renderer) { return (this.#internalRT = this.#ensureRT(this.#internalRT, renderer)); } #ensureAsPassNodeRT(renderer) { return (this.#asPassNodeRT = this.#ensureRT(this.#asPassNodeRT, renderer)); } #ensureRT(rt, renderer) { const pixelRatio = renderer.getPixelRatio?.() ?? 1; const w = Math.max(1, Math.floor(this.width * pixelRatio)); const h = Math.max(1, Math.floor(this.height * pixelRatio)); if (!rt) { return new RenderTarget(w, h); } if (rt.width !== w || rt.height !== h) { rt.setSize(w, h); } return rt; } #applyClear(renderer) { const oldClearAlpha = renderer.getClearAlpha(); let colorWasOverridden = false; if (this.#clearColor != null) { renderer.getClearColor(this.#oldClearColor); renderer.setClearColor(this.#clearColor, this.clearAlpha); colorWasOverridden = true; } else { renderer.setClearAlpha(this.clearAlpha); } renderer.clear(this.clearColorBuffer, this.clearDepthBuffer, this.clearStencilBuffer); if (colorWasOverridden) { renderer.setClearColor(this.#oldClearColor, oldClearAlpha); } else { renderer.setClearAlpha(oldClearAlpha); } } dispose() { this.#internalRT?.dispose(); this.#internalRT = undefined; this.#asPassNodeRT?.dispose(); this.#asPassNodeRT = undefined; this.pipeline?.dispose(); this.pipeline = undefined; } renderStage(stageItem, renderer) { stageItem.stage.renderTo(renderer); } get orderedStages() { if (this.#orderedStages) return this.#orderedStages; const renderOrder = this.renderOrderArray; if (renderOrder.length === 0 || (renderOrder.length === 1 && (renderOrder[0] === '' || renderOrder[0] === '*'))) { return this.stages; } const explicitlyNamedStages = new Map(); const otherStages = this.stages.slice(); renderOrder.forEach((name) => { if (name !== '*') { const index = otherStages.findIndex((stage) => stage.stage.name === name); if (index !== -1) { const stage = otherStages.splice(index, 1)[0]; explicitlyNamedStages.set(name, stage); } } }); const orderedStages = renderOrder .map((name) => { if (name === '*') { return otherStages; } return explicitlyNamedStages.get(name); }) .flat() .filter(Boolean); explicitlyNamedStages.clear(); this.#orderedStages = orderedStages; return orderedStages; } #getIndex(stage) { return this.stages.findIndex((item) => item.stage === stage); } hasStage(stage) { return this.#getIndex(stage) !== -1; } add(stage) { if (!this.hasStage(stage)) { if (this.#renderOrder !== '*' && this.stages.some((item) => item.stage.name === stage.name)) { console.warn(`StageRenderer: a stage named ${JSON.stringify(stage.name)} is already present and renderOrder=${JSON.stringify(this.#renderOrder)} cannot disambiguate. Set unique names on your stages.`); } const si = { stage, width: 0, height: 0, }; this.stages.push(si); this.#orderedStages = undefined; this.#outputDirty = true; this.resizeStage(si, this.width, this.height); emit(this, OnStageAdded, { stage, renderer: this }); } return this; } remove(stage) { const index = this.#getIndex(stage); if (index !== -1) { this.stages.splice(index, 1); this.#orderedStages = undefined; this.#outputDirty = true; emit(this, OnStageRemoved, { stage, renderer: this }); } return this; } } _a = StageRenderer; //# sourceMappingURL=StageRenderer.js.map