Better cities and towns labels in 3D view (#635)

* Better cities and towns labels in 3D view

* Small changes (let -> const)

* Lower city labels

* Change the way the label faces the camera

* Use font from svg instead of hardcoded

* Use color from svg

* Optionalize 3d labels

* Use pack.burgs instead of svg + Icon use style from svg

* Replace jQuery with d3

* Labels adapts to height scale

* Fix bug: options are not applied when changing layer

* Add states labels + Replace canvas by svg

* Fix: States labels adapts to height scale

* Requested fixes

* All fonts working properly + Big memory optimization + Minor fixes

* Fix rotating animation + More optimization

* use Raycaster for positioning the labels

* Use canvas to render burg labels + Some tweaks

Co-authored-by: Rayzeq <zachariedubrulle@gmail.com>
This commit is contained in:
Rayzeq 2021-07-17 14:52:17 +02:00 committed by GitHub
parent 53c20a48da
commit 4575edc92e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 247 additions and 13 deletions

View file

@ -3381,6 +3381,11 @@
<input id="options3dSunZ" type="number" min=-1500 max=1500 step=100 style="width:4.7em"> <input id="options3dSunZ" type="number" min=-1500 max=1500 step=100 style="width:4.7em">
</div> </div>
<div data-tip="Toggle 3d labels" style="margin: .6em 0 .3em -.2em">
<input id="options3dMeshLabels3d" class="checkbox" type="checkbox">
<label for="options3dMeshLabels3d" class="checkbox-label"><i>Show 3d labels</i></label>
</div>
<div data-tip="Toggle sky mode" style="margin: .6em 0 .3em -.2em"> <div data-tip="Toggle sky mode" style="margin: .6em 0 .3em -.2em">
<input id="options3dMeshSkyMode" class="checkbox" type="checkbox"> <input id="options3dMeshSkyMode" class="checkbox" type="checkbox">
<label for="options3dMeshSkyMode" class="checkbox-label"><i>Show sky and extend water</i></label> <label for="options3dMeshSkyMode" class="checkbox-label"><i>Show sky and extend water</i></label>

View file

@ -138,7 +138,7 @@ async function saveTiles() {
} }
// parse map svg to object url // parse map svg to object url
async function getMapURL(type, subtype) { async function getMapURL(type, {globe=false, noLabels=false, noWater=false}) {
const cloneEl = document.getElementById("map").cloneNode(true); // clone svg const cloneEl = document.getElementById("map").cloneNode(true); // clone svg
cloneEl.id = "fantasyMap"; cloneEl.id = "fantasyMap";
document.body.appendChild(cloneEl); document.body.appendChild(cloneEl);
@ -150,8 +150,13 @@ async function getMapURL(type, subtype) {
const isFirefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1; const isFirefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
if (isFirefox && type === "mesh") clone.select("#oceanPattern").remove(); if (isFirefox && type === "mesh") clone.select("#oceanPattern").remove();
if (subtype === "globe") clone.select("#scaleBar").remove(); if (globe) clone.select("#scaleBar").remove();
if (subtype === "noWater") { if (noLabels) {
clone.select("#labels #states").remove();
clone.select("#labels #burgLabels").remove();
clone.select("#icons #burgIcons").remove();
}
if (noWater) {
clone.select("#oceanBase").attr("opacity", 0); clone.select("#oceanBase").attr("opacity", 0);
clone.select("#oceanPattern").attr("opacity", 0); clone.select("#oceanPattern").attr("opacity", 0);
} }

View file

@ -6,12 +6,17 @@
// set default options // set default options
const options = {scale: 50, lightness: .7, shadow: .5, sun: {x: 100, y: 600, z: 1000}, rotateMesh: 0, rotateGlobe: .5, const options = {scale: 50, lightness: .7, shadow: .5, sun: {x: 100, y: 600, z: 1000}, rotateMesh: 0, rotateGlobe: .5,
skyColor: "#9ecef5", waterColor: "#466eab", extendedWater: 0, resolution: 2}; skyColor: "#9ecef5", waterColor: "#466eab", extendedWater: 0, labels3d: 0, resolution: 2};
// set variables // set variables
let Renderer, scene, camera, controls, animationFrame, material, texture, let Renderer, scene, camera, controls, animationFrame, material, texture,
geometry, mesh, ambientLight, spotLight, waterPlane, waterMaterial, waterMesh, geometry, mesh, ambientLight, spotLight, waterPlane, waterMaterial, waterMesh,
objexporter; objexporter, square_geometry, texture_loader, raycaster;
const drawCtx = document.createElement("canvas").getContext('2d');
const drawSVG = document.createElementNS("http://www.w3.org/2000/svg", "svg");
document.body.appendChild(drawSVG);
let textMeshs = [], iconMeshs = [];
const fontCache = {"Georgia": "", "Times New Roman": "", "Comic Sans MS": "", "Lucida Sans Unicode": "", "Courier New": "", "Verdana": "", "Arial": "", "Impact": ""} // default are web-safe fonts
// initiate 3d scene // initiate 3d scene
const create = async function(canvas, type = "viewMesh") { const create = async function(canvas, type = "viewMesh") {
@ -42,6 +47,7 @@ const stop = function() {
material.dispose(); material.dispose();
if (waterPlane) waterPlane.dispose(); if (waterPlane) waterPlane.dispose();
if (waterMaterial) waterMaterial.dispose(); if (waterMaterial) waterMaterial.dispose();
deleteLabels();
Renderer.renderLists.dispose(); // is it required? Renderer.renderLists.dispose(); // is it required?
Renderer.dispose(); Renderer.dispose();
@ -65,11 +71,21 @@ const stop = function() {
const setScale = function(scale) { const setScale = function(scale) {
options.scale = scale; options.scale = scale;
geometry.vertices.forEach((v, i) => v.z = getMeshHeight(i)); geometry.vertices.forEach((v, i) => v.z = getMeshHeight(i));
geometry.verticesNeedUpdate = true; geometry.verticesNeedUpdate = true;
geometry.computeVertexNormals(); geometry.computeVertexNormals();
render(); render();
geometry.verticesNeedUpdate = false; geometry.verticesNeedUpdate = false;
for (const textMesh of textMeshs) {
raycaster.ray.origin.x = textMesh.position.x; raycaster.ray.origin.z = textMesh.position.z;
textMesh.position.y = raycaster.intersectObject(mesh)[0].point.y + textMesh.base_height;
}
for (const iconMesh of iconMeshs) {
raycaster.ray.origin.x = iconMesh.position.x; raycaster.ray.origin.z = iconMesh.position.z;
iconMesh.position.y = raycaster.intersectObject(mesh)[0].point.y;
}
} }
const setLightness = function(intensity) { const setLightness = function(intensity) {
@ -85,11 +101,9 @@ const setSun = function(x, y, z) {
} }
const setRotation = function(speed) { const setRotation = function(speed) {
cancelAnimationFrame(animationFrame);
if (options.isGlobe) options.rotateGlobe = speed; else options.rotateMesh = speed; if (options.isGlobe) options.rotateGlobe = speed; else options.rotateMesh = speed;
controls.autoRotateSpeed = speed; controls.autoRotateSpeed = speed;
controls.autoRotate = Boolean(controls.autoRotateSpeed); controls.autoRotate = Boolean(controls.autoRotateSpeed);
if (controls.autoRotate) animate();
} }
const toggleSky = function() { const toggleSky = function() {
@ -103,6 +117,17 @@ const toggleSky = function() {
redraw(); redraw();
} }
const toggleLabels = function() {
options.labels3d = !options.labels3d;
if (options.labels3d) {
createLabels().then(() => update());
} else {
deleteLabels();
update();
}
}
const setColors = function(sky, water) { const setColors = function(sky, water) {
options.skyColor = sky; options.skyColor = sky;
scene.background = scene.fog.color = new THREE.Color(sky); scene.background = scene.fog.color = new THREE.Color(sky);
@ -166,16 +191,195 @@ async function newMesh(canvas) {
controls.maxPolarAngle = Math.PI/2; controls.maxPolarAngle = Math.PI/2;
controls.autoRotate = Boolean(options.rotateMesh); controls.autoRotate = Boolean(options.rotateMesh);
controls.autoRotateSpeed = options.rotateMesh; controls.autoRotateSpeed = options.rotateMesh;
if (controls.autoRotate) animate(); animate();
controls.addEventListener("change", render); controls.addEventListener("change", render);
return true; return true;
} }
function svg2base64(svg) {
const str_xml = new XMLSerializer().serializeToString(svg);
return 'data:image/svg+xml;base64,' + btoa(str_xml);
}
function texture2mesh(texture, width=1, height=1, backface=false) {
texture = new texture_loader.load(texture);
texture.minFilter = THREE.LinearFilter; // remove `texture has been resized` warning
const material = new THREE.MeshBasicMaterial({map: texture, side: backface ? THREE.DoubleSide : THREE.FrontSide, depthWrite: false});
material.transparent = true;
const mesh = new THREE.Mesh(
square_geometry,
material
);
mesh.scale.x = width;
mesh.scale.y = height;
mesh.renderOrder = 1;
return mesh;
}
async function createStateText(font, size, color, label, quality=10) {
drawSVG.innerHTML = "<defs></defs>";
drawSVG.appendChild(label.cloneNode(true));
if (fontCache[font] == undefined) {fontCache[font] = (await GFontToDataURI(`https://fonts.googleapis.com/css?family=${font}`)).join('\n');}
drawSVG.children[0].innerHTML = `<style type="text/css">${fontCache[font]}</style>`;
drawSVG.children[0].appendChild(svg.select(label.childNodes[0].href.baseVal).node().cloneNode(true)); // href of path in defs
drawSVG.children[1].setAttribute("transform", `scale(${quality} ${quality})`)
drawSVG.children[1].setAttribute('font-family', font);
drawSVG.children[1].setAttribute('font-size', size);
drawSVG.children[1].setAttribute('fill', color);
drawSVG.removeAttribute("viewBox");
const bbox = drawSVG.getBBox();
drawSVG.setAttribute("viewBox", [bbox.x, bbox.y, bbox.width, bbox.height].join(" "));
drawSVG.setAttribute("width", bbox.width);
drawSVG.setAttribute("height", bbox.height);
const mesh = texture2mesh(svg2base64(drawSVG), bbox.width / quality, bbox.height / quality, true);
mesh.rotation.set(THREE.Math.degToRad(-90), 0, 0);
return mesh;
}
async function createBurgText(text, font, size, color, quality=30) {
drawCtx.font = `${size * quality}px ${font}`;
drawCtx.canvas.width = drawCtx.measureText(text).width;
drawCtx.canvas.height = size*quality * (1 + 1/4); // adding a margin of 1/4 of the size because text sometime overflow the font size
drawCtx.clearRect(0, 0, drawCtx.canvas.width, drawCtx.canvas.height);
drawCtx.font = `${size * quality}px ${font}`;
drawCtx.fillStyle = color;
drawCtx.fillText(text, 0, size * quality);
return texture2mesh(drawCtx.canvas.toDataURL(), drawCtx.canvas.width / quality, drawCtx.canvas.height / quality);
}
function get3dCoords(base_x, base_y) {
const x = base_x - graphWidth/2;
const z = base_y - graphHeight/2;
raycaster.ray.origin.x = x; raycaster.ray.origin.z = z;
const y = raycaster.intersectObject(mesh)[0].point.y;
return [x, y, z];
}
async function createLabels() {
square_geometry = new THREE.PlaneGeometry(1, 1);
texture_loader = new THREE.TextureLoader();
raycaster = new THREE.Raycaster();
raycaster.set(new THREE.Vector3(0, 1000, 0), new THREE.Vector3(0, -1, 0));
// Burg labels
const cities = svg.select("#viewbox #labels #burgLabels #cities");
const towns = svg.select('#viewbox #labels #burgLabels #towns');
const cities_icons = svg.select('#viewbox #icons #burgIcons #cities');
const towns_icons = svg.select('#viewbox #icons #burgIcons #towns');
const citie_icon_material = new THREE.MeshBasicMaterial({color: cities_icons.attr('fill')});
const town_icon_material = new THREE.MeshBasicMaterial({color: towns_icons.attr('fill')});
const citie_icon_geometry = new THREE.SphereGeometry(cities_icons.attr("size") * 2, 8, 8);
const town_icon_geometry = new THREE.SphereGeometry(towns_icons.attr("size") * 2, 8, 8);
for (let i = 1; i < pack.burgs.length; i++) {
const burg = pack.burgs[i];
const [x, y, z] = get3dCoords(burg.x, burg.y)
if(layerIsOn("toggleLabels")) {
if (burg.capital) {
var text_mesh = await createBurgText(burg.name, cities.attr('font-family'), cities.attr('font-size'), cities.attr('fill'));
} else {
var text_mesh = await createBurgText(burg.name, towns.attr('font-family'), towns.attr('font-size'), towns.attr('fill'));
}
if (burg.capital) {
text_mesh.position.set(x, y + 10, z);
text_mesh.base_height = 15;
text_mesh.animate = function () {
this.rotation.copy(camera.rotation);
}
} else {
text_mesh.position.set(x, y + 5, z);
text_mesh.base_height = 5;
text_mesh.animate = function () {
if(this.position.distanceTo(camera.position) > 200) {
this.visible = false;
} else {
this.visible = true;
this.rotation.copy(camera.rotation);
}
}
}
textMeshs.push(text_mesh);
scene.add(text_mesh);
}
// Icon
if(layerIsOn("toggleIcons")) {
const icon_mesh = new THREE.Mesh(
burg.capital ? citie_icon_geometry : town_icon_geometry,
burg.capital ? citie_icon_material : town_icon_material
);
icon_mesh.position.set(x, y, z)
iconMeshs.push(icon_mesh);
scene.add(icon_mesh);
}
}
// State labels
const state_labels = svg.select("#viewbox #labels #states")
for (const label of state_labels.node().children) {
const text_mesh = await createStateText(state_labels.attr("font-family"), state_labels.attr("font-size"), state_labels.attr("fill"), label);
const id = label.id.match(/\d+$/);
const pos = pack.states[id].pole
const [x, y, z] = get3dCoords(pos[0], pos[1])
text_mesh.position.set(x, y + 25, z);
text_mesh.base_height = 25;
textMeshs.push(text_mesh)
scene.add(text_mesh);
}
}
function deleteLabels() {
if (square_geometry) square_geometry.dispose();
square_geometry = undefined;
texture_loader = undefined;
raycaster = undefined;
for (const [i, mesh] of textMeshs.entries()) {
scene.remove(mesh);
mesh.material.map.dispose();
mesh.material.dispose();
mesh.geometry.dispose();
delete mesh.material.map;
delete mesh.material;
delete mesh.geometry;
delete textMeshs[i];
}
textMeshs = [];
for (const [i, mesh] of iconMeshs.entries()) {
scene.remove(mesh);
mesh.material.dispose();
mesh.geometry.dispose();
delete mesh.material;
delete mesh.geometry;
delete iconMeshs[i];
}
iconMeshs = [];
}
// create a mesh from pixel data // create a mesh from pixel data
async function createMesh(width, height, segmentsX, segmentsY) { async function createMesh(width, height, segmentsX, segmentsY) {
const url = await getMapURL("mesh", options.extendedWater ? "noWater" : null); const mapOptions = {}
if (options.labels3d) mapOptions.noLabels = true;
if (options.extendedWater) mapOptions.noWater = true;
const url = await getMapURL("mesh", mapOptions);
window.setTimeout(() => window.URL.revokeObjectURL(url), 3000); window.setTimeout(() => window.URL.revokeObjectURL(url), 3000);
if (texture) texture.dispose(); if (texture) texture.dispose();
texture = new THREE.TextureLoader().load(url, render); texture = new THREE.TextureLoader().load(url, render);
texture.needsUpdate = true; texture.needsUpdate = true;
@ -196,6 +400,12 @@ async function createMesh(width, height, segmentsX, segmentsY) {
mesh.castShadow = true; mesh.castShadow = true;
mesh.receiveShadow = true; mesh.receiveShadow = true;
scene.add(mesh); scene.add(mesh);
render(); // needed for Raycaster to work, but why ?
deleteLabels();
if (options.labels3d) {
await createLabels();
}
} }
function getMeshHeight(i) { function getMeshHeight(i) {
@ -218,7 +428,10 @@ function extendWater(width, height) {
async function update3dTexture() { async function update3dTexture() {
if (texture) texture.dispose(); if (texture) texture.dispose();
const url = await getMapURL("mesh"); const mapOptions = {}
if (options.labels3d) mapOptions.noLabels = true;
if (options.extendedWater) mapOptions.noWater = true;
const url = await getMapURL("mesh", mapOptions);
window.setTimeout(() => window.URL.revokeObjectURL(url), 3000); window.setTimeout(() => window.URL.revokeObjectURL(url), 3000);
texture = new THREE.TextureLoader().load(url, render); texture = new THREE.TextureLoader().load(url, render);
material.map = texture; material.map = texture;
@ -242,7 +455,7 @@ async function newGlobe(canvas) {
updateGlobeTexure(true); updateGlobeTexure(true);
// camera // camera
camera = new THREE.PerspectiveCamera(45, canvas.width / canvas.height, 0.1, 1000).translateZ(5); camera = new THREE.PerspectiveCamera(45, canvas.width / canvas.height, 0.1, 1000).translateZ(5);
// controls // controls
controls = await OrbitControls(camera, Renderer.domElement); controls = await OrbitControls(camera, Renderer.domElement);
@ -291,7 +504,7 @@ async function updateGlobeTexure(addMesh) {
material.map = texture; material.map = texture;
if (addMesh) addGlobe3dMesh(); if (addMesh) addGlobe3dMesh();
}; };
img2.src = await getMapURL("mesh", "globe");; img2.src = await getMapURL("mesh", {globe: true});
} }
async function getOBJ() { async function getOBJ() {
@ -317,6 +530,11 @@ function render() {
function animate() { function animate() {
animationFrame = requestAnimationFrame(animate); animationFrame = requestAnimationFrame(animate);
controls.update(); controls.update();
for(const mesh of textMeshs) {
if(mesh.animate) {
mesh.animate();
}
}
Renderer.render(scene, camera); Renderer.render(scene, camera);
} }
@ -356,6 +574,6 @@ function OBJExporter() {
}); });
} }
return {create, redraw, update, stop, options, setScale, setLightness, setSun, setRotation, toggleSky, setResolution, setColors, saveScreenshot, saveOBJ}; return {create, redraw, update, stop, options, setScale, setLightness, setSun, setRotation, toggleLabels, toggleSky, setResolution, setColors, saveScreenshot, saveOBJ};
}))); })));

View file

@ -912,6 +912,7 @@ function toggle3dOptions() {
document.getElementById("options3dMeshRotationNumber").addEventListener("change", changeRotation); document.getElementById("options3dMeshRotationNumber").addEventListener("change", changeRotation);
document.getElementById("options3dGlobeRotationRange").addEventListener("input", changeRotation); document.getElementById("options3dGlobeRotationRange").addEventListener("input", changeRotation);
document.getElementById("options3dGlobeRotationNumber").addEventListener("change", changeRotation); document.getElementById("options3dGlobeRotationNumber").addEventListener("change", changeRotation);
document.getElementById("options3dMeshLabels3d").addEventListener("change", toggleLabels3d);
document.getElementById("options3dMeshSkyMode").addEventListener("change", toggleSkyMode); document.getElementById("options3dMeshSkyMode").addEventListener("change", toggleSkyMode);
document.getElementById("options3dMeshSky").addEventListener("input", changeColors); document.getElementById("options3dMeshSky").addEventListener("input", changeColors);
document.getElementById("options3dMeshWater").addEventListener("input", changeColors); document.getElementById("options3dMeshWater").addEventListener("input", changeColors);
@ -928,6 +929,7 @@ function toggle3dOptions() {
options3dSunZ.value = ThreeD.options.sun.z; options3dSunZ.value = ThreeD.options.sun.z;
options3dMeshRotationRange.value = options3dMeshRotationNumber.value = ThreeD.options.rotateMesh; options3dMeshRotationRange.value = options3dMeshRotationNumber.value = ThreeD.options.rotateMesh;
options3dGlobeRotationRange.value = options3dGlobeRotationNumber.value = ThreeD.options.rotateGlobe; options3dGlobeRotationRange.value = options3dGlobeRotationNumber.value = ThreeD.options.rotateGlobe;
options3dMeshLabels3d.value = ThreeD.options.labels3d;
options3dMeshSkyMode.value = ThreeD.options.extendedWater; options3dMeshSkyMode.value = ThreeD.options.extendedWater;
options3dColorSection.style.display = ThreeD.options.extendedWater ? "block" : "none"; options3dColorSection.style.display = ThreeD.options.extendedWater ? "block" : "none";
options3dMeshSky.value = ThreeD.options.skyColor; options3dMeshSky.value = ThreeD.options.skyColor;
@ -958,6 +960,10 @@ function toggle3dOptions() {
ThreeD.setRotation(speed); ThreeD.setRotation(speed);
} }
function toggleLabels3d() {
ThreeD.toggleLabels();
}
function toggleSkyMode() { function toggleSkyMode() {
const hide = ThreeD.options.extendedWater; const hide = ThreeD.options.extendedWater;
options3dColorSection.style.display = hide ? "none" : "block"; options3dColorSection.style.display = hide ? "none" : "block";