Unlock a world of possibilities! Login now and discover the exclusive benefits awaiting you.
Couple of months ago I blogged about Mapbox GL and Nebula.js https://community.qlik.com/t5/Qlik-Design-Blog/Using-Mapbox-GL-with-Nebula-js/ba-p/1817621.
Today, I will take that example and add some 3D Bars with Three.js.
I will be using the observable notation but you can substitute "require" with "import" on your React/Angular apps
First, fork or follow the setup as described in my previous blog. Then, we have to add the installation and importing of Three and GSAP for the animation.
// Observable
GSAP = require('gsap');
TweenMax = GSAP.TweenMax;
// React / Angular
import { TweenMax } from 'gsap';
import * as THREE from 'three/build/three';
Lets define the constants
let maxBarΝumberFromData = 0;
let maxNumberOfBars = 0;
let map;
let camera;
let scene;
let renderer;
const barWidth = 100;
const barOpacity = 1;
// parameters to ensure the model is georeferenced correctly on the map
const modelOrigin = [-30, 55];
const modelAltitude = 0;
const modelRotate = [Math.PI / 2, 0, 0];
const modelAsMercatorCoordinate = mapboxgl.MercatorCoordinate.fromLngLat(
modelOrigin,
modelAltitude,
);
// transformation parameters to position, rotate and scale the 3D model onto the map
const modelTransform = {
translateX: modelAsMercatorCoordinate.x,
translateY: modelAsMercatorCoordinate.y,
translateZ: modelAsMercatorCoordinate.z,
rotateX: modelRotate[0],
rotateY: modelRotate[1],
rotateZ: modelRotate[2],
/* Since our 3D model is in real world meters, a scale transform needs to be
* applied since the CustomLayerInterface expects units in MercatorCoordinates.
*/
scale: modelAsMercatorCoordinate.meterInMercatorCoordinateUnits(),
};
Now we can add the function that creates the bars on the map and animates the height
const createBar = (posx, posz, posy, order) => {
const max = 3000;
const ratio = Number(posy) / Number(maxBarΝumberFromData);
const y = max * ratio;
const _posy = 1;
const geometry = new THREE.BoxGeometry(barWidth, 1, barWidth, 1, 1, 1);
const material = new THREE.MeshLambertMaterial({ color: 0xfffff, transparent: true });
const bar = new THREE.Mesh(geometry, material);
bar.position.set(posx, _posy, posz);
bar.name = `bar-${order}`;
bar.userData.y = y;
bar.material.opacity = barOpacity;
scene.add(bar);
// Animate
TweenMax.to(bar.scale, 1, { y, delay: order * 0.01 });
TweenMax.to(bar.position, 1, { y: y / 2, delay: order * 0.01 });
maxNumberOfBars = order;
};
Now, lets switch the "buildLayer" function with this one so we can create a custom 3d layer using three.js
// Create the layer that will hold the bars
const buildLayer = () => {
const layer = {
id: '3d-model',
type: 'custom',
renderingMode: '3d',
onAdd(_map, gl) {
camera = new THREE.Camera();
scene = new THREE.Scene();
// create two three.js lights to illuminate the model
const directionalLight = new THREE.DirectionalLight(0xffffff);
directionalLight.position.set(-90, 200, 130).normalize();
scene.add(directionalLight);
// sky color ground color intensity
const directionalLight2 = new THREE.DirectionalLight(0xffffff, 0.3);
directionalLight2.position.set(90, 20, -100).normalize();
scene.add(directionalLight2);
qMatrix.forEach((row, index) => {
maxBarΝumberFromData = (maxBarΝumberFromData < row[1].qNum) ? row[1].qNum : maxBarΝumberFromData;
})
qMatrix.forEach((row, index) => {
createBar(row[2].qNum * 150, row[1].qNum * 150, row[5].qNum, index);
})
// scale up geometry
scene.scale.set(300, 300, 300);
// use the Mapbox GL JS map canvas for three.js
renderer = new THREE.WebGLRenderer({
canvas: _map.getCanvas(),
context: gl,
antialias: true,
});
renderer.autoClear = false;
},
render(gl, matrix) {
const rotationX = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(1, 0, 0),
modelTransform.rotateX,
);
const rotationY = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(0, 1, 0),
modelTransform.rotateY,
);
const rotationZ = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(0, 0, 1),
modelTransform.rotateZ,
);
const m = new THREE.Matrix4().fromArray(matrix);
const l = new THREE.Matrix4()
.makeTranslation(
modelTransform.translateX,
modelTransform.translateY,
modelTransform.translateZ,
)
.scale(
new THREE.Vector3(
modelTransform.scale,
-modelTransform.scale,
modelTransform.scale,
),
)
.multiply(rotationX)
.multiply(rotationY)
.multiply(rotationZ);
camera.projectionMatrix = m.multiply(l);
renderer.state.reset();
renderer.render(scene, camera);
map.triggerRepaint();
},
};
return layer;
}
This is it! The final result should be similar to this:
You can view, fork and play with the above demo at
https://observablehq.com/@yianni-ververis/nebula-js-mapbox-with-three-js?collection=@yianni-ververis...
/Yianni
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.