threepipe
Version:
A 3D viewer framework built on top of three.js in TypeScript with a focus on quality rendering, modularity and extensibility.
279 lines • 13.8 kB
JavaScript
import { ImageUtils } from 'three';
import { RGBEPNGLoader } from '../import/RGBEPNGLoader';
import { halfFloatToRgbe } from '../../three';
export class GLTFViewerConfigExtension {
// region Import
/**
* Import viewer config from glTF(exported from {@link GLTFViewerConfigExtension.ExportViewerConfig}) and sets in scene.importedViewerConfig
* Must be called from afterRoot in gltf loader. Used in {@link GLTFLoader2.setup}
* Only imports, does not apply.
* @param parser
* @param viewer
* @param resultScenes
* @param scene
*/
static async ImportViewerConfig(parser, viewer, resultScenes, scene) {
if (!scene) {
const scenes = parser.json.scenes || [];
if (scenes.length !== 1) {
for (const scene1 of scenes) {
const i = scenes.indexOf(scene1);
await this.ImportViewerConfig(parser, viewer, i >= 0 ? [resultScenes[i]] : resultScenes, scene1);
}
return {};
}
scene = scenes[0];
}
const resultScene = resultScenes.length > 0 ? resultScenes[0] : undefined;
const viewerConfig1 = scene.extensions?.[this.ViewerConfigGLTFExtension];
// console.log({...viewerConfig?.resources})
if (!viewerConfig1)
return {};
const viewerConfig = {
type: 'ThreeViewer',
version: '0',
plugins: [],
assetType: 'config',
...viewerConfig1,
};
if (viewerConfig.resources) {
await this._parseArrayBuffers(viewerConfig.resources, parser);
// Find empty resources and try to find them in the glTF as a dependency by saved UUID.
const extraResources = await this._parseExtraResources(viewerConfig.resources, parser, viewer);
viewerConfig.resources = await viewer.loadConfigResources(viewerConfig.resources || {}, extraResources);
}
if (resultScene)
resultScene.importedViewerConfig = viewerConfig;
return viewerConfig;
}
/**
* Find resources in parser from uuid
* @param currentResources
* @param parser
* @param viewer
* @private
*/
static async _parseExtraResources(currentResources, parser, viewer) {
const extraResources = {
textures: {},
materials: {},
};
if (currentResources.textures && parser.json.textures)
for (const [uuid, texture] of [...Object.entries(currentResources.textures)]) {
// console.log(texture) // todo: texture should be {} but its {userData:undefined}, why?
if (texture.uuid || !uuid)
continue;
delete currentResources.textures[uuid];
const texIndex = parser.json.textures.findIndex((t) => t.extras?.uuid === uuid ||
parser.json.samplers?.[t.sampler]?.extras?.uuid === uuid ||
parser.json.images?.[t.source]?.extras?.t_uuid === uuid);
// This HAS To be called from afterRoot in gltf loader.
// And make sure that texture is not cloned in any gltf extension like khr_texture_transform, which happens in three.js by default and it's commented in custom fork.
if (texIndex >= 0)
extraResources.textures[uuid] = await parser.getDependency('texture', texIndex);
}
// todo: need to test, because materials are also cloned in GLTFLoader.js
if (currentResources.materials && parser.json.materials)
for (const [uuid, material] of [...Object.entries(currentResources.materials)]) {
// console.log(material)
if (material.uuid || !uuid)
continue;
delete currentResources.materials[uuid];
const matIndex = parser.json.materials.findIndex((m) => m.extras?.uuid === uuid);
if (matIndex >= 0) {
const mat = await parser.getDependency('material', matIndex);
extraResources.materials[uuid] = viewer.assetManager.materials.convertToIMaterial(mat);
}
}
// todo: do same for other dependencies?
return extraResources;
}
static async _parseArrayBuffers(resources, parser) {
const buffers = [];
Object.values(resources).forEach((res) => {
Object.values(res).forEach((item) => {
if (!item.url)
return;
if (item.url.type === 'Uint16Array' && item.url.data) {
// item.url.data = new Uint16Array(item.url.data)
buffers.push(item.url);
}
if (item.url.type === 'Uint8Array' && item.url.data) {
// item.url.data = new Uint8Array(item.url.data)
buffers.push(item.url);
}
});
});
for (const buff of buffers) {
const imgIndex = buff.data.image;
const img = parser.json.images[imgIndex];
const bufferView = await parser.getDependency('bufferView', img.bufferView);
// todo: add more checks
if (img.mimeType.startsWith('image/') && buff.type === 'Uint16Array' && buff.encoding === 'rgbe') {
// todo: find a optimal way, this has too many cross conversions
// const view2 = (bufferView as ArrayBuffer).slice(0, bufferView.byteLength - 4)
const blob = new Blob([bufferView]);
// const blob2 = new Blob([await blob.text()], {type: img.mimeType})
let url = URL.createObjectURL(blob);
const encodingVersion = buff.encodingVersion || 1;
if (encodingVersion < 2) {
url = 'data:image/png;base64,' + btoa(await blob.text());
}
// fetch(url).then(async r=>r.blob()).then(b=>console.log(b))
// console.log(view2)
buff.data = (await new RGBEPNGLoader().parseAsync(url, undefined, encodingVersion < 3)).data;
URL.revokeObjectURL(url);
delete buff.encoding;
delete buff.encodingVersion;
}
else {
buff.data = bufferView;
}
}
}
// endregion
// region Export
/**
* Export viewer config to glTF(can be imported by {@link GLTFViewerConfigExtension.ImportViewerConfig}).
* Used in {@link GLTFExporter2}
* @param viewer
* @param writer
* @constructor
*/
static ExportViewerConfig(viewer, writer) {
const viewerData = viewer.toJSON(true, undefined);
const json = writer.json;
this._bundleExtraResources(json, viewerData);
this._bundleArrayBuffers(viewerData, writer);
const scene = writer.json.scenes[writer.json.scene || 0];
if (!scene.extensions)
scene.extensions = {};
writer.extensionsUsed[this.ViewerConfigGLTFExtension] = true;
scene.extensions[this.ViewerConfigGLTFExtension] = viewerData;
}
static _bundleArrayBuffers(viewerData, writer) {
// For DataTextures like env map with custom rgbe encoding
// Create objects of TypedArray
const buffers = [];
Object.values(viewerData.resources).forEach((res) => {
if (res)
Object.values(res).forEach((item) => {
if (!item.url)
return;
if (item.url.type === 'Uint16Array' && item.url.data) {
if (!(item.url.data instanceof Uint16Array))
item.url.data = new Uint16Array(item.url.data);
buffers.push(item.url);
}
if (item.url.type === 'Uint8Array' && item.url.data) {
if (!(item.url.data instanceof Uint8Array))
item.url.data = new Uint8Array(item.url.data);
buffers.push(item.url); // todo: just use jpeg or PNG for this
}
});
});
// console.log(writer)
for (const buffer of buffers) {
// todo:[update: done one case below] check if buffer is of image, if yes convert to rgbe with png compression blob. [or this can be done while serializing the DataTexture]
let mime = 'application/octet-stream';
if (buffer.mimeType)
mime = buffer.mimeType;
// console.log(buffer, buffer.data)
const encodeUint16Rgbe = writer.options.exporterOptions.encodeUint16Rgbe; // disabled for now, todo: add a UI option to enable this
if (encodeUint16Rgbe && buffer.type === 'Uint16Array' && buffer.width > 0 && buffer.height > 0) { // import for this is handled in gltf.ts:importViewer.
// todo: also check if this is indeed an hdr image or something else like LUT or other kind of embedded file.
const encodingVersion = 3;
// todo: can we optimize this? this is too many steps
const d = encodingVersion < 3 ? halfFloatToRgbe2(buffer.data, 4) : halfFloatToRgbe(buffer.data, 4);
const id = new ImageData(d, buffer.width, buffer.height);
const b64 = ImageUtils.getDataURL(id, true).split(',')[1];
mime = 'image/png';
if (encodingVersion === 1) {
buffer.data = atob(b64);
}
else if (encodingVersion === 2 || encodingVersion === 3) {
buffer.data = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
}
else {
throw new Error('Invalid encoding version');
}
buffer.encoding = 'rgbe';
buffer.encodingVersion = encodingVersion;
}
// console.log(mime, buffer)
// const blob = new Blob([buffer.data], {type: mime})
if (!writer.json.images)
writer.json.images = [];
const img = {
mimeType: mime,
};
// console.log(buffer, img)
const imgIndex = writer.json.images.push(img) - 1;
const data = buffer.data;
img.bufferView = writer.processBufferViewImageBuffer(data);
// console.log(buffer)
buffer.data = { image: imgIndex };
}
}
/**
* Find the resources that are in the viewer config AND in writer.json and use the ones in writer and remove from viewer Config.
* For now (for the lack of a better way) we can let the resources be exported twice and removed from resources. Overhead will be just for some images.
* @param json
* @param viewerData
* @private
*/
static _bundleExtraResources(json, viewerData) {
if (json.textures && json.samplers && json.images && viewerData.resources.textures)
[...Object.entries(viewerData.resources.textures)].forEach(([uuid, texture]) => {
const tex = json.textures.find((t) => // find same texture in gltf writer
t.extras?.uuid === uuid ||
json.samplers[t.sampler]?.extras?.uuid === uuid ||
json.images[t.source]?.extras?.t_uuid === uuid // todo: remove t_uuid when sampler extras supported by gltf-transform: https://github.com/donmccurdy/glTF-Transform/issues/645
);
if (!tex)
return;
// console.log('Removing texture', uuid, tex, texture)
if (texture.image && viewerData.resources.images && viewerData.resources.images[texture.image]) {
delete viewerData.resources.images[texture.image]; // assuming images are only referenced once.
}
viewerData.resources.textures[uuid] = {}; // set to empty, can be read from the gltf data after loading gltf
});
// todo: test
if (json.materials && viewerData.resources.materials)
[...Object.entries(viewerData.resources.materials)].forEach(([uuid, _]) => {
const mat = json.materials.find((m) => m.extras?.uuid === uuid); // same material in gltf writer
if (!mat)
return;
viewerData.resources.materials[uuid] = {}; // set to empty, can be read from the gltf data after loading gltf
});
// todo: do same for object references?
}
}
GLTFViewerConfigExtension.ViewerConfigGLTFExtension = 'WEBGI_viewer';
/**
* @deprecated old version. see {@link halfFloatToRgbe} to convert half float buffer to rgbe
* adapted from https://github.com/enkimute/hdrpng.js/blob/3a62b3ae2940189777df9f669df5ece3e78d9c16/hdrpng.js#L235
* channels = 4 for RGBA data or 3 for RGB data. buffer from THREE.DataTexture
* @param buffer
* @param channels
* @param res
*/
function halfFloatToRgbe2(buffer, channels = 3, res) {
let r, g, b, v, s;
const l = buffer.byteLength / (channels * 2) | 0;
res = res || new Uint8ClampedArray(l * 4);
for (let i = 0; i < l; i++) {
r = buffer[i * channels];
g = buffer[i * channels + 1];
b = buffer[i * channels + 2];
v = Math.max(Math.max(r, g), b);
const e = Math.ceil(Math.log2(v));
s = Math.pow(2, e - 8);
res[i * 4] = r / s | 0;
res[i * 4 + 1] = g / s | 0;
res[i * 4 + 2] = b / s | 0;
res[i * 4 + 3] = e + 128;
}
return res;
}
//# sourceMappingURL=GLTFViewerConfigExtension.js.map