UNPKG

@pixiv/three-vrm

Version:

VRM file loader for three.js.

1,190 lines (1,171 loc) 2.23 MB
/*! * @pixiv/three-vrm v2.1.0 * VRM file loader for three.js. * * Copyright (c) 2019-2024 pixiv Inc. * @pixiv/three-vrm is distributed under MIT License * https://github.com/pixiv/three-vrm/blob/release/LICENSE */ import * as THREE from 'three'; import { BufferAttribute } from 'three'; /*! * @pixiv/three-vrm-core v2.1.0 * The implementation of core features of VRM, for @pixiv/three-vrm * * Copyright (c) 2020-2024 pixiv Inc. * @pixiv/three-vrm-core is distributed under MIT License * https://github.com/pixiv/three-vrm/blob/release/LICENSE */ // animationMixer の監視対象は、Scene の中に入っている必要がある。 // そのため、表示オブジェクトではないけれど、Object3D を継承して Scene に投入できるようにする。 class VRMExpression extends THREE.Object3D { /** * 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.0 < this.weight ? 1.0 : 0.0; } else if (this.overrideBlink === 'blend') { return this.weight; } else { return 0.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.0 < this.weight ? 1.0 : 0.0; } else if (this.overrideLookAt === 'blend') { return this.weight; } else { return 0.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.0 < this.weight ? 1.0 : 0.0; } else if (this.overrideMouth === 'blend') { return this.weight; } else { return 0.0; } } constructor(expressionName) { super(); /** * The current weight of the expression. */ this.weight = 0.0; /** * Interpret values greater than 0.5 as 1.0, ortherwise 0.0. */ this.isBinary = false; /** * Specify how the expression overrides blink expressions. */ this.overrideBlink = 'none'; /** * Specify how the expression overrides lookAt expressions. */ this.overrideLookAt = 'none'; /** * Specify how the expression overrides mouth expressions. */ this.overrideMouth = 'none'; this._binds = []; this.name = `VRMExpression_${expressionName}`; this.expressionName = expressionName; // traverse 時の救済手段として Object3D ではないことを明示しておく this.type = 'VRMExpression'; // 表示目的のオブジェクトではないので、負荷軽減のために visible を false にしておく。 // これにより、このインスタンスに対する毎フレームの matrix 自動計算を省略できる。 this.visible = false; } addBind(bind) { this._binds.push(bind); } /** * Apply weight to every assigned blend shapes. * Should be called every frame. */ applyWeight(options) { var _a; let actualWeight = this.isBinary ? (this.weight <= 0.5 ? 0.0 : 1.0) : this.weight; actualWeight *= (_a = options === null || options === void 0 ? void 0 : options.multiplier) !== null && _a !== void 0 ? _a : 1.0; this._binds.forEach((bind) => bind.applyWeight(actualWeight)); } /** * Clear previously assigned blend shapes. */ clearAppliedWeight() { this._binds.forEach((bind) => bind.clearAppliedWeight()); } } /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ function __awaiter$6(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; function extractPrimitivesInternal(gltf, nodeIndex, node) { var _a, _b; const json = gltf.parser.json; /** * Let's list up every possible patterns that parsed gltf nodes with a mesh can have,,, * * "*" indicates that those meshes should be listed up using this function * * ### A node with a (mesh, a signle primitive) * * - `THREE.Mesh`: The only primitive of the mesh * * * ### A node with a (mesh, multiple primitives) * * - `THREE.Group`: The root of the mesh * - `THREE.Mesh`: A primitive of the mesh * * - `THREE.Mesh`: A primitive of the mesh (2) * * * ### A node with a (mesh, multiple primitives) AND (a child with a mesh, a single primitive) * * - `THREE.Group`: The root of the mesh * - `THREE.Mesh`: A primitive of the mesh * * - `THREE.Mesh`: A primitive of the mesh (2) * * - `THREE.Mesh`: A primitive of a MESH OF THE CHILD * * ### A node with a (mesh, multiple primitives) AND (a child with a mesh, multiple primitives) * * - `THREE.Group`: The root of the mesh * - `THREE.Mesh`: A primitive of the mesh * * - `THREE.Mesh`: A primitive of the mesh (2) * * - `THREE.Group`: The root of a MESH OF THE CHILD * - `THREE.Mesh`: A primitive of the mesh of the child * - `THREE.Mesh`: A primitive of the mesh of the child (2) * * ### A node with a (mesh, multiple primitives) BUT the node is a bone * * - `THREE.Bone`: The root of the node, as a bone * - `THREE.Group`: The root of the mesh * - `THREE.Mesh`: A primitive of the mesh * * - `THREE.Mesh`: A primitive of the mesh (2) * * * ### A node with a (mesh, multiple primitives) AND (a child with a mesh, multiple primitives) BUT the node is a bone * * - `THREE.Bone`: The root of the node, as a bone * - `THREE.Group`: The root of the mesh * - `THREE.Mesh`: A primitive of the mesh * * - `THREE.Mesh`: A primitive of the mesh (2) * * - `THREE.Group`: The root of a MESH OF THE CHILD * - `THREE.Mesh`: A primitive of the mesh of the child * - `THREE.Mesh`: A primitive of the mesh of the child (2) * * ...I will take a strategy that traverses the root of the node and take first (primitiveCount) meshes. */ // Make sure that the node has a mesh const schemaNode = (_a = json.nodes) === null || _a === void 0 ? 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; } // How many primitives the mesh has? const schemaMesh = (_b = json.meshes) === null || _b === void 0 ? 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; // Traverse the node and take first (primitiveCount) meshes const primitives = []; node.traverse((object) => { if (primitives.length < primitiveCount) { if (object.isMesh) { primitives.push(object); } } }); return primitives; } /** * Extract primitives ( `THREE.Mesh[]` ) of a node from a loaded GLTF. * The main purpose of this function is to distinguish primitives and children from a node that has both meshes and children. * * It utilizes the behavior that GLTFLoader adds mesh primitives to the node object ( `THREE.Group` ) first then adds its children. * * @param gltf A GLTF object taken from GLTFLoader * @param nodeIndex The index of the node */ function gltfExtractPrimitivesFromNode(gltf, nodeIndex) { return __awaiter$6(this, void 0, void 0, function* () { const node = yield gltf.parser.getDependency('node', nodeIndex); return extractPrimitivesInternal(gltf, nodeIndex, node); }); } /** * Extract primitives ( `THREE.Mesh[]` ) of nodes from a loaded GLTF. * See {@link gltfExtractPrimitivesFromNode} for more details. * * It returns a map from node index to extraction result. * If a node does not have a mesh, the entry for the node will not be put in the returning map. * * @param gltf A GLTF object taken from GLTFLoader */ function gltfExtractPrimitivesFromNodes(gltf) { return __awaiter$6(this, void 0, void 0, function* () { const nodes = yield gltf.parser.getDependencies('node'); const map = new Map(); nodes.forEach((node, index) => { const result = extractPrimitivesInternal(gltf, index, node); if (result != null) { map.set(index, result); } }); return map; }); } /** * Get a material definition index of glTF from associated material. * It's basically a comat code between Three.js r133 or above and previous versions. * @param parser GLTFParser * @param material A material of gltf * @returns Material definition index of glTF */ function gltfGetAssociatedMaterialIndex(parser, material) { var _a, _b; const threeRevision = parseInt(THREE.REVISION, 10); let index = null; if (threeRevision >= 133) { index = (_b = (_a = parser.associations.get(material)) === null || _a === void 0 ? void 0 : _a.materials) !== null && _b !== void 0 ? _b : null; } else { const associations = parser.associations; const reference = associations.get(material); if ((reference === null || reference === void 0 ? void 0 : reference.type) === 'materials') { index = reference.index; } } return index; } /* eslint-disable @typescript-eslint/naming-convention */ const 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', }; /** * Clamp the input value within [0.0 - 1.0]. * * @param value The input value */ function saturate(value) { return Math.max(Math.min(value, 1.0), 0.0); } class VRMExpressionManager { 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; } /** * Create a new {@link VRMExpressionManager}. */ constructor() { /** * A set of name or preset name of expressions that will be overridden by {@link VRMExpression.overrideBlink}. */ this.blinkExpressionNames = ['blink', 'blinkLeft', 'blinkRight']; /** * A set of name or preset name of expressions that will be overridden by {@link VRMExpression.overrideLookAt}. */ this.lookAtExpressionNames = ['lookLeft', 'lookRight', 'lookUp', 'lookDown']; /** * A set of name or preset name of expressions that will be overridden by {@link VRMExpression.overrideMouth}. */ this.mouthExpressionNames = ['aa', 'ee', 'ih', 'oh', 'ou']; /** * A set of {@link VRMExpression}. * When you want to register expressions, use {@link registerExpression} */ this._expressions = []; /** * A map from name to expression. */ this._expressionMap = {}; // do nothing } /** * Copy the given {@link VRMExpressionManager} into this one. * @param source The {@link VRMExpressionManager} you want to copy * @returns this */ copy(source) { // first unregister all the expression it has const expressions = this._expressions.concat(); expressions.forEach((expression) => { this.unregisterExpression(expression); }); // then register all the expression of the source source._expressions.forEach((expression) => { this.registerExpression(expression); }); // copy remaining members 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 !== void 0 ? _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 || expression === void 0 ? void 0 : expression.weight) !== null && _a !== void 0 ? _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); } } /** * 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() { // see how much we should override certain expressions const weightMultipliers = this._calculateWeightMultipliers(); // reset expression binds first this._expressions.forEach((expression) => { expression.clearAppliedWeight(); }); // then apply binds this._expressions.forEach((expression) => { let multiplier = 1.0; 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.0; let lookAt = 1.0; let mouth = 1.0; this._expressions.forEach((expression) => { blink -= expression.overrideBlinkAmount; lookAt -= expression.overrideLookAtAmount; mouth -= expression.overrideMouthAmount; }); blink = Math.max(0.0, blink); lookAt = Math.max(0.0, lookAt); mouth = Math.max(0.0, mouth); return { blink, lookAt, mouth }; } } /* eslint-disable @typescript-eslint/naming-convention */ const VRMExpressionMaterialColorType = { Color: 'color', EmissionColor: 'emissionColor', ShadeColor: 'shadeColor', MatcapColor: 'matcapColor', RimColor: 'rimColor', OutlineColor: 'outlineColor', }; const v0ExpressionMaterialColorMap = { _Color: VRMExpressionMaterialColorType.Color, _EmissionColor: VRMExpressionMaterialColorType.EmissionColor, _ShadeColor: VRMExpressionMaterialColorType.ShadeColor, _RimColor: VRMExpressionMaterialColorType.RimColor, _OutlineColor: VRMExpressionMaterialColorType.OutlineColor, }; const _color = new THREE.Color(); /** * A bind of expression influences to a material color. */ class VRMExpressionMaterialColorBind { constructor({ material, type, targetValue, targetAlpha, }) { this.material = material; this.type = type; this.targetValue = targetValue; this.targetAlpha = targetAlpha !== null && targetAlpha !== void 0 ? targetAlpha : 1.0; // init bind state 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 != undefined) { target.add(_color.copy(deltaValue).multiplyScalar(weight)); } } if (alpha != null) { const { propertyName, deltaValue } = alpha; const target = this.material[propertyName]; if (target != undefined) { 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 != undefined) { target.copy(initialValue); } } if (alpha != null) { const { propertyName, initialValue } = alpha; const target = this.material[propertyName]; if (target != undefined) { this.material[propertyName] = initialValue; } } } _initColorBindState() { var _a, _b, _c; const { material, type, targetValue } = this; const propertyNameMap = this._getPropertyNameMap(); const propertyName = (_b = (_a = propertyNameMap === null || propertyNameMap === void 0 ? void 0 : propertyNameMap[type]) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : null; if (propertyName == null) { console.warn(`Tried to add a material color bind to the material ${(_c = material.name) !== null && _c !== void 0 ? _c : '(no name)'}, the type ${type} but the material or the type is not supported.`); return null; } const target = material[propertyName]; const initialValue = target.clone(); // 負の値を保持するためにColor.subを使わずに差分を計算する const deltaValue = new THREE.Color(targetValue.r - initialValue.r, targetValue.g - initialValue.g, targetValue.b - initialValue.b); return { propertyName, initialValue, deltaValue }; } _initAlphaBindState() { var _a, _b, _c; const { material, type, targetAlpha } = this; const propertyNameMap = this._getPropertyNameMap(); const propertyName = (_b = (_a = propertyNameMap === null || propertyNameMap === void 0 ? void 0 : propertyNameMap[type]) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : null; if (propertyName == null && targetAlpha !== 1.0) { console.warn(`Tried to add a material alpha bind to the material ${(_c = material.name) !== null && _c !== void 0 ? _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]; const deltaValue = targetAlpha - initialValue; return { propertyName, initialValue, deltaValue }; } _getPropertyNameMap() { var _a, _b; return ((_b = (_a = Object.entries(VRMExpressionMaterialColorBind._propertyNameMapMap).find(([distinguisher]) => { return this.material[distinguisher] === true; })) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : null); } } /** * 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 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], }, }; /** * A bind of {@link VRMExpression} influences to morph targets. */ class VRMExpressionMorphTargetBind { 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 || _a === void 0 ? 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 || _a === void 0 ? void 0 : _a[this.index]) != null) { mesh.morphTargetInfluences[this.index] = 0.0; } }); } } const _v2 = new THREE.Vector2(); /** * A bind of expression influences to texture transforms. */ class VRMExpressionTextureTransformBind { constructor({ material, scale, offset, }) { var _a, _b; this.material = material; this.scale = scale; this.offset = offset; const propertyNames = (_a = Object.entries(VRMExpressionTextureTransformBind._propertyNamesMap).find(([distinguisher]) => { return material[distinguisher] === true; })) === null || _a === void 0 ? void 0 : _a[1]; if (propertyNames == null) { console.warn(`Tried to add a texture transform bind to the material ${(_b = material.name) !== null && _b !== void 0 ? _b : '(no name)'} but the material is not supported.`); this._properties = []; } else { this._properties = []; propertyNames.forEach((propertyName) => { var _a; const texture = (_a = material[propertyName]) === null || _a === void 0 ? void 0 : _a.clone(); if (!texture) { return null; } material[propertyName] = texture; // because the texture is cloned 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 === undefined) { return; } // TODO: we should kick this at `addMaterialValue` 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 === undefined) { return; } // TODO: we should kick this at `addMaterialValue` 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', ], }; /** * Possible spec versions it recognizes. */ const POSSIBLE_SPEC_VERSIONS$4 = new Set(['1.0', '1.0-beta']); /** * A plugin of GLTFLoader that imports a {@link VRMExpressionManager} from a VRM extension of a GLTF. */ class VRMExpressionLoaderPlugin { get name() { // We should use the extension name instead but we have multiple plugins for an extension... return 'VRMExpressionLoaderPlugin'; } constructor(parser) { this.parser = parser; } afterRoot(gltf) { return __awaiter$6(this, void 0, void 0, 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 __awaiter$6(this, void 0, void 0, 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) { var _a, _b; return __awaiter$6(this, void 0, void 0, function* () { const json = this.parser.json; // early abort if it doesn't use vrm const isVRMUsed = ((_a = json.extensionsUsed) === null || _a === void 0 ? void 0 : _a.indexOf('VRMC_vrm')) !== -1; if (!isVRMUsed) { return null; } const extension = (_b = json.extensions) === null || _b === void 0 ? void 0 : _b['VRMC_vrm']; if (!extension) { return null; } const specVersion = extension.specVersion; if (!POSSIBLE_SPEC_VERSIONS$4.has(specVersion)) { console.warn(`VRMExpressionLoaderPlugin: Unknown VRMC_vrm specVersion "${specVersion}"`); return null; } const schemaExpressions = extension.expressions; if (!schemaExpressions) { return null; } // list expressions const presetNameSet = new Set(Object.values(VRMExpressionPresetName)); const nameSchemaExpressionMap = new Map(); if (schemaExpressions.preset != null) { Object.entries(schemaExpressions.preset).forEach(([name, schemaExpression]) => { if (schemaExpression == null) { return; } // typescript 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); }); } // prepare manager const manager = new VRMExpressionManager(); // load expressions yield Promise.all(Array.from(nameSchemaExpressionMap.entries()).map(([name, schemaExpression]) => __awaiter$6(this, void 0, void 0, function* () { var _c, _d, _e, _f, _g, _h, _j; const expression = new VRMExpression(name); gltf.scene.add(expression); expression.isBinary = (_c = schemaExpression.isBinary) !== null && _c !== void 0 ? _c : false; expression.overrideBlink = (_d = schemaExpression.overrideBlink) !== null && _d !== void 0 ? _d : 'none'; expression.overrideLookAt = (_e = schemaExpression.overrideLookAt) !== null && _e !== void 0 ? _e : 'none'; expression.overrideMouth = (_f = schemaExpression.overrideMouth) !== null && _f !== void 0 ? _f : 'none'; (_g = schemaExpression.morphTargetBinds) === null || _g === void 0 ? void 0 : _g.forEach((bind) => __awaiter$6(this, void 0, void 0, function* () { var _k; if (bind.node === undefined || bind.index === undefined) { return; } const primitives = (yield gltfExtractPrimitivesFromNode(gltf, bind.node)); const morphTargetIndex = bind.index; // check if the mesh has the target morph target 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: (_k = bind.weight) !== null && _k !== void 0 ? _k : 1.0, })); })); if (schemaExpression.materialColorBinds || schemaExpression.textureTransformBinds) { // list up every material in `gltf.scene` const gltfMaterials = []; gltf.scene.traverse((object) => { const material = object.material; if (material) { gltfMaterials.push(material); } }); (_h = schemaExpression.materialColorBinds) === null || _h === void 0 ? void 0 : _h.forEach((bind) => __awaiter$6(this, void 0, void 0, function* () { const materials = gltfMaterials.filter((material) => { const materialIndex = gltfGetAssociatedMaterialIndex(this.parser, material); return bind.material === materialIndex; }); materials.forEach((material) => { expression.addBind(new VRMExpressionMaterialColorBind({ material, type: bind.type, targetValue: new THREE.Color().fromArray(bind.targetValue), targetAlpha: bind.targetValue[3], })); }); })); (_j = schemaExpression.textureTransformBinds) === null || _j === void 0 ? void 0 : _j.forEach((bind) => __awaiter$6(this, void 0, void 0, function* () { const materials = gltfMaterials.filter((material) => { const materialIndex = gltfGetAssociatedMaterialIndex(this.parser, material); return bind.material === materialIndex; }); materials.forEach((material) => { var _a, _b; expression.addBind(new VRMExpressionTextureTransformBind({ material, offset: new THREE.Vector2().fromArray((_a = bind.offset) !== null && _a !== void 0 ? _a : [0.0, 0.0]), scale: new THREE.Vector2().fromArray((_b = bind.scale) !== null && _b !== void 0 ? _b : [1.0, 1.0]), })); }); })); } manager.registerExpression(expression); }))); return manager; }); } _v0Import(gltf) { var _a; return __awaiter$6(this, void 0, void 0, function* () { const json = this.parser.json; // early abort if it doesn't use vrm const vrmExt = (_a = json.extensions) === null || _a === void 0 ? 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 = new Set(); yield Promise.all(schemaBlendShapeGroups.map((schemaGroup) => __awaiter$6(this, void 0, void 0, function* () { var _b; const v0PresetName = schemaGroup.presetName; const v1PresetName = (v0PresetName != null && VRMExpressionLoaderPlugin.v0v1PresetNameMap[v0PresetName]) || null; const name = v1PresetName !== null && v1PresetName !== void 0 ? v1PresetName : schemaGroup.name; if (name == null) { console.warn('VRMExpressionLoaderPlugin: One of custom expressions has no name. Ignoring the expression'); return; } // duplication check 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 = (_b = schemaGroup.isBinary) !== null && _b !== void 0 ? _b : false; // v0 doesn't have ignore properties // Bind morphTarget if (schemaGroup.binds) { schemaGroup.binds.forEach((bind) => __awaiter$6(this, void 0, void 0, function* () { var _c; if (bind.mesh === undefined || bind.index === undefined) { return; } const nodesUsingMesh = []; (_c = json.nodes) === null || _c === void 0 ? void 0 : _c.forEach((node, i) => { if (node.mesh === bind.mesh) { nodesUsingMesh.push(i); } }); const morphTargetIndex = bind.index; yield Promise.all(nodesUsingMesh.map((nodeIndex) => __awaiter$6(this, void 0, void 0, function* () { var _d; const primitives = (yield gltfExtractPrimitivesFromNode(gltf, nodeIndex)); // check if the mesh has the target morph target 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: 0.01 * ((_d = bind.weight) !== null && _d !== void 0 ? _d : 100), // narrowing the range from [ 0.0 - 100.0 ] to [ 0.0 - 1.0 ] })); }))); })); } // Bind MaterialColor and TextureTransform const materialValues = schemaGroup.materialValues; if (materialValues && materialValues.length !== 0) { materialValues.forEach((materialValue) => { if (materialValue.materialName === undefined || materialValue.propertyName === undefined || materialValue.targetValue === undefined) { return; } /** * アバターのオブジェクトに設定されているマテリアルの内から * materialValueで指定されているマテリアルを集める。 * * 特定には名前を使用する。 * アウトライン描画用のマテリアルも同時に集める。 */ 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) => { // TextureTransformBind if (materialPropertyName === '_MainTex_ST') { const scale = new THREE.Vector2(materialValue.targetValue[0], materialValue.targetValue[1]); const offset = new THREE.Vector2(materialValue.targetValue[2], materialValue.targetValue[3]); offset.y = 1.0 - offset.y - scale.y; expression.addBind(new VRMExpressionTextureTransformBind({ material, scale, offset, })); return; } // MaterialColorBind const materialColorType = v0ExpressionMaterialColorMap[materialPropertyName]; if (materialColorType) { expression.addBind(new VRMExpressionMaterialColorBind({ material, type: materialColorType, targetValue: new THREE.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', // eslint-disable-next-line @typescript-eslint/naming-convention blink_l: 'blinkLeft', // eslint-disable-next-line @typescript-eslint/naming-convention blink_r: 'blinkRight', neutral: 'neutral', }; /* eslint-disable @typescript-eslint/naming-convention */ const VRMExpressionOverrideType = { None: 'none', Block: 'block', Blend: 'blend', }; class VRMFirstPerson { /** * Create a new VRMFirstPerson object. * * @param humanoid A {@link VRMHumanoid} * @param meshAnnotations A renderer settings. See the description of [[RendererFirstPersonFlags]] for more info */ constructor(humanoid, meshAnnotations) { this._firstPersonOnlyLayer = VRMFirstPerson.DEFAULT_FIRSTPERSON_ONLY_LAYER; this._thirdPersonOnlyLayer = VRMFirstPerson.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 VRMFirstPerson(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 = VRMFirstPerson.DEFAULT_FIRSTPERSON_ONLY_LAYER, thirdPersonOnlyLayer = VRMFirstPerson.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.traver