From 594ece8d6803a7ba639bbf80e2f232d7a35fdb33 Mon Sep 17 00:00:00 2001 From: 0b5vr <0b5vr@0b5vr.com> Date: Tue, 28 Dec 2021 18:57:57 +0900 Subject: [PATCH] feature: add `VRMUtils.removeUnnecessaryVertices` To address the issue that morph textures consumes gigantic amount of VRAM See: https://github.com/mrdoob/three.js/issues/23095 --- packages/three-vrm/src/VRMUtils/index.ts | 2 + .../src/VRMUtils/removeUnnecessaryVertices.ts | 151 ++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 packages/three-vrm/src/VRMUtils/removeUnnecessaryVertices.ts diff --git a/packages/three-vrm/src/VRMUtils/index.ts b/packages/three-vrm/src/VRMUtils/index.ts index fa9bb7816..78b52f207 100644 --- a/packages/three-vrm/src/VRMUtils/index.ts +++ b/packages/three-vrm/src/VRMUtils/index.ts @@ -1,5 +1,6 @@ import { deepDispose } from './deepDispose'; import { removeUnnecessaryJoints } from './removeUnnecessaryJoints'; +import { removeUnnecessaryVertices } from './removeUnnecessaryVertices'; import { rotateVRM0 } from './rotateVRM0'; export class VRMUtils { @@ -9,5 +10,6 @@ export class VRMUtils { public static deepDispose = deepDispose; public static removeUnnecessaryJoints = removeUnnecessaryJoints; + public static removeUnnecessaryVertices = removeUnnecessaryVertices; public static rotateVRM0 = rotateVRM0; } diff --git a/packages/three-vrm/src/VRMUtils/removeUnnecessaryVertices.ts b/packages/three-vrm/src/VRMUtils/removeUnnecessaryVertices.ts new file mode 100644 index 000000000..9d852db58 --- /dev/null +++ b/packages/three-vrm/src/VRMUtils/removeUnnecessaryVertices.ts @@ -0,0 +1,151 @@ +import * as THREE from 'three'; +import { BufferAttribute } from 'three'; + +/** + * Traverse given object and remove unnecessary vertices from every BufferGeometries. + * This only processes buffer geometries with index buffer. + * + * Three.js creates morph textures for each geometries and it sometimes consumes unnecessary amount of VRAM for certain models. + * This function will optimize geometries to reduce the size of morph texture. + * See: https://github.com/mrdoob/three.js/issues/23095 + * + * @param root Root object that will be traversed + */ +export function removeUnnecessaryVertices(root: THREE.Object3D): void { + const geometryMap = new Map(); + + // Traverse an entire tree + root.traverse((obj) => { + if (!(obj as any).isMesh) { + return; + } + + const mesh = obj as THREE.Mesh; + const geometry = mesh.geometry; + + // if the geometry does not have an index buffer it does not need to process + const origianlIndex = geometry.index; + if (origianlIndex == null) { + return; + } + + // skip already processed geometry + const newGeometryAlreadyExisted = geometryMap.get(geometry); + if (newGeometryAlreadyExisted != null) { + mesh.geometry = newGeometryAlreadyExisted; + return; + } + + const newGeometry = new THREE.BufferGeometry(); + + newGeometry.morphTargetsRelative = geometry.morphTargetsRelative; + newGeometry.setDrawRange(geometry.drawRange.start, geometry.drawRange.count); + geometry.groups.forEach((group) => { + newGeometry.addGroup(group.start, group.count, group.materialIndex); + }); + + geometryMap.set(geometry, newGeometry); + + /** from original index to new index */ + const originalIndexNewIndexMap: number[] = []; + + /** from new index to original index */ + const newIndexOriginalIndexMap: number[] = []; + + // reorganize indices + { + const originalIndexArray = origianlIndex.array; + const newIndexArray = new (originalIndexArray.constructor as any)(originalIndexArray.length); + + let indexHead = 0; + + for (let i = 0; i < originalIndexArray.length; i++) { + const originalIndex = originalIndexArray[i]; + + let newIndex = originalIndexNewIndexMap[originalIndex]; + if (newIndex == null) { + originalIndexNewIndexMap[originalIndex] = indexHead; + newIndexOriginalIndexMap[indexHead] = originalIndex; + newIndex = indexHead; + indexHead++; + } + newIndexArray[i] = newIndex; + } + + newGeometry.setIndex(new BufferAttribute(newIndexArray, 1, false)); + } + + // reorganize attributes + Object.keys(geometry.attributes).forEach((attributeName) => { + const originalAttribute = geometry.attributes[attributeName] as THREE.BufferAttribute; + + if ((originalAttribute as any).isInterleavedBufferAttribute) { + throw new Error('removeUnnecessaryVertices: InterlavedBufferAttribute is not supported'); + } + + const originalAttributeArray = originalAttribute.array; + const { itemSize, normalized } = originalAttribute; + + const newAttributeArray = new (originalAttributeArray.constructor as any)( + newIndexOriginalIndexMap.length * itemSize, + ); + + newIndexOriginalIndexMap.forEach((originalIndex, i) => { + for (let j = 0; j < itemSize; j++) { + newAttributeArray[i * itemSize + j] = originalAttributeArray[originalIndex * itemSize + j]; + } + }); + + newGeometry.setAttribute(attributeName, new BufferAttribute(newAttributeArray, itemSize, normalized)); + }); + + // reorganize morph attributes + /** True if all morphs are zero. */ + let isNullMorph = true; + + Object.keys(geometry.morphAttributes).forEach((attributeName) => { + newGeometry.morphAttributes[attributeName] = []; + + const morphs = geometry.morphAttributes[attributeName]; + for (let iMorph = 0; iMorph < morphs.length; iMorph++) { + const originalAttribute = morphs[iMorph] as THREE.BufferAttribute; + + if ((originalAttribute as any).isInterleavedBufferAttribute) { + throw new Error('removeUnnecessaryVertices: InterlavedBufferAttribute is not supported'); + } + + const originalAttributeArray = originalAttribute.array; + const { itemSize, normalized } = originalAttribute; + + const newAttributeArray = new (originalAttributeArray.constructor as any)( + newIndexOriginalIndexMap.length * itemSize, + ); + + newIndexOriginalIndexMap.forEach((originalIndex, i) => { + for (let j = 0; j < itemSize; j++) { + newAttributeArray[i * itemSize + j] = originalAttributeArray[originalIndex * itemSize + j]; + } + }); + + isNullMorph = isNullMorph && newAttributeArray.every((v: number) => v === 0); + + newGeometry.morphAttributes[attributeName][iMorph] = new BufferAttribute( + newAttributeArray, + itemSize, + normalized, + ); + } + }); + + // If all morphs are zero, just discard the morph attributes we've just made + if (isNullMorph) { + newGeometry.morphAttributes = {}; + } + + mesh.geometry = newGeometry; + }); + + Array.from(geometryMap.keys()).forEach((originalGeometry) => { + originalGeometry.dispose(); + }); +}