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();