diff --git a/sandbox/modelLoad/CesiumMilkTruck.glb b/sandbox/modelLoad/CesiumMilkTruck.glb new file mode 100644 index 00000000..2f1a5b83 Binary files /dev/null and b/sandbox/modelLoad/CesiumMilkTruck.glb differ diff --git a/sandbox/modelLoad/dracoLoader.html b/sandbox/modelLoad/dracoLoader.html new file mode 100644 index 00000000..483cd943 --- /dev/null +++ b/sandbox/modelLoad/dracoLoader.html @@ -0,0 +1,25 @@ + + + Draco loader sample + + + + + + + + + +
+ +
+ + diff --git a/sandbox/modelLoad/dracoLoader.js b/sandbox/modelLoad/dracoLoader.js new file mode 100644 index 00000000..37e8cc41 --- /dev/null +++ b/sandbox/modelLoad/dracoLoader.js @@ -0,0 +1,253 @@ +/* eslint-disable no-undef */ +import { + Globe, + control, + Vector, + LonLat, + Entity, + OpenStreetMap, + EmptyTerrain, + RgbTerrain, + GlobusRgbTerrain, + Object3d, + mercator, + Bing, + GeoVideo, + XYZ, + utils, + PlanetCamera, + Framebuffer, + input, + Program, + Vec4, + Vec2, + GeoImage, + Renderer, + Vec3, + Mat4, + RenderNode, + EntityCollection, + scene, + Gltf +} from "../../lib/og.es.js"; + +let renderer = new Renderer("frame", { + msaa: 8, + controls: [new control.SimpleNavigation({ speed: 0.01 }), new control.GeoObjectEditor()], + autoActivate: true +}); + +class MyScene extends RenderNode { + constructor() { + super("MyScene"); + } + + init() { + const baseObj = Object3d.createCube(0.4, 2, 0.4).translate(new Vec3(0, 1, 0)).setMaterial({ + ambient: "#882a2a", + diffuse: "#fb3434", + shininess: 1 + }); + + const frustumObj = Object3d.createFrustum(3, 2, 1).setMaterial({ + ambient: "#236028", + diffuse: "#1cdd23", + shininess: 1 + }); + + const cylinderObj = Object3d.createCylinder(1, 0, 1) + .applyMat4(new Mat4().setRotation(new Vec3(1, 0, 0), (90 * Math.PI) / 180)) + .setMaterial({ + ambient: "#773381", + diffuse: "#ef00ff", + shininess: 1 + }); + + let parentEntity = new Entity({ + cartesian: new Vec3(0, 0, 0), + independentPicking: true + // geoObject: { + // color: "rgb(90,90,90)", + // scale: 1, + // instanced: true, + // tag: `baseObj`, + // object3d: baseObj + // } + }); + + let childEntity = new Entity({ + cartesian: new Vec3(0, 1, 0), + independentPicking: true, + relativePosition: true, + geoObject: { + color: "rgb(90,90,90)", + instanced: true, + tag: `frustumObj`, + object3d: frustumObj + } + }); + + let childChildEntity = new Entity({ + cartesian: new Vec3(0, 3, -1), + independentPicking: true, + relativePosition: true, + geoObject: { + color: "rgb(90,90,90)", + instanced: true, + tag: `cylinderObj`, + object3d: cylinderObj + } + }); + + childEntity.appendChild(childChildEntity); + parentEntity.appendChild(childEntity); + + let collection = new EntityCollection({ + entities: [parentEntity] + }); + + collection.addTo(this); + + this.renderer.activeCamera.set(new Vec3(-4, 21, 23), new Vec3(1, 0, 0)); + + this.renderer.activeCamera.update(); + DracoDecoderModule().then((decoderModule) => { + Gltf.connectDracoDecoderModule(decoderModule); + Gltf.loadGlb("./maxwell_the_cat.glb").then((gltf) => { + // const models = gltf.getObjects3d(); + const entities = gltf.toEntities(); + console.log("entities", entities); + const cat = entities[0]; + cat.setScale(0.1); + childChildEntity.appendChild(entities[0]); + // childChildEntity.appendChild( + // new Entity({ + // cartesian: new Vec3(0, 3, -1), + // independentPicking: true, + // relativePosition: true, + // geoObject: { + // color: "rgb(90,90,90)", + // instanced: true, + // tag: `circleObj`, + // object3d: models[0] + // } + // }) + // ); + this.renderer.activeCamera.update(); + }); + }); + + // Gltf.loadGlb("./CesiumMilkTruck.glb").then((gltf) => { + // const entity = gltf.toEntities()[0]; + // entity.setScale(1); + // console.log('truck entity', entity); + // childChildEntity.appendChild(entity); + // this.renderer.activeCamera.update(); + // }); + } +} + +renderer.addNodes([new scene.Axes(), new MyScene()]); + +async function loadGLB(url) { + const response = await fetch(url); + const buffer = response.arrayBuffer(); + return await buffer; +} + +function parseGLB(arrayBuffer) { + const dv = new DataView(arrayBuffer); + const magic = dv.getUint32(0, true); + if (magic !== 0x46546c67) throw new Error("Not a valid GLB"); + + const jsonChunkLength = dv.getUint32(12, true); + const jsonChunkStart = 20; + const jsonChunk = new TextDecoder().decode( + new Uint8Array(arrayBuffer, jsonChunkStart, jsonChunkLength) + ); + const gltf = JSON.parse(jsonChunk); + const chunks = []; + const binChunkStart = 20 + jsonChunkLength; + for (let i = 0; i < gltf.bufferViews.length; i++) { + const bufferView = gltf.bufferViews[i]; + const offset = i + 1; + const start = binChunkStart + 8 * offset + bufferView.byteOffset; + chunks.push(arrayBuffer.slice(start, start + bufferView.byteLength)); + } + + return { gltf, chunks }; +} + +function getDracoCompressedAccessor(gltf) { + for (const mesh of gltf.meshes) { + for (const primitive of mesh.primitives) { + if (primitive.extensions && primitive.extensions["KHR_draco_mesh_compression"]) { + return primitive.extensions["KHR_draco_mesh_compression"]; + } + } + } + return null; +} + +function decodeDraco(decoderModule, binChunk, gltf) { + console.log(gltf); + const byteOffset = 0; // optional: read from bufferView + const dracoBufferView = new Uint8Array(binChunk, byteOffset); // if bufferView.byteOffset is available, use it + const decoder = new decoderModule.Decoder(); + const buffer = new decoderModule.DecoderBuffer(); + buffer.Init(dracoBufferView, dracoBufferView.byteLength); + + const geometryType = decoder.GetEncodedGeometryType(buffer); + if (geometryType !== decoderModule.TRIANGULAR_MESH) { + throw new Error("Not a mesh"); + } + + const mesh = new decoderModule.Mesh(); + const status = decoder.DecodeBufferToMesh(buffer, mesh); + if (!status.ok() || mesh.ptr === 0) { + console.log(status.error_msg()); + throw new Error("Failed to decode Draco mesh"); + } + + const posAttrId = decoder.GetAttributeId(mesh, decoderModule.POSITION); + const posAttr = decoder.GetAttribute(mesh, posAttrId); + const numPoints = mesh.num_points(); + + const pos = new decoderModule.DracoFloat32Array(); + decoder.GetAttributeFloatForAllPoints(mesh, posAttr, pos); + + const positions = new Float32Array(numPoints * 3); + for (let i = 0; i < positions.length; i++) { + positions[i] = pos.GetValue(i); + } + + decoderModule.destroy(pos); + decoderModule.destroy(mesh); + decoderModule.destroy(decoder); + decoderModule.destroy(buffer); + + return positions; +} + +const test = async () => { + // const arrayBuffer = await loadGLB("./CesiumMilkTruck.glb"); + // const { gltf, chunks } = parseGLB(arrayBuffer); + // // const dracoExt = getDracoCompressedAccessor(gltf); + // console.log(gltf, chunks); + // // eslint-disable-next-line no-undef + // const decoderModule = await DracoDecoderModule(); + // await decoderModule.ready; + // const positions = decodeDraco(decoderModule, chunks[2], gltf); + // console.log(positions); + // const buffer = new decoderModule.DecoderBuffer(); + // buffer.Init(cat, cat.length); + // const decoder = new decoderModule.Decoder(); + // const geometryType = decoder.GetEncodedGeometryType(buffer); + // console.log(decoderModule, geometryType); + // const gltf = await Gltf.loadGlb("./CesiumMilkTruck.glb"); + // const objects = gltf.getObjects3d(); + // console.log(objects); + // await Gltf.loadGlb("./maxwell_the_cat.glb"); +}; + +test(); diff --git a/sandbox/modelLoad/maxwell_the_cat.glb b/sandbox/modelLoad/maxwell_the_cat.glb new file mode 100644 index 00000000..d1cc63d0 Binary files /dev/null and b/sandbox/modelLoad/maxwell_the_cat.glb differ diff --git a/src/Object3d.ts b/src/Object3d.ts index f2dd5156..2d40f2d3 100644 --- a/src/Object3d.ts +++ b/src/Object3d.ts @@ -13,7 +13,7 @@ function getColor(color?: number[] | TypedArray | string): Float32Array { } else if (typeof color === 'string') { return htmlColorToFloat32Array(color); } - return new Float32Array([1.0, 1.0, 1.0, 1.0]); + return new Float32Array([0.5, 0.5, 0.5, 1]); } function getColor3v(color?: NumberArray3 | TypedArray | string): Float32Array { @@ -44,9 +44,12 @@ interface IObject3dParams { diffuse?: string | NumberArray3; specular?: string | NumberArray3; shininess?: number; - colorTexture?: string; - normalTexture?: string; - metallicRoughnessTexture?: string; + colorTextureSrc?: string; + normalTextureSrc?: string; + metallicRoughnessTextureSrc?: string; + colorTextureImage?: HTMLImageElement; + normalTextureImage?: HTMLImageElement; + metallicRoughnessTextureImage?: HTMLImageElement; } type MaterialParams = Pick; @@ -67,13 +70,15 @@ class Object3d { public diffuse: Float32Array; public specular: Float32Array; public shininess: number; - public colorTexture: string; - public normalTexture: string; - public metallicRoughnessTexture: string; + public colorTextureSrc: string | null; + public colorTextureImage: HTMLImageElement | null; + public normalTextureSrc: string | null; + public normalTextureImage: HTMLImageElement | null; + public metallicRoughnessTextureSrc: string | null; + public metallicRoughnessTextureImage: HTMLImageElement | null; public center: Vec3; constructor(data: IObject3dParams = {}) { - this._name = data.name || "noname"; this._vertices = data.vertices || []; this._numVertices = this._vertices.length / 3; @@ -90,9 +95,12 @@ class Object3d { this.diffuse = getColor3v(data.diffuse); this.specular = getColor3v(data.specular); this.shininess = data.shininess || 100; - this.colorTexture = data.colorTexture || ""; - this.normalTexture = data.normalTexture || ""; - this.metallicRoughnessTexture = data.metallicRoughnessTexture || ""; + this.colorTextureSrc = data.colorTextureSrc || null; + this.colorTextureImage = data.colorTextureImage || null; + this.normalTextureSrc = data.normalTextureSrc || null; + this.normalTextureImage = data.normalTextureImage || null; + this.metallicRoughnessTextureSrc = data.metallicRoughnessTextureSrc || null; + this.metallicRoughnessTextureImage = data.metallicRoughnessTextureImage || null; if (data.scale) { let s = data.scale; @@ -629,9 +637,9 @@ class Object3d { specular: mat.specular, shininess: mat.shininess, color: mat.color, - colorTexture: baseUrl ? `${baseUrl}/${mat.colorTexture}` : mat.colorTexture, - normalTexture: baseUrl ? `${baseUrl}/${mat.normalTexture}` : mat.normalTexture, - metallicRoughnessTexture: baseUrl ? `${baseUrl}/${mat.metallicRoughnessTexture}` : mat.metallicRoughnessTexture + colorTextureSrc: baseUrl ? `${baseUrl}/${mat.colorTexture}` : mat.colorTexture, + normalTextureSrc: baseUrl ? `${baseUrl}/${mat.normalTexture}` : mat.normalTexture, + metallicRoughnessTextureSrc: baseUrl ? `${baseUrl}/${mat.metallicRoughnessTexture}` : mat.metallicRoughnessTexture }) } ); @@ -658,9 +666,9 @@ class Object3d { specular: mat.specular, shininess: mat.shininess, color: mat.color, - colorTexture: mat.colorTexture, - normalTexture: mat.normalTexture, - metallicRoughnessTexture: mat.metallicRoughnessTexture + colorTextureSrc: mat.colorTexture, + normalTextureSrc: mat.normalTexture, + metallicRoughnessTextureSrc: mat.metallicRoughnessTexture }) } ); diff --git a/src/entity/GeoObject.ts b/src/entity/GeoObject.ts index fd019963..28293df8 100644 --- a/src/entity/GeoObject.ts +++ b/src/entity/GeoObject.ts @@ -113,7 +113,11 @@ class GeoObject { this._localPosition = new Vec3(); - this._color = utils.createColorRGBA(options.color, new Vec4(0.15, 0.15, 0.15, 1.0)); + this._color = utils.createColorRGBA( + options.color, options.object3d?.color + ? new Vec4(...Array.from(options.object3d.color)) + : new Vec4(0.15, 0.15, 0.15, 1.0) + ); this._handler = null; this._handlerIndex = -1; diff --git a/src/entity/GeoObjectHandler.ts b/src/entity/GeoObjectHandler.ts index 20dfa22b..faaf2c53 100644 --- a/src/entity/GeoObjectHandler.ts +++ b/src/entity/GeoObjectHandler.ts @@ -122,28 +122,49 @@ export class GeoObjectHandler { this.update(); } - public setColorTextureTag(src: string, tag: string) { + public setColorTextureTag(src: string | HTMLImageElement, tag: string) { const tagData = this._instanceDataMap.get(tag); if (tagData) { - tagData._colorTextureSrc = src; + if (typeof src === 'string') { + tagData._colorTextureSrc = src; + tagData._colorTextureImage = null; + } + if (src instanceof HTMLImageElement) { + tagData._colorTextureSrc = null; + tagData._colorTextureImage = src; + } this._instanceDataMap.set(tag, tagData); this._loadColorTexture(tagData); } } - public setNormalTextureTag(src: string, tag: string) { + public setNormalTextureTag(src: string | HTMLImageElement, tag: string) { const tagData = this._instanceDataMap.get(tag); if (tagData) { - tagData._normalTextureSrc = src; + if (typeof src === 'string') { + tagData._normalTextureSrc = src; + tagData._normalTextureImage = null; + } + if (src instanceof HTMLImageElement) { + tagData._normalTextureSrc = null; + tagData._normalTextureImage = src; + } this._instanceDataMap.set(tag, tagData); this._loadNormalTexture(tagData); } } - public setMetallicRoughnessTextureTag(src: string, tag: string) { + public setMetallicRoughnessTextureTag(src: string | HTMLImageElement, tag: string) { const tagData = this._instanceDataMap.get(tag); if (tagData) { - tagData._metallicRoughnessTextureSrc = src; + if (typeof src === 'string') { + tagData._metallicRoughnessTextureSrc = src; + tagData._metallicRoughnessTextureImage = null; + } + if (src instanceof HTMLImageElement) { + tagData._metallicRoughnessTextureSrc = null; + tagData._metallicRoughnessTextureImage = src; + } this._instanceDataMap.set(tag, tagData); this._loadMetallicRoughnessTexture(tagData); } @@ -182,9 +203,13 @@ export class GeoObjectHandler { tagData._changedBuffers[TEXCOORD_BUFFER] = true; } - tagData._colorTextureSrc = object.colorTexture; - tagData._normalTextureSrc = object.normalTexture; - tagData._metallicRoughnessTexture = object.metallicRoughnessTexture; + tagData._colorTextureSrc = object.colorTextureSrc; + tagData._normalTextureSrc = object.normalTextureSrc; + tagData._metallicRoughnessTexture = object.metallicRoughnessTextureSrc; + tagData._colorTextureImage = object.colorTextureImage; + tagData._normalTextureImage = object.normalTextureImage; + tagData._metallicRoughnessTextureImage = object.metallicRoughnessTextureImage; + this._loadColorTexture(tagData); this._loadNormalTexture(tagData); @@ -212,9 +237,12 @@ export class GeoObjectHandler { tagData._indicesArr = geoObject.indices; tagData._texCoordArr = geoObject.texCoords; - tagData._colorTextureSrc = geoObject.object3d.colorTexture; - tagData._normalTextureSrc = geoObject.object3d.normalTexture; - tagData._metallicRoughnessTextureSrc = geoObject.object3d.metallicRoughnessTexture; + tagData._colorTextureSrc = geoObject.object3d.colorTextureSrc; + tagData._normalTextureSrc = geoObject.object3d.normalTextureSrc; + tagData._metallicRoughnessTextureSrc = geoObject.object3d.metallicRoughnessTextureSrc; + tagData._colorTextureImage = geoObject.object3d.colorTextureImage; + tagData._normalTextureImage = geoObject.object3d.normalTextureImage; + tagData._metallicRoughnessTextureImage = geoObject.object3d.metallicRoughnessTextureImage; tagData.setMaterialParams( geoObject.object3d.ambient, @@ -489,23 +517,50 @@ export class GeoObjectHandler { } async _loadColorTexture(tagData: InstanceData) { - if (this._renderer && tagData._colorTextureSrc) { + if (!this._renderer) { + return; + } + if (tagData._colorTextureSrc) { const image = await loadImage(tagData._colorTextureSrc); tagData.createColorTexture(image); + return; + } + if (tagData._colorTextureImage) { + await tagData._colorTextureImage.decode(); + tagData.createColorTexture(tagData._colorTextureImage); + return; } } async _loadNormalTexture(tagData: InstanceData) { - if (this._renderer && tagData._normalTextureSrc) { + if (!this._renderer) { + return; + } + if (tagData._normalTextureSrc) { const image = await loadImage(tagData._normalTextureSrc); tagData.createNormalTexture(image); + return; + } + if (tagData._normalTextureImage) { + await tagData._normalTextureImage.decode(); + tagData.createNormalTexture(tagData._normalTextureImage); + return; } } async _loadMetallicRoughnessTexture(tagData: InstanceData) { - if (this._renderer && tagData._metallicRoughnessTextureSrc) { + if (!this._renderer) { + return; + } + if (tagData._metallicRoughnessTextureSrc) { const image = await loadImage(tagData._metallicRoughnessTextureSrc); tagData.createMetallicRoughnessTexture(image); + return; + } + if (tagData._metallicRoughnessTextureImage) { + await tagData._metallicRoughnessTextureImage.decode(); + tagData.createMetallicRoughnessTexture(tagData._metallicRoughnessTextureImage); + return; } } diff --git a/src/entity/InstanceData.ts b/src/entity/InstanceData.ts index 7c8c7404..339c6d57 100644 --- a/src/entity/InstanceData.ts +++ b/src/entity/InstanceData.ts @@ -46,6 +46,9 @@ export class InstanceData { public _colorTextureSrc: string | null; public _normalTextureSrc: string | null; public _metallicRoughnessTextureSrc: string | null; + public _colorTextureImage: HTMLImageElement | null; + public _normalTextureImage: HTMLImageElement | null; + public _metallicRoughnessTextureImage: HTMLImageElement | null; public _objectSrc?: string; @@ -99,12 +102,15 @@ export class InstanceData { this._colorTexture = null; this._colorTextureSrc = null; + this._colorTextureImage = null; this._normalTexture = null; this._normalTextureSrc = null; + this._normalTextureImage = null; this._metallicRoughnessTexture = null; this._metallicRoughnessTextureSrc = null; + this._metallicRoughnessTextureImage = null; this._sizeArr = []; this._translateArr = []; @@ -413,7 +419,6 @@ export class InstanceData { } this._sizeArr = makeArrayTyped(this._sizeArr); - h.setStreamArrayBuffer(this._sizeBuffer, this._sizeArr as Float32Array); } diff --git a/src/index.ts b/src/index.ts index 299f3fc3..a53b4e93 100644 --- a/src/index.ts +++ b/src/index.ts @@ -86,6 +86,7 @@ import { } from './terrain/index'; import {MoveAxisEntity} from "./control/geoObjectEditor/MoveAxisEntity"; +import { Gltf } from './utils/gltf/gltfParser'; export { bv, @@ -142,6 +143,6 @@ export { EarthQuadTreeStrategy, Wgs84QuadTreeStrategy, Object3d, - + Gltf, MoveAxisEntity, }; \ No newline at end of file diff --git a/src/utils/gltf/glbParser.ts b/src/utils/gltf/glbParser.ts new file mode 100644 index 00000000..b5730f94 --- /dev/null +++ b/src/utils/gltf/glbParser.ts @@ -0,0 +1,87 @@ +import { GltfData, GltfMetadata } from "./types"; + +interface GlbChunk { + length: number; + type: ChunkType; + chunkData: ArrayBuffer; +} + +enum ChunkType { + JSON, + BIN, + INVALID +} + +export class Glb { + public static async load(src: string): Promise { + const response = await fetch(src); + if (!response.ok) { + throw new Error(`Unable to load '${src}'`); + } + + const buffer = await response.arrayBuffer(); + + const dv = new DataView(buffer); + const magic = dv.getUint32(0, true); + + if (magic !== 0x46546c67) { + throw new Error("Not a valid GLB"); + } + const version = dv.getUint32(4, true); + const chunks = this.getChunks(dv); + return this.parseChunks(chunks); + } + + private static getChunks(dv: DataView): GlbChunk[] { + const chunks: GlbChunk[] = []; + let currentOffset = 12; // skip magic, version and total length + do { + const chunk = this.getChunk(dv, currentOffset); + currentOffset += 8 + chunk.length; + chunks.push(chunk); + } while (currentOffset < dv.byteLength); + return chunks; + } + + private static getChunk(dv: DataView, offset: number): GlbChunk { + const length = dv.getUint32(offset, true); + const type = this.getChunkType(dv.getUint32(offset + 4, true)); + const chunkData = dv.buffer.slice(offset + 8, offset + 8 + length); + return { length, type, chunkData }; + } + + private static getChunkType(type: number): ChunkType { + switch (type) { + case 0x4e4f534a: + return ChunkType.JSON; + case 0x004e4942: + return ChunkType.BIN; + default: + return ChunkType.INVALID; + } + } + + private static parseChunks(chunks: GlbChunk[]): GltfData { + const result = { + gltf: null as unknown as GltfMetadata, + bin: [] as ArrayBuffer[], + }; + for (const chunk of chunks) { + switch (chunk.type) { + case ChunkType.JSON: + result.gltf = this.parseJsonChunk(chunk); + break; + case ChunkType.BIN: + result.bin.push(chunk.chunkData); + break; + default: + break; + } + } + return result; + } + + private static parseJsonChunk(chunk: GlbChunk): GltfMetadata { + return JSON.parse(new TextDecoder().decode(chunk.chunkData)); + } +} diff --git a/src/utils/gltf/gltfParser.ts b/src/utils/gltf/gltfParser.ts new file mode 100644 index 00000000..5532c6b5 --- /dev/null +++ b/src/utils/gltf/gltfParser.ts @@ -0,0 +1,442 @@ +import { Entity } from "../../entity"; +import { Vec3 } from "../../math/Vec3"; +import { Object3d } from "../../Object3d"; +import { Glb } from "./glbParser"; +import { + Accessor, + AccessorComponentType, + AccessorDataType, + GltfData, + TextureImage, + Material, + Mesh, + Primitive, + PrimitiveMode, + Texture, + MimeType, + GltfNode, + GltfMesh, + GltfPrimitive +} from "./types"; + +export class Gltf { + private static dracoDecoderModule: any = null; + public static connectDracoDecoderModule(decoder: any): void { + this.dracoDecoderModule = decoder; + } + public static async loadGlb(url: string) { + const data = await Glb.load(url); + if (this.dracoDecoderModule !== null) { + await this.dracoDecoderModule.ready; + } + console.log("load glb", data); + return new Gltf(data); + } + + private materials: Material[] = []; + private images: TextureImage[] = []; + public meshes: Mesh[] = []; + + constructor(private gltf: GltfData) { + if ( + gltf.gltf.extensionsRequired?.includes("KHR_draco_mesh_compression") && + Gltf.dracoDecoderModule === null + ) { + throw new Error("Unable to import GLTF. Draco decoder module is not connected"); + } + this.initImages(); + this.initMaterials(); + this.initMeshes(); + } + + public getObjects3d(): Object3d[] { + return this.meshes + .map((mesh) => mesh.primitives.map((primitive) => Gltf.toObject3d(primitive))) + .flat(); + } + + public toEntities(): Entity[] { + const result: Entity[] = []; + for (const scene of this.gltf.gltf.scenes) { + if (scene === undefined || scene.nodes === undefined) { + return []; + } + for (const node of scene.nodes) { + const nodeData = this.gltf.gltf.nodes[node]; + if (nodeData.mesh === undefined && nodeData.children === undefined) { + continue; + } + result.push(this.nodeToEntity(nodeData)); + } + } + + return result; + } + + private nodeToEntity(node: GltfNode): Entity { + const entity = new Entity({ + name: `node_${node.name}`, + cartesian: new Vec3(0, 0, 0), + relativePosition: true, + }); + let meshEntity: Entity | null = null; + if (node.translation !== undefined) { + entity.relativePosition = true; + entity.setCartesian(node.translation[0], node.translation[1], node.translation[2]); + } + if (node.rotation !== undefined) { + // TODO: implement rotation by quaternion + } + if (node.matrix !== undefined) { + // TODO: implement matrix apply + } + if (node.scale !== undefined) { + entity.setScale3v(new Vec3(node.scale[0], node.scale[1], node.scale[2])); + } + if (node.mesh !== undefined) { + meshEntity = this.meshToEntity(this.meshes[node.mesh]); + entity.appendChild(meshEntity); + } + if (node.children !== undefined) { + for (const child of node.children) { + const childEntity = this.nodeToEntity(this.gltf.gltf.nodes[child]); + if (meshEntity) { + meshEntity.appendChild(childEntity); + } else { + entity.appendChild(childEntity); + } + } + } + return entity; + } + + public meshToEntity(mesh: Mesh): Entity { + const entity = new Entity({ + name: mesh.name, + cartesian: new Vec3(0, 0, 0), + relativePosition: true, + independentPicking: true + }); + mesh.primitives.map((primitive) => { + entity.appendChild( + new Entity({ + name: primitive.name, + relativePosition: true, + geoObject: { + object3d: primitive.object3d, + tag: primitive.name + } + }) + ); + }); + return entity; + } + + private initImages() { + if (!this.gltf.gltf.images) { + return; + } + for (const image of this.gltf.gltf.images) { + this.images.push({ + src: image.uri, + element: this.getImage(image.mimeType, image.bufferView), + mimeType: image.mimeType, + name: image.name + }); + } + } + + private getImage(mimeType?: MimeType, bufferView?: number): HTMLImageElement | undefined { + if (bufferView && mimeType) { + const view = this.gltf.gltf.bufferViews[bufferView]; + const url = URL.createObjectURL( + new Blob( + [ + this.gltf.bin[view.buffer].slice( + view.byteOffset, + view.byteOffset + view.byteLength + ) + ], + { + type: mimeType + } + ) + ); + const img = new Image(); + img.src = url; + return img; + } + } + + private initMaterials() { + if (!this.gltf.gltf.materials) { + return; + } + for (const material of this.gltf.gltf.materials) { + const mat: Material = { + name: material.name, + emissiveFactor: material.emissiveFactor, + alphaMode: material.alphaMode, + alphaCutoff: material.alphaCutoff, + doubleSided: material.doubleSided + }; + if (material.pbrMetallicRoughness) { + if (material.pbrMetallicRoughness.baseColorFactor) { + mat.baseColorFactor = material.pbrMetallicRoughness.baseColorFactor; + } + if (material.pbrMetallicRoughness.baseColorTexture) { + const source = + this.gltf.gltf.textures[ + material.pbrMetallicRoughness.baseColorTexture.index + ].source; + if (source !== undefined) { + mat.baseColorTexture = { + image: this.images[source], + texCoord: material.pbrMetallicRoughness.baseColorTexture.texCoord + }; + } + } + if (material.pbrMetallicRoughness.metallicRoughnessTexture) { + const source = + this.gltf.gltf.textures[ + material.pbrMetallicRoughness.metallicRoughnessTexture.index + ].source; + if (source !== undefined) { + mat.metallicRoughnessTexture = { + image: this.images[source], + texCoord: + material.pbrMetallicRoughness.metallicRoughnessTexture.texCoord + }; + } + } + } + if (material.normalTexture) { + const source = this.gltf.gltf.textures[material.normalTexture.index].source; + if (source !== undefined) { + mat.normalTexture = { + image: this.images[source], + texCoord: material.normalTexture.texCoord, + scale: material.normalTexture.scale + }; + } + } + if (material.occlusionTexture) { + const source = this.gltf.gltf.textures[material.occlusionTexture.index].source; + if (source !== undefined) { + mat.occlusionTexture = { + image: this.images[source], + texCoord: material.occlusionTexture.texCoord, + strength: material.occlusionTexture.strength + }; + } + } + if (material.emissiveTexture) { + const source = this.gltf.gltf.textures[material.emissiveTexture.index].source; + if (source !== undefined) { + mat.emissiveTexture = { + image: this.images[source], + texCoord: material.emissiveTexture.texCoord + }; + } + } + this.materials.push(mat); + } + } + + private initMeshes() { + this.meshes = []; + for (const meshData of this.gltf.gltf.meshes) { + const mesh: Mesh = { + name: meshData.name, + primitives: [] + }; + for (let i = 0; i < meshData.primitives.length; i++) { + mesh.primitives.push(this.buildPrimitive(meshData, meshData.primitives[i], i)); + } + this.meshes.push(mesh); + } + } + + private buildPrimitive( + meshData: GltfMesh, + primitiveData: GltfPrimitive, + index: number = 0 + ): Primitive { + let primitive: Primitive | null = null; + const material = this.materials[primitiveData.material || 0]; + const texcoord = material.baseColorTexture?.texCoord + ? `TEXCOORD_${material.baseColorTexture.texCoord}` + : `TEXCOORD_0`; + if (primitiveData.extensions?.KHR_draco_mesh_compression) { + const dracoExt = primitiveData.extensions.KHR_draco_mesh_compression; + const bufferView = this.gltf.gltf.bufferViews[dracoExt.bufferView]; + const bvOffset = bufferView.byteOffset || 0; + const draco = Gltf.dracoDecoderModule; + const decoder = new draco.Decoder(); + const decoderBuffer = new draco.DecoderBuffer(); + decoderBuffer.Init( + new Uint8Array( + this.gltf.bin[bufferView.buffer].slice( + bvOffset, + bvOffset + bufferView.byteLength + ) + ), + bufferView.byteLength + ); + + const geometryType = decoder.GetEncodedGeometryType(decoderBuffer); + if (geometryType !== draco.TRIANGULAR_MESH) { + throw new Error("Draco compressed data is not a mesh"); + } + + const mesh = new draco.Mesh(); + const status = decoder.DecodeBufferToMesh(decoderBuffer, mesh); + if (!status.ok() || mesh.ptr === 0) { + throw new Error("Failed to decode Draco mesh"); + } + + const numFaces = mesh.num_faces(); + const numIndices = numFaces * 3; + const indices = new Uint32Array(numIndices); + const ia = new draco.DracoInt32Array(); + for (let i = 0; i < numFaces; i++) { + decoder.GetFaceFromMesh(mesh, i, ia); + indices[i * 3] = ia.GetValue(0); + indices[i * 3 + 1] = ia.GetValue(1); + indices[i * 3 + 2] = ia.GetValue(2); + } + draco.destroy(ia); + + const attributes: { [name: string]: Float32Array } = {}; + for (const gltfAttrName in dracoExt.attributes) { + const attrId = dracoExt.attributes[gltfAttrName]; + const dracoAttr = decoder.GetAttributeByUniqueId(mesh, attrId); + const numPoints = mesh.num_points(); + const numComponents = dracoAttr.num_components(); // 3 for POSITION, 2 for UVs, etc. + + const attrArray = new draco.DracoFloat32Array(); + decoder.GetAttributeFloatForAllPoints(mesh, dracoAttr, attrArray); + + const typedArray = new Float32Array(numPoints * numComponents); + for (let i = 0; i < typedArray.length; i++) { + typedArray[i] = attrArray.GetValue(i); + } + draco.destroy(attrArray); + + attributes[gltfAttrName] = typedArray; + } + + // Cleanup + draco.destroy(mesh); + draco.destroy(decoderBuffer); + draco.destroy(decoder); + + primitive = { + name: `${meshData.name}_${material.name}_${index}`, + vertices: attributes.POSITION, + indices: indices, + mode: primitiveData.mode ? primitiveData.mode : PrimitiveMode.triangles, + material: this.materials[primitiveData.material || 0] || undefined, + normals: attributes.NORMAL, + texCoords: undefined + }; + } else { + const texcoordAccessorKey = texcoord ? primitiveData.attributes[texcoord] : undefined; + const texcoordAccessor = texcoordAccessorKey + ? this.gltf.gltf.accessors[texcoordAccessorKey] + : undefined; + primitive = { + name: `${meshData.name}_${material.name}_${index}`, + indices: primitiveData.indices + ? Gltf.access(this.gltf.gltf.accessors[primitiveData.indices], this.gltf) + : undefined, + mode: primitiveData.mode ? primitiveData.mode : PrimitiveMode.triangles, + material: this.materials[primitiveData.material || 0] || undefined, + vertices: Gltf.access( + this.gltf.gltf.accessors[primitiveData.attributes.POSITION], + this.gltf + ), + normals: Gltf.access( + this.gltf.gltf.accessors[primitiveData.attributes.NORMAL], + this.gltf + ), + texCoords: texcoordAccessor ? Gltf.access(texcoordAccessor, this.gltf) : undefined + }; + } + if (primitive === null) { + throw new Error("Unable to build primitive"); + } + primitive.object3d = Gltf.toObject3d(primitive); + return primitive; + } + + private static toObject3d(primitive: Primitive): Object3d { + console.log('building object3d', primitive); + return new Object3d({ + name: primitive.name, + vertices: Array.from(primitive.vertices as Float32Array), + normals: Array.from(primitive.normals as Float32Array), + texCoords: primitive.texCoords + ? Array.from(primitive.texCoords as Float32Array) + : undefined, + indices: Array.from(primitive.indices as Uint8Array), + normalTextureImage: primitive.material?.normalTexture?.image.element, + normalTextureSrc: primitive.material?.normalTexture?.image.src, + colorTextureImage: primitive.material?.baseColorTexture?.image.element, + colorTextureSrc: primitive.material?.baseColorTexture?.image.src, + metallicRoughnessTextureImage: primitive.material?.occlusionTexture?.image.element, + metallicRoughnessTextureSrc: primitive.material?.occlusionTexture?.image.src, + color: primitive.material?.baseColorFactor, + }); + } + + private static access(accessor: Accessor, gltf: GltfData): ArrayBufferLike { + const bufferView = gltf.gltf.bufferViews[accessor.bufferView]; + const arrbuff = gltf.bin[bufferView.buffer]; + const offset = bufferView.byteOffset || 0; + const dv = arrbuff.slice(offset, offset + bufferView.byteLength); + switch (accessor.type) { + case AccessorDataType.scalar: + return this.getTensor(dv, accessor, 1); + case AccessorDataType.vec2: + return this.getTensor(dv, accessor, 2); + case AccessorDataType.vec3: + return this.getTensor(dv, accessor, 3); + case AccessorDataType.vec4: + case AccessorDataType.mat2: + return this.getTensor(dv, accessor, 4); + case AccessorDataType.mat3: + return this.getTensor(dv, accessor, 9); + case AccessorDataType.mat4: + return this.getTensor(dv, accessor, 16); + default: + throw new Error("Unknown accessor type"); + } + } + + private static getTensor( + buffer: ArrayBuffer, + accessor: Accessor, + numOfComponents: number + ): ArrayBufferLike { + if (accessor.componentType === AccessorComponentType.ushort) { + return new Uint16Array(buffer, 0, accessor.count * numOfComponents); + } + if (accessor.componentType === AccessorComponentType.short) { + return new Int16Array(buffer, 0, accessor.count * numOfComponents); + } + if (accessor.componentType === AccessorComponentType.uint) { + return new Uint32Array(buffer, 0, accessor.count * numOfComponents); + } + if (accessor.componentType === AccessorComponentType.float) { + return new Float32Array(buffer, 0, accessor.count * numOfComponents); + } + if (accessor.componentType === AccessorComponentType.ubyte) { + return new Uint8Array(buffer, 0, accessor.count * numOfComponents); + } + if (accessor.componentType === AccessorComponentType.byte) { + return new Int8Array(buffer, 0, accessor.count * numOfComponents); + } + throw new Error("Unknown component type"); + } +} diff --git a/src/utils/gltf/types.ts b/src/utils/gltf/types.ts new file mode 100644 index 00000000..252e1cc3 --- /dev/null +++ b/src/utils/gltf/types.ts @@ -0,0 +1,216 @@ +import { Object3d } from "../../Object3d"; + +export interface GltfData { + bin: ArrayBuffer[]; + gltf: GltfMetadata; +} + +export interface GltfMetadata { + accessors: Accessor[]; + bufferViews: BufferView[]; + meshes: GltfMesh[]; + materials: { + name: string; + pbrMetallicRoughness?: { + baseColorFactor?: number[]; + metallicFactor?: number; + roughnessFactor?: number; + baseColorTexture?: { + index: number; + texCoord?: number; + }; + metallicRoughnessTexture?: { + index: number; + texCoord?: number; + }; + }; + normalTexture?: { + index: number; + texCoord?: number; + scale?: number; + }; + occlusionTexture?: { + index: number; + texCoord?: number; + strength?: number; + }; + emissiveTexture?: { + index: number; + texCoord?: number; + }; + emissiveFactor?: number[]; + alphaMode?: AlphaMode; + alphaCutoff?: number; + doubleSided?: boolean; + }[]; + textures: { + sampler?: number; + source?: number; + name?: string; + }[]; + images?: { + uri?: string; + mimeType?: MimeType; + bufferView?: number; + name?: string; + }[]; + scene: number; + scenes: { + nodes?: number[]; + name?: string; + }[]; + nodes: GltfNode[]; + extensionsRequired?: string[]; + extensionsUsed?: string[]; +} + +export interface GltfMesh { + name: string; + primitives: GltfPrimitive[]; +} + +export interface GltfPrimitive { + indices?: number; + material?: number; + mode?: PrimitiveMode; + attributes: { + POSITION: number; + NORMAL: number; + [key: string]: any; + }; + extensions?: { + [key: string]: any; + KHR_draco_mesh_compression?: { + bufferView: number; + attributes: { + POSITION: number; + NORMAL: number; + [key: string]: any; + }; + } + }; +} + +export interface GltfNode { + camera?: number; + children?: number[]; + skin?: number; + matrix?: number[]; + mesh?: number; + rotation?: number[]; + scale?: number[]; + translation?: number[]; + weights?: number[]; + name?: string; +} + +export enum MimeType { + JPEG = "image/jpeg", + PNG = "image/png" +} + +export interface Accessor { + bufferView: number; + componentType: AccessorComponentType; + count: number; + type: AccessorDataType; + max: number[]; + min: number[]; +} + +export interface BufferView { + buffer: number; + byteLength: number; + byteOffset: number; + target?: BufferViewTarget; +} + +export enum BufferViewTarget { + ARRAY_BUFFER = 34962, + ELEMENT_ARRAY_BUFFER = 34963 +} + +export enum AccessorComponentType { + byte = 5120, + ubyte = 5121, + short = 5122, + ushort = 5123, + uint = 5125, + float = 5126 +} + +export enum AccessorDataType { + scalar = "SCALAR", + vec2 = "VEC2", + vec3 = "VEC3", + vec4 = "VEC4", + mat2 = "MAT2", + mat3 = "MAT3", + mat4 = "MAT4" +} + +export interface Mesh { + name: string; + primitives: Primitive[]; +} + +export interface Primitive { + name: string; + indices?: ArrayBufferLike; + vertices: ArrayBufferLike; + normals: ArrayBufferLike; + texCoords?: ArrayBufferLike; + material?: Material; + mode: PrimitiveMode; + object3d?: Object3d; +} + +export interface Material { + name: string; + baseColorFactor?: number[]; + baseColorTexture?: Texture; + metallicRoughnessTexture?: Texture; + normalTexture?: NormalTexture; + occlusionTexture?: OcclusionTexture; + emissiveTexture?: Texture; + emissiveFactor?: number[]; + alphaMode?: AlphaMode; + alphaCutoff?: number; + doubleSided?: boolean; +} + +export interface Texture { + image: TextureImage; + texCoord?: number; +} + +export interface NormalTexture extends Texture { + scale?: number; +} + +export interface OcclusionTexture extends Texture { + strength?: number; +} + +export interface TextureImage { + src?: string; + element?: HTMLImageElement; + mimeType?: MimeType; + name?: string; +} + +export enum AlphaMode { + OPAQUE = "OPAQUE", + MASK = "MASK", + BLEND = "BLEND" +} + +export enum PrimitiveMode { + points = 0, + lines = 1, + lineLoop = 2, + lineStrip = 3, + triangles = 4, + triangleStrip = 5, + triangleFan = 6 +}