diff --git a/src/MapViewer.js b/src/MapViewer.js index a391524..7776d49 100644 --- a/src/MapViewer.js +++ b/src/MapViewer.js @@ -27,7 +27,7 @@ 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 {alert, dispatchEvent, elementOffset, generateCacheHash, htmlToElement, softClamp} from "./util/Utils"; import {TileManager} from "./map/TileManager"; import {HIRES_VERTEX_SHADER} from "./map/hires/HiresVertexShader"; import {HIRES_FRAGMENT_SHADER} from "./map/hires/HiresFragmentShader"; @@ -177,7 +177,14 @@ export class MapViewer { 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); + /* + const intersectScenes = [this.map.hiresTileManager.scene, this.markers]; + for (let i = 0; i < this.map.lowresTileManager.length; i++) { + intersectScenes.push(this.map.lowresTileManager[i].scene); + } + */ + + let intersects = this.raycaster.intersectObjects([this.map.hiresTileManager.scene, this.markers], true); let hit = null; let lowresHit = null; let hiresHit = null; @@ -201,9 +208,11 @@ export class MapViewer { 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]; @@ -216,9 +225,9 @@ export class MapViewer { })) return; } - if (parentRoot !== this.map.lowresTileManager.scene) { + //if (parentRoot !== this.map.lowresTileManager.scene) { covered = true; - } + //} } } } @@ -288,8 +297,13 @@ export class MapViewer { //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(); + const highestLod = this.map.lowresTileManager.length - 1; + for (let i = this.map.lowresTileManager.length - 1; i >= 0; i--) { + if (i === highestLod || this.controlsManager.distance < 1000 * Math.pow(this.map.data.lowres.lodFactor, i + 1)) { + this.renderer.render(this.map.lowresTileManager[i].scene, this.camera); + this.renderer.clearDepth(); + } + } if (this.controlsManager.distance < 1000) { this.renderer.render(this.map.hiresTileManager.scene, this.camera); @@ -363,7 +377,9 @@ export class MapViewer { this.tileCacheHash = newTileCacheHash; if (this.map) { - this.map.lowresTileManager.tileLoader.tileCacheHash = this.tileCacheHash; + for (let i = 0; i < this.map.lowresTileManager.length; i++) { + this.map.lowresTileManager[i].tileLoader.tileCacheHash = this.tileCacheHash; + } this.map.hiresTileManager.tileLoader.tileCacheHash = this.tileCacheHash; } } diff --git a/src/controls/map/MapControls.js b/src/controls/map/MapControls.js index 4b0d23a..ef2b727 100644 --- a/src/controls/map/MapControls.js +++ b/src/controls/map/MapControls.js @@ -83,6 +83,9 @@ export class MapControls { this.lastTap = -1; this.lastTapCenter = null; + + this.minDistance = 5; + this.maxDistance = 100000; } /** @@ -164,7 +167,7 @@ export class MapControls { this.keyZoom.update(delta, map); this.touchZoom.update(delta, map); - this.manager.distance = softClamp(this.manager.distance, 5, 10000, 0.8); + this.manager.distance = softClamp(this.manager.distance, this.minDistance, this.maxDistance, 0.8); // max angle for current distance let maxAngleForZoom = this.getMaxPerspectiveAngleForDistance(this.manager.distance); diff --git a/src/map/LowresTileLoader.js b/src/map/LowresTileLoader.js index 593e5f4..20b4398 100644 --- a/src/map/LowresTileLoader.js +++ b/src/map/LowresTileLoader.js @@ -33,34 +33,37 @@ import { NearestFilter, ClampToEdgeWrapping, NearestMipMapLinearFilter, - RepeatWrapping, + Vector2 } from "three"; export class LowresTileLoader { - constructor(tilePath, vertexShader, fragmentShader, tileCacheHash = 0, layer = 0) { + constructor(tilePath, tileSettings, lod, vertexShader, fragmentShader, uniforms, tileCacheHash = 0, layer = 0) { Object.defineProperty( this, 'isLowresTileLoader', { value: true } ); this.tilePath = tilePath; + this.tileSettings = tileSettings; + this.lod = lod; this.layer = layer; this.tileCacheHash = tileCacheHash; this.vertexShader = vertexShader; this.fragmentShader = fragmentShader; + this.uniforms = uniforms; this.textureLoader = new TextureLoader(); this.geometry = new PlaneBufferGeometry( - 500, 500, - 100, 100 + tileSettings.tileSize.x + 1, tileSettings.tileSize.z + 1, + Math.ceil(100 / (lod * 2)), Math.ceil(100 / (lod * 2)) ); this.geometry.deleteAttribute('normal'); this.geometry.deleteAttribute('uv'); this.geometry.rotateX(-Math.PI / 2); - this.geometry.translate(250, 0, 250); + this.geometry.translate(tileSettings.tileSize.x / 2 + 1, 0, tileSettings.tileSize.x / 2 + 1); } load = (tileX, tileZ) => { - let tileUrl = this.tilePath + pathFromCoords(tileX, tileZ) + '.png'; + let tileUrl = this.tilePath + this.lod + "/" + pathFromCoords(tileX, tileZ) + '.png'; return new Promise((resolve, reject) => { this.textureLoader.load(tileUrl + '?' + this.tileCacheHash, @@ -76,6 +79,13 @@ export class LowresTileLoader { let material = new ShaderMaterial({ uniforms: { + ...this.uniforms, + tileSize: { + value: new Vector2(this.tileSettings.tileSize.x, this.tileSettings.tileSize.z) + }, + textureSize: { + value: new Vector2(texture.image.width, texture.image.height) + }, textureImage: { type: 't', value: texture @@ -94,13 +104,12 @@ export class LowresTileLoader { let object = new Mesh(this.geometry, material); if (this.layer) object.layers.set(this.layer); - let tileSize = {x:500, z:500}; - let translate = {x:0, z:0}; - let scale = {x:1, z:1}; - object.position.set(tileX * tileSize.x + translate.x, 0, tileZ * tileSize.z + translate.z); - object.scale.set(scale.x, 1, scale.z); + const scale = Math.pow(this.tileSettings.lodFactor, this.lod - 1); + object.position.set(tileX * this.tileSettings.tileSize.x * scale, 0, tileZ * this.tileSettings.tileSize.z * scale); + object.scale.set(scale, 1, scale); object.userData.tileUrl = tileUrl; + object.userData.tileType = "lowres"; object.updateMatrixWorld(true); diff --git a/src/map/Map.js b/src/map/Map.js index 6188c39..c51c75b 100644 --- a/src/map/Map.js +++ b/src/map/Map.js @@ -36,7 +36,7 @@ import { Vector3, VertexColors } from "three"; -import {alert, dispatchEvent, generateCacheHash, hashTile, stringToImage, vecArrToObj} from "../util/Utils"; +import {alert, dispatchEvent, generateCacheHash, getPixel, hashTile, stringToImage, vecArrToObj} from "../util/Utils"; import {TileManager} from "./TileManager"; import {TileLoader} from "./TileLoader"; import {LowresTileLoader} from "./LowresTileLoader"; @@ -70,8 +70,8 @@ export class Map { }, lowres: { tileSize: {x: 32, z: 32}, - scale: {x: 1, z: 1}, - translate: {x: 2, z: 2} + lodFactor: 5, + lodCount: 3 } }; @@ -86,7 +86,7 @@ export class Map { /** @type {TileManager} */ this.hiresTileManager = null; - /** @type {TileManager} */ + /** @type {TileManager[]} */ this.lowresTileManager = null; } @@ -116,10 +116,13 @@ export class Map { this.hiresMaterial = this.createHiresMaterial(hiresVertexShader, hiresFragmentShader, uniforms, textures); this.hiresTileManager = new TileManager(new Scene(), new TileLoader(`${this.data.dataUrl}tiles/0/`, this.hiresMaterial, this.data.hires, tileCacheHash), this.onTileLoad("hires"), this.onTileUnload("hires"), this.events); - this.lowresTileManager = new TileManager(new Scene(), new LowresTileLoader(`${this.data.dataUrl}tiles/1/`, lowresVertexShader, lowresFragmentShader, tileCacheHash), this.onTileLoad("lowres"), this.onTileUnload("lowres"), this.events); + this.hiresTileManager.scene.autoUpdate = false; - this.hiresTileManager.scene.autoUpdate = false; - this.lowresTileManager.scene.autoUpdate = false; + this.lowresTileManager = []; + for (let i = 0; i < this.data.lowres.lodCount; i++) { + this.lowresTileManager[i] = new TileManager(new Scene(), new LowresTileLoader(`${this.data.dataUrl}tiles/`, this.data.lowres, i + 1, lowresVertexShader, lowresFragmentShader, uniforms, tileCacheHash), this.onTileLoad("lowres"), this.onTileUnload("lowres"), this.events); + this.lowresTileManager[i].scene.autoUpdate = false; + } alert(this.events, `Map '${this.data.id}' is loaded.`, "fine"); }); @@ -158,8 +161,8 @@ export class Map { }; this.data.lowres = { tileSize: {...this.data.lowres.tileSize, ...vecArrToObj(worldSettings.lowres.tileSize, true)}, - scale: {...this.data.lowres.scale, ...vecArrToObj(worldSettings.lowres.scale, true)}, - translate: {...this.data.lowres.translate, ...vecArrToObj(worldSettings.lowres.translate, true)} + lodFactor: worldSettings.lowres.lodFactor !== undefined ? worldSettings.lowres.lodFactor : this.data.lowres.lodFactor, + lodCount: worldSettings.lowres.lodCount !== undefined ? worldSettings.lowres.lodCount : this.data.lowres.lodCount }; alert(this.events, `Settings for map '${this.data.id}' loaded.`, "fine"); @@ -189,18 +192,21 @@ export class Map { loadMapArea(x, z, hiresViewDistance, lowresViewDistance) { if (!this.isLoaded) return; - let hiresX = Math.floor((x - this.data.hires.translate.x) / this.data.hires.tileSize.x); - let hiresZ = Math.floor((z - this.data.hires.translate.z) / this.data.hires.tileSize.z); - let hiresViewX = Math.floor(hiresViewDistance / this.data.hires.tileSize.x); - let hiresViewZ = Math.floor(hiresViewDistance / this.data.hires.tileSize.z); - - let lowresX = Math.floor((x - this.data.lowres.translate.x) / this.data.lowres.tileSize.x); - let lowresZ = Math.floor((z - this.data.lowres.translate.z) / this.data.lowres.tileSize.z); - let lowresViewX = Math.floor(lowresViewDistance / this.data.lowres.tileSize.x); - let lowresViewZ = Math.floor(lowresViewDistance / this.data.lowres.tileSize.z); - + const hiresX = Math.floor((x - this.data.hires.translate.x) / this.data.hires.tileSize.x); + const hiresZ = Math.floor((z - this.data.hires.translate.z) / this.data.hires.tileSize.z); + const hiresViewX = Math.floor(hiresViewDistance / this.data.hires.tileSize.x); + const hiresViewZ = Math.floor(hiresViewDistance / this.data.hires.tileSize.z); this.hiresTileManager.loadAroundTile(hiresX, hiresZ, hiresViewX, hiresViewZ); - this.lowresTileManager.loadAroundTile(lowresX, lowresZ, lowresViewX, lowresViewZ); + + for (let i = 0; i < this.lowresTileManager.length; i++) { + const lod = i + 1; + const scale = Math.pow(this.data.lowres.lodFactor, lod - 1); + const lowresX = Math.floor(x / (this.data.lowres.tileSize.x * scale)); + const lowresZ = Math.floor(z / (this.data.lowres.tileSize.z * scale)); + const lowresViewX = Math.floor(lowresViewDistance / this.data.lowres.tileSize.x); + const lowresViewZ = Math.floor(lowresViewDistance / this.data.lowres.tileSize.z); + this.lowresTileManager[i].loadAroundTile(lowresX, lowresZ, lowresViewX, lowresViewZ); + } } /** @@ -332,8 +338,12 @@ export class Map { if (this.hiresTileManager) this.hiresTileManager.unload(); this.hiresTileManager = null; - if (this.lowresTileManager) this.lowresTileManager.unload(); - this.lowresTileManager = null; + if (this.lowresTileManager) { + for (let i = 0; i < this.lowresTileManager.length; i++) { + this.lowresTileManager[i].unload(); + } + this.lowresTileManager = null; + } if (this.hiresMaterial) this.hiresMaterial.forEach(material => material.dispose()); this.hiresMaterial = null; @@ -364,23 +374,40 @@ export class Map { let hiresTileHash = hashTile(Math.floor((x - this.data.hires.translate.x) / this.data.hires.tileSize.x), Math.floor((z - this.data.hires.translate.z) / this.data.hires.tileSize.z)); let tile = this.hiresTileManager.tiles.get(hiresTileHash); - if (!tile || !tile.model) { - let lowresTileHash = hashTile(Math.floor((x - this.data.lowres.translate.x) / this.data.lowres.tileSize.x), Math.floor((z - this.data.lowres.translate.z) / this.data.lowres.tileSize.z)); - tile = this.lowresTileManager.tiles.get(lowresTileHash); - } - if (!tile || !tile.model){ - return false; - } - - try { - let intersects = this.raycaster.intersectObjects([tile.model]); - if (intersects.length > 0) { - return intersects[0].point.y; + if (tile?.model) { + try { + let intersects = this.raycaster.intersectObjects([tile.model]); + if (intersects.length > 0) { + return intersects[0].point.y; + } + } catch (ignore) { + //empty } - } catch (err) { - return false; } + + for (let i = 0; i < this.lowresTileManager.length; i++) { + const lod = i + 1; + const scale = Math.pow(this.data.lowres.lodFactor, lod - 1); + const scaledTileSize = { + x: this.data.lowres.tileSize.x * scale, + z: this.data.lowres.tileSize.z * scale + } + const tileX = Math.floor(x / scaledTileSize.x); + const tileZ = Math.floor(z / scaledTileSize.z); + let lowresTileHash = hashTile(tileX, tileZ); + tile = this.lowresTileManager[i].tiles.get(lowresTileHash); + + if (!tile || !tile.model) continue; + + const texture = tile.model.material.uniforms?.textureImage?.value?.image; + if (texture == null) continue; + + const color = getPixel(texture, x - tileX * scaledTileSize.x, z - tileZ * scaledTileSize.z + this.data.lowres.tileSize.z); + return color[2]; + } + + return false; } dispose() { diff --git a/src/map/Tile.js b/src/map/Tile.js index db398fe..239c46b 100644 --- a/src/map/Tile.js +++ b/src/map/Tile.js @@ -60,7 +60,7 @@ export class Tile { return tileLoader.load(this.x, this.z) .then(model => { if (this.unloaded){ - model.geometry.dispose(); + Tile.disposeModel(model); return; } @@ -77,11 +77,23 @@ export class Tile { if (this.model) { this.onUnload(this); - this.model.geometry.dispose(); + Tile.disposeModel(this.model); + this.model = null; } } + static disposeModel(model) { + if (model.userData?.tileType === "hires") { + model.geometry.dispose(); + } + + else if (model.userData?.tileType === "lowres") { + model.material.uniforms.textureImage.value.dispose(); + model.material.dispose(); + } + } + /** * @returns {boolean} */ diff --git a/src/map/TileLoader.js b/src/map/TileLoader.js index 3ba27ce..a2f961f 100644 --- a/src/map/TileLoader.js +++ b/src/map/TileLoader.js @@ -79,6 +79,7 @@ export class TileLoader { object.scale.set(scale.x, 1, scale.z); object.userData.tileUrl = tileUrl; + object.userData.tileType = "hires"; object.updateMatrixWorld(true); diff --git a/src/map/TileMap.js b/src/map/TileMap.js index 056a1c8..cc5e2fe 100644 --- a/src/map/TileMap.js +++ b/src/map/TileMap.js @@ -22,7 +22,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ -import {ClampToEdgeWrapping, LinearFilter, Texture} from "three"; +import {ClampToEdgeWrapping, LinearFilter, NearestFilter, Texture} from "three"; export class TileMap { diff --git a/src/map/lowres/LowresFragmentShader.js b/src/map/lowres/LowresFragmentShader.js index 8659176..76c4c57 100644 --- a/src/map/lowres/LowresFragmentShader.js +++ b/src/map/lowres/LowresFragmentShader.js @@ -39,41 +39,59 @@ struct TileMap { vec2 pos; }; -//uniform float sunlightStrength; -//uniform float ambientLight; -//uniform TileMap hiresTileMap; +uniform float sunlightStrength; +uniform float ambientLight; +uniform TileMap hiresTileMap; uniform sampler2D textureImage; +uniform vec2 tileSize; +uniform vec2 textureSize; varying vec3 vPosition; varying vec3 vWorldPosition; -varying vec3 vNormal; -varying vec2 vUv; -varying vec3 vColor; varying float vDistance; +float metaToHeight(vec4 meta) { + float heightUnsigned = meta.g * 65280.0 + meta.b * 255.0; + if (heightUnsigned >= 32768.0) { + return -(65535.0 - heightUnsigned); + } else { + return heightUnsigned; + } +} + +float metaToLight(vec4 meta) { + return meta.r * 255.0; +} + +vec2 posToColorUV(vec2 pos) { + return vec2(pos.x / textureSize.x, min(pos.y, tileSize.y) / textureSize.y); +} + +vec2 posToMetaUV(vec2 pos) { + return vec2(pos.x / textureSize.x, pos.y / textureSize.y + 0.5); +} + void main() { //discard if hires tile is loaded at that position - //if (vDistance < 1900.0 && texture(hiresTileMap.map, ((vWorldPosition.xz - hiresTileMap.translate) / hiresTileMap.scale - hiresTileMap.pos) / hiresTileMap.size + 0.5).r >= 1.0) discard; + //if (vDistance < 900.0 && texture(hiresTileMap.map, ((vWorldPosition.xz - hiresTileMap.translate) / hiresTileMap.scale - hiresTileMap.pos) / hiresTileMap.size + 0.5).r > 0.75) discard; - //vec4 color = vec4(vColor, 1.0); - - //float diff = max(dot(vNormal, vec3(0.3637, 0.7274, 0.5819)), 0.0) * 0.3 + 0.7; - //color *= diff; - - //color *= mix(sunlightStrength, 1.0, ambientLight); - - //vec4 color = vec4(0.3637, 0.7274, 0.5819, 1.0); - vec4 color = texture(textureImage, vec2(vPosition.x / 500.0, vPosition.z / 1000.0)); + vec4 color = texture(textureImage, posToColorUV(vPosition.xz)); + vec4 meta = texture(textureImage, posToMetaUV(vPosition.xz)); - float height = texture(textureImage, vec2(vPosition.x / 500.0, (vPosition.z + 500.0) / 1000.0)).b * 255.0; - float heightX = texture(textureImage, vec2((vPosition.x + 1.0) / 500.0, (vPosition.z + 500.0) / 1000.0)).b * 255.0; - float heightZ = texture(textureImage, vec2(vPosition.x / 500.0, (vPosition.z + 501.0) / 1000.0)).b * 255.0; + float height = metaToHeight(meta); + + float heightX = metaToHeight(texture(textureImage, posToMetaUV(vPosition.xz + vec2(1.0, 0.0)))); + float heightZ = metaToHeight(texture(textureImage, posToMetaUV(vPosition.xz + vec2(0.0, 1.0)))); float diff = (height - heightX) + (height - heightZ); - color.rgb += clamp(diff * 0.04, -0.2, 0.04); + float blockLight = metaToLight(meta); + float light = mix(blockLight, 15.0, sunlightStrength); + color.rgb *= mix(ambientLight, 1.0, light / 15.0); + gl_FragColor = color; ${ShaderChunk.logdepthbuf_fragment} } + `; diff --git a/src/map/lowres/LowresVertexShader.js b/src/map/lowres/LowresVertexShader.js index 6ab35d3..5229a7f 100644 --- a/src/map/lowres/LowresVertexShader.js +++ b/src/map/lowres/LowresVertexShader.js @@ -29,31 +29,41 @@ export const LOWRES_VERTEX_SHADER = ` ${ShaderChunk.logdepthbuf_pars_vertex} uniform sampler2D textureImage; +uniform vec2 tileSize; +uniform vec2 textureSize; varying vec3 vPosition; varying vec3 vWorldPosition; -varying vec3 vNormal; -varying vec2 vUv; -varying vec3 vColor; varying float vDistance; +float metaToHeight(vec4 meta) { + float heightUnsigned = meta.g * 65280.0 + meta.b * 255.0; + if (heightUnsigned >= 32768.0) { + return -(65535.0 - heightUnsigned); + } else { + return heightUnsigned; + } +} + +vec2 posToMetaUV(vec2 pos) { + return vec2(pos.x / textureSize.x, pos.y / textureSize.y + 0.5); +} + void main() { vPosition = position; - vec4 color = texture(textureImage, vec2(position.x / 500.0, (position.z + 500.0) / 1000.0)); - vPosition.y += color.b * 255.0; + vec4 meta = texture(textureImage, posToMetaUV(position.xz)); + vPosition.y += metaToHeight(meta); vec4 worldPos = modelMatrix * vec4(vPosition, 1); vec4 viewPos = viewMatrix * worldPos; vWorldPosition = worldPos.xyz; - //vNormal = normal; - //vUv = uv; - //vColor = color; vDistance = -viewPos.z; gl_Position = projectionMatrix * viewPos; ${ShaderChunk.logdepthbuf_vertex} } + `; diff --git a/src/util/Utils.js b/src/util/Utils.js index 7d18426..2bffb68 100644 --- a/src/util/Utils.js +++ b/src/util/Utils.js @@ -380,3 +380,13 @@ export const vecArrToObj = (val, useZ = false) => { } return {}; } + +const pixel = document.createElement('canvas'); +pixel.width = 1; +pixel.height = 1; +const pixelContext = pixel.getContext('2d'); + +export const getPixel = (img, x, y) => { + pixelContext.drawImage(img, x, y, 1, 1, 0, 0, 1, 1); + return pixelContext.getImageData(0, 0, 1, 1).data; +}