Skip to content

Commit 17cea8b

Browse files
authoredDec 4, 2024··
Merge pull request #1546 from pixiv/combine-morphs
VRMUtils.combineMorphs
2 parents b35bc21 + e5c8643 commit 17cea8b

17 files changed

+206
-1
lines changed
 

‎packages/three-vrm-core/src/expressions/VRMExpression.ts

+27
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,18 @@ export class VRMExpression extends THREE.Object3D {
4141
*/
4242
public overrideMouth: VRMExpressionOverrideType = 'none';
4343

44+
/**
45+
* Binds that this expression influences.
46+
*/
4447
private _binds: VRMExpressionBind[] = [];
4548

49+
/**
50+
* Binds that this expression influences.
51+
*/
52+
public get binds(): readonly VRMExpressionBind[] {
53+
return this._binds;
54+
}
55+
4656
override readonly type: string | 'VRMExpression';
4757

4858
/**
@@ -112,10 +122,27 @@ export class VRMExpression extends THREE.Object3D {
112122
this.visible = false;
113123
}
114124

125+
/**
126+
* Add an expression bind to the expression.
127+
*
128+
* @param bind A bind to add
129+
*/
115130
public addBind(bind: VRMExpressionBind): void {
116131
this._binds.push(bind);
117132
}
118133

134+
/**
135+
* Delete an expression bind from the expression.
136+
*
137+
* @param bind A bind to delete
138+
*/
139+
public deleteBind(bind: VRMExpressionBind): void {
140+
const index = this._binds.indexOf(bind);
141+
if (index >= 0) {
142+
this._binds.splice(index, 1);
143+
}
144+
}
145+
119146
/**
120147
* Apply weight to every assigned blend shapes.
121148
* Should be called every frame.

‎packages/three-vrm/examples/animations.html

+1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
// calling these functions greatly improves the performance
8383
VRMUtils.removeUnnecessaryVertices( gltf.scene );
8484
VRMUtils.combineSkeletons( gltf.scene );
85+
VRMUtils.combineMorphs( vrm );
8586

8687
// Disable frustum culling
8788
vrm.scene.traverse( ( obj ) => {

‎packages/three-vrm/examples/basic.html

+1
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
// calling these functions greatly improves the performance
8484
VRMUtils.removeUnnecessaryVertices( gltf.scene );
8585
VRMUtils.combineSkeletons( gltf.scene );
86+
VRMUtils.combineMorphs( vrm );
8687

8788
// Disable frustum culling
8889
vrm.scene.traverse( ( obj ) => {

‎packages/three-vrm/examples/bones.html

+1
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
// calling these functions greatly improves the performance
8282
VRMUtils.removeUnnecessaryVertices( gltf.scene );
8383
VRMUtils.combineSkeletons( gltf.scene );
84+
VRMUtils.combineMorphs( vrm );
8485

8586
// Disable frustum culling
8687
vrm.scene.traverse( ( obj ) => {

‎packages/three-vrm/examples/debug.html

+1
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
// calling this function greatly improves the performance
9090
VRMUtils.removeUnnecessaryVertices( gltf.scene );
9191
VRMUtils.combineSkeletons( gltf.scene );
92+
VRMUtils.combineMorphs( vrm );
9293

9394
// Disable frustum culling
9495
vrm.scene.traverse( ( obj ) => {

‎packages/three-vrm/examples/dnd.html

+1
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
// calling these functions greatly improves the performance
8484
VRMUtils.removeUnnecessaryVertices( gltf.scene );
8585
VRMUtils.combineSkeletons( gltf.scene );
86+
VRMUtils.combineMorphs( vrm );
8687

8788
if ( currentVrm ) {
8889

‎packages/three-vrm/examples/expressions.html

+2-1
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,11 @@
7777
( gltf ) => {
7878

7979
const vrm = gltf.userData.vrm;
80-
80+
8181
// calling these functions greatly improves the performance
8282
VRMUtils.removeUnnecessaryVertices( gltf.scene );
8383
VRMUtils.combineSkeletons( gltf.scene );
84+
VRMUtils.combineMorphs( vrm );
8485

8586
// Disable frustum culling
8687
vrm.scene.traverse( ( obj ) => {

‎packages/three-vrm/examples/firstperson.html

+1
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
// calling these functions greatly improves the performance
8282
VRMUtils.removeUnnecessaryVertices( gltf.scene );
8383
VRMUtils.combineSkeletons( gltf.scene );
84+
VRMUtils.combineMorphs( vrm );
8485

8586
// Disable frustum culling
8687
vrm.scene.traverse( ( obj ) => {

‎packages/three-vrm/examples/humanoidAnimation/main.js

+5
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ function loadVRM( modelUrl ) {
6262

6363
const vrm = gltf.userData.vrm;
6464

65+
// calling this function greatly improves the performance
66+
VRMUtils.removeUnnecessaryVertices( gltf.scene );
67+
VRMUtils.combineSkeletons( gltf.scene );
68+
VRMUtils.combineMorphs( vrm );
69+
6570
if ( currentVrm ) {
6671

6772
scene.remove( currentVrm.scene );

‎packages/three-vrm/examples/lookat-advanced.html

+1
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@
152152
// calling these functions greatly improves the performance
153153
VRMUtils.removeUnnecessaryVertices( gltf.scene );
154154
VRMUtils.combineSkeletons( gltf.scene );
155+
VRMUtils.combineMorphs( vrm );
155156

156157
// Disable frustum culling
157158
vrm.scene.traverse( ( obj ) => {

‎packages/three-vrm/examples/lookat.html

+1
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
// calling these functions greatly improves the performance
8686
VRMUtils.removeUnnecessaryVertices( gltf.scene );
8787
VRMUtils.combineSkeletons( gltf.scene );
88+
VRMUtils.combineMorphs( vrm );
8889

8990
// Disable frustum culling
9091
vrm.scene.traverse( ( obj ) => {

‎packages/three-vrm/examples/materials-debug.html

+1
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
// calling these functions greatly improves the performance
8282
VRMUtils.removeUnnecessaryVertices( gltf.scene );
8383
VRMUtils.combineSkeletons( gltf.scene );
84+
VRMUtils.combineMorphs( vrm );
8485

8586
// Disable frustum culling
8687
vrm.scene.traverse( ( obj ) => {

‎packages/three-vrm/examples/meta.html

+1
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
// calling these functions greatly improves the performance
9999
VRMUtils.removeUnnecessaryVertices( gltf.scene );
100100
VRMUtils.combineSkeletons( gltf.scene );
101+
VRMUtils.combineMorphs( vrm );
101102

102103
// Disable frustum culling
103104
vrm.scene.traverse( ( obj ) => {

‎packages/three-vrm/examples/mouse.html

+1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
// calling these functions greatly improves the performance
7777
VRMUtils.removeUnnecessaryVertices( gltf.scene );
7878
VRMUtils.combineSkeletons( gltf.scene );
79+
VRMUtils.combineMorphs( vrm );
7980

8081
// Disable frustum culling
8182
vrm.scene.traverse( ( obj ) => {

‎packages/three-vrm/examples/webgpu-dnd.html

+1
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@
9999
// calling these functions greatly improves the performance
100100
VRMUtils.removeUnnecessaryVertices( gltf.scene );
101101
VRMUtils.combineSkeletons( gltf.scene );
102+
VRMUtils.combineMorphs( vrm );
102103

103104
if ( currentVrm ) {
104105

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import * as THREE from 'three';
2+
import { VRMCore, VRMExpressionMorphTargetBind } from '@pixiv/three-vrm-core';
3+
4+
/**
5+
* Traverse an entire tree and collect meshes.
6+
*/
7+
function collectMeshes(scene: THREE.Group): Set<THREE.Mesh> {
8+
const meshes = new Set<THREE.Mesh>();
9+
10+
scene.traverse((obj) => {
11+
if (!(obj as any).isMesh) {
12+
return;
13+
}
14+
15+
const mesh = obj as THREE.Mesh;
16+
meshes.add(mesh);
17+
});
18+
19+
return meshes;
20+
}
21+
22+
function combineMorph(
23+
positionAttributes: (THREE.BufferAttribute | THREE.InterleavedBufferAttribute)[],
24+
binds: Iterable<VRMExpressionMorphTargetBind>,
25+
morphTargetsRelative: boolean,
26+
): THREE.BufferAttribute {
27+
const newArray = new Float32Array(positionAttributes[0].count * 3);
28+
let weightSum = 0.0;
29+
30+
if (morphTargetsRelative) {
31+
weightSum = 1.0;
32+
} else {
33+
for (const bind of binds) {
34+
weightSum += bind.weight;
35+
}
36+
}
37+
38+
for (const bind of binds) {
39+
const src = positionAttributes[bind.index];
40+
const weight = bind.weight / weightSum;
41+
42+
for (let i = 0; i < src.count; i++) {
43+
newArray[i * 3 + 0] += src.getX(i) * weight;
44+
newArray[i * 3 + 1] += src.getY(i) * weight;
45+
newArray[i * 3 + 2] += src.getZ(i) * weight;
46+
}
47+
}
48+
49+
const newAttribute = new THREE.BufferAttribute(newArray, 3);
50+
return newAttribute;
51+
}
52+
53+
/**
54+
* A map from expression names to a set of morph target binds.
55+
*/
56+
type NameBindSetMap = Map<string, Set<VRMExpressionMorphTargetBind>>;
57+
58+
/**
59+
* Combine morph targets by VRM expressions.
60+
*
61+
* This function prevents crashes caused by the limitation of the number of morph targets, especially on mobile devices.
62+
*
63+
* @param vrm The VRM instance
64+
*/
65+
export function combineMorphs(vrm: VRMCore): void {
66+
const meshes = collectMeshes(vrm.scene);
67+
68+
// Iterate over all expressions and check which morph targets are used
69+
const meshNameBindSetMapMap = new Map<THREE.Mesh, NameBindSetMap>();
70+
71+
const expressionMap = vrm.expressionManager?.expressionMap;
72+
if (expressionMap != null) {
73+
for (const [expressionName, expression] of Object.entries(expressionMap)) {
74+
const bindsToDeleteSet = new Set<VRMExpressionMorphTargetBind>();
75+
for (const bind of expression.binds) {
76+
if (bind instanceof VRMExpressionMorphTargetBind) {
77+
if (bind.weight !== 0.0) {
78+
for (const mesh of bind.primitives) {
79+
let nameBindSetMap = meshNameBindSetMapMap.get(mesh);
80+
if (nameBindSetMap == null) {
81+
nameBindSetMap = new Map();
82+
meshNameBindSetMapMap.set(mesh, nameBindSetMap);
83+
}
84+
85+
let bindSet = nameBindSetMap.get(expressionName);
86+
if (bindSet == null) {
87+
bindSet = new Set();
88+
nameBindSetMap.set(expressionName, bindSet);
89+
}
90+
91+
bindSet.add(bind);
92+
}
93+
}
94+
bindsToDeleteSet.add(bind);
95+
}
96+
}
97+
98+
for (const bind of bindsToDeleteSet) {
99+
expression.deleteBind(bind);
100+
}
101+
}
102+
}
103+
104+
// Combine morphs
105+
for (const mesh of meshes) {
106+
const nameBindSetMap = meshNameBindSetMapMap.get(mesh);
107+
if (nameBindSetMap == null) {
108+
continue;
109+
}
110+
111+
const geometry = mesh.geometry.clone();
112+
mesh.geometry = geometry;
113+
const morphTargetsRelative = geometry.morphTargetsRelative;
114+
115+
const hasPMorph = geometry.morphAttributes.position != null;
116+
const hasNMorph = geometry.morphAttributes.normal != null;
117+
118+
const morphAttributes: typeof geometry.morphAttributes = {};
119+
const morphTargetDictionary: typeof mesh.morphTargetDictionary = {};
120+
const morphTargetInfluences: typeof mesh.morphTargetInfluences = [];
121+
122+
if (hasPMorph || hasNMorph) {
123+
if (hasPMorph) {
124+
morphAttributes.position = [];
125+
}
126+
if (hasNMorph) {
127+
morphAttributes.normal = [];
128+
}
129+
130+
let i = 0;
131+
for (const [name, bindSet] of nameBindSetMap) {
132+
if (hasPMorph) {
133+
morphAttributes.position[i] = combineMorph(geometry.morphAttributes.position, bindSet, morphTargetsRelative);
134+
}
135+
if (hasNMorph) {
136+
morphAttributes.normal[i] = combineMorph(geometry.morphAttributes.normal, bindSet, morphTargetsRelative);
137+
}
138+
139+
expressionMap?.[name].addBind(
140+
new VRMExpressionMorphTargetBind({
141+
index: i,
142+
weight: 1.0,
143+
primitives: [mesh],
144+
}),
145+
);
146+
147+
morphTargetDictionary[name] = i;
148+
morphTargetInfluences.push(0.0);
149+
150+
i++;
151+
}
152+
}
153+
154+
geometry.morphAttributes = morphAttributes;
155+
mesh.morphTargetDictionary = morphTargetDictionary;
156+
mesh.morphTargetInfluences = morphTargetInfluences;
157+
}
158+
}

‎packages/three-vrm/src/VRMUtils/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { combineMorphs } from './combineMorphs';
12
import { combineSkeletons } from './combineSkeletons';
23
import { deepDispose } from './deepDispose';
34
import { removeUnnecessaryJoints } from './removeUnnecessaryJoints';
@@ -9,6 +10,7 @@ export class VRMUtils {
910
// this class is not meant to be instantiated
1011
}
1112

13+
public static combineMorphs = combineMorphs;
1214
public static combineSkeletons = combineSkeletons;
1315
public static deepDispose = deepDispose;
1416
public static removeUnnecessaryJoints = removeUnnecessaryJoints;

0 commit comments

Comments
 (0)
Please sign in to comment.