UNPKG

@itwin/core-frontend

Version:
1,012 lines • 59.1 kB
"use strict"; /*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ /** @packageDocumentation * @module WebGL */ Object.defineProperty(exports, "__esModule", { value: true }); exports.OffScreenTarget = exports.OnScreenTarget = exports.Target = void 0; const core_bentley_1 = require("@itwin/core-bentley"); const core_geometry_1 = require("@itwin/core-geometry"); const core_common_1 = require("@itwin/core-common"); const ViewRect_1 = require("../../../common/ViewRect"); const ImageUtil_1 = require("../../../common/ImageUtil"); const Pixel_1 = require("../../../render/Pixel"); const RenderPlan_1 = require("../RenderPlan"); const RenderTarget_1 = require("../../../render/RenderTarget"); const RenderTargetDebugControl_1 = require("../RenderTargetDebugControl"); const BranchState_1 = require("./BranchState"); const CachedGeometry_1 = require("./CachedGeometry"); const ColorInfo_1 = require("./ColorInfo"); const DrawCommand_1 = require("./DrawCommand"); const FrameBuffer_1 = require("./FrameBuffer"); const GL_1 = require("./GL"); const Graphic_1 = require("./Graphic"); const IModelFrameLifecycle_1 = require("./IModelFrameLifecycle"); const PlanarClassifier_1 = require("./PlanarClassifier"); const Primitive_1 = require("./Primitive"); const RenderState_1 = require("./RenderState"); const SceneCompositor_1 = require("./SceneCompositor"); const ScratchDrawParams_1 = require("./ScratchDrawParams"); const ShaderProgram_1 = require("./ShaderProgram"); const Sync_1 = require("./Sync"); const System_1 = require("./System"); const TargetUniforms_1 = require("./TargetUniforms"); const Texture_1 = require("./Texture"); const TargetGraphics_1 = require("./TargetGraphics"); const VisibleTileFeatures_1 = require("./VisibleTileFeatures"); const FrameStatsCollector_1 = require("../FrameStatsCollector"); const AnimationNodeId_1 = require("../../../common/internal/render/AnimationNodeId"); const Symbols_1 = require("../../../common/internal/Symbols"); function swapImageByte(image, i0, i1) { const tmp = image.data[i0]; image.data[i0] = image.data[i1]; image.data[i1] = tmp; } class EmptyHiliteSet { elements; subcategories; models; isEmpty = true; modelSubCategoryMode = "union"; constructor() { this.elements = this.subcategories = this.models = new core_bentley_1.Id64.Uint32Set(); } } /** @internal */ class Target extends RenderTarget_1.RenderTarget { [Symbols_1._implementationProhibited] = undefined; graphics = new TargetGraphics_1.TargetGraphics(); _planarClassifiers; _textureDrapes; _worldDecorations; _currPickExclusions = new core_bentley_1.Id64.Uint32Set(); _swapPickExclusions = new core_bentley_1.Id64.Uint32Set(); pickExclusionsSyncTarget = { syncKey: Number.MIN_SAFE_INTEGER }; _hilites = new EmptyHiliteSet(); _hiliteSyncTarget = { syncKey: Number.MIN_SAFE_INTEGER }; _flashed = { lower: 0, upper: 0 }; _flashedId = core_bentley_1.Id64.invalid; _flashIntensity = 0; _renderCommands; _overlayRenderState; _compositor; _fbo; _dcAssigned = false; performanceMetrics; decorationsState = BranchState_1.BranchState.createForDecorations(); // Used when rendering view background and view/world overlays. uniforms = new TargetUniforms_1.TargetUniforms(this); renderRect = new ViewRect_1.ViewRect(); analysisStyle; analysisTexture; ambientOcclusionSettings = core_common_1.AmbientOcclusion.Settings.defaults; _wantAmbientOcclusion = false; _batches = []; plan = (0, RenderPlan_1.createEmptyRenderPlan)(); _animationBranches; _isReadPixelsInProgress = false; _readPixelsSelector = Pixel_1.Pixel.Selector.None; _readPixelReusableResources; _drawNonLocatable = true; _currentlyDrawingClassifier; _analysisFraction = 0; _antialiasSamples = 1; // This exists strictly to be forwarded to ScreenSpaceEffects. Do not use it for anything else. _viewport; _screenSpaceEffects = []; isFadeOutActive = false; activeVolumeClassifierTexture; activeVolumeClassifierProps; activeVolumeClassifierModelId; _currentAnimationTransformNodeId; // RenderTargetDebugControl vcSupportIntersectingVolumes = false; drawForReadPixels = false; drawingBackgroundForReadPixels = false; primitiveVisibility = RenderTargetDebugControl_1.PrimitiveVisibility.All; displayDrapeFrustum = false; displayMaskFrustum = false; displayRealityTilePreload = false; displayRealityTileRanges = false; logRealityTiles = false; displayNormalMaps = true; freezeRealityTiles = false; get shadowFrustum() { const map = this.solarShadowMap; return map.isEnabled && map.isReady ? map.frustum : undefined; } get debugControl() { return this; } get viewRect() { return this.renderRect; } constructor(rect) { super(); this._renderCommands = this.uniforms.branch.createRenderCommands(this.uniforms.batch.state); this._overlayRenderState = new RenderState_1.RenderState(); this._overlayRenderState.flags.depthMask = false; this._overlayRenderState.flags.blend = true; this._overlayRenderState.blend.setBlendFunc(GL_1.GL.BlendFactor.One, GL_1.GL.BlendFactor.OneMinusSrcAlpha); this._compositor = SceneCompositor_1.SceneCompositor.create(this); // compositor is created but not yet initialized... we are still undisposed this.renderRect = rect ? rect : new ViewRect_1.ViewRect(); // if the rect is undefined, expect that it will be updated dynamically in an OnScreenTarget if (undefined !== System_1.System.instance.antialiasSamples) this._antialiasSamples = System_1.System.instance.antialiasSamples; else this._antialiasSamples = (undefined !== System_1.System.instance.options.antialiasSamples ? System_1.System.instance.options.antialiasSamples : 1); } get compositor() { return this._compositor; } get isReadPixelsInProgress() { return this._isReadPixelsInProgress; } get readPixelsSelector() { return this._readPixelsSelector; } get drawNonLocatable() { return this._drawNonLocatable; } get techniques() { return this.renderSystem.techniques; } get hilites() { return this._hilites; } get hiliteSyncTarget() { return this._hiliteSyncTarget; } get pickExclusions() { return this._currPickExclusions; } get flashed() { return core_bentley_1.Id64.isValid(this._flashedId) ? this._flashed : undefined; } get flashedId() { return this._flashedId; } get flashIntensity() { return this._flashIntensity; } get analysisFraction() { return this._analysisFraction; } set analysisFraction(fraction) { this._analysisFraction = fraction; } get animationBranches() { return this._animationBranches; } set animationBranches(branches) { this.disposeAnimationBranches(); this._animationBranches = branches; } disposeAnimationBranches() { this._animationBranches = undefined; } get antialiasSamples() { return this._antialiasSamples; } set antialiasSamples(numSamples) { this._antialiasSamples = numSamples; } get solarShadowMap() { return this.compositor.solarShadowMap; } get isDrawingShadowMap() { return this.solarShadowMap.isEnabled && this.solarShadowMap.isDrawing; } getPlanarClassifier(id) { return undefined !== this._planarClassifiers ? this._planarClassifiers.get(id) : undefined; } createPlanarClassifier(properties) { return PlanarClassifier_1.PlanarClassifier.create(properties, this); } getTextureDrape(id) { return undefined !== this._textureDrapes ? this._textureDrapes.get(id) : undefined; } getWorldDecorations(decs) { if (undefined === this._worldDecorations) { // Don't allow flags like monochrome etc to affect world decorations. Allow lighting in 3d only. const vf = new core_common_1.ViewFlags({ renderMode: core_common_1.RenderMode.SmoothShade, clipVolume: false, whiteOnWhiteReversal: false, lighting: !this.is2d, shadows: false, }); this._worldDecorations = new Graphic_1.WorldDecorations(vf); } this._worldDecorations.init(decs); return this._worldDecorations; } get currentBranch() { return this.uniforms.branch.top; } get currentViewFlags() { return this.currentBranch.viewFlags; } get currentTransform() { return this.currentBranch.transform; } get currentTransparencyThreshold() { return this.currentEdgeSettings.transparencyThreshold; } get currentEdgeSettings() { return this.currentBranch.edgeSettings; } get currentFeatureSymbologyOverrides() { return this.currentBranch.symbologyOverrides; } get currentPlanarClassifier() { return this.currentBranch.planarClassifier; } get currentlyDrawingClassifier() { return this._currentlyDrawingClassifier; } get currentTextureDrape() { const drape = this.currentBranch.textureDrape; return undefined !== drape && drape.isReady ? drape : undefined; } get currentPlanarClassifierOrDrape() { const drape = this.currentTextureDrape; return undefined === drape ? this.currentPlanarClassifier : drape; } get currentContours() { return this.currentBranch.contourLine; } modelToView(modelPt, result) { return this.uniforms.branch.modelViewMatrix.multiplyPoint3dQuietNormalize(modelPt, result); } get is2d() { return this.uniforms.frustum.is2d; } get is3d() { return !this.is2d; } _isDisposed = false; get isDisposed() { return this.graphics.isDisposed && undefined === this._fbo && undefined === this._worldDecorations && undefined === this._planarClassifiers && undefined === this._textureDrapes && this._renderCommands.isEmpty && 0 === this._batches.length && this.uniforms.thematic.isDisposed && this._isDisposed; } allocateFbo() { if (this._fbo) return this._fbo; const rect = this.viewRect; const color = Texture_1.TextureHandle.createForAttachment(rect.width, rect.height, GL_1.GL.Texture.Format.Rgba, GL_1.GL.Texture.DataType.UnsignedByte); if (undefined === color) return undefined; const depth = System_1.System.instance.createDepthBuffer(rect.width, rect.height, 1); if (undefined === depth) { color[Symbol.dispose](); return undefined; } this._fbo = FrameBuffer_1.FrameBuffer.create([color], depth); if (undefined === this._fbo) { color[Symbol.dispose](); depth[Symbol.dispose](); return undefined; } return this._fbo; } disposeFbo() { if (!this._fbo) return; const tx = this._fbo.getColor(0); const db = this._fbo.depthBuffer; this._fbo = (0, core_bentley_1.dispose)(this._fbo); this._dcAssigned = false; // We allocated our framebuffer's color attachment, so must dispose of it too. (0, core_bentley_1.assert)(undefined !== tx); (0, core_bentley_1.dispose)(tx); // We allocated our framebuffer's depth attachment, so must dispose of it too. (0, core_bentley_1.assert)(undefined !== db); (0, core_bentley_1.dispose)(db); } [Symbol.dispose]() { this.reset(); this.disposeFbo(); (0, core_bentley_1.dispose)(this._compositor); this._viewport = undefined; this._isDisposed = true; } pushBranch(branch) { this.uniforms.branch.pushBranch(branch); } pushState(state) { this.uniforms.branch.pushState(state); } popBranch() { this.uniforms.branch.pop(); } pushViewClip() { this.uniforms.branch.pushViewClip(); } popViewClip() { this.uniforms.branch.popViewClip(); } /** @internal */ isRangeOutsideActiveVolume(range) { return this.uniforms.branch.clipStack.isRangeClipped(range, this.currentTransform); } _scratchRange = new core_geometry_1.Range3d(); /** @internal */ isGeometryOutsideActiveVolume(geom) { if (!this.uniforms.branch.clipStack.hasClip || this.uniforms.branch.clipStack.hasOutsideColor) return false; const range = geom.computeRange(this._scratchRange); return this.isRangeOutsideActiveVolume(range); } pushBatch(batch) { this.uniforms.batch.setCurrentBatch(batch, this.currentBranch); } popBatch() { this.uniforms.batch.clearCurrentBatch(); } addBatch(batch) { (0, core_bentley_1.assert)(this._batches.indexOf(batch) < 0); this._batches.push(batch); } onBatchDisposed(batch) { const index = this._batches.indexOf(batch); (0, core_bentley_1.assert)(index > -1); this._batches.splice(index, 1); } get wantAmbientOcclusion() { return this._wantAmbientOcclusion; } get wantThematicDisplay() { return this.currentViewFlags.thematicDisplay && this.is3d && undefined !== this.uniforms.thematic.thematicDisplay; } get wantAtmosphere() { return undefined !== this.plan.atmosphere; } get wantThematicSensors() { const thematic = this.plan.thematic; return this.wantThematicDisplay && undefined !== thematic && core_common_1.ThematicDisplayMode.InverseDistanceWeightedSensors === thematic.displayMode && thematic.sensorSettings.sensors.length > 0; } updateSolarShadows(context) { this.compositor.updateSolarShadows(context); } // ---- Implementation of RenderTarget interface ---- // get renderSystem() { return System_1.System.instance; } get planFraction() { return this.uniforms.frustum.planFraction; } get planFrustum() { return this.uniforms.frustum.planFrustum; } changeDecorations(decs) { this.graphics.decorations = decs; } changeScene(scene) { this.graphics.changeScene(scene); this.changeTextureDrapes(scene.textureDrapes); this.changePlanarClassifiers(scene.planarClassifiers); this.changeDrapesOrClassifiers(this._planarClassifiers, scene.planarClassifiers); this._planarClassifiers = scene.planarClassifiers; this.activeVolumeClassifierProps = scene.volumeClassifier?.classifier; this.activeVolumeClassifierModelId = scene.volumeClassifier?.modelId; } onBeforeRender(viewport, setSceneNeedRedraw) { this._viewport = viewport; IModelFrameLifecycle_1.IModelFrameLifecycle.onBeforeRender.raiseEvent({ renderSystem: this.renderSystem, viewport, setSceneNeedRedraw, }); } changeDrapesOrClassifiers(oldMap, newMap) { if (undefined === newMap) { if (undefined !== oldMap) for (const value of oldMap.values()) value[Symbol.dispose](); return; } if (undefined !== oldMap) { for (const entry of oldMap) if (newMap.get(entry[0]) !== entry[1]) entry[1][Symbol.dispose](); } } changeTextureDrapes(textureDrapes) { this.changeDrapesOrClassifiers(this._textureDrapes, textureDrapes); this._textureDrapes = textureDrapes; } changePlanarClassifiers(planarClassifiers) { this.changeDrapesOrClassifiers(this._planarClassifiers, planarClassifiers); this._planarClassifiers = planarClassifiers; } changeDynamics(foreground, overlay) { this.graphics.changeDynamics(foreground, overlay); } overrideFeatureSymbology(ovr) { this.uniforms.branch.overrideFeatureSymbology(ovr); } setHiliteSet(hilite) { this._hilites = hilite; (0, Sync_1.desync)(this._hiliteSyncTarget); } setFlashed(id, intensity) { if (id !== this._flashedId) { this._flashedId = id; this._flashed = core_bentley_1.Id64.getUint32Pair(id); } this._flashIntensity = intensity; } changeFrustum(newFrustum, newFraction, is3d) { this.uniforms.frustum.changeFrustum(newFrustum, newFraction, is3d); } changeRenderPlan(plan) { this.plan = plan; if (this._dcAssigned && plan.is3d !== this.is3d) { // changed the dimensionality of the Target. World decorations no longer valid. // (lighting is enabled or disabled based on 2d vs 3d). this._worldDecorations = (0, core_bentley_1.dispose)(this._worldDecorations); // Turn off shadows if switching from 3d to 2d if (!plan.is3d) this.updateSolarShadows(undefined); } if (plan.is3d !== this.decorationsState.is3d) this.decorationsState.changeRenderPlan(this.decorationsState.viewFlags, plan.is3d, undefined); if (!this.assignDC()) return; this.isFadeOutActive = plan.isFadeOutActive; this.analysisStyle = plan.analysisStyle; this.analysisTexture = plan.analysisTexture; this.uniforms.branch.updateViewClip(plan.clip, plan.clipStyle); let vf = plan.viewFlags; if (!plan.is3d) vf = vf.withRenderMode(core_common_1.RenderMode.Wireframe); if (core_common_1.RenderMode.SmoothShade === vf.renderMode && plan.is3d && undefined !== plan.ao && vf.ambientOcclusion) { this._wantAmbientOcclusion = true; this.ambientOcclusionSettings = plan.ao; } else { this._wantAmbientOcclusion = false; vf = vf.with("ambientOcclusion", false); } this.uniforms.branch.changeRenderPlan(vf, plan.is3d, plan.hline, plan.contours); this.changeFrustum(plan.frustum, plan.fraction, plan.is3d); this.uniforms.thematic.update(this); this.uniforms.contours.update(this); this.uniforms.atmosphere.update(this); // NB: This must be done after changeFrustum() as some of the uniforms depend on the frustum. this.uniforms.updateRenderPlan(plan); } drawFrame(sceneMilSecElapsed) { (0, core_bentley_1.assert)(this.renderSystem.frameBufferStack.isEmpty); if (!this.assignDC()) return; this.paintScene(sceneMilSecElapsed); this.drawOverlayDecorations(); (0, core_bentley_1.assert)(this.renderSystem.frameBufferStack.isEmpty); } drawOverlayDecorations() { } /** * Invoked via Viewport.changeView() when the owning Viewport is changed to look at a different view. * Invoked via dispose() when the target is being destroyed. * The primary difference is that in the former case we retain the SceneCompositor. */ reset(_realityMapLayerChanged) { this.graphics[Symbol.dispose](); this._worldDecorations = (0, core_bentley_1.dispose)(this._worldDecorations); (0, core_bentley_1.dispose)(this.uniforms.thematic); // Ensure that only necessary classifiers are removed. If the reality map layer has not changed, // removing all classifiers would result in the loss of draping effects without triggering a refresh. if (_realityMapLayerChanged) { this.changePlanarClassifiers(undefined); } else if (this._planarClassifiers) { const filteredClassifiers = new Map([...this._planarClassifiers.entries()] .filter(([key]) => key.toLowerCase().includes("maplayer"))); this.changePlanarClassifiers(filteredClassifiers.size > 0 ? filteredClassifiers : undefined); } this.changeTextureDrapes(undefined); this._renderCommands.clear(); // Clear FeatureOverrides for this Target. // This may not be strictly necessary as the Target may still be viewing some of these batches, but better to clean up and recreate // than to leave unused in memory. for (const batch of this._batches) batch.onTargetDisposed(this); this._batches = []; this.disposeAnimationBranches(); (0, ScratchDrawParams_1.freeDrawParams)(); ShaderProgram_1.ShaderProgramExecutor.freeParams(); Primitive_1.Primitive.freeParams(); } get wantInvertBlackBackground() { return false; } computeEdgeWeight(pass, baseWeight) { return this.currentEdgeSettings.getWeight(pass, this.currentViewFlags) ?? baseWeight; } computeEdgeLineCode(pass, baseCode) { return this.currentEdgeSettings.getLineCode(pass, this.currentViewFlags) ?? baseCode; } computeEdgeColor(baseColor) { const color = this.currentEdgeSettings.getColor(this.currentViewFlags); return undefined !== color ? ColorInfo_1.ColorInfo.createUniform(color) : baseColor; } beginPerfMetricFrame(sceneMilSecElapsed, readPixels = false) { if (!readPixels || (this.performanceMetrics && !this.performanceMetrics.gatherCurPerformanceMetrics)) { // only capture readPixel data if in disp-perf-test-app if (this.renderSystem.isGLTimerSupported) this.renderSystem.glTimer.beginFrame(); if (this.performanceMetrics) this.performanceMetrics.beginFrame(sceneMilSecElapsed); } } endPerfMetricFrame(readPixels = false) { if (!readPixels || (this.performanceMetrics && !this.performanceMetrics.gatherCurPerformanceMetrics)) { // only capture readPixel data if in disp-perf-test-app if (this.renderSystem.isGLTimerSupported) this.renderSystem.glTimer.endFrame(); if (undefined === this.performanceMetrics) return; this.performanceMetrics.endOperation(); // End the 'CPU Total Time' operation this.performanceMetrics.completeFrameTimings(this._fbo); } } beginPerfMetricRecord(operation, readPixels = false) { if (!readPixels || (this.performanceMetrics && !this.performanceMetrics.gatherCurPerformanceMetrics)) { // only capture readPixel data if in disp-perf-test-app if (this.renderSystem.isGLTimerSupported) this.renderSystem.glTimer.beginOperation(operation); if (this.performanceMetrics) this.performanceMetrics.beginOperation(operation); } } endPerfMetricRecord(readPixels = false) { if (!readPixels || (this.performanceMetrics && !this.performanceMetrics.gatherCurPerformanceMetrics)) { // only capture readPixel data if in disp-perf-test-app if (this.renderSystem.isGLTimerSupported) this.renderSystem.glTimer.endOperation(); if (this.performanceMetrics) this.performanceMetrics.endOperation(); } } _frameStatsCollector = new FrameStatsCollector_1.FrameStatsCollector(); get frameStatsCollector() { return this._frameStatsCollector; } assignFrameStatsCollector(collector) { this._frameStatsCollector = collector; } paintScene(sceneMilSecElapsed) { if (!this._dcAssigned) return; this._frameStatsCollector.beginTime("totalFrameTime"); this.beginPerfMetricFrame(sceneMilSecElapsed, this.drawForReadPixels); this.beginPerfMetricRecord("Begin Paint", this.drawForReadPixels); (0, core_bentley_1.assert)(undefined !== this._fbo); this._beginPaint(this._fbo); this.endPerfMetricRecord(this.drawForReadPixels); const gl = this.renderSystem.context; const rect = this.viewRect; gl.viewport(0, 0, rect.width, rect.height); // Set this to true to visualize the output of readPixels()...useful for debugging pick. if (this.drawForReadPixels) { this.beginReadPixels(Pixel_1.Pixel.Selector.Feature); this.compositor.drawForReadPixels(this._renderCommands, this.graphics.overlays, this.graphics.decorations?.worldOverlay, this.graphics.decorations?.viewOverlay); this.endReadPixels(); } else { // After the Target is first created or any time its dimensions change, SceneCompositor.preDraw() must update // the compositor's textures, framebuffers, etc. This *must* occur before any drawing occurs. // SceneCompositor.draw() checks this, but solar shadow maps, planar classifiers, and texture drapes try to draw // before then. So do it now. this.compositor.preDraw(); this._frameStatsCollector.beginTime("classifiersTime"); this.beginPerfMetricRecord("Planar Classifiers"); this.drawPlanarClassifiers(); this.endPerfMetricRecord(); this._frameStatsCollector.endTime("classifiersTime"); this._frameStatsCollector.beginTime("shadowsTime"); this.beginPerfMetricRecord("Shadow Maps"); this.drawSolarShadowMap(); this.endPerfMetricRecord(); this._frameStatsCollector.endTime("shadowsTime"); this.beginPerfMetricRecord("Texture Drapes"); this.drawTextureDrapes(); this.endPerfMetricRecord(); this.beginPerfMetricRecord("Init Commands"); this._renderCommands.initForRender(this.graphics); this.endPerfMetricRecord(); this.compositor.draw(this._renderCommands); // scene compositor gets disposed and then re-initialized... target remains undisposed this._frameStatsCollector.beginTime("overlaysTime"); this.beginPerfMetricRecord("Overlay Draws"); this.beginPerfMetricRecord("World Overlays"); this.drawPass(12 /* RenderPass.WorldOverlay */); this.endPerfMetricRecord(); this.beginPerfMetricRecord("View Overlays"); this.drawPass(13 /* RenderPass.ViewOverlay */); this.endPerfMetricRecord(); this.endPerfMetricRecord(); // End "Overlay Draws" this._frameStatsCollector.endTime("overlaysTime"); } // Apply screen-space effects. Note we do not reset this._isReadPixelsInProgress until *after* doing so, as screen-space effects only apply // during readPixels() if the effect shifts pixels from their original locations. this._frameStatsCollector.beginTime("screenspaceEffectsTime"); this.beginPerfMetricRecord("Screenspace Effects", this.drawForReadPixels); this.renderSystem.screenSpaceEffects.apply(this); this.endPerfMetricRecord(this.drawForReadPixels); this._frameStatsCollector.endTime("screenspaceEffectsTime"); // Reset the batch IDs in all batches drawn for this call. this.uniforms.batch.resetBatchState(); this.beginPerfMetricRecord("End Paint", this.drawForReadPixels); this._endPaint(); this.endPerfMetricRecord(this.drawForReadPixels); this.endPerfMetricFrame(this.drawForReadPixels); this._frameStatsCollector.endTime("totalFrameTime"); } drawPass(pass) { this.renderSystem.applyRenderState(this.getRenderState(pass)); this.techniques.execute(this, this._renderCommands.getCommands(pass), pass); } getRenderState(pass) { // the other passes are handled by SceneCompositor (0, core_bentley_1.assert)(13 /* RenderPass.ViewOverlay */ === pass || 12 /* RenderPass.WorldOverlay */ === pass); return this._overlayRenderState; } assignDC() { if (this._dcAssigned) return true; if (!this._assignDC()) return false; const rect = this.viewRect; if (rect.width < 1 || rect.height < 1) return false; this.uniforms.viewRect.update(rect.width, rect.height); this._dcAssigned = true; return true; } readPixels(rect, selector, receiver, excludeNonLocatable, excludedElements) { if (!this.assignDC()) return; // if (this.performanceMetrics && !this.performanceMetrics.gatherCurPerformanceMetrics) this.beginPerfMetricFrame(undefined, true); rect = this.cssViewRectToDeviceViewRect(rect); const gl = this.renderSystem.context; const viewRect = this.viewRect; gl.viewport(0, 0, viewRect.width, viewRect.height); // We can't reuse the previous frame's data for a variety of reasons, chief among them that some types of geometry (surfaces, translucent stuff) don't write // to the pick buffers and others we don't want - such as non-pickable decorations - do. // Render to an offscreen buffer so that we don't destroy the current color buffer. const resources = this.createOrReuseReadPixelResources(rect); if (resources === undefined) { receiver(undefined); return; } let result; this.renderSystem.frameBufferStack.execute(resources.fbo, true, false, () => { let updatedExclusions = false; if (excludedElements) { const swap = this._swapPickExclusions; swap.clear(); for (const exclusion of excludedElements) { swap.addId(exclusion); } if (!this._currPickExclusions.equals(swap)) { this._swapPickExclusions = this._currPickExclusions; this._currPickExclusions = swap; updatedExclusions = true; (0, Sync_1.desync)(this.pickExclusionsSyncTarget); } } this._drawNonLocatable = !excludeNonLocatable; result = this.readPixelsFromFbo(rect, selector); this._drawNonLocatable = true; if (updatedExclusions) { this._currPickExclusions.clear(); (0, Sync_1.desync)(this.pickExclusionsSyncTarget); } }); this.disposeOrReuseReadPixelResources(resources); receiver(result); // Reset the batch IDs in all batches drawn for this call. this.uniforms.batch.resetBatchState(); } createOrReuseReadPixelResources(rect) { if (this._readPixelReusableResources !== undefined) { // To reuse a texture, we need it to be the same size or bigger than what we need if (this._readPixelReusableResources.texture.width >= rect.width && this._readPixelReusableResources.texture.height >= rect.height) { const resources = this._readPixelReusableResources; this._readPixelReusableResources = undefined; return resources; } } // Create a new texture/fbo const texture = Texture_1.TextureHandle.createForAttachment(rect.width, rect.height, GL_1.GL.Texture.Format.Rgba, GL_1.GL.Texture.DataType.UnsignedByte); if (texture === undefined) return undefined; const fbo = FrameBuffer_1.FrameBuffer.create([texture]); if (fbo === undefined) { (0, core_bentley_1.dispose)(texture); return undefined; } return { texture, fbo }; } disposeOrReuseReadPixelResources({ texture, fbo }) { const maxReusableTextureSize = 256; const isTooBigToReuse = texture.width > maxReusableTextureSize || texture.height > maxReusableTextureSize; let reuseResources = !isTooBigToReuse; if (reuseResources && this._readPixelReusableResources !== undefined) { // Keep the biggest texture if (this._readPixelReusableResources.texture.width > texture.width && this._readPixelReusableResources.texture.height > texture.height) { reuseResources = false; // The current resources being reused are better } else { // Free memory of the current reusable resources before replacing them (0, core_bentley_1.dispose)(this._readPixelReusableResources.fbo); (0, core_bentley_1.dispose)(this._readPixelReusableResources.texture); } } if (reuseResources) { this._readPixelReusableResources = { texture, fbo }; } else { (0, core_bentley_1.dispose)(fbo); (0, core_bentley_1.dispose)(texture); } } beginReadPixels(selector, cullingFrustum) { this.beginPerfMetricRecord("Init Commands", true); this._isReadPixelsInProgress = true; this._readPixelsSelector = selector; // Temporarily turn off lighting to speed things up. // ###TODO: Disable textures *unless* they contain transparency. If we turn them off unconditionally then readPixels() will locate fully-transparent pixels, which we don't want. const vf = this.currentViewFlags.copy({ transparency: false, lighting: false, shadows: false, acsTriad: false, grid: false, monochrome: false, materials: false, ambientOcclusion: false, thematicDisplay: this.currentViewFlags.thematicDisplay && this.uniforms.thematic.wantIsoLines, }); const top = this.currentBranch; const state = new BranchState_1.BranchState({ viewFlags: vf, symbologyOverrides: top.symbologyOverrides, is3d: top.is3d, edgeSettings: top.edgeSettings, transform: core_geometry_1.Transform.createIdentity(), clipVolume: top.clipVolume, contourLine: top.contourLine, }); this.pushState(state); // Repopulate the command list, omitting non-pickable decorations and putting transparent stuff into the opaque passes. if (cullingFrustum) this._renderCommands.setCheckRange(cullingFrustum); this._renderCommands.initForReadPixels(this.graphics); this._renderCommands.clearCheckRange(); this.endPerfMetricRecord(true); } endReadPixels(preserveBatchState = false) { // Pop the BranchState pushed by beginReadPixels. this.uniforms.branch.pop(); if (!preserveBatchState) this.uniforms.batch.resetBatchState(); this._isReadPixelsInProgress = false; } _scratchTmpFrustum = new core_common_1.Frustum(); _scratchRectFrustum = new core_common_1.Frustum(); readPixelsFromFbo(rect, selector) { // Create a culling frustum based on the input rect. We can't do this if a screen-space effect is going to move pixels around. let rectFrust; if (!this.renderSystem.screenSpaceEffects.shouldApply(this)) { const viewRect = this.viewRect; const leftScale = (rect.left - viewRect.left) / (viewRect.right - viewRect.left); const rightScale = (viewRect.right - rect.right) / (viewRect.right - viewRect.left); const topScale = (rect.top - viewRect.top) / (viewRect.bottom - viewRect.top); const bottomScale = (viewRect.bottom - rect.bottom) / (viewRect.bottom - viewRect.top); const tmpFrust = this._scratchTmpFrustum; const planFrust = this.planFrustum; interpolateFrustumPoint(tmpFrust, planFrust, core_common_1.Npc._000, leftScale, core_common_1.Npc._100); interpolateFrustumPoint(tmpFrust, planFrust, core_common_1.Npc._100, rightScale, core_common_1.Npc._000); interpolateFrustumPoint(tmpFrust, planFrust, core_common_1.Npc._010, leftScale, core_common_1.Npc._110); interpolateFrustumPoint(tmpFrust, planFrust, core_common_1.Npc._110, rightScale, core_common_1.Npc._010); interpolateFrustumPoint(tmpFrust, planFrust, core_common_1.Npc._001, leftScale, core_common_1.Npc._101); interpolateFrustumPoint(tmpFrust, planFrust, core_common_1.Npc._101, rightScale, core_common_1.Npc._001); interpolateFrustumPoint(tmpFrust, planFrust, core_common_1.Npc._011, leftScale, core_common_1.Npc._111); interpolateFrustumPoint(tmpFrust, planFrust, core_common_1.Npc._111, rightScale, core_common_1.Npc._011); rectFrust = this._scratchRectFrustum; interpolateFrustumPoint(rectFrust, tmpFrust, core_common_1.Npc._000, bottomScale, core_common_1.Npc._010); interpolateFrustumPoint(rectFrust, tmpFrust, core_common_1.Npc._100, bottomScale, core_common_1.Npc._110); interpolateFrustumPoint(rectFrust, tmpFrust, core_common_1.Npc._010, topScale, core_common_1.Npc._000); interpolateFrustumPoint(rectFrust, tmpFrust, core_common_1.Npc._110, topScale, core_common_1.Npc._100); interpolateFrustumPoint(rectFrust, tmpFrust, core_common_1.Npc._001, bottomScale, core_common_1.Npc._011); interpolateFrustumPoint(rectFrust, tmpFrust, core_common_1.Npc._101, bottomScale, core_common_1.Npc._111); interpolateFrustumPoint(rectFrust, tmpFrust, core_common_1.Npc._011, topScale, core_common_1.Npc._001); interpolateFrustumPoint(rectFrust, tmpFrust, core_common_1.Npc._111, topScale, core_common_1.Npc._101); } this.beginReadPixels(selector, rectFrust); // Draw the scene this.compositor.drawForReadPixels(this._renderCommands, this.graphics.overlays, this.graphics.decorations?.worldOverlay, this.graphics.decorations?.viewOverlay); if (this.performanceMetrics && !this.performanceMetrics.gatherCurPerformanceMetrics) { // Only collect readPixels data if in disp-perf-test-app this.performanceMetrics.endOperation(); // End the 'CPU Total Time' operation if (this.performanceMetrics.gatherGlFinish && !this.renderSystem.isGLTimerSupported) { // Ensure all previously queued webgl commands are finished by reading back one pixel since gl.Finish didn't work this.performanceMetrics.beginOperation("Finish GPU Queue"); const gl = this.renderSystem.context; const bytes = new Uint8Array(4); this.renderSystem.frameBufferStack.execute(this._fbo, true, false, () => { gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, bytes); }); this.performanceMetrics.endOperation(); } } // Apply any screen-space effects that shift pixels from their original locations. this.beginPerfMetricRecord("Screenspace Effects", true); this.renderSystem.screenSpaceEffects.apply(this); this.endPerfMetricRecord(true); // End "Screenspace Effects" this.endReadPixels(true); this.beginPerfMetricRecord("Read Pixels", true); const result = this.compositor.readPixels(rect, selector); this.endPerfMetricRecord(true); if (this.performanceMetrics && !this.performanceMetrics.gatherCurPerformanceMetrics) { // Only collect readPixels data if in disp-perf-test-app if (this.renderSystem.isGLTimerSupported) this.renderSystem.glTimer.endFrame(); if (this.performanceMetrics) this.performanceMetrics.endFrame(); } return result; } queryVisibleTileFeatures(options, iModel, callback) { this.beginReadPixels(Pixel_1.Pixel.Selector.Feature); callback(new VisibleTileFeatures_1.VisibleTileFeatures(this._renderCommands, options, this, iModel)); this.endReadPixels(); } readImagePixels(out, x, y, w, h) { (0, core_bentley_1.assert)(this._fbo !== undefined); if (this._fbo === undefined) return false; const context = this.renderSystem.context; let didSucceed = true; this.renderSystem.frameBufferStack.execute(this._fbo, true, false, () => { try { context.readPixels(x, y, w, h, context.RGBA, context.UNSIGNED_BYTE, out); } catch { didSucceed = false; } }); return didSucceed; } /** Returns a new size scaled up to a maximum size while maintaining proper aspect ratio. The new size will be * curSize adjusted so that it fits fully within maxSize in one dimension, maintaining its original aspect ratio. */ static _applyAspectRatioCorrection(curSize, maxSize) { const widthRatio = maxSize.x / curSize.x; const heightRatio = maxSize.y / curSize.y; const bestRatio = Math.min(widthRatio, heightRatio); return new core_geometry_1.Point2d(curSize.x * bestRatio, curSize.y * bestRatio); } readImageBuffer(args) { if (!this.assignDC()) return undefined; // Determine and validate capture rect. const viewRect = this.renderRect; // already has device pixel ratio applied const captureRect = args?.rect ? this.cssViewRectToDeviceViewRect(args.rect) : viewRect; if (captureRect.isNull) return undefined; const topLeft = core_geometry_1.Point3d.create(captureRect.left, captureRect.top); const bottomRight = core_geometry_1.Point2d.create(captureRect.right - 1, captureRect.bottom - 1); if (!viewRect.containsPoint(topLeft) || !viewRect.containsPoint(bottomRight)) return undefined; // ViewRect origin is at top-left. GL origin is at bottom-left. const bottom = viewRect.height - captureRect.bottom; const imageData = new Uint8Array(4 * captureRect.width * captureRect.height); if (!this.readImagePixels(imageData, captureRect.left, bottom, captureRect.width, captureRect.height)) return undefined; // Alpha has already been blended. Make all pixels opaque, *except* for background pixels if background color is fully transparent. const preserveBGAlpha = 0 === this.uniforms.style.backgroundAlpha; let isEmptyImage = true; for (let i = 3; i < imageData.length; i += 4) { const a = imageData[i]; if (!preserveBGAlpha || a > 0) { imageData[i] = 0xff; isEmptyImage = false; } } // Optimization for view attachments: if image consists entirely of transparent background pixels, don't bother producing an image. let image = !isEmptyImage ? core_common_1.ImageBuffer.create(imageData, core_common_1.ImageBufferFormat.Rgba, captureRect.width) : undefined; if (!image) return undefined; // Scale image. if (args?.size && (args.size.x !== captureRect.width || args.size.y !== captureRect.height)) { if (args.size.x <= 0 || args.size.y <= 0) return undefined; let canvas = (0, ImageUtil_1.imageBufferToCanvas)(image, true); if (!canvas) return undefined; const adjustedSize = Target._applyAspectRatioCorrection({ x: captureRect.width, y: captureRect.height }, args.size); canvas = (0, ImageUtil_1.canvasToResizedCanvasWithBars)(canvas, adjustedSize, new core_geometry_1.Point2d(args.size.x - adjustedSize.x, args.size.y - adjustedSize.y), this.uniforms.style.backgroundHexString); image = (0, ImageUtil_1.canvasToImageBuffer)(canvas); if (!image) return undefined; } // Our image is upside-down by default. Flip it unless otherwise specified. if (!args?.upsideDown) { const halfHeight = Math.floor(image.height / 2); const numBytesPerRow = image.width * 4; for (let loY = 0; loY < halfHeight; loY++) { for (let x = 0; x < image.width; x++) { const hiY = (image.height - 1) - loY; const loIdx = loY * numBytesPerRow + x * 4; const hiIdx = hiY * numBytesPerRow + x * 4; swapImageByte(image, loIdx, hiIdx); swapImageByte(image, loIdx + 1, hiIdx + 1); swapImageByte(image, loIdx + 2, hiIdx + 2); swapImageByte(image, loIdx + 3, hiIdx + 3); } } } return image; } copyImageToCanvas(overlayCanvas) { const image = this.readImageBuffer(); const canvas = undefined !== image ? (0, ImageUtil_1.imageBufferToCanvas)(image, false) : undefined; const retCanvas = undefined !== canvas ? canvas : document.createElement("canvas"); if (overlayCanvas) { const ctx = retCanvas.getContext("2d"); ctx.drawImage(overlayCanvas, 0, 0); } const pixelRatio = this.devicePixelRatio; retCanvas.getContext("2d").scale(pixelRatio, pixelRatio); return retCanvas; } drawPlanarClassifiers() { if (this._planarClassifiers) { this._planarClassifiers.forEach((classifier) => { this._currentlyDrawingClassifier = classifier; this._currentlyDrawingClassifier.draw(this); this._currentlyDrawingClassifier = undefined; }); } } drawSolarShadowMap() { if (this.solarShadowMap.isEnabled) this.solarShadowMap.draw(this); } drawTextureDrapes() { if (this._textureDrapes) this._textureDrapes.forEach((drape) => drape.draw(this)); } get screenSpaceEffects() { return this._screenSpaceEffects; } set screenSpaceEffects(effects) { this._screenSpaceEffects = [...effects]; } get screenSpaceEffectContext() { (0, core_bentley_1.assert)(undefined !== this._viewport); return { viewport: this._viewport }; } get currentAnimationTransformNodeId() { return this._currentAnimationTransformNodeId; } set currentAnimationTransformNodeId(id) { (0, core_bentley_1.assert)(undefined === this._currentAnimationTransformNodeId || undefined === id); this._currentAnimationTransformNodeId = id; } /** Given GraphicBranch.animationId identifying *any* node in the scene's schedule script, return the transform node Id * that should be used to filter the branch's graphics for display, or undefined if no filtering should be applied. */ getAnimationTransformNodeId(animationNodeId) { if (undefined === this.animationBranches || undefined === this.currentAnimationTransformNodeId || undefined === animationNodeId) return undefined; return this.animationBranches.transformNodeIds.has(animationNodeId) ? animationNodeId : AnimationNodeId_1.AnimationNodeId.Untransformed; } collectStatistics(stats) { this._compositor.collectStatistics(stats); const thematicBytes = this.uniforms.thematic.bytesUsed; if (0 < thematicBytes) stats.addThematicTexture(thematicBytes); const clipBytes = this.uniforms.branch.clipStack.bytesUsed; if (clipBytes) stats.addClipVolume(clipBytes); } cssViewRectToDeviceViewRect(rect) { // NB: ViewRect constructor *floors* inputs. const ratio = this.devicePixelRatio; return new ViewRect_1.ViewRect(Math.floor(rect.left * ratio), Math.floor(rect.top * ratio), Math.floor(rect.right * ratio), Math.floor(rect.bottom * ratio)); } getRenderCommands() { return this._renderCommands.dump(); } } exports.Target = Target; class CanvasState { canvas; needsClear = false; _isWebGLCanvas; constructor(canvas) { this.canvas = canvas; this._isWebGLCanvas = this.canvas === System_1.System.instance.canvas; } // Returns true if the rect actually changed. updateDimensions(pixelRatio) { const w = Math.floor(this.canvas.clientWidth * pixelRatio); const h = Math.floor(this.canvas.clientHeight * pixelRatio); // Do not update the dimensions if not needed, or if new width or height is 0, which is invalid. // NB: the 0-dimension check indirectly resolves an issue when a viewport is dropped and immediately re-added // to the view manager. See ViewManager.test.ts for more details. 0 is also the case when vpDiv.removeChild // is done on webGLCanvas, due to client sizes being 0 afterward. if (w === this.canvas.width && h === this.canvas.height || (0 === w || 0 === h)) return false; // Must ensure internal bitmap grid dimensions of on-screen canvas match its own on-screen appearance. this.canvas.width = w; this.canvas.height = h; if (!this._isWebGLCanvas) { const ctx = this.canvas.getContext("2d"); ctx.scale(pixelRatio, pixelRatio); // apply the pixelRatio as a scale on the 2d context for drawing of decorations, etc. ctx.save(); } return true; } get width() { return this.canvas.width; } get height() { return this.canvas.height; } } /** A Target that renders to a canvas on the screen * @internal */ class OnScreenTarget extends Target { _2dCanvas; _webglCanvas; _usingWebGLCanvas = false; _blitGeom; _scratchProgParams; _scratchDrawParams; _devicePixelRatioOverride; get _curCanvas() { return this._usingWebGLCanvas ? this._webglCanvas : this._2dCanvas; } constructor(canvas) { super(); this._2dCanvas = new CanvasState(canvas); this._webglCanvas = new CanvasState(this.renderSystem.canvas); } get isDisposed() { return undefined === this._blitGeom && undefined === this._scratchProgParams && undefined === this._scratchDrawParams && super.i