mirror of
https://github.com/BlueMap-Minecraft/BlueMapWeb.git
synced 2024-11-24 19:05:10 +01:00
432 lines
12 KiB
JavaScript
432 lines
12 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 {Color, PerspectiveCamera, Raycaster, Scene, Vector2, Vector3, WebGLRenderer} from "three";
|
|
import {Map} from "./map/Map";
|
|
import {SkyboxScene} from "./skybox/SkyboxScene";
|
|
import {ControlsManager} from "./controls/ControlsManager";
|
|
import Stats from "./util/Stats";
|
|
import {alert, dispatchEvent, elementOffset, generateCacheHash, htmlToElement} from "./util/Utils";
|
|
import {TileManager} from "./map/TileManager";
|
|
import {HIRES_VERTEX_SHADER} from "./map/hires/HiresVertexShader";
|
|
import {HIRES_FRAGMENT_SHADER} from "./map/hires/HiresFragmentShader";
|
|
import {LOWRES_VERTEX_SHADER} from "./map/lowres/LowresVertexShader";
|
|
import {LOWRES_FRAGMENT_SHADER} from "./map/lowres/LowresFragmentShader";
|
|
import {CombinedCamera} from "./util/CombinedCamera";
|
|
import {CSS2DRenderer} from "./util/CSS2DRenderer";
|
|
import {MarkerSet} from "./markers/MarkerSet";
|
|
|
|
export class MapViewer {
|
|
|
|
/**
|
|
* @param element {Element}
|
|
* @param events {EventTarget}
|
|
*/
|
|
constructor(element, events = element) {
|
|
Object.defineProperty( this, 'isMapViewer', { value: true } );
|
|
|
|
this.rootElement = element;
|
|
this.events = events;
|
|
|
|
this.data = {
|
|
map: null,
|
|
camera: null,
|
|
controlsManager: null,
|
|
uniforms: {
|
|
sunlightStrength: { value: 1 },
|
|
ambientLight: { value: 0 },
|
|
skyColor: { value: new Color(0.5, 0.5, 1) },
|
|
hiresTileMap: {
|
|
value: {
|
|
map: null,
|
|
size: TileManager.tileMapSize,
|
|
scale: new Vector2(1, 1),
|
|
translate: new Vector2(),
|
|
pos: new Vector2(),
|
|
}
|
|
}
|
|
},
|
|
superSampling: 1,
|
|
loadedCenter: new Vector2(0, 0),
|
|
loadedHiresViewDistance: 200,
|
|
loadedLowresViewDistance: 2000,
|
|
}
|
|
|
|
this.tileCacheHash = generateCacheHash();
|
|
|
|
this.stats = new Stats();
|
|
this.stats.hide();
|
|
|
|
// renderer
|
|
this.renderer = new WebGLRenderer({
|
|
antialias: true,
|
|
sortObjects: true,
|
|
preserveDrawingBuffer: true,
|
|
logarithmicDepthBuffer: true,
|
|
});
|
|
this.renderer.autoClear = false;
|
|
this.renderer.uniforms = this.data.uniforms;
|
|
|
|
// CSS2D renderer
|
|
this.css2dRenderer = new CSS2DRenderer(this.events);
|
|
|
|
this.skyboxScene = new SkyboxScene(this.data.uniforms);
|
|
|
|
this.camera = new CombinedCamera(75, 1, 0.1, 10000, 0);
|
|
this.skyboxCamera = new PerspectiveCamera(75, 1, 0.1, 10000);
|
|
|
|
this.controlsManager = new ControlsManager(this, this.camera);
|
|
|
|
this.raycaster = new Raycaster();
|
|
this.raycaster.layers.enableAll();
|
|
this.raycaster.params.Line2 = {threshold: 20}
|
|
|
|
/** @type {Map} */
|
|
this.map = null;
|
|
|
|
this.markers = new MarkerSet("bm-root");
|
|
|
|
this.lastFrame = 0;
|
|
|
|
// initialize
|
|
this.initializeRootElement();
|
|
|
|
// handle window resizes
|
|
window.addEventListener("resize", this.handleContainerResize);
|
|
|
|
// start render-loop
|
|
requestAnimationFrame(this.renderLoop);
|
|
}
|
|
|
|
/**
|
|
* Initializes the root-element
|
|
*/
|
|
initializeRootElement() {
|
|
this.rootElement.innerHTML = "";
|
|
|
|
let outerDiv = htmlToElement(`<div style="position: relative; width: 100%; height: 100%; overflow: hidden;"></div>`);
|
|
this.rootElement.appendChild(outerDiv);
|
|
|
|
// 3d-canvas
|
|
outerDiv.appendChild(this.renderer.domElement);
|
|
|
|
// html-markers
|
|
this.css2dRenderer.domElement.style.position = 'absolute';
|
|
this.css2dRenderer.domElement.style.top = '0';
|
|
this.css2dRenderer.domElement.style.left = '0';
|
|
this.css2dRenderer.domElement.style.pointerEvents = 'none';
|
|
outerDiv.appendChild(this.css2dRenderer.domElement);
|
|
|
|
// performance monitor
|
|
outerDiv.appendChild(this.stats.dom);
|
|
|
|
this.handleContainerResize();
|
|
}
|
|
|
|
/**
|
|
* Updates the render-resolution and aspect ratio based on the size of the root-element
|
|
*/
|
|
handleContainerResize = () => {
|
|
this.renderer.setSize(this.rootElement.clientWidth, this.rootElement.clientHeight);
|
|
this.renderer.setPixelRatio(window.devicePixelRatio * this.superSampling);
|
|
|
|
this.css2dRenderer.setSize(this.rootElement.clientWidth, this.rootElement.clientHeight);
|
|
|
|
this.camera.aspect = this.rootElement.clientWidth / this.rootElement.clientHeight;
|
|
this.camera.updateProjectionMatrix();
|
|
};
|
|
|
|
/**
|
|
* Triggers an interaction on the screen (map), e.g. a mouse-click.
|
|
*
|
|
* This will first attempt to invoke the onClick() method on the Object3D (e.g. Markers) that has been clicked.
|
|
* And if none of those consumed the event, it will fire a <code>bluemapMapInteraction</code> event.
|
|
*
|
|
* @param screenPosition {Vector2} - Clicked position on the screen (usually event.x, event.y)
|
|
* @param data {object} - Custom event data that will be added to the interaction-event
|
|
*/
|
|
handleMapInteraction(screenPosition, data = {}) {
|
|
let rootOffset = elementOffset(this.rootElement);
|
|
let normalizedScreenPos = new Vector2(
|
|
((screenPosition.x - rootOffset.top) / this.rootElement.clientWidth) * 2 - 1,
|
|
-((screenPosition.y - rootOffset.left) / this.rootElement.clientHeight) * 2 + 1
|
|
);
|
|
|
|
if (this.map && this.map.isLoaded){
|
|
this.raycaster.setFromCamera(normalizedScreenPos, this.camera);
|
|
|
|
// check Object3D interactions
|
|
let intersects = this.raycaster.intersectObjects([this.map.hiresTileManager.scene, this.map.lowresTileManager.scene, this.markers], true);
|
|
let hit = null;
|
|
let lowresHit = null;
|
|
let hiresHit = null;
|
|
let covered = false;
|
|
for (let i = 0; i < intersects.length; i++) {
|
|
if (intersects[i].object){
|
|
let object = intersects[i].object;
|
|
|
|
// check if deeply-visible
|
|
let parent = object;
|
|
let visible = parent.visible;
|
|
while (visible && parent.parent){
|
|
parent = parent.parent;
|
|
visible = parent.visible;
|
|
}
|
|
|
|
if (visible) {
|
|
if (!hit) hit = intersects[i];
|
|
|
|
// find root-scene
|
|
let parentRoot = object;
|
|
while(parentRoot.parent) parentRoot = parentRoot.parent;
|
|
|
|
if (parentRoot === this.map.lowresTileManager.scene) {
|
|
if (!lowresHit) lowresHit = intersects[i];
|
|
}
|
|
|
|
if (parentRoot === this.map.hiresTileManager.scene) {
|
|
if (!hiresHit) hiresHit = intersects[i];
|
|
}
|
|
|
|
if (!covered || (object.material && !object.material.depthTest)) {
|
|
if (object.onClick && object.onClick({
|
|
data: data,
|
|
intersection: intersects[i]
|
|
})) return;
|
|
}
|
|
|
|
if (parentRoot !== this.map.lowresTileManager.scene) {
|
|
covered = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// fire event
|
|
dispatchEvent(this.events, "bluemapMapInteraction", {
|
|
data: data,
|
|
hit: hit,
|
|
hiresHit: hiresHit,
|
|
lowresHit: lowresHit,
|
|
intersections: intersects,
|
|
ray: this.raycaster.ray
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* The render-loop to update and possibly render a new frame.
|
|
* @param now {number} the current time in milliseconds
|
|
*/
|
|
renderLoop = (now) => {
|
|
requestAnimationFrame(this.renderLoop);
|
|
|
|
// calculate delta time
|
|
if (this.lastFrame <= 0) this.lastFrame = now;
|
|
let delta = now - this.lastFrame;
|
|
this.lastFrame = now;
|
|
|
|
// update stats
|
|
this.stats.begin();
|
|
|
|
// update controls
|
|
if (this.map != null) {
|
|
this.controlsManager.update(delta, this.map);
|
|
}
|
|
|
|
// render
|
|
this.render(delta);
|
|
|
|
// update stats
|
|
this.stats.update();
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
* Renders a frame
|
|
* @param delta {number}
|
|
*/
|
|
render(delta) {
|
|
dispatchEvent(this.events, "bluemapRenderFrame", {
|
|
delta: delta,
|
|
});
|
|
|
|
//prepare camera
|
|
this.camera.updateProjectionMatrix();
|
|
this.skyboxCamera.rotation.copy(this.camera.rotation);
|
|
this.skyboxCamera.updateProjectionMatrix();
|
|
|
|
//render
|
|
this.renderer.clear();
|
|
|
|
this.renderer.render(this.skyboxScene, this.skyboxCamera);
|
|
this.renderer.clearDepth();
|
|
|
|
if (this.map && this.map.isLoaded) {
|
|
//update uniforms
|
|
this.data.uniforms.hiresTileMap.value.pos.copy(this.map.hiresTileManager.centerTile);
|
|
|
|
this.renderer.render(this.map.lowresTileManager.scene, this.camera);
|
|
this.renderer.clearDepth();
|
|
|
|
if (this.controlsManager.distance < 2000) {
|
|
this.renderer.render(this.map.hiresTileManager.scene, this.camera);
|
|
}
|
|
}
|
|
|
|
// render markers
|
|
this.renderer.render(this.markers, this.camera);
|
|
this.css2dRenderer.render(this.markers, this.camera);
|
|
}
|
|
|
|
/**
|
|
* Changes / Sets the map that will be loaded and displayed
|
|
* @param map {Map}
|
|
* @returns Promise<void>
|
|
*/
|
|
switchMap(map = null) {
|
|
if (this.map && this.map.isMap) this.map.unload();
|
|
|
|
this.map = map;
|
|
|
|
if (this.map && this.map.isMap) {
|
|
return map.load(HIRES_VERTEX_SHADER, HIRES_FRAGMENT_SHADER, LOWRES_VERTEX_SHADER, LOWRES_FRAGMENT_SHADER, this.data.uniforms, this.tileCacheHash)
|
|
.then(() => {
|
|
for (let texture of this.map.loadedTextures){
|
|
this.renderer.initTexture(texture);
|
|
}
|
|
|
|
this.data.uniforms.skyColor.value = map.data.skyColor;
|
|
this.data.uniforms.ambientLight.value = map.data.ambientLight;
|
|
this.data.uniforms.hiresTileMap.value.map = map.hiresTileManager.tileMap.texture;
|
|
this.data.uniforms.hiresTileMap.value.scale.set(map.data.hires.tileSize.x, map.data.hires.tileSize.z);
|
|
this.data.uniforms.hiresTileMap.value.translate.set(map.data.hires.translate.x, map.data.hires.translate.z);
|
|
|
|
setTimeout(this.updateLoadedMapArea);
|
|
|
|
dispatchEvent(this.events, "bluemapMapChanged", {
|
|
map: map
|
|
});
|
|
})
|
|
.catch(error => {
|
|
alert(this.events, error, "error");
|
|
});
|
|
} else {
|
|
return Promise.resolve();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads the given area on the map (and unloads everything outside that area)
|
|
* @param centerX {number}
|
|
* @param centerZ {number}
|
|
* @param hiresViewDistance {number?}
|
|
* @param lowresViewDistance {number?}
|
|
*/
|
|
loadMapArea(centerX, centerZ, hiresViewDistance = -1, lowresViewDistance = -1) {
|
|
this.data.loadedCenter.set(centerX, centerZ);
|
|
if (hiresViewDistance >= 0) this.data.loadedHiresViewDistance = hiresViewDistance;
|
|
if (lowresViewDistance >= 0) this.data.loadedLowresViewDistance = lowresViewDistance;
|
|
|
|
this.updateLoadedMapArea();
|
|
}
|
|
|
|
updateLoadedMapArea = () => {
|
|
if (!this.map) return;
|
|
this.map.loadMapArea(this.data.loadedCenter.x, this.data.loadedCenter.y, this.data.loadedHiresViewDistance, this.data.loadedLowresViewDistance);
|
|
}
|
|
|
|
clearTileCache(newTileCacheHash) {
|
|
if (!newTileCacheHash) newTileCacheHash = generateCacheHash();
|
|
|
|
this.tileCacheHash = newTileCacheHash;
|
|
if (this.map) {
|
|
this.map.lowresTileManager.tileLoader.tileCacheHash = this.tileCacheHash;
|
|
this.map.hiresTileManager.tileLoader.tileCacheHash = this.tileCacheHash;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @returns {number}
|
|
*/
|
|
get superSampling() {
|
|
return this.data.superSampling;
|
|
}
|
|
|
|
/**
|
|
* @param value {number}
|
|
*/
|
|
set superSampling(value) {
|
|
this.data.superSampling = value;
|
|
this.handleContainerResize();
|
|
}
|
|
|
|
/**
|
|
* @returns {CombinedCamera}
|
|
*/
|
|
get camera() {
|
|
return this._camera;
|
|
}
|
|
|
|
/**
|
|
* @param value {CombinedCamera}
|
|
*/
|
|
set camera(value) {
|
|
this._camera = value;
|
|
this.data.camera = value.data;
|
|
}
|
|
|
|
/**
|
|
* @returns {ControlsManager}
|
|
*/
|
|
get controlsManager() {
|
|
return this._controlsManager;
|
|
}
|
|
|
|
/**
|
|
* @param value {ControlsManager}
|
|
*/
|
|
set controlsManager(value) {
|
|
this._controlsManager = value;
|
|
this.data.controlsManager = value.data;
|
|
}
|
|
|
|
/**
|
|
* @returns {Map}
|
|
*/
|
|
get map() {
|
|
return this._map;
|
|
}
|
|
|
|
/**
|
|
* @param value {Map}
|
|
*/
|
|
set map(value) {
|
|
this._map = value;
|
|
if (value) this.data.map = value.data;
|
|
}
|
|
|
|
}
|