-
Notifications
You must be signed in to change notification settings - Fork 303
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for other data types in potree pointclouds #767
Changes from 4 commits
a250f90
b319777
79b08d8
e2f7e92
276c504
d2e1f55
93b3e27
bb7e431
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,52 +1,107 @@ | ||
import * as THREE from 'three'; | ||
|
||
// See the different constants holding ordinal, name, numElements, byteSize in PointAttributes.cpp in PotreeConverter | ||
// elementByteSize is byteSize / numElements | ||
const POINT_ATTTRIBUTES = { | ||
POSITION_CARTESIAN: { | ||
numElements: 3, | ||
arrayType: Float32Array, | ||
attributeName: 'position', | ||
}, | ||
COLOR_PACKED: { | ||
numElements: 4, | ||
arrayType: Uint8Array, | ||
attributeName: 'color', | ||
normalized: true, | ||
}, | ||
INTENSITY: { | ||
numElements: 1, | ||
numByte: 2, | ||
// using Float32Array because Float16Array doesn't exist | ||
arrayType: Float32Array, | ||
attributeName: 'intensity', | ||
normalized: true, | ||
}, | ||
CLASSIFICATION: { | ||
numElements: 1, | ||
arrayType: Uint8Array, | ||
attributeName: 'classification', | ||
}, | ||
// Note: at the time of writing, PotreeConverter will only generate normals in Oct16 format | ||
// see PotreeConverter.cpp:121 | ||
// we keep all the historical value to still supports old conversion | ||
NORMAL_SPHEREMAPPED: { | ||
numElements: 2, | ||
arrayType: Uint8Array, | ||
attributeName: 'sphereMappedNormal', | ||
}, | ||
// see https://web.archive.org/web/20150303053317/http://lgdv.cs.fau.de/get/1602 | ||
NORMAL_OCT16: { | ||
numElements: 2, | ||
arrayType: Uint8Array, | ||
attributeName: 'oct16Normal', | ||
}, | ||
NORMAL: { | ||
numElements: 3, | ||
arrayType: Float32Array, | ||
attributeName: 'normal', | ||
}, | ||
}; | ||
|
||
for (const potreeName of Object.keys(POINT_ATTTRIBUTES)) { | ||
const attr = POINT_ATTTRIBUTES[potreeName]; | ||
attr.potreeName = potreeName; | ||
attr.numByte = attr.numByte || attr.arrayType.BYTES_PER_ELEMENT; | ||
attr.byteSize = attr.numElements * attr.numByte; | ||
attr.normalized = attr.normalized || false; | ||
// chrome is known to perform badly when we call a method without respecting its arity | ||
const fnName = `getUint${attr.numByte * 8}`; | ||
attr.getValue = attr.numByte === 1 ? | ||
function getValue(view, offset) { return view[fnName](offset); } : | ||
function getValue(view, offset) { return view[fnName](offset, true); }; | ||
} | ||
|
||
export default { | ||
/** @module PotreeBinParser */ | ||
/** Parse .bin PotreeConverter format and convert to a THREE.BufferGeometry | ||
* @function parse | ||
* @param {ArrayBuffer} buffer - the bin buffer. | ||
* @param {Object} pointAttributes - the point attributes information contained in layer.metadata coming from cloud.js | ||
* @return {Promise} - a promise that resolves with a THREE.BufferGeometry. | ||
* | ||
*/ | ||
parse: function parse(buffer) { | ||
parse: function parse(buffer, pointAttributes) { | ||
if (!buffer) { | ||
throw new Error('No array buffer provided.'); | ||
} | ||
|
||
const view = new DataView(buffer); | ||
// Format: X1,Y1,Z1,R1,G1,B1,A1,[...],XN,YN,ZN,RN,GN,BN,AN | ||
const numPoints = Math.floor(buffer.byteLength / 16); | ||
|
||
const positions = new Float32Array(3 * numPoints); | ||
const colors = new Uint8Array(4 * numPoints); | ||
|
||
const box = new THREE.Box3(); | ||
box.min.set(Infinity, Infinity, Infinity); | ||
box.max.set(-Infinity, -Infinity, -Infinity); | ||
const tmp = new THREE.Vector3(); | ||
|
||
let offset = 0; | ||
for (let i = 0; i < numPoints; i++) { | ||
positions[3 * i] = view.getUint32(offset + 0, true); | ||
positions[3 * i + 1] = view.getUint32(offset + 4, true); | ||
positions[3 * i + 2] = view.getUint32(offset + 8, true); | ||
|
||
tmp.fromArray(positions, 3 * i); | ||
box.min.min(tmp); | ||
box.max.max(tmp); | ||
|
||
colors[4 * i] = view.getUint8(offset + 12); | ||
colors[4 * i + 1] = view.getUint8(offset + 13); | ||
colors[4 * i + 2] = view.getUint8(offset + 14); | ||
colors[4 * i + 3] = 255; | ||
|
||
offset += 16; | ||
let pointByteSize = 0; | ||
for (const potreeName of pointAttributes) { | ||
pointByteSize += POINT_ATTTRIBUTES[potreeName].byteSize; | ||
} | ||
const numPoints = Math.floor(buffer.byteLength / pointByteSize); | ||
|
||
const geometry = new THREE.BufferGeometry(); | ||
geometry.addAttribute('position', new THREE.BufferAttribute(positions, 3)); | ||
geometry.addAttribute('color', new THREE.BufferAttribute(colors, 4, true)); | ||
geometry.boundingBox = box; | ||
let elemOffset = 0; | ||
let attrOffset = 0; | ||
for (const potreeName of pointAttributes) { | ||
const attr = POINT_ATTTRIBUTES[potreeName]; | ||
const arrayLength = attr.numElements * numPoints; | ||
const array = new attr.arrayType(arrayLength); | ||
for (let arrayOffset = 0; arrayOffset < arrayLength; arrayOffset += attr.numElements) { | ||
for (let elemIdx = 0; elemIdx < attr.numElements; elemIdx++) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if the previous attr loop goes in global scope, the array could be instanced here (just after There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Put some code please, that will be faster and clearer. |
||
array[arrayOffset + elemIdx] = attr.getValue(view, attrOffset + elemIdx * attr.numByte); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. would not it be simpler to use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed. I've checked the implementation, and it is essentially the same as here. It'll add a for loop, but I don't expect its cost to be perceivable. |
||
attrOffset += pointByteSize; | ||
} | ||
elemOffset += attr.byteSize; | ||
attrOffset = elemOffset; | ||
geometry.addAttribute(attr.attributeName, new THREE.BufferAttribute(array, attr.numElements, attr.normalized)); | ||
} | ||
|
||
geometry.computeBoundingBox(); | ||
|
||
return Promise.resolve(geometry); | ||
}, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,7 +3,7 @@ import Fetcher from './Fetcher'; | |
import PointCloudProcessing from '../Process/PointCloudProcessing'; | ||
import PotreeBinParser from '../Parser/PotreeBinParser'; | ||
import PotreeCinParser from '../Parser/PotreeCinParser'; | ||
import PointsMaterial from '../Renderer/PointsMaterial'; | ||
import PointsMaterial, { MODE } from '../Renderer/PointsMaterial'; | ||
import Picking from '../Core/Picking'; | ||
import Extent from '../Core/Geographic/Extent'; | ||
|
||
|
@@ -142,6 +142,55 @@ function addPickingAttribute(points) { | |
return points; | ||
} | ||
|
||
function parseMetadata(layer, metadata) { | ||
layer.metadata = metadata; | ||
|
||
let bbox; | ||
var customBinFormat = true; | ||
|
||
// Lopocs pointcloud server can expose the same file structure as PotreeConverter output. | ||
// The only difference is the metadata root file (cloud.js vs infos/sources), and we can | ||
// check for the existence of a `scale` field. | ||
// (if `scale` is defined => we're fetching files from PotreeConverter) | ||
if (layer.metadata.scale != undefined) { | ||
// PotreeConverter format | ||
customBinFormat = layer.metadata.pointAttributes === 'CIN'; | ||
bbox = new THREE.Box3( | ||
new THREE.Vector3(metadata.boundingBox.lx, metadata.boundingBox.ly, metadata.boundingBox.lz), | ||
new THREE.Vector3(metadata.boundingBox.ux, metadata.boundingBox.uy, metadata.boundingBox.uz)); | ||
|
||
// do we have normal information | ||
const normal = Array.isArray(layer.metadata.pointAttributes) && | ||
layer.metadata.pointAttributes.find(elem => elem.startsWith('NORMAL')); | ||
if (normal) { | ||
layer.material.defines[normal] = 1; | ||
} | ||
} else { | ||
// Lopocs | ||
layer.metadata.scale = 1; | ||
layer.metadata.octreeDir = `itowns/${layer.table}.points`; | ||
layer.metadata.hierarchyStepSize = 1000000; // ignore this with lopocs | ||
customBinFormat = true; | ||
|
||
let idx = 0; | ||
for (const entry of metadata) { | ||
if (entry.table == layer.table) { | ||
break; | ||
} | ||
idx++; | ||
} | ||
bbox = new THREE.Box3( | ||
new THREE.Vector3(metadata[idx].bbox.xmin, metadata[idx].bbox.ymin, metadata[idx].bbox.zmin), | ||
new THREE.Vector3(metadata[idx].bbox.xmax, metadata[idx].bbox.ymax, metadata[idx].bbox.zmax)); | ||
} | ||
|
||
layer.parse = customBinFormat ? PotreeCinParser.parse : PotreeBinParser.parse; | ||
layer.extension = customBinFormat ? 'cin' : 'bin'; | ||
layer.supportsProgressiveDisplay = customBinFormat; | ||
|
||
return bbox; | ||
} | ||
|
||
export default { | ||
preprocessDataLayer(layer, view) { | ||
if (!layer.file) { | ||
|
@@ -169,6 +218,8 @@ export default { | |
layer.type = 'geometry'; | ||
layer.material = layer.material || {}; | ||
layer.material = layer.material.isMaterial ? layer.material : new PointsMaterial(layer.material); | ||
layer.material.defines = layer.material.defines || {}; | ||
layer.mode = MODE.COLOR; | ||
|
||
// default update methods | ||
layer.preUpdate = PointCloudProcessing.preUpdate; | ||
|
@@ -178,58 +229,18 @@ export default { | |
// this probably needs to be moved to somewhere else | ||
layer.pickObjectsAt = (view, mouse, radius) => Picking.pickPointsAt(view, mouse, radius, layer); | ||
|
||
return Fetcher.json(`${layer.url}/${layer.file}`, layer.fetchOptions).then((cloud) => { | ||
layer.metadata = cloud; | ||
|
||
let bbox; | ||
var customBinFormat = true; | ||
|
||
// Lopocs pointcloud server can expose the same file structure as PotreeConverter output. | ||
// The only difference is the metadata root file (cloud.js vs infos/sources), and we can | ||
// check for the existence of a `scale` field. | ||
// (if `scale` is defined => we're fetching files from PotreeConverter) | ||
if (layer.metadata.scale != undefined) { | ||
// PotreeConverter format | ||
customBinFormat = layer.metadata.pointAttributes === 'CIN'; | ||
bbox = new THREE.Box3( | ||
new THREE.Vector3(cloud.boundingBox.lx, cloud.boundingBox.ly, cloud.boundingBox.lz), | ||
new THREE.Vector3(cloud.boundingBox.ux, cloud.boundingBox.uy, cloud.boundingBox.uz)); | ||
} else { | ||
// Lopocs | ||
layer.metadata.scale = 1; | ||
layer.metadata.octreeDir = `itowns/${layer.table}.points`; | ||
layer.metadata.hierarchyStepSize = 1000000; // ignore this with lopocs | ||
customBinFormat = true; | ||
|
||
let idx = 0; | ||
for (const entry of cloud) { | ||
if (entry.table == layer.table) { | ||
break; | ||
} | ||
idx++; | ||
} | ||
bbox = new THREE.Box3( | ||
new THREE.Vector3(cloud[idx].bbox.xmin, cloud[idx].bbox.ymin, cloud[idx].bbox.zmin), | ||
new THREE.Vector3(cloud[idx].bbox.xmax, cloud[idx].bbox.ymax, cloud[idx].bbox.zmax)); | ||
} | ||
|
||
layer.parse = customBinFormat ? PotreeCinParser.parse : PotreeBinParser.parse; | ||
layer.extension = customBinFormat ? 'cin' : 'bin'; | ||
layer.supportsProgressiveDisplay = customBinFormat; | ||
|
||
return parseOctree( | ||
layer, | ||
layer.metadata.hierarchyStepSize, | ||
{ baseurl: `${layer.url}/${cloud.octreeDir}/r`, name: '', bbox }); | ||
}).then((root) => { | ||
// eslint-disable-next-line no-console | ||
console.log('LAYER metadata:', root); | ||
layer.root = root; | ||
root.findChildrenByName = findChildrenByName.bind(root, root); | ||
layer.extent = Extent.fromBox3(view.referenceCrs, root.bbox); | ||
|
||
return layer; | ||
}); | ||
return Fetcher.json(`${layer.url}/${layer.file}`, layer.fetchOptions) | ||
.then(metadata => parseMetadata(layer, metadata)) | ||
.then(bbox => parseOctree(layer, layer.metadata.hierarchyStepSize, { baseurl: `${layer.url}/${layer.metadata.octreeDir}/r`, name: '', bbox })) | ||
.then((root) => { | ||
// eslint-disable-next-line no-console | ||
console.log('LAYER metadata:', root); | ||
layer.root = root; | ||
root.findChildrenByName = findChildrenByName.bind(root, root); | ||
layer.extent = Extent.fromBox3(view.referenceCrs, root.bbox); | ||
|
||
return layer; | ||
}); | ||
}, | ||
|
||
executeCommand(command) { | ||
|
@@ -247,7 +258,7 @@ export default { | |
// when we request .hrc files) | ||
const url = `${node.baseurl}/r${node.name}.${layer.extension}?isleaf=${command.isLeaf ? 1 : 0}`; | ||
|
||
return Fetcher.arrayBuffer(url, layer.fetchOptions).then(layer.parse).then((geometry) => { | ||
return Fetcher.arrayBuffer(url, layer.fetchOptions).then(buffer => layer.parse(buffer, layer.metadata.pointAttributes)).then((geometry) => { | ||
const points = new THREE.Points(geometry, layer.material.clone()); | ||
addPickingAttribute(points); | ||
points.frustumCulled = false; | ||
|
@@ -263,3 +274,7 @@ export default { | |
}); | ||
}, | ||
}; | ||
|
||
export const _testing = { | ||
parseMetadata, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not okay with this, please move it to the default export, even if I understand that this is for testing purpose only. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for your review. Can you elaborate on the reasons you don't like this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To elaborate on my position, I'm pretty against exposing this to the default export, because it's a private method that has no reason to be exposed. Once you expose such a method, it becomes part of the public api, which means we have an implicit commitment to it. I know that this way of thinking is not very common on this project, but I'd like not to introduce api breakage when we can avoid it. For me There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't really know, maybe in fact it is a good idea. We could agree on a special exported namespace we could use to test internal methods. So in fact maybe keep it and find a more suitable name ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Providers are not in the public API so no problems here, but see my change of pov above ;) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Just a quick note on this: I'm afraid they are. Everything importable from a ES6 configuration is part of the public API and we should treat it that way. Every transpiled file in
I don't know, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok I see one possible solution: surround testing exports with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (and then rename There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (sorry for being so undecided) yeah |
||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe you should add a comment to explain why you need this? (AFAIU it's because
Float16Array
doesn't exist so you emulate it)