@itwin/core-frontend
Version:
iTwin.js frontend components
1,012 lines • 59.1 kB
JavaScript
"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