Przeglądaj źródła

GLTF Import/Export support for preparser, processors, add GLB encryption processor, Add embedImagePreviews in GLB and material/texture export. fix for loading a gltf with no scene, improve RGBEPNGLoader encoding for compressed hdr in glb, export halfFloatToRgbe, textureDataToImageData, textureToDataUrl, imageToCanvas

master
Palash Bansal 3 lat temu
rodzic
commit
593b27db6e
No account linked to committer's email address

+ 7
- 7
package.json Wyświetl plik

@@ -1,6 +1,6 @@
{
"name": "threepipe",
"version": "0.0.5",
"version": "0.0.6-dev",
"description": "A 3D viewer framework built on top of three.js in TypeScript with a focus on quality rendering, modularity and extensibility.",
"main": "src/index.ts",
"module": "dist/index.mjs",
@@ -87,7 +87,7 @@
"rollup-plugin-license": "^3.0.1",
"rollup-plugin-postcss": "^4.0.2",
"stats.js": "^0.17.0",
"three": "https://github.com/repalash/three.js-modded/releases/download/v0.152.2006/package.tgz",
"three": "https://github.com/repalash/three.js-modded/releases/download/v0.152.2007/package.tgz",
"tslib": "^2.5.0",
"tweakpane": "^3.1.9",
"@tweakpane/core": "^1.1.8",
@@ -99,7 +99,7 @@
"tweakpane-image-plugin": "https://github.com/repalash/tweakpane-image-plugin/releases/download/v1.1.403/package.tgz"
},
"dependencies": {
"@types/three": "https://github.com/repalash/three-ts-types/releases/download/v0.152.1006/package.tgz",
"@types/three": "https://github.com/repalash/three-ts-types/releases/download/v0.152.1008/package.tgz",
"@types/webxr": "^0.5.1",
"@types/wicg-file-system-access": "^2020.9.5",
"ts-browser-helpers": "^0.6.0"
@@ -109,10 +109,10 @@
"uiconfig.js": "^0.0.4",
"ts-browser-helpers": "^0.5.0",
"uiconfig-tweakpane": "^0.0.3",
"three": "https://github.com/repalash/three.js-modded/releases/download/v0.152.2006/package.tgz",
"three-f": "https://github.com/repalash/three.js-modded/archive/refs/tags/v0.152.2006.tar.gz",
"@types/three": "https://github.com/repalash/three-ts-types/releases/download/v0.152.1006/package.tgz",
"@types/three-f": "https://github.com/repalash/three-ts-types/archive/refs/tags/v0.152.1006.tar.gz",
"three": "https://github.com/repalash/three.js-modded/releases/download/v0.152.2007/package.tgz",
"three-f": "https://github.com/repalash/three.js-modded/archive/refs/tags/v0.152.2007.tar.gz",
"@types/three": "https://github.com/repalash/three-ts-types/releases/download/v0.152.1008/package.tgz",
"@types/three-f": "https://github.com/repalash/three-ts-types/archive/refs/tags/v0.152.1008.tar.gz",
"@types/three-pkg": "https://gitpkg.now.sh/repalash/three-ts-types/types/three?modded_three",
"tweakpane-image-plugin": "git+ssh://github.com/repalash/tweakpane-image-plugin.git#52d5542047fd07d2e7225b5b67c9f7620366f2c7"
},

+ 55
- 15
src/assetmanager/export/GLTFExporter2.ts Wyświetl plik

@@ -1,23 +1,29 @@
import {GLTFExporter, GLTFExporterOptions, GLTFExporterPlugin} from 'three/examples/jsm/exporters/GLTFExporter.js'
import {IExportParser} from '../IExporter'
import {AnyOptions} from 'ts-browser-helpers'
import {GLTFWriter2} from './GLTFWriter2'
import {Object3D} from 'three'
import {ThreeViewer} from '../../viewer'
import {GLTFObject3DExtrasExtension} from '../gltf/GLTFObject3DExtrasExtension'
import {GLTFLightExtrasExtension} from '../gltf/GLTFLightExtrasExtension'
import {GLTFMaterialsBumpMapExtension} from '../gltf/GLTFMaterialsBumpMapExtension'
import {GLTFMaterialsDisplacementMapExtension} from '../gltf/GLTFMaterialsDisplacementMapExtension'
import {GLTFMaterialsLightMapExtension} from '../gltf/GLTFMaterialsLightMapExtension'
import {GLTFMaterialsAlphaMapExtension} from '../gltf/GLTFMaterialsAlphaMapExtension'
import {GLTFMaterialExtrasExtension} from '../gltf/GLTFMaterialExtrasExtension'
import {GLTFViewerConfigExtension} from '../gltf/GLTFViewerConfigExtension'
import {
GLTFLightExtrasExtension,
GLTFMaterialExtrasExtension,
GLTFMaterialsAlphaMapExtension,
GLTFMaterialsBumpMapExtension,
GLTFMaterialsDisplacementMapExtension,
GLTFMaterialsLightMapExtension,
GLTFObject3DExtrasExtension,
GLTFViewerConfigExtension,
} from '../gltf'
import {glbEncryptionProcessor} from '../gltf/gltfEncyptionHelpers'

export type GLTFExporter2Options = GLTFExporterOptions & {
/**
* embed images in glb even when remote url is available, default = false
* embed images in glb even when remote url is available, {@default false}
*/
embedUrlImages?: boolean,
/**
* Embed previews of images in glb, {@default false}
*/
embedUrlImagePreviews?: boolean,
/**
* export viewer config (scene settings)
*/
@@ -27,19 +33,53 @@ export type GLTFExporter2Options = GLTFExporterOptions & {
*/
exportExt?: string,
preserveUUIDs?: boolean,
externalImagesInExtras?: boolean, // see GLTFDracoExporter and extras extension
encodeUint16Rgbe?: boolean // see GLTFViewerExport->processViewer, default = true
/**
* see GLTFDracoExporter and {@link GLTFMaterialExtrasExtension}
*/
externalImagesInExtras?: boolean,
/**
* see GLTFViewerExport->processViewer, {@default false}
*/
encodeUint16Rgbe?: boolean
/**
* Number of spaces to use when exporting to json, {@default 2}
*/
jsonSpaces?: number,
/**
* Encrypt the exported file in a GLB container using {@link encryptKey}, {@default false}. Works only for glb export.
*/
encrypt?: boolean,
/**
* Encryption key, if not provided, will be prompted, {@default undefined}. Works only for glb export.
*/
encryptKey?: string|Uint8Array,

[key: string]: any
}

export class GLTFExporter2 extends GLTFExporter implements IExportParser {

constructor() {
super()
this.processors.push(glbEncryptionProcessor)
}

register(callback: (writer: GLTFWriter2)=>GLTFExporterPlugin): this {
return super.register(callback as any)
}

async parseAsync(obj: any, options: AnyOptions): Promise<Blob> {
processors: ((obj: ArrayBuffer|any|Blob, options: GLTFExporter2Options) => Promise<ArrayBuffer|any|Blob>)[] = []

async parseAsync(obj: ArrayBuffer|any, options: GLTFExporter2Options): Promise<Blob> {
if (!obj) throw new Error('No object to export')
const gltf = !obj.__isGLTFOutput && (Array.isArray(obj) || obj.isObject3D) ? await new Promise((resolve, reject) => this.parse(obj, resolve, reject, options)) : obj
let gltf = !obj.__isGLTFOutput && (Array.isArray(obj) || obj.isObject3D) ? await new Promise((resolve, reject) => this.parse(obj, resolve, reject, options)) : obj

for (const processor of this.processors) {
gltf = await processor(gltf, options)
}

if (gltf && gltf instanceof Blob) return gltf

if (gltf && typeof gltf === 'object' && !gltf.byteLength) { // byteLength is for ArrayBuffer
return new Blob([JSON.stringify(gltf, (k, v)=> k.startsWith('__') ? undefined : v, options.jsonSpaces ?? 2)], {type: 'model/gltf+json'})
} else if (gltf) {
@@ -54,7 +94,7 @@ export class GLTFExporter2 extends GLTFExporter implements IExportParser {
onDone: (gltf: ArrayBuffer | {[key: string]: any}) => void,
onError: (error: ErrorEvent) => void,
options: GLTFExporter2Options = {},
): any {
): void {
const gltfOptions: GLTFWriter2['options'] = {
// default options
binary: false,

+ 20
- 4
src/assetmanager/export/GLTFWriter2.ts Wyświetl plik

@@ -172,7 +172,10 @@ export class GLTFWriter2 extends GLTFExporter.Utils.GLTFWriter {
!this.options.exporterOptions.embedUrlImages
&& (map.userData.rootPath.startsWith('http') || map.userData.rootPath.startsWith('data:'))
) {
map.source.data = null // handled below in GLTFWriter2.processImage
if (map.source.data) { // handled below in GLTFWriter2.processImage
if (!this.options.exporterOptions.embedUrlImagePreviews || (map as any).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
}

@@ -198,14 +201,26 @@ export class GLTFWriter2 extends GLTFExporter.Utils.GLTFWriter {
if (map.userData.rootPath && !this.options.exporterOptions.embedUrlImages
&& (map.userData.rootPath.startsWith('http') || map.userData.rootPath.startsWith('data:'))
) {
map.source.data = srcData
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)
// 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)
}
@@ -220,7 +235,8 @@ export class GLTFWriter2 extends GLTFExporter.Utils.GLTFWriter {
// Add extra check for null images. This is set in processTexture when we have a rootPath
processImage(image: any, format: PixelFormat, flipY: boolean, mimeType = 'image/png') {
if (!image) return -1
return super.processImage(image, format, flipY, mimeType)
// @ts-expect-error todo: update in three-ts-types
return super.processImage(image, format, flipY, mimeType, image._savePreview ? 32 : undefined, image._savePreview ? 32 : undefined)
}

/**

+ 8
- 1
src/assetmanager/gltf/GLTFMaterialExtrasExtension.ts Wyświetl plik

@@ -2,6 +2,7 @@ import type {GLTF, GLTFLoaderPlugin, GLTFParser} from 'three/examples/jsm/loader
import {ThreeSerialization} from '../../utils/serialization'
import {DoubleSide, Material} from 'three'
import type {GLTFExporterPlugin, GLTFWriter} from 'three/examples/jsm/exporters/GLTFExporter'
import {ITexture} from '../../core'

export class GLTFMaterialExtrasExtension {
static readonly WebGiMaterialExtrasExtension = 'WEBGI_material_extras'
@@ -205,9 +206,15 @@ export class GLTFMaterialExtrasExtension {

const resources = this.materialExternalResources[material.uuid]
if (resources) {
Object.entries(resources).forEach(([k, v]) => {
Object.entries(resources).forEach(([k, v]: [string, any|ITexture]) => {
if (k.startsWith('_')) return
let setFlag = false
if (v?.userData && v.userData.embedUrlImagePreviews === undefined) { // check ThreeSerialization texture serialization and GLTFWriter2.processTexture
v.userData.embedUrlImagePreviews = w.options.exporterOptions?.embedUrlImagePreviews
setFlag = true
}
dat[k] = ThreeSerialization.Serialize(v, this.serializedMeta)
if (v?.userData && setFlag) delete v.userData.embedUrlImagePreviews
})
}
if (Object.keys(dat).length > 0) {

+ 32
- 16
src/assetmanager/gltf/GLTFViewerConfigExtension.ts Wyświetl plik

@@ -3,7 +3,9 @@ import type {GLTFWriter} from 'three/examples/jsm/exporters/GLTFExporter'
import {ISerializedViewerConfig, ThreeViewer} from '../../viewer'
import {Group, ImageUtils} from 'three'
import {RGBEPNGLoader} from '../import/RGBEPNGLoader'
import {SerializationResourcesType} from '../../utils/serialization'
import {SerializationResourcesType} from '../../utils'
import {RootSceneImportResult} from '../IAssetImporter'
import {halfFloatToRgbe} from '../../three'

export class GLTFViewerConfigExtension {

@@ -30,13 +32,20 @@ export class GLTFViewerConfigExtension {
}
scene = scenes[0]
}
const resultScene = resultScenes[0]
const resultScene = resultScenes.length > 0 ? resultScenes[0] : undefined

const viewerConfig: Partial<ISerializedViewerConfig> = scene.extensions?.[this.ViewerConfigGLTFExtension]
const viewerConfig1: Partial<ISerializedViewerConfig> = scene.extensions?.[this.ViewerConfigGLTFExtension]
// console.log({...viewerConfig?.resources})

if (!viewerConfig) return {}
if (!viewerConfig1) return {}

const viewerConfig: ISerializedViewerConfig = {
type: 'ThreeViewer',
version: '0',
plugins: [],
assetType: 'config',
...viewerConfig1,
}
if (viewerConfig.resources) {
await this._parseArrayBuffers(viewerConfig.resources, parser)

@@ -45,7 +54,7 @@ export class GLTFViewerConfigExtension {

viewerConfig.resources = await viewer.loadConfigResources(viewerConfig.resources || {}, extraResources)

;(resultScene as any).importedViewerConfig = viewerConfig // todo
if (resultScene) (resultScene as RootSceneImportResult).importedViewerConfig = viewerConfig
}

return viewerConfig
@@ -128,12 +137,13 @@ export class GLTFViewerConfigExtension {
const blob = new Blob([bufferView])
// const blob2 = new Blob([await blob.text()], {type: img.mimeType})
let url = URL.createObjectURL(blob)
if ((buff.encodingVersion || 1) < 2) {
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, true)).data
buff.data = (await new RGBEPNGLoader().parseAsync(url, undefined, encodingVersion < 3)).data
URL.revokeObjectURL(url)
delete buff.encoding
delete buff.encodingVersion
@@ -196,19 +206,19 @@ export class GLTFViewerConfigExtension {
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: any = 3

// todo: can we optimize this? this is too many steps
const d = halfFloatToRgbe(buffer.data, 4)
const d = encodingVersion < 3 ? halfFloatToRgbe2(buffer.data, 4) : halfFloatToRgbe(buffer.data, 4)
const id = new ImageData(d, buffer.width, buffer.height)
// @ts-expect-error patched three
const b64 = ImageUtils.getDataURL(id, true).split(',')[1]
// console.log(b64)

const encodingVersion: any = 2
// @ts-expect-error patched three. todo: update the types
const b64 = ImageUtils.getDataURL(id, true).split(',')[1]

mime = 'image/png'
if (encodingVersion === 1) {
buffer.data = atob(b64)
} else if (encodingVersion === 2) {
} else if (encodingVersion === 2 || encodingVersion === 3) {
buffer.data = Uint8Array.from(atob(b64), c => c.charCodeAt(0))
} else {
throw new Error('Invalid encoding version')
@@ -271,9 +281,15 @@ export class GLTFViewerConfigExtension {

}

// 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
function halfFloatToRgbe(buffer: Uint16Array, channels = 3, res?: Uint8ClampedArray): Uint8ClampedArray {
/**
* @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: Uint16Array, channels = 3, res?: Uint8ClampedArray): Uint8ClampedArray {
let r, g, b, v, s
const l = buffer.byteLength / (channels * 2) | 0
res = res || new Uint8ClampedArray(l * 4)

+ 68
- 0
src/assetmanager/gltf/gltfEncyptionHelpers.ts Wyświetl plik

@@ -0,0 +1,68 @@
import {aesGcmDecrypt, aesGcmEncrypt, getOrCall} from 'ts-browser-helpers'
import {makeGLBFile} from '../../utils'
import {GLTFExporter2Options} from '../export'
import {GLTFBinaryExtension} from 'three/examples/jsm/loaders/GLTFLoader.js'
import {GLTFPreparser} from '../import'

/**
* Sample encryption processor for {@link GLTFExporter2} that wraps the glb in a new glb with encrypted content and encryption metadata.
* Uses AES-GCM ({@link aesGcmEncrypt}) for encryption since it is widely supported across browsers and js environments.
* @param gltf
* @param options
*/
export const glbEncryptionProcessor = async(gltf: ArrayBuffer|any, options: GLTFExporter2Options) => {
if (!gltf || typeof gltf === 'object' || !gltf.byteLength) return gltf
if (!options.encryptKey && window && window.prompt) {
options.encryptKey = window.prompt('GLTFEncryption: Enter encryption key/password') || ''
}
if (!options.encryptKey) {
console.warn('GLTF Export: encryption key not provided, skipping encryption')
return gltf
}
const buffer = await aesGcmEncrypt(new Uint8Array(gltf as ArrayBuffer), options.encryptKey)
return makeGLBFile(buffer, {
asset: {
version: '2.0',
generator: 'ThreePipeGLBWrapper',
encryption: {
type: 'aesgcm',
version: 1,
},
},
})
}

export interface IGLBEncryptionPreparser extends GLTFPreparser{
key: string | ((encryption: any, json: any)=>string|Promise<string>)
}

/**
* Sample encryption preparser for {@link GLTFLoader2} that unwraps the glb container and decrypts the content. The encryption key can be provided in the file or set in this const is prompted from the user.
*/
export const glbEncryptionPreparser: IGLBEncryptionPreparser = {
key: (encryption: any, _: any) => {
return encryption.key || window && window.prompt && window.prompt('GLTFEncryption: Please enter the password/key for this model') || ''
},
async process(dat: string | ArrayBuffer) {
if (typeof dat === 'string') return dat
const prefixBytes = 100
const prefix = new TextDecoder().decode(new Uint8Array(dat, 0, prefixBytes))
if (!prefix.includes('GLBWrapper')) return dat
const binaryExtension = new GLTFBinaryExtension(dat)
const json = JSON.parse(binaryExtension.content || '{}')
let dat2 = binaryExtension.body || dat
const encryption = json.asset?.encryption
if (!encryption) return dat2
const type = encryption.type
const version = encryption.version
if (type === 'aesgcm' && version === 1) {
const key = await getOrCall(this.key, encryption, json) || ''
try {
dat2 = (await aesGcmDecrypt(new Uint8Array(dat2), key)).buffer
} catch (e) {
throw new ErrorEvent('decryption error')
}
}
return dat2
},
}

+ 1
- 0
src/assetmanager/gltf/index.ts Wyświetl plik

@@ -6,3 +6,4 @@ export {GLTFMaterialsDisplacementMapExtension} from './GLTFMaterialsDisplacement
export {GLTFMaterialsLightMapExtension} from './GLTFMaterialsLightMapExtension'
export {GLTFObject3DExtrasExtension} from './GLTFObject3DExtrasExtension'
export {GLTFViewerConfigExtension} from './GLTFViewerConfigExtension'
export {glbEncryptionPreparser, glbEncryptionProcessor, type IGLBEncryptionPreparser} from './gltfEncyptionHelpers'

+ 31
- 2
src/assetmanager/import/GLTFLoader2.ts Wyświetl plik

@@ -13,12 +13,13 @@ import {GLTFMaterialsDisplacementMapExtension} from '../gltf/GLTFMaterialsDispla
import {GLTFMaterialsAlphaMapExtension} from '../gltf/GLTFMaterialsAlphaMapExtension'
import {RootSceneImportResult} from '../IAssetImporter'
import {ILoader} from '../IImporter'
import {glbEncryptionPreparser} from '../gltf'

export class GLTFLoader2 extends GLTFLoader implements ILoader<GLTF, Object3D|undefined> {
isGLTFLoader2 = true
constructor(manager: LoadingManager) {
super(manager)
this.preparsers.push(glbEncryptionPreparser)
}

static ImportExtensions: ((parser: GLTFParser) => GLTFLoaderPlugin)[] = [
@@ -30,7 +31,32 @@ export class GLTFLoader2 extends GLTFLoader implements ILoader<GLTF, Object3D|un
GLTFMaterialsAlphaMapExtension.Import,
]

/**
* Preparsers are run on the arraybuffer/string before parsing to read the glb/gltf data
*/
preparsers: GLTFPreparser[] = []

async preparse(data: ArrayBuffer | string): Promise<ArrayBuffer | string> {
for (const preparser of this.preparsers) {
data = await preparser.process(data)
}
return data
}

parse(data: ArrayBuffer | string, path: string, onLoad: (gltf: GLTF) => void, onError?: (event: ErrorEvent) => void) {
this.preparse.call(this, data)
.then((res: ArrayBuffer | string) => res ? super.parse(res, path, onLoad, onError) : onError && onError(new ErrorEvent('no data')))
.catch((e: any) => {
console.error(e)
if (onError) onError(e ?? new ErrorEvent('unknown error'))
})
}

/**
* This is run post parse to extract the result scene from the GLTF object
* @param res
* @param _
*/
transform(res: GLTF, _: AnyOptions): Object3D|undefined {
// todo: support loading of multiple scenes?
const scene: RootSceneImportResult|undefined = res ? res.scene || !!res.scenes && res.scenes.length > 0 && res.scenes[0] : undefined as any
@@ -49,7 +75,6 @@ export class GLTFLoader2 extends GLTFLoader implements ILoader<GLTF, Object3D|un
return scene
}


register(callback: (parser: GLTFParser) => GLTFLoaderPlugin): this {
return super.register(callback) as this
}
@@ -104,3 +129,7 @@ export class GLTFLoader2 extends GLTFLoader implements ILoader<GLTF, Object3D|un

}

export interface GLTFPreparser{
process(dat: string | ArrayBuffer): Promise<string | ArrayBuffer>
[key: string]: any
}

+ 1
- 1
src/assetmanager/import/RGBEPNGLoader.ts Wyświetl plik

@@ -47,7 +47,7 @@ export class RGBEPNGLoader extends FileLoader {
if (created) URL.revokeObjectURL(url)
let aType: any = Uint8Array
if (this.type === HalfFloatType) aType = Uint16Array
else if (this.type === FloatType) aType = Uint32Array
else if (this.type === FloatType) aType = Float32Array
const buffer = rgbeToHalfFloat(imageData.data, 4, aType, isFloat16Data)
return {data: buffer, width: imageData.width, height: imageData.height}
}

+ 1
- 1
src/assetmanager/import/index.ts Wyświetl plik

@@ -3,6 +3,6 @@ export {SimpleJSONLoader} from './SimpleJSONLoader'
export {MTLLoader2, type MaterialCreator} from './MTLLoader2'
export {OBJLoader2} from './OBJLoader2'
export {ZipLoader} from './ZipLoader'
export {GLTFLoader2} from './GLTFLoader2'
export {GLTFLoader2, type GLTFPreparser} from './GLTFLoader2'
export {DRACOLoader2} from './DRACOLoader2'
export {RGBEPNGLoader} from './RGBEPNGLoader'

+ 1
- 0
src/core/ITexture.ts Wyświetl plik

@@ -4,6 +4,7 @@ import {ChangeEvent} from 'uiconfig.js'

export interface ITextureUserData{
mimeType?: string
embedUrlImagePreviews?: boolean
disposeOnIdle?: boolean // automatically dispose when added to a material and then not used in any material
__appliedMaterials?: Set<IMaterial>
}

+ 27
- 1
src/three/utils/conversion.ts Wyświetl plik

@@ -1,4 +1,4 @@
import {Color, DataTexture, LinearSRGBColorSpace, RGBAFormat, UnsignedByteType, Vector4} from 'three'
import {Color, DataTexture, DataUtils, LinearSRGBColorSpace, RGBAFormat, UnsignedByteType, Vector4} from 'three'

export function dataTextureFromColor(color: Color) {
const dataTexture = new DataTexture(new Uint8Array([Math.floor(color.r * 255), Math.floor(color.g * 255), Math.floor(color.b * 255), 255]), 1, 1, RGBAFormat, UnsignedByteType)
@@ -12,3 +12,29 @@ export function dataTextureFromVec4(color: Vector4) {
dataTexture.needsUpdate = true
return dataTexture
}

/**
* 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
*/
export function halfFloatToRgbe(buffer: Uint16Array, channels = 3, res?: Uint8ClampedArray): Uint8ClampedArray {
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 = DataUtils.fromHalfFloat(buffer[i * channels]) * 65504
g = DataUtils.fromHalfFloat(buffer[i * channels + 1]) * 65504
b = DataUtils.fromHalfFloat(buffer[i * channels + 2]) * 65504
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
}

+ 2
- 2
src/three/utils/index.ts Wyświetl plik

@@ -1,9 +1,9 @@
export {computeScreenSpaceBoundingBox} from './bbox'
export {overrideThreeCache} from './cache'
export {dataTextureFromColor, dataTextureFromVec4} from './conversion'
export {dataTextureFromColor, dataTextureFromVec4, halfFloatToRgbe} from './conversion'
export {uniform, matDefine} from './decorators'
export {getEncodingComponents, getTexelEncoding, getTexelDecoding, getTexelDecoding2, getTexelDecodingFunction, getTexelEncodingFunction, getTextureColorSpaceFromMap} from './encoding'
export {generateUUID, toIndexedGeometry} from './misc'
export {getTextureDataType} from './texture'
export {getTextureDataType, textureToCanvas, textureDataToImageData, textureToDataUrl, imageToCanvas} from './texture'

// export {} from './constants'

+ 99
- 1
src/three/utils/texture.ts Wyświetl plik

@@ -1,4 +1,17 @@
import {FloatType, HalfFloatType, TextureDataType, UnsignedByteType, WebGLRenderer} from 'three'
import {
ColorSpace,
DataTexture,
DataUtils,
FloatType,
HalfFloatType,
LinearSRGBColorSpace,
Texture,
TextureDataType,
UnsignedByteType,
WebGLRenderer,
} from 'three'
import {TextureImageData} from 'three/src/textures/types'
import {LinearToSRGB} from 'ts-browser-helpers'

export function getTextureDataType(renderer?: WebGLRenderer): TextureDataType {
if (!renderer) return UnsignedByteType
@@ -7,3 +20,88 @@ export function getTextureDataType(renderer?: WebGLRenderer): TextureDataType {
return halfFloatSupport ? HalfFloatType : floatSupport ? FloatType : UnsignedByteType
}

export function textureDataToImageData(imgData: TextureImageData | ImageData | {data: Float32Array|Uint16Array|Uint8Array, width: number, height: number}, colorSpace?: ColorSpace, outData?: ImageData) {
const data = outData?.data ?? new Uint8ClampedArray(imgData.height * imgData.width * 4)
const isFloat32 = imgData.data instanceof Float32Array
const isUint16 = imgData.data instanceof Uint16Array

for (let i = 0; i < data.length; i++) {

if (isFloat32) { // Float32
data[i] = imgData.data[i] * 255
} else if (isUint16) { // Uint16 (half float)
data[i] = DataUtils.fromHalfFloat(imgData.data[i]) * 255
} else { // Uint8
data[i] = imgData.data[i]
}

if (colorSpace === LinearSRGBColorSpace) {
data[i] = LinearToSRGB(data[i] / 255.0) * 255
}
// todo: rgbm?

}
return outData ?? new ImageData(data, imgData.width, imgData.height)
}

/**
*
* @param texture
* @param maxWidth
* @param flipY
* @param canvas
*/
export function textureToCanvas(texture: Texture|DataTexture, maxWidth: number, flipY = false, canvas?: HTMLCanvasElement) {
let img
if ((texture as DataTexture).isDataTexture) img = textureDataToImageData(texture.image, texture.colorSpace)
else img = texture.image
return imageToCanvas(img, maxWidth, flipY, canvas)
}

export function imageToCanvas(image: TexImageSource, maxWidth: number, flipY = false, canvas?: HTMLCanvasElement) {
canvas = canvas || document.createElement('canvas')
// resize it to the size of our image
canvas.width = Math.min(maxWidth, image.width as number)
canvas.height = Math.floor(1.0 + canvas.width * (image.height as number) / (image.width as number))

const ctx = canvas.getContext('2d')
if (!ctx) {
console.error('textureToDataUrl: could not get canvas context')
return canvas
}

if (flipY === true) {

ctx.translate(0, canvas.height)
ctx.scale(1, -1)

}

if ((image as ImageData).data !== undefined) { // THREE.DataTexture
const imageData = image as ImageData

if (image.width !== canvas.width || image.height !== canvas.height) {
const tempCanvas = document.createElement('canvas')
tempCanvas.width = image.width
tempCanvas.height = image.height
const tempCtx = tempCanvas.getContext('2d')
if (!tempCtx) {
console.error('textureToDataUrl: could not get temp canvas context')
ctx.putImageData(imageData, 0, 0)
} else {
tempCtx.putImageData(imageData, 0, 0)
ctx.drawImage(tempCanvas, 0, 0, canvas.width, canvas.height)
}
} else {
ctx.putImageData(imageData, 0, 0)
}

} else {
ctx.drawImage(image as any, 0, 0, canvas.width, canvas.height)
}
return canvas
}

export function textureToDataUrl(texture: Texture|DataTexture, maxWidth: number, flipY: boolean, mimeType?: string, quality?: number) {
return textureToCanvas(texture, maxWidth, flipY).toDataURL(mimeType, quality)
}

+ 86
- 0
src/utils/gltf.ts Wyświetl plik

@@ -0,0 +1,86 @@
import type {BlobExt} from '../assetmanager'

/**
* Returns a buffer aligned to 4-byte boundary.
* https://github.com/mrdoob/three.js/blob/4dbd0065f2ec29b89c250d8582f61e9f4792e077/examples/jsm/exporters/GLTFExporter.js#L381
* @param arrayBuffer Buffer to pad
* @param paddingByte (Optional)
* @returns The same buffer if it's already aligned to 4-byte boundary or a new buffer
*/
function getPaddedArrayBuffer(arrayBuffer: ArrayBuffer, paddingByte = 0): ArrayBuffer {
const paddedLength = getPaddedBufferSize(arrayBuffer.byteLength)
if (paddedLength !== arrayBuffer.byteLength) {
const array = new Uint8Array(paddedLength)
array.set(new Uint8Array(arrayBuffer))
if (paddingByte !== 0) {
for (let i = arrayBuffer.byteLength; i < paddedLength; i++) {
array[ i ] = paddingByte
}
}
return array.buffer
}
return arrayBuffer
}


/**
* Get the required size + padding for a buffer, rounded to the next 4-byte boundary.
* https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#data-alignment
*
* @param bufferSize The size the original buffer.
* @returns new buffer size with required padding.
*
*/
function getPaddedBufferSize(bufferSize: number) {

return Math.ceil(bufferSize / 4) * 4

}



// GLB constants
// https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#glb-file-format-specification

const GLB_HEADER_BYTES = 12
const GLB_HEADER_MAGIC = 0x46546C67
const GLB_VERSION = 2

const GLB_CHUNK_PREFIX_BYTES = 8
const GLB_CHUNK_TYPE_JSON = 0x4E4F534A
const GLB_CHUNK_TYPE_BIN = 0x004E4942

// https://github.com/mrdoob/three.js/blob/4dbd0065f2ec29b89c250d8582f61e9f4792e077/examples/jsm/exporters/GLTFExporter.js#L558
export function makeGLBFile(buffers: ArrayBuffer, json: any): BlobExt {
// Binary chunk.
const binaryChunk = getPaddedArrayBuffer(buffers)
const binaryChunkPrefix = new DataView(new ArrayBuffer(GLB_CHUNK_PREFIX_BYTES))
binaryChunkPrefix.setUint32(0, binaryChunk.byteLength, true)
binaryChunkPrefix.setUint32(4, GLB_CHUNK_TYPE_BIN, true)

// JSON chunk.
const jsonChunk = getPaddedArrayBuffer(new TextEncoder().encode(JSON.stringify(json || {})).buffer, 0x20)
const jsonChunkPrefix = new DataView(new ArrayBuffer(GLB_CHUNK_PREFIX_BYTES))
jsonChunkPrefix.setUint32(0, jsonChunk.byteLength, true)
jsonChunkPrefix.setUint32(4, GLB_CHUNK_TYPE_JSON, true)

// GLB header.
const header = new ArrayBuffer(GLB_HEADER_BYTES)
const headerView = new DataView(header)
headerView.setUint32(0, GLB_HEADER_MAGIC, true)
headerView.setUint32(4, GLB_VERSION, true)
const totalByteLength = GLB_HEADER_BYTES
+ jsonChunkPrefix.byteLength + jsonChunk.byteLength
+ binaryChunkPrefix.byteLength + binaryChunk.byteLength
headerView.setUint32(8, totalByteLength, true)

const glbBlob: BlobExt = new Blob([
header,
jsonChunkPrefix,
jsonChunk,
binaryChunkPrefix,
binaryChunk,
], {type: 'model/gltf+binary'}) as any
glbBlob.ext = 'glb'
return glbBlob
}

+ 1
- 0
src/utils/index.ts Wyświetl plik

@@ -5,5 +5,6 @@ export {CustomContextMenu} from './CustomContextMenu'
export {Dropzone, type DropFile, type ListenerCallback, type DropEventType} from './Dropzone'
export {ThreeSerialization, type SerializationMetaType, type SerializationResourcesType, MetaImporter, metaToResources, getEmptyMeta, metaFromResources, convertArrayBufferToStringsInMeta, convertStringsToArrayBuffersInMeta, copyMaterialUserData, copyObject3DUserData, copyUserData, copyTextureUserData} from './serialization'
export {shaderReplaceString} from './shader-helpers'
export {makeGLBFile} from './gltf'

export {serialize, serializable, Serialization} from 'ts-browser-helpers'

+ 63
- 21
src/utils/serialization.ts Wyświetl plik

@@ -18,6 +18,7 @@ import {IAssetImporter} from '../assetmanager'
import {ThreeViewer} from '../viewer'
import {ITexture} from '../core'
import {IRenderTarget, RenderManager} from '../rendering'
import {textureToCanvas} from '../three/utils/texture'

const copier = (c: any) => (v: any, o: any) => o?.copy?.(v) ?? new c().copy(v)
export class ThreeSerialization {
@@ -49,18 +50,28 @@ export class ThreeSerialization {
serialize: (obj: any, meta?: SerializationMetaType) => {
if (!obj?.isTexture) throw new Error('Expected a texture')
if (obj.isRenderTargetTexture) return undefined // todo: support render targets
// if (obj.isRenderTargetTexture && !obj.userData?.serializableRenderTarget) return undefined
if (meta?.textures[obj.uuid]) return {uuid: obj.uuid, resource: 'textures'}
const imgData = obj.source.data
if (obj.userData.rootPath) obj.source.data = null // if root-path exists we don't need to serialize the image data
const hasRootPath = !obj.isRenderTargetTexture && obj.userData.rootPath
if (hasRootPath) {
if (obj.source.data) {
if (!obj.userData.embedUrlImagePreviews) // todo make sure its only Texture, check for svg etc
obj.source.data = null // handled in GLTFWriter2.processImage
else {
obj.source.data = textureToCanvas(obj, 16, obj.flipY) // todo: check flipY
}
}
}
const ud = obj.userData
obj.userData = {} // toJSON will call JSON.stringify, which will serialize userData
const meta2 = {images: {} as any} // in-case meta is undefined
let res = obj.toJSON(meta || meta2)
if (!meta && res.image) res.image = obj.userData.rootPath ? undefined : meta2.images[res.image]
if (!meta && res.image) res.image = hasRootPath && !obj.userData.embedUrlImagePreviews ? undefined : meta2.images[res.image]
obj.userData = ud
res.userData = Serialization.Serialize(copyTextureUserData({}, ud), meta, false)
if (obj.userData.rootPath) {
if (meta) delete meta.images[obj.source.uuid] // because its empty. uuid still stored in the texture.image
if (hasRootPath) {
if (meta && !obj.userData.embedUrlImagePreviews) delete meta.images[obj.source.uuid] // because its empty. uuid still stored in the texture.image
obj.source.data = imgData
}

@@ -612,6 +623,15 @@ export class MetaImporter {
// })
// }

if (Array.isArray(json.textures)) {
console.error('TODO: check file format')
json.textures = json.textures.reduce((acc, cur) => {
if (!cur) return acc
acc[cur.uuid] = cur
return acc
})
}

await MetaImporter.LoadRootPathTextures({textures: json.textures, images: resources.images}, assetImporter)

// console.log(json.textures)
@@ -623,6 +643,20 @@ export class MetaImporter {
}
resources.textures = json.textures ? objLoader.parseTextures(textures, resources.images) : {}

// replace the source of the textures(which has preview) with the loaded images, see {@link LoadRootPathTextures} for `rootPathPromise`
// todo: should this be moved after processRaw?
const textures2 = {...resources.textures}
for (const inpTexture of Object.values(json.textures)) {
inpTexture.rootPathPromise?.then((v: Source|null) => {
if (!v) return
const texture = textures2[inpTexture.uuid]
texture.dispose()
texture.source = v
texture.source.needsUpdate = true
texture.needsUpdate = true
})
}

for (const entry of Object.entries(resources.textures)) {
entry[1] = await assetImporter.processRawSingle(entry[1], {})
if (entry[1]) resources.textures[entry[0]] = entry[1]
@@ -687,25 +721,33 @@ export class MetaImporter {

static async LoadRootPathTextures({textures, images}: Pick<SerializationMetaType, 'textures'|'images'>, importer: IAssetImporter) {
const pms = []
for (const inpTexture of Object.values(textures ?? {} as any) as any as any[]) {
const path = inpTexture?.userData?.rootPath // done separately(from parseTextures2) for hdr etc textures.
if (path && (!inpTexture.image || !images[inpTexture.image])) {
pms.push(importer.importSingle<ITexture>(path, {processRaw: false}).then(texture => {
const source = texture?.source as any
// const image = texture?.image as any
if (!texture || !source) return
// console.log(typeof image)
const source2 = new Source(source.data)
if (inpTexture.image) source2.uuid = inpTexture.image

for (const inpTexture of Array.isArray(textures) ? textures : Object.values(textures ?? {} as any) as any as any[]) {
const path = inpTexture?.userData?.rootPath
const hasImage = inpTexture.image && images[inpTexture.image] // its possible to have both image and rootPath, then the image will be preview image.
if (!path) continue
// console.warn(path, inpTexture, images)
const promise = importer.importSingle<ITexture>(path, {processRaw: false}).then((texture) => {
const source = texture?.source as any
// const image = texture?.image as any
if (!texture || !source) return null
// console.log(typeof image)
const source2 = new Source(source.data)
if (inpTexture.image) source2.uuid = inpTexture.image
inpTexture.image = source2.uuid
if (!hasImage)
images[source2.uuid] = source2
inpTexture.image = source2.uuid
texture.dispose() // todo: what happens when we reimport a cached disposed texture asset, is three.js able to recreate the webgl texture on render?
}).catch((e)=>{
console.error(e)
delete inpTexture.userData.rootPath
}))
}
texture.dispose() // todo: what happens when we reimport a cached disposed texture asset, is three.js able to recreate the webgl texture on render?
return source2
}).catch((e) => {
console.error(e)
delete inpTexture.userData.rootPath
return null
})
if (hasImage) inpTexture.rootPathPromise = promise
else pms.push(promise)
}

await Promise.allSettled(pms)
}


Ładowanie…
Anuluj
Zapisz