Create 3D city map and put it on Mars In this 3D map, you can hover over an 3D object, and “magic” will happen: which is letting you
Overview
Step 1: Generate a 3D map Step 2: Load these asset using two pre-written example function from library THREE NOTE: You need identify those individual element; Step 3: Set up raycaster and normalized mouse casting;
Step 0: Having a Node Enrionment
First you need already have “node.js” and “vite” set up in your directory and having an index.html and a script in your directory (Install Three.js).
├── index.html
├── script.js
In script.js
// script.js
// a1-3D-City-Map.html
import * as THREE from 'three';
// STL loader
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
// For camera view
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
// if loading a project or Gltf use this
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
// ...
Add this script to html anywhere as a module
<body>
<script type="module" src="a1-3D-City-Map.js"></script>
</body>
To start serving this three-D scene, just go
# in your project terminal go
npx vite
Step 1: Generate Fake 3D City Map
Utilize open source software to generate a FAKE map by probabletrain: Probabletrain’s 3D Map Generator This will give you a directory looks like this:
public/model 2
├── README.txt
├── blocks.stl
├── buildings.stl
├── coastline.stl
├── domain.stl
├── river.stl
├── roads.stl
└── sea.stl
The whole building part. What I do is turn the bundle file into parts using blender for advanced editing.
public/model 2/building-parts
├── building.bin
├── building.gltf
├── individuals.bin
├── individuals.glb
├── individuals.gltf
├── part.bin
├── part.gltf
└── parts.stl
Step 2: Loading: Two Different Loading
When exporting from Blender
there are two 3D object format, one is .stl
, the other is .gltf
. They seem to be written for a few examples for THREE.js, but so useful that we may as well just use them here.
Single STL is a single gometry file
- Within each stl is a single “geoemtry” object (vector specifying the shape of geometry)
- Geometry is just a skeleton, now we want to add skins, known as materials [[/Building and Road Materials]]
- The magic fomula is
Matrial + Geometry = Mesh
!
let road = null; .load( loader'/model 2/roads.stl',// 1** the first arg is stl file location function (geometry) { // **2.1 create a mesh using stil geometry const mesh = new THREE.Mesh(geometry, road_material); = mesh; // **2.2 expose this object so we can reference later road .obj_type = "road" // set a custom attribute for reference road.add(mesh); // 2.3 add this mesh to scene scene, // 2** the second tell THREE to add scene }=> { (xhr) console.log((xhr.loaded / xhr.total) * 100 + '% loaded') , // 3** the thrid is side effect or midware? }=> { (error) console.log(error) // 4** error handle } )
GLTF is like a collection of gomemetry mesh material, animation exported out of blender
- GLTF has attribute “scene”
let buildings = null
.load(
gltf_loader"/model 2/building-parts/building.gltf",
function(gltf) {
// because out of blender the rotating is wrong
.scene.rotation.x = Math.PI / 2;
gltf// this is label not copy
= gltf.scene.children
buildings // console.log(buildings)
.map(mesh=> mesh.material=building_material.clone()) // clone is actually important, if you don't clone materials, the materials will reference the same thing, so when you change material color the whole thing changes
buildings.map(mesh=>mesh.obj_type = "building")
buildings.map((mesh,index)=>mesh.obj_id = index)
buildings// this modify the whole thing in the mesh
.add(gltf.scene)
scene
} )
Step 3: Set up orbit control and ray cast!
Setup Raycaster
In a 2D space, your cursor’s x & y can be used to indicate which element on a 2D plan is selected, in a 3D space however, you need projection. For your 2D computer screen to “know” which 3D object is selected, you need imformation about viewing angle - this is your camera. * mouse: you need track cursor x and y. If you have event listener, this is “clientX” & “clientY” (W3School teach you basic javascript: W3Schools online HTML editor). This is expressed relative to client screen size. For three to use they need to be normalized * raycaster : think of raycaster as a red layser beam pointing at any 3D blocks. Finally, to find what object are calculated were “penetrated” through the “raycaster” you use this method from “raycaster” object called “setFromcamera”.
.setFromCamera(mouse, camera); raycaster
In context of THREE scene:
// Function to detect the object under the mouse and change its color
window.addEventListener('mousemove', onMouseMove, false);
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function onMouseMove(event) {
// Calculate mouse position in normalized device coordinates (-1 to +1) for both components
.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
mouse; }
Control Color Change on Element Selected
raycaster have a property for storing any object the “ray” penetrates. This vector store a tremendours amout of json property. The one we want is the one called “object”. We want whatever individual building block is selected, the material color needs changing.
// change color of selected geometry
function hoverInteraction() {
// Update the raycaster with the current camera and mouse positions
.setFromCamera(mouse, camera);
raycaster
// Calculate objects intersecting the raycaster
const intersects = raycaster.intersectObjects(buildings,false);
// If there's an intersected object, change its color
// if(building) {
if(intersects.length != 0) {
const intersectedObject = intersects[0].object;
if(!!intersectedObject.obj_type) {
if(intersectedObject.obj_type == "building") {
// dirty way to change only selected building color only
.material.color.set("#3498db")
intersectedObject// if we want to be fancy, we can also change material here, something more glossy
}
} else {
} .map(mesh=> mesh.material.color.set("#f7f9f9"))
buildings
} }
Eventually the renderer function (onMonseMove
will go on to animate and render) #### Camera Control & Animate Animate by convension may not make sense for us if we are just rotating a static 3D object. But in fact the concept “Animate” is important in implementing 3D interactivity to web development. Its is intuitive for web user to think they are interacting with a virtual object, when in fact what they are interacting with is a “two dimension plane” and “videos”, or “steaming frames” the webserver sent every second for your eyes only.
So the task of rotating viewing angle of the object is infact, tracking how my click movement has moved and rotate my camera based on it.
Fortunately someone have already wrote this task for us.
const controls = new OrbitControls(camera, renderer.domElement) //renderer is webGL render
.enableDamping = true controls
Finally, we setup streaming for the object to animate:
function render() {
.render(scene, camera)
renderer
}function animate() {
requestAnimationFrame(animate);
hoverInteraction(); // just defined in previous step
.update();
controlsrender()
}animate()
Travia
Here are the code you will need when building ### THREE.js scene & View #### Setup a 3D scene in THREE.js
// script.js
// ... import library ...
// setup a scene
const scene = new THREE.Scene()
.background = new THREE.Color("#85929e")
scene.backgroundBlurriness = 0.5
scene
// lightings, no light some of your material will not work
const directionalLight = new THREE.DirectionalLight("#aed6f1",10)
.position.z = 4
directionalLight.position.y = -15
directionalLight
// shiny another lighting
const directionalLight2 = new THREE.DirectionalLight("#fef9e7",5)
.position.z = -1
directionalLight2.position.y = 10
directionalLight2.add(directionalLight,directionalLight2)
scene
// you always need camera for any scene
const camera = new THREE.PerspectiveCamera(
100,
window.innerWidth / window.innerHeight,
0.1,
1000
)
// move camera
.position.z = 4
camera.position.y = -15
camera
// render sence and add to html
const renderer = new THREE.WebGLRenderer()
.setSize(window.innerWidth, window.innerHeight)
renderer
// append directly to you html document, rather than finding the corresponding id
document.body.appendChild(renderer.domElement)
.setPixelRatio( window.devicePixelRatio ); renderer
What I use for Building and Road Materials
// script.js
// ... set up scene ...
const building_material = new THREE.MeshStandardMaterial({
color: "#f7f9f9", roughness: 0, transparent: true, opacity: 0.85
})const road_material = new THREE.MeshBasicMaterial({
color: "#eaf2f8", transparent: false
})
Reference
- Install Three.js - Introducing correct way to view font-end js code. You have to use
npx vite
. - Probabletrain’s 3D Map Generator - let you export the 3D object
- Alt. 3D map generator
- Trouble Importing THREE/JSM examples - the
rayCaster
andSTLloader
are written example not official function, so sometimes