Skip to content
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

feat(gltf): Add GLBArrowLoader #3160

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added examples/website/pointcloud/DragonGeom.glb
Binary file not shown.
6 changes: 4 additions & 2 deletions examples/website/pointcloud/app-arrow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {Mesh} from '@loaders.gl/schema';
import {convertTableToMesh} from '@loaders.gl/schema-utils';

import {DracoArrowLoader} from '@loaders.gl/draco';
import {GLBArrowLoader} from '@loaders.gl/gltf';
import {LASArrowLoader} from '@loaders.gl/las';
import {PLYArrowLoader} from '@loaders.gl/ply';
import {PCDArrowLoader} from '@loaders.gl/pcd';
Expand All @@ -24,7 +25,7 @@ import {ExamplePanel, Example, MetadataViewer} from './components/example-panel'
import {EXAMPLES} from './examples';

// Additional format support can be added here, see
const POINT_CLOUD_LOADERS = [DracoArrowLoader, LASArrowLoader, PLYArrowLoader, PCDArrowLoader, OBJArrowLoader];
const POINT_CLOUD_LOADERS = [DracoArrowLoader, GLBArrowLoader, LASArrowLoader, PLYArrowLoader, PCDArrowLoader, OBJArrowLoader];

const INITIAL_VIEW_STATE = {
target: [0, 0, 0],
Expand Down Expand Up @@ -157,7 +158,8 @@ export default function App(props: AppProps = {}) {

const {url} = example;
try {
const arrowTable = await load(url, POINT_CLOUD_LOADERS);
let arrowTable = await load(url, POINT_CLOUD_LOADERS);
arrowTable = Array.isArray(arrowTable) ? arrowTable[0][0] : arrowTable; // TODO: Support transforms?
const pointCloud = convertTableToMesh(arrowTable);
const {schema, header, loaderData, attributes} = pointCloud;

Expand Down
11 changes: 11 additions & 0 deletions examples/website/pointcloud/examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ const DECK_DATA_URI = 'https://raw.githubusercontent.com/visgl/deck.gl-data/mast
const LOADERS_URI = 'https://raw.githubusercontent.com/visgl/loaders.gl/master';

export const EXAMPLES: Record<string, Record<string, Example>> = {
GLTF: {
DragonGeom: {
type: 'glb',
url: `./DragonGeom.glb`
},
DamagedHelmet: {
type: 'glb',
url: `${LOADERS_URI}/modules/gltf/test/data/glb/DamagedHelmet.glb`
}
},

PLY: {
'Richmond Azaelias': {
type: 'ply',
Expand Down
2 changes: 1 addition & 1 deletion examples/website/pointcloud/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<div id="app"></div>
</body>
<script type="module">
import {renderToDOM} from './app.tsx';
import {renderToDOM} from './app-arrow.tsx';
renderToDOM(document.getElementById('app'));
</script>
</html>
17 changes: 11 additions & 6 deletions examples/website/pointcloud/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@
"@deck.gl/core": "^9.0.1",
"@deck.gl/layers": "^9.0.1",
"@deck.gl/react": "^9.0.1",
"@loaders.gl/core": "4.4.0-alpha.0",
"@loaders.gl/draco": "4.4.0-alpha.0",
"@loaders.gl/las": "4.4.0-alpha.0",
"@loaders.gl/obj": "4.4.0-alpha.0",
"@loaders.gl/pcd": "4.4.0-alpha.0",
"@loaders.gl/ply": "4.4.0-alpha.0",
"@loaders.gl/core": "portal:../../../modules/core",
"@loaders.gl/draco": "portal:../../../modules/draco",
"@loaders.gl/gltf": "portal:../../../modules/gltf",
"@loaders.gl/las": "portal:../../../modules/las",
"@loaders.gl/obj": "portal:../../../modules/obj",
"@loaders.gl/pcd": "portal:../../../modules/pcd",
"@loaders.gl/ply": "portal:../../../modules/ply",
"@monaco-editor/react": "^4.5.0",
"prop-types": "^15.7.2",
"react": "^16.3.0",
Expand All @@ -28,5 +29,9 @@
"devDependencies": {
"typescript": "^5.3.0",
"vite": "^5.0.0"
},
"volta": {
"node": "20.18.0",
"yarn": "4.4.1"
}
}
10 changes: 6 additions & 4 deletions modules/gltf/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,17 @@
"build-bundle-dev": "ocular-bundle ./bundle.ts --env=dev --output=dist/dist.dev.js"
},
"dependencies": {
"@gltf-transform/core": "^4.1.0",
"@gltf-transform/extensions": "^4.1.0",
"@gltf-transform/functions": "^4.1.0",
"@loaders.gl/draco": "4.4.0-alpha.0",
"@loaders.gl/images": "4.4.0-alpha.0",
"@loaders.gl/loader-utils": "4.4.0-alpha.0",
"@loaders.gl/schema": "4.4.0-alpha.0",
"@loaders.gl/schema-utils": "4.4.0-alpha.0",
"@loaders.gl/textures": "4.4.0-alpha.0",
"@math.gl/core": "^4.1.0"
},
"peerDependencies": {
"@loaders.gl/core": "4.4.0-alpha.0"
"@math.gl/core": "^4.1.0",
"apache-arrow": ">= 15.0.0"
},
"gitHead": "3213679d79e6ff2814d48fd3337acfa446c74099"
}
135 changes: 135 additions & 0 deletions modules/gltf/src/glb-arrow-loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import {Matrix4} from '@math.gl/core';
import {Accessor, Node, PlatformIO, Primitive, WebIO} from '@gltf-transform/core';
import {KHRONOS_EXTENSIONS} from '@gltf-transform/extensions';
import {unweld, uninstance, dequantize} from '@gltf-transform/functions';
import * as arrow from 'apache-arrow';
import type {LoaderWithParser, LoaderOptions} from '@loaders.gl/loader-utils';
import {ArrowTable, DataType, Field, Schema, SchemaMetadata} from '@loaders.gl/schema';
import {deserializeArrowField, deserializeArrowType} from '@loaders.gl/schema-utils';
import {GLBLoader} from './glb-loader';

/** GLB Arrow loader options */
export type GLBArrowLoaderOptions = LoaderOptions & {
io?: PlatformIO;
};
Comment on lines +12 to +14
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the application is running in a non-browser environment (Node, Deno) or requires other dependencies (Draco, Meshopt) then a custom I/O class should be provided. Example:

import { NodeIO } from '@gltf-transform/core';
import { KHRONOS_EXTENSIONS } from '@gltf-transform/extensions';
import draco3d from 'draco3dgltf';

const io = new NodeIO()
  .registerExtensions(KHRONOS_EXTENSIONS)
  .registerDependencies({
    'draco3d.decoder': await draco3d.createDecoderModule()
  });

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Loaders.gl has a bunch of abstractions like ReadableFile etc that have implementations that work under Node, Browser, HTTPs etc. This seems to duplicate some of the IO class responsibilities.

I wonder if we could create a LoadersIO custom IO class for gltf-transform on top of the loaders.gl abstractions that glued the two libraries together, so that your gltf-transform would work with the abstractions we typically use in loaders.

That way this gltf-transform based loader wouldn't become a one-off loader that doesn't fully work like other loaders do.


export type ArrowTableTransformList = [ArrowTable, Matrix4][];
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBD what the output should ideally be here?


/**
* GLB Loader -
* GLB is the binary container format for GLTF
*/
export const GLBArrowLoader = {
...GLBLoader,
dataType: null as unknown as ArrowTableTransformList,
batchType: null as never,
worker: false,
parse,
parseSync: undefined
} as const satisfies LoaderWithParser<ArrowTableTransformList, never, GLBArrowLoaderOptions>;

async function parse(
arrayBuffer: ArrayBuffer,
options?: GLBArrowLoaderOptions
): Promise<ArrowTableTransformList> {
const io = options?.io || new WebIO().registerExtensions(KHRONOS_EXTENSIONS);
const document = await io.readBinary(new Uint8Array(arrayBuffer));

// Unclear how represent indexed, instanced, or normalized meshes as
// ArrowTable. Convert to simpler representations for now.
await document.transform(unweld(), uninstance(), dequantize());
Comment on lines +38 to +40
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pre-processing to ensure the vertex buffers can be represented as Arrow tables. Possibly quantized meshes (int8 or int16 vertex attributes) could be represented, but we'd need to put the 'normalized: boolean' option somewhere.

Similarly there could be ways to represent instanced draws with a second Arrow table for the instance transforms, if we want to go that direction.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thoughts about indexed tables was to add an index column as a List<Uint32> and put all the indexes in the first row. We'd need to store an offset to the end of the index list in every other row (in case we had such rows).

const indexes = new Uint32Array([0, 1, 2, 3, 4, 5]);
const nextIndex = indexes.length;
const indexOffsets = new Uint32Array(5).fill(nextIndex);

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can add metadata on every column (field) has a metadata: Map<string, string> and we can even add a mesharrow: JSON.stringify(...) key there.


const scene = document.getRoot().getDefaultScene() || document.getRoot().listScenes()[0];
const meshList: ArrowTableTransformList = [];

// Traverse the default scene, creating a list of mesh primitives and their
// corresponding scene transforms.
scene.traverse((node: Node) => {
if (node.getMesh()) {
const matrix = new Matrix4(node.getWorldMatrix());
for (const prim of node.getMesh()!.listPrimitives()) {
meshList.push([convertPrimitiveToArrowTable(prim), matrix]);
}
}
});

return meshList;
}

/**
* Encodes a glTF Transform Primitive as an ArrowTable. Currently ignores
* materials, morph targets, and extras.
*/
function convertPrimitiveToArrowTable(prim: Primitive): ArrowTable {
const fields: Field[] = [];
const arrowFields: arrow.Field[] = [];
const arrowAttributes: arrow.Data[] = [];

let vertexCount = -1;

for (const name of prim.listSemantics()) {
const attribute = prim.getAttribute(name)!;
const type = componentTypeToDataType(attribute.getComponentType());

const field: Field = {name, type};
const arrowField = deserializeArrowField(field);
const arrowAttribute = accessorToArrowListData(attribute);

fields.push(field);
arrowFields.push(arrowField);
arrowAttributes.push(arrowAttribute);

if (vertexCount <= 0) {
vertexCount = attribute.getCount();
}
}

const metadata: SchemaMetadata = {};
const schema: Schema = {fields, metadata};

const arrowSchema = new arrow.Schema(arrowFields);
const arrowStruct = new arrow.Struct(arrowFields);
const arrowData = new arrow.Data(arrowStruct, 0, vertexCount, 0, undefined, arrowAttributes);
const arrowRecordBatch = new arrow.RecordBatch(arrowSchema, arrowData);
const arrowTable = new arrow.Table([arrowRecordBatch]);
Comment on lines +90 to +94
Copy link
Collaborator Author

@donmccurdy donmccurdy Nov 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we happen to know that a scene contains ~100 mesh primitives that are similar1, possibly they could be returned as a single Table, with a separate table of transforms?

Footnotes

  1. Where 'similar' might mean ... sharing a material? ... same vertex attribute types? ... same node transform?


return {shape: 'arrow-table', schema, data: arrowTable};
}

/** Encodes a glTF component type as an equivalent DataType string. */
function componentTypeToDataType(componentType: number): DataType {
switch (componentType) {
case Accessor.ComponentType.FLOAT:
return 'float32';
case Accessor.ComponentType.UNSIGNED_BYTE:
return 'uint8';
case Accessor.ComponentType.UNSIGNED_SHORT:
return 'uint16';
case Accessor.ComponentType.UNSIGNED_INT:
return 'uint32';
case Accessor.ComponentType.BYTE:
return 'int8';
case Accessor.ComponentType.SHORT:
return 'int16';
case Accessor.ComponentType.INT:
return 'int32';
default:
throw new Error(`Unexpected component type, ${componentType}`);
}
}

/** Encodes a glTF Transform Accessor as an arrow.Data list. */
function accessorToArrowListData(accessor: Accessor): arrow.Data<arrow.FixedSizeList> {
const size = accessor.getElementSize();
const count = accessor.getCount();
const type = componentTypeToDataType(accessor.getComponentType());
const arrowType = deserializeArrowType(type);
const arrowList = new arrow.FixedSizeList(size, new arrow.Field('value', arrowType));
const arrowNestedType = arrowList.children[0].type; // TODO: Eh?
const buffers = {[arrow.BufferType.DATA]: accessor.getArray()};
const arrowNestedData = new arrow.Data(arrowNestedType, 0, size * count, 0, buffers);
const arrowData = new arrow.Data<arrow.FixedSizeList>(arrowList, 0, count, 0, undefined, [
arrowNestedData
]);
return arrowData;
}
1 change: 1 addition & 0 deletions modules/gltf/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export {GLTFWriter} from './gltf-writer';

// GLB Loader & Writer (for custom formats that want to leverage the GLB binary "envelope")
export {GLBLoader} from './glb-loader';
export {GLBArrowLoader} from './glb-arrow-loader';
export {GLBWriter} from './glb-writer';

// glTF Data Access Helper Class
Expand Down
Loading