UNPKG

@giro3d/giro3d

Version:

A JS/WebGL framework for 3D geospatial data visualization

454 lines (427 loc) 21.4 kB
import { Color, GLSL3, Matrix4, NoBlending, NormalBlending, ShaderMaterial, Uniform, Vector2, Vector3, Vector4 } from 'three'; import ColorMap from '../core/ColorMap'; import OffsetScale from '../core/OffsetScale'; import MaterialUtils from './MaterialUtils'; /* babel-plugin-inline-import './shader/PointsFS.glsl' */ const PointsFS = "#include <giro3d_precision_qualifiers>\n#include <giro3d_fragment_shader_header>\n#include <giro3d_common>\n\n#include <common>\n#include <logdepthbuf_pars_fragment>\n#include <clipping_planes_pars_fragment>\n#include <fog_pars_fragment>\n\nvarying vec4 vColor;\nuniform vec3 brightnessContrastSaturation;\n\nconst float HALF_LENGTH = 0.5;\nconst vec2 POINT_CENTER = vec2(HALF_LENGTH, HALF_LENGTH);\nconst float HALF_LENGTH_SQUARED = HALF_LENGTH * HALF_LENGTH;\n\nfloat sqLength(in vec2 v) {\n return v.x * v.x + v.y * v.y;\n}\n\nvoid main() {\n if (vColor.a < 0.001) {\n discard;\n return;\n }\n\n // circular point rendering\n if (sqLength(gl_PointCoord - POINT_CENTER) > HALF_LENGTH_SQUARED){\n discard;\n return;\n }\n\n #include <clipping_planes_fragment>\n\n gl_FragColor = vec4(adjustBrightnessContrastSaturation(vColor.rgb, brightnessContrastSaturation), vColor.a);\n\n #include <colorspace_fragment>\n #include <fog_fragment>\n #include <logdepthbuf_fragment>\n}\n"; /* babel-plugin-inline-import './shader/PointsVS.glsl' */ const PointsVS = "#include <giro3d_precision_qualifiers>\n#include <giro3d_common>\n\n#include <common>\n#include <logdepthbuf_pars_vertex>\n#include <clipping_planes_pars_vertex>\n#include <fog_pars_vertex>\n\n#define EPSILON 1e-6\n\nuniform float size;\n\nuniform uint pickingId;\nuniform int mode;\nuniform float opacity;\nuniform vec4 overlayColor;\nattribute vec3 color;\n\nstruct PointCloudColorMap {\n float min;\n float max;\n sampler2D lut;\n};\n\nuniform PointCloudColorMap colorMap;\n\n#if defined(INTENSITY)\n// INTENSITY_TYPE is a define macro\nattribute INTENSITY_TYPE intensity;\n#endif\n\n#if defined(CLASSIFICATION)\nstruct Classification {\n vec3 color;\n bool visible;\n};\n\nuniform Classification[256] classifications;\nattribute uint classification;\n#endif\n\n#if defined(NORMAL_OCT16)\nattribute vec2 oct16Normal;\n#elif defined(NORMAL_SPHEREMAPPED)\nattribute vec2 sphereMappedNormal;\n#endif\n\nuniform sampler2D overlayTexture;\nuniform int decimation;\nuniform float hasOverlayTexture;\nuniform vec4 offsetScale;\nuniform vec2 extentBottomLeft;\nuniform vec2 extentSize;\n\nvarying vec4 vColor;\n\n// see https://web.archive.org/web/20150303053317/http://lgdv.cs.fau.de/get/1602\n// and implementation in PotreeConverter (BINPointReader.cpp) and potree (BinaryDecoderWorker.js)\n#if defined(NORMAL_OCT16)\nvec3 decodeOct16Normal(vec2 encodedNormal) {\n vec2 nNorm = 2. * (encodedNormal / 255.) - 1.;\n vec3 n;\n n.z = 1. - abs(nNorm.x) - abs(nNorm.y);\n if (n.z >= 0.) {\n n.x = nNorm.x;\n n.y = nNorm.y;\n } else {\n n.x = sign(nNorm.x) - sign(nNorm.x) * sign(nNorm.y) * nNorm.y;\n n.y = sign(nNorm.y) - sign(nNorm.y) * sign(nNorm.x) * nNorm.x;\n }\n return normalize(n);\n}\n#elif defined(NORMAL_SPHEREMAPPED)\n// see http://aras-p.info/texts/CompactNormalStorage.html method #4\n// or see potree's implementation in BINPointReader.cpp\nvec3 decodeSphereMappedNormal(vec2 encodedNormal) {\n vec2 fenc = 2. * encodedNormal / 255. - 1.;\n float f = dot(fenc,fenc);\n float g = 2. * sqrt(1. - f);\n vec3 n;\n n.xy = fenc * g;\n n.z = 1. - 2. * f;\n return n;\n}\n#endif\n\n#ifdef DEFORMATION_SUPPORT\nuniform int enableDeformations;\nstruct Deformation {\n mat4 transformation;\n vec3 vec;\n vec2 origin;\n vec2 influence;\n vec4 colors;\n};\n\nuniform Deformation deformations[NUM_TRANSFO];\n#endif\n\nvoid main() {\n if (decimation > 1 && gl_VertexID % decimation != 0) {\n // Move the vertex out of the render area to prevent calling the fragment shader for\n // this point, saving a lot of GPU processing time when millions of points are displayed.\n gl_PointSize = 0.0;\n gl_Position = vec4(-9999.0, -9999.0, -9999.0, 0.0);\n return;\n }\n\n#if defined(NORMAL_OCT16)\n vec3 normal = decodeOct16Normal(oct16Normal);\n#elif defined(NORMAL_SPHEREMAPPED)\n vec3 normal = decodeSphereMappedNormal(sphereMappedNormal);\n#elif defined(NORMAL)\n // nothing to do\n#else\n // default to color\n vec3 normal = color;\n#endif\n\n if (pickingId > uint(0)) {\n // In picking mode, we simply output the point id in the red channel and the object id in the green channel.\n // No need to encode them because we are rendering to a float texture.\n vColor = vec4(float(gl_VertexID), float(pickingId), 0, 1);\n#if defined(INTENSITY)\n } else if (mode == MODE_INTENSITY) {\n vColor = sampleColorMap(float(intensity), colorMap.min, colorMap.max, colorMap.lut, 0.0);\n vColor.a *= opacity;\n#endif\n } else if (mode == MODE_NORMAL) {\n vColor = vec4(abs(normal), opacity);\n } else if (mode == MODE_TEXTURE) {\n vec2 pp = (modelMatrix * vec4(position, 1.0)).xy;\n // offsetScale is from bottomleft\n pp.x -= extentBottomLeft.x;\n pp.y -= extentBottomLeft.y;\n pp *= offsetScale.zw / extentSize;\n pp += offsetScale.xy;\n vec3 textureColor = texture2D(overlayTexture, pp).rgb;\n vColor = vec4(mix(textureColor, overlayColor.rgb, overlayColor.a), opacity * hasOverlayTexture);\n } else if (mode == MODE_ELEVATION) {\n float z = (modelMatrix * vec4(position, 1.0)).z;\n vColor = sampleColorMap(z, colorMap.min, colorMap.max, colorMap.lut, 0.0);\n vColor.a *= opacity;\n#if defined(CLASSIFICATION)\n } else if (mode == MODE_CLASSIFICATION) {\n Classification classif = classifications[classification];\n vColor.rgb = classif.color;\n vColor.a = classif.visible ? opacity : 0.0;\n#endif\n } else {\n // default to color mode\n\n // We need to convert to linear color space because the colors are in sRGB and they\n // are not automatically converted to sRGB-linear. This is due to the fact that those\n // colors come from a vertex buffer and not from a texture (automatically converted)\n // or a single color uniform (also automatically converted).\n vec4 linear = sRGBToLinear(vec4(color, 1.0));\n vColor = vec4(mix(linear.rgb, overlayColor.rgb, overlayColor.a), opacity);\n }\n\n mat4 mvMatrix = modelViewMatrix;\n\n #ifdef DEFORMATION_SUPPORT\n if (!pickingMode) {\n vColor = enableDeformations > 0 ?\n vec4(0.0, 1.0, 1.0, 1.0):\n vec4(1.0, 0.0, 1.0, 1.0);\n }\n if (enableDeformations > 0) {\n vec4 mPosition = modelMatrix * vec4(position, 1.0);\n float minDistance = 1000.0;\n int bestChoice = -1;\n for (int i = 0; i < NUM_TRANSFO; i++) {\n if (i >= enableDeformations) {\n break;\n }\n vec2 v = deformations[i].vec.xy;\n float length = deformations[i].vec.z;\n float depassement_x =\n length * (deformations[i].influence.x - 1.0);\n\n vec2 diff = mPosition.xy - origin[i];\n float distance_x = dot(diff, v);\n\n if (-depassement_x <= distance_x &&\n distance_x <= (length + depassement_x)) {\n vec2 normal = vec2(-v.y, v.x);\n float d = abs(dot(diff, normal));\n if (d < minDistance && d <= deformations[i].influence.y) {\n minDistance = d;\n bestChoice = i;\n }\n }\n }\n\n if (bestChoice >= 0) {\n // override modelViewMatrix\n mvMatrix = deformations[bestChoice].transformation;\n vColor = mix(\n deformations[bestChoice].color,\n vec4(color, 1.0),\n 0.5);\n }\n }\n #endif\n\n #include <begin_vertex>\n #include <project_vertex>\n\n if (size > 0.) {\n gl_PointSize = size;\n } else {\n gl_PointSize = clamp(-size / gl_Position.w, 3.0, 10.0);\n }\n\n #include <fog_vertex>\n #include <logdepthbuf_vertex>\n #include <clipping_planes_vertex>\n}\n"; const tmpDims = new Vector2(); /** * Specifies the way points are colored. */ export let MODE = /*#__PURE__*/function (MODE) { /** The points are colored using their own color */ MODE[MODE["COLOR"] = 0] = "COLOR"; /** The points are colored using their intensity */ MODE[MODE["INTENSITY"] = 1] = "INTENSITY"; /** The points are colored using their classification */ MODE[MODE["CLASSIFICATION"] = 2] = "CLASSIFICATION"; /** The points are colored using their normal */ MODE[MODE["NORMAL"] = 3] = "NORMAL"; /** The points are colored using an external texture, such as a color layer */ MODE[MODE["TEXTURE"] = 4] = "TEXTURE"; /** The points are colored using their elevation */ MODE[MODE["ELEVATION"] = 5] = "ELEVATION"; return MODE; }({}); const NUM_TRANSFO = 16; /** * Paremeters for a point cloud classification. */ export class Classification { /** * The color of this classification. */ /** * Toggles the visibility of points with this classification. */ constructor(color, visible = true) { this.color = new Color(color); this.visible = visible; } /** * Clones this classification. * @returns The cloned object. */ clone() { return new Classification(this.color.clone(), this.visible); } } /** * A set of 256 pre-defined classifications following the ASPRS scheme, with pre-defined colors for * classifications 0 to 18. The remaining classifications have the default color (#FF8100) * * See https://www.asprs.org/wp-content/uploads/2010/12/LAS_Specification.pdf */ export const ASPRS_CLASSIFICATIONS = new Array(256); const DEFAULT_CLASSIFICATION = new Classification(0xff8100); for (let i = 0; i < ASPRS_CLASSIFICATIONS.length; i++) { ASPRS_CLASSIFICATIONS[i] = DEFAULT_CLASSIFICATION.clone(); } ASPRS_CLASSIFICATIONS[0] = new Classification('#858585'); // Created, never classified ASPRS_CLASSIFICATIONS[1] = new Classification('#bfbfbf'); // Unclassified ASPRS_CLASSIFICATIONS[2] = new Classification('#834000'); // Ground ASPRS_CLASSIFICATIONS[3] = new Classification('#008100'); // Low vegetation ASPRS_CLASSIFICATIONS[4] = new Classification('#00bf00'); // Medium vegetation ASPRS_CLASSIFICATIONS[5] = new Classification('#00ff00'); // High vegetation ASPRS_CLASSIFICATIONS[6] = new Classification('#0081c1'); // Building ASPRS_CLASSIFICATIONS[7] = new Classification('#ff0000'); // Low point (noise) ASPRS_CLASSIFICATIONS[8] = DEFAULT_CLASSIFICATION.clone(); // Reserved ASPRS_CLASSIFICATIONS[9] = new Classification('#0000ff'); // Water ASPRS_CLASSIFICATIONS[10] = new Classification('#606d73'); // Rail ASPRS_CLASSIFICATIONS[11] = new Classification('#858585'); // Road surface ASPRS_CLASSIFICATIONS[12] = DEFAULT_CLASSIFICATION.clone(); // Reserved ASPRS_CLASSIFICATIONS[13] = new Classification('#ede440'); // Wire - Guard (Shield) ASPRS_CLASSIFICATIONS[14] = new Classification('#ed6840'); // Wire - Conductor (Phase) ASPRS_CLASSIFICATIONS[15] = new Classification('#29fff8'); // Transmission Tower ASPRS_CLASSIFICATIONS[16] = new Classification('#5e441d'); // Wire Structure connector (e.g Insulator) ASPRS_CLASSIFICATIONS[17] = new Classification('#7992c7'); // Bridge Deck ASPRS_CLASSIFICATIONS[18] = new Classification('#cd27d6'); // High Noise function createDefaultColorMap() { const colors = [new Color('black'), new Color('white')]; return new ColorMap({ colors, min: 0, max: 1000 }); } /** * Material used for point clouds. */ class PointCloudMaterial extends ShaderMaterial { isPointCloudMaterial = true; disposed = false; _colorMap = createDefaultColorMap(); /** * @internal */ // @ts-expect-error property is not assignable. /** * @internal */ /** * Gets or sets the point size. */ get size() { return this.uniforms.size.value; } set size(value) { this.uniforms.size.value = value; } /** * Gets or sets the point decimation value. * A decimation value of N means that we take every Nth point and discard the rest. */ get decimation() { return this.uniforms.decimation.value; } set decimation(value) { this.uniforms.decimation.value = value; } /** * Gets or sets the display mode (color, classification...) */ get mode() { return this.uniforms.mode.value; } set mode(mode) { this.uniforms.mode.value = mode; } /** * Update material uniforms related to intensity and classification attributes. */ setupFromGeometry(geometry) { this.enableClassification = geometry.hasAttribute('classification'); if (geometry.hasAttribute('intensity')) { const intensityType = MaterialUtils.getVertexAttributeType(geometry.getAttribute('intensity')); MaterialUtils.setDefine(this, 'INTENSITY', true); MaterialUtils.setDefineValue(this, 'INTENSITY_TYPE', intensityType); } } /** * @internal */ get pickingId() { return this.uniforms.pickingId.value; } /** * @internal */ set pickingId(id) { this.uniforms.pickingId.value = id; } /** * Gets or sets the overlay color (default color). */ get overlayColor() { return this.uniforms.overlayColor.value; } set overlayColor(color) { this.uniforms.overlayColor.value = color; } /** * Gets or sets the brightness of the points. */ get brightness() { return this.uniforms.brightnessContrastSaturation.value.x; } set brightness(v) { this.uniforms.brightnessContrastSaturation.value.setX(v); } /** * Gets or sets the contrast of the points. */ get contrast() { return this.uniforms.brightnessContrastSaturation.value.y; } set contrast(v) { this.uniforms.brightnessContrastSaturation.value.setY(v); } /** * Gets or sets the saturation of the points. */ get saturation() { return this.uniforms.brightnessContrastSaturation.value.z; } set saturation(v) { this.uniforms.brightnessContrastSaturation.value.setZ(v); } /** * Gets or sets the classifications of the points. * Up to 256 values are supported (i.e classifications in the range 0-255). * @defaultValue {@link ASPRS_CLASSIFICATIONS} (see https://www.asprs.org/wp-content/uploads/2010/12/LAS_Specification.pdf) */ get classifications() { if (this.uniforms.classifications == null) { // Initialize with default values this.uniforms.classifications = new Uniform(ASPRS_CLASSIFICATIONS); } return this.uniforms.classifications.value; } set classifications(classifications) { let actual = classifications; if (classifications.length > 256) { actual = classifications.slice(0, 256); console.warn('The provided classification array has been truncated to 256 elements'); } else if (classifications.length < 256) { actual = new Array(256); for (let i = 0; i < actual.length; i++) { if (i < classifications.length) { actual[i] = classifications[i]; } else { actual[i] = DEFAULT_CLASSIFICATION.clone(); } } } if (this.uniforms.classifications == null) { // Initialize with default values this.uniforms.classifications = new Uniform(actual); } this.uniforms.classifications.value = actual; } /** * @internal */ get enableClassification() { return this.defines.CLASSIFICATION !== undefined; } /** * @internal */ set enableClassification(enable) { MaterialUtils.setDefine(this, 'CLASSIFICATION', enable); if (enable && this.uniforms.classifications == null) { // Initialize with default values this.uniforms.classifications = new Uniform(ASPRS_CLASSIFICATIONS); } } get colorMap() { return this._colorMap; } set colorMap(colorMap) { this._colorMap = colorMap; } /** * Creates a PointsMaterial using the specified options. * * @param options - The options. */ constructor(options = {}) { super({ clipping: true, glslVersion: GLSL3 }); this.vertexShader = PointsVS; this.fragmentShader = PointsFS; // Default this.defines = { INTENSITY_TYPE: 'uint' }; for (const key of Object.keys(MODE)) { if (Object.prototype.hasOwnProperty.call(MODE, key)) { // @ts-expect-error a weird pattern indeed this.defines[`MODE_${key}`] = MODE[key]; } } this.fog = true; this.colorLayer = null; this.needsUpdate = true; this.uniforms = { fogDensity: new Uniform(0.00025), fogNear: new Uniform(1), fogFar: new Uniform(2000), decimation: new Uniform(1), fogColor: new Uniform(new Color(0xffffff)), classifications: new Uniform(ASPRS_CLASSIFICATIONS), // Texture-related uniforms extentBottomLeft: new Uniform(new Vector2(0, 0)), extentSize: new Uniform(new Vector2(0, 0)), overlayTexture: new Uniform(null), hasOverlayTexture: new Uniform(0), offsetScale: new Uniform(new OffsetScale(0, 0, 1, 1)), colorMap: new Uniform({ lut: this.colorMap.getTexture(), min: this.colorMap.min, max: this.colorMap.max }), size: new Uniform(options.size ?? 0), mode: new Uniform(options.mode ?? MODE.COLOR), pickingId: new Uniform(0), opacity: new Uniform(this.opacity), overlayColor: new Uniform(options.overlayColor ?? new Vector4(0, 0, 0, 0)), brightnessContrastSaturation: new Uniform(new Vector3(0, 1, 1)), enableDeformations: new Uniform(false), deformations: new Uniform([]) }; for (let i = 0; i < NUM_TRANSFO; i++) { this.uniforms.deformations.value.push({ transformation: new Matrix4(), vec: new Vector3(), origin: new Vector2(), influence: new Vector2(), color: new Color() }); } } dispose() { if (this.disposed) { return; } this.dispatchEvent({ type: 'dispose' }); this.disposed = true; } clone() { const cl = super.clone(); cl.update(this); return cl; } /** * Internally used for picking. * @internal */ enablePicking(picking) { this.pickingId = picking; this.blending = picking ? NoBlending : NormalBlending; } hasColorLayer(layer) { return this.colorLayer === layer; } updateUniforms() { this.uniforms.opacity.value = this.opacity; const colorMapUniform = this.uniforms.colorMap.value; colorMapUniform.min = this.colorMap.min; colorMapUniform.max = this.colorMap.max; colorMapUniform.lut = this.colorMap.getTexture(); } onBeforeRender() { this.uniforms.opacity.value = this.opacity; this.transparent = this.opacity < 1 || this.colorMap.opacity != null; } update(source) { if (source) { this.visible = source.visible; this.opacity = source.opacity; this.transparent = source.transparent; this.needsUpdate = true; this.size = source.size; this.mode = source.mode; this.overlayColor.copy(source.overlayColor); this.classifications = source.classifications; this.brightness = source.brightness; this.contrast = source.contrast; this.saturation = source.saturation; this.colorMap = source.colorMap; this.decimation = source.decimation; } this.updateUniforms(); if (source) { Object.assign(this.defines, source.defines); } return this; } removeColorLayer() { this.mode = MODE.COLOR; this.colorLayer = null; this.uniforms.overlayTexture.value = null; this.needsUpdate = true; this.uniforms.hasOverlayTexture.value = 0; } pushColorLayer(layer, extent) { this.mode = MODE.TEXTURE; this.colorLayer = layer; this.uniforms.extentBottomLeft.value.set(extent.west, extent.south); const dim = extent.dimensions(tmpDims); this.uniforms.extentSize.value.copy(dim); this.needsUpdate = true; } indexOfColorLayer(layer) { if (layer === this.colorLayer) { return 0; } return -1; } getColorTexture(layer) { if (layer !== this.colorLayer) { return null; } return this.uniforms.overlayTexture?.value; } setColorTextures(layer, textureAndPitch) { const { texture } = textureAndPitch; this.uniforms.overlayTexture.value = texture; this.uniforms.hasOverlayTexture.value = 1; } setLayerVisibility() { // no-op } setLayerOpacity() { // no-op } setLayerElevationRange() { // no-op } setColorimetry() { // Not implemented because the points have their own BCS controls } /** * Unused for now. * @internal */ enableTransfo(v) { if (v) { this.defines.DEFORMATION_SUPPORT = 1; this.defines.NUM_TRANSFO = NUM_TRANSFO; } else { delete this.defines.DEFORMATION_SUPPORT; delete this.defines.NUM_TRANSFO; } this.needsUpdate = true; } static isPointCloudMaterial = obj => obj?.isPointCloudMaterial; } export default PointCloudMaterial;