@giro3d/giro3d
Version:
A JS/WebGL framework for 3D geospatial data visualization
340 lines (329 loc) • 10.6 kB
JavaScript
/*
* Copyright (c) 2015-2018, IGN France.
* Copyright (c) 2018-2026, Giro3D team.
* SPDX-License-Identifier: MIT
*/
import { defined } from '../../utils/tsutils';
import { DEFAULT_VALUE_RANGES } from '../las/dimension';
/**
* The names of attributes for legacy Potree point clouds (the ones with .bin files in a hierarchy).
* For LAZ-based potree datasets, the names are the actual LAS dimension names.
*/
/**
* A function that reads point at the given index, and fills the array with the point data.
*/
var PotreeDataType = /*#__PURE__*/function (PotreeDataType) {
PotreeDataType[PotreeDataType["Uint8"] = 0] = "Uint8";
PotreeDataType[PotreeDataType["Uint16"] = 1] = "Uint16";
PotreeDataType[PotreeDataType["Uint32"] = 2] = "Uint32";
PotreeDataType[PotreeDataType["Float"] = 3] = "Float";
PotreeDataType[PotreeDataType["Double"] = 4] = "Double";
return PotreeDataType;
}(PotreeDataType || {});
/**
* Point cloud attribute for LAZ-based Potree datasets.
*/
/**
* Point cloud attribute for BIN-based Potree datasets.
*/
function getDefaultMin(type) {
switch (type) {
case PotreeDataType.Uint8:
case PotreeDataType.Uint16:
case PotreeDataType.Uint32:
return 0;
case PotreeDataType.Float:
case PotreeDataType.Double:
return undefined;
}
}
function createAttribute(type, dimension, normalized, interpretation, min, max) {
return {
type,
dimension,
normalized: normalized ?? false,
interpretation: interpretation ?? 'unknown',
min: min ?? getDefaultMin(type),
max
};
}
const POTREE_ATTRIBUTES = {
POSITION_CARTESIAN: createAttribute(PotreeDataType.Float, 3),
// Color attributes. We don't support the alpha component.
COLOR_PACKED: createAttribute(PotreeDataType.Uint8, 4, true, 'color'),
RGBA_PACKED: createAttribute(PotreeDataType.Uint8, 4, true, 'color'),
RGB_PACKED: createAttribute(PotreeDataType.Uint8, 3, true, 'color'),
// Normal attributes (unsupported)
NORMAL_FLOATS: createAttribute(PotreeDataType.Float, 3),
NORMAL: createAttribute(PotreeDataType.Float, 3),
NORMAL_SPHEREMAPPED: createAttribute(PotreeDataType.Uint8, 2),
NORMAL_OCT16: createAttribute(PotreeDataType.Uint8, 2),
// LAS-like attributes
INTENSITY: createAttribute(PotreeDataType.Uint16, 1),
CLASSIFICATION: createAttribute(PotreeDataType.Uint8, 1, false, 'classification'),
RETURN_NUMBER: createAttribute(PotreeDataType.Uint8, 1),
NUMBER_OF_RETURNS: createAttribute(PotreeDataType.Uint8, 1),
SOURCE_ID: createAttribute(PotreeDataType.Uint16, 1),
GPS_TIME: createAttribute(PotreeDataType.Double, 1),
// Misc
SPACING: createAttribute(PotreeDataType.Float, 1),
INDICES: createAttribute(PotreeDataType.Uint32, 1)
};
/** The list of attributes that we expose to the API. */
export const EXPOSED_ATTRIBUTES = new Set([
// BIN based potree clouds
'COLOR_PACKED', 'RGBA_PACKED', 'RGB_PACKED', 'INTENSITY', 'CLASSIFICATION', 'RETURN_NUMBER', 'NUMBER_OF_RETURNS', 'SOURCE_ID', 'GPS_TIME', 'SPACING', 'INDICES',
// LAZ-based potree clouds
'Color', 'Classification', 'Intensity', 'GpsTime', 'ReturnNumber', 'PointSourceId', 'NumberOfReturns', 'Z']);
const UNSUPPORTED_ATTRIBUTES = new Set(['NORMAL', 'NORMAL_FLOATS', 'NORMAL_OCT16', 'NORMAL_SPHEREMAPPED']);
function getSize(type) {
switch (type) {
case PotreeDataType.Uint8:
return 1;
case PotreeDataType.Uint16:
return 2;
case PotreeDataType.Uint32:
case PotreeDataType.Float:
return 4;
case PotreeDataType.Double:
return 8;
}
}
function mapSize(type) {
switch (type) {
case PotreeDataType.Uint8:
return 1;
case PotreeDataType.Uint16:
return 2;
case PotreeDataType.Uint32:
case PotreeDataType.Float:
case PotreeDataType.Double:
// We have to downcast 64-bit numbers to 32-bit.
return 4;
}
}
function mapType(type) {
switch (type) {
case PotreeDataType.Uint8:
case PotreeDataType.Uint16:
case PotreeDataType.Uint32:
return 'unsigned';
case PotreeDataType.Float:
case PotreeDataType.Double:
return 'float';
}
}
function mapDimension(input) {
switch (input) {
case 4:
// The point cloud source does not support 4-component vectors (i.e RGBA colors),
// so we have to ignore the 4th component.
return 3;
case 2:
// This should not happen since Vec2 attributes such as NORMAL_OCT16 are not exposed.
throw new Error('not supported.');
default:
return input;
}
}
/**
* Generates a reader function from an attribute and a byte offset.
*
* Note: Potree .bin data has interleaved attributes: X0,Y0,Z0,R0,G0,B0,A0,[...],Xn,Yn,Zn,Rn,Gn,Bn,An
*/
const readScalar = (attribute, attributeOffset, pointByteSize) => {
let readFn;
switch (attribute.type) {
case PotreeDataType.Uint8:
readFn = (view, offset) => view.getUint8(offset);
break;
case PotreeDataType.Uint16:
readFn = (view, offset) => view.getUint16(offset, true);
break;
case PotreeDataType.Uint32:
readFn = (view, offset) => view.getUint32(offset, true);
break;
case PotreeDataType.Float:
readFn = (view, offset) => view.getFloat32(offset, true);
break;
case PotreeDataType.Double:
readFn = (view, offset) => view.getFloat64(offset, true);
break;
}
return (view, pointIndex, target) => {
target[pointIndex] = readFn(view, pointIndex * pointByteSize + attributeOffset);
};
};
const readPositionCartesian = (_attribute, attributeOffset, pointByteSize) => {
const itemSize = 4; // 4 bytes per component
return (view, pointIndex, target) => {
const offset = pointIndex * pointByteSize + attributeOffset;
const x = view.getUint32(offset + 0 * itemSize, true);
const y = view.getUint32(offset + 1 * itemSize, true);
const z = view.getUint32(offset + 2 * itemSize, true);
target[pointIndex * 3 + 0] = x;
target[pointIndex * 3 + 1] = y;
target[pointIndex * 3 + 2] = z;
};
};
const readColor = (attribute, attributeOffset, pointByteSize) => {
return (view, pointIndex, target) => {
const offset = pointIndex * pointByteSize + attributeOffset;
// Note that we ignore the alpha component (i.e RGBA_PACKED is equivalent to RGB_PACKED)
const r = view.getUint8(offset + 0);
const g = view.getUint8(offset + 1);
const b = view.getUint8(offset + 2);
target[pointIndex * 3 + 0] = r;
target[pointIndex * 3 + 1] = g;
target[pointIndex * 3 + 2] = b;
};
};
export function createReader(attribute, pointByteSize) {
const {
name,
offset,
potreeAttribute
} = attribute;
if (name === 'POSITION_CARTESIAN') {
return readPositionCartesian(potreeAttribute, offset, pointByteSize);
}
// Some special readers
switch (attribute.interpretation) {
case 'color':
return readColor(potreeAttribute, offset, pointByteSize);
}
return readScalar(potreeAttribute, offset, pointByteSize);
}
export function processLazAttributes(boundingBox) {
const result = [];
function attr(name, dimension, size, type, interp) {
const attribute = {
name,
dimension,
size,
type,
interpretation: interp ?? 'unknown'
};
if (name === 'Color') {
attribute.min = 0;
attribute.max = 255;
} else if (name === 'Z') {
attribute.min = boundingBox.lz;
attribute.max = boundingBox.uz;
} else if (DEFAULT_VALUE_RANGES[name] != null) {
const {
min,
max
} = DEFAULT_VALUE_RANGES[name];
attribute.min = min;
attribute.max = max;
}
return attribute;
}
result.push(attr('Z', 1, 4, 'float'));
result.push(attr('Color', 3, 1, 'unsigned', 'color'));
result.push(attr('Intensity', 1, 2, 'unsigned'));
result.push(attr('Classification', 1, 1, 'unsigned', 'classification'));
result.push(attr('GpsTime', 1, 4, 'float'));
result.push(attr('NumberOfReturns', 1, 1, 'unsigned'));
result.push(attr('ReturnNumber', 1, 1, 'unsigned'));
result.push(attr('PointSourceId', 1, 2, 'unsigned'));
return result;
}
/**
* Given a list of attribute names, returns a list of actual attributes.
*/
export function processAttributes(names) {
let offset = 0;
let pointByteSize = 0;
const attributes = [];
// Loop once to compute the total byte size of a single point, including all attributes.
// This will be used to compute the byte offsets when reading the data buffer.
for (const name of names) {
const potreeAttribute = defined(POTREE_ATTRIBUTES, name);
const sizeBytes = potreeAttribute.dimension * getSize(potreeAttribute.type);
pointByteSize += sizeBytes;
}
for (const name of names) {
if (UNSUPPORTED_ATTRIBUTES.has(name)) {
continue;
}
const potreeAttribute = defined(POTREE_ATTRIBUTES, name);
const sizeBytes = potreeAttribute.dimension * getSize(potreeAttribute.type);
const minmax = getMinMax(name);
const sourceAttribute = {
name,
normalized: potreeAttribute.normalized,
interpretation: potreeAttribute.interpretation,
dimension: mapDimension(potreeAttribute.dimension),
size: mapSize(potreeAttribute.type),
type: mapType(potreeAttribute.type),
potreeAttribute,
offset,
min: minmax?.min,
max: minmax?.max
};
attributes.push(sourceAttribute);
offset += sizeBytes;
}
return {
attributes,
pointByteSize
};
}
export function getTypedArray(type, size, dimension, itemCount) {
const arrayLength = itemCount * dimension;
switch (type) {
case 'signed':
switch (size) {
case 1:
return new Int8Array(arrayLength);
case 2:
return new Int16Array(arrayLength);
case 4:
return new Int32Array(arrayLength);
}
break;
case 'unsigned':
switch (size) {
case 1:
return new Uint8Array(arrayLength);
case 2:
return new Uint16Array(arrayLength);
case 4:
return new Uint32Array(arrayLength);
}
break;
case 'float':
return new Float32Array(arrayLength);
}
}
function getMinMax(name) {
switch (name) {
case 'RGB_PACKED':
case 'RGBA_PACKED':
case 'COLOR_PACKED':
case 'CLASSIFICATION':
return {
min: 0,
max: 255
};
case 'INTENSITY':
return {
min: 0,
max: 65535
};
case 'RETURN_NUMBER':
return {
min: 0,
max: 7
};
case 'NUMBER_OF_RETURNS':
return {
min: 0,
max: 7
};
default:
return undefined;
}
}