lecodes-sdk 0.20.2 → 1.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.
- package/dist/global.d.ts +35 -9
- package/dist/types/animate/tween/Animation.d.ts +69 -0
- package/dist/types/animate/tween/Timeline.d.ts +55 -0
- package/dist/types/animate/tween/animateValue.d.ts +27 -0
- package/dist/types/animate/tween/easing.d.ts +29 -0
- package/dist/types/animate/tween/spec.d.ts +178 -0
- package/dist/types/canvas/Canvas.d.ts +2 -0
- package/dist/types/g2/Node2D.d.ts +16 -0
- package/dist/types/g2/Sprite.d.ts +11 -1
- package/dist/types/gl/Camera.d.ts +15 -1
- package/dist/types/gl/DecalSet.d.ts +48 -3
- package/dist/types/gl/Foliage.d.ts +47 -0
- package/dist/types/gl/Geometry.d.ts +36 -0
- package/dist/types/gl/Light.d.ts +25 -7
- package/dist/types/gl/Lightmap.d.ts +90 -51
- package/dist/types/gl/Material.d.ts +32 -20
- package/dist/types/gl/Mesh.d.ts +7 -1
- package/dist/types/gl/Model.d.ts +41 -5
- package/dist/types/gl/Node.d.ts +18 -0
- package/dist/types/gl/Particles.d.ts +53 -1
- package/dist/types/gl/Scene.d.ts +21 -1
- package/dist/types/gl/animation/AnimationClip.d.ts +19 -0
- package/dist/types/gl/animation/Animator.d.ts +27 -0
- package/dist/types/gl/animation/DynamicBone.d.ts +184 -0
- package/dist/types/gl/animation/IK.d.ts +109 -0
- package/dist/types/gl/{Locomotion.d.ts → animation/Locomotion.d.ts} +6 -6
- package/dist/types/gl/animation/Warp.d.ts +2 -1
- package/dist/types/gl/animation/core.d.ts +35 -4
- package/dist/types/gl/{AudioSource.d.ts → audio/AudioSource.d.ts} +6 -6
- package/dist/types/gl/{AudioZone.d.ts → audio/AudioZone.d.ts} +4 -4
- package/dist/types/gl/{SceneAudio.d.ts → audio/SceneAudio.d.ts} +1 -1
- package/dist/types/gl/{NavAgent.d.ts → nav/NavAgent.d.ts} +4 -4
- package/dist/types/gl/{NavMesh.d.ts → nav/NavMesh.d.ts} +4 -4
- package/dist/types/gl/{CharacterController.d.ts → physics/CharacterController.d.ts} +5 -5
- package/dist/types/gl/{Physics.d.ts → physics/Physics.d.ts} +6 -5
- package/dist/types/gl/physics/Ragdoll.d.ts +161 -0
- package/dist/types/gl/{Shape.d.ts → physics/Shape.d.ts} +3 -3
- package/dist/types/gl/{Trigger.d.ts → physics/Trigger.d.ts} +2 -2
- package/dist/types/gl/{Terrain.d.ts → terrain/Terrain.d.ts} +10 -8
- package/dist/types/gl/{terrainMesh.d.ts → terrain/terrainMesh.d.ts} +1 -1
- package/dist/types/gl/vehicle/Vehicle.d.ts +300 -0
- package/dist/types/gl/vehicle/Wheel.d.ts +147 -0
- package/dist/types/inject.d.ts +35 -27
- package/dist/types/runtime/files.d.ts +24 -1
- package/dist/types/scene/defineScene.d.ts +50 -31
- package/dist/types/ui/UIButton.d.ts +3 -1
- package/dist/types/ui/UIInput.d.ts +5 -1
- package/dist/types/ui/UINode.d.ts +24 -24
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/prompts/README.md +142 -142
- package/prompts/core-design.md +27 -4
- package/prompts/core.md +35 -6
- package/prompts/select.ts +19 -4
- package/src/animate/tween/Animation.ts +378 -0
- package/src/animate/tween/Timeline.ts +175 -0
- package/src/animate/tween/animateValue.ts +100 -0
- package/src/animate/tween/easing.ts +172 -0
- package/src/animate/tween/spec.ts +479 -0
- package/src/bridges.d.ts +1760 -1481
- package/src/canvas/Canvas.ts +21 -0
- package/src/compile/__tests__/assetMacro.test.ts +26 -0
- package/src/compile/__tests__/compile.test.ts +11 -0
- package/src/compile/__tests__/detectEntry.test.ts +19 -0
- package/src/compile/__tests__/serverSplit.test.ts +27 -0
- package/src/compile/bundler.ts +34 -4
- package/src/compile/compileProject.ts +31 -1
- package/src/compile/detectEntry.ts +8 -3
- package/src/compile/header.ts +6 -3
- package/src/compile/index.ts +3 -0
- package/src/compile/sceneEditor.ts +42 -1
- package/src/compile/serverSplit.ts +9 -3
- package/src/g2/Node2D.ts +38 -0
- package/src/g2/Sprite.ts +20 -1
- package/src/gl/Camera.ts +34 -1
- package/src/gl/DecalSet.ts +132 -5
- package/src/gl/Foliage.ts +102 -0
- package/src/gl/Geometry.ts +109 -0
- package/src/gl/Light.ts +46 -16
- package/src/gl/Lightmap.ts +440 -249
- package/src/gl/Material.ts +69 -36
- package/src/gl/Mesh.ts +120 -102
- package/src/gl/Model.ts +167 -124
- package/src/gl/Node.ts +40 -1
- package/src/gl/Particles.ts +82 -5
- package/src/gl/Scene.ts +35 -2
- package/src/gl/animation/AnimationClip.ts +52 -0
- package/src/gl/animation/Animator.ts +42 -2
- package/src/gl/animation/DynamicBone.ts +482 -0
- package/src/gl/animation/IK.ts +214 -0
- package/src/gl/{Locomotion.ts → animation/Locomotion.ts} +7 -7
- package/src/gl/animation/Playback.ts +5 -4
- package/src/gl/animation/Warp.ts +5 -2
- package/src/gl/animation/core.ts +65 -4
- package/src/gl/{AudioSource.ts → audio/AudioSource.ts} +7 -7
- package/src/gl/{AudioZone.ts → audio/AudioZone.ts} +75 -75
- package/src/gl/{SceneAudio.ts → audio/SceneAudio.ts} +2 -2
- package/src/gl/{NavAgent.ts → nav/NavAgent.ts} +5 -5
- package/src/gl/{NavMesh.ts → nav/NavMesh.ts} +8 -8
- package/src/gl/{CharacterController.ts → physics/CharacterController.ts} +5 -5
- package/src/gl/{Physics.ts → physics/Physics.ts} +12 -5
- package/src/gl/physics/Ragdoll.ts +451 -0
- package/src/gl/{Shape.ts → physics/Shape.ts} +3 -3
- package/src/gl/{Trigger.ts → physics/Trigger.ts} +45 -45
- package/src/gl/{physicsEvents.ts → physics/physicsEvents.ts} +1 -1
- package/src/gl/{Terrain.ts → terrain/Terrain.ts} +14 -12
- package/src/gl/{terrainMesh.ts → terrain/terrainMesh.ts} +1 -1
- package/src/gl/vehicle/Vehicle.ts +666 -0
- package/src/gl/vehicle/Wheel.ts +290 -0
- package/src/inject.ts +236 -224
- package/src/runtime/files.ts +32 -2
- package/src/scene/defineScene.ts +92 -66
- package/src/scene/level.ts +2 -2
- package/src/ui/UIButton.ts +2 -2
- package/src/ui/UIInput.ts +3 -3
- package/src/ui/UINode.ts +61 -36
- package/dist/types/animate/animate.d.ts +0 -20
- package/dist/types/gl/Gearbox.d.ts +0 -86
- package/dist/types/gl/IK.d.ts +0 -53
- package/dist/types/gl/Ragdoll.d.ts +0 -86
- package/dist/types/gl/Vehicle.d.ts +0 -191
- package/dist/types/gl/Wheel.d.ts +0 -95
- package/src/animate/animate.ts +0 -238
- package/src/gl/Gearbox.ts +0 -212
- package/src/gl/IK.ts +0 -193
- package/src/gl/Ragdoll.ts +0 -270
- package/src/gl/Vehicle.ts +0 -473
- package/src/gl/Wheel.ts +0 -240
- /package/dist/types/gl/{physicsEvents.d.ts → physics/physicsEvents.d.ts} +0 -0
package/src/gl/DecalSet.ts
CHANGED
|
@@ -35,6 +35,14 @@ export type DecalOptions = {
|
|
|
35
35
|
fadeIn?: number
|
|
36
36
|
/** Seconds of fade at the end of `life` (ignored with life 0). Default 0. */
|
|
37
37
|
fadeOut?: number
|
|
38
|
+
/** Opacity multipliers at the image's BOTTOM and TOP edge (−Y / +Y), interpolated along it — a
|
|
39
|
+
* tire mark that darkens as the slide deepens. Default `[1, 1]`. */
|
|
40
|
+
gradient?: [number, number]
|
|
41
|
+
/** Tilt of the image's bottom and top edge, as a unit-space slope (Δy per Δx across the box):
|
|
42
|
+
* the edge pivots on the corner of the side it keeps and cuts INTO the box on the other, so two
|
|
43
|
+
* boxes can meet on one shared line — what `DecalTrail` uses to mitre its joints. `0` = square.
|
|
44
|
+
* Default `[0, 0]`. */
|
|
45
|
+
caps?: [number, number]
|
|
38
46
|
}
|
|
39
47
|
|
|
40
48
|
export type DecalSetOptions = DecalOptions & {
|
|
@@ -63,8 +71,10 @@ export type DecalSetOptions = DecalOptions & {
|
|
|
63
71
|
/** HDR boost of the image (0 = none). Default material only. */
|
|
64
72
|
emissive?: number
|
|
65
73
|
name?: string
|
|
66
|
-
/** Coarse draw order, 0 (first) … 7 (last)
|
|
67
|
-
*
|
|
74
|
+
/** Coarse draw order, 0 (first) … 7 (last) — see `Mesh.renderPriority`. Default 3: a decal is
|
|
75
|
+
* part of the surface it sits on, so it draws before every other blended thing (meshes 4,
|
|
76
|
+
* particles 5) — the engine's depth sort compares object centres, and a set spread over the
|
|
77
|
+
* level has no useful centre. */
|
|
68
78
|
renderPriority?: number
|
|
69
79
|
}
|
|
70
80
|
|
|
@@ -86,7 +96,7 @@ export type DecalPlacement = DecalOptions & {
|
|
|
86
96
|
rotation?: QuatLike | Vec3Like
|
|
87
97
|
}
|
|
88
98
|
|
|
89
|
-
const RECORD =
|
|
99
|
+
const RECORD = 27
|
|
90
100
|
const NO_SLOT = 0xFFFFFFFF
|
|
91
101
|
|
|
92
102
|
export class DecalSet extends Node {
|
|
@@ -116,7 +126,7 @@ export class DecalSet extends Node {
|
|
|
116
126
|
}
|
|
117
127
|
if (_creator.createDecalSet) {
|
|
118
128
|
_creator.createDecalSet(this.id, Material.idOf(this._material), this._max)
|
|
119
|
-
|
|
129
|
+
this.renderPriority = options.renderPriority ?? 3
|
|
120
130
|
} else if (!DecalSet._warned) {
|
|
121
131
|
DecalSet._warned = true
|
|
122
132
|
console.warn("[decals] this host has no createDecalSet — decals are not drawn")
|
|
@@ -200,7 +210,8 @@ export class DecalSet extends Node {
|
|
|
200
210
|
return this._write(slot, right, up, normal, new Vec3(p.position), p)
|
|
201
211
|
}
|
|
202
212
|
|
|
203
|
-
|
|
213
|
+
/** @internal One decal's frame → record → host (`DecalTrail` writes its segments through here). */
|
|
214
|
+
_write(slot: number, right: Vec3, up: Vec3, normal: Vec3, centre: Vec3, o: DecalOptions): number {
|
|
204
215
|
const d = this._defaults
|
|
205
216
|
const size = o.size ?? d.size!
|
|
206
217
|
const w = typeof size === "number" ? size : size[0]
|
|
@@ -222,6 +233,10 @@ export class DecalSet extends Node {
|
|
|
222
233
|
const opacity = o.opacity ?? d.opacity!
|
|
223
234
|
r[16] = tr; r[17] = tg; r[18] = tb; r[19] = ta * opacity
|
|
224
235
|
r[20] = o.life ?? d.life!; r[21] = o.fadeIn ?? d.fadeIn!; r[22] = o.fadeOut ?? d.fadeOut!
|
|
236
|
+
const caps = o.caps ?? d.caps
|
|
237
|
+
r[23] = caps ? caps[0] : 0; r[24] = caps ? caps[1] : 0
|
|
238
|
+
const gradient = o.gradient ?? d.gradient
|
|
239
|
+
r[25] = gradient ? gradient[0] : 1; r[26] = gradient ? gradient[1] : 1
|
|
225
240
|
if (slot === NO_SLOT) {
|
|
226
241
|
if (!_creator.addDecal) return -1
|
|
227
242
|
const s = _creator.addDecal(this.id, r)
|
|
@@ -231,3 +246,115 @@ export class DecalSet extends Node {
|
|
|
231
246
|
return slot
|
|
232
247
|
}
|
|
233
248
|
}
|
|
249
|
+
|
|
250
|
+
/** A `DecalTrail`'s look: the strip's width, and what every segment carries. */
|
|
251
|
+
export type DecalTrailOptions = Omit<DecalOptions, "size" | "caps" | "gradient"> & {
|
|
252
|
+
/** Width of the strip on the surface, world units. */
|
|
253
|
+
width: number
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// The mitre stops making sense past this turn between two segments (a sharp reversal would need a
|
|
257
|
+
// box longer than the strip is wide); the trail breaks there and starts afresh.
|
|
258
|
+
const TRAIL_MAX_SLOPE = 1.75 // tan 60°
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* A continuous strip of decals along a path — a tire's skid mark, a dragged body, a tread track.
|
|
262
|
+
* Feed it points (`add`) as the thing moves; every new point becomes one box-decal SEGMENT from the
|
|
263
|
+
* previous point, its image's up along the travel, and the joints between segments are MITRED: each
|
|
264
|
+
* box is cut along the bisector it shares with its neighbour (`caps`), so the strip has no overlaps
|
|
265
|
+
* darkening the outside of a bend and no wedges of gap on the inside. The opacity given with each
|
|
266
|
+
* point is interpolated along the segment (`gradient`), so a mark can darken as a slide deepens and
|
|
267
|
+
* fade as it ends. Segments are the set's decals — same ring, same `max` budget: a set of 900 holds
|
|
268
|
+
* 900 segments across every trail drawn from it.
|
|
269
|
+
*
|
|
270
|
+
* The previous segment is REWRITTEN when the next point arrives (its far cap becomes the shared
|
|
271
|
+
* mitre), through `DecalSet.update`'s path — a recycled slot is simply left alone by the engine.
|
|
272
|
+
*/
|
|
273
|
+
export class DecalTrail {
|
|
274
|
+
private _prev: { point: Vec3, opacity: number } | null = null
|
|
275
|
+
private _seg: {
|
|
276
|
+
slot: number, start: Vec3, end: Vec3, dir: Vec3, right: Vec3, normal: Vec3,
|
|
277
|
+
slopeStart: number, a0: number, a1: number,
|
|
278
|
+
} | null = null
|
|
279
|
+
|
|
280
|
+
readonly set: DecalSet
|
|
281
|
+
readonly options: DecalTrailOptions
|
|
282
|
+
|
|
283
|
+
constructor(set: DecalSet, options: DecalTrailOptions) {
|
|
284
|
+
this.set = set
|
|
285
|
+
this.options = options
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Extend the strip to `point` on a surface with `normal`, `opacity` (0..1) at that point. The
|
|
289
|
+
* first call after a start / `end()` only anchors the strip. Returns the new segment's slot, or
|
|
290
|
+
* −1 when nothing was drawn (the anchor, a point that did not move, no host support). */
|
|
291
|
+
add(point: Vec3Like, normal: Vec3Like, opacity = 1): number {
|
|
292
|
+
const p = new Vec3(point)
|
|
293
|
+
const n = new Vec3(normal).normalize()
|
|
294
|
+
const prev = this._prev
|
|
295
|
+
this._prev = { point: p, opacity }
|
|
296
|
+
if (!prev) return -1
|
|
297
|
+
// The segment, flattened onto the surface plane.
|
|
298
|
+
let d = p.sub(prev.point)
|
|
299
|
+
d = d.sub(n.scale(d.dot(n)))
|
|
300
|
+
const len = d.length()
|
|
301
|
+
if (len < 1e-3) { this._prev = prev; return -1 } // not moved: wait for a real step
|
|
302
|
+
const dir = d.scale(1 / len)
|
|
303
|
+
const right = dir.cross(n) // DecalSet.spawn's frame: right × up = normal, image unmirrored from the normal's side
|
|
304
|
+
const w = this.options.width
|
|
305
|
+
|
|
306
|
+
// Mitre with the previous segment: the joint's bisector is the sum of the two right normals,
|
|
307
|
+
// and each box takes it as a cap — the previous one at its END (rewritten now), this one at its
|
|
308
|
+
// START — with the slopes measured in each box's own frame. A trail's first segment, or one
|
|
309
|
+
// after a break, starts square.
|
|
310
|
+
let slopeStart = 0
|
|
311
|
+
const last = this._seg
|
|
312
|
+
if (last) {
|
|
313
|
+
const lastDir = last.dir.sub(n.scale(last.dir.dot(n))).normalize()
|
|
314
|
+
const lastRight = lastDir.cross(n)
|
|
315
|
+
const m = lastRight.add(right)
|
|
316
|
+
const denom = m.dot(right)
|
|
317
|
+
const slopeEnd = Math.abs(denom) > 1e-4 ? m.dot(lastDir) / denom : Infinity
|
|
318
|
+
if (Math.abs(slopeEnd) <= TRAIL_MAX_SLOPE) {
|
|
319
|
+
this._rewrite(last, slopeEnd)
|
|
320
|
+
slopeStart = -slopeEnd
|
|
321
|
+
} else {
|
|
322
|
+
this._rewrite(last, 0) // too sharp to mitre: leave the last one square, start afresh
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const seg = { slot: -1, start: prev.point, end: p, dir, right, normal: n, slopeStart, a0: prev.opacity, a1: opacity }
|
|
326
|
+
seg.slot = this._box(seg, 0, -1)
|
|
327
|
+
this._seg = seg
|
|
328
|
+
return seg.slot
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Finish the strip. With a `point`, one last segment runs out to it at opacity 0 — a mark that
|
|
332
|
+
* tapers away instead of stopping dead. The next `add` anchors a new strip. */
|
|
333
|
+
end(point?: Vec3Like, normal: Vec3Like = Vec3.up): this {
|
|
334
|
+
if (point && this._prev) this.add(point, normal, 0)
|
|
335
|
+
this._prev = null
|
|
336
|
+
this._seg = null
|
|
337
|
+
return this
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private _rewrite(seg: NonNullable<DecalTrail["_seg"]>, slopeEnd: number): void {
|
|
341
|
+
if (seg.slot >= 0) this._box(seg, slopeEnd, seg.slot)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Write one segment's box: the strip between its joints, extended past each mitred joint by the
|
|
345
|
+
* mitre's reach (w/2 · |slope|) so the tilted cap still passes through the joint's centre. */
|
|
346
|
+
private _box(seg: NonNullable<DecalTrail["_seg"]>, slopeEnd: number, slot: number): number {
|
|
347
|
+
const w = this.options.width
|
|
348
|
+
const L = seg.end.sub(seg.start).length()
|
|
349
|
+
const extStart = 0.5 * w * Math.abs(seg.slopeStart)
|
|
350
|
+
const extEnd = 0.5 * w * Math.abs(slopeEnd)
|
|
351
|
+
const boxLen = L + extStart + extEnd
|
|
352
|
+
const centre = seg.start.add(seg.dir.scale(0.5 * boxLen - extStart))
|
|
353
|
+
// World slopes → unit-space slopes: x is measured in widths, y in box lengths.
|
|
354
|
+
const caps: [number, number] = [ seg.slopeStart * (w / boxLen), slopeEnd * (w / boxLen) ]
|
|
355
|
+
const { width: _w, ...rest } = this.options
|
|
356
|
+
const s = this.set._write(slot < 0 ? NO_SLOT : slot, seg.right, seg.dir, seg.normal, centre,
|
|
357
|
+
{ ...rest, size: [ w, boxLen ], caps, gradient: [ seg.a0, seg.a1 ] })
|
|
358
|
+
return slot < 0 ? s : slot
|
|
359
|
+
}
|
|
360
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Foliage — the vegetation tier of the lightmap material family (creator-gl materials/src/foliage.mat).
|
|
2
|
+
//
|
|
3
|
+
// A model loaded with `Model.load(src, { foliage: true })` (scene files: `foliage: true` on a `model`
|
|
4
|
+
// node) takes a shader whose vertices sway in the wind, bend away from BENDERS (the hero, enemies —
|
|
5
|
+
// any node registered with `Foliage.bend`) and thin out over a distance range, and whose fragment
|
|
6
|
+
// tints every copy a little differently. All of it is level-wide state the engine keeps: the wind
|
|
7
|
+
// and the sway options apply to every foliage model, present and future; benders are read from
|
|
8
|
+
// their live world transforms each frame (the 8 nearest the camera reach the shader); a fade is per
|
|
9
|
+
// ASSET — every copy of that model, present and future.
|
|
10
|
+
//
|
|
11
|
+
// Hosts without the tier (no `foliageSet*` bridge) ignore every call; the models then load through
|
|
12
|
+
// the ordinary lightmap tier where that exists, else the standard shader.
|
|
13
|
+
import { Model } from "./Model"
|
|
14
|
+
import { Node } from "./Node"
|
|
15
|
+
|
|
16
|
+
export type FoliageWind = {
|
|
17
|
+
/** Direction on the ground (x, z); normalised by the engine. Default [1, 0.3]. */
|
|
18
|
+
direction?: [number, number]
|
|
19
|
+
/** Metres of sway at `bendHeight` (taller parts sway more). Default 0.07. 0 = still. */
|
|
20
|
+
strength?: number
|
|
21
|
+
/** Radians per second of the main sway. Default 1.2. */
|
|
22
|
+
speed?: number
|
|
23
|
+
/** 0..1: how much the strength breathes over a slow envelope. Default 0.6. */
|
|
24
|
+
gust?: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type FoliageOptions = {
|
|
28
|
+
/** The wind, or `false` for none. */
|
|
29
|
+
wind?: FoliageWind | false
|
|
30
|
+
/** Height above the instance origin (m) where the full sway / touch applies; the base never moves.
|
|
31
|
+
* Default 1.5. */
|
|
32
|
+
bendHeight?: number
|
|
33
|
+
/** Metres the TOP of a small plant leans away from a bender standing at its centre; the whole plant leans
|
|
34
|
+
* as one, rooted at its base. Plants over ~2 m react less, trees over ~5 m not at all (the engine
|
|
35
|
+
* measures each asset). Default 0.6. 0 = no touch reaction. */
|
|
36
|
+
touch?: number
|
|
37
|
+
/** 0..1: how far each copy's tint drifts from the texture (warmer/darker to cooler/lighter, by a hash
|
|
38
|
+
* of its position). Default 0.35. */
|
|
39
|
+
variation?: number
|
|
40
|
+
/** 0..1: wrapped sun lighting — thin leaves let light through. Default 0.5. */
|
|
41
|
+
sunWrap?: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const state: Required<Omit<FoliageOptions, "wind">> & { wind: Required<FoliageWind> | false } = {
|
|
45
|
+
wind: { direction: [ 1, 0.3 ], strength: 0.07, speed: 1.2, gust: 0.6 },
|
|
46
|
+
bendHeight: 1.5,
|
|
47
|
+
touch: 0.6,
|
|
48
|
+
variation: 0.35,
|
|
49
|
+
sunWrap: 0.5,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const pushWind = (): void => {
|
|
53
|
+
const w = state.wind
|
|
54
|
+
if (w === false) _creator.foliageSetWind?.(1, 0, 0, 1, 0)
|
|
55
|
+
else _creator.foliageSetWind?.(w.direction[0], w.direction[1], w.strength, w.speed, w.gust)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const Foliage = {
|
|
59
|
+
/** Level-wide options; each field given replaces the current value (a scene file's `env.foliage`
|
|
60
|
+
* goes through here). */
|
|
61
|
+
configure(options: FoliageOptions): void {
|
|
62
|
+
if (options.wind !== undefined) Foliage.wind(options.wind)
|
|
63
|
+
let sway = false
|
|
64
|
+
if (options.bendHeight !== undefined) { state.bendHeight = options.bendHeight; sway = true }
|
|
65
|
+
if (options.touch !== undefined) { state.touch = options.touch; sway = true }
|
|
66
|
+
if (options.variation !== undefined) { state.variation = options.variation; sway = true }
|
|
67
|
+
if (options.sunWrap !== undefined) { state.sunWrap = options.sunWrap; sway = true }
|
|
68
|
+
if (sway) _creator.foliageSetOptions?.(state.bendHeight, state.touch, state.variation, state.sunWrap)
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
/** The wind: fields given replace the current ones; `false` = calm. Live. */
|
|
72
|
+
wind(wind: FoliageWind | false): void {
|
|
73
|
+
if (wind === false) state.wind = false
|
|
74
|
+
else {
|
|
75
|
+
const cur = state.wind === false ? { direction: [ 1, 0.3 ] as [number, number], strength: 0.07, speed: 1.2, gust: 0.6 } : state.wind
|
|
76
|
+
state.wind = { direction: wind.direction ?? cur.direction, strength: wind.strength ?? cur.strength, speed: wind.speed ?? cur.speed, gust: wind.gust ?? cur.gust }
|
|
77
|
+
}
|
|
78
|
+
pushWind()
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
/** A node whose live position pushes the vegetation within `radius` metres (the hero: ~0.7; a
|
|
82
|
+
* boulder: its size). Any number may be registered; the 8 nearest the camera act each frame. The
|
|
83
|
+
* engine also tracks the node's velocity (smoothed over ~0.2 s): a plant you run through is brushed
|
|
84
|
+
* forward and settles behind you instead of flipping as you cross its centre.
|
|
85
|
+
* Returns the unregister function; a destroyed node drops out on its own. */
|
|
86
|
+
bend(node: Node, radius = 0.7): () => void {
|
|
87
|
+
_creator.foliageSetBender?.(node.id, radius > 0 ? radius : 0.01)
|
|
88
|
+
return () => Foliage.unbend(node)
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
/** Stop a node from bending the vegetation. */
|
|
92
|
+
unbend(node: Node): void {
|
|
93
|
+
_creator.foliageSetBender?.(node.id, 0)
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
/** Distance fade for EVERY copy of this model's asset (present and future): the cards thin out
|
|
97
|
+
* from `start` to `end` metres from the camera and past `end` the copy leaves the frame entirely.
|
|
98
|
+
* Small ground cover (grass, clover, flowers) is the target; `end` 0 = no fade. */
|
|
99
|
+
fade(model: Model, start: number, end: number): void {
|
|
100
|
+
_creator.foliageSetFade?.(model.id, start, end)
|
|
101
|
+
},
|
|
102
|
+
}
|
package/src/gl/Geometry.ts
CHANGED
|
@@ -3,9 +3,25 @@
|
|
|
3
3
|
// worker/src/primitives/*.
|
|
4
4
|
|
|
5
5
|
import { cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
6
|
+
import { fetch } from "../runtime/fetch"
|
|
6
7
|
|
|
7
8
|
export type MeshKind = "triangles" | "vertices" | "edges"
|
|
8
9
|
|
|
10
|
+
/** A geometry file — what `lecodes assets geometry <model.glb>` writes (`<model>.geometry.json`):
|
|
11
|
+
* one merged, world-space triangle buffer. `Geometry.load(asset('./casing.geometry.json'))`. */
|
|
12
|
+
export type GeometryData = {
|
|
13
|
+
format?: "lecodes-geometry"
|
|
14
|
+
version?: number
|
|
15
|
+
/** xyz per vertex. */
|
|
16
|
+
vertices: ArrayLike<number>
|
|
17
|
+
/** xyz per vertex (unit length). */
|
|
18
|
+
normals: ArrayLike<number>
|
|
19
|
+
/** uv per vertex. */
|
|
20
|
+
uv: ArrayLike<number>
|
|
21
|
+
/** Triangle list, ≤ 65535 vertices addressed. */
|
|
22
|
+
indices: ArrayLike<number>
|
|
23
|
+
}
|
|
24
|
+
|
|
9
25
|
// local tuple helpers — geometry building works on raw Float32Array buffers, so it stays out of the
|
|
10
26
|
// Vec3 wrapper (no per-vertex allocation).
|
|
11
27
|
type Tup3 = [number, number, number]
|
|
@@ -65,7 +81,39 @@ export class Geometry {
|
|
|
65
81
|
static box(size: Vec3Like | number = 1): Geometry { return createCube(size) }
|
|
66
82
|
static sphere(options?: SphereOptions): Geometry { return createSphere(options) }
|
|
67
83
|
static cylinder(options?: CylinderOptions): Geometry { return createCylinder(options) }
|
|
84
|
+
/** A capsule along Y, centred at the origin: a cylinder `length` long (between the two cap centres)
|
|
85
|
+
* with a hemisphere of `radius` on each end — one closed surface, smooth across the seams. */
|
|
86
|
+
static capsule(options?: CapsuleOptions): Geometry { return createCapsule(options) }
|
|
68
87
|
static plane(options?: PlaneOptions): Geometry { return createPlane(options) }
|
|
88
|
+
|
|
89
|
+
/** A Geometry from a geometry file's contents (`GeometryData` — see `Geometry.load`). The arrays
|
|
90
|
+
* are copied into typed buffers, so the source object can be dropped. */
|
|
91
|
+
static fromData(data: GeometryData): Geometry {
|
|
92
|
+
if (!data || !data.vertices || !data.indices) throw new Error("Geometry.fromData: not a geometry file (no vertices/indices)")
|
|
93
|
+
if (data.format !== undefined && data.format !== "lecodes-geometry") throw new Error(`Geometry.fromData: unknown format "${data.format}"`)
|
|
94
|
+
const nv = data.vertices.length / 3
|
|
95
|
+
if (nv > 65535) throw new Error(`Geometry.fromData: ${nv} vertices — at most 65535 (uint16 indices)`)
|
|
96
|
+
const vertices = Float32Array.from(data.vertices)
|
|
97
|
+
const normals = data.normals && data.normals.length === vertices.length ? Float32Array.from(data.normals) : new Float32Array(vertices.length)
|
|
98
|
+
const uv = data.uv && data.uv.length === nv * 2 ? Float32Array.from(data.uv) : new Float32Array(nv * 2)
|
|
99
|
+
return new Geometry(vertices, normals, Uint16Array.from(data.indices), uv)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Load a geometry file: `lecodes assets geometry casing.glb` → `casing.geometry.json` →
|
|
103
|
+
* `await Geometry.load(asset('./casing.geometry.json'))`. A GLB's triangles as a plain buffer,
|
|
104
|
+
* for `Particles({ mesh })` debris (shell casings, rubble) and `Mesh.from`.
|
|
105
|
+
* A `.json` project file is INLINED by the compiler — `asset('./x.json')` is the parsed data, not
|
|
106
|
+
* a URL — so that form (and `import data from './x.json'`) is read directly, no fetch; a string
|
|
107
|
+
* (a URL, a non-`.json` asset such as `.geo`) is fetched. */
|
|
108
|
+
static async load(source: string | GeometryData): Promise<Geometry> {
|
|
109
|
+
if (typeof source !== "string") return Geometry.fromData(source)
|
|
110
|
+
const url = source
|
|
111
|
+
const resp = await fetch(url, { useOnce: true })
|
|
112
|
+
if (resp.status >= 400) { resp.dispose(); throw new Error(`Geometry.load: ${url} → HTTP ${resp.status}`) }
|
|
113
|
+
const data = resp.json<GeometryData>()
|
|
114
|
+
resp.dispose()
|
|
115
|
+
return Geometry.fromData(data)
|
|
116
|
+
}
|
|
69
117
|
}
|
|
70
118
|
|
|
71
119
|
// Gutter between lightmap charts (fraction of the chart square): the bake dilates each rect by a
|
|
@@ -193,6 +241,67 @@ const createSphere = (options: SphereOptions = {}): Geometry => {
|
|
|
193
241
|
return new Geometry(vertices, normals, indices, uvs)
|
|
194
242
|
}
|
|
195
243
|
|
|
244
|
+
// --- capsule ---
|
|
245
|
+
export type CapsuleOptions = {
|
|
246
|
+
radius?: number
|
|
247
|
+
/** Distance between the two cap centres (the straight part); 0 = a sphere. Default 1. */
|
|
248
|
+
length?: number
|
|
249
|
+
/** Segments around the axis. */
|
|
250
|
+
widthSegments?: number
|
|
251
|
+
/** Rings per hemisphere (pole to equator). */
|
|
252
|
+
capSegments?: number
|
|
253
|
+
}
|
|
254
|
+
const createCapsule = (options: CapsuleOptions = {}): Geometry => {
|
|
255
|
+
const { radius = 0.5, length = 1, widthSegments = 32, capSegments = 8 } = options
|
|
256
|
+
const W = widthSegments, half = length / 2
|
|
257
|
+
// rings from the top cap's first ring down to the bottom cap's last: the equator appears twice (once per
|
|
258
|
+
// cap, `length` apart) — the band between those two rings is the cylinder; the poles are fans, like the sphere
|
|
259
|
+
const R = capSegments * 2
|
|
260
|
+
const verticesCount = (W + 1) * R + W * 2
|
|
261
|
+
const vertices = new Float32Array(verticesCount * 3)
|
|
262
|
+
const normals = new Float32Array(verticesCount * 3)
|
|
263
|
+
const uvs = new Float32Array(verticesCount * 2)
|
|
264
|
+
const indices = new Uint16Array(W * (R - 1) * 6 + W * 3 * 2)
|
|
265
|
+
const total = Math.PI * radius + length // arc from pole to pole, for v
|
|
266
|
+
let vertexCnt = 0, indicesCnt = 0
|
|
267
|
+
for (let j = 0; j < R; j++) {
|
|
268
|
+
const top = j < capSegments
|
|
269
|
+
const theta = (top ? j + 1 : j) / capSegments * (Math.PI / 2) // 0 = the top pole, π = the bottom
|
|
270
|
+
const yOff = top ? half : -half
|
|
271
|
+
const sinT = Math.sin(theta), cosT = Math.cos(theta)
|
|
272
|
+
const v = ((Math.PI - theta) * radius + (top ? length : 0)) / total
|
|
273
|
+
for (let ix = 0; ix <= W; ix++) {
|
|
274
|
+
const u = ix / W
|
|
275
|
+
const nx = -Math.cos(u * Math.PI * 2) * sinT, nz = Math.sin(u * Math.PI * 2) * sinT
|
|
276
|
+
vertices.set([ nx * radius, cosT * radius + yOff, nz * radius ], vertexCnt * 3)
|
|
277
|
+
normals.set([ nx, cosT, nz ], vertexCnt * 3)
|
|
278
|
+
uvs.set([ u, v ], vertexCnt * 2)
|
|
279
|
+
if (j > 0 && ix > 0) {
|
|
280
|
+
const s = vertexCnt - 1, s1 = vertexCnt - (W + 1) - 1
|
|
281
|
+
indices.set([ s + 1, s1, s, s1, s + 1, s1 + 1 ], indicesCnt)
|
|
282
|
+
indicesCnt += 6
|
|
283
|
+
}
|
|
284
|
+
vertexCnt++
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const lastRing = vertexCnt - (W + 1)
|
|
288
|
+
for (const side of [ 1, -1 ]) {
|
|
289
|
+
const circleStart = side > 0 ? 0 : lastRing
|
|
290
|
+
for (let ix = 0; ix < W; ix++) {
|
|
291
|
+
const u = (ix + 0.5) / W
|
|
292
|
+
vertices.set([ 0, side * (radius + half), 0 ], vertexCnt * 3)
|
|
293
|
+
normals.set([ 0, side, 0 ], vertexCnt * 3)
|
|
294
|
+
uvs.set([ u, side > 0 ? 1 : 0 ], vertexCnt * 2)
|
|
295
|
+
const s = circleStart + ix
|
|
296
|
+
if (side > 0) indices.set([ s, s + 1, vertexCnt ], indicesCnt)
|
|
297
|
+
else indices.set([ vertexCnt, s + 1, s ], indicesCnt)
|
|
298
|
+
indicesCnt += 3
|
|
299
|
+
vertexCnt++
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return new Geometry(vertices, normals, indices, uvs)
|
|
303
|
+
}
|
|
304
|
+
|
|
196
305
|
// --- cylinder ---
|
|
197
306
|
export type CylinderOptions = { radius?: number, radiusTop?: number, radiusBottom?: number, edges?: number, smooth?: boolean }
|
|
198
307
|
const createCylinder = (options: CylinderOptions = {}): Geometry => {
|
package/src/gl/Light.ts
CHANGED
|
@@ -3,7 +3,15 @@
|
|
|
3
3
|
|
|
4
4
|
import { Color, type ColorInput } from "../core/color"
|
|
5
5
|
import type { Vec3Like } from "../math/vec"
|
|
6
|
-
import { Node } from "./Node"
|
|
6
|
+
import { Node, type NodeTweenProps } from "./Node"
|
|
7
|
+
import { DOM_GL_LIGHT, colorValue, floatValue, type TweenChannel, type TweenMeta } from "../animate/tween/spec"
|
|
8
|
+
import type { Animation } from "../animate/tween/Animation"
|
|
9
|
+
|
|
10
|
+
/** Animatable light props on top of the node transform. */
|
|
11
|
+
export type LightTweenProps = NodeTweenProps & {
|
|
12
|
+
intensity?: number | number[],
|
|
13
|
+
color?: ColorInput | ColorInput[],
|
|
14
|
+
}
|
|
7
15
|
|
|
8
16
|
export type SunOptions = {
|
|
9
17
|
/** Light direction (points where the light travels). Defaults to a typical key-light angle. */
|
|
@@ -46,26 +54,42 @@ export type PointOptions = {
|
|
|
46
54
|
/** Point-light shadows are a cubemap render per light; off by default. */
|
|
47
55
|
castShadows?: boolean
|
|
48
56
|
/**
|
|
49
|
-
* Whether `lecodes lightmap bake` bakes this light into the level's lightmap
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* flicker, a lamp the player can shoot out
|
|
53
|
-
*
|
|
57
|
+
* Whether `lecodes lightmap bake` bakes this light into the level's lightmap. A baked lamp lights the statics
|
|
58
|
+
* from the atlas and the MOVERS from the level's light grid, and is held dark in real time while that grid is
|
|
59
|
+
* loaded (without a grid it keeps lighting the movers live); a light
|
|
60
|
+
* that must reach the statics live too — a flicker, a lamp the player can shoot out, a muzzle flash —
|
|
61
|
+
* says `baked: false`: it is kept out of the bake and put on the statics' light channel. Default true.
|
|
54
62
|
*/
|
|
55
63
|
baked?: boolean
|
|
64
|
+
/**
|
|
65
|
+
* THE BAKE'S SHAPE of this lamp: `[width, height]` in metres = an AREA light - a rectangle in the light's local XZ
|
|
66
|
+
* plane, emitting along its local -Y (down, for an unrotated node) with the same lumens: a ceiling panel. The bake
|
|
67
|
+
* lights from the whole rectangle (soft shadows, light from where the panel is and not from a point inside the
|
|
68
|
+
* fixture). Real time it is still the point light above. Ignored with `baked: false`.
|
|
69
|
+
*/
|
|
70
|
+
bakeArea?: readonly [number, number]
|
|
56
71
|
}
|
|
57
72
|
|
|
58
73
|
export class Light extends Node {
|
|
59
74
|
private _intensity = 0
|
|
75
|
+
|
|
76
|
+
/** Tween `intensity` / `color` (a flash, a sunrise) and the transform — see {@link Node.animateTo}. */
|
|
77
|
+
override animateTo(props: LightTweenProps & TweenMeta): Animation { return super.animateTo(props) }
|
|
78
|
+
override animateFrom(props: LightTweenProps & TweenMeta): Animation { return super.animateFrom(props) }
|
|
79
|
+
/** @internal */
|
|
80
|
+
override _tweenChannel(prop: string): TweenChannel | null {
|
|
81
|
+
const id = () => this.id
|
|
82
|
+
if (prop === "intensity") return { domain: DOM_GL_LIGHT, id, value: floatValue, commit: (v) => { this._intensity = v as number } }
|
|
83
|
+
if (prop === "color") return { domain: DOM_GL_LIGHT, id, value: (v) => colorValue(v, Color.toRgba01) }
|
|
84
|
+
return super._tweenChannel(prop)
|
|
85
|
+
}
|
|
60
86
|
/** @internal PointOptions.baked — a candidate for the lightmap bake (default true). */
|
|
61
87
|
_baked = true
|
|
62
|
-
/** @internal the bake carries this light: the real-time one is held dark (Lightmap.load). */
|
|
63
|
-
_bakedOff = false
|
|
64
88
|
/** @internal every live point light, for the bake / the switch-off; a destroyed one leaves. */
|
|
65
89
|
static _points = new Set<Light>()
|
|
66
|
-
/** @internal sun direction as created (the engine keeps it
|
|
90
|
+
/** @internal sun direction as created (the engine keeps it). */
|
|
67
91
|
_direction: [number, number, number] = [ 0, -1, 0 ]
|
|
68
|
-
/** The most recently created sun
|
|
92
|
+
/** The most recently created sun. */
|
|
69
93
|
static lastSun: Light | null = null
|
|
70
94
|
/** @internal the colour as created / last set (a settings menu recreates a sun from these). */
|
|
71
95
|
_color: ColorInput = 0xffffff
|
|
@@ -113,6 +137,9 @@ export class Light extends Node {
|
|
|
113
137
|
options.range ?? 10,
|
|
114
138
|
options.castShadows ?? false,
|
|
115
139
|
)
|
|
140
|
+
if (options.bakeArea && light._baked) _creator.setLightBakeArea?.(light.id, options.bakeArea[0], options.bakeArea[1])
|
|
141
|
+
// an unbaked lamp lights the baked statics live: Filament channel 1 (see Lightmap)
|
|
142
|
+
if (!light._baked) _creator.setLightChannel?.(light.id, 1, true)
|
|
116
143
|
return light
|
|
117
144
|
}
|
|
118
145
|
|
|
@@ -123,14 +150,17 @@ export class Light extends Node {
|
|
|
123
150
|
get intensity(): number { return this._intensity }
|
|
124
151
|
set intensity(value: number) {
|
|
125
152
|
this._intensity = value
|
|
126
|
-
_creator.setLightIntensity?.(this.id,
|
|
153
|
+
if (!this._heldDark) _creator.setLightIntensity?.(this.id, value)
|
|
127
154
|
}
|
|
128
155
|
|
|
129
|
-
/** @internal
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
156
|
+
/** @internal A BAKED lamp is held dark while the level's LIGHT GRID is loaded (Lightmap): the statics have it in the
|
|
157
|
+
* atlas and the movers in the grid's ambient cubes - left burning it lit every mover and unbaked prop a second time,
|
|
158
|
+
* without a shadow (through walls), and put a hot spot on its own fixture. `intensity` keeps the authored value. */
|
|
159
|
+
_heldDark = false
|
|
160
|
+
_holdDark(dark: boolean): void {
|
|
161
|
+
if (this._heldDark === dark) return
|
|
162
|
+
this._heldDark = dark
|
|
163
|
+
_creator.setLightIntensity?.(this.id, dark ? 0 : this._intensity)
|
|
134
164
|
}
|
|
135
165
|
|
|
136
166
|
destroy(): void {
|