diff --git a/packages/three-vrm-core/src/expressions/VRMExpressionLoaderPlugin.ts b/packages/three-vrm-core/src/expressions/VRMExpressionLoaderPlugin.ts index 1e8fdb16b..f98969306 100644 --- a/packages/three-vrm-core/src/expressions/VRMExpressionLoaderPlugin.ts +++ b/packages/three-vrm-core/src/expressions/VRMExpressionLoaderPlugin.ts @@ -202,6 +202,7 @@ export class VRMExpressionLoaderPlugin implements GLTFLoaderPlugin { material, type: bind.type, targetValue: new THREE.Color().fromArray(bind.targetValue), + targetAlpha: bind.targetValue[3], }), ); }); @@ -394,7 +395,8 @@ export class VRMExpressionLoaderPlugin implements GLTFLoaderPlugin { new VRMExpressionMaterialColorBind({ material, type: materialColorType, - targetValue: new THREE.Color(...materialValue.targetValue!.slice(0, 3)), + targetValue: new THREE.Color().fromArray(materialValue.targetValue!), + targetAlpha: materialValue.targetValue![3], }), ); diff --git a/packages/three-vrm-core/src/expressions/VRMExpressionMaterialColorBind.ts b/packages/three-vrm-core/src/expressions/VRMExpressionMaterialColorBind.ts index 0b60f41b6..9d428dad0 100644 --- a/packages/three-vrm-core/src/expressions/VRMExpressionMaterialColorBind.ts +++ b/packages/three-vrm-core/src/expressions/VRMExpressionMaterialColorBind.ts @@ -4,30 +4,51 @@ import type { VRMExpressionMaterialColorType } from './VRMExpressionMaterialColo const _color = new THREE.Color(); +interface ColorBindState { + propertyName: string; + initialValue: THREE.Color; + deltaValue: THREE.Color; +} + +interface AlphaBindState { + propertyName: string; + initialValue: number; + deltaValue: number; +} + +interface BindState { + color: ColorBindState | null; + alpha: AlphaBindState | null; +} + /** * A bind of expression influences to a material color. */ export class VRMExpressionMaterialColorBind implements VRMExpressionBind { /** * Mapping of property names from VRMC/materialColorBinds.type to three.js/Material. + * The first element stands for color channels, the second element stands for the alpha channel. + * The second element can be null if the target property doesn't exist. */ + // TODO: We might want to use the `satisfies` operator once we bump TS to 4.9 or higher + // See: https://github.com/pixiv/three-vrm/pull/1323#discussion_r1374020035 private static _propertyNameMapMap: { - [distinguisher: string]: { [type in VRMExpressionMaterialColorType]?: string }; + [distinguisher: string]: { [type in VRMExpressionMaterialColorType]?: readonly [string, string | null] }; } = { isMeshStandardMaterial: { - color: 'color', - emissionColor: 'emissive', + color: ['color', 'opacity'], + emissionColor: ['emissive', null], }, isMeshBasicMaterial: { - color: 'color', + color: ['color', 'opacity'], }, isMToonMaterial: { - color: 'color', - emissionColor: 'emissive', - outlineColor: 'outlineColorFactor', - matcapColor: 'matcapFactor', - rimColor: 'parametricRimColorFactor', - shadeColor: 'shadeColorFactor', + color: ['color', 'opacity'], + emissionColor: ['emissive', null], + outlineColor: ['outlineColorFactor', null], + matcapColor: ['matcapFactor', null], + rimColor: ['parametricRimColorFactor', null], + shadeColor: ['shadeColorFactor', null], }, }; @@ -47,19 +68,21 @@ export class VRMExpressionMaterialColorBind implements VRMExpressionBind { public readonly targetValue: THREE.Color; /** - * Its state. - * If it cannot find the target property in constructor, it will be null instead. + * The target alpha. */ - private _state: { - propertyName: string; - initialValue: THREE.Color; - deltaValue: THREE.Color; - } | null; + public readonly targetAlpha: number; + + /** + * Its binding state. + * If it cannot find the target property in the constructor, each property will be null instead. + */ + private _state: BindState; public constructor({ material, type, targetValue, + targetAlpha, }: { /** * The target material. @@ -75,18 +98,72 @@ export class VRMExpressionMaterialColorBind implements VRMExpressionBind { * The target color. */ targetValue: THREE.Color; + + /** + * The target alpha. + */ + targetAlpha?: number; }) { this.material = material; this.type = type; this.targetValue = targetValue; + this.targetAlpha = targetAlpha ?? 1.0; + + // init bind state + const color = this._initColorBindState(); + const alpha = this._initAlphaBindState(); + this._state = { color, alpha }; + } + + public applyWeight(weight: number): void { + const { color, alpha } = this._state; + + if (color != null) { + const { propertyName, deltaValue } = color; + + const target = (this.material as any)[propertyName] as THREE.Color; + if (target != undefined) { + target.add(_color.copy(deltaValue).multiplyScalar(weight)); + } + } + + if (alpha != null) { + const { propertyName, deltaValue } = alpha; + + const target = (this.material as any)[propertyName] as number; + if (target != undefined) { + ((this.material as any)[propertyName] as number) += deltaValue * weight; + } + } + } + + public clearAppliedWeight(): void { + const { color, alpha } = this._state; + + if (color != null) { + const { propertyName, initialValue } = color; + + const target = (this.material as any)[propertyName] as THREE.Color; + if (target != undefined) { + target.copy(initialValue); + } + } + + if (alpha != null) { + const { propertyName, initialValue } = alpha; + + const target = (this.material as any)[propertyName] as number; + if (target != undefined) { + ((this.material as any)[propertyName] as number) = initialValue; + } + } + } + + private _initColorBindState(): ColorBindState | null { + const { material, type, targetValue } = this; - // init property name - const propertyNameMap = Object.entries(VRMExpressionMaterialColorBind._propertyNameMapMap).find( - ([distinguisher]) => { - return (material as any)[distinguisher] === true; - }, - )?.[1]; - const propertyName = propertyNameMap?.[type] ?? null; + const propertyNameMap = this._getPropertyNameMap(); + const propertyName = propertyNameMap?.[type]?.[0] ?? null; if (propertyName == null) { console.warn( @@ -95,64 +172,57 @@ export class VRMExpressionMaterialColorBind implements VRMExpressionBind { }, the type ${type} but the material or the type is not supported.`, ); - this._state = null; - } else { - const target = (material as any)[propertyName] as THREE.Color; + return null; + } - const initialValue = target.clone(); + const target = (material as any)[propertyName] as THREE.Color; - // 負の値を保持するためにColor.subを使わずに差分を計算する - const deltaValue = new THREE.Color( - targetValue.r - initialValue.r, - targetValue.g - initialValue.g, - targetValue.b - initialValue.b, - ); + const initialValue = target.clone(); - this._state = { - propertyName, - initialValue, - deltaValue, - }; - } - } + // 負の値を保持するためにColor.subを使わずに差分を計算する + const deltaValue = new THREE.Color( + targetValue.r - initialValue.r, + targetValue.g - initialValue.g, + targetValue.b - initialValue.b, + ); - public applyWeight(weight: number): void { - if (this._state == null) { - // warning is already emitted in constructor - return; - } + return { propertyName, initialValue, deltaValue }; + } - const { propertyName, deltaValue } = this._state; + private _initAlphaBindState(): AlphaBindState | null { + const { material, type, targetAlpha } = this; - const target = (this.material as any)[propertyName] as THREE.Color; - if (target === undefined) { - return; - } // TODO: we should kick this at `addMaterialValue` + const propertyNameMap = this._getPropertyNameMap(); + const propertyName = propertyNameMap?.[type]?.[1] ?? null; - target.add(_color.copy(deltaValue).multiplyScalar(weight)); + if (propertyName == null && targetAlpha !== 1.0) { + console.warn( + `Tried to add a material alpha bind to the material ${ + material.name ?? '(no name)' + }, the type ${type} but the material or the type does not support alpha.`, + ); - if (typeof (this.material as any).shouldApplyUniforms === 'boolean') { - (this.material as any).shouldApplyUniforms = true; + return null; } - } - public clearAppliedWeight(): void { - if (this._state == null) { - // warning is already emitted in constructor - return; + if (propertyName == null) { + return null; } - const { propertyName, initialValue } = this._state; + const initialValue = (material as any)[propertyName] as number; - const target = (this.material as any)[propertyName] as THREE.Color; - if (target === undefined) { - return; - } // TODO: we should kick this at `addMaterialValue` + const deltaValue = targetAlpha - initialValue; - target.copy(initialValue); + return { propertyName, initialValue, deltaValue }; + } - if (typeof (this.material as any).shouldApplyUniforms === 'boolean') { - (this.material as any).shouldApplyUniforms = true; - } + private _getPropertyNameMap(): + | { [type in VRMExpressionMaterialColorType]?: readonly [string, string | null] } + | null { + return ( + Object.entries(VRMExpressionMaterialColorBind._propertyNameMapMap).find(([distinguisher]) => { + return (this.material as any)[distinguisher] === true; + })?.[1] ?? null + ); } }