@itwin/core-frontend
Version:
iTwin.js frontend components
424 lines • 22 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.
*--------------------------------------------------------------------------------------------*/
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(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);
}
}
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(), core_geometry_1.Matrix3d.createRigidHeadsUp(this._params.direction.negate()).inverse());
const worldToMap = core_geometry_1.Matrix4d.createTransform(worldToMapTransform);
const mapToWorld = 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 = 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