lecodes-cli 0.17.0 → 0.17.2
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/dist/index.js +69 -11
- package/package.json +3 -3
- package/runtime/sdk/gl/CharacterController.ts +3 -0
- package/runtime/sdk/gl/IK.ts +174 -170
- package/runtime/sdk/gl/InstancedMesh.ts +132 -0
- package/runtime/sdk/gl/Material.ts +12 -0
- package/runtime/sdk/gl/Node.ts +285 -272
- package/runtime/sdk/gl/Particles.ts +135 -5
- package/runtime/sdk/gl/Physics.ts +126 -119
- package/runtime/sdk/gl/Scene.ts +299 -291
- package/runtime/sdk/gl/Shape.ts +37 -1
- package/runtime/sdk/gl/Trigger.ts +50 -46
- package/runtime/sdk/inject.ts +162 -160
- package/runtime/sdk-types.json +1 -1
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { Color, type ColorInput } from "../core/color"
|
|
14
14
|
import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
15
|
+
import type { Geometry } from "./Geometry"
|
|
15
16
|
import { Material, _sheetColsRows, type ParticlesMaterialOptions } from "./Material"
|
|
16
17
|
import { Node } from "./Node"
|
|
17
18
|
|
|
@@ -34,6 +35,11 @@ const TAG_SPACE = 12
|
|
|
34
35
|
const TAG_INHERIT_VELOCITY = 13
|
|
35
36
|
const TAG_RATE_DISTANCE = 14
|
|
36
37
|
const TAG_RENDER_MODE = 15
|
|
38
|
+
const TAG_MESH_GEOMETRY = 16
|
|
39
|
+
const TAG_ANGULAR_VELOCITY = 17
|
|
40
|
+
const TAG_GROUND = 18
|
|
41
|
+
const TAG_VELOCITY_LIFE = 19
|
|
42
|
+
const TAG_ORDER = 20
|
|
37
43
|
|
|
38
44
|
/** @internal — exported for the tag-sync test only. */
|
|
39
45
|
export const _particleTags = {
|
|
@@ -41,7 +47,9 @@ export const _particleTags = {
|
|
|
41
47
|
VELOCITY_RND: TAG_VELOCITY_RND, ACCELERATION: TAG_ACCELERATION, DRAG: TAG_DRAG,
|
|
42
48
|
NOISE: TAG_NOISE, SEED: TAG_SEED, PARAM_CURVE: TAG_PARAM_CURVE, COLOR_CURVE: TAG_COLOR_CURVE,
|
|
43
49
|
SPACE: TAG_SPACE, INHERIT_VELOCITY: TAG_INHERIT_VELOCITY, RATE_DISTANCE: TAG_RATE_DISTANCE,
|
|
44
|
-
RENDER_MODE: TAG_RENDER_MODE,
|
|
50
|
+
RENDER_MODE: TAG_RENDER_MODE, MESH_GEOMETRY: TAG_MESH_GEOMETRY,
|
|
51
|
+
ANGULAR_VELOCITY: TAG_ANGULAR_VELOCITY, GROUND: TAG_GROUND, VELOCITY_LIFE: TAG_VELOCITY_LIFE,
|
|
52
|
+
ORDER: TAG_ORDER,
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
// --- curve builders -----------------------------------------------------------------------------
|
|
@@ -206,13 +214,51 @@ type VelocityValue = {
|
|
|
206
214
|
randomizeAngle?: { min: Vec3Like, max: Vec3Like }
|
|
207
215
|
} & ({ from: Vec3Like } | { to: Vec3Like } | { dir: Vec3Like } | {})
|
|
208
216
|
|
|
217
|
+
/** Where particles are born, in emitter space. Sphere / hemisphere / cone have an axis of +Y (the
|
|
218
|
+
* hemisphere is the +Y half, the cone opens along +Y from its base disc at `center`) — aim the
|
|
219
|
+
* node with `lookAt(target, 'y')`. `thickness` 0 emits from the surface only, 1 (default) from the
|
|
220
|
+
* whole volume. A sphere or cone also sets the LAUNCH DIRECTION (outward / from the cone's apex)
|
|
221
|
+
* when `startVelocity` gives a speed but no direction, like Unity's shapes. */
|
|
209
222
|
type Shape =
|
|
210
223
|
| { type: "point", v: Vec3Like }
|
|
211
224
|
| { type: "box", min: Vec3Like, max: Vec3Like }
|
|
212
225
|
| { type: "circle", center: Vec3Like, radius: number }
|
|
226
|
+
| { type: "sphere", center?: Vec3Like, radius: number, thickness?: number }
|
|
227
|
+
| { type: "hemisphere", center?: Vec3Like, radius: number, thickness?: number }
|
|
228
|
+
| { type: "cone", center?: Vec3Like, radius: number, /** half-angle, radians */ angle?: number, /** emit this far up the axis too */ length?: number, thickness?: number }
|
|
229
|
+
/** Extra velocity over each particle's life — evaluated from the curves every frame and ADDED as a
|
|
230
|
+
* displacement (not accumulated like `acceleration`): `radial` pushes outward along the particle's
|
|
231
|
+
* birth direction (a burst that decays: `curve(4).to(0)`), `x`/`y`/`z` drift in the simulation
|
|
232
|
+
* space (rising smoke: `y: curve(2).from(0).to(1)`). Unity's velocityOverLifetime. */
|
|
233
|
+
export type VelocityOverLife = { radial?: ParticleValue, x?: ParticleValue, y?: ParticleValue, z?: ParticleValue }
|
|
213
234
|
type Noise = { strength?: number, frequency?: number, speed?: number }
|
|
214
235
|
|
|
236
|
+
/** Ground plane for bouncing particles (all render modes). `height` is in the simulation space:
|
|
237
|
+
* world y with `space: 'world'`, emitter-local y otherwise. */
|
|
238
|
+
export type ParticleGround = {
|
|
239
|
+
/** Plane height (default 0). */
|
|
240
|
+
height?: number
|
|
241
|
+
/** Contact radius — particles stop `radius` above the plane (a mesh's half-extent; default 0). */
|
|
242
|
+
radius?: number
|
|
243
|
+
/** Fraction of the vertical speed kept on impact, 0..1 (default 0.3). */
|
|
244
|
+
bounce?: number
|
|
245
|
+
/** Tangential damping while in contact, per second — like `drag` (default 8). */
|
|
246
|
+
friction?: number
|
|
247
|
+
}
|
|
248
|
+
|
|
215
249
|
export type ParticlesOptions = ParticlesMaterialOptions & {
|
|
250
|
+
/** Draw every particle as this mesh — lit, tumbling 3D debris (shell casings, rubble, leaves,
|
|
251
|
+
* shrapnel) in one draw call. The system switches to the mesh render mode; the default
|
|
252
|
+
* material becomes `Material.lit()` (pass `material` for color/roughness/metallic — the
|
|
253
|
+
* sprite look options are ignored). `size` scales the mesh (default 1); `rotation`/`opacity`/
|
|
254
|
+
* `frame` don't apply. See `angularVelocity` and `ground`. */
|
|
255
|
+
mesh?: Geometry
|
|
256
|
+
/** (mesh) Spin, radians per second about the particle's own axes — a vector, or a per-component
|
|
257
|
+
* `{ min, max }` range randomized at spawn. Particles are born in the emitter's orientation. */
|
|
258
|
+
angularVelocity?: Vec3Like | { min: Vec3Like, max: Vec3Like }
|
|
259
|
+
/** A ground plane the particles bounce on and come to rest on (any render mode); `null`
|
|
260
|
+
* disables it. */
|
|
261
|
+
ground?: ParticleGround | null
|
|
216
262
|
/** Custom draw material. Unset = the default point-sprite material, configured by the
|
|
217
263
|
* `ParticlesMaterialOptions` sugar (map/sheet/emissive/blend/soft) — which is ignored
|
|
218
264
|
* when an explicit material is passed. */
|
|
@@ -232,7 +278,14 @@ export type ParticlesOptions = ParticlesMaterialOptions & {
|
|
|
232
278
|
* spread evenly along the path — trail density independent of speed, no per-frame clumps.
|
|
233
279
|
* Meant for `space: 'world'`. */
|
|
234
280
|
rateOverDistance?: number
|
|
281
|
+
/** Draw order among blended systems at the same depth — higher draws later, i.e. on top (Unity's
|
|
282
|
+
* sortingOrder). Systems are depth-sorted by their node, so the emitters of one effect tie and
|
|
283
|
+
* without this the engine picks who covers whom per frame (smoke popping over a fireball).
|
|
284
|
+
* Default 0; a fireball wants 2, its smoke 1, a smoke trail -1. */
|
|
285
|
+
order?: number
|
|
235
286
|
shape?: Shape
|
|
287
|
+
/** Extra velocity over life (radial burst that decays, axis drift) — see `VelocityOverLife`. */
|
|
288
|
+
velocityOverLife?: VelocityOverLife
|
|
236
289
|
startVelocity?: Vec3Like | VelocityValue
|
|
237
290
|
/** Downward acceleration, world units/s² — positive pulls particles DOWN (sugar for
|
|
238
291
|
* `acceleration: [0, -g, 0]`). */
|
|
@@ -293,10 +346,54 @@ const pushColor = (out: number[], value: ParticleColor): void => {
|
|
|
293
346
|
for (let i = 0; i < d.length; i++) out.push(d[i])
|
|
294
347
|
}
|
|
295
348
|
|
|
349
|
+
const pushMesh = (out: number[], g: Geometry): void => {
|
|
350
|
+
const nv = g.vertices.length / 3
|
|
351
|
+
const ni = g.indices.length
|
|
352
|
+
out.push(TAG_MESH_GEOMETRY, 2 + nv * 8 + ni, nv, ni)
|
|
353
|
+
for (let i = 0; i < nv * 3; i++) out.push(g.vertices[i])
|
|
354
|
+
for (let i = 0; i < nv * 3; i++) out.push(g.normals[i])
|
|
355
|
+
for (let i = 0; i < nv * 2; i++) out.push(g.uv[i])
|
|
356
|
+
for (let i = 0; i < ni; i++) out.push(g.indices[i])
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const pushAngularVelocity = (out: number[], v: Vec3Like | { min: Vec3Like, max: Vec3Like }): void => {
|
|
360
|
+
const r = v as { min: Vec3Like, max: Vec3Like }
|
|
361
|
+
if (typeof r === "object" && r !== null && !Array.isArray(r) && "min" in r && !(v instanceof Float32Array) && !(v instanceof Vec3)) {
|
|
362
|
+
out.push(TAG_ANGULAR_VELOCITY, 6, cx(r.min), cy(r.min), cz(r.min), cx(r.max), cy(r.max), cz(r.max))
|
|
363
|
+
} else {
|
|
364
|
+
const a = v as Vec3Like
|
|
365
|
+
out.push(TAG_ANGULAR_VELOCITY, 6, cx(a), cy(a), cz(a), cx(a), cy(a), cz(a))
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const pushGround = (out: number[], g: ParticleGround | null): void => {
|
|
370
|
+
if (g === null) out.push(TAG_GROUND, 5, 0, 0, 0, 0, 0)
|
|
371
|
+
else out.push(TAG_GROUND, 5, 1, g.height ?? 0, g.radius ?? 0, g.bounce ?? 0.3, g.friction ?? 8)
|
|
372
|
+
}
|
|
373
|
+
|
|
296
374
|
const pushShape = (out: number[], shape: Shape): void => {
|
|
297
375
|
if (shape.type === "point") out.push(TAG_SHAPE, 4, 0, cx(shape.v), cy(shape.v), cz(shape.v))
|
|
298
376
|
else if (shape.type === "box") out.push(TAG_SHAPE, 7, 1, cx(shape.min), cy(shape.min), cz(shape.min), cx(shape.max), cy(shape.max), cz(shape.max))
|
|
299
|
-
else out.push(TAG_SHAPE, 5, 2, cx(shape.center), cy(shape.center), cz(shape.center), shape.radius)
|
|
377
|
+
else if (shape.type === "circle") out.push(TAG_SHAPE, 5, 2, cx(shape.center), cy(shape.center), cz(shape.center), shape.radius)
|
|
378
|
+
else {
|
|
379
|
+
const c = shape.center ?? [ 0, 0, 0 ]
|
|
380
|
+
const thickness = shape.thickness ?? 1
|
|
381
|
+
if (shape.type === "cone") out.push(TAG_SHAPE, 8, 5, cx(c), cy(c), cz(c), shape.radius, shape.angle ?? 0, shape.length ?? 0, thickness)
|
|
382
|
+
else out.push(TAG_SHAPE, 6, shape.type === "sphere" ? 3 : 4, cx(c), cy(c), cz(c), shape.radius, thickness)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const pushVelocityLife = (out: number[], v: VelocityOverLife): void => {
|
|
387
|
+
const axes: Array<[number, ParticleValue | undefined]> = [ [ 0, v.radial ], [ 1, v.x ], [ 2, v.y ], [ 3, v.z ] ]
|
|
388
|
+
for (const [ axis, value ] of axes) {
|
|
389
|
+
if (value === undefined) continue
|
|
390
|
+
if (typeof value === "number") out.push(TAG_VELOCITY_LIFE, 5, axis, 0, value, value, 0)
|
|
391
|
+
else if (hasData(value)) {
|
|
392
|
+
const d = value._data
|
|
393
|
+
out.push(TAG_VELOCITY_LIFE, d.length + 1, axis)
|
|
394
|
+
for (let i = 0; i < d.length; i++) out.push(d[i])
|
|
395
|
+
} else out.push(TAG_VELOCITY_LIFE, 5, axis, 0, value.min, value.max, 0)
|
|
396
|
+
}
|
|
300
397
|
}
|
|
301
398
|
|
|
302
399
|
const pushVelocity = (out: number[], val: Vec3Like | VelocityValue): void => {
|
|
@@ -338,7 +435,7 @@ export class Particles extends Node {
|
|
|
338
435
|
|
|
339
436
|
constructor(options: ParticlesOptions = {}) {
|
|
340
437
|
super()
|
|
341
|
-
this._material = options.material ?? Material.particles(options)
|
|
438
|
+
this._material = options.material ?? (options.mesh ? Material.lit() : Material.particles(options))
|
|
342
439
|
_creator.createParticleSystem(this.id, Material.idOf(this._material), options.maxParticles ?? 0)
|
|
343
440
|
|
|
344
441
|
const id = this.id
|
|
@@ -355,14 +452,21 @@ export class Particles extends Node {
|
|
|
355
452
|
})
|
|
356
453
|
|
|
357
454
|
const out: number[] = []
|
|
358
|
-
if (options.
|
|
455
|
+
if (options.mesh) {
|
|
456
|
+
out.push(TAG_RENDER_MODE, 1, 3)
|
|
457
|
+
pushMesh(out, options.mesh)
|
|
458
|
+
} else if (options.render !== undefined && options.render !== "point") {
|
|
359
459
|
out.push(TAG_RENDER_MODE, 1, options.render === "ribbon" ? 2 : 1)
|
|
360
460
|
}
|
|
461
|
+
if (options.angularVelocity !== undefined) pushAngularVelocity(out, options.angularVelocity)
|
|
462
|
+
if (options.ground !== undefined) pushGround(out, options.ground)
|
|
361
463
|
if (options.space !== undefined) out.push(TAG_SPACE, 1, options.space === "world" ? 1 : 0)
|
|
362
464
|
if (options.inheritVelocity !== undefined) out.push(TAG_INHERIT_VELOCITY, 1, options.inheritVelocity)
|
|
363
465
|
if (options.rateOverDistance !== undefined) out.push(TAG_RATE_DISTANCE, 1, options.rateOverDistance)
|
|
466
|
+
if (options.order !== undefined) out.push(TAG_ORDER, 1, options.order)
|
|
364
467
|
if (options.rate !== undefined) out.push(TAG_RATE, 1, options.rate)
|
|
365
468
|
if (options.shape !== undefined) pushShape(out, options.shape)
|
|
469
|
+
if (options.velocityOverLife !== undefined) pushVelocityLife(out, options.velocityOverLife)
|
|
366
470
|
if (options.startVelocity !== undefined) pushVelocity(out, options.startVelocity)
|
|
367
471
|
if (options.acceleration !== undefined) {
|
|
368
472
|
const a = options.acceleration
|
|
@@ -381,7 +485,7 @@ export class Particles extends Node {
|
|
|
381
485
|
if (options.opacity !== undefined) pushParam(out, 2, options.opacity)
|
|
382
486
|
if (options.frame !== undefined) {
|
|
383
487
|
pushParam(out, 3, options.frame)
|
|
384
|
-
} else if (options.material === undefined) {
|
|
488
|
+
} else if (options.material === undefined && !options.mesh) {
|
|
385
489
|
const [ cols, rows ] = _sheetColsRows(options.sheet)
|
|
386
490
|
const frames = cols * rows
|
|
387
491
|
if (frames > 1) {
|
|
@@ -412,6 +516,13 @@ export class Particles extends Node {
|
|
|
412
516
|
set space(val: "local" | "world") { this._send([ TAG_SPACE, 1, val === "world" ? 1 : 0 ]) }
|
|
413
517
|
set inheritVelocity(val: number) { this._send([ TAG_INHERIT_VELOCITY, 1, val ]) }
|
|
414
518
|
set rateOverDistance(val: number) { this._send([ TAG_RATE_DISTANCE, 1, val ]) }
|
|
519
|
+
set order(val: number) { this._send([ TAG_ORDER, 1, val ]) }
|
|
520
|
+
|
|
521
|
+
set velocityOverLife(v: VelocityOverLife) {
|
|
522
|
+
const out: number[] = []
|
|
523
|
+
pushVelocityLife(out, v)
|
|
524
|
+
this._send(out)
|
|
525
|
+
}
|
|
415
526
|
|
|
416
527
|
set shape(shape: Shape) {
|
|
417
528
|
const out: number[] = []
|
|
@@ -419,6 +530,25 @@ export class Particles extends Node {
|
|
|
419
530
|
this._send(out)
|
|
420
531
|
}
|
|
421
532
|
|
|
533
|
+
/** (mesh mode) Replace the particle mesh; live particles keep flying as the new shape. */
|
|
534
|
+
set mesh(g: Geometry) {
|
|
535
|
+
const out: number[] = [ TAG_RENDER_MODE, 1, 3 ]
|
|
536
|
+
pushMesh(out, g)
|
|
537
|
+
this._send(out)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
set angularVelocity(v: Vec3Like | { min: Vec3Like, max: Vec3Like }) {
|
|
541
|
+
const out: number[] = []
|
|
542
|
+
pushAngularVelocity(out, v)
|
|
543
|
+
this._send(out)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
set ground(g: ParticleGround | null) {
|
|
547
|
+
const out: number[] = []
|
|
548
|
+
pushGround(out, g)
|
|
549
|
+
this._send(out)
|
|
550
|
+
}
|
|
551
|
+
|
|
422
552
|
set startVelocity(val: Vec3Like | VelocityValue) {
|
|
423
553
|
const out: number[] = []
|
|
424
554
|
pushVelocity(out, val)
|
|
@@ -1,119 +1,126 @@
|
|
|
1
|
-
// Rigid body as an aspect on a 3D Node, backed by a JoltPhysics body in creator-gl. Requires a Shape
|
|
2
|
-
// aspect on the same node (its geometry becomes the body's collision shape).
|
|
3
|
-
//
|
|
4
|
-
// Physics.configure({ gravity: [0, -9.81, 0] }) // once, before creating bodies
|
|
5
|
-
// ground.aspect(Shape, { box: [20, 0.5, 20] }).aspect(Physics, { motion: 'static' })
|
|
6
|
-
// crate.aspect(Shape, {}).aspect(Physics, { mass: 2 }) // auto box from the mesh
|
|
7
|
-
// crate.physics.applyImpulse([0, 6, 0])
|
|
8
|
-
// crate.addEventListener('enter', other => …) // contact / trigger overlap began
|
|
9
|
-
//
|
|
10
|
-
// For a `dynamic` body, physics OWNS the transform — do NOT set node.position per frame; drive it via
|
|
11
|
-
// velocity / applyImpulse. Reading node.position is free (native already wrote it). Kinematic bodies
|
|
12
|
-
// are driven with moveTo.
|
|
13
|
-
|
|
14
|
-
import { Aspect } from "../core/Aspect"
|
|
15
|
-
import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
16
|
-
import { nodeRegistry, type Node } from "./Node"
|
|
17
|
-
import { Shape } from "./Shape"
|
|
18
|
-
|
|
19
|
-
export type MotionType = "static" | "dynamic" | "kinematic"
|
|
20
|
-
const MOTION: Record<MotionType, number> = { static: 0, kinematic: 1, dynamic: 2 }
|
|
21
|
-
|
|
22
|
-
export interface PhysicsConfig {
|
|
23
|
-
/** Gravity in world units/s² (Y-up: down is negative). Default [0, -9.81, 0]. */
|
|
24
|
-
gravity?: Vec3Like
|
|
25
|
-
/** Max simultaneous bodies (resizes the world; only before any body exists). Default 4096. */
|
|
26
|
-
maxBodies?: number
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Closest raycast hit. */
|
|
30
|
-
export interface RayHit {
|
|
31
|
-
node: Node | null
|
|
32
|
-
point: Vec3
|
|
33
|
-
normal: Vec3
|
|
34
|
-
fraction: number
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export class Physics extends Aspect<"physics", Node> {
|
|
38
|
-
static readonly aspect = "physics"
|
|
39
|
-
|
|
40
|
-
/** Render interpolation of body transforms between the fixed 60 Hz steps (global; default on). Turn
|
|
41
|
-
* off to save per-frame transform writes when you have many moving bodies / don't need >60 Hz
|
|
42
|
-
* smoothness — bodies then advance in discrete steps. */
|
|
43
|
-
private static _interpolation = true
|
|
44
|
-
static get interpolation(): boolean { return Physics._interpolation }
|
|
45
|
-
static set interpolation(on: boolean) { Physics._interpolation = on; _creator.setInterpolation(on) }
|
|
46
|
-
|
|
47
|
-
/** Default "dynamic". */
|
|
48
|
-
motion: MotionType = "dynamic"
|
|
49
|
-
/** Mass in kg for dynamic bodies (default 1). Ignored for static/kinematic. */
|
|
50
|
-
mass = 1
|
|
51
|
-
|
|
52
|
-
private _bodyId = 0
|
|
53
|
-
|
|
54
|
-
onAttach(): void {
|
|
55
|
-
if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return
|
|
56
|
-
const shape = this.node.get(Shape)
|
|
57
|
-
if (!shape) {
|
|
58
|
-
throw new Error("Physics requires a Shape aspect — add it first: node.aspect(Shape, {…}).aspect(Physics, {…})")
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
1
|
+
// Rigid body as an aspect on a 3D Node, backed by a JoltPhysics body in creator-gl. Requires a Shape
|
|
2
|
+
// aspect on the same node (its geometry becomes the body's collision shape).
|
|
3
|
+
//
|
|
4
|
+
// Physics.configure({ gravity: [0, -9.81, 0] }) // once, before creating bodies
|
|
5
|
+
// ground.aspect(Shape, { box: [20, 0.5, 20] }).aspect(Physics, { motion: 'static' })
|
|
6
|
+
// crate.aspect(Shape, {}).aspect(Physics, { mass: 2 }) // auto box from the mesh
|
|
7
|
+
// crate.physics.applyImpulse([0, 6, 0])
|
|
8
|
+
// crate.addEventListener('enter', other => …) // contact / trigger overlap began
|
|
9
|
+
//
|
|
10
|
+
// For a `dynamic` body, physics OWNS the transform — do NOT set node.position per frame; drive it via
|
|
11
|
+
// velocity / applyImpulse. Reading node.position is free (native already wrote it). Kinematic bodies
|
|
12
|
+
// are driven with moveTo.
|
|
13
|
+
|
|
14
|
+
import { Aspect } from "../core/Aspect"
|
|
15
|
+
import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
16
|
+
import { nodeRegistry, type Node } from "./Node"
|
|
17
|
+
import { Shape } from "./Shape"
|
|
18
|
+
|
|
19
|
+
export type MotionType = "static" | "dynamic" | "kinematic"
|
|
20
|
+
const MOTION: Record<MotionType, number> = { static: 0, kinematic: 1, dynamic: 2 }
|
|
21
|
+
|
|
22
|
+
export interface PhysicsConfig {
|
|
23
|
+
/** Gravity in world units/s² (Y-up: down is negative). Default [0, -9.81, 0]. */
|
|
24
|
+
gravity?: Vec3Like
|
|
25
|
+
/** Max simultaneous bodies (resizes the world; only before any body exists). Default 4096. */
|
|
26
|
+
maxBodies?: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Closest raycast hit. */
|
|
30
|
+
export interface RayHit {
|
|
31
|
+
node: Node | null
|
|
32
|
+
point: Vec3
|
|
33
|
+
normal: Vec3
|
|
34
|
+
fraction: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class Physics extends Aspect<"physics", Node> {
|
|
38
|
+
static readonly aspect = "physics"
|
|
39
|
+
|
|
40
|
+
/** Render interpolation of body transforms between the fixed 60 Hz steps (global; default on). Turn
|
|
41
|
+
* off to save per-frame transform writes when you have many moving bodies / don't need >60 Hz
|
|
42
|
+
* smoothness — bodies then advance in discrete steps. */
|
|
43
|
+
private static _interpolation = true
|
|
44
|
+
static get interpolation(): boolean { return Physics._interpolation }
|
|
45
|
+
static set interpolation(on: boolean) { Physics._interpolation = on; _creator.setInterpolation(on) }
|
|
46
|
+
|
|
47
|
+
/** Default "dynamic". */
|
|
48
|
+
motion: MotionType = "dynamic"
|
|
49
|
+
/** Mass in kg for dynamic bodies (default 1). Ignored for static/kinematic. */
|
|
50
|
+
mass = 1
|
|
51
|
+
|
|
52
|
+
private _bodyId = 0
|
|
53
|
+
|
|
54
|
+
onAttach(): void {
|
|
55
|
+
if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return
|
|
56
|
+
const shape = this.node.get(Shape)
|
|
57
|
+
if (!shape) {
|
|
58
|
+
throw new Error("Physics requires a Shape aspect — add it first: node.aspect(Shape, {…}).aspect(Physics, {…})")
|
|
59
|
+
}
|
|
60
|
+
if (this.motion === "dynamic" && shape._isTriangleMesh) {
|
|
61
|
+
throw new Error("Physics: a dynamic body cannot use Shape { mesh: true } (triangle meshes are static/kinematic only) — use { mesh: 'convex' } or a primitive shape")
|
|
62
|
+
}
|
|
63
|
+
const shapeId = shape._claim() // drop the pick-only body; reuse its shape
|
|
64
|
+
this._bodyId = _creator.physicsCreateBody(this.node.id, shapeId, MOTION[this.motion], this.mass, false, false)
|
|
65
|
+
shape._ownBody(this._bodyId)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
onDetach(): void {
|
|
69
|
+
if (this._bodyId) { _creator.physicsRemoveBody(this._bodyId); this._bodyId = 0 }
|
|
70
|
+
const shape = this.node.get(Shape)
|
|
71
|
+
if (shape) {
|
|
72
|
+
shape._ownBody(0) // the Shape held OUR body id — forget it, or its own detach removes it twice (native crash)
|
|
73
|
+
shape._recreatePickBody() // keep the node pickable if the Shape stays
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Native Jolt body id. 0 until attached, or if the build has no physics support. */
|
|
78
|
+
get id(): number { return this._bodyId }
|
|
79
|
+
|
|
80
|
+
/** Linear velocity in world units/second (fresh Vec3 on read). */
|
|
81
|
+
get velocity(): Vec3 {
|
|
82
|
+
const out = new Float32Array(3)
|
|
83
|
+
if (this._bodyId) _creator.physicsGetLinearVelocity(this._bodyId, out)
|
|
84
|
+
return new Vec3(out[0], out[1], out[2])
|
|
85
|
+
}
|
|
86
|
+
set velocity(v: Vec3Like) {
|
|
87
|
+
if (this._bodyId) _creator.physicsSetLinearVelocity(this._bodyId, cx(v), cy(v), cz(v))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Apply an instantaneous impulse (kg·m/s) and wake the body. */
|
|
91
|
+
applyImpulse(v: Vec3Like): this {
|
|
92
|
+
if (this._bodyId) _creator.physicsApplyImpulse(this._bodyId, cx(v), cy(v), cz(v))
|
|
93
|
+
return this
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Teleport / drive a kinematic (or dynamic) body to a world position. */
|
|
97
|
+
moveTo(p: Vec3Like): this {
|
|
98
|
+
if (this._bodyId) _creator.physicsSetBodyPosition(this._bodyId, cx(p), cy(p), cz(p))
|
|
99
|
+
return this
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// --- world (static) -------------------------------------------------------------------------
|
|
103
|
+
/** Configure gravity / limits. Call once before creating bodies. */
|
|
104
|
+
static configure(config: PhysicsConfig = {}): void {
|
|
105
|
+
const g = config.gravity ?? [0, -9.81, 0]
|
|
106
|
+
_creator.physicsConfigure(cx(g), cy(g), cz(g), config.maxBodies ?? 0)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Whether this build has physics support (CREATOR_GL_PHYSICS). */
|
|
110
|
+
static get supported(): boolean {
|
|
111
|
+
return !!_creator.physicsHasSupport && _creator.physicsHasSupport()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Closest pickable body hit by the ray from `origin` along `dir` (up to `maxDist`), or null. */
|
|
115
|
+
static raycast(origin: Vec3Like, dir: Vec3Like, maxDist = 1000): RayHit | null {
|
|
116
|
+
const out = new Float32Array(7)
|
|
117
|
+
const id = _creator.physicsRaycast(cx(origin), cy(origin), cz(origin), cx(dir), cy(dir), cz(dir), maxDist, out)
|
|
118
|
+
if (!id) return null
|
|
119
|
+
return {
|
|
120
|
+
node: nodeRegistry.get(id) ?? null,
|
|
121
|
+
point: new Vec3(out[0], out[1], out[2]),
|
|
122
|
+
normal: new Vec3(out[3], out[4], out[5]),
|
|
123
|
+
fraction: out[6],
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|