UNPKG

itowns

Version:

A JS/WebGL framework for 3D geospatial data visualization

382 lines (361 loc) 15.1 kB
import * as THREE from 'three'; /* babel-plugin-inline-import './Shader/TileVS.glsl' */ const TileVS = "#include <itowns/precision_qualifier>\n#include <common>\n#include <itowns/elevation_pars_vertex>\n#include <logdepthbuf_pars_vertex>\n#if NUM_CRS > 1\nattribute float uv_1;\n#endif\n\nuniform bool lightingEnabled;\nvarying vec2 vHighPrecisionZW;\n\n#if MODE == MODE_FINAL\n#include <fog_pars_vertex>\nvarying vec3 vUv;\nvarying vec3 vNormal;\n#endif\nvoid main() {\n #include <begin_vertex>\n #include <itowns/elevation_vertex>\n #include <itowns/geoid_vertex>\n #include <project_vertex>\n #include <logdepthbuf_vertex>\n vHighPrecisionZW = gl_Position.zw;\n#if MODE == MODE_FINAL\n #include <fog_vertex>\n #if NUM_CRS > 1\n vUv = vec3(uv, (uv_1 > 0.) ? uv_1 : uv.y); // set uv_1 = uv if uv_1 is undefined\n #else\n vUv = vec3(uv, 0.0);\n #endif\n vNormal = normalize ( mat3( modelMatrix[0].xyz, modelMatrix[1].xyz, modelMatrix[2].xyz ) * normal );\n#endif\n}\n"; /* babel-plugin-inline-import './Shader/TileFS.glsl' */ const TileFS = "#include <itowns/precision_qualifier>\n#include <logdepthbuf_pars_fragment>\n#include <itowns/pitUV>\n#include <itowns/color_layers_pars_fragment>\n#if MODE == MODE_FINAL\n#include <fog_pars_fragment>\n#include <itowns/overlay_pars_fragment>\n#include <itowns/lighting_pars_fragment>\n#endif\n#include <itowns/mode_pars_fragment>\n\nuniform vec3 diffuse;\nuniform float opacity;\nvarying vec3 vUv; // uv.x/uv_1.x, uv.y, uv_1.y\nvarying vec2 vHighPrecisionZW;\n\nvoid main() {\n #include <logdepthbuf_fragment>\n\n#if MODE == MODE_ID\n\n #include <itowns/mode_id_fragment>\n\n#elif MODE == MODE_DEPTH\n\n #include <itowns/mode_depth_fragment>\n\n#else\n\n gl_FragColor = vec4(diffuse, opacity);\n\n uvs[0] = vec3(vUv.xy, 0.);\n\n#if NUM_CRS > 1\n uvs[1] = vec3(vUv.x, fract(vUv.z), floor(vUv.z));\n#endif\n\n vec4 color;\n #pragma unroll_loop\n for ( int i = 0; i < NUM_FS_TEXTURES; i ++ ) {\n color = getLayerColor( i , colorTextures, colorOffsetScales[ i ], colorLayers[ i ]);\n gl_FragColor.rgb = mix(gl_FragColor.rgb, color.rgb, color.a);\n }\n\n #if DEBUG == 1\n if (showOutline) {\n #pragma unroll_loop\n for ( int i = 0; i < NUM_CRS; i ++) {\n color = getOutlineColor( outlineColors[ i ], uvs[ i ].xy);\n gl_FragColor.rgb = mix(gl_FragColor.rgb, color.rgb, color.a);\n }\n }\n #endif\n\n #include <fog_fragment>\n #include <itowns/lighting_fragment>\n #include <itowns/overlay_fragment>\n\n#endif\n}\n"; import ShaderUtils from "./Shader/ShaderUtils.js"; import Capabilities from "../Core/System/Capabilities.js"; import RenderMode from "./RenderMode.js"; import { LRUCache } from 'lru-cache'; import { makeDataArrayRenderTarget } from "./WebGLComposer.js"; const identityOffsetScale = new THREE.Vector4(0.0, 0.0, 1.0, 1.0); // from three.js packDepthToRGBA const UnpackDownscale = 255 / 256; // 0..1 -> fraction (excluding 1) const bitSh = new THREE.Vector4(UnpackDownscale, UnpackDownscale / 256.0, UnpackDownscale / (256.0 * 256.0), UnpackDownscale / (256.0 * 256.0 * 256.0)); export function unpack1K(color, factor) { return factor ? bitSh.dot(color) * factor : bitSh.dot(color); } const samplersElevationCount = 1; export function getMaxColorSamplerUnitsCount() { const maxSamplerUnitsCount = Capabilities.getMaxTextureUnitsCount(); return maxSamplerUnitsCount - samplersElevationCount; } export const colorLayerEffects = { noEffect: 0, removeLightColor: 1, removeWhiteColor: 2, customEffect: 3 }; /** GPU struct for color layers */ /** GPU struct for elevation layers */ /** Default GPU struct values for initialization QoL */ const defaultStructLayers = { color: { textureOffset: 0, crs: 0, opacity: 0, effect_parameter: 0, effect_type: colorLayerEffects.noEffect, transparent: false }, elevation: { scale: 0, bias: 0, mode: 0, zmin: 0, zmax: 0 } }; const rtCache = new LRUCache({ max: 200, dispose: rt => { rt.dispose(); } }); /** * Updates the uniforms for layered textures, * including populating the DataArrayTexture * with content from individual 2D textures on the GPU. * * @param uniforms - The uniforms object for your material. * @param tiles - An array of RasterTile objects, each containing textures. * @param max - The maximum number of layers for the DataArrayTexture. * @param type - Layer set identifier: 'c' for color, 'e' for elevation. * @param renderer - The renderer used to render textures. */ function updateLayersUniforms(uniforms, tiles, max, type, renderer) { // Aliases for readability const uLayers = uniforms.layers.value; const uTextures = uniforms.textures; const uOffsetScales = uniforms.offsetScales.value; const uTextureCount = uniforms.textureCount; // Flatten the 2d array: [i, j] -> layers[_layerIds[i]].textures[j] let count = 0; let width = 0; let height = 0; // Determine total count of textures and dimensions // (assuming all textures are same size) let textureSetId = type; for (const tile of tiles) { // FIXME: RasterElevationTile are always passed to this function alone // so this works, but it's really not great even ignoring the dynamic // addition of a field. // @ts-expect-error: adding field to passed layer tile.textureOffset = count; for (let i = 0; i < tile.textures.length && count < max; ++i, ++count) { const texture = tile.textures[i]; if (!texture) { continue; } textureSetId += `${texture.id}.`; uOffsetScales[count] = tile.offsetScales[i]; uLayers[count] = tile; const img = texture.image; if (!img || img.width <= 0 || img.height <= 0) { console.error('Texture image not loaded or has zero dimensions'); uTextureCount.value = 0; return; } else if (count == 0) { width = img.width; height = img.height; } else if (width !== img.width || height !== img.height) { console.error('Texture dimensions mismatch'); uTextureCount.value = 0; return; } } } const cachedRT = rtCache.get(textureSetId); if (cachedRT) { uTextures.value = cachedRT.texture; uTextureCount.value = count; return; } // Memory management of these textures is only done by `textureArraysCache`, // so we don't have to dispose of them manually. const rt = makeDataArrayRenderTarget(width, height, count, tiles, max, renderer); if (!rt) { uTextureCount.value = 0; return; } rtCache.set(textureSetId, rt); uniforms.textures.value = rt.texture; if (count > max) { console.warn(`LayeredMaterial: Not enough texture units (${max} < ${count}), ` + 'excess textures have been discarded.'); } uTextureCount.value = count; } export const ELEVATION_MODES = { RGBA: 0, COLOR: 1, DATA: 2 }; /** * Convenience type that wraps all of the generic type's fields in * [THREE.IUniform]s. */ /** List of the uniform types required for a LayeredMaterial. */ let nbSamplers; const fragmentShader = []; /** Replacing the default uniforms dynamic type with our own static map. */ /** Fills in a Partial object's field and narrows the type accordingly. */ function fillInProp(obj, name, value) { if (obj[name] === undefined) { obj[name] = value; } } /** * Initialiszes elevation and render mode defines and narrows the type * accordingly. */ function initModeDefines(defines) { Object.keys(ELEVATION_MODES).forEach(key => fillInProp(defines, `ELEVATION_${key}`, ELEVATION_MODES[key])); Object.keys(RenderMode.MODES).forEach(key => fillInProp(defines, `MODE_${key}`, RenderMode.MODES[key])); } /** Material that handles the overlap of multiple raster tiles. */ export class LayeredMaterial extends THREE.ShaderMaterial { _visible = true; constructor() { let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; let crsCount = arguments.length > 1 ? arguments[1] : undefined; super(options); nbSamplers ??= [samplersElevationCount, getMaxColorSamplerUnitsCount()]; const defines = {}; fillInProp(defines, 'NUM_VS_TEXTURES', nbSamplers[0]); fillInProp(defines, 'NUM_FS_TEXTURES', nbSamplers[1]); fillInProp(defines, 'NUM_CRS', crsCount); initModeDefines(defines); fillInProp(defines, 'MODE', RenderMode.MODES.FINAL); fillInProp(defines, 'DEBUG', +false); this.defines = defines; this.fog = true; // receive the fog defined on the scene, if any this.vertexShader = TileVS; // three loop unrolling of ShaderMaterial only supports integer bounds, // see https://github.com/mrdoob/three.js/issues/28020 fragmentShader[crsCount] ??= ShaderUtils.unrollLoops(TileFS, defines); this.fragmentShader = fragmentShader[crsCount]; this.initUniforms({ // Color uniforms diffuse: new THREE.Color(0.04, 0.23, 0.35), opacity: this.opacity, // Lighting uniforms lightingEnabled: false, lightPosition: new THREE.Vector3(-0.5, 0.0, 1.0), // Misc properties fogNear: 1, fogFar: 1000, fogColor: new THREE.Color(0.76, 0.85, 1.0), fogDensity: 0.00025, overlayAlpha: 0, overlayColor: new THREE.Color(1.0, 0.3, 0.0), objectId: 0, geoidHeight: 0.0, // > 0 produces gaps, // < 0 causes oversampling of textures // = 0 causes sampling artefacts due to bad estimation of texture-uv // gradients // best is a small negative number minBorderDistance: -0.01 }); // LayeredMaterialLayers this.colorTiles = []; this.colorTileIds = []; this.layersNeedUpdate = false; // elevation/color layer uniforms, to be updated using updateUniforms() this.initUniforms({ elevationLayers: new Array(nbSamplers[0]).fill(defaultStructLayers.elevation), elevationTextures: null, elevationOffsetScales: new Array(nbSamplers[0]).fill(identityOffsetScale), elevationTextureCount: 0, colorLayers: new Array(nbSamplers[1]).fill(defaultStructLayers.color), colorTextures: null, colorOffsetScales: new Array(nbSamplers[1]).fill(identityOffsetScale), colorTextureCount: 0 }); // Can't do an ES6 getter/setter here because it would override the // Material::visible property with accessors, which is not allowed. Object.defineProperty(this, 'visible', { // Knowing the visibility of a `LayeredMaterial` is useful. For // example in a `GlobeView`, if you zoom in, "parent" tiles seems // hidden; in fact, there are not, it is only their material (so // `LayeredMaterial`) that is set to not visible. // Adding an event when changing this property can be useful to // hide others things, like in `TileDebug`, or in later PR to come // (#1303 for example). // TODO : verify if there is a better mechanism to avoid this event get() { return this._visible; }, set(v) { if (this._visible != v) { this._visible = v; this.dispatchEvent({ type: v ? 'shown' : 'hidden' }); } } }); // setTimeout(() => console.log(this), 2); } get mode() { return this.defines.MODE; } set mode(mode) { if (this.defines.MODE != mode) { this.defines.MODE = mode; this.needsUpdate = true; } } getUniform(name) { return this.uniforms[name]?.value; } setUniform(name, value) { const uniform = this.uniforms[name]; if (uniform === undefined) { return; } if (uniform.value !== value) { uniform.value = value; } } initUniforms(uniforms) { for (const [name, value] of Object.entries(uniforms)) { if (this.uniforms[name] === undefined) { this.uniforms[name] = { value }; } } } setUniforms(uniforms) { for (const [name, value] of Object.entries(uniforms)) { this.setUniform(name, value); } } getLayerUniforms(type) { return { layers: this.uniforms[`${type}Layers`], textures: this.uniforms[`${type}Textures`], offsetScales: this.uniforms[`${type}OffsetScales`], textureCount: this.uniforms[`${type}TextureCount`] }; } updateLayersUniforms(renderer) { const colorlayers = this.colorTiles.filter(rt => rt.visible && rt.opacity > 0); colorlayers.sort((a, b) => this.colorTileIds.indexOf(a.id) - this.colorTileIds.indexOf(b.id)); updateLayersUniforms(this.getLayerUniforms('color'), colorlayers, this.defines.NUM_FS_TEXTURES, 'c', renderer); if (this.elevationTileId !== undefined && this.getElevationTile()) { if (this.elevationTile !== undefined) { updateLayersUniforms(this.getLayerUniforms('elevation'), [this.elevationTile], this.defines.NUM_VS_TEXTURES, 'e', renderer); } } this.layersNeedUpdate = false; } dispose() { this.dispatchEvent({ type: 'dispose' }); this.colorTiles.forEach(l => l.dispose(true)); this.colorTiles.length = 0; this.elevationTile?.dispose(true); this.layersNeedUpdate = true; } setColorTileIds(ids) { this.colorTileIds = ids; this.layersNeedUpdate = true; } setElevationTileId(id) { this.elevationTileId = id; this.layersNeedUpdate = true; } removeTile(tileId) { const index = this.colorTiles.findIndex(l => l.id === tileId); if (index > -1) { this.colorTiles[index].dispose(); this.colorTiles.splice(index, 1); const idSeq = this.colorTileIds.indexOf(tileId); if (idSeq > -1) { this.colorTileIds.splice(idSeq, 1); } return; } if (this.elevationTileId === tileId) { this.elevationTile?.dispose(); this.elevationTileId = undefined; this.elevationTile = undefined; } } addColorTile(rasterTile) { if (rasterTile.layer.id in this.colorTiles) { console.warn('Layer "{layer.id}" already present in material, overwriting.'); } this.colorTiles.push(rasterTile); } setElevationTile(rasterTile) { const old = this.elevationTile; if (old !== undefined) { old.dispose(); } this.elevationTile = rasterTile; } getColorTile(id) { return this.colorTiles.find(l => l.id === id); } getElevationTile() { return this.elevationTile; } getTile(id) { return this.elevationTile?.id === id ? this.elevationTile : this.colorTiles.find(l => l.id === id); } getTiles(ids) { // NOTE: this could instead be a mapping with an undefined in place of // unfound IDs. Need to identify a use case for it though as it would // probably have a performance cost (albeit minor in the grand scheme of // things). const res = this.colorTiles.filter(l => ids.includes(l.id)); if (this.elevationTile !== undefined && ids.includes(this.elevationTile?.id)) { res.push(this.elevationTile); } return res; } }