@opsydyn/effect-three 0.0.0-stage → 0.1.0

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.
Files changed (166) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/LICENSE +21 -0
  3. package/README.md +191 -2
  4. package/dist/index-4045wme0.js +129 -0
  5. package/dist/index-4045wme0.js.map +10 -0
  6. package/dist/index-ef8pbv9g.js +5118 -0
  7. package/dist/index-ef8pbv9g.js.map +40 -0
  8. package/dist/index.d.ts +12 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +1361 -0
  11. package/dist/index.js.map +14 -0
  12. package/dist/kernel/animation.d.ts +14 -0
  13. package/dist/kernel/animation.d.ts.map +1 -0
  14. package/dist/kernel/behaviours.d.ts +16 -0
  15. package/dist/kernel/behaviours.d.ts.map +1 -0
  16. package/dist/kernel/bodies.d.ts +60 -0
  17. package/dist/kernel/bodies.d.ts.map +1 -0
  18. package/dist/kernel/bodySources.d.ts +47 -0
  19. package/dist/kernel/bodySources.d.ts.map +1 -0
  20. package/dist/kernel/build.d.ts +564 -0
  21. package/dist/kernel/build.d.ts.map +1 -0
  22. package/dist/kernel/cameraMotion.d.ts +39 -0
  23. package/dist/kernel/cameraMotion.d.ts.map +1 -0
  24. package/dist/kernel/clock.d.ts +17 -0
  25. package/dist/kernel/clock.d.ts.map +1 -0
  26. package/dist/kernel/contactShadowPasses.d.ts +47 -0
  27. package/dist/kernel/contactShadowPasses.d.ts.map +1 -0
  28. package/dist/kernel/description.d.ts +1120 -0
  29. package/dist/kernel/description.d.ts.map +1 -0
  30. package/dist/kernel/diff.d.ts +90 -0
  31. package/dist/kernel/diff.d.ts.map +1 -0
  32. package/dist/kernel/environmentPresets.d.ts +30 -0
  33. package/dist/kernel/environmentPresets.d.ts.map +1 -0
  34. package/dist/kernel/equality.d.ts +64 -0
  35. package/dist/kernel/equality.d.ts.map +1 -0
  36. package/dist/kernel/frames.d.ts +55 -0
  37. package/dist/kernel/frames.d.ts.map +1 -0
  38. package/dist/kernel/index.d.ts +37 -0
  39. package/dist/kernel/index.d.ts.map +1 -0
  40. package/dist/kernel/index.js +335 -0
  41. package/dist/kernel/index.js.map +9 -0
  42. package/dist/kernel/instances.d.ts +26 -0
  43. package/dist/kernel/instances.d.ts.map +1 -0
  44. package/dist/kernel/labels.d.ts +45 -0
  45. package/dist/kernel/labels.d.ts.map +1 -0
  46. package/dist/kernel/loop.d.ts +49 -0
  47. package/dist/kernel/loop.d.ts.map +1 -0
  48. package/dist/kernel/materials.d.ts +24 -0
  49. package/dist/kernel/materials.d.ts.map +1 -0
  50. package/dist/kernel/motion.d.ts +36 -0
  51. package/dist/kernel/motion.d.ts.map +1 -0
  52. package/dist/kernel/orbit.d.ts +127 -0
  53. package/dist/kernel/orbit.d.ts.map +1 -0
  54. package/dist/kernel/overlays.d.ts +16 -0
  55. package/dist/kernel/overlays.d.ts.map +1 -0
  56. package/dist/kernel/performance.d.ts +41 -0
  57. package/dist/kernel/performance.d.ts.map +1 -0
  58. package/dist/kernel/physics.d.ts +166 -0
  59. package/dist/kernel/physics.d.ts.map +1 -0
  60. package/dist/kernel/physicsDebug.d.ts +18 -0
  61. package/dist/kernel/physicsDebug.d.ts.map +1 -0
  62. package/dist/kernel/pointer.d.ts +357 -0
  63. package/dist/kernel/pointer.d.ts.map +1 -0
  64. package/dist/kernel/renderKit.d.ts +48 -0
  65. package/dist/kernel/renderKit.d.ts.map +1 -0
  66. package/dist/kernel/renderer.d.ts +83 -0
  67. package/dist/kernel/renderer.d.ts.map +1 -0
  68. package/dist/kernel/resources.d.ts +112 -0
  69. package/dist/kernel/resources.d.ts.map +1 -0
  70. package/dist/kernel/sceneGraph.d.ts +155 -0
  71. package/dist/kernel/sceneGraph.d.ts.map +1 -0
  72. package/dist/kernel/sceneIndex.d.ts +90 -0
  73. package/dist/kernel/sceneIndex.d.ts.map +1 -0
  74. package/dist/kernel/stage.d.ts +87 -0
  75. package/dist/kernel/stage.d.ts.map +1 -0
  76. package/dist/kernel/textLayout.d.ts +29 -0
  77. package/dist/kernel/textLayout.d.ts.map +1 -0
  78. package/dist/kernel/tween.d.ts +35 -0
  79. package/dist/kernel/tween.d.ts.map +1 -0
  80. package/dist/kernel/xrInput.d.ts +107 -0
  81. package/dist/kernel/xrInput.d.ts.map +1 -0
  82. package/dist/kernel/xrLocomotion.d.ts +38 -0
  83. package/dist/kernel/xrLocomotion.d.ts.map +1 -0
  84. package/dist/runtime/FrameSource.d.ts +29 -0
  85. package/dist/runtime/FrameSource.d.ts.map +1 -0
  86. package/dist/runtime/ResourceRegistry.d.ts +18 -0
  87. package/dist/runtime/ResourceRegistry.d.ts.map +1 -0
  88. package/dist/runtime/SceneRuntime.d.ts +68 -0
  89. package/dist/runtime/SceneRuntime.d.ts.map +1 -0
  90. package/dist/runtime/SceneSchema.d.ts +2536 -0
  91. package/dist/runtime/SceneSchema.d.ts.map +1 -0
  92. package/dist/runtime/errors.d.ts +19 -0
  93. package/dist/runtime/errors.d.ts.map +1 -0
  94. package/dist/runtime/index.d.ts +11 -0
  95. package/dist/runtime/index.d.ts.map +1 -0
  96. package/dist/webgl/contactShadows.d.ts +5 -0
  97. package/dist/webgl/contactShadows.d.ts.map +1 -0
  98. package/dist/webgl/index.d.ts +11 -0
  99. package/dist/webgl/index.d.ts.map +1 -0
  100. package/dist/webgl/index.js +239 -0
  101. package/dist/webgl/index.js.map +13 -0
  102. package/dist/webgl/shaderGrid.d.ts +27 -0
  103. package/dist/webgl/shaderGrid.d.ts.map +1 -0
  104. package/dist/webgl/sky.d.ts +4 -0
  105. package/dist/webgl/sky.d.ts.map +1 -0
  106. package/dist/webgpu/contactShadows.d.ts +5 -0
  107. package/dist/webgpu/contactShadows.d.ts.map +1 -0
  108. package/dist/webgpu/index.d.ts +10 -0
  109. package/dist/webgpu/index.d.ts.map +1 -0
  110. package/dist/webgpu/index.js +216 -0
  111. package/dist/webgpu/index.js.map +13 -0
  112. package/dist/webgpu/shaderGrid.d.ts +5 -0
  113. package/dist/webgpu/shaderGrid.d.ts.map +1 -0
  114. package/dist/webgpu/sky.d.ts +4 -0
  115. package/dist/webgpu/sky.d.ts.map +1 -0
  116. package/package.json +82 -4
  117. package/src/index.ts +11 -0
  118. package/src/kernel/animation.ts +24 -0
  119. package/src/kernel/behaviours.ts +36 -0
  120. package/src/kernel/bodies.ts +338 -0
  121. package/src/kernel/bodySources.ts +203 -0
  122. package/src/kernel/build.ts +1325 -0
  123. package/src/kernel/cameraMotion.ts +202 -0
  124. package/src/kernel/clock.ts +35 -0
  125. package/src/kernel/contactShadowPasses.ts +207 -0
  126. package/src/kernel/description.ts +1197 -0
  127. package/src/kernel/diff.ts +383 -0
  128. package/src/kernel/environmentPresets.ts +38 -0
  129. package/src/kernel/equality.ts +604 -0
  130. package/src/kernel/frames.ts +124 -0
  131. package/src/kernel/index.ts +274 -0
  132. package/src/kernel/instances.ts +97 -0
  133. package/src/kernel/labels.ts +48 -0
  134. package/src/kernel/loop.ts +248 -0
  135. package/src/kernel/materials.ts +258 -0
  136. package/src/kernel/motion.ts +75 -0
  137. package/src/kernel/orbit.ts +554 -0
  138. package/src/kernel/overlays.ts +130 -0
  139. package/src/kernel/performance.ts +101 -0
  140. package/src/kernel/physics.ts +361 -0
  141. package/src/kernel/physicsDebug.ts +68 -0
  142. package/src/kernel/pointer.ts +785 -0
  143. package/src/kernel/renderKit.ts +53 -0
  144. package/src/kernel/renderer.ts +136 -0
  145. package/src/kernel/resources.ts +504 -0
  146. package/src/kernel/sceneGraph.ts +1440 -0
  147. package/src/kernel/sceneIndex.ts +219 -0
  148. package/src/kernel/stage.ts +157 -0
  149. package/src/kernel/textLayout.ts +102 -0
  150. package/src/kernel/tween.ts +80 -0
  151. package/src/kernel/xrInput.ts +230 -0
  152. package/src/kernel/xrLocomotion.ts +72 -0
  153. package/src/runtime/FrameSource.ts +41 -0
  154. package/src/runtime/ResourceRegistry.ts +24 -0
  155. package/src/runtime/SceneRuntime.ts +229 -0
  156. package/src/runtime/SceneSchema.ts +983 -0
  157. package/src/runtime/errors.ts +18 -0
  158. package/src/runtime/index.ts +10 -0
  159. package/src/webgl/contactShadows.ts +67 -0
  160. package/src/webgl/index.ts +19 -0
  161. package/src/webgl/shaderGrid.ts +196 -0
  162. package/src/webgl/sky.ts +42 -0
  163. package/src/webgpu/contactShadows.ts +83 -0
  164. package/src/webgpu/index.ts +19 -0
  165. package/src/webgpu/shaderGrid.ts +158 -0
  166. package/src/webgpu/sky.ts +35 -0
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Frame-rate monitoring with a performance factor, after Drei's `PerformanceMonitor`
3
+ * (`drei-main/src/core/PerformanceMonitor.tsx` at `bf6f4ad`): frames are averaged over
4
+ * windows of `ms`; once `iterations` averages are in, the factor steps down when more than
5
+ * `threshold` of them fell below the lower bound, and up when more than `threshold` rose
6
+ * above the upper bound. The application decides what the factor means, typically a
7
+ * render resolution.
8
+ *
9
+ * Two differences from Drei: the factor starts at 1 (draw at full resolution until proven
10
+ * slow), and a gap between frames longer than `idleMs` restarts the window, because an
11
+ * on-demand scene that is asleep is idle, not slow.
12
+ *
13
+ * @module
14
+ */
15
+
16
+ export type PerformanceOptions = {
17
+ /** Milliseconds per fps average. Default 250. */
18
+ readonly ms?: number;
19
+ /** Averages per decision. Default 10. */
20
+ readonly iterations?: number;
21
+ /** Share of averages that must agree to move the factor. Default 0.75. */
22
+ readonly threshold?: number;
23
+ /** How far one decision moves the factor. Default 0.1. */
24
+ readonly step?: number;
25
+ /** Lower and upper fps bounds for a refresh rate. Default as Drei. */
26
+ readonly bounds?: (refreshRate: number) => readonly [lower: number, upper: number];
27
+ /** A pause between frames longer than this restarts measurement. Default 100. */
28
+ readonly idleMs?: number;
29
+ };
30
+
31
+ /** A change of performance factor, with the fps average that caused it. */
32
+ export type PerformanceChange = { readonly fps: number; readonly factor: number };
33
+
34
+ export type PerformanceMonitor = {
35
+ /** Records a frame at `nowMs`; returns a change when the factor moved. */
36
+ readonly sample: (nowMs: number) => PerformanceChange | undefined;
37
+ readonly factor: () => number;
38
+ };
39
+
40
+ const defaultBounds = (refreshRate: number): readonly [number, number] =>
41
+ refreshRate > 100 ? [60, 100] : [40, 60];
42
+
43
+ /** Creates a frame-rate monitor. Allocation-free per frame except when a decision is made. */
44
+ export const createPerformanceMonitor = (options: PerformanceOptions = {}): PerformanceMonitor => {
45
+ const ms = options.ms ?? 250;
46
+ const iterations = options.iterations ?? 10;
47
+ const threshold = options.threshold ?? 0.75;
48
+ const step = options.step ?? 0.1;
49
+ const bounds = options.bounds ?? defaultBounds;
50
+ const idleMs = options.idleMs ?? 100;
51
+ const averages = new Float64Array(iterations);
52
+ let collected = 0;
53
+ let windowStart: number | undefined;
54
+ let lastFrame: number | undefined;
55
+ let framesInWindow = 0;
56
+ let refreshRate = 0;
57
+ let factor = 1;
58
+
59
+ const restart = (nowMs: number): void => {
60
+ windowStart = nowMs;
61
+ framesInWindow = 0;
62
+ };
63
+
64
+ const decide = (fps: number): PerformanceChange | undefined => {
65
+ collected = 0;
66
+ const [lower, upper] = bounds(refreshRate);
67
+ let above = 0;
68
+ let below = 0;
69
+ for (const average of averages) {
70
+ if (average >= upper) above++;
71
+ if (average < lower) below++;
72
+ }
73
+ const before = factor;
74
+ if (above > iterations * threshold) factor = Math.min(1, factor + step);
75
+ if (below > iterations * threshold) factor = Math.max(0, factor - step);
76
+ // Round away floating-point drift from repeated steps.
77
+ factor = Math.round(factor * 1000) / 1000;
78
+ return factor === before ? undefined : { fps, factor };
79
+ };
80
+
81
+ return {
82
+ sample: (nowMs) => {
83
+ const idle = lastFrame === undefined || nowMs - lastFrame > idleMs;
84
+ lastFrame = nowMs;
85
+ if (idle || windowStart === undefined) {
86
+ restart(nowMs);
87
+ return undefined;
88
+ }
89
+ framesInWindow++;
90
+ const elapsed = nowMs - windowStart;
91
+ if (elapsed < ms) return undefined;
92
+ const fps = Math.round((framesInWindow / elapsed) * 1000);
93
+ refreshRate = Math.max(refreshRate, fps);
94
+ averages[collected] = fps;
95
+ collected++;
96
+ restart(nowMs);
97
+ return collected === iterations ? decide(fps) : undefined;
98
+ },
99
+ factor: () => factor,
100
+ };
101
+ };
@@ -0,0 +1,361 @@
1
+ /**
2
+ * The kernel's side of physics: an engine-neutral world contract, and colliders worked
3
+ * out from geometry. An engine package (Rapier, in `@opsydyn/effect-three-rapier`)
4
+ * implements `PhysicsEngine`; the scene graph steps it and moves meshes to its poses.
5
+ *
6
+ * Behavioural reference: react-three-rapier at `ae5c3fe` (2.2.0):
7
+ * `getColliderArgsFromGeometry` in `src/utils/utils-collider.ts` for colliders (a cuboid
8
+ * from the bounding box, a ball from the bounding sphere, a hull or trimesh from the
9
+ * vertices, each scaled by the mesh's world scale), and the step loop in
10
+ * `src/components/Physics.tsx` (fixed steps, delta clamped to 0.5 s, poses interpolated
11
+ * between the last two steps).
12
+ *
13
+ * @module
14
+ */
15
+ import {
16
+ type BufferGeometry,
17
+ Euler,
18
+ Matrix4,
19
+ type Mesh,
20
+ type Object3D,
21
+ Quaternion,
22
+ Vector3,
23
+ } from 'three';
24
+
25
+ import type {
26
+ Body,
27
+ BodyCollider,
28
+ ColliderDescription,
29
+ ColliderShapeDescription,
30
+ InteractionGroups,
31
+ JointKind,
32
+ Physics,
33
+ Vec3,
34
+ } from './description.ts';
35
+
36
+ /** A quaternion, `[x, y, z, w]`. */
37
+ export type Quat = readonly [x: number, y: number, z: number, w: number];
38
+
39
+ /** Where a body is, in world space. */
40
+ export type BodyPose = { readonly position: Vec3; readonly quaternion: Quat };
41
+
42
+ /** A collider's shape, scaled as its mesh is, centred on its own origin. */
43
+ export type ColliderShape =
44
+ | { readonly _tag: 'Cuboid'; readonly halfExtents: Vec3 }
45
+ | { readonly _tag: 'Ball'; readonly radius: number }
46
+ | { readonly _tag: 'Hull'; readonly points: Float32Array }
47
+ /** Along y: a cylinder `halfHeight` each way, capped by `radius` (Rapier's capsule). */
48
+ | { readonly _tag: 'Capsule'; readonly halfHeight: number; readonly radius: number }
49
+ | { readonly _tag: 'Cylinder'; readonly halfHeight: number; readonly radius: number }
50
+ | { readonly _tag: 'Cone'; readonly halfHeight: number; readonly radius: number }
51
+ | { readonly _tag: 'Trimesh'; readonly vertices: Float32Array; readonly indices: Uint32Array };
52
+
53
+ /** A collider placed in its body's space: one per mesh in the body. */
54
+ export type Collider = {
55
+ readonly shape: ColliderShape;
56
+ readonly translation: Vec3;
57
+ readonly rotation: Quat;
58
+ /** The part of its body it is, for collisions to name: a mesh's or collider's name. */
59
+ readonly part?: string;
60
+ };
61
+
62
+ /**
63
+ * Two bodies began (`started`) or stopped touching, by the ids they were added with: a
64
+ * node's key, or for an instance the node's and instance's keys joined by a NUL.
65
+ */
66
+ export type Collision = {
67
+ readonly a: string;
68
+ readonly b: string;
69
+ /** The colliders' parts, when they have names. */
70
+ readonly aPart?: string;
71
+ readonly bPart?: string;
72
+ readonly started: boolean;
73
+ };
74
+
75
+ /** A joint as an engine makes it: a fixed joint's frames as quaternions. */
76
+ export type PhysicsJoint = {
77
+ readonly anchorA: Vec3;
78
+ readonly anchorB: Vec3;
79
+ readonly contacts: boolean;
80
+ readonly kind:
81
+ | Exclude<JointKind, { readonly _tag: 'Fixed' }>
82
+ | { readonly _tag: 'Fixed'; readonly frameA: Quat; readonly frameB: Quat };
83
+ };
84
+
85
+ /**
86
+ * Groups as Rapier packs them (react-three-rapier's `interactionGroups`): memberships in
87
+ * the high 16 bits, filters in the low 16 (all when `null`). `null` groups are all-all.
88
+ */
89
+ export const interactionBits = (groups: InteractionGroups | null): number => {
90
+ const mask = (list: ReadonlyArray<number>): number =>
91
+ list.reduce((bits, group) => bits | (1 << group), 0);
92
+ if (groups === null) return 0xffff_ffff;
93
+ const filters = groups.filters === null ? 0xffff : mask(groups.filters);
94
+ return ((mask(groups.memberships) << 16) | filters) >>> 0;
95
+ };
96
+
97
+ /**
98
+ * Two bodies pushing on each other harder than a body's `contactForces` threshold in one
99
+ * step, by the ids they were added with: Rapier's contact force event.
100
+ */
101
+ export type ContactForce = {
102
+ readonly a: string;
103
+ readonly b: string;
104
+ readonly aPart?: string;
105
+ readonly bPart?: string;
106
+ readonly totalForce: Vec3;
107
+ readonly totalForceMagnitude: number;
108
+ readonly maxForceDirection: Vec3;
109
+ readonly maxForceMagnitude: number;
110
+ };
111
+
112
+ /** Lines drawing a world's colliders and joints: segment ends, and RGBA per end. */
113
+ export type DebugLines = { readonly vertices: Float32Array; readonly colors: Float32Array };
114
+
115
+ /** One physics world, its bodies keyed by id. Every method is synchronous. */
116
+ export type PhysicsWorld = {
117
+ readonly configure: (physics: Physics) => void;
118
+ /** Adds a body with its colliders (none, for a model not yet loaded). */
119
+ readonly add: (
120
+ key: string,
121
+ body: Body,
122
+ colliders: ReadonlyArray<Collider>,
123
+ pose: BodyPose,
124
+ ) => void;
125
+ /** Applies a body's settings that need no new collider: type, mass, friction and so on. */
126
+ readonly update: (key: string, body: Body) => void;
127
+ /** Puts a body at `pose`, at rest. */
128
+ readonly teleport: (key: string, pose: BodyPose) => void;
129
+ /** Where a kinematic body is to be after the next step. */
130
+ readonly moveKinematic: (key: string, pose: BodyPose) => void;
131
+ readonly applyImpulse: (key: string, impulse: Vec3) => void;
132
+ /** Removes a body, and any joint holding it. */
133
+ readonly remove: (key: string) => void;
134
+ /** Joins bodies `a` and `b` (both added). */
135
+ readonly addJoint: (id: string, joint: PhysicsJoint, a: string, b: string) => void;
136
+ /** Removes a joint; nothing if it is gone already (with one of its bodies). */
137
+ readonly removeJoint: (id: string) => void;
138
+ /** Advances by `seconds` and returns the collisions that began or ended. */
139
+ readonly step: (seconds: number) => ReadonlyArray<Collision>;
140
+ /** Contact forces over their bodies' thresholds in the steps since the last call. */
141
+ readonly contactForces: () => ReadonlyArray<ContactForce>;
142
+ /** The world as lines, for `physics({ debug: true })`. */
143
+ readonly debugLines: () => DebugLines;
144
+ readonly pose: (key: string) => BodyPose | undefined;
145
+ /** Whether any dynamic body is still moving (not asleep). */
146
+ readonly awake: () => boolean;
147
+ readonly dispose: () => void;
148
+ };
149
+
150
+ /** Makes physics worlds. Provide one to the scene graph (in Foldkit, the environment). */
151
+ export type PhysicsEngine = { readonly createWorld: (physics: Physics) => PhysicsWorld };
152
+
153
+ const toVec3 = (vector: Vector3): Vec3 => [vector.x, vector.y, vector.z];
154
+
155
+ const size = new Vector3();
156
+
157
+ /** The geometry's positions, scaled. */
158
+ const scaledPositions = (geometry: BufferGeometry, scale: Vector3): Float32Array => {
159
+ const positions = geometry.getAttribute('position');
160
+ const points = new Float32Array(positions.count * 3);
161
+ for (let i = 0; i < positions.count; i++) {
162
+ points[i * 3] = positions.getX(i) * scale.x;
163
+ points[i * 3 + 1] = positions.getY(i) * scale.y;
164
+ points[i * 3 + 2] = positions.getZ(i) * scale.z;
165
+ }
166
+ return points;
167
+ };
168
+
169
+ /** Triangle indices: the geometry's own, or one triangle per three positions. */
170
+ const triangles = (geometry: BufferGeometry): Uint32Array => {
171
+ const index = geometry.getIndex();
172
+ if (index !== null) return Uint32Array.from(index.array);
173
+ return Uint32Array.from({ length: geometry.getAttribute('position').count }, (_, i) => i);
174
+ };
175
+
176
+ /** A tiny ball for an empty geometry (text before its font, say). */
177
+ const speck: { readonly shape: ColliderShape; readonly offset: Vector3 } = {
178
+ shape: { _tag: 'Ball', radius: 1e-3 },
179
+ offset: new Vector3(),
180
+ };
181
+
182
+ /**
183
+ * The shape for a geometry at world `scale`, and where its centre is (scaled), as
184
+ * react-three-rapier's `getColliderArgsFromGeometry` sizes auto colliders.
185
+ */
186
+ const shapeFor = (
187
+ geometry: BufferGeometry,
188
+ collider: BodyCollider,
189
+ scale: Vector3,
190
+ ): { readonly shape: ColliderShape; readonly offset: Vector3 } => {
191
+ if (geometry.getAttribute('position') === undefined) return speck;
192
+ switch (collider) {
193
+ case 'cuboid': {
194
+ geometry.computeBoundingBox();
195
+ const box = geometry.boundingBox;
196
+ if (box === null) return speck;
197
+ box.getSize(size).multiply(scale).multiplyScalar(0.5);
198
+ return {
199
+ shape: { _tag: 'Cuboid', halfExtents: toVec3(size) },
200
+ offset: box.getCenter(new Vector3()).multiply(scale),
201
+ };
202
+ }
203
+ case 'ball': {
204
+ geometry.computeBoundingSphere();
205
+ const sphere = geometry.boundingSphere;
206
+ if (sphere === null) return speck;
207
+ const largest = Math.max(Math.abs(scale.x), Math.abs(scale.y), Math.abs(scale.z));
208
+ return {
209
+ shape: { _tag: 'Ball', radius: sphere.radius * largest },
210
+ offset: sphere.center.clone().multiply(scale),
211
+ };
212
+ }
213
+ case 'hull':
214
+ return {
215
+ shape: { _tag: 'Hull', points: scaledPositions(geometry, scale) },
216
+ offset: new Vector3(),
217
+ };
218
+ case 'trimesh':
219
+ return {
220
+ shape: {
221
+ _tag: 'Trimesh',
222
+ vertices: scaledPositions(geometry, scale),
223
+ indices: triangles(geometry),
224
+ },
225
+ offset: new Vector3(),
226
+ };
227
+ }
228
+ };
229
+
230
+ /**
231
+ * The collider for one geometry at world `scale`, centred where its geometry is: what a
232
+ * mesh with a body of its own gets.
233
+ */
234
+ export const colliderFor = (
235
+ geometry: BufferGeometry,
236
+ collider: BodyCollider,
237
+ scale: Vector3,
238
+ ): Collider => {
239
+ const { shape, offset } = shapeFor(geometry, collider, scale);
240
+ return { shape, translation: toVec3(offset), rotation: [0, 0, 0, 1] };
241
+ };
242
+
243
+ const turn3 = new Quaternion();
244
+ const turnEuler = new Euler();
245
+ const largest = (a: number, b: number): number => Math.max(Math.abs(a), Math.abs(b));
246
+
247
+ /** A described shape at world `scale`: lengths along an axis by that axis's scale. */
248
+ const scaledShape = (shape: ColliderShapeDescription, scale: Vector3): ColliderShape => {
249
+ switch (shape._tag) {
250
+ case 'Cuboid': {
251
+ const [x, y, z] = shape.halfExtents;
252
+ return { _tag: 'Cuboid', halfExtents: [x * scale.x, y * scale.y, z * scale.z] };
253
+ }
254
+ case 'Ball':
255
+ return {
256
+ _tag: 'Ball',
257
+ radius: shape.radius * Math.max(Math.abs(scale.x), Math.abs(scale.y), Math.abs(scale.z)),
258
+ };
259
+ case 'Capsule':
260
+ case 'Cylinder':
261
+ case 'Cone':
262
+ return {
263
+ _tag: shape._tag,
264
+ halfHeight: shape.halfHeight * Math.abs(scale.y),
265
+ radius: shape.radius * largest(scale.x, scale.z),
266
+ };
267
+ }
268
+ };
269
+
270
+ /**
271
+ * A body's own colliders at its world `scale`: each shape scaled, its position too. Unlike
272
+ * react-three-rapier's `scaleColliderArgs`, which scales arguments by position (x, y, z,
273
+ * x, x), each length scales along its own axis.
274
+ */
275
+ export const describedColliders = (
276
+ colliders: ReadonlyArray<ColliderDescription>,
277
+ scale: Vector3,
278
+ ): ReadonlyArray<Collider> => colliders.map((collider) => describedCollider(collider, scale));
279
+
280
+ /** One of a body's own colliders at its world `scale`. */
281
+ function describedCollider(collider: ColliderDescription, scale: Vector3): Collider {
282
+ const [x, y, z] = collider.position;
283
+ turn3.setFromEuler(turnEuler.fromArray([...collider.rotation]));
284
+ return {
285
+ shape: scaledShape(collider.shape, scale),
286
+ translation: [x * scale.x, y * scale.y, z * scale.z],
287
+ rotation: [turn3.x, turn3.y, turn3.z, turn3.w],
288
+ ...(collider.name === null ? {} : { part: collider.name }),
289
+ };
290
+ }
291
+
292
+ const relative = new Matrix4();
293
+ const bodyInverse = new Matrix4();
294
+ const where = new Vector3();
295
+ const turn = new Quaternion();
296
+ const relativeScale = new Vector3();
297
+ const worldScale = new Vector3();
298
+ const worldPosition = new Vector3();
299
+ const worldTurn = new Quaternion();
300
+
301
+ const isMesh = (object: Object3D): object is Mesh => 'isMesh' in object && object.isMesh === true;
302
+
303
+ /** A mesh's collider in the body whose inverse world matrix is `bodyInverse`. */
304
+ function placedCollider(child: Mesh, collider: BodyCollider, body: Object3D): Collider {
305
+ child.matrixWorld.decompose(worldPosition, worldTurn, worldScale);
306
+ relative.copy(child.matrixWorld).premultiply(bodyInverse).decompose(where, turn, relativeScale);
307
+ const { shape, offset } = shapeFor(child.geometry, collider, worldScale);
308
+ where.add(offset.applyQuaternion(turn));
309
+ return {
310
+ shape,
311
+ translation: toVec3(where),
312
+ rotation: [turn.x, turn.y, turn.z, turn.w],
313
+ // A mesh inside a group or model is a part of it; the body's own mesh is the body.
314
+ ...(child === body || child.name === '' ? {} : { part: child.name }),
315
+ };
316
+ }
317
+
318
+ /**
319
+ * A number as signature text, to five places with `-0` as `0`: the simulation turns a body,
320
+ * and its matrices pick up rounding that must not read as a change of shape.
321
+ */
322
+ const settled = (value: number): string => (Math.round(value * 1e5) / 1e5 || 0).toString();
323
+
324
+ /** A mesh's geometry and placement in the body whose inverse is `bodyInverse`. */
325
+ function meshSignature(child: Mesh): string {
326
+ relative.copy(child.matrixWorld).premultiply(bodyInverse);
327
+ child.matrixWorld.decompose(worldPosition, worldTurn, worldScale);
328
+ const placed = relative.elements.map(settled).join(',');
329
+ return `${child.geometry.uuid}:${placed}:${worldScale.toArray().map(settled).join(',')}`;
330
+ }
331
+
332
+ /** The visible meshes in `body`, itself included. */
333
+ function visibleMeshes(body: Object3D): ReadonlyArray<Mesh> {
334
+ const meshes: Array<Mesh> = [];
335
+ body.traverseVisible((child) => {
336
+ if (isMesh(child)) meshes.push(child);
337
+ });
338
+ return meshes;
339
+ }
340
+
341
+ /**
342
+ * A collider for every visible mesh in `body` (itself included), placed by its transform
343
+ * relative to the body and sized by its world scale, as react-three-rapier's
344
+ * `createColliderPropsFromChildren` makes a group's or a model's colliders. Unlike there,
345
+ * a geometry's off-centre middle is turned with its mesh.
346
+ */
347
+ export const collidersFor = (body: Object3D, collider: BodyCollider): ReadonlyArray<Collider> => {
348
+ body.updateWorldMatrix(true, true);
349
+ bodyInverse.copy(body.matrixWorld).invert();
350
+ return visibleMeshes(body).map((child) => placedCollider(child, collider, body));
351
+ };
352
+
353
+ /**
354
+ * What a body's colliders are made from: each visible mesh's geometry and placement.
355
+ * When it changes, the colliders must be made again.
356
+ */
357
+ export const colliderSignature = (body: Object3D): string => {
358
+ body.updateWorldMatrix(true, true);
359
+ bodyInverse.copy(body.matrixWorld).invert();
360
+ return visibleMeshes(body).map(meshSignature).join('|');
361
+ };
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The physics world drawn as lines, as react-three-rapier's `Debug` (`ae5c3fe`,
3
+ * `src/components/Debug.tsx`) draws `world.debugRender()`: one `LineSegments` with
4
+ * per-vertex colours, never culled. Here its buffers are reused while their size holds.
5
+ *
6
+ * @module
7
+ */
8
+ import {
9
+ BufferAttribute,
10
+ BufferGeometry,
11
+ LineBasicMaterial,
12
+ LineSegments,
13
+ type Object3D,
14
+ } from 'three';
15
+
16
+ import type { DebugLines } from './physics.ts';
17
+
18
+ export type PhysicsDebug = {
19
+ /** Shows `lines` (adding the drawing to the scene the first time). */
20
+ readonly show: (lines: DebugLines) => void;
21
+ /** Takes the drawing away and frees it. */
22
+ readonly hide: () => void;
23
+ };
24
+
25
+ /** Writes `values` into the geometry's attribute `name`, reusing it when it fits. */
26
+ const write = (
27
+ geometry: BufferGeometry,
28
+ name: 'position' | 'color',
29
+ values: Float32Array,
30
+ size: number,
31
+ ): void => {
32
+ const current = geometry.getAttribute(name);
33
+ if (current instanceof BufferAttribute && current.array.length === values.length) {
34
+ current.array.set(values);
35
+ current.needsUpdate = true;
36
+ return;
37
+ }
38
+ geometry.setAttribute(name, new BufferAttribute(values.slice(), size));
39
+ };
40
+
41
+ /** Creates the debug drawing for a scene. */
42
+ export const createPhysicsDebug = (scene: Object3D): PhysicsDebug => {
43
+ let lines: LineSegments<BufferGeometry, LineBasicMaterial> | undefined;
44
+ return {
45
+ show: ({ vertices, colors }) => {
46
+ if (lines === undefined) {
47
+ lines = new LineSegments(
48
+ new BufferGeometry(),
49
+ new LineBasicMaterial({ color: 0xffffff, vertexColors: true }),
50
+ );
51
+ lines.name = 'physics-debug';
52
+ lines.frustumCulled = false;
53
+ lines.raycast = () => undefined;
54
+ scene.add(lines);
55
+ }
56
+ write(lines.geometry, 'position', vertices, 3);
57
+ write(lines.geometry, 'color', colors, 4);
58
+ lines.geometry.setDrawRange(0, vertices.length / 3);
59
+ },
60
+ hide: () => {
61
+ if (lines === undefined) return;
62
+ lines.removeFromParent();
63
+ lines.geometry.dispose();
64
+ lines.material.dispose();
65
+ lines = undefined;
66
+ },
67
+ };
68
+ };