UNPKG

@itwin/core-frontend

Version:
1,051 lines • 57.2 kB
/*--------------------------------------------------------------------------------------------- * 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 */ import { assert, dispose, expectDefined, expectNotNull, Id64 } from "@itwin/core-bentley"; import { Point2d, Point3d, Range3d, Transform } from "@itwin/core-geometry"; import { AmbientOcclusion, Frustum, ImageBuffer, ImageBufferFormat, Npc, RenderMode, ThematicDisplayMode, ViewFlags, } from "@itwin/core-common"; import { ViewRect } from "../../../common/ViewRect"; import { canvasToImageBuffer, canvasToResizedCanvasWithBars, imageBufferToCanvas } from "../../../common/ImageUtil"; import { Pixel } from "../../../render/Pixel"; import { createEmptyRenderPlan } from "../RenderPlan"; import { RenderTarget } from "../../../render/RenderTarget"; import { PrimitiveVisibility } from "../RenderTargetDebugControl"; import { BranchState } from "./BranchState"; import { SingleTexturedViewportQuadGeometry } from "./CachedGeometry"; import { ColorInfo } from "./ColorInfo"; import { DrawParams, ShaderProgramParams } from "./DrawCommand"; import { FrameBuffer } from "./FrameBuffer"; import { GL } from "./GL"; import { WorldDecorations } from "./Graphic"; import { IModelFrameLifecycle } from "./IModelFrameLifecycle"; import { PlanarClassifier } from "./PlanarClassifier"; import { Primitive } from "./Primitive"; import { RenderState } from "./RenderState"; import { SceneCompositor } from "./SceneCompositor"; import { freeDrawParams } from "./ScratchDrawParams"; import { ShaderProgramExecutor } from "./ShaderProgram"; import { desync } from "./Sync"; import { System } from "./System"; import { TargetUniforms } from "./TargetUniforms"; import { TextureHandle } from "./Texture"; import { TargetGraphics } from "./TargetGraphics"; import { VisibleTileFeatures } from "./VisibleTileFeatures"; import { FrameStatsCollector } from "../FrameStatsCollector"; import { AnimationNodeId } from "../../../common/internal/render/AnimationNodeId"; import { _implementationProhibited } from "../../../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 Id64.Uint32Set(); } } /** @internal */ export class Target extends RenderTarget { [_implementationProhibited] = undefined; graphics = new TargetGraphics(); _planarClassifiers; _textureDrapes; _worldDecorations; _currPickExclusions = new Id64.Uint32Set(); _swapPickExclusions = new Id64.Uint32Set(); pickExclusionsSyncTarget = { syncKey: Number.MIN_SAFE_INTEGER }; _hilites = new EmptyHiliteSet(); _hiliteSyncTarget = { syncKey: Number.MIN_SAFE_INTEGER }; _flashed = { lower: 0, upper: 0 }; _flashedId = Id64.invalid; _flashIntensity = 0; _renderCommands; _overlayRenderState; _compositor; _fbo; _dcAssigned = false; performanceMetrics; decorationsState = BranchState.createForDecorations(); // Used when rendering view background and view/world overlays. uniforms = new TargetUniforms(this); renderRect = new ViewRect(); analysisStyle; analysisTexture; ambientOcclusionSettings = AmbientOcclusion.Settings.defaults; _wantAmbientOcclusion = false; _batches = []; plan = createEmptyRenderPlan(); _animationBranches; _isReadPixelsInProgress = false; _readPixelsSelector = 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 = 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(); this._overlayRenderState.flags.depthMask = false; this._overlayRenderState.flags.blend = true; this._overlayRenderState.blend.setBlendFunc(GL.BlendFactor.One, GL.BlendFactor.OneMinusSrcAlpha); this._compositor = SceneCompositor.create(this); // compositor is created but not yet initialized... we are still undisposed this.renderRect = rect ? rect : new ViewRect(); // if the rect is undefined, expect that it will be updated dynamically in an OnScreenTarget if (undefined !== System.instance.antialiasSamples) this._antialiasSamples = System.instance.antialiasSamples; else this._antialiasSamples = (undefined !== System.instance.options.antialiasSamples ? 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 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.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 ViewFlags({ renderMode: RenderMode.SmoothShade, clipVolume: false, whiteOnWhiteReversal: false, lighting: !this.is2d, shadows: false, }); this._worldDecorations = new 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 = TextureHandle.createForAttachment(rect.width, rect.height, GL.Texture.Format.Rgba, GL.Texture.DataType.UnsignedByte); if (undefined === color) return undefined; const depth = System.instance.createDepthBuffer(rect.width, rect.height, 1); if (undefined === depth) { color[Symbol.dispose](); return undefined; } this._fbo = 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 = dispose(this._fbo); this._dcAssigned = false; // We allocated our framebuffer's color attachment, so must dispose of it too. assert(undefined !== tx); dispose(tx); // We allocated our framebuffer's depth attachment, so must dispose of it too. assert(undefined !== db); dispose(db); } [Symbol.dispose]() { this.reset(); this.disposeFbo(); 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 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) { assert(this._batches.indexOf(batch) < 0); this._batches.push(batch); } onBatchDisposed(batch) { const index = this._batches.indexOf(batch); 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 && ThematicDisplayMode.InverseDistanceWeightedSensors === thematic.displayMode && thematic.sensorSettings.sensors.length > 0; } updateSolarShadows(context) { this.compositor.updateSolarShadows(context); } // ---- Implementation of RenderTarget interface ---- // get renderSystem() { return 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.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; desync(this._hiliteSyncTarget); } setFlashed(id, intensity) { if (id !== this._flashedId) { this._flashedId = id; this._flashed = 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 = 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(RenderMode.Wireframe); if (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) { assert(this.renderSystem.frameBufferStack.isEmpty); if (!this.assignDC()) return; this.paintScene(sceneMilSecElapsed); this.drawOverlayDecorations(); 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 = dispose(this._worldDecorations); 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(); freeDrawParams(); ShaderProgramExecutor.freeParams(); 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.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(expectDefined(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(); 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); 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.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 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; desync(this.pickExclusionsSyncTarget); } } this._drawNonLocatable = !excludeNonLocatable; result = this.readPixelsFromFbo(rect, selector); this._drawNonLocatable = true; if (updatedExclusions) { this._currPickExclusions.clear(); 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 = TextureHandle.createForAttachment(rect.width, rect.height, GL.Texture.Format.Rgba, GL.Texture.DataType.UnsignedByte); if (texture === undefined) return undefined; const fbo = FrameBuffer.create([texture]); if (fbo === undefined) { 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 dispose(this._readPixelReusableResources.fbo); dispose(this._readPixelReusableResources.texture); } } if (reuseResources) { this._readPixelReusableResources = { texture, fbo }; } else { dispose(fbo); 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({ viewFlags: vf, symbologyOverrides: top.symbologyOverrides, is3d: top.is3d, edgeSettings: top.edgeSettings, transform: 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 Frustum(); _scratchRectFrustum = new 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, Npc._000, leftScale, Npc._100); interpolateFrustumPoint(tmpFrust, planFrust, Npc._100, rightScale, Npc._000); interpolateFrustumPoint(tmpFrust, planFrust, Npc._010, leftScale, Npc._110); interpolateFrustumPoint(tmpFrust, planFrust, Npc._110, rightScale, Npc._010); interpolateFrustumPoint(tmpFrust, planFrust, Npc._001, leftScale, Npc._101); interpolateFrustumPoint(tmpFrust, planFrust, Npc._101, rightScale, Npc._001); interpolateFrustumPoint(tmpFrust, planFrust, Npc._011, leftScale, Npc._111); interpolateFrustumPoint(tmpFrust, planFrust, Npc._111, rightScale, Npc._011); rectFrust = this._scratchRectFrustum; interpolateFrustumPoint(rectFrust, tmpFrust, Npc._000, bottomScale, Npc._010); interpolateFrustumPoint(rectFrust, tmpFrust, Npc._100, bottomScale, Npc._110); interpolateFrustumPoint(rectFrust, tmpFrust, Npc._010, topScale, Npc._000); interpolateFrustumPoint(rectFrust, tmpFrust, Npc._110, topScale, Npc._100); interpolateFrustumPoint(rectFrust, tmpFrust, Npc._001, bottomScale, Npc._011); interpolateFrustumPoint(rectFrust, tmpFrust, Npc._101, bottomScale, Npc._111); interpolateFrustumPoint(rectFrust, tmpFrust, Npc._011, topScale, Npc._001); interpolateFrustumPoint(rectFrust, tmpFrust, Npc._111, topScale, 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(expectDefined(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.Selector.Feature); callback(new VisibleTileFeatures(this._renderCommands, options, this, iModel)); this.endReadPixels(); } readImagePixels(out, x, y, w, h) { 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 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 = Point3d.create(captureRect.left, captureRect.top); const bottomRight = 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 ? ImageBuffer.create(imageData, 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 = imageBufferToCanvas(image, true); if (!canvas) return undefined; const adjustedSize = Target._applyAspectRatioCorrection({ x: captureRect.width, y: captureRect.height }, args.size); canvas = canvasToResizedCanvasWithBars(canvas, adjustedSize, new Point2d(args.size.x - adjustedSize.x, args.size.y - adjustedSize.y), this.uniforms.style.backgroundHexString); image = 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 ? imageBufferToCanvas(image, false) : undefined; const retCanvas = undefined !== canvas ? canvas : document.createElement("canvas"); if (overlayCanvas) { const ctx = expectNotNull(retCanvas.getContext("2d")); ctx.drawImage(overlayCanvas, 0, 0); } const pixelRatio = this.devicePixelRatio; expectNotNull(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() { assert(undefined !== this._viewport); return { viewport: this._viewport }; } get currentAnimationTransformNodeId() { return this._currentAnimationTransformNodeId; } set currentAnimationTransformNodeId(id) { 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.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(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(); } } class CanvasState { canvas; needsClear = false; _isWebGLCanvas; constructor(canvas) { this.canvas = canvas; this._isWebGLCanvas = this.canvas === 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 = expectNotNull(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 */ export 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.isDisposed; } [Symbol.dispose]() { this._blitGeom = dispose(this._blitGeom); this._scratchProgParams = undefined; this._scratchDrawParams = undefined; super[Symbol.dispose](); } collectStatistics(stats) { super.collectStatistics(stats); if (undefined !== this._blitGeom) this._blitGeom.collectStatistics(stats); } get devicePixelRatioOverride() { return this._devicePixelRatioOverride; } set devicePixelRatioOverride(ovr) { this._devicePixelRatioOverride = ovr; } get devicePixelRatio() { if (undefined !== this.devicePixelRatioOverride) return this.devicePixelRatioOverride; if (false === this.renderSystem.options.dpiAwareViewports) return 1.0; if (undefined !== this.renderSystem.options.devicePixelRatioOverride) return this.renderSystem.options.devicePixelRatioOverride; return window.devicePixelRatio || 1.0; } setViewRect(_rect, _temporary) { assert(false); } /** Internal-only function for testing. Returns true if the FBO dimensions match the canvas dimensions */ checkFboDimensions() { if (undefined !== this._fbo) { const tx = this._fbo.getColor(0); if (tx.width !== this._curCanvas.width || tx.height !== this._curCanvas.height) return false; } return true; } _assignDC() { this.disposeFbo(); const fbo = this.allocateFbo(); if (!fbo) return false; const tx = fbo.getColor(0); assert(undefined !== tx.getHandle()); this._blitGeom = SingleTexturedViewportQuadGeometry.createGeometry(expectDefined(tx.getHandle()), 19 /* Techniqu