@series-inc/rundot-3d-engine 0.3.0 → 0.4.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.
- package/LICENSE.txt +6 -6
- package/docs/core/Component.md +321 -321
- package/docs/core/GameObject.md +204 -204
- package/docs/core/VenusGame.md +316 -316
- package/docs/patterns/ComponentCommunication.md +337 -337
- package/docs/patterns/CreatingGameObjects.md +290 -290
- package/docs/patterns/MeshColliders.md +338 -338
- package/docs/patterns/MeshLoading.md +316 -316
- package/docs/physics/Colliders.md +249 -249
- package/docs/physics/PhysicsSystem.md +151 -151
- package/docs/physics/RigidBodyComponent.md +201 -201
- package/docs/rendering/AssetManager.md +308 -308
- package/docs/rendering/InstancedRenderer.md +286 -286
- package/docs/rendering/MeshRenderer.md +286 -286
- package/docs/rendering/SkeletalRenderer.md +308 -308
- package/docs/systems/AnimationSystem.md +75 -75
- package/docs/systems/AudioSystem.md +79 -79
- package/docs/systems/InputManager.md +101 -101
- package/docs/systems/LightingSystem.md +101 -101
- package/docs/systems/ParticleSystem.md +44 -44
- package/docs/systems/PrefabSystem.md +60 -60
- package/docs/systems/StowKitSystem.md +77 -77
- package/docs/systems/TweenSystem.md +132 -132
- package/docs/systems/UISystem.md +73 -73
- package/package.json +1 -1
- package/scripts/postinstall.mjs +63 -51
|
@@ -1,338 +1,338 @@
|
|
|
1
|
-
# Mesh Colliders Pattern
|
|
2
|
-
|
|
3
|
-
How to load 3D meshes and attach collision bounds in the Rundot 3D Engine.
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
When loading meshes, you need to decide between:
|
|
8
|
-
- **Auto-fitted bounds colliders** - Accurate collision that matches the mesh shape
|
|
9
|
-
- **Primitive colliders** - Simpler, more performant approximate collision
|
|
10
|
-
|
|
11
|
-
## Auto-Fitted Bounds Pattern
|
|
12
|
-
|
|
13
|
-
### Basic Example: fitToMesh
|
|
14
|
-
|
|
15
|
-
The simplest approach - automatically fit the collider to the mesh bounds:
|
|
16
|
-
|
|
17
|
-
```typescript
|
|
18
|
-
import { Component, GameObject, MeshRenderer } from "@series-ai/rundot-3d-engine"
|
|
19
|
-
import { RigidBodyComponentThree, RigidBodyType, ColliderShape } from "@series-ai/rundot-3d-engine/systems"
|
|
20
|
-
|
|
21
|
-
class Crate extends Component {
|
|
22
|
-
private rendererObject: GameObject | null = null
|
|
23
|
-
|
|
24
|
-
protected onCreate(): void {
|
|
25
|
-
this.createMeshWithCollider()
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
private createMeshWithCollider(): void {
|
|
29
|
-
// 1. Create and attach mesh renderer
|
|
30
|
-
const renderer = new MeshRenderer("crate_mesh")
|
|
31
|
-
this.rendererObject = new GameObject("CrateRenderer")
|
|
32
|
-
this.rendererObject.addComponent(renderer)
|
|
33
|
-
this.gameObject.add(this.rendererObject)
|
|
34
|
-
|
|
35
|
-
// 2. Add auto-fitted collision
|
|
36
|
-
const rigidBody = new RigidBodyComponentThree({
|
|
37
|
-
type: RigidBodyType.DYNAMIC,
|
|
38
|
-
shape: ColliderShape.BOX,
|
|
39
|
-
fitToMesh: true, // ✨ Automatically calculates collision bounds
|
|
40
|
-
mass: 10
|
|
41
|
-
})
|
|
42
|
-
this.gameObject.addComponent(rigidBody)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
protected onCleanup(): void {
|
|
46
|
-
this.rendererObject?.dispose()
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
### Advanced Example: Manual Bounds with Timing
|
|
52
|
-
|
|
53
|
-
Wait for mesh to load before calculating bounds:
|
|
54
|
-
|
|
55
|
-
```typescript
|
|
56
|
-
import { Component, GameObject, MeshRenderer } from "@series-ai/rundot-3d-engine"
|
|
57
|
-
import { RigidBodyComponentThree, RigidBodyType, ColliderShape } from "@series-ai/rundot-3d-engine/systems"
|
|
58
|
-
|
|
59
|
-
class ComplexObject extends Component {
|
|
60
|
-
private rendererObject: GameObject | null = null
|
|
61
|
-
private renderer: MeshRenderer | null = null
|
|
62
|
-
private hasAddedCollider: boolean = false
|
|
63
|
-
|
|
64
|
-
protected onCreate(): void {
|
|
65
|
-
// 1. Create mesh renderer
|
|
66
|
-
this.renderer = new MeshRenderer("complex_mesh")
|
|
67
|
-
this.rendererObject = new GameObject("ComplexRenderer")
|
|
68
|
-
this.rendererObject.addComponent(this.renderer)
|
|
69
|
-
this.gameObject.add(this.rendererObject)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
public update(deltaTime: number): void {
|
|
73
|
-
// 2. Check if mesh is loaded and collider hasn't been added yet
|
|
74
|
-
if (!this.hasAddedCollider && this.renderer && this.renderer.isLoaded()) {
|
|
75
|
-
this.addColliderFromBounds()
|
|
76
|
-
this.hasAddedCollider = true
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
private addColliderFromBounds(): void {
|
|
81
|
-
// 3. Calculate bounds from loaded mesh
|
|
82
|
-
const bounds = this.renderer!.getBounds()
|
|
83
|
-
|
|
84
|
-
if (bounds) {
|
|
85
|
-
// 4. Create collider with calculated bounds
|
|
86
|
-
const rigidBody = RigidBodyComponentThree.fromBounds(bounds, {
|
|
87
|
-
type: RigidBodyType.STATIC,
|
|
88
|
-
shape: ColliderShape.BOX
|
|
89
|
-
})
|
|
90
|
-
this.gameObject.addComponent(rigidBody)
|
|
91
|
-
|
|
92
|
-
console.log(`Created collider with bounds: ${bounds.x}, ${bounds.y}, ${bounds.z}`)
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
protected onCleanup(): void {
|
|
97
|
-
this.rendererObject?.dispose()
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
### Using getMeshBounds Static Method
|
|
103
|
-
|
|
104
|
-
Calculate bounds from the entire GameObject hierarchy:
|
|
105
|
-
|
|
106
|
-
```typescript
|
|
107
|
-
private addBoundsCollider(): void {
|
|
108
|
-
// Calculate bounds from all meshes in GameObject
|
|
109
|
-
const bounds = RigidBodyComponentThree.getMeshBounds(this.gameObject)
|
|
110
|
-
|
|
111
|
-
console.log(`Calculated bounds: width=${bounds.x}, height=${bounds.y}, depth=${bounds.z}`)
|
|
112
|
-
|
|
113
|
-
// Create collider with bounds
|
|
114
|
-
const rigidBody = new RigidBodyComponentThree({
|
|
115
|
-
type: RigidBodyType.STATIC,
|
|
116
|
-
shape: ColliderShape.BOX,
|
|
117
|
-
size: bounds
|
|
118
|
-
})
|
|
119
|
-
this.gameObject.addComponent(rigidBody)
|
|
120
|
-
}
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
## Primitive Colliders for Performance
|
|
124
|
-
|
|
125
|
-
For complex meshes or many instances, use simpler primitive colliders instead of bounds:
|
|
126
|
-
|
|
127
|
-
### Example: Complex Mesh with Simple Collider
|
|
128
|
-
|
|
129
|
-
```typescript
|
|
130
|
-
class Tree extends Component {
|
|
131
|
-
private rendererObject: GameObject | null = null
|
|
132
|
-
|
|
133
|
-
protected onCreate(): void {
|
|
134
|
-
// 1. Load detailed tree mesh
|
|
135
|
-
const renderer = new MeshRenderer("detailed_tree_mesh")
|
|
136
|
-
this.rendererObject = new GameObject("TreeRenderer")
|
|
137
|
-
this.rendererObject.addComponent(renderer)
|
|
138
|
-
this.gameObject.add(this.rendererObject)
|
|
139
|
-
|
|
140
|
-
// 2. Use simple capsule collider (not bounds)
|
|
141
|
-
// Much better performance than fitting to complex tree mesh
|
|
142
|
-
const rigidBody = new RigidBodyComponentThree({
|
|
143
|
-
type: RigidBodyType.STATIC,
|
|
144
|
-
shape: ColliderShape.CAPSULE,
|
|
145
|
-
radius: 0.5, // Approximate trunk radius
|
|
146
|
-
height: 8.0, // Approximate tree height
|
|
147
|
-
mass: 1000 // Heavy (won't move)
|
|
148
|
-
})
|
|
149
|
-
this.gameObject.addComponent(rigidBody)
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
### Example: Character with Mixed Colliders
|
|
155
|
-
|
|
156
|
-
Use simple colliders for the character, but bounds for attachments:
|
|
157
|
-
|
|
158
|
-
```typescript
|
|
159
|
-
class Character extends Component {
|
|
160
|
-
protected onCreate(): void {
|
|
161
|
-
// Character uses simple capsule (better for movement)
|
|
162
|
-
const characterBody = new RigidBodyComponentThree({
|
|
163
|
-
type: RigidBodyType.DYNAMIC,
|
|
164
|
-
shape: ColliderShape.CAPSULE,
|
|
165
|
-
radius: 0.5,
|
|
166
|
-
height: 1.8,
|
|
167
|
-
lockRotationX: true,
|
|
168
|
-
lockRotationY: true,
|
|
169
|
-
lockRotationZ: true
|
|
170
|
-
})
|
|
171
|
-
this.gameObject.addComponent(characterBody)
|
|
172
|
-
|
|
173
|
-
// Add visual mesh (doesn't need to match collision exactly)
|
|
174
|
-
const renderer = new MeshRenderer("character_mesh")
|
|
175
|
-
const rendererObj = new GameObject("CharacterRenderer")
|
|
176
|
-
rendererObj.addComponent(renderer)
|
|
177
|
-
this.gameObject.add(rendererObj)
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
## When to Use Each Approach
|
|
183
|
-
|
|
184
|
-
### Use Auto-Fitted Bounds When:
|
|
185
|
-
|
|
186
|
-
✅ **Accurate collision is critical**
|
|
187
|
-
- Pickups that need precise click detection
|
|
188
|
-
- Interactive objects with complex shapes
|
|
189
|
-
- Puzzle pieces that must fit together
|
|
190
|
-
- Static environment meshes (walls, platforms)
|
|
191
|
-
|
|
192
|
-
✅ **Mesh shape is regular**
|
|
193
|
-
- Box-like objects (crates, buildings)
|
|
194
|
-
- Objects without many protrusions
|
|
195
|
-
- Static decorations
|
|
196
|
-
|
|
197
|
-
✅ **Performance isn't critical**
|
|
198
|
-
- Small number of objects (< 50)
|
|
199
|
-
- Static objects that don't move
|
|
200
|
-
|
|
201
|
-
### Use Primitive Colliders When:
|
|
202
|
-
|
|
203
|
-
✅ **Performance is critical**
|
|
204
|
-
- Many instances (trees, rocks, debris)
|
|
205
|
-
- Complex meshes with thousands of vertices
|
|
206
|
-
- Moving/dynamic objects
|
|
207
|
-
- Objects using InstancedRenderer
|
|
208
|
-
|
|
209
|
-
✅ **Approximate collision is acceptable**
|
|
210
|
-
- Background decorations
|
|
211
|
-
- Organic shapes (trees, bushes, rocks)
|
|
212
|
-
- Characters (capsule works better than mesh)
|
|
213
|
-
|
|
214
|
-
✅ **Simpler physics behavior desired**
|
|
215
|
-
- Spheres for smooth rolling
|
|
216
|
-
- Capsules for smooth character movement
|
|
217
|
-
- Boxes for stackable objects
|
|
218
|
-
|
|
219
|
-
## Performance Comparison
|
|
220
|
-
|
|
221
|
-
```typescript
|
|
222
|
-
// ❌ BAD: Complex mesh with many instances using bounds
|
|
223
|
-
for (let i = 0; i < 100; i++) {
|
|
224
|
-
const rock = new GameObject(`Rock_${i}`)
|
|
225
|
-
const renderer = new MeshRenderer("complex_rock_mesh") // 10k vertices
|
|
226
|
-
rock.addComponent(renderer)
|
|
227
|
-
|
|
228
|
-
rock.addComponent(new RigidBodyComponentThree({
|
|
229
|
-
shape: ColliderShape.BOX,
|
|
230
|
-
fitToMesh: true // Calculates bounds for each rock
|
|
231
|
-
}))
|
|
232
|
-
}
|
|
233
|
-
// Result: Expensive bounds calculations, potential performance issues
|
|
234
|
-
|
|
235
|
-
// ✅ GOOD: Complex mesh with simple sphere collider
|
|
236
|
-
for (let i = 0; i < 100; i++) {
|
|
237
|
-
const rock = new GameObject(`Rock_${i}`)
|
|
238
|
-
const renderer = new MeshRenderer("complex_rock_mesh")
|
|
239
|
-
rock.addComponent(renderer)
|
|
240
|
-
|
|
241
|
-
rock.addComponent(new RigidBodyComponentThree({
|
|
242
|
-
shape: ColliderShape.SPHERE,
|
|
243
|
-
radius: 0.8 // Simple approximate collision
|
|
244
|
-
}))
|
|
245
|
-
}
|
|
246
|
-
// Result: Fast, efficient collision with acceptable accuracy
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
## Complete Example: Pickup Item
|
|
250
|
-
|
|
251
|
-
Combining mesh loading with auto-fitted trigger collision:
|
|
252
|
-
|
|
253
|
-
```typescript
|
|
254
|
-
import { Component, GameObject, MeshRenderer } from "@series-ai/rundot-3d-engine"
|
|
255
|
-
import { RigidBodyComponentThree, RigidBodyType, ColliderShape } from "@series-ai/rundot-3d-engine/systems"
|
|
256
|
-
|
|
257
|
-
class CoinPickup extends Component {
|
|
258
|
-
private rendererObject: GameObject | null = null
|
|
259
|
-
|
|
260
|
-
protected onCreate(): void {
|
|
261
|
-
// 1. Create visual mesh
|
|
262
|
-
const renderer = new MeshRenderer("coin_mesh")
|
|
263
|
-
this.rendererObject = new GameObject("CoinRenderer")
|
|
264
|
-
this.rendererObject.addComponent(renderer)
|
|
265
|
-
this.gameObject.add(this.rendererObject)
|
|
266
|
-
|
|
267
|
-
// 2. Add trigger collider fitted to mesh bounds
|
|
268
|
-
const trigger = new RigidBodyComponentThree({
|
|
269
|
-
type: RigidBodyType.STATIC,
|
|
270
|
-
shape: ColliderShape.BOX,
|
|
271
|
-
fitToMesh: true, // Automatically fit to coin mesh
|
|
272
|
-
isSensor: true // Make it a trigger
|
|
273
|
-
})
|
|
274
|
-
this.gameObject.addComponent(trigger)
|
|
275
|
-
|
|
276
|
-
// 3. Handle pickup collision
|
|
277
|
-
trigger.onTriggerEnter((other: GameObject) => {
|
|
278
|
-
if (other.name === "Player") {
|
|
279
|
-
console.log("Coin collected!")
|
|
280
|
-
this.collect()
|
|
281
|
-
}
|
|
282
|
-
})
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
private collect(): void {
|
|
286
|
-
// Animate and remove
|
|
287
|
-
this.gameObject.dispose()
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
protected onCleanup(): void {
|
|
291
|
-
this.rendererObject?.dispose()
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
## Tips & Best Practices
|
|
297
|
-
|
|
298
|
-
### 1. fitToMesh Timing
|
|
299
|
-
`fitToMesh` calculates bounds during onCreate, so meshes should be loaded or loading. The calculation works even if the mesh isn't fully loaded yet (it recalculates when ready).
|
|
300
|
-
|
|
301
|
-
### 2. Scaling Considerations
|
|
302
|
-
If you scale your GameObject, bounds are calculated in world space:
|
|
303
|
-
|
|
304
|
-
```typescript
|
|
305
|
-
this.gameObject.scale.set(2, 2, 2) // Double size
|
|
306
|
-
const rigidBody = new RigidBodyComponentThree({
|
|
307
|
-
fitToMesh: true // Bounds will account for scale
|
|
308
|
-
})
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
### 3. Multiple Meshes
|
|
312
|
-
`getMeshBounds()` combines all meshes in the GameObject hierarchy:
|
|
313
|
-
|
|
314
|
-
```typescript
|
|
315
|
-
// Parent with multiple child meshes
|
|
316
|
-
const composite = new GameObject("Composite")
|
|
317
|
-
composite.add(createMesh("part1"))
|
|
318
|
-
composite.add(createMesh("part2"))
|
|
319
|
-
|
|
320
|
-
// Bounds will include both meshes
|
|
321
|
-
const bounds = RigidBodyComponentThree.getMeshBounds(composite)
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
### 4. Debug Visualization
|
|
325
|
-
Enable physics debug rendering to see collision bounds:
|
|
326
|
-
|
|
327
|
-
```typescript
|
|
328
|
-
// In your game class
|
|
329
|
-
PhysicsSystem.getInstance().setDebugRender(true)
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
## Related Patterns
|
|
333
|
-
|
|
334
|
-
- [Mesh Loading](MeshLoading.md) - Loading and displaying meshes
|
|
335
|
-
- [Creating GameObjects](CreatingGameObjects.md) - GameObject best practices
|
|
336
|
-
- [Colliders](../physics/Colliders.md) - Full collider documentation
|
|
337
|
-
- [RigidBodyComponent](../physics/RigidBodyComponent.md) - Physics body documentation
|
|
338
|
-
|
|
1
|
+
# Mesh Colliders Pattern
|
|
2
|
+
|
|
3
|
+
How to load 3D meshes and attach collision bounds in the Rundot 3D Engine.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
When loading meshes, you need to decide between:
|
|
8
|
+
- **Auto-fitted bounds colliders** - Accurate collision that matches the mesh shape
|
|
9
|
+
- **Primitive colliders** - Simpler, more performant approximate collision
|
|
10
|
+
|
|
11
|
+
## Auto-Fitted Bounds Pattern
|
|
12
|
+
|
|
13
|
+
### Basic Example: fitToMesh
|
|
14
|
+
|
|
15
|
+
The simplest approach - automatically fit the collider to the mesh bounds:
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { Component, GameObject, MeshRenderer } from "@series-ai/rundot-3d-engine"
|
|
19
|
+
import { RigidBodyComponentThree, RigidBodyType, ColliderShape } from "@series-ai/rundot-3d-engine/systems"
|
|
20
|
+
|
|
21
|
+
class Crate extends Component {
|
|
22
|
+
private rendererObject: GameObject | null = null
|
|
23
|
+
|
|
24
|
+
protected onCreate(): void {
|
|
25
|
+
this.createMeshWithCollider()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private createMeshWithCollider(): void {
|
|
29
|
+
// 1. Create and attach mesh renderer
|
|
30
|
+
const renderer = new MeshRenderer("crate_mesh")
|
|
31
|
+
this.rendererObject = new GameObject("CrateRenderer")
|
|
32
|
+
this.rendererObject.addComponent(renderer)
|
|
33
|
+
this.gameObject.add(this.rendererObject)
|
|
34
|
+
|
|
35
|
+
// 2. Add auto-fitted collision
|
|
36
|
+
const rigidBody = new RigidBodyComponentThree({
|
|
37
|
+
type: RigidBodyType.DYNAMIC,
|
|
38
|
+
shape: ColliderShape.BOX,
|
|
39
|
+
fitToMesh: true, // ✨ Automatically calculates collision bounds
|
|
40
|
+
mass: 10
|
|
41
|
+
})
|
|
42
|
+
this.gameObject.addComponent(rigidBody)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
protected onCleanup(): void {
|
|
46
|
+
this.rendererObject?.dispose()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Advanced Example: Manual Bounds with Timing
|
|
52
|
+
|
|
53
|
+
Wait for mesh to load before calculating bounds:
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { Component, GameObject, MeshRenderer } from "@series-ai/rundot-3d-engine"
|
|
57
|
+
import { RigidBodyComponentThree, RigidBodyType, ColliderShape } from "@series-ai/rundot-3d-engine/systems"
|
|
58
|
+
|
|
59
|
+
class ComplexObject extends Component {
|
|
60
|
+
private rendererObject: GameObject | null = null
|
|
61
|
+
private renderer: MeshRenderer | null = null
|
|
62
|
+
private hasAddedCollider: boolean = false
|
|
63
|
+
|
|
64
|
+
protected onCreate(): void {
|
|
65
|
+
// 1. Create mesh renderer
|
|
66
|
+
this.renderer = new MeshRenderer("complex_mesh")
|
|
67
|
+
this.rendererObject = new GameObject("ComplexRenderer")
|
|
68
|
+
this.rendererObject.addComponent(this.renderer)
|
|
69
|
+
this.gameObject.add(this.rendererObject)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public update(deltaTime: number): void {
|
|
73
|
+
// 2. Check if mesh is loaded and collider hasn't been added yet
|
|
74
|
+
if (!this.hasAddedCollider && this.renderer && this.renderer.isLoaded()) {
|
|
75
|
+
this.addColliderFromBounds()
|
|
76
|
+
this.hasAddedCollider = true
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private addColliderFromBounds(): void {
|
|
81
|
+
// 3. Calculate bounds from loaded mesh
|
|
82
|
+
const bounds = this.renderer!.getBounds()
|
|
83
|
+
|
|
84
|
+
if (bounds) {
|
|
85
|
+
// 4. Create collider with calculated bounds
|
|
86
|
+
const rigidBody = RigidBodyComponentThree.fromBounds(bounds, {
|
|
87
|
+
type: RigidBodyType.STATIC,
|
|
88
|
+
shape: ColliderShape.BOX
|
|
89
|
+
})
|
|
90
|
+
this.gameObject.addComponent(rigidBody)
|
|
91
|
+
|
|
92
|
+
console.log(`Created collider with bounds: ${bounds.x}, ${bounds.y}, ${bounds.z}`)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
protected onCleanup(): void {
|
|
97
|
+
this.rendererObject?.dispose()
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Using getMeshBounds Static Method
|
|
103
|
+
|
|
104
|
+
Calculate bounds from the entire GameObject hierarchy:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
private addBoundsCollider(): void {
|
|
108
|
+
// Calculate bounds from all meshes in GameObject
|
|
109
|
+
const bounds = RigidBodyComponentThree.getMeshBounds(this.gameObject)
|
|
110
|
+
|
|
111
|
+
console.log(`Calculated bounds: width=${bounds.x}, height=${bounds.y}, depth=${bounds.z}`)
|
|
112
|
+
|
|
113
|
+
// Create collider with bounds
|
|
114
|
+
const rigidBody = new RigidBodyComponentThree({
|
|
115
|
+
type: RigidBodyType.STATIC,
|
|
116
|
+
shape: ColliderShape.BOX,
|
|
117
|
+
size: bounds
|
|
118
|
+
})
|
|
119
|
+
this.gameObject.addComponent(rigidBody)
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Primitive Colliders for Performance
|
|
124
|
+
|
|
125
|
+
For complex meshes or many instances, use simpler primitive colliders instead of bounds:
|
|
126
|
+
|
|
127
|
+
### Example: Complex Mesh with Simple Collider
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
class Tree extends Component {
|
|
131
|
+
private rendererObject: GameObject | null = null
|
|
132
|
+
|
|
133
|
+
protected onCreate(): void {
|
|
134
|
+
// 1. Load detailed tree mesh
|
|
135
|
+
const renderer = new MeshRenderer("detailed_tree_mesh")
|
|
136
|
+
this.rendererObject = new GameObject("TreeRenderer")
|
|
137
|
+
this.rendererObject.addComponent(renderer)
|
|
138
|
+
this.gameObject.add(this.rendererObject)
|
|
139
|
+
|
|
140
|
+
// 2. Use simple capsule collider (not bounds)
|
|
141
|
+
// Much better performance than fitting to complex tree mesh
|
|
142
|
+
const rigidBody = new RigidBodyComponentThree({
|
|
143
|
+
type: RigidBodyType.STATIC,
|
|
144
|
+
shape: ColliderShape.CAPSULE,
|
|
145
|
+
radius: 0.5, // Approximate trunk radius
|
|
146
|
+
height: 8.0, // Approximate tree height
|
|
147
|
+
mass: 1000 // Heavy (won't move)
|
|
148
|
+
})
|
|
149
|
+
this.gameObject.addComponent(rigidBody)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Example: Character with Mixed Colliders
|
|
155
|
+
|
|
156
|
+
Use simple colliders for the character, but bounds for attachments:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
class Character extends Component {
|
|
160
|
+
protected onCreate(): void {
|
|
161
|
+
// Character uses simple capsule (better for movement)
|
|
162
|
+
const characterBody = new RigidBodyComponentThree({
|
|
163
|
+
type: RigidBodyType.DYNAMIC,
|
|
164
|
+
shape: ColliderShape.CAPSULE,
|
|
165
|
+
radius: 0.5,
|
|
166
|
+
height: 1.8,
|
|
167
|
+
lockRotationX: true,
|
|
168
|
+
lockRotationY: true,
|
|
169
|
+
lockRotationZ: true
|
|
170
|
+
})
|
|
171
|
+
this.gameObject.addComponent(characterBody)
|
|
172
|
+
|
|
173
|
+
// Add visual mesh (doesn't need to match collision exactly)
|
|
174
|
+
const renderer = new MeshRenderer("character_mesh")
|
|
175
|
+
const rendererObj = new GameObject("CharacterRenderer")
|
|
176
|
+
rendererObj.addComponent(renderer)
|
|
177
|
+
this.gameObject.add(rendererObj)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## When to Use Each Approach
|
|
183
|
+
|
|
184
|
+
### Use Auto-Fitted Bounds When:
|
|
185
|
+
|
|
186
|
+
✅ **Accurate collision is critical**
|
|
187
|
+
- Pickups that need precise click detection
|
|
188
|
+
- Interactive objects with complex shapes
|
|
189
|
+
- Puzzle pieces that must fit together
|
|
190
|
+
- Static environment meshes (walls, platforms)
|
|
191
|
+
|
|
192
|
+
✅ **Mesh shape is regular**
|
|
193
|
+
- Box-like objects (crates, buildings)
|
|
194
|
+
- Objects without many protrusions
|
|
195
|
+
- Static decorations
|
|
196
|
+
|
|
197
|
+
✅ **Performance isn't critical**
|
|
198
|
+
- Small number of objects (< 50)
|
|
199
|
+
- Static objects that don't move
|
|
200
|
+
|
|
201
|
+
### Use Primitive Colliders When:
|
|
202
|
+
|
|
203
|
+
✅ **Performance is critical**
|
|
204
|
+
- Many instances (trees, rocks, debris)
|
|
205
|
+
- Complex meshes with thousands of vertices
|
|
206
|
+
- Moving/dynamic objects
|
|
207
|
+
- Objects using InstancedRenderer
|
|
208
|
+
|
|
209
|
+
✅ **Approximate collision is acceptable**
|
|
210
|
+
- Background decorations
|
|
211
|
+
- Organic shapes (trees, bushes, rocks)
|
|
212
|
+
- Characters (capsule works better than mesh)
|
|
213
|
+
|
|
214
|
+
✅ **Simpler physics behavior desired**
|
|
215
|
+
- Spheres for smooth rolling
|
|
216
|
+
- Capsules for smooth character movement
|
|
217
|
+
- Boxes for stackable objects
|
|
218
|
+
|
|
219
|
+
## Performance Comparison
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
// ❌ BAD: Complex mesh with many instances using bounds
|
|
223
|
+
for (let i = 0; i < 100; i++) {
|
|
224
|
+
const rock = new GameObject(`Rock_${i}`)
|
|
225
|
+
const renderer = new MeshRenderer("complex_rock_mesh") // 10k vertices
|
|
226
|
+
rock.addComponent(renderer)
|
|
227
|
+
|
|
228
|
+
rock.addComponent(new RigidBodyComponentThree({
|
|
229
|
+
shape: ColliderShape.BOX,
|
|
230
|
+
fitToMesh: true // Calculates bounds for each rock
|
|
231
|
+
}))
|
|
232
|
+
}
|
|
233
|
+
// Result: Expensive bounds calculations, potential performance issues
|
|
234
|
+
|
|
235
|
+
// ✅ GOOD: Complex mesh with simple sphere collider
|
|
236
|
+
for (let i = 0; i < 100; i++) {
|
|
237
|
+
const rock = new GameObject(`Rock_${i}`)
|
|
238
|
+
const renderer = new MeshRenderer("complex_rock_mesh")
|
|
239
|
+
rock.addComponent(renderer)
|
|
240
|
+
|
|
241
|
+
rock.addComponent(new RigidBodyComponentThree({
|
|
242
|
+
shape: ColliderShape.SPHERE,
|
|
243
|
+
radius: 0.8 // Simple approximate collision
|
|
244
|
+
}))
|
|
245
|
+
}
|
|
246
|
+
// Result: Fast, efficient collision with acceptable accuracy
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Complete Example: Pickup Item
|
|
250
|
+
|
|
251
|
+
Combining mesh loading with auto-fitted trigger collision:
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
import { Component, GameObject, MeshRenderer } from "@series-ai/rundot-3d-engine"
|
|
255
|
+
import { RigidBodyComponentThree, RigidBodyType, ColliderShape } from "@series-ai/rundot-3d-engine/systems"
|
|
256
|
+
|
|
257
|
+
class CoinPickup extends Component {
|
|
258
|
+
private rendererObject: GameObject | null = null
|
|
259
|
+
|
|
260
|
+
protected onCreate(): void {
|
|
261
|
+
// 1. Create visual mesh
|
|
262
|
+
const renderer = new MeshRenderer("coin_mesh")
|
|
263
|
+
this.rendererObject = new GameObject("CoinRenderer")
|
|
264
|
+
this.rendererObject.addComponent(renderer)
|
|
265
|
+
this.gameObject.add(this.rendererObject)
|
|
266
|
+
|
|
267
|
+
// 2. Add trigger collider fitted to mesh bounds
|
|
268
|
+
const trigger = new RigidBodyComponentThree({
|
|
269
|
+
type: RigidBodyType.STATIC,
|
|
270
|
+
shape: ColliderShape.BOX,
|
|
271
|
+
fitToMesh: true, // Automatically fit to coin mesh
|
|
272
|
+
isSensor: true // Make it a trigger
|
|
273
|
+
})
|
|
274
|
+
this.gameObject.addComponent(trigger)
|
|
275
|
+
|
|
276
|
+
// 3. Handle pickup collision
|
|
277
|
+
trigger.onTriggerEnter((other: GameObject) => {
|
|
278
|
+
if (other.name === "Player") {
|
|
279
|
+
console.log("Coin collected!")
|
|
280
|
+
this.collect()
|
|
281
|
+
}
|
|
282
|
+
})
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private collect(): void {
|
|
286
|
+
// Animate and remove
|
|
287
|
+
this.gameObject.dispose()
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
protected onCleanup(): void {
|
|
291
|
+
this.rendererObject?.dispose()
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Tips & Best Practices
|
|
297
|
+
|
|
298
|
+
### 1. fitToMesh Timing
|
|
299
|
+
`fitToMesh` calculates bounds during onCreate, so meshes should be loaded or loading. The calculation works even if the mesh isn't fully loaded yet (it recalculates when ready).
|
|
300
|
+
|
|
301
|
+
### 2. Scaling Considerations
|
|
302
|
+
If you scale your GameObject, bounds are calculated in world space:
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
this.gameObject.scale.set(2, 2, 2) // Double size
|
|
306
|
+
const rigidBody = new RigidBodyComponentThree({
|
|
307
|
+
fitToMesh: true // Bounds will account for scale
|
|
308
|
+
})
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### 3. Multiple Meshes
|
|
312
|
+
`getMeshBounds()` combines all meshes in the GameObject hierarchy:
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
// Parent with multiple child meshes
|
|
316
|
+
const composite = new GameObject("Composite")
|
|
317
|
+
composite.add(createMesh("part1"))
|
|
318
|
+
composite.add(createMesh("part2"))
|
|
319
|
+
|
|
320
|
+
// Bounds will include both meshes
|
|
321
|
+
const bounds = RigidBodyComponentThree.getMeshBounds(composite)
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### 4. Debug Visualization
|
|
325
|
+
Enable physics debug rendering to see collision bounds:
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
// In your game class
|
|
329
|
+
PhysicsSystem.getInstance().setDebugRender(true)
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## Related Patterns
|
|
333
|
+
|
|
334
|
+
- [Mesh Loading](MeshLoading.md) - Loading and displaying meshes
|
|
335
|
+
- [Creating GameObjects](CreatingGameObjects.md) - GameObject best practices
|
|
336
|
+
- [Colliders](../physics/Colliders.md) - Full collider documentation
|
|
337
|
+
- [RigidBodyComponent](../physics/RigidBodyComponent.md) - Physics body documentation
|
|
338
|
+
|