itowns
Version:
A JS/WebGL framework for 3D geospatial data visualization
485 lines (462 loc) • 20.8 kB
JavaScript
import * as THREE from 'three';
/* babel-plugin-inline-import './Shader/PointsVS.glsl' */
const PointsVS = "#include <common>\n#include <fog_pars_vertex>\n#include <morphtarget_pars_vertex>\n#include <logdepthbuf_pars_vertex>\n#include <clipping_planes_pars_vertex>\nvarying vec4 vColor; // color_pars_vertex\n\n#ifdef USE_POINTS_UV\n varying vec2 vUv;\n uniform mat3 uvTransform;\n#endif\n\n#define NB_CLASS 8.\n\nuniform float size;\nuniform float scale;\n\nuniform bool picking;\nuniform int mode;\n\nuniform vec2 elevationRange;\nuniform vec2 intensityRange;\nuniform vec2 angleRange;\n\nuniform sampler2D classificationTexture;\nuniform sampler2D discreteTexture;\nuniform sampler2D gradientTexture;\nuniform int sizeMode;\nuniform float minAttenuatedSize;\nuniform float maxAttenuatedSize;\n\nattribute vec4 unique_id;\nattribute float intensity;\nattribute float classification;\nattribute float pointSourceID;\n\nattribute float returnNumber;\nattribute float numberOfReturns;\nattribute float scanAngle;\n\nvoid main() {\n vColor = vec4(1.0);\n if (picking) {\n vColor = unique_id;\n } else {\n if (mode == PNTS_MODE_CLASSIFICATION) {\n vec2 uv = vec2(classification/255., 0.5);\n vColor = texture2D(classificationTexture, uv);\n } else if (mode == PNTS_MODE_NORMAL) {\n vColor.rgb = abs(normal);\n } else if (mode == PNTS_MODE_COLOR) {\n#if defined(USE_COLOR)\n vColor.rgb = color.rgb;\n#elif defined(USE_COLOR_ALPHA)\n vColor = color;\n#endif\n } else if (mode == PNTS_MODE_RETURN_NUMBER) {\n vec2 uv = vec2(returnNumber/255., 0.5);\n vColor = texture2D(discreteTexture, uv);\n } else if (mode == PNTS_MODE_RETURN_TYPE) {\n float returnType;\n if (returnNumber > numberOfReturns) {\n returnType = 4.;\n } else if (returnNumber == 1.) {\n if (numberOfReturns == 1.) {\n // single\n returnType = 0.;\n } else {\n // first\n returnType = 1.;\n }\n } else {\n if (returnNumber == numberOfReturns) {\n // last\n returnType = 3.;\n } else {\n // intermediate\n returnType = 2.;\n }\n }\n vec2 uv = vec2(returnType/255., 0.5);\n vColor = texture2D(discreteTexture, uv);\n } else if (mode == PNTS_MODE_RETURN_COUNT) {\n vec2 uv = vec2(numberOfReturns/255., 0.5);\n vColor = texture2D(discreteTexture, uv);\n } else if (mode == PNTS_MODE_POINT_SOURCE_ID) {\n vec2 uv = vec2(mod(pointSourceID, NB_CLASS)/255., 0.5);\n vColor = texture2D(discreteTexture, uv);\n } else if (mode == PNTS_MODE_SCAN_ANGLE) {\n float i = (scanAngle - angleRange.x) / (angleRange.y - angleRange.x);\n vec2 uv = vec2(i, (1. - i));\n vColor = texture2D(gradientTexture, uv);\n } else if (mode == PNTS_MODE_INTENSITY) {\n float i = (intensity - intensityRange.x) / (intensityRange.y - intensityRange.x);\n vec2 uv = vec2(i, (1. - i));\n vColor = texture2D(gradientTexture, uv);\n } else if (mode == PNTS_MODE_ELEVATION) {\n float z = (modelMatrix * vec4(position, 1.0)).z;\n float i = (z - elevationRange.x) / (elevationRange.y - elevationRange.x);\n vec2 uv = vec2(i, (1. - i));\n vColor = texture2D(gradientTexture, uv);\n }\n }\n\n#define USE_COLOR_ALPHA\n#include <morphcolor_vertex>\n#include <begin_vertex>\n#include <morphtarget_vertex>\n#include <project_vertex>\n\n gl_PointSize = size;\n\n if (sizeMode == PNTS_SIZE_MODE_ATTENUATED) {\n bool isPerspective = isPerspectiveMatrix(projectionMatrix);\n\n if (isPerspective) {\n gl_PointSize *= scale / -mvPosition.z;\n gl_PointSize = clamp(gl_PointSize, minAttenuatedSize, maxAttenuatedSize);\n }\n }\n\n#include <logdepthbuf_vertex>\n#include <clipping_planes_vertex>\n#include <worldpos_vertex>\n#include <fog_vertex>\n}\n";
/* babel-plugin-inline-import './Shader/PointsFS.glsl' */
const PointsFS = "#define USE_COLOR_ALPHA\n\n#include <color_pars_fragment>\n#include <map_particle_pars_fragment>\n#include <alphatest_pars_fragment>\n#include <alphahash_pars_fragment>\n#include <fog_pars_fragment>\n#include <logdepthbuf_pars_fragment>\n#include <clipping_planes_pars_fragment>\n\nuniform vec3 diffuse;\nuniform float opacity;\n\nuniform bool picking;\nuniform int shape;\n\nvoid main() {\n\n// Early discard (clipping planes and shape)\n#include <clipping_planes_fragment>\n if (shape == PNTS_SHAPE_CIRCLE) {\n //circular rendering in glsl\n if ((length(gl_PointCoord - 0.5) > 0.5)) {\n discard;\n }\n }\n\n#include <logdepthbuf_fragment>\n\n vec4 diffuseColor = vec4(diffuse, opacity);\n#include <map_particle_fragment>\n#include <color_fragment>\n\n#include <alphatest_fragment>\n#include <alphahash_fragment>\n\n vec3 outgoingLight = diffuseColor.rgb;\n#include <opaque_fragment> // gl_FragColor\n#include <tonemapping_fragment>\n#include <fog_fragment>\n#include <premultiplied_alpha_fragment>\n\n}\n";
import CommonMaterial from "./CommonMaterial.js";
import Gradients from "../Utils/Gradients.js";
export const PNTS_MODE = {
COLOR: 0,
INTENSITY: 1,
CLASSIFICATION: 2,
ELEVATION: 3,
RETURN_NUMBER: 4,
RETURN_TYPE: 5,
RETURN_COUNT: 6,
POINT_SOURCE_ID: 7,
SCAN_ANGLE: 8,
NORMAL: 9
};
export const PNTS_SHAPE = {
CIRCLE: 0,
SQUARE: 1
};
export const PNTS_SIZE_MODE = {
VALUE: 0,
ATTENUATED: 1
};
const white = new THREE.Color(1.0, 1.0, 1.0);
/**
* Every lidar point can have a classification assigned to it that defines
* the type of object that has reflected the laser pulse. Lidar points can be
* classified into a number of categories including bare earth or ground,
* top of canopy, and water. The different classes are defined using numeric
* integer codes in the files.
*
* @typedef {Object} Classification
* @property {boolean} visible - category visibility,
* @property {string} name - category name,
* @property {THREE.Color} color - category color,
* @property {number} opacity - category opacity,
*/
export const ClassificationScheme = {
DEFAULT: {
0: {
visible: true,
name: 'never classified',
color: new THREE.Color(0.5, 0.5, 0.5),
opacity: 1.0
},
1: {
visible: true,
name: 'unclassified',
color: new THREE.Color(0.5, 0.5, 0.5),
opacity: 1.0
},
2: {
visible: true,
name: 'ground',
color: new THREE.Color(0.63, 0.32, 0.18),
opacity: 1.0
},
3: {
visible: true,
name: 'low vegetation',
color: new THREE.Color(0.0, 1.0, 0.0),
opacity: 1.0
},
4: {
visible: true,
name: 'medium vegetation',
color: new THREE.Color(0.0, 0.8, 0.0),
opacity: 1.0
},
5: {
visible: true,
name: 'high vegetation',
color: new THREE.Color(0.0, 0.6, 0.0),
opacity: 1.0
},
6: {
visible: true,
name: 'building',
color: new THREE.Color(1.0, 0.66, 0.0),
opacity: 1.0
},
7: {
visible: true,
name: 'low point(noise)',
color: new THREE.Color(1.0, 0.0, 1.0),
opacity: 1.0
},
8: {
visible: true,
name: 'key-point',
color: new THREE.Color(1.0, 0.0, 0.0),
opacity: 1.0
},
9: {
visible: true,
name: 'water',
color: new THREE.Color(0.0, 0.0, 1.0),
opacity: 1.0
},
10: {
visible: true,
name: 'rail',
color: new THREE.Color(0.8, 0.8, 1.0),
opacity: 1.0
},
11: {
visible: true,
name: 'road Surface',
color: new THREE.Color(0.4, 0.4, 0.7),
opacity: 1.0
},
12: {
visible: true,
name: 'overlap',
color: new THREE.Color(1.0, 1.0, 0.0),
opacity: 1.0
},
DEFAULT: {
visible: true,
name: 'default',
color: new THREE.Color(0.3, 0.6, 0.6),
opacity: 1.0
}
}
};
const DiscreteScheme = {
DEFAULT: {
0: {
visible: true,
name: '0',
color: new THREE.Color('rgb(67, 99, 216)'),
opacity: 1.0
},
1: {
visible: true,
name: '1',
color: new THREE.Color('rgb(60, 180, 75);'),
opacity: 1.0
},
2: {
visible: true,
name: '2',
color: new THREE.Color('rgb(255, 255, 25)'),
opacity: 1.0
},
3: {
visible: true,
name: '3',
color: new THREE.Color('rgb(145, 30, 180)'),
opacity: 1.0
},
4: {
visible: true,
name: '4',
color: new THREE.Color('rgb(245, 130, 49)'),
opacity: 1.0
},
5: {
visible: true,
name: '5',
color: new THREE.Color('rgb(230, 25, 75)'),
opacity: 1.0
},
6: {
visible: true,
name: '6',
color: new THREE.Color('rgb(66, 212, 244)'),
opacity: 1.0
},
7: {
visible: true,
name: '7',
color: new THREE.Color('rgb(240, 50, 230)'),
opacity: 1.0
},
DEFAULT: {
visible: true,
name: 'default',
color: white,
opacity: 1.0
}
}
};
// Taken from Potree. Copyright (c) 2011-2020, Markus Schütz All rights reserved.
// https://github.com/potree/potree/blob/develop/src/materials/PointCloudMaterial.js
function generateGradientTexture(gradient) {
const size = 64;
// create canvas
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
// get context
const context = canvas.getContext('2d');
// draw gradient
context.rect(0, 0, size, size);
const ctxGradient = context.createLinearGradient(0, 0, size, size);
for (let i = 0; i < gradient.length; i++) {
const step = gradient[i];
ctxGradient.addColorStop(step[0], `#${step[1].getHexString()}`);
}
context.fillStyle = ctxGradient;
context.fill();
const texture = new THREE.CanvasTexture(canvas);
texture.needsUpdate = true;
texture.minFilter = THREE.LinearFilter;
texture.wrap = THREE.RepeatWrapping;
texture.repeat = 2;
return texture;
}
function recomputeTexture(scheme, texture, nbClass) {
let needTransparency;
const data = texture.image.data;
const width = texture.image.width;
if (!nbClass) {
nbClass = Object.keys(scheme).length;
}
for (let i = 0; i < width; i++) {
let color;
let opacity;
let visible = true;
if (scheme[i]) {
color = scheme[i].color;
visible = scheme[i].visible;
opacity = scheme[i].opacity;
} else if (scheme[i % nbClass]) {
color = scheme[i % nbClass].color;
visible = scheme[i % nbClass].visible;
opacity = scheme[i % nbClass].opacity;
} else if (scheme.DEFAULT) {
color = scheme.DEFAULT.color;
visible = scheme.DEFAULT.visible;
opacity = scheme.DEFAULT.opacity;
} else {
color = white;
opacity = 1.0;
}
const j = 4 * i;
data[j + 0] = parseInt(255 * color.r, 10);
data[j + 1] = parseInt(255 * color.g, 10);
data[j + 2] = parseInt(255 * color.b, 10);
data[j + 3] = visible ? parseInt(255 * opacity, 10) : 0;
needTransparency = needTransparency || opacity < 1 || !visible;
}
texture.needsUpdate = true;
return needTransparency;
}
class PointsMaterial extends THREE.ShaderMaterial {
/**
* @class PointsMaterial
* @param {object} [options={}] The options
* @param {number} [options.size=1] point size
* @param {number} [options.mode=PNTS_MODE.COLOR] display mode.
* @param {number} [options.shape=PNTS_SHAPE.CIRCLE] rendered points shape.
* @param {THREE.Vector4} [options.overlayColor=new THREE.Vector4(0, 0, 0, 0)] overlay color.
* @param {Scheme} [options.classificationScheme] LUT for point classification colorization.
* @param {Scheme} [options.discreteScheme] LUT for other discret point values colorization.
* @param {string} [options.gradient] Descrition of the gradient to use for continuous point values.
* (Default value will be the 'SPECTRAL' gradient from Utils/Gradients)
* @param {number} [options.sizeMode=PNTS_SIZE_MODE.VALUE] point cloud size mode. Only 'VALUE' or 'ATTENUATED' are possible. VALUE use constant size, ATTENUATED compute size depending on distance from point to camera.
* @param {number} [options.minAttenuatedSize=3] minimum scale used by 'ATTENUATED' size mode
* @param {number} [options.maxAttenuatedSize=10] maximum scale used by 'ATTENUATED' size mode
*
* @property {THREE.Vector2} [options.intensityRange=new THREE.Vector2(1, 65536)] intensity range (default value will be [1, 65536] if not defined at Layer level).
* @property {THREE.Vector2} [options.elevationRange=new THREE.Vector2(0, 1000)] elevation range (default value will be [0, 1000] if not defined at Layer level).
* @property {THREE.Vector2} [options.angleRange=new THREE.Vector2(-90, 90)] scan angle range (default value will be [-90, 90] if not defined at Layer level).
* @property {Scheme} classificationScheme - Color scheme for point classification values.
* @property {Scheme} discreteScheme - Color scheme for all other discrete values.
* @property {object} gradients - Descriptions of all available gradients.
* @property {object} gradient - Description of the gradient to use for display.
* @property {THREE.CanvasTexture} gradientTexture - The texture generate from the choosen gradient.
*
* @example
* // change color category classification
* const pointMaterial = new PointsMaterial();
* pointMaterial.classification[3].color.setStyle('red');
* pointMaterial.recomputeClassification();
*/
constructor() {
let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
const gradients = {
...options.gradient,
...Gradients
};
options.gradient = Object.values(gradients)[0];
const {
intensityRange = new THREE.Vector2(1, 65536),
elevationRange = new THREE.Vector2(0, 1000),
angleRange = new THREE.Vector2(-90, 90),
classificationScheme = ClassificationScheme.DEFAULT,
discreteScheme = DiscreteScheme.DEFAULT,
size = 1,
mode = PNTS_MODE.COLOR,
shape = PNTS_SHAPE.CIRCLE,
sizeMode = PNTS_SIZE_MODE.ATTENUATED,
minAttenuatedSize = 3,
maxAttenuatedSize = 10,
gradient,
scale = 0.05 * 0.5 / Math.tan(1.0 / 2.0),
...materialOptions
} = options;
super({
...materialOptions,
fog: true,
precision: 'highp',
vertexColors: true
});
this.uniforms = THREE.UniformsUtils.merge([
// THREE.PointsMaterial uniforms
THREE.UniformsLib.points, THREE.UniformsLib.fog]);
this.vertexShader = PointsVS;
this.fragmentShader = PointsFS;
this.userData.needTransparency = {};
this.gradients = gradients;
this.gradientTexture = new THREE.CanvasTexture();
CommonMaterial.setDefineMapping(this, 'PNTS_MODE', PNTS_MODE);
CommonMaterial.setDefineMapping(this, 'PNTS_SHAPE', PNTS_SHAPE);
CommonMaterial.setDefineMapping(this, 'PNTS_SIZE_MODE', PNTS_SIZE_MODE);
this.size = size;
CommonMaterial.setUniformProperty(this, 'mode', mode);
CommonMaterial.setUniformProperty(this, 'shape', shape);
CommonMaterial.setUniformProperty(this, 'picking', false);
CommonMaterial.setUniformProperty(this, 'opacity', this.opacity);
CommonMaterial.setUniformProperty(this, 'intensityRange', intensityRange);
CommonMaterial.setUniformProperty(this, 'elevationRange', elevationRange);
CommonMaterial.setUniformProperty(this, 'angleRange', angleRange);
CommonMaterial.setUniformProperty(this, 'sizeMode', sizeMode);
CommonMaterial.setUniformProperty(this, 'scale', scale);
CommonMaterial.setUniformProperty(this, 'minAttenuatedSize', minAttenuatedSize);
CommonMaterial.setUniformProperty(this, 'maxAttenuatedSize', maxAttenuatedSize);
// add classification texture to apply classification lut.
const data = new Uint8Array(256 * 4);
const texture = new THREE.DataTexture(data, 256, 1, THREE.RGBAFormat);
texture.needsUpdate = true;
texture.magFilter = THREE.NearestFilter;
CommonMaterial.setUniformProperty(this, 'classificationTexture', texture);
// add texture to applying the discrete lut.
const dataLUT = new Uint8Array(256 * 4);
const textureLUT = new THREE.DataTexture(dataLUT, 256, 1, THREE.RGBAFormat);
textureLUT.needsUpdate = true;
textureLUT.magFilter = THREE.NearestFilter;
CommonMaterial.setUniformProperty(this, 'discreteTexture', textureLUT);
// Classification and other discrete values scheme
this.classificationScheme = classificationScheme;
this.discreteScheme = discreteScheme;
// Update classification and discrete Texture
this.recomputeClassification();
this.recomputeDiscreteTexture();
// Gradient texture for continuous values
this.gradient = gradient;
CommonMaterial.setUniformProperty(this, 'gradientTexture', this.gradientTexture);
}
/**
* Copy the parameters from the passed material into this material.
* @override
* @param {THREE.PointsMaterial} source
* @returns {this}
*/
copy(source) {
// Manually copy this needTransparency if source doesn't have one. Prevents losing it when copying a three
// PointsMaterial into this PointsMaterial
const needTransparency = source.userData.needTransparency !== undefined ? source.userData.needTransparency : this.userData.needTransparency;
if (source.isShaderMaterial) {
super.copy(source);
} else {
THREE.Material.prototype.copy.call(this, source);
}
// Parameters of THREE.PointsMaterial
this.color.copy(source.color);
this.map = source.map;
this.alphaMap = source.alphaMap;
this.size = source.size;
this.sizeAttenuation = source.sizeAttenuation;
this.fog = source.fog;
this.userData.needTransparency = needTransparency;
return this;
}
/** @returns {THREE.Color} */
get color() {
return this.uniforms.diffuse.value;
}
/** @param {THREE.Color} color */
set color(color) {
this.uniforms.diffuse.value.copy(color);
}
/** @returns {THREE.Texture | null} */
get map() {
return this.uniforms.map.value;
}
/** @param {THREE.Texture | null} map */
set map(map) {
this.uniforms.map.value = map;
if (!map) {
return;
}
if (map.matrixAutoUpdate) {
map.updateMatrix();
}
this.uniforms.uvTransform.value.copy(map.matrix);
}
/** @returns {THREE.Texture | null} */
get alphaMap() {
return this.uniforms.alphaMap.value;
}
/** @param {THREE.Texture | null} map */
set alphaMap(map) {
this.uniforms.alphaMap.value = map;
if (!map) {
return;
}
if (map.matrixAutoUpdate) {
map.updateMatrix();
}
this.uniforms.alphaMapTransform.value.copy(map.matrix);
}
/** @returns {number} */
get size() {
return this.uniforms.size.value;
}
/** @param {number} size */
set size(size) {
this.uniforms.size.value = size;
}
/** @returns {boolean} */
get sizeAttenuation() {
return this.sizeMode !== PNTS_SIZE_MODE.VALUE;
}
/** @param {boolean} value */
set sizeAttenuation(value) {
this.sizeMode = value ? PNTS_SIZE_MODE.ATTENUATED : PNTS_SIZE_MODE.VALUE;
}
recomputeClassification() {
const needTransparency = recomputeTexture(this.classificationScheme, this.classificationTexture, 256);
this.userData.needTransparency[PNTS_MODE.CLASSIFICATION] = needTransparency;
this.dispatchEvent({
type: 'material_property_changed',
target: this.uniforms
});
}
recomputeDiscreteTexture() {
const needTransparency = recomputeTexture(this.discreteScheme, this.discreteTexture);
this.userData.needTransparency[PNTS_MODE.RETURN_NUMBER] = needTransparency;
this.userData.needTransparency[PNTS_MODE.RETURN_TYPE] = needTransparency;
this.userData.needTransparency[PNTS_MODE.RETURN_COUNT] = needTransparency;
this.userData.needTransparency[PNTS_MODE.POINT_SOURCE_ID] = needTransparency;
this.dispatchEvent({
type: 'material_property_changed',
target: this.uniforms
});
}
enablePicking(picking) {
this.picking = picking;
this.blending = picking ? THREE.NoBlending : THREE.NormalBlending;
}
set gradient(value) {
this.gradientTexture = generateGradientTexture(value);
}
}
export default PointsMaterial;