Skip to content

Instantly share code, notes, and snippets.

@wallabyway
Created June 30, 2022 04:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wallabyway/d022f97191599c5d9dde4827aecec1e5 to your computer and use it in GitHub Desktop.
Save wallabyway/d022f97191599c5d9dde4827aecec1e5 to your computer and use it in GitHub Desktop.
convert 3D-Tiles v1 to v1.1 (3D-Tiles-Next) for point clouds
// PURPOSE: convert draco-pnts to glb (ie. 3dTiles v1.0 to v1.1)
// INSTALL: npm install draco3d gltfpack
// RUN:
// > node infolderPNTS outfolderGLB
import fs from 'fs';
import draco3d from 'draco3d';
import gltfpack from 'gltfpack';
const inFolder = process.argv.slice(2)[0] || 'infolder';
const outFolder = process.argv.slice(2)[1] || 'outfolder';
const uris = [];
function recurveNode(node) {
uris.push(node.content.uri);
for (var n in node.children) {
recurveNode(node.children[n]);
};
return node;
}
// https://github.com/Nicktho/batch-promises/blob/3b73b218b165b974d116b2cc5a7720895af489db/index.js#L4
async function batchPromises(batchSize, collection, callback) {
const arr = await Promise.resolve(collection);
return arr
.map((_, i) => (i % batchSize ? [] : arr.slice(i, i + batchSize)))
.map((group) => (res) => Promise.all(group.map(callback)).then((r) => res.concat(r)))
.reduce((chain, work) => chain.then(work), Promise.resolve([]));
}
let decoderModule = null;
draco3d.createDecoderModule({}).then(async function (module) {
decoderModule = module;
console.log('Draco decoder Module Initialized!');
//main
const szTileset = fs.readFileSync(`${inFolder}/tileset.json`, 'utf8');
recurveNode(JSON.parse(szTileset).root);
fs.promises.mkdir(outFolder, { recursive: true }); // make destination folder, if not exist
updateTilesetFile(szTileset,outFolder);
await convertPNTS(uris, inFolder, outFolder);
console.log('done');
});
function decodeDracoData(rawBuffer, decoder) {
const buffer = new decoderModule.DecoderBuffer();
buffer.Init(new Int8Array(rawBuffer), rawBuffer.byteLength);
const geometryType = decoder.GetEncodedGeometryType(buffer);
const dracoGeometry = new decoderModule.PointCloud();
const status = decoder.DecodeBufferToPointCloud(buffer, dracoGeometry);
return dracoGeometry;
}
function indexOfDraco(data) {
const sz = new TextDecoder('ascii').decode(data.slice(0, 500));
return sz.indexOf("DRACO");
}
async function convertPNTS(filenames, infolder, outfolder) {
const decoder = new decoderModule.Decoder();
await batchPromises(4, filenames, convertFile);
await batchPromises(4, filenames, compressToGLB);
async function convertFile(filename) {
const data = fs.readFileSync(`${infolder}/${filename}`);
const decodedGeometry = decodeDracoData(data.slice(indexOfDraco(data)), decoder);
var numPoints = decodedGeometry.num_points();
console.log(`Decoding ${filename}, size ${data.byteLength}, points:${numPoints}`);
const numValues = encodeGLTF(decodedGeometry);
function encodeGLTF(buffer) {
const attrs = { POSITION: 3, COLOR: 3 };
let numValues = 0;
Object.keys(attrs).forEach((attr) => {
numValues = numPoints * 3;
const decoderAttr = decoderModule[attr];
const attrId = decoder.GetAttributeId(buffer, decoderAttr);
if (attrId < 0) return;
const attribute = decoder.GetAttribute(buffer, attrId);
const attributeData = new decoderModule.DracoFloat32Array();
decoder.GetAttributeFloatForAllPoints(buffer, attribute, attributeData);
const attributeDataArray = new Float32Array(numValues);
for (let i = 0; i < numValues; ++i) {
const div = (attr == "COLOR") ? 256 : 1;
attributeDataArray[i] = attributeData.GetValue(i) / div;
}
decoderModule.destroy(attributeData);
// write gltf/bin files
fs.writeFileSync(`${outfolder}/${filename}-${attr}.bin`, attributeDataArray);
});
return numValues;
}
decoderModule.destroy(decodedGeometry);
const szgltf = getglTFString(`${filename}-POSITION`, `${filename}-COLOR`, numValues / 3);
fs.writeFileSync(`${outfolder}/${filename}.gltf`, szgltf);
};
async function compressToGLB(filename) {
console.log(`Compressing ${filename}.gltf to GLB`);
await gltfpack.pack(['-cc', '-i', `${outfolder}/${filename}.gltf`, '-o', `${outfolder}/${filename.slice(0,-5)}.glb`], { read: fs.readFileSync, write: fs.writeFileSync});
fs.rmSync(`${outfolder}/${filename}.gltf`);
fs.rmSync(`${outfolder}/${filename}-POSITION.bin`);
fs.rmSync(`${outfolder}/${filename}-COLOR.bin`);
}
decoderModule.destroy(decoder);
}
function updateTilesetFile(szjson, outfolder) {
const szresult = szjson.replaceAll('pnts','glb');
fs.writeFileSync(`${outfolder}/tileset.json`, szresult);
console.log("updated tileset.json");
}
function getglTFString(filenamePOS, filenameRGB, count) {
return `
{
"asset": {
"version": "2.0"
},
"scene": 0,
"scenes": [
{
"nodes": [
0
]
}
],
"accessors": [
{
"bufferView": 0,
"componentType": 5126,
"count": ${count},
"max": [
1,
1,
1
],
"min": [
0,
0,
0
],
"type": "VEC3"
},
{
"bufferView": 1,
"componentType": 5126,
"count": ${count},
"max": [
1,
1,
1
],
"min": [
0,
0,
0
],
"type": "VEC3"
}
],
"bufferViews": [
{
"buffer": 0,
"byteLength": ${count * 3 * 4},
"byteOffset": 0,
"byteStride": 12,
"target": 34962
},
{
"buffer": 1,
"byteLength": ${count * 3 * 4},
"byteOffset": 0,
"byteStride": 12,
"target": 34962
}
],
"buffers": [
{
"byteLength": ${count * 3 * 4},
"uri": "${filenamePOS}.bin"
},
{
"byteLength": ${count * 3 * 4},
"uri": "${filenameRGB}.bin"
}
],
"extensionsUsed": [
"KHR_materials_unlit"
],
"materials": [
{
"extensions": {
"KHR_materials_unlit": {}
}
}
],
"meshes": [
{
"primitives": [
{
"attributes": {
"COLOR_0": 1,
"POSITION": 0
},
"material": 0,
"mode": 0
}
]
}
],
"nodes": [
{
"children": [
1
],
"name": "RootNode (gltf orientation matrix)",
"rotation": [
-0.70710678118654746,
-0,
-0,
0.70710678118654757
]
},
{
"mesh": 0,
"name": ""
}
]
}
`
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment