Fantasy-Map-Generator/modules/ui/3d.js
Evolvedexperiment 3cb5b69d83 Added 3d options
2019-10-28 19:46:21 +00:00

470 lines
14 KiB
JavaScript

"use strict";
let threeD = {}; // master object for 3d scane and parameters
let threeDscale = 50; // 3d scene scale
// start 3d view and heightmap edit preview
async function start3d(canvas) {
const loaded = await loadTHREE();
if (!loaded) {tip("Cannot load 3d library", false, "error", 4000); return false};
threeD.Renderer = new THREE.WebGLRenderer({canvas, antialias: true, preserveDrawingBuffer: true});
threeD.scene = new THREE.Scene();
//threeD.scene.background = new THREE.Color(0x53679f);
threeD.camera = new THREE.PerspectiveCamera(70, canvas.width / canvas.height, 0.1, 3000);
threeD.camera.name = "camera";
threeD.camera.position.x = 0;
threeD.camera.position.z = 350;
threeD.camera.position.y = 285;
threeD.controls = await OrbitControls(threeD.camera, threeD.Renderer.domElement);
threeD.controls.minDistance = 10; threeD.controls.maxDistance = 1000;
threeD.controls.maxPolarAngle = Math.PI/2;
threeD.controls.keys = {};
threeD.Renderer.setSize(canvas.width, canvas.height);
add3dMesh(graphWidth, graphHeight, grid.cellsX, grid.cellsY);
addLight();
updateWaterMesh();
updateFog();
updateSkybox();
updateFogDistance();
updateSceneBackground();
updateLight();
updateShadows();
threeD.controls.addEventListener("change", render);
return true;
}
// create a mesh from pixel data
async function add3dMesh(width, height, segmentsX, segmentsY) {
const geometry = new THREE.PlaneGeometry(width, height, segmentsX-1, segmentsY-1);
// generateTexture
//threeD.material = new THREE.MeshBasicMaterial();
//const texture = new THREE.CanvasTexture(generateTexture(grid.cells.h, grid.cellsX, grid.cellsY));
//threeD.material.map = texture;
const url = await getMapURL("mesh");
threeD.material = new THREE.MeshLambertMaterial();
const texture = new THREE.TextureLoader().load(url, render);
texture.needsUpdate = true;
threeD.material.map = texture;
threeD.material.transparent = true;
threeDscale = parseInt(threeDHeightScale.value);
geometry.vertices.forEach((v, i) => v.z = getMeshHeight(i));
geometry.computeVertexNormals();
threeD.Renderer.shadowMap.enabled = true;
threeD.mesh = new THREE.Mesh(geometry, threeD.material);
threeD.mesh.name = "ground";
threeD.mesh.rotation.x = -Math.PI / 2;
if (threeDShadows.checked) {
threeD.mesh.castShadow = true;
threeD.mesh.receiveShadow = true;
threeD.Renderer.shadowMap.enabled = true;
} else {
threeD.mesh.castShadow = false;
threeD.mesh.receiveShadow = false;
threeD.Renderer.shadowMap.enabled = false;
}
threeD.scene.add(threeD.mesh);
}
async function addWaterMesh(width, height) {
const waterPlane = new THREE.PlaneGeometry(width*16, height*16, 10, 10);
const waterMaterial = new THREE.MeshLambertMaterial();
const urlWater = await getWater(width*16, height*16);
waterMaterial.map = new THREE.TextureLoader().load(urlWater);
// this is to hide the ocean when viewing from sea level
waterMaterial.polygonOffset = true;
waterMaterial.polygonOffsetFactor = 2;
threeD.waterMesh = new THREE.Mesh(waterPlane, waterMaterial);
threeD.waterMesh.rotation.x = -Math.PI / 2;
threeD.waterMesh.position.y -= 3; // fix for z-fighting, gap is visible at sea level
threeD.waterMesh.castShadow = false;
threeD.waterMesh.receiveShadow = false;
threeD.scene.add(threeD.waterMesh);
}
function updateHeightScale() {
update3dPreview(canvas3d);
}
async function updateWaterMesh() {
if (threeDExtendedWater.checked) {
await addWaterMesh(graphWidth, graphHeight);
update3dPreview(canvas3d);
} else {
if (threeD.waterMesh) {
threeD.scene.remove(threeD.waterMesh);
threeD.waterMesh = undefined;
update3dPreview(canvas3d);
}
}
render();
}
function addLight() {
threeD.ambientLight = new THREE.AmbientLight(0xcccccc, .7);
threeD.scene.add(threeD.ambientLight);
threeD.spotLight = new THREE.SpotLight(0xcccccc, .8, 1000, .7, 0, 0);
threeD.spotLight.position.set(100, 600, parseInt(threeDSpotlightDistance.value));
threeD.spotLight.castShadow = threeDShadows.checked;
threeD.scene.add(threeD.spotLight);
//threeD.scene.add(new THREE.SpotLightHelper(spotLight));
}
function updateLight() {
if (threeDSpotlight.checked) {
if (threeD.spotLight) { threeD.spotLight.visible = true; }
} else {
if (threeD.spotLight) { threeD.spotLight.visible = false; }
}
render();
}
function updateSpotlightDistance() {
threeD.spotLight.position.z = parseInt(threeDSpotlightDistance.value);
render();
}
function updateShadows() {
var b = threeDShadows.checked;
if (threeD.mesh) {
threeD.mesh.castShadow = b;
threeD.mesh.receiveShadow = b;
threeD.Renderer.shadowMap.enabled = b;
}
if (threeD.spotLight) {
threeD.spotLight.castShadow = b;
}
render();
}
function addFog() {
var fogColor = new THREE.Color(threeDFogColorOutput.value);
threeD.scene.background = fogColor;
threeD.scene.fog = new THREE.Fog(fogColor, parseInt(threeDFogDistance.value), 3000);
}
function updateFog() {
const w = document.getElementById("threeDFog");
if (w.checked) {
addFog();
} else {
threeD.scene.fog = undefined;
render();
updateSceneBackground();
}
render();
}
function updateFogDistance() {
if (threeD.scene.fog) { threeD.scene.fog.near = parseInt(threeDFogDistance.value); render(); }
}
function updateSceneBackground() {
var bgColor = new THREE.Color(threeDBackgroundColorOutput.value);
threeD.scene.background = bgColor;
render();
}
function getRGB(color) {
var rgb = color.match(/^rgb?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
return {
r: parseInt(rgb[1]),
g: parseInt(rgb[2]),
b: parseInt(rgb[3]),
};
}
function getSkyboxTexture(width, height) {
var url = threeDSkyboxURL.value;
if (url != "") {
return url;
} else {
// this is a light-blue gradient
var canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
var color = d3.scaleSequential(d3.interpolateBlues);
var ctx = canvas.getContext('2d');
var image = ctx.createImageData(width, height);
for (var y=0; y < height; y++) {
let v = (y / height * 0.6) + (0.2); // use center 60% of "Blues"
let rgb = getRGB(color(1-v));
for (var x=0; x < width; x++) {
let i = (y * width + x) * 4;
image.data[i+0] = rgb.r;
image.data[i+1] = rgb.g;
image.data[i+2] = rgb.b;
image.data[i+3] = 255;
}
}
ctx.putImageData(image,0,0);
return canvas.toDataURL();
}
}
function addSkybox() {
const shader = THREE.ShaderLib.equirect;
const skyMaterial = new THREE.ShaderMaterial({
fragmentShader: shader.fragmentShader,
vertexShader: shader.vertexShader,
uniforms: shader.uniforms,
depthWrite: false,
side: THREE.BackSide,
});
const texture = new THREE.TextureLoader().load(getSkyboxTexture(640, 320));
texture.magFilter = THREE.LinearFilter;
texture.minFilter = THREE.LinearFilter;
skyMaterial.uniforms.tEquirect.value = texture;
var plane = new THREE.SphereGeometry(1900, 32, 32);
threeD.skyMesh = new THREE.Mesh(plane, skyMaterial);
threeD.scene.add(threeD.skyMesh);
}
async function updateSkybox() {
if (threeDSkybox.checked) {
await addSkybox();
} else {
if (threeD.skyMesh) {
threeD.scene.remove(threeD.skyMesh);
threeD.skyMesh = undefined;
}
}
render();
}
async function updateSkyboxType() {
if (threeD.skyMesh) {
threeD.scene.remove(threeD.skyMesh);
threeD.skyMesh = undefined;
}
await addSkybox();
render();
}
function getMeshHeight(i) {
const h = grid.cells.h[i];
return h < 20 ? 0 : (h - 18) / 82 * threeDscale;
}
function generateTexture(data, width, height) {
let context, image, imageData;
const vector3 = new THREE.Vector3(0, 0, 0);
const sun = new THREE.Vector3(1, 1, 1);
sun.normalize();
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
context = canvas.getContext('2d');
context.fillStyle = '#000';
context.fillRect(0, 0, width, height);
image = context.getImageData(0, 0, canvas.width, canvas.height);
imageData = image.data;
for (let i = 0, j = 0; i < imageData.length; i += 4, j ++) {
vector3.x = data[j - 2] - data[j + 2];
vector3.y = 2;
vector3.z = data[j - width * 2] - data[j + width * 2];
vector3.normalize();
const shade = vector3.dot(sun);
// initial: r 96 + shade * 128, g 32 + shade * 96, b shade * 96;
const clr = (shade * 255) * (.5 + data[j] * .007); // new: black and white
imageData[i] = imageData[i + 1] = imageData[i + 2] = clr;
}
context.putImageData(image, 0, 0);
const canvasScaled = document.createElement('canvas');
canvasScaled.width = width * 4;
canvasScaled.height = height * 4;
context = canvasScaled.getContext('2d');
context.scale(4, 4);
context.drawImage(canvas, 0, 0);
image = context.getImageData(0, 0, canvasScaled.width, canvasScaled.height);
imageData = image.data;
for (let i = 0; i < imageData.length; i += 4) {
const v = ~~(Math.random() * 5);
imageData[i] += v;
imageData[i + 1] += v;
imageData[i + 2] += v;
}
context.putImageData(image, 0, 0);
return canvasScaled;
}
function loadTHREE() {
if (window.THREE) return Promise.resolve(true);
return new Promise(resolve => {
const script = document.createElement('script');
script.src = "libs/three.min.js"
document.head.append(script);
script.onload = () => resolve(true);
script.onerror = () => resolve(false);
});
}
function OrbitControls(camera, domElement) {
if (THREE.OrbitControls) new THREE.OrbitControls(camera, domElement);
return new Promise(resolve => {
const script = document.createElement('script');
script.src = "libs/orbitControls.min.js"
document.head.append(script);
script.onload = () => resolve(new THREE.OrbitControls(camera, domElement));
script.onerror = () => resolve(false);
});
}
function update3dPreview(canvas) {
if (threeD.mesh) {
threeD.mesh.geometry.dispose();
threeD.mesh.material.dispose();
threeD.scene.remove(threeD.mesh);
}
threeD.Renderer.setSize(canvas.width, canvas.height);
if (canvas.dataset.type === "viewGlobe") addGlobe3dMesh();
else add3dMesh(graphWidth, graphHeight, grid.cellsX, grid.cellsY);
render();
}
async function update3d() {
const url = await getMapURL("mesh");
threeD.material.map = new THREE.TextureLoader().load(url, render);
}
function stop3d() {
if (!threeD.controls || !threeD.Renderer) return;
threeD.controls.dispose();
threeD.Renderer.dispose()
cancelAnimationFrame(threeD.animationFrame);
threeD = {};
if (modules.edit3d) {
$("#threeDOptions").dialog("close");
}
}
async function startGlobe(canvas) {
const loaded = await loadTHREE();
if (!loaded) {tip("Cannot load 3d library", false, "error", 4000); return false};
threeD.scene = new THREE.Scene();
threeD.scene.background = new THREE.TextureLoader().load("https://i0.wp.com/azgaar.files.wordpress.com/2019/10/stars.png", render);
threeD.Renderer = new THREE.WebGLRenderer({canvas, antialias: true, preserveDrawingBuffer: true});
threeD.Renderer.setSize(canvas.width, canvas.height);
threeD.camera = new THREE.PerspectiveCamera(45, canvas.width / canvas.height, 0.1, 1000).translateZ(5);
threeD.controls = await OrbitControls(threeD.camera, threeD.Renderer.domElement);
threeD.controls.minDistance = 1.8; threeD.controls.maxDistance = 10;
threeD.controls.autoRotate = true;
threeD.controls.keys = {};
const ambientLight = new THREE.AmbientLight(0xcccccc, .9);
threeD.scene.add(ambientLight);
const spotLight = new THREE.SpotLight(0xcccccc, .6, 200, .7, .1, 0);
spotLight.position.set(700, 300, 200);
spotLight.castShadow = false;
threeD.scene.add(spotLight);
//threeD.scene.add(new THREE.SpotLightHelper(spotLight));
addGlobe3dMesh();
threeD.controls.addEventListener("change", render);
threeD.animationFrame = requestAnimationFrame(animate);
return true;
}
// create globe mesh just from svg
async function addGlobe3dMesh() {
threeD.material = new THREE.MeshLambertMaterial();
const url = await getMapURL("mesh", "globe");
threeD.material.map = new THREE.TextureLoader().load(url, render);
threeD.mesh = new THREE.Mesh(new THREE.SphereBufferGeometry(1, 64, 64), threeD.material);
threeD.scene.add(threeD.mesh);
}
// render 3d scene and camera, do only on controls change
function render() {
threeD.Renderer.render(threeD.scene, threeD.camera);
}
// animate 3d scene and camera
function animate() {
threeD.animationFrame = requestAnimationFrame(animate);
threeD.controls.update();
threeD.Renderer.render(threeD.scene, threeD.camera);
}
function toggleRotation() {
const rotate = threeD.controls.autoRotate = !threeD.controls.autoRotate;
rotate ? requestAnimationFrame(animate) : cancelAnimationFrame(threeD.animationFrame);
}
// download screenshot
async function saveScreenshot() {
const URL = threeD.Renderer.domElement.toDataURL("image/jpeg");
const link = document.createElement("a");
link.download = getFileName() + ".jpeg";
link.href = URL;
document.body.appendChild(link);
link.click();
tip(`Screenshot is saved. Open "Downloads" screen (CTRL + J) to check`, true, "success", 7000);
window.setTimeout(() => window.URL.revokeObjectURL(URL), 5000);
}
function close3d() {
modules.edit3d = false;
}
function show3doptions(type) {
if (type === "view3D") {
if (modules.edit3d) return;
modules.edit3d = true;
$("#threeDOptions").dialog({
title: "3D Options", resizable: false, width: fitContent(), close: close3d,
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
});
threeDHeightScale.addEventListener("click", updateHeightScale);
threeDExtendedWater.addEventListener("click", updateWaterMesh);
threeDFog.addEventListener("click", updateFog);
threeDFogDistance.addEventListener("change", updateFogDistance);
threeDSkybox.addEventListener("click", updateSkybox);
threeDSpotlight.addEventListener("click", updateLight);
threeDSpotlightDistance.addEventListener("change", updateSpotlightDistance);
threeDShadows.addEventListener("click", updateShadows);
threeDFogColor.addEventListener("input", function() {
threeDFogColor.value = threeDFogColorOutput.value = d3.color(this.value).hex();
updateFog();
});
threeDBackgroundColor.addEventListener("input", function() {
threeDBackgroundColor.value = threeDBackgroundColorOutput.value = d3.color(this.value).hex();
updateSceneBackground();
});
} else {
if (modules.edit3d) {
$("#threeDOptions").dialog("close");
}
}
}