From 238f7a6d9be1d4f55f74062ae20602b658e7554f Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Sun, 10 Apr 2022 19:21:59 -0400 Subject: [PATCH 01/57] Add avatar-optimizer.js --- avatar-optimizer.js | 490 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 490 insertions(+) create mode 100644 avatar-optimizer.js diff --git a/avatar-optimizer.js b/avatar-optimizer.js new file mode 100644 index 0000000000..903f76dfeb --- /dev/null +++ b/avatar-optimizer.js @@ -0,0 +1,490 @@ +import * as THREE from 'three'; +import {MaxRectsPacker} from 'maxrects-packer'; +import {getRenderer} from './renderer.js'; + +const defaultTextureSize = 4096; +const startAtlasSize = 512; + +const localVector2D = new THREE.Vector2(); +const localVector2D2 = new THREE.Vector2(); +// const localVector4D = new THREE.Vector4(); +// const localVector4D2 = new THREE.Vector4(); + +const _getMergeableObjects = model => { + const renderer = getRenderer(); + + const mergeables = new Map(); + model.traverse(o => { + if (o.isMesh) { + let type; + if (o.isSkinnedMesh) { + type = 'skinnedMesh'; + } else { + type = 'mesh'; + } + + const objectGeometry = o.geometry; + const objectMaterials = Array.isArray(o.material) ? o.material : [o.material]; + for (const objectMaterial of objectMaterials) { + const { + map = null, + emissiveMap = null, + normalMap = null, + shadeTexture = null, + } = objectMaterial; + // console.log('got material', objectMaterial); + + const key = [ + type, + renderer.getProgramCacheKey(o, objectMaterial), + ].join(','); + + let m = mergeables.get(key); + if (!m) { + m = { + type, + material: objectMaterial, + geometries: [], + maps: [], + emissiveMaps: [], + normalMaps: [], + shadeTextures: [], + }; + mergeables.set(key, m); + } + + m.geometries.push(objectGeometry); + m.maps.push(map); + m.emissiveMaps.push(emissiveMap); + m.normalMaps.push(normalMap); + m.shadeTextures.push(shadeTexture); + } + } + }); + return Array.from(mergeables.values()); +}; + +class AttributeLayout { + constructor(name, TypedArrayConstructor, itemSize) { + this.name = name; + this.TypedArrayConstructor = TypedArrayConstructor; + this.itemSize = itemSize; + this.index = 0; + this.count = 0; + this.depth = 0; + } +} +const optimizeAvatarModel = (model, options = {}) => { + const mergeables = _getMergeableObjects(model); + console.log('got mergeables', mergeables); + return mergeables; + + const atlasTextures = !!(options.textures ?? true); + const textureSize = options.textureSize ?? defaultTextureSize; + + const textureTypes = [ + 'map', + 'emissiveMap', + 'normalMap', + ]; + + const _collectObjects = () => { + const meshes = []; + const geometries = []; + const materials = []; + const textures = {}; + for (const textureType of textureTypes) { + textures[textureType] = []; + } + let textureGroupsMap = new WeakMap(); + const skeletons = []; + { + let indexIndex = 0; + model.traverse(node => { + if (node.isMesh && !node.parent?.isBone) { + meshes.push(node); + + const geometry = node.geometry; + geometries.push(geometry); + + const startIndex = indexIndex; + const count = geometry.index.count; + const _pushMaterial = material => { + materials.push(material); + for (const k of textureTypes) { + const texture = material[k]; + if (texture) { + const texturesOfType = textures[k]; + if (!texturesOfType.includes(texture)) { + texturesOfType.push(texture); + } + let textureGroups = textureGroupsMap.get(texture); + if (!textureGroups) { + textureGroups = []; + textureGroupsMap.set(texture, textureGroups); + } + textureGroups.push({ + startIndex, + count, + }); + } + } + }; + + let material = node.material; + if (Array.isArray(material)) { + for (let i = 0; i < material.length; i++) { + _pushMaterial(material[i]); + } + } else { + _pushMaterial(material); + } + + if (node.skeleton) { + if (!skeletons.includes(node.skeleton)) { + skeletons.push(node.skeleton); + } + } + + indexIndex += geometry.index.count; + } + }); + } + return { + meshes, + geometries, + materials, + textures, + textureGroupsMap, + skeletons, + }; + }; + + // collect objects + const { + meshes, + geometries, + materials, + textures, + textureGroupsMap, + skeletons, + } = _collectObjects(); + + // generate atlas layouts + const _packAtlases = () => { + const _attempt = (k, atlasSize) => { + const maxRectsPacker = new MaxRectsPacker(atlasSize, atlasSize, 1); + const rects = textures[k].map(t => { + const w = t.image.width; + const h = t.image.height; + const image = t.image; + const groups = textureGroupsMap.get(t); + return { + width: w, + height: h, + data: { + image, + groups, + }, + }; + }); + maxRectsPacker.addArray(rects); + let oversized = maxRectsPacker.bins.length > 1; + maxRectsPacker.bins.forEach(bin => { + bin.rects.forEach(rect => { + if (rect.oversized) { + oversized = true; + } + }); + }); + if (!oversized) { + return maxRectsPacker; + } else { + return null; + } + }; + + const atlases = {}; + for (const k of textureTypes) { + let atlas; + let atlasSize = startAtlasSize; + while (!(atlas = _attempt(k, atlasSize))) { + atlasSize *= 2; + } + atlases[k] = atlas; + } + return atlases; + }; + const atlases = atlasTextures ? _packAtlases() : null; + + // build attribute layouts + const _makeAttributeLayoutsFromGeometries = geometries => { + // collect attribut layout + const geometry = geometries[0]; + const attributes = geometry.attributes; + const attributeLayouts = []; + for (const attributeName in attributes) { + const attribute = attributes[attributeName]; + const layout = new AttributeLayout(attributeName, attribute.array.constructor, attribute.itemSize); + attributeLayouts.push(layout); + } + + return attributeLayouts; + }; + const _forceGeomtryiesAttributeLayouts = (attributeLayouts, geometries) => { + for (const layout of attributeLayouts) { + for (const g of geometries) { + let gAttribute = g.attributes[layout.name]; + if (!gAttribute) { + if (layout.name === 'skinIndex' || layout.name === 'skinWeight') { + gAttribute = new THREE.BufferAttribute(new Float32Array(g.attributes.position.count * layout.itemSize), layout.itemSize); + g.setAttribute(layout.name, gAttribute); + } else { + throw new Error('unknown layout'); + } + } + layout.count += gAttribute.count * gAttribute.itemSize; + } + } + }; + const _makeMorphAttributeLayoutsFromGeometries = geometries => { + // create morph layouts + const morphAttributeLayouts = []; + for (const geometry of geometries) { + const morphAttributes = geometry.morphAttributes; + for (const morphAttributeName in morphAttributes) { + const morphAttribute = morphAttributes[morphAttributeName]; + let morphLayout = morphAttributeLayouts.find(l => l.name === morphAttributeName); + if (!morphLayout) { + morphLayout = new AttributeLayout(morphAttributeName, morphAttribute[0].array.constructor, morphAttribute[0].itemSize); + morphLayout.depth = morphAttribute.length; + morphAttributeLayouts.push(morphLayout); + } + } + } + + // compute morph layouts sizes + for (const morphLayout of morphAttributeLayouts) { + for (const g of geometries) { + const morphAttribute = g.morphAttributes[morphLayout.name]; + if (morphAttribute) { + morphLayout.count += morphAttribute[0].count * morphAttribute[0].itemSize; + // console.log('morph layout add 1', morphLayout.count, morphAttribute[0].count, morphAttribute[0].itemSize); + } else { + const matchingGeometryAttribute = g.attributes[morphLayout.name]; + if (matchingGeometryAttribute) { + morphLayout.count += matchingGeometryAttribute.count * matchingGeometryAttribute.itemSize; + // console.log('morph layout add 2', morphLayout.count, matchingGeometryAttribute.count, matchingGeometryAttribute.itemSize); + } else { + console.warn('geometry attributes desynced with morph attributes', g.attributes, morphAttribute); + } + } + } + } + return morphAttributeLayouts; + }; + const attributeLayouts = _makeAttributeLayoutsFromGeometries(geometries); + const morphAttributeLayouts = _makeMorphAttributeLayoutsFromGeometries(geometries); + + // validate attribute layouts + for (let i = 0; i < meshes.length; i++) { + const mesh = meshes[i]; + const geometry = mesh.geometry; + if (!geometry.index) { + console.log('no index', mesh); + } + } + if (skeletons.length !== 1) { + console.log('did not have single skeleton', skeletons); + } + + // build geometry + const geometry = new THREE.BufferGeometry(); + // attributes + _forceGeomtryiesAttributeLayouts(attributeLayouts, geometries); + for (const layout of attributeLayouts) { + const attributeData = new layout.TypedArrayConstructor(layout.count); + const attribute = new THREE.BufferAttribute(attributeData, layout.itemSize); + for (const g of geometries) { + const gAttribute = g.attributes[layout.name]; + attributeData.set(gAttribute.array, layout.index); + layout.index += gAttribute.count * gAttribute.itemSize; + } + geometry.setAttribute(layout.name, attribute); + } + // morph attributes + for (const morphLayout of morphAttributeLayouts) { + const morphsArray = Array(morphLayout.depth); + for (let i = 0; i < morphLayout.depth; i++) { + const morphData = new morphLayout.TypedArrayConstructor(morphLayout.count); + let morphDataIndex = 0; + const morphAttribute = new THREE.BufferAttribute(morphData, morphLayout.itemSize); + morphsArray[i] = morphAttribute; + for (const g of geometries) { + let gMorphAttribute = g.morphAttributes[morphLayout.name]; + gMorphAttribute = gMorphAttribute && gMorphAttribute[i]; + if (gMorphAttribute) { + morphData.set(gMorphAttribute.array, morphDataIndex); + morphDataIndex += gMorphAttribute.count * gMorphAttribute.itemSize; + // console.log('new index 1', morphLayout.name, gMorphAttribute.array.some(n => n !== 0), morphDataIndex, gMorphAttribute.count, gMorphAttribute.itemSize); + } else { + const matchingAttribute = g.attributes[morphLayout.name]; + morphDataIndex += matchingAttribute.count * matchingAttribute.itemSize; + // console.log('new index 2', g, morphDataIndex, matchingAttribute.count, matchingAttribute.itemSize); + } + } + if (morphDataIndex !== morphLayout.count) { + console.warn('desynced morph data', morphLayout.name, morphDataIndex, morphLayout.count); + } + } + geometry.morphAttributes[morphLayout.name] = morphsArray; + } + // index + let indexCount = 0; + for (const g of geometries) { + indexCount += g.index.count; + } + const indexData = new Uint32Array(indexCount); + let positionOffset = 0; + let indexOffset = 0; + for (const g of geometries) { + const srcIndexData = g.index.array; + for (let i = 0; i < srcIndexData.length; i++) { + indexData[indexOffset++] = srcIndexData[i] + positionOffset; + } + positionOffset += g.attributes.position.count; + } + geometry.setIndex(new THREE.BufferAttribute(indexData, 1)); + geometry.morphTargetsRelative = true; + + /* const uv3Data = new Float32Array(geometry.attributes.uv.count * 4); + const uv3 = new THREE.BufferAttribute(uv3Data, 4); + geometry.setAttribute('uv3', uv3); */ + + /* // these uvs can be used to color code the mesh by material or texture + const uv4Data = new Float32Array(geometry.attributes.uv.count * 4); + const uv4 = new THREE.BufferAttribute(uv4Data, 4); + geometry.setAttribute('uv4', uv4); */ + + // verify + for (const layout of attributeLayouts) { + if (layout.index !== layout.count) { + console.log('bad layout count', layout.index, layout.count); + } + } + if (indexOffset !== indexCount) { + console.log('bad final index', indexOffset, indexCount); + } + + // draw the atlas + const _drawAtlases = () => { + const seenUvIndexes = new Map(); + const _drawAtlas = atlas => { + const canvas = document.createElement('canvas'); + const canvasSize = Math.min(atlas.width, textureSize); + const canvasScale = canvasSize / atlas.width; + canvas.width = canvasSize; + canvas.height = canvasSize; + const ctx = canvas.getContext('2d'); + + atlas.bins.forEach(bin => { + bin.rects.forEach(rect => { + const {x, y, width: w, height: h, data: {image, groups}} = rect; + // draw the image in the correct box on the canvas + const tx = x * canvasScale; + const ty = y * canvasScale; + const tw = w * canvasScale; + const th = h * canvasScale; + ctx.drawImage(image, 0, 0, image.width, image.height, tx, ty, tw, th); + + // const testUv = new THREE.Vector2(Math.random(), Math.random()); + for (const group of groups) { + const {startIndex, count} = group; + for (let i = 0; i < count; i++) { + const uvIndex = geometry.index.array[startIndex + i]; + + // XXX NOTE: this code is slightly wrong. it will generate a unified uv map (first come first served to the uv index) + // that means that the different maps might get the wrong uv. + // the diffuse map takes priority so it looks ok. + // the right way to do this is to have a separate uv map for each map. + if (!seenUvIndexes.get(uvIndex)) { + seenUvIndexes.set(uvIndex, true); + + localVector2D.fromArray(geometry.attributes.uv.array, uvIndex * 2); + localVector2D.multiply( + localVector2D2.set(tw/canvasSize, th/canvasSize) + ).add( + localVector2D2.set(tx/canvasSize, ty/canvasSize) + ); + localVector2D.toArray(geometry.attributes.uv.array, uvIndex * 2); + + /* localVector4D.set(x/atlas.width, y/atlas.height, w/atlas.width, h/atlas.height); + localVector4D.toArray(geometry.attributes.uv3.array, uvIndex * 4); */ + /* localVector4D.set(testUv.x, testUv.y, testUv.x, testUv.y); + localVector4D.toArray(geometry.attributes.uv4.array, uvIndex * 4); */ + } + } + } + }); + }); + atlas.image = canvas; + + return atlas; + }; + + // generate atlas for each map; they are all separate + const result = {}; + { + let canvasIndex = 0; + for (const k of textureTypes) { + const atlas = atlases[k]; + const atlas2 = _drawAtlas(atlas); + + /* const displaySize = 256; + atlas2.image.style.cssText = `\ + position: fixed; + top: 0; + left: ${canvasIndex * displaySize}px; + width: ${displaySize}px; + height: ${displaySize}px; + z-index: 10; + `; + document.body.appendChild(atlas2.image); */ + + result[k] = atlas2; + + canvasIndex++; + } + } + return result; + }; + const textureAtlases = atlasTextures ? _drawAtlases() : null; + + // create material + // const material = new THREE.MeshStandardMaterial(); + const material = new THREE.MeshBasicMaterial(); + if (atlasTextures) { + for (const k of textureTypes) { + const t = new THREE.Texture(textureAtlases[k].image); + t.flipY = false; + t.needsUpdate = true; + material[k] = t; + } + } + material.roughness = 1; + material.alphaTest = 0.1; + material.transparent = true; + + // create mesh + const crunchedModel = new THREE.SkinnedMesh(geometry, material); + crunchedModel.skeleton = skeletons[0]; + const deepestMorphMesh = meshes.find(m => (m.morphTargetInfluences ? m.morphTargetInfluences.length : 0) === morphAttributeLayouts[0].depth); + crunchedModel.morphTargetDictionary = deepestMorphMesh.morphTargetDictionary; + crunchedModel.morphTargetInfluences = deepestMorphMesh.morphTargetInfluences; + + return crunchedModel; +}; + +export { + optimizeAvatarModel, +}; \ No newline at end of file From faeb7d606788f90d72a279150a2ff86a84a71ddd Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Sun, 10 Apr 2022 19:22:46 -0400 Subject: [PATCH 02/57] Hook in optimized model test in avatars.js high quality mode --- avatars/avatars.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/avatars/avatars.js b/avatars/avatars.js index 5b7896fcba..8e3fdedc76 100644 --- a/avatars/avatars.js +++ b/avatars/avatars.js @@ -22,6 +22,7 @@ import { // avatarInterpolationNumFrames, } from '../constants.js'; // import {FixedTimeStep} from '../interpolants.js'; +import * as avatarOptimizer from '../avatar-optimizer.js'; import * as avatarCruncher from '../avatar-cruncher.js'; import * as avatarSpriter from '../avatar-spriter.js'; // import * as sceneCruncher from '../scene-cruncher.js'; @@ -1380,7 +1381,9 @@ class Avatar { break; } case 3: { - console.log('not implemented'); // XXX + const optimizedModel = avatarOptimizer.optimizeAvatarModel(this.model); + console.log('optimized model', optimizedModel); // XXX + this.model.visible = true; break; } From f16f82b1a86a084bd6284a98b9726a17c554b77f Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Sun, 10 Apr 2022 19:24:08 -0400 Subject: [PATCH 03/57] Exports cleanup --- avatar-cruncher.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/avatar-cruncher.js b/avatar-cruncher.js index ab6c485d59..9dae03432f 100644 --- a/avatar-cruncher.js +++ b/avatar-cruncher.js @@ -457,4 +457,6 @@ const crunchAvatarModel = (model, options = {}) => { return crunchedModel; }; -export {crunchAvatarModel}; \ No newline at end of file +export { + crunchAvatarModel, +}; \ No newline at end of file From 0cd0dbf5baea2f339cb7574fda39dd95baa12166 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Sun, 10 Apr 2022 19:30:22 -0400 Subject: [PATCH 04/57] Hook in avatarOptimizer to metaversefile --- metaversefile-api.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/metaversefile-api.js b/metaversefile-api.js index 4b54b05075..6df3048f1b 100644 --- a/metaversefile-api.js +++ b/metaversefile-api.js @@ -24,6 +24,7 @@ import {makeId, getRandomString, getPlayerPrefix, memoize} from './util.js'; import JSON6 from 'json-6'; import * as materials from './materials.js'; import * as geometries from './geometries.js'; +import * as avatarOptimizer from './avatar-optimizer.js'; import * as avatarCruncher from './avatar-cruncher.js'; import * as avatarSpriter from './avatar-spriter.js'; import {chatManager} from './chat-manager.js'; @@ -427,6 +428,9 @@ metaversefile.setApi({ useVoices() { return voices; }, + useAvatarOptimizer() { + return avatarOptimizer; + }, useAvatarCruncher() { return avatarCruncher; }, From 751943dcb0fe3e1f6bb307c6557d5b1ddb2277bb Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Sun, 10 Apr 2022 19:30:39 -0400 Subject: [PATCH 05/57] Spacing cleanup --- metaversefile-api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metaversefile-api.js b/metaversefile-api.js index 6df3048f1b..423d95a9a2 100644 --- a/metaversefile-api.js +++ b/metaversefile-api.js @@ -905,7 +905,7 @@ metaversefile.setApi({ onWaitPromise(p); } } - + return app; }, createApp(spec) { From cf8d759043d4491fd9d20514fd9bbc0bb6472f41 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Tue, 12 Apr 2022 11:31:12 -0400 Subject: [PATCH 06/57] Bump three --- packages/three | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/three b/packages/three index 3b72a3dc3c..949807f268 160000 --- a/packages/three +++ b/packages/three @@ -1 +1 @@ -Subproject commit 3b72a3dc3c59e4b94a0e5e3cf228601c45ac5cb6 +Subproject commit 949807f268918831eb2497d4dc173f5b618bc19c From 96e3b460ed702a3708d6cf78029657e13f02f207 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Tue, 12 Apr 2022 13:18:51 -0400 Subject: [PATCH 07/57] Major avatar optimizer work --- avatar-optimizer.js | 366 ++++++++++++++++++++++++++------------------ 1 file changed, 215 insertions(+), 151 deletions(-) diff --git a/avatar-optimizer.js b/avatar-optimizer.js index 903f76dfeb..45a0c47bab 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -10,6 +10,12 @@ const localVector2D2 = new THREE.Vector2(); // const localVector4D = new THREE.Vector4(); // const localVector4D2 = new THREE.Vector4(); +const textureTypes = [ + 'map', + 'emissiveMap', + 'normalMap', +]; + const _getMergeableObjects = model => { const renderer = getRenderer(); @@ -30,7 +36,7 @@ const _getMergeableObjects = model => { map = null, emissiveMap = null, normalMap = null, - shadeTexture = null, + // shadeTexture = null, } = objectMaterial; // console.log('got material', objectMaterial); @@ -48,7 +54,7 @@ const _getMergeableObjects = model => { maps: [], emissiveMaps: [], normalMaps: [], - shadeTextures: [], + // shadeTextures: [], }; mergeables.set(key, m); } @@ -57,7 +63,7 @@ const _getMergeableObjects = model => { m.maps.push(map); m.emissiveMaps.push(emissiveMap); m.normalMaps.push(normalMap); - m.shadeTextures.push(shadeTexture); + // m.shadeTextures.push(shadeTexture); } } }); @@ -75,18 +81,216 @@ class AttributeLayout { } } const optimizeAvatarModel = (model, options = {}) => { + const atlasTextures = !!(options.textures ?? true); + const textureSize = options.textureSize ?? defaultTextureSize; + const mergeables = _getMergeableObjects(model); console.log('got mergeables', mergeables); - return mergeables; - const atlasTextures = !!(options.textures ?? true); - const textureSize = options.textureSize ?? defaultTextureSize; + const _mergeMesh = (mergeable, mergeableIndex) => { + const { + type, + geometries, + maps, + emissiveMaps, + normalMaps, + } = mergeable; + + // compute texture sizes + const textureSizes = maps.map((map, i) => { + const emissiveMap = emissiveMaps[i]; + const normalMap = normalMaps[i]; + + const maxSize = new THREE.Vector2(0, 0); + if (map) { + maxSize.x = Math.max(maxSize.x, map.image.width); + maxSize.y = Math.max(maxSize.y, map.image.height); + } + if (emissiveMap) { + maxSize.x = Math.max(maxSize.x, emissiveMap.image.width); + maxSize.y = Math.max(maxSize.y, emissiveMap.image.height); + } + if (normalMap) { + maxSize.x = Math.max(maxSize.x, normalMap.image.width); + maxSize.y = Math.max(maxSize.y, normalMap.image.height); + } + return maxSize; + }); + + // generate atlas layouts + const _packAtlases = () => { + const _attemptPack = (textureSizes, atlasSize) => { + const maxRectsPacker = new MaxRectsPacker(atlasSize, atlasSize, 1); + const rects = textureSizes.map((textureSize, index) => { + // const w = t.image.width; + // const h = t.image.height; + // const image = t.image; + const {x: width, y: height} = textureSize; + return { + width, + height, + data: { + index, + }, + }; + }); + maxRectsPacker.addArray(rects); + let oversized = maxRectsPacker.bins.length > 1; + maxRectsPacker.bins.forEach(bin => { + bin.rects.forEach(rect => { + if (rect.oversized) { + oversized = true; + } + }); + }); + if (!oversized) { + return maxRectsPacker; + } else { + return null; + } + }; + const _makeEmptyAtlas = () => new MaxRectsPacker(0, 0, 1); + + const hasTextures = textureSizes.some(textureSize => textureSize.x > 0 || textureSize.y > 0); + if (hasTextures) { + let atlas; + let atlasSize = startAtlasSize; + while (!(atlas = _attemptPack(textureSizes, atlasSize))) { + atlasSize *= 2; + } + return atlas; + } else { + return _makeEmptyAtlas(); + } + }; + const atlas = atlasTextures ? _packAtlases() : null; + + // draw atlas images + const _drawAtlasImages = atlas => { + const _drawAtlasImage = textureType => { + const canvas = document.createElement('canvas'); + const canvasSize = Math.min(atlas.width, textureSize); + const canvasScale = canvasSize / atlas.width; + canvas.width = canvasSize; + canvas.height = canvasSize; + const ctx = canvas.getContext('2d'); + + atlas.bins.forEach(bin => { + bin.rects.forEach(rect => { + const {x, y, width: w, height: h, data: {index}} = rect; + const textures = mergeable[`${textureType}s`]; + const texture = textures[index]; + if (texture) { + const image = texture.image; + + // draw the image in the correct box on the canvas + const tx = x * canvasScale; + const ty = y * canvasScale; + const tw = w * canvasScale; + const th = h * canvasScale; + ctx.drawImage(image, 0, 0, image.width, image.height, tx, ty, tw, th); + } + }); + }); + + return canvas; + }; + + const atlasImages = {}; + for (const textureType of textureTypes) { + const atlasImage = _drawAtlasImage(textureType); + atlasImages[textureType] = atlasImage; + } + return atlasImages; + }; + const atlasImages = atlasTextures ? _drawAtlasImages(atlas) : null; + + // XXX debug + { + const debugWidth = 300; + let textureTypeIndex = 0; + for (const textureType of textureTypes) { + const atlasImage = atlasImages[textureType]; + atlasImage.style.cssText = `\ + position: fixed; + top: ${mergeableIndex * debugWidth}px; + left: ${textureTypeIndex * debugWidth}px; + min-width: ${debugWidth}px; + max-width: ${debugWidth}px; + min-height: ${debugWidth}px; + z-index: 100; + `; + document.body.appendChild(atlasImage); + textureTypeIndex++; + } + } + + return new THREE.Mesh(); + }; + const mergedMeshes = mergeables.map((mergeable, i) => _mergeMesh(mergeable, i)); + + /* // draw atlas textures + const _drawAtlases = atlases => { + const seenUvIndexes = new Map(); + const _drawAtlas = atlas => { + const canvas = document.createElement('canvas'); + const canvasSize = Math.min(atlas.width, textureSize); + const canvasScale = canvasSize / atlas.width; + canvas.width = canvasSize; + canvas.height = canvasSize; + const ctx = canvas.getContext('2d'); + + atlas.bins.forEach(bin => { + bin.rects.forEach(rect => { + const {x, y, width: w, height: h, data: {image, groups}} = rect; + // draw the image in the correct box on the canvas + const tx = x * canvasScale; + const ty = y * canvasScale; + const tw = w * canvasScale; + const th = h * canvasScale; + ctx.drawImage(image, 0, 0, image.width, image.height, tx, ty, tw, th); + + // const testUv = new THREE.Vector2(Math.random(), Math.random()); + for (const group of groups) { + const {startIndex, count} = group; + for (let i = 0; i < count; i++) { + const uvIndex = geometry.index.array[startIndex + i]; + + // XXX NOTE: this code is slightly wrong. it will generate a unified uv map (first come first served to the uv index) + // that means that the different maps might get the wrong uv. + // the diffuse map takes priority so it looks ok. + // the right way to do this is to have a separate uv map for each map. + if (!seenUvIndexes.get(uvIndex)) { + seenUvIndexes.set(uvIndex, true); + + localVector2D.fromArray(geometry.attributes.uv.array, uvIndex * 2); + localVector2D.multiply( + localVector2D2.set(tw/canvasSize, th/canvasSize) + ).add( + localVector2D2.set(tx/canvasSize, ty/canvasSize) + ); + localVector2D.toArray(geometry.attributes.uv.array, uvIndex * 2); + } + } + } + }); + }); + atlas.image = canvas; + + return atlas; + }; - const textureTypes = [ - 'map', - 'emissiveMap', - 'normalMap', - ]; + const atlasImages = {}; + for (const textureType of textureTypes) { + const atlas = atlases[textureType]; + const atlasImage = _drawAtlas(atlas); + atlasImages[textureType] = atlasImage; + } + return atlasImages; + }; + _remapGeometryUvs(); */ + + return mergedMeshes; const _collectObjects = () => { const meshes = []; @@ -170,53 +374,6 @@ const optimizeAvatarModel = (model, options = {}) => { skeletons, } = _collectObjects(); - // generate atlas layouts - const _packAtlases = () => { - const _attempt = (k, atlasSize) => { - const maxRectsPacker = new MaxRectsPacker(atlasSize, atlasSize, 1); - const rects = textures[k].map(t => { - const w = t.image.width; - const h = t.image.height; - const image = t.image; - const groups = textureGroupsMap.get(t); - return { - width: w, - height: h, - data: { - image, - groups, - }, - }; - }); - maxRectsPacker.addArray(rects); - let oversized = maxRectsPacker.bins.length > 1; - maxRectsPacker.bins.forEach(bin => { - bin.rects.forEach(rect => { - if (rect.oversized) { - oversized = true; - } - }); - }); - if (!oversized) { - return maxRectsPacker; - } else { - return null; - } - }; - - const atlases = {}; - for (const k of textureTypes) { - let atlas; - let atlasSize = startAtlasSize; - while (!(atlas = _attempt(k, atlasSize))) { - atlasSize *= 2; - } - atlases[k] = atlas; - } - return atlases; - }; - const atlases = atlasTextures ? _packAtlases() : null; - // build attribute layouts const _makeAttributeLayoutsFromGeometries = geometries => { // collect attribut layout @@ -357,15 +514,6 @@ const optimizeAvatarModel = (model, options = {}) => { geometry.setIndex(new THREE.BufferAttribute(indexData, 1)); geometry.morphTargetsRelative = true; - /* const uv3Data = new Float32Array(geometry.attributes.uv.count * 4); - const uv3 = new THREE.BufferAttribute(uv3Data, 4); - geometry.setAttribute('uv3', uv3); */ - - /* // these uvs can be used to color code the mesh by material or texture - const uv4Data = new Float32Array(geometry.attributes.uv.count * 4); - const uv4 = new THREE.BufferAttribute(uv4Data, 4); - geometry.setAttribute('uv4', uv4); */ - // verify for (const layout of attributeLayouts) { if (layout.index !== layout.count) { @@ -376,90 +524,6 @@ const optimizeAvatarModel = (model, options = {}) => { console.log('bad final index', indexOffset, indexCount); } - // draw the atlas - const _drawAtlases = () => { - const seenUvIndexes = new Map(); - const _drawAtlas = atlas => { - const canvas = document.createElement('canvas'); - const canvasSize = Math.min(atlas.width, textureSize); - const canvasScale = canvasSize / atlas.width; - canvas.width = canvasSize; - canvas.height = canvasSize; - const ctx = canvas.getContext('2d'); - - atlas.bins.forEach(bin => { - bin.rects.forEach(rect => { - const {x, y, width: w, height: h, data: {image, groups}} = rect; - // draw the image in the correct box on the canvas - const tx = x * canvasScale; - const ty = y * canvasScale; - const tw = w * canvasScale; - const th = h * canvasScale; - ctx.drawImage(image, 0, 0, image.width, image.height, tx, ty, tw, th); - - // const testUv = new THREE.Vector2(Math.random(), Math.random()); - for (const group of groups) { - const {startIndex, count} = group; - for (let i = 0; i < count; i++) { - const uvIndex = geometry.index.array[startIndex + i]; - - // XXX NOTE: this code is slightly wrong. it will generate a unified uv map (first come first served to the uv index) - // that means that the different maps might get the wrong uv. - // the diffuse map takes priority so it looks ok. - // the right way to do this is to have a separate uv map for each map. - if (!seenUvIndexes.get(uvIndex)) { - seenUvIndexes.set(uvIndex, true); - - localVector2D.fromArray(geometry.attributes.uv.array, uvIndex * 2); - localVector2D.multiply( - localVector2D2.set(tw/canvasSize, th/canvasSize) - ).add( - localVector2D2.set(tx/canvasSize, ty/canvasSize) - ); - localVector2D.toArray(geometry.attributes.uv.array, uvIndex * 2); - - /* localVector4D.set(x/atlas.width, y/atlas.height, w/atlas.width, h/atlas.height); - localVector4D.toArray(geometry.attributes.uv3.array, uvIndex * 4); */ - /* localVector4D.set(testUv.x, testUv.y, testUv.x, testUv.y); - localVector4D.toArray(geometry.attributes.uv4.array, uvIndex * 4); */ - } - } - } - }); - }); - atlas.image = canvas; - - return atlas; - }; - - // generate atlas for each map; they are all separate - const result = {}; - { - let canvasIndex = 0; - for (const k of textureTypes) { - const atlas = atlases[k]; - const atlas2 = _drawAtlas(atlas); - - /* const displaySize = 256; - atlas2.image.style.cssText = `\ - position: fixed; - top: 0; - left: ${canvasIndex * displaySize}px; - width: ${displaySize}px; - height: ${displaySize}px; - z-index: 10; - `; - document.body.appendChild(atlas2.image); */ - - result[k] = atlas2; - - canvasIndex++; - } - } - return result; - }; - const textureAtlases = atlasTextures ? _drawAtlases() : null; - // create material // const material = new THREE.MeshStandardMaterial(); const material = new THREE.MeshBasicMaterial(); From 348d771be5fa6900108e55e57448de65ae3d51d8 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Tue, 12 Apr 2022 14:25:57 -0400 Subject: [PATCH 08/57] More major avatar optimizer work --- avatar-optimizer.js | 139 ++++++++++++++++++++++++-------------------- 1 file changed, 75 insertions(+), 64 deletions(-) diff --git a/avatar-optimizer.js b/avatar-optimizer.js index 45a0c47bab..371a1de228 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -16,6 +16,23 @@ const textureTypes = [ 'normalMap', ]; +class AttributeLayout { + constructor(name, TypedArrayConstructor, itemSize) { + this.name = name; + this.TypedArrayConstructor = TypedArrayConstructor; + this.itemSize = itemSize; + this.index = 0; + this.count = 0; + this.depth = 0; + } +} +class MorphAttributeLayout extends AttributeLayout { + constructor(name, TypedArrayConstructor, itemSize, depth) { + super(name, TypedArrayConstructor, itemSize); + this.depth = depth; + } +} + const _getMergeableObjects = model => { const renderer = getRenderer(); @@ -70,16 +87,6 @@ const _getMergeableObjects = model => { return Array.from(mergeables.values()); }; -class AttributeLayout { - constructor(name, TypedArrayConstructor, itemSize) { - this.name = name; - this.TypedArrayConstructor = TypedArrayConstructor; - this.itemSize = itemSize; - this.index = 0; - this.count = 0; - this.depth = 0; - } -} const optimizeAvatarModel = (model, options = {}) => { const atlasTextures = !!(options.textures ?? true); const textureSize = options.textureSize ?? defaultTextureSize; @@ -225,12 +232,67 @@ const optimizeAvatarModel = (model, options = {}) => { } } + // build attribute layouts + const _makeAttributeLayoutsFromGeometries = geometries => { + const attributeLayouts = []; + for (const geometry of geometries) { + const attributes = geometry.attributes; + for (const attributeName in attributes) { + const attribute = attributes[attributeName]; + let layout = attributeLayouts.find(layout => layout.name === attributeName); + if (layout) { + // sanity check that item size is the same + if (layout.itemSize !== attribute.itemSize) { + throw new Error(`attribute ${attributeName} has different itemSize: ${layout.itemSize}, ${attribute.itemSize}`); + } + } else { + layout = new AttributeLayout( + attributeName, + attribute.array.constructor, + attribute.itemSize + ); + attributeLayouts.push(layout); + } + + layout.count += attribute.count * attribute.itemSize; + } + } + return attributeLayouts; + }; + const attributeLayouts = _makeAttributeLayoutsFromGeometries(geometries); + + const _makeMorphAttributeLayoutsFromGeometries = geometries => { + // create morph layouts + const morphAttributeLayouts = []; + for (const geometry of geometries) { + const morphAttributes = geometry.morphAttributes; + for (const morphAttributeName in morphAttributes) { + const morphAttribute = morphAttributes[morphAttributeName]; + let morphLayout = morphAttributeLayouts.find(l => l.name === morphAttributeName); + if (!morphLayout) { + morphLayout = new MorphAttributeLayout( + morphAttributeName, + morphAttribute[0].array.constructor, + morphAttribute[0].itemSize, + morphAttribute.length + ); + morphAttributeLayouts.push(morphLayout); + } + + morphLayout.count += morphAttribute[0].count * morphAttribute[0].itemSize; + } + } + return morphAttributeLayouts; + }; + const morphAttributeLayouts = _makeMorphAttributeLayoutsFromGeometries(geometries); + console.log('got attribute layouts', attributeLayouts, morphAttributeLayouts); + return new THREE.Mesh(); }; const mergedMeshes = mergeables.map((mergeable, i) => _mergeMesh(mergeable, i)); /* // draw atlas textures - const _drawAtlases = atlases => { + const _remapGeometryUvs = atlases => { const seenUvIndexes = new Map(); const _drawAtlas = atlas => { const canvas = document.createElement('canvas'); @@ -374,21 +436,7 @@ const optimizeAvatarModel = (model, options = {}) => { skeletons, } = _collectObjects(); - // build attribute layouts - const _makeAttributeLayoutsFromGeometries = geometries => { - // collect attribut layout - const geometry = geometries[0]; - const attributes = geometry.attributes; - const attributeLayouts = []; - for (const attributeName in attributes) { - const attribute = attributes[attributeName]; - const layout = new AttributeLayout(attributeName, attribute.array.constructor, attribute.itemSize); - attributeLayouts.push(layout); - } - - return attributeLayouts; - }; - const _forceGeomtryiesAttributeLayouts = (attributeLayouts, geometries) => { + const _forceGeometriesAttributeLayouts = (attributeLayouts, geometries) => { for (const layout of attributeLayouts) { for (const g of geometries) { let gAttribute = g.attributes[layout.name]; @@ -400,45 +448,8 @@ const optimizeAvatarModel = (model, options = {}) => { throw new Error('unknown layout'); } } - layout.count += gAttribute.count * gAttribute.itemSize; - } - } - }; - const _makeMorphAttributeLayoutsFromGeometries = geometries => { - // create morph layouts - const morphAttributeLayouts = []; - for (const geometry of geometries) { - const morphAttributes = geometry.morphAttributes; - for (const morphAttributeName in morphAttributes) { - const morphAttribute = morphAttributes[morphAttributeName]; - let morphLayout = morphAttributeLayouts.find(l => l.name === morphAttributeName); - if (!morphLayout) { - morphLayout = new AttributeLayout(morphAttributeName, morphAttribute[0].array.constructor, morphAttribute[0].itemSize); - morphLayout.depth = morphAttribute.length; - morphAttributeLayouts.push(morphLayout); - } - } - } - - // compute morph layouts sizes - for (const morphLayout of morphAttributeLayouts) { - for (const g of geometries) { - const morphAttribute = g.morphAttributes[morphLayout.name]; - if (morphAttribute) { - morphLayout.count += morphAttribute[0].count * morphAttribute[0].itemSize; - // console.log('morph layout add 1', morphLayout.count, morphAttribute[0].count, morphAttribute[0].itemSize); - } else { - const matchingGeometryAttribute = g.attributes[morphLayout.name]; - if (matchingGeometryAttribute) { - morphLayout.count += matchingGeometryAttribute.count * matchingGeometryAttribute.itemSize; - // console.log('morph layout add 2', morphLayout.count, matchingGeometryAttribute.count, matchingGeometryAttribute.itemSize); - } else { - console.warn('geometry attributes desynced with morph attributes', g.attributes, morphAttribute); - } - } } } - return morphAttributeLayouts; }; const attributeLayouts = _makeAttributeLayoutsFromGeometries(geometries); const morphAttributeLayouts = _makeMorphAttributeLayoutsFromGeometries(geometries); @@ -458,7 +469,7 @@ const optimizeAvatarModel = (model, options = {}) => { // build geometry const geometry = new THREE.BufferGeometry(); // attributes - _forceGeomtryiesAttributeLayouts(attributeLayouts, geometries); + _forceGeometriesAttributeLayouts(attributeLayouts, geometries); for (const layout of attributeLayouts) { const attributeData = new layout.TypedArrayConstructor(layout.count); const attribute = new THREE.BufferAttribute(attributeData, layout.itemSize); From 8bc3a87777cad267b56569af37d749b83c5645d7 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Tue, 12 Apr 2022 14:52:27 -0400 Subject: [PATCH 09/57] More major avatar optimizer work --- avatar-optimizer.js | 208 +++++++++++++++++++++++++++----------------- 1 file changed, 130 insertions(+), 78 deletions(-) diff --git a/avatar-optimizer.js b/avatar-optimizer.js index 371a1de228..9e226fb8a7 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -21,21 +21,22 @@ class AttributeLayout { this.name = name; this.TypedArrayConstructor = TypedArrayConstructor; this.itemSize = itemSize; - this.index = 0; + this.count = 0; - this.depth = 0; } } class MorphAttributeLayout extends AttributeLayout { - constructor(name, TypedArrayConstructor, itemSize, depth) { + constructor(name, TypedArrayConstructor, itemSize, arraySize) { super(name, TypedArrayConstructor, itemSize); - this.depth = depth; + this.arraySize = arraySize; } } const _getMergeableObjects = model => { const renderer = getRenderer(); + console.log('got model', model); + const mergeables = new Map(); model.traverse(o => { if (o.isMesh) { @@ -56,6 +57,7 @@ const _getMergeableObjects = model => { // shadeTexture = null, } = objectMaterial; // console.log('got material', objectMaterial); + const skeleton = o.skeleton ?? null; const key = [ type, @@ -72,6 +74,7 @@ const _getMergeableObjects = model => { emissiveMaps: [], normalMaps: [], // shadeTextures: [], + skeletons: [], }; mergeables.set(key, m); } @@ -81,6 +84,7 @@ const _getMergeableObjects = model => { m.emissiveMaps.push(emissiveMap); m.normalMaps.push(normalMap); // m.shadeTextures.push(shadeTexture); + m.skeletons.push(skeleton); } } }); @@ -101,6 +105,7 @@ const optimizeAvatarModel = (model, options = {}) => { maps, emissiveMaps, normalMaps, + skeletons, } = mergeable; // compute texture sizes @@ -287,7 +292,126 @@ const optimizeAvatarModel = (model, options = {}) => { const morphAttributeLayouts = _makeMorphAttributeLayoutsFromGeometries(geometries); console.log('got attribute layouts', attributeLayouts, morphAttributeLayouts); - return new THREE.Mesh(); + const _forceGeometriesAttributeLayouts = (attributeLayouts, geometries) => { + for (const layout of attributeLayouts) { + for (const g of geometries) { + let gAttribute = g.attributes[layout.name]; + if (!gAttribute) { + if (layout.name === 'skinIndex' || layout.name === 'skinWeight') { + gAttribute = new THREE.BufferAttribute(new Float32Array(g.attributes.position.count * layout.itemSize), layout.itemSize); + g.setAttribute(layout.name, gAttribute); + } else { + throw new Error(`unknown layout ${layout.name}`); + } + } + } + } + }; + const _mergeGeometryies = geometries => { + const geometry = new THREE.BufferGeometry(); + + // attributes + _forceGeometriesAttributeLayouts(attributeLayouts, geometries); + for (const layout of attributeLayouts) { + const attributeData = new layout.TypedArrayConstructor(layout.count); + const attribute = new THREE.BufferAttribute(attributeData, layout.itemSize); + let attributeDataIndex = 0; + for (const g of geometries) { + const gAttribute = g.attributes[layout.name]; + attributeData.set(gAttribute.array, layout.index); + attributeDataIndex += gAttribute.count * gAttribute.itemSize; + } + geometry.setAttribute(layout.name, attribute); + } + + // morph attributes + for (const morphLayout of morphAttributeLayouts) { + const morphsArray = Array(morphLayout.arraySize); + for (let i = 0; i < morphLayout.arraySize; i++) { + const morphData = new morphLayout.TypedArrayConstructor(morphLayout.count); + const morphAttribute = new THREE.BufferAttribute(morphData, morphLayout.itemSize); + morphsArray[i] = morphAttribute; + let morphDataIndex = 0; + for (const g of geometries) { + let gMorphAttribute = g.morphAttributes[morphLayout.name]; + gMorphAttribute = gMorphAttribute?.[i]; + if (gMorphAttribute) { + morphData.set(gMorphAttribute.array, morphDataIndex); + morphDataIndex += gMorphAttribute.count * gMorphAttribute.itemSize; + } else { + const matchingAttribute = g.attributes[morphLayout.name]; + morphDataIndex += matchingAttribute.count * matchingAttribute.itemSize; + } + } + if (morphDataIndex !== morphLayout.count) { + console.warn('desynced morph data', morphLayout.name, morphDataIndex, morphLayout.count); + } + } + geometry.morphAttributes[morphLayout.name] = morphsArray; + } + + // index + let indexCount = 0; + for (const g of geometries) { + indexCount += g.index.count; + } + const indexData = new Uint32Array(indexCount); + + let positionOffset = 0; + let indexOffset = 0; + for (const g of geometries) { + const srcIndexData = g.index.array; + for (let i = 0; i < srcIndexData.length; i++) { + indexData[indexOffset++] = srcIndexData[i] + positionOffset; + } + positionOffset += g.attributes.position.count; + } + geometry.setIndex(new THREE.BufferAttribute(indexData, 1)); + geometry.morphTargetsRelative = true; + + return geometry; + }; + const geometry = _mergeGeometryies(geometries); + console.log('got geometry', geometry); + + const _makeMaterial = () => { + // XXX use the original material, but set the new textures + const material = new THREE.MeshBasicMaterial(); + if (atlasTextures) { + for (const textureType of textureTypes) { + const t = new THREE.Texture(atlasImages[textureType].image); + t.flipY = false; + t.needsUpdate = true; + material[textureType] = t; + } + } + material.roughness = 1; + material.alphaTest = 0.1; + material.transparent = true; + }; + const material = _makeMaterial(); + console.log('got material', material); + + const _makeMesh = () => { + if (type === 'mesh') { + const mesh = new THREE.Mesh(geometry, material); + return mesh; + } else if (type === 'skinnedMesh') { + const skinnedMesh = new THREE.SkinnedMesh(geometry, material); + skinnedMesh.skeleton = skeletons[0]; + // XXX get this from the list accumulated during the initial scan + const deepestMorphMesh = meshes.find(m => (m.morphTargetInfluences ? m.morphTargetInfluences.length : 0) === morphAttributeLayouts[0].depth); + skinnedMesh.morphTargetDictionary = deepestMorphMesh.morphTargetDictionary; + skinnedMesh.morphTargetInfluences = deepestMorphMesh.morphTargetInfluences; + return skinnedMesh; + } else { + throw new Error(`unknown type ${type}`); + } + }; + const mesh = _makeMesh(); + console.log('got mesh', mesh); + + return mesh; }; const mergedMeshes = mergeables.map((mergeable, i) => _mergeMesh(mergeable, i)); @@ -436,21 +560,6 @@ const optimizeAvatarModel = (model, options = {}) => { skeletons, } = _collectObjects(); - const _forceGeometriesAttributeLayouts = (attributeLayouts, geometries) => { - for (const layout of attributeLayouts) { - for (const g of geometries) { - let gAttribute = g.attributes[layout.name]; - if (!gAttribute) { - if (layout.name === 'skinIndex' || layout.name === 'skinWeight') { - gAttribute = new THREE.BufferAttribute(new Float32Array(g.attributes.position.count * layout.itemSize), layout.itemSize); - g.setAttribute(layout.name, gAttribute); - } else { - throw new Error('unknown layout'); - } - } - } - } - }; const attributeLayouts = _makeAttributeLayoutsFromGeometries(geometries); const morphAttributeLayouts = _makeMorphAttributeLayoutsFromGeometries(geometries); @@ -466,64 +575,7 @@ const optimizeAvatarModel = (model, options = {}) => { console.log('did not have single skeleton', skeletons); } - // build geometry - const geometry = new THREE.BufferGeometry(); - // attributes - _forceGeometriesAttributeLayouts(attributeLayouts, geometries); - for (const layout of attributeLayouts) { - const attributeData = new layout.TypedArrayConstructor(layout.count); - const attribute = new THREE.BufferAttribute(attributeData, layout.itemSize); - for (const g of geometries) { - const gAttribute = g.attributes[layout.name]; - attributeData.set(gAttribute.array, layout.index); - layout.index += gAttribute.count * gAttribute.itemSize; - } - geometry.setAttribute(layout.name, attribute); - } - // morph attributes - for (const morphLayout of morphAttributeLayouts) { - const morphsArray = Array(morphLayout.depth); - for (let i = 0; i < morphLayout.depth; i++) { - const morphData = new morphLayout.TypedArrayConstructor(morphLayout.count); - let morphDataIndex = 0; - const morphAttribute = new THREE.BufferAttribute(morphData, morphLayout.itemSize); - morphsArray[i] = morphAttribute; - for (const g of geometries) { - let gMorphAttribute = g.morphAttributes[morphLayout.name]; - gMorphAttribute = gMorphAttribute && gMorphAttribute[i]; - if (gMorphAttribute) { - morphData.set(gMorphAttribute.array, morphDataIndex); - morphDataIndex += gMorphAttribute.count * gMorphAttribute.itemSize; - // console.log('new index 1', morphLayout.name, gMorphAttribute.array.some(n => n !== 0), morphDataIndex, gMorphAttribute.count, gMorphAttribute.itemSize); - } else { - const matchingAttribute = g.attributes[morphLayout.name]; - morphDataIndex += matchingAttribute.count * matchingAttribute.itemSize; - // console.log('new index 2', g, morphDataIndex, matchingAttribute.count, matchingAttribute.itemSize); - } - } - if (morphDataIndex !== morphLayout.count) { - console.warn('desynced morph data', morphLayout.name, morphDataIndex, morphLayout.count); - } - } - geometry.morphAttributes[morphLayout.name] = morphsArray; - } - // index - let indexCount = 0; - for (const g of geometries) { - indexCount += g.index.count; - } - const indexData = new Uint32Array(indexCount); - let positionOffset = 0; - let indexOffset = 0; - for (const g of geometries) { - const srcIndexData = g.index.array; - for (let i = 0; i < srcIndexData.length; i++) { - indexData[indexOffset++] = srcIndexData[i] + positionOffset; - } - positionOffset += g.attributes.position.count; - } - geometry.setIndex(new THREE.BufferAttribute(indexData, 1)); - geometry.morphTargetsRelative = true; + // verify for (const layout of attributeLayouts) { From a1b3510b71b8ad8aeaa6436123c9fbf19dd80d99 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Tue, 12 Apr 2022 14:53:04 -0400 Subject: [PATCH 10/57] Dead locals cleanup --- avatar-optimizer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/avatar-optimizer.js b/avatar-optimizer.js index 9e226fb8a7..59f65dabef 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -5,8 +5,8 @@ import {getRenderer} from './renderer.js'; const defaultTextureSize = 4096; const startAtlasSize = 512; -const localVector2D = new THREE.Vector2(); -const localVector2D2 = new THREE.Vector2(); +// const localVector2D = new THREE.Vector2(); +// const localVector2D2 = new THREE.Vector2(); // const localVector4D = new THREE.Vector4(); // const localVector4D2 = new THREE.Vector4(); From 3a7840b332f3be866e7ea72f0128ca63bb796e67 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Tue, 12 Apr 2022 18:02:29 -0400 Subject: [PATCH 11/57] Avatars spacing debugging --- avatars/avatars.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/avatars/avatars.js b/avatars/avatars.js index 8e3fdedc76..e57af6b4dc 100644 --- a/avatars/avatars.js +++ b/avatars/avatars.js @@ -1362,21 +1362,21 @@ class Avatar { async setQuality(quality) { this.model.visible = false; - if ( this.crunchedModel ) this.crunchedModel.visible = false; - if ( this.spriteMegaAvatarMesh ) this.spriteMegaAvatarMesh.visible = false; + if (this.crunchedModel) this.crunchedModel.visible = false; + if (this.spriteMegaAvatarMesh) this.spriteMegaAvatarMesh.visible = false; switch (quality) { case 1: { const skinnedMesh = await this.object.cloneVrm(); - this.spriteMegaAvatarMesh = this.spriteMegaAvatarMesh ?? avatarSpriter.createSpriteMegaMesh( skinnedMesh ); - scene.add( this.spriteMegaAvatarMesh ); + this.spriteMegaAvatarMesh = this.spriteMegaAvatarMesh ?? avatarSpriter.createSpriteMegaMesh(skinnedMesh); + scene.add(this.spriteMegaAvatarMesh); this.spriteMegaAvatarMesh.visible = true; break; } case 2: { - this.crunchedModel = this.crunchedModel ?? avatarCruncher.crunchAvatarModel( this.model ); + this.crunchedModel = this.crunchedModel ?? avatarCruncher.crunchAvatarModel(this.model); this.crunchedModel.frustumCulled = false; - scene.add( this.crunchedModel ); + scene.add(this.crunchedModel); this.crunchedModel.visible = true; break; } From 8daead800670000263bc5d197079580499deb605 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Tue, 12 Apr 2022 18:04:05 -0400 Subject: [PATCH 12/57] Avatars.js hook in optimized model --- avatars/avatars.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/avatars/avatars.js b/avatars/avatars.js index e57af6b4dc..d818103d62 100644 --- a/avatars/avatars.js +++ b/avatars/avatars.js @@ -1381,10 +1381,14 @@ class Avatar { break; } case 3: { - const optimizedModel = avatarOptimizer.optimizeAvatarModel(this.model); - console.log('optimized model', optimizedModel); // XXX - - this.model.visible = true; + this.optimizedModel = avatarOptimizer.optimizeAvatarModel(this.model); + this.optimizedModel.traverse(o => { + if (o.isMesh) { + o.frustumCulled = false; + } + }); + scene.add(this.optimizedModel); + this.optimizedModel.visible = true; break; } case 4: { From 8a531f11794f3d4eeb68a874c8344a1a08da5ac7 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Tue, 12 Apr 2022 18:11:15 -0400 Subject: [PATCH 13/57] Avatar optimizer morph targets latching --- avatar-optimizer.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/avatar-optimizer.js b/avatar-optimizer.js index 59f65dabef..b57688ff37 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -48,6 +48,8 @@ const _getMergeableObjects = model => { } const objectGeometry = o.geometry; + const morphTargetDictionary = o.morphTargetDictionary; + const morphTargetInfluences = o.morphTargetInfluences; const objectMaterials = Array.isArray(o.material) ? o.material : [o.material]; for (const objectMaterial of objectMaterials) { const { @@ -75,6 +77,8 @@ const _getMergeableObjects = model => { normalMaps: [], // shadeTextures: [], skeletons: [], + morphTargetDictionaryArray: [], + morphTargetInfluencesArray: [], }; mergeables.set(key, m); } @@ -85,6 +89,8 @@ const _getMergeableObjects = model => { m.normalMaps.push(normalMap); // m.shadeTextures.push(shadeTexture); m.skeletons.push(skeleton); + m.morphTargetDictionaryArray.push(morphTargetDictionary); + m.morphTargetInfluencesArray.push(morphTargetInfluences); } } }); @@ -106,6 +112,8 @@ const optimizeAvatarModel = (model, options = {}) => { emissiveMaps, normalMaps, skeletons, + morphTargetDictionaryArray, + morphTargetInfluencesArray, } = mergeable; // compute texture sizes @@ -399,10 +407,8 @@ const optimizeAvatarModel = (model, options = {}) => { } else if (type === 'skinnedMesh') { const skinnedMesh = new THREE.SkinnedMesh(geometry, material); skinnedMesh.skeleton = skeletons[0]; - // XXX get this from the list accumulated during the initial scan - const deepestMorphMesh = meshes.find(m => (m.morphTargetInfluences ? m.morphTargetInfluences.length : 0) === morphAttributeLayouts[0].depth); - skinnedMesh.morphTargetDictionary = deepestMorphMesh.morphTargetDictionary; - skinnedMesh.morphTargetInfluences = deepestMorphMesh.morphTargetInfluences; + skinnedMesh.morphTargetDictionary = morphTargetDictionaryArray[0]; + skinnedMesh.morphTargetInfluences = morphTargetInfluencesArray[0]; return skinnedMesh; } else { throw new Error(`unknown type ${type}`); From 53355d318a7dcb24a320afd576fe67641708ea66 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Tue, 12 Apr 2022 18:11:32 -0400 Subject: [PATCH 14/57] Avatar optimizer material rewriting --- avatar-optimizer.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/avatar-optimizer.js b/avatar-optimizer.js index b57688ff37..7d1432dace 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -107,6 +107,7 @@ const optimizeAvatarModel = (model, options = {}) => { const _mergeMesh = (mergeable, mergeableIndex) => { const { type, + material, geometries, maps, emissiveMaps, @@ -382,22 +383,22 @@ const optimizeAvatarModel = (model, options = {}) => { const geometry = _mergeGeometryies(geometries); console.log('got geometry', geometry); - const _makeMaterial = () => { - // XXX use the original material, but set the new textures - const material = new THREE.MeshBasicMaterial(); + const _updateMaterial = () => { if (atlasTextures) { for (const textureType of textureTypes) { - const t = new THREE.Texture(atlasImages[textureType].image); + const image = atlasImages[textureType]; + const t = new THREE.Texture(image); t.flipY = false; t.needsUpdate = true; material[textureType] = t; } } - material.roughness = 1; + /* material.roughness = 1; material.alphaTest = 0.1; - material.transparent = true; + material.transparent = true; */ }; - const material = _makeMaterial(); + _updateMaterial(); + // const material = _makeMaterial(); console.log('got material', material); const _makeMesh = () => { From 2afcdefbb72245f425fd7e72abc6bd1c9160766a Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Tue, 12 Apr 2022 18:11:46 -0400 Subject: [PATCH 15/57] Avatar optimizer return object with child meshes --- avatar-optimizer.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/avatar-optimizer.js b/avatar-optimizer.js index 7d1432dace..a078d10b3c 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -483,7 +483,11 @@ const optimizeAvatarModel = (model, options = {}) => { }; _remapGeometryUvs(); */ - return mergedMeshes; + const object = new THREE.Object3D(); + for (const mesh of mergedMeshes) { + object.add(mesh); + } + return object; const _collectObjects = () => { const meshes = []; From 1bd31603c0636a32b4201f5ecf81a9ea1f717494 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Tue, 12 Apr 2022 23:56:01 -0400 Subject: [PATCH 16/57] More major avatar optimizer work --- avatar-optimizer.js | 230 +++++++++++++++++++++++++++----------------- 1 file changed, 144 insertions(+), 86 deletions(-) diff --git a/avatar-optimizer.js b/avatar-optimizer.js index a078d10b3c..bee5b9c36c 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -5,10 +5,8 @@ import {getRenderer} from './renderer.js'; const defaultTextureSize = 4096; const startAtlasSize = 512; -// const localVector2D = new THREE.Vector2(); -// const localVector2D2 = new THREE.Vector2(); -// const localVector4D = new THREE.Vector4(); -// const localVector4D2 = new THREE.Vector4(); +const localVector2D = new THREE.Vector2(); +const localVector2D2 = new THREE.Vector2(); const textureTypes = [ 'map', @@ -175,10 +173,11 @@ const optimizeAvatarModel = (model, options = {}) => { const hasTextures = textureSizes.some(textureSize => textureSize.x > 0 || textureSize.y > 0); if (hasTextures) { let atlas; - let atlasSize = startAtlasSize; - while (!(atlas = _attemptPack(textureSizes, atlasSize))) { - atlasSize *= 2; + let atlasScale = 1; + while (!(atlas = _attemptPack(textureSizes, startAtlasSize * atlasScale))) { + atlasScale *= 2; } + // atlas.scale = atlasScale; return atlas; } else { return _makeEmptyAtlas(); @@ -249,8 +248,8 @@ const optimizeAvatarModel = (model, options = {}) => { // build attribute layouts const _makeAttributeLayoutsFromGeometries = geometries => { const attributeLayouts = []; - for (const geometry of geometries) { - const attributes = geometry.attributes; + for (const g of geometries) { + const attributes = g.attributes; for (const attributeName in attributes) { const attribute = attributes[attributeName]; let layout = attributeLayouts.find(layout => layout.name === attributeName); @@ -278,8 +277,8 @@ const optimizeAvatarModel = (model, options = {}) => { const _makeMorphAttributeLayoutsFromGeometries = geometries => { // create morph layouts const morphAttributeLayouts = []; - for (const geometry of geometries) { - const morphAttributes = geometry.morphAttributes; + for (const g of geometries) { + const morphAttributes = g.morphAttributes; for (const morphAttributeName in morphAttributes) { const morphAttribute = morphAttributes[morphAttributeName]; let morphLayout = morphAttributeLayouts.find(l => l.name === morphAttributeName); @@ -316,24 +315,24 @@ const optimizeAvatarModel = (model, options = {}) => { } } }; - const _mergeGeometryies = geometries => { - const geometry = new THREE.BufferGeometry(); - - // attributes - _forceGeometriesAttributeLayouts(attributeLayouts, geometries); + const _mergeAttributes = (geometry, geometries, attributeLayouts) => { for (const layout of attributeLayouts) { const attributeData = new layout.TypedArrayConstructor(layout.count); const attribute = new THREE.BufferAttribute(attributeData, layout.itemSize); let attributeDataIndex = 0; for (const g of geometries) { const gAttribute = g.attributes[layout.name]; - attributeData.set(gAttribute.array, layout.index); + attributeData.set(gAttribute.array, attributeDataIndex); attributeDataIndex += gAttribute.count * gAttribute.itemSize; } + // sanity check + if (attributeDataIndex !== layout.count) { + console.warn('desynced morph data', layout.name, attributeDataIndex, layout.count); + } geometry.setAttribute(layout.name, attribute); } - - // morph attributes + }; + const _mergeMorphAttributes = (geometry, geometries, morphAttributeLayouts) => { for (const morphLayout of morphAttributeLayouts) { const morphsArray = Array(morphLayout.arraySize); for (let i = 0; i < morphLayout.arraySize; i++) { @@ -352,14 +351,15 @@ const optimizeAvatarModel = (model, options = {}) => { morphDataIndex += matchingAttribute.count * matchingAttribute.itemSize; } } + // sanity check if (morphDataIndex !== morphLayout.count) { console.warn('desynced morph data', morphLayout.name, morphDataIndex, morphLayout.count); } } geometry.morphAttributes[morphLayout.name] = morphsArray; } - - // index + }; + const _mergeIndices = (geometry, geometries) => { let indexCount = 0; for (const g of geometries) { indexCount += g.index.count; @@ -376,11 +376,126 @@ const optimizeAvatarModel = (model, options = {}) => { positionOffset += g.attributes.position.count; } geometry.setIndex(new THREE.BufferAttribute(indexData, 1)); + }; + /* const _drawAtlases = () => { + const seenUvIndexes = new Map(); + const _drawAtlas = atlas => { + const canvas = document.createElement('canvas'); + const canvasSize = Math.min(atlas.width, textureSize); + const canvasScale = canvasSize / atlas.width; + canvas.width = canvasSize; + canvas.height = canvasSize; + const ctx = canvas.getContext('2d'); + + atlas.bins.forEach(bin => { + bin.rects.forEach(rect => { + const {x, y, width: w, height: h, data: {image, groups}} = rect; + // draw the image in the correct box on the canvas + const tx = x * canvasScale; + const ty = y * canvasScale; + const tw = w * canvasScale; + const th = h * canvasScale; + ctx.drawImage(image, 0, 0, image.width, image.height, tx, ty, tw, th); + + // const testUv = new THREE.Vector2(Math.random(), Math.random()); + for (const group of groups) { + const {startIndex, count} = group; + for (let i = 0; i < count; i++) { + const uvIndex = geometry.index.array[startIndex + i]; + + // XXX NOTE: this code is slightly wrong. it will generate a unified uv map (first come first served to the uv index) + // that means that the different maps might get the wrong uv. + // the diffuse map takes priority so it looks ok. + // the right way to do this is to have a separate uv map for each map. + if (!seenUvIndexes.get(uvIndex)) { + seenUvIndexes.set(uvIndex, true); + + localVector2D.fromArray(geometry.attributes.uv.array, uvIndex * 2); + localVector2D.multiply( + localVector2D2.set(tw/canvasSize, th/canvasSize) + ).add( + localVector2D2.set(tx/canvasSize, ty/canvasSize) + ); + localVector2D.toArray(geometry.attributes.uv.array, uvIndex * 2); + } + } + } + }); + }); + atlas.image = canvas; + + return atlas; + }; + + // generate atlas for each map; they are all separate + const result = {}; + { + let canvasIndex = 0; + for (const k of textureTypes) { + const atlas = atlases[k]; + const atlas2 = _drawAtlas(atlas); + + result[k] = atlas2; + + canvasIndex++; + } + } + return result; + }; */ + const _remapGeometryUvs = (geometry, geometries) => { + let indexIndex = 0; + const geometryOffsets = geometries.map(g => { + const start = indexIndex; + const count = g.index.count; + indexIndex += count; + return { + start, + count, + }; + }); + + const canvasSize = Math.min(atlas.width, textureSize); + const canvasScale = canvasSize / atlas.width; + atlas.bins.forEach(bin => { + bin.rects.forEach(rect => { + const {x, y, width: w, height: h, data: {index}} = rect; + + if (w > 0 && h > 0) { + const {start, count} = geometryOffsets[index]; + + const tx = x * canvasScale; + const ty = y * canvasScale; + const tw = w * canvasScale; + const th = h * canvasScale; + + for (let i = 0; i < count; i++) { + const uvIndex = geometry.index.array[start + i]; + + localVector2D.fromArray(geometry.attributes.uv.array, uvIndex * 2); + localVector2D.multiply( + localVector2D2.set(tw/canvasSize, th/canvasSize) + ).add( + localVector2D2.set(tx/canvasSize, ty/canvasSize) + ); + localVector2D.toArray(geometry.attributes.uv.array, uvIndex * 2); + } + } + }); + }); + }; + const _mergeGeometries = geometries => { + const geometry = new THREE.BufferGeometry(); geometry.morphTargetsRelative = true; + _forceGeometriesAttributeLayouts(attributeLayouts, geometries); + _mergeAttributes(geometry, geometries, attributeLayouts); + _mergeMorphAttributes(geometry, geometries, morphAttributeLayouts); + _mergeIndices(geometry, geometries); + _remapGeometryUvs(geometry, geometries); + return geometry; }; - const geometry = _mergeGeometryies(geometries); + const geometry = _mergeGeometries(geometries); console.log('got geometry', geometry); const _updateMaterial = () => { @@ -398,15 +513,19 @@ const optimizeAvatarModel = (model, options = {}) => { material.transparent = true; */ }; _updateMaterial(); - // const material = _makeMaterial(); console.log('got material', material); const _makeMesh = () => { + const m = new THREE.MeshPhongMaterial({ + color: 0xFF0000, + }); + /* const mesh = new THREE.Mesh(geometry, m); + return mesh; */ if (type === 'mesh') { - const mesh = new THREE.Mesh(geometry, material); + const mesh = new THREE.Mesh(geometry, m); return mesh; } else if (type === 'skinnedMesh') { - const skinnedMesh = new THREE.SkinnedMesh(geometry, material); + const skinnedMesh = new THREE.SkinnedMesh(geometry, m); skinnedMesh.skeleton = skeletons[0]; skinnedMesh.morphTargetDictionary = morphTargetDictionaryArray[0]; skinnedMesh.morphTargetInfluences = morphTargetInfluencesArray[0]; @@ -422,67 +541,6 @@ const optimizeAvatarModel = (model, options = {}) => { }; const mergedMeshes = mergeables.map((mergeable, i) => _mergeMesh(mergeable, i)); - /* // draw atlas textures - const _remapGeometryUvs = atlases => { - const seenUvIndexes = new Map(); - const _drawAtlas = atlas => { - const canvas = document.createElement('canvas'); - const canvasSize = Math.min(atlas.width, textureSize); - const canvasScale = canvasSize / atlas.width; - canvas.width = canvasSize; - canvas.height = canvasSize; - const ctx = canvas.getContext('2d'); - - atlas.bins.forEach(bin => { - bin.rects.forEach(rect => { - const {x, y, width: w, height: h, data: {image, groups}} = rect; - // draw the image in the correct box on the canvas - const tx = x * canvasScale; - const ty = y * canvasScale; - const tw = w * canvasScale; - const th = h * canvasScale; - ctx.drawImage(image, 0, 0, image.width, image.height, tx, ty, tw, th); - - // const testUv = new THREE.Vector2(Math.random(), Math.random()); - for (const group of groups) { - const {startIndex, count} = group; - for (let i = 0; i < count; i++) { - const uvIndex = geometry.index.array[startIndex + i]; - - // XXX NOTE: this code is slightly wrong. it will generate a unified uv map (first come first served to the uv index) - // that means that the different maps might get the wrong uv. - // the diffuse map takes priority so it looks ok. - // the right way to do this is to have a separate uv map for each map. - if (!seenUvIndexes.get(uvIndex)) { - seenUvIndexes.set(uvIndex, true); - - localVector2D.fromArray(geometry.attributes.uv.array, uvIndex * 2); - localVector2D.multiply( - localVector2D2.set(tw/canvasSize, th/canvasSize) - ).add( - localVector2D2.set(tx/canvasSize, ty/canvasSize) - ); - localVector2D.toArray(geometry.attributes.uv.array, uvIndex * 2); - } - } - } - }); - }); - atlas.image = canvas; - - return atlas; - }; - - const atlasImages = {}; - for (const textureType of textureTypes) { - const atlas = atlases[textureType]; - const atlasImage = _drawAtlas(atlas); - atlasImages[textureType] = atlasImage; - } - return atlasImages; - }; - _remapGeometryUvs(); */ - const object = new THREE.Object3D(); for (const mesh of mergedMeshes) { object.add(mesh); From 0d64491924b0e4fe54e41317700390f66aed1aec Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Wed, 13 Apr 2022 11:17:04 -0400 Subject: [PATCH 17/57] Avatar optimizer cleanup --- avatar-optimizer.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/avatar-optimizer.js b/avatar-optimizer.js index bee5b9c36c..c99d762e74 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -173,11 +173,10 @@ const optimizeAvatarModel = (model, options = {}) => { const hasTextures = textureSizes.some(textureSize => textureSize.x > 0 || textureSize.y > 0); if (hasTextures) { let atlas; - let atlasScale = 1; - while (!(atlas = _attemptPack(textureSizes, startAtlasSize * atlasScale))) { - atlasScale *= 2; + let atlasSize = startAtlasSize; + while (!(atlas = _attemptPack(textureSizes, atlasSize))) { + atlasSize *= 2; } - // atlas.scale = atlasScale; return atlas; } else { return _makeEmptyAtlas(); From 632bf575c48328016d431a4a8e0528fe4d8e0d0b Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Wed, 13 Apr 2022 11:17:30 -0400 Subject: [PATCH 18/57] UV remap debugging --- avatar-optimizer.js | 53 +++++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/avatar-optimizer.js b/avatar-optimizer.js index c99d762e74..a5621e8790 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -454,33 +454,44 @@ const optimizeAvatarModel = (model, options = {}) => { }); const canvasSize = Math.min(atlas.width, textureSize); - const canvasScale = canvasSize / atlas.width; - atlas.bins.forEach(bin => { - bin.rects.forEach(rect => { - const {x, y, width: w, height: h, data: {index}} = rect; + if (canvasSize > 0) { + const canvasScale = canvasSize / atlas.width; + const seenUvIndexes = new Int32Array(geometry.attributes.uv.count).fill(-1); + atlas.bins.forEach(bin => { + bin.rects.forEach(rect => { + const {x, y, width: w, height: h, data: {index}} = rect; - if (w > 0 && h > 0) { - const {start, count} = geometryOffsets[index]; + if (w > 0 && h > 0) { + const {start, count} = geometryOffsets[index]; - const tx = x * canvasScale; - const ty = y * canvasScale; - const tw = w * canvasScale; - const th = h * canvasScale; + const tx = x * canvasScale; + const ty = y * canvasScale; + const tw = w * canvasScale; + const th = h * canvasScale; - for (let i = 0; i < count; i++) { - const uvIndex = geometry.index.array[start + i]; + for (let i = 0; i < count; i++) { + const indexIndex = start + i; + const uvIndex = geometry.index.array[indexIndex]; + if (seenUvIndexes[uvIndex] === -1) { + seenUvIndexes[uvIndex] = index; - localVector2D.fromArray(geometry.attributes.uv.array, uvIndex * 2); - localVector2D.multiply( - localVector2D2.set(tw/canvasSize, th/canvasSize) - ).add( - localVector2D2.set(tx/canvasSize, ty/canvasSize) - ); - localVector2D.toArray(geometry.attributes.uv.array, uvIndex * 2); + localVector2D.fromArray(geometry.attributes.uv.array, uvIndex * 2); + localVector2D.multiply( + localVector2D2.set(tw/canvasSize, th/canvasSize) + ).add( + localVector2D2.set(tx/canvasSize, ty/canvasSize) + ); + localVector2D.toArray(geometry.attributes.uv.array, uvIndex * 2); + } else { + if (seenUvIndexes[uvIndex] !== index) { + debugger; + } + } + } } - } + }); }); - }); + } }; const _mergeGeometries = geometries => { const geometry = new THREE.BufferGeometry(); From 8f89d684e38d9325ee310aaf0cc625d56c6d1564 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Wed, 13 Apr 2022 11:31:31 -0400 Subject: [PATCH 19/57] New UV rewrite --- avatar-optimizer.js | 40 ++++++++++++++++------------------------ 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/avatar-optimizer.js b/avatar-optimizer.js index a5621e8790..dba093fe6a 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -442,11 +442,11 @@ const optimizeAvatarModel = (model, options = {}) => { return result; }; */ const _remapGeometryUvs = (geometry, geometries) => { - let indexIndex = 0; - const geometryOffsets = geometries.map(g => { - const start = indexIndex; - const count = g.index.count; - indexIndex += count; + let uvIndex = 0; + const geometryUvOffsets = geometries.map(g => { + const start = uvIndex; + const count = g.attributes.uv.count; + uvIndex += count; return { start, count, @@ -456,13 +456,13 @@ const optimizeAvatarModel = (model, options = {}) => { const canvasSize = Math.min(atlas.width, textureSize); if (canvasSize > 0) { const canvasScale = canvasSize / atlas.width; - const seenUvIndexes = new Int32Array(geometry.attributes.uv.count).fill(-1); + // const seenUvIndexes = new Int32Array(geometry.attributes.uv.count).fill(-1); atlas.bins.forEach(bin => { bin.rects.forEach(rect => { const {x, y, width: w, height: h, data: {index}} = rect; if (w > 0 && h > 0) { - const {start, count} = geometryOffsets[index]; + const {start, count} = geometryUvOffsets[index]; const tx = x * canvasScale; const ty = y * canvasScale; @@ -470,23 +470,15 @@ const optimizeAvatarModel = (model, options = {}) => { const th = h * canvasScale; for (let i = 0; i < count; i++) { - const indexIndex = start + i; - const uvIndex = geometry.index.array[indexIndex]; - if (seenUvIndexes[uvIndex] === -1) { - seenUvIndexes[uvIndex] = index; - - localVector2D.fromArray(geometry.attributes.uv.array, uvIndex * 2); - localVector2D.multiply( - localVector2D2.set(tw/canvasSize, th/canvasSize) - ).add( - localVector2D2.set(tx/canvasSize, ty/canvasSize) - ); - localVector2D.toArray(geometry.attributes.uv.array, uvIndex * 2); - } else { - if (seenUvIndexes[uvIndex] !== index) { - debugger; - } - } + const uvIndex = start + i; + + localVector2D.fromArray(geometry.attributes.uv.array, uvIndex * 2); + localVector2D.multiply( + localVector2D2.set(tw/canvasSize, th/canvasSize) + ).add( + localVector2D2.set(tx/canvasSize, ty/canvasSize) + ); + localVector2D.toArray(geometry.attributes.uv.array, uvIndex * 2); } } }); From bac652fbafe77462a0d67cc20643f245f7197c6f Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Wed, 13 Apr 2022 11:32:53 -0400 Subject: [PATCH 20/57] Hook in phong debug material to avatar optimizer --- avatar-optimizer.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/avatar-optimizer.js b/avatar-optimizer.js index dba093fe6a..ccc9f9242e 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -500,6 +500,9 @@ const optimizeAvatarModel = (model, options = {}) => { const geometry = _mergeGeometries(geometries); console.log('got geometry', geometry); + const m = new THREE.MeshPhongMaterial({ + color: 0xFF0000, + }); const _updateMaterial = () => { if (atlasTextures) { for (const textureType of textureTypes) { @@ -507,22 +510,18 @@ const optimizeAvatarModel = (model, options = {}) => { const t = new THREE.Texture(image); t.flipY = false; t.needsUpdate = true; - material[textureType] = t; + m[textureType] = t; } } - /* material.roughness = 1; - material.alphaTest = 0.1; - material.transparent = true; */ + // m.roughness = 1; + m.alphaTest = 0.1; + m.transparent = true; + m.needsUpdate = true; }; _updateMaterial(); - console.log('got material', material); + console.log('got material', m); const _makeMesh = () => { - const m = new THREE.MeshPhongMaterial({ - color: 0xFF0000, - }); - /* const mesh = new THREE.Mesh(geometry, m); - return mesh; */ if (type === 'mesh') { const mesh = new THREE.Mesh(geometry, m); return mesh; From 957a7235c65831820393002daf71e8bab674c992 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Wed, 13 Apr 2022 11:51:36 -0400 Subject: [PATCH 21/57] Avatar optimizer shader debugging --- avatar-optimizer.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/avatar-optimizer.js b/avatar-optimizer.js index ccc9f9242e..db77c8de56 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -500,9 +500,10 @@ const optimizeAvatarModel = (model, options = {}) => { const geometry = _mergeGeometries(geometries); console.log('got geometry', geometry); - const m = new THREE.MeshPhongMaterial({ + /* const m = new THREE.MeshPhongMaterial({ color: 0xFF0000, - }); + }); */ + const m = material; const _updateMaterial = () => { if (atlasTextures) { for (const textureType of textureTypes) { @@ -511,11 +512,18 @@ const optimizeAvatarModel = (model, options = {}) => { t.flipY = false; t.needsUpdate = true; m[textureType] = t; + /* if (m[textureType] !== t) { + throw new Error('texture update failed'); + } */ + if (m.uniforms) { + m.uniforms[textureType].value = t; + m.uniforms[textureType].needsUpdate = true; + } } } // m.roughness = 1; - m.alphaTest = 0.1; - m.transparent = true; + // m.alphaTest = 0.1; + // m.transparent = true; m.needsUpdate = true; }; _updateMaterial(); From 4a0f171855b87fd1cb9f23477db5a1ff04612df1 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Wed, 13 Apr 2022 14:11:05 -0400 Subject: [PATCH 22/57] Port shade textures in avatar optimizer --- avatar-optimizer.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/avatar-optimizer.js b/avatar-optimizer.js index db77c8de56..abcb833793 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -12,6 +12,7 @@ const textureTypes = [ 'map', 'emissiveMap', 'normalMap', + 'shadeTexture', ]; class AttributeLayout { @@ -54,9 +55,8 @@ const _getMergeableObjects = model => { map = null, emissiveMap = null, normalMap = null, - // shadeTexture = null, + shadeTexture = null, } = objectMaterial; - // console.log('got material', objectMaterial); const skeleton = o.skeleton ?? null; const key = [ @@ -73,7 +73,7 @@ const _getMergeableObjects = model => { maps: [], emissiveMaps: [], normalMaps: [], - // shadeTextures: [], + shadeTextures: [], skeletons: [], morphTargetDictionaryArray: [], morphTargetInfluencesArray: [], @@ -85,7 +85,7 @@ const _getMergeableObjects = model => { m.maps.push(map); m.emissiveMaps.push(emissiveMap); m.normalMaps.push(normalMap); - // m.shadeTextures.push(shadeTexture); + m.shadeTextures.push(shadeTexture); m.skeletons.push(skeleton); m.morphTargetDictionaryArray.push(morphTargetDictionary); m.morphTargetInfluencesArray.push(morphTargetInfluences); From bcc8e6549c115d3580b3a34be4dcb002c9b8818a Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Wed, 13 Apr 2022 14:12:11 -0400 Subject: [PATCH 23/57] Remove avatar optimizer dead addendum --- avatar-optimizer.js | 133 -------------------------------------------- 1 file changed, 133 deletions(-) diff --git a/avatar-optimizer.js b/avatar-optimizer.js index abcb833793..7de0459793 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -555,139 +555,6 @@ const optimizeAvatarModel = (model, options = {}) => { object.add(mesh); } return object; - - const _collectObjects = () => { - const meshes = []; - const geometries = []; - const materials = []; - const textures = {}; - for (const textureType of textureTypes) { - textures[textureType] = []; - } - let textureGroupsMap = new WeakMap(); - const skeletons = []; - { - let indexIndex = 0; - model.traverse(node => { - if (node.isMesh && !node.parent?.isBone) { - meshes.push(node); - - const geometry = node.geometry; - geometries.push(geometry); - - const startIndex = indexIndex; - const count = geometry.index.count; - const _pushMaterial = material => { - materials.push(material); - for (const k of textureTypes) { - const texture = material[k]; - if (texture) { - const texturesOfType = textures[k]; - if (!texturesOfType.includes(texture)) { - texturesOfType.push(texture); - } - let textureGroups = textureGroupsMap.get(texture); - if (!textureGroups) { - textureGroups = []; - textureGroupsMap.set(texture, textureGroups); - } - textureGroups.push({ - startIndex, - count, - }); - } - } - }; - - let material = node.material; - if (Array.isArray(material)) { - for (let i = 0; i < material.length; i++) { - _pushMaterial(material[i]); - } - } else { - _pushMaterial(material); - } - - if (node.skeleton) { - if (!skeletons.includes(node.skeleton)) { - skeletons.push(node.skeleton); - } - } - - indexIndex += geometry.index.count; - } - }); - } - return { - meshes, - geometries, - materials, - textures, - textureGroupsMap, - skeletons, - }; - }; - - // collect objects - const { - meshes, - geometries, - materials, - textures, - textureGroupsMap, - skeletons, - } = _collectObjects(); - - const attributeLayouts = _makeAttributeLayoutsFromGeometries(geometries); - const morphAttributeLayouts = _makeMorphAttributeLayoutsFromGeometries(geometries); - - // validate attribute layouts - for (let i = 0; i < meshes.length; i++) { - const mesh = meshes[i]; - const geometry = mesh.geometry; - if (!geometry.index) { - console.log('no index', mesh); - } - } - if (skeletons.length !== 1) { - console.log('did not have single skeleton', skeletons); - } - - - - // verify - for (const layout of attributeLayouts) { - if (layout.index !== layout.count) { - console.log('bad layout count', layout.index, layout.count); - } - } - if (indexOffset !== indexCount) { - console.log('bad final index', indexOffset, indexCount); - } - - // create material - // const material = new THREE.MeshStandardMaterial(); - const material = new THREE.MeshBasicMaterial(); - if (atlasTextures) { - for (const k of textureTypes) { - const t = new THREE.Texture(textureAtlases[k].image); - t.flipY = false; - t.needsUpdate = true; - material[k] = t; - } - } - material.roughness = 1; - material.alphaTest = 0.1; - material.transparent = true; - - // create mesh - const crunchedModel = new THREE.SkinnedMesh(geometry, material); - crunchedModel.skeleton = skeletons[0]; - const deepestMorphMesh = meshes.find(m => (m.morphTargetInfluences ? m.morphTargetInfluences.length : 0) === morphAttributeLayouts[0].depth); - crunchedModel.morphTargetDictionary = deepestMorphMesh.morphTargetDictionary; - crunchedModel.morphTargetInfluences = deepestMorphMesh.morphTargetInfluences; - - return crunchedModel; }; export { From c0020bef391dcd5ac054ef924047906120eaf657 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Wed, 13 Apr 2022 14:15:20 -0400 Subject: [PATCH 24/57] Avatar optimizer commenting cleanup --- avatar-optimizer.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/avatar-optimizer.js b/avatar-optimizer.js index 7de0459793..ddea856eb9 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -141,8 +141,6 @@ const optimizeAvatarModel = (model, options = {}) => { const _attemptPack = (textureSizes, atlasSize) => { const maxRectsPacker = new MaxRectsPacker(atlasSize, atlasSize, 1); const rects = textureSizes.map((textureSize, index) => { - // const w = t.image.width; - // const h = t.image.height; // const image = t.image; const {x: width, y: height} = textureSize; return { From b4a6ea55ef7e49c2b736190f8c5fa6f9e51f5b82 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Wed, 13 Apr 2022 14:16:11 -0400 Subject: [PATCH 25/57] Major avatar optimizer work --- avatar-optimizer.js | 196 +++++++++++++++++--------------------------- 1 file changed, 76 insertions(+), 120 deletions(-) diff --git a/avatar-optimizer.js b/avatar-optimizer.js index ddea856eb9..a6895ddba9 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -100,7 +100,7 @@ const optimizeAvatarModel = (model, options = {}) => { const textureSize = options.textureSize ?? defaultTextureSize; const mergeables = _getMergeableObjects(model); - console.log('got mergeables', mergeables); + // console.log('got mergeables', mergeables); const _mergeMesh = (mergeable, mergeableIndex) => { const { @@ -166,7 +166,6 @@ const optimizeAvatarModel = (model, options = {}) => { return null; } }; - const _makeEmptyAtlas = () => new MaxRectsPacker(0, 0, 1); const hasTextures = textureSizes.some(textureSize => textureSize.x > 0 || textureSize.y > 0); if (hasTextures) { @@ -177,40 +176,51 @@ const optimizeAvatarModel = (model, options = {}) => { } return atlas; } else { - return _makeEmptyAtlas(); + return null; } }; const atlas = atlasTextures ? _packAtlases() : null; // draw atlas images + const originalTextures = new WeakMap(); // map of canvas to the texture that generated it const _drawAtlasImages = atlas => { const _drawAtlasImage = textureType => { - const canvas = document.createElement('canvas'); - const canvasSize = Math.min(atlas.width, textureSize); - const canvasScale = canvasSize / atlas.width; - canvas.width = canvasSize; - canvas.height = canvasSize; - const ctx = canvas.getContext('2d'); - - atlas.bins.forEach(bin => { - bin.rects.forEach(rect => { - const {x, y, width: w, height: h, data: {index}} = rect; - const textures = mergeable[`${textureType}s`]; - const texture = textures[index]; - if (texture) { - const image = texture.image; - - // draw the image in the correct box on the canvas - const tx = x * canvasScale; - const ty = y * canvasScale; - const tw = w * canvasScale; - const th = h * canvasScale; - ctx.drawImage(image, 0, 0, image.width, image.height, tx, ty, tw, th); - } + const textures = mergeable[`${textureType}s`]; + + if (atlas && textures.some(t => t !== null)) { + const canvasSize = Math.min(atlas.width, textureSize); + const canvasScale = canvasSize / atlas.width; + + const canvas = document.createElement('canvas'); + canvas.width = canvasSize; + canvas.height = canvasSize; + const ctx = canvas.getContext('2d'); + + atlas.bins.forEach(bin => { + bin.rects.forEach(rect => { + const {x, y, width: w, height: h, data: {index}} = rect; + const texture = textures[index]; + if (texture) { + const image = texture.image; + + // draw the image in the correct box on the canvas + const tx = x * canvasScale; + const ty = y * canvasScale; + const tw = w * canvasScale; + const th = h * canvasScale; + ctx.drawImage(image, 0, 0, image.width, image.height, tx, ty, tw, th); + + if (!originalTextures.has(canvas)) { + originalTextures.set(canvas, texture); + } + } + }); }); - }); - return canvas; + return canvas; + } else { + return null; + } }; const atlasImages = {}; @@ -222,7 +232,7 @@ const optimizeAvatarModel = (model, options = {}) => { }; const atlasImages = atlasTextures ? _drawAtlasImages(atlas) : null; - // XXX debug + /* // XXX debug { const debugWidth = 300; let textureTypeIndex = 0; @@ -240,7 +250,7 @@ const optimizeAvatarModel = (model, options = {}) => { document.body.appendChild(atlasImage); textureTypeIndex++; } - } + } */ // build attribute layouts const _makeAttributeLayoutsFromGeometries = geometries => { @@ -295,7 +305,7 @@ const optimizeAvatarModel = (model, options = {}) => { return morphAttributeLayouts; }; const morphAttributeLayouts = _makeMorphAttributeLayoutsFromGeometries(geometries); - console.log('got attribute layouts', attributeLayouts, morphAttributeLayouts); + // console.log('got attribute layouts', attributeLayouts, morphAttributeLayouts); const _forceGeometriesAttributeLayouts = (attributeLayouts, geometries) => { for (const layout of attributeLayouts) { @@ -374,87 +384,21 @@ const optimizeAvatarModel = (model, options = {}) => { } geometry.setIndex(new THREE.BufferAttribute(indexData, 1)); }; - /* const _drawAtlases = () => { - const seenUvIndexes = new Map(); - const _drawAtlas = atlas => { - const canvas = document.createElement('canvas'); - const canvasSize = Math.min(atlas.width, textureSize); - const canvasScale = canvasSize / atlas.width; - canvas.width = canvasSize; - canvas.height = canvasSize; - const ctx = canvas.getContext('2d'); - - atlas.bins.forEach(bin => { - bin.rects.forEach(rect => { - const {x, y, width: w, height: h, data: {image, groups}} = rect; - // draw the image in the correct box on the canvas - const tx = x * canvasScale; - const ty = y * canvasScale; - const tw = w * canvasScale; - const th = h * canvasScale; - ctx.drawImage(image, 0, 0, image.width, image.height, tx, ty, tw, th); - - // const testUv = new THREE.Vector2(Math.random(), Math.random()); - for (const group of groups) { - const {startIndex, count} = group; - for (let i = 0; i < count; i++) { - const uvIndex = geometry.index.array[startIndex + i]; - - // XXX NOTE: this code is slightly wrong. it will generate a unified uv map (first come first served to the uv index) - // that means that the different maps might get the wrong uv. - // the diffuse map takes priority so it looks ok. - // the right way to do this is to have a separate uv map for each map. - if (!seenUvIndexes.get(uvIndex)) { - seenUvIndexes.set(uvIndex, true); - - localVector2D.fromArray(geometry.attributes.uv.array, uvIndex * 2); - localVector2D.multiply( - localVector2D2.set(tw/canvasSize, th/canvasSize) - ).add( - localVector2D2.set(tx/canvasSize, ty/canvasSize) - ); - localVector2D.toArray(geometry.attributes.uv.array, uvIndex * 2); - } - } - } - }); + const _remapGeometryUvs = (geometry, geometries) => { + if (atlas) { + let uvIndex = 0; + const geometryUvOffsets = geometries.map(g => { + const start = uvIndex; + const count = g.attributes.uv.count; + uvIndex += count; + return { + start, + count, + }; }); - atlas.image = canvas; - - return atlas; - }; - - // generate atlas for each map; they are all separate - const result = {}; - { - let canvasIndex = 0; - for (const k of textureTypes) { - const atlas = atlases[k]; - const atlas2 = _drawAtlas(atlas); - result[k] = atlas2; - - canvasIndex++; - } - } - return result; - }; */ - const _remapGeometryUvs = (geometry, geometries) => { - let uvIndex = 0; - const geometryUvOffsets = geometries.map(g => { - const start = uvIndex; - const count = g.attributes.uv.count; - uvIndex += count; - return { - start, - count, - }; - }); - - const canvasSize = Math.min(atlas.width, textureSize); - if (canvasSize > 0) { + const canvasSize = Math.min(atlas.width, textureSize); const canvasScale = canvasSize / atlas.width; - // const seenUvIndexes = new Int32Array(geometry.attributes.uv.count).fill(-1); atlas.bins.forEach(bin => { bin.rects.forEach(rect => { const {x, y, width: w, height: h, data: {index}} = rect; @@ -496,7 +440,7 @@ const optimizeAvatarModel = (model, options = {}) => { return geometry; }; const geometry = _mergeGeometries(geometries); - console.log('got geometry', geometry); + // console.log('got geometry', geometry); /* const m = new THREE.MeshPhongMaterial({ color: 0xFF0000, @@ -506,16 +450,28 @@ const optimizeAvatarModel = (model, options = {}) => { if (atlasTextures) { for (const textureType of textureTypes) { const image = atlasImages[textureType]; - const t = new THREE.Texture(image); - t.flipY = false; - t.needsUpdate = true; - m[textureType] = t; - /* if (m[textureType] !== t) { - throw new Error('texture update failed'); - } */ - if (m.uniforms) { - m.uniforms[textureType].value = t; - m.uniforms[textureType].needsUpdate = true; + + if (image) { + const originalTexture = originalTextures.get(image); + + const t = new THREE.Texture(image); + t.minFilter = originalTexture.minFilter; + t.magFilter = originalTexture.magFilter; + t.wrapS = originalTexture.wrapS; + t.wrapT = originalTexture.wrapT; + t.mapping = originalTexture.mapping; + // t.encoding = originalTexture.encoding; + + t.flipY = false; + t.needsUpdate = true; + m[textureType] = t; + /* if (m[textureType] !== t) { + throw new Error('texture update failed'); + } */ + if (m.uniforms) { + m.uniforms[textureType].value = t; + m.uniforms[textureType].needsUpdate = true; + } } } } @@ -542,7 +498,7 @@ const optimizeAvatarModel = (model, options = {}) => { } }; const mesh = _makeMesh(); - console.log('got mesh', mesh); + // console.log('got mesh', mesh); return mesh; }; From 1b7a8268e657ff4da80559e98b48979228cd56f2 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Wed, 13 Apr 2022 14:30:09 -0400 Subject: [PATCH 26/57] Update package-lock.json --- package-lock.json | 103 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/package-lock.json b/package-lock.json index c30d0a2819..0584b36ca3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2684,6 +2684,11 @@ "node": ">=12.0.0" } }, + "node_modules/abab": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", + "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==" + }, "node_modules/abbrev": { "version": "1.1.1", "dev": true, @@ -4375,6 +4380,50 @@ "node": ">= 12" } }, + "node_modules/data-urls": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.1.tgz", + "integrity": "sha512-Ds554NeT5Gennfoo9KN50Vh6tpgtvYEwraYjejXnyTpu1C7oXKxdFk75REooENHE8ndTVOJuv+BEs4/J/xcozw==", + "dependencies": { + "abab": "^2.0.3", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^10.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-10.0.0.tgz", + "integrity": "sha512-CLxxCmdUby142H5FZzn4D8ikO1cmypvXVQktsgosNy4a4BHrDHeciBBGZhb0bNoR5/MltoCatso+vFjjGx8t0w==", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/date-fns": { "version": "2.28.0", "dev": true, @@ -12606,6 +12655,14 @@ "dev": true, "license": "MIT" }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "engines": { + "node": ">=12" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "license": "MIT", @@ -13022,6 +13079,7 @@ "dependencies": { "@babel/core": "^7.15.0", "@babel/preset-react": "^7.14.5", + "data-urls": "^3.0.1", "mime-types": "^2.1.32", "node-fetch": "^2.6.1", "pako": "^2.0.4", @@ -14775,6 +14833,11 @@ "react-refresh": "^0.10.0" } }, + "abab": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", + "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==" + }, "abbrev": { "version": "1.1.1", "dev": true @@ -15913,6 +15976,40 @@ "data-uri-to-buffer": { "version": "4.0.0" }, + "data-urls": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.1.tgz", + "integrity": "sha512-Ds554NeT5Gennfoo9KN50Vh6tpgtvYEwraYjejXnyTpu1C7oXKxdFk75REooENHE8ndTVOJuv+BEs4/J/xcozw==", + "requires": { + "abab": "^2.0.3", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^10.0.0" + }, + "dependencies": { + "tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "requires": { + "punycode": "^2.1.1" + } + }, + "webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" + }, + "whatwg-url": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-10.0.0.tgz", + "integrity": "sha512-CLxxCmdUby142H5FZzn4D8ikO1cmypvXVQktsgosNy4a4BHrDHeciBBGZhb0bNoR5/MltoCatso+vFjjGx8t0w==", + "requires": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + } + } + } + }, "date-fns": { "version": "2.28.0", "dev": true @@ -18391,6 +18488,7 @@ "requires": { "@babel/core": "^7.15.0", "@babel/preset-react": "^7.14.5", + "data-urls": "^3.0.1", "mime-types": "^2.1.32", "node-fetch": "^2.6.1", "pako": "^2.0.4", @@ -21396,6 +21494,11 @@ "version": "3.6.2", "dev": true }, + "whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==" + }, "whatwg-url": { "version": "5.0.0", "requires": { From 2e065567a189158556869696af08437dab1d27b7 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Wed, 13 Apr 2022 15:16:25 -0400 Subject: [PATCH 27/57] Only merge BufferGeometry in avatar optimizer --- avatar-optimizer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avatar-optimizer.js b/avatar-optimizer.js index a6895ddba9..e3614b1c04 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -38,7 +38,7 @@ const _getMergeableObjects = model => { const mergeables = new Map(); model.traverse(o => { - if (o.isMesh) { + if (o.isMesh && o.geometry.type === 'BufferGeometry') { let type; if (o.isSkinnedMesh) { type = 'skinnedMesh'; From d36bfb91fab44a3ad7e5b5428c6baae3dd2cc4f1 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Wed, 13 Apr 2022 15:19:02 -0400 Subject: [PATCH 28/57] Avatar optimizer logging cleanup --- avatar-optimizer.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/avatar-optimizer.js b/avatar-optimizer.js index e3614b1c04..5217fe3270 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -34,8 +34,6 @@ class MorphAttributeLayout extends AttributeLayout { const _getMergeableObjects = model => { const renderer = getRenderer(); - console.log('got model', model); - const mergeables = new Map(); model.traverse(o => { if (o.isMesh && o.geometry.type === 'BufferGeometry') { @@ -99,8 +97,9 @@ const optimizeAvatarModel = (model, options = {}) => { const atlasTextures = !!(options.textures ?? true); const textureSize = options.textureSize ?? defaultTextureSize; + console.log('got model', model); const mergeables = _getMergeableObjects(model); - // console.log('got mergeables', mergeables); + console.log('got mergeables', mergeables); const _mergeMesh = (mergeable, mergeableIndex) => { const { From f4236cbf7008f08ca1272de066870315de357f39 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Wed, 13 Apr 2022 16:12:49 -0400 Subject: [PATCH 29/57] Avatar optimizer clamp uvs --- avatar-optimizer.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/avatar-optimizer.js b/avatar-optimizer.js index 5217fe3270..d74e170f15 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -31,6 +31,12 @@ class MorphAttributeLayout extends AttributeLayout { } } +const clampUvEpsilon = 0.02; +const _clampUv = uv => { + uv.x = Math.max(clampUvEpsilon, Math.min(1 - clampUvEpsilon, uv.x)); + uv.y = Math.max(clampUvEpsilon, Math.min(1 - clampUvEpsilon, uv.y)); + return uv; +}; const _getMergeableObjects = model => { const renderer = getRenderer(); @@ -414,11 +420,14 @@ const optimizeAvatarModel = (model, options = {}) => { const uvIndex = start + i; localVector2D.fromArray(geometry.attributes.uv.array, uvIndex * 2); - localVector2D.multiply( - localVector2D2.set(tw/canvasSize, th/canvasSize) - ).add( - localVector2D2.set(tx/canvasSize, ty/canvasSize) - ); + _clampUv(localVector2D); + localVector2D + .multiply( + localVector2D2.set(tw/canvasSize, th/canvasSize) + ) + .add( + localVector2D2.set(tx/canvasSize, ty/canvasSize) + ); localVector2D.toArray(geometry.attributes.uv.array, uvIndex * 2); } } From 461df17d64cce4d948619a99481521ccd930a9be Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Wed, 13 Apr 2022 16:13:15 -0400 Subject: [PATCH 30/57] Avatar optimizer atlas debug code cleanup --- avatar-optimizer.js | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/avatar-optimizer.js b/avatar-optimizer.js index d74e170f15..d09b40505f 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -237,25 +237,28 @@ const optimizeAvatarModel = (model, options = {}) => { }; const atlasImages = atlasTextures ? _drawAtlasImages(atlas) : null; - /* // XXX debug + // XXX debug { const debugWidth = 300; let textureTypeIndex = 0; for (const textureType of textureTypes) { const atlasImage = atlasImages[textureType]; - atlasImage.style.cssText = `\ - position: fixed; - top: ${mergeableIndex * debugWidth}px; - left: ${textureTypeIndex * debugWidth}px; - min-width: ${debugWidth}px; - max-width: ${debugWidth}px; - min-height: ${debugWidth}px; - z-index: 100; - `; - document.body.appendChild(atlasImage); - textureTypeIndex++; + if (atlasImage) { + atlasImage.style.cssText = `\ + position: fixed; + top: ${mergeableIndex * debugWidth}px; + left: ${textureTypeIndex * debugWidth}px; + min-width: ${debugWidth}px; + max-width: ${debugWidth}px; + min-height: ${debugWidth}px; + z-index: 100; + `; + atlasImage.setAttribute('type', textureType); + document.body.appendChild(atlasImage); + textureTypeIndex++; + } } - } */ + } // build attribute layouts const _makeAttributeLayoutsFromGeometries = geometries => { From c5dccf3a8a1ac96b482a7cd050d0ee24ee688d17 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Wed, 13 Apr 2022 16:13:40 -0400 Subject: [PATCH 31/57] Comment out avatar optimizer debug code --- avatar-optimizer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/avatar-optimizer.js b/avatar-optimizer.js index d09b40505f..bc3aef7168 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -237,7 +237,7 @@ const optimizeAvatarModel = (model, options = {}) => { }; const atlasImages = atlasTextures ? _drawAtlasImages(atlas) : null; - // XXX debug + /* // XXX debug { const debugWidth = 300; let textureTypeIndex = 0; @@ -258,7 +258,7 @@ const optimizeAvatarModel = (model, options = {}) => { textureTypeIndex++; } } - } + } */ // build attribute layouts const _makeAttributeLayoutsFromGeometries = geometries => { From 01bdb7c5845cbe883cb6f76360cbb23e23c8aaaa Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Wed, 13 Apr 2022 16:14:23 -0400 Subject: [PATCH 32/57] Avatar optimizer cleanup --- avatar-optimizer.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/avatar-optimizer.js b/avatar-optimizer.js index bc3aef7168..75b1ef8103 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -103,9 +103,9 @@ const optimizeAvatarModel = (model, options = {}) => { const atlasTextures = !!(options.textures ?? true); const textureSize = options.textureSize ?? defaultTextureSize; - console.log('got model', model); + // console.log('got model', model); const mergeables = _getMergeableObjects(model); - console.log('got mergeables', mergeables); + // console.log('got mergeables', mergeables); const _mergeMesh = (mergeable, mergeableIndex) => { const { @@ -492,7 +492,7 @@ const optimizeAvatarModel = (model, options = {}) => { m.needsUpdate = true; }; _updateMaterial(); - console.log('got material', m); + // console.log('got material', m); const _makeMesh = () => { if (type === 'mesh') { From 2dcc0c79558c10a040bacc342b5c1e0f53062296 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Wed, 13 Apr 2022 16:27:04 -0400 Subject: [PATCH 33/57] Avatar optimizer mod uvs instead of clamping --- avatar-optimizer.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/avatar-optimizer.js b/avatar-optimizer.js index 75b1ef8103..c8b71545ae 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -1,6 +1,7 @@ import * as THREE from 'three'; import {MaxRectsPacker} from 'maxrects-packer'; import {getRenderer} from './renderer.js'; +import {mod} from './util.js'; const defaultTextureSize = 4096; const startAtlasSize = 512; @@ -31,10 +32,9 @@ class MorphAttributeLayout extends AttributeLayout { } } -const clampUvEpsilon = 0.02; const _clampUv = uv => { - uv.x = Math.max(clampUvEpsilon, Math.min(1 - clampUvEpsilon, uv.x)); - uv.y = Math.max(clampUvEpsilon, Math.min(1 - clampUvEpsilon, uv.y)); + uv.x = mod(uv.x, 1); + uv.y = mod(uv.y, 1); return uv; }; const _getMergeableObjects = model => { From a0d9e956955dfcaae3d2172c5a240b567e9669be Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Wed, 13 Apr 2022 16:45:24 -0400 Subject: [PATCH 34/57] Avatar optimizer refactoring --- avatar-optimizer.js | 58 ++++++++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/avatar-optimizer.js b/avatar-optimizer.js index c8b71545ae..f5d898c8b4 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -453,32 +453,49 @@ const optimizeAvatarModel = (model, options = {}) => { const geometry = _mergeGeometries(geometries); // console.log('got geometry', geometry); + const _makeAtlasTextures = atlasImages => { + const _makeAtlasTexture = atlasImage => { + if (atlasImage) { + const originalTexture = originalTextures.get(atlasImage); + + const t = new THREE.Texture(atlasImage); + t.minFilter = originalTexture.minFilter; + t.magFilter = originalTexture.magFilter; + t.wrapS = originalTexture.wrapS; + t.wrapT = originalTexture.wrapT; + t.mapping = originalTexture.mapping; + // t.encoding = originalTexture.encoding; + + t.flipY = false; + t.needsUpdate = true; + + return t; + } else { + return null; + } + }; + + const result = {}; + for (const textureType of textureTypes) { + const atlasImage = atlasImages[textureType]; + const atlasTexture = _makeAtlasTexture(atlasImage); + result[textureType] = atlasTexture; + } + return result; + }; + const ts = atlasImages ? _makeAtlasTextures(atlasImages) : null; + /* const m = new THREE.MeshPhongMaterial({ color: 0xFF0000, }); */ const m = material; const _updateMaterial = () => { - if (atlasTextures) { + if (ts) { for (const textureType of textureTypes) { - const image = atlasImages[textureType]; - - if (image) { - const originalTexture = originalTextures.get(image); - - const t = new THREE.Texture(image); - t.minFilter = originalTexture.minFilter; - t.magFilter = originalTexture.magFilter; - t.wrapS = originalTexture.wrapS; - t.wrapT = originalTexture.wrapT; - t.mapping = originalTexture.mapping; - // t.encoding = originalTexture.encoding; - - t.flipY = false; - t.needsUpdate = true; + const t = ts[textureType]; + + if (t) { m[textureType] = t; - /* if (m[textureType] !== t) { - throw new Error('texture update failed'); - } */ if (m.uniforms) { m.uniforms[textureType].value = t; m.uniforms[textureType].needsUpdate = true; @@ -486,9 +503,6 @@ const optimizeAvatarModel = (model, options = {}) => { } } } - // m.roughness = 1; - // m.alphaTest = 0.1; - // m.transparent = true; m.needsUpdate = true; }; _updateMaterial(); From deb7a59388c822fdcadaeb2a547d4131f0febe8d Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Wed, 13 Apr 2022 17:01:54 -0400 Subject: [PATCH 35/57] Add avatar optimizer caching --- avatar-optimizer.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/avatar-optimizer.js b/avatar-optimizer.js index f5d898c8b4..adce5ef040 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -187,11 +187,10 @@ const optimizeAvatarModel = (model, options = {}) => { const atlas = atlasTextures ? _packAtlases() : null; // draw atlas images - const originalTextures = new WeakMap(); // map of canvas to the texture that generated it + const originalTextures = new Map(); // map of canvas to the texture that generated it const _drawAtlasImages = atlas => { - const _drawAtlasImage = textureType => { - const textures = mergeable[`${textureType}s`]; - + const _getTexturesKey = textures => textures.map(t => t ? t.uuid : '').join(','); + const _drawAtlasImage = textures => { if (atlas && textures.some(t => t !== null)) { const canvasSize = Math.min(atlas.width, textureSize); const canvasScale = canvasSize / atlas.width; @@ -229,8 +228,16 @@ const optimizeAvatarModel = (model, options = {}) => { }; const atlasImages = {}; + const atlasImagesMap = new Map(); for (const textureType of textureTypes) { - const atlasImage = _drawAtlasImage(textureType); + const textures = mergeable[`${textureType}s`]; + const key = _getTexturesKey(textures); + + let atlasImage = atlasImagesMap.get(key); + if (!atlasImage) { + atlasImage = _drawAtlasImage(textures); + atlasImagesMap.set(key, atlasImage); + } atlasImages[textureType] = atlasImage; } return atlasImages; From dfbcb1000463f1f9b0177d6b53ed35a0a6119e46 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Wed, 13 Apr 2022 17:11:25 -0400 Subject: [PATCH 36/57] Avatar optimizer textures aliasing --- avatar-optimizer.js | 53 ++++++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/avatar-optimizer.js b/avatar-optimizer.js index adce5ef040..a0f52d467b 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -228,14 +228,17 @@ const optimizeAvatarModel = (model, options = {}) => { }; const atlasImages = {}; - const atlasImagesMap = new Map(); + const atlasImagesMap = new Map(); // cache to alias identical textures for (const textureType of textureTypes) { const textures = mergeable[`${textureType}s`]; const key = _getTexturesKey(textures); let atlasImage = atlasImagesMap.get(key); - if (!atlasImage) { + if (atlasImage === undefined) { // cache miss atlasImage = _drawAtlasImage(textures); + if (atlasImage !== null) { + atlasImage.key = key; + } atlasImagesMap.set(key, atlasImage); } atlasImages[textureType] = atlasImage; @@ -462,31 +465,37 @@ const optimizeAvatarModel = (model, options = {}) => { const _makeAtlasTextures = atlasImages => { const _makeAtlasTexture = atlasImage => { - if (atlasImage) { - const originalTexture = originalTextures.get(atlasImage); - - const t = new THREE.Texture(atlasImage); - t.minFilter = originalTexture.minFilter; - t.magFilter = originalTexture.magFilter; - t.wrapS = originalTexture.wrapS; - t.wrapT = originalTexture.wrapT; - t.mapping = originalTexture.mapping; - // t.encoding = originalTexture.encoding; - - t.flipY = false; - t.needsUpdate = true; - - return t; - } else { - return null; - } + const originalTexture = originalTextures.get(atlasImage); + + const t = new THREE.Texture(atlasImage); + t.minFilter = originalTexture.minFilter; + t.magFilter = originalTexture.magFilter; + t.wrapS = originalTexture.wrapS; + t.wrapT = originalTexture.wrapT; + t.mapping = originalTexture.mapping; + // t.encoding = originalTexture.encoding; + + t.flipY = false; + t.needsUpdate = true; + + return t; }; const result = {}; + const textureMap = new Map(); // cache to alias identical textures for (const textureType of textureTypes) { const atlasImage = atlasImages[textureType]; - const atlasTexture = _makeAtlasTexture(atlasImage); - result[textureType] = atlasTexture; + + if (atlasImage) { + let atlasTexture = textureMap.get(atlasImage.key); + if (atlasTexture === undefined) { // cache miss + atlasTexture = _makeAtlasTexture(atlasImage); + textureMap.set(atlasImage.key, atlasTexture); + } + result[textureType] = atlasTexture; + } else { + result[textureType] = null; + } } return result; }; From e776a7969eb5bb1647d3f01e03f07d37b926e5c2 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Wed, 13 Apr 2022 17:12:55 -0400 Subject: [PATCH 37/57] Commented code cleanup --- avatar-optimizer.js | 1 - 1 file changed, 1 deletion(-) diff --git a/avatar-optimizer.js b/avatar-optimizer.js index a0f52d467b..ec88e1374c 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -146,7 +146,6 @@ const optimizeAvatarModel = (model, options = {}) => { const _attemptPack = (textureSizes, atlasSize) => { const maxRectsPacker = new MaxRectsPacker(atlasSize, atlasSize, 1); const rects = textureSizes.map((textureSize, index) => { - // const image = t.image; const {x: width, y: height} = textureSize; return { width, From f328cb0307a6cd7566bce4ca8097f2063d3d0754 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Fri, 22 Apr 2022 19:06:35 -0400 Subject: [PATCH 38/57] Break out avatar optimizer crunching --- avatar-optimizer.js | 802 +++++++++++++++++++++++--------------------- 1 file changed, 428 insertions(+), 374 deletions(-) diff --git a/avatar-optimizer.js b/avatar-optimizer.js index ec88e1374c..e8ffb40dad 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -1,7 +1,7 @@ import * as THREE from 'three'; import {MaxRectsPacker} from 'maxrects-packer'; import {getRenderer} from './renderer.js'; -import {mod} from './util.js'; +import {modUv} from './util.js'; const defaultTextureSize = 4096; const startAtlasSize = 512; @@ -24,22 +24,31 @@ class AttributeLayout { this.count = 0; } + makeDefault(g) { + return new THREE.BufferAttribute( + new this.TypedArrayConstructor(g.attributes.position.count * this.itemSize), + this.itemSize + ); + } } class MorphAttributeLayout extends AttributeLayout { constructor(name, TypedArrayConstructor, itemSize, arraySize) { super(name, TypedArrayConstructor, itemSize); this.arraySize = arraySize; } + makeDefault(g) { + return Array(this.arraySize).fill(super.makeDefault(g)); + } } -const _clampUv = uv => { - uv.x = mod(uv.x, 1); - uv.y = mod(uv.y, 1); - return uv; -}; -const _getMergeableObjects = model => { +const getObjectKeyDefault = (type, object, material) => { const renderer = getRenderer(); - + return [ + type, + renderer.getProgramCacheKey(object, material), + ].join(','); +}; +export const getMergeableObjects = (model, getObjectKey = getObjectKeyDefault) => { const mergeables = new Map(); model.traverse(o => { if (o.isMesh && o.geometry.type === 'BufferGeometry') { @@ -63,10 +72,7 @@ const _getMergeableObjects = model => { } = objectMaterial; const skeleton = o.skeleton ?? null; - const key = [ - type, - renderer.getProgramCacheKey(o, objectMaterial), - ].join(','); + const key = getObjectKey(type, o, objectMaterial); let m = mergeables.get(key); if (!m) { @@ -99,420 +105,472 @@ const _getMergeableObjects = model => { return Array.from(mergeables.values()); }; -const optimizeAvatarModel = (model, options = {}) => { - const atlasTextures = !!(options.textures ?? true); - const textureSize = options.textureSize ?? defaultTextureSize; - - // console.log('got model', model); - const mergeables = _getMergeableObjects(model); - // console.log('got mergeables', mergeables); - - const _mergeMesh = (mergeable, mergeableIndex) => { - const { - type, - material, - geometries, - maps, - emissiveMaps, - normalMaps, - skeletons, - morphTargetDictionaryArray, - morphTargetInfluencesArray, - } = mergeable; +export const mergeGeometryTextureAtlas = (mergeable, textureSize) => { + const { + type, + material, + geometries, + maps, + emissiveMaps, + normalMaps, + skeletons, + morphTargetDictionaryArray, + morphTargetInfluencesArray, + } = mergeable; + + // compute texture sizes + const textureSizes = maps.map((map, i) => { + const emissiveMap = emissiveMaps[i]; + const normalMap = normalMaps[i]; + + const maxSize = new THREE.Vector2(0, 0); + if (map) { + maxSize.x = Math.max(maxSize.x, map.image.width); + maxSize.y = Math.max(maxSize.y, map.image.height); + } + if (emissiveMap) { + maxSize.x = Math.max(maxSize.x, emissiveMap.image.width); + maxSize.y = Math.max(maxSize.y, emissiveMap.image.height); + } + if (normalMap) { + maxSize.x = Math.max(maxSize.x, normalMap.image.width); + maxSize.y = Math.max(maxSize.y, normalMap.image.height); + } + return maxSize; + }); - // compute texture sizes - const textureSizes = maps.map((map, i) => { - const emissiveMap = emissiveMaps[i]; - const normalMap = normalMaps[i]; - - const maxSize = new THREE.Vector2(0, 0); - if (map) { - maxSize.x = Math.max(maxSize.x, map.image.width); - maxSize.y = Math.max(maxSize.y, map.image.height); - } - if (emissiveMap) { - maxSize.x = Math.max(maxSize.x, emissiveMap.image.width); - maxSize.y = Math.max(maxSize.y, emissiveMap.image.height); + // generate atlas layouts + const _packAtlases = () => { + const _attemptPack = (textureSizes, atlasSize) => { + const maxRectsPacker = new MaxRectsPacker(atlasSize, atlasSize, 1); + const rects = textureSizes.map((textureSize, index) => { + const {x: width, y: height} = textureSize; + return { + width, + height, + data: { + index, + }, + }; + }); + maxRectsPacker.addArray(rects); + let oversized = maxRectsPacker.bins.length > 1; + maxRectsPacker.bins.forEach(bin => { + bin.rects.forEach(rect => { + if (rect.oversized) { + oversized = true; + } + }); + }); + if (!oversized) { + return maxRectsPacker; + } else { + return null; } - if (normalMap) { - maxSize.x = Math.max(maxSize.x, normalMap.image.width); - maxSize.y = Math.max(maxSize.y, normalMap.image.height); + }; + + const hasTextures = textureSizes.some(textureSize => textureSize.x > 0 || textureSize.y > 0); + if (hasTextures) { + let atlas; + let atlasSize = startAtlasSize; + while (!(atlas = _attemptPack(textureSizes, atlasSize))) { + atlasSize *= 2; } - return maxSize; - }); - - // generate atlas layouts - const _packAtlases = () => { - const _attemptPack = (textureSizes, atlasSize) => { - const maxRectsPacker = new MaxRectsPacker(atlasSize, atlasSize, 1); - const rects = textureSizes.map((textureSize, index) => { - const {x: width, y: height} = textureSize; - return { - width, - height, - data: { - index, - }, - }; - }); - maxRectsPacker.addArray(rects); - let oversized = maxRectsPacker.bins.length > 1; - maxRectsPacker.bins.forEach(bin => { + return atlas; + } else { + return null; + } + }; + const atlas = _packAtlases(); + + // draw atlas images + const originalTextures = new Map(); // map of canvas to the texture that generated it + const _drawAtlasImages = atlas => { + const _getTexturesKey = textures => textures.map(t => t ? t.uuid : '').join(','); + const _drawAtlasImage = textures => { + if (atlas && textures.some(t => t !== null)) { + const canvasSize = Math.min(atlas.width, textureSize); + const canvasScale = canvasSize / atlas.width; + + const canvas = document.createElement('canvas'); + canvas.width = canvasSize; + canvas.height = canvasSize; + const ctx = canvas.getContext('2d'); + + atlas.bins.forEach(bin => { bin.rects.forEach(rect => { - if (rect.oversized) { - oversized = true; + const {x, y, width: w, height: h, data: {index}} = rect; + const texture = textures[index]; + if (texture) { + const image = texture.image; + + // draw the image in the correct box on the canvas + const tx = x * canvasScale; + const ty = y * canvasScale; + const tw = w * canvasScale; + const th = h * canvasScale; + ctx.drawImage(image, 0, 0, image.width, image.height, tx, ty, tw, th); + + if (!originalTextures.has(canvas)) { + originalTextures.set(canvas, texture); + } } }); }); - if (!oversized) { - return maxRectsPacker; - } else { - return null; - } - }; - - const hasTextures = textureSizes.some(textureSize => textureSize.x > 0 || textureSize.y > 0); - if (hasTextures) { - let atlas; - let atlasSize = startAtlasSize; - while (!(atlas = _attemptPack(textureSizes, atlasSize))) { - atlasSize *= 2; - } - return atlas; + + return canvas; } else { return null; } }; - const atlas = atlasTextures ? _packAtlases() : null; - - // draw atlas images - const originalTextures = new Map(); // map of canvas to the texture that generated it - const _drawAtlasImages = atlas => { - const _getTexturesKey = textures => textures.map(t => t ? t.uuid : '').join(','); - const _drawAtlasImage = textures => { - if (atlas && textures.some(t => t !== null)) { - const canvasSize = Math.min(atlas.width, textureSize); - const canvasScale = canvasSize / atlas.width; - - const canvas = document.createElement('canvas'); - canvas.width = canvasSize; - canvas.height = canvasSize; - const ctx = canvas.getContext('2d'); - - atlas.bins.forEach(bin => { - bin.rects.forEach(rect => { - const {x, y, width: w, height: h, data: {index}} = rect; - const texture = textures[index]; - if (texture) { - const image = texture.image; - - // draw the image in the correct box on the canvas - const tx = x * canvasScale; - const ty = y * canvasScale; - const tw = w * canvasScale; - const th = h * canvasScale; - ctx.drawImage(image, 0, 0, image.width, image.height, tx, ty, tw, th); - - if (!originalTextures.has(canvas)) { - originalTextures.set(canvas, texture); - } - } - }); - }); - return canvas; - } else { - return null; + const atlasImages = {}; + const atlasImagesMap = new Map(); // cache to alias identical textures + for (const textureType of textureTypes) { + const textures = mergeable[`${textureType}s`]; + const key = _getTexturesKey(textures); + + let atlasImage = atlasImagesMap.get(key); + if (atlasImage === undefined) { // cache miss + atlasImage = _drawAtlasImage(textures); + if (atlasImage !== null) { + atlasImage.key = key; } - }; - - const atlasImages = {}; - const atlasImagesMap = new Map(); // cache to alias identical textures - for (const textureType of textureTypes) { - const textures = mergeable[`${textureType}s`]; - const key = _getTexturesKey(textures); - - let atlasImage = atlasImagesMap.get(key); - if (atlasImage === undefined) { // cache miss - atlasImage = _drawAtlasImage(textures); - if (atlasImage !== null) { - atlasImage.key = key; + atlasImagesMap.set(key, atlasImage); + } + atlasImages[textureType] = atlasImage; + } + return atlasImages; + }; + const atlasImages = _drawAtlasImages(atlas); + + /* // XXX debug + { + const debugWidth = 300; + let textureTypeIndex = 0; + for (const textureType of textureTypes) { + const atlasImage = atlasImages[textureType]; + if (atlasImage) { + atlasImage.style.cssText = `\ + position: fixed; + top: ${mergeableIndex * debugWidth}px; + left: ${textureTypeIndex * debugWidth}px; + min-width: ${debugWidth}px; + max-width: ${debugWidth}px; + min-height: ${debugWidth}px; + z-index: 100; + `; + atlasImage.setAttribute('type', textureType); + document.body.appendChild(atlasImage); + textureTypeIndex++; + } + } + } */ + + // build attribute layouts + const _makeAttributeLayoutsFromGeometries = geometries => { + const attributeLayouts = []; + for (const g of geometries) { + const attributes = g.attributes; + for (const attributeName in attributes) { + const attribute = attributes[attributeName]; + let layout = attributeLayouts.find(layout => layout.name === attributeName); + if (layout) { + // sanity check that item size is the same + if (layout.itemSize !== attribute.itemSize) { + throw new Error(`attribute ${attributeName} has different itemSize: ${layout.itemSize}, ${attribute.itemSize}`); } - atlasImagesMap.set(key, atlasImage); + } else { + layout = new AttributeLayout( + attributeName, + attribute.array.constructor, + attribute.itemSize + ); + attributeLayouts.push(layout); } - atlasImages[textureType] = atlasImage; + + layout.count += attribute.count * attribute.itemSize; } - return atlasImages; - }; - const atlasImages = atlasTextures ? _drawAtlasImages(atlas) : null; - - /* // XXX debug - { - const debugWidth = 300; - let textureTypeIndex = 0; - for (const textureType of textureTypes) { - const atlasImage = atlasImages[textureType]; - if (atlasImage) { - atlasImage.style.cssText = `\ - position: fixed; - top: ${mergeableIndex * debugWidth}px; - left: ${textureTypeIndex * debugWidth}px; - min-width: ${debugWidth}px; - max-width: ${debugWidth}px; - min-height: ${debugWidth}px; - z-index: 100; - `; - atlasImage.setAttribute('type', textureType); - document.body.appendChild(atlasImage); - textureTypeIndex++; + } + return attributeLayouts; + }; + const attributeLayouts = _makeAttributeLayoutsFromGeometries(geometries); + + const _makeMorphAttributeLayoutsFromGeometries = geometries => { + // create morph layouts + const morphAttributeLayouts = []; + for (const g of geometries) { + const morphAttributes = g.morphAttributes; + for (const morphAttributeName in morphAttributes) { + const morphAttribute = morphAttributes[morphAttributeName]; + let morphLayout = morphAttributeLayouts.find(l => l.name === morphAttributeName); + if (!morphLayout) { + morphLayout = new MorphAttributeLayout( + morphAttributeName, + morphAttribute[0].array.constructor, + morphAttribute[0].itemSize, + morphAttribute.length + ); + morphAttributeLayouts.push(morphLayout); } + + morphLayout.count += morphAttribute[0].count * morphAttribute[0].itemSize; } - } */ + } + return morphAttributeLayouts; + }; + const morphAttributeLayouts = _makeMorphAttributeLayoutsFromGeometries(geometries); + // console.log('got attribute layouts', attributeLayouts, morphAttributeLayouts); - // build attribute layouts - const _makeAttributeLayoutsFromGeometries = geometries => { - const attributeLayouts = []; + const _forceGeometriesAttributeLayouts = (attributeLayouts, geometries) => { + for (const layout of attributeLayouts) { for (const g of geometries) { - const attributes = g.attributes; - for (const attributeName in attributes) { - const attribute = attributes[attributeName]; - let layout = attributeLayouts.find(layout => layout.name === attributeName); - if (layout) { - // sanity check that item size is the same - if (layout.itemSize !== attribute.itemSize) { - throw new Error(`attribute ${attributeName} has different itemSize: ${layout.itemSize}, ${attribute.itemSize}`); - } + let gAttribute = g.attributes[layout.name]; + if (!gAttribute) { + if (layout.name === 'skinIndex' || layout.name === 'skinWeight') { + gAttribute = layout.makeDefault(g); + g.setAttribute(layout.name, gAttribute); + + layout.count += gAttribute.count * gAttribute.itemSize; } else { - layout = new AttributeLayout( - attributeName, - attribute.array.constructor, - attribute.itemSize - ); - attributeLayouts.push(layout); + throw new Error(`unknown layout ${layout.name}`); } - - layout.count += attribute.count * attribute.itemSize; } } - return attributeLayouts; - }; - const attributeLayouts = _makeAttributeLayoutsFromGeometries(geometries); + } - const _makeMorphAttributeLayoutsFromGeometries = geometries => { - // create morph layouts - const morphAttributeLayouts = []; + for (const morphLayout of morphAttributeLayouts) { for (const g of geometries) { - const morphAttributes = g.morphAttributes; - for (const morphAttributeName in morphAttributes) { - const morphAttribute = morphAttributes[morphAttributeName]; - let morphLayout = morphAttributeLayouts.find(l => l.name === morphAttributeName); - if (!morphLayout) { - morphLayout = new MorphAttributeLayout( - morphAttributeName, - morphAttribute[0].array.constructor, - morphAttribute[0].itemSize, - morphAttribute.length - ); - morphAttributeLayouts.push(morphLayout); - } + let morphAttribute = g.morphAttributes[morphLayout.name]; + if (!morphAttribute) { + // console.log('missing morph attribute', morphLayout, morphAttribute); + + morphAttribute = morphLayout.makeDefault(g); + g.morphAttributes[morphLayout.name] = morphAttribute; morphLayout.count += morphAttribute[0].count * morphAttribute[0].itemSize; - } - } - return morphAttributeLayouts; - }; - const morphAttributeLayouts = _makeMorphAttributeLayoutsFromGeometries(geometries); - // console.log('got attribute layouts', attributeLayouts, morphAttributeLayouts); - const _forceGeometriesAttributeLayouts = (attributeLayouts, geometries) => { - for (const layout of attributeLayouts) { - for (const g of geometries) { - let gAttribute = g.attributes[layout.name]; - if (!gAttribute) { - if (layout.name === 'skinIndex' || layout.name === 'skinWeight') { - gAttribute = new THREE.BufferAttribute(new Float32Array(g.attributes.position.count * layout.itemSize), layout.itemSize); - g.setAttribute(layout.name, gAttribute); - } else { - throw new Error(`unknown layout ${layout.name}`); - } - } + /* if (layout.name === 'skinIndex' || layout.name === 'skinWeight') { + gAttribute = new THREE.BufferAttribute(new Float32Array(g.attributes.position.count * layout.itemSize), layout.itemSize); + g.setAttribute(layout.name, gAttribute); + } else { + throw new Error(`unknown layout ${layout.name}`); + } */ } } - }; - const _mergeAttributes = (geometry, geometries, attributeLayouts) => { - for (const layout of attributeLayouts) { - const attributeData = new layout.TypedArrayConstructor(layout.count); - const attribute = new THREE.BufferAttribute(attributeData, layout.itemSize); - let attributeDataIndex = 0; + } + }; + const _mergeAttributes = (geometry, geometries, attributeLayouts) => { + for (const layout of attributeLayouts) { + const attributeData = new layout.TypedArrayConstructor(layout.count); + const attribute = new THREE.BufferAttribute(attributeData, layout.itemSize); + let attributeDataIndex = 0; + for (const g of geometries) { + const gAttribute = g.attributes[layout.name]; + attributeData.set(gAttribute.array, attributeDataIndex); + attributeDataIndex += gAttribute.count * gAttribute.itemSize; + } + // sanity check + if (attributeDataIndex !== layout.count) { + console.warn('desynced attribute data 1', layout.name, attributeDataIndex, layout.count); + debugger; + } + geometry.setAttribute(layout.name, attribute); + } + }; + const _mergeMorphAttributes = (geometry, geometries, morphAttributeLayouts) => { + for (const morphLayout of morphAttributeLayouts) { + const morphsArray = Array(morphLayout.arraySize); + for (let i = 0; i < morphLayout.arraySize; i++) { + const morphData = new morphLayout.TypedArrayConstructor(morphLayout.count); + const morphAttribute = new THREE.BufferAttribute(morphData, morphLayout.itemSize); + morphsArray[i] = morphAttribute; + let morphDataIndex = 0; for (const g of geometries) { - const gAttribute = g.attributes[layout.name]; - attributeData.set(gAttribute.array, attributeDataIndex); - attributeDataIndex += gAttribute.count * gAttribute.itemSize; + let gMorphAttribute = g.morphAttributes[morphLayout.name]; + gMorphAttribute = gMorphAttribute?.[i]; + if (gMorphAttribute) { + morphData.set(gMorphAttribute.array, morphDataIndex); + morphDataIndex += gMorphAttribute.count * gMorphAttribute.itemSize; + } else { + const matchingAttribute = g.attributes[morphLayout.name]; + morphDataIndex += matchingAttribute.count * matchingAttribute.itemSize; + } } // sanity check - if (attributeDataIndex !== layout.count) { - console.warn('desynced morph data', layout.name, attributeDataIndex, layout.count); + if (morphDataIndex !== morphLayout.count) { + console.warn('desynced morph data 2', morphLayout.name, morphDataIndex, morphLayout.count); } - geometry.setAttribute(layout.name, attribute); } - }; - const _mergeMorphAttributes = (geometry, geometries, morphAttributeLayouts) => { - for (const morphLayout of morphAttributeLayouts) { - const morphsArray = Array(morphLayout.arraySize); - for (let i = 0; i < morphLayout.arraySize; i++) { - const morphData = new morphLayout.TypedArrayConstructor(morphLayout.count); - const morphAttribute = new THREE.BufferAttribute(morphData, morphLayout.itemSize); - morphsArray[i] = morphAttribute; - let morphDataIndex = 0; - for (const g of geometries) { - let gMorphAttribute = g.morphAttributes[morphLayout.name]; - gMorphAttribute = gMorphAttribute?.[i]; - if (gMorphAttribute) { - morphData.set(gMorphAttribute.array, morphDataIndex); - morphDataIndex += gMorphAttribute.count * gMorphAttribute.itemSize; - } else { - const matchingAttribute = g.attributes[morphLayout.name]; - morphDataIndex += matchingAttribute.count * matchingAttribute.itemSize; + geometry.morphAttributes[morphLayout.name] = morphsArray; + } + }; + const _mergeIndices = (geometry, geometries) => { + let indexCount = 0; + for (const g of geometries) { + indexCount += g.index.count; + } + const indexData = new Uint32Array(indexCount); + + let positionOffset = 0; + let indexOffset = 0; + for (const g of geometries) { + const srcIndexData = g.index.array; + for (let i = 0; i < srcIndexData.length; i++) { + indexData[indexOffset++] = srcIndexData[i] + positionOffset; + } + positionOffset += g.attributes.position.count; + } + geometry.setIndex(new THREE.BufferAttribute(indexData, 1)); + }; + const _remapGeometryUvs = (geometry, geometries) => { + if (atlas) { + let uvIndex = 0; + const geometryUvOffsets = geometries.map(g => { + const start = uvIndex; + const count = g.attributes.uv.count; + uvIndex += count; + return { + start, + count, + }; + }); + + const canvasSize = Math.min(atlas.width, textureSize); + const canvasScale = canvasSize / atlas.width; + atlas.bins.forEach(bin => { + bin.rects.forEach(rect => { + const {x, y, width: w, height: h, data: {index}} = rect; + + if (w > 0 && h > 0) { + const {start, count} = geometryUvOffsets[index]; + + const tx = x * canvasScale; + const ty = y * canvasScale; + const tw = w * canvasScale; + const th = h * canvasScale; + + for (let i = 0; i < count; i++) { + const uvIndex = start + i; + + localVector2D.fromArray(geometry.attributes.uv.array, uvIndex * 2); + modUv(localVector2D); + localVector2D + .multiply( + localVector2D2.set(tw/canvasSize, th/canvasSize) + ) + .add( + localVector2D2.set(tx/canvasSize, ty/canvasSize) + ); + localVector2D.toArray(geometry.attributes.uv.array, uvIndex * 2); } } - // sanity check - if (morphDataIndex !== morphLayout.count) { - console.warn('desynced morph data', morphLayout.name, morphDataIndex, morphLayout.count); - } - } - geometry.morphAttributes[morphLayout.name] = morphsArray; - } - }; - const _mergeIndices = (geometry, geometries) => { - let indexCount = 0; - for (const g of geometries) { - indexCount += g.index.count; - } - const indexData = new Uint32Array(indexCount); - - let positionOffset = 0; - let indexOffset = 0; - for (const g of geometries) { - const srcIndexData = g.index.array; - for (let i = 0; i < srcIndexData.length; i++) { - indexData[indexOffset++] = srcIndexData[i] + positionOffset; - } - positionOffset += g.attributes.position.count; - } - geometry.setIndex(new THREE.BufferAttribute(indexData, 1)); - }; - const _remapGeometryUvs = (geometry, geometries) => { - if (atlas) { - let uvIndex = 0; - const geometryUvOffsets = geometries.map(g => { - const start = uvIndex; - const count = g.attributes.uv.count; - uvIndex += count; - return { - start, - count, - }; }); + }); + } + }; + const _mergeGeometries = geometries => { + const geometry = new THREE.BufferGeometry(); + geometry.morphTargetsRelative = true; - const canvasSize = Math.min(atlas.width, textureSize); - const canvasScale = canvasSize / atlas.width; - atlas.bins.forEach(bin => { - bin.rects.forEach(rect => { - const {x, y, width: w, height: h, data: {index}} = rect; - - if (w > 0 && h > 0) { - const {start, count} = geometryUvOffsets[index]; + _forceGeometriesAttributeLayouts(attributeLayouts, geometries); + _mergeAttributes(geometry, geometries, attributeLayouts); + _mergeMorphAttributes(geometry, geometries, morphAttributeLayouts); + _mergeIndices(geometry, geometries); + _remapGeometryUvs(geometry, geometries); - const tx = x * canvasScale; - const ty = y * canvasScale; - const tw = w * canvasScale; - const th = h * canvasScale; + return geometry; + }; + const geometry = _mergeGeometries(geometries); + // console.log('got geometry', geometry); - for (let i = 0; i < count; i++) { - const uvIndex = start + i; - - localVector2D.fromArray(geometry.attributes.uv.array, uvIndex * 2); - _clampUv(localVector2D); - localVector2D - .multiply( - localVector2D2.set(tw/canvasSize, th/canvasSize) - ) - .add( - localVector2D2.set(tx/canvasSize, ty/canvasSize) - ); - localVector2D.toArray(geometry.attributes.uv.array, uvIndex * 2); - } - } - }); - }); - } + const _makeAtlasTextures = atlasImages => { + const _makeAtlasTexture = atlasImage => { + const originalTexture = originalTextures.get(atlasImage); + + const t = new THREE.Texture(atlasImage); + t.minFilter = originalTexture.minFilter; + t.magFilter = originalTexture.magFilter; + t.wrapS = originalTexture.wrapS; + t.wrapT = originalTexture.wrapT; + t.mapping = originalTexture.mapping; + // t.encoding = originalTexture.encoding; + + t.flipY = false; + t.needsUpdate = true; + + return t; }; - const _mergeGeometries = geometries => { - const geometry = new THREE.BufferGeometry(); - geometry.morphTargetsRelative = true; - - _forceGeometriesAttributeLayouts(attributeLayouts, geometries); - _mergeAttributes(geometry, geometries, attributeLayouts); - _mergeMorphAttributes(geometry, geometries, morphAttributeLayouts); - _mergeIndices(geometry, geometries); - _remapGeometryUvs(geometry, geometries); - return geometry; - }; - const geometry = _mergeGeometries(geometries); - // console.log('got geometry', geometry); + const result = {}; + const textureMap = new Map(); // cache to alias identical textures + for (const textureType of textureTypes) { + const atlasImage = atlasImages[textureType]; - const _makeAtlasTextures = atlasImages => { - const _makeAtlasTexture = atlasImage => { - const originalTexture = originalTextures.get(atlasImage); - - const t = new THREE.Texture(atlasImage); - t.minFilter = originalTexture.minFilter; - t.magFilter = originalTexture.magFilter; - t.wrapS = originalTexture.wrapS; - t.wrapT = originalTexture.wrapT; - t.mapping = originalTexture.mapping; - // t.encoding = originalTexture.encoding; - - t.flipY = false; - t.needsUpdate = true; - - return t; - }; - - const result = {}; - const textureMap = new Map(); // cache to alias identical textures - for (const textureType of textureTypes) { - const atlasImage = atlasImages[textureType]; - - if (atlasImage) { - let atlasTexture = textureMap.get(atlasImage.key); - if (atlasTexture === undefined) { // cache miss - atlasTexture = _makeAtlasTexture(atlasImage); - textureMap.set(atlasImage.key, atlasTexture); - } - result[textureType] = atlasTexture; - } else { - result[textureType] = null; + if (atlasImage) { + let atlasTexture = textureMap.get(atlasImage.key); + if (atlasTexture === undefined) { // cache miss + atlasTexture = _makeAtlasTexture(atlasImage); + textureMap.set(atlasImage.key, atlasTexture); } + result[textureType] = atlasTexture; + } else { + result[textureType] = null; } - return result; - }; - const ts = atlasImages ? _makeAtlasTextures(atlasImages) : null; + } + return result; + }; + const atlasTextures = atlasImages ? _makeAtlasTextures(atlasImages) : null; + + return { + atlas, + atlasImages, + attributeLayouts, + morphAttributeLayouts, + geometry, + atlasTextures, + }; +}; + +export const optimizeAvatarModel = (model, options = {}) => { + const textureSize = options.textureSize ?? defaultTextureSize; + + const mergeables = getMergeableObjects(model); + + const _mergeMesh = (mergeable, mergeableIndex) => { + const { + type, + material, + geometries, + maps, + emissiveMaps, + normalMaps, + skeletons, + morphTargetDictionaryArray, + morphTargetInfluencesArray, + } = mergeable; + const { + atlas, + atlasImages, + attributeLayouts, + morphAttributeLayouts, + geometry, + atlasTextures, + } = mergeGeometryTextureAtlas(mergeable, textureSize); /* const m = new THREE.MeshPhongMaterial({ color: 0xFF0000, }); */ const m = material; const _updateMaterial = () => { - if (ts) { + if (atlasTextures) { for (const textureType of textureTypes) { - const t = ts[textureType]; + const atlasTexture = atlasTextures[textureType]; - if (t) { - m[textureType] = t; + if (atlasTexture) { + m[textureType] = atlasTexture; if (m.uniforms) { - m.uniforms[textureType].value = t; + m.uniforms[textureType].value = atlasTexture; m.uniforms[textureType].needsUpdate = true; } } @@ -549,8 +607,4 @@ const optimizeAvatarModel = (model, options = {}) => { object.add(mesh); } return object; -}; - -export { - optimizeAvatarModel, }; \ No newline at end of file From 380322568ff66454355f670e222295a45464d04b Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Fri, 22 Apr 2022 19:09:14 -0400 Subject: [PATCH 39/57] Route avatar chruncher through avatar optimizer methods --- avatar-cruncher.js | 459 +++++---------------------------------------- 1 file changed, 43 insertions(+), 416 deletions(-) diff --git a/avatar-cruncher.js b/avatar-cruncher.js index 9dae03432f..b4de98ba73 100644 --- a/avatar-cruncher.js +++ b/avatar-cruncher.js @@ -1,5 +1,8 @@ import * as THREE from 'three'; import {MaxRectsPacker} from 'maxrects-packer'; +import {modUv} from './util.js'; + +import {getMergeableObjects, mergeGeometryTextureAtlas} from './avatar-optimizer.js'; const defaultTextureSize = 4096; const startAtlasSize = 512; @@ -14,13 +17,19 @@ class AttributeLayout { this.name = name; this.TypedArrayConstructor = TypedArrayConstructor; this.itemSize = itemSize; - this.index = 0; + this.count = 0; - this.depth = 0; } } -const crunchAvatarModel = (model, options = {}) => { - const atlasTextures = !!(options.textures ?? true); +class MorphAttributeLayout extends AttributeLayout { + constructor(name, TypedArrayConstructor, itemSize, arraySize) { + super(name, TypedArrayConstructor, itemSize); + this.arraySize = arraySize; + } +} + +export const crunchAvatarModel = (model, options = {}) => { + // const atlasTexturesEnabled = !!(options.textures ?? true); const textureSize = options.textureSize ?? defaultTextureSize; const textureTypes = [ @@ -29,420 +38,43 @@ const crunchAvatarModel = (model, options = {}) => { 'normalMap', ]; - const _collectObjects = () => { - const meshes = []; - const geometries = []; - const materials = []; - const textures = {}; - for (const textureType of textureTypes) { - textures[textureType] = []; - } - let textureGroupsMap = new WeakMap(); - const skeletons = []; - { - let indexIndex = 0; - model.traverse(node => { - if (node.isMesh && !node.parent?.isBone) { - meshes.push(node); - - const geometry = node.geometry; - geometries.push(geometry); - - const startIndex = indexIndex; - const count = geometry.index.count; - const _pushMaterial = material => { - materials.push(material); - for (const k of textureTypes) { - const texture = material[k]; - if (texture) { - const texturesOfType = textures[k]; - if (!texturesOfType.includes(texture)) { - texturesOfType.push(texture); - } - let textureGroups = textureGroupsMap.get(texture); - if (!textureGroups) { - textureGroups = []; - textureGroupsMap.set(texture, textureGroups); - } - textureGroups.push({ - startIndex, - count, - }); - } - } - }; - - let material = node.material; - if (Array.isArray(material)) { - for (let i = 0; i < material.length; i++) { - _pushMaterial(material[i]); - } - } else { - _pushMaterial(material); - } - - if (node.skeleton) { - if (!skeletons.includes(node.skeleton)) { - skeletons.push(node.skeleton); - } - } - - indexIndex += geometry.index.count; - } - }); - } - return { - meshes, - geometries, - materials, - textures, - textureGroupsMap, - skeletons, - }; - }; - - // collect objects + const getObjectKey = () => ''; + const mergeables = getMergeableObjects(model, getObjectKey); + const mergeable = mergeables[0]; const { - meshes, - geometries, - materials, - textures, - textureGroupsMap, skeletons, - } = _collectObjects(); - - // generate atlas layouts - const _packAtlases = () => { - const _attempt = (k, atlasSize) => { - const maxRectsPacker = new MaxRectsPacker(atlasSize, atlasSize, 1); - const rects = textures[k].map(t => { - const w = t.image.width; - const h = t.image.height; - const image = t.image; - const groups = textureGroupsMap.get(t); - return { - width: w, - height: h, - data: { - image, - groups, - }, - }; - }); - maxRectsPacker.addArray(rects); - let oversized = maxRectsPacker.bins.length > 1; - maxRectsPacker.bins.forEach(bin => { - bin.rects.forEach(rect => { - if (rect.oversized) { - oversized = true; - } - }); - }); - if (!oversized) { - return maxRectsPacker; - } else { - return null; - } - }; - - const atlases = {}; - for (const k of textureTypes) { - let atlas; - let atlasSize = startAtlasSize; - while (!(atlas = _attempt(k, atlasSize))) { - atlasSize *= 2; - } - atlases[k] = atlas; - } - return atlases; - }; - const atlases = atlasTextures ? _packAtlases() : null; - - // build attribute layouts - const _makeAttributeLayoutsFromGeometries = geometries => { - const geometry = geometries[0]; - const attributes = geometry.attributes; - const attributeLayouts = []; - for (const attributeName in attributes) { - const attribute = attributes[attributeName]; - const layout = new AttributeLayout(attributeName, attribute.array.constructor, attribute.itemSize); - attributeLayouts.push(layout); - } - - for (const layout of attributeLayouts) { - for (const g of geometries) { - let gAttribute = g.attributes[layout.name]; - if (!gAttribute) { - if (layout.name === 'skinIndex' || layout.name === 'skinWeight') { - gAttribute = new THREE.BufferAttribute(new Float32Array(g.attributes.position.count * layout.itemSize), layout.itemSize); - g.setAttribute(layout.name, gAttribute); - } else { - throw new Error('unknown layout'); - } - } - layout.count += gAttribute.count * gAttribute.itemSize; - } - } - - return attributeLayouts; - }; - const _makeMorphAttributeLayoutsFromGeometries = geometries => { - // create morph layouts - const morphAttributeLayouts = []; - for (const geometry of geometries) { - const morphAttributes = geometry.morphAttributes; - for (const morphAttributeName in morphAttributes) { - const morphAttribute = morphAttributes[morphAttributeName]; - let morphLayout = morphAttributeLayouts.find(l => l.name === morphAttributeName); - if (!morphLayout) { - morphLayout = new AttributeLayout(morphAttributeName, morphAttribute[0].array.constructor, morphAttribute[0].itemSize); - morphLayout.depth = morphAttribute.length; - morphAttributeLayouts.push(morphLayout); - } - } - } - - // compute morph layouts sizes - for (const morphLayout of morphAttributeLayouts) { - for (const g of geometries) { - const morphAttribute = g.morphAttributes[morphLayout.name]; - if (morphAttribute) { - morphLayout.count += morphAttribute[0].count * morphAttribute[0].itemSize; - // console.log('morph layout add 1', morphLayout.count, morphAttribute[0].count, morphAttribute[0].itemSize); - } else { - const matchingGeometryAttribute = g.attributes[morphLayout.name]; - if (matchingGeometryAttribute) { - morphLayout.count += matchingGeometryAttribute.count * matchingGeometryAttribute.itemSize; - // console.log('morph layout add 2', morphLayout.count, matchingGeometryAttribute.count, matchingGeometryAttribute.itemSize); - } else { - console.warn('geometry attributes desynced with morph attributes', g.attributes, morphAttribute); - } - } - } - } - return morphAttributeLayouts; - }; - const attributeLayouts = _makeAttributeLayoutsFromGeometries(geometries); - const morphAttributeLayouts = _makeMorphAttributeLayoutsFromGeometries(geometries); - - // validate attribute layouts - for (let i = 0; i < meshes.length; i++) { - const mesh = meshes[i]; - /* if (!mesh.skeleton) { - console.log('no skeleton', mesh);; - } */ - - const geometry = mesh.geometry; - if (!geometry.index) { - console.log('no index', mesh); - } - } - if (skeletons.length !== 1) { - console.log('did not have single skeleton', skeletons); - } - - /* { - let canvasOffsetX = 0; - let canvasOffsetY = 0; - for (const texture of textures.map) { - // copy the texture to canvas and attach it to the DOM - const canvas = document.createElement('canvas'); - canvas.width = texture.image.width; - canvas.height = texture.image.height; - const ctx = canvas.getContext('2d'); - ctx.drawImage(texture.image, 0, 0); - const displaySize = texture.image.width / 16; - canvas.style.cssText = `\ - position: fixed; - top: ${canvasOffsetY}px; - left: ${canvasOffsetX}px; - width: ${displaySize}px; - height: ${displaySize}px; - z-index: 10; - `; - document.body.appendChild(canvas); - canvasOffsetX += displaySize; - if (canvasOffsetX >= 256) { - canvasOffsetX = 0; - canvasOffsetY += 128; - } - } - } */ - - // console.log('got avatar breakout', meshes, geometries, materials, textures, skeletons, morphAttributeLayouts); - - // build geometry - const geometry = new THREE.BufferGeometry(); - // attributes - for (const layout of attributeLayouts) { - const attributeData = new layout.TypedArrayConstructor(layout.count); - const attribute = new THREE.BufferAttribute(attributeData, layout.itemSize); - for (const g of geometries) { - const gAttribute = g.attributes[layout.name]; - attributeData.set(gAttribute.array, layout.index); - layout.index += gAttribute.count * gAttribute.itemSize; - } - geometry.setAttribute(layout.name, attribute); - } - // morph attributes - for (const morphLayout of morphAttributeLayouts) { - const morphsArray = Array(morphLayout.depth); - for (let i = 0; i < morphLayout.depth; i++) { - const morphData = new morphLayout.TypedArrayConstructor(morphLayout.count); - let morphDataIndex = 0; - const morphAttribute = new THREE.BufferAttribute(morphData, morphLayout.itemSize); - morphsArray[i] = morphAttribute; - for (const g of geometries) { - let gMorphAttribute = g.morphAttributes[morphLayout.name]; - gMorphAttribute = gMorphAttribute && gMorphAttribute[i]; - if (gMorphAttribute) { - morphData.set(gMorphAttribute.array, morphDataIndex); - morphDataIndex += gMorphAttribute.count * gMorphAttribute.itemSize; - // console.log('new index 1', morphLayout.name, gMorphAttribute.array.some(n => n !== 0), morphDataIndex, gMorphAttribute.count, gMorphAttribute.itemSize); - } else { - const matchingAttribute = g.attributes[morphLayout.name]; - morphDataIndex += matchingAttribute.count * matchingAttribute.itemSize; - // console.log('new index 2', g, morphDataIndex, matchingAttribute.count, matchingAttribute.itemSize); - } - } - if (morphDataIndex !== morphLayout.count) { - console.warn('desynced morph data', morphLayout.name, morphDataIndex, morphLayout.count); - } - } - geometry.morphAttributes[morphLayout.name] = morphsArray; - } - // index - let indexCount = 0; - for (const g of geometries) { - indexCount += g.index.count; - } - const indexData = new Uint32Array(indexCount); - let positionOffset = 0; - let indexOffset = 0; - for (const g of geometries) { - const srcIndexData = g.index.array; - for (let i = 0; i < srcIndexData.length; i++) { - indexData[indexOffset++] = srcIndexData[i] + positionOffset; - } - positionOffset += g.attributes.position.count; - } - geometry.setIndex(new THREE.BufferAttribute(indexData, 1)); - geometry.morphTargetsRelative = true; - - /* const uv3Data = new Float32Array(geometry.attributes.uv.count * 4); - const uv3 = new THREE.BufferAttribute(uv3Data, 4); - geometry.setAttribute('uv3', uv3); */ - - /* // these uvs can be used to color code the mesh by material or texture - const uv4Data = new Float32Array(geometry.attributes.uv.count * 4); - const uv4 = new THREE.BufferAttribute(uv4Data, 4); - geometry.setAttribute('uv4', uv4); */ - - // verify - for (const layout of attributeLayouts) { - if (layout.index !== layout.count) { - console.log('bad layout count', layout.index, layout.count); - } - } - if (indexOffset !== indexCount) { - console.log('bad final index', indexOffset, indexCount); - } - - // draw the atlas - const _drawAtlases = () => { - const seenUvIndexes = new Map(); - const _drawAtlas = atlas => { - const canvas = document.createElement('canvas'); - const canvasSize = Math.min(atlas.width, textureSize); - const canvasScale = canvasSize / atlas.width; - canvas.width = canvasSize; - canvas.height = canvasSize; - const ctx = canvas.getContext('2d'); - - atlas.bins.forEach(bin => { - bin.rects.forEach(rect => { - const {x, y, width: w, height: h, data: {image, groups}} = rect; - // draw the image in the correct box on the canvas - const tx = x * canvasScale; - const ty = y * canvasScale; - const tw = w * canvasScale; - const th = h * canvasScale; - ctx.drawImage(image, 0, 0, image.width, image.height, tx, ty, tw, th); - - // const testUv = new THREE.Vector2(Math.random(), Math.random()); - for (const group of groups) { - const {startIndex, count} = group; - for (let i = 0; i < count; i++) { - const uvIndex = geometry.index.array[startIndex + i]; - - // XXX NOTE: this code is slightly wrong. it will generate a unified uv map (first come first served to the uv index) - // that means that the different maps might get the wrong uv. - // the diffuse map takes priority so it looks ok. - // the right way to do this is to have a separate uv map for each map. - if (!seenUvIndexes.get(uvIndex)) { - seenUvIndexes.set(uvIndex, true); - - localVector2D.fromArray(geometry.attributes.uv.array, uvIndex * 2); - localVector2D.multiply( - localVector2D2.set(tw/canvasSize, th/canvasSize) - ).add( - localVector2D2.set(tx/canvasSize, ty/canvasSize) - ); - localVector2D.toArray(geometry.attributes.uv.array, uvIndex * 2); - - /* localVector4D.set(x/atlas.width, y/atlas.height, w/atlas.width, h/atlas.height); - localVector4D.toArray(geometry.attributes.uv3.array, uvIndex * 4); */ - /* localVector4D.set(testUv.x, testUv.y, testUv.x, testUv.y); - localVector4D.toArray(geometry.attributes.uv4.array, uvIndex * 4); */ - } - } - } - }); - }); - atlas.image = canvas; - - return atlas; - }; - - // generate atlas for each map; they are all separate - const result = {}; - { - let canvasIndex = 0; - for (const k of textureTypes) { - const atlas = atlases[k]; - const atlas2 = _drawAtlas(atlas); - - /* const displaySize = 256; - atlas2.image.style.cssText = `\ - position: fixed; - top: 0; - left: ${canvasIndex * displaySize}px; - width: ${displaySize}px; - height: ${displaySize}px; - z-index: 10; - `; - document.body.appendChild(atlas2.image); */ - - result[k] = atlas2; - - canvasIndex++; - } - } - return result; - }; - const textureAtlases = atlasTextures ? _drawAtlases() : null; + morphTargetDictionaryArray, + morphTargetInfluencesArray, + } = mergeable; + const { + atlas, + atlasImages, + attributeLayouts, + morphAttributeLayouts, + geometry, + atlasTextures, + } = mergeGeometryTextureAtlas(mergeable, textureSize); + /* console.log('got atlas', { + atlas, + atlasImages, + attributeLayouts, + morphAttributeLayouts, + geometry, + atlasTextures, + }); */ // create material // const material = new THREE.MeshStandardMaterial(); const material = new THREE.MeshBasicMaterial(); - if (atlasTextures) { + // if (atlasTextures) { for (const k of textureTypes) { - const t = new THREE.Texture(textureAtlases[k].image); + /* const t = new THREE.Texture(textureAtlases[k].image); t.flipY = false; - t.needsUpdate = true; + t.needsUpdate = true; */ + const t = atlasTextures[k]; material[k] = t; } - } + // } material.roughness = 1; material.alphaTest = 0.1; material.transparent = true; @@ -450,13 +82,8 @@ const crunchAvatarModel = (model, options = {}) => { // create mesh const crunchedModel = new THREE.SkinnedMesh(geometry, material); crunchedModel.skeleton = skeletons[0]; - const deepestMorphMesh = meshes.find(m => (m.morphTargetInfluences ? m.morphTargetInfluences.length : 0) === morphAttributeLayouts[0].depth); - crunchedModel.morphTargetDictionary = deepestMorphMesh.morphTargetDictionary; - crunchedModel.morphTargetInfluences = deepestMorphMesh.morphTargetInfluences; - + crunchedModel.morphTargetDictionary = morphTargetDictionaryArray[0]; + crunchedModel.morphTargetInfluences = morphTargetInfluencesArray[0]; + crunchedModel.frustumCulled = false; return crunchedModel; -}; - -export { - crunchAvatarModel, }; \ No newline at end of file From 7022d5e0fe07a235dccbe25acf6639864e78c6ef Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Fri, 22 Apr 2022 19:22:49 -0400 Subject: [PATCH 40/57] Add geometry-texture-atlas.js --- geometry-texture-atlas.js | 533 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 533 insertions(+) create mode 100644 geometry-texture-atlas.js diff --git a/geometry-texture-atlas.js b/geometry-texture-atlas.js new file mode 100644 index 0000000000..78d9ffcf10 --- /dev/null +++ b/geometry-texture-atlas.js @@ -0,0 +1,533 @@ +import * as THREE from 'three'; +import {MaxRectsPacker} from 'maxrects-packer'; +import {getRenderer} from './renderer.js'; +import {modUv} from './util.js'; + +const defaultTextureSize = 4096; +const startAtlasSize = 512; + +const localVector2D = new THREE.Vector2(); +const localVector2D2 = new THREE.Vector2(); + +const textureTypes = [ + 'map', + 'emissiveMap', + 'normalMap', + 'shadeTexture', +]; + +class AttributeLayout { + constructor(name, TypedArrayConstructor, itemSize) { + this.name = name; + this.TypedArrayConstructor = TypedArrayConstructor; + this.itemSize = itemSize; + + this.count = 0; + } + makeDefault(g) { + return new THREE.BufferAttribute( + new this.TypedArrayConstructor(g.attributes.position.count * this.itemSize), + this.itemSize + ); + } +} +class MorphAttributeLayout extends AttributeLayout { + constructor(name, TypedArrayConstructor, itemSize, arraySize) { + super(name, TypedArrayConstructor, itemSize); + this.arraySize = arraySize; + } + makeDefault(g) { + return Array(this.arraySize).fill(super.makeDefault(g)); + } +} + +const getObjectKeyDefault = (type, object, material) => { + const renderer = getRenderer(); + return [ + type, + renderer.getProgramCacheKey(object, material), + ].join(','); +}; +export const getMergeableObjects = (model, getObjectKey = getObjectKeyDefault) => { + const mergeables = new Map(); + model.traverse(o => { + if (o.isMesh && o.geometry.type === 'BufferGeometry') { + let type; + if (o.isSkinnedMesh) { + type = 'skinnedMesh'; + } else { + type = 'mesh'; + } + + const objectGeometry = o.geometry; + const morphTargetDictionary = o.morphTargetDictionary; + const morphTargetInfluences = o.morphTargetInfluences; + const objectMaterials = Array.isArray(o.material) ? o.material : [o.material]; + for (const objectMaterial of objectMaterials) { + const { + map = null, + emissiveMap = null, + normalMap = null, + shadeTexture = null, + } = objectMaterial; + const skeleton = o.skeleton ?? null; + + const key = getObjectKey(type, o, objectMaterial); + + let m = mergeables.get(key); + if (!m) { + m = { + type, + material: objectMaterial, + geometries: [], + maps: [], + emissiveMaps: [], + normalMaps: [], + shadeTextures: [], + skeletons: [], + morphTargetDictionaryArray: [], + morphTargetInfluencesArray: [], + }; + mergeables.set(key, m); + } + + m.geometries.push(objectGeometry); + m.maps.push(map); + m.emissiveMaps.push(emissiveMap); + m.normalMaps.push(normalMap); + m.shadeTextures.push(shadeTexture); + m.skeletons.push(skeleton); + m.morphTargetDictionaryArray.push(morphTargetDictionary); + m.morphTargetInfluencesArray.push(morphTargetInfluences); + } + } + }); + return Array.from(mergeables.values()); +}; + +export const mergeGeometryTextureAtlas = (mergeable, textureSize) => { + const { + type, + material, + geometries, + maps, + emissiveMaps, + normalMaps, + skeletons, + morphTargetDictionaryArray, + morphTargetInfluencesArray, + } = mergeable; + + // compute texture sizes + const textureSizes = maps.map((map, i) => { + const emissiveMap = emissiveMaps[i]; + const normalMap = normalMaps[i]; + + const maxSize = new THREE.Vector2(0, 0); + if (map) { + maxSize.x = Math.max(maxSize.x, map.image.width); + maxSize.y = Math.max(maxSize.y, map.image.height); + } + if (emissiveMap) { + maxSize.x = Math.max(maxSize.x, emissiveMap.image.width); + maxSize.y = Math.max(maxSize.y, emissiveMap.image.height); + } + if (normalMap) { + maxSize.x = Math.max(maxSize.x, normalMap.image.width); + maxSize.y = Math.max(maxSize.y, normalMap.image.height); + } + return maxSize; + }); + + // generate atlas layouts + const _packAtlases = () => { + const _attemptPack = (textureSizes, atlasSize) => { + const maxRectsPacker = new MaxRectsPacker(atlasSize, atlasSize, 1); + const rects = textureSizes.map((textureSize, index) => { + const {x: width, y: height} = textureSize; + return { + width, + height, + data: { + index, + }, + }; + }); + maxRectsPacker.addArray(rects); + let oversized = maxRectsPacker.bins.length > 1; + maxRectsPacker.bins.forEach(bin => { + bin.rects.forEach(rect => { + if (rect.oversized) { + oversized = true; + } + }); + }); + if (!oversized) { + return maxRectsPacker; + } else { + return null; + } + }; + + const hasTextures = textureSizes.some(textureSize => textureSize.x > 0 || textureSize.y > 0); + if (hasTextures) { + let atlas; + let atlasSize = startAtlasSize; + while (!(atlas = _attemptPack(textureSizes, atlasSize))) { + atlasSize *= 2; + } + return atlas; + } else { + return null; + } + }; + const atlas = _packAtlases(); + + // draw atlas images + const originalTextures = new Map(); // map of canvas to the texture that generated it + const _drawAtlasImages = atlas => { + const _getTexturesKey = textures => textures.map(t => t ? t.uuid : '').join(','); + const _drawAtlasImage = textures => { + if (atlas && textures.some(t => t !== null)) { + const canvasSize = Math.min(atlas.width, textureSize); + const canvasScale = canvasSize / atlas.width; + + const canvas = document.createElement('canvas'); + canvas.width = canvasSize; + canvas.height = canvasSize; + const ctx = canvas.getContext('2d'); + + atlas.bins.forEach(bin => { + bin.rects.forEach(rect => { + const {x, y, width: w, height: h, data: {index}} = rect; + const texture = textures[index]; + if (texture) { + const image = texture.image; + + // draw the image in the correct box on the canvas + const tx = x * canvasScale; + const ty = y * canvasScale; + const tw = w * canvasScale; + const th = h * canvasScale; + ctx.drawImage(image, 0, 0, image.width, image.height, tx, ty, tw, th); + + if (!originalTextures.has(canvas)) { + originalTextures.set(canvas, texture); + } + } + }); + }); + + return canvas; + } else { + return null; + } + }; + + const atlasImages = {}; + const atlasImagesMap = new Map(); // cache to alias identical textures + for (const textureType of textureTypes) { + const textures = mergeable[`${textureType}s`]; + const key = _getTexturesKey(textures); + + let atlasImage = atlasImagesMap.get(key); + if (atlasImage === undefined) { // cache miss + atlasImage = _drawAtlasImage(textures); + if (atlasImage !== null) { + atlasImage.key = key; + } + atlasImagesMap.set(key, atlasImage); + } + atlasImages[textureType] = atlasImage; + } + return atlasImages; + }; + const atlasImages = _drawAtlasImages(atlas); + + /* // XXX debug + { + const debugWidth = 300; + let textureTypeIndex = 0; + for (const textureType of textureTypes) { + const atlasImage = atlasImages[textureType]; + if (atlasImage) { + atlasImage.style.cssText = `\ + position: fixed; + top: ${mergeableIndex * debugWidth}px; + left: ${textureTypeIndex * debugWidth}px; + min-width: ${debugWidth}px; + max-width: ${debugWidth}px; + min-height: ${debugWidth}px; + z-index: 100; + `; + atlasImage.setAttribute('type', textureType); + document.body.appendChild(atlasImage); + textureTypeIndex++; + } + } + } */ + + // build attribute layouts + const _makeAttributeLayoutsFromGeometries = geometries => { + const attributeLayouts = []; + for (const g of geometries) { + const attributes = g.attributes; + for (const attributeName in attributes) { + const attribute = attributes[attributeName]; + let layout = attributeLayouts.find(layout => layout.name === attributeName); + if (layout) { + // sanity check that item size is the same + if (layout.itemSize !== attribute.itemSize) { + throw new Error(`attribute ${attributeName} has different itemSize: ${layout.itemSize}, ${attribute.itemSize}`); + } + } else { + layout = new AttributeLayout( + attributeName, + attribute.array.constructor, + attribute.itemSize + ); + attributeLayouts.push(layout); + } + + layout.count += attribute.count * attribute.itemSize; + } + } + return attributeLayouts; + }; + const attributeLayouts = _makeAttributeLayoutsFromGeometries(geometries); + + const _makeMorphAttributeLayoutsFromGeometries = geometries => { + // create morph layouts + const morphAttributeLayouts = []; + for (const g of geometries) { + const morphAttributes = g.morphAttributes; + for (const morphAttributeName in morphAttributes) { + const morphAttribute = morphAttributes[morphAttributeName]; + let morphLayout = morphAttributeLayouts.find(l => l.name === morphAttributeName); + if (!morphLayout) { + morphLayout = new MorphAttributeLayout( + morphAttributeName, + morphAttribute[0].array.constructor, + morphAttribute[0].itemSize, + morphAttribute.length + ); + morphAttributeLayouts.push(morphLayout); + } + + morphLayout.count += morphAttribute[0].count * morphAttribute[0].itemSize; + } + } + return morphAttributeLayouts; + }; + const morphAttributeLayouts = _makeMorphAttributeLayoutsFromGeometries(geometries); + // console.log('got attribute layouts', attributeLayouts, morphAttributeLayouts); + + const _forceGeometriesAttributeLayouts = (attributeLayouts, geometries) => { + for (const layout of attributeLayouts) { + for (const g of geometries) { + let gAttribute = g.attributes[layout.name]; + if (!gAttribute) { + if (layout.name === 'skinIndex' || layout.name === 'skinWeight') { + gAttribute = layout.makeDefault(g); + g.setAttribute(layout.name, gAttribute); + + layout.count += gAttribute.count * gAttribute.itemSize; + } else { + throw new Error(`unknown layout ${layout.name}`); + } + } + } + } + + for (const morphLayout of morphAttributeLayouts) { + for (const g of geometries) { + let morphAttribute = g.morphAttributes[morphLayout.name]; + if (!morphAttribute) { + // console.log('missing morph attribute', morphLayout, morphAttribute); + + morphAttribute = morphLayout.makeDefault(g); + g.morphAttributes[morphLayout.name] = morphAttribute; + + morphLayout.count += morphAttribute[0].count * morphAttribute[0].itemSize; + + /* if (layout.name === 'skinIndex' || layout.name === 'skinWeight') { + gAttribute = new THREE.BufferAttribute(new Float32Array(g.attributes.position.count * layout.itemSize), layout.itemSize); + g.setAttribute(layout.name, gAttribute); + } else { + throw new Error(`unknown layout ${layout.name}`); + } */ + } + } + } + }; + const _mergeAttributes = (geometry, geometries, attributeLayouts) => { + for (const layout of attributeLayouts) { + const attributeData = new layout.TypedArrayConstructor(layout.count); + const attribute = new THREE.BufferAttribute(attributeData, layout.itemSize); + let attributeDataIndex = 0; + for (const g of geometries) { + const gAttribute = g.attributes[layout.name]; + attributeData.set(gAttribute.array, attributeDataIndex); + attributeDataIndex += gAttribute.count * gAttribute.itemSize; + } + // sanity check + if (attributeDataIndex !== layout.count) { + console.warn('desynced attribute data 1', layout.name, attributeDataIndex, layout.count); + debugger; + } + geometry.setAttribute(layout.name, attribute); + } + }; + const _mergeMorphAttributes = (geometry, geometries, morphAttributeLayouts) => { + for (const morphLayout of morphAttributeLayouts) { + const morphsArray = Array(morphLayout.arraySize); + for (let i = 0; i < morphLayout.arraySize; i++) { + const morphData = new morphLayout.TypedArrayConstructor(morphLayout.count); + const morphAttribute = new THREE.BufferAttribute(morphData, morphLayout.itemSize); + morphsArray[i] = morphAttribute; + let morphDataIndex = 0; + for (const g of geometries) { + let gMorphAttribute = g.morphAttributes[morphLayout.name]; + gMorphAttribute = gMorphAttribute?.[i]; + if (gMorphAttribute) { + morphData.set(gMorphAttribute.array, morphDataIndex); + morphDataIndex += gMorphAttribute.count * gMorphAttribute.itemSize; + } else { + const matchingAttribute = g.attributes[morphLayout.name]; + morphDataIndex += matchingAttribute.count * matchingAttribute.itemSize; + } + } + // sanity check + if (morphDataIndex !== morphLayout.count) { + console.warn('desynced morph data 2', morphLayout.name, morphDataIndex, morphLayout.count); + } + } + geometry.morphAttributes[morphLayout.name] = morphsArray; + } + }; + const _mergeIndices = (geometry, geometries) => { + let indexCount = 0; + for (const g of geometries) { + indexCount += g.index.count; + } + const indexData = new Uint32Array(indexCount); + + let positionOffset = 0; + let indexOffset = 0; + for (const g of geometries) { + const srcIndexData = g.index.array; + for (let i = 0; i < srcIndexData.length; i++) { + indexData[indexOffset++] = srcIndexData[i] + positionOffset; + } + positionOffset += g.attributes.position.count; + } + geometry.setIndex(new THREE.BufferAttribute(indexData, 1)); + }; + const _remapGeometryUvs = (geometry, geometries) => { + if (atlas) { + let uvIndex = 0; + const geometryUvOffsets = geometries.map(g => { + const start = uvIndex; + const count = g.attributes.uv.count; + uvIndex += count; + return { + start, + count, + }; + }); + + const canvasSize = Math.min(atlas.width, textureSize); + const canvasScale = canvasSize / atlas.width; + atlas.bins.forEach(bin => { + bin.rects.forEach(rect => { + const {x, y, width: w, height: h, data: {index}} = rect; + + if (w > 0 && h > 0) { + const {start, count} = geometryUvOffsets[index]; + + const tx = x * canvasScale; + const ty = y * canvasScale; + const tw = w * canvasScale; + const th = h * canvasScale; + + for (let i = 0; i < count; i++) { + const uvIndex = start + i; + + localVector2D.fromArray(geometry.attributes.uv.array, uvIndex * 2); + modUv(localVector2D); + localVector2D + .multiply( + localVector2D2.set(tw/canvasSize, th/canvasSize) + ) + .add( + localVector2D2.set(tx/canvasSize, ty/canvasSize) + ); + localVector2D.toArray(geometry.attributes.uv.array, uvIndex * 2); + } + } + }); + }); + } + }; + const _mergeGeometries = geometries => { + const geometry = new THREE.BufferGeometry(); + geometry.morphTargetsRelative = true; + + _forceGeometriesAttributeLayouts(attributeLayouts, geometries); + _mergeAttributes(geometry, geometries, attributeLayouts); + _mergeMorphAttributes(geometry, geometries, morphAttributeLayouts); + _mergeIndices(geometry, geometries); + _remapGeometryUvs(geometry, geometries); + + return geometry; + }; + const geometry = _mergeGeometries(geometries); + // console.log('got geometry', geometry); + + const _makeAtlasTextures = atlasImages => { + const _makeAtlasTexture = atlasImage => { + const originalTexture = originalTextures.get(atlasImage); + + const t = new THREE.Texture(atlasImage); + t.minFilter = originalTexture.minFilter; + t.magFilter = originalTexture.magFilter; + t.wrapS = originalTexture.wrapS; + t.wrapT = originalTexture.wrapT; + t.mapping = originalTexture.mapping; + // t.encoding = originalTexture.encoding; + + t.flipY = false; + t.needsUpdate = true; + + return t; + }; + + const result = {}; + const textureMap = new Map(); // cache to alias identical textures + for (const textureType of textureTypes) { + const atlasImage = atlasImages[textureType]; + + if (atlasImage) { + let atlasTexture = textureMap.get(atlasImage.key); + if (atlasTexture === undefined) { // cache miss + atlasTexture = _makeAtlasTexture(atlasImage); + textureMap.set(atlasImage.key, atlasTexture); + } + result[textureType] = atlasTexture; + } else { + result[textureType] = null; + } + } + return result; + }; + const atlasTextures = atlasImages ? _makeAtlasTextures(atlasImages) : null; + + return { + atlas, + atlasImages, + attributeLayouts, + morphAttributeLayouts, + geometry, + atlasTextures, + }; +}; \ No newline at end of file From bf0844b931416c4ca0fad9f30c1ed87df8ff0f57 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Fri, 22 Apr 2022 19:23:42 -0400 Subject: [PATCH 41/57] Dead code cleanup --- avatar-cruncher.js | 49 +++++----------------------------------------- 1 file changed, 5 insertions(+), 44 deletions(-) diff --git a/avatar-cruncher.js b/avatar-cruncher.js index b4de98ba73..22ad32efa6 100644 --- a/avatar-cruncher.js +++ b/avatar-cruncher.js @@ -1,35 +1,10 @@ import * as THREE from 'three'; -import {MaxRectsPacker} from 'maxrects-packer'; -import {modUv} from './util.js'; -import {getMergeableObjects, mergeGeometryTextureAtlas} from './avatar-optimizer.js'; +import {getMergeableObjects, mergeGeometryTextureAtlas} from './geometry-texture-atlas.js'; const defaultTextureSize = 4096; -const startAtlasSize = 512; - -const localVector2D = new THREE.Vector2(); -const localVector2D2 = new THREE.Vector2(); -// const localVector4D = new THREE.Vector4(); -// const localVector4D2 = new THREE.Vector4(); - -class AttributeLayout { - constructor(name, TypedArrayConstructor, itemSize) { - this.name = name; - this.TypedArrayConstructor = TypedArrayConstructor; - this.itemSize = itemSize; - - this.count = 0; - } -} -class MorphAttributeLayout extends AttributeLayout { - constructor(name, TypedArrayConstructor, itemSize, arraySize) { - super(name, TypedArrayConstructor, itemSize); - this.arraySize = arraySize; - } -} export const crunchAvatarModel = (model, options = {}) => { - // const atlasTexturesEnabled = !!(options.textures ?? true); const textureSize = options.textureSize ?? defaultTextureSize; const textureTypes = [ @@ -54,27 +29,13 @@ export const crunchAvatarModel = (model, options = {}) => { geometry, atlasTextures, } = mergeGeometryTextureAtlas(mergeable, textureSize); - /* console.log('got atlas', { - atlas, - atlasImages, - attributeLayouts, - morphAttributeLayouts, - geometry, - atlasTextures, - }); */ // create material - // const material = new THREE.MeshStandardMaterial(); const material = new THREE.MeshBasicMaterial(); - // if (atlasTextures) { - for (const k of textureTypes) { - /* const t = new THREE.Texture(textureAtlases[k].image); - t.flipY = false; - t.needsUpdate = true; */ - const t = atlasTextures[k]; - material[k] = t; - } - // } + for (const k of textureTypes) { + const t = atlasTextures[k]; + material[k] = t; + } material.roughness = 1; material.alphaTest = 0.1; material.transparent = true; From b76091bfd8fed7df70ed39a5a5144650b308f32c Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Fri, 22 Apr 2022 20:02:14 -0400 Subject: [PATCH 42/57] Avatar spriter export cleanup --- avatar-spriter.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/avatar-spriter.js b/avatar-spriter.js index ad92bbf2b5..a3402c7526 100644 --- a/avatar-spriter.js +++ b/avatar-spriter.js @@ -1700,12 +1700,8 @@ const _renderSpriteImages = skinnedVrm => { return spriteImages; }; -function createSpriteMegaMesh(skinnedVrm) { +export const createSpriteMegaMesh = skinnedVrm => { const spriteImages = _renderSpriteImages(skinnedVrm); const spriteMegaAvatarMesh = new SpriteMegaAvatarMesh(spriteImages); return spriteMegaAvatarMesh; -} - -export { - createSpriteMegaMesh -}; +}; \ No newline at end of file From d0e87a8b2e954ee82b77618812170d73ef8e9729 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Fri, 22 Apr 2022 20:03:04 -0400 Subject: [PATCH 43/57] Avatar spriter imports cleanup --- avatar-spriter.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/avatar-spriter.js b/avatar-spriter.js index a3402c7526..565a1fd323 100644 --- a/avatar-spriter.js +++ b/avatar-spriter.js @@ -6,6 +6,7 @@ const {useApp, useFrame, useLocalPlayer, usePhysics, useGeometries, useMaterials import {DoubleSidedPlaneGeometry, CameraGeometry} from './geometries.js'; import {WebaverseShaderMaterial} from './materials.js'; import Avatar from './avatars/avatars.js'; +import {mod, angleDifference} from './util.js'; const preview = false; // whether to draw debug meshes @@ -682,14 +683,6 @@ class SpriteMegaAvatarMesh extends THREE.Mesh { } } -function mod(a, n) { - return ((a % n) + n) % n; -} -function angleDifference(angle1, angle2) { - let a = angle2 - angle1; - a = mod(a + Math.PI, Math.PI*2) - Math.PI; - return a; -} const animationAngles = [ {name: 'left', angle: Math.PI/2}, {name: 'right', angle: -Math.PI/2}, From 1f1ccbcf792ab29b4580b7acf8fa48674f6473a4 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Fri, 22 Apr 2022 20:06:11 -0400 Subject: [PATCH 44/57] Avatar spriter rendering cleanup --- avatar-spriter.js | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/avatar-spriter.js b/avatar-spriter.js index 565a1fd323..c2484b57e5 100644 --- a/avatar-spriter.js +++ b/avatar-spriter.js @@ -1,7 +1,7 @@ import * as THREE from 'three'; // import easing from './easing.js'; import metaversefile from 'metaversefile'; -const {useApp, useFrame, useLocalPlayer, usePhysics, useGeometries, useMaterials, useAvatarAnimations, useCleanup} = metaversefile; +// const {useApp, useFrame, useLocalPlayer, usePhysics, useGeometries, useMaterials, useAvatarAnimations, useCleanup} = metaversefile; // import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'; import {DoubleSidedPlaneGeometry, CameraGeometry} from './geometries.js'; import {WebaverseShaderMaterial} from './materials.js'; @@ -463,7 +463,9 @@ class SpriteMegaAvatarMesh extends THREE.Mesh { this.texs = texs; } setTexture(name) { - const tex = this.texs.find(t => t.name === name); + const spriteSpecs = getSpriteSpecs(); + const spriteSpecIndex = spriteSpecs.findIndex(spriteSpec => spriteSpec.name === name); + const tex = this.texs[spriteSpecIndex]; if (tex) { this.material.uniforms.uTex.value = tex; this.material.uniforms.uTex.needsUpdate = true; @@ -1500,7 +1502,7 @@ class AvatarSpriteDepthMaterial extends THREE.MeshNormalMaterial { } } -const _renderSpriteImages = skinnedVrm => { +export const renderSpriteImages = skinnedVrm => { const localRig = new Avatar(skinnedVrm, { fingers: true, hair: true, @@ -1576,12 +1578,13 @@ const _renderSpriteImages = skinnedVrm => { const canvas = document.createElement('canvas'); canvas.width = size; canvas.height = size; - // canvas.style.cssText = `position: fixed; top: ${canvasIndex2*1024}px; left: 0; width: 1024px; height: 1024px; z-index: 10;`; const ctx = canvas.getContext('2d'); - const tex = new THREE.Texture(canvas); - tex.name = name; - // tex.minFilter = THREE.NearestFilter; - // tex.magFilter = THREE.NearestFilter; + + let tex; + if (preview) { + tex = new THREE.Texture(canvas); + tex.name = name; + } let canvasIndex = 0; // console.log('generate sprite', name); @@ -1651,7 +1654,9 @@ const _renderSpriteImages = skinnedVrm => { 0, renderer.domElement.height - texSize, texSize, texSize, x * texSize, y * texSize, texSize, texSize ); - tex.needsUpdate = true; + if (preview) { + tex.needsUpdate = true; + } // await _timeout(50); } @@ -1687,14 +1692,18 @@ const _renderSpriteImages = skinnedVrm => { canvasIndex2++; - spriteImages.push(tex); + spriteImages.push(canvas); } - // console.timeEnd('render'); return spriteImages; }; export const createSpriteMegaMesh = skinnedVrm => { - const spriteImages = _renderSpriteImages(skinnedVrm); - const spriteMegaAvatarMesh = new SpriteMegaAvatarMesh(spriteImages); + const spriteImages = renderSpriteImages(skinnedVrm); + const spriteTextures = spriteImages.map(img => { + const t = new THREE.Texture(img); + t.needsUpdate = true; + return t; + }); + const spriteMegaAvatarMesh = new SpriteMegaAvatarMesh(spriteTextures); return spriteMegaAvatarMesh; }; \ No newline at end of file From 6c99d92a366ce9564a33fbac094109988b65982f Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Fri, 22 Apr 2022 20:20:51 -0400 Subject: [PATCH 45/57] Avatar spriter reset methods cleanup --- avatar-spriter.js | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/avatar-spriter.js b/avatar-spriter.js index c2484b57e5..ef0a1f826b 100644 --- a/avatar-spriter.js +++ b/avatar-spriter.js @@ -777,7 +777,6 @@ const getSpriteSpecs = () => { init({angle, avatar: localRig}) { let positionOffset = 0; return { - reset() {}, update(timestamp, timeDiffMs) { // positionOffset -= walkSpeed/1000 * timeDiffMs; @@ -801,7 +800,6 @@ const getSpriteSpecs = () => { init({angle, avatar: localRig}) { let positionOffset = 0; return { - reset() {}, update(timestamp, timeDiffMs) { positionOffset -= walkSpeed/1000 * timeDiffMs; @@ -825,7 +823,6 @@ const getSpriteSpecs = () => { init({angle, avatar: localRig}) { let positionOffset = 0; return { - reset() {}, update(timestamp, timeDiffMs) { positionOffset -= walkSpeed/1000 * timeDiffMs; @@ -849,7 +846,6 @@ const getSpriteSpecs = () => { init({angle, avatar: localRig}) { let positionOffset = 0; return { - reset() {}, update(timestamp, timeDiffMs) { positionOffset += walkSpeed/1000 * timeDiffMs; @@ -873,7 +869,6 @@ const getSpriteSpecs = () => { init({angle, avatar: localRig}) { let positionOffset = 0; return { - reset() {}, update(timestamp, timeDiffMs) { positionOffset += walkSpeed/1000 * timeDiffMs; @@ -897,7 +892,6 @@ const getSpriteSpecs = () => { init({angle, avatar: localRig}) { let positionOffset = 0; return { - reset() {}, update(timestamp, timeDiffMs) { positionOffset -= runSpeed/1000 * timeDiffMs; @@ -921,7 +915,6 @@ const getSpriteSpecs = () => { init({angle, avatar: localRig}) { let positionOffset = 0; return { - reset() {}, update(timestamp, timeDiffMs) { positionOffset -= runSpeed/1000 * timeDiffMs; @@ -945,7 +938,6 @@ const getSpriteSpecs = () => { init({angle, avatar: localRig}) { let positionOffset = 0; return { - reset() {}, update(timestamp, timeDiffMs) { positionOffset += runSpeed/1000 * timeDiffMs; @@ -969,7 +961,6 @@ const getSpriteSpecs = () => { init({angle, avatar: localRig}) { let positionOffset = 0; return { - reset() {}, update(timestamp, timeDiffMs) { positionOffset += runSpeed/1000 * timeDiffMs; @@ -1009,7 +1000,6 @@ const getSpriteSpecs = () => { localRig.update(timestamp, timeDiffMs); }, - reset() {}, cleanup() { localRig.crouchTime = maxCrouchTime; }, @@ -1038,7 +1028,6 @@ const getSpriteSpecs = () => { localRig.update(timestamp, timeDiffMs); }, - reset() {}, cleanup() { localRig.crouchTime = maxCrouchTime; }, @@ -1067,7 +1056,6 @@ const getSpriteSpecs = () => { localRig.update(timestamp, timeDiffMs); }, - reset() {}, cleanup() { localRig.crouchTime = maxCrouchTime; }, @@ -1096,7 +1084,6 @@ const getSpriteSpecs = () => { localRig.update(timestamp, timeDiffMs); }, - reset() {}, cleanup() { localRig.crouchTime = maxCrouchTime; }, @@ -1125,7 +1112,6 @@ const getSpriteSpecs = () => { localRig.update(timestamp, timeDiffMs); }, - reset() {}, cleanup() { localRig.crouchTime = maxCrouchTime; }, @@ -1175,8 +1161,7 @@ const getSpriteSpecs = () => { init({angle, avatar: localRig}) { let positionOffset = 0; - const defaultJumpTime = 0; - let jumpTime = defaultJumpTime; + let jumpTime = 0; // const jumpIncrementSpeed = 400; return { @@ -1204,7 +1189,7 @@ const getSpriteSpecs = () => { localRig.update(timestamp, timeDiffMs); }, reset() { - jumpTime = defaultJumpTime; + jumpTime = 0; }, cleanup() { localRig.jumpState = false; @@ -1612,7 +1597,6 @@ export const renderSpriteImages = skinnedVrm => { // pre-run the animation one cycle first, to stabilize the hair physics let now = 0; const startAngleIndex = angleIndex; - // localRig.springBoneManager.reset(); { const startNow = now; for (let j = 0; j < numFrames; j++) { @@ -1624,7 +1608,7 @@ export const renderSpriteImages = skinnedVrm => { } const initialPositionOffset = localRig.inputs.hmd.position.z; - spriteGenerator.reset(); + spriteGenerator.reset && spriteGenerator.reset(); // now perform the real capture const startNow = now; From 7f5999f6c7ef93eac003bce92563d4552dba6971 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Fri, 22 Apr 2022 20:33:12 -0400 Subject: [PATCH 46/57] Rename classes in avatar spriter --- avatar-spriter.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/avatar-spriter.js b/avatar-spriter.js index ef0a1f826b..7cc4b54f35 100644 --- a/avatar-spriter.js +++ b/avatar-spriter.js @@ -54,7 +54,7 @@ const alphaTest = 0.9; const planeSpriteMeshes = []; const spriteAvatarMeshes = []; -class SpritePlaneMesh extends THREE.Mesh { +class SpriteAnimationPlaneMesh extends THREE.Mesh { constructor(tex, {angleIndex}) { const planeSpriteMaterial = new WebaverseShaderMaterial({ uniforms: { @@ -189,7 +189,7 @@ class SpritePlaneMesh extends THREE.Mesh { return this; } } -class SpriteAvatarMesh extends THREE.Mesh { +class SpriteAnimation360Mesh extends THREE.Mesh { constructor(tex) { const avatarSpriteMaterial = new WebaverseShaderMaterial({ uniforms: { @@ -321,13 +321,12 @@ class SpriteAvatarMesh extends THREE.Mesh { this.customPostMaterial = new AvatarSpriteDepthMaterial(undefined, { tex, }); - // return spriteAvatarMesh; this.lastSpriteSpecName = ''; this.lastSpriteSpecTimestamp = 0; } } -class SpriteMegaAvatarMesh extends THREE.Mesh { +class SpriteAvatarMesh extends THREE.Mesh { constructor(texs) { const tex = texs[0]; const avatarMegaSpriteMaterial = new WebaverseShaderMaterial({ @@ -1646,7 +1645,7 @@ export const renderSpriteImages = skinnedVrm => { } if (preview) { - const planeSpriteMesh = new SpritePlaneMesh(tex, { + const planeSpriteMesh = new SpriteAnimationPlaneMesh(tex, { angleIndex: startAngleIndex, }); planeSpriteMesh.position.set(-canvasIndex*worldSize, 2, -canvasIndex2*worldSize); @@ -1662,7 +1661,7 @@ export const renderSpriteImages = skinnedVrm => { } if (preview) { - const spriteAvatarMesh = new SpriteAvatarMesh(tex); + const spriteAvatarMesh = new SpriteAnimation360Mesh(tex); spriteAvatarMesh.position.set( -canvasIndex*worldSize, 0, @@ -1681,13 +1680,13 @@ export const renderSpriteImages = skinnedVrm => { return spriteImages; }; -export const createSpriteMegaMesh = skinnedVrm => { +export const createSpriteAvatarMesh = skinnedVrm => { const spriteImages = renderSpriteImages(skinnedVrm); const spriteTextures = spriteImages.map(img => { const t = new THREE.Texture(img); t.needsUpdate = true; return t; }); - const spriteMegaAvatarMesh = new SpriteMegaAvatarMesh(spriteTextures); - return spriteMegaAvatarMesh; + const spriteAvatarMesh = new SpriteAvatarMesh(spriteTextures); + return spriteAvatarMesh; }; \ No newline at end of file From 9cc9fdcaa798acdcfc9294c119f9812b558250c1 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Fri, 22 Apr 2022 20:33:26 -0400 Subject: [PATCH 47/57] Avatars dead import cleanup --- avatars/avatars.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avatars/avatars.js b/avatars/avatars.js index da6193a8ac..58c4d073e0 100644 --- a/avatars/avatars.js +++ b/avatars/avatars.js @@ -16,7 +16,7 @@ import { import { crouchMaxTime, // useMaxTime, - aimMaxTime, + // aimMaxTime, // avatarInterpolationFrameRate, // avatarInterpolationTimeDelay, // avatarInterpolationNumFrames, From 36576929b89e926231645e70568cc5e7cc52b040 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Fri, 22 Apr 2022 20:34:37 -0400 Subject: [PATCH 48/57] Avatars hook in more async subavatars rendering --- avatars/avatars.js | 100 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 77 insertions(+), 23 deletions(-) diff --git a/avatars/avatars.js b/avatars/avatars.js index 58c4d073e0..846b4e4bcb 100644 --- a/avatars/avatars.js +++ b/avatars/avatars.js @@ -438,6 +438,7 @@ class Avatar { this.model = model; this.spriteMegaAvatarMesh = null; this.crunchedModel = null; + this.optimizedModel = null; this.options = options; this.vrmExtension = object?.parser?.json?.extensions?.VRM; @@ -868,6 +869,8 @@ class Avatar { this.microphoneWorker = null; this.volume = -1; + this.quality = 4; + this.shoulderTransforms.Start(); this.legsManager.Start(); @@ -1365,44 +1368,85 @@ class Avatar { return localEuler.y; } async setQuality(quality) { + this.quality = quality; + switch (this.quality) { + case 1: { + if (!this.spriteAvatarMesh) { + const skinnedMesh = await this.object.cloneVrm(); + this.spriteAvatarMesh = avatarSpriter.createSpriteAvatarMesh(skinnedMesh); + this.spriteAvatarMesh.visible = false; + this.spriteAvatarMesh.enabled = true; // XXX + scene.add(this.spriteAvatarMesh); + } + break; + } + case 2: { + if (!this.crunchedModel) { + this.crunchedModel = avatarCruncher.crunchAvatarModel(this.model); + this.crunchedModel.visible = false; + this.crunchedModel.enabled = true; // XXX + scene.add(this.crunchedModel); + } + break; + } + case 3: { + if (!this.optimizedModel) { + this.optimizedModel = avatarOptimizer.optimizeAvatarModel(this.model); + this.optimizedModel.visible = false; + this.optimizedModel.enabled = true; // XXX + scene.add(this.optimizedModel); + } + break; + } + case 3: + case 4: { + break; + } + default: { + throw new Error('unknown avatar quality: ' + this.quality); + } + } + + this.#updateVisibility(); + } + #updateVisibility() { this.model.visible = false; - if (this.crunchedModel) this.crunchedModel.visible = false; - if (this.spriteMegaAvatarMesh) this.spriteMegaAvatarMesh.visible = false; + if (this.spriteAvatarMesh) { + this.spriteAvatarMesh.visible = false; + } + if (this.crunchedModel) { + this.crunchedModel.visible = false; + } + if (this.optimizedModel) { + this.optimizedModel.visible = false; + } - switch (quality) { + switch (this.quality) { case 1: { - const skinnedMesh = await this.object.cloneVrm(); - this.spriteMegaAvatarMesh = this.spriteMegaAvatarMesh ?? avatarSpriter.createSpriteMegaMesh(skinnedMesh); - scene.add(this.spriteMegaAvatarMesh); - this.spriteMegaAvatarMesh.visible = true; + if (this.spriteAvatarMesh && this.spriteAvatarMesh.enabled) { + this.spriteAvatarMesh.visible = true; + } break; } case 2: { - this.crunchedModel = this.crunchedModel ?? avatarCruncher.crunchAvatarModel(this.model); - this.crunchedModel.frustumCulled = false; - scene.add(this.crunchedModel); - this.crunchedModel.visible = true; + if (this.crunchedModel && this.crunchedModel.enabled) { + this.crunchedModel.visible = true; + } break; } case 3: { - this.optimizedModel = avatarOptimizer.optimizeAvatarModel(this.model); - this.optimizedModel.traverse(o => { - if (o.isMesh) { - o.frustumCulled = false; - } - }); - scene.add(this.optimizedModel); - this.optimizedModel.visible = true; + if (this.optimizedModel && this.optimizedModel.enabled) { + this.optimizedModel.visible = true; + } break; } case 4: { - console.log('not implemented'); // XXX this.model.visible = true; break; } default: { - throw new Error('unknown avatar quality: ' + quality); + throw new Error('unknown avatar quality: ' + this.quality); } } } @@ -1755,8 +1799,8 @@ class Avatar { const _updateSubAvatars = () => { - if (this.spriteMegaAvatarMesh) { - this.spriteMegaAvatarMesh.update(timestamp, timeDiff, { + if (this.spriteAvatarMesh) { + this.spriteAvatarMesh.update(timestamp, timeDiff, { playerAvatar: this, camera, }); @@ -2001,6 +2045,16 @@ class Avatar { } */ destroy() { + if (this.spriteAvatarMesh) { + scene.remove(this.spriteAvatarMesh); + } + if (this.crunchedModel) { + scene.remove(this.crunchedModel); + } + if (this.optimizedModel) { + scene.remove(this.optimizedModel); + } + this.setAudioEnabled(false); } } From 6bfceb7e70a824117a7b69c1037e83a80ee73233 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Fri, 22 Apr 2022 20:34:44 -0400 Subject: [PATCH 49/57] Add util.js modUv method --- util.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/util.js b/util.js index 41ba7bb02a..58e33b3353 100644 --- a/util.js +++ b/util.js @@ -632,6 +632,11 @@ export const handleDiscordLogin = async (code, id) => { export function mod(a, n) { return (a % n + n) % n; } +export const modUv = uv => { + uv.x = mod(uv.x, 1); + uv.y = mod(uv.y, 1); + return uv; +}; export function angleDifference(angle1, angle2) { let a = angle2 - angle1; From d2e68465db3103929e377a32c0ad1fb1fd1b656f Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Thu, 28 Jul 2022 03:30:54 -0400 Subject: [PATCH 50/57] Add new exporters.js --- exporters.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 exporters.js diff --git a/exporters.js b/exporters.js new file mode 100644 index 0000000000..ebb364ff10 --- /dev/null +++ b/exporters.js @@ -0,0 +1,14 @@ +import {GLTFExporter} from 'three/examples/jsm/exporters/GLTFExporter.js'; +import {memoize} from './util.js'; + +const _gltfExporter = memoize(() => { + const gltfExporter = new GLTFExporter(); + return gltfExporter; +}); + +const exporters = { + get gltfExporter() { + return _gltfExporter(); + }, +}; +export default exporters; \ No newline at end of file From 5ed08d4c0f1620edeff6ead692c26833edff0ed9 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Thu, 28 Jul 2022 03:31:40 -0400 Subject: [PATCH 51/57] Add new avatar renderer --- avatars/avatar-renderer.js | 400 +++++++++++++++++++++++++++++++++++++ 1 file changed, 400 insertions(+) create mode 100644 avatars/avatar-renderer.js diff --git a/avatars/avatar-renderer.js b/avatars/avatar-renderer.js new file mode 100644 index 0000000000..6837bb18dc --- /dev/null +++ b/avatars/avatar-renderer.js @@ -0,0 +1,400 @@ +/* this file implements avatar optimization and THREE.js Object management + rendering */ +import * as THREE from 'three'; +import * as avatarOptimizer from '../avatar-optimizer.js'; +import * as avatarCruncher from '../avatar-cruncher.js'; +import * as avatarSpriter from '../avatar-spriter.js'; +import offscreenEngineManager from '../offscreen-engine-manager.js'; +import loaders from '../loaders.js'; +import { + defaultAvatarQuality, +} from '../constants.js'; + +const avatarPlaceholderImagePromise = (async () => { + const avatarPlaceholderImage = new Image(); + avatarPlaceholderImage.src = '/avatars/images/user.png'; + await new Promise((accept, reject) => { + avatarPlaceholderImage.onload = accept; + avatarPlaceholderImage.onerror = reject; + }); + return avatarPlaceholderImage; +})(); +const waitForAvatarPlaceholderImage = () => avatarPlaceholderImagePromise; +const avatarPlaceholderTexture = new THREE.Texture(); +(async() => { + const avatarPlaceholderImage = await waitForAvatarPlaceholderImage(); + avatarPlaceholderTexture.image = avatarPlaceholderImage; + avatarPlaceholderTexture.needsUpdate = true; +})(); +const geometry = new THREE.PlaneBufferGeometry(0.1, 0.1); +const material = new THREE.MeshBasicMaterial({ + map: avatarPlaceholderTexture, +}); +const _makeAvatarPlaceholderMesh = () => { + const mesh = new THREE.Mesh(geometry, material); + return mesh; +}; +const _bindSkeleton = (dstModel, srcObject) => { + console.log('bind skeleton', dstModel, srcObject); + const srcModel = srcObject.scene; + + const _findBoneInSrc = (srcBoneName) => { + let result = null; + const _recurse = o => { + if (o.isBone && o.name === srcBoneName) { + result = o; + return false; + } + for (const child of o.children) { + if (_recurse(child) === false) { + return false; + } + } + return true; + }; + _recurse(srcModel); + return result; + }; + const _findSrcSkeletonFromBoneName = (boneName) => { + let skeleton = null; + + const bone = _findBoneInSrc(boneName); + if (bone !== null) { + const _recurse = o => { + if (o.isSkinnedMesh) { + if (o.skeleton.bones.includes(bone)) { + skeleton = o.skeleton; + return false; + } + } + for (const child of o.children) { + if (_recurse(child) === false) { + return false; + } + } + return true; + }; + _recurse(srcModel); + } + + return skeleton; + }; + const _findSrcSkeletonFromDstSkeleton = skeleton => { + return _findSrcSkeletonFromBoneName(skeleton.bones[0].name); + }; + const _findSkinnedMeshInSrc = () => { + let result = null; + const _recurse = o => { + if (o.isSkinnedMesh) { + result = o; + return false; + } + for (const child of o.children) { + if (_recurse(child) === false) { + return false; + } + } + return true; + }; + _recurse(srcModel); + return result; + }; + dstModel.traverse(o => { + if (o.isSkinnedMesh) { + // bind the skeleton + const {skeleton: dstSkeleton} = o; + const srcSkeleton = _findSrcSkeletonFromDstSkeleton(dstSkeleton); + o.skeleton = srcSkeleton; + } + if (o.isMesh) { + // also bind the blend shapes + // skinnedMesh.skeleton = skeletons[0]; + const skinnedMesh = _findSkinnedMeshInSrc(); + // console.log('map blend shapes', o, skinnedMesh); + o.morphTargetDictionary = skinnedMesh.morphTargetDictionary; + o.morphTargetInfluences = skinnedMesh.morphTargetInfluences; + // o.geometry.morphAttributes = skinnedMesh.geometry.morphAttributes; + // o.morphAttributes = skinnedMesh.morphAttributes; + // o.morphAttributesRelative = skinnedMesh.morphAttributesRelative; + + o.geometry.morphAttributes.position.forEach(attr => { + attr.onUploadCallback = () => { + console.log('upload callback'); + }; + + for (let i = 0; i < attr.array.length; i++) { + // if ((attr.array[i]) != 0) { + attr.array[i] *= 10; + // attr.array[i] = Math.random(); + // } + } + }); + + // o.onBeforeRender = () => {debugger;} + o.material.onBeforeCompile = (shader) => { + console.log('compile avatar shader', shader); + }; + // window.o = o; + + const _frame = () => { + window.requestAnimationFrame(_frame); + + // console.log('got frame', o.morphTargetInfluences.join(',')); + }; + _frame(); + } + }); +}; + +export class AvatarRenderer { + constructor(object, { + quality = defaultAvatarQuality, + } = {}) { + this.object = object; + this.quality = quality; + + // + + this.scene = new THREE.Object3D(); + this.placeholderMesh = _makeAvatarPlaceholderMesh(); + + // + + this.createSpriteAvatarMesh = offscreenEngineManager.createFunction([ + `\ + import * as THREE from 'three'; + import * as avatarSpriter from './avatar-spriter.js'; + import loaders from './loaders.js'; + + `, + async function(arrayBuffer, srcUrl) { + if (!arrayBuffer) { + debugger; + } + + const parseVrm = (arrayBuffer, srcUrl) => new Promise((accept, reject) => { + const { gltfLoader } = loaders; + gltfLoader.parse(arrayBuffer, srcUrl, object => { + accept(object.scene); + }, reject); + }); + + const skinnedMesh = await parseVrm(arrayBuffer, srcUrl); + if (!skinnedMesh) { + debugger; + } + const textureImages = avatarSpriter.renderSpriteImages(skinnedMesh); + return textureImages; + } + ]); + this.crunchAvatarModel = offscreenEngineManager.createFunction([ + `\ + import * as THREE from 'three'; + import * as avatarCruncher from './avatar-cruncher.js'; + import loaders from './loaders.js'; + + `, + async function(arrayBuffer, srcUrl) { + if (!arrayBuffer) { + debugger; + } + + const parseVrm = (arrayBuffer, srcUrl) => new Promise((accept, reject) => { + const { gltfLoader } = loaders; + gltfLoader.parse(arrayBuffer, srcUrl, object => { + accept(object.scene); + }, reject); + }); + + const model = await parseVrm(arrayBuffer, srcUrl); + if (!model) { + debugger; + } + const glbData = await avatarCruncher.crunchAvatarModel(model); + return glbData; + } + ]); + this.optimizeAvatarModel = offscreenEngineManager.createFunction([ + `\ + import * as THREE from 'three'; + import * as avatarOptimizer from './avatar-optimizer.js'; + import loaders from './loaders.js'; + import exporters from './exporters.js'; + + `, + async function(arrayBuffer, srcUrl) { + if (!arrayBuffer) { + debugger; + } + + const parseVrm = (arrayBuffer, srcUrl) => new Promise((accept, reject) => { + const { gltfLoader } = loaders; + gltfLoader.parse(arrayBuffer, srcUrl, object => { + accept(object); + }, reject); + }); + + const object = await parseVrm(arrayBuffer, srcUrl); + + const model = object.scene; + const glbData = await avatarOptimizer.optimizeAvatarModel(model); + + /* const glbData = await new Promise((accept, reject) => { + const {gltfExporter} = exporters; + gltfExporter.parse( + object.scene, + function onCompleted(arrayBuffer) { + accept(arrayBuffer); + }, function onError(error) { + reject(error); + }, + { + binary: true, + includeCustomExtensions: true, + }, + ); + }); */ + + // const parsedObject = await parseVrm(glbData, srcUrl); + // console.log('compare skeletons', object, parsedObject); + + return glbData; + } + ]); + + this.setQuality(quality); + } + async setQuality(quality) { + this.quality = quality; + + // XXX destroy the old avatars? + this.scene.clear(); + this.scene.add(this.placeholderMesh); + + // console.log('set quality', quality, new Error().stack); + + switch (this.quality) { + case 1: { + if (!this.spriteAvatarMesh) { + if (!this.object.arrayBuffer) { + debugger; + } + const textureImages = await this.createSpriteAvatarMesh([this.object.arrayBuffer, this.object.srcUrl]); + this.spriteAvatarMesh = avatarSpriter.createSpriteAvatarMeshFromTextures(textureImages); + // this.spriteAvatarMesh.visible = false; + // this.spriteAvatarMesh.enabled = true; // XXX + this.scene.add(this.spriteAvatarMesh); + } + break; + } + case 2: { + if (!this.crunchedModel) { + if (!this.object.arrayBuffer) { + debugger; + } + const glbData = await this.crunchAvatarModel([this.object.arrayBuffer, this.object.srcUrl]); + const glb = await new Promise((accept, reject) => { + const {gltfLoader} = loaders; + gltfLoader.parse(glbData, this.object.srcUrl, object => { + accept(object.scene); + }, reject); + }); + this.crunchedModel = glb; + // this.crunchedModel.visible = false; + // this.crunchedModel.enabled = true; // XXX + this.scene.add(this.crunchedModel); + } + break; + } + case 3: { + if (!this.optimizedModel) { + if (!this.object.arrayBuffer) { + debugger; + } + const glbData = await this.optimizeAvatarModel([this.object.arrayBuffer, this.object.srcUrl]); + const glb = await new Promise((accept, reject) => { + const {gltfLoader} = loaders; + gltfLoader.parse(glbData, this.object.srcUrl, object => { + accept(object.scene); + }, reject); + }); + _bindSkeleton(glb, this.object); + /* glb.traverse(o => { + if (o.isMesh) { + console.log('set flag', o); + o.onBeforeRender = () => { + debugger; + }; + o.frustumCulled = false; + + for (const k in o.material) { + const v = o.material[k]; + if (v?.isTexture) { + v.onUpdate = () => { + debugger; + }; + } + } + } + }); */ + this.optimizedModel = glb; + // this.optimizedModel.visible = false; + // this.optimizedModel.enabled = true; // XXX + this.scene.add(this.optimizedModel); + + // window.optimizedModel = this.optimizedModel; + } + break; + } + case 4: { + break; + } + default: { + throw new Error('unknown avatar quality: ' + this.quality); + } + } + + this.scene.remove(this.placeholderMesh); + + // this.#updateVisibility(); + } + /* #updateVisibility() { + this.object.visible = false; + if (this.spriteAvatarMesh) { + this.spriteAvatarMesh.visible = false; + } + if (this.crunchedModel) { + this.crunchedModel.visible = false; + } + if (this.optimizedModel) { + this.optimizedModel.visible = false; + } + + switch (this.quality) { + case 1: { + if (this.spriteAvatarMesh && this.spriteAvatarMesh.enabled) { + this.spriteAvatarMesh.visible = true; + } + break; + } + case 2: { + if (this.crunchedModel && this.crunchedModel.enabled) { + this.crunchedModel.visible = true; + } + break; + } + case 3: { + if (this.optimizedModel && this.optimizedModel.enabled) { + this.optimizedModel.visible = true; + } + break; + } + case 4: { + this.object.visible = true; + break; + } + default: { + throw new Error('unknown avatar quality: ' + this.quality); + } + } + } */ +} \ No newline at end of file From a6c788494f3c2557e1ca092e50d84071c5b9ffac Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Thu, 28 Jul 2022 03:32:15 -0400 Subject: [PATCH 52/57] Latch new avatar renderer in avatars.js --- avatar-cruncher.js | 24 ++++++--- avatar-optimizer.js | 123 +++++++++++++++++++++++++++++++++++++------ avatar-spriter.js | 7 ++- avatars/avatars.js | 110 +++++++------------------------------- constants.js | 2 + metaversefile-api.js | 4 +- 6 files changed, 151 insertions(+), 119 deletions(-) diff --git a/avatar-cruncher.js b/avatar-cruncher.js index 22ad32efa6..9f29cc5948 100644 --- a/avatar-cruncher.js +++ b/avatar-cruncher.js @@ -1,10 +1,10 @@ import * as THREE from 'three'; - import {getMergeableObjects, mergeGeometryTextureAtlas} from './geometry-texture-atlas.js'; +import exporters from './exporters.js'; const defaultTextureSize = 4096; -export const crunchAvatarModel = (model, options = {}) => { +export const crunchAvatarModel = async (model, options = {}) => { const textureSize = options.textureSize ?? defaultTextureSize; const textureTypes = [ @@ -22,10 +22,10 @@ export const crunchAvatarModel = (model, options = {}) => { morphTargetInfluencesArray, } = mergeable; const { - atlas, - atlasImages, - attributeLayouts, - morphAttributeLayouts, + // atlas, + // atlasImages, + // attributeLayouts, + // morphAttributeLayouts, geometry, atlasTextures, } = mergeGeometryTextureAtlas(mergeable, textureSize); @@ -46,5 +46,15 @@ export const crunchAvatarModel = (model, options = {}) => { crunchedModel.morphTargetDictionary = morphTargetDictionaryArray[0]; crunchedModel.morphTargetInfluences = morphTargetInfluencesArray[0]; crunchedModel.frustumCulled = false; - return crunchedModel; + + const glbData = await new Promise((accept, reject) => { + const {gltfExporter} = exporters; + gltfExporter.parse(crunchedModel, function onCompleted(arrayBuffer) { + accept(arrayBuffer); + }, function onError(error) { + reject(error); + } + ); + }); + return glbData; }; \ No newline at end of file diff --git a/avatar-optimizer.js b/avatar-optimizer.js index e8ffb40dad..61c2423948 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -2,6 +2,7 @@ import * as THREE from 'three'; import {MaxRectsPacker} from 'maxrects-packer'; import {getRenderer} from './renderer.js'; import {modUv} from './util.js'; +import exporters from './exporters.js'; const defaultTextureSize = 4096; const startAtlasSize = 512; @@ -48,6 +49,24 @@ const getObjectKeyDefault = (type, object, material) => { renderer.getProgramCacheKey(object, material), ].join(','); }; +export const getSkeletons = (object) => { + const skeletons = []; + object.traverse((o) => { + if (o.isSkeleton) { + skeletons.push(o); + } + }); + return skeletons; +}; +export const getBones = (object) => { + const bones = []; + object.traverse((o) => { + if (o.isBone) { + bones.push(o); + } + }); + return bones; +} export const getMergeableObjects = (model, getObjectKey = getObjectKeyDefault) => { const mergeables = new Map(); model.traverse(o => { @@ -107,15 +126,15 @@ export const getMergeableObjects = (model, getObjectKey = getObjectKeyDefault) = export const mergeGeometryTextureAtlas = (mergeable, textureSize) => { const { - type, - material, + // type, + // material, geometries, maps, emissiveMaps, normalMaps, - skeletons, - morphTargetDictionaryArray, - morphTargetInfluencesArray, + // skeletons, + // morphTargetDictionaryArray, + // morphTargetInfluencesArray, } = mergeable; // compute texture sizes @@ -297,14 +316,18 @@ export const mergeGeometryTextureAtlas = (mergeable, textureSize) => { const attributeLayouts = _makeAttributeLayoutsFromGeometries(geometries); const _makeMorphAttributeLayoutsFromGeometries = geometries => { + console.log('got geomtries', geometries); + // create morph layouts const morphAttributeLayouts = []; for (const g of geometries) { const morphAttributes = g.morphAttributes; + // console.log('got keys', Object.keys(morphAttributes)); for (const morphAttributeName in morphAttributes) { const morphAttribute = morphAttributes[morphAttributeName]; let morphLayout = morphAttributeLayouts.find(l => l.name === morphAttributeName); if (!morphLayout) { + // console.log('missing morph layout', morphAttributeName, morphAttribute); morphLayout = new MorphAttributeLayout( morphAttributeName, morphAttribute[0].array.constructor, @@ -317,6 +340,11 @@ export const mergeGeometryTextureAtlas = (mergeable, textureSize) => { morphLayout.count += morphAttribute[0].count * morphAttribute[0].itemSize; } } + + /* for (const g of geometries) { + morphLayout.count += morphAttribute[0].count * morphAttribute[0].itemSize; + } */ + return morphAttributeLayouts; }; const morphAttributeLayouts = _makeMorphAttributeLayoutsFromGeometries(geometries); @@ -328,6 +356,9 @@ export const mergeGeometryTextureAtlas = (mergeable, textureSize) => { let gAttribute = g.attributes[layout.name]; if (!gAttribute) { if (layout.name === 'skinIndex' || layout.name === 'skinWeight') { + console.log('force layout', layout); + debugger; + gAttribute = layout.makeDefault(g); g.setAttribute(layout.name, gAttribute); @@ -343,7 +374,8 @@ export const mergeGeometryTextureAtlas = (mergeable, textureSize) => { for (const g of geometries) { let morphAttribute = g.morphAttributes[morphLayout.name]; if (!morphAttribute) { - // console.log('missing morph attribute', morphLayout, morphAttribute); + console.log('missing morph attribute', morphLayout, morphAttribute); + debugger; morphAttribute = morphLayout.makeDefault(g); g.morphAttributes[morphLayout.name] = morphAttribute; @@ -386,13 +418,34 @@ export const mergeGeometryTextureAtlas = (mergeable, textureSize) => { const morphAttribute = new THREE.BufferAttribute(morphData, morphLayout.itemSize); morphsArray[i] = morphAttribute; let morphDataIndex = 0; + + /* const geometries2 = geometries.slice(); + // randomize the order of geometries2 + for (let j = 0; j < geometries2.length; j++) { + const j2 = Math.floor(Math.random() * geometries2.length); + const tmp = geometries2[j]; + geometries2[j] = geometries2[j2]; + geometries2[j2] = tmp; + } */ + for (const g of geometries) { let gMorphAttribute = g.morphAttributes[morphLayout.name]; gMorphAttribute = gMorphAttribute?.[i]; if (gMorphAttribute) { morphData.set(gMorphAttribute.array, morphDataIndex); + + const nz = morphData.filter(n => n != 0); + console.log('case 1', nz.join(',')); + + /* for (let i = 0; i < morphData.length; i++) { + morphData[i] = Math.random(); + } */ + if ((gMorphAttribute.count * gMorphAttribute.itemSize) !== gMorphAttribute.array.length) { + debugger; + } morphDataIndex += gMorphAttribute.count * gMorphAttribute.itemSize; } else { + console.log('case 2'); const matchingAttribute = g.attributes[morphLayout.name]; morphDataIndex += matchingAttribute.count * matchingAttribute.itemSize; } @@ -532,7 +585,13 @@ export const mergeGeometryTextureAtlas = (mergeable, textureSize) => { }; }; -export const optimizeAvatarModel = (model, options = {}) => { +export const optimizeAvatarModel = async (model, options = {}) => { + /* if (!model) { + debugger; + } + if (!model.traverse) { + debugger; + } */ const textureSize = options.textureSize ?? defaultTextureSize; const mergeables = getMergeableObjects(model); @@ -541,19 +600,19 @@ export const optimizeAvatarModel = (model, options = {}) => { const { type, material, - geometries, - maps, - emissiveMaps, - normalMaps, + // geometries, + // maps, + // emissiveMaps, + // normalMaps, skeletons, morphTargetDictionaryArray, morphTargetInfluencesArray, } = mergeable; const { - atlas, - atlasImages, - attributeLayouts, - morphAttributeLayouts, + // atlas, + // atlasImages, + // attributeLayouts, + // morphAttributeLayouts, geometry, atlasTextures, } = mergeGeometryTextureAtlas(mergeable, textureSize); @@ -606,5 +665,37 @@ export const optimizeAvatarModel = (model, options = {}) => { for (const mesh of mergedMeshes) { object.add(mesh); } - return object; + + /* // also need skeletons or else the parse will crash + const skeletons = getSkeletons(model); + for (const skeleton of skeletons) { + object.add(skeleton); + } */ + + // same as above, but for bones + const bones = getBones(model); + for (const bone of bones) { + object.add(bone); + } + // console.log('got bones', model, bones); + + const glbData = await new Promise((accept, reject) => { + const {gltfExporter} = exporters; + gltfExporter.parse( + object, + function onCompleted(arrayBuffer) { + accept(arrayBuffer); + }, function onError(error) { + reject(error); + }, + { + binary: true, + onlyVisible: false, + // forceIndices: true, + truncateDrawRange: false, + includeCustomExtensions: true, + }, + ); + }); + return glbData; }; \ No newline at end of file diff --git a/avatar-spriter.js b/avatar-spriter.js index 7cc4b54f35..c6f7fb01c5 100644 --- a/avatar-spriter.js +++ b/avatar-spriter.js @@ -1680,8 +1680,7 @@ export const renderSpriteImages = skinnedVrm => { return spriteImages; }; -export const createSpriteAvatarMesh = skinnedVrm => { - const spriteImages = renderSpriteImages(skinnedVrm); +export const createSpriteAvatarMeshFromTextures = spriteImages => { const spriteTextures = spriteImages.map(img => { const t = new THREE.Texture(img); t.needsUpdate = true; @@ -1689,4 +1688,8 @@ export const createSpriteAvatarMesh = skinnedVrm => { }); const spriteAvatarMesh = new SpriteAvatarMesh(spriteTextures); return spriteAvatarMesh; +}; +export const createSpriteAvatarMesh = skinnedVrm => { + const spriteImages = renderSpriteImages(skinnedVrm); + return createSpriteAvatarMeshFromTextures(spriteImages); }; \ No newline at end of file diff --git a/avatars/avatars.js b/avatars/avatars.js index b41e27f36f..53f7868284 100644 --- a/avatars/avatars.js +++ b/avatars/avatars.js @@ -24,9 +24,7 @@ import { // avatarInterpolationNumFrames, } from '../constants.js'; // import {FixedTimeStep} from '../interpolants.js'; -import * as avatarOptimizer from '../avatar-optimizer.js'; -import * as avatarCruncher from '../avatar-cruncher.js'; -import * as avatarSpriter from '../avatar-spriter.js'; +import {AvatarRenderer} from './avatar-renderer.js'; // import * as sceneCruncher from '../scene-cruncher.js'; import { idleFactorSpeed, @@ -398,8 +396,6 @@ const _makeDebugMesh = (avatar) => { // const testMesh = new THREE.Mesh(g, m); // scene.add(testMesh); - - class Avatar { constructor(object, options = {}) { if (!object) { @@ -442,7 +438,17 @@ class Avatar { return o; })(); - this.model = model; + this.model = model; // XXX still needed? + this.model.visible = false; + + { + this.renderer = new AvatarRenderer(object); + scene.add(this.renderer.scene); // XXX debug + this.renderer.scene.updateMatrixWorld(); + globalThis.avatarRenderer = this.renderer; + globalThis.scene = scene; + } + this.spriteMegaAvatarMesh = null; this.crunchedModel = null; this.optimizedModel = null; @@ -461,10 +467,10 @@ class Avatar { flipZ, flipY, flipLeg, - tailBones, - armature, - armatureQuaternion, - armatureMatrixInverse, + // tailBones, + // armature, + // armatureQuaternion, + // armatureMatrixInverse, // retargetedAnimations, } = Avatar.bindAvatar(object); this.skinnedMeshes = skinnedMeshes; @@ -880,7 +886,7 @@ class Avatar { this.microphoneWorker = null; this.volume = -1; - this.quality = 4; + // this.quality = 4; this.shoulderTransforms.Start(); this.legsManager.Start(); @@ -1408,87 +1414,7 @@ class Avatar { return localEuler.y; } async setQuality(quality) { - this.quality = quality; - - switch (this.quality) { - case 1: { - if (!this.spriteAvatarMesh) { - const skinnedMesh = await this.object.cloneVrm(); - this.spriteAvatarMesh = avatarSpriter.createSpriteAvatarMesh(skinnedMesh); - this.spriteAvatarMesh.visible = false; - this.spriteAvatarMesh.enabled = true; // XXX - scene.add(this.spriteAvatarMesh); - } - break; - } - case 2: { - if (!this.crunchedModel) { - this.crunchedModel = avatarCruncher.crunchAvatarModel(this.model); - this.crunchedModel.visible = false; - this.crunchedModel.enabled = true; // XXX - scene.add(this.crunchedModel); - } - break; - } - case 3: { - if (!this.optimizedModel) { - this.optimizedModel = avatarOptimizer.optimizeAvatarModel(this.model); - this.optimizedModel.visible = false; - this.optimizedModel.enabled = true; // XXX - scene.add(this.optimizedModel); - } - break; - } - case 3: - case 4: { - break; - } - default: { - throw new Error('unknown avatar quality: ' + this.quality); - } - } - - this.#updateVisibility(); - } - #updateVisibility() { - this.model.visible = false; - if (this.spriteAvatarMesh) { - this.spriteAvatarMesh.visible = false; - } - if (this.crunchedModel) { - this.crunchedModel.visible = false; - } - if (this.optimizedModel) { - this.optimizedModel.visible = false; - } - - switch (this.quality) { - case 1: { - if (this.spriteAvatarMesh && this.spriteAvatarMesh.enabled) { - this.spriteAvatarMesh.visible = true; - } - break; - } - case 2: { - if (this.crunchedModel && this.crunchedModel.enabled) { - this.crunchedModel.visible = true; - } - break; - } - case 3: { - if (this.optimizedModel && this.optimizedModel.enabled) { - this.optimizedModel.visible = true; - } - break; - } - case 4: { - this.model.visible = true; - break; - } - default: { - throw new Error('unknown avatar quality: ' + this.quality); - } - } + await this.renderer.setQuality(quality); } lerpShoulderTransforms() { if (this.shoulderTransforms.handsEnabled[0]) { diff --git a/constants.js b/constants.js index 1383da819f..2afca19601 100644 --- a/constants.js +++ b/constants.js @@ -160,6 +160,8 @@ export const defaultDioramaSize = 512; export const defaultChunkSize = 16; export const defaultWorldSeed = 100; +export const defaultAvatarQuality = 3; + export const defaultVoiceEndpoint = `Sweetie Belle`; export const defaultVoicePackName = `ShiShi voice pack`; diff --git a/metaversefile-api.js b/metaversefile-api.js index abc5ec348b..21c609c182 100644 --- a/metaversefile-api.js +++ b/metaversefile-api.js @@ -453,9 +453,9 @@ metaversefile.setApi({ useAvatarOptimizer() { return avatarOptimizer; }, - useAvatarCruncher() { + /* useAvatarCruncher() { return avatarCruncher; - }, + }, */ useAvatarSpriter() { return avatarSpriter; }, From 0332eac6c5738d37baf8981ee33de0bdaa26ee07 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Thu, 28 Jul 2022 22:12:16 -0400 Subject: [PATCH 53/57] Major morphs debugging --- avatar-optimizer.js | 47 +++++++++++++++---- avatars/avatar-renderer.js | 94 +++++++++++++++++++++++++------------- 2 files changed, 102 insertions(+), 39 deletions(-) diff --git a/avatar-optimizer.js b/avatar-optimizer.js index 61c2423948..82b547c18a 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -98,6 +98,7 @@ export const getMergeableObjects = (model, getObjectKey = getObjectKeyDefault) = m = { type, material: objectMaterial, + objects: [], geometries: [], maps: [], emissiveMaps: [], @@ -110,6 +111,7 @@ export const getMergeableObjects = (model, getObjectKey = getObjectKeyDefault) = mergeables.set(key, m); } + m.objects.push(o); m.geometries.push(objectGeometry); m.maps.push(map); m.emissiveMaps.push(emissiveMap); @@ -128,6 +130,7 @@ export const mergeGeometryTextureAtlas = (mergeable, textureSize) => { const { // type, // material, + objects, geometries, maps, emissiveMaps, @@ -316,7 +319,10 @@ export const mergeGeometryTextureAtlas = (mergeable, textureSize) => { const attributeLayouts = _makeAttributeLayoutsFromGeometries(geometries); const _makeMorphAttributeLayoutsFromGeometries = geometries => { - console.log('got geomtries', geometries); + // console.log('got geomtries', geometries); + /* for (const geometry of geometries) { + geometry. + } */ // create morph layouts const morphAttributeLayouts = []; @@ -410,7 +416,10 @@ export const mergeGeometryTextureAtlas = (mergeable, textureSize) => { geometry.setAttribute(layout.name, attribute); } }; - const _mergeMorphAttributes = (geometry, geometries, morphAttributeLayouts) => { + const _mergeMorphAttributes = (geometry, geometries, objects, morphAttributeLayouts) => { + // console.log('morphAttributeLayouts', morphAttributeLayouts); + // globalThis.morphAttributeLayouts = morphAttributeLayouts; + for (const morphLayout of morphAttributeLayouts) { const morphsArray = Array(morphLayout.arraySize); for (let i = 0; i < morphLayout.arraySize; i++) { @@ -427,15 +436,31 @@ export const mergeGeometryTextureAtlas = (mergeable, textureSize) => { geometries2[j] = geometries2[j2]; geometries2[j2] = tmp; } */ + + // console.log('num geos', geometries.length); - for (const g of geometries) { + let first = 0; + for (let i = 0; i < geometries.length; i++) { + // const object = objects[i]; + const g = geometries[i]; + + const r = Math.random(); + let gMorphAttribute = g.morphAttributes[morphLayout.name]; gMorphAttribute = gMorphAttribute?.[i]; if (gMorphAttribute) { + // console.log('src', first, object, g, gMorphAttribute); morphData.set(gMorphAttribute.array, morphDataIndex); - const nz = morphData.filter(n => n != 0); - console.log('case 1', nz.join(',')); + // const nz = gMorphAttribute.array.filter(n => n != 0); + // console.log('case 1', first, nz.length, object, g, gMorphAttribute); + + if (first === 2 || first === 1) { + for (let i = 0; i < gMorphAttribute.array.length; i++) { + // morphData[morphDataIndex + i] = r; + // morphData[morphDataIndex + i] *= 100; + } + } /* for (let i = 0; i < morphData.length; i++) { morphData[i] = Math.random(); @@ -446,13 +471,17 @@ export const mergeGeometryTextureAtlas = (mergeable, textureSize) => { morphDataIndex += gMorphAttribute.count * gMorphAttribute.itemSize; } else { console.log('case 2'); + debugger; const matchingAttribute = g.attributes[morphLayout.name]; morphDataIndex += matchingAttribute.count * matchingAttribute.itemSize; } + + first++; } // sanity check if (morphDataIndex !== morphLayout.count) { console.warn('desynced morph data 2', morphLayout.name, morphDataIndex, morphLayout.count); + debugger; } } geometry.morphAttributes[morphLayout.name] = morphsArray; @@ -522,19 +551,19 @@ export const mergeGeometryTextureAtlas = (mergeable, textureSize) => { }); } }; - const _mergeGeometries = geometries => { + const _mergeGeometries = (geometries, objects) => { const geometry = new THREE.BufferGeometry(); geometry.morphTargetsRelative = true; _forceGeometriesAttributeLayouts(attributeLayouts, geometries); _mergeAttributes(geometry, geometries, attributeLayouts); - _mergeMorphAttributes(geometry, geometries, morphAttributeLayouts); + _mergeMorphAttributes(geometry, geometries, objects, morphAttributeLayouts); _mergeIndices(geometry, geometries); _remapGeometryUvs(geometry, geometries); return geometry; }; - const geometry = _mergeGeometries(geometries); + const geometry = _mergeGeometries(geometries, objects); // console.log('got geometry', geometry); const _makeAtlasTextures = atlasImages => { @@ -649,6 +678,8 @@ export const optimizeAvatarModel = async (model, options = {}) => { skinnedMesh.skeleton = skeletons[0]; skinnedMesh.morphTargetDictionary = morphTargetDictionaryArray[0]; skinnedMesh.morphTargetInfluences = morphTargetInfluencesArray[0]; + // skinnedMesh.updateMorphTargets(); + // console.log('got influences', skinnedMesh.morphTargetInfluences); return skinnedMesh; } else { throw new Error(`unknown type ${type}`); diff --git a/avatars/avatar-renderer.js b/avatars/avatar-renderer.js index 6837bb18dc..a706023472 100644 --- a/avatars/avatar-renderer.js +++ b/avatars/avatar-renderer.js @@ -5,9 +5,16 @@ import * as avatarCruncher from '../avatar-cruncher.js'; import * as avatarSpriter from '../avatar-spriter.js'; import offscreenEngineManager from '../offscreen-engine-manager.js'; import loaders from '../loaders.js'; +import exporters from '../exporters.js'; import { defaultAvatarQuality, } from '../constants.js'; +import { downloadFile } from '../util.js'; + +window.morphTargetDictionaries = []; +window.morphTargetInfluences = []; +window.srcMorphTargetDictionaries = []; +window.srcMorphTargetInfluences = []; const avatarPlaceholderImagePromise = (async () => { const avatarPlaceholderImage = new Image(); @@ -34,7 +41,7 @@ const _makeAvatarPlaceholderMesh = () => { return mesh; }; const _bindSkeleton = (dstModel, srcObject) => { - console.log('bind skeleton', dstModel, srcObject); + // console.log('bind skeleton', dstModel, srcObject); const srcModel = srcObject.scene; const _findBoneInSrc = (srcBoneName) => { @@ -103,6 +110,9 @@ const _bindSkeleton = (dstModel, srcObject) => { // bind the skeleton const {skeleton: dstSkeleton} = o; const srcSkeleton = _findSrcSkeletonFromDstSkeleton(dstSkeleton); + if (!srcSkeleton) { + debugger; + } o.skeleton = srcSkeleton; } if (o.isMesh) { @@ -110,8 +120,15 @@ const _bindSkeleton = (dstModel, srcObject) => { // skinnedMesh.skeleton = skeletons[0]; const skinnedMesh = _findSkinnedMeshInSrc(); // console.log('map blend shapes', o, skinnedMesh); - o.morphTargetDictionary = skinnedMesh.morphTargetDictionary; - o.morphTargetInfluences = skinnedMesh.morphTargetInfluences; + + // o.morphTargetDictionary = skinnedMesh.morphTargetDictionary; + // o.morphTargetInfluences = skinnedMesh.morphTargetInfluences; + + window.morphTargetDictionaries.push(o.morphTargetDictionary); + window.morphTargetInfluences.push(o.morphTargetInfluences); + window.srcMorphTargetDictionaries.push(skinnedMesh.morphTargetDictionary); + window.srcMorphTargetInfluences.push(skinnedMesh.morphTargetInfluences); + // o.geometry.morphAttributes = skinnedMesh.geometry.morphAttributes; // o.morphAttributes = skinnedMesh.morphAttributes; // o.morphAttributesRelative = skinnedMesh.morphAttributesRelative; @@ -123,7 +140,7 @@ const _bindSkeleton = (dstModel, srcObject) => { for (let i = 0; i < attr.array.length; i++) { // if ((attr.array[i]) != 0) { - attr.array[i] *= 10; + // attr.array[i] *= 10; // attr.array[i] = Math.random(); // } } @@ -138,7 +155,12 @@ const _bindSkeleton = (dstModel, srcObject) => { const _frame = () => { window.requestAnimationFrame(_frame); - // console.log('got frame', o.morphTargetInfluences.join(',')); + if (o.morphTargetInfluences.length !== skinnedMesh.morphTargetInfluences.length) { + debugger; + } + for (let i = 0; i < o.morphTargetInfluences.length; i++) { + o.morphTargetInfluences[i] = skinnedMesh.morphTargetInfluences[i]; + } }; _frame(); } @@ -307,41 +329,51 @@ export class AvatarRenderer { } case 3: { if (!this.optimizedModel) { - if (!this.object.arrayBuffer) { - debugger; - } - const glbData = await this.optimizeAvatarModel([this.object.arrayBuffer, this.object.srcUrl]); + this.optimizedModel = true; + + // const glbData = await this.optimizeAvatarModel([this.object.arrayBuffer, this.object.srcUrl]); + + const parseVrm = (arrayBuffer, srcUrl) => new Promise((accept, reject) => { + const { gltfLoader } = loaders; + gltfLoader.parse(arrayBuffer, srcUrl, object => { + accept(object); + }, reject); + }); + const object = await parseVrm(this.object.arrayBuffer, this.object.srcUrl); + object.scene.updateMatrixWorld(); + const glbData = await new Promise((accept, reject) => { + const {gltfExporter} = exporters; + gltfExporter.parse( + object.scene, + function onCompleted(arrayBuffer) { + accept(arrayBuffer); + }, function onError(error) { + reject(error); + }, + { + binary: true, + includeCustomExtensions: true, + }, + ); + }); + const glb = await new Promise((accept, reject) => { const {gltfLoader} = loaders; gltfLoader.parse(glbData, this.object.srcUrl, object => { + // window.o15 = object; accept(object.scene); }, reject); }); + _bindSkeleton(glb, this.object); - /* glb.traverse(o => { - if (o.isMesh) { - console.log('set flag', o); - o.onBeforeRender = () => { - debugger; - }; - o.frustumCulled = false; - - for (const k in o.material) { - const v = o.material[k]; - if (v?.isTexture) { - v.onUpdate = () => { - debugger; - }; - } - } - } - }); */ this.optimizedModel = glb; - // this.optimizedModel.visible = false; - // this.optimizedModel.enabled = true; // XXX + + // object.scene.position.x = -10; + // object.scene.updateMatrixWorld(); + // this.scene.add(object.scene); + + this.optimizedModel.updateMatrixWorld(); this.scene.add(this.optimizedModel); - - // window.optimizedModel = this.optimizedModel; } break; } From 18fcd6469160915c74b41fdec4482c24ee70b733 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Thu, 28 Jul 2022 22:25:49 -0400 Subject: [PATCH 54/57] More morphs debugging --- avatar-optimizer.js | 18 +++++++++--------- avatars/avatar-renderer.js | 17 +++++++++-------- avatars/avatars.js | 4 ++++ character-controller.js | 5 +++-- 4 files changed, 25 insertions(+), 19 deletions(-) diff --git a/avatar-optimizer.js b/avatar-optimizer.js index 82b547c18a..d9fd8e4a1a 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -444,23 +444,23 @@ export const mergeGeometryTextureAtlas = (mergeable, textureSize) => { // const object = objects[i]; const g = geometries[i]; - const r = Math.random(); + // const r = Math.random(); let gMorphAttribute = g.morphAttributes[morphLayout.name]; gMorphAttribute = gMorphAttribute?.[i]; if (gMorphAttribute) { - // console.log('src', first, object, g, gMorphAttribute); + console.log('src', first, g, gMorphAttribute); morphData.set(gMorphAttribute.array, morphDataIndex); // const nz = gMorphAttribute.array.filter(n => n != 0); // console.log('case 1', first, nz.length, object, g, gMorphAttribute); - if (first === 2 || first === 1) { + /* if (first === 2 || first === 1) { for (let i = 0; i < gMorphAttribute.array.length; i++) { // morphData[morphDataIndex + i] = r; // morphData[morphDataIndex + i] *= 100; } - } + } */ /* for (let i = 0; i < morphData.length; i++) { morphData[i] = Math.random(); @@ -676,9 +676,9 @@ export const optimizeAvatarModel = async (model, options = {}) => { } else if (type === 'skinnedMesh') { const skinnedMesh = new THREE.SkinnedMesh(geometry, m); skinnedMesh.skeleton = skeletons[0]; - skinnedMesh.morphTargetDictionary = morphTargetDictionaryArray[0]; - skinnedMesh.morphTargetInfluences = morphTargetInfluencesArray[0]; - // skinnedMesh.updateMorphTargets(); + // skinnedMesh.morphTargetDictionary = morphTargetDictionaryArray[0]; + // skinnedMesh.morphTargetInfluences = morphTargetInfluencesArray[0]; + skinnedMesh.updateMorphTargets(); // console.log('got influences', skinnedMesh.morphTargetInfluences); return skinnedMesh; } else { @@ -721,9 +721,9 @@ export const optimizeAvatarModel = async (model, options = {}) => { }, { binary: true, - onlyVisible: false, + // onlyVisible: false, // forceIndices: true, - truncateDrawRange: false, + // truncateDrawRange: false, includeCustomExtensions: true, }, ); diff --git a/avatars/avatar-renderer.js b/avatars/avatar-renderer.js index a706023472..ff55c3a5a2 100644 --- a/avatars/avatar-renderer.js +++ b/avatars/avatar-renderer.js @@ -133,7 +133,7 @@ const _bindSkeleton = (dstModel, srcObject) => { // o.morphAttributes = skinnedMesh.morphAttributes; // o.morphAttributesRelative = skinnedMesh.morphAttributesRelative; - o.geometry.morphAttributes.position.forEach(attr => { + /* o.geometry.morphAttributes.position.forEach(attr => { attr.onUploadCallback = () => { console.log('upload callback'); }; @@ -144,12 +144,12 @@ const _bindSkeleton = (dstModel, srcObject) => { // attr.array[i] = Math.random(); // } } - }); + }); */ // o.onBeforeRender = () => {debugger;} - o.material.onBeforeCompile = (shader) => { + /* o.material.onBeforeCompile = (shader) => { console.log('compile avatar shader', shader); - }; + }; */ // window.o = o; const _frame = () => { @@ -159,7 +159,8 @@ const _bindSkeleton = (dstModel, srcObject) => { debugger; } for (let i = 0; i < o.morphTargetInfluences.length; i++) { - o.morphTargetInfluences[i] = skinnedMesh.morphTargetInfluences[i]; + // o.morphTargetInfluences[i] = skinnedMesh.morphTargetInfluences[i]; + o.morphTargetInfluences[i] = 1; } }; _frame(); @@ -331,9 +332,9 @@ export class AvatarRenderer { if (!this.optimizedModel) { this.optimizedModel = true; - // const glbData = await this.optimizeAvatarModel([this.object.arrayBuffer, this.object.srcUrl]); + const glbData = await this.optimizeAvatarModel([this.object.arrayBuffer, this.object.srcUrl]); - const parseVrm = (arrayBuffer, srcUrl) => new Promise((accept, reject) => { + /* const parseVrm = (arrayBuffer, srcUrl) => new Promise((accept, reject) => { const { gltfLoader } = loaders; gltfLoader.parse(arrayBuffer, srcUrl, object => { accept(object); @@ -355,7 +356,7 @@ export class AvatarRenderer { includeCustomExtensions: true, }, ); - }); + }); */ const glb = await new Promise((accept, reject) => { const {gltfLoader} = loaders; diff --git a/avatars/avatars.js b/avatars/avatars.js index 53f7868284..024a815951 100644 --- a/avatars/avatars.js +++ b/avatars/avatars.js @@ -438,6 +438,10 @@ class Avatar { return o; })(); + // if (!model.parent) { + console.log('model parent', model.parent, new Error().stack); + // } + this.model = model; // XXX still needed? this.model.visible = false; diff --git a/character-controller.js b/character-controller.js index f68c90eabe..2a08a5fe86 100644 --- a/character-controller.js +++ b/character-controller.js @@ -12,7 +12,7 @@ import {getRenderer, scene, camera, dolly} from './renderer.js'; import physicsManager from './physics-manager.js'; import {world} from './world.js'; // import cameraManager from './camera-manager.js'; -import physx from './physx.js'; +// import physx from './physx.js'; import audioManager from './audio-manager.js'; import metaversefile from 'metaversefile'; import { @@ -23,7 +23,7 @@ import { activateMaxTime, // useMaxTime, aimTransitionMaxTime, - avatarInterpolationFrameRate, + // avatarInterpolationFrameRate, avatarInterpolationTimeDelay, avatarInterpolationNumFrames, // groundFriction, @@ -1056,6 +1056,7 @@ class LocalPlayer extends UninterpolatedPlayer { async setAvatarUrl(u) { const localAvatarEpoch = ++this.avatarEpoch; const avatarApp = await this.appManager.addTrackedApp(u); + // avatarApp.parent.remove(avatarApp); if (this.avatarEpoch !== localAvatarEpoch) { this.appManager.removeTrackedApp(avatarApp.instanceId); return; From d42392e823007deb6c7fac22b44bb3ad5e76c13b Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Fri, 29 Jul 2022 07:42:44 -0400 Subject: [PATCH 55/57] Add more morph targets resolution --- avatar-optimizer.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/avatar-optimizer.js b/avatar-optimizer.js index d9fd8e4a1a..f11de569f9 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -136,8 +136,8 @@ export const mergeGeometryTextureAtlas = (mergeable, textureSize) => { emissiveMaps, normalMaps, // skeletons, - // morphTargetDictionaryArray, - // morphTargetInfluencesArray, + morphTargetDictionaryArray, + morphTargetInfluencesArray, } = mergeable; // compute texture sizes @@ -440,13 +440,16 @@ export const mergeGeometryTextureAtlas = (mergeable, textureSize) => { // console.log('num geos', geometries.length); let first = 0; - for (let i = 0; i < geometries.length; i++) { + for (let j = 0; j < geometries.length; j++) { // const object = objects[i]; - const g = geometries[i]; + const g = geometries[j]; // const r = Math.random(); let gMorphAttribute = g.morphAttributes[morphLayout.name]; + if (gMorphAttribute.length !== morphsArray.length) { + debugger; + } gMorphAttribute = gMorphAttribute?.[i]; if (gMorphAttribute) { console.log('src', first, g, gMorphAttribute); @@ -676,9 +679,9 @@ export const optimizeAvatarModel = async (model, options = {}) => { } else if (type === 'skinnedMesh') { const skinnedMesh = new THREE.SkinnedMesh(geometry, m); skinnedMesh.skeleton = skeletons[0]; - // skinnedMesh.morphTargetDictionary = morphTargetDictionaryArray[0]; - // skinnedMesh.morphTargetInfluences = morphTargetInfluencesArray[0]; - skinnedMesh.updateMorphTargets(); + skinnedMesh.morphTargetDictionary = morphTargetDictionaryArray[0]; + skinnedMesh.morphTargetInfluences = morphTargetInfluencesArray[0]; + // skinnedMesh.updateMorphTargets(); // console.log('got influences', skinnedMesh.morphTargetInfluences); return skinnedMesh; } else { From 86fad17d86f68f391f64c36423b9fd35c57f174e Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Fri, 29 Jul 2022 11:52:23 -0400 Subject: [PATCH 56/57] More morphs work --- avatar-optimizer.js | 168 ++++++++++++++++++++++++++++++++++--- avatars/avatar-renderer.js | 16 +++- 2 files changed, 169 insertions(+), 15 deletions(-) diff --git a/avatar-optimizer.js b/avatar-optimizer.js index f11de569f9..1d5ef65adb 100644 --- a/avatar-optimizer.js +++ b/avatar-optimizer.js @@ -136,8 +136,8 @@ export const mergeGeometryTextureAtlas = (mergeable, textureSize) => { emissiveMaps, normalMaps, // skeletons, - morphTargetDictionaryArray, - morphTargetInfluencesArray, + // morphTargetDictionaryArray, + // morphTargetInfluencesArray, } = mergeable; // compute texture sizes @@ -343,13 +343,30 @@ export const mergeGeometryTextureAtlas = (mergeable, textureSize) => { morphAttributeLayouts.push(morphLayout); } + for (let i = 1; i < morphAttribute.length; i++) { + const attribute = morphAttribute[i]; + if (attribute.count !== morphAttribute[0].count) { + debugger; + } + if (attribute.itemSize !== morphAttribute[0].itemSize) { + debugger; + } + if (attribute.array.constructor !== morphAttribute[0].array.constructor) { + debugger; + } + } + morphLayout.count += morphAttribute[0].count * morphAttribute[0].itemSize; } } - /* for (const g of geometries) { - morphLayout.count += morphAttribute[0].count * morphAttribute[0].itemSize; - } */ + for (let i = 0; i < geometries.length; i++) { + const g = geometries[i]; + for (const k in g.morphAttributes) { + const morphAttribute = g.morphAttributes[k]; + console.log('got morph attr', i, k, morphAttribute); + } + } return morphAttributeLayouts; }; @@ -364,7 +381,7 @@ export const mergeGeometryTextureAtlas = (mergeable, textureSize) => { if (layout.name === 'skinIndex' || layout.name === 'skinWeight') { console.log('force layout', layout); debugger; - + gAttribute = layout.makeDefault(g); g.setAttribute(layout.name, gAttribute); @@ -416,6 +433,122 @@ export const mergeGeometryTextureAtlas = (mergeable, textureSize) => { geometry.setAttribute(layout.name, attribute); } }; + /* function mergeBufferAttributes( attributes ) { + + let TypedArray; + let itemSize; + let normalized; + let arrayLength = 0; + + for ( let i = 0; i < attributes.length; ++ i ) { + + const attribute = attributes[ i ]; + + if ( attribute.isInterleavedBufferAttribute ) { + + console.error( 'THREE.BufferGeometryUtils: .mergeBufferAttributes() failed. InterleavedBufferAttributes are not supported.' ); + return null; + + } + + if ( TypedArray === undefined ) TypedArray = attribute.array.constructor; + if ( TypedArray !== attribute.array.constructor ) { + + console.error( 'THREE.BufferGeometryUtils: .mergeBufferAttributes() failed. BufferAttribute.array must be of consistent array types across matching attributes.' ); + return null; + + } + + if ( itemSize === undefined ) itemSize = attribute.itemSize; + if ( itemSize !== attribute.itemSize ) { + + console.error( 'THREE.BufferGeometryUtils: .mergeBufferAttributes() failed. BufferAttribute.itemSize must be consistent across matching attributes.' ); + return null; + + } + + if ( normalized === undefined ) normalized = attribute.normalized; + if ( normalized !== attribute.normalized ) { + + console.error( 'THREE.BufferGeometryUtils: .mergeBufferAttributes() failed. BufferAttribute.normalized must be consistent across matching attributes.' ); + return null; + + } + + arrayLength += attribute.array.length; + + } + + const array = new TypedArray( arrayLength ); + let offset = 0; + + for ( let i = 0; i < attributes.length; ++ i ) { + + array.set( attributes[ i ].array, offset ); + + offset += attributes[ i ].array.length; + + } + + return new THREE.BufferAttribute( array, itemSize, normalized ); + + } + const _mergeMorphAttributes = (geometry, geometries, objects, morphAttributeLayouts) => { + // collect + const morphAttributesUsed = new Set( Object.keys( geometries[ 0 ].morphAttributes ) ); + const morphAttributes = {}; + for (const geometry of geometries) { + for ( const name in geometry.morphAttributes ) { + + if ( ! morphAttributesUsed.has( name ) ) { + + console.error( 'THREE.BufferGeometryUtils: .mergeBufferGeometries() failed with geometry at index ' + i + '. .morphAttributes must be consistent throughout all geometries.' ); + return null; + + } + + if ( morphAttributes[ name ] === undefined ) morphAttributes[ name ] = []; + + morphAttributes[ name ].push( geometry.morphAttributes[ name ] ); + + } + } + + // merge + for ( const name in morphAttributes ) { + + const numMorphTargets = morphAttributes[ name ][ 0 ].length; + + if ( numMorphTargets === 0 ) break; + + geometry.morphAttributes = geometry.morphAttributes || {}; + geometry.morphAttributes[ name ] = []; + + for ( let i = 0; i < numMorphTargets; ++ i ) { + + const morphAttributesToMerge = []; + + for ( let j = 0; j < morphAttributes[ name ].length; ++ j ) { + + morphAttributesToMerge.push( morphAttributes[ name ][ j ][ i ] ); + + } + + const mergedMorphAttribute = mergeBufferAttributes( morphAttributesToMerge ); + + if ( ! mergedMorphAttribute ) { + + console.error( 'THREE.BufferGeometryUtils: .mergeBufferGeometries() failed while trying to merge the ' + name + ' morphAttribute.' ); + return null; + + } + + geometry.morphAttributes[ name ].push( mergedMorphAttribute ); + + } + + } + }; */ const _mergeMorphAttributes = (geometry, geometries, objects, morphAttributeLayouts) => { // console.log('morphAttributeLayouts', morphAttributeLayouts); // globalThis.morphAttributeLayouts = morphAttributeLayouts; @@ -437,7 +570,7 @@ export const mergeGeometryTextureAtlas = (mergeable, textureSize) => { geometries2[j2] = tmp; } */ - // console.log('num geos', geometries.length); + console.log('num geos', geometries); let first = 0; for (let j = 0; j < geometries.length; j++) { @@ -451,12 +584,15 @@ export const mergeGeometryTextureAtlas = (mergeable, textureSize) => { debugger; } gMorphAttribute = gMorphAttribute?.[i]; + if (gMorphAttribute.count !== g.attributes.position.count) { + debugger; + } if (gMorphAttribute) { - console.log('src', first, g, gMorphAttribute); + // console.log('src', first, g, gMorphAttribute, morphAttribute, object); morphData.set(gMorphAttribute.array, morphDataIndex); - // const nz = gMorphAttribute.array.filter(n => n != 0); - // console.log('case 1', first, nz.length, object, g, gMorphAttribute); + const nz = gMorphAttribute.array.filter(n => Math.abs(n) >= 0.01); + console.log('case 1', first, nz.length); /* if (first === 2 || first === 1) { for (let i = 0; i < gMorphAttribute.array.length; i++) { @@ -488,6 +624,7 @@ export const mergeGeometryTextureAtlas = (mergeable, textureSize) => { } } geometry.morphAttributes[morphLayout.name] = morphsArray; + // geometry.morphTargetsRelative = true; } }; const _mergeIndices = (geometry, geometries) => { @@ -504,6 +641,9 @@ export const mergeGeometryTextureAtlas = (mergeable, textureSize) => { for (let i = 0; i < srcIndexData.length; i++) { indexData[indexOffset++] = srcIndexData[i] + positionOffset; } + if (g.attributes.position.count !== g.morphAttributes.position[0].count) { + debugger; + } positionOffset += g.attributes.position.count; } geometry.setIndex(new THREE.BufferAttribute(indexData, 1)); @@ -713,7 +853,11 @@ export const optimizeAvatarModel = async (model, options = {}) => { } // console.log('got bones', model, bones); - const glbData = await new Promise((accept, reject) => { + // XXX this should anti-index flattened index ranges for the multi-materials case + + return object; + + /* const glbData = await new Promise((accept, reject) => { const {gltfExporter} = exporters; gltfExporter.parse( object, @@ -731,5 +875,5 @@ export const optimizeAvatarModel = async (model, options = {}) => { }, ); }); - return glbData; + return glbData; */ }; \ No newline at end of file diff --git a/avatars/avatar-renderer.js b/avatars/avatar-renderer.js index ff55c3a5a2..f1d88631fa 100644 --- a/avatars/avatar-renderer.js +++ b/avatars/avatar-renderer.js @@ -332,7 +332,15 @@ export class AvatarRenderer { if (!this.optimizedModel) { this.optimizedModel = true; - const glbData = await this.optimizeAvatarModel([this.object.arrayBuffer, this.object.srcUrl]); + const parseVrm = (arrayBuffer, srcUrl) => new Promise((accept, reject) => { + const { gltfLoader } = loaders; + gltfLoader.parse(arrayBuffer, srcUrl, object => { + accept(object); + }, reject); + }); + const object = await parseVrm(this.object.arrayBuffer, this.object.srcUrl); + + const glb = await avatarOptimizer.optimizeAvatarModel(object.scene); /* const parseVrm = (arrayBuffer, srcUrl) => new Promise((accept, reject) => { const { gltfLoader } = loaders; @@ -358,13 +366,13 @@ export class AvatarRenderer { ); }); */ - const glb = await new Promise((accept, reject) => { + /* const glb = await new Promise((accept, reject) => { const {gltfLoader} = loaders; gltfLoader.parse(glbData, this.object.srcUrl, object => { // window.o15 = object; accept(object.scene); }, reject); - }); + }); */ _bindSkeleton(glb, this.object); this.optimizedModel = glb; @@ -373,6 +381,8 @@ export class AvatarRenderer { // object.scene.updateMatrixWorld(); // this.scene.add(object.scene); + // window.glb = glb; + this.optimizedModel.updateMatrixWorld(); this.scene.add(this.optimizedModel); } From 9c526b427c5b2edbae84efa2f31866bc20d48861 Mon Sep 17 00:00:00 2001 From: Avaer Kazmer Date: Fri, 29 Jul 2022 12:01:08 -0400 Subject: [PATCH 57/57] Temporarily lock out avatar icon --- src/AvatarIcon.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/AvatarIcon.jsx b/src/AvatarIcon.jsx index 2d256c7fc0..fc3b7708cd 100644 --- a/src/AvatarIcon.jsx +++ b/src/AvatarIcon.jsx @@ -25,6 +25,8 @@ const CharacterIcon = () => { const canvasRef = useRef(); useEffect(() => { + return; + const canvas = canvasRef.current; if (canvas) { const localPlayer = playersManager.getLocalPlayer();