Skip to content

Commit

Permalink
[MAPS3D-831] Shadows: tree shadows and shadow map frustum optimization (
Browse files Browse the repository at this point in the history
#470)

* Tree shadows, casting and receiving

Basic implementation, no instancing nor frustums optimization yet.

* Update dynamic style

Still unsupported:
- icon-emissive-strength using measure-light
- model-height-based-emissive-strength-multiplier (waiting on landmarks)
- model-emissive-strength using measure-light (waiting on landmark)

* Shadow frustum culling for trees

* Remove u_cascade_distances and fade between cascades. Extend shadow map far to fill the top of tilted shadow map camera

Set dynamic style (rebase to latest dynamic.json) as default. Enable usage or original style  fill extrusions (not adding from 3d-playground)

* render tests

* setEventedParent for modelManager

* load lights from style if present

* Fix shader after rebase

* revert back end fade range behavior (was using cascade[1].far * 0.75 on GPU earlier

* shadow map extend frustum far on terrain. shadow cast and receive

terrain raster shadows use non biased ground version to prevent peter panning.
  • Loading branch information
astojilj authored May 9, 2023
1 parent 6d3549c commit 38a60de
Show file tree
Hide file tree
Showing 26 changed files with 3,787 additions and 2,458 deletions.
14 changes: 12 additions & 2 deletions 3d-style/data/bucket/model_bucket.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {EvaluationFeature} from '../../../src/data/evaluation_feature.js';
import EvaluationParameters from '../../../src/style/evaluation_parameters.js';
import Point from '@mapbox/point-geometry';
import type {Mat4} from 'gl-matrix';
import type {CanonicalTileID} from '../../../src/source/tile_id.js';
import type {CanonicalTileID, OverscaledTileID} from '../../../src/source/tile_id.js';
import type {
Bucket,
BucketParameters,
Expand Down Expand Up @@ -79,7 +79,11 @@ class ModelBucket implements Bucket {

// elevation is baked into vertex buffer together with evaluated instance translation
validForExaggeration: number;
validForDEMTile: ?CanonicalTileID;
validForDEMTile: ?OverscaledTileID;
maxVerticalOffset: number; // for tile AABB calculation
maxScale: number; // across all dimensions, for tile AABB calculation
maxHeight: number; // calculated from previous two, during rendering, when models are available.
isInsideFirstShadowMapFrustum: boolean; // evaluated during first shadows pass and cached here for the second shadow pass.

/* $FlowIgnore[incompatible-type-arg] Doesn't need to know about all the implementations */
constructor(options: BucketParameters<ModelStyleLayer>) {
Expand All @@ -94,6 +98,9 @@ class ModelBucket implements Bucket {
this.hasPattern = false;
this.instancesPerModel = {};
this.validForExaggeration = 0;
this.maxVerticalOffset = 0;
this.maxScale = 0;
this.maxHeight = 0;
}

populate(features: Array<IndexedFeature>, options: PopulateParameters, canonical: CanonicalTileID, tileTransform: TileTransform) {
Expand Down Expand Up @@ -136,6 +143,7 @@ class ModelBucket implements Bucket {
}
}
}
this.maxHeight = 0; // needs to be recalculated.
}

isEmpty(): boolean {
Expand Down Expand Up @@ -227,6 +235,8 @@ class ModelBucket implements Bucket {
const color = layer.paint.get('model-color').evaluate(evaluationFeature, featureState, canonical);
color.a = layer.paint.get('model-color-mix-intensity').evaluate(evaluationFeature, featureState, canonical);
const rotationScaleYZFlip: Mat4 = [];
if (this.maxVerticalOffset < translation[2]) this.maxVerticalOffset = translation[2];
this.maxScale = Math.max(Math.max(this.maxScale, scale[0]), Math.max(scale[1], scale[2]));

rotationScaleYZFlipMatrix(rotationScaleYZFlip, (rotation: any), (scale: any));

Expand Down
167 changes: 130 additions & 37 deletions 3d-style/render/draw_model.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {OverscaledTileID} from '../../src/source/tile_id.js';
import {number as interpolate} from '../../src/style-spec/util/interpolate.js';
import {FeatureVertexArray} from '../../src/data/array_types.js';
import {featureAttributes} from '../data/model_attributes.js';
import {Aabb} from '../../src/util/primitives.js';

export default drawModels;

Expand All @@ -45,6 +46,13 @@ type SortedMesh = {
nodeModelMatrix: Mat4;
}

type RenderData = {
shadowUniformsInitialized: boolean;
tileMatrix: Float64Array;
shadowTileMatrix: Float32Array;
aabb: Aabb;
}

function fogMatrixForModel(modelMatrix: Mat4, transform: Transform): Mat4 {
// convert model matrix from the default world size to the one used by the fog
const fogMatrix = [...modelMatrix];
Expand Down Expand Up @@ -279,16 +287,32 @@ function drawModels(painter: Painter, sourceCache: SourceCache, layer: ModelStyl
if (opacity === 0) {
return;
}
const castShadows = layer.paint.get('model-cast-shadows');
if (painter.renderPass === 'shadow' && !castShadows) {
return;
}
const shadowRenderer = painter.shadowRenderer;
const receiveShadows = layer.paint.get('model-cast-shadows');
if (shadowRenderer && !receiveShadows) {
shadowRenderer.enabled = false;
}
const cleanup = () => {
if (shadowRenderer && !receiveShadows) {
shadowRenderer.enabled = true;
}
};

const modelSource = sourceCache.getSource();

if (!modelSource.loaded()) return;
if (modelSource.type === 'vector' || modelSource.type === 'geojson') {
drawInstancedModels(painter, sourceCache, layer, coords);
cleanup();
return;
}
if (modelSource.type === 'batched-model') {
drawBatchedModels(painter, sourceCache, layer, coords);
cleanup();
return;
}
const models = (modelSource: any).getModels();
Expand Down Expand Up @@ -335,6 +359,7 @@ function drawModels(painter: Painter, sourceCache: SourceCache, layer: ModelStyl
drawShadowCaster(transparentMesh.mesh, transparentMesh.nodeModelMatrix, painter, layer);
}
// Finish the render pass
cleanup();
return;
}

Expand All @@ -358,6 +383,7 @@ function drawModels(painter: Painter, sourceCache: SourceCache, layer: ModelStyl
for (const transparentMesh of transparentMeshes) {
drawMesh(transparentMesh, painter, layer, modelParametersVector[transparentMesh.modelIndex], StencilMode.disabled, painter.colorModeForRenderPass());
}
cleanup();
}

// If terrain changes, update elevations (baked in translation).
Expand All @@ -374,7 +400,7 @@ function updateModelBucketsElevation(painter: Painter, bucket: ModelBucket, buck
}
}
if (exaggeration === bucket.validForExaggeration &&
(exaggeration === 0 || (dem && dem._demTile && dem._demTile.tileID.canonical === bucket.validForDEMTile))) {
(exaggeration === 0 || (dem && dem._demTile && dem._demTile.tileID === bucket.validForDEMTile))) {
return;
}

Expand All @@ -389,11 +415,19 @@ function updateModelBucketsElevation(painter: Painter, bucket: ModelBucket, buck
}
}
bucket.validForExaggeration = exaggeration;
bucket.validForDEMTile = dem && dem._demTile ? dem._demTile.tileID.canonical : undefined;
bucket.validForDEMTile = dem && dem._demTile ? dem._demTile.tileID : undefined;
bucket.uploaded = false;
bucket.upload(painter.context);
}

// preallocate structure used to reduce re-allocation during rendering and flow checks
const renderData: RenderData = {
shadowUniformsInitialized: false,
tileMatrix: new Float64Array(16),
shadowTileMatrix: new Float32Array(16),
aabb: new Aabb([0, 0, 0], [EXTENT, EXTENT, 0])
};

function drawInstancedModels(painter: Painter, source: SourceCache, layer: ModelStyleLayer, coords: Array<OverscaledTileID>) {
const tr = painter.transform;
if (tr.projection.name !== 'mercator') {
Expand All @@ -409,6 +443,20 @@ function drawInstancedModels(painter: Painter, source: SourceCache, layer: Model
const tile = source.getTile(coord);
const bucket: ?ModelBucket = (tile.getBucket(layer): any);
if (!bucket || bucket.projection.name !== tr.projection.name) continue;

renderData.shadowUniformsInitialized = false;
if (painter.renderPass === 'shadow' && painter.shadowRenderer) {
const shadowRenderer = painter.shadowRenderer;
if (painter.currentShadowCascade === 1 && bucket.isInsideFirstShadowMapFrustum) continue;

const tileMatrix = tr.calculatePosMatrix(coord.toUnwrapped(), tr.worldSize);
renderData.tileMatrix.set(tileMatrix);
renderData.shadowTileMatrix = Float32Array.from(shadowRenderer.calculateShadowPassMatrixFromMatrix(tileMatrix));
renderData.aabb.min.fill(0);
renderData.aabb.max[0] = renderData.aabb.max[1] = EXTENT;
renderData.aabb.max[2] = 0;
if (calculateTileShadowPassCulling(bucket, renderData, painter)) continue;
}
updateModelBucketsElevation(painter, bucket, coord);

// camera position in the tile coordinates
Expand All @@ -421,61 +469,74 @@ function drawInstancedModels(painter: Painter, source: SourceCache, layer: Model
if (!model) continue;
const modelInstances = bucket.instancesPerModel[modelId];
for (const node of model.nodes) {
drawInstancedNode(painter, layer, node, modelInstances, cameraPos, coord);
drawInstancedNode(painter, layer, node, modelInstances, cameraPos, coord, renderData);
}
}
}
}

function drawInstancedNode(painter, layer, node, modelInstances, cameraPos, coord) {
function drawInstancedNode(painter, layer, node, modelInstances, cameraPos, coord, renderData) {
const context = painter.context;
const isShadowPass = painter.renderPass === 'shadow';
const shadowRenderer = painter.shadowRenderer;
const depthMode = isShadowPass && shadowRenderer ? shadowRenderer.getShadowPassDepthMode() : new DepthMode(context.gl.LEQUAL, DepthMode.ReadWrite, painter.depthRangeFor3D);
const colorMode = isShadowPass && shadowRenderer ? shadowRenderer.getShadowPassColorMode() : painter.colorModeForRenderPass();

if (node.meshes) {
const depthMode = new DepthMode(context.gl.LEQUAL, DepthMode.ReadWrite, painter.depthRangeFor3D);
for (const mesh of node.meshes) {
const definesValues = [];
const definesValues = ['MODEL_POSITION_ON_GPU'];
const dynamicBuffers = [];
setupMeshDraw(definesValues, dynamicBuffers, mesh, painter);
definesValues.push('MODEL_POSITION_ON_GPU');
const program = painter.useProgram('model', null, ((definesValues: any): DynamicDefinesType[]));

const isShadowPass = painter.renderPass === 'shadow';
const shadowRenderer = painter.shadowRenderer;
if (!isShadowPass && shadowRenderer) {
shadowRenderer.setupShadows(coord.toUnwrapped(), program);
let program;
let uniformValues;

if (isShadowPass && shadowRenderer) {
program = painter.useProgram('modelDepth', null, ((definesValues: any): DynamicDefinesType[]));
uniformValues = modelDepthUniformValues(renderData.shadowTileMatrix, renderData.shadowTileMatrix, Float32Array.from(node.matrix));
} else {
setupMeshDraw(definesValues, dynamicBuffers, mesh, painter);
program = painter.useProgram('model', null, ((definesValues: any): DynamicDefinesType[]));
const material = mesh.material;
const pbr = material.pbrMetallicRoughness;

uniformValues = modelUniformValues(
coord.projMatrix,
Float32Array.from(node.matrix),
new Float32Array(16),
painter,
layer.paint.get('model-opacity'),
pbr.baseColorFactor,
material.emissiveFactor,
pbr.metallicFactor,
pbr.roughnessFactor,
material,
layer,
cameraPos);
if (shadowRenderer) {
if (!renderData.shadowUniformsInitialized) {
shadowRenderer.setupShadows(coord.toUnwrapped(), program);
renderData.shadowUniformsInitialized = true;
} else {
program.setShadowUniformValues(context, shadowRenderer.getShadowUniformValues());
}
}
}

painter.uploadCommonUniforms(context, program, coord.toUnwrapped());

const material = mesh.material;
const pbr = material.pbrMetallicRoughness;

const uniformValues = modelUniformValues(
coord.projMatrix,
Float32Array.from(node.matrix),
new Float32Array(16),
painter,
layer.paint.get('model-opacity'),
pbr.baseColorFactor,
material.emissiveFactor,
pbr.metallicFactor,
pbr.roughnessFactor,
material,
layer,
cameraPos);

assert(modelInstances.instancedDataArray.bytesPerElement === 64);

const instanceUniform = isShadowPass ? "u_instance" : "u_normal_matrix";
for (let i = 0; i < modelInstances.instancedDataArray.length; ++i) {
uniformValues["u_normal_matrix"] = new Float32Array(modelInstances.instancedDataArray.arrayBuffer, i * 64, 16);
program.draw(context, context.gl.TRIANGLES, depthMode, StencilMode.disabled, painter.colorModeForRenderPass(), CullFaceMode.disabled,
uniformValues, layer.id, mesh.vertexBuffer, mesh.indexBuffer, mesh.segments, layer.paint, painter.transform.zoom,
undefined, dynamicBuffers);
/* $FlowIgnore[prop-missing] modelDepth uses u_instance and model uses u_normal_matrix for packing instance data */
uniformValues[instanceUniform] = new Float32Array(modelInstances.instancedDataArray.arrayBuffer, i * 64, 16);
program.draw(context, context.gl.TRIANGLES, depthMode, StencilMode.disabled, colorMode, CullFaceMode.disabled,
uniformValues, layer.id, mesh.vertexBuffer, mesh.indexBuffer, mesh.segments, layer.paint, painter.transform.zoom,
undefined, dynamicBuffers);
}
}
}
if (node.children) {
for (const child of node.children) {
drawInstancedNode(painter, layer, child, modelInstances, cameraPos, coord);
drawInstancedNode(painter, layer, child, modelInstances, cameraPos, coord, renderData);
}
}
}
Expand Down Expand Up @@ -672,3 +733,35 @@ function drawBatchedModels(painter: Painter, source: SourceCache, layer: ModelSt
}
}

function calculateTileShadowPassCulling(bucket, renderData, painter) {
if (!painter.style.modelManager) return true;
const modelManager = painter.style.modelManager;
if (!painter.shadowRenderer) return true;
const shadowRenderer = painter.shadowRenderer;
assert(painter.renderPass === 'shadow');
const aabb = renderData.aabb;
let allModelsLoaded = true;
let maxHeight = bucket.maxHeight;
if (maxHeight === 0) {
let maxDim = 0;
for (const modelId in bucket.instancesPerModel) {
const model = modelManager.getModel(modelId);
if (!model) {
allModelsLoaded = false;
continue;
}
maxDim = Math.max(maxDim, Math.max(Math.max(model.aabb.max[0], model.aabb.max[1]), model.aabb.max[2]));
}
maxHeight = bucket.maxScale * maxDim * 1.41 + bucket.maxVerticalOffset;
if (allModelsLoaded) bucket.maxHeight = maxHeight;
}
aabb.max[2] = maxHeight;
vec3.transformMat4(aabb.min, aabb.min, renderData.tileMatrix);
vec3.transformMat4(aabb.max, aabb.max, renderData.tileMatrix);
const intersection = aabb.intersects(shadowRenderer.getCurrentCascadeFrustum());
if (painter.currentShadowCascade === 0) {
bucket.isInsideFirstShadowMapFrustum = intersection === 2;
}
return intersection === 0;
}

17 changes: 13 additions & 4 deletions 3d-style/render/program/model_program.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,18 +129,27 @@ const modelUniformValues = (
};

export type ModelDepthUniformsType = {|
'u_matrix': UniformMatrix4f
'u_matrix': UniformMatrix4f,
'u_instance': UniformMatrix4f,
'u_node_matrix': UniformMatrix4f
|};

const modelDepthUniforms = (context: Context): ModelDepthUniformsType => ({
'u_matrix': new UniformMatrix4f(context)
'u_matrix': new UniformMatrix4f(context),
'u_instance': new UniformMatrix4f(context),
'u_node_matrix': new UniformMatrix4f(context)
});

const emptyMat4 = new Float32Array(16);
const modelDepthUniformValues = (
matrix: Float32Array
matrix: Float32Array,
instance: Float32Array = emptyMat4,
nodeMatrix: Float32Array = emptyMat4
): UniformValues<ModelDepthUniformsType> => {
return {
'u_matrix': matrix
'u_matrix': matrix,
'u_instance': instance,
'u_node_matrix': nodeMatrix
};
};

Expand Down
Loading

0 comments on commit 38a60de

Please sign in to comment.