Skip to content

Instantly share code, notes, and snippets.

@sknightq
Forked from danvk/sprite-custom-layer.tsx
Last active February 24, 2023 09:46
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save sknightq/ddd06add373539cf2f1bd4a670a5b4b2 to your computer and use it in GitHub Desktop.
Save sknightq/ddd06add373539cf2f1bd4a670a5b4b2 to your computer and use it in GitHub Desktop.
Mapbox custom layer which renders multiple models in a THREE.js scene
import * as THREE from 'three'
import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { setPosition } from '@/components/Mapbox/utils'
import MapboxGL, { LngLatLike, MercatorCoordinate } from 'mapbox-gl'
import { LayerConfig, ModelTransform } from './types'
import { FeatureCollection } from 'geojson'
import { Object3D } from 'three'
function transform(layerConfig: LayerConfig, modelConfig: { position: LngLatLike; altitude: number }, center: mapboxgl.MercatorCoordinate): THREE.Matrix4 {
const { modelScale, modelRotate } = layerConfig
const { position, altitude } = modelConfig
const rotationX = new THREE.Matrix4().makeRotationAxis(new THREE.Vector3(1, 0, 0), modelRotate[0])
const rotationY = new THREE.Matrix4().makeRotationAxis(new THREE.Vector3(0, 1, 0), modelRotate[1])
const rotationZ = new THREE.Matrix4().makeRotationAxis(new THREE.Vector3(0, 0, 1), modelRotate[2])
const coord = MercatorCoordinate.fromLngLat(position, altitude)
const scale = coord.meterInMercatorCoordinateUnits() * (modelScale as number)
return new THREE.Matrix4()
.makeTranslation(coord.x - center.x, coord.y - center.y, coord.z! - center.z!)
.scale(new THREE.Vector3(scale, -scale, scale))
.multiply(rotationX)
.multiply(rotationY)
.multiply(rotationZ)
}
class CustomLayer implements MapboxGL.CustomLayerInterface {
type = 'custom' as const
renderingMode = '3d' as const
id: string
options: LayerConfig = {
path: '',
modelRotate: [Math.PI / 2, 0, 0],
modelScale: 1
}
map!: MapboxGL.Map
center!: MapboxGL.MercatorCoordinate
camera!: THREE.Camera
cameraTransform!: THREE.Matrix4
scene!: THREE.Scene
renderer!: THREE.WebGLRenderer
model: Promise<GLTF>
constructor(id: string, options: LayerConfig) {
this.id = id
this.options = { ...this.options, ...options }
this.model = new Promise<GLTF>((resolve, reject) => {
const loader = new GLTFLoader()
loader.load(
options.path,
(gltf: GLTF) => {
resolve(gltf)
},
() => {
// progress is being made; bytes loaded = xhr.loaded / xhr.total
},
e => {
const xhr = e.target as XMLHttpRequest
const message = `Unable to load ${options.path}: ${xhr.status} ${xhr.statusText}`
// console.error(message); // tslint:disable-line
reject(message)
}
)
})
}
createScene() {
const scene = new THREE.Scene()
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444)
hemiLight.position.set(0, 20, 0)
scene.add(hemiLight)
const dirLight = new THREE.DirectionalLight(0xffffff)
dirLight.position.set(-3, 10, -10)
dirLight.castShadow = true
dirLight.shadow.camera.top = 2
dirLight.shadow.camera.bottom = -2
dirLight.shadow.camera.left = -2
dirLight.shadow.camera.right = 2
dirLight.shadow.camera.near = 0.1
dirLight.shadow.camera.far = 40
scene.add(dirLight)
if (this.options.hasAxis) {
const axesHelper = new THREE.AxesHelper(5)
this.scene.add(axesHelper)
}
return scene
}
onAdd(map: MapboxGL.Map, gl: WebGLRenderingContext) {
this.center = MercatorCoordinate.fromLngLat(map.getCenter(), 0)
this.camera = new THREE.Camera()
const { x, y, z } = this.center
this.cameraTransform = new THREE.Matrix4().makeTranslation(x, y, z || 0)
this.scene = this.createScene()
this.map = map
this.renderer = new THREE.WebGLRenderer({
canvas: map.getCanvas(),
context: gl,
antialias: true
})
this.renderer.autoClear = false
}
render(gl: WebGLRenderingContext, matrix: number[]) {
this.camera.projectionMatrix = new THREE.Matrix4().fromArray(matrix).multiply(this.cameraTransform)
// const l = setPosition(this.modelTransform)
// this.camera.projectionMatrix = new THREE.Matrix4().fromArray(matrix).multiply(l)
this.renderer.resetState()
this.renderer.render(this.scene, this.camera)
this.map.triggerRepaint()
}
async addFeatures(geojson: FeatureCollection, callback: (objects: Object3D[], scene: THREE.Scene) => void | undefined) {
const model = await this.model
this.scene = this.createScene()
const objects = geojson.features.map(f => {
const { geometry } = f
if (geometry.type !== 'Point') {
throw new Error(`Sprite layers must have Point geometries; got ${f.geometry.type}`)
}
const { coordinates } = geometry
const object = model.scene.clone()
object.applyMatrix4(
transform(
this.options,
{
position: {
lng: coordinates[0],
lat: coordinates[1]
},
altitude: 0
},
this.center
)
)
return object
})
if (typeof callback === 'function') {
callback(objects, this.scene)
} else {
for (const o of objects) {
this.scene.add(o)
}
}
}
}
@cuongnc5
Copy link

How to use the js?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment