Skip to content

Commit

Permalink
Add support for globe, terrain and fog in model layer (#496)
Browse files Browse the repository at this point in the history
* Add support for globe and terrain in model layer

* Flow and lint fixes

* Fixes for render tests

Added padded terrain tiles to avoid non-deterministic behaviour in tile borders in the render-tests

* Skip model fill-extrusion terrain tests as they are flaky in CI

* Address review comments
  • Loading branch information
jtorresfabra authored Mar 24, 2023
1 parent 672b6bb commit be4b7d8
Show file tree
Hide file tree
Showing 44 changed files with 1,716 additions and 64 deletions.
126 changes: 100 additions & 26 deletions 3d-style/data/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,18 @@ import type IndexBuffer from '../../src/gl/index_buffer.js';
import {ModelLayoutArray, TriangleIndexArray, NormalLayoutArray, TexcoordLayoutArray} from '../../src/data/array_types.js';
import {StructArray} from '../../src/util/struct_array.js';
import type VertexBuffer from '../../src/gl/vertex_buffer.js';
import type {Mat4, Vec3} from 'gl-matrix';
import type {Mat4, Vec3, Quat} from 'gl-matrix';
import type Context from "../../src/gl/context.js";
import {Aabb} from '../../src/util/primitives.js';
import {mat4} from 'gl-matrix';
import {mat4, vec4} from 'gl-matrix';
import {modelAttributes, normalAttributes, texcoordAttributes, color3fAttributes, color4fAttributes} from './model_attributes.js';
import type {TextureImage, TextureWrap, TextureFilter} from '../../src/render/texture.js';
import SegmentVector from '../../src/data/segment.js';
import Projection from '../../src/geo/projection/projection.js';
import {degToRad} from '../../src/util/util.js';
import {globeToMercatorTransition} from '../../src/geo/projection/globe_util.js';
import {number as interpolate} from '../../src/style-spec/util/interpolate.js';
import MercatorCoordinate, {getMetersPerPixelAtLatitude, getLatitudeScale, mercatorZfromAltitude} from '../../src/geo/mercator_coordinate.js';
import Transform from '../../src/geo/transform.js';
import {rotationScaleYZFlipMatrix, getBoxBottomFace, rotationFor3Points, convertModelMatrixForGlobe} from '../util/model_util.js';

export type Sampler = {
minFilter: TextureFilter;
Expand Down Expand Up @@ -90,7 +93,7 @@ export default class Model {
constructor(id: string, uri: string, position: [number, number], orientation: [number, number, number], nodes: Array<Node>) {
this.id = id;
this.uri = uri;
this.position = position !== undefined ? new LngLat(position[1], position[0]) : new LngLat(0, 0);
this.position = position !== undefined ? new LngLat(position[0], position[1]) : new LngLat(0, 0);

this.orientation = orientation !== undefined ? orientation : [0, 0, 0];
this.nodes = nodes;
Expand Down Expand Up @@ -123,51 +126,122 @@ export default class Model {
}
}

_rotationScaleYZFlipMatrix(out: Mat4, rotation: Vec3, scale: Vec3) {
mat4.identity(out);
mat4.rotateZ(out, out, degToRad(rotation[2]));
mat4.rotateX(out, out, degToRad(rotation[0]));
mat4.rotateY(out, out, degToRad(rotation[1]));
_positionModelOnTerrain(transform: Transform, rotationOnTerrain: Quat): number {
const elevation = transform.elevation;
if (!elevation) {
return 0.0;
}
const corners = Aabb.projectAabbCorners(this.aabb, this.matrix);
const meterToMercator = mercatorZfromAltitude(1, this.position.lat) * transform.worldSize;
const bottomFace = getBoxBottomFace(corners, meterToMercator);

const b0 = corners[bottomFace[0]];
const b1 = corners[bottomFace[1]];
const b2 = corners[bottomFace[2]];
const b3 = corners[bottomFace[3]];

mat4.scale(out, out, scale);
const e0 = elevation.getAtPointOrZero(new MercatorCoordinate(b0[0] / transform.worldSize, b0[1] / transform.worldSize), 0);
const e1 = elevation.getAtPointOrZero(new MercatorCoordinate(b1[0] / transform.worldSize, b1[1] / transform.worldSize), 0);
const e2 = elevation.getAtPointOrZero(new MercatorCoordinate(b2[0] / transform.worldSize, b2[1] / transform.worldSize), 0);
const e3 = elevation.getAtPointOrZero(new MercatorCoordinate(b3[0] / transform.worldSize, b3[1] / transform.worldSize), 0);

// gltf spec uses right handed coordinate space where +y is up. Coordinate space transformation matrix
// has to be created for the initial transform to our left handed coordinate space
const coordSpaceTransform = [
1, 0, 0, 0,
0, 0, 1, 0,
0, 1, 0, 0,
0, 0, 0, 1
];
const d03 = (e0 + e3) / 2;
const d12 = (e1 + e2) / 2;

mat4.multiply(out, out, coordSpaceTransform);
if (d03 > d12) {
if (e1 < e2) {
rotationFor3Points(rotationOnTerrain, b1, b3, b0, e1, e3, e0, meterToMercator);
} else {
rotationFor3Points(rotationOnTerrain, b2, b0, b3, e2, e0, e3, meterToMercator);
}
} else {
if (e0 < e3) {
rotationFor3Points(rotationOnTerrain, b0, b1, b2, e0, e1, e2, meterToMercator);
} else {
rotationFor3Points(rotationOnTerrain, b3, b2, b1, e3, e2, e1, meterToMercator);
}
}
return Math.max(d03, d12);
}

computeModelMatrix(painter: Painter, rotation: Vec3, scale: Vec3, translation: Vec3) {
computeModelMatrix(painter: Painter, rotation: Vec3, scale: Vec3, translation: Vec3, applyElevation: boolean, followTerrainSlope: boolean, viewportScale: boolean = false) {
const state = painter.transform;
const zoom = state.zoom;
const projectedPoint = state.project(this.position);
const modelMetersPerPixel = Projection.getMetersPerPixelAtLatitude(this.position.lat, zoom);
const modelMetersPerPixel = getMetersPerPixelAtLatitude(this.position.lat, zoom);
const modelPixelsPerMeter = 1.0 / modelMetersPerPixel;
mat4.identity(this.matrix);
const offset = [projectedPoint.x + translation[0] * modelPixelsPerMeter, projectedPoint.y + translation[1] * modelPixelsPerMeter, translation[2]];
mat4.translate(this.matrix, this.matrix, offset);
const scaleXY = [modelPixelsPerMeter, modelPixelsPerMeter, 1.0];
mat4.scale(this.matrix, this.matrix, scaleXY);
let scaleXY = 1.0;
let scaleZ = 1.0;
const worldSize = state.worldSize;
if (viewportScale) {
if (state.projection.name === 'mercator') {
let elevation = 0.0;
if (state.elevation) {
elevation = state.elevation.getAtPointOrZero(new MercatorCoordinate(projectedPoint.x / worldSize, projectedPoint.y / worldSize), 0.0);
}
const mercProjPos = vec4.transformMat4([], [projectedPoint.x, projectedPoint.y, elevation, 1.0], state.projMatrix);
const mercProjectionScale = mercProjPos[3] / state.cameraToCenterDistance;
const viewMetersPerPixel = getMetersPerPixelAtLatitude(state.center.lat, zoom);
scaleXY = mercProjectionScale;
scaleZ = mercProjectionScale * viewMetersPerPixel;
} else if (state.projection.name === 'globe') {
const globeMatrix = convertModelMatrixForGlobe(this.matrix, state);
const worldViewProjection = mat4.multiply([], state.projMatrix, globeMatrix);
const globeProjPos = [0, 0, 0, 1];
vec4.transformMat4(globeProjPos, globeProjPos, worldViewProjection);
const globeProjectionScale = globeProjPos[3] / state.cameraToCenterDistance;
const transition = globeToMercatorTransition(zoom);
const modelPixelConv = state.projection.pixelsPerMeter(this.position.lat, worldSize) * getMetersPerPixelAtLatitude(this.position.lat, zoom);
const viewPixelConv = state.projection.pixelsPerMeter(state.center.lat, worldSize) * getMetersPerPixelAtLatitude(state.center.lat, zoom);
const viewLatScale = getLatitudeScale(state.center.lat);
// Compensate XY size difference from model latitude, taking into account globe-mercator transition
scaleXY = globeProjectionScale / interpolate(modelPixelConv, viewLatScale, transition);
// Compensate height difference from model latitude.
// No interpolation, because the Z axis is fixed in globe projection.
scaleZ = globeProjectionScale * modelMetersPerPixel / modelPixelConv;
// In globe projection, zoom and scale do not match anymore.
// Use pixelScaleConversion to scale to correct worldSize.
scaleXY *= viewPixelConv;
scaleZ *= viewPixelConv;
}
} else {
scaleXY = modelPixelsPerMeter;
}

mat4.scale(this.matrix, this.matrix, [scaleXY, scaleXY, scaleZ]);

// When applying physics (rotation) we need to insert rotation matrix
// between model rotation and transforms above. Keep the intermediate results.
const modelMatrixBeforeRotationScaleYZFlip = this.matrix;
const modelMatrixBeforeRotationScaleYZFlip = [...this.matrix];

const orientation = this.orientation;

const rotationScaleYZFlip: Mat4 = [];
this._rotationScaleYZFlipMatrix(rotationScaleYZFlip,
rotationScaleYZFlipMatrix(rotationScaleYZFlip,
[orientation[0] + rotation[0],
orientation[1] + rotation[1],
orientation[2] + rotation[2]],
scale);
mat4.multiply(this.matrix, modelMatrixBeforeRotationScaleYZFlip, rotationScaleYZFlip);

if (applyElevation && state.elevation) {
let elevate = 0;
const rotateOnTerrain = [];
if (followTerrainSlope && state.elevation) {
elevate = this._positionModelOnTerrain(state, rotateOnTerrain);
const rotationOnTerrain = mat4.fromQuat([], rotateOnTerrain);
const appendRotation = mat4.multiply([], rotationOnTerrain, rotationScaleYZFlip);
mat4.multiply(this.matrix, modelMatrixBeforeRotationScaleYZFlip, appendRotation);
} else {
elevate = state.elevation.getAtPointOrZero(new MercatorCoordinate(projectedPoint.x / worldSize, projectedPoint.y / worldSize), 0.0);
}
if (elevate !== 0) {
this.matrix[14] += elevate;
}
}
}

_uploadTexture(texture: ModelTexture, context: Context,) {
Expand Down
57 changes: 44 additions & 13 deletions 3d-style/render/draw_model.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ import {modelUniformValues} from './program/model_program.js';
import type {Mesh, Node} from '../data/model.js';
import type {DynamicDefinesType} from '../../src/render/program/program_uniforms.js';

import Projection from '../../src/geo/projection/projection.js';
import Transform from '../../src/geo/transform.js';
import StencilMode from '../../src/gl/stencil_mode.js';
import ColorMode from '../../src/gl/color_mode.js';
import DepthMode from '../../src/gl/depth_mode.js';
import CullFaceMode from '../../src/gl/cull_face_mode.js';
import {mat4, vec3} from 'gl-matrix';
import type {Mat4} from 'gl-matrix';
import MercatorCoordinate from '../../src/geo/mercator_coordinate.js';
import MercatorCoordinate, {getMetersPerPixelAtLatitude} from '../../src/geo/mercator_coordinate.js';
import TextureSlots from './texture_slots.js';
import {convertModelMatrixForGlobe} from '../util/model_util.js';

export default drawModels;

Expand All @@ -33,6 +34,17 @@ type SortedMesh = {
nodeModelMatrix: Mat4;
}

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];
const scale = transform.cameraWorldSizeForFog / transform.worldSize;
const scaleMatrix = mat4.identity([]);
mat4.scale(scaleMatrix, scaleMatrix, [scale, scale, 1]);
mat4.multiply(fogMatrix, scaleMatrix, fogMatrix);
mat4.multiply(fogMatrix, transform.worldToFogMatrix, fogMatrix);
return fogMatrix;
}

function drawMesh(sortedMesh: SortedMesh, painter: Painter, layer: ModelStyleLayer, modelParameters: ModelParameters, stencilMode, colorMode) {

// early return if totally transparent
Expand All @@ -50,7 +62,12 @@ function drawMesh(sortedMesh: SortedMesh, painter: Painter, layer: ModelStyleLay
const material = mesh.material;
const pbr = material.pbrMetallicRoughness;

const lightingMatrix = mat4.multiply([], modelParameters.zScaleMatrix, sortedMesh.nodeModelMatrix);
let lightingMatrix;
if (painter.transform.projection.zAxisUnit === "pixels") {
lightingMatrix = [...sortedMesh.nodeModelMatrix];
} else {
lightingMatrix = mat4.multiply([], modelParameters.zScaleMatrix, sortedMesh.nodeModelMatrix);
}
mat4.multiply(lightingMatrix, modelParameters.negCameraPosMatrix, lightingMatrix);
const normalMatrix = mat4.invert([], lightingMatrix);
mat4.transpose(normalMatrix, normalMatrix);
Expand Down Expand Up @@ -142,7 +159,12 @@ function drawMesh(sortedMesh: SortedMesh, painter: Painter, layer: ModelStyleLay
definesValues.push('USE_STANDARD_DERIVATIVES');

const program = painter.useProgram('model', null, ((definesValues: any): DynamicDefinesType[]));
program.draw(context, context.gl.TRIANGLES, depthMode, stencilMode, colorMode, CullFaceMode.disabled,
if (painter.style.fog) {
const fogMatrix = fogMatrixForModel(sortedMesh.nodeModelMatrix, painter.transform);
definesValues.push('FOG');
painter.uploadCommonUniforms(context, program, null, new Float32Array(fogMatrix));
}
program.draw(context, context.gl.TRIANGLES, depthMode, stencilMode, colorMode, CullFaceMode.backCCW,
uniformValues, layer.id, mesh.vertexBuffer, mesh.indexBuffer, mesh.segments, layer.paint, painter.transform.zoom,
undefined, dynamicBuffers);
}
Expand All @@ -158,8 +180,15 @@ export function upload(painter: Painter, sourceCache: SourceCache) {
}
}

function prepareMeshes(node: Node, modelMatrix: Mat4, projectionMatrix: Mat4, modelIndex: number, transparentMeshes: Array<SortedMesh>, opaqueMeshes: Array<SortedMesh>) {
const nodeModelMatrix = mat4.multiply([], modelMatrix, node.matrix);
function prepareMeshes(transform: Transform, node: Node, modelMatrix: Mat4, projectionMatrix: Mat4, modelIndex: number, transparentMeshes: Array<SortedMesh>, opaqueMeshes: Array<SortedMesh>) {

let nodeModelMatrix;
if (transform.projection.name === 'globe') {
nodeModelMatrix = convertModelMatrixForGlobe(modelMatrix, transform);
} else {
nodeModelMatrix = [...modelMatrix];
}
mat4.multiply(nodeModelMatrix, nodeModelMatrix, node.matrix);
const worldViewProjection = mat4.multiply([], projectionMatrix, nodeModelMatrix);
if (node.meshes) {
for (const mesh of node.meshes) {
Expand All @@ -179,7 +208,7 @@ function prepareMeshes(node: Node, modelMatrix: Mat4, projectionMatrix: Mat4, mo
}
if (node.children) {
for (const child of node.children) {
prepareMeshes(child, modelMatrix, projectionMatrix, modelIndex, transparentMeshes, opaqueMeshes);
prepareMeshes(transform, child, modelMatrix, projectionMatrix, modelIndex, transparentMeshes, opaqueMeshes);
}
}
}
Expand All @@ -194,6 +223,7 @@ function drawModels(painter: Painter, sourceCache: SourceCache, layer: ModelStyl

const mercCameraPos = painter.transform.getFreeCameraOptions().position || new MercatorCoordinate(0, 0, 0);
const cameraPos = vec3.scale([], [mercCameraPos.x, mercCameraPos.y, mercCameraPos.z], painter.transform.worldSize);
vec3.negate(cameraPos, cameraPos);
const transparentMeshes: SortedMesh[] = [];
const opaqueMeshes: SortedMesh[] = [];
let modelIndex = 0;
Expand All @@ -203,19 +233,18 @@ function drawModels(painter: Painter, sourceCache: SourceCache, layer: ModelStyl
const scale = layer.paint.get('model-scale').constantOr((null: any));
const translation = layer.paint.get('model-translation').constantOr((null: any));
// update model matrices
model.computeModelMatrix(painter, rotation, scale, translation);
model.computeModelMatrix(painter, rotation, scale, translation, true, true, false);

// compute model parameters matrices
const negCameraPosMatrix = mat4.identity([]);
const modelMetersPerPixel = Projection.getMetersPerPixelAtLatitude(model.position.lat, painter.transform.zoom);
const modelMetersPerPixel = getMetersPerPixelAtLatitude(model.position.lat, painter.transform.zoom);
const modelPixelsPerMeter = 1.0 / modelMetersPerPixel;
const zScaleMatrix = mat4.fromScaling([], [1.0, 1.0, modelPixelsPerMeter]);
mat4.translate(negCameraPosMatrix, negCameraPosMatrix, vec3.negate(cameraPos, cameraPos));
mat4.translate(negCameraPosMatrix, negCameraPosMatrix, cameraPos);
const modelParameters = {zScaleMatrix, negCameraPosMatrix};
modelParametersVector.push(modelParameters);

for (const node of model.nodes) {
prepareMeshes(node, model.matrix, painter.transform.projMatrix, modelIndex, transparentMeshes, opaqueMeshes);
prepareMeshes(painter.transform, node, model.matrix, painter.transform.projMatrix, modelIndex, transparentMeshes, opaqueMeshes);
}
modelIndex++;
}
Expand All @@ -234,9 +263,11 @@ function drawModels(painter: Painter, sourceCache: SourceCache, layer: ModelStyl
for (const opaqueMesh of opaqueMeshes) {
// If we have layer opacity draw with two passes opaque meshes
drawMesh(opaqueMesh, painter, layer, modelParametersVector[opaqueMesh.modelIndex], StencilMode.disabled, ColorMode.disabled);
}
for (const opaqueMesh of opaqueMeshes) {
drawMesh(opaqueMesh, painter, layer, modelParametersVector[opaqueMesh.modelIndex], painter.stencilModeFor3D(), painter.colorModeForRenderPass());
painter.resetStencilClippingMasks();
}
painter.resetStencilClippingMasks();
}

// Draw transparent sorted meshes
Expand Down
Loading

0 comments on commit be4b7d8

Please sign in to comment.