mapbox-gl
Version:
A WebGL interactive maps library
372 lines (300 loc) • 14.2 kB
JavaScript
// @flow
import DepthMode from '../gl/depth_mode.js';
import CullFaceMode from '../gl/cull_face_mode.js';
import {terrainRasterUniformValues} from './terrain_raster_program.js';
import {globeRasterUniformValues} from './globe_raster_program.js';
import {Terrain} from './terrain.js';
import Tile from '../source/tile.js';
import assert from 'assert';
import {easeCubicInOut} from '../util/util.js';
import {mercatorXfromLng, mercatorYfromLat} from '../geo/mercator_coordinate.js';
import type Painter from '../render/painter.js';
import type SourceCache from '../source/source_cache.js';
import {OverscaledTileID, CanonicalTileID} from '../source/tile_id.js';
import StencilMode from '../gl/stencil_mode.js';
import ColorMode from '../gl/color_mode.js';
import {mat4} from 'gl-matrix';
import {
calculateGlobeMercatorMatrix,
globeToMercatorTransition,
globePoleMatrixForTile,
getGridMatrix,
globeTileLatLngCorners
} from '../geo/projection/globe_util.js';
import extend from '../style-spec/util/extend.js';
export {
drawTerrainRaster,
drawTerrainDepth
};
type DEMChain = {
startTime: number,
phase: number,
duration: number, // Interpolation duration in milliseconds
from: Tile,
to: Tile,
queued: ?Tile
};
class VertexMorphing {
operations: {[string | number]: DEMChain };
constructor() {
this.operations = {};
}
newMorphing(key: number, from: Tile, to: Tile, now: number, duration: number) {
assert(from.demTexture && to.demTexture);
assert(from.tileID.key !== to.tileID.key);
if (key in this.operations) {
const op = this.operations[key];
assert(op.from && op.to);
// Queue the target tile unless it's being morphed to already
if (op.to.tileID.key !== to.tileID.key)
op.queued = to;
} else {
this.operations[key] = {
startTime: now,
phase: 0.0,
duration,
from,
to,
queued: null
};
}
}
getMorphValuesForProxy(key: number): ?{from: Tile, to: Tile, phase: number} {
if (!(key in this.operations))
return null;
const op = this.operations[key];
const from = op.from;
const to = op.to;
assert(from && to);
return {from, to, phase: op.phase};
}
update(now: number) {
for (const key in this.operations) {
const op = this.operations[key];
assert(op.from && op.to);
op.phase = (now - op.startTime) / op.duration;
// Start the queued operation if the current one is finished or the data has expired
while (op.phase >= 1.0 || !this._validOp(op)) {
if (!this._nextOp(op, now)) {
delete this.operations[key];
break;
}
}
}
}
_nextOp(op: DEMChain, now: number): boolean {
if (!op.queued)
return false;
op.from = op.to;
op.to = op.queued;
op.queued = null;
op.phase = 0.0;
op.startTime = now;
return true;
}
_validOp(op: DEMChain): boolean {
return op.from.hasData() && op.to.hasData();
}
}
function demTileChanged(prev: ?Tile, next: ?Tile): boolean {
if (prev == null || next == null)
return false;
if (!prev.hasData() || !next.hasData())
return false;
if (prev.demTexture == null || next.demTexture == null)
return false;
return prev.tileID.key !== next.tileID.key;
}
const vertexMorphing = new VertexMorphing();
const SHADER_DEFAULT = 0;
const SHADER_MORPHING = 1;
const SHADER_TERRAIN_WIREFRAME = 2;
const defaultDuration = 250;
const shaderDefines = {
"0": null,
"1": 'TERRAIN_VERTEX_MORPHING',
"2": 'TERRAIN_WIREFRAME'
};
function drawTerrainForGlobe(painter: Painter, terrain: Terrain, sourceCache: SourceCache, tileIDs: Array<OverscaledTileID>, now: number) {
const context = painter.context;
const gl = context.gl;
let program, programMode;
const showWireframe = painter.options.showTerrainWireframe ? SHADER_TERRAIN_WIREFRAME : SHADER_DEFAULT;
const setShaderMode = (mode, isWireframe) => {
if (programMode === mode)
return;
const defines = [shaderDefines[mode], 'PROJECTION_GLOBE_VIEW'];
if (isWireframe) {
defines.push(shaderDefines[showWireframe]);
}
program = painter.useProgram('globeRaster', null, defines);
programMode = mode;
};
const colorMode = painter.colorModeForRenderPass();
const depthMode = new DepthMode(gl.LEQUAL, DepthMode.ReadWrite, painter.depthRangeFor3D);
vertexMorphing.update(now);
const tr = painter.transform;
const globeMercatorMatrix = calculateGlobeMercatorMatrix(tr);
const mercatorCenter = [mercatorXfromLng(tr.center.lng), mercatorYfromLat(tr.center.lat)];
const batches = showWireframe ? [false, true] : [false];
const sharedBuffers = painter.globeSharedBuffers;
batches.forEach(isWireframe => {
// This code assumes the rendering is batched into mesh terrain and then wireframe
// terrain (if applicable) so that this is enough to ensure the correct program is
// set when we switch from one to the other.
programMode = -1;
const primitive = isWireframe ? gl.LINES : gl.TRIANGLES;
for (const coord of tileIDs) {
const tile = sourceCache.getTile(coord);
const stencilMode = StencilMode.disabled;
const prevDemTile = terrain.prevTerrainTileForTile[coord.key];
const nextDemTile = terrain.terrainTileForTile[coord.key];
if (demTileChanged(prevDemTile, nextDemTile)) {
vertexMorphing.newMorphing(coord.key, prevDemTile, nextDemTile, now, defaultDuration);
}
// Bind the main draped texture
context.activeTexture.set(gl.TEXTURE0);
tile.texture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE);
const morph = vertexMorphing.getMorphValuesForProxy(coord.key);
const shaderMode = morph ? SHADER_MORPHING : SHADER_DEFAULT;
const elevationOptions = {useDenormalizedUpVectorScale: true};
if (morph) {
extend(elevationOptions, {morphing: {srcDemTile: morph.from, dstDemTile: morph.to, phase: easeCubicInOut(morph.phase)}});
}
const globeMatrix = Float32Array.from(tr.globeMatrix);
const corners = globeTileLatLngCorners(coord.canonical);
const gridMatrix = getGridMatrix(coord.canonical, corners);
const uniformValues = globeRasterUniformValues(
tr.projMatrix, globeMatrix, globeMercatorMatrix,
globeToMercatorTransition(tr.zoom), mercatorCenter, gridMatrix);
setShaderMode(shaderMode, isWireframe);
terrain.setupElevationDraw(tile, program, elevationOptions);
painter.prepareDrawProgram(context, program, coord.toUnwrapped());
if (sharedBuffers) {
const [buffer, indexBuffer, segments] = isWireframe ?
sharedBuffers.getWirefameBuffers(painter.context) :
sharedBuffers.getGridBuffers();
program.draw(context, primitive, depthMode, stencilMode, colorMode, CullFaceMode.backCCW,
uniformValues, "globe_raster", buffer, indexBuffer, segments);
}
}
});
// Render the poles.
if (sharedBuffers) {
const defines = ['GLOBE_POLES', 'PROJECTION_GLOBE_VIEW'];
program = painter.useProgram('globeRaster', null, defines);
for (const coord of tileIDs) {
// Fill poles by extrapolating adjacent border tiles
const {x, y, z} = coord.canonical;
const topCap = y === 0;
const bottomCap = y === (1 << z) - 1;
const [northPoleBuffer, southPoleBuffer, indexBuffer, segment] = sharedBuffers.getPoleBuffers(z);
if (segment && (topCap || bottomCap)) {
const tile = sourceCache.getTile(coord);
// Bind the main draped texture
context.activeTexture.set(gl.TEXTURE0);
tile.texture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE);
let poleMatrix = globePoleMatrixForTile(z, x, tr);
const drawPole = (program, vertexBuffer) => program.draw(
context, gl.TRIANGLES, depthMode, StencilMode.disabled, colorMode, CullFaceMode.disabled,
globeRasterUniformValues(tr.projMatrix, poleMatrix, poleMatrix, 0.0, mercatorCenter),
"globe_pole_raster", vertexBuffer, indexBuffer, segment);
terrain.setupElevationDraw(tile, program, {});
painter.prepareDrawProgram(context, program, coord.toUnwrapped());
if (topCap) {
drawPole(program, northPoleBuffer);
}
if (bottomCap) {
poleMatrix = mat4.scale(mat4.create(), poleMatrix, [1, -1, 1]);
drawPole(program, southPoleBuffer);
}
}
}
}
}
function drawTerrainRaster(painter: Painter, terrain: Terrain, sourceCache: SourceCache, tileIDs: Array<OverscaledTileID>, now: number) {
if (painter.transform.projection.name === 'globe') {
drawTerrainForGlobe(painter, terrain, sourceCache, tileIDs, now);
} else {
const context = painter.context;
const gl = context.gl;
let program, programMode;
const showWireframe = painter.options.showTerrainWireframe ? SHADER_TERRAIN_WIREFRAME : SHADER_DEFAULT;
const setShaderMode = (mode, isWireframe) => {
if (programMode === mode)
return;
const modes = [shaderDefines[mode]];
if (isWireframe) modes.push(shaderDefines[showWireframe]);
program = painter.useProgram('terrainRaster', null, modes);
programMode = mode;
};
const colorMode = painter.colorModeForRenderPass();
const depthMode = new DepthMode(gl.LEQUAL, DepthMode.ReadWrite, painter.depthRangeFor3D);
vertexMorphing.update(now);
const tr = painter.transform;
const skirt = skirtHeight(tr.zoom) * terrain.exaggeration();
const batches = showWireframe ? [false, true] : [false];
batches.forEach(isWireframe => {
// This code assumes the rendering is batched into mesh terrain and then wireframe
// terrain (if applicable) so that this is enough to ensure the correct program is
// set when we switch from one to the other.
programMode = -1;
const primitive = isWireframe ? gl.LINES : gl.TRIANGLES;
const [buffer, segments] = isWireframe ? terrain.getWirefameBuffer() : [terrain.gridIndexBuffer, terrain.gridSegments];
for (const coord of tileIDs) {
const tile = sourceCache.getTile(coord);
const stencilMode = StencilMode.disabled;
const prevDemTile = terrain.prevTerrainTileForTile[coord.key];
const nextDemTile = terrain.terrainTileForTile[coord.key];
if (demTileChanged(prevDemTile, nextDemTile)) {
vertexMorphing.newMorphing(coord.key, prevDemTile, nextDemTile, now, defaultDuration);
}
// Bind the main draped texture
context.activeTexture.set(gl.TEXTURE0);
tile.texture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE, gl.LINEAR_MIPMAP_NEAREST);
const morph = vertexMorphing.getMorphValuesForProxy(coord.key);
const shaderMode = morph ? SHADER_MORPHING : SHADER_DEFAULT;
let elevationOptions;
if (morph) {
elevationOptions = {morphing: {srcDemTile: morph.from, dstDemTile: morph.to, phase: easeCubicInOut(morph.phase)}};
}
const uniformValues = terrainRasterUniformValues(coord.projMatrix, isEdgeTile(coord.canonical, tr.renderWorldCopies) ? skirt / 10 : skirt);
setShaderMode(shaderMode, isWireframe);
terrain.setupElevationDraw(tile, program, elevationOptions);
painter.prepareDrawProgram(context, program, coord.toUnwrapped());
program.draw(context, primitive, depthMode, stencilMode, colorMode, CullFaceMode.backCCW,
uniformValues, "terrain_raster", terrain.gridBuffer, buffer, segments);
}
});
}
}
function drawTerrainDepth(painter: Painter, terrain: Terrain, sourceCache: SourceCache, tileIDs: Array<OverscaledTileID>) {
if (painter.transform.projection.name === 'globe') {
return;
}
assert(painter.renderPass === 'offscreen');
const context = painter.context;
const gl = context.gl;
context.clear({depth: 1});
const program = painter.useProgram('terrainDepth');
const depthMode = new DepthMode(gl.LESS, DepthMode.ReadWrite, painter.depthRangeFor3D);
for (const coord of tileIDs) {
const tile = sourceCache.getTile(coord);
const uniformValues = terrainRasterUniformValues(coord.projMatrix, 0);
terrain.setupElevationDraw(tile, program);
program.draw(context, gl.TRIANGLES, depthMode, StencilMode.disabled, ColorMode.unblended, CullFaceMode.backCCW,
uniformValues, "terrain_depth", terrain.gridBuffer, terrain.gridIndexBuffer, terrain.gridNoSkirtSegments);
}
}
function skirtHeight(zoom) {
// Skirt height calculation is heuristic: provided value hides
// seams between tiles and it is not too large: 9 at zoom 22, ~20000m at zoom 0.
return 6 * Math.pow(1.5, 22 - zoom);
}
function isEdgeTile(cid: CanonicalTileID, renderWorldCopies: boolean): boolean {
const numTiles = 1 << cid.z;
return (!renderWorldCopies && (cid.x === 0 || cid.x === numTiles - 1)) || cid.y === 0 || cid.y === numTiles - 1;
}
export {
VertexMorphing
};