partial implementation

This commit is contained in:
Aigars Zeiza 2025-07-04 13:11:29 +03:00
parent d55bbc535c
commit 15fb55f897
12 changed files with 1131 additions and 35 deletions

Binary file not shown.

View File

@ -0,0 +1,25 @@
<html>
<head>
<title>Draco loader sample</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script
type="text/javascript"
src="https://www.gstatic.com/draco/versioned/decoders/1.5.7/draco_decoder.js"
></script>
<script src="./dracoLoader.js" type="module"></script>
<link rel="stylesheet" href="../../css/og.css" type="text/css" />
<style>
body {
padding: 0;
margin: 0;
}
</style>
</head>
<body>
<div style="width: 100%; height: 100%">
<canvas id="frame" style="width: 100%; height: 100%"> </canvas>
</div>
</body>
</html>

View File

@ -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();

Binary file not shown.

View File

@ -13,7 +13,7 @@ function getColor(color?: number[] | TypedArray | string): Float32Array {
} else if (typeof color === 'string') { } else if (typeof color === 'string') {
return htmlColorToFloat32Array(color); 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 { function getColor3v(color?: NumberArray3 | TypedArray | string): Float32Array {
@ -44,9 +44,12 @@ interface IObject3dParams {
diffuse?: string | NumberArray3; diffuse?: string | NumberArray3;
specular?: string | NumberArray3; specular?: string | NumberArray3;
shininess?: number; shininess?: number;
colorTexture?: string; colorTextureSrc?: string;
normalTexture?: string; normalTextureSrc?: string;
metallicRoughnessTexture?: string; metallicRoughnessTextureSrc?: string;
colorTextureImage?: HTMLImageElement;
normalTextureImage?: HTMLImageElement;
metallicRoughnessTextureImage?: HTMLImageElement;
} }
type MaterialParams = Pick<IObject3dParams, 'ambient' | 'diffuse' | 'specular' | 'shininess'>; type MaterialParams = Pick<IObject3dParams, 'ambient' | 'diffuse' | 'specular' | 'shininess'>;
@ -67,13 +70,15 @@ class Object3d {
public diffuse: Float32Array; public diffuse: Float32Array;
public specular: Float32Array; public specular: Float32Array;
public shininess: number; public shininess: number;
public colorTexture: string; public colorTextureSrc: string | null;
public normalTexture: string; public colorTextureImage: HTMLImageElement | null;
public metallicRoughnessTexture: string; public normalTextureSrc: string | null;
public normalTextureImage: HTMLImageElement | null;
public metallicRoughnessTextureSrc: string | null;
public metallicRoughnessTextureImage: HTMLImageElement | null;
public center: Vec3; public center: Vec3;
constructor(data: IObject3dParams = {}) { constructor(data: IObject3dParams = {}) {
this._name = data.name || "noname"; this._name = data.name || "noname";
this._vertices = data.vertices || []; this._vertices = data.vertices || [];
this._numVertices = this._vertices.length / 3; this._numVertices = this._vertices.length / 3;
@ -90,9 +95,12 @@ class Object3d {
this.diffuse = getColor3v(data.diffuse); this.diffuse = getColor3v(data.diffuse);
this.specular = getColor3v(data.specular); this.specular = getColor3v(data.specular);
this.shininess = data.shininess || 100; this.shininess = data.shininess || 100;
this.colorTexture = data.colorTexture || ""; this.colorTextureSrc = data.colorTextureSrc || null;
this.normalTexture = data.normalTexture || ""; this.colorTextureImage = data.colorTextureImage || null;
this.metallicRoughnessTexture = data.metallicRoughnessTexture || ""; this.normalTextureSrc = data.normalTextureSrc || null;
this.normalTextureImage = data.normalTextureImage || null;
this.metallicRoughnessTextureSrc = data.metallicRoughnessTextureSrc || null;
this.metallicRoughnessTextureImage = data.metallicRoughnessTextureImage || null;
if (data.scale) { if (data.scale) {
let s = data.scale; let s = data.scale;
@ -629,9 +637,9 @@ class Object3d {
specular: mat.specular, specular: mat.specular,
shininess: mat.shininess, shininess: mat.shininess,
color: mat.color, color: mat.color,
colorTexture: baseUrl ? `${baseUrl}/${mat.colorTexture}` : mat.colorTexture, colorTextureSrc: baseUrl ? `${baseUrl}/${mat.colorTexture}` : mat.colorTexture,
normalTexture: baseUrl ? `${baseUrl}/${mat.normalTexture}` : mat.normalTexture, normalTextureSrc: baseUrl ? `${baseUrl}/${mat.normalTexture}` : mat.normalTexture,
metallicRoughnessTexture: baseUrl ? `${baseUrl}/${mat.metallicRoughnessTexture}` : mat.metallicRoughnessTexture metallicRoughnessTextureSrc: baseUrl ? `${baseUrl}/${mat.metallicRoughnessTexture}` : mat.metallicRoughnessTexture
}) })
} }
); );
@ -658,9 +666,9 @@ class Object3d {
specular: mat.specular, specular: mat.specular,
shininess: mat.shininess, shininess: mat.shininess,
color: mat.color, color: mat.color,
colorTexture: mat.colorTexture, colorTextureSrc: mat.colorTexture,
normalTexture: mat.normalTexture, normalTextureSrc: mat.normalTexture,
metallicRoughnessTexture: mat.metallicRoughnessTexture metallicRoughnessTextureSrc: mat.metallicRoughnessTexture
}) })
} }
); );

View File

@ -113,7 +113,11 @@ class GeoObject {
this._localPosition = new Vec3(); 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._handler = null;
this._handlerIndex = -1; this._handlerIndex = -1;

View File

@ -122,28 +122,49 @@ export class GeoObjectHandler {
this.update(); this.update();
} }
public setColorTextureTag(src: string, tag: string) { public setColorTextureTag(src: string | HTMLImageElement, tag: string) {
const tagData = this._instanceDataMap.get(tag); const tagData = this._instanceDataMap.get(tag);
if (tagData) { if (tagData) {
if (typeof src === 'string') {
tagData._colorTextureSrc = src; tagData._colorTextureSrc = src;
tagData._colorTextureImage = null;
}
if (src instanceof HTMLImageElement) {
tagData._colorTextureSrc = null;
tagData._colorTextureImage = src;
}
this._instanceDataMap.set(tag, tagData); this._instanceDataMap.set(tag, tagData);
this._loadColorTexture(tagData); this._loadColorTexture(tagData);
} }
} }
public setNormalTextureTag(src: string, tag: string) { public setNormalTextureTag(src: string | HTMLImageElement, tag: string) {
const tagData = this._instanceDataMap.get(tag); const tagData = this._instanceDataMap.get(tag);
if (tagData) { if (tagData) {
if (typeof src === 'string') {
tagData._normalTextureSrc = src; tagData._normalTextureSrc = src;
tagData._normalTextureImage = null;
}
if (src instanceof HTMLImageElement) {
tagData._normalTextureSrc = null;
tagData._normalTextureImage = src;
}
this._instanceDataMap.set(tag, tagData); this._instanceDataMap.set(tag, tagData);
this._loadNormalTexture(tagData); this._loadNormalTexture(tagData);
} }
} }
public setMetallicRoughnessTextureTag(src: string, tag: string) { public setMetallicRoughnessTextureTag(src: string | HTMLImageElement, tag: string) {
const tagData = this._instanceDataMap.get(tag); const tagData = this._instanceDataMap.get(tag);
if (tagData) { if (tagData) {
if (typeof src === 'string') {
tagData._metallicRoughnessTextureSrc = src; tagData._metallicRoughnessTextureSrc = src;
tagData._metallicRoughnessTextureImage = null;
}
if (src instanceof HTMLImageElement) {
tagData._metallicRoughnessTextureSrc = null;
tagData._metallicRoughnessTextureImage = src;
}
this._instanceDataMap.set(tag, tagData); this._instanceDataMap.set(tag, tagData);
this._loadMetallicRoughnessTexture(tagData); this._loadMetallicRoughnessTexture(tagData);
} }
@ -182,9 +203,13 @@ export class GeoObjectHandler {
tagData._changedBuffers[TEXCOORD_BUFFER] = true; tagData._changedBuffers[TEXCOORD_BUFFER] = true;
} }
tagData._colorTextureSrc = object.colorTexture; tagData._colorTextureSrc = object.colorTextureSrc;
tagData._normalTextureSrc = object.normalTexture; tagData._normalTextureSrc = object.normalTextureSrc;
tagData._metallicRoughnessTexture = object.metallicRoughnessTexture; tagData._metallicRoughnessTexture = object.metallicRoughnessTextureSrc;
tagData._colorTextureImage = object.colorTextureImage;
tagData._normalTextureImage = object.normalTextureImage;
tagData._metallicRoughnessTextureImage = object.metallicRoughnessTextureImage;
this._loadColorTexture(tagData); this._loadColorTexture(tagData);
this._loadNormalTexture(tagData); this._loadNormalTexture(tagData);
@ -212,9 +237,12 @@ export class GeoObjectHandler {
tagData._indicesArr = geoObject.indices; tagData._indicesArr = geoObject.indices;
tagData._texCoordArr = geoObject.texCoords; tagData._texCoordArr = geoObject.texCoords;
tagData._colorTextureSrc = geoObject.object3d.colorTexture; tagData._colorTextureSrc = geoObject.object3d.colorTextureSrc;
tagData._normalTextureSrc = geoObject.object3d.normalTexture; tagData._normalTextureSrc = geoObject.object3d.normalTextureSrc;
tagData._metallicRoughnessTextureSrc = geoObject.object3d.metallicRoughnessTexture; tagData._metallicRoughnessTextureSrc = geoObject.object3d.metallicRoughnessTextureSrc;
tagData._colorTextureImage = geoObject.object3d.colorTextureImage;
tagData._normalTextureImage = geoObject.object3d.normalTextureImage;
tagData._metallicRoughnessTextureImage = geoObject.object3d.metallicRoughnessTextureImage;
tagData.setMaterialParams( tagData.setMaterialParams(
geoObject.object3d.ambient, geoObject.object3d.ambient,
@ -489,23 +517,50 @@ export class GeoObjectHandler {
} }
async _loadColorTexture(tagData: InstanceData) { async _loadColorTexture(tagData: InstanceData) {
if (this._renderer && tagData._colorTextureSrc) { if (!this._renderer) {
return;
}
if (tagData._colorTextureSrc) {
const image = await loadImage(tagData._colorTextureSrc); const image = await loadImage(tagData._colorTextureSrc);
tagData.createColorTexture(image); tagData.createColorTexture(image);
return;
}
if (tagData._colorTextureImage) {
await tagData._colorTextureImage.decode();
tagData.createColorTexture(tagData._colorTextureImage);
return;
} }
} }
async _loadNormalTexture(tagData: InstanceData) { async _loadNormalTexture(tagData: InstanceData) {
if (this._renderer && tagData._normalTextureSrc) { if (!this._renderer) {
return;
}
if (tagData._normalTextureSrc) {
const image = await loadImage(tagData._normalTextureSrc); const image = await loadImage(tagData._normalTextureSrc);
tagData.createNormalTexture(image); tagData.createNormalTexture(image);
return;
}
if (tagData._normalTextureImage) {
await tagData._normalTextureImage.decode();
tagData.createNormalTexture(tagData._normalTextureImage);
return;
} }
} }
async _loadMetallicRoughnessTexture(tagData: InstanceData) { async _loadMetallicRoughnessTexture(tagData: InstanceData) {
if (this._renderer && tagData._metallicRoughnessTextureSrc) { if (!this._renderer) {
return;
}
if (tagData._metallicRoughnessTextureSrc) {
const image = await loadImage(tagData._metallicRoughnessTextureSrc); const image = await loadImage(tagData._metallicRoughnessTextureSrc);
tagData.createMetallicRoughnessTexture(image); tagData.createMetallicRoughnessTexture(image);
return;
}
if (tagData._metallicRoughnessTextureImage) {
await tagData._metallicRoughnessTextureImage.decode();
tagData.createMetallicRoughnessTexture(tagData._metallicRoughnessTextureImage);
return;
} }
} }

View File

@ -46,6 +46,9 @@ export class InstanceData {
public _colorTextureSrc: string | null; public _colorTextureSrc: string | null;
public _normalTextureSrc: string | null; public _normalTextureSrc: string | null;
public _metallicRoughnessTextureSrc: string | null; public _metallicRoughnessTextureSrc: string | null;
public _colorTextureImage: HTMLImageElement | null;
public _normalTextureImage: HTMLImageElement | null;
public _metallicRoughnessTextureImage: HTMLImageElement | null;
public _objectSrc?: string; public _objectSrc?: string;
@ -99,12 +102,15 @@ export class InstanceData {
this._colorTexture = null; this._colorTexture = null;
this._colorTextureSrc = null; this._colorTextureSrc = null;
this._colorTextureImage = null;
this._normalTexture = null; this._normalTexture = null;
this._normalTextureSrc = null; this._normalTextureSrc = null;
this._normalTextureImage = null;
this._metallicRoughnessTexture = null; this._metallicRoughnessTexture = null;
this._metallicRoughnessTextureSrc = null; this._metallicRoughnessTextureSrc = null;
this._metallicRoughnessTextureImage = null;
this._sizeArr = []; this._sizeArr = [];
this._translateArr = []; this._translateArr = [];
@ -413,7 +419,6 @@ export class InstanceData {
} }
this._sizeArr = makeArrayTyped(this._sizeArr); this._sizeArr = makeArrayTyped(this._sizeArr);
h.setStreamArrayBuffer(this._sizeBuffer, this._sizeArr as Float32Array); h.setStreamArrayBuffer(this._sizeBuffer, this._sizeArr as Float32Array);
} }

View File

@ -86,6 +86,7 @@ import {
} from './terrain/index'; } from './terrain/index';
import {MoveAxisEntity} from "./control/geoObjectEditor/MoveAxisEntity"; import {MoveAxisEntity} from "./control/geoObjectEditor/MoveAxisEntity";
import { Gltf } from './utils/gltf/gltfParser';
export { export {
bv, bv,
@ -142,6 +143,6 @@ export {
EarthQuadTreeStrategy, EarthQuadTreeStrategy,
Wgs84QuadTreeStrategy, Wgs84QuadTreeStrategy,
Object3d, Object3d,
Gltf,
MoveAxisEntity, MoveAxisEntity,
}; };

View File

@ -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<GltfData> {
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));
}
}

View File

@ -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");
}
}

216
src/utils/gltf/types.ts Normal file
View File

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