diff --git a/examples/website/pointcloud/DragonGeom.glb b/examples/website/pointcloud/DragonGeom.glb new file mode 100644 index 0000000000..37fd965b63 Binary files /dev/null and b/examples/website/pointcloud/DragonGeom.glb differ diff --git a/examples/website/pointcloud/app-arrow.tsx b/examples/website/pointcloud/app-arrow.tsx index b56093a993..2228b3c339 100644 --- a/examples/website/pointcloud/app-arrow.tsx +++ b/examples/website/pointcloud/app-arrow.tsx @@ -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'; @@ -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], @@ -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; diff --git a/examples/website/pointcloud/examples.ts b/examples/website/pointcloud/examples.ts index 28eb980eef..619e7f9269 100644 --- a/examples/website/pointcloud/examples.ts +++ b/examples/website/pointcloud/examples.ts @@ -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> = { + 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', diff --git a/examples/website/pointcloud/index.html b/examples/website/pointcloud/index.html index db08107ddf..d1c83fa3f1 100644 --- a/examples/website/pointcloud/index.html +++ b/examples/website/pointcloud/index.html @@ -23,7 +23,7 @@
diff --git a/examples/website/pointcloud/package.json b/examples/website/pointcloud/package.json index bc54ae04cb..c75717d395 100644 --- a/examples/website/pointcloud/package.json +++ b/examples/website/pointcloud/package.json @@ -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", @@ -28,5 +29,9 @@ "devDependencies": { "typescript": "^5.3.0", "vite": "^5.0.0" + }, + "volta": { + "node": "20.18.0", + "yarn": "4.4.1" } } diff --git a/modules/gltf/package.json b/modules/gltf/package.json index ec3302c180..2d1e16df08 100644 --- a/modules/gltf/package.json +++ b/modules/gltf/package.json @@ -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" } diff --git a/modules/gltf/src/glb-arrow-loader.ts b/modules/gltf/src/glb-arrow-loader.ts new file mode 100644 index 0000000000..a4026bed31 --- /dev/null +++ b/modules/gltf/src/glb-arrow-loader.ts @@ -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; +}; + +export type ArrowTableTransformList = [ArrowTable, Matrix4][]; + +/** + * 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; + +async function parse( + arrayBuffer: ArrayBuffer, + options?: GLBArrowLoaderOptions +): Promise { + 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()); + + 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]); + + 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 { + 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(arrowList, 0, count, 0, undefined, [ + arrowNestedData + ]); + return arrowData; +} diff --git a/modules/gltf/src/index.ts b/modules/gltf/src/index.ts index e14ce53a34..c2486c38c2 100644 --- a/modules/gltf/src/index.ts +++ b/modules/gltf/src/index.ts @@ -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