@pmndrs/viverse 0.2.25 → 0.2.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ import type { VRMHumanBoneName } from '@pixiv/three-vrm';
2
+ /**
3
+ * Maps standard BVH bone names to VRM humanoid bone names.
4
+ *
5
+ * Kept as a `.ts` module rather than a `.json` import on purpose: importing JSON requires
6
+ * import attributes (`with { type: 'json' }`), which survive into the published ESM build and
7
+ * break in-browser bundlers like the docs' Sandpack (Babel: "experimental syntax
8
+ * 'moduleAttributes' isn't currently enabled").
9
+ */
10
+ declare const bvhBoneMap: Record<string, VRMHumanBoneName>;
11
+ export default bvhBoneMap;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Maps standard BVH bone names to VRM humanoid bone names.
3
+ *
4
+ * Kept as a `.ts` module rather than a `.json` import on purpose: importing JSON requires
5
+ * import attributes (`with { type: 'json' }`), which survive into the published ESM build and
6
+ * break in-browser bundlers like the docs' Sandpack (Babel: "experimental syntax
7
+ * 'moduleAttributes' isn't currently enabled").
8
+ */
9
+ const bvhBoneMap = {
10
+ Hips: 'hips',
11
+ Spine: 'spine',
12
+ Spine1: 'chest',
13
+ Spine2: 'upperChest',
14
+ Neck: 'neck',
15
+ Head: 'head',
16
+ LeftShoulder: 'leftShoulder',
17
+ LeftArm: 'leftUpperArm',
18
+ LeftForeArm: 'leftLowerArm',
19
+ LeftHand: 'leftHand',
20
+ RightShoulder: 'rightShoulder',
21
+ RightArm: 'rightUpperArm',
22
+ RightForeArm: 'rightLowerArm',
23
+ RightHand: 'rightHand',
24
+ LeftUpLeg: 'leftUpperLeg',
25
+ LeftLeg: 'leftLowerLeg',
26
+ LeftFoot: 'leftFoot',
27
+ LeftToe: 'leftToes',
28
+ RightUpLeg: 'rightUpperLeg',
29
+ RightLeg: 'rightLowerLeg',
30
+ RightFoot: 'rightFoot',
31
+ RightToe: 'rightToes',
32
+ };
33
+ export default bvhBoneMap;
@@ -25,4 +25,4 @@ export declare function flattenCharacterAnimationOptions(options: Exclude<Charac
25
25
  export declare function loadCharacterAnimation(model: CharacterModel, url: string | DefaultUrl, type?: CharacterAnimationOptions['type'], removeXZMovement?: boolean, trimStartTime?: number | undefined, trimEndTime?: number | undefined, boneMap?: Record<string, VRMHumanBoneName> | undefined, scaleTime?: number | undefined, mask?: CharacterAnimationMask): Promise<AnimationClip>;
26
26
  export declare const mixamoBoneMap: Record<string, VRMHumanBoneName>;
27
27
  export declare const bvhBoneMap: Record<string, VRMHumanBoneName>;
28
- export declare const allBoneNames: string[];
28
+ export declare const allBoneNames: Array<string>;
@@ -4,10 +4,10 @@ import { BVHLoader } from 'three/examples/jsm/loaders/BVHLoader.js';
4
4
  import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js';
5
5
  import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
6
6
  import { applyAnimationBoneMap } from './bone-map.js';
7
- import _bvhBoneMap from './bvh-bone-map.json' with { type: 'json' };
7
+ import _bvhBoneMap from './bvh-bone-map.js';
8
8
  import { resolveDefaultCharacterAnimationUrl } from './default.js';
9
9
  import { applyMask } from './mask.js';
10
- import _mixamoBoneMap from './mixamo-bone-map.json' with { type: 'json' };
10
+ import _mixamoBoneMap from './mixamo-bone-map.js';
11
11
  import { scaleAnimationClipTime, trimAnimationClip } from './utils.js';
12
12
  import { vrmaLoader } from '../model/index.js';
13
13
  //helper variables for the quaternion retargeting
@@ -0,0 +1,11 @@
1
+ import type { VRMHumanBoneName } from '@pixiv/three-vrm';
2
+ /**
3
+ * Maps Mixamo bone names (`mixamorig*`) to VRM humanoid bone names.
4
+ *
5
+ * Kept as a `.ts` module rather than a `.json` import on purpose: importing JSON requires
6
+ * import attributes (`with { type: 'json' }`), which survive into the published ESM build and
7
+ * break in-browser bundlers like the docs' Sandpack (Babel: "experimental syntax
8
+ * 'moduleAttributes' isn't currently enabled").
9
+ */
10
+ declare const mixamoBoneMap: Record<string, VRMHumanBoneName>;
11
+ export default mixamoBoneMap;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Maps Mixamo bone names (`mixamorig*`) to VRM humanoid bone names.
3
+ *
4
+ * Kept as a `.ts` module rather than a `.json` import on purpose: importing JSON requires
5
+ * import attributes (`with { type: 'json' }`), which survive into the published ESM build and
6
+ * break in-browser bundlers like the docs' Sandpack (Babel: "experimental syntax
7
+ * 'moduleAttributes' isn't currently enabled").
8
+ */
9
+ const mixamoBoneMap = {
10
+ mixamorigHips: 'hips',
11
+ mixamorigSpine: 'spine',
12
+ mixamorigSpine1: 'chest',
13
+ mixamorigSpine2: 'upperChest',
14
+ mixamorigNeck: 'neck',
15
+ mixamorigHead: 'head',
16
+ mixamorigLeftShoulder: 'leftShoulder',
17
+ mixamorigLeftArm: 'leftUpperArm',
18
+ mixamorigLeftForeArm: 'leftLowerArm',
19
+ mixamorigLeftHand: 'leftHand',
20
+ mixamorigLeftHandThumb1: 'leftThumbMetacarpal',
21
+ mixamorigLeftHandThumb2: 'leftThumbProximal',
22
+ mixamorigLeftHandThumb3: 'leftThumbDistal',
23
+ mixamorigLeftHandIndex1: 'leftIndexProximal',
24
+ mixamorigLeftHandIndex2: 'leftIndexIntermediate',
25
+ mixamorigLeftHandIndex3: 'leftIndexDistal',
26
+ mixamorigLeftHandMiddle1: 'leftMiddleProximal',
27
+ mixamorigLeftHandMiddle2: 'leftMiddleIntermediate',
28
+ mixamorigLeftHandMiddle3: 'leftMiddleDistal',
29
+ mixamorigLeftHandRing1: 'leftRingProximal',
30
+ mixamorigLeftHandRing2: 'leftRingIntermediate',
31
+ mixamorigLeftHandRing3: 'leftRingDistal',
32
+ mixamorigLeftHandPinky1: 'leftLittleProximal',
33
+ mixamorigLeftHandPinky2: 'leftLittleIntermediate',
34
+ mixamorigLeftHandPinky3: 'leftLittleDistal',
35
+ mixamorigRightShoulder: 'rightShoulder',
36
+ mixamorigRightArm: 'rightUpperArm',
37
+ mixamorigRightForeArm: 'rightLowerArm',
38
+ mixamorigRightHand: 'rightHand',
39
+ mixamorigRightHandPinky1: 'rightLittleProximal',
40
+ mixamorigRightHandPinky2: 'rightLittleIntermediate',
41
+ mixamorigRightHandPinky3: 'rightLittleDistal',
42
+ mixamorigRightHandRing1: 'rightRingProximal',
43
+ mixamorigRightHandRing2: 'rightRingIntermediate',
44
+ mixamorigRightHandRing3: 'rightRingDistal',
45
+ mixamorigRightHandMiddle1: 'rightMiddleProximal',
46
+ mixamorigRightHandMiddle2: 'rightMiddleIntermediate',
47
+ mixamorigRightHandMiddle3: 'rightMiddleDistal',
48
+ mixamorigRightHandIndex1: 'rightIndexProximal',
49
+ mixamorigRightHandIndex2: 'rightIndexIntermediate',
50
+ mixamorigRightHandIndex3: 'rightIndexDistal',
51
+ mixamorigRightHandThumb1: 'rightThumbMetacarpal',
52
+ mixamorigRightHandThumb2: 'rightThumbProximal',
53
+ mixamorigRightHandThumb3: 'rightThumbDistal',
54
+ mixamorigLeftUpLeg: 'leftUpperLeg',
55
+ mixamorigLeftLeg: 'leftLowerLeg',
56
+ mixamorigLeftFoot: 'leftFoot',
57
+ mixamorigLeftToeBase: 'leftToes',
58
+ mixamorigRightUpLeg: 'rightUpperLeg',
59
+ mixamorigRightLeg: 'rightLowerLeg',
60
+ mixamorigRightFoot: 'rightFoot',
61
+ mixamorigRightToeBase: 'rightToes',
62
+ };
63
+ export default mixamoBoneMap;
@@ -1,18 +1,62 @@
1
- import { Box3, DoubleSide, InstancedMesh, Matrix4, Mesh, Ray, Vector3, } from 'three';
2
- import { computeBoundsTree, StaticGeometryGenerator, ExtendedTriangle } from 'three-mesh-bvh';
1
+ import { BatchedMesh, Box3, BufferAttribute, BufferGeometry, DoubleSide, InstancedMesh, Matrix4, Mesh, Ray, SkinnedMesh, Vector3, } from 'three';
2
+ import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
3
+ import { computeBoundsTree, ExtendedTriangle } from 'three-mesh-bvh';
3
4
  const rayHelper = new Ray();
4
5
  const farPointHelper = new Vector3();
5
6
  const boxHelper = new Box3();
6
7
  const triangleHelper = new ExtendedTriangle();
7
8
  const matrixHelper = new Matrix4();
8
9
  /**
9
- * Identifies which geometries StaticGeometryGenerator can merge together. Geometries are mergeable when they share
10
- * the same set of attributes and agree on whether they are indexed, so two geometries with an equal signature are
11
- * guaranteed not to trigger the "Make sure all geometries have the same number of attributes." error.
10
+ * Bakes a mesh's collision geometry into `matrix` space. A bvh only needs positions and (optionally) an index, so
11
+ * everything else is dropped - this keeps the merge below trivial and sidesteps three's "geometries must have the
12
+ * same attributes" restriction that a body mixing meshes with different attribute sets (e.g. gltf primitives) hits.
13
+ * Positions are read into a fresh float buffer so interleaved or quantized (e.g. meshopt/draco int) source
14
+ * attributes are de-interleaved and de-quantized - otherwise applyMatrix4 would overflow the source storage and
15
+ * mergeGeometries would reject the mismatching layouts.
12
16
  */
13
- function geometryAttributeSignature(geometry) {
14
- const attributeNames = Object.keys(geometry.attributes).sort().join(',');
15
- return `${geometry.index == null ? 'non-indexed' : 'indexed'}:${attributeNames}`;
17
+ function bakeCollisionGeometry(mesh, matrix) {
18
+ const source = mesh.geometry.getAttribute('position');
19
+ const position = new BufferAttribute(new Float32Array(source.count * 3), 3);
20
+ for (let i = 0; i < source.count; i++) {
21
+ position.setXYZ(i, source.getX(i), source.getY(i), source.getZ(i));
22
+ }
23
+ const geometry = new BufferGeometry();
24
+ geometry.setAttribute('position', position);
25
+ if (mesh.geometry.index != null) {
26
+ geometry.setIndex(mesh.geometry.index.clone());
27
+ }
28
+ return geometry.applyMatrix4(matrix);
29
+ }
30
+ /**
31
+ * A mesh whose positions are actively displaced by morph targets - its resting geometry doesn't match what's drawn,
32
+ * so (like a skinned mesh) it is skipped rather than baked into a wrong-shaped collider.
33
+ */
34
+ function isMorphDeformed(mesh) {
35
+ return (mesh.geometry.morphAttributes.position != null &&
36
+ (mesh.morphTargetInfluences?.some((influence) => influence !== 0) ?? false));
37
+ }
38
+ /**
39
+ * Extracts one geometry of a BatchedMesh into a standalone geometry in its local space. A BatchedMesh packs every
40
+ * geometry into one shared buffer, so a single bvh over that buffer would collide against all of them at once -
41
+ * instead each geometry gets its own bvh, referenced per instance with the instance's matrix at query time.
42
+ */
43
+ function extractBatchedCollisionGeometry(mesh, geometryId) {
44
+ const range = mesh.getGeometryRangeAt(geometryId);
45
+ if (range == null) {
46
+ throw new Error(`BvhPhysicsWorld: BatchedMesh has no geometry ${geometryId}`);
47
+ }
48
+ const source = mesh.geometry.getAttribute('position');
49
+ const index = mesh.geometry.index;
50
+ const start = index != null ? range.indexStart : range.vertexStart;
51
+ const count = index != null ? range.indexCount : range.vertexCount;
52
+ const position = new BufferAttribute(new Float32Array(count * 3), 3);
53
+ for (let i = 0; i < count; i++) {
54
+ const vertexIndex = index != null ? index.getX(start + i) : start + i;
55
+ position.setXYZ(i, source.getX(vertexIndex), source.getY(vertexIndex), source.getZ(vertexIndex));
56
+ }
57
+ const geometry = new BufferGeometry();
58
+ geometry.setAttribute('position', position);
59
+ return geometry;
16
60
  }
17
61
  export class BvhPhysicsWorld {
18
62
  bodies = [];
@@ -38,15 +82,23 @@ export class BvhPhysicsWorld {
38
82
  }
39
83
  computeBvhEntries(object, isStatic) {
40
84
  object.updateWorldMatrix(true, true);
85
+ //Static bodies are baked in world space; kinematic bodies are baked in the body's local space so that its
86
+ //(changing) world matrix can be applied at query time instead (see computeMatrix), hence the inverse here.
87
+ const worldToBakeSpace = isStatic ? undefined : new Matrix4().copy(object.matrixWorld).invert();
88
+ const bakeMatrix = new Matrix4();
41
89
  const result = [];
42
- //StaticGeometryGenerator merges all meshes into a single geometry and throws if they don't all expose
43
- //the same attributes ("Make sure all geometries have the same number of attributes."). Group the meshes by
44
- //their attribute signature so every merge only ever sees compatible geometries and generate one bvh per group.
45
- const meshGroups = new Map();
46
- object.traverse((entry) => {
90
+ //A merge of collision geometries only works when they agree on being indexed, so keep indexed and
91
+ //non-indexed meshes apart and turn each group into its own bvh.
92
+ const indexedGeometries = [];
93
+ const nonIndexedGeometries = [];
94
+ //traverseVisible so hidden meshes (toggled decorations, LODs) don't silently become colliders.
95
+ object.traverseVisible((entry) => {
47
96
  if (entry instanceof InstancedMesh) {
48
97
  const bvh = computeBoundsTree.apply(entry.geometry);
49
- result.push(...Array.from({ length: entry.instanceMatrix.count }, (_, instanceIndex) => ({
98
+ result.push(
99
+ //entry.count (active instances), not instanceMatrix.count (allocated capacity), to avoid phantom
100
+ //colliders at the stale matrices of unused instance slots.
101
+ ...Array.from({ length: entry.count }, (_, instanceIndex) => ({
50
102
  object: entry,
51
103
  bvh,
52
104
  instanceIndex,
@@ -54,30 +106,47 @@ export class BvhPhysicsWorld {
54
106
  })));
55
107
  return;
56
108
  }
57
- if (entry instanceof Mesh) {
58
- const signature = geometryAttributeSignature(entry.geometry);
59
- let group = meshGroups.get(signature);
60
- if (group == null) {
61
- meshGroups.set(signature, (group = []));
109
+ if (entry instanceof BatchedMesh) {
110
+ //Each geometry in the batch gets its own bvh, referenced once per visible instance with the instance's
111
+ //matrix applied at query time (like InstancedMesh). Instances deleted before addBody aren't handled -
112
+ //three offers no way to enumerate active instances across the gaps a deletion leaves.
113
+ const geometryBvhs = new Map();
114
+ for (let instanceIndex = 0; instanceIndex < entry.instanceCount; instanceIndex++) {
115
+ if (!entry.getVisibleAt(instanceIndex)) {
116
+ continue;
117
+ }
118
+ const geometryId = entry.getGeometryIdAt(instanceIndex);
119
+ let bvh = geometryBvhs.get(geometryId);
120
+ if (bvh == null) {
121
+ bvh = computeBoundsTree.apply(extractBatchedCollisionGeometry(entry, geometryId));
122
+ geometryBvhs.set(geometryId, bvh);
123
+ }
124
+ result.push({ object: entry, bvh, instanceIndex, isStatic });
62
125
  }
63
- group.push(entry);
126
+ return;
64
127
  }
65
- });
66
- if (meshGroups.size > 0) {
67
- const parent = object.parent;
68
- if (!isStatic) {
69
- object.parent = null;
70
- object.updateMatrixWorld(true);
128
+ //skip skinned and morph-deformed meshes (their shape comes from per-vertex deformation a single baked
129
+ //matrix can't capture - use a primitive collider) and meshes that have no positions to collide against.
130
+ if (!(entry instanceof Mesh) ||
131
+ entry instanceof SkinnedMesh ||
132
+ isMorphDeformed(entry) ||
133
+ entry.geometry.getAttribute('position') == null) {
134
+ return;
71
135
  }
72
- for (const meshes of meshGroups.values()) {
73
- const geometry = new StaticGeometryGenerator(meshes).generate();
74
- const bvh = computeBoundsTree.apply(geometry);
75
- result.push({ object, bvh, isStatic });
136
+ bakeMatrix.copy(entry.matrixWorld);
137
+ if (worldToBakeSpace != null) {
138
+ bakeMatrix.premultiply(worldToBakeSpace);
76
139
  }
77
- if (!isStatic) {
78
- object.parent = parent;
79
- object.updateMatrixWorld(true);
140
+ const geometry = bakeCollisionGeometry(entry, bakeMatrix);
141
+ (geometry.index == null ? nonIndexedGeometries : indexedGeometries).push(geometry);
142
+ });
143
+ for (const geometries of [indexedGeometries, nonIndexedGeometries]) {
144
+ if (geometries.length === 0) {
145
+ continue;
80
146
  }
147
+ const geometry = geometries.length === 1 ? geometries[0] : mergeGeometries(geometries);
148
+ const bvh = computeBoundsTree.apply(geometry);
149
+ result.push({ object, bvh, isStatic });
81
150
  }
82
151
  return result;
83
152
  }
package/package.json CHANGED
@@ -21,7 +21,7 @@
21
21
  "peerDependencies": {
22
22
  "three": "*"
23
23
  },
24
- "version": "0.2.25",
24
+ "version": "0.2.27",
25
25
  "type": "module",
26
26
  "dependencies": {
27
27
  "@pixiv/three-vrm": "^3.4.2",
@@ -1,24 +0,0 @@
1
- {
2
- "Hips": "hips",
3
- "Spine": "spine",
4
- "Spine1": "chest",
5
- "Spine2": "upperChest",
6
- "Neck": "neck",
7
- "Head": "head",
8
- "LeftShoulder": "leftShoulder",
9
- "LeftArm": "leftUpperArm",
10
- "LeftForeArm": "leftLowerArm",
11
- "LeftHand": "leftHand",
12
- "RightShoulder": "rightShoulder",
13
- "RightArm": "rightUpperArm",
14
- "RightForeArm": "rightLowerArm",
15
- "RightHand": "rightHand",
16
- "LeftUpLeg": "leftUpperLeg",
17
- "LeftLeg": "leftLowerLeg",
18
- "LeftFoot": "leftFoot",
19
- "LeftToe": "leftToes",
20
- "RightUpLeg": "rightUpperLeg",
21
- "RightLeg": "rightLowerLeg",
22
- "RightFoot": "rightFoot",
23
- "RightToe": "rightToes"
24
- }
@@ -1,54 +0,0 @@
1
- {
2
- "mixamorigHips": "hips",
3
- "mixamorigSpine": "spine",
4
- "mixamorigSpine1": "chest",
5
- "mixamorigSpine2": "upperChest",
6
- "mixamorigNeck": "neck",
7
- "mixamorigHead": "head",
8
- "mixamorigLeftShoulder": "leftShoulder",
9
- "mixamorigLeftArm": "leftUpperArm",
10
- "mixamorigLeftForeArm": "leftLowerArm",
11
- "mixamorigLeftHand": "leftHand",
12
- "mixamorigLeftHandThumb1": "leftThumbMetacarpal",
13
- "mixamorigLeftHandThumb2": "leftThumbProximal",
14
- "mixamorigLeftHandThumb3": "leftThumbDistal",
15
- "mixamorigLeftHandIndex1": "leftIndexProximal",
16
- "mixamorigLeftHandIndex2": "leftIndexIntermediate",
17
- "mixamorigLeftHandIndex3": "leftIndexDistal",
18
- "mixamorigLeftHandMiddle1": "leftMiddleProximal",
19
- "mixamorigLeftHandMiddle2": "leftMiddleIntermediate",
20
- "mixamorigLeftHandMiddle3": "leftMiddleDistal",
21
- "mixamorigLeftHandRing1": "leftRingProximal",
22
- "mixamorigLeftHandRing2": "leftRingIntermediate",
23
- "mixamorigLeftHandRing3": "leftRingDistal",
24
- "mixamorigLeftHandPinky1": "leftLittleProximal",
25
- "mixamorigLeftHandPinky2": "leftLittleIntermediate",
26
- "mixamorigLeftHandPinky3": "leftLittleDistal",
27
- "mixamorigRightShoulder": "rightShoulder",
28
- "mixamorigRightArm": "rightUpperArm",
29
- "mixamorigRightForeArm": "rightLowerArm",
30
- "mixamorigRightHand": "rightHand",
31
- "mixamorigRightHandPinky1": "rightLittleProximal",
32
- "mixamorigRightHandPinky2": "rightLittleIntermediate",
33
- "mixamorigRightHandPinky3": "rightLittleDistal",
34
- "mixamorigRightHandRing1": "rightRingProximal",
35
- "mixamorigRightHandRing2": "rightRingIntermediate",
36
- "mixamorigRightHandRing3": "rightRingDistal",
37
- "mixamorigRightHandMiddle1": "rightMiddleProximal",
38
- "mixamorigRightHandMiddle2": "rightMiddleIntermediate",
39
- "mixamorigRightHandMiddle3": "rightMiddleDistal",
40
- "mixamorigRightHandIndex1": "rightIndexProximal",
41
- "mixamorigRightHandIndex2": "rightIndexIntermediate",
42
- "mixamorigRightHandIndex3": "rightIndexDistal",
43
- "mixamorigRightHandThumb1": "rightThumbMetacarpal",
44
- "mixamorigRightHandThumb2": "rightThumbProximal",
45
- "mixamorigRightHandThumb3": "rightThumbDistal",
46
- "mixamorigLeftUpLeg": "leftUpperLeg",
47
- "mixamorigLeftLeg": "leftLowerLeg",
48
- "mixamorigLeftFoot": "leftFoot",
49
- "mixamorigLeftToeBase": "leftToes",
50
- "mixamorigRightUpLeg": "rightUpperLeg",
51
- "mixamorigRightLeg": "rightLowerLeg",
52
- "mixamorigRightFoot": "rightFoot",
53
- "mixamorigRightToeBase": "rightToes"
54
- }