@pmndrs/viverse 0.2.26 → 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.
@@ -1,4 +1,4 @@
1
- import { Box3, BufferGeometry, DoubleSide, InstancedMesh, Matrix4, Mesh, Ray, Vector3, } from 'three';
1
+ import { BatchedMesh, Box3, BufferAttribute, BufferGeometry, DoubleSide, InstancedMesh, Matrix4, Mesh, Ray, SkinnedMesh, Vector3, } from 'three';
2
2
  import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
3
3
  import { computeBoundsTree, ExtendedTriangle } from 'three-mesh-bvh';
4
4
  const rayHelper = new Ray();
@@ -10,15 +10,54 @@ const matrixHelper = new Matrix4();
10
10
  * Bakes a mesh's collision geometry into `matrix` space. A bvh only needs positions and (optionally) an index, so
11
11
  * everything else is dropped - this keeps the merge below trivial and sidesteps three's "geometries must have the
12
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.
13
16
  */
14
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
+ }
15
23
  const geometry = new BufferGeometry();
16
- geometry.setAttribute('position', mesh.geometry.getAttribute('position').clone());
24
+ geometry.setAttribute('position', position);
17
25
  if (mesh.geometry.index != null) {
18
26
  geometry.setIndex(mesh.geometry.index.clone());
19
27
  }
20
28
  return geometry.applyMatrix4(matrix);
21
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;
60
+ }
22
61
  export class BvhPhysicsWorld {
23
62
  bodies = [];
24
63
  sensors = [];
@@ -52,10 +91,14 @@ export class BvhPhysicsWorld {
52
91
  //non-indexed meshes apart and turn each group into its own bvh.
53
92
  const indexedGeometries = [];
54
93
  const nonIndexedGeometries = [];
55
- object.traverse((entry) => {
94
+ //traverseVisible so hidden meshes (toggled decorations, LODs) don't silently become colliders.
95
+ object.traverseVisible((entry) => {
56
96
  if (entry instanceof InstancedMesh) {
57
97
  const bvh = computeBoundsTree.apply(entry.geometry);
58
- 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) => ({
59
102
  object: entry,
60
103
  bvh,
61
104
  instanceIndex,
@@ -63,7 +106,31 @@ export class BvhPhysicsWorld {
63
106
  })));
64
107
  return;
65
108
  }
66
- if (!(entry instanceof Mesh)) {
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 });
125
+ }
126
+ return;
127
+ }
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) {
67
134
  return;
68
135
  }
69
136
  bakeMatrix.copy(entry.matrixWorld);
package/package.json CHANGED
@@ -21,7 +21,7 @@
21
21
  "peerDependencies": {
22
22
  "three": "*"
23
23
  },
24
- "version": "0.2.26",
24
+ "version": "0.2.27",
25
25
  "type": "module",
26
26
  "dependencies": {
27
27
  "@pixiv/three-vrm": "^3.4.2",