From 35624a96de5c97b50d3a45d8b97b28539f930045 Mon Sep 17 00:00:00 2001 From: Thomas O'Neill Date: Wed, 13 Jul 2022 13:40:13 +0100 Subject: [PATCH 1/2] fix: GLTFExporter update --- package.json | 4 +- src/controls/ArcballControls.ts | 6 +- src/controls/TransformControls.ts | 1 - src/exporters/GLTFExporter.ts | 274 +++++++++++++++++++++--------- yarn.lock | 23 ++- 5 files changed, 213 insertions(+), 95 deletions(-) diff --git a/package.json b/package.json index 994be2dc..53ba0bab 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "@storybook/preset-typescript": "^3.0.0", "@types/draco3d": "^1.4.0", "@types/offscreencanvas": "^2019.6.4", - "@types/three": "^0.137.0", + "@types/three": "^0.141.0", "@typescript-eslint/eslint-plugin": "^4.28.0", "@typescript-eslint/parser": "^4.28.0", "babel-loader": "^8.2.3", @@ -90,7 +90,7 @@ "rollup-plugin-node-resolve": "^5.2.0", "rollup-plugin-size-snapshot": "^0.12.0", "rollup-plugin-terser": "^7.0.2", - "three": "^0.137.4", + "three": "^0.142.0", "tslib": "^2.2.0", "typescript": "^4.2.4", "webpack": "^5.65.0" diff --git a/src/controls/ArcballControls.ts b/src/controls/ArcballControls.ts index c9b7c5ec..f28bc627 100644 --- a/src/controls/ArcballControls.ts +++ b/src/controls/ArcballControls.ts @@ -1950,9 +1950,9 @@ class ArcballControls extends EventDispatcher { const curveGeometry = new BufferGeometry().setFromPoints(points) //material - const curveMaterialX = new LineBasicMaterial({ color: 0xff8080, fog: false, transparent: true, opacity: 0.6 }) - const curveMaterialY = new LineBasicMaterial({ color: 0x80ff80, fog: false, transparent: true, opacity: 0.6 }) - const curveMaterialZ = new LineBasicMaterial({ color: 0x8080ff, fog: false, transparent: true, opacity: 0.6 }) + const curveMaterialX = new LineBasicMaterial({ color: 0xff8080, transparent: true, opacity: 0.6 }) + const curveMaterialY = new LineBasicMaterial({ color: 0x80ff80, transparent: true, opacity: 0.6 }) + const curveMaterialZ = new LineBasicMaterial({ color: 0x8080ff, transparent: true, opacity: 0.6 }) //line const gizmoX = new Line(curveGeometry, curveMaterialX) diff --git a/src/controls/TransformControls.ts b/src/controls/TransformControls.ts index a76677e9..dc19c3b5 100644 --- a/src/controls/TransformControls.ts +++ b/src/controls/TransformControls.ts @@ -697,7 +697,6 @@ class TransformControlsGizmo extends Object3D { depthWrite: false, transparent: true, linewidth: 1, - fog: false, toneMapped: false, }) diff --git a/src/exporters/GLTFExporter.ts b/src/exporters/GLTFExporter.ts index a1fa3a43..4d3bc3ec 100644 --- a/src/exporters/GLTFExporter.ts +++ b/src/exporters/GLTFExporter.ts @@ -49,7 +49,10 @@ import { Vector3Tuple, DirectionalLight, PointLight, + Source, SpotLight, + sRGBEncoding, + LinearEncoding, } from 'three' export interface GLTFExporterOptions { @@ -544,12 +547,11 @@ class GLTFWriter { trs?: boolean onlyVisible?: boolean truncateDrawRange?: boolean - embedImages?: boolean maxTextureSize?: number animations?: AnimationClip[] includeCustomExtensions?: boolean } & GLTFExporterOptions - private pending: Promise[] + private pending: Promise[] private buffers: ArrayBuffer[] private byteOffset: number @@ -695,7 +697,7 @@ class GLTFWriter { if (options.binary) { // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#glb-file-format-specification - const reader = new window.FileReader() + const reader = new FileReader() reader.readAsArrayBuffer(blob) reader.onloadend = (): void => { if (reader.result !== null && typeof reader.result !== 'string') { @@ -728,7 +730,7 @@ class GLTFWriter { type: 'application/octet-stream', }) - const glbReader = new window.FileReader() + const glbReader = new FileReader() glbReader.readAsArrayBuffer(glbBlob) glbReader.onloadend = function (): void { if (glbReader.result !== null && typeof glbReader.result !== 'string') { @@ -739,7 +741,7 @@ class GLTFWriter { } } else { if (json.buffers && json.buffers.length > 0) { - const reader = new window.FileReader() + const reader = new FileReader() reader.readAsDataURL(blob) reader.onloadend = function (): void { const base64data = reader.result @@ -796,15 +798,23 @@ class GLTFWriter { } /** - * Assign and return a temporal unique id for an object - * especially which doesn't have .uuid + * Return ids for buffer attributes. * @param {Object} object * @return {Integer} */ - private getUID(object: { [key: string]: any }): number { - if (!this.uids.has(object)) this.uids.set(object, this.uid++) + private getUID(attribute: any, isRelativeCopy?: any): any { + if (this.uids.has(attribute) === false) { + const uids: any = new Map() - return this.uids.get(object)! + uids.set(true, this.uid++) + uids.set(false, this.uid++) + + this.uids.set(attribute, uids) + } + + const uids: any = this.uids.get(attribute) + + return uids.get(isRelativeCopy) } /** @@ -902,6 +912,78 @@ class GLTFWriter { } } + public buildMetalRoughTexture(metalnessMap: Texture, roughnessMap: Texture): Texture { + if (metalnessMap === roughnessMap) return metalnessMap + + function getEncodingConversion(map: Texture): (c: number) => number { + if (map.encoding === sRGBEncoding) { + return function SRGBToLinear(c: number): number { + return c < 0.04045 ? c * 0.0773993808 : Math.pow(c * 0.9478672986 + 0.0521327014, 2.4) + } + } + + return function LinearToLinear(c: number): number { + return c + } + } + + console.warn('THREE.GLTFExporter: Merged metalnessMap and roughnessMap textures.') + + const metalness = metalnessMap?.image + const roughness = roughnessMap?.image + + const width = Math.max(metalness?.width || 0, roughness?.width || 0) + const height = Math.max(metalness?.height || 0, roughness?.height || 0) + + const canvas = this.getCanvas() + canvas.width = width + canvas.height = height + + const context = canvas.getContext('2d') + + if (!context) throw new Error('No context') + + context.fillStyle = '#00ffff' + context.fillRect(0, 0, width, height) + + const composite = context.getImageData(0, 0, width, height) + + if (metalness) { + context.drawImage(metalness, 0, 0, width, height) + + const convert = getEncodingConversion(metalnessMap) + const data = context.getImageData(0, 0, width, height).data + + for (let i = 2; i < data.length; i += 4) { + composite.data[i] = convert(data[i] / 256) * 256 + } + } + + if (roughness) { + context.drawImage(roughness, 0, 0, width, height) + + const convert = getEncodingConversion(roughnessMap) + const data = context.getImageData(0, 0, width, height).data + + for (let i = 1; i < data.length; i += 4) { + composite.data[i] = convert(data[i] / 256) * 256 + } + } + + context.putImageData(composite, 0, 0) + + // + + const reference = metalnessMap || roughnessMap + + const texture = reference.clone() + + texture.source = new Source(canvas) + texture.encoding = LinearEncoding + + return texture + } + /** * Process a buffer to append to the default one. * @param {ArrayBuffer} buffer @@ -1028,7 +1110,7 @@ class GLTFWriter { if (!json.bufferViews) json.bufferViews = [] return new Promise((resolve) => { - const reader = new window.FileReader() + const reader = new FileReader() reader.readAsArrayBuffer(blob) reader.onloadend = (): void => { if (reader.result !== null && typeof reader.result !== 'string' && json.bufferViews !== undefined) { @@ -1142,7 +1224,12 @@ class GLTFWriter { * @param {Boolean} flipY before writing out the image * @return {Integer} Index of the processed texture in the "images" array */ - private processImage(image: ImageRepresentation, format: number, flipY: boolean): number { + private processImage( + image: ImageRepresentation, + format: number, + flipY: boolean, + mimeType: string = 'image/png', + ): number { const writer = this const cache = writer.cache const json = writer.json @@ -1152,7 +1239,6 @@ class GLTFWriter { if (!cache.images.has(image)) cache.images.set(image, {}) const cachedImages = cache.images.get(image) - const mimeType = format === RGBAFormat ? 'image/png' : 'image/jpeg' const key = mimeType + ':flipY/' + flipY.toString() if (cachedImages !== undefined && cachedImages[key] !== undefined) return cachedImages[key] @@ -1161,72 +1247,76 @@ class GLTFWriter { const imageDef: ImageDef = { mimeType: mimeType } - if (options.embedImages && options.maxTextureSize !== undefined) { - const canvas = (this.cachedCanvas = this.cachedCanvas || document.createElement('canvas')) + const canvas = this.getCanvas() - canvas.width = Math.min(image.width, options.maxTextureSize) - canvas.height = Math.min(image.height, options.maxTextureSize) + canvas.width = Math.min(image.width, options.maxTextureSize!) + canvas.height = Math.min(image.height, options.maxTextureSize!) - const ctx = canvas.getContext('2d') + const ctx = canvas.getContext('2d') - if (flipY) { - ctx?.translate(0, canvas.height) - ctx?.scale(1, -1) - } + if (flipY === true) { + ctx?.translate(0, canvas.height) + ctx?.scale(1, -1) + } - if ( - (typeof HTMLImageElement !== 'undefined' && image instanceof HTMLImageElement) || - (typeof HTMLCanvasElement !== 'undefined' && image instanceof HTMLCanvasElement) || - (typeof OffscreenCanvas !== 'undefined' && image instanceof OffscreenCanvas) || - (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap) - ) { - ctx?.drawImage(image, 0, 0, canvas.width, canvas.height) - } else { - if (format !== RGBAFormat) { - console.error('GLTFExporter: Only RGBA format is supported.') - } + if (image instanceof ImageData && image.data !== undefined) { + // THREE.DataTexture - if (image.width > options.maxTextureSize || image.height > options.maxTextureSize) { - console.warn('GLTFExporter: Image size is bigger than maxTextureSize', image) - } + if (format !== RGBAFormat) { + console.error('GLTFExporter: Only RGBAFormat is supported.') + } - const data = new Uint8ClampedArray(image.height * image.width * 4) + if (image.width > options.maxTextureSize! || image.height > options.maxTextureSize!) { + console.warn('GLTFExporter: Image size is bigger than maxTextureSize', image) + } - if (image instanceof ImageData) { - for (let i = 0; i < data.length; i += 4) { - data[i + 0] = image.data[i + 0] - data[i + 1] = image.data[i + 1] - data[i + 2] = image.data[i + 2] - data[i + 3] = image.data[i + 3] - } - } + const data = new Uint8ClampedArray(image.height * image.width * 4) - ctx?.putImageData(new ImageData(data, image.width, image.height), 0, 0) + for (let i = 0; i < data.length; i += 4) { + data[i + 0] = image.data[i + 0] + data[i + 1] = image.data[i + 1] + data[i + 2] = image.data[i + 2] + data[i + 3] = image.data[i + 3] } - if (options.binary) { - pending.push( - new Promise(function (resolve) { - canvas.toBlob(function (blob) { - if (blob !== null) { - writer.processBufferViewImage(blob).then(function (bufferViewIndex) { - imageDef.bufferView = bufferViewIndex - // @ts-expect-error - resolve() - }) - } - }, mimeType) + ctx?.putImageData(new ImageData(data, image.width, image.height), 0, 0) + } else { + ctx?.drawImage(image, 0, 0, canvas.width, canvas.height) + } + + if (options.binary === true) { + pending.push( + this.getToBlobPromise(canvas, mimeType) + .then((blob) => writer.processBufferViewImage(blob)) + .then((bufferViewIndex: number) => { + imageDef.bufferView = bufferViewIndex }), - ) - } else { + ) + } else { + if (canvas instanceof HTMLCanvasElement && canvas.toDataURL !== undefined) { imageDef.uri = canvas.toDataURL(mimeType) + } else { + pending.push( + this.getToBlobPromise(canvas, mimeType) + .then((blob) => { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = (): void => { + return resolve(reader.result as string) + } + reader.onerror = reject + reader.readAsDataURL(blob) + }) + }) + .then((dataURL: string) => { + imageDef.uri = dataURL + }), + ) } - } else if (image instanceof Image) { - imageDef.uri = image.src } const index = json.images.push(imageDef) - 1 - if (cachedImages !== undefined) cachedImages[key] = index + if (cachedImages) cachedImages[key] = index return index } @@ -1263,9 +1353,13 @@ class GLTFWriter { if (!json.textures) json.textures = [] + let mimeType = map.userData.mimeType + + if (mimeType === 'image/webp') mimeType = 'image/png' + const textureDef: TextureDef = { sampler: this.processSampler(map), - source: this.processImage(map.image, map.format, map.flipY), + source: this.processImage(map.image, map.format, map.flipY, mimeType), } if (map.name) textureDef.name = map.name @@ -1335,8 +1429,11 @@ class GLTFWriter { (material instanceof MeshStandardMaterial && material.roughnessMap) ) { if (material.metalnessMap === material.roughnessMap && material.metalnessMap !== null) { - const metalRoughMapDef = { index: this.processTexture(material.metalnessMap) } - this.applyTextureTransform(metalRoughMapDef, material.metalnessMap) + const metalRoughTexture = material.roughnessMap + ? this.buildMetalRoughTexture(material.metalnessMap, material.roughnessMap) + : material.metalnessMap + const metalRoughMapDef = { index: this.processTexture(metalRoughTexture) } + this.applyTextureTransform(metalRoughMapDef, metalRoughTexture) materialDef.pbrMetallicRoughness.metallicRoughnessTexture = metalRoughMapDef } else { console.warn( @@ -1681,7 +1778,6 @@ class GLTFWriter { let cacheKey = this.getUID(geometry.index) if (groups[i].start !== undefined || groups[i].count !== undefined) { - // @ts-expect-error cacheKey += `:${groups[i].start}:${groups[i].count}` } @@ -2100,20 +2196,7 @@ class GLTFWriter { * @return {ArrayBuffer} */ private stringToArrayBuffer(text: string): ArrayBuffer { - if (window.TextEncoder !== undefined) { - return new TextEncoder().encode(text).buffer - } - - const array = new Uint8Array(new ArrayBuffer(text.length)) - - for (let i = 0, il = text.length; i < il; i++) { - const value = text.charCodeAt(i) - - // Replacing multi-byte character with space(0x20). - array[i] = value > 0xff ? 0x20 : value - } - - return array.buffer + return new TextEncoder().encode(text).buffer } private isIdentityMatrix(matrix: Matrix4): boolean { @@ -2187,6 +2270,35 @@ class GLTFWriter { return arrayBuffer } + + private getCanvas(): OffscreenCanvas | HTMLCanvasElement { + if (typeof document === 'undefined' && typeof OffscreenCanvas !== 'undefined') { + return new OffscreenCanvas(1, 1) + } + + return document.createElement('canvas') + } + + private getToBlobPromise(canvas: HTMLCanvasElement | OffscreenCanvas, mimeType: string): Promise { + if (canvas instanceof HTMLCanvasElement && canvas.toBlob !== undefined) { + return new Promise((resolve: any) => canvas.toBlob(resolve, mimeType)) + } + + let quality + + // Blink's implementation of convertToBlob seems to default to a quality level of 100% + // Use the Blink default quality levels of toBlob instead so that file sizes are comparable. + if (mimeType === 'image/jpeg') { + quality = 0.92 + } else if (mimeType === 'image/webp') { + quality = 0.8 + } + + return (canvas as OffscreenCanvas).convertToBlob({ + type: mimeType, + quality: quality, + }) + } } /** diff --git a/yarn.lock b/yarn.lock index aed376cc..c8e79154 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2936,10 +2936,12 @@ resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.8.tgz#b94a4391c85666c7b73299fd3ad79d4faa435310" integrity sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ== -"@types/three@^0.137.0": - version "0.137.0" - resolved "https://registry.yarnpkg.com/@types/three/-/three-0.137.0.tgz#6047e0658262b4de7c464a40288f9071ddd9a6d5" - integrity sha512-Xc5EAlfYmgrCLI/VlSVqiRJAtzhWF0Rw2jSq48nqJy+Hcb5sfDOXyfZn1+RNcHyi9l8CeCAXCZygO8IyeOJVEA== +"@types/three@^0.141.0": + version "0.141.0" + resolved "https://registry.yarnpkg.com/@types/three/-/three-0.141.0.tgz#d9d81a54b28ebc2a56931dfd4d9c54d25c20d6c8" + integrity sha512-OJdKDgTPVBUgc+s74DYoy4aLznbFFC38Xm4ElmU1YwGNgR7GGFVvFCX7lpVgOsT6S1zSJtGdajTsOYE8/xY9nA== + dependencies: + "@types/webxr" "*" "@types/uglify-js@*": version "3.13.1" @@ -2979,6 +2981,11 @@ anymatch "^3.0.0" source-map "^0.6.0" +"@types/webxr@*": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@types/webxr/-/webxr-0.4.0.tgz#ad06c96a324293e0d5175d13dd5ded5931f90ba3" + integrity sha512-LQvrACV3Pj17GpkwHwXuTd733gfY+D7b9mKdrTmLdO7vo7P/o6209Qqtk63y/FCv/lspdmi0pWz6Qe/ull9kQg== + "@types/yargs-parser@*": version "20.2.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.0.tgz#dd3e6699ba3237f0348cd085e4698780204842f9" @@ -11264,10 +11271,10 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= -three@^0.137.4: - version "0.137.4" - resolved "https://registry.yarnpkg.com/three/-/three-0.137.4.tgz#ec73b6a6c40b733d56544b13d0c3cdb0bce5d0a7" - integrity sha512-kUyOZNX+dMbvaS0mGYM1BaXHkHVNQdpryWH8dBg3mn725dJcTo9/5rjyH+OJ8V0r+XbZPz7sncV+c3Gjpc9UBA== +three@^0.142.0: + version "0.142.0" + resolved "https://registry.yarnpkg.com/three/-/three-0.142.0.tgz#89e226a16221f212eb1d40f0786604b711f28aed" + integrity sha512-ESjPO+3geFr+ZUfVMpMnF/eVU2uJPOh0e2ZpMFqjNca1wApS9lJb7E4MjwGIczgt9iuKd8PEm6Pfgp2bJ92Xtg== throttle-debounce@^3.0.1: version "3.0.1" From 861a62fee41f0400e9343494934bad4e3465e298 Mon Sep 17 00:00:00 2001 From: Thomas O'Neill Date: Fri, 22 Jul 2022 13:41:30 +0100 Subject: [PATCH 2/2] pin @types/three --- package.json | 2 +- yarn.lock | 15 ++++----------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 53ba0bab..80ebe95e 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "@storybook/preset-typescript": "^3.0.0", "@types/draco3d": "^1.4.0", "@types/offscreencanvas": "^2019.6.4", - "@types/three": "^0.141.0", + "@types/three": "~0.140.0", "@typescript-eslint/eslint-plugin": "^4.28.0", "@typescript-eslint/parser": "^4.28.0", "babel-loader": "^8.2.3", diff --git a/yarn.lock b/yarn.lock index c8e79154..92881481 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2936,12 +2936,10 @@ resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.8.tgz#b94a4391c85666c7b73299fd3ad79d4faa435310" integrity sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ== -"@types/three@^0.141.0": - version "0.141.0" - resolved "https://registry.yarnpkg.com/@types/three/-/three-0.141.0.tgz#d9d81a54b28ebc2a56931dfd4d9c54d25c20d6c8" - integrity sha512-OJdKDgTPVBUgc+s74DYoy4aLznbFFC38Xm4ElmU1YwGNgR7GGFVvFCX7lpVgOsT6S1zSJtGdajTsOYE8/xY9nA== - dependencies: - "@types/webxr" "*" +"@types/three@~0.140.0": + version "0.140.0" + resolved "https://registry.yarnpkg.com/@types/three/-/three-0.140.0.tgz#3b74a021a69a6e2cd0e2550def15aa4c1c20e924" + integrity sha512-YPJLSIY6uKUOp1k6BZYDq5GtEIdhfeK04UCbc9IPAVbdn/RNjkfrbnyd7smrsNkJhc0IFASLpd3AAYgwqgXKVQ== "@types/uglify-js@*": version "3.13.1" @@ -2981,11 +2979,6 @@ anymatch "^3.0.0" source-map "^0.6.0" -"@types/webxr@*": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@types/webxr/-/webxr-0.4.0.tgz#ad06c96a324293e0d5175d13dd5ded5931f90ba3" - integrity sha512-LQvrACV3Pj17GpkwHwXuTd733gfY+D7b9mKdrTmLdO7vo7P/o6209Qqtk63y/FCv/lspdmi0pWz6Qe/ull9kQg== - "@types/yargs-parser@*": version "20.2.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.0.tgz#dd3e6699ba3237f0348cd085e4698780204842f9"