diff --git a/src/Converter/convertToTile.js b/src/Converter/convertToTile.js index 56fd6420a6..8e2180d154 100644 --- a/src/Converter/convertToTile.js +++ b/src/Converter/convertToTile.js @@ -5,7 +5,7 @@ */ import * as THREE from 'three'; import TileMesh from 'Core/TileMesh'; -import LayeredMaterial from 'Renderer/LayeredMaterial'; +import { LayeredMaterial } from 'Renderer/LayeredMaterial'; import { newTileGeometry } from 'Core/Prefab/TileBuilder'; import ReferLayerProperties from 'Layer/ReferencingLayerProperties'; import { geoidLayerIsVisible } from 'Layer/GeoidLayer'; diff --git a/src/Layer/ColorLayer.js b/src/Layer/ColorLayer.js index 082879f4f6..2d3b94fce8 100644 --- a/src/Layer/ColorLayer.js +++ b/src/Layer/ColorLayer.js @@ -146,9 +146,9 @@ class ColorLayer extends RasterLayer { setupRasterNode(node) { const rasterColorNode = new RasterColorTile(node.material, this); - node.material.addLayer(rasterColorNode); + node.material.addColorLayer(rasterColorNode); // set up ColorLayer ordering. - node.material.setSequence(this.parent.colorLayersOrder); + node.material.setColorLayerIds(this.parent.colorLayersOrder); return rasterColorNode; } diff --git a/src/Layer/ElevationLayer.js b/src/Layer/ElevationLayer.js index 2821298cab..33378f9b04 100644 --- a/src/Layer/ElevationLayer.js +++ b/src/Layer/ElevationLayer.js @@ -117,8 +117,8 @@ class ElevationLayer extends RasterLayer { setupRasterNode(node) { const rasterElevationNode = new RasterElevationTile(node.material, this); - node.material.addLayer(rasterElevationNode); - node.material.setSequenceElevation(this.id); + node.material.setElevationLayer(rasterElevationNode); + node.material.setElevationLayerId(this.id); // bounding box initialisation const updateBBox = () => node.setBBoxZ({ min: rasterElevationNode.min, max: rasterElevationNode.max, scale: this.scale, diff --git a/src/Layer/TiledGeometryLayer.js b/src/Layer/TiledGeometryLayer.js index 1502e50a7a..23a1ee0fea 100644 --- a/src/Layer/TiledGeometryLayer.js +++ b/src/Layer/TiledGeometryLayer.js @@ -403,7 +403,7 @@ class TiledGeometryLayer extends GeometryLayer { if (layerUpdateState[c.id] && layerUpdateState[c.id].inError()) { continue; } - nodeLayer = node.material.getLayer(c.id); + nodeLayer = node.material.getColorLayer(c.id); if (c.source.extentInsideLimit(node.extent, zoom) && (!nodeLayer || nodeLayer.level < 0)) { return false; } diff --git a/src/Process/LayeredMaterialNodeProcessing.js b/src/Process/LayeredMaterialNodeProcessing.js index e53b513111..304776a9a3 100644 --- a/src/Process/LayeredMaterialNodeProcessing.js +++ b/src/Process/LayeredMaterialNodeProcessing.js @@ -190,8 +190,8 @@ export function updateLayeredMaterialNodeElevation(context, layer, node, parent) // Possible conditions to *not* update the elevation texture if (layer.frozen || - !material.visible || - !node.layerUpdateState[layer.id].canTryUpdate()) { + !material.visible || + !node.layerUpdateState[layer.id].canTryUpdate()) { return; } @@ -238,7 +238,7 @@ export function removeLayeredMaterialNodeLayer(layerId) { */ return function removeLayeredMaterialNodeLayer(node) { if (node.material?.removeLayer) { - if (node.material.elevationLayerIds.indexOf(layerId) > -1) { + if (node.material.elevationTile !== undefined) { node.setBBoxZ({ min: 0, max: 0 }); } node.material.removeLayer(layerId); diff --git a/src/Renderer/LayeredMaterial.js b/src/Renderer/LayeredMaterial.js deleted file mode 100644 index bf1d50c2bc..0000000000 --- a/src/Renderer/LayeredMaterial.js +++ /dev/null @@ -1,275 +0,0 @@ -import * as THREE from 'three'; -import TileVS from 'Renderer/Shader/TileVS.glsl'; -import TileFS from 'Renderer/Shader/TileFS.glsl'; -import ShaderUtils from 'Renderer/Shader/ShaderUtils'; -import Capabilities from 'Core/System/Capabilities'; -import RenderMode from 'Renderer/RenderMode'; -import CommonMaterial from 'Renderer/CommonMaterial'; - -const identityOffsetScale = new THREE.Vector4(0.0, 0.0, 1.0, 1.0); -const defaultTex = new THREE.Texture(); - -// 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); -} - -// Max sampler color count to LayeredMaterial -// Because there's a statement limitation to unroll, in getColorAtIdUv method -const maxSamplersColorCount = 15; -const samplersElevationCount = 1; - -export function getMaxColorSamplerUnitsCount() { - const maxSamplerUnitsCount = Capabilities.getMaxTextureUnitsCount(); - return Math.min(maxSamplerUnitsCount - samplersElevationCount, maxSamplersColorCount); -} - -export const colorLayerEffects = { - noEffect: 0, - removeLightColor: 1, - removeWhiteColor: 2, - customEffect: 3, -}; - -const defaultStructLayer = { - bias: 0, - noDataValue: -99999, - zmin: 0, - zmax: 0, - scale: 0, - mode: 0, - textureOffset: 0, - opacity: 0, - crs: 0, - effect_parameter: 0, - effect_type: colorLayerEffects.noEffect, - transparent: false, -}; - -function updateLayersUniforms(uniforms, olayers, max) { - // prepare convenient access to elevation or color uniforms - const layers = uniforms.layers.value; - const textures = uniforms.textures.value; - const offsetScales = uniforms.offsetScales.value; - const textureCount = uniforms.textureCount; - - // flatten the 2d array [i,j] -> layers[_layerIds[i]].textures[j] - let count = 0; - for (const layer of olayers) { - // textureOffset property is added to RasterTile - layer.textureOffset = count; - for (let i = 0, il = layer.textures.length; i < il; ++i, ++count) { - if (count < max) { - offsetScales[count] = layer.offsetScales[i]; - textures[count] = layer.textures[i]; - layers[count] = layer; - } - } - } - if (count > max) { - console.warn(`LayeredMaterial: Not enough texture units (${max} < ${count}), excess textures have been discarded.`); - } - textureCount.value = count; - - // WebGL 2.0 doesn't support the undefined uniforms. - // So the undefined uniforms are defined by default value. - for (let i = count; i < textures.length; i++) { - textures[i] = defaultTex; - offsetScales[i] = identityOffsetScale; - layers[i] = defaultStructLayer; - } -} - -export const ELEVATION_MODES = { - RGBA: 0, - COLOR: 1, - DATA: 2, -}; - -let nbSamplers; -const fragmentShader = []; -class LayeredMaterial extends THREE.ShaderMaterial { - #_visible = true; - constructor(options = {}, crsCount) { - super(options); - - nbSamplers = nbSamplers || [samplersElevationCount, getMaxColorSamplerUnitsCount()]; - - this.defines.NUM_VS_TEXTURES = nbSamplers[0]; - this.defines.NUM_FS_TEXTURES = nbSamplers[1]; - // TODO: We do not use the fog from the scene, is this a desired - // behavior? - this.defines.USE_FOG = 1; - this.defines.NUM_CRS = crsCount; - - CommonMaterial.setDefineMapping(this, 'ELEVATION', ELEVATION_MODES); - CommonMaterial.setDefineMapping(this, 'MODE', RenderMode.MODES); - CommonMaterial.setDefineProperty(this, 'mode', 'MODE', RenderMode.MODES.FINAL); - - if (__DEBUG__) { - this.defines.DEBUG = 1; - const outlineColors = [new THREE.Vector3(1.0, 0.0, 0.0)]; - if (crsCount > 1) { - outlineColors.push(new THREE.Vector3(1.0, 0.5, 0.0)); - } - CommonMaterial.setUniformProperty(this, 'showOutline', true); - CommonMaterial.setUniformProperty(this, 'outlineWidth', 0.008); - CommonMaterial.setUniformProperty(this, 'outlineColors', outlineColors); - } - - this.vertexShader = TileVS; - // three loop unrolling of ShaderMaterial only supports integer bounds, - // see https://github.com/mrdoob/three.js/issues/28020 - fragmentShader[crsCount] = fragmentShader[crsCount] || ShaderUtils.unrollLoops(TileFS, this.defines); - this.fragmentShader = fragmentShader[crsCount]; - - // Color uniforms - CommonMaterial.setUniformProperty(this, 'diffuse', new THREE.Color(0.04, 0.23, 0.35)); - CommonMaterial.setUniformProperty(this, 'opacity', this.opacity); - - // Lighting uniforms - CommonMaterial.setUniformProperty(this, 'lightingEnabled', false); - CommonMaterial.setUniformProperty(this, 'lightPosition', new THREE.Vector3(-0.5, 0.0, 1.0)); - - // Misc properties - CommonMaterial.setUniformProperty(this, 'fogDistance', 1000000000.0); - CommonMaterial.setUniformProperty(this, 'fogColor', new THREE.Color(0.76, 0.85, 1.0)); - CommonMaterial.setUniformProperty(this, 'overlayAlpha', 0); - CommonMaterial.setUniformProperty(this, 'overlayColor', new THREE.Color(1.0, 0.3, 0.0)); - CommonMaterial.setUniformProperty(this, 'objectId', 0); - - CommonMaterial.setUniformProperty(this, '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 - CommonMaterial.setUniformProperty(this, 'minBorderDistance', -0.01); - - // LayeredMaterialLayers - this.layers = []; - this.elevationLayerIds = []; - this.colorLayerIds = []; - - // elevation layer uniforms, to be updated using updateUniforms() - this.uniforms.elevationLayers = new THREE.Uniform(new Array(nbSamplers[0]).fill(defaultStructLayer)); - this.uniforms.elevationTextures = new THREE.Uniform(new Array(nbSamplers[0]).fill(defaultTex)); - this.uniforms.elevationOffsetScales = new THREE.Uniform(new Array(nbSamplers[0]).fill(identityOffsetScale)); - this.uniforms.elevationTextureCount = new THREE.Uniform(0); - - // color layer uniforms, to be updated using updateUniforms() - this.uniforms.colorLayers = new THREE.Uniform(new Array(nbSamplers[1]).fill(defaultStructLayer)); - this.uniforms.colorTextures = new THREE.Uniform(new Array(nbSamplers[1]).fill(defaultTex)); - this.uniforms.colorOffsetScales = new THREE.Uniform(new Array(nbSamplers[1]).fill(identityOffsetScale)); - this.uniforms.colorTextureCount = new THREE.Uniform(0); - - // can't do an ES6 setter/getter here - 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' }); - } - }, - }); - } - - getUniformByType(type) { - return { - layers: this.uniforms[`${type}Layers`], - textures: this.uniforms[`${type}Textures`], - offsetScales: this.uniforms[`${type}OffsetScales`], - textureCount: this.uniforms[`${type}TextureCount`], - }; - } - - updateLayersUniforms() { - const colorlayers = this.layers.filter(l => this.colorLayerIds.includes(l.id) && l.visible && l.opacity > 0); - colorlayers.sort((a, b) => this.colorLayerIds.indexOf(a.id) - this.colorLayerIds.indexOf(b.id)); - updateLayersUniforms(this.getUniformByType('color'), colorlayers, this.defines.NUM_FS_TEXTURES); - - if (this.elevationLayerIds.some(id => this.getLayer(id)) || - (this.uniforms.elevationTextureCount.value && !this.elevationLayerIds.length)) { - const elevationLayer = this.getElevationLayer() ? [this.getElevationLayer()] : []; - updateLayersUniforms(this.getUniformByType('elevation'), elevationLayer, this.defines.NUM_VS_TEXTURES); - } - this.layersNeedUpdate = false; - } - - dispose() { - this.dispatchEvent({ type: 'dispose' }); - this.layers.forEach(l => l.dispose(true)); - this.layers.length = 0; - this.layersNeedUpdate = true; - } - - // TODO: rename to setColorLayerIds and add setElevationLayerIds ? - setSequence(sequenceLayer) { - this.colorLayerIds = sequenceLayer; - this.layersNeedUpdate = true; - } - - setSequenceElevation(layerId) { - this.elevationLayerIds[0] = layerId; - this.layersNeedUpdate = true; - } - - removeLayer(layerId) { - const index = this.layers.findIndex(l => l.id === layerId); - if (index > -1) { - this.layers[index].dispose(); - this.layers.splice(index, 1); - const idSeq = this.colorLayerIds.indexOf(layerId); - if (idSeq > -1) { - this.colorLayerIds.splice(idSeq, 1); - } else { - this.elevationLayerIds = []; - } - } - } - - addLayer(rasterNode) { - if (rasterNode.layer.id in this.layers) { - console.warn('The "{layer.id}" layer was already present in the material, overwritting.'); - } - this.layers.push(rasterNode); - } - - getLayer(id) { - return this.layers.find(l => l.id === id); - } - - getLayers(ids) { - return this.layers.filter(l => ids.includes(l.id)); - } - - getElevationLayer() { - return this.layers.find(l => l.id === this.elevationLayerIds[0]); - } - - setElevationScale(scale) { - if (this.elevationLayerIds.length) { - this.getElevationLayer().scale = scale; - } - } -} - -export default LayeredMaterial; diff --git a/src/Renderer/LayeredMaterial.ts b/src/Renderer/LayeredMaterial.ts new file mode 100644 index 0000000000..1fcbb25df0 --- /dev/null +++ b/src/Renderer/LayeredMaterial.ts @@ -0,0 +1,536 @@ +import * as THREE from 'three'; +// @ts-expect-error: importing non-ts file +import TileVS from 'Renderer/Shader/TileVS.glsl'; +// @ts-expect-error: importing non-ts file +import TileFS from 'Renderer/Shader/TileFS.glsl'; +import ShaderUtils from 'Renderer/Shader/ShaderUtils'; +import Capabilities from 'Core/System/Capabilities'; +import RenderMode from 'Renderer/RenderMode'; +import { RasterTile, RasterElevationTile, RasterColorTile } from './RasterTile'; + +const identityOffsetScale = new THREE.Vector4(0.0, 0.0, 1.0, 1.0); +const defaultTex = new THREE.Texture(); + +// 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: THREE.Vector4Like, factor: number): number { + return factor ? bitSh.dot(color) * factor : bitSh.dot(color); +} + +// Max sampler color count to LayeredMaterial +// Because there's a statement limitation to unroll, in getColorAtIdUv method +const maxSamplersColorCount = 15; +const samplersElevationCount = 1; + +export function getMaxColorSamplerUnitsCount(): number { + const maxSamplerUnitsCount = Capabilities.getMaxTextureUnitsCount(); + return Math.min( + maxSamplerUnitsCount - samplersElevationCount, + maxSamplersColorCount, + ); +} + +export const colorLayerEffects: Record = { + noEffect: 0, + removeLightColor: 1, + removeWhiteColor: 2, + customEffect: 3, +}; + +/** GPU struct for color layers */ +interface StructColorLayer { + textureOffset: number; + crs: number; + opacity: number; + effect_parameter: number; + effect_type: number; + transparent: boolean; +} + +/** GPU struct for elevation layers */ +interface StructElevationLayer { + scale: number; + bias: number; + mode: number; + zmin: number; + zmax: number; +} + +/** Default GPU struct values for initialization QoL */ +const defaultStructLayers: Readonly<{ + color: StructColorLayer, + elevation: StructElevationLayer +}> = { + 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, + }, +}; + + +function updateLayersUniforms( + uniforms: { [name: string]: THREE.IUniform }, + tiles: RasterTile[], + max: number, +) { + // Aliases for readability + const uLayers = uniforms.layers.value; + const uTextures = uniforms.textures.value; + const uOffsetScales = uniforms.offsetScales.value; + const uTextureCount = uniforms.textureCount; + + // Flatten the 2d array: [i, j] -> layers[_layerIds[i]].textures[j] + let count = 0; + 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 + ) { + uOffsetScales[count] = tile.offsetScales[i]; + uTextures[count] = tile.textures[i]; + uLayers[count] = tile; + } + } + 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. + */ +type MappedUniforms = { + [name in keyof Uniforms]: THREE.IUniform; +}; + +/** List of the uniform types required for a LayeredMaterial. */ +interface LayeredMaterialRawUniforms { + // Color + diffuse: THREE.Color; + opacity: number; + + // Lighting + lightingEnabled: boolean; + lightPosition: THREE.Vector3; + + // Misc + fogDistance: number; + fogColor: THREE.Color; + overlayAlpha: number; + overlayColor: THREE.Color; + objectId: number; + geoidHeight: number; + + // > 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: number; + + // Debug + showOutline: boolean, + outlineWidth: number, + outlineColors: THREE.Color[] + + // Elevation layers + elevationLayers: Array, + elevationTextures: Array, + elevationOffsetScales: Array, + elevationTextureCount: number, + + // Color layers + colorLayers: Array, + colorTextures: Array, + colorOffsetScales: Array, + colorTextureCount: number, +} + +let nbSamplers: [number, number] | undefined; +const fragmentShader: string[] = []; + +/** Replacing the default uniforms dynamic type with our own static map. */ +export type LayeredMaterialParameters = + Omit + & { uniforms?: MappedUniforms }; + +type DefineMapping> = { + [Name in Extract as `${Prefix}_${Name}`]: Mapping[Name] +}; + +/** Fills in a Partial object's field and narrows the type accordingly. */ +function fillInProp< + Obj extends Partial>, + Name extends keyof Obj, + Value extends Obj[Name], +>( + obj: Obj, + name: Name, + value: Value, +): asserts obj is Obj & { [P in Name]: Value } { + if (obj[name] === undefined) { + (obj as Record)[name] = value; + } +} + +type ElevationModeDefines = DefineMapping<'ELEVATION', typeof ELEVATION_MODES>; +type RenderModeDefines = DefineMapping<'MODE', typeof RenderMode.MODES>; +type LayeredMaterialDefines = { + NUM_VS_TEXTURES: number; + NUM_FS_TEXTURES: number; + USE_FOG: number; + NUM_CRS: number; + DEBUG: number; + MODE: number; +} & ElevationModeDefines + & RenderModeDefines; + +/** + * Initialiszes elevation and render mode defines and narrows the type + * accordingly. + */ +function initModeDefines( + defines: Partial, +): asserts defines is Partial & ElevationModeDefines & RenderModeDefines { + (Object.keys(ELEVATION_MODES) as (keyof typeof ELEVATION_MODES)[]) + .forEach(key => fillInProp(defines, `ELEVATION_${key}`, ELEVATION_MODES[key])); + (Object.keys(RenderMode.MODES) as (keyof typeof 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 { + private _visible = true; + + public colorTiles: RasterColorTile[]; + public elevationTile: RasterElevationTile | undefined; + + public colorTileIds: string[]; + public elevationTileId: string | undefined; + + public layersNeedUpdate: boolean; + + public override defines: LayeredMaterialDefines; + + constructor(options: LayeredMaterialParameters = {}, crsCount: number) { + super(options); + + nbSamplers ??= [samplersElevationCount, getMaxColorSamplerUnitsCount()]; + + const defines: Partial = {}; + + fillInProp(defines, 'NUM_VS_TEXTURES', nbSamplers[0]); + fillInProp(defines, 'NUM_FS_TEXTURES', nbSamplers[1]); + // TODO: We do not use the fog from the scene, is this a desired + // behavior? + fillInProp(defines, 'USE_FOG', 1); + fillInProp(defines, 'NUM_CRS', crsCount); + + initModeDefines(defines); + fillInProp(defines, 'MODE', RenderMode.MODES.FINAL); + + // @ts-expect-error: global constexpr + fillInProp(defines, 'DEBUG', +__DEBUG__); + + // @ts-expect-error: global constexpr + if (__DEBUG__) { + const outlineColors = [new THREE.Color(1.0, 0.0, 0.0)]; + if (crsCount > 1) { + outlineColors.push(new THREE.Color(1.0, 0.5, 0.0)); + } + + this.initUniforms({ + showOutline: true, + outlineWidth: 0.008, + outlineColors, + }); + } + + this.defines = defines; + + 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 + fogDistance: 1000000000.0, + fogColor: new THREE.Color(0.76, 0.85, 1.0), + 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: new Array(nbSamplers[0]).fill(defaultTex), + elevationOffsetScales: new Array(nbSamplers[0]) + .fill(identityOffsetScale), + elevationTextureCount: 0, + + colorLayers: new Array(nbSamplers[1]) + .fill(defaultStructLayers.color), + colorTextures: new Array(nbSamplers[1]).fill(defaultTex), + 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' }); + } + }, + }); + } + + public get mode(): number { + return this.defines.MODE; + } + + public set mode(mode: number) { + if (this.defines.MODE != mode) { + this.defines.MODE = mode; + this.needsUpdate = true; + } + } + + public getUniform( + name: Name, + ): LayeredMaterialRawUniforms[Name] | undefined { + return this.uniforms[name]?.value; + } + + public setUniform< + Name extends keyof LayeredMaterialRawUniforms, + Value extends LayeredMaterialRawUniforms[Name], + >(name: Name, value: Value): void { + const uniform = this.uniforms[name]; + if (uniform === undefined) { + return; + } + if (uniform.value !== value) { + uniform.value = value; + } + } + + public initUniforms(uniforms: { + [Name in keyof LayeredMaterialRawUniforms + ]?: LayeredMaterialRawUniforms[Name] + }): void { + for (const [name, value] of Object.entries(uniforms)) { + if (this.uniforms[name] === undefined) { + this.uniforms[name] = { value }; + } + } + } + + public setUniforms(uniforms: { + [Name in keyof LayeredMaterialRawUniforms + ]?: LayeredMaterialRawUniforms[Name] + }): void { + for (const [name, value] of Object.entries(uniforms)) { + const uniform = this.uniforms[name]; + if (uniform === undefined) { + continue; + } + if (uniform.value !== value) { + uniform.value = value; + } + } + } + + public getLayerUniforms(type: Type): + MappedUniforms<{ + layers: Array, + textures: Array, + offsetScales: Array, + textureCount: number, + }> { + return { + layers: this.uniforms[`${type}Layers`], + textures: this.uniforms[`${type}Textures`], + offsetScales: this.uniforms[`${type}OffsetScales`], + textureCount: this.uniforms[`${type}TextureCount`], + }; + } + + public updateLayersUniforms(): void { + 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, + ); + + if ((this.elevationTileId !== undefined + && this.getColorLayer(this.elevationTileId)) + || (this.uniforms.elevationTextureCount.value + && this.elevationTileId !== undefined) + ) { + if (this.elevationTile !== undefined) { + updateLayersUniforms( + this.getLayerUniforms('elevation'), + [this.elevationTile], + this.defines.NUM_VS_TEXTURES, + ); + } + } + + this.layersNeedUpdate = false; + } + + public dispose(): void { + this.dispatchEvent({ type: 'dispose' }); + + this.colorTiles.forEach(l => l.dispose(true)); + this.colorTiles.length = 0; + + this.elevationTile?.dispose(true); + + this.layersNeedUpdate = true; + } + + public setColorLayerIds(ids: string[]): void { + this.colorTileIds = ids; + this.layersNeedUpdate = true; + } + + public setElevationLayerId(id: string): void { + this.elevationTileId = id; + this.layersNeedUpdate = true; + } + + public removeLayer(layerId: string): void { + const index = this.colorTiles.findIndex(l => l.id === layerId); + if (index > -1) { + this.colorTiles[index].dispose(); + this.colorTiles.splice(index, 1); + const idSeq = this.colorTileIds.indexOf(layerId); + if (idSeq > -1) { + this.colorTileIds.splice(idSeq, 1); + } else { + this.elevationTileId = undefined; + } + } + } + + public addColorLayer(rasterNode: RasterColorTile) { + if (rasterNode.layer.id in this.colorTiles) { + console.warn( + 'Layer "{layer.id}" already present in material, overwriting.', + ); + } + this.colorTiles.push(rasterNode); + } + + public setElevationLayer(rasterNode: RasterElevationTile) { + const old = this.elevationTile; + if (old !== undefined) { + old.dispose(); + } + + this.elevationTile = rasterNode; + } + + public getColorLayer(id: string): RasterColorTile | undefined { + return this.colorTiles.find(l => l.id === id); + } + + public getColorLayers(ids: string[]): RasterColorTile[] { + return this.colorTiles.filter(l => ids.includes(l.id)); + } + + public getElevationLayer(): RasterElevationTile | undefined { + return this.elevationTile; + } + + public getLayer(id: string): RasterTile | undefined { + return this.elevationTile?.id === id + ? this.elevationTile : this.colorTiles.find(l => l.id === id); + } +} diff --git a/src/Renderer/RasterTile.js b/src/Renderer/RasterTile.js index af87699426..8ae9c2f58a 100644 --- a/src/Renderer/RasterTile.js +++ b/src/Renderer/RasterTile.js @@ -27,7 +27,7 @@ function getIndiceWithPitch(i, pitch, w) { * * @class RasterTile */ -class RasterTile extends THREE.EventDispatcher { +export class RasterTile extends THREE.EventDispatcher { constructor(material, layer) { super(); this.layer = layer; @@ -112,8 +112,6 @@ class RasterTile extends THREE.EventDispatcher { } } -export default RasterTile; - export class RasterColorTile extends RasterTile { get effect_type() { return this.layer.effect_type; diff --git a/test/unit/dataSourceProvider.js b/test/unit/dataSourceProvider.js index 6bbbbb52fa..232689fd49 100644 --- a/test/unit/dataSourceProvider.js +++ b/test/unit/dataSourceProvider.js @@ -19,7 +19,7 @@ import GeometryLayer from 'Layer/GeometryLayer'; import PlanarLayer from 'Core/Prefab/Planar/PlanarLayer'; import Style from 'Core/Style'; import Feature2Mesh from 'Converter/Feature2Mesh'; -import LayeredMaterial from 'Renderer/LayeredMaterial'; +import { LayeredMaterial } from 'Renderer/LayeredMaterial'; import { EMPTY_TEXTURE_ZOOM } from 'Renderer/RasterTile'; import sinon from 'sinon'; @@ -75,7 +75,7 @@ describe('Provide in Sources', function () { planarlayer.attach(colorlayer); planarlayer.attach(elevationlayer); - const fakeNode = { material, setBBoxZ: () => {}, addEventListener: () => {} }; + const fakeNode = { material, setBBoxZ: () => { }, addEventListener: () => { } }; colorlayer.setupRasterNode(fakeNode); elevationlayer.setupRasterNode(fakeNode); @@ -145,7 +145,7 @@ describe('Provide in Sources', function () { const tile = new TileMesh(geom, material, planarlayer, extent); material.visible = true; nodeLayer.level = EMPTY_TEXTURE_ZOOM; - tile.parent = { }; + tile.parent = {}; updateLayeredMaterialNodeImagery(context, colorlayer, tile, tile.parent); updateLayeredMaterialNodeImagery(context, colorlayer, tile, tile.parent); @@ -204,7 +204,7 @@ describe('Provide in Sources', function () { const tile = new TileMesh(geom, material, planarlayer, extent, zoom); material.visible = true; nodeLayer.level = EMPTY_TEXTURE_ZOOM; - tile.parent = { }; + tile.parent = {}; updateLayeredMaterialNodeImagery(context, colorlayer, tile, tile.parent); updateLayeredMaterialNodeImagery(context, colorlayer, tile, tile.parent); @@ -220,7 +220,7 @@ describe('Provide in Sources', function () { const tile = new TileMesh(geom, material, planarlayer, extent, zoom); material.visible = true; nodeLayer.level = EMPTY_TEXTURE_ZOOM; - tile.parent = { }; + tile.parent = {}; planarlayer.subdivideNode(context, tile); TileProvider.executeCommand(context.scheduler.commands[0]) @@ -290,7 +290,8 @@ describe('Provide in Sources', function () { nodeLayer.level = EMPTY_TEXTURE_ZOOM; tile.material.visible = true; featureLayer.source.uid = 22; - const colorlayerWfs = new ColorLayer('color', { crs: 'EPSG:3857', + const colorlayerWfs = new ColorLayer('color', { + crs: 'EPSG:3857', source: featureLayer.source, style: { fill: { @@ -335,13 +336,13 @@ describe('Provide in Sources', function () { const tile = new TileMesh(geom, new LayeredMaterial(), planarlayer, extent); tile.material.visible = true; nodeLayer.level = EMPTY_TEXTURE_ZOOM; - tile.parent = { }; + tile.parent = {}; updateLayeredMaterialNodeImagery(context, colorlayer, tile, tile.parent); updateLayeredMaterialNodeImagery(context, colorlayer, tile, tile.parent); DataSourceProvider.executeCommand(context.scheduler.commands[0]) .then((result) => { - tile.material.setSequence([colorlayer.id]); + tile.material.setColorLayerIds([colorlayer.id]); tile.material.getLayer(colorlayer.id).setTextures(result, [new THREE.Vector4()]); assert.equal(tile.material.uniforms.colorTextures.value[0].anisotropy, 1); tile.material.updateLayersUniforms(); diff --git a/test/unit/layeredmaterial.js b/test/unit/layeredmaterial.js index 89f171abf0..5d82c2845d 100644 --- a/test/unit/layeredmaterial.js +++ b/test/unit/layeredmaterial.js @@ -8,7 +8,7 @@ import TileMesh from 'Core/TileMesh'; import * as THREE from 'three'; import Tile from 'Core/Tile/Tile'; import OBB from 'Renderer/OBB'; -import LayeredMaterial from 'Renderer/LayeredMaterial'; +import { LayeredMaterial } from 'Renderer/LayeredMaterial'; import sinon from 'sinon'; import Fetcher from 'Provider/Fetcher'; import Renderer from './bootstrap'; @@ -40,7 +40,7 @@ describe('material state vs layer state', function () { const geom = new THREE.BufferGeometry(); geom.OBB = new OBB(new THREE.Vector3(), new THREE.Vector3(1, 1, 1)); node = new TileMesh(geom, material, view.tileLayer, extent); - node.parent = { }; + node.parent = {}; context = { view, scheduler: view.mainLoop.scheduler }; }); diff --git a/test/unit/tilemesh.js b/test/unit/tilemesh.js index aea00caef4..75223d8db8 100644 --- a/test/unit/tilemesh.js +++ b/test/unit/tilemesh.js @@ -177,6 +177,8 @@ describe('TileMesh', function () { const material = new THREE.Material(); material.addLayer = () => { }; material.setSequenceElevation = () => { }; + material.setElevationLayer = () => { }; + material.setElevationLayerId = () => { }; it('event rasterElevationLevelChanged RasterElevationTile sets TileMesh bounding box ', () => { const tileMesh = new TileMesh(geom, material, planarlayer, tile.toExtent('EPSG:3857'), 0);