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
+}