diff --git a/3d-style/data/model.js b/3d-style/data/model.js index e3255433867..c296a9abdf8 100644 --- a/3d-style/data/model.js +++ b/3d-style/data/model.js @@ -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; @@ -90,7 +93,7 @@ export default class Model { constructor(id: string, uri: string, position: [number, number], orientation: [number, number, number], nodes: Array) { 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; @@ -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,) { diff --git a/3d-style/render/draw_model.js b/3d-style/render/draw_model.js index 97ee1512455..9cd31f273e2 100644 --- a/3d-style/render/draw_model.js +++ b/3d-style/render/draw_model.js @@ -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; @@ -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 @@ -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); @@ -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); } @@ -158,8 +180,15 @@ export function upload(painter: Painter, sourceCache: SourceCache) { } } -function prepareMeshes(node: Node, modelMatrix: Mat4, projectionMatrix: Mat4, modelIndex: number, transparentMeshes: Array, opaqueMeshes: Array) { - const nodeModelMatrix = mat4.multiply([], modelMatrix, node.matrix); +function prepareMeshes(transform: Transform, node: Node, modelMatrix: Mat4, projectionMatrix: Mat4, modelIndex: number, transparentMeshes: Array, opaqueMeshes: Array) { + + 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) { @@ -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); } } } @@ -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; @@ -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++; } @@ -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 diff --git a/3d-style/util/model_util.js b/3d-style/util/model_util.js new file mode 100644 index 00000000000..0d3e7a39c4e --- /dev/null +++ b/3d-style/util/model_util.js @@ -0,0 +1,204 @@ +// @flow + +import { + lngFromMercatorX, + latFromMercatorY, + mercatorZfromAltitude, + getMetersPerPixelAtLatitude +} from '../../src/geo/mercator_coordinate.js'; +import {getProjectionInterpolationT} from '../../src/geo/projection/adjustments.js'; +import {mat4, vec3, quat} from 'gl-matrix'; +import type {Mat4, Vec3, Quat} from 'gl-matrix'; +import {degToRad} from '../../src/util/util.js'; +import { + interpolateVec3, + globeToMercatorTransition, + globeECEFUnitsToPixelScale, + latLngToECEF, + GLOBE_RADIUS +} from '../../src/geo/projection/globe_util.js'; +import {number as interpolate} from '../../src/style-spec/util/interpolate.js'; +import Transform from '../../src/geo/transform.js'; +import assert from 'assert'; + +export function 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])); + + mat4.scale(out, out, scale); + + // 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 + ]; + + mat4.multiply(out, out, coordSpaceTransform); +} + +type BoxFace = { + corners: [number, number, number, number]; + dotProductWithUp: number; +} + +// corners are in world coordinates. +export function getBoxBottomFace(corners: Array, meterToMercator: number): [number, number, number, number] { + const zUp = [0, 0, 1]; + const boxFaces: BoxFace[] = [{corners: [0, 1, 3, 2], dotProductWithUp : 0}, + {corners: [1, 5, 2, 6], dotProductWithUp : 0}, + {corners: [0, 4, 1, 5], dotProductWithUp : 0}, + {corners: [2, 6, 3, 7], dotProductWithUp : 0}, + {corners: [4, 7, 5, 6], dotProductWithUp : 0}, + {corners: [0, 3, 4, 7], dotProductWithUp : 0}]; + for (const face of boxFaces) { + const p0 = corners[face.corners[0]]; + const p1 = corners[face.corners[1]]; + const p2 = corners[face.corners[2]]; + const a = [p1[0] - p0[0], p1[1] - p0[1], meterToMercator * (p1[2] - p0[2])]; + const b = [p2[0] - p0[0], p2[1] - p0[1], meterToMercator * (p2[2] - p0[2])]; + const normal = vec3.cross(a, a, b); + vec3.normalize(normal, normal); + face.dotProductWithUp = vec3.dot(normal, zUp); + } + + boxFaces.sort((a, b) => { + return a.dotProductWithUp - b.dotProductWithUp; + }); + return boxFaces[0].corners; +} + +export function rotationFor3Points(out: Quat, p0: Vec3, p1: Vec3, p2: Vec3, h0: number, h1: number, h2: number, meterToMercator: number): Quat { + const p0p1 = [p1[0] - p0[0], p1[1] - p0[1], 0.0]; + const p0p2 = [p2[0] - p0[0], p2[1] - p0[1], 0.0]; + // If model scale is zero, all bounding box points are identical and no rotation can be calculated + if (vec3.length(p0p1) < 1e-12 || vec3.length(p0p2) < 1e-12) { + return quat.identity(out); + } + const from = vec3.cross([], p0p1, p0p2); + vec3.normalize(from, from); + vec3.subtract(p0p2, p2, p0); + p0p1[2] = (h1 - h0) * meterToMercator; + p0p2[2] = (h2 - h0) * meterToMercator; + const to = p0p1; + vec3.cross(to, p0p1, p0p2); + vec3.normalize(to, to); + return quat.rotationTo(out, from, to); +} + +export function coordinateFrameAtEcef(ecef: Vec3): Mat4 { + const zAxis = [ecef[0], ecef[1], ecef[2]]; + let yAxis = [0.0, 1.0, 0.0]; + const xAxis = vec3.cross([], yAxis, zAxis); + vec3.cross(yAxis, zAxis, xAxis); + if (vec3.squaredLength(yAxis) === 0.0) { + // Coordinate space is ambiguous if the model is placed directly at north or south pole + yAxis = [0.0, 1.0, 0.0]; + vec3.cross(xAxis, zAxis, yAxis); + assert(vec3.squaredLength(xAxis) > 0.0); + } + vec3.normalize(xAxis, xAxis); + vec3.normalize(yAxis, yAxis); + vec3.normalize(zAxis, zAxis); + return [xAxis[0], xAxis[1], xAxis[2], 0.0, + yAxis[0], yAxis[1], yAxis[2], 0.0, + zAxis[0], zAxis[1], zAxis[2], 0.0, + ecef[0], ecef[1], ecef[2], 1.0]; +} + +export function convertModelMatrix(matrix: Mat4, transform: Transform, scaleWithViewport: boolean): Mat4 { + // The provided transformation matrix is expected to define model position and orientation in pixel units + // with the exception of z-axis being in meters. Converting this into globe-aware matrix requires following steps: + // 1. Take the (pixel) position from the last column of the matrix and convert it to lat&lng and then to + // ecef-presentation. + // 2. Scale the model from (px, px, m) units to ecef-units and apply pixels-per-meter correction. Also + // remove translation component from the matrix as it represents position in Mercator coordinates. + // 3. Compute coordinate frame at the desired lat&lng position by aligning coordinate axes x,y & z with + // the tangent plane at the said location. + // 4. Prepend the original matrix with the new coordinate frame matrix and apply translation in ecef-units. + // After this operation the matrix presents correct position in ecef-space + // 5. Multiply the matrix with globe matrix for getting the final pixel space position + const worldSize = transform.worldSize; + const position = [matrix[12], matrix[13], matrix[14]]; + const lat = latFromMercatorY(position[1] / worldSize); + const lng = lngFromMercatorX(position[0] / worldSize); + // Construct a matrix for scaling the original one to ecef space and removing the translation in mercator space + const mercToEcef = mat4.identity([]); + const sourcePixelsPerMeter = mercatorZfromAltitude(1, lat) * worldSize; + const pixelsPerMeterConversion = mercatorZfromAltitude(1, 0) * worldSize * getMetersPerPixelAtLatitude(lat, transform.zoom); + const pixelsToEcef = 1.0 / globeECEFUnitsToPixelScale(worldSize); + let scale = pixelsPerMeterConversion * pixelsToEcef; + if (scaleWithViewport) { + // Keep the size relative to viewport + const t = getProjectionInterpolationT(transform.projection, transform.zoom, transform.width, transform.height, 1024); + const projectionScaler = transform.projection.pixelSpaceConversion(transform.center.lat, worldSize, t); + scale = pixelsToEcef * projectionScaler; + } + // Construct coordinate space matrix at the provided location in ecef space. + const ecefCoord = latLngToECEF(lat, lng); + // add altitude + vec3.add(ecefCoord, ecefCoord, vec3.scale([], vec3.normalize([], ecefCoord), sourcePixelsPerMeter * scale * position[2])); + const ecefFrame = coordinateFrameAtEcef(ecefCoord); + mat4.scale(mercToEcef, mercToEcef, [scale, scale, scale * sourcePixelsPerMeter]); + mat4.translate(mercToEcef, mercToEcef, [-position[0], -position[1], -position[2]]); + const result = mat4.multiply([], transform.globeMatrix, ecefFrame); + mat4.multiply(result, result, mercToEcef); + mat4.multiply(result, result, matrix); + return result; +} + +// Computes a matrix for representing the provided transformation matrix (in mercator projection) in globe +export function mercatorToGlobeMatrix(matrix: Mat4, transform: Transform): Mat4 { + const worldSize = transform.worldSize; + + const pixelsPerMeterConversion = mercatorZfromAltitude(1, 0) * worldSize * getMetersPerPixelAtLatitude(transform.center.lat, transform.zoom); + const pixelsToEcef = pixelsPerMeterConversion / globeECEFUnitsToPixelScale(worldSize); + const pixelsPerMeter = mercatorZfromAltitude(1, transform.center.lat) * worldSize; + + const m = mat4.identity([]); + mat4.rotateY(m, m, degToRad(transform.center.lng)); + mat4.rotateX(m, m, degToRad(transform.center.lat)); + + mat4.translate(m, m, [0, 0, GLOBE_RADIUS]); + mat4.scale(m, m, [pixelsToEcef, pixelsToEcef, pixelsToEcef * pixelsPerMeter]); + + mat4.translate(m, m, [transform.point.x - 0.5 * worldSize, transform.point.y - 0.5 * worldSize, 0.0]); + mat4.multiply(m, m, matrix); + return mat4.multiply(m, transform.globeMatrix, m); +} + +function affineMatrixLerp(a: Mat4, b: Mat4, t: number): Mat4 { + // Interpolate each of the coordinate axes separately while also preserving their length + const lerpAxis = (ax: Vec3, bx: Vec3, t: number) => { + const axLen = vec3.length(ax); + const bxLen = vec3.length(bx); + const c = interpolateVec3(ax, bx, t); + return vec3.scale(c, c, 1.0 / vec3.length(c) * interpolate(axLen, bxLen, t)); + }; + + const xAxis = lerpAxis([a[0], a[1], a[2]], [b[0], b[1], b[2]], t); + const yAxis = lerpAxis([a[4], a[5], a[6]], [b[4], b[5], b[6]], t); + const zAxis = lerpAxis([a[8], a[9], a[10]], [b[8], b[9], b[10]], t); + const pos = interpolateVec3([a[12], a[13], a[14]], [b[12], b[13], b[14]], t); + + return [ + xAxis[0], xAxis[1], xAxis[2], 0, + yAxis[0], yAxis[1], yAxis[2], 0, + zAxis[0], zAxis[1], zAxis[2], 0, + pos[0], pos[1], pos[2], 1 + ]; +} + +export function convertModelMatrixForGlobe(matrix: Mat4, transform: Transform, scaleWithViewport: boolean = false): Mat4 { + const t = globeToMercatorTransition(transform.zoom); + const modelMatrix = convertModelMatrix(matrix, transform, scaleWithViewport); + if (t > 0.0) { + const mercatorMatrix = mercatorToGlobeMatrix(matrix, transform); + return affineMatrixLerp(modelMatrix, mercatorMatrix, t); + } + return modelMatrix; +} diff --git a/flow-typed/gl-matrix.js b/flow-typed/gl-matrix.js index 806b50a1a0b..fe1c0ff234a 100644 --- a/flow-typed/gl-matrix.js +++ b/flow-typed/gl-matrix.js @@ -24,7 +24,6 @@ declare module "gl-matrix" { dot(Vec3, Vec3): number, equals(Vec3, Vec3): boolean, exactEquals(Vec3, Vec3): boolean, - clone(T): T, normalize(T, Vec3): T, add(T, Vec3, Vec3): T, @@ -110,6 +109,7 @@ declare module "gl-matrix" { identity(T): T, rotateX(T, Quat, number): T, rotateY(T, Quat, number): T, - rotateZ(T, Quat, number): T + rotateZ(T, Quat, number): T, + rotationTo(T, Quat, Quat): T } } diff --git a/src/geo/mercator_coordinate.js b/src/geo/mercator_coordinate.js index 2e8e6669eca..cfc272ca66f 100644 --- a/src/geo/mercator_coordinate.js +++ b/src/geo/mercator_coordinate.js @@ -2,7 +2,10 @@ import LngLat, {earthCircumference} from '../geo/lng_lat.js'; import type {LngLatLike} from '../geo/lng_lat.js'; +import {clamp, degToRad} from '../util/util.js'; +const DEFAULT_MIN_ZOOM = 0; +const DEFAULT_MAX_ZOOM = 25.5; /* * The circumference at a line of latitude in meters. */ @@ -37,6 +40,16 @@ export function altitudeFromMercatorZ(z: number, y: number): number { export const MAX_MERCATOR_LATITUDE = 85.051129; +export function getLatitudeScale(lat: number): number { + return Math.cos(degToRad(clamp(lat, -MAX_MERCATOR_LATITUDE, MAX_MERCATOR_LATITUDE))); +} + +export function getMetersPerPixelAtLatitude(lat: number, zoom: number): number { + const constrainedZoom = clamp(zoom, DEFAULT_MIN_ZOOM, DEFAULT_MAX_ZOOM); + const constrainedScale = Math.pow(2.0, constrainedZoom); + return getLatitudeScale(lat) * earthCircumference / (constrainedScale * 512.0); +} + /** * Determine the Mercator scale factor for a given latitude, see * https://en.wikipedia.org/wiki/Mercator_projection#Scale_factor diff --git a/src/geo/projection/projection.js b/src/geo/projection/projection.js index 7ff19d4612a..1d7e433f189 100644 --- a/src/geo/projection/projection.js +++ b/src/geo/projection/projection.js @@ -1,6 +1,6 @@ // @flow -import LngLat, {earthCircumference} from '../lng_lat.js'; -import {MAX_MERCATOR_LATITUDE, mercatorZfromAltitude} from '../mercator_coordinate.js'; +import LngLat from '../lng_lat.js'; +import {mercatorZfromAltitude} from '../mercator_coordinate.js'; import Point from '@mapbox/point-geometry'; import {farthestPixelDistanceOnPlane} from './far_z.js'; import {mat4} from 'gl-matrix'; @@ -12,8 +12,6 @@ import type {Vec3} from 'gl-matrix'; import type MercatorCoordinate from '../mercator_coordinate.js'; import type {ProjectionSpecification} from '../../style-spec/types.js'; import type {CanonicalTileID, UnwrappedTileID} from '../../source/tile_id.js'; -import {clamp, degToRad} from '../../util/util.js'; -import {DEFAULT_MIN_ZOOM, DEFAULT_MAX_ZOOM} from '../transform.js'; export type ProjectedPoint = { x: number; @@ -62,16 +60,6 @@ export default class Projection { this.range = [3.5, 7]; } - static getLatitudeScale(lat: number): number { - return Math.cos(degToRad(clamp(lat, -MAX_MERCATOR_LATITUDE, MAX_MERCATOR_LATITUDE))); - } - - static getMetersPerPixelAtLatitude(lat: number, zoom: number): number { - const constrainedZoom = clamp(zoom, DEFAULT_MIN_ZOOM, DEFAULT_MAX_ZOOM); - const constrainedScale = Math.pow(2.0, constrainedZoom); - return this.getLatitudeScale(lat) * earthCircumference / (constrainedScale * 512.0); - } - project(lng: number, lat: number): ProjectedPoint { // eslint-disable-line return {x: 0, y: 0, z: 0}; // overriden in subclasses } diff --git a/src/render/fog.js b/src/render/fog.js index 69c47f5ddfc..c281dc3e069 100644 --- a/src/render/fog.js +++ b/src/render/fog.js @@ -53,14 +53,15 @@ export const fogUniformValues = ( frustumDirBl: [number, number, number], globePosition: [number, number, number], globeRadius: number, - viewport: [number, number] + viewport: [number, number], + fogMatrix: ?Float32Array ): UniformValues => { const tr = painter.transform; const fogColor = fog.properties.get('color').toArray01(); fogColor[3] = fogOpacity; // Update Alpha const temporalOffset = (painter.frameCounter / 1000.0) % 1; return { - 'u_fog_matrix': tileID ? tr.calculateFogTileMatrix(tileID) : painter.identityMat, + 'u_fog_matrix': tileID ? tr.calculateFogTileMatrix(tileID) : fogMatrix ? fogMatrix : painter.identityMat, 'u_fog_range': fog.getFovAdjustedRange(tr._fov), 'u_fog_color': fogColor, 'u_fog_horizon_blend': fog.properties.get('horizon-blend'), diff --git a/src/render/painter.js b/src/render/painter.js index 1d2bad1ab12..d31d9e61a97 100644 --- a/src/render/painter.js +++ b/src/render/painter.js @@ -1075,7 +1075,7 @@ class Painter { } } - uploadCommonUniforms(context: Context, program: Program<*>, tileID: ?UnwrappedTileID) { + uploadCommonUniforms(context: Context, program: Program<*>, tileID: ?UnwrappedTileID, fogMatrix: ?Float32Array) { this.uploadCommonLightUniforms(context, program); // Fog is not enabled when rendering to texture so we @@ -1099,7 +1099,8 @@ class Painter { [ this.transform.width * browser.devicePixelRatio, this.transform.height * browser.devicePixelRatio - ]); + ], + fogMatrix); program.setFogUniformValues(context, fogUniforms); } diff --git a/src/terrain/draw_terrain_raster.js b/src/terrain/draw_terrain_raster.js index ed773518fe9..5813e9a9772 100644 --- a/src/terrain/draw_terrain_raster.js +++ b/src/terrain/draw_terrain_raster.js @@ -172,7 +172,7 @@ function drawTerrainForGlobe(painter: Painter, terrain: Terrain, sourceCache: So batches.forEach(isWireframe => { const tr = painter.transform; - const skirtHeightValue = skirtHeight(tr.zoom) * terrain.exaggeration(); + const skirtHeightValue = skirtHeight(tr.zoom, terrain.exaggeration(), terrain.sourceCache._source.tileSize); // 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 @@ -300,7 +300,7 @@ function drawTerrainRaster(painter: Painter, terrain: Terrain, sourceCache: Sour 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 skirt = skirtHeight(tr.zoom, terrain.exaggeration(), terrain.sourceCache._source.tileSize); const batches = showWireframe ? [false, true] : [false]; @@ -380,10 +380,12 @@ function drawTerrainDepth(painter: Painter, terrain: Terrain, sourceCache: Sourc } } -function skirtHeight(zoom) { +function skirtHeight(zoom, terrainExaggeration, tileSize) { // 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); + if (terrainExaggeration === 0) return 0; + const exaggerationFactor = (terrainExaggeration < 1.0 && tileSize === 514) ? 0.25 / terrainExaggeration : 1.0; + return 6 * Math.pow(1.5, 22 - zoom) * Math.max(terrainExaggeration, 1.0) * exaggerationFactor; } function isEdgeTile(cid: CanonicalTileID, renderWorldCopies: boolean): boolean { diff --git a/src/util/primitives.js b/src/util/primitives.js index 6f8e5db6c52..a1b6dd6eef3 100644 --- a/src/util/primitives.js +++ b/src/util/primitives.js @@ -194,6 +194,15 @@ class Aabb { return Aabb.fromPoints(corners); } + static projectAabbCorners(aabb: Aabb, transform: Mat4): Array { + const corners = aabb.getCorners(); + + for (let i = 0; i < corners.length; ++i) { + vec3.transformMat4(corners[i], corners[i], transform); + } + return corners; + } + constructor(min_: Vec3, max_: Vec3) { this.min = min_; this.max = max_; diff --git a/test/ignores/all.js b/test/ignores/all.js index 94984500e94..55bf80dfe13 100644 --- a/test/ignores/all.js +++ b/test/ignores/all.js @@ -152,8 +152,13 @@ const skip = [ // fill-extrusion-rounded-roof not implemented in -js "render-tests/lighting-3d-mode/fill-extrusion/rounded-flat-roof", - // alpha textures not supported in js + // alpha textures not supported in -js "render-tests/model-layer/model-opacity-cutout-texture", + // GLTF interleaved arrays not supported in -js + "render-tests/model-layer/model-opacity-no-cutoff", + // terrain model tests are flaky in CI + "render-tests/model-layer/fill-extrusion--default-terrain-opacity", + "render-tests/model-layer/fill-extrusion--default-terrain", // icon-image-cross-fade not supported in -js "render-tests/measure-light/global-brightness-icon-image-fade", diff --git a/test/integration/models/dem/12-655-1582.terrain.512.png b/test/integration/models/dem/12-655-1582.terrain.512.png new file mode 100644 index 00000000000..af87be3771b Binary files /dev/null and b/test/integration/models/dem/12-655-1582.terrain.512.png differ diff --git a/test/integration/models/dem/13-1310-3166.terrain.514.png b/test/integration/models/dem/13-1310-3166.terrain.514.png new file mode 100644 index 00000000000..bcf8833b3a0 Binary files /dev/null and b/test/integration/models/dem/13-1310-3166.terrain.514.png differ diff --git a/test/integration/models/dem/14-2618-6334-terrain.514.png b/test/integration/models/dem/14-2618-6334-terrain.514.png new file mode 100644 index 00000000000..83da6454ba3 Binary files /dev/null and b/test/integration/models/dem/14-2618-6334-terrain.514.png differ diff --git a/test/integration/models/dem/8-128-128.terrain.514.png b/test/integration/models/dem/8-128-128.terrain.514.png new file mode 100644 index 00000000000..04900d94801 Binary files /dev/null and b/test/integration/models/dem/8-128-128.terrain.514.png differ diff --git a/test/integration/models/puck.gltf b/test/integration/models/puck.gltf new file mode 100644 index 00000000000..d2e15fe05ec --- /dev/null +++ b/test/integration/models/puck.gltf @@ -0,0 +1 @@ +{"accessors" : [{"bufferView" : 0, "byteOffset" : 0, "componentType" : 5123, "count" : 6, "type" : "SCALAR", "max" : [4], "min" : [0]}, {"bufferView" : 1, "byteOffset" : 0, "componentType" : 5126, "count" : 5, "type" : "VEC3", "max" : [10, 0, 10], "min" : [-10, 0, -10], "name" : "POSITION"}, {"bufferView" : 1, "byteOffset" : 12, "componentType" : 5126, "count" : 5, "type" : "VEC2", "max" : [1, -0], "min" : [0, -1], "name" : "TEXCOORD_0"}, {"bufferView" : 1, "byteOffset" : 20, "componentType" : 5126, "count" : 5, "type" : "VEC3", "max" : [0, 1.0000044107437134, 0], "min" : [0, 1.0000044107437134, 0], "name" : "NORMAL"}], "asset" : {"generator" : "Aspose.3D 21.10", "version" : "2.0"}, "buffers" : [{"uri" : "data:application/octet-stream;base64,AAABAAIAAAADAAQAAAAAAAAAIMEAAAAAAAAgQQAAgD8AAACAAAAAACUAgD8AAAAAAAAgQQAAAAAAACBBAACAPwAAgL8AAAAAJQCAPwAAAAAAACBBAAAAAAAAIMEAAAAAAACAvwAAAAAlAIA/AAAAAAAAIEEAAAAAAAAgwQAAAAAAAIC/AAAAACUAgD8AAAAAAAAgwQAAAAAAACDBAAAAAAAAAIAAAAAAJQCAPwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAA=", "byteLength" : 212}], "bufferViews" : [{"buffer" : 0, "byteOffset" : 0, "byteLength" : 12, "target" : 34963}, {"buffer" : 0, "byteOffset" : 16, "byteLength" : 192, "byteStride" : 32, "target" : 34962}], "images" : [{"uri" : "data:image/png;base64,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"}], "materials" : [{"name" : "", "pbrMetallicRoughness" : {"baseColorFactor" : [1.0, 1.0, 1.0, 1], "baseColorTexture" : {"index" : 0, "texCoord" : 0}, "metallicFactor" : 0, "roughnessFactor" : 0}, "emissiveFactor" : [0, 0, 0], "alphaMode" : "BLEND", "doubleSided" : false}], "meshes" : [{"primitives" : [{"attributes" : {"POSITION" : 1, "TEXCOORD_0" : 2, "NORMAL" : 3}, "indices" : 0, "material" : 0, "mode" : 4}], "name" : ""}], "nodes" : [{"mesh" : 0, "name" : ""}], "samplers" : [{"magFilter" : 9729, "minFilter" : 9729, "wrapS" : 10497, "wrapT" : 10497}], "scene" : 0, "scenes" : [{"nodes" : [0]}], "textures" : [{"sampler" : 0, "source" : 0}]} \ No newline at end of file diff --git a/test/integration/render-tests/model-layer/fill-extrusion--default-terrain-opacity/expected.png b/test/integration/render-tests/model-layer/fill-extrusion--default-terrain-opacity/expected.png new file mode 100644 index 00000000000..7ba07a6b0a7 Binary files /dev/null and b/test/integration/render-tests/model-layer/fill-extrusion--default-terrain-opacity/expected.png differ diff --git a/test/integration/render-tests/model-layer/fill-extrusion--default-terrain-opacity/style.json b/test/integration/render-tests/model-layer/fill-extrusion--default-terrain-opacity/style.json new file mode 100644 index 00000000000..9c71c40a281 --- /dev/null +++ b/test/integration/render-tests/model-layer/fill-extrusion--default-terrain-opacity/style.json @@ -0,0 +1,166 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 1024, + "height": 1024, + "allowed": 0.0003 + } + }, + "center": [ + 0.0001, + 0.0001 + ], + "pitch": 78, + "zoom": 22, + "bearing": 18, + "terrain": { + "source": "mapbox-dem", + "exaggeration": 0.1 + }, + "sources": { + "mapbox-dem": { + "type": "raster-dem", + "maxzoom": 18, + "tileSize": 514, + "tiles": ["local://models/dem/14-2618-6334-terrain.514.png"] + }, + "model": { + "type": "model", + "models": { + "model-1" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.0001, 0.0001], + "orientation": [0, 0, 45] + }, + "model-2" : { + "uri": "local://models/cubes-depth.gltf", + "position": [0.00006, 0.0001], + "orientation": [0, 0, 90] + }, + "model-3" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00006, 0.00015], + "orientation": [0, 0, 180] + }, + "model-4" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00006, 0.00005], + "orientation": [0, 0, 180] + }, + "model-5" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00010, 0.00005], + "orientation": [0, 0, 135] + }, + "model-6" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00014, 0.0001], + "orientation": [0, 0, 45] + }, + "model-7" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00014, 0.00015], + "orientation": [0, 0, -180] + }, + "model-8" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.0001, 0.00015], + "orientation": [0, 0, -180] + } + } + }, + "building1": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "type": "building", + "height": 5 + }, + "geometry": { + "type": "Polygon", + "coordinates": [[ + [ 0.00006, 0.00015 ], + [ 0.0001, 0.00015 ], + [ 0.0001, 0.00011 ], + [ 0.00006, 0.00011 ], + [ 0.00006, 0.00015 ] + ]] + } + } + ] + } + }, + "building2": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "type": "building", + "height": 7 + }, + "geometry": { + "type": "Polygon", + "coordinates": [[ + [ 0.0001, 0.0001 ], + [ 0.00014, 0.0001 ], + [ 0.00014, 0.00006 ], + [ 0.0001, 0.00006 ], + [ 0.0001, 0.0001 ] + ]] + } + } + ] + } + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "terrain-hillshade", + "type": "hillshade", + "source": "mapbox-dem" + }, + { + "id": "extrusion1", + "type": "fill-extrusion", + "source": "building1", + "paint": { + "fill-extrusion-color": "blue", + "fill-extrusion-height": ["get", "height"], + "fill-extrusion-opacity": 0.5 + } + }, + { + "id": "model", + "type": "model", + "source": "model", + "paint": { + "model-opacity": 0.6 + } + }, + { + "id": "extrusion2", + "type": "fill-extrusion", + "source": "building2", + "paint": { + "fill-extrusion-color": "pink", + "fill-extrusion-height": ["get", "height"], + "fill-extrusion-opacity": 0.5 + } + } + ] +} diff --git a/test/integration/render-tests/model-layer/fill-extrusion--default-terrain/expected.png b/test/integration/render-tests/model-layer/fill-extrusion--default-terrain/expected.png new file mode 100644 index 00000000000..e9819f8dc56 Binary files /dev/null and b/test/integration/render-tests/model-layer/fill-extrusion--default-terrain/expected.png differ diff --git a/test/integration/render-tests/model-layer/fill-extrusion--default-terrain/style.json b/test/integration/render-tests/model-layer/fill-extrusion--default-terrain/style.json new file mode 100644 index 00000000000..4d04fdf8899 --- /dev/null +++ b/test/integration/render-tests/model-layer/fill-extrusion--default-terrain/style.json @@ -0,0 +1,163 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 1024, + "height": 1024, + "allowed": 0.00026 + } + }, + "center": [ + 0.0001, + 0.0001 + ], + "pitch": 78, + "zoom": 22, + "bearing": 18, + "terrain": { + "source": "mapbox-dem", + "exaggeration": 0.1 + }, + "sources": { + "mapbox-dem": { + "type": "raster-dem", + "maxzoom": 18, + "tileSize": 514, + "tiles": ["local://models/dem/14-2618-6334-terrain.514.png"] + }, + "model": { + "type": "model", + "models": { + "model-1" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.0001, 0.0001], + "orientation": [0, 0, 45] + }, + "model-2" : { + "uri": "local://models/cubes-depth.gltf", + "position": [0.00006, 0.0001], + "orientation": [0, 0, 90] + }, + "model-3" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00006, 0.00015], + "orientation": [0, 0, 180] + }, + "model-4" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00006, 0.00005], + "orientation": [0, 0, 180] + }, + "model-5" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00010, 0.00005], + "orientation": [0, 0, 135] + }, + "model-6" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00014, 0.0001], + "orientation": [0, 0, 45] + }, + "model-7" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00014, 0.00015], + "orientation": [0, 0, -180] + }, + "model-8" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.0001, 0.00015], + "orientation": [0, 0, -180] + } + } + }, + "building1": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "type": "building", + "height": 5 + }, + "geometry": { + "type": "Polygon", + "coordinates": [[ + [ 0.00006, 0.00015 ], + [ 0.0001, 0.00015 ], + [ 0.0001, 0.00011 ], + [ 0.00006, 0.00011 ], + [ 0.00006, 0.00015 ] + ]] + } + } + ] + } + }, + "building2": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "type": "building", + "height": 7 + }, + "geometry": { + "type": "Polygon", + "coordinates": [[ + [ 0.0001, 0.0001 ], + [ 0.00014, 0.0001 ], + [ 0.00014, 0.00006 ], + [ 0.0001, 0.00006 ], + [ 0.0001, 0.0001 ] + ]] + } + } + ] + } + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "terrain-hillshade", + "type": "hillshade", + "source": "mapbox-dem" + }, + { + "id": "extrusion1", + "type": "fill-extrusion", + "source": "building1", + "paint": { + "fill-extrusion-color": "blue", + "fill-extrusion-height": ["get", "height"], + "fill-extrusion-opacity": 0.5 + } + }, + { + "id": "model", + "type": "model", + "source": "model" + }, + { + "id": "extrusion2", + "type": "fill-extrusion", + "source": "building2", + "paint": { + "fill-extrusion-color": "pink", + "fill-extrusion-height": ["get", "height"], + "fill-extrusion-opacity": 0.5 + } + } + ] +} diff --git a/test/integration/render-tests/model-layer/fill-extrusion--default/expected.png b/test/integration/render-tests/model-layer/fill-extrusion--default/expected.png new file mode 100644 index 00000000000..4edf7efdf91 Binary files /dev/null and b/test/integration/render-tests/model-layer/fill-extrusion--default/expected.png differ diff --git a/test/integration/render-tests/model-layer/fill-extrusion--default/style.json b/test/integration/render-tests/model-layer/fill-extrusion--default/style.json new file mode 100644 index 00000000000..d0a0e37175e --- /dev/null +++ b/test/integration/render-tests/model-layer/fill-extrusion--default/style.json @@ -0,0 +1,141 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 1024, + "height": 1024, + "allowed": 0.0002 + } + }, + "center": [ + 0.0001, + 0.0001 + ], + "pitch": 60, + "zoom": 22, + "bearing": 18, + "sources": { + "model": { + "type": "model", + "models": { + "model-1" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.0001, 0.0001], + "orientation": [0, 0, 45] + }, + "model-2" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00006, 0.0001], + "orientation": [0, 0, 90] + }, + "model-3" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00006, 0.00015], + "orientation": [0, 0, 180] + }, + "model-4" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00006, 0.00005], + "orientation": [0, 0, 180] + }, + "model-5" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00010, 0.00005], + "orientation": [0, 0, 135] + }, + "model-6" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00014, 0.0001], + "orientation": [0, 0, 45] + }, + "model-7" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00014, 0.00015], + "orientation": [0, 0, -180] + }, + "model-8" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.0001, 0.00015], + "orientation": [0, 0, -180] + } + } + }, + "building1": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "type": "building", + "height": 5 + }, + "geometry": { + "type": "Polygon", + "coordinates": [[ + [ 0.00006, 0.00015 ], + [ 0.0001, 0.00015 ], + [ 0.0001, 0.00011 ], + [ 0.00006, 0.00011 ], + [ 0.00006, 0.00015 ] + ]] + } + } + ] + } + }, + "building2": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "type": "building", + "height": 7 + }, + "geometry": { + "type": "Polygon", + "coordinates": [[ + [ 0.0001, 0.0001 ], + [ 0.00014, 0.0001 ], + [ 0.00014, 0.00006 ], + [ 0.0001, 0.00006 ], + [ 0.0001, 0.0001 ] + ]] + } + } + ] + } + } + }, + "layers": [ + { + "id": "extrusion1", + "type": "fill-extrusion", + "source": "building1", + "paint": { + "fill-extrusion-color": "blue", + "fill-extrusion-height": ["get", "height"], + "fill-extrusion-opacity": 0.5 + } + }, + { + "id": "model", + "type": "model", + "source": "model" + }, + { + "id": "extrusion2", + "type": "fill-extrusion", + "source": "building2", + "paint": { + "fill-extrusion-color": "pink", + "fill-extrusion-height": ["get", "height"], + "fill-extrusion-opacity": 0.5 + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/model-layer/model-fog-default/expected.png b/test/integration/render-tests/model-layer/model-fog-default/expected.png new file mode 100644 index 00000000000..1023c16c487 Binary files /dev/null and b/test/integration/render-tests/model-layer/model-fog-default/expected.png differ diff --git a/test/integration/render-tests/model-layer/model-fog-default/style.json b/test/integration/render-tests/model-layer/model-fog-default/style.json new file mode 100644 index 00000000000..91a3d72ad95 --- /dev/null +++ b/test/integration/render-tests/model-layer/model-fog-default/style.json @@ -0,0 +1,74 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 1024, + "height": 1024 + } + }, + "center": [ + -0.00020, + 0 + ], + "pitch": 80, + "bearing": -85, + "zoom": 21, + "terrain": { + "source": "mapbox-dem" + }, + "sources": { + "mapbox-dem": { + "type": "raster-dem", + "maxzoom": 14, + "tileSize": 514, + "tiles": ["local://models/dem/8-128-128.terrain.514.png"] + }, + "model": { + "type": "model", + "models": { + "model-1" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0, 0] + }, + "model-on-roof" : { + "uri": "local://models/low-poly-car.gltf", + "position": [-0.00010, 0] + }, + "model-on-bumper-1" : { + "uri": "local://models/low-poly-car.gltf", + "position": [-0.00030, 0] + }, + "model-on-roof-resting" : { + "uri": "local://models/low-poly-car.gltf", + "position": [-0.00060, 0] + }, + "model-on-bumper-resting" : { + "uri": "local://models/low-poly-car.gltf", + "position": [-0.00100, 0] + }, + "model-on-side1" : { + "uri": "local://models/low-poly-car.gltf", + "position": [-0.00200, 0] + }, + "model-on-side2" : { + "uri": "local://models/low-poly-car.gltf", + "position": [-0.00400, 0] + }, + "model-on-side3" : { + "uri": "local://models/low-poly-car.gltf", + "position": [-0.01000, 0] + } + } + } + }, + "fog": { + "star-intensity": 0 + }, + "layers": [ + { + "id": "model", + "type": "model", + "source": "model" + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/model-layer/model-opacity-no-cutoff/expected.png b/test/integration/render-tests/model-layer/model-opacity-no-cutoff/expected.png new file mode 100644 index 00000000000..b4d39c72e7c Binary files /dev/null and b/test/integration/render-tests/model-layer/model-opacity-no-cutoff/expected.png differ diff --git a/test/integration/render-tests/model-layer/model-opacity-no-cutoff/style.json b/test/integration/render-tests/model-layer/model-opacity-no-cutoff/style.json new file mode 100644 index 00000000000..1881db7a664 --- /dev/null +++ b/test/integration/render-tests/model-layer/model-opacity-no-cutoff/style.json @@ -0,0 +1,44 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 512, + "height": 512 + } + }, + "center": [ + 0, + 0 + ], + "pitch": 50, + "zoom": 20, + "sources": { + "model": { + "type": "model", + "models": { + "model-1" : { + "uri": "local://models/puck.gltf", + "position": [0, 0], + "orientation": [0, 0, 0] + } + } + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "model", + "type": "model", + "source": "model", + "paint": { + "model-opacity" : 0.5 + } + } + ] +} diff --git a/test/integration/render-tests/model-layer/model-padded-terrain/expected.png b/test/integration/render-tests/model-layer/model-padded-terrain/expected.png new file mode 100644 index 00000000000..e1838a9cd8f Binary files /dev/null and b/test/integration/render-tests/model-layer/model-padded-terrain/expected.png differ diff --git a/test/integration/render-tests/model-layer/model-padded-terrain/style.json b/test/integration/render-tests/model-layer/model-padded-terrain/style.json new file mode 100644 index 00000000000..6af8e006206 --- /dev/null +++ b/test/integration/render-tests/model-layer/model-padded-terrain/style.json @@ -0,0 +1,87 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 512, + "height": 512, + "allowed": 0.00025 + } + }, + "pitch": 75, + "zoom": 22, + "bearing": 264, + "center": [ + -122.4025, + 37.7842 + ], + "terrain": { + "source": "mapbox-dem", + "exaggeration": 8.0 + }, + "sources": { + "mapbox-dem": { + "type": "raster-dem", + "maxzoom": 18, + "tileSize": 514, + "tiles": ["local://models/dem/{z}-{x}-{y}.terrain.514.png"] + }, + "mapbox": { + "type": "vector", + "maxzoom": 15, + "tiles": [ + "local://models/vector/{z}-{x}-{y}.vector.pbf" + ] + }, + "model": { + "type": "model", + "models": { + "model-1" : { + "uri": "local://models/low-poly-car.gltf", + "position": [ + -122.4025, + 37.7842 + ], + "orientation": [0, 0, 90] + } + } + }, + "geojson": { + "type": "geojson", + "data": { + "type": "MultiPoint", + "coordinates": [ + [-122.4025, 37.7842] + ] + } + } + }, + "layers": [ + { + "id": "road", + "type": "line", + "source": "mapbox", + "source-layer": "road", + "paint": { + "line-color": "lightyellow", + "line-width": 10, + "line-opacity": 0.3 + } + }, + { + "id": "circle", + "type": "circle", + "source": "geojson", + "paint": { + "circle-radius": 200, + "circle-color": "#ffffff", + "circle-pitch-alignment": "map", + "circle-blur": 1 + } + }, + { + "id": "model", + "type": "model", + "source": "model" + } + ] +} diff --git a/test/integration/render-tests/model-layer/models-on-globe-near-pole/expected.png b/test/integration/render-tests/model-layer/models-on-globe-near-pole/expected.png new file mode 100644 index 00000000000..1b2bd214781 Binary files /dev/null and b/test/integration/render-tests/model-layer/models-on-globe-near-pole/expected.png differ diff --git a/test/integration/render-tests/model-layer/models-on-globe-near-pole/style.json b/test/integration/render-tests/model-layer/models-on-globe-near-pole/style.json new file mode 100644 index 00000000000..3311a9324c0 --- /dev/null +++ b/test/integration/render-tests/model-layer/models-on-globe-near-pole/style.json @@ -0,0 +1,63 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 1024, + "height": 1024, + "operations": [ + ["setProjection", "globe"], + ["wait"] + ] + } + }, + "center": [ 0, 70 ], + "zoom": 2.50, + "pitch": 40, + "fog": { + "star-intensity": 0.0 + }, + "sources": { + "model": { + "type": "model", + "models": { + "model-0" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0, 70] + }, + "model-1" : { + "uri": "local://models/low-poly-car.gltf", + "position": [-40, 70] + }, + "model-2" : { + "uri": "local://models/low-poly-car.gltf", + "position": [-80, 70] + }, + "model-3" : { + "uri": "local://models/low-poly-car.gltf", + "position": [-120, 70] + }, + "model-4" : { + "uri": "local://models/low-poly-car.gltf", + "position": [-160, 70] + } + } + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "brown" + } + }, + { + "id": "model", + "type": "model", + "source": "model", + "paint": { + "model-scale" : [200000, 200000, 200000] + } + } + ] +} diff --git a/test/integration/render-tests/model-layer/models-on-globe-nested/expected.png b/test/integration/render-tests/model-layer/models-on-globe-nested/expected.png new file mode 100644 index 00000000000..f705e1a81b9 Binary files /dev/null and b/test/integration/render-tests/model-layer/models-on-globe-nested/expected.png differ diff --git a/test/integration/render-tests/model-layer/models-on-globe-nested/style.json b/test/integration/render-tests/model-layer/models-on-globe-nested/style.json new file mode 100644 index 00000000000..429a5eec081 --- /dev/null +++ b/test/integration/render-tests/model-layer/models-on-globe-nested/style.json @@ -0,0 +1,74 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 1024, + "height": 1024, + "operations": [ + ["setProjection", "globe"], + ["wait"] + ] + } + }, + "center": [ -20, 20 ], + "zoom": 2.50, + "pitch": 45, + "bearing": 45, + "fog": { + "star-intensity": 0.0 + }, + "sources": { + "model": { + "type": "model", + "models": { + "model-0" : { + "uri": "local://models/nested-cubes.glb", + "position": [0, 0], + "orientation": [0, 0, 45] + }, + "model-1" : { + "uri": "local://models/nested-cubes.glb", + "position": [0, 20], + "orientation": [0, 0, 0] + } + } + }, + "model-elevated": { + "type": "model", + "models": { + "model-0" : { + "uri": "local://models/nested-cubes.glb", + "position": [0, 40], + "orientation": [0, 0, 45] + } + } + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "model", + "type": "model", + "source": "model", + "paint": { + "model-scale" : [200000, 200000, 200000], + "model-translation": [0, 0, 200000] + } + }, + { + "id": "model-elevated", + "type": "model", + "source": "model-elevated", + "paint": { + "model-scale" : [200000, 200000, 200000], + "model-translation": [0, 0, 500000] + } + } + ] +} diff --git a/test/integration/render-tests/model-layer/models-on-globe/expected.png b/test/integration/render-tests/model-layer/models-on-globe/expected.png new file mode 100644 index 00000000000..a94c99abdee Binary files /dev/null and b/test/integration/render-tests/model-layer/models-on-globe/expected.png differ diff --git a/test/integration/render-tests/model-layer/models-on-globe/style.json b/test/integration/render-tests/model-layer/models-on-globe/style.json new file mode 100644 index 00000000000..2d4fa8a3b3b --- /dev/null +++ b/test/integration/render-tests/model-layer/models-on-globe/style.json @@ -0,0 +1,83 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 1024, + "height": 1024, + "operations": [ + ["setProjection", "globe"], + ["wait"] + ] + } + }, + "center": [ 0, 0 ], + "zoom": 3.00, + "pitch": 60, + "fog": { + "star-intensity": 0.0 + }, + "sources": { + "model": { + "type": "model", + "models": { + "model-1" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0, 0], + "orientation": [0, 0, 45] + }, + "model-on-roof" : { + "uri": "local://models/low-poly-car.gltf", + "position": [-20, 0], + "orientation": [0, 140, 90] + }, + "model-on-bumper-1" : { + "uri": "local://models/low-poly-car.gltf", + "position": [-20, 20], + "orientation": [0, 110, 180] + }, + "model-on-roof-resting" : { + "uri": "local://models/low-poly-car.gltf", + "position": [-20, -20], + "orientation": [0, 180, 180] + }, + "model-on-bumper-resting" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0, -20], + "orientation": [0, 90, 180] + }, + "model-on-side1" : { + "uri": "local://models/low-poly-car.gltf", + "position": [20, 0], + "orientation": [-90, 0, 45] + }, + "model-on-side2" : { + "uri": "local://models/low-poly-car.gltf", + "position": [20, 20], + "orientation": [-45, 0, 45] + }, + "model-on-side3" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00000, 20], + "orientation": [0, 45, 45] + } + } + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "model", + "type": "model", + "source": "model", + "paint": { + "model-scale" : [200000, 200000, 200000] + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/model-layer/multiple-models-terrain-fog/expected.png b/test/integration/render-tests/model-layer/multiple-models-terrain-fog/expected.png new file mode 100644 index 00000000000..5c3102d1f2b Binary files /dev/null and b/test/integration/render-tests/model-layer/multiple-models-terrain-fog/expected.png differ diff --git a/test/integration/render-tests/model-layer/multiple-models-terrain-fog/style.json b/test/integration/render-tests/model-layer/multiple-models-terrain-fog/style.json new file mode 100644 index 00000000000..e56a68afaa4 --- /dev/null +++ b/test/integration/render-tests/model-layer/multiple-models-terrain-fog/style.json @@ -0,0 +1,94 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 1024, + "height": 1024 + } + }, + "center": [ + 0.0001, + 0.0001 + ], + "pitch": 65, + "bearing": -90, + "zoom": 21, + "terrain": { + "source": "mapbox-dem", + "exaggeration": 0.1 + }, + "fog": { + "star-intensity": 0.0, + "color": "green", + "range": [-0.25, 1.5] + }, + "sources": { + "mapbox-dem": { + "type": "raster-dem", + "maxzoom": 18, + "tileSize": 514, + "tiles": ["local://models/dem/14-2618-6334-terrain.514.png"] + }, + "model": { + "type": "model", + "models": { + "model-1" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.0001, 0.0001], + "orientation": [0, 0, 45] + }, + "model-2" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00006, 0.0001], + "orientation": [0, 0, 90] + }, + "model-3" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00006, 0.00015], + "orientation": [0, 0, 180] + }, + "model-4" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00006, 0.00005], + "orientation": [0, 0, 180] + }, + "model-5" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00010, 0.00005], + "orientation": [0, 0, 135] + }, + "model-6" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00014, 0.0001], + "orientation": [0, 0, 45] + }, + "model-7" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00014, 0.00015], + "orientation": [0, 0, -180] + }, + "model-8" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.0001, 0.00015], + "orientation": [0, 0, -180] + } + } + } + }, + "layers": [ + { + "id": "background", + "type": "background" + }, + { + "id": "terrain-hillshade", + "type": "hillshade", + "source": "mapbox-dem" + }, + { + "id": "model", + "type": "model", + "source": "model" + } + ] +} diff --git a/test/integration/render-tests/model-layer/multiple-models-terrain/expected.png b/test/integration/render-tests/model-layer/multiple-models-terrain/expected.png new file mode 100644 index 00000000000..382347fab11 Binary files /dev/null and b/test/integration/render-tests/model-layer/multiple-models-terrain/expected.png differ diff --git a/test/integration/render-tests/model-layer/multiple-models-terrain/style.json b/test/integration/render-tests/model-layer/multiple-models-terrain/style.json new file mode 100644 index 00000000000..365cfbee994 --- /dev/null +++ b/test/integration/render-tests/model-layer/multiple-models-terrain/style.json @@ -0,0 +1,85 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 1024, + "height": 1024, + "allowed": 0.00025 + } + }, + "center": [ + 0.0001, + 0.0001 + ], + "pitch": 50, + "zoom": 24, + "terrain": { + "source": "mapbox-dem", + "exaggeration": 0.1 + }, + "sources": { + "mapbox-dem": { + "type": "raster-dem", + "maxzoom": 18, + "tileSize": 514, + "tiles": ["local://models/dem/14-2618-6334-terrain.514.png"] + }, + "model": { + "type": "model", + "models": { + "model-1" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.0001, 0.0001], + "orientation": [0, 0, 45] + }, + "model-2" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00006, 0.0001], + "orientation": [0, 0, 90] + }, + "model-3" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00006, 0.00015], + "orientation": [0, 0, 180] + }, + "model-4" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00006, 0.00005], + "orientation": [0, 0, 180] + }, + "model-5" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00010, 0.00005], + "orientation": [0, 0, 135] + }, + "model-6" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00014, 0.0001], + "orientation": [0, 0, 45] + }, + "model-7" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00014, 0.00015], + "orientation": [0, 0, -180] + }, + "model-8" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.0001, 0.00015], + "orientation": [0, 0, -180] + } + } + } + }, + "layers": [ + { + "id": "terrain-hillshade", + "type": "hillshade", + "source": "mapbox-dem" + }, + { + "id": "model", + "type": "model", + "source": "model" + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/model-layer/multiple-models-translated-missing-terrain/expected.png b/test/integration/render-tests/model-layer/multiple-models-translated-missing-terrain/expected.png new file mode 100644 index 00000000000..a16ea3fb52b Binary files /dev/null and b/test/integration/render-tests/model-layer/multiple-models-translated-missing-terrain/expected.png differ diff --git a/test/integration/render-tests/model-layer/multiple-models-translated-missing-terrain/style.json b/test/integration/render-tests/model-layer/multiple-models-translated-missing-terrain/style.json new file mode 100644 index 00000000000..1d9379d8a65 --- /dev/null +++ b/test/integration/render-tests/model-layer/multiple-models-translated-missing-terrain/style.json @@ -0,0 +1,82 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 1024, + "height": 1024, + "allowed": 0.00038 + } + }, + "center": [ + 0, + 0 + ], + "pitch": 45, + "zoom": 24, + "terrain": { + "source": "mapbox-dem" + }, + "sources": { + "mapbox-dem": { + "type": "raster-dem", + "maxzoom": 14, + "tileSize": 514, + "tiles": ["local://models/dem/8-128-128.terrain.514.png"] + }, + "model": { + "type": "model", + "models": { + "model-1" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0, 0], + "orientation": [0, 0, 45] + }, + "model-on-roof" : { + "uri": "local://models/low-poly-car.gltf", + "position": [-0.00004, 0], + "orientation": [0, 140, 90] + }, + "model-on-roof-1" : { + "uri": "local://models/low-poly-car.gltf", + "position": [-0.00004, 0.00005], + "orientation": [0, 110, 180] + }, + "model-on-roof-resting" : { + "uri": "local://models/low-poly-car.gltf", + "position": [-0.00004, -0.00005], + "orientation": [0, 180, 180] + }, + "model-on-bumper-resting" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0, -0.00005], + "orientation": [0, 90, 180] + }, + "model-on-side" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00004, 0], + "orientation": [-90, 0, 45] + }, + "model-on-corner" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00004, 0.00005], + "orientation": [-45, 135, 0] + }, + "model-on-corner-1" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00000, 0.00005], + "orientation": [45, 45, 0] + } + } + } + }, + "layers": [ + { + "id": "model", + "type": "model", + "source": "model", + "paint": { + "model-translation" : [0, 0, 2] + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/model-layer/multiple-models-zero-terrain/expected.png b/test/integration/render-tests/model-layer/multiple-models-zero-terrain/expected.png new file mode 100644 index 00000000000..c937b388624 Binary files /dev/null and b/test/integration/render-tests/model-layer/multiple-models-zero-terrain/expected.png differ diff --git a/test/integration/render-tests/model-layer/multiple-models-zero-terrain/style.json b/test/integration/render-tests/model-layer/multiple-models-zero-terrain/style.json new file mode 100644 index 00000000000..6662e8173b1 --- /dev/null +++ b/test/integration/render-tests/model-layer/multiple-models-zero-terrain/style.json @@ -0,0 +1,79 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 1024, + "height": 1024, + "allowed": 0.00022 + } + }, + "center": [ + 0, + 0 + ], + "pitch": 45, + "zoom": 24, + "terrain": { + "source": "mapbox-dem" + }, + "sources": { + "mapbox-dem": { + "type": "raster-dem", + "maxzoom": 14, + "tileSize": 514, + "tiles": ["local://models/dem/8-128-128.terrain.514.png"] + }, + "model": { + "type": "model", + "models": { + "model-1" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0, 0], + "orientation": [0, 0, 45] + }, + "model-on-roof" : { + "uri": "local://models/low-poly-car.gltf", + "position": [-0.00004, 0], + "orientation": [0, 140, 90] + }, + "model-on-bumper-1" : { + "uri": "local://models/low-poly-car.gltf", + "position": [-0.00004, 0.00005], + "orientation": [0, 110, 180] + }, + "model-on-roof-resting" : { + "uri": "local://models/low-poly-car.gltf", + "position": [-0.00004, -0.00005], + "orientation": [0, 180, 180] + }, + "model-on-bumper-resting" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0, -0.00005], + "orientation": [0, 90, 180] + }, + "model-on-side1" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00004, 0], + "orientation": [-90, 0, 45] + }, + "model-on-side2" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00004, 0.00005], + "orientation": [-45, 0, 45] + }, + "model-on-side3" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00000, 0.00005], + "orientation": [0, 45, 45] + } + } + } + }, + "layers": [ + { + "id": "model", + "type": "model", + "source": "model" + } + ] +} \ No newline at end of file diff --git a/test/integration/render-tests/model-layer/terrain-2-wheels-stunt/expected.png b/test/integration/render-tests/model-layer/terrain-2-wheels-stunt/expected.png new file mode 100644 index 00000000000..c2b2f78ec58 Binary files /dev/null and b/test/integration/render-tests/model-layer/terrain-2-wheels-stunt/expected.png differ diff --git a/test/integration/render-tests/model-layer/terrain-2-wheels-stunt/style.json b/test/integration/render-tests/model-layer/terrain-2-wheels-stunt/style.json new file mode 100644 index 00000000000..eeb89088ac7 --- /dev/null +++ b/test/integration/render-tests/model-layer/terrain-2-wheels-stunt/style.json @@ -0,0 +1,88 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 1024, + "height": 1024, + "allowed": 0.00025 + } + }, + "center": [ + 0.0001, + 0.0001 + ], + "pitch": 50, + "zoom": 24, + "terrain": { + "source": "mapbox-dem", + "exaggeration": 0.1 + }, + "sources": { + "mapbox-dem": { + "type": "raster-dem", + "maxzoom": 18, + "tileSize": 514, + "tiles": ["local://models/dem/14-2618-6334-terrain.514.png"] + }, + "model": { + "type": "model", + "models": { + "model-1" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.0001, 0.0001], + "orientation": [45, 0, 45] + }, + "model-2" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00006, 0.0001], + "orientation": [45, 0, 45] + }, + "model-3" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00006, 0.00015], + "orientation": [-45, 0, 180] + }, + "model-4" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00006, 0.00005], + "orientation": [45, 0, 45] + }, + "model-5" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00010, 0.00005], + "orientation": [-45, 0, 180] + }, + "model-6" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00014, 0.0001], + "orientation": [45, 0, 45] + }, + "model-7" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.00014, 0.00015], + "orientation": [-45, 0, 180] + }, + "model-8" : { + "uri": "local://models/low-poly-car.gltf", + "position": [0.0001, 0.00015], + "orientation": [-45, 0, 180] + } + } + } + }, + "layers": [ + { + "id": "terrain-hillshade", + "type": "hillshade", + "source": "mapbox-dem" + }, + { + "id": "model", + "type": "model", + "source": "model", + "paint": { + "model-translation" : [0, 0, 1.5] + } + } + ] +} \ No newline at end of file