From 3913e8156ca0364ddcf6d823f681ef1886f03daa Mon Sep 17 00:00:00 2001 From: 0b5vr <0b5vr@0b5vr.com> Date: Fri, 6 Sep 2024 15:57:30 +0900 Subject: [PATCH 1/4] change: modify behavior of expressions according the recent discussion It's about interaction between isBinary and override expressions See the discussion: https://github.com/pixiv/three-vrm/issues/1484 Also add a test of VRMExpression to confirm the desired behavior --- .../src/expressions/VRMExpression.ts | 35 +++- .../expressions/tests/VRMExpression.test.ts | 191 ++++++++++++++++++ 2 files changed, 219 insertions(+), 7 deletions(-) create mode 100644 packages/three-vrm-core/src/expressions/tests/VRMExpression.test.ts diff --git a/packages/three-vrm-core/src/expressions/VRMExpression.ts b/packages/three-vrm-core/src/expressions/VRMExpression.ts index 509d6faf2..66302813b 100644 --- a/packages/three-vrm-core/src/expressions/VRMExpression.ts +++ b/packages/three-vrm-core/src/expressions/VRMExpression.ts @@ -1,6 +1,7 @@ import * as THREE from 'three'; import { VRMExpressionBind } from './VRMExpressionBind'; import type { VRMExpressionOverrideType } from './VRMExpressionOverrideType'; +import type { VRMExpressionManager } from './VRMExpressionManager'; // animationMixer の監視対象は、Scene の中に入っている必要がある。 // そのため、表示オブジェクトではないけれど、Object3D を継承して Scene に投入できるようにする。 @@ -13,6 +14,10 @@ export class VRMExpression extends THREE.Object3D { /** * The current weight of the expression. + * + * You usually want to set the weight via {@link VRMExpressionManager.setValue}. + * + * It might also be controlled by the Three.js animation system. */ public weight = 0.0; @@ -46,9 +51,9 @@ export class VRMExpression extends THREE.Object3D { */ public get overrideBlinkAmount(): number { if (this.overrideBlink === 'block') { - return 0.0 < this.weight ? 1.0 : 0.0; + return 0.0 < this.outputWeight ? 1.0 : 0.0; } else if (this.overrideBlink === 'blend') { - return this.weight; + return this.outputWeight; } else { return 0.0; } @@ -60,9 +65,9 @@ export class VRMExpression extends THREE.Object3D { */ public get overrideLookAtAmount(): number { if (this.overrideLookAt === 'block') { - return 0.0 < this.weight ? 1.0 : 0.0; + return 0.0 < this.outputWeight ? 1.0 : 0.0; } else if (this.overrideLookAt === 'blend') { - return this.weight; + return this.outputWeight; } else { return 0.0; } @@ -74,14 +79,25 @@ export class VRMExpression extends THREE.Object3D { */ public get overrideMouthAmount(): number { if (this.overrideMouth === 'block') { - return 0.0 < this.weight ? 1.0 : 0.0; + return 0.0 < this.outputWeight ? 1.0 : 0.0; } else if (this.overrideMouth === 'blend') { - return this.weight; + return this.outputWeight; } else { return 0.0; } } + /** + * An output weight of this expression, considering the {@link isBinary}. + */ + public get outputWeight(): number { + if (this.isBinary) { + return this.weight >= 0.5 ? 1.0 : 0.0; + } + + return this.weight; + } + constructor(expressionName: string) { super(); @@ -112,9 +128,14 @@ export class VRMExpression extends THREE.Object3D { */ multiplier?: number; }): void { - let actualWeight = this.isBinary ? (this.weight <= 0.5 ? 0.0 : 1.0) : this.weight; + let actualWeight = this.outputWeight; actualWeight *= options?.multiplier ?? 1.0; + // if the expression is binary, the override value must be also treated as binary + if (this.isBinary && actualWeight < 1.0) { + actualWeight = 0.0; + } + this._binds.forEach((bind) => bind.applyWeight(actualWeight)); } diff --git a/packages/three-vrm-core/src/expressions/tests/VRMExpression.test.ts b/packages/three-vrm-core/src/expressions/tests/VRMExpression.test.ts new file mode 100644 index 000000000..3cd92341d --- /dev/null +++ b/packages/three-vrm-core/src/expressions/tests/VRMExpression.test.ts @@ -0,0 +1,191 @@ +import * as THREE from 'three'; +import { VRMExpression } from '../VRMExpression'; +import { VRMExpressionBind } from '../VRMExpressionBind'; + +class VRMExpressionMockBind implements VRMExpressionBind { + public weight = 0.0; + + public applyWeight(weight: number): void { + this.weight += weight; + } + + public clearAppliedWeight(): void { + this.weight = 0.0; + } +} + +describe('VRMExpression', () => { + let expression: VRMExpression; + + beforeEach(() => { + expression = new VRMExpression('aa'); + }); + + describe('outputWeight', () => { + it('returns the weight if the expression is not binary', () => { + expression.weight = 0.64; + expect(expression.outputWeight).toBe(0.64); + }); + + it('returns 0.0 if the expression is binary and the weight is less than 0.5', () => { + expression.isBinary = true; + expression.weight = 0.3; + expect(expression.outputWeight).toBe(0.0); + }); + + it('returns 1.0 if the expression is binary and the weight is more than 0.5', () => { + expression.isBinary = true; + expression.weight = 0.7; + expect(expression.outputWeight).toBe(1.0); + }); + + it('returns 1.0 if the expression is binary and the weight is exactly 0.5', () => { + expression.isBinary = true; + expression.weight = 0.5; + expect(expression.outputWeight).toBe(1.0); + }); + }); + + describe('overrideBlinkAmount', () => { + it('returns 0.0 when the overrideBlink is none', () => { + expression.overrideBlink = 'none'; + expression.weight = 0.75; + expect(expression.overrideBlinkAmount).toBe(0.0); + }); + + it('returns the override amount when the overrideBlink is blend', () => { + expression.overrideBlink = 'blend'; + expression.weight = 0.75; + expect(expression.overrideBlinkAmount).toBe(0.75); + }); + + it('returns 1.0 when the overrideBlink is block and the weight is not zero', () => { + expression.overrideBlink = 'block'; + expression.weight = 0.1; + expect(expression.overrideBlinkAmount).toBe(1.0); + }); + + it('returns 0.0 when the overrideBlink is block and the weight is exactly zero', () => { + expression.overrideBlink = 'block'; + expression.weight = 0.0; + expect(expression.overrideBlinkAmount).toBe(0.0); + }); + + it('returns 0.0 when the expression is binary, the overrideBlink is blend, and the weight is less than 0.5', () => { + expression.overrideBlink = 'blend'; + expression.isBinary = true; + expression.weight = 0.3; + expect(expression.overrideBlinkAmount).toBe(0.0); + }); + + it('returns 1.0 when the expression is binary, the overrideBlink is blend, and the weight is more than 0.5', () => { + expression.overrideBlink = 'blend'; + expression.isBinary = true; + expression.weight = 0.7; + expect(expression.overrideBlinkAmount).toBe(1.0); + }); + + it('returns 0.0 when the expression is binary, the overrideBlink is block, and the weight is less than 0.5', () => { + expression.overrideBlink = 'block'; + expression.isBinary = true; + expression.weight = 0.3; + expect(expression.overrideBlinkAmount).toBe(0.0); + }); + + it('returns 1.0 when the expression is binary, the overrideBlink is block, and the weight is more than 0.5', () => { + expression.overrideBlink = 'block'; + expression.isBinary = true; + expression.weight = 0.7; + expect(expression.overrideBlinkAmount).toBe(1.0); + }); + }); + + describe('applyWeight', () => { + it('applies the weight to the binds', () => { + const bind1 = new VRMExpressionMockBind(); + const bind2 = new VRMExpressionMockBind(); + expression.addBind(bind1); + expression.addBind(bind2); + + expression.weight = 0.64; + expression.applyWeight(); + + expect(bind1.weight).toBe(0.64); + expect(bind2.weight).toBe(0.64); + }); + + it('applies the 0.0 if the expression is binary and the weight is less than 0.5', () => { + expression.isBinary = true; + + const bind = new VRMExpressionMockBind(); + expression.addBind(bind); + + expression.weight = 0.3; + expression.applyWeight(); + + expect(bind.weight).toBe(0.0); + }); + + it('applies the 1.0 if the expression is binary and the weight is more than 0.5', () => { + expression.isBinary = true; + + const bind = new VRMExpressionMockBind(); + expression.addBind(bind); + + expression.weight = 0.7; + expression.applyWeight(); + + expect(bind.weight).toBe(1.0); + }); + + it('applies the 1.0 if the expression is binary and the weight is exactly 0.5', () => { + expression.isBinary = true; + + const bind = new VRMExpressionMockBind(); + expression.addBind(bind); + + expression.weight = 0.5; + expression.applyWeight(); + + expect(bind.weight).toBe(1.0); + }); + + it('applies the weight with the override multiplier', () => { + const bind = new VRMExpressionMockBind(); + expression.addBind(bind); + + expression.weight = 0.75; + expression.applyWeight({ multiplier: 0.5 }); + + expect(bind.weight).toBe(0.375); + }); + + it('applies the 0.0 if the expression is binary and the override multiplier is less than 1.0', () => { + expression.isBinary = true; + + const bind = new VRMExpressionMockBind(); + expression.addBind(bind); + + expression.weight = 0.7; + expression.applyWeight({ multiplier: 0.99 }); + + expect(bind.weight).toBe(0.0); + }); + }); + + describe('clearAppliedWeight', () => { + it('clears the applied weight from the binds', () => { + const bind1 = new VRMExpressionMockBind(); + const bind2 = new VRMExpressionMockBind(); + bind1.applyWeight(0.82); + bind2.applyWeight(0.48); + expression.addBind(bind1); + expression.addBind(bind2); + + expression.clearAppliedWeight(); + + expect(bind1.weight).toBe(0.0); + expect(bind2.weight).toBe(0.0); + }); + }); +}); From 5afd3fc57807f033e2b85d618e785e9fd985733f Mon Sep 17 00:00:00 2001 From: 0b5vr <0b5vr@0b5vr.com> Date: Mon, 21 Oct 2024 17:31:04 +0900 Subject: [PATCH 2/4] fix: expressions, update isBinary behavior according to the spec I will update test cases in the next commit See: https://github.com/vrm-c/vrm-specification/blob/master/specification/VRMC_vrm-1.0/expressions.md#expression-specification The suggestion: https://github.com/pixiv/three-vrm/pull/1489 Co-authored-by: ke456-png <108649297+ke456-png@users.noreply.github.com> --- packages/three-vrm-core/src/expressions/VRMExpression.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/three-vrm-core/src/expressions/VRMExpression.ts b/packages/three-vrm-core/src/expressions/VRMExpression.ts index 66302813b..4dfbd5706 100644 --- a/packages/three-vrm-core/src/expressions/VRMExpression.ts +++ b/packages/three-vrm-core/src/expressions/VRMExpression.ts @@ -92,7 +92,7 @@ export class VRMExpression extends THREE.Object3D { */ public get outputWeight(): number { if (this.isBinary) { - return this.weight >= 0.5 ? 1.0 : 0.0; + return this.weight > 0.5 ? 1.0 : 0.0; } return this.weight; From abe19fdeca9a4e38b5d558035f6341f73365971e Mon Sep 17 00:00:00 2001 From: 0b5vr <0b5vr@0b5vr.com> Date: Mon, 21 Oct 2024 17:31:51 +0900 Subject: [PATCH 3/4] test: expressions, update test according to the spec See: https://github.com/vrm-c/vrm-specification/blob/master/specification/VRMC_vrm-1.0/expressions.md#expression-specification The suggestion: https://github.com/pixiv/three-vrm/pull/1489 --- .../src/expressions/tests/VRMExpression.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/three-vrm-core/src/expressions/tests/VRMExpression.test.ts b/packages/three-vrm-core/src/expressions/tests/VRMExpression.test.ts index 3cd92341d..be7e2a314 100644 --- a/packages/three-vrm-core/src/expressions/tests/VRMExpression.test.ts +++ b/packages/three-vrm-core/src/expressions/tests/VRMExpression.test.ts @@ -39,10 +39,10 @@ describe('VRMExpression', () => { expect(expression.outputWeight).toBe(1.0); }); - it('returns 1.0 if the expression is binary and the weight is exactly 0.5', () => { + it('returns 0.0 if the expression is binary and the weight is exactly 0.5', () => { expression.isBinary = true; expression.weight = 0.5; - expect(expression.outputWeight).toBe(1.0); + expect(expression.outputWeight).toBe(0.0); }); }); @@ -138,7 +138,7 @@ describe('VRMExpression', () => { expect(bind.weight).toBe(1.0); }); - it('applies the 1.0 if the expression is binary and the weight is exactly 0.5', () => { + it('applies the 0.0 if the expression is binary and the weight is exactly 0.5', () => { expression.isBinary = true; const bind = new VRMExpressionMockBind(); @@ -147,7 +147,7 @@ describe('VRMExpression', () => { expression.weight = 0.5; expression.applyWeight(); - expect(bind.weight).toBe(1.0); + expect(bind.weight).toBe(0.0); }); it('applies the weight with the override multiplier', () => { From 68a10ad9f5abb0322fe69186ac183d3612d0431a Mon Sep 17 00:00:00 2001 From: 0b5vr <0b5vr@0b5vr.com> Date: Mon, 21 Oct 2024 17:32:15 +0900 Subject: [PATCH 4/4] refactor: trivial unused code cleanup --- .../three-vrm-core/src/expressions/tests/VRMExpression.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/three-vrm-core/src/expressions/tests/VRMExpression.test.ts b/packages/three-vrm-core/src/expressions/tests/VRMExpression.test.ts index be7e2a314..1b4af5a04 100644 --- a/packages/three-vrm-core/src/expressions/tests/VRMExpression.test.ts +++ b/packages/three-vrm-core/src/expressions/tests/VRMExpression.test.ts @@ -1,4 +1,3 @@ -import * as THREE from 'three'; import { VRMExpression } from '../VRMExpression'; import { VRMExpressionBind } from '../VRMExpressionBind';