diff --git a/3d-style/data/bucket/model_bucket.js b/3d-style/data/bucket/model_bucket.js new file mode 100644 index 00000000000..9c24ddae298 --- /dev/null +++ b/3d-style/data/bucket/model_bucket.js @@ -0,0 +1,286 @@ +// @flow + +import EXTENT from '../../../src/data/extent.js'; +import {register} from '../../../src/util/web_worker_transfer.js'; +import loadGeometry from '../../../src/data/load_geometry.js'; +import toEvaluationFeature from '../../../src/data/evaluation_feature.js'; +import type {EvaluationFeature} from '../../../src/data/evaluation_feature.js'; +import EvaluationParameters from '../../../src/style/evaluation_parameters.js'; +import Point from '@mapbox/point-geometry'; +import type {Mat4} from 'gl-matrix'; +import type {CanonicalTileID} from '../../../src/source/tile_id.js'; +import type { + Bucket, + BucketParameters, + BucketFeature, + IndexedFeature, + PopulateParameters +} from '../../../src/data/bucket.js'; + +import type Context from '../../../src/gl/context.js'; +import type VertexBuffer from '../../../src/gl/vertex_buffer.js'; +import type {FeatureStates} from '../../../src/source/source_state.js'; +import type {SpritePositions} from '../../../src/util/image.js'; +import type {ProjectionSpecification} from '../../../src/style-spec/types.js'; +import type {TileTransform} from '../../../src/geo/projection/tile_transform.js'; +import type {IVectorTileLayer} from '@mapbox/vector-tile'; +import {InstanceVertexArray} from '../../../src/data/array_types.js'; +import assert from 'assert'; +import {warnOnce} from '../../../src/util/util.js'; +import ModelStyleLayer from '../../style/style_layer/model_style_layer.js'; +import {rotationScaleYZFlipMatrix} from '../../util/model_util.js'; +import {tileToMeter} from '../../../src/geo/mercator_coordinate.js'; + +class ModelFeature { + feature: EvaluationFeature; + instancedDataOffset: number; + instancedDataCount: number; + + constructor(feature: EvaluationFeature, offset: number) { + this.feature = feature; + this.instancedDataOffset = offset; + this.instancedDataCount = 0; + } +} + +class PerModelAttributes { + // If node has meshes, instancedDataArray gets an entry for each feature instance (used for all meshes or the node). + instancedDataArray: InstanceVertexArray; + instancedDataBuffer: VertexBuffer; + instancesEvaluatedElevation: Array; // Gets added to DEM elevation of the instance to produce value in instancedDataArray. + + features: Array; + idToFeaturesIndex: {[string | number]: number}; // via this.features, enable lookup instancedDataArray based on feature ID. + + constructor() { + this.instancedDataArray = new InstanceVertexArray(); + this.instancesEvaluatedElevation = []; + this.features = []; + this.idToFeaturesIndex = {}; + } +} + +class ModelBucket implements Bucket { + zoom: number; + index: number; + canonical: CanonicalTileID; + layers: Array; + layerIds: Array; + stateDependentLayers: Array; + stateDependentLayerIds: Array; + hasPattern: boolean; + + instancesPerModel: {string: PerModelAttributes}; + + uploaded: boolean; + + tileToMeter: number; + projection: ProjectionSpecification; + + // elevation is baked into vertex buffer together with evaluated instance translation + validForExaggeration: number; + validForDEMTile: ?CanonicalTileID; + + /* $FlowIgnore[incompatible-type-arg] Doesn't need to know about all the implementations */ + constructor(options: BucketParameters) { + this.zoom = options.zoom; + this.canonical = options.canonical; + this.layers = options.layers; + this.layerIds = this.layers.map(layer => layer.id); + this.projection = options.projection; + this.index = options.index; + + this.stateDependentLayerIds = this.layers.filter((l) => l.isStateDependent()).map((l) => l.id); + this.hasPattern = false; + this.instancesPerModel = {}; + this.validForExaggeration = 0; + } + + populate(features: Array, options: PopulateParameters, canonical: CanonicalTileID, tileTransform: TileTransform) { + this.tileToMeter = tileToMeter(canonical); + const needGeometry = this.layers[0]._featureFilter.needGeometry; + + for (const {feature, id, index, sourceLayerIndex} of features) { + const evaluationFeature = toEvaluationFeature(feature, needGeometry); + + if (!this.layers[0]._featureFilter.filter(new EvaluationParameters(this.zoom), evaluationFeature, canonical)) continue; + + const bucketFeature: BucketFeature = { + id, + sourceLayerIndex, + index, + geometry: needGeometry ? evaluationFeature.geometry : loadGeometry(feature, canonical, tileTransform), + properties: feature.properties, + type: feature.type, + patterns: {} + }; + + const modelId = this.addFeature(bucketFeature, bucketFeature.geometry, evaluationFeature); + + if (modelId) { + options.featureIndex.insert(feature, bucketFeature.geometry, index, sourceLayerIndex, this.index, this.instancesPerModel[modelId].instancedDataArray.length); + } + } + } + + // eslint-disable-next-line no-unused-vars + update(states: FeatureStates, vtLayer: IVectorTileLayer, availableImages: Array, imagePositions: SpritePositions) { + // called when setFeature state API is used + for (const modelId in this.instancesPerModel) { + const instances = this.instancesPerModel[modelId]; + for (const id in states) { + if (instances.idToFeaturesIndex.hasOwnProperty(id)) { + const feature = instances.features[instances.idToFeaturesIndex[id]]; + this.evaluate(feature, states[id], instances, true); + } + } + } + } + + isEmpty(): boolean { + for (const modelId in this.instancesPerModel) { + const perModelAttributes = this.instancesPerModel[modelId]; + if (perModelAttributes.instancedDataArray.length !== 0) return false; + } + return true; + } + + uploadPending(): boolean { + return !this.uploaded; + } + + upload(context: Context) { + // if buffer size is less than the threshold, do not upload instance buffer. + // if instance buffer is not uploaded, instances are rendered one by one. + const useInstancingThreshold = Number.MAX_SAFE_INTEGER; + if (!this.uploaded) { + for (const modelId in this.instancesPerModel) { + const perModelAttributes = this.instancesPerModel[modelId]; + if (perModelAttributes.instancedDataArray.length < useInstancingThreshold || perModelAttributes.instancedDataArray.length === 0) continue; + if (!perModelAttributes.instancedDataBuffer) { + perModelAttributes.instancedDataBuffer = context.createVertexBuffer(perModelAttributes.instancedDataArray, perModelAttributes.instancedDataArray.members, true); + } else { + perModelAttributes.instancedDataBuffer.updateData(perModelAttributes.instancedDataArray); + } + } + } + this.uploaded = true; + } + + destroy() { + for (const modelId in this.instancesPerModel) { + const perModelAttributes = this.instancesPerModel[modelId]; + if (perModelAttributes.instancedDataArray.length === 0) continue; + if (perModelAttributes.instancedDataBuffer) { + perModelAttributes.instancedDataBuffer.destroy(); + } + } + } + + addFeature(feature: BucketFeature, geometry: Array>, evaluationFeature: EvaluationFeature): string { + const layer = this.layers[0]; + const modelIdProperty = layer.layout.get('model-id'); + assert(modelIdProperty); + const modelId = modelIdProperty.evaluate(evaluationFeature, {}, this.canonical); + if (!modelId) { + warnOnce(`modelId is not evaluated for layer ${layer.id} and it is not going to get rendered.`); + return modelId; + } + if (!this.instancesPerModel[modelId]) { + this.instancesPerModel[modelId] = new PerModelAttributes(); + } + const perModelVertexArray = this.instancesPerModel[modelId]; + const instancedDataArray = perModelVertexArray.instancedDataArray; + + const modelFeature = new ModelFeature(evaluationFeature, instancedDataArray.length); + for (const geometries of geometry) { + for (const point of geometries) { + if (point.x < 0 || point.x >= EXTENT || point.y < 0 || point.y >= EXTENT) { + continue; // Clip on tile borders to prevent duplicates + } + const i = instancedDataArray.length; + instancedDataArray.resize(i + 1); + perModelVertexArray.instancesEvaluatedElevation.push(0); + instancedDataArray.float32[i * 16] = point.x; + instancedDataArray.float32[i * 16 + 1] = point.y; + } + } + modelFeature.instancedDataCount = perModelVertexArray.instancedDataArray.length - modelFeature.instancedDataOffset; + if (modelFeature.instancedDataCount > 0) { + if (feature.id) { + perModelVertexArray.idToFeaturesIndex[feature.id] = perModelVertexArray.features.length; + } + perModelVertexArray.features.push(modelFeature); + this.evaluate(modelFeature, {}, perModelVertexArray, false); + } + return modelId; + } + + evaluate(feature: ModelFeature, featureState: FeatureStates, perModelVertexArray: PerModelAttributes, update: boolean) { + const layer = this.layers[0]; + const evaluationFeature = feature.feature; + const canonical = this.canonical; + const rotation = layer.paint.get('model-rotation').evaluate(evaluationFeature, featureState, canonical); + const scale = layer.paint.get('model-scale').evaluate(evaluationFeature, featureState, canonical); + const translation = layer.paint.get('model-translation').evaluate(evaluationFeature, featureState, canonical); + const color = layer.paint.get('model-color').evaluate(evaluationFeature, featureState, canonical); + color.a = layer.paint.get('model-color-mix-intensity').evaluate(evaluationFeature, featureState, canonical); + const rotationScaleYZFlip: Mat4 = []; + + rotationScaleYZFlipMatrix(rotationScaleYZFlip, (rotation: any), (scale: any)); + + // https://github.com/mapbox/mapbox-gl-native-internal/blob/c380f9492220906accbdca1f02cca5ee489d97fc/src/mbgl/renderer/layers/render_model_layer.cpp#L1282 + const constantTileToMeterAcrossTile = 10; + assert(perModelVertexArray.instancedDataArray.bytesPerElement === 64); + + const vaOffset2 = Math.round(100.0 * color.a) + color.b / 1.05; + + for (let i = 0; i < feature.instancedDataCount; ++i) { + const instanceOffset = feature.instancedDataOffset + i; + const offset = instanceOffset * 16; + + const va = perModelVertexArray.instancedDataArray.float32; + let terrainElevationContribution = 0; + if (update) { + terrainElevationContribution = va[offset + 6] - perModelVertexArray.instancesEvaluatedElevation[instanceOffset]; + } + + // All per-instance attributes are packed to one 4x4 float matrix. Data is not expected + // to change on every frame when e.g. camera or light changes. + // Column major order. Elements: + // 0 & 1: tile coordinates stored in integer part of float, R and G color components, + // originally in range [0..1], scaled to range [0..0.952(arbitrary, just needs to be + // under 1)]. + const pointY = va[offset + 1] | 0; // point.y stored in integer part + va[offset] = (va[offset] | 0) + color.r / 1.05; // point.x stored in integer part + va[offset + 1] = pointY + color.g / 1.05; + // Element 2: packs color's alpha (as integer part) and blue component in fractional part. + va[offset + 2] = vaOffset2; + // tileToMeter is taken at center of tile. Prevent recalculating it over again for + // thousands of trees. + // Element 3: tileUnitsToMeter conversion. + va[offset + 3] = 1.0 / (canonical.z > constantTileToMeterAcrossTile ? this.tileToMeter : tileToMeter(canonical, pointY)); + // Elements [4..6]: translation evaluated for the feature. + va[offset + 4] = translation[0]; + va[offset + 5] = translation[1]; + va[offset + 6] = translation[2] + terrainElevationContribution; + // Elements [7..16] Instance modelMatrix holds combined rotation and scale 3x3, + va[offset + 7] = rotationScaleYZFlip[0]; + va[offset + 8] = rotationScaleYZFlip[1]; + va[offset + 9] = rotationScaleYZFlip[2]; + va[offset + 10] = rotationScaleYZFlip[4]; + va[offset + 11] = rotationScaleYZFlip[5]; + va[offset + 12] = rotationScaleYZFlip[6]; + va[offset + 13] = rotationScaleYZFlip[8]; + va[offset + 14] = rotationScaleYZFlip[9]; + va[offset + 15] = rotationScaleYZFlip[10]; + perModelVertexArray.instancesEvaluatedElevation[instanceOffset] = translation[2]; + } + } +} + +register(ModelBucket, 'ModelBucket', {omit: ['layers']}); +register(PerModelAttributes, 'PerModelAttributes'); +register(ModelFeature, 'ModelFeature'); + +export default ModelBucket; diff --git a/3d-style/data/model.js b/3d-style/data/model.js index c296a9abdf8..40b782e4498 100644 --- a/3d-style/data/model.js +++ b/3d-style/data/model.js @@ -90,12 +90,12 @@ export default class Model { uploaded: boolean; aabb: Aabb; - constructor(id: string, uri: string, position: [number, number], orientation: [number, number, number], nodes: Array) { + 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[0], position[1]) : new LngLat(0, 0); + this.position = position != null ? new LngLat(position[0], position[1]) : new LngLat(0, 0); - this.orientation = orientation !== undefined ? orientation : [0, 0, 0]; + this.orientation = orientation != null ? orientation : [0, 0, 0]; this.nodes = nodes; this.uploaded = false; this.aabb = new Aabb([Infinity, Infinity, Infinity], [-Infinity, -Infinity, -Infinity]); diff --git a/3d-style/data/model_attributes.js b/3d-style/data/model_attributes.js index f850540d89f..3ff5c969a13 100644 --- a/3d-style/data/model_attributes.js +++ b/3d-style/data/model_attributes.js @@ -23,4 +23,11 @@ export const normalAttributes: StructArrayLayout = createLayout([ {name: 'a_normal_3f', components: 3, type: 'Float32'} ]); +export const instanceAttributes: StructArrayLayout = createLayout([ + {name: 'a_normal_matrix0', components: 4, type: 'Float32'}, + {name: 'a_normal_matrix1', components: 4, type: 'Float32'}, + {name: 'a_normal_matrix2', components: 4, type: 'Float32'}, + {name: 'a_normal_matrix3', components: 4, type: 'Float32'} +]); + export const {members, size, alignment} = modelAttributes; diff --git a/3d-style/render/draw_model.js b/3d-style/render/draw_model.js index 9cd31f273e2..d74b66d05d0 100644 --- a/3d-style/render/draw_model.js +++ b/3d-style/render/draw_model.js @@ -9,15 +9,21 @@ import type {Mesh, Node} from '../data/model.js'; import type {DynamicDefinesType} from '../../src/render/program/program_uniforms.js'; import Transform from '../../src/geo/transform.js'; +import EXTENT from '../../src/data/extent.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, {getMetersPerPixelAtLatitude} from '../../src/geo/mercator_coordinate.js'; +import {getMetersPerPixelAtLatitude} from '../../src/geo/mercator_coordinate.js'; import TextureSlots from './texture_slots.js'; import {convertModelMatrixForGlobe} from '../util/model_util.js'; +import {warnOnce} from '../../src/util/util.js'; +import ModelBucket from '../data/bucket/model_bucket.js'; +import assert from 'assert'; +import {DEMSampler} from '../../src/terrain/elevation.js'; +import {OverscaledTileID} from '../../src/source/tile_id.js'; export default drawModels; @@ -45,50 +51,16 @@ function fogMatrixForModel(modelMatrix: Mat4, transform: Transform): Mat4 { return fogMatrix; } -function drawMesh(sortedMesh: SortedMesh, painter: Painter, layer: ModelStyleLayer, modelParameters: ModelParameters, stencilMode, colorMode) { - - // early return if totally transparent - const opacity = layer.paint.get('model-opacity'); - if (opacity === 0) { - return; - } - - const context = painter.context; - const depthMode = new DepthMode(painter.context.gl.LEQUAL, DepthMode.ReadWrite, painter.depthRangeFor3D); - - const definesValues = []; - - const mesh = sortedMesh.mesh; +// Collect defines and dynamic buffers (colors, normals, uv) and bind textures. Used for single mesh and instanced draw. +function setupMeshDraw(definesValues, dynamicBuffers, mesh, painter) { const material = mesh.material; const pbr = material.pbrMetallicRoughness; - - 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); - - const uniformValues = modelUniformValues( - new Float32Array(sortedMesh.worldViewProjection), - new Float32Array(lightingMatrix), - new Float32Array(normalMatrix), - painter, - opacity, - pbr.baseColorFactor, - material.emissiveFactor, - pbr.metallicFactor, - pbr.roughnessFactor, - material, - layer); + const context = painter.context; // Textures if (pbr.baseColorTexture) { definesValues.push('HAS_TEXTURE_u_baseColorTexture'); - painter.context.activeTexture.set(context.gl.TEXTURE0 + TextureSlots.BaseColor); + context.activeTexture.set(context.gl.TEXTURE0 + TextureSlots.BaseColor); const sampler = pbr.baseColorTexture.sampler; if (pbr.baseColorTexture.gfxTexture) { pbr.baseColorTexture.gfxTexture.bindExtraParam(sampler.minFilter, sampler.magFilter, sampler.wrapS, sampler.wrapT); @@ -97,7 +69,7 @@ function drawMesh(sortedMesh: SortedMesh, painter: Painter, layer: ModelStyleLay if (pbr.metallicRoughnessTexture) { definesValues.push('HAS_TEXTURE_u_metallicRoughnessTexture'); - painter.context.activeTexture.set(context.gl.TEXTURE0 + TextureSlots.MetallicRoughness); + context.activeTexture.set(context.gl.TEXTURE0 + TextureSlots.MetallicRoughness); const sampler = pbr.metallicRoughnessTexture.sampler; if (pbr.metallicRoughnessTexture.gfxTexture) { pbr.metallicRoughnessTexture.gfxTexture.bindExtraParam(sampler.minFilter, sampler.magFilter, sampler.wrapS, sampler.wrapT); @@ -106,7 +78,7 @@ function drawMesh(sortedMesh: SortedMesh, painter: Painter, layer: ModelStyleLay if (material.normalTexture) { definesValues.push('HAS_TEXTURE_u_normalTexture'); - painter.context.activeTexture.set(context.gl.TEXTURE0 + TextureSlots.Normal); + context.activeTexture.set(context.gl.TEXTURE0 + TextureSlots.Normal); const sampler = material.normalTexture.sampler; if (material.normalTexture.gfxTexture) { material.normalTexture.gfxTexture.bindExtraParam(sampler.minFilter, sampler.magFilter, sampler.wrapS, sampler.wrapT); @@ -115,7 +87,7 @@ function drawMesh(sortedMesh: SortedMesh, painter: Painter, layer: ModelStyleLay if (material.occlusionTexture) { definesValues.push('HAS_TEXTURE_u_occlusionTexture'); - painter.context.activeTexture.set(context.gl.TEXTURE0 + TextureSlots.Occlusion); + context.activeTexture.set(context.gl.TEXTURE0 + TextureSlots.Occlusion); const sampler = material.occlusionTexture.sampler; if (material.occlusionTexture.gfxTexture) { material.occlusionTexture.gfxTexture.bindExtraParam(sampler.minFilter, sampler.magFilter, sampler.wrapS, sampler.wrapT); @@ -124,15 +96,13 @@ function drawMesh(sortedMesh: SortedMesh, painter: Painter, layer: ModelStyleLay if (material.emissionTexture) { definesValues.push('HAS_TEXTURE_u_emissionTexture'); - painter.context.activeTexture.set(context.gl.TEXTURE0 + TextureSlots.Emission); + context.activeTexture.set(context.gl.TEXTURE0 + TextureSlots.Emission); const sampler = material.emissionTexture.sampler; if (material.emissionTexture.gfxTexture) { material.emissionTexture.gfxTexture.bindExtraParam(sampler.minFilter, sampler.magFilter, sampler.wrapS, sampler.wrapT); } } - // Extra buffers (colors, normals, texCoords) - const dynamicBuffers = []; if (mesh.texcoordBuffer) { definesValues.push('HAS_ATTRIBUTE_a_uv_2f'); dynamicBuffers.push(mesh.texcoordBuffer); @@ -157,6 +127,46 @@ function drawMesh(sortedMesh: SortedMesh, painter: Painter, layer: ModelStyleLay } definesValues.push('USE_STANDARD_DERIVATIVES'); +} + +function drawMesh(sortedMesh: SortedMesh, painter: Painter, layer: ModelStyleLayer, modelParameters: ModelParameters, stencilMode, colorMode) { + const opacity = layer.paint.get('model-opacity'); + assert(opacity > 0); + const context = painter.context; + const depthMode = new DepthMode(painter.context.gl.LEQUAL, DepthMode.ReadWrite, painter.depthRangeFor3D); + + const mesh = sortedMesh.mesh; + const material = mesh.material; + const pbr = material.pbrMetallicRoughness; + + 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); + + const uniformValues = modelUniformValues( + new Float32Array(sortedMesh.worldViewProjection), + new Float32Array(lightingMatrix), + new Float32Array(normalMatrix), + painter, + opacity, + pbr.baseColorFactor, + material.emissiveFactor, + pbr.metallicFactor, + pbr.roughnessFactor, + material, + layer); + + const definesValues = []; + // Extra buffers (colors, normals, texCoords) + const dynamicBuffers = []; + + setupMeshDraw(definesValues, dynamicBuffers, mesh, painter); const program = painter.useProgram('model', null, ((definesValues: any): DynamicDefinesType[])); if (painter.style.fog) { @@ -172,6 +182,13 @@ function drawMesh(sortedMesh: SortedMesh, painter: Painter, layer: ModelStyleLay export function upload(painter: Painter, sourceCache: SourceCache) { const modelSource = sourceCache.getSource(); if (!modelSource.loaded()) return; + if (modelSource.type === 'vector' || modelSource.type === 'geojson') { + if (painter.style.modelManager) { + // Do it here, to prevent modelManager handling in Painter. + painter.style.modelManager.upload(painter); + } + return; + } const models = (modelSource: any).getModels(); // Upload models @@ -213,15 +230,25 @@ function prepareMeshes(transform: Transform, node: Node, modelMatrix: Mat4, proj } } -function drawModels(painter: Painter, sourceCache: SourceCache, layer: ModelStyleLayer) { +function drawModels(painter: Painter, sourceCache: SourceCache, layer: ModelStyleLayer, coords: Array) { if (painter.renderPass !== 'translucent') return; + // early return if totally transparent + const opacity = layer.paint.get('model-opacity'); + if (opacity === 0) { + return; + } + const modelSource = sourceCache.getSource(); if (!modelSource.loaded()) return; + if (modelSource.type === 'vector' || modelSource.type === 'geojson') { + drawInstancedModels(painter, sourceCache, layer, coords); + return; + } const models = (modelSource: any).getModels(); const modelParametersVector: ModelParameters[] = []; - const mercCameraPos = painter.transform.getFreeCameraOptions().position || new MercatorCoordinate(0, 0, 0); + const mercCameraPos = (painter.transform.getFreeCameraOptions().position: any); const cameraPos = vec3.scale([], [mercCameraPos.x, mercCameraPos.y, mercCameraPos.z], painter.transform.worldSize); vec3.negate(cameraPos, cameraPos); const transparentMeshes: SortedMesh[] = []; @@ -254,7 +281,6 @@ function drawModels(painter: Painter, sourceCache: SourceCache, layer: ModelStyl }); // Draw opaque meshes - const opacity = layer.paint.get('model-opacity'); if (opacity === 1) { for (const opaqueMesh of opaqueMeshes) { drawMesh(opaqueMesh, painter, layer, modelParametersVector[opaqueMesh.modelIndex], StencilMode.disabled, painter.colorModeForRenderPass()); @@ -276,3 +302,123 @@ function drawModels(painter: Painter, sourceCache: SourceCache, layer: ModelStyl } } +// If terrain changes, update elevations (baked in translation). +function updateModelBucketsElevation(painter: Painter, bucket: ModelBucket, bucketTileID: OverscaledTileID) { + let exaggeration = painter.terrain ? painter.terrain.exaggeration() : 0; + let dem: ?DEMSampler; + if (painter.terrain && exaggeration > 0) { + const terrain = painter.terrain; + const demTile = terrain.findDEMTileFor(bucketTileID); + if (demTile && demTile.dem) { + dem = DEMSampler.create(terrain, bucketTileID, demTile); + } else { + exaggeration = 0; + } + } + if (exaggeration === bucket.validForExaggeration && + (exaggeration === 0 || (dem && dem._demTile && dem._demTile.tileID.canonical === bucket.validForDEMTile))) { + return; + } + + for (const modelId in bucket.instancesPerModel) { + const instances = bucket.instancesPerModel[modelId]; + assert(instances.instancedDataArray.bytesPerElement === 64); + for (let i = 0; i < instances.instancedDataArray.length; ++i) { + const x = instances.instancedDataArray.float32[i * 16] | 0; + const y = instances.instancedDataArray.float32[i * 16 + 1] | 0; + const elevation = (dem ? exaggeration * dem.getElevationAt(x, y, true) : 0) + instances.instancesEvaluatedElevation[i]; + instances.instancedDataArray.float32[i * 16 + 6] = elevation; + } + } + bucket.validForExaggeration = exaggeration; + bucket.validForDEMTile = dem && dem._demTile ? dem._demTile.tileID.canonical : undefined; + bucket.uploaded = false; + bucket.upload(painter.context); +} + +function drawInstancedModels(painter: Painter, source: SourceCache, layer: ModelStyleLayer, coords: Array) { + const tr = painter.transform; + if (tr.projection.name !== 'mercator') { + warnOnce(`Drawing 3D models for ${tr.projection.name} projection is not yet implemented`); + return; + } + + const mercCameraPos = (painter.transform.getFreeCameraOptions().position: any); + if (!painter.style.modelManager) return; + const modelManager = painter.style.modelManager; + + for (const coord of coords) { + const tile = source.getTile(coord); + const bucket: ?ModelBucket = (tile.getBucket(layer): any); + if (!bucket || bucket.projection.name !== tr.projection.name) continue; + updateModelBucketsElevation(painter, bucket, coord); + + // camera position in the tile coordinates + let cameraPos = vec3.scale([], [mercCameraPos.x, mercCameraPos.y, mercCameraPos.z], (1 << coord.canonical.z)); + cameraPos = [(cameraPos[0] - coord.canonical.x - coord.wrap * (1 << coord.canonical.z)) * EXTENT, + (cameraPos[1] - coord.canonical.y) * EXTENT, cameraPos[2] * EXTENT]; + + for (const modelId in bucket.instancesPerModel) { + const model = modelManager.getModel(modelId); + if (!model) continue; + const modelInstances = bucket.instancesPerModel[modelId]; + for (const node of model.nodes) { + drawInstancedNode(painter, layer, node, modelInstances, cameraPos, coord); + } + } + } +} + +function drawInstancedNode(painter, layer, node, modelInstances, cameraPos, coord) { + const context = painter.context; + if (node.meshes) { + const depthMode = new DepthMode(context.gl.LEQUAL, DepthMode.ReadWrite, painter.depthRangeFor3D); + for (const mesh of node.meshes) { + const definesValues = []; + const dynamicBuffers = []; + setupMeshDraw(definesValues, dynamicBuffers, mesh, painter); + definesValues.push('MODEL_POSITION_ON_GPU'); + const program = painter.useProgram('model', null, ((definesValues: any): DynamicDefinesType[])); + + const isShadowPass = painter.renderPass === 'shadow'; + const shadowRenderer = painter.shadowRenderer; + if (!isShadowPass && shadowRenderer) { + shadowRenderer.setupShadows(coord.toUnwrapped(), program); + } + + painter.uploadCommonUniforms(context, program, coord.toUnwrapped()); + + const material = mesh.material; + const pbr = material.pbrMetallicRoughness; + + const uniformValues = modelUniformValues( + coord.projMatrix, + Float32Array.from(node.matrix), + new Float32Array(16), + painter, + layer.paint.get('model-opacity'), + pbr.baseColorFactor, + material.emissiveFactor, + pbr.metallicFactor, + pbr.roughnessFactor, + material, + layer, + cameraPos); + + assert(modelInstances.instancedDataArray.bytesPerElement === 64); + + for (let i = 0; i < modelInstances.instancedDataArray.length; ++i) { + uniformValues["u_normal_matrix"] = new Float32Array(modelInstances.instancedDataArray.arrayBuffer, i * 64, 16); + program.draw(context, context.gl.TRIANGLES, depthMode, StencilMode.disabled, painter.colorModeForRenderPass(), CullFaceMode.disabled, + uniformValues, layer.id, mesh.vertexBuffer, mesh.indexBuffer, mesh.segments, layer.paint, painter.transform.zoom, + undefined, dynamicBuffers); + } + } + } + if (node.children) { + for (const child of node.children) { + drawInstancedNode(painter, layer, child, modelInstances, cameraPos, coord); + } + } +} + diff --git a/3d-style/render/model_manager.js b/3d-style/render/model_manager.js new file mode 100644 index 00000000000..19a37eb9074 --- /dev/null +++ b/3d-style/render/model_manager.js @@ -0,0 +1,70 @@ +// @flow + +import {Event, Evented} from '../../src/util/evented.js'; +import assert from 'assert'; + +import Model from '../data/model.js'; +import convertModel from '../source/model_loader.js'; + +import {load} from '@loaders.gl/core'; +import {GLTFLoader} from '@loaders.gl/gltf'; +import {RequestManager} from '../../src/util/mapbox.js'; +import Painter from '../../src/render/painter.js'; + +class ModelManager extends Evented { + models: {[_: string]: Model}; + loaded: boolean; + + constructor(_models: Object, requestManager: RequestManager) { + super(); + this.models = {}; + this.loaded = false; + const modelUris = {..._models}; + for (const modelId in modelUris) { + modelUris[modelId] = requestManager.normalizeModelURL(modelUris[modelId]); + } + this.load(modelUris); + } + + async load(modelUris: Object) { + for (const modelId in modelUris) { + const modelUri = modelUris[modelId]; + const gltf = await load(modelUri, GLTFLoader, {gltf: {postProcess: false, loadBuffers: true, loadImages: true}}); + const nodes = convertModel(gltf); + const model = new Model(modelId, modelUri, undefined, undefined, nodes); + model.computeBoundsAndApplyParent(); + this.models[modelId] = model; + } + this.loaded = true; + this.fire(new Event('data', {dataType: 'style'})); + } + + isLoaded(): boolean { + return this.loaded; + } + + hasModel(id: string): boolean { + return !!this.getModel(id); + } + + getModel(id: string): ?Model { + return this.models[id]; + } + + addModel(id: string, model: Model) { + assert(!this.models[id]); + this.models[id] = model; + } + + listModels(): Array { + return Object.keys(this.models); + } + + upload(painter: Painter) { + for (const modelId in this.models) { + this.models[modelId].upload(painter.context); + } + } +} + +export default ModelManager; diff --git a/3d-style/render/program/model_program.js b/3d-style/render/program/model_program.js index 61a88bf99bf..c27883adc51 100644 --- a/3d-style/render/program/model_program.js +++ b/3d-style/render/program/model_program.js @@ -24,6 +24,7 @@ export type ModelUniformsType = { 'u_lightpos': Uniform3f, 'u_lightintensity': Uniform1f, 'u_lightcolor': Uniform3f, + 'u_camera_pos': Uniform3f, 'u_opacity': Uniform1f, 'u_baseColorFactor': Uniform4f, 'u_emissiveFactor': Uniform4f, @@ -48,6 +49,7 @@ const modelUniforms = (context: Context): ModelUniformsType => ({ 'u_lightpos': new Uniform3f(context), 'u_lightintensity': new Uniform1f(context), 'u_lightcolor': new Uniform3f(context), + 'u_camera_pos': new Uniform3f(context), 'u_opacity': new Uniform1f(context), 'u_baseColorFactor': new Uniform4f(context), 'u_emissiveFactor': new Uniform4f(context), @@ -77,7 +79,8 @@ const modelUniformValues = ( metallicFactor: number, roughnessFactor: number, material: Material, - layer: ModelStyleLayer): UniformValues => { + layer: ModelStyleLayer, + cameraPos: [number, number, number] = [0, 0, 0]): UniformValues => { const light = painter.style.light; const _lp = light.properties.get('position'); @@ -101,6 +104,7 @@ const modelUniformValues = ( 'u_lightpos': lightPos, 'u_lightintensity': light.properties.get('intensity'), 'u_lightcolor': [lightColor.r, lightColor.g, lightColor.b], + 'u_camera_pos': cameraPos, 'u_opacity': opacity, 'u_baseTextureIsAlpha': 0, 'u_alphaMask': +alphaMask, diff --git a/3d-style/source/model_loader.js b/3d-style/source/model_loader.js index e1e1ba63fd8..0fe06d6d881 100644 --- a/3d-style/source/model_loader.js +++ b/3d-style/source/model_loader.js @@ -9,6 +9,8 @@ import {mat4} from 'gl-matrix'; import {TriangleIndexArray, ModelLayoutArray, NormalLayoutArray, TexcoordLayoutArray, Color3fLayoutArray, Color4fLayoutArray} from '../../src/data/array_types.js'; import window from '../../src/util/window.js'; +import {warnOnce} from '../../src/util/util.js'; +import assert from 'assert'; // From https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#accessor-data-types @@ -86,6 +88,9 @@ function convertTextures(gltf: Object, images: Array): Array 0) { mesh.texcoordArray = new TexcoordLayoutArray(); - const texcoordAccessor = gltf.json.accessors[attributeMap.TEXCOORD_0]; + const texcoordAccessor = typeof attributeMap.TEXCOORD_0 === "object" ? attributeMap.TEXCOORD_0 : gltf.json.accessors[attributeMap.TEXCOORD_0]; mesh.texcoordArray.reserve(texcoordAccessor.count); const texcoordArrayBuffer = getBufferData(gltf, texcoordAccessor); for (let i = 0; i < texcoordAccessor.count; i++) { diff --git a/3d-style/style/style_layer/model_style_layer.js b/3d-style/style/style_layer/model_style_layer.js index a19711dec98..b5278431f3d 100644 --- a/3d-style/style/style_layer/model_style_layer.js +++ b/3d-style/style/style_layer/model_style_layer.js @@ -1,17 +1,51 @@ // @flow import StyleLayer from '../../../src/style/style_layer.js'; - +import ModelBucket from '../../data/bucket/model_bucket.js'; import type {LayerSpecification} from '../../../src/style-spec/types.js'; import properties from './model_style_layer_properties.js'; -import type {PaintProps} from './model_style_layer_properties.js'; -import {PossiblyEvaluated} from '../../../src/style/properties.js'; +import type {PaintProps, LayoutProps} from './model_style_layer_properties.js'; +import type {BucketParameters} from '../../../src/data/bucket.js'; +import {Transitionable, Transitioning, PossiblyEvaluated, PropertyValue} from '../../../src/style/properties.js'; class ModelStyleLayer extends StyleLayer { + _transitionablePaint: Transitionable; + _transitioningPaint: Transitioning; paint: PossiblyEvaluated; + layout: PossiblyEvaluated; + constructor(layer: LayerSpecification) { super(layer, properties); } + + createBucket(parameters: BucketParameters): ModelBucket { + return new ModelBucket(parameters); + } + + getProgramIds(): Array { + return ['model']; + } + + is3D(): boolean { + return true; + } + + hasShadowPass(): boolean { + return true; + } + + queryRadius(): number { + return 0; + } + + _handleOverridablePaintPropertyUpdate(name: string, oldValue: PropertyValue, newValue: PropertyValue): boolean { + if (!this.layout || oldValue.isDataDriven() || newValue.isDataDriven()) { + return false; + } + // relayout on programatically setPaintProperty for all non-data-driven properties that get baked into vertex data. + // Buckets could be updated without relayout later, if needed to optimize. + return name === "model-color" || name === "model-color-mix-intensity" || name === "model-rotation" || name === "model-scale" || name === "model-translation"; + } } export default ModelStyleLayer; diff --git a/build/generate-struct-arrays.js b/build/generate-struct-arrays.js index cdcfb0e9c52..7379a35ef48 100644 --- a/build/generate-struct-arrays.js +++ b/build/generate-struct-arrays.js @@ -131,7 +131,7 @@ import dashAttributes from '../src/data/bucket/dash_attributes.js'; import skyboxAttributes from '../src/render/skybox_attributes.js'; import tileBoundsAttributes from '../src/data/bounds_attributes.js'; import {fillExtrusionAttributes, fillExtrusionAttributesExt, centroidAttributes} from '../src/data/bucket/fill_extrusion_attributes.js'; -import {modelAttributes, color3fAttributes, color4fAttributes, normalAttributes, texcoordAttributes} from '../3d-style/data/model_attributes.js'; +import {modelAttributes, color3fAttributes, color4fAttributes, normalAttributes, texcoordAttributes, instanceAttributes} from '../3d-style/data/model_attributes.js'; // layout vertex arrays @@ -229,6 +229,7 @@ createStructArrayType(`color3f_layout`, color3fAttributes); createStructArrayType(`color4f_layout`, color4fAttributes); createStructArrayType(`texcoord_layout`, texcoordAttributes); createStructArrayType(`normal_layout`, normalAttributes); +createStructArrayType(`instance_vertex`, instanceAttributes); // paint vertex arrays diff --git a/debug/3d-playground.html b/debug/3d-playground.html index 86fd4cd36df..b7410125acb 100644 --- a/debug/3d-playground.html +++ b/debug/3d-playground.html @@ -35,6 +35,7 @@
+