Skip to content

Commit

Permalink
Merge pull request #1323 from pixiv/fix-material-bind-color
Browse files Browse the repository at this point in the history
fix (three-vrm-core): Fix expressions material bind to respect alpha
  • Loading branch information
0b5vr authored Nov 2, 2023
2 parents 484bdfa + 2dded77 commit f64ffb5
Show file tree
Hide file tree
Showing 2 changed files with 141 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ export class VRMExpressionLoaderPlugin implements GLTFLoaderPlugin {
material,
type: bind.type,
targetValue: new THREE.Color().fromArray(bind.targetValue),
targetAlpha: bind.targetValue[3],
}),
);
});
Expand Down Expand Up @@ -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],
}),
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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],
},
};

Expand All @@ -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.
Expand All @@ -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(
Expand All @@ -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
);
}
}

0 comments on commit f64ffb5

Please sign in to comment.