lecodes-sdk 1.1.0 → 1.2.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/inject.js +260 -361
- package/dist/types/gl/Light.d.ts +3 -0
- package/dist/types/gl/Particles.d.ts +15 -0
- package/dist/types/gl/animation/Locomotion.d.ts +52 -3
- package/dist/types/gl/terrain/Terrain.d.ts +8 -0
- package/dist/types/runtime/input.d.ts +7 -0
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/prompts/README.md +142 -142
- package/prompts/dist/2d-game.md +197 -408
- package/prompts/dist/3d-app.md +166 -491
- package/prompts/dist/ar-app.md +163 -373
- package/prompts/dist/design.md +87 -83
- package/prompts/dist/ui-app.md +136 -325
- package/src/animate/tween/Animation.ts +378 -378
- package/src/animate/tween/Timeline.ts +175 -175
- package/src/animate/tween/animateValue.ts +100 -100
- package/src/animate/tween/spec.ts +479 -479
- package/src/audio/audio.ts +161 -161
- package/src/bridges.d.ts +10 -1
- package/src/gl/CameraPlace.ts +52 -52
- package/src/gl/Light.ts +3 -0
- package/src/gl/Mesh.ts +120 -120
- package/src/gl/Model.ts +167 -167
- package/src/gl/Particles.ts +19 -0
- package/src/gl/animation/DynamicBone.ts +482 -482
- package/src/gl/animation/Locomotion.ts +72 -8
- package/src/gl/scenarios.ts +291 -291
- package/src/gl/terrain/Terrain.ts +29 -0
- package/src/inject.ts +236 -236
- package/src/runtime/input.ts +11 -0
- package/src/scene/gizmos.ts +148 -148
package/src/audio/audio.ts
CHANGED
|
@@ -1,161 +1,161 @@
|
|
|
1
|
-
// The `audio` global (docs/audio-plan.md §2): 2D playback, one-shots at a world point, buses.
|
|
2
|
-
// 3D playback from a node goes through the AudioSource aspect (gl/AudioSource.ts), which calls
|
|
3
|
-
// back into `_play` here with its source id; the listener lives on `scene.audio`.
|
|
4
|
-
//
|
|
5
|
-
// audio.play(click, { bus: 'ui' })
|
|
6
|
-
// audio.playAt(impact, hit.point, { maxDistance: 25 })
|
|
7
|
-
// audio.bus('sfx').reverb = 'cave'
|
|
8
|
-
// audio.stopAll(0.2)
|
|
9
|
-
|
|
10
|
-
import { cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
11
|
-
import { Bus } from "./Bus"
|
|
12
|
-
import type { Sound } from "./Sound"
|
|
13
|
-
import { audioSupported } from "./support"
|
|
14
|
-
import { Voice } from "./Voice"
|
|
15
|
-
|
|
16
|
-
export type PlaySoundOptions = {
|
|
17
|
-
/** 0 … 1 (and above, at your own risk). Default 1. */
|
|
18
|
-
volume?: number
|
|
19
|
-
/** Playback rate, 1 = unchanged. Default 1. */
|
|
20
|
-
pitch?: number
|
|
21
|
-
loop?: boolean
|
|
22
|
-
/** Bus name; default: the source's bus, `sfx` for 2D. */
|
|
23
|
-
bus?: string
|
|
24
|
-
/** Higher survives voice stealing when the pool is full. Default 0. */
|
|
25
|
-
priority?: number
|
|
26
|
-
/** Fade-in seconds. Default 0. */
|
|
27
|
-
fade?: number
|
|
28
|
-
/** Seconds into the clip to start from. */
|
|
29
|
-
startAt?: number
|
|
30
|
-
/** 2D only: -1 left … 1 right. */
|
|
31
|
-
pan?: number
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export type Rolloff = "none" | "inverse" | "linear" | "exponential"
|
|
35
|
-
|
|
36
|
-
/** Distance model of an AudioSource / playAt. */
|
|
37
|
-
export type SpatialOptions = {
|
|
38
|
-
/** Metres of full volume around the source. Default 1. */
|
|
39
|
-
minDistance?: number
|
|
40
|
-
/** Metres beyond which the source no longer gets quieter. Default 50. */
|
|
41
|
-
maxDistance?: number
|
|
42
|
-
/** How volume falls between the two: `inverse` (default, physical), `linear`, `exponential`, `none`. */
|
|
43
|
-
rolloff?: Rolloff
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export const ROLLOFF: Record<Rolloff, number> = { none: 0, inverse: 1, linear: 2, exponential: 3 }
|
|
47
|
-
|
|
48
|
-
/** @internal CAUD_SRC_* order. */
|
|
49
|
-
export function sourceParams(o: { minDistance: number, maxDistance: number, rolloff: Rolloff, coneInner: number, coneOuter: number, coneOuterGain: number, doppler: number, spread: number, occlusion: boolean, bus: number }): Float32Array {
|
|
50
|
-
return new Float32Array([ o.minDistance, o.maxDistance, ROLLOFF[o.rolloff] ?? 1, o.coneInner, o.coneOuter, o.coneOuterGain, o.doppler, o.spread, o.occlusion ? 1 : 0, o.bus ])
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export type AudioStats = {
|
|
54
|
-
voicesPlaying: number
|
|
55
|
-
voicesMono: number
|
|
56
|
-
voicesStereo: number
|
|
57
|
-
/** Voices displaced (or dropped) by a fuller pool since start. */
|
|
58
|
-
stolen: number
|
|
59
|
-
clips: number
|
|
60
|
-
clipBytes: number
|
|
61
|
-
/** Master peak since the previous read, linear (1 = full scale). */
|
|
62
|
-
peak: number
|
|
63
|
-
sampleRate: number
|
|
64
|
-
/** The reverb zone the listener is in (0 = none) and how far inside (0 … 1). */
|
|
65
|
-
listenerZone: number
|
|
66
|
-
zoneBlend: number
|
|
67
|
-
/** Voices rendered binaurally right now (see `audio.hrtf`). */
|
|
68
|
-
hrtfVoices: number
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const DEFAULT_BUSES = [ "master", "sfx", "music", "ui", "voice" ]
|
|
72
|
-
|
|
73
|
-
class AudioSystem {
|
|
74
|
-
private readonly _buses = new Map<string, Bus>()
|
|
75
|
-
private _hrtf = false
|
|
76
|
-
private _hrtfVoices = 16
|
|
77
|
-
private _timeScalePitch = false
|
|
78
|
-
|
|
79
|
-
/** True when this host mixes sound. Everything below is a silent no-op otherwise. */
|
|
80
|
-
get supported(): boolean { return audioSupported }
|
|
81
|
-
|
|
82
|
-
/** HRTF binaural rendering (MIT KEMAR filters on the CPU): sounds get a real up / down / behind in
|
|
83
|
-
* HEADPHONES. Off by default — on speakers it only smears the image. Applies to the `hrtfVoices`
|
|
84
|
-
* nearest 3D voices; the rest keep plain panning. */
|
|
85
|
-
get hrtf(): boolean { return this._hrtf }
|
|
86
|
-
set hrtf(v: boolean) { this._hrtf = v; if (audioSupported) _creatorAudio.setHrtf(v, this._hrtfVoices) }
|
|
87
|
-
/** `Time.scale` also pitches the sfx bus: slow motion drops every effect's tone, like a film. Default
|
|
88
|
-
* false — a pause only mutes sfx, the menu click keeps its pitch. */
|
|
89
|
-
get timeScalePitch(): boolean { return this._timeScalePitch }
|
|
90
|
-
set timeScalePitch(v: boolean) { this._timeScalePitch = v; if (audioSupported) _creatorAudio.setTimeScalePitch(v) }
|
|
91
|
-
/** How many voices get the (CPU-heavier) binaural path. Default 16. */
|
|
92
|
-
get hrtfVoices(): number { return this._hrtfVoices }
|
|
93
|
-
set hrtfVoices(n: number) { this._hrtfVoices = n; if (audioSupported && this._hrtf) _creatorAudio.setHrtf(true, n) }
|
|
94
|
-
|
|
95
|
-
/** A bus by name — the five built-ins, or an app-defined one created on first use. */
|
|
96
|
-
bus(name: string): Bus {
|
|
97
|
-
let b = this._buses.get(name)
|
|
98
|
-
if (b) return b
|
|
99
|
-
let id = -1
|
|
100
|
-
if (audioSupported) {
|
|
101
|
-
id = _creatorAudio.busId(name)
|
|
102
|
-
if (id < 0 && !DEFAULT_BUSES.includes(name)) id = _creatorAudio.createBus(name)
|
|
103
|
-
if (id < 0) console.warn(`audio.bus('${name}'): no free bus slot (16 max)`)
|
|
104
|
-
}
|
|
105
|
-
b = new Bus(name, id)
|
|
106
|
-
this._buses.set(name, b)
|
|
107
|
-
return b
|
|
108
|
-
}
|
|
109
|
-
get master(): Bus { return this.bus("master") }
|
|
110
|
-
|
|
111
|
-
/** @internal the bus id for an options object (or -1 = the source's). */
|
|
112
|
-
_busId(name: string | undefined): number {
|
|
113
|
-
if (name === undefined) return -1
|
|
114
|
-
return this.bus(name)._id
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** 2D playback (UI, stingers, music one-shots). */
|
|
118
|
-
play(sound: Sound, options: PlaySoundOptions = {}): Voice {
|
|
119
|
-
return this._play(sound, 0, options)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** A one-shot at a world position with its own transient source — impacts, ricochets, debris.
|
|
123
|
-
* The source is freed when the voice ends. */
|
|
124
|
-
playAt(sound: Sound, position: Vec3Like, options: PlaySoundOptions & SpatialOptions = {}): Voice {
|
|
125
|
-
if (!audioSupported || !sound.ready) return new Voice(0, options.volume ?? 1, options.pitch ?? 1, 0)
|
|
126
|
-
const src = _creatorAudio.createSource()
|
|
127
|
-
_creatorAudio.setSourceParams(src, sourceParams({
|
|
128
|
-
minDistance: options.minDistance ?? 1, maxDistance: options.maxDistance ?? 50, rolloff: options.rolloff ?? "inverse",
|
|
129
|
-
coneInner: 360, coneOuter: 360, coneOuterGain: 1, doppler: 0, spread: 0, occlusion: false, bus: this._busId(options.bus ?? "sfx"),
|
|
130
|
-
}))
|
|
131
|
-
_creatorAudio.setSourcePosition(src, cx(position), cy(position), cz(position))
|
|
132
|
-
const v = this._play(sound, src, options)
|
|
133
|
-
if (v._tok === 0) { _creatorAudio.destroySource(src); return v }
|
|
134
|
-
v._ownSource = src
|
|
135
|
-
v._watch()
|
|
136
|
-
return v
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** @internal shared by play / playAt / AudioSource.play. */
|
|
140
|
-
_play(sound: Sound, sourceId: number, options: PlaySoundOptions): Voice {
|
|
141
|
-
const volume = options.volume ?? 1, pitch = options.pitch ?? 1, pan = options.pan ?? 0
|
|
142
|
-
if (!audioSupported || !sound.ready) return new Voice(0, volume, pitch, pan)
|
|
143
|
-
const clip = sound._pick()
|
|
144
|
-
if (clip === 0) return new Voice(0, volume, pitch, pan)
|
|
145
|
-
const tok = _creatorAudio.play(clip, sourceId, this._busId(options.bus), volume, pitch, !!options.loop,
|
|
146
|
-
options.priority ?? 0, options.fade ?? 0, options.startAt ?? 0, pan)
|
|
147
|
-
return new Voice(tok, volume, pitch, pan)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** Stop every voice on every bus (fade in seconds). */
|
|
151
|
-
stopAll(fade = 0): void { if (audioSupported) _creatorAudio.stopAll(fade) }
|
|
152
|
-
|
|
153
|
-
/** Engine counters for a debug overlay or a perf log. */
|
|
154
|
-
get stats(): AudioStats {
|
|
155
|
-
const s = audioSupported ? _creatorAudio.stats() : new Float32Array(11)
|
|
156
|
-
return { voicesPlaying: s[0], voicesMono: s[1], voicesStereo: s[2], stolen: s[3], clips: s[4], clipBytes: s[5], peak: s[6], sampleRate: s[7], listenerZone: s[8], zoneBlend: s[9], hrtfVoices: s[10] ?? 0 }
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export const audio = new AudioSystem()
|
|
161
|
-
export type { AudioSystem }
|
|
1
|
+
// The `audio` global (docs/audio-plan.md §2): 2D playback, one-shots at a world point, buses.
|
|
2
|
+
// 3D playback from a node goes through the AudioSource aspect (gl/AudioSource.ts), which calls
|
|
3
|
+
// back into `_play` here with its source id; the listener lives on `scene.audio`.
|
|
4
|
+
//
|
|
5
|
+
// audio.play(click, { bus: 'ui' })
|
|
6
|
+
// audio.playAt(impact, hit.point, { maxDistance: 25 })
|
|
7
|
+
// audio.bus('sfx').reverb = 'cave'
|
|
8
|
+
// audio.stopAll(0.2)
|
|
9
|
+
|
|
10
|
+
import { cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
11
|
+
import { Bus } from "./Bus"
|
|
12
|
+
import type { Sound } from "./Sound"
|
|
13
|
+
import { audioSupported } from "./support"
|
|
14
|
+
import { Voice } from "./Voice"
|
|
15
|
+
|
|
16
|
+
export type PlaySoundOptions = {
|
|
17
|
+
/** 0 … 1 (and above, at your own risk). Default 1. */
|
|
18
|
+
volume?: number
|
|
19
|
+
/** Playback rate, 1 = unchanged. Default 1. */
|
|
20
|
+
pitch?: number
|
|
21
|
+
loop?: boolean
|
|
22
|
+
/** Bus name; default: the source's bus, `sfx` for 2D. */
|
|
23
|
+
bus?: string
|
|
24
|
+
/** Higher survives voice stealing when the pool is full. Default 0. */
|
|
25
|
+
priority?: number
|
|
26
|
+
/** Fade-in seconds. Default 0. */
|
|
27
|
+
fade?: number
|
|
28
|
+
/** Seconds into the clip to start from. */
|
|
29
|
+
startAt?: number
|
|
30
|
+
/** 2D only: -1 left … 1 right. */
|
|
31
|
+
pan?: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type Rolloff = "none" | "inverse" | "linear" | "exponential"
|
|
35
|
+
|
|
36
|
+
/** Distance model of an AudioSource / playAt. */
|
|
37
|
+
export type SpatialOptions = {
|
|
38
|
+
/** Metres of full volume around the source. Default 1. */
|
|
39
|
+
minDistance?: number
|
|
40
|
+
/** Metres beyond which the source no longer gets quieter. Default 50. */
|
|
41
|
+
maxDistance?: number
|
|
42
|
+
/** How volume falls between the two: `inverse` (default, physical), `linear`, `exponential`, `none`. */
|
|
43
|
+
rolloff?: Rolloff
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const ROLLOFF: Record<Rolloff, number> = { none: 0, inverse: 1, linear: 2, exponential: 3 }
|
|
47
|
+
|
|
48
|
+
/** @internal CAUD_SRC_* order. */
|
|
49
|
+
export function sourceParams(o: { minDistance: number, maxDistance: number, rolloff: Rolloff, coneInner: number, coneOuter: number, coneOuterGain: number, doppler: number, spread: number, occlusion: boolean, bus: number }): Float32Array {
|
|
50
|
+
return new Float32Array([ o.minDistance, o.maxDistance, ROLLOFF[o.rolloff] ?? 1, o.coneInner, o.coneOuter, o.coneOuterGain, o.doppler, o.spread, o.occlusion ? 1 : 0, o.bus ])
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type AudioStats = {
|
|
54
|
+
voicesPlaying: number
|
|
55
|
+
voicesMono: number
|
|
56
|
+
voicesStereo: number
|
|
57
|
+
/** Voices displaced (or dropped) by a fuller pool since start. */
|
|
58
|
+
stolen: number
|
|
59
|
+
clips: number
|
|
60
|
+
clipBytes: number
|
|
61
|
+
/** Master peak since the previous read, linear (1 = full scale). */
|
|
62
|
+
peak: number
|
|
63
|
+
sampleRate: number
|
|
64
|
+
/** The reverb zone the listener is in (0 = none) and how far inside (0 … 1). */
|
|
65
|
+
listenerZone: number
|
|
66
|
+
zoneBlend: number
|
|
67
|
+
/** Voices rendered binaurally right now (see `audio.hrtf`). */
|
|
68
|
+
hrtfVoices: number
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const DEFAULT_BUSES = [ "master", "sfx", "music", "ui", "voice" ]
|
|
72
|
+
|
|
73
|
+
class AudioSystem {
|
|
74
|
+
private readonly _buses = new Map<string, Bus>()
|
|
75
|
+
private _hrtf = false
|
|
76
|
+
private _hrtfVoices = 16
|
|
77
|
+
private _timeScalePitch = false
|
|
78
|
+
|
|
79
|
+
/** True when this host mixes sound. Everything below is a silent no-op otherwise. */
|
|
80
|
+
get supported(): boolean { return audioSupported }
|
|
81
|
+
|
|
82
|
+
/** HRTF binaural rendering (MIT KEMAR filters on the CPU): sounds get a real up / down / behind in
|
|
83
|
+
* HEADPHONES. Off by default — on speakers it only smears the image. Applies to the `hrtfVoices`
|
|
84
|
+
* nearest 3D voices; the rest keep plain panning. */
|
|
85
|
+
get hrtf(): boolean { return this._hrtf }
|
|
86
|
+
set hrtf(v: boolean) { this._hrtf = v; if (audioSupported) _creatorAudio.setHrtf(v, this._hrtfVoices) }
|
|
87
|
+
/** `Time.scale` also pitches the sfx bus: slow motion drops every effect's tone, like a film. Default
|
|
88
|
+
* false — a pause only mutes sfx, the menu click keeps its pitch. */
|
|
89
|
+
get timeScalePitch(): boolean { return this._timeScalePitch }
|
|
90
|
+
set timeScalePitch(v: boolean) { this._timeScalePitch = v; if (audioSupported) _creatorAudio.setTimeScalePitch(v) }
|
|
91
|
+
/** How many voices get the (CPU-heavier) binaural path. Default 16. */
|
|
92
|
+
get hrtfVoices(): number { return this._hrtfVoices }
|
|
93
|
+
set hrtfVoices(n: number) { this._hrtfVoices = n; if (audioSupported && this._hrtf) _creatorAudio.setHrtf(true, n) }
|
|
94
|
+
|
|
95
|
+
/** A bus by name — the five built-ins, or an app-defined one created on first use. */
|
|
96
|
+
bus(name: string): Bus {
|
|
97
|
+
let b = this._buses.get(name)
|
|
98
|
+
if (b) return b
|
|
99
|
+
let id = -1
|
|
100
|
+
if (audioSupported) {
|
|
101
|
+
id = _creatorAudio.busId(name)
|
|
102
|
+
if (id < 0 && !DEFAULT_BUSES.includes(name)) id = _creatorAudio.createBus(name)
|
|
103
|
+
if (id < 0) console.warn(`audio.bus('${name}'): no free bus slot (16 max)`)
|
|
104
|
+
}
|
|
105
|
+
b = new Bus(name, id)
|
|
106
|
+
this._buses.set(name, b)
|
|
107
|
+
return b
|
|
108
|
+
}
|
|
109
|
+
get master(): Bus { return this.bus("master") }
|
|
110
|
+
|
|
111
|
+
/** @internal the bus id for an options object (or -1 = the source's). */
|
|
112
|
+
_busId(name: string | undefined): number {
|
|
113
|
+
if (name === undefined) return -1
|
|
114
|
+
return this.bus(name)._id
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** 2D playback (UI, stingers, music one-shots). */
|
|
118
|
+
play(sound: Sound, options: PlaySoundOptions = {}): Voice {
|
|
119
|
+
return this._play(sound, 0, options)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** A one-shot at a world position with its own transient source — impacts, ricochets, debris.
|
|
123
|
+
* The source is freed when the voice ends. */
|
|
124
|
+
playAt(sound: Sound, position: Vec3Like, options: PlaySoundOptions & SpatialOptions = {}): Voice {
|
|
125
|
+
if (!audioSupported || !sound.ready) return new Voice(0, options.volume ?? 1, options.pitch ?? 1, 0)
|
|
126
|
+
const src = _creatorAudio.createSource()
|
|
127
|
+
_creatorAudio.setSourceParams(src, sourceParams({
|
|
128
|
+
minDistance: options.minDistance ?? 1, maxDistance: options.maxDistance ?? 50, rolloff: options.rolloff ?? "inverse",
|
|
129
|
+
coneInner: 360, coneOuter: 360, coneOuterGain: 1, doppler: 0, spread: 0, occlusion: false, bus: this._busId(options.bus ?? "sfx"),
|
|
130
|
+
}))
|
|
131
|
+
_creatorAudio.setSourcePosition(src, cx(position), cy(position), cz(position))
|
|
132
|
+
const v = this._play(sound, src, options)
|
|
133
|
+
if (v._tok === 0) { _creatorAudio.destroySource(src); return v }
|
|
134
|
+
v._ownSource = src
|
|
135
|
+
v._watch()
|
|
136
|
+
return v
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** @internal shared by play / playAt / AudioSource.play. */
|
|
140
|
+
_play(sound: Sound, sourceId: number, options: PlaySoundOptions): Voice {
|
|
141
|
+
const volume = options.volume ?? 1, pitch = options.pitch ?? 1, pan = options.pan ?? 0
|
|
142
|
+
if (!audioSupported || !sound.ready) return new Voice(0, volume, pitch, pan)
|
|
143
|
+
const clip = sound._pick()
|
|
144
|
+
if (clip === 0) return new Voice(0, volume, pitch, pan)
|
|
145
|
+
const tok = _creatorAudio.play(clip, sourceId, this._busId(options.bus), volume, pitch, !!options.loop,
|
|
146
|
+
options.priority ?? 0, options.fade ?? 0, options.startAt ?? 0, pan)
|
|
147
|
+
return new Voice(tok, volume, pitch, pan)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Stop every voice on every bus (fade in seconds). */
|
|
151
|
+
stopAll(fade = 0): void { if (audioSupported) _creatorAudio.stopAll(fade) }
|
|
152
|
+
|
|
153
|
+
/** Engine counters for a debug overlay or a perf log. */
|
|
154
|
+
get stats(): AudioStats {
|
|
155
|
+
const s = audioSupported ? _creatorAudio.stats() : new Float32Array(11)
|
|
156
|
+
return { voicesPlaying: s[0], voicesMono: s[1], voicesStereo: s[2], stolen: s[3], clips: s[4], clipBytes: s[5], peak: s[6], sampleRate: s[7], listenerZone: s[8], zoneBlend: s[9], hrtfVoices: s[10] ?? 0 }
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export const audio = new AudioSystem()
|
|
161
|
+
export type { AudioSystem }
|
package/src/bridges.d.ts
CHANGED
|
@@ -616,7 +616,7 @@ declare global {
|
|
|
616
616
|
/** THE LIGHT GRID for movers (`lecodes lightmap bake` writes `<name>.lgrid`, the .bake's `volume` names it):
|
|
617
617
|
* `lightVolumeLoad(fetchId)` hands the engine the fetched file (false = not a grid); `lightVolumeTrack(entityId, on)`
|
|
618
618
|
* marks a GLB instance root or a Mesh entity as a MOVER — every frame the engine gives it the ambient cube of the
|
|
619
|
-
* place it is at (`setAmbientCube`), blended from the reachable cells around it and eased over ~0.
|
|
619
|
+
* place it is at (`setAmbientCube`), blended from the reachable cells around it and eased over ~0.06 s;
|
|
620
620
|
* `fixed` = a STATIC the atlas left out (glass, a material the lightmap shader declines): sampled once a grid, and
|
|
621
621
|
* ignored when every primitive of it is baked;
|
|
622
622
|
* `lightVolumeSample(x, y, z)` = that cube at a point (26 floats, null = no grid or no valid cell near), for tools;
|
|
@@ -1589,6 +1589,15 @@ declare global {
|
|
|
1589
1589
|
* setLoop tick (creator-pkg drains its dispatch queue after the frame's timers, so a poll can
|
|
1590
1590
|
* see the new state one tick before the event; web/headless deliver synchronously). */
|
|
1591
1591
|
registerInputEvent?(callback: (kind: number, code: string, gamepad: number, repeat: number) => void): void
|
|
1592
|
+
/** Dual-motor rumble on pad `gamepad` (0..3) — the W3C "dual-rumble" effect. `strong` = the
|
|
1593
|
+
* low-frequency (left, heavy) motor, `weak` = the high-frequency (right, light) one, both 0..1
|
|
1594
|
+
* (hosts clamp); `durationMs` > 0, hosts cap it at 5000. A new call REPLACES the running effect
|
|
1595
|
+
* on that pad; `strong = weak = 0` (any duration) stops it. The HOST owns the clock: the motors
|
|
1596
|
+
* stop by themselves after `durationMs`, on focus loss, when the pad disconnects and when the
|
|
1597
|
+
* world that started the effect is torn down — an app never has to clean up. Returns whether
|
|
1598
|
+
* the pad took the command (false: no such pad, or it has no motors). Optional: a host without
|
|
1599
|
+
* it has no rumble. */
|
|
1600
|
+
inputRumble?(gamepad: number, strong: number, weak: number, durationMs: number): boolean
|
|
1592
1601
|
// ---- World control (run/restart/quit — the platform's `location` analog) --------------------
|
|
1593
1602
|
// Worlds carry two host-tracked bits: an IDENTITY (project uuid) and a TRUST flag. The host's
|
|
1594
1603
|
// own boot world (the bundled launcher) is trusted; a world created by `run` is trusted only
|
package/src/gl/CameraPlace.ts
CHANGED
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
// CameraPlace: "the camera as a place". A scene has ONE camera (`scene.camera`); a CameraPlace marks
|
|
2
|
-
// a node as somewhere that camera can be — with the lens it uses there. Nothing is created engine-
|
|
3
|
-
// side (no second camera, no ABI): it is a pose + a projection record on any node: an empty, an
|
|
4
|
-
// empty mounted on a bone (`mount:`), a node a path aspect moves (a dolly shot), a model node.
|
|
5
|
-
//
|
|
6
|
-
// • In a scene FILE that runs (`open()` / `load()`), the `active: true` place drives
|
|
7
|
-
// `scene.camera` (exactly `scene.camera.follow(node)`); exactly one per file.
|
|
8
|
-
// • Inside a prefab or an `instantiate()`d subtree a place is inert DATA — the hosting scene
|
|
9
|
-
// owns its camera. The host reads it (`inst.nodes.eye.get(CameraPlace)` → world pose + fov)
|
|
10
|
-
// or anchors on it (`inst.alignTo(inst.nodes.eye)`).
|
|
11
|
-
// • From code: `scene.camera.follow(dolly.get(CameraPlace))` — a cutscene shot; `follow(null)`
|
|
12
|
-
// releases.
|
|
13
|
-
// • Edit mode: draws an anchored frustum gizmo (the editor's camera preview reads the same place).
|
|
14
|
-
// See docs/scene-camera-place-plan.md.
|
|
15
|
-
|
|
16
|
-
import { Aspect } from "../core/Aspect"
|
|
17
|
-
import type { FieldMeta } from "../core/fields"
|
|
18
|
-
import { Gizmos } from "../scene/gizmos"
|
|
19
|
-
import type { Camera } from "./Camera"
|
|
20
|
-
import type { Node } from "./Node"
|
|
21
|
-
|
|
22
|
-
export class CameraPlace extends Aspect<"cameraPlace", Node> {
|
|
23
|
-
static readonly aspect = "cameraPlace"
|
|
24
|
-
/** Vertical field of view in degrees (default 60) — smaller is a longer lens. */
|
|
25
|
-
fov = 60
|
|
26
|
-
/** Near clip distance. */
|
|
27
|
-
near = 0.01
|
|
28
|
-
/** Far clip distance = view range. */
|
|
29
|
-
far = 1000
|
|
30
|
-
/** The place that drives `scene.camera` when the file declaring it RUNS. Exactly one per file;
|
|
31
|
-
* ignored inside prefabs / instantiated subtrees (the host scene owns its camera). */
|
|
32
|
-
active = false
|
|
33
|
-
/** Aspect ratio of the frustum gizmo only (the real aspect is the viewport's). */
|
|
34
|
-
static fields: FieldMeta<CameraPlace> = {
|
|
35
|
-
fov: { label: "FOV", min: 1, max: 179, step: 1 },
|
|
36
|
-
near: { min: 0.001, max: 10, step: 0.01 },
|
|
37
|
-
far: { min: 1, max: 100000, step: 1 },
|
|
38
|
-
active: { label: "Active camera" },
|
|
39
|
-
}
|
|
40
|
-
static editor = { rebuild: true }
|
|
41
|
-
|
|
42
|
-
/** Copy this place's projection onto a camera (`camera.follow(place)` does it for you). */
|
|
43
|
-
applyTo(camera: Camera): void {
|
|
44
|
-
camera.setProjection({ fov: this.fov, near: this.near, far: this.far })
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Edit mode: the frustum marker (−Z = view), anchored on the node so it follows drags live and
|
|
48
|
-
* clicking it selects the node. Play mode never calls this. */
|
|
49
|
-
rebuild(): void {
|
|
50
|
-
Gizmos.frustum(this.fov, { node: this.node, color: this.active ? "#ffffff" : "#9aa3b2" })
|
|
51
|
-
}
|
|
52
|
-
}
|
|
1
|
+
// CameraPlace: "the camera as a place". A scene has ONE camera (`scene.camera`); a CameraPlace marks
|
|
2
|
+
// a node as somewhere that camera can be — with the lens it uses there. Nothing is created engine-
|
|
3
|
+
// side (no second camera, no ABI): it is a pose + a projection record on any node: an empty, an
|
|
4
|
+
// empty mounted on a bone (`mount:`), a node a path aspect moves (a dolly shot), a model node.
|
|
5
|
+
//
|
|
6
|
+
// • In a scene FILE that runs (`open()` / `load()`), the `active: true` place drives
|
|
7
|
+
// `scene.camera` (exactly `scene.camera.follow(node)`); exactly one per file.
|
|
8
|
+
// • Inside a prefab or an `instantiate()`d subtree a place is inert DATA — the hosting scene
|
|
9
|
+
// owns its camera. The host reads it (`inst.nodes.eye.get(CameraPlace)` → world pose + fov)
|
|
10
|
+
// or anchors on it (`inst.alignTo(inst.nodes.eye)`).
|
|
11
|
+
// • From code: `scene.camera.follow(dolly.get(CameraPlace))` — a cutscene shot; `follow(null)`
|
|
12
|
+
// releases.
|
|
13
|
+
// • Edit mode: draws an anchored frustum gizmo (the editor's camera preview reads the same place).
|
|
14
|
+
// See docs/scene-camera-place-plan.md.
|
|
15
|
+
|
|
16
|
+
import { Aspect } from "../core/Aspect"
|
|
17
|
+
import type { FieldMeta } from "../core/fields"
|
|
18
|
+
import { Gizmos } from "../scene/gizmos"
|
|
19
|
+
import type { Camera } from "./Camera"
|
|
20
|
+
import type { Node } from "./Node"
|
|
21
|
+
|
|
22
|
+
export class CameraPlace extends Aspect<"cameraPlace", Node> {
|
|
23
|
+
static readonly aspect = "cameraPlace"
|
|
24
|
+
/** Vertical field of view in degrees (default 60) — smaller is a longer lens. */
|
|
25
|
+
fov = 60
|
|
26
|
+
/** Near clip distance. */
|
|
27
|
+
near = 0.01
|
|
28
|
+
/** Far clip distance = view range. */
|
|
29
|
+
far = 1000
|
|
30
|
+
/** The place that drives `scene.camera` when the file declaring it RUNS. Exactly one per file;
|
|
31
|
+
* ignored inside prefabs / instantiated subtrees (the host scene owns its camera). */
|
|
32
|
+
active = false
|
|
33
|
+
/** Aspect ratio of the frustum gizmo only (the real aspect is the viewport's). */
|
|
34
|
+
static fields: FieldMeta<CameraPlace> = {
|
|
35
|
+
fov: { label: "FOV", min: 1, max: 179, step: 1 },
|
|
36
|
+
near: { min: 0.001, max: 10, step: 0.01 },
|
|
37
|
+
far: { min: 1, max: 100000, step: 1 },
|
|
38
|
+
active: { label: "Active camera" },
|
|
39
|
+
}
|
|
40
|
+
static editor = { rebuild: true }
|
|
41
|
+
|
|
42
|
+
/** Copy this place's projection onto a camera (`camera.follow(place)` does it for you). */
|
|
43
|
+
applyTo(camera: Camera): void {
|
|
44
|
+
camera.setProjection({ fov: this.fov, near: this.near, far: this.far })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Edit mode: the frustum marker (−Z = view), anchored on the node so it follows drags live and
|
|
48
|
+
* clicking it selects the node. Play mode never calls this. */
|
|
49
|
+
rebuild(): void {
|
|
50
|
+
Gizmos.frustum(this.fov, { node: this.node, color: this.active ? "#ffffff" : "#9aa3b2" })
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/gl/Light.ts
CHANGED
|
@@ -27,6 +27,9 @@ export type SunOptions = {
|
|
|
27
27
|
* 3 - 2048 map, contact-hardening soft edges (PCSS: sharp where the caster touches, softer
|
|
28
28
|
* away; discrete-GPU territory, ~+60 % of a frame on an iGPU)
|
|
29
29
|
* A lightmapped level already carries every static shadow, so 0 is a legitimate choice there.
|
|
30
|
+
* On a level with a baked light grid the variance / PCSS filters cannot be used (they need every
|
|
31
|
+
* receiver in the shadow map, and the baked statics are kept out of it): 3 renders as PCSS on the
|
|
32
|
+
* depth map - the same contact-hardening look - and 2 renders as 1.
|
|
30
33
|
*/
|
|
31
34
|
shadowsQuality?: 0 | 1 | 2 | 3
|
|
32
35
|
/**
|