UNPKG

@itwin/core-frontend

Version:
426 lines • 22.3 kB
"use strict"; /*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); exports.SolarShadowMap = void 0; /** @packageDocumentation * @module WebGL */ 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 webgl_compatibility_1 = require("@itwin/webgl-compatibility"); const internal_1 = require("../../../tile/internal"); const BatchState_1 = require("./BatchState"); const CachedGeometry_1 = require("./CachedGeometry"); const FrameBuffer_1 = require("./FrameBuffer"); const GL_1 = require("./GL"); const RenderCommands_1 = require("./RenderCommands"); const RenderFlags_1 = require("./RenderFlags"); const RenderState_1 = require("./RenderState"); const ScratchDrawParams_1 = require("./ScratchDrawParams"); const System_1 = require("./System"); const Texture_1 = require("./Texture"); function createDrawArgs(sceneContext, solarShadowMap, tree, frustumPlanes, processTiles) { class SolarShadowMapDrawArgs extends internal_1.TileDrawArgs { _mapFrustumPlanes; _shadowMap; _useViewportMap; _processTiles; constructor(_mapFrustumPlanes, _shadowMap, args, process) { super(args); this._mapFrustumPlanes = _mapFrustumPlanes; this._shadowMap = _shadowMap; this._processTiles = process; } // The solar shadow projection is parallel - which can cause excessive tile selection if it is along an axis of an unbounded tile // tree such as the OSM buildings. Rev limit the selection here. get maxRealityTreeSelectionCount() { return 500; } processSelectedTiles(tiles) { this._processTiles(tiles); } get frustumPlanes() { if (true === this._useViewportMap) return super.frustumPlanes; else return this._mapFrustumPlanes; } get worldToViewMap() { if (true === this._useViewportMap) return super.worldToViewMap; else return this._shadowMap.worldToViewMap; } drawGraphics() { const graphics = this.produceGraphics(); if (graphics) this._shadowMap.addGraphic(graphics); } getPixelSize(tile) { // For tiles that are part of the scene, size them based on the viewport frustum so that shadow map uses same resolution tiles as scene // - otherwise artifacts like shadow acne may result. // For tiles that are NOT part of the scene, size them based on the shadow frustum, not the viewport frustum // - otherwise excessive numbers of excessively detailed may be requested for the shadow map. if (undefined === this._useViewportMap) { this._useViewportMap = true; const vis = tile.computeVisibility(this); this._useViewportMap = internal_1.TileVisibility.OutsideFrustum !== vis; } const size = super.getPixelSize(tile); this._useViewportMap = undefined; return size; } static create(context, shadowMap, tileTree, planes, process) { const args = tileTree.createDrawArgs(context); return undefined !== args ? new SolarShadowMapDrawArgs(planes, shadowMap, args, process) : undefined; } } return SolarShadowMapDrawArgs.create(sceneContext, solarShadowMap, tree, frustumPlanes, processTiles); } const shadowMapWidth = 4096; // size of original depth buffer map const shadowMapHeight = shadowMapWidth; // TBD - Adjust for aspect ratio. const evsmWidth = shadowMapWidth / 2; // EVSM buffer is 1/2 size each direction const evsmHeight = shadowMapHeight / 2; const postProjectionMatrixNpc = core_geometry_1.Matrix4d.createRowValues(/* Row 1 */ 0, 1, 0, 0, /* Row 2 */ 0, 0, 1, 0, /* Row 3 */ 1, 0, 0, 0, /* Row 4 */ 0, 0, 0, 1); // Bundles up the disposable, create-once-and-reuse members of a SolarShadowMap. class Bundle { depthTexture; shadowMapTexture; fbo; fboSM; evsmGeom; renderCommands; constructor(depthTexture, shadowMapTexture, fbo, fboSM, evsmGeom, renderCommands) { this.depthTexture = depthTexture; this.shadowMapTexture = shadowMapTexture; this.fbo = fbo; this.fboSM = fboSM; this.evsmGeom = evsmGeom; this.renderCommands = renderCommands; } static create(target, stack, batch) { const depthTextureHandle = System_1.System.instance.createDepthBuffer(shadowMapWidth, shadowMapHeight); if (undefined === depthTextureHandle) return undefined; let pixelDataType = GL_1.GL.Texture.DataType.Float; switch (System_1.System.instance.maxRenderType) { case webgl_compatibility_1.RenderType.TextureFloat: break; case webgl_compatibility_1.RenderType.TextureHalfFloat: pixelDataType = System_1.System.instance.context.HALF_FLOAT; break; /* falls through */ default: return undefined; } const colorTextures = []; const fbo = FrameBuffer_1.FrameBuffer.create(colorTextures, depthTextureHandle); if (undefined === fbo) return undefined; // shadowMap texture is 1/4 size the depth texture (and averaged down when converting) const shadowMapTextureHandle = Texture_1.TextureHandle.createForAttachment(evsmWidth, evsmHeight, GL_1.GL.Texture.Format.Rgba, pixelDataType); if (undefined === shadowMapTextureHandle) return undefined; const fboSM = FrameBuffer_1.FrameBuffer.create([shadowMapTextureHandle]); if (undefined === fboSM) return undefined; const depthTexture = new Texture_1.Texture({ ownership: "external", type: core_common_1.RenderTexture.Type.TileSection, handle: depthTextureHandle, transparency: core_common_1.TextureTransparency.Opaque }); const evsmGeom = CachedGeometry_1.EVSMGeometry.createGeometry((0, core_bentley_1.expectDefined)(depthTexture.texture.getHandle()), shadowMapWidth, shadowMapHeight); if (undefined === evsmGeom) return undefined; const shadowMapTexture = new Texture_1.Texture({ type: core_common_1.RenderTexture.Type.Normal, ownership: "external", handle: shadowMapTextureHandle, transparency: core_common_1.TextureTransparency.Opaque }); const renderCommands = new RenderCommands_1.RenderCommands(target, stack, batch); return new Bundle(depthTexture, shadowMapTexture, fbo, fboSM, evsmGeom, renderCommands); } get isDisposed() { return this.depthTexture.isDisposed && this.shadowMapTexture.isDisposed && this.fbo.isDisposed && this.fboSM.isDisposed && this.evsmGeom.isDisposed; } [Symbol.dispose]() { (0, core_bentley_1.dispose)(this.depthTexture); (0, core_bentley_1.dispose)(this.shadowMapTexture); (0, core_bentley_1.dispose)(this.fbo); (0, core_bentley_1.dispose)(this.fboSM); (0, core_bentley_1.dispose)(this.evsmGeom); } } /** Describes the set of parameters which, when they change, require us to recreate the shadow map. */ class ShadowMapParams { direction = new core_geometry_1.Vector3d(); viewFrustum = new core_common_1.Frustum(); settings; constructor(viewFrustum, direction, settings) { direction.clone(this.direction); this.viewFrustum.setFrom(viewFrustum); this.settings = settings; } update(viewFrustum, direction, settings) { this.settings = settings; this.viewFrustum.setFrom(viewFrustum); direction.clone(this.direction); } } // We know that normalize won't fail on this hard-coded vector. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const defaultSunDirection = core_geometry_1.Vector3d.create(-1, -1, -1).normalize(); const scratchFrustum = new core_common_1.Frustum(); const scratchFrustumPlanes = core_common_1.FrustumPlanes.createEmpty(); class SolarShadowMap { _bundle; _projectionMatrix = core_geometry_1.Matrix4d.createIdentity(); _graphics = []; _shadowFrustum = new core_common_1.Frustum(); _isReady = false; _isDrawing = false; _enabled = false; _params; _scratchRange = core_geometry_1.Range3d.createNull(); _scratchTransform = core_geometry_1.Transform.createIdentity(); _scratchViewFlags = new core_common_1.ViewFlags(); _renderState; _noZRenderState; _batchState; _worldToViewMap = core_geometry_1.Map4d.createIdentity(); _target; // This exists chiefly for debugging. See ToggleShadowMapTilesTool. onGraphicsChanged; getBundle(target) { if (undefined === this._bundle) { this._bundle = Bundle.create(target, target.uniforms.branch.stack, this._batchState); (0, core_bentley_1.assert)(undefined !== this._bundle); } return this._bundle; } get isReady() { return this._isReady; } get isDrawing() { return this._isDrawing; } get isEnabled() { return this._enabled; } get projectionMatrix() { return this._projectionMatrix; } get depthTexture() { return undefined !== this._bundle ? this._bundle.depthTexture : undefined; } get shadowMapTexture() { return undefined !== this._bundle ? this._bundle.shadowMapTexture : undefined; } get settings() { return undefined !== this._params ? this._params.settings : undefined; } get direction() { return undefined !== this._params ? this._params.direction : undefined; } get frustum() { return this._shadowFrustum; } get worldToViewMap() { return this._worldToViewMap; } addGraphic(graphic) { this._graphics.push(graphic); } constructor(target) { this._target = target; this._renderState = new RenderState_1.RenderState(); this._renderState.flags.depthMask = true; this._renderState.flags.blend = false; this._renderState.flags.depthTest = true; this._noZRenderState = new RenderState_1.RenderState(); this._noZRenderState.flags.depthMask = false; this._batchState = new BatchState_1.BatchState(target.uniforms.branch.stack); } disable() { this._enabled = this._isReady = false; this._bundle = (0, core_bentley_1.dispose)(this._bundle); this.clearGraphics(true); this._target.uniforms.shadow.update(); } collectStatistics(stats) { const bundle = this._bundle; if (undefined !== bundle) stats.addShadowMap(bundle.depthTexture.bytesUsed + bundle.shadowMapTexture.bytesUsed); } get isDisposed() { return undefined === this._bundle && 0 === this._graphics.length; } [Symbol.dispose]() { this._bundle = (0, core_bentley_1.dispose)(this._bundle); this.clearGraphics(true); } clearGraphics(notify) { for (const graphic of this._graphics) graphic[Symbol.dispose](); this._graphics.length = 0; if (notify) this.notifyGraphicsChanged(); } notifyGraphicsChanged() { if (undefined !== this.onGraphicsChanged) this.onGraphicsChanged(this._graphics); } update(context) { this._isReady = false; this.clearGraphics(false); if (undefined === context || !context.viewport.view.isSpatialView()) { this.disable(); this.notifyGraphicsChanged(); return; } const view = context.viewport.view; const style = view.getDisplayStyle3d(); const sunDirection = style.sunDirection ?? defaultSunDirection; const minimumHorizonDirection = -.01; if (sunDirection.z > minimumHorizonDirection) { this.notifyGraphicsChanged(); return; } this._enabled = true; const viewFrustum = context.viewingSpace.getFrustum(); const settings = style.settings.solarShadows; if (undefined === this._params) this._params = new ShadowMapParams(viewFrustum, sunDirection, settings); else this._params.update(viewFrustum, sunDirection, settings); const iModel = view.iModel; const worldToMapTransform = core_geometry_1.Transform.createRefs(core_geometry_1.Point3d.createZero(), (0, core_bentley_1.expectDefined)(core_geometry_1.Matrix3d.createRigidHeadsUp(this._params.direction.negate()).inverse())); const worldToMap = core_geometry_1.Matrix4d.createTransform(worldToMapTransform); const mapToWorld = (0, core_bentley_1.expectDefined)(worldToMap.createInverse()); // Start with entire project. const shadowRange = worldToMapTransform.multiplyRange(iModel.projectExtents); // Limit the map to only displayed models. const viewTileRange = core_geometry_1.Range3d.createNull(); for (const ref of view.getTileTreeRefs()) { if (ref.castsShadows) { if (ref.isGlobal) { // A shadow-casting tile tree that spans the globe. Limit its range to the viewed extents. for (const p3 of viewFrustum.points) { const p4 = worldToMap.multiplyPoint3d(p3, 1); if (p4.w > 0.0001) viewTileRange.extendXYZW(p4.x, p4.y, p4.z, p4.w); else viewTileRange.high.z = Math.max(1.0, viewTileRange.high.z); // behind eye plane. } } else { ref.accumulateTransformedRange(viewTileRange, worldToMap, undefined); } } } if (!viewTileRange.isNull) viewTileRange.clone(shadowRange); // Expand shadow range to include both the shadowers and shadowed portion of background map. scratchFrustum.initFromRange(shadowRange); mapToWorld.multiplyPoint3dArrayQuietNormalize(scratchFrustum.points); // This frustum represents the shadowing geometry. Intersect it with background geometry and expand the range depth to include that intersection. const backgroundMapGeometry = context.viewport.view.displayStyle.getBackgroundMapGeometry(); if (undefined !== backgroundMapGeometry) { const backgroundDepthRange = backgroundMapGeometry.getFrustumIntersectionDepthRange(this._shadowFrustum, iModel.projectExtents); if (!backgroundDepthRange.isNull) shadowRange.low.z = Math.min(shadowRange.low.z, backgroundDepthRange.low); } this._params.viewFrustum.transformBy(worldToMapTransform, scratchFrustum); scratchFrustumPlanes.init(scratchFrustum); const viewIntersectShadowRange = core_geometry_1.Range3d.createNull(); const viewClipPlanes = core_geometry_1.ConvexClipPlaneSet.createPlanes(scratchFrustumPlanes.planes); core_geometry_1.ClipUtilities.announceLoopsOfConvexClipPlaneSetIntersectRange(viewClipPlanes, shadowRange, (points) => { for (const point of points.getPoint3dArray()) viewIntersectShadowRange.extendPoint(point); }); if (viewIntersectShadowRange.isNull) { this.notifyGraphicsChanged(); return; } viewIntersectShadowRange.high.z = shadowRange.high.z; // We need to include shadowing geometry that may be outside view (along the solar axis). this._shadowFrustum.initFromRange(viewIntersectShadowRange); mapToWorld.multiplyPoint3dArrayQuietNormalize(this._shadowFrustum.points); const tileRange = core_geometry_1.Range3d.createNull(); scratchFrustumPlanes.init(this._shadowFrustum); for (const ref of view.getTileTreeRefs()) { if (!ref.castsShadows) continue; const drawArgs = createDrawArgs(context, this, ref, scratchFrustumPlanes, (tiles) => { for (const tile of tiles) tileRange.extendRange(tileToMapTransform.multiplyRange(tile.range, this._scratchRange)); }); if (undefined === drawArgs) continue; const tileToMapTransform = worldToMapTransform.multiplyTransformTransform(drawArgs.location, this._scratchTransform); drawArgs.tree.draw(drawArgs); } if (tileRange.isNull) { this.clearGraphics(true); } else if (this._graphics.length > 0) { // Avoid an uninvertible matrix on empty range... if (core_geometry_1.Geometry.isAlmostEqualNumber(shadowRange.low.x, shadowRange.high.x) || core_geometry_1.Geometry.isAlmostEqualNumber(shadowRange.low.y, shadowRange.high.y) || core_geometry_1.Geometry.isAlmostEqualNumber(shadowRange.low.z, shadowRange.high.z)) { this.clearGraphics(true); return; } const frustumMap = this._shadowFrustum.toMap4d(); if (undefined === frustumMap) { this.clearGraphics(true); (0, core_bentley_1.assert)(false); return; } this._projectionMatrix = frustumMap.transform0.clone(); const worldToNpc = postProjectionMatrixNpc.multiplyMatrixMatrix(this._projectionMatrix); const npcToView = (0, core_bentley_1.expectDefined)(core_geometry_1.Map4d.createBoxMap(core_geometry_1.Point3d.create(0, 0, 0), core_geometry_1.Point3d.create(1, 1, 1), core_geometry_1.Point3d.create(0, 0, 0), core_geometry_1.Point3d.create(shadowMapWidth, shadowMapHeight, 1))); const npcToWorld = worldToNpc.createInverse(); if (undefined === npcToWorld) { this.clearGraphics(true); return; } const worldToNpcMap = core_geometry_1.Map4d.createRefs(worldToNpc, npcToWorld); this._worldToViewMap = npcToView.multiplyMapMap(worldToNpcMap); } this._target.uniforms.shadow.update(); this.notifyGraphicsChanged(); } draw(target) { (0, core_bentley_1.assert)(this.isEnabled); if (this.isReady || 0 === this._graphics.length) return; const bundle = this.getBundle(target); if (undefined === bundle) return; this._isDrawing = true; const prevState = System_1.System.instance.currentRenderState.clone(); const gl = System_1.System.instance.context; gl.viewport(0, 0, shadowMapWidth, shadowMapHeight); // NB: textures and materials are needed because their transparencies affect whether or not a surface casts shadows const viewFlags = target.currentViewFlags.copy({ renderMode: core_common_1.RenderMode.SmoothShade, wiremesh: false, transparency: false, lighting: false, shadows: false, monochrome: false, ambientOcclusion: false, visibleEdges: false, hiddenEdges: false, }); System_1.System.instance.applyRenderState(this._renderState); const prevPlan = target.plan; target.changeFrustum(this._shadowFrustum, this._shadowFrustum.getFraction(), true); target.uniforms.branch.changeRenderPlan(viewFlags, target.plan.is3d, target.plan.hline); const renderCommands = bundle.renderCommands; renderCommands.reset(target, target.uniforms.branch.stack, this._batchState); renderCommands.addGraphics(this._graphics); System_1.System.instance.frameBufferStack.execute(bundle.fbo, true, false, () => { System_1.System.instance.context.clearDepth(1.0); System_1.System.instance.context.clear(GL_1.GL.BufferBit.Depth); target.techniques.execute(target, renderCommands.getCommands(3 /* RenderPass.OpaquePlanar */), 19 /* RenderPass.PlanarClassification */); // Draw these with RenderPass.PlanarClassification (rather than Opaque...) so that the pick ordering is avoided. target.techniques.execute(target, renderCommands.getCommands(5 /* RenderPass.OpaqueGeneral */), 19 /* RenderPass.PlanarClassification */); // Draw these with RenderPass.PlanarClassification (rather than Opaque...) so that the pick ordering is avoided. }); // copy depth buffer to EVSM shadow buffer and average down for AA effect gl.viewport(0, 0, evsmWidth, evsmHeight); System_1.System.instance.frameBufferStack.execute(bundle.fboSM, true, false, () => { System_1.System.instance.applyRenderState(this._noZRenderState); const params = (0, ScratchDrawParams_1.getDrawParams)(target, bundle.evsmGeom); target.techniques.draw(params); }); // mipmap resulting EVSM texture and set filtering options System_1.System.instance.activateTexture2d(RenderFlags_1.TextureUnit.ShadowMap, bundle.shadowMapTexture.texture.getHandle()); gl.generateMipmap(gl.TEXTURE_2D); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); System_1.System.instance.setMaxAnisotropy(undefined); // target.recordPerformanceMetric("Compute EVSM"); this._batchState.reset(); // Reset the batch Ids... target.changeRenderPlan(prevPlan); System_1.System.instance.applyRenderState(prevState); System_1.System.instance.context.viewport(0, 0, target.viewRect.width, target.viewRect.height); // Restore viewport this.clearGraphics(false); this._isDrawing = false; this._isReady = true; } } exports.SolarShadowMap = SolarShadowMap; //# sourceMappingURL=SolarShadowMap.js.map