itowns
Version:
A JS/WebGL framework for 3D geospatial data visualization
382 lines (361 loc) • 15.1 kB
JavaScript
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;
}
}