1739 lines
63 KiB
JavaScript
1739 lines
63 KiB
JavaScript
import { Ai as Object3D, Cn as GLBufferAttribute, Cs as Vector2, Gr as MathUtils, Jr as Matrix4, Nn as Group, So as SkinnedMesh, at as Color, bo as Skeleton, ki as NumberKeyframeTrack, oa as Quaternion, sa as QuaternionKeyframeTrack, un as Euler, ws as Vector3, y as AnimationClip } from "./three.module-C9LqGydR.js";
|
|
//#region node_modules/@pixiv/three-vrm-animation/lib/three-vrm-animation.module.js
|
|
/*!
|
|
* @pixiv/three-vrm-animation v3.4.1
|
|
* The implementation of VRM Animation
|
|
*
|
|
* Copyright (c) 2019-2025 pixiv Inc.
|
|
* @pixiv/three-vrm-animation is distributed under MIT License
|
|
* https://github.com/pixiv/three-vrm/blob/release/LICENSE
|
|
*/
|
|
var __async = (__this, __arguments, generator) => {
|
|
return new Promise((resolve, reject) => {
|
|
var fulfilled = (value) => {
|
|
try {
|
|
step(generator.next(value));
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
};
|
|
var rejected = (value) => {
|
|
try {
|
|
step(generator.throw(value));
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
};
|
|
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
|
|
step((generator = generator.apply(__this, __arguments)).next());
|
|
});
|
|
};
|
|
var __async2 = (__this, __arguments, generator) => {
|
|
return new Promise((resolve, reject) => {
|
|
var fulfilled = (value) => {
|
|
try {
|
|
step(generator.next(value));
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
};
|
|
var rejected = (value) => {
|
|
try {
|
|
step(generator.throw(value));
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
};
|
|
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
|
|
step((generator = generator.apply(__this, __arguments)).next());
|
|
});
|
|
};
|
|
var VRMExpression = class extends Object3D {
|
|
constructor(expressionName) {
|
|
super();
|
|
this.weight = 0;
|
|
this.isBinary = false;
|
|
this.overrideBlink = "none";
|
|
this.overrideLookAt = "none";
|
|
this.overrideMouth = "none";
|
|
this._binds = [];
|
|
this.name = `VRMExpression_${expressionName}`;
|
|
this.expressionName = expressionName;
|
|
this.type = "VRMExpression";
|
|
this.visible = false;
|
|
}
|
|
/**
|
|
* Binds that this expression influences.
|
|
*/
|
|
get binds() {
|
|
return this._binds;
|
|
}
|
|
/**
|
|
* A value represents how much it should override blink expressions.
|
|
* `0.0` == no override at all, `1.0` == completely block the expressions.
|
|
*/
|
|
get overrideBlinkAmount() {
|
|
if (this.overrideBlink === "block") return 0 < this.outputWeight ? 1 : 0;
|
|
else if (this.overrideBlink === "blend") return this.outputWeight;
|
|
else return 0;
|
|
}
|
|
/**
|
|
* A value represents how much it should override lookAt expressions.
|
|
* `0.0` == no override at all, `1.0` == completely block the expressions.
|
|
*/
|
|
get overrideLookAtAmount() {
|
|
if (this.overrideLookAt === "block") return 0 < this.outputWeight ? 1 : 0;
|
|
else if (this.overrideLookAt === "blend") return this.outputWeight;
|
|
else return 0;
|
|
}
|
|
/**
|
|
* A value represents how much it should override mouth expressions.
|
|
* `0.0` == no override at all, `1.0` == completely block the expressions.
|
|
*/
|
|
get overrideMouthAmount() {
|
|
if (this.overrideMouth === "block") return 0 < this.outputWeight ? 1 : 0;
|
|
else if (this.overrideMouth === "blend") return this.outputWeight;
|
|
else return 0;
|
|
}
|
|
/**
|
|
* An output weight of this expression, considering the {@link isBinary}.
|
|
*/
|
|
get outputWeight() {
|
|
if (this.isBinary) return this.weight > .5 ? 1 : 0;
|
|
return this.weight;
|
|
}
|
|
/**
|
|
* Add an expression bind to the expression.
|
|
*
|
|
* @param bind A bind to add
|
|
*/
|
|
addBind(bind) {
|
|
this._binds.push(bind);
|
|
}
|
|
/**
|
|
* Delete an expression bind from the expression.
|
|
*
|
|
* @param bind A bind to delete
|
|
*/
|
|
deleteBind(bind) {
|
|
const index = this._binds.indexOf(bind);
|
|
if (index >= 0) this._binds.splice(index, 1);
|
|
}
|
|
/**
|
|
* Apply weight to every assigned blend shapes.
|
|
* Should be called every frame.
|
|
*/
|
|
applyWeight(options) {
|
|
var _a;
|
|
let actualWeight = this.outputWeight;
|
|
actualWeight *= (_a = options == null ? void 0 : options.multiplier) != null ? _a : 1;
|
|
if (this.isBinary && actualWeight < 1) actualWeight = 0;
|
|
this._binds.forEach((bind) => bind.applyWeight(actualWeight));
|
|
}
|
|
/**
|
|
* Clear previously assigned blend shapes.
|
|
*/
|
|
clearAppliedWeight() {
|
|
this._binds.forEach((bind) => bind.clearAppliedWeight());
|
|
}
|
|
};
|
|
function extractPrimitivesInternal(gltf, nodeIndex, node) {
|
|
var _a, _b;
|
|
const json = gltf.parser.json;
|
|
const schemaNode = (_a = json.nodes) == null ? void 0 : _a[nodeIndex];
|
|
if (schemaNode == null) {
|
|
console.warn(`extractPrimitivesInternal: Attempt to use nodes[${nodeIndex}] of glTF but the node doesn't exist`);
|
|
return null;
|
|
}
|
|
const meshIndex = schemaNode.mesh;
|
|
if (meshIndex == null) return null;
|
|
const schemaMesh = (_b = json.meshes) == null ? void 0 : _b[meshIndex];
|
|
if (schemaMesh == null) {
|
|
console.warn(`extractPrimitivesInternal: Attempt to use meshes[${meshIndex}] of glTF but the mesh doesn't exist`);
|
|
return null;
|
|
}
|
|
const primitiveCount = schemaMesh.primitives.length;
|
|
const primitives = [];
|
|
node.traverse((object) => {
|
|
if (primitives.length < primitiveCount) {
|
|
if (object.isMesh) primitives.push(object);
|
|
}
|
|
});
|
|
return primitives;
|
|
}
|
|
function gltfExtractPrimitivesFromNode(gltf, nodeIndex) {
|
|
return __async2(this, null, function* () {
|
|
return extractPrimitivesInternal(gltf, nodeIndex, yield gltf.parser.getDependency("node", nodeIndex));
|
|
});
|
|
}
|
|
var VRMExpressionPresetName = {
|
|
Aa: "aa",
|
|
Ih: "ih",
|
|
Ou: "ou",
|
|
Ee: "ee",
|
|
Oh: "oh",
|
|
Blink: "blink",
|
|
Happy: "happy",
|
|
Angry: "angry",
|
|
Sad: "sad",
|
|
Relaxed: "relaxed",
|
|
LookUp: "lookUp",
|
|
Surprised: "surprised",
|
|
LookDown: "lookDown",
|
|
LookLeft: "lookLeft",
|
|
LookRight: "lookRight",
|
|
BlinkLeft: "blinkLeft",
|
|
BlinkRight: "blinkRight",
|
|
Neutral: "neutral"
|
|
};
|
|
function saturate(value) {
|
|
return Math.max(Math.min(value, 1), 0);
|
|
}
|
|
var VRMExpressionManager = class _VRMExpressionManager {
|
|
/**
|
|
* Create a new {@link VRMExpressionManager}.
|
|
*/
|
|
constructor() {
|
|
this.blinkExpressionNames = [
|
|
"blink",
|
|
"blinkLeft",
|
|
"blinkRight"
|
|
];
|
|
this.lookAtExpressionNames = [
|
|
"lookLeft",
|
|
"lookRight",
|
|
"lookUp",
|
|
"lookDown"
|
|
];
|
|
this.mouthExpressionNames = [
|
|
"aa",
|
|
"ee",
|
|
"ih",
|
|
"oh",
|
|
"ou"
|
|
];
|
|
this._expressions = [];
|
|
this._expressionMap = {};
|
|
}
|
|
get expressions() {
|
|
return this._expressions.concat();
|
|
}
|
|
get expressionMap() {
|
|
return Object.assign({}, this._expressionMap);
|
|
}
|
|
/**
|
|
* A map from name to expression, but excluding custom expressions.
|
|
*/
|
|
get presetExpressionMap() {
|
|
const result = {};
|
|
const presetNameSet = new Set(Object.values(VRMExpressionPresetName));
|
|
Object.entries(this._expressionMap).forEach(([name, expression]) => {
|
|
if (presetNameSet.has(name)) result[name] = expression;
|
|
});
|
|
return result;
|
|
}
|
|
/**
|
|
* A map from name to expression, but excluding preset expressions.
|
|
*/
|
|
get customExpressionMap() {
|
|
const result = {};
|
|
const presetNameSet = new Set(Object.values(VRMExpressionPresetName));
|
|
Object.entries(this._expressionMap).forEach(([name, expression]) => {
|
|
if (!presetNameSet.has(name)) result[name] = expression;
|
|
});
|
|
return result;
|
|
}
|
|
/**
|
|
* Copy the given {@link VRMExpressionManager} into this one.
|
|
* @param source The {@link VRMExpressionManager} you want to copy
|
|
* @returns this
|
|
*/
|
|
copy(source) {
|
|
this._expressions.concat().forEach((expression) => {
|
|
this.unregisterExpression(expression);
|
|
});
|
|
source._expressions.forEach((expression) => {
|
|
this.registerExpression(expression);
|
|
});
|
|
this.blinkExpressionNames = source.blinkExpressionNames.concat();
|
|
this.lookAtExpressionNames = source.lookAtExpressionNames.concat();
|
|
this.mouthExpressionNames = source.mouthExpressionNames.concat();
|
|
return this;
|
|
}
|
|
/**
|
|
* Returns a clone of this {@link VRMExpressionManager}.
|
|
* @returns Copied {@link VRMExpressionManager}
|
|
*/
|
|
clone() {
|
|
return new _VRMExpressionManager().copy(this);
|
|
}
|
|
/**
|
|
* Return a registered expression.
|
|
* If it cannot find an expression, it will return `null` instead.
|
|
*
|
|
* @param name Name or preset name of the expression
|
|
*/
|
|
getExpression(name) {
|
|
var _a;
|
|
return (_a = this._expressionMap[name]) != null ? _a : null;
|
|
}
|
|
/**
|
|
* Register an expression.
|
|
*
|
|
* @param expression {@link VRMExpression} that describes the expression
|
|
*/
|
|
registerExpression(expression) {
|
|
this._expressions.push(expression);
|
|
this._expressionMap[expression.expressionName] = expression;
|
|
}
|
|
/**
|
|
* Unregister an expression.
|
|
*
|
|
* @param expression The expression you want to unregister
|
|
*/
|
|
unregisterExpression(expression) {
|
|
const index = this._expressions.indexOf(expression);
|
|
if (index === -1) console.warn("VRMExpressionManager: The specified expressions is not registered");
|
|
this._expressions.splice(index, 1);
|
|
delete this._expressionMap[expression.expressionName];
|
|
}
|
|
/**
|
|
* Get the current weight of the specified expression.
|
|
* If it doesn't have an expression of given name, it will return `null` instead.
|
|
*
|
|
* @param name Name of the expression
|
|
*/
|
|
getValue(name) {
|
|
var _a;
|
|
const expression = this.getExpression(name);
|
|
return (_a = expression == null ? void 0 : expression.weight) != null ? _a : null;
|
|
}
|
|
/**
|
|
* Set a weight to the specified expression.
|
|
*
|
|
* @param name Name of the expression
|
|
* @param weight Weight
|
|
*/
|
|
setValue(name, weight) {
|
|
const expression = this.getExpression(name);
|
|
if (expression) expression.weight = saturate(weight);
|
|
}
|
|
/**
|
|
* Reset weights of all expressions to `0.0`.
|
|
*/
|
|
resetValues() {
|
|
this._expressions.forEach((expression) => {
|
|
expression.weight = 0;
|
|
});
|
|
}
|
|
/**
|
|
* Get a track name of specified expression.
|
|
* This track name is needed to manipulate its expression via keyframe animations.
|
|
*
|
|
* @example Manipulate an expression using keyframe animation
|
|
* ```js
|
|
* const trackName = vrm.expressionManager.getExpressionTrackName( 'blink' );
|
|
* const track = new THREE.NumberKeyframeTrack(
|
|
* name,
|
|
* [ 0.0, 0.5, 1.0 ], // times
|
|
* [ 0.0, 1.0, 0.0 ] // values
|
|
* );
|
|
*
|
|
* const clip = new THREE.AnimationClip(
|
|
* 'blink', // name
|
|
* 1.0, // duration
|
|
* [ track ] // tracks
|
|
* );
|
|
*
|
|
* const mixer = new THREE.AnimationMixer( vrm.scene );
|
|
* const action = mixer.clipAction( clip );
|
|
* action.play();
|
|
* ```
|
|
*
|
|
* @param name Name of the expression
|
|
*/
|
|
getExpressionTrackName(name) {
|
|
const expression = this.getExpression(name);
|
|
return expression ? `${expression.name}.weight` : null;
|
|
}
|
|
/**
|
|
* Update every expressions.
|
|
*/
|
|
update() {
|
|
const weightMultipliers = this._calculateWeightMultipliers();
|
|
this._expressions.forEach((expression) => {
|
|
expression.clearAppliedWeight();
|
|
});
|
|
this._expressions.forEach((expression) => {
|
|
let multiplier = 1;
|
|
const name = expression.expressionName;
|
|
if (this.blinkExpressionNames.indexOf(name) !== -1) multiplier *= weightMultipliers.blink;
|
|
if (this.lookAtExpressionNames.indexOf(name) !== -1) multiplier *= weightMultipliers.lookAt;
|
|
if (this.mouthExpressionNames.indexOf(name) !== -1) multiplier *= weightMultipliers.mouth;
|
|
expression.applyWeight({ multiplier });
|
|
});
|
|
}
|
|
/**
|
|
* Calculate sum of override amounts to see how much we should multiply weights of certain expressions.
|
|
*/
|
|
_calculateWeightMultipliers() {
|
|
let blink = 1;
|
|
let lookAt = 1;
|
|
let mouth = 1;
|
|
this._expressions.forEach((expression) => {
|
|
blink -= expression.overrideBlinkAmount;
|
|
lookAt -= expression.overrideLookAtAmount;
|
|
mouth -= expression.overrideMouthAmount;
|
|
});
|
|
blink = Math.max(0, blink);
|
|
lookAt = Math.max(0, lookAt);
|
|
mouth = Math.max(0, mouth);
|
|
return {
|
|
blink,
|
|
lookAt,
|
|
mouth
|
|
};
|
|
}
|
|
};
|
|
var VRMExpressionMaterialColorType = {
|
|
Color: "color",
|
|
EmissionColor: "emissionColor",
|
|
ShadeColor: "shadeColor",
|
|
MatcapColor: "matcapColor",
|
|
RimColor: "rimColor",
|
|
OutlineColor: "outlineColor"
|
|
};
|
|
var v0ExpressionMaterialColorMap = {
|
|
_Color: VRMExpressionMaterialColorType.Color,
|
|
_EmissionColor: VRMExpressionMaterialColorType.EmissionColor,
|
|
_ShadeColor: VRMExpressionMaterialColorType.ShadeColor,
|
|
_RimColor: VRMExpressionMaterialColorType.RimColor,
|
|
_OutlineColor: VRMExpressionMaterialColorType.OutlineColor
|
|
};
|
|
var _color = new Color();
|
|
var _VRMExpressionMaterialColorBind = class _VRMExpressionMaterialColorBind2 {
|
|
constructor({ material, type, targetValue, targetAlpha }) {
|
|
this.material = material;
|
|
this.type = type;
|
|
this.targetValue = targetValue;
|
|
this.targetAlpha = targetAlpha != null ? targetAlpha : 1;
|
|
const color = this._initColorBindState();
|
|
const alpha = this._initAlphaBindState();
|
|
this._state = {
|
|
color,
|
|
alpha
|
|
};
|
|
}
|
|
applyWeight(weight) {
|
|
const { color, alpha } = this._state;
|
|
if (color != null) {
|
|
const { propertyName, deltaValue } = color;
|
|
const target = this.material[propertyName];
|
|
if (target != void 0) target.add(_color.copy(deltaValue).multiplyScalar(weight));
|
|
}
|
|
if (alpha != null) {
|
|
const { propertyName, deltaValue } = alpha;
|
|
if (this.material[propertyName] != void 0) this.material[propertyName] += deltaValue * weight;
|
|
}
|
|
}
|
|
clearAppliedWeight() {
|
|
const { color, alpha } = this._state;
|
|
if (color != null) {
|
|
const { propertyName, initialValue } = color;
|
|
const target = this.material[propertyName];
|
|
if (target != void 0) target.copy(initialValue);
|
|
}
|
|
if (alpha != null) {
|
|
const { propertyName, initialValue } = alpha;
|
|
if (this.material[propertyName] != void 0) this.material[propertyName] = initialValue;
|
|
}
|
|
}
|
|
_initColorBindState() {
|
|
var _a, _b, _c;
|
|
const { material, type, targetValue } = this;
|
|
const propertyNameMap = this._getPropertyNameMap();
|
|
const propertyName = (_b = (_a = propertyNameMap == null ? void 0 : propertyNameMap[type]) == null ? void 0 : _a[0]) != null ? _b : null;
|
|
if (propertyName == null) {
|
|
console.warn(`Tried to add a material color bind to the material ${(_c = material.name) != null ? _c : "(no name)"}, the type ${type} but the material or the type is not supported.`);
|
|
return null;
|
|
}
|
|
const initialValue = material[propertyName].clone();
|
|
return {
|
|
propertyName,
|
|
initialValue,
|
|
deltaValue: new Color(targetValue.r - initialValue.r, targetValue.g - initialValue.g, targetValue.b - initialValue.b)
|
|
};
|
|
}
|
|
_initAlphaBindState() {
|
|
var _a, _b, _c;
|
|
const { material, type, targetAlpha } = this;
|
|
const propertyNameMap = this._getPropertyNameMap();
|
|
const propertyName = (_b = (_a = propertyNameMap == null ? void 0 : propertyNameMap[type]) == null ? void 0 : _a[1]) != null ? _b : null;
|
|
if (propertyName == null && targetAlpha !== 1) {
|
|
console.warn(`Tried to add a material alpha bind to the material ${(_c = material.name) != null ? _c : "(no name)"}, the type ${type} but the material or the type does not support alpha.`);
|
|
return null;
|
|
}
|
|
if (propertyName == null) return null;
|
|
const initialValue = material[propertyName];
|
|
return {
|
|
propertyName,
|
|
initialValue,
|
|
deltaValue: targetAlpha - initialValue
|
|
};
|
|
}
|
|
_getPropertyNameMap() {
|
|
var _a, _b;
|
|
return (_b = (_a = Object.entries(_VRMExpressionMaterialColorBind2._propertyNameMapMap).find(([distinguisher]) => {
|
|
return this.material[distinguisher] === true;
|
|
})) == null ? void 0 : _a[1]) != null ? _b : null;
|
|
}
|
|
};
|
|
_VRMExpressionMaterialColorBind._propertyNameMapMap = {
|
|
isMeshStandardMaterial: {
|
|
color: ["color", "opacity"],
|
|
emissionColor: ["emissive", null]
|
|
},
|
|
isMeshBasicMaterial: { color: ["color", "opacity"] },
|
|
isMToonMaterial: {
|
|
color: ["color", "opacity"],
|
|
emissionColor: ["emissive", null],
|
|
outlineColor: ["outlineColorFactor", null],
|
|
matcapColor: ["matcapFactor", null],
|
|
rimColor: ["parametricRimColorFactor", null],
|
|
shadeColor: ["shadeColorFactor", null]
|
|
}
|
|
};
|
|
var VRMExpressionMaterialColorBind = _VRMExpressionMaterialColorBind;
|
|
var VRMExpressionMorphTargetBind = class {
|
|
constructor({ primitives, index, weight }) {
|
|
this.primitives = primitives;
|
|
this.index = index;
|
|
this.weight = weight;
|
|
}
|
|
applyWeight(weight) {
|
|
this.primitives.forEach((mesh) => {
|
|
var _a;
|
|
if (((_a = mesh.morphTargetInfluences) == null ? void 0 : _a[this.index]) != null) mesh.morphTargetInfluences[this.index] += this.weight * weight;
|
|
});
|
|
}
|
|
clearAppliedWeight() {
|
|
this.primitives.forEach((mesh) => {
|
|
var _a;
|
|
if (((_a = mesh.morphTargetInfluences) == null ? void 0 : _a[this.index]) != null) mesh.morphTargetInfluences[this.index] = 0;
|
|
});
|
|
}
|
|
};
|
|
var _v2 = new Vector2();
|
|
var _VRMExpressionTextureTransformBind = class _VRMExpressionTextureTransformBind2 {
|
|
constructor({ material, scale, offset }) {
|
|
var _a, _b;
|
|
this.material = material;
|
|
this.scale = scale;
|
|
this.offset = offset;
|
|
const propertyNames = (_a = Object.entries(_VRMExpressionTextureTransformBind2._propertyNamesMap).find(([distinguisher]) => {
|
|
return material[distinguisher] === true;
|
|
})) == null ? void 0 : _a[1];
|
|
if (propertyNames == null) {
|
|
console.warn(`Tried to add a texture transform bind to the material ${(_b = material.name) != null ? _b : "(no name)"} but the material is not supported.`);
|
|
this._properties = [];
|
|
} else {
|
|
this._properties = [];
|
|
propertyNames.forEach((propertyName) => {
|
|
var _a2;
|
|
const texture = (_a2 = material[propertyName]) == null ? void 0 : _a2.clone();
|
|
if (!texture) return null;
|
|
material[propertyName] = texture;
|
|
const initialOffset = texture.offset.clone();
|
|
const initialScale = texture.repeat.clone();
|
|
const deltaOffset = offset.clone().sub(initialOffset);
|
|
const deltaScale = scale.clone().sub(initialScale);
|
|
this._properties.push({
|
|
name: propertyName,
|
|
initialOffset,
|
|
deltaOffset,
|
|
initialScale,
|
|
deltaScale
|
|
});
|
|
});
|
|
}
|
|
}
|
|
applyWeight(weight) {
|
|
this._properties.forEach((property) => {
|
|
const target = this.material[property.name];
|
|
if (target === void 0) return;
|
|
target.offset.add(_v2.copy(property.deltaOffset).multiplyScalar(weight));
|
|
target.repeat.add(_v2.copy(property.deltaScale).multiplyScalar(weight));
|
|
});
|
|
}
|
|
clearAppliedWeight() {
|
|
this._properties.forEach((property) => {
|
|
const target = this.material[property.name];
|
|
if (target === void 0) return;
|
|
target.offset.copy(property.initialOffset);
|
|
target.repeat.copy(property.initialScale);
|
|
});
|
|
}
|
|
};
|
|
_VRMExpressionTextureTransformBind._propertyNamesMap = {
|
|
isMeshStandardMaterial: [
|
|
"map",
|
|
"emissiveMap",
|
|
"bumpMap",
|
|
"normalMap",
|
|
"displacementMap",
|
|
"roughnessMap",
|
|
"metalnessMap",
|
|
"alphaMap"
|
|
],
|
|
isMeshBasicMaterial: [
|
|
"map",
|
|
"specularMap",
|
|
"alphaMap"
|
|
],
|
|
isMToonMaterial: [
|
|
"map",
|
|
"normalMap",
|
|
"emissiveMap",
|
|
"shadeMultiplyTexture",
|
|
"rimMultiplyTexture",
|
|
"outlineWidthMultiplyTexture",
|
|
"uvAnimationMaskTexture"
|
|
]
|
|
};
|
|
var VRMExpressionTextureTransformBind = _VRMExpressionTextureTransformBind;
|
|
var POSSIBLE_SPEC_VERSIONS = /* @__PURE__ */ new Set(["1.0", "1.0-beta"]);
|
|
var _VRMExpressionLoaderPlugin = class _VRMExpressionLoaderPlugin2 {
|
|
get name() {
|
|
return "VRMExpressionLoaderPlugin";
|
|
}
|
|
constructor(parser) {
|
|
this.parser = parser;
|
|
}
|
|
afterRoot(gltf) {
|
|
return __async2(this, null, function* () {
|
|
gltf.userData.vrmExpressionManager = yield this._import(gltf);
|
|
});
|
|
}
|
|
/**
|
|
* Import a {@link VRMExpressionManager} from a VRM.
|
|
*
|
|
* @param gltf A parsed result of GLTF taken from GLTFLoader
|
|
*/
|
|
_import(gltf) {
|
|
return __async2(this, null, function* () {
|
|
const v1Result = yield this._v1Import(gltf);
|
|
if (v1Result) return v1Result;
|
|
const v0Result = yield this._v0Import(gltf);
|
|
if (v0Result) return v0Result;
|
|
return null;
|
|
});
|
|
}
|
|
_v1Import(gltf) {
|
|
return __async2(this, null, function* () {
|
|
var _a, _b;
|
|
const json = this.parser.json;
|
|
if (!(((_a = json.extensionsUsed) == null ? void 0 : _a.indexOf("VRMC_vrm")) !== -1)) return null;
|
|
const extension = (_b = json.extensions) == null ? void 0 : _b["VRMC_vrm"];
|
|
if (!extension) return null;
|
|
const specVersion = extension.specVersion;
|
|
if (!POSSIBLE_SPEC_VERSIONS.has(specVersion)) {
|
|
console.warn(`VRMExpressionLoaderPlugin: Unknown VRMC_vrm specVersion "${specVersion}"`);
|
|
return null;
|
|
}
|
|
const schemaExpressions = extension.expressions;
|
|
if (!schemaExpressions) return null;
|
|
const presetNameSet = new Set(Object.values(VRMExpressionPresetName));
|
|
const nameSchemaExpressionMap = /* @__PURE__ */ new Map();
|
|
if (schemaExpressions.preset != null) Object.entries(schemaExpressions.preset).forEach(([name, schemaExpression]) => {
|
|
if (schemaExpression == null) return;
|
|
if (!presetNameSet.has(name)) {
|
|
console.warn(`VRMExpressionLoaderPlugin: Unknown preset name "${name}" detected. Ignoring the expression`);
|
|
return;
|
|
}
|
|
nameSchemaExpressionMap.set(name, schemaExpression);
|
|
});
|
|
if (schemaExpressions.custom != null) Object.entries(schemaExpressions.custom).forEach(([name, schemaExpression]) => {
|
|
if (presetNameSet.has(name)) {
|
|
console.warn(`VRMExpressionLoaderPlugin: Custom expression cannot have preset name "${name}". Ignoring the expression`);
|
|
return;
|
|
}
|
|
nameSchemaExpressionMap.set(name, schemaExpression);
|
|
});
|
|
const manager = new VRMExpressionManager();
|
|
yield Promise.all(Array.from(nameSchemaExpressionMap.entries()).map((_0) => __async2(this, [_0], function* ([name, schemaExpression]) {
|
|
var _a2, _b2, _c, _d, _e, _f, _g;
|
|
const expression = new VRMExpression(name);
|
|
gltf.scene.add(expression);
|
|
expression.isBinary = (_a2 = schemaExpression.isBinary) != null ? _a2 : false;
|
|
expression.overrideBlink = (_b2 = schemaExpression.overrideBlink) != null ? _b2 : "none";
|
|
expression.overrideLookAt = (_c = schemaExpression.overrideLookAt) != null ? _c : "none";
|
|
expression.overrideMouth = (_d = schemaExpression.overrideMouth) != null ? _d : "none";
|
|
(_e = schemaExpression.morphTargetBinds) == null || _e.forEach((bind) => __async2(this, null, function* () {
|
|
var _a3;
|
|
if (bind.node === void 0 || bind.index === void 0) return;
|
|
const primitives = yield gltfExtractPrimitivesFromNode(gltf, bind.node);
|
|
const morphTargetIndex = bind.index;
|
|
if (!primitives.every((primitive) => Array.isArray(primitive.morphTargetInfluences) && morphTargetIndex < primitive.morphTargetInfluences.length)) {
|
|
console.warn(`VRMExpressionLoaderPlugin: ${schemaExpression.name} attempts to index morph #${morphTargetIndex} but not found.`);
|
|
return;
|
|
}
|
|
expression.addBind(new VRMExpressionMorphTargetBind({
|
|
primitives,
|
|
index: morphTargetIndex,
|
|
weight: (_a3 = bind.weight) != null ? _a3 : 1
|
|
}));
|
|
}));
|
|
if (schemaExpression.materialColorBinds || schemaExpression.textureTransformBinds) {
|
|
const gltfMaterials = [];
|
|
gltf.scene.traverse((object) => {
|
|
const material = object.material;
|
|
if (material) if (Array.isArray(material)) gltfMaterials.push(...material);
|
|
else gltfMaterials.push(material);
|
|
});
|
|
(_f = schemaExpression.materialColorBinds) == null || _f.forEach((bind) => __async2(this, null, function* () {
|
|
gltfMaterials.filter((material) => {
|
|
var _a3;
|
|
const materialIndex = (_a3 = this.parser.associations.get(material)) == null ? void 0 : _a3.materials;
|
|
return bind.material === materialIndex;
|
|
}).forEach((material) => {
|
|
expression.addBind(new VRMExpressionMaterialColorBind({
|
|
material,
|
|
type: bind.type,
|
|
targetValue: new Color().fromArray(bind.targetValue),
|
|
targetAlpha: bind.targetValue[3]
|
|
}));
|
|
});
|
|
}));
|
|
(_g = schemaExpression.textureTransformBinds) == null || _g.forEach((bind) => __async2(this, null, function* () {
|
|
gltfMaterials.filter((material) => {
|
|
var _a3;
|
|
const materialIndex = (_a3 = this.parser.associations.get(material)) == null ? void 0 : _a3.materials;
|
|
return bind.material === materialIndex;
|
|
}).forEach((material) => {
|
|
var _a3, _b3;
|
|
expression.addBind(new VRMExpressionTextureTransformBind({
|
|
material,
|
|
offset: new Vector2().fromArray((_a3 = bind.offset) != null ? _a3 : [0, 0]),
|
|
scale: new Vector2().fromArray((_b3 = bind.scale) != null ? _b3 : [1, 1])
|
|
}));
|
|
});
|
|
}));
|
|
}
|
|
manager.registerExpression(expression);
|
|
})));
|
|
return manager;
|
|
});
|
|
}
|
|
_v0Import(gltf) {
|
|
return __async2(this, null, function* () {
|
|
var _a;
|
|
const json = this.parser.json;
|
|
const vrmExt = (_a = json.extensions) == null ? void 0 : _a.VRM;
|
|
if (!vrmExt) return null;
|
|
const schemaBlendShape = vrmExt.blendShapeMaster;
|
|
if (!schemaBlendShape) return null;
|
|
const manager = new VRMExpressionManager();
|
|
const schemaBlendShapeGroups = schemaBlendShape.blendShapeGroups;
|
|
if (!schemaBlendShapeGroups) return manager;
|
|
const blendShapeNameSet = /* @__PURE__ */ new Set();
|
|
yield Promise.all(schemaBlendShapeGroups.map((schemaGroup) => __async2(this, null, function* () {
|
|
var _a2;
|
|
const v0PresetName = schemaGroup.presetName;
|
|
const v1PresetName = v0PresetName != null && _VRMExpressionLoaderPlugin2.v0v1PresetNameMap[v0PresetName] || null;
|
|
const name = v1PresetName != null ? v1PresetName : schemaGroup.name;
|
|
if (name == null) {
|
|
console.warn("VRMExpressionLoaderPlugin: One of custom expressions has no name. Ignoring the expression");
|
|
return;
|
|
}
|
|
if (blendShapeNameSet.has(name)) {
|
|
console.warn(`VRMExpressionLoaderPlugin: An expression preset ${v0PresetName} has duplicated entries. Ignoring the expression`);
|
|
return;
|
|
}
|
|
blendShapeNameSet.add(name);
|
|
const expression = new VRMExpression(name);
|
|
gltf.scene.add(expression);
|
|
expression.isBinary = (_a2 = schemaGroup.isBinary) != null ? _a2 : false;
|
|
if (schemaGroup.binds) schemaGroup.binds.forEach((bind) => __async2(this, null, function* () {
|
|
var _a3;
|
|
if (bind.mesh === void 0 || bind.index === void 0) return;
|
|
const nodesUsingMesh = [];
|
|
(_a3 = json.nodes) == null || _a3.forEach((node, i) => {
|
|
if (node.mesh === bind.mesh) nodesUsingMesh.push(i);
|
|
});
|
|
const morphTargetIndex = bind.index;
|
|
yield Promise.all(nodesUsingMesh.map((nodeIndex) => __async2(this, null, function* () {
|
|
var _a4;
|
|
const primitives = yield gltfExtractPrimitivesFromNode(gltf, nodeIndex);
|
|
if (!primitives.every((primitive) => Array.isArray(primitive.morphTargetInfluences) && morphTargetIndex < primitive.morphTargetInfluences.length)) {
|
|
console.warn(`VRMExpressionLoaderPlugin: ${schemaGroup.name} attempts to index ${morphTargetIndex}th morph but not found.`);
|
|
return;
|
|
}
|
|
expression.addBind(new VRMExpressionMorphTargetBind({
|
|
primitives,
|
|
index: morphTargetIndex,
|
|
weight: .01 * ((_a4 = bind.weight) != null ? _a4 : 100)
|
|
}));
|
|
})));
|
|
}));
|
|
const materialValues = schemaGroup.materialValues;
|
|
if (materialValues && materialValues.length !== 0) materialValues.forEach((materialValue) => {
|
|
if (materialValue.materialName === void 0 || materialValue.propertyName === void 0 || materialValue.targetValue === void 0) return;
|
|
const materials = [];
|
|
gltf.scene.traverse((object) => {
|
|
if (object.material) {
|
|
const material = object.material;
|
|
if (Array.isArray(material)) materials.push(...material.filter((mtl) => (mtl.name === materialValue.materialName || mtl.name === materialValue.materialName + " (Outline)") && materials.indexOf(mtl) === -1));
|
|
else if (material.name === materialValue.materialName && materials.indexOf(material) === -1) materials.push(material);
|
|
}
|
|
});
|
|
const materialPropertyName = materialValue.propertyName;
|
|
materials.forEach((material) => {
|
|
if (materialPropertyName === "_MainTex_ST") {
|
|
const scale = new Vector2(materialValue.targetValue[0], materialValue.targetValue[1]);
|
|
const offset = new Vector2(materialValue.targetValue[2], materialValue.targetValue[3]);
|
|
offset.y = 1 - offset.y - scale.y;
|
|
expression.addBind(new VRMExpressionTextureTransformBind({
|
|
material,
|
|
scale,
|
|
offset
|
|
}));
|
|
return;
|
|
}
|
|
const materialColorType = v0ExpressionMaterialColorMap[materialPropertyName];
|
|
if (materialColorType) {
|
|
expression.addBind(new VRMExpressionMaterialColorBind({
|
|
material,
|
|
type: materialColorType,
|
|
targetValue: new Color().fromArray(materialValue.targetValue),
|
|
targetAlpha: materialValue.targetValue[3]
|
|
}));
|
|
return;
|
|
}
|
|
console.warn(materialPropertyName + " is not supported");
|
|
});
|
|
});
|
|
manager.registerExpression(expression);
|
|
})));
|
|
return manager;
|
|
});
|
|
}
|
|
};
|
|
_VRMExpressionLoaderPlugin.v0v1PresetNameMap = {
|
|
a: "aa",
|
|
e: "ee",
|
|
i: "ih",
|
|
o: "oh",
|
|
u: "ou",
|
|
blink: "blink",
|
|
joy: "happy",
|
|
angry: "angry",
|
|
sorrow: "sad",
|
|
fun: "relaxed",
|
|
lookup: "lookUp",
|
|
lookdown: "lookDown",
|
|
lookleft: "lookLeft",
|
|
lookright: "lookRight",
|
|
blink_l: "blinkLeft",
|
|
blink_r: "blinkRight",
|
|
neutral: "neutral"
|
|
};
|
|
var _VRMFirstPerson = class _VRMFirstPerson2 {
|
|
/**
|
|
* Create a new VRMFirstPerson object.
|
|
*
|
|
* @param humanoid A {@link VRMHumanoid}
|
|
* @param meshAnnotations A {@link VRMFirstPersonMeshAnnotation}
|
|
*/
|
|
constructor(humanoid, meshAnnotations) {
|
|
this._firstPersonOnlyLayer = _VRMFirstPerson2.DEFAULT_FIRSTPERSON_ONLY_LAYER;
|
|
this._thirdPersonOnlyLayer = _VRMFirstPerson2.DEFAULT_THIRDPERSON_ONLY_LAYER;
|
|
this._initializedLayers = false;
|
|
this.humanoid = humanoid;
|
|
this.meshAnnotations = meshAnnotations;
|
|
}
|
|
/**
|
|
* Copy the given {@link VRMFirstPerson} into this one.
|
|
* {@link humanoid} must be same as the source one.
|
|
* @param source The {@link VRMFirstPerson} you want to copy
|
|
* @returns this
|
|
*/
|
|
copy(source) {
|
|
if (this.humanoid !== source.humanoid) throw new Error("VRMFirstPerson: humanoid must be same in order to copy");
|
|
this.meshAnnotations = source.meshAnnotations.map((annotation) => ({
|
|
meshes: annotation.meshes.concat(),
|
|
type: annotation.type
|
|
}));
|
|
return this;
|
|
}
|
|
/**
|
|
* Returns a clone of this {@link VRMFirstPerson}.
|
|
* @returns Copied {@link VRMFirstPerson}
|
|
*/
|
|
clone() {
|
|
return new _VRMFirstPerson2(this.humanoid, this.meshAnnotations).copy(this);
|
|
}
|
|
/**
|
|
* A camera layer represents `FirstPersonOnly` layer.
|
|
* Note that **you must call {@link setup} first before you use the layer feature** or it does not work properly.
|
|
*
|
|
* The value is {@link DEFAULT_FIRSTPERSON_ONLY_LAYER} by default but you can change the layer by specifying via {@link setup} if you prefer.
|
|
*
|
|
* @see https://vrm.dev/en/univrm/api/univrm_use_firstperson/
|
|
* @see https://threejs.org/docs/#api/en/core/Layers
|
|
*/
|
|
get firstPersonOnlyLayer() {
|
|
return this._firstPersonOnlyLayer;
|
|
}
|
|
/**
|
|
* A camera layer represents `ThirdPersonOnly` layer.
|
|
* Note that **you must call {@link setup} first before you use the layer feature** or it does not work properly.
|
|
*
|
|
* The value is {@link DEFAULT_THIRDPERSON_ONLY_LAYER} by default but you can change the layer by specifying via {@link setup} if you prefer.
|
|
*
|
|
* @see https://vrm.dev/en/univrm/api/univrm_use_firstperson/
|
|
* @see https://threejs.org/docs/#api/en/core/Layers
|
|
*/
|
|
get thirdPersonOnlyLayer() {
|
|
return this._thirdPersonOnlyLayer;
|
|
}
|
|
/**
|
|
* In this method, it assigns layers for every meshes based on mesh annotations.
|
|
* You must call this method first before you use the layer feature.
|
|
*
|
|
* This is an equivalent of [VRMFirstPerson.Setup](https://github.com/vrm-c/UniVRM/blob/73a5bd8fcddaa2a7a8735099a97e63c9db3e5ea0/Assets/VRM/Runtime/FirstPerson/VRMFirstPerson.cs#L295-L299) of the UniVRM.
|
|
*
|
|
* The `cameraLayer` parameter specifies which layer will be assigned for `FirstPersonOnly` / `ThirdPersonOnly`.
|
|
* In UniVRM, we specified those by naming each desired layer as `FIRSTPERSON_ONLY_LAYER` / `THIRDPERSON_ONLY_LAYER`
|
|
* but we are going to specify these layers at here since we are unable to name layers in Three.js.
|
|
*
|
|
* @param cameraLayer Specify which layer will be for `FirstPersonOnly` / `ThirdPersonOnly`.
|
|
*/
|
|
setup({ firstPersonOnlyLayer = _VRMFirstPerson2.DEFAULT_FIRSTPERSON_ONLY_LAYER, thirdPersonOnlyLayer = _VRMFirstPerson2.DEFAULT_THIRDPERSON_ONLY_LAYER } = {}) {
|
|
if (this._initializedLayers) return;
|
|
this._firstPersonOnlyLayer = firstPersonOnlyLayer;
|
|
this._thirdPersonOnlyLayer = thirdPersonOnlyLayer;
|
|
this.meshAnnotations.forEach((item) => {
|
|
item.meshes.forEach((mesh) => {
|
|
if (item.type === "firstPersonOnly") {
|
|
mesh.layers.set(this._firstPersonOnlyLayer);
|
|
mesh.traverse((child) => child.layers.set(this._firstPersonOnlyLayer));
|
|
} else if (item.type === "thirdPersonOnly") {
|
|
mesh.layers.set(this._thirdPersonOnlyLayer);
|
|
mesh.traverse((child) => child.layers.set(this._thirdPersonOnlyLayer));
|
|
} else if (item.type === "auto") this._createHeadlessModel(mesh);
|
|
});
|
|
});
|
|
this._initializedLayers = true;
|
|
}
|
|
_excludeTriangles(triangles, bws, skinIndex, exclude) {
|
|
let count = 0;
|
|
if (bws != null && bws.length > 0) for (let i = 0; i < triangles.length; i += 3) {
|
|
const a = triangles[i];
|
|
const b = triangles[i + 1];
|
|
const c = triangles[i + 2];
|
|
const bw0 = bws[a];
|
|
const skin0 = skinIndex[a];
|
|
if (bw0[0] > 0 && exclude.includes(skin0[0])) continue;
|
|
if (bw0[1] > 0 && exclude.includes(skin0[1])) continue;
|
|
if (bw0[2] > 0 && exclude.includes(skin0[2])) continue;
|
|
if (bw0[3] > 0 && exclude.includes(skin0[3])) continue;
|
|
const bw1 = bws[b];
|
|
const skin1 = skinIndex[b];
|
|
if (bw1[0] > 0 && exclude.includes(skin1[0])) continue;
|
|
if (bw1[1] > 0 && exclude.includes(skin1[1])) continue;
|
|
if (bw1[2] > 0 && exclude.includes(skin1[2])) continue;
|
|
if (bw1[3] > 0 && exclude.includes(skin1[3])) continue;
|
|
const bw2 = bws[c];
|
|
const skin2 = skinIndex[c];
|
|
if (bw2[0] > 0 && exclude.includes(skin2[0])) continue;
|
|
if (bw2[1] > 0 && exclude.includes(skin2[1])) continue;
|
|
if (bw2[2] > 0 && exclude.includes(skin2[2])) continue;
|
|
if (bw2[3] > 0 && exclude.includes(skin2[3])) continue;
|
|
triangles[count++] = a;
|
|
triangles[count++] = b;
|
|
triangles[count++] = c;
|
|
}
|
|
return count;
|
|
}
|
|
_createErasedMesh(src, erasingBonesIndex) {
|
|
const dst = new SkinnedMesh(src.geometry.clone(), src.material);
|
|
dst.name = `${src.name}(erase)`;
|
|
dst.frustumCulled = src.frustumCulled;
|
|
dst.layers.set(this._firstPersonOnlyLayer);
|
|
const geometry = dst.geometry;
|
|
const skinIndexAttr = geometry.getAttribute("skinIndex");
|
|
const skinIndexAttrArray = skinIndexAttr instanceof GLBufferAttribute ? [] : skinIndexAttr.array;
|
|
const skinIndex = [];
|
|
for (let i = 0; i < skinIndexAttrArray.length; i += 4) skinIndex.push([
|
|
skinIndexAttrArray[i],
|
|
skinIndexAttrArray[i + 1],
|
|
skinIndexAttrArray[i + 2],
|
|
skinIndexAttrArray[i + 3]
|
|
]);
|
|
const skinWeightAttr = geometry.getAttribute("skinWeight");
|
|
const skinWeightAttrArray = skinWeightAttr instanceof GLBufferAttribute ? [] : skinWeightAttr.array;
|
|
const skinWeight = [];
|
|
for (let i = 0; i < skinWeightAttrArray.length; i += 4) skinWeight.push([
|
|
skinWeightAttrArray[i],
|
|
skinWeightAttrArray[i + 1],
|
|
skinWeightAttrArray[i + 2],
|
|
skinWeightAttrArray[i + 3]
|
|
]);
|
|
const index = geometry.getIndex();
|
|
if (!index) throw new Error("The geometry doesn't have an index buffer");
|
|
const oldTriangles = Array.from(index.array);
|
|
const count = this._excludeTriangles(oldTriangles, skinWeight, skinIndex, erasingBonesIndex);
|
|
const newTriangle = [];
|
|
for (let i = 0; i < count; i++) newTriangle[i] = oldTriangles[i];
|
|
geometry.setIndex(newTriangle);
|
|
if (src.onBeforeRender) dst.onBeforeRender = src.onBeforeRender;
|
|
dst.bind(new Skeleton(src.skeleton.bones, src.skeleton.boneInverses), new Matrix4());
|
|
return dst;
|
|
}
|
|
_createHeadlessModelForSkinnedMesh(parent, mesh) {
|
|
const eraseBoneIndexes = [];
|
|
mesh.skeleton.bones.forEach((bone, index) => {
|
|
if (this._isEraseTarget(bone)) eraseBoneIndexes.push(index);
|
|
});
|
|
if (!eraseBoneIndexes.length) {
|
|
mesh.layers.enable(this._thirdPersonOnlyLayer);
|
|
mesh.layers.enable(this._firstPersonOnlyLayer);
|
|
return;
|
|
}
|
|
mesh.layers.set(this._thirdPersonOnlyLayer);
|
|
const newMesh = this._createErasedMesh(mesh, eraseBoneIndexes);
|
|
parent.add(newMesh);
|
|
}
|
|
_createHeadlessModel(node) {
|
|
if (node.type === "Group") {
|
|
node.layers.set(this._thirdPersonOnlyLayer);
|
|
if (this._isEraseTarget(node)) node.traverse((child) => child.layers.set(this._thirdPersonOnlyLayer));
|
|
else {
|
|
const parent = new Group();
|
|
parent.name = `_headless_${node.name}`;
|
|
parent.layers.set(this._firstPersonOnlyLayer);
|
|
node.parent.add(parent);
|
|
node.children.filter((child) => child.type === "SkinnedMesh").forEach((child) => {
|
|
const skinnedMesh = child;
|
|
this._createHeadlessModelForSkinnedMesh(parent, skinnedMesh);
|
|
});
|
|
}
|
|
} else if (node.type === "SkinnedMesh") {
|
|
const skinnedMesh = node;
|
|
this._createHeadlessModelForSkinnedMesh(node.parent, skinnedMesh);
|
|
} else if (this._isEraseTarget(node)) {
|
|
node.layers.set(this._thirdPersonOnlyLayer);
|
|
node.traverse((child) => child.layers.set(this._thirdPersonOnlyLayer));
|
|
}
|
|
}
|
|
_isEraseTarget(bone) {
|
|
if (bone === this.humanoid.getRawBoneNode("head")) return true;
|
|
else if (!bone.parent) return false;
|
|
else return this._isEraseTarget(bone.parent);
|
|
}
|
|
};
|
|
_VRMFirstPerson.DEFAULT_FIRSTPERSON_ONLY_LAYER = 9;
|
|
_VRMFirstPerson.DEFAULT_THIRDPERSON_ONLY_LAYER = 10;
|
|
new Vector3();
|
|
new Vector3();
|
|
new Quaternion();
|
|
var VRMHumanBoneParentMap = {
|
|
hips: null,
|
|
spine: "hips",
|
|
chest: "spine",
|
|
upperChest: "chest",
|
|
neck: "upperChest",
|
|
head: "neck",
|
|
leftEye: "head",
|
|
rightEye: "head",
|
|
jaw: "head",
|
|
leftUpperLeg: "hips",
|
|
leftLowerLeg: "leftUpperLeg",
|
|
leftFoot: "leftLowerLeg",
|
|
leftToes: "leftFoot",
|
|
rightUpperLeg: "hips",
|
|
rightLowerLeg: "rightUpperLeg",
|
|
rightFoot: "rightLowerLeg",
|
|
rightToes: "rightFoot",
|
|
leftShoulder: "upperChest",
|
|
leftUpperArm: "leftShoulder",
|
|
leftLowerArm: "leftUpperArm",
|
|
leftHand: "leftLowerArm",
|
|
rightShoulder: "upperChest",
|
|
rightUpperArm: "rightShoulder",
|
|
rightLowerArm: "rightUpperArm",
|
|
rightHand: "rightLowerArm",
|
|
leftThumbMetacarpal: "leftHand",
|
|
leftThumbProximal: "leftThumbMetacarpal",
|
|
leftThumbDistal: "leftThumbProximal",
|
|
leftIndexProximal: "leftHand",
|
|
leftIndexIntermediate: "leftIndexProximal",
|
|
leftIndexDistal: "leftIndexIntermediate",
|
|
leftMiddleProximal: "leftHand",
|
|
leftMiddleIntermediate: "leftMiddleProximal",
|
|
leftMiddleDistal: "leftMiddleIntermediate",
|
|
leftRingProximal: "leftHand",
|
|
leftRingIntermediate: "leftRingProximal",
|
|
leftRingDistal: "leftRingIntermediate",
|
|
leftLittleProximal: "leftHand",
|
|
leftLittleIntermediate: "leftLittleProximal",
|
|
leftLittleDistal: "leftLittleIntermediate",
|
|
rightThumbMetacarpal: "rightHand",
|
|
rightThumbProximal: "rightThumbMetacarpal",
|
|
rightThumbDistal: "rightThumbProximal",
|
|
rightIndexProximal: "rightHand",
|
|
rightIndexIntermediate: "rightIndexProximal",
|
|
rightIndexDistal: "rightIndexIntermediate",
|
|
rightMiddleProximal: "rightHand",
|
|
rightMiddleIntermediate: "rightMiddleProximal",
|
|
rightMiddleDistal: "rightMiddleIntermediate",
|
|
rightRingProximal: "rightHand",
|
|
rightRingIntermediate: "rightRingProximal",
|
|
rightRingDistal: "rightRingIntermediate",
|
|
rightLittleProximal: "rightHand",
|
|
rightLittleIntermediate: "rightLittleProximal",
|
|
rightLittleDistal: "rightLittleIntermediate"
|
|
};
|
|
function quatInvertCompat(target) {
|
|
if (target.invert) target.invert();
|
|
else target.inverse();
|
|
return target;
|
|
}
|
|
new Vector3();
|
|
new Quaternion();
|
|
new Vector3();
|
|
new Quaternion();
|
|
new Vector3();
|
|
new Quaternion();
|
|
new Quaternion();
|
|
new Vector3();
|
|
new Vector3();
|
|
var SQRT_2_OVER_2 = Math.sqrt(2) / 2;
|
|
new Quaternion(0, 0, -SQRT_2_OVER_2, SQRT_2_OVER_2);
|
|
new Vector3(0, 1, 0);
|
|
var _position = new Vector3();
|
|
var _scale = new Vector3();
|
|
function getWorldQuaternionLite(object, out) {
|
|
object.matrixWorld.decompose(_position, out, _scale);
|
|
return out;
|
|
}
|
|
function calcAzimuthAltitude(vector) {
|
|
return [Math.atan2(-vector.z, vector.x), Math.atan2(vector.y, Math.sqrt(vector.x * vector.x + vector.z * vector.z))];
|
|
}
|
|
function sanitizeAngle(angle) {
|
|
const roundTurn = Math.round(angle / 2 / Math.PI);
|
|
return angle - 2 * Math.PI * roundTurn;
|
|
}
|
|
var VEC3_POSITIVE_Z = new Vector3(0, 0, 1);
|
|
var _v3A5 = new Vector3();
|
|
var _v3B3 = new Vector3();
|
|
var _v3C = new Vector3();
|
|
var _quatA5 = new Quaternion();
|
|
var _quatB2 = new Quaternion();
|
|
var _quatC = new Quaternion();
|
|
var _quatD = new Quaternion();
|
|
var _eulerA = new Euler();
|
|
var _VRMLookAt = class _VRMLookAt2 {
|
|
/**
|
|
* Create a new {@link VRMLookAt}.
|
|
*
|
|
* @param humanoid A {@link VRMHumanoid}
|
|
* @param applier A {@link VRMLookAtApplier}
|
|
*/
|
|
constructor(humanoid, applier) {
|
|
this.offsetFromHeadBone = new Vector3();
|
|
this.autoUpdate = true;
|
|
this.faceFront = new Vector3(0, 0, 1);
|
|
this.humanoid = humanoid;
|
|
this.applier = applier;
|
|
this._yaw = 0;
|
|
this._pitch = 0;
|
|
this._needsUpdate = true;
|
|
this._restHeadWorldQuaternion = this.getLookAtWorldQuaternion(new Quaternion());
|
|
}
|
|
/**
|
|
* Its current angle around Y axis, in degree.
|
|
*/
|
|
get yaw() {
|
|
return this._yaw;
|
|
}
|
|
/**
|
|
* Its current angle around Y axis, in degree.
|
|
*/
|
|
set yaw(value) {
|
|
this._yaw = value;
|
|
this._needsUpdate = true;
|
|
}
|
|
/**
|
|
* Its current angle around X axis, in degree.
|
|
*/
|
|
get pitch() {
|
|
return this._pitch;
|
|
}
|
|
/**
|
|
* Its current angle around X axis, in degree.
|
|
*/
|
|
set pitch(value) {
|
|
this._pitch = value;
|
|
this._needsUpdate = true;
|
|
}
|
|
/**
|
|
* @deprecated Use {@link getEuler} instead.
|
|
*/
|
|
get euler() {
|
|
console.warn("VRMLookAt: euler is deprecated. use getEuler() instead.");
|
|
return this.getEuler(new Euler());
|
|
}
|
|
/**
|
|
* Get its yaw-pitch angles as an `Euler`.
|
|
* Does NOT consider {@link faceFront}; it returns `Euler(0, 0, 0; "YXZ")` by default regardless of the faceFront value.
|
|
*
|
|
* @param target The target euler
|
|
*/
|
|
getEuler(target) {
|
|
return target.set(MathUtils.DEG2RAD * this._pitch, MathUtils.DEG2RAD * this._yaw, 0, "YXZ");
|
|
}
|
|
/**
|
|
* Copy the given {@link VRMLookAt} into this one.
|
|
* {@link humanoid} must be same as the source one.
|
|
* {@link applier} will reference the same instance as the source one.
|
|
* @param source The {@link VRMLookAt} you want to copy
|
|
* @returns this
|
|
*/
|
|
copy(source) {
|
|
if (this.humanoid !== source.humanoid) throw new Error("VRMLookAt: humanoid must be same in order to copy");
|
|
this.offsetFromHeadBone.copy(source.offsetFromHeadBone);
|
|
this.applier = source.applier;
|
|
this.autoUpdate = source.autoUpdate;
|
|
this.target = source.target;
|
|
this.faceFront.copy(source.faceFront);
|
|
return this;
|
|
}
|
|
/**
|
|
* Returns a clone of this {@link VRMLookAt}.
|
|
* Note that {@link humanoid} and {@link applier} will reference the same instance as this one.
|
|
* @returns Copied {@link VRMLookAt}
|
|
*/
|
|
clone() {
|
|
return new _VRMLookAt2(this.humanoid, this.applier).copy(this);
|
|
}
|
|
/**
|
|
* Reset the lookAt direction (yaw and pitch) to the initial direction.
|
|
*/
|
|
reset() {
|
|
this._yaw = 0;
|
|
this._pitch = 0;
|
|
this._needsUpdate = true;
|
|
}
|
|
/**
|
|
* Get its lookAt position in world coordinate.
|
|
*
|
|
* @param target A target `THREE.Vector3`
|
|
*/
|
|
getLookAtWorldPosition(target) {
|
|
const head = this.humanoid.getRawBoneNode("head");
|
|
return target.copy(this.offsetFromHeadBone).applyMatrix4(head.matrixWorld);
|
|
}
|
|
/**
|
|
* Get its lookAt rotation in world coordinate.
|
|
* Does NOT consider {@link faceFront}.
|
|
*
|
|
* @param target A target `THREE.Quaternion`
|
|
*/
|
|
getLookAtWorldQuaternion(target) {
|
|
return getWorldQuaternionLite(this.humanoid.getRawBoneNode("head"), target);
|
|
}
|
|
/**
|
|
* Get a quaternion that rotates the +Z unit vector of the humanoid Head to the {@link faceFront} direction.
|
|
*
|
|
* @param target A target `THREE.Quaternion`
|
|
*/
|
|
getFaceFrontQuaternion(target) {
|
|
if (this.faceFront.distanceToSquared(VEC3_POSITIVE_Z) < .01) return target.copy(this._restHeadWorldQuaternion).invert();
|
|
const [faceFrontAzimuth, faceFrontAltitude] = calcAzimuthAltitude(this.faceFront);
|
|
_eulerA.set(0, .5 * Math.PI + faceFrontAzimuth, faceFrontAltitude, "YZX");
|
|
return target.setFromEuler(_eulerA).premultiply(_quatD.copy(this._restHeadWorldQuaternion).invert());
|
|
}
|
|
/**
|
|
* Get its LookAt direction in world coordinate.
|
|
*
|
|
* @param target A target `THREE.Vector3`
|
|
*/
|
|
getLookAtWorldDirection(target) {
|
|
this.getLookAtWorldQuaternion(_quatB2);
|
|
this.getFaceFrontQuaternion(_quatC);
|
|
return target.copy(VEC3_POSITIVE_Z).applyQuaternion(_quatB2).applyQuaternion(_quatC).applyEuler(this.getEuler(_eulerA));
|
|
}
|
|
/**
|
|
* Set its lookAt target position.
|
|
*
|
|
* Note that its result will be instantly overwritten if {@link VRMLookAtHead.autoUpdate} is enabled.
|
|
*
|
|
* If you want to track an object continuously, you might want to use {@link target} instead.
|
|
*
|
|
* @param position A target position, in world space
|
|
*/
|
|
lookAt(position) {
|
|
const headRotDiffInv = _quatA5.copy(this._restHeadWorldQuaternion).multiply(quatInvertCompat(this.getLookAtWorldQuaternion(_quatB2)));
|
|
const headPos = this.getLookAtWorldPosition(_v3B3);
|
|
const lookAtDir = _v3C.copy(position).sub(headPos).applyQuaternion(headRotDiffInv).normalize();
|
|
const [azimuthFrom, altitudeFrom] = calcAzimuthAltitude(this.faceFront);
|
|
const [azimuthTo, altitudeTo] = calcAzimuthAltitude(lookAtDir);
|
|
const yaw = sanitizeAngle(azimuthTo - azimuthFrom);
|
|
const pitch = sanitizeAngle(altitudeFrom - altitudeTo);
|
|
this._yaw = MathUtils.RAD2DEG * yaw;
|
|
this._pitch = MathUtils.RAD2DEG * pitch;
|
|
this._needsUpdate = true;
|
|
}
|
|
/**
|
|
* Update the VRMLookAtHead.
|
|
* If {@link autoUpdate} is enabled, this will make it look at the {@link target}.
|
|
*
|
|
* @param delta deltaTime, it isn't used though. You can use the parameter if you want to use this in your own extended {@link VRMLookAt}.
|
|
*/
|
|
update(delta) {
|
|
if (this.target != null && this.autoUpdate) this.lookAt(this.target.getWorldPosition(_v3A5));
|
|
if (this._needsUpdate) {
|
|
this._needsUpdate = false;
|
|
this.applier.applyYawPitch(this._yaw, this._pitch);
|
|
}
|
|
}
|
|
};
|
|
_VRMLookAt.EULER_ORDER = "YXZ";
|
|
var VRMLookAt = _VRMLookAt;
|
|
var VEC3_POSITIVE_Z2 = new Vector3(0, 0, 1);
|
|
var _quatA6 = new Quaternion();
|
|
var _quatB3 = new Quaternion();
|
|
var _eulerA2 = new Euler(0, 0, 0, "YXZ");
|
|
var VRMLookAtBoneApplier = class {
|
|
/**
|
|
* Create a new {@link VRMLookAtBoneApplier}.
|
|
*
|
|
* @param humanoid A {@link VRMHumanoid}
|
|
* @param rangeMapHorizontalInner A {@link VRMLookAtRangeMap} used for inner transverse direction
|
|
* @param rangeMapHorizontalOuter A {@link VRMLookAtRangeMap} used for outer transverse direction
|
|
* @param rangeMapVerticalDown A {@link VRMLookAtRangeMap} used for down direction
|
|
* @param rangeMapVerticalUp A {@link VRMLookAtRangeMap} used for up direction
|
|
*/
|
|
constructor(humanoid, rangeMapHorizontalInner, rangeMapHorizontalOuter, rangeMapVerticalDown, rangeMapVerticalUp) {
|
|
this.humanoid = humanoid;
|
|
this.rangeMapHorizontalInner = rangeMapHorizontalInner;
|
|
this.rangeMapHorizontalOuter = rangeMapHorizontalOuter;
|
|
this.rangeMapVerticalDown = rangeMapVerticalDown;
|
|
this.rangeMapVerticalUp = rangeMapVerticalUp;
|
|
this.faceFront = new Vector3(0, 0, 1);
|
|
this._restQuatLeftEye = new Quaternion();
|
|
this._restQuatRightEye = new Quaternion();
|
|
this._restLeftEyeParentWorldQuat = new Quaternion();
|
|
this._restRightEyeParentWorldQuat = new Quaternion();
|
|
const leftEye = this.humanoid.getRawBoneNode("leftEye");
|
|
const rightEye = this.humanoid.getRawBoneNode("rightEye");
|
|
if (leftEye) {
|
|
this._restQuatLeftEye.copy(leftEye.quaternion);
|
|
getWorldQuaternionLite(leftEye.parent, this._restLeftEyeParentWorldQuat);
|
|
}
|
|
if (rightEye) {
|
|
this._restQuatRightEye.copy(rightEye.quaternion);
|
|
getWorldQuaternionLite(rightEye.parent, this._restRightEyeParentWorldQuat);
|
|
}
|
|
}
|
|
/**
|
|
* Apply the input angle to its associated VRM model.
|
|
*
|
|
* @param yaw Rotation around Y axis, in degree
|
|
* @param pitch Rotation around X axis, in degree
|
|
*/
|
|
applyYawPitch(yaw, pitch) {
|
|
const leftEye = this.humanoid.getRawBoneNode("leftEye");
|
|
const rightEye = this.humanoid.getRawBoneNode("rightEye");
|
|
const leftEyeNormalized = this.humanoid.getNormalizedBoneNode("leftEye");
|
|
const rightEyeNormalized = this.humanoid.getNormalizedBoneNode("rightEye");
|
|
if (leftEye) {
|
|
if (pitch < 0) _eulerA2.x = -MathUtils.DEG2RAD * this.rangeMapVerticalDown.map(-pitch);
|
|
else _eulerA2.x = MathUtils.DEG2RAD * this.rangeMapVerticalUp.map(pitch);
|
|
if (yaw < 0) _eulerA2.y = -MathUtils.DEG2RAD * this.rangeMapHorizontalInner.map(-yaw);
|
|
else _eulerA2.y = MathUtils.DEG2RAD * this.rangeMapHorizontalOuter.map(yaw);
|
|
_quatA6.setFromEuler(_eulerA2);
|
|
this._getWorldFaceFrontQuat(_quatB3);
|
|
leftEyeNormalized.quaternion.copy(_quatB3).multiply(_quatA6).multiply(_quatB3.invert());
|
|
_quatA6.copy(this._restLeftEyeParentWorldQuat);
|
|
leftEye.quaternion.copy(leftEyeNormalized.quaternion).multiply(_quatA6).premultiply(_quatA6.invert()).multiply(this._restQuatLeftEye);
|
|
}
|
|
if (rightEye) {
|
|
if (pitch < 0) _eulerA2.x = -MathUtils.DEG2RAD * this.rangeMapVerticalDown.map(-pitch);
|
|
else _eulerA2.x = MathUtils.DEG2RAD * this.rangeMapVerticalUp.map(pitch);
|
|
if (yaw < 0) _eulerA2.y = -MathUtils.DEG2RAD * this.rangeMapHorizontalOuter.map(-yaw);
|
|
else _eulerA2.y = MathUtils.DEG2RAD * this.rangeMapHorizontalInner.map(yaw);
|
|
_quatA6.setFromEuler(_eulerA2);
|
|
this._getWorldFaceFrontQuat(_quatB3);
|
|
rightEyeNormalized.quaternion.copy(_quatB3).multiply(_quatA6).multiply(_quatB3.invert());
|
|
_quatA6.copy(this._restRightEyeParentWorldQuat);
|
|
rightEye.quaternion.copy(rightEyeNormalized.quaternion).multiply(_quatA6).premultiply(_quatA6.invert()).multiply(this._restQuatRightEye);
|
|
}
|
|
}
|
|
/**
|
|
* @deprecated Use {@link applyYawPitch} instead.
|
|
*/
|
|
lookAt(euler) {
|
|
console.warn("VRMLookAtBoneApplier: lookAt() is deprecated. use apply() instead.");
|
|
const yaw = MathUtils.RAD2DEG * euler.y;
|
|
const pitch = MathUtils.RAD2DEG * euler.x;
|
|
this.applyYawPitch(yaw, pitch);
|
|
}
|
|
/**
|
|
* Get a quaternion that rotates the world-space +Z unit vector to the {@link faceFront} direction.
|
|
*
|
|
* @param target A target `THREE.Quaternion`
|
|
*/
|
|
_getWorldFaceFrontQuat(target) {
|
|
if (this.faceFront.distanceToSquared(VEC3_POSITIVE_Z2) < .01) return target.identity();
|
|
const [faceFrontAzimuth, faceFrontAltitude] = calcAzimuthAltitude(this.faceFront);
|
|
_eulerA2.set(0, .5 * Math.PI + faceFrontAzimuth, faceFrontAltitude, "YZX");
|
|
return target.setFromEuler(_eulerA2);
|
|
}
|
|
};
|
|
VRMLookAtBoneApplier.type = "bone";
|
|
var VRMLookAtExpressionApplier = class {
|
|
/**
|
|
* Create a new {@link VRMLookAtExpressionApplier}.
|
|
*
|
|
* @param expressions A {@link VRMExpressionManager}
|
|
* @param rangeMapHorizontalInner A {@link VRMLookAtRangeMap} used for inner transverse direction
|
|
* @param rangeMapHorizontalOuter A {@link VRMLookAtRangeMap} used for outer transverse direction
|
|
* @param rangeMapVerticalDown A {@link VRMLookAtRangeMap} used for down direction
|
|
* @param rangeMapVerticalUp A {@link VRMLookAtRangeMap} used for up direction
|
|
*/
|
|
constructor(expressions, rangeMapHorizontalInner, rangeMapHorizontalOuter, rangeMapVerticalDown, rangeMapVerticalUp) {
|
|
this.expressions = expressions;
|
|
this.rangeMapHorizontalInner = rangeMapHorizontalInner;
|
|
this.rangeMapHorizontalOuter = rangeMapHorizontalOuter;
|
|
this.rangeMapVerticalDown = rangeMapVerticalDown;
|
|
this.rangeMapVerticalUp = rangeMapVerticalUp;
|
|
}
|
|
/**
|
|
* Apply the input angle to its associated VRM model.
|
|
*
|
|
* @param yaw Rotation around Y axis, in degree
|
|
* @param pitch Rotation around X axis, in degree
|
|
*/
|
|
applyYawPitch(yaw, pitch) {
|
|
if (pitch < 0) {
|
|
this.expressions.setValue("lookDown", 0);
|
|
this.expressions.setValue("lookUp", this.rangeMapVerticalUp.map(-pitch));
|
|
} else {
|
|
this.expressions.setValue("lookUp", 0);
|
|
this.expressions.setValue("lookDown", this.rangeMapVerticalDown.map(pitch));
|
|
}
|
|
if (yaw < 0) {
|
|
this.expressions.setValue("lookLeft", 0);
|
|
this.expressions.setValue("lookRight", this.rangeMapHorizontalOuter.map(-yaw));
|
|
} else {
|
|
this.expressions.setValue("lookRight", 0);
|
|
this.expressions.setValue("lookLeft", this.rangeMapHorizontalOuter.map(yaw));
|
|
}
|
|
}
|
|
/**
|
|
* @deprecated Use {@link applyYawPitch} instead.
|
|
*/
|
|
lookAt(euler) {
|
|
console.warn("VRMLookAtBoneApplier: lookAt() is deprecated. use apply() instead.");
|
|
const yaw = MathUtils.RAD2DEG * euler.y;
|
|
const pitch = MathUtils.RAD2DEG * euler.x;
|
|
this.applyYawPitch(yaw, pitch);
|
|
}
|
|
};
|
|
VRMLookAtExpressionApplier.type = "expression";
|
|
var RAD2DEG = 180 / Math.PI;
|
|
var _eulerA3 = /* @__PURE__ */ new Euler();
|
|
var VRMLookAtQuaternionProxy = class extends Object3D {
|
|
constructor(lookAt) {
|
|
super();
|
|
this.vrmLookAt = lookAt;
|
|
this.type = "VRMLookAtQuaternionProxy";
|
|
const prevRotationOnChangeCallback = this.rotation._onChangeCallback;
|
|
this.rotation._onChange(() => {
|
|
prevRotationOnChangeCallback();
|
|
this._applyToLookAt();
|
|
});
|
|
const prevQuaternionOnChangeCallback = this.quaternion._onChangeCallback;
|
|
this.quaternion._onChange(() => {
|
|
prevQuaternionOnChangeCallback();
|
|
this._applyToLookAt();
|
|
});
|
|
}
|
|
_applyToLookAt() {
|
|
_eulerA3.setFromQuaternion(this.quaternion, VRMLookAt.EULER_ORDER);
|
|
this.vrmLookAt.yaw = RAD2DEG * _eulerA3.y;
|
|
this.vrmLookAt.pitch = RAD2DEG * _eulerA3.x;
|
|
}
|
|
};
|
|
function createVRMAnimationHumanoidTracks(vrmAnimation, humanoid, metaVersion) {
|
|
var _a, _b;
|
|
const translation = /* @__PURE__ */ new Map();
|
|
const rotation = /* @__PURE__ */ new Map();
|
|
for (const [name, origTrack] of vrmAnimation.humanoidTracks.rotation.entries()) {
|
|
const nodeName = (_a = humanoid.getNormalizedBoneNode(name)) == null ? void 0 : _a.name;
|
|
if (nodeName != null) {
|
|
const track = new QuaternionKeyframeTrack(`${nodeName}.quaternion`, origTrack.times, origTrack.values.map((v, i) => metaVersion === "0" && i % 2 === 0 ? -v : v));
|
|
rotation.set(name, track);
|
|
}
|
|
}
|
|
for (const [name, origTrack] of vrmAnimation.humanoidTracks.translation.entries()) {
|
|
const nodeName = (_b = humanoid.getNormalizedBoneNode(name)) == null ? void 0 : _b.name;
|
|
if (nodeName != null) {
|
|
const animationY = vrmAnimation.restHipsPosition.y;
|
|
const scale = humanoid.normalizedRestPose.hips.position[1] / animationY;
|
|
const track = origTrack.clone();
|
|
track.values = track.values.map((v, i) => (metaVersion === "0" && i % 3 !== 1 ? -v : v) * scale);
|
|
track.name = `${nodeName}.position`;
|
|
translation.set(name, track);
|
|
}
|
|
}
|
|
return {
|
|
translation,
|
|
rotation
|
|
};
|
|
}
|
|
function createVRMAnimationExpressionTracks(vrmAnimation, expressionManager) {
|
|
const preset = /* @__PURE__ */ new Map();
|
|
const custom = /* @__PURE__ */ new Map();
|
|
for (const [name, origTrack] of vrmAnimation.expressionTracks.preset.entries()) {
|
|
const trackName = expressionManager.getExpressionTrackName(name);
|
|
if (trackName != null) {
|
|
const track = origTrack.clone();
|
|
track.name = trackName;
|
|
preset.set(name, track);
|
|
}
|
|
}
|
|
for (const [name, origTrack] of vrmAnimation.expressionTracks.custom.entries()) {
|
|
const trackName = expressionManager.getExpressionTrackName(name);
|
|
if (trackName != null) {
|
|
const track = origTrack.clone();
|
|
track.name = trackName;
|
|
custom.set(name, track);
|
|
}
|
|
}
|
|
return {
|
|
preset,
|
|
custom
|
|
};
|
|
}
|
|
function createVRMAnimationLookAtTrack(vrmAnimation, trackName) {
|
|
if (vrmAnimation.lookAtTrack == null) return null;
|
|
const track = vrmAnimation.lookAtTrack.clone();
|
|
track.name = trackName;
|
|
return track;
|
|
}
|
|
function createVRMAnimationClip(vrmAnimation, vrm) {
|
|
const tracks = [];
|
|
const humanoidTracks = createVRMAnimationHumanoidTracks(vrmAnimation, vrm.humanoid, vrm.meta.metaVersion);
|
|
tracks.push(...humanoidTracks.translation.values());
|
|
tracks.push(...humanoidTracks.rotation.values());
|
|
if (vrm.expressionManager != null) {
|
|
const expressionTracks = createVRMAnimationExpressionTracks(vrmAnimation, vrm.expressionManager);
|
|
tracks.push(...expressionTracks.preset.values());
|
|
tracks.push(...expressionTracks.custom.values());
|
|
}
|
|
if (vrm.lookAt != null) {
|
|
let proxy = vrm.scene.children.find((obj) => obj instanceof VRMLookAtQuaternionProxy);
|
|
if (proxy == null) {
|
|
console.warn("createVRMAnimationClip: VRMLookAtQuaternionProxy is not found. Creating a new one automatically. To suppress this warning, create a VRMLookAtQuaternionProxy manually");
|
|
proxy = new VRMLookAtQuaternionProxy(vrm.lookAt);
|
|
proxy.name = "VRMLookAtQuaternionProxy";
|
|
vrm.scene.add(proxy);
|
|
} else if (proxy.name === "") {
|
|
console.warn("createVRMAnimationClip: VRMLookAtQuaternionProxy is found but its name is not set. Setting the name automatically. To suppress this warning, set the name manually");
|
|
proxy.name = "VRMLookAtQuaternionProxy";
|
|
}
|
|
const track = createVRMAnimationLookAtTrack(vrmAnimation, `${proxy.name}.quaternion`);
|
|
if (track != null) tracks.push(track);
|
|
}
|
|
return new AnimationClip("Clip", vrmAnimation.duration, tracks);
|
|
}
|
|
var VRMAnimation = class {
|
|
constructor() {
|
|
this.duration = 0;
|
|
this.restHipsPosition = new Vector3();
|
|
this.humanoidTracks = {
|
|
translation: /* @__PURE__ */ new Map(),
|
|
rotation: /* @__PURE__ */ new Map()
|
|
};
|
|
this.expressionTracks = {
|
|
preset: /* @__PURE__ */ new Map(),
|
|
custom: /* @__PURE__ */ new Map()
|
|
};
|
|
this.lookAtTrack = null;
|
|
}
|
|
};
|
|
function arrayChunk(array, every) {
|
|
const N = array.length;
|
|
const ret = [];
|
|
let current = [];
|
|
let remaining = 0;
|
|
for (let i = 0; i < N; i++) {
|
|
const el = array[i];
|
|
if (remaining <= 0) {
|
|
remaining = every;
|
|
current = [];
|
|
ret.push(current);
|
|
}
|
|
current.push(el);
|
|
remaining--;
|
|
}
|
|
return ret;
|
|
}
|
|
var MAT4_IDENTITY = /* @__PURE__ */ new Matrix4();
|
|
var _v3A6 = /* @__PURE__ */ new Vector3();
|
|
var _quatA7 = /* @__PURE__ */ new Quaternion();
|
|
var _quatB4 = /* @__PURE__ */ new Quaternion();
|
|
var _quatC2 = /* @__PURE__ */ new Quaternion();
|
|
var POSSIBLE_SPEC_VERSIONS2 = /* @__PURE__ */ new Set(["1.0", "1.0-draft"]);
|
|
var vrmExpressionPresetNameSet = /* @__PURE__ */ new Set(Object.values(VRMExpressionPresetName));
|
|
var VRMAnimationLoaderPlugin = class {
|
|
constructor(parser) {
|
|
this.parser = parser;
|
|
}
|
|
get name() {
|
|
return "VRMC_vrm_animation";
|
|
}
|
|
afterRoot(gltf) {
|
|
return __async(this, null, function* () {
|
|
var _a, _b, _c;
|
|
const defGltf = gltf.parser.json;
|
|
const defExtensionsUsed = defGltf.extensionsUsed;
|
|
if (defExtensionsUsed == null || defExtensionsUsed.indexOf(this.name) == -1) return;
|
|
const defExtension = (_a = defGltf.extensions) == null ? void 0 : _a[this.name];
|
|
if (defExtension == null) return;
|
|
const specVersion = defExtension.specVersion;
|
|
if (specVersion == null) console.warn("VRMAnimationLoaderPlugin: specVersion of the VRMA is not defined. Consider updating the animation file. Assuming the spec version is 1.0.");
|
|
else {
|
|
if (!POSSIBLE_SPEC_VERSIONS2.has(specVersion)) {
|
|
console.warn(`VRMAnimationLoaderPlugin: Unknown VRMC_vrm_animation spec version: ${specVersion}`);
|
|
return;
|
|
}
|
|
if (specVersion === "1.0-draft") console.warn("VRMAnimationLoaderPlugin: Using a draft spec version: 1.0-draft. Some behaviors may be different. Consider updating the animation file.");
|
|
}
|
|
const nodeMap = this._createNodeMap(defExtension);
|
|
const worldMatrixMap = yield this._createBoneWorldMatrixMap(gltf, defExtension);
|
|
const hipsNode = (_c = (_b = defExtension.humanoid) == null ? void 0 : _b.humanBones["hips"]) == null ? void 0 : _c.node;
|
|
const hips = hipsNode != null ? yield gltf.parser.getDependency("node", hipsNode) : null;
|
|
const restHipsPosition = new Vector3();
|
|
hips?.getWorldPosition(restHipsPosition);
|
|
if (restHipsPosition.y < .001) console.warn("VRMAnimationLoaderPlugin: The loaded VRM Animation might violate the VRM T-pose (The y component of the rest hips position is approximately zero or below.)");
|
|
const animations = gltf.animations.map((clip, iAnimation) => {
|
|
const defAnimation = defGltf.animations[iAnimation];
|
|
const animation = this._parseAnimation(clip, defAnimation, nodeMap, worldMatrixMap);
|
|
animation.restHipsPosition = restHipsPosition;
|
|
return animation;
|
|
});
|
|
gltf.userData.vrmAnimations = animations;
|
|
});
|
|
}
|
|
_createNodeMap(defExtension) {
|
|
var _a, _b, _c, _d, _e;
|
|
const humanoidIndexToName = /* @__PURE__ */ new Map();
|
|
const expressionsIndexToName = /* @__PURE__ */ new Map();
|
|
const humanBones = (_a = defExtension.humanoid) == null ? void 0 : _a.humanBones;
|
|
if (humanBones) Object.entries(humanBones).forEach(([name, bone]) => {
|
|
const node = bone == null ? void 0 : bone.node;
|
|
if (node != null) humanoidIndexToName.set(node, name);
|
|
});
|
|
const preset = (_b = defExtension.expressions) == null ? void 0 : _b.preset;
|
|
if (preset) Object.entries(preset).forEach(([name, expression]) => {
|
|
const node = expression == null ? void 0 : expression.node;
|
|
if (node != null) expressionsIndexToName.set(node, name);
|
|
});
|
|
const custom = (_c = defExtension.expressions) == null ? void 0 : _c.custom;
|
|
if (custom) Object.entries(custom).forEach(([name, expression]) => {
|
|
const { node } = expression;
|
|
expressionsIndexToName.set(node, name);
|
|
});
|
|
return {
|
|
humanoidIndexToName,
|
|
expressionsIndexToName,
|
|
lookAtIndex: (_e = (_d = defExtension.lookAt) == null ? void 0 : _d.node) != null ? _e : null
|
|
};
|
|
}
|
|
_createBoneWorldMatrixMap(gltf, defExtension) {
|
|
return __async(this, null, function* () {
|
|
var _a, _b;
|
|
gltf.scene.updateWorldMatrix(false, true);
|
|
const threeNodes = yield gltf.parser.getDependencies("node");
|
|
const worldMatrixMap = /* @__PURE__ */ new Map();
|
|
if (defExtension.humanoid == null) return worldMatrixMap;
|
|
for (const [boneName, humanBone] of Object.entries(defExtension.humanoid.humanBones)) {
|
|
const node = humanBone == null ? void 0 : humanBone.node;
|
|
if (node != null) {
|
|
const threeNode = threeNodes[node];
|
|
worldMatrixMap.set(boneName, threeNode.matrixWorld);
|
|
if (boneName === "hips") worldMatrixMap.set("hipsParent", (_b = (_a = threeNode.parent) == null ? void 0 : _a.matrixWorld) != null ? _b : MAT4_IDENTITY);
|
|
}
|
|
}
|
|
return worldMatrixMap;
|
|
});
|
|
}
|
|
_parseAnimation(animationClip, defAnimation, nodeMap, worldMatrixMap) {
|
|
const tracks = animationClip.tracks;
|
|
const defChannels = defAnimation.channels;
|
|
const result = new VRMAnimation();
|
|
result.duration = animationClip.duration;
|
|
defChannels.forEach((channel, iChannel) => {
|
|
const { node, path } = channel.target;
|
|
const origTrack = tracks[iChannel];
|
|
if (node == null) return;
|
|
const boneName = nodeMap.humanoidIndexToName.get(node);
|
|
if (boneName != null) {
|
|
let parentBoneName = VRMHumanBoneParentMap[boneName];
|
|
while (parentBoneName != null && worldMatrixMap.get(parentBoneName) == null) parentBoneName = VRMHumanBoneParentMap[parentBoneName];
|
|
if (parentBoneName == null) parentBoneName = "hipsParent";
|
|
if (path === "translation") if (boneName !== "hips") console.warn(`The loading animation contains a translation track for ${boneName}, which is not permitted in the VRMC_vrm_animation spec. ignoring the track`);
|
|
else {
|
|
const hipsParentWorldMatrix = worldMatrixMap.get("hipsParent");
|
|
const trackValues = arrayChunk(origTrack.values, 3).flatMap((v) => _v3A6.fromArray(v).applyMatrix4(hipsParentWorldMatrix).toArray());
|
|
const track = origTrack.clone();
|
|
track.values = new Float32Array(trackValues);
|
|
result.humanoidTracks.translation.set(boneName, track);
|
|
}
|
|
else if (path === "rotation") {
|
|
const worldMatrix = worldMatrixMap.get(boneName);
|
|
const parentWorldMatrix = worldMatrixMap.get(parentBoneName);
|
|
worldMatrix.decompose(_v3A6, _quatA7, _v3A6);
|
|
_quatA7.invert();
|
|
parentWorldMatrix.decompose(_v3A6, _quatB4, _v3A6);
|
|
const trackValues = arrayChunk(origTrack.values, 4).flatMap((q) => _quatC2.fromArray(q).premultiply(_quatB4).multiply(_quatA7).toArray());
|
|
const track = origTrack.clone();
|
|
track.values = new Float32Array(trackValues);
|
|
result.humanoidTracks.rotation.set(boneName, track);
|
|
} else throw new Error(`Invalid path "${path}"`);
|
|
return;
|
|
}
|
|
const expressionName = nodeMap.expressionsIndexToName.get(node);
|
|
if (expressionName != null) {
|
|
if (path === "translation") {
|
|
const times = origTrack.times;
|
|
const values = new Float32Array(origTrack.values.length / 3);
|
|
for (let i = 0; i < values.length; i++) values[i] = origTrack.values[3 * i];
|
|
const newTrack = new NumberKeyframeTrack(`${expressionName}.weight`, times, values);
|
|
if (vrmExpressionPresetNameSet.has(expressionName)) result.expressionTracks.preset.set(expressionName, newTrack);
|
|
else result.expressionTracks.custom.set(expressionName, newTrack);
|
|
} else throw new Error(`Invalid path "${path}"`);
|
|
return;
|
|
}
|
|
if (node === nodeMap.lookAtIndex) if (path === "rotation") result.lookAtTrack = origTrack;
|
|
else throw new Error(`Invalid path "${path}"`);
|
|
});
|
|
return result;
|
|
}
|
|
};
|
|
/*!
|
|
* @pixiv/three-vrm-core v3.4.1
|
|
* The implementation of core features of VRM, for @pixiv/three-vrm
|
|
*
|
|
* Copyright (c) 2019-2025 pixiv Inc.
|
|
* @pixiv/three-vrm-core is distributed under MIT License
|
|
* https://github.com/pixiv/three-vrm/blob/release/LICENSE
|
|
*/
|
|
//#endregion
|
|
export { VRMAnimation, VRMAnimationLoaderPlugin, VRMLookAtQuaternionProxy, createVRMAnimationClip, createVRMAnimationExpressionTracks, createVRMAnimationHumanoidTracks, createVRMAnimationLookAtTrack };
|
|
|
|
//# sourceMappingURL=@pixiv_three-vrm-animation.js.map
|