UNPKG

@giro3d/giro3d

Version:

A JS/WebGL framework for 3D geospatial data visualization

256 lines (218 loc) 7.99 kB
/* * Copyright (c) 2015-2018, IGN France. * Copyright (c) 2018-2026, Giro3D team. * SPDX-License-Identifier: MIT */ import { CanvasTexture, FloatType, GLSL3, ShaderMaterial, Uniform, type AnyPixelFormat, type IUniform, type Texture, type TextureDataType, } from 'three'; import Interpretation, { Mode, type InterpretationUniform } from '../../core/layer/Interpretation'; import TextureGenerator from '../../utils/TextureGenerator'; import FragmentShader from './ComposerTileFS.glsl'; import VertexShader from './ComposerTileVS.glsl'; // Matches the NoDataOptions struct in the shader interface NoDataOptions { replacementAlpha: number; radius: number; enabled: boolean; } export interface Options { texture: Texture; interpretation: Interpretation; flipY: boolean; noDataOptions: NoDataOptions; showImageOutlines: boolean; showEmptyTexture: boolean; transparent: boolean; expandRGB: boolean; convertRGFloatToRGBAUnsignedByte: { precision: number; offset: number } | null; } function createGridTexture(): CanvasTexture { const canvas = document.createElement('canvas'); canvas.width = 512; canvas.height = 512; const w = canvas.width; const h = canvas.height; const ctx = canvas.getContext('2d', { willReadFrequently: true }); if (!ctx) { throw new Error('could not acquire 2D rendering context'); } const back = 'black'; const fore = 'yellow'; const borderWidth = 4; const lineWidth = 3; ctx.strokeStyle = back; ctx.lineWidth = lineWidth + 2 * borderWidth; ctx.strokeRect(0, 0, w, h); ctx.strokeStyle = fore; ctx.lineWidth = lineWidth; ctx.strokeRect(0, 0, w, h); ctx.strokeStyle = fore; ctx.setLineDash([8, 8]); ctx.lineWidth = 2; const subdivs = 2; const xWidth = w / subdivs; for (let i = 1; i < subdivs; i++) { const x = i * xWidth; ctx.moveTo(x, 0); ctx.lineTo(x, h); ctx.stroke(); } const yWidth = h / subdivs; for (let i = 1; i < subdivs; i++) { const y = i * yWidth; ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); } // Center of the image const radius = 4; const centerX = w / 2; const centerY = h / 2; ctx.fillStyle = back; ctx.beginPath(); ctx.ellipse(centerX, centerY, radius + borderWidth, radius + borderWidth, 0, 0, 2 * Math.PI); ctx.fill(); ctx.fillStyle = fore; ctx.beginPath(); ctx.ellipse(centerX, centerY, radius, radius, 0, 0, 2 * Math.PI); ctx.fill(); return new CanvasTexture(canvas); } const POOL: unknown[] = []; const POOL_SIZE = 2048; let GRID_TEXTURE: Texture; type Uniforms = { tex: IUniform<Texture | null>; gridTexture: IUniform<Texture | null>; flipY: IUniform<boolean>; showImageOutlines: IUniform<boolean>; expandRGB: IUniform<boolean>; opacity: IUniform<number>; channelCount: IUniform<number>; showEmptyTexture: IUniform<boolean>; isEmptyTexture: IUniform<boolean>; noDataOptions: IUniform<NoDataOptions>; interpretation: IUniform<InterpretationUniform>; convertRGFloatToRGBAUnsignedByte: IUniform<boolean>; heightPrecision: IUniform<number>; heightOffset: IUniform<number>; } & Record<string, IUniform>; class ComposerTileMaterial extends ShaderMaterial { public readonly isComposerTileMaterial = true as const; public dataType?: TextureDataType; public pixelFormat?: AnyPixelFormat; public override readonly uniforms: Uniforms; /** * Creates an instance of ComposerTileMaterial. * * @param options - The options */ public constructor(options: Options) { super({ glslVersion: GLSL3 }); this.fragmentShader = FragmentShader; this.vertexShader = VertexShader; this.depthTest = false; this.uniforms = { tex: new Uniform(null), gridTexture: new Uniform(null), interpretation: new Uniform({ max: 1, min: 0, mode: 0, negateValues: false }), flipY: new Uniform(false), noDataOptions: new Uniform({ enabled: false, radius: 0, replacementAlpha: 0 }), showImageOutlines: new Uniform(false), opacity: new Uniform(this.opacity), channelCount: new Uniform(3), expandRGB: new Uniform(options.expandRGB ?? false), showEmptyTexture: new Uniform(options.showEmptyTexture ?? false), isEmptyTexture: new Uniform(false), convertRGFloatToRGBAUnsignedByte: new Uniform( options.convertRGFloatToRGBAUnsignedByte != null, ), heightPrecision: new Uniform( options.convertRGFloatToRGBAUnsignedByte?.precision ?? 0.1, ), heightOffset: new Uniform(options.convertRGFloatToRGBAUnsignedByte?.offset ?? 20000), }; if (options != null) { this.init(options); } } private init(options: Options): void { const interp = options.interpretation ?? Interpretation.Raw; this.dataType = interp.mode !== Mode.Raw ? FloatType : options.texture.type; this.pixelFormat = options.texture.format; const interpValue = {} as InterpretationUniform; interp.setUniform(interpValue); // The no-data filling algorithm does not like transparent images this.needsUpdate = this.transparent !== options.transparent; this.transparent = options.transparent ?? false; this.opacity = 1; this.uniforms.opacity.value = this.opacity; this.uniforms.interpretation.value = interpValue; this.uniforms.tex.value = options.texture; this.uniforms.flipY.value = options.flipY ?? false; this.uniforms.noDataOptions.value = options.noDataOptions ?? { enabled: false, radius: 0, replacementAlpha: 0, }; this.uniforms.showImageOutlines.value = options.showImageOutlines ?? false; this.uniforms.expandRGB.value = options.expandRGB ?? false; this.uniforms.showEmptyTexture.value = options.showEmptyTexture ?? false; this.uniforms.isEmptyTexture.value = TextureGenerator.isEmptyTexture(options.texture); this.uniforms.convertRGFloatToRGBAUnsignedByte.value = options.convertRGFloatToRGBAUnsignedByte != null; this.uniforms.heightPrecision.value = options.convertRGFloatToRGBAUnsignedByte?.precision ?? 0.1; this.uniforms.heightOffset.value = options.convertRGFloatToRGBAUnsignedByte?.offset ?? 0.1; const channelCount = TextureGenerator.getChannelCount(this.pixelFormat); this.uniforms.channelCount.value = channelCount; if (options.showImageOutlines) { if (GRID_TEXTURE == null) { GRID_TEXTURE = createGridTexture(); } this.uniforms.gridTexture.value = GRID_TEXTURE; } } private reset(): void { this.uniforms.tex.value = null; } /** * Acquires a pooled material. * * @param opts - The options. */ public static acquire(opts: Options): ComposerTileMaterial { if (POOL.length > 0) { const mat = POOL.pop() as ComposerTileMaterial; mat.init(opts); return mat; } return new ComposerTileMaterial(opts); } /** * Releases the material back into the pool. * * @param material - The material. */ public static release(material: ComposerTileMaterial): void { material.reset(); if (POOL.length < POOL_SIZE) { POOL.push(material); } else { material.dispose(); } } } export function isComposerTileMaterial(obj: unknown): obj is ComposerTileMaterial { return (obj as ComposerTileMaterial)?.isComposerTileMaterial; } export default ComposerTileMaterial;