BlueMap/BlueMapCore/src/main/webroot/js/libs/BlueMap.js

593 lines
17 KiB
JavaScript

/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import $ from 'jquery';
import {
BackSide,
BufferGeometryLoader,
ClampToEdgeWrapping,
SphereGeometry,
FileLoader,
FrontSide,
Mesh,
NearestFilter,
NearestMipMapLinearFilter,
PerspectiveCamera,
Scene,
ShaderMaterial,
Texture,
VertexColors,
WebGLRenderer,
Vector3,
} from 'three';
import { CSS2DRenderer } from './hud/CSS2DRenderer';
import UI from './ui/UI.js';
import Controls from './Controls.js';
import TileManager from './TileManager.js';
import HIRES_VERTEX_SHADER from './shaders/HiresVertexShader.js';
import HIRES_FRAGMENT_SHADER from './shaders/HiresFragmentShader.js';
import LOWRES_VERTEX_SHADER from './shaders/LowresVertexShader.js';
import LOWRES_FRAGMENT_SHADER from './shaders/LowresFragmentShader.js';
import SKY_VERTEX_SHADER from './shaders/SkyVertexShader.js';
import SKY_FRAGMENT_SHADER from './shaders/SkyFragmentShader.js';
import { stringToImage, pathFromCoords } from './utils.js';
import {getCookie, setCookie} from "./utils";
export default class BlueMap {
constructor(element, dataRoot) {
this.element = $('<div class="bluemap-container"></div>').appendTo(element)[0];
this.dataRoot = dataRoot;
this.locationHash = '';
this.hiresViewDistance = 160;
this.lowresViewDistance = 3200;
this.targetSunLightStrength = 1;
this.sunLightStrength = {
value: this.targetSunLightStrength
};
this.mobSpawnOverlay = {
value: false
};
this.ambientLight = {
value: 0
};
this.skyColor = {
value: new Vector3(0, 0, 0)
};
this.debugInfo = this.loadUserSetting("debugInfo", true);
this.fileLoader = new FileLoader();
this.blobLoader = new FileLoader();
this.blobLoader.setResponseType('blob');
this.bufferGeometryLoader = new BufferGeometryLoader();
this.ui = new UI(this);
this.loadingNoticeElement = $('<div>loading...</div>').appendTo($(this.element));
window.onerror = this.onLoadError;
this.initStage();
this.controls = new Controls(this.camera, this.element, this.hiresScene);
this.loadSettings().then(async () => {
await this.loadHiresMaterial();
await this.loadLowresMaterial();
this.debugInfo = false;
this.loadUserSettings();
this.handleContainerResize();
this.changeMap(this.maps[0], false);
await this.ui.load();
this.start();
}).catch(error => {
this.onLoadError("Initialization: " + error.toString());
});
}
changeMap(map, loadTiles = true) {
if (this.debugInfo) console.debug("changing map: ", map);
if (this.map === map) return;
if (this.hiresTileManager !== undefined) this.hiresTileManager.close();
if (this.lowresTileManager !== undefined) this.lowresTileManager.close();
this.map = map;
let startPos = {
x: this.settings.maps[this.map]["startPos"]["x"],
z: this.settings.maps[this.map]["startPos"]["z"]
};
this.ambientLight.value = this.settings.maps[this.map]["ambientLight"];
this.skyColor.value.set(
this.settings.maps[this.map]["skyColor"].r,
this.settings.maps[this.map]["skyColor"].g,
this.settings.maps[this.map]["skyColor"].b
);
this.controls.setTileSize(this.settings.maps[this.map]['hires']['tileSize']);
this.controls.resetPosition();
this.controls.targetPosition.set(startPos.x, this.controls.targetPosition.y, startPos.z);
this.controls.position.copy(this.controls.targetPosition);
this.lowresTileManager = new TileManager(
this,
this.lowresViewDistance,
this.loadLowresTile,
this.lowresScene,
this.settings.maps[this.map]['lowres']['tileSize'],
this.settings.maps[this.map]['lowres']['translate'],
startPos
);
this.hiresTileManager = new TileManager(
this,
this.hiresViewDistance,
this.loadHiresTile,
this.hiresScene,
this.settings.maps[this.map]['hires']['tileSize'],
this.settings.maps[this.map]['hires']['translate'],
startPos
);
if (loadTiles) {
this.lowresTileManager.update();
this.hiresTileManager.update();
}
document.dispatchEvent(new Event('bluemap-map-change'));
}
loadLocationHash(smooth = false) {
let hashVars = window.location.hash.substring(1).split(':');
if (hashVars.length >= 1){
if (this.settings.maps[hashVars[0]] !== undefined && this.map !== hashVars[0]){
this.changeMap(hashVars[0]);
}
}
if (hashVars.length >= 3){
let x = parseInt(hashVars[1]);
let z = parseInt(hashVars[2]);
if (!isNaN(x) && !isNaN(z)){
this.controls.targetPosition.x = x + 0.5;
this.controls.targetPosition.z = z + 0.5;
}
}
if (hashVars.length >= 6){
let dir = parseFloat(hashVars[3]);
let dist = parseFloat(hashVars[4]);
let angle = parseFloat(hashVars[5]);
if (!isNaN(dir)) this.controls.targetDirection = dir;
if (!isNaN(dist)) this.controls.targetDistance = dist;
if (!isNaN(angle)) this.controls.targetAngle = angle;
if (!smooth) {
this.controls.direction = this.controls.targetDirection;
this.controls.distance = this.controls.targetDistance;
this.controls.angle = this.controls.targetAngle;
this.controls.targetPosition.y = this.controls.minHeight;
this.controls.position.copy(this.controls.targetPosition);
}
}
if (hashVars.length >= 7){
let height = parseInt(hashVars[6]);
if (!isNaN(height)){
this.controls.minHeight = height;
this.controls.targetPosition.y = height;
if (!smooth) {
this.controls.position.copy(this.controls.targetPosition);
}
}
}
}
start() {
this.loadingNoticeElement.remove();
this.loadLocationHash();
$(window).on('hashchange', () => {
if (this.locationHash === window.location.hash) return;
this.loadLocationHash(true);
});
this.update();
this.render();
this.lowresTileManager.update();
this.hiresTileManager.update();
}
update = () => {
setTimeout(this.update, 1000);
this.saveUserSettings();
this.lowresTileManager.setPosition(this.controls.targetPosition);
if (this.camera.position.y < 400) {
this.hiresTileManager.setPosition(this.controls.targetPosition);
}
this.locationHash =
'#' + this.map
+ ':' + Math.floor(this.controls.targetPosition.x)
+ ':' + Math.floor(this.controls.targetPosition.z)
+ ':' + Math.round(this.controls.targetDirection * 100) / 100
+ ':' + Math.round(this.controls.targetDistance * 100) / 100
+ ':' + Math.ceil(this.controls.targetAngle * 100) / 100
+ ':' + Math.floor(this.controls.targetPosition.y);
// only update hash when changed
if (window.location.hash !== this.locationHash) {
history.replaceState(undefined, undefined, this.locationHash);
}
};
render = () => {
requestAnimationFrame(this.render);
//update controls
if (this.controls.update()) this.updateFrame = true;
//update lighting
let targetLight = this.targetSunLightStrength;
if (this.camera.position.y > 400){
targetLight = Math.max(targetLight, 0.5);
}
if (Math.abs(targetLight - this.sunLightStrength.value) > 0.01) {
this.sunLightStrength.value += (targetLight - this.sunLightStrength.value) * 0.1;
this.updateFrame = true;
}
//don't render if nothing has changed
if (!this.updateFrame) return;
this.updateFrame = false;
//render event
document.dispatchEvent(new Event('bluemap-update-frame'));
//render
this.skyboxCamera.rotation.copy(this.camera.rotation);
this.skyboxCamera.updateProjectionMatrix();
this.renderer.clear();
this.renderer.render(this.skyboxScene, this.skyboxCamera);
this.renderer.clearDepth();
this.renderer.render(this.lowresScene, this.camera);
this.renderer.clearDepth();
if (this.camera.position.y < 400) {
this.renderer.render(this.hiresScene, this.camera);
}
this.renderer.render(this.shapeScene, this.camera);
this.hudRenderer.render(this.hudScene, this.camera);
};
handleContainerResize = () => {
this.camera.aspect = this.element.clientWidth / this.element.clientHeight;
this.camera.updateProjectionMatrix();
this.skyboxCamera.aspect = this.element.clientWidth / this.element.clientHeight;
this.skyboxCamera.updateProjectionMatrix();
this.renderer.setSize(this.element.clientWidth * this.quality, this.element.clientHeight * this.quality);
$(this.renderer.domElement)
.css('width', this.element.clientWidth)
.css('height', this.element.clientHeight);
this.hudRenderer.setSize(this.element.clientWidth, this.element.clientHeight);
this.updateFrame = true;
};
async loadSettings() {
return new Promise(resolve => {
this.fileLoader.load(this.dataRoot + 'settings.json', settings => {
try {
this.settings = JSON.parse(settings);
this.maps = [];
for (let map in this.settings.maps) {
if (this.settings["maps"].hasOwnProperty(map) && this.settings.maps[map].enabled) {
this.maps.push(map);
}
}
this.maps.sort((map1, map2) => {
let sort = this.settings.maps[map1].ordinal - this.settings.maps[map2].ordinal;
if (isNaN(sort)) return 0;
return sort;
});
resolve();
} catch (e) {
reject(e);
}
});
});
}
initStage() {
this.updateFrame = true;
this.quality = 1;
this.renderer = new WebGLRenderer({
antialias: true,
sortObjects: true,
preserveDrawingBuffer: true,
logarithmicDepthBuffer: true,
});
this.renderer.autoClear = false;
this.hudRenderer = new CSS2DRenderer();
this.camera = new PerspectiveCamera(75, this.element.scrollWidth / this.element.scrollHeight, 0.1, 10000);
this.camera.updateProjectionMatrix();
this.skyboxCamera = this.camera.clone();
this.skyboxCamera.updateProjectionMatrix();
this.skyboxScene = new Scene();
this.skyboxScene.add(this.createSkybox());
this.lowresScene = new Scene();
this.hiresScene = new Scene();
this.shapeScene = new Scene();
this.hudScene = new Scene();
$(this.renderer.domElement).addClass("map-canvas").appendTo(this.element);
$(this.hudRenderer.domElement).addClass("map-canvas-hud").appendTo(this.element);
this.handleContainerResize();
$(window).resize(this.handleContainerResize);
}
loadUserSettings(){
if (!this.settings["useCookies"]) return;
this.mobSpawnOverlay.value = this.loadUserSetting("mobSpawnOverlay", this.mobSpawnOverlay.value);
this.targetSunLightStrength = this.loadUserSetting("sunLightStrength", this.targetSunLightStrength);
this.quality = this.loadUserSetting("renderQuality", this.quality);
this.hiresViewDistance = this.loadUserSetting("hiresViewDistance", this.hiresViewDistance);
this.lowresViewDistance = this.loadUserSetting("lowresViewDistance", this.lowresViewDistance);
this.controls.settings.zoom.max = this.loadUserSetting("maxZoomDistance", this.controls.settings.zoom.max);
this.debugInfo = this.loadUserSetting("debugInfo", this.debugInfo);
}
saveUserSettings(){
if (!this.settings["useCookies"]) return;
if (this.savedUserSettings === undefined) this.savedUserSettings = {};
this.saveUserSetting("mobSpawnOverlay", this.mobSpawnOverlay.value);
this.saveUserSetting("sunLightStrength", this.targetSunLightStrength);
this.saveUserSetting("renderQuality", this.quality);
this.saveUserSetting("hiresViewDistance", this.hiresViewDistance);
this.saveUserSetting("lowresViewDistance", this.lowresViewDistance);
this.saveUserSetting("maxZoomDistance", this.controls.settings.zoom.max);
this.saveUserSetting("debugInfo", this.debugInfo);
}
loadUserSetting(key, defaultValue){
let value = getCookie("bluemap-" + key);
if (value === undefined) return defaultValue;
return value;
}
saveUserSetting(key, value){
if (this.savedUserSettings[key] !== value){
this.savedUserSettings[key] = value;
setCookie("bluemap-" + key, value);
}
}
createSkybox() {
let geometry = new SphereGeometry(10, 10, 10);
let material = new ShaderMaterial({
uniforms: {
sunlightStrength: this.sunLightStrength,
ambientLight: this.ambientLight,
skyColor: this.skyColor,
},
vertexShader: SKY_VERTEX_SHADER,
fragmentShader: SKY_FRAGMENT_SHADER,
side: BackSide
});
return new Mesh(geometry, material);
}
async loadHiresMaterial() {
return new Promise(resolve => {
this.fileLoader.load(this.dataRoot + 'textures.json', textures => {
textures = JSON.parse(textures);
let materials = [];
for (let i = 0; i < textures['textures'].length; i++) {
let t = textures['textures'][i];
let opaque = t['color'][3] === 1;
let transparent = t['transparent'];
let texture = new Texture();
texture.image = stringToImage(t['texture']);
texture.anisotropy = 1;
texture.generateMipmaps = opaque || transparent;
texture.magFilter = NearestFilter;
texture.minFilter = texture.generateMipmaps ? NearestMipMapLinearFilter : NearestFilter;
texture.wrapS = ClampToEdgeWrapping;
texture.wrapT = ClampToEdgeWrapping;
texture.flipY = false;
texture.flatShading = true;
texture.needsUpdate = true;
let uniforms = {
texture: {
type: 't',
value: texture
},
sunlightStrength: this.sunLightStrength,
mobSpawnOverlay: this.mobSpawnOverlay,
ambientLight: this.ambientLight,
};
let material = new ShaderMaterial({
uniforms: uniforms,
vertexShader: HIRES_VERTEX_SHADER,
fragmentShader: HIRES_FRAGMENT_SHADER,
transparent: transparent,
depthWrite: true,
depthTest: true,
vertexColors: VertexColors,
side: FrontSide,
wireframe: false,
});
material.needsUpdate = true;
materials[i] = material;
}
this.hiresMaterial = materials;
resolve();
});
});
}
async loadLowresMaterial() {
this.lowresMaterial = new ShaderMaterial({
uniforms: {
sunlightStrength: this.sunLightStrength,
ambientLight: this.ambientLight,
},
vertexShader: LOWRES_VERTEX_SHADER,
fragmentShader: LOWRES_FRAGMENT_SHADER,
transparent: false,
depthWrite: true,
depthTest: true,
vertexColors: VertexColors,
side: FrontSide,
wireframe: false
});
}
async loadHiresTile(tileX, tileZ) {
let path = this.dataRoot + this.map + '/hires/';
path += pathFromCoords(tileX, tileZ);
path += '.json';
return new Promise((resolve, reject) => {
this.bufferGeometryLoader.load(path, geometry => {
let object = new Mesh(geometry, this.hiresMaterial);
let tileSize = this.settings.maps[this.map]['hires']['tileSize'];
let translate = this.settings.maps[this.map]['hires']['translate'];
let scale = this.settings.maps[this.map]['hires']['scale'];
object.position.set(tileX * tileSize.x + translate.x, 0, tileZ * tileSize.z + translate.z);
object.scale.set(scale.x, 1, scale.z);
resolve(object);
}, () => {
}, reject);
});
}
async loadLowresTile(tileX, tileZ) {
let path = this.dataRoot + this.map + '/lowres/';
path += pathFromCoords(tileX, tileZ);
path += '.json';
return new Promise((reslove, reject) => {
this.bufferGeometryLoader.load(path, geometry => {
let object = new Mesh(geometry, this.lowresMaterial);
let tileSize = this.settings.maps[this.map]['lowres']['tileSize'];
let translate = this.settings.maps[this.map]['lowres']['translate'];
let scale = this.settings.maps[this.map]['lowres']['scale'];
object.position.set(tileX * tileSize.x + translate.x, 0, tileZ * tileSize.z + translate.z);
object.scale.set(scale.x, 1, scale.z);
reslove(object);
}, () => {
}, reject);
})
}
onLoadError = (message, url, line, col) => {
this.loadingNoticeElement.remove();
this.toggleAlert(undefined, `
<div style="max-width: 50rem">
<h1>Error</h1>
<p style="color: red; font-family: monospace">${message}</p>
</div>
`);
};
toggleAlert(id, content) {
let alertBox = $(this.element).find('.alert-box');
if (alertBox.length === 0){
alertBox = $('<div class="alert-box"></div>').appendTo(this.ui.hud);
}
let displayAlert = () => {
let alert = $(`<div class="alert" data-alert-id="${id}" style="display: none;"><div class="close-button"></div>${content}</div>`).appendTo(alertBox);
alert.find('.close-button').click(() => {
alert.stop().fadeOut(200, () => alert.remove());
});
alert.stop().fadeIn(200);
};
if (id !== undefined) {
let sameAlert = alertBox.find(`.alert[data-alert-id=${id}]`);
if (sameAlert.length > 0) {
alertBox.stop().fadeOut(200, () => {
alertBox.html('');
alertBox.show();
});
return;
}
}
let oldAlerts = alertBox.find('.alert');
if (oldAlerts.length > 0){
alertBox.stop().fadeOut(200, () => {
alertBox.html('');
alertBox.show();
displayAlert();
});
return;
}
displayAlert();
}
}