UNPKG

@itwin/core-frontend

Version:
570 lines • 28.4 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 { BeEvent, dispose } from "@itwin/core-bentley"; import { ColorDef, FrustumPlanes, RenderMode, RenderTexture, SpatialClassifierInsideDisplay, SpatialClassifierOutsideDisplay, TextureTransparency, } from "@itwin/core-common"; import { Matrix4d, Plane3dByOriginAndUnitNormal, Point3d, Range3d, Vector3d } from "@itwin/core-geometry"; import { GraphicsCollectorDrawArgs } from "../../../tile/internal"; import { RenderPlanarClassifier } from "../RenderPlanarClassifier"; import { BatchState } from "./BatchState"; import { BranchStack } from "./BranchStack"; import { Combine3TexturesGeometry, CombineTexturesGeometry, ViewportQuadGeometry } from "./CachedGeometry"; import { FrameBuffer } from "./FrameBuffer"; import { GL } from "./GL"; import { Batch, Branch } from "./Graphic"; import { PlanarTextureProjection } from "./PlanarTextureProjection"; import { RenderCommands } from "./RenderCommands"; import { RenderState } from "./RenderState"; import { getDrawParams } from "./ScratchDrawParams"; import { System } from "./System"; import { Texture, TextureHandle } from "./Texture"; export var PlanarClassifierContent; (function (PlanarClassifierContent) { PlanarClassifierContent[PlanarClassifierContent["None"] = 0] = "None"; PlanarClassifierContent[PlanarClassifierContent["MaskOnly"] = 1] = "MaskOnly"; PlanarClassifierContent[PlanarClassifierContent["ClassifierOnly"] = 2] = "ClassifierOnly"; PlanarClassifierContent[PlanarClassifierContent["ClassifierAndMask"] = 3] = "ClassifierAndMask"; })(PlanarClassifierContent || (PlanarClassifierContent = {})); function createTexture(handle) { return new Texture({ handle, ownership: "external", type: RenderTexture.Type.TileSection, transparency: TextureTransparency.Opaque, }); } function createTextureHandle(width, height, heightMult = 1.0) { return TextureHandle.createForAttachment(width, height * heightMult, GL.Texture.Format.Rgba, GL.Texture.DataType.UnsignedByte); } class ClassifierTextures { color; feature; hilite; constructor(color, feature, hilite) { this.color = color; this.feature = feature; this.hilite = hilite; } get isDisposed() { return this.color.isDisposed && this.feature.isDisposed && this.hilite.isDisposed; } [Symbol.dispose]() { dispose(this.color); dispose(this.feature); dispose(this.hilite); } collectStatistics(stats) { stats.addPlanarClassifier(this.color.bytesUsed); stats.addPlanarClassifier(this.feature.bytesUsed); stats.addPlanarClassifier(this.hilite.bytesUsed); } static create(width, height) { const hColor = createTextureHandle(width, height); const hFeature = createTextureHandle(width, height); const hHilite = createTextureHandle(width, height); if (!hColor || !hFeature || !hHilite) return undefined; const color = createTexture(hColor); const feature = createTexture(hFeature); const hilite = createTexture(hHilite); if (!color || !feature || !hilite) return undefined; return new ClassifierTextures(color, feature, hilite); } } class ClassifierFrameBuffers { textures; _hilite; _fbo; _clearGeom; constructor(textures, _hilite, _fbo, _clearGeom) { this.textures = textures; this._hilite = _hilite; this._fbo = _fbo; this._clearGeom = _clearGeom; } get isDisposed() { return this.textures.isDisposed && this._hilite.isDisposed && this._fbo.isDisposed && this._clearGeom.isDisposed; } [Symbol.dispose]() { dispose(this._fbo); dispose(this._clearGeom); dispose(this.textures); dispose(this._hilite); } draw(cmds, target) { System.instance.frameBufferStack.execute(this._fbo, true, false, () => { target.techniques.draw(getDrawParams(target, this._clearGeom)); target.techniques.execute(target, cmds, 19 /* RenderPass.PlanarClassification */); }); } drawHilite(cmds, target) { const system = System.instance; const gl = system.context; system.frameBufferStack.execute(this._hilite, true, false, () => { gl.clearColor(0, 0, 0, 0); gl.clear(GL.BufferBit.Color); target.techniques.execute(target, cmds, 10 /* RenderPass.Hilite */); }); } static create(width, height) { const textures = ClassifierTextures.create(width, height); if (undefined === textures) return undefined; const hiliteFbo = FrameBuffer.create([textures.hilite.texture]); if (undefined === hiliteFbo) return undefined; const fbo = FrameBuffer.create([textures.color.texture, textures.feature.texture]); if (undefined === fbo) return undefined; const geom = ViewportQuadGeometry.create(21 /* TechniqueId.ClearPickAndColor */); return undefined !== geom ? new this(textures, hiliteFbo, fbo, geom) : undefined; } } class SingleTextureFrameBuffer { texture; fbo; get isDisposed() { return this.texture.isDisposed && this.fbo.isDisposed; } collectStatistics(stats) { stats.addPlanarClassifier(this.texture.bytesUsed); } constructor(textureAndFbo) { this.texture = textureAndFbo.texture; this.fbo = textureAndFbo.fbo; } [Symbol.dispose]() { dispose(this.texture); dispose(this.fbo); } static createTextureAndFrameBuffer(width, height) { const hTexture = TextureHandle.createForAttachment(width, height, GL.Texture.Format.Rgba, GL.Texture.DataType.UnsignedByte); if (!hTexture) return undefined; const texture = new Texture({ type: RenderTexture.Type.TileSection, ownership: "external", handle: hTexture, transparency: TextureTransparency.Opaque }); if (!texture) return undefined; const fbo = FrameBuffer.create([texture.texture]); if (undefined === fbo) return undefined; return { texture, fbo }; } } class MaskFrameBuffer extends SingleTextureFrameBuffer { static create(width, height) { const textureFbo = SingleTextureFrameBuffer.createTextureAndFrameBuffer(width, height); return undefined === textureFbo ? undefined : new MaskFrameBuffer(textureFbo); } draw(cmds, target) { const system = System.instance; const gl = system.context; system.frameBufferStack.execute(this.fbo, true, false, () => { gl.clearColor(0, 0, 0, 0); gl.clear(GL.BufferBit.Color); target.techniques.execute(target, cmds, 19 /* RenderPass.PlanarClassification */); }); } } class CombineTexturesFrameBuffer extends SingleTextureFrameBuffer { _combineGeom; _width; _height; _heightMult; constructor(textureAndFbo, _combineGeom, _width, _height, _heightMult) { super(textureAndFbo); this._combineGeom = _combineGeom; this._width = _width; this._height = _height; this._heightMult = _heightMult; } compose(target) { const system = System.instance; const gl = system.context; system.context.viewport(0, 0, this._width, this._heightMult * this._height); system.frameBufferStack.execute(this.fbo, true, false, () => { gl.clearColor(0, 0, 0, 0); gl.clear(GL.BufferBit.Color); target.techniques.draw(getDrawParams(target, this._combineGeom)); }); } } class ClassifierCombinationBuffer extends CombineTexturesFrameBuffer { static create(width, height, classifierColor, classifierFeature) { const combineGeom = CombineTexturesGeometry.createGeometry(classifierColor.texture.getHandle(), classifierFeature.texture.getHandle()); if (undefined === combineGeom) return undefined; const textureFbo = SingleTextureFrameBuffer.createTextureAndFrameBuffer(width, 2 * height); return undefined === textureFbo ? undefined : new ClassifierCombinationBuffer(textureFbo, combineGeom, width, height, 2); } } class ClassifierAndMaskCombinationBuffer extends CombineTexturesFrameBuffer { static create(width, height, classifierColor, classifierFeature, mask) { const combineGeom = Combine3TexturesGeometry.createGeometry(classifierColor.texture.getHandle(), classifierFeature.texture.getHandle(), mask.texture.getHandle()); if (undefined === combineGeom) return undefined; const textureFbo = SingleTextureFrameBuffer.createTextureAndFrameBuffer(width, 3 * height); return undefined === textureFbo ? undefined : new ClassifierAndMaskCombinationBuffer(textureFbo, combineGeom, width, height, 3); } } const scratchPrevRenderState = new RenderState(); /** @internal */ export class PlanarClassifier extends RenderPlanarClassifier { _classifierBuffers; _maskBuffer; _classifierCombinedBuffer; _classifierAndMaskCombinedBuffer; _projectionMatrix = Matrix4d.createIdentity(); _graphics; _classifierGraphics = []; _maskGraphics = []; _frustum; _width = 0; _height = 0; _baseBatchId = 0; _anyHilited = false; _anyOpaque = false; _anyTranslucent = false; _classifier; _plane = Plane3dByOriginAndUnitNormal.create(new Point3d(0, 0, 0), new Vector3d(0, 0, 1)); // TBD -- Support other planes - default to X-Y for now. _renderState = new RenderState(); _renderCommands; _branchStack = new BranchStack(); _batchState; _planarClipMask; _classifierTreeRef; _planarClipMaskOverrides; _overridesDirty = true; _contentMode = PlanarClassifierContent.None; _removeMe; _featureSymbologySource = { onSourceDisposed: new BeEvent(), }; ; static _postProjectionMatrix = Matrix4d.createRowValues(0, 1, 0, 0, 0, 0, -1, 0, 1, 0, 0, 0, 0, 0, 0, 1); _debugFrustum; _doDebugFrustum = false; _debugFrustumGraphic = undefined; _isClassifyingPointCloud; // we will detect this the first time we draw _bgColor = ColorDef.from(0, 0, 0, 255); constructor(classifier, target) { super(); this._classifier = classifier; const flags = this._renderState.flags; flags.depthMask = flags.blend = flags.depthTest = false; this._batchState = new BatchState(this._branchStack); this._renderCommands = new RenderCommands(target, this._branchStack, this._batchState); } get textureImageCount() { return this._contentMode; } getParams(params) { params[0] = this.insideDisplay; params[1] = this.outsideDisplay; params[2] = this._contentMode; if (this._planarClipMask?.settings.invert) // If the mask sense is inverted, negate the contentMode to indicate this to the shader. params[2] = -params[2]; params[3] = (this._planarClipMask?.settings.transparency === undefined) ? -1 : this._planarClipMask.settings.transparency; } get hiliteTexture() { return undefined !== this._classifierBuffers ? this._classifierBuffers.textures.hilite : undefined; } get projectionMatrix() { return this._projectionMatrix; } // public get properties(): SpatialClassifier { return this._classifier; } get baseBatchId() { return this._baseBatchId; } get anyHilited() { return this._anyHilited; } get anyOpaque() { return this._anyOpaque; } get anyTranslucent() { return this._anyTranslucent; } get insideDisplay() { return this._classifier ? this._classifier.flags.inside : SpatialClassifierInsideDisplay.Off; } get outsideDisplay() { return this._classifier ? this._classifier.flags.outside : SpatialClassifierOutsideDisplay.On; } get isClassifyingPointCloud() { return true === this._isClassifyingPointCloud; } addGraphic(graphic) { this._graphics.push(graphic); } static create(properties, target) { return new PlanarClassifier(properties, target); } collectStatistics(stats) { if (undefined !== this._classifierBuffers) this._classifierBuffers.textures.collectStatistics(stats); if (undefined !== this._maskBuffer) this._maskBuffer.collectStatistics(stats); if (undefined !== this._classifierCombinedBuffer) this._classifierCombinedBuffer.collectStatistics(stats); if (undefined !== this._classifierAndMaskCombinedBuffer) this._classifierAndMaskCombinedBuffer.collectStatistics(stats); } get isDisposed() { return undefined === this._classifierBuffers; } [Symbol.dispose]() { this._classifierBuffers = dispose(this._classifierBuffers); this._maskBuffer = dispose(this._maskBuffer); this._classifierCombinedBuffer = dispose(this._classifierCombinedBuffer); this._classifierAndMaskCombinedBuffer = dispose(this._classifierAndMaskCombinedBuffer); if (this._removeMe) { this._removeMe(); this._removeMe = undefined; } this._featureSymbologySource.onSourceDisposed.raiseEvent(); } get texture() { switch (this._contentMode) { case PlanarClassifierContent.None: return undefined; case PlanarClassifierContent.ClassifierOnly: return this._classifierCombinedBuffer?.texture; case PlanarClassifierContent.MaskOnly: return this._maskBuffer?.texture; case PlanarClassifierContent.ClassifierAndMask: return this._classifierAndMaskCombinedBuffer?.texture; } } getOrCreateClassifierTexture() { if (undefined === this._classifierBuffers) this._classifierBuffers = ClassifierFrameBuffers.create(this._width, this._height); if (undefined !== this._classifierBuffers && undefined === this._classifierCombinedBuffer) this._classifierCombinedBuffer = ClassifierCombinationBuffer.create(this._width, this._height, this._classifierBuffers.textures.color, this._classifierBuffers.textures.feature); return this._classifierCombinedBuffer?.texture; } pushBatches(batchState, graphics) { graphics.forEach((graphic) => { if (graphic instanceof Batch) { batchState.push(graphic, true); batchState.pop(); } else if (graphic instanceof Branch) { this.pushBatches(batchState, graphic.branch.entries); } }); } get sourceTransparency() { return this._classifierTreeRef?.transparency; } pushBatchState(batchState) { this._baseBatchId = batchState.nextBatchId - 1; if (undefined !== this._classifierGraphics) this.pushBatches(batchState, this._classifierGraphics); } setSource(classifierTreeRef, planarClipMask) { this._classifierTreeRef = classifierTreeRef; this._classifier = classifierTreeRef?.activeClassifier; this._planarClipMask = planarClipMask; } collectGraphics(context, target) { this._classifierGraphics.length = this._maskGraphics.length = 0; if (undefined === context.viewingSpace) return; const viewState = context.viewingSpace.view; if (!viewState.isSpatialView()) return; this._doDebugFrustum = context.target.debugControl?.displayMaskFrustum ?? false; const maxTextureSize = System.instance.maxTexSizeAllow; const requiredHeight = maxTextureSize; const requiredWidth = maxTextureSize; if (requiredWidth !== this._width || requiredHeight !== this._height) this[Symbol.dispose](); this._width = requiredWidth; this._height = requiredHeight; const maskRange = Range3d.createNull(); const maskTrees = this._planarClipMask?.getTileTrees(context, target.modelId, maskRange); if (!maskTrees && !this._classifierTreeRef) return; const allTrees = maskTrees ? maskTrees.slice() : new Array(); if (this._classifierTreeRef) allTrees.push(this._classifierTreeRef); const projection = PlanarTextureProjection.computePlanarTextureProjection(this._plane, context, target, allTrees, viewState, this._width, this._height, maskRange); if (!projection.textureFrustum || !projection.projectionMatrix || !projection.worldToViewMap) return; this._projectionMatrix = projection.projectionMatrix; this._frustum = projection.textureFrustum; this._debugFrustum = projection.debugFrustum; if (this._overridesDirty) { this._overridesDirty = false; this._planarClipMaskOverrides = this._planarClipMask?.getPlanarClipMaskSymbologyOverrides(context, this._featureSymbologySource); } if (!this._planarClipMask?.usingViewportOverrides && this._removeMe) { this._removeMe(); this._removeMe = undefined; } else if (this._planarClipMask?.usingViewportOverrides && !this._removeMe) { this._removeMe = context.viewport.onFeatureOverridesChanged.addListener(() => { this._overridesDirty = true; context.viewport.requestRedraw(); }); } const drawTree = (treeRef, graphics) => { this._graphics = graphics; const frustumPlanes = this._frustum ? FrustumPlanes.fromFrustum(this._frustum) : FrustumPlanes.createEmpty(); const drawArgs = GraphicsCollectorDrawArgs.create(context, this, treeRef, frustumPlanes, projection.worldToViewMap); if (undefined !== drawArgs) treeRef.draw(drawArgs); this._graphics = undefined; }; if (this._classifierTreeRef) drawTree(this._classifierTreeRef, this._classifierGraphics); if (maskTrees) maskTrees.forEach((maskTree) => drawTree(maskTree, this._maskGraphics)); // Shader behaves slightly differently when classifying surfaces vs point clouds. this._isClassifyingPointCloud = target.isPointCloud; if (this._doDebugFrustum) { this._debugFrustumGraphic = dispose(this._debugFrustumGraphic); const builder = context.createSceneGraphicBuilder(); builder.setSymbology(ColorDef.green, ColorDef.green, 2); builder.addFrustum(context.viewingSpace.getFrustum()); builder.setSymbology(ColorDef.red, ColorDef.red, 2); builder.addFrustum(this._debugFrustum); builder.setSymbology(ColorDef.blue, ColorDef.blue, 2); builder.addFrustum(this._frustum); builder.setSymbology(ColorDef.from(0, 200, 0, 222), ColorDef.from(0, 200, 0, 222), 2); builder.addFrustumSides(context.viewingSpace.getFrustum()); builder.setSymbology(ColorDef.from(200, 0, 0, 222), ColorDef.from(200, 0, 0, 222), 2); builder.addFrustumSides(this._debugFrustum); builder.setSymbology(ColorDef.from(0, 0, 200, 222), ColorDef.from(0, 0, 200, 222), 2); builder.addFrustumSides(this._frustum); this._debugFrustumGraphic = builder.finish(); context.outputGraphic(this._debugFrustumGraphic); } } draw(target) { if (undefined === this._frustum) return; this._contentMode = PlanarClassifierContent.None; let combinationBuffer; if (this._classifierGraphics.length === 0) { if (this._maskGraphics.length === 0) { return; } else { if (undefined === this._maskBuffer) { this._maskBuffer = MaskFrameBuffer.create(this._width, this._height); if (undefined === this._maskBuffer) return; } this._contentMode = PlanarClassifierContent.MaskOnly; } } else { if (undefined === this._classifierBuffers) { this._classifierBuffers = ClassifierFrameBuffers.create(this._width, this._height); if (undefined === this._classifierBuffers) return; } if (this._maskGraphics.length === 0) { if (undefined === this._classifierCombinedBuffer) { combinationBuffer = this._classifierCombinedBuffer = ClassifierCombinationBuffer.create(this._width, this._height, this._classifierBuffers.textures.color, this._classifierBuffers.textures.feature); if (undefined === this._classifierCombinedBuffer) return; } this._contentMode = PlanarClassifierContent.ClassifierOnly; combinationBuffer = this._classifierCombinedBuffer; } else { if (undefined === this._maskBuffer) { this._maskBuffer = MaskFrameBuffer.create(this._width, this._height); if (undefined === this._maskBuffer) return; } if (undefined === this._classifierAndMaskCombinedBuffer) { combinationBuffer = this._classifierAndMaskCombinedBuffer = ClassifierAndMaskCombinationBuffer.create(this._width, this._height, this._classifierBuffers.textures.color, this._classifierBuffers.textures.feature, this._maskBuffer.texture); if (undefined === this._classifierAndMaskCombinedBuffer) return; } combinationBuffer = this._classifierAndMaskCombinedBuffer; this._contentMode = PlanarClassifierContent.ClassifierAndMask; } } // Temporarily override the Target's state. const system = System.instance; const maskViewFlags = { renderMode: RenderMode.SmoothShade, wiremesh: false, transparency: !this.isClassifyingPointCloud, // point clouds don't support transparency. textures: false, lighting: false, shadows: false, monochrome: false, materials: false, ambientOcclusion: false, visibleEdges: false, hiddenEdges: false, }; const prevState = system.currentRenderState.clone(scratchPrevRenderState); system.context.viewport(0, 0, this._width, this._height); const vf = target.currentViewFlags.copy(this._classifierTreeRef ? this._classifierTreeRef.viewFlags : maskViewFlags); system.applyRenderState(this._renderState); const prevPlan = target.plan; const prevOverrides = target.currentFeatureSymbologyOverrides; target.uniforms.style.changeBackgroundColor(this._bgColor); // Avoid white on white reversal. Will be reset in changeRenderPlan below. target.changeFrustum(this._frustum, this._frustum.getFraction(), true); this._anyTranslucent = false; const prevProjMatrix = target.uniforms.frustum.projectionMatrix; target.uniforms.frustum.changeProjectionMatrix(PlanarClassifier._postProjectionMatrix.multiplyMatrixMatrix(prevProjMatrix)); target.uniforms.branch.changeRenderPlan(vf, target.plan.is3d, target.plan.hline, target.plan.contours); const addCmds = (oldCmds, newCmds) => { if (undefined === newCmds) return oldCmds; if (newCmds.length > 50000) { // This method is slower for smaller array sizes, but when the size of newCmds gets larger it's performance is ok. return oldCmds.concat(newCmds); } else { // This method runs faster, but gets a stack overflow when the size of newCmds is too large. oldCmds.push(...newCmds); return oldCmds; } }; const renderCommands = this._renderCommands; const getDrawCommands = (graphics) => { this._batchState.reset(); renderCommands.reset(target, this._branchStack, this._batchState); if (this._planarClipMask?.overridesModelVisibility) { // We're using batched tiles and the mask is overriding which models are visible versus those visible in the view. // We don't want the BatchedTileTreeReference to hide models that belong in the mask. // The target's root branch's symbology overrides are set up correctly for the mask, and we never push branches to the target // (we have a separate branch stack), so just use the root branch as the appearance provider instead of the branch stack. // NOTE: this doesn't work if we're inside a GraphicalEditingScope displaying temporary graphics for some elements, because those // elements are hidden in the tiles by a FeatureAppearanceProvider. But we'll never use a GraphicalEditingScope with batched tiles, // and non-batched tiles never hide models using symbology overrides. renderCommands.appearanceProvider = target.currentBranch; } renderCommands.collectGraphicsForPlanarProjection(graphics); // Draw the classifiers into our attachments. // When using Display.ElementColor, the color and transparency come from the classifier geometry. Therefore we may need to draw the classified geometry // in a different pass - or both passes - depending on the transparency of the classifiers. // NB: "Outside" geometry by definition cannot take color/transparency from element... let cmds = renderCommands.getCommands(3 /* RenderPass.OpaquePlanar */); // NB: We don't strictly require the classifier geometry to be planar, and sometimes (e.g., "planar" polyface/bspsurf) we do not detect planarity. cmds = addCmds(cmds, renderCommands.getCommands(5 /* RenderPass.OpaqueGeneral */)); cmds = addCmds(cmds, renderCommands.getCommands(2 /* RenderPass.OpaqueLinear */)); this._anyOpaque = cmds.length > 0; const transCmds = renderCommands.getCommands(8 /* RenderPass.Translucent */); if (transCmds.length > 0) { cmds = addCmds(cmds, renderCommands.getCommands(8 /* RenderPass.Translucent */)); this._anyTranslucent = true; } return cmds; }; if (this._classifierGraphics.length > 0 && this._classifierBuffers) { this._classifierBuffers.draw(getDrawCommands(this._classifierGraphics), target); // Draw any hilited classifiers. const hiliteCommands = renderCommands.getCommands(10 /* RenderPass.Hilite */); this._anyHilited = 0 !== hiliteCommands.length; if (this._anyHilited) this._classifierBuffers.drawHilite(hiliteCommands, target); } if (this._maskGraphics.length > 0 && this._maskBuffer) { if (this._planarClipMaskOverrides) { target.overrideFeatureSymbology(this._planarClipMaskOverrides); this._branchStack.setSymbologyOverrides(this._planarClipMaskOverrides); } if (this._planarClipMask && this._planarClipMask.settings.transparency !== undefined && this._planarClipMask.settings.transparency > 0.0) this._anyTranslucent = true; this._maskBuffer.draw(getDrawCommands(this._maskGraphics), target); } if (combinationBuffer) combinationBuffer.compose(target); this._batchState.reset(); target.changeRenderPlan(prevPlan); target.overrideFeatureSymbology(prevOverrides); system.applyRenderState(prevState); system.context.viewport(0, 0, target.viewRect.width, target.viewRect.height); } } //# sourceMappingURL=PlanarClassifier.js.map