threepipe
Version:
A 3D viewer framework built on top of three.js in TypeScript with a focus on quality rendering, modularity and extensibility.
231 lines • 9.53 kB
JavaScript
import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js';
import { MeshStandardMaterial } from 'three';
import { blobToDataURL } from 'ts-browser-helpers';
import { ThreeSerialization } from '../../utils';
export class GLTFWriter2 extends GLTFExporter.Utils.GLTFWriter {
constructor() {
super(...arguments);
this._defaultMaterial = new MeshStandardMaterial();
}
serializeUserData(object, objectDef) {
const userData = object.userData;
const temp = {};
if (userData.__disposed) {
console.error('Serializing a disposed object', object);
}
Object.entries(userData).forEach(([key, value]) => {
if (!value ||
typeof value === 'function' ||
value.isObject3D ||
value.isTexture ||
value.isMaterial ||
value.assetType != null ||
key.startsWith('_') // private data
) {
temp[key] = value;
delete userData[key];
}
});
const ud2 = ThreeSerialization.Serialize(userData);
Object.entries(temp).forEach(([key, value]) => {
userData[key] = value;
delete temp[key];
});
object.userData = ud2;
super.serializeUserData(object, objectDef);
object.userData = userData;
}
processObjects(objects) {
if (objects.length === 1 && objects[0]?.userData.rootSceneModelRoot) {
// objects[0].isScene = true
this.processScene(objects[0]);
// delete objects[0].isScene
}
else
super.processObjects(objects);
}
/**
* Checks for shader material and does the same thing...
* @param material
*/
processMaterial(material) {
if (this.cache.materials.has(material))
return this.cache.materials.get(material);
let mat = material;
// set default material when material is null. shader material is processed further below for custom extensions like diamonds.
if (!mat || mat.isShaderMaterial)
mat = this._defaultMaterial;
const defIndex = super.processMaterial(mat);
if (defIndex === null) {
console.error('GLTFWriter2: Unexpected error: Failed to process material', material);
return null;
}
// when not a shader material
if (!material || mat === material)
return defIndex; // todo: this line needds to be tested.
// when shader material
const defaultDef = JSON.stringify(this.json.materials[defIndex]);
const materialDef = JSON.parse(defaultDef); // for deep clone
// console.log(defIndex, defaultDef, materialDef)
this.serializeUserData(material, materialDef);
this._invokeAll((ext) => {
ext.writeMaterial && ext.writeMaterial(material, materialDef);
});
// todo: test remove this
// if (JSON.stringify(materialDef) === defaultDef) {
// return defIndex
// }
const index = this.json.materials.push(materialDef) - 1;
this.cache.materials.set(material, index);
return index;
}
/**
* Same as processImage but for image blobs
* @param blob
* @param texture
*/
processImageBlob(blob, texture) {
if (!blob)
return -1;
const cache = this.cache;
const options = this.options;
const pending = this.pending;
const json = this.json;
const image = texture.image;
if (!cache.images.has(image))
cache.images.set(image, {});
const cachedImages = cache.images.get(image);
const key = blob.type + ':flipY/' + texture.flipY.toString();
if (cachedImages[key] !== undefined)
return cachedImages[key];
if (!json.images)
json.images = [];
const imageDef = { mimeType: blob.type };
if (options.binary === true) {
pending.push(new Promise((resolve) => {
this.processBufferViewImage(blob).then((bufferViewIndex) => {
imageDef.bufferView = bufferViewIndex;
resolve();
});
}));
}
else {
pending.push(blobToDataURL(blob).then((dataURL) => {
imageDef.uri = dataURL;
}));
}
const index = json.images.push(imageDef) - 1;
cachedImages[key] = index;
return index;
}
processSampler(map) {
const samplerIndex = super.processSampler(map);
// todo: uncomment when sampler extras supported by gltf-transform: https://github.com/donmccurdy/glTF-Transform/issues/645
// const samplerDef = this.json.samplers[samplerIndex]
// if (!samplerDef.extras) samplerDef.extras = {}
// samplerDef.extras.uuid = map.uuid
return samplerIndex;
}
processTexture(map) {
const cache = this.cache;
const json = this.json;
if (cache.textures.has(map))
return cache.textures.get(map);
const srcData = map.source.data;
const mimeType = map.userData.mimeType;
if (map.userData.rootPath &&
!this.options.exporterOptions.embedUrlImages
&& (map.userData.rootPath.startsWith('http') || map.userData.rootPath.startsWith('data:'))) {
if (map.source.data) { // handled below in GLTFWriter2.processImage
if (!this.options.exporterOptions.embedUrlImagePreviews || map.isDataTexture)
map.source.data = null; // todo make sure its only Texture, check for svg etc
else
map.source.data._savePreview = true;
}
delete map.userData.mimeType; // for extensions like ktx2
}
const processed = super.processTexture(map);
const textureDef = json.textures[processed];
if (!textureDef) {
console.error('No texture def', processed, map);
return processed;
}
// if (!textureDef.extras) textureDef.extras = {}
const imageDef = json.images ? json.images[textureDef.source] : null;
if (imageDef) {
if (!imageDef.extras)
imageDef.extras = {};
if (map.source)
imageDef.extras.uuid = map.source.uuid;
imageDef.extras.t_uuid = map.uuid; // todo: remove when extras supported by gltf-transform: https://github.com/donmccurdy/glTF-Transform/issues/645
}
// map uuid saved in processSampler.
if (map.userData.rootPath && !this.options.exporterOptions.embedUrlImages
&& (map.userData.rootPath.startsWith('http') || map.userData.rootPath.startsWith('data:'))) {
if (map.source.data)
delete map.source.data._savePreview;
else
map.source.data = srcData;
map.userData.mimeType = mimeType;
if (!textureDef) {
console.error('textureDef is null', processed, map);
return processed;
}
if (textureDef.source >= 0) {
// console.warn('textureDef.source is already set', processed, map)
const img = this.json.images[textureDef.source];
if (img.uri) {
console.warn('uri already set', img.uri);
}
else {
img.uri = map.userData.rootPath;
img.mimeType = mimeType;
if (!img.extras)
img.extras = {};
img.extras.flipY = map.flipY;
img.extras.uri = map.userData.rootPath; // uri is removed by gltf-transform if bufferView is set
}
}
else {
textureDef.source = this.processImageUri(map.image, map.userData.rootPath, map.flipY, mimeType);
}
}
if (textureDef.source < 0) {
console.error('textureDef.source cannot be saved', textureDef, map);
}
return processed;
}
// Add extra check for null images. This is set in processTexture when we have a rootPath
processImage(image, format, flipY, mimeType = 'image/png') {
if (!image)
return -1;
return super.processImage(image, format, flipY, mimeType, image._savePreview ? 32 : undefined, image._savePreview ? 32 : undefined);
}
/**
* Used in GLTFWriter2.processTexture for rootPath. Note that this does not check for options.exporterOptions.embedUrlImages, it must be done separately.
* @param image
* @param uri
* @param flipY
* @param mimeType
*/
processImageUri(image, uri, flipY, mimeType = 'image/png') {
const cache = this.cache;
const json = this.json;
if (!cache.images.has(image))
cache.images.set(image, {});
const cachedImages = cache.images.get(image);
const key = mimeType + ':flipY/' + flipY.toString();
if (cachedImages[key] !== undefined)
return cachedImages[key];
if (!json.images)
json.images = [];
const imageDef = {
mimeType, uri,
extras: { flipY },
};
const index = json.images.push(imageDef) - 1;
cachedImages[key] = index;
return index;
}
}
//# sourceMappingURL=GLTFWriter2.js.map