lecodes-sdk 0.20.0 → 1.0.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 +48 -5
- package/dist/inject.js +361 -260
- package/dist/types/audio/Bus.d.ts +45 -0
- package/dist/types/audio/Sound.d.ts +28 -0
- package/dist/types/audio/Voice.d.ts +27 -0
- package/dist/types/audio/audio.d.ts +83 -0
- package/dist/types/audio/support.d.ts +1 -0
- package/dist/types/canvas/Canvas.d.ts +2 -0
- package/dist/types/gl/DecalSet.d.ts +148 -0
- package/dist/types/gl/Geometry.d.ts +17 -0
- package/dist/types/gl/Light.d.ts +7 -0
- package/dist/types/gl/Lightmap.d.ts +9 -0
- package/dist/types/gl/Material.d.ts +90 -2
- package/dist/types/gl/Mesh.d.ts +18 -1
- package/dist/types/gl/Model.d.ts +34 -0
- package/dist/types/gl/Particles.d.ts +13 -0
- package/dist/types/gl/Scene.d.ts +23 -0
- package/dist/types/gl/Texture.d.ts +29 -1
- package/dist/types/gl/animation/AnimationClip.d.ts +25 -12
- package/dist/types/gl/animation/DynamicBone.d.ts +173 -0
- package/dist/types/gl/{IK.d.ts → animation/IK.d.ts} +4 -4
- package/dist/types/gl/{Locomotion.d.ts → animation/Locomotion.d.ts} +6 -6
- package/dist/types/gl/animation/core.d.ts +15 -10
- package/dist/types/gl/audio/AudioSource.d.ts +60 -0
- package/dist/types/gl/audio/AudioZone.d.ts +32 -0
- package/dist/types/gl/audio/SceneAudio.d.ts +11 -0
- 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/{Ragdoll.d.ts → physics/Ragdoll.d.ts} +4 -4
- 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/state.d.ts +0 -1
- package/dist/types/gl/{Terrain.d.ts → terrain/Terrain.d.ts} +6 -6
- 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 +33 -21
- package/dist/types/runtime/files.d.ts +24 -1
- package/dist/types/runtime/input.d.ts +11 -0
- package/dist/types/scene/defineScene.d.ts +10 -3
- package/dist/types/ui/UIImage.d.ts +15 -5
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/prompts/README.md +142 -142
- package/prompts/dist/2d-game.md +408 -197
- package/prompts/dist/3d-app.md +491 -166
- package/prompts/dist/ar-app.md +373 -163
- package/prompts/dist/design.md +83 -87
- package/prompts/dist/ui-app.md +325 -136
- package/src/audio/Bus.ts +102 -0
- package/src/audio/Sound.ts +96 -0
- package/src/audio/Voice.ts +102 -0
- package/src/audio/audio.ts +161 -0
- package/src/audio/support.ts +6 -0
- package/src/bridges.d.ts +279 -32
- package/src/canvas/Canvas.ts +21 -0
- package/src/compile/__tests__/compile.test.ts +11 -0
- package/src/compile/compileProject.ts +30 -15
- package/src/compile/header.ts +6 -3
- package/src/compile/index.ts +4 -0
- package/src/compile/sceneEditor.ts +42 -1
- package/src/core/Aspect.ts +33 -8
- package/src/g2/Scene2D.ts +7 -0
- package/src/gl/CameraPlace.ts +52 -52
- package/src/gl/DecalSet.ts +360 -0
- package/src/gl/Geometry.ts +348 -279
- package/src/gl/Light.ts +16 -0
- package/src/gl/Lightmap.ts +35 -7
- package/src/gl/Material.ts +173 -4
- package/src/gl/Mesh.ts +120 -83
- package/src/gl/Model.ts +33 -1
- package/src/gl/Node.ts +1 -1
- package/src/gl/Particles.ts +21 -3
- package/src/gl/Scene.ts +41 -7
- package/src/gl/Texture.ts +43 -3
- package/src/gl/animation/AnimationClip.ts +43 -20
- package/src/gl/animation/Animator.ts +4 -3
- package/src/gl/animation/DynamicBone.ts +459 -0
- package/src/gl/{IK.ts → animation/IK.ts} +4 -4
- package/src/gl/{Locomotion.ts → animation/Locomotion.ts} +7 -7
- package/src/gl/animation/core.ts +20 -15
- package/src/gl/audio/AudioSource.ts +113 -0
- package/src/gl/audio/AudioZone.ts +75 -0
- package/src/gl/audio/SceneAudio.ts +26 -0
- 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/{Ragdoll.ts → physics/Ragdoll.ts} +272 -270
- package/src/gl/{Shape.ts → physics/Shape.ts} +3 -3
- package/src/gl/{Trigger.ts → physics/Trigger.ts} +2 -2
- package/src/gl/{physicsEvents.ts → physics/physicsEvents.ts} +1 -1
- package/src/gl/scenarios.ts +291 -291
- package/src/gl/state.ts +1 -1
- package/src/gl/{Terrain.ts → terrain/Terrain.ts} +10 -10
- 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 +226 -212
- package/src/runtime/files.ts +32 -2
- package/src/runtime/input.ts +6 -1
- package/src/scene/defineScene.ts +26 -10
- package/src/scene/gizmos.ts +148 -148
- package/src/scene/level.ts +2 -2
- package/src/ui/UIImage.ts +21 -7
- package/dist/types/gl/Gearbox.d.ts +0 -86
- package/dist/types/gl/Vehicle.d.ts +0 -191
- package/dist/types/gl/Wheel.d.ts +0 -95
- package/src/gl/Gearbox.ts +0 -212
- 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/Model.ts
CHANGED
|
@@ -9,6 +9,27 @@ import { Node } from "./Node"
|
|
|
9
9
|
import { Animator, _pushLod, type LodMode } from "./animation/Animator"
|
|
10
10
|
import { Material } from "./Material"
|
|
11
11
|
|
|
12
|
+
/** One polygon under a screen point — what `Model.pickTriangle` returns. `bones` are the vertex's raw
|
|
13
|
+
* JOINTS_0 / WEIGHTS_0 pairs (weight > 0; empty = unweighted), `bind` its position in mesh space,
|
|
14
|
+
* `world` its skinned position this frame. */
|
|
15
|
+
export interface TrianglePick {
|
|
16
|
+
/** the Model's root entity, and the mesh node's own entity (0 when the host has no entity for it) */
|
|
17
|
+
entity: number
|
|
18
|
+
node: number
|
|
19
|
+
nodeName: string
|
|
20
|
+
mesh: string
|
|
21
|
+
primitive: number
|
|
22
|
+
material: string
|
|
23
|
+
/** triangle index within the primitive (index-buffer order), and whether the ray came from behind */
|
|
24
|
+
triangle: number
|
|
25
|
+
backface: boolean
|
|
26
|
+
distance: number
|
|
27
|
+
point: [number, number, number]
|
|
28
|
+
bary: [number, number, number]
|
|
29
|
+
skin: string
|
|
30
|
+
vertices: { index: number, bind: [number, number, number], world: [number, number, number], bones: { name: string, weight: number }[] }[]
|
|
31
|
+
}
|
|
32
|
+
|
|
12
33
|
export class Model extends Node {
|
|
13
34
|
/** The model's Animator — always present, its clip table = the GLB's embedded clips. Configure
|
|
14
35
|
* more (external clips, blend spaces, layers) with `model.aspect(Animator, {...})`. */
|
|
@@ -69,6 +90,16 @@ export class Model extends Node {
|
|
|
69
90
|
/** @internal both flags travel together — the bridge walks the instance once. */
|
|
70
91
|
_applyShadows(): void { _creator.setGlbShadows?.(this.id, this._castShadows, this._receiveShadows) }
|
|
71
92
|
|
|
93
|
+
/** DEBUG: the closest polygon of this model under a screen point (logical px — `Input.mouse.position`,
|
|
94
|
+
* a touch event's clientX/Y), tested against the CPU-skinned CURRENT pose, both faces. Names the
|
|
95
|
+
* triangle, its three vertices (bind + skinned positions) and their raw bone weights, so a stretched
|
|
96
|
+
* or misbound polygon can be traced to its binding. One full CPU skin of the model per call: click-rate
|
|
97
|
+
* only. null = miss, or a host without the pick (desktop today). */
|
|
98
|
+
pickTriangle(screenX: number, screenY: number): TrianglePick | null {
|
|
99
|
+
const json = _creator.pickTriangle?.(this.id, screenX, screenY)
|
|
100
|
+
return json ? JSON.parse(json) as TrianglePick : null
|
|
101
|
+
}
|
|
102
|
+
|
|
72
103
|
/** Duplicate this model — a deep copy of the GLB (meshes, skeleton, animation clips), attached to
|
|
73
104
|
* the same parent and scene and sharing this model's current transform. The clone has its own
|
|
74
105
|
* independent animation state (reach it via clone.anim). Mirrors this model's culling flag. */
|
|
@@ -110,7 +141,8 @@ export class Model extends Node {
|
|
|
110
141
|
return new Promise<Model>((resolve, reject) => {
|
|
111
142
|
const mode = options.lightmap ?? Model._lightmapDefault
|
|
112
143
|
const lightmapped = !!mode && !!_creator.setNextGlbLightmapped
|
|
113
|
-
|
|
144
|
+
// the tier + its masked twin (alpha-MASK materials — foliage — take the lightmap shader too, 2026-09-11)
|
|
145
|
+
if (lightmapped) _creator.setNextGlbLightmapped!(Material.idOf(Material._lightmapTemplate()), Material.idOf(Material._lightmapMaskedTemplate()))
|
|
114
146
|
_creator.createGlb((source as unknown as { _id: number })._id, (entityId: number) => {
|
|
115
147
|
if (entityId === 0) { reject(new Error("Failed to load GLB")); return }
|
|
116
148
|
const model = new Model(entityId)
|
package/src/gl/Node.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { Quat, cw, type QuatLike } from "../math/quat"
|
|
|
12
12
|
import { Mat4, type Mat4Like } from "../math/mat4"
|
|
13
13
|
import type { Geometry } from "./Geometry"
|
|
14
14
|
import { registerTouchEndEvent, registerTouchStartEvent } from "./touch"
|
|
15
|
-
import { ensurePhysicsEvents } from "./physicsEvents"
|
|
15
|
+
import { ensurePhysicsEvents } from "./physics/physicsEvents"
|
|
16
16
|
import { Material } from "./Material"
|
|
17
17
|
import type { CompAxis, CompWriter } from "../core/compWrite"
|
|
18
18
|
import type { ClickEvent, TouchStartEvent } from "../runtime/touch"
|
package/src/gl/Particles.ts
CHANGED
|
@@ -231,7 +231,15 @@ type Shape =
|
|
|
231
231
|
* birth direction (a burst that decays: `curve(4).to(0)`), `x`/`y`/`z` drift in the simulation
|
|
232
232
|
* space (rising smoke: `y: curve(2).from(0).to(1)`). Unity's velocityOverLifetime. */
|
|
233
233
|
export type VelocityOverLife = { radial?: ParticleValue, x?: ParticleValue, y?: ParticleValue, z?: ParticleValue }
|
|
234
|
-
type Noise = {
|
|
234
|
+
type Noise = {
|
|
235
|
+
strength?: number, frequency?: number, speed?: number,
|
|
236
|
+
/** How much of the displacement a particle earns with AGE (default 0 = all of it from birth).
|
|
237
|
+
* The field is sampled by position, so at 0 every particle born at the same spot is pushed the
|
|
238
|
+
* same way and a plume's root wanders off its emitter by up to `strength` metres; at 1 a particle
|
|
239
|
+
* is born exactly where the emitter put it and drifts into the field over its life — what you
|
|
240
|
+
* want whenever the source is visible (an exhaust pipe, a contact patch, a muzzle). */
|
|
241
|
+
ramp?: number,
|
|
242
|
+
}
|
|
235
243
|
|
|
236
244
|
/** Ground plane for bouncing particles (all render modes). `height` is in the simulation space:
|
|
237
245
|
* world y with `space: 'world'`, emitter-local y otherwise. */
|
|
@@ -283,6 +291,11 @@ export type ParticlesOptions = ParticlesMaterialOptions & {
|
|
|
283
291
|
* without this the engine picks who covers whom per frame (smoke popping over a fireball).
|
|
284
292
|
* Default 0; a fireball wants 2, its smoke 1, a smoke trail -1. */
|
|
285
293
|
order?: number
|
|
294
|
+
/** Coarse draw order among ALL blended draws, 0 (first) … 7 (last) — see `Mesh.renderPriority`.
|
|
295
|
+
* Default 5: meshes sit at 4 and decals at 3, so smoke covers a car's glass and its skid marks
|
|
296
|
+
* whatever the camera does (the engine's depth sort compares object centres, and an emitter's
|
|
297
|
+
* centre says nothing about where its cloud is). `order` breaks ties inside one priority. */
|
|
298
|
+
renderPriority?: number
|
|
286
299
|
shape?: Shape
|
|
287
300
|
/** Extra velocity over life (radial burst that decays, axis drift) — see `VelocityOverLife`. */
|
|
288
301
|
velocityOverLife?: VelocityOverLife
|
|
@@ -437,6 +450,7 @@ export class Particles extends Node {
|
|
|
437
450
|
super()
|
|
438
451
|
this._material = options.material ?? (options.mesh ? Material.lit() : Material.particles(options))
|
|
439
452
|
_creator.createParticleSystem(this.id, Material.idOf(this._material), options.maxParticles ?? 0)
|
|
453
|
+
this.renderPriority = options.renderPriority ?? 5
|
|
440
454
|
|
|
441
455
|
const id = this.id
|
|
442
456
|
this.custom = new Proxy([] as ParticleValue[], {
|
|
@@ -496,7 +510,7 @@ export class Particles extends Node {
|
|
|
496
510
|
}
|
|
497
511
|
if (options.noise !== undefined) {
|
|
498
512
|
const n = options.noise
|
|
499
|
-
out.push(TAG_NOISE,
|
|
513
|
+
out.push(TAG_NOISE, 4, n ? n.strength ?? 1 : 0, n ? n.frequency ?? 1 : 0, n ? n.speed ?? 1 : 0, n ? n.ramp ?? 0 : 0)
|
|
500
514
|
}
|
|
501
515
|
if (options.seed !== undefined) out.push(TAG_SEED, 1, options.seed)
|
|
502
516
|
|
|
@@ -517,6 +531,10 @@ export class Particles extends Node {
|
|
|
517
531
|
set inheritVelocity(val: number) { this._send([ TAG_INHERIT_VELOCITY, 1, val ]) }
|
|
518
532
|
set rateOverDistance(val: number) { this._send([ TAG_RATE_DISTANCE, 1, val ]) }
|
|
519
533
|
set order(val: number) { this._send([ TAG_ORDER, 1, val ]) }
|
|
534
|
+
/** Coarse draw order, 0 … 7 — see `Mesh.renderPriority`. Write-only. */
|
|
535
|
+
set renderPriority(v: number) {
|
|
536
|
+
if (_creator.setRenderPriority) _creator.setRenderPriority(this.id, Math.max(0, Math.min(7, Math.round(v))))
|
|
537
|
+
}
|
|
520
538
|
|
|
521
539
|
set velocityOverLife(v: VelocityOverLife) {
|
|
522
540
|
const out: number[] = []
|
|
@@ -585,7 +603,7 @@ export class Particles extends Node {
|
|
|
585
603
|
|
|
586
604
|
set noise(noise: Noise | null) {
|
|
587
605
|
this._send(noise
|
|
588
|
-
? [ TAG_NOISE,
|
|
606
|
+
? [ TAG_NOISE, 4, noise.strength ?? 1, noise.frequency ?? 1, noise.speed ?? 1, noise.ramp ?? 0 ]
|
|
589
607
|
: [ TAG_NOISE, 3, 0, 0, 0 ])
|
|
590
608
|
}
|
|
591
609
|
}
|
package/src/gl/Scene.ts
CHANGED
|
@@ -13,8 +13,10 @@ import type { ClickEvent, TouchStartEvent } from "../runtime/touch"
|
|
|
13
13
|
import type { FetchResponse } from "../runtime/fetch"
|
|
14
14
|
import { _requestCameraPermission } from "../plugins/permission"
|
|
15
15
|
import { Camera } from "./Camera"
|
|
16
|
+
import { SceneAudio } from "./audio/SceneAudio"
|
|
16
17
|
import { attachControls, type ControlsHandle, type ControlsOptions } from "./controls"
|
|
17
18
|
import { Material } from "./Material"
|
|
19
|
+
import { Texture } from "./Texture"
|
|
18
20
|
import { Node, nodeRegistry } from "./Node"
|
|
19
21
|
import { glState } from "./state"
|
|
20
22
|
import { registerTouchEndEvent, registerTouchStartEvent } from "./touch"
|
|
@@ -107,6 +109,9 @@ export type SceneOptions = {
|
|
|
107
109
|
* takes over). Unset keeps the host default (desktop 4×, mobile/web off). The biggest single
|
|
108
110
|
* fill-rate cost after resolution — turn it down on big screens before anything else. */
|
|
109
111
|
antialias?: boolean | 2 | 4
|
|
112
|
+
/** Keep a stencil buffer for this scene (off by default: it costs memory and a clear per frame).
|
|
113
|
+
* Needed before any material's `stencil` test or write does anything. */
|
|
114
|
+
stencil?: boolean
|
|
110
115
|
/** Render the 3D at this fraction of the viewport (0.25–1) and upscale; the UI stays at native
|
|
111
116
|
* resolution. A fixed, predictable cut of per-pixel GPU work — `0.75` is ~45 % cheaper and
|
|
112
117
|
* barely visible in motion, `0.5` quarters it. Headless renders ignore it. */
|
|
@@ -128,6 +133,10 @@ export type SceneOptions = {
|
|
|
128
133
|
* scene file — `env` is applied before the nodes build) and leaves already-loaded ones alone.
|
|
129
134
|
* On the desktop host `CREATOR_TEXTURE_ANISOTROPY` overrides it, for tuning without a rebuild. */
|
|
130
135
|
anisotropy?: number
|
|
136
|
+
/** Engine-wide cap on texture size, 0 / unset = none (see `Texture.maxSize`): a KTX2 above it
|
|
137
|
+
* loses its top mip levels on load, a glTF image is downsampled. Applied before this scene's
|
|
138
|
+
* assets load; like `anisotropy` it does not touch textures already loaded. */
|
|
139
|
+
maxTextureSize?: number
|
|
131
140
|
/** The look on an HDR display (a screen with headroom above SDR white — Apple XDR panels, the
|
|
132
141
|
* macOS host today); ignored on SDR. `strength` 0..1 is how much of the picture reaches for the
|
|
133
142
|
* display's headroom (0 only what SDR clipped, 1 nearly everything; default 0.35). `paperWhite`
|
|
@@ -148,17 +157,16 @@ const installRenderSyncedFrames = (): void => {
|
|
|
148
157
|
framesInstalled = true
|
|
149
158
|
_installAspectFrames((early, late) => {
|
|
150
159
|
if (typeof _creator.setEarlyUpdate !== "function") return false
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
_creator.setEarlyUpdate(
|
|
160
|
+
// (The per-phase read-cache stamp — `_phaseStamp` in core/Aspect.ts — advances inside the phase
|
|
161
|
+
// runners themselves, so it is right on this path and on the setLoop fallback alike.)
|
|
162
|
+
_creator.setEarlyUpdate(early)
|
|
154
163
|
_creator.setLateUpdate(late)
|
|
155
164
|
return true
|
|
156
165
|
}, (fixed) => {
|
|
157
166
|
// The fixed phase runs INSIDE the engine's substep loop (creator-physics' frame protocol) when the host has the
|
|
158
|
-
// hook
|
|
159
|
-
// an older host returns false and the dispatcher steps its own 1/60 accumulator.
|
|
167
|
+
// hook. Feature-detected: an older host returns false and the dispatcher steps its own 1/60 accumulator.
|
|
160
168
|
if (typeof _creator.setFixedUpdate !== "function") return false
|
|
161
|
-
_creator.setFixedUpdate(
|
|
169
|
+
_creator.setFixedUpdate(fixed)
|
|
162
170
|
return true
|
|
163
171
|
})
|
|
164
172
|
// Time.scale / Time.paused reach the engine's own clocks (physics, animators, particles) through
|
|
@@ -172,6 +180,8 @@ export class Scene implements Presentable {
|
|
|
172
180
|
/** @internal the IBL intensity this scene was built with (Lightmap.load's ambient floor). */
|
|
173
181
|
_environmentIntensity = 20000
|
|
174
182
|
readonly camera: Camera
|
|
183
|
+
/** The listener + global 3D audio knobs (docs/audio-plan.md). */
|
|
184
|
+
readonly audio: SceneAudio = new SceneAudio()
|
|
175
185
|
|
|
176
186
|
/** @internal touch listeners, read by the dispatch system. */
|
|
177
187
|
readonly _clickListeners: Array<(ev: ClickEvent<Node | null>) => void> = []
|
|
@@ -220,9 +230,11 @@ export class Scene implements Presentable {
|
|
|
220
230
|
// `false` must reach the host: desktop defaults to 4× MSAA, so an explicit off is a real change.
|
|
221
231
|
_creator.setSceneMultiSampleAntiAliasing(this._id, a !== false, typeof a === "number" ? a : 4)
|
|
222
232
|
}
|
|
233
|
+
if (options.stencil !== undefined) this.setStencil(options.stencil)
|
|
223
234
|
// Before anything else in this constructor that could load a texture: the value has to be in
|
|
224
235
|
// place by the time assets bind their samplers.
|
|
225
|
-
if (options.anisotropy !== undefined)
|
|
236
|
+
if (options.anisotropy !== undefined) Texture.anisotropy = options.anisotropy
|
|
237
|
+
if (options.maxTextureSize !== undefined) Texture.maxSize = options.maxTextureSize
|
|
226
238
|
if (options.hdr) {
|
|
227
239
|
if (options.hdr.strength !== undefined) device.hdr.strength = options.hdr.strength
|
|
228
240
|
if (options.hdr.paperWhite !== undefined) device.hdr.paperWhite = options.hdr.paperWhite
|
|
@@ -280,6 +292,28 @@ export class Scene implements Presentable {
|
|
|
280
292
|
setAntialias(enabled: boolean, scale = 4): void {
|
|
281
293
|
_creator.setSceneMultiSampleAntiAliasing(this._id, enabled, scale)
|
|
282
294
|
}
|
|
295
|
+
/** Depth-reading effects on / off (a graphics-settings menu): soft particles and projected decals
|
|
296
|
+
* read the scene depth, which costs a depth pre-pass of every opaque draw (~12 % of a fill-bound
|
|
297
|
+
* frame). Off = hard-edged particles, no decals, no pre-pass. Engine-wide, live. */
|
|
298
|
+
setDepthEffects(enabled: boolean): void {
|
|
299
|
+
_creator.setDepthEffects?.(enabled)
|
|
300
|
+
}
|
|
301
|
+
/** LOD distance (a graphics-settings menu): the engine's LOD thresholds × `bias`. 2 = every level
|
|
302
|
+
* switches at half the distance (a model must look twice as big on screen to keep its detail),
|
|
303
|
+
* 0.5 = full detail twice as far, 1 = the defaults. Engine-wide, live. Only GLBs that carry
|
|
304
|
+
* `_LOD<n>` meshes (`lecodes assets doctor --lod`) have levels to switch. */
|
|
305
|
+
setLodBias(bias: number): void {
|
|
306
|
+
_creator.setLodBias?.(bias)
|
|
307
|
+
}
|
|
308
|
+
/** Runtime form of `bloom` / `bloomIntensity` (a graphics-settings menu). */
|
|
309
|
+
setBloom(enabled: boolean, intensity = 0.2): void {
|
|
310
|
+
_creator.setBloomOptions(this._id, enabled, intensity, 1)
|
|
311
|
+
}
|
|
312
|
+
/** The scene's stencil buffer on / off (see `SceneOptions.stencil`). */
|
|
313
|
+
setStencil(enabled: boolean): void {
|
|
314
|
+
if (_creator.setSceneStencil) _creator.setSceneStencil(this._id, enabled)
|
|
315
|
+
else if (enabled) console.warn("[scene] this host has no setSceneStencil — stencil ignored")
|
|
316
|
+
}
|
|
283
317
|
|
|
284
318
|
add(...nodes: Node[]): this {
|
|
285
319
|
for (const n of nodes) _creator.addEntityToScene(this._id, n.id)
|
package/src/gl/Texture.ts
CHANGED
|
@@ -2,7 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
import { fetch, type FetchResponse, type File } from "../runtime/fetch"
|
|
4
4
|
|
|
5
|
+
/** `Texture.load` options. */
|
|
6
|
+
export type TextureLoadOptions = {
|
|
7
|
+
/** `true` (default): colour, stored sRGB. `false`: data (normal map, mask, heightmap) — kept linear. */
|
|
8
|
+
srgb?: boolean
|
|
9
|
+
/** Ignore `Texture.maxSize` for this texture (a lightmap page, a lookup table). */
|
|
10
|
+
fullSize?: boolean
|
|
11
|
+
}
|
|
12
|
+
/** `_creator.createTexture` flags bit: linear data (creator-gl CREATOR_TEXTURE_LINEAR). */
|
|
13
|
+
const TEXTURE_FLAG_LINEAR = 1
|
|
14
|
+
/** `_creator.createTexture` flags bit: exempt from `Texture.maxSize` (creator-gl CREATOR_TEXTURE_FULL_SIZE). */
|
|
15
|
+
const TEXTURE_FLAG_FULL_SIZE = 2
|
|
16
|
+
|
|
5
17
|
export class Texture {
|
|
18
|
+
static _maxSize = 0
|
|
19
|
+
/** Engine-wide cap on texture size (a "texture quality" setting), 0 = none. A KTX2 wider or
|
|
20
|
+
* taller than this loses its top mip levels on load (nothing resampled, less memory and
|
|
21
|
+
* bandwidth), a glTF PNG/JPEG is downsampled. Reaches textures loaded AFTER it is set — a loaded
|
|
22
|
+
* level keeps its textures — so set it up front (`SceneOptions.maxTextureSize`, or before the
|
|
23
|
+
* level loads) and apply a menu change on the next level load. Lightmap pages are exempt
|
|
24
|
+
* (`TextureLoadOptions.fullSize`). A host may pin it (desktop `CREATOR_TEXTURE_MAX_SIZE`). */
|
|
25
|
+
static _anisotropy = 2
|
|
26
|
+
/** Engine-wide anisotropic filtering, 1 (off) … 16 (default 2; `SceneOptions.anisotropy` sets it up
|
|
27
|
+
* front). A sampler is baked when its texture is bound, so like `maxSize` this reaches textures
|
|
28
|
+
* loaded AFTER it — set it before the level loads. Measured on a lightmapped interior at 720p:
|
|
29
|
+
* 4× costs ~20 % of the frame over 1× on an integrated GPU. A host may pin it. */
|
|
30
|
+
static get anisotropy(): number { return Texture._anisotropy }
|
|
31
|
+
static set anisotropy(level: number) {
|
|
32
|
+
Texture._anisotropy = level >= 1 ? Math.min(16, level) : 1
|
|
33
|
+
_creator.setTextureAnisotropy?.(Texture._anisotropy)
|
|
34
|
+
}
|
|
35
|
+
static get maxSize(): number { return Texture._maxSize }
|
|
36
|
+
static set maxSize(size: number) {
|
|
37
|
+
Texture._maxSize = size > 0 ? Math.floor(size) : 0
|
|
38
|
+
_creator.setTextureMaxSize?.(Texture._maxSize)
|
|
39
|
+
}
|
|
6
40
|
/** @internal native texture handle. */
|
|
7
41
|
readonly _id: number
|
|
8
42
|
readonly width: number
|
|
@@ -47,17 +81,23 @@ export class Texture {
|
|
|
47
81
|
_creator.updateTexturePixels?.(this._id, x, y, width, height, data)
|
|
48
82
|
}
|
|
49
83
|
|
|
50
|
-
|
|
84
|
+
/** Decode an image (PNG / JPG, or a KTX2 the core transcodes) into a texture. `srgb` (default
|
|
85
|
+
* true) says the bytes are COLOUR, stored sRGB so the GPU linearises them on sample; pass
|
|
86
|
+
* `false` for DATA — a normal map, a mask, a heightmap — which must come back as stored (a
|
|
87
|
+
* flat normal read through sRGB bends by ~35°). A KTX2 decides by its own header. Image
|
|
88
|
+
* textures get a mip chain (trilinear) on hosts that build one. */
|
|
89
|
+
static load(source: string | FetchResponse | File, options: TextureLoadOptions = {}): Promise<Texture> {
|
|
51
90
|
if (typeof source === "string") {
|
|
52
91
|
return fetch(source, { useOnce: true }).then((resp) => {
|
|
53
92
|
if (resp.status >= 400) {
|
|
54
93
|
return Promise.reject(new Error(`Failed to fetch texture from ${source}. HTTP ${resp.status}`))
|
|
55
94
|
}
|
|
56
|
-
return Texture.load(resp)
|
|
95
|
+
return Texture.load(resp, options)
|
|
57
96
|
})
|
|
58
97
|
}
|
|
98
|
+
const flags = (options.srgb === false ? TEXTURE_FLAG_LINEAR : 0) | (options.fullSize ? TEXTURE_FLAG_FULL_SIZE : 0)
|
|
59
99
|
return new Promise<Texture>((resolve, reject) => {
|
|
60
|
-
_creator.createTexture((source as any)._id, (id, w, h) => resolve(new Texture(w, h, id)), reject)
|
|
100
|
+
_creator.createTexture((source as any)._id, (id, w, h) => resolve(new Texture(w, h, id)), reject, flags)
|
|
61
101
|
})
|
|
62
102
|
}
|
|
63
103
|
}
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
// AnimationClip.load(asset('./run.glb')),
|
|
11
11
|
// ])
|
|
12
12
|
// const slash = await AnimationClip.load(asset('./attacks.glb'), 'Slash') // one out of many
|
|
13
|
-
// const bob = AnimationClip.
|
|
13
|
+
// const bob = AnimationClip.fromCurves({ tracks: { Hips: { position: [[0, [0,0,0]], [1, [0,0.05,0]]] } } })
|
|
14
|
+
// const stopM = AnimationClip.from(stop, { mirror: true }) // the other side of the body
|
|
15
|
+
// const kick = AnimationClip.from(take, { from: 0.2, to: 1.1 }) // a window of a longer take
|
|
14
16
|
|
|
15
17
|
import { fetch, type FetchResponse } from "../../runtime/fetch"
|
|
16
18
|
import type { Vec3Like } from "../../math/vec"
|
|
@@ -37,6 +39,20 @@ export type ClipDef = {
|
|
|
37
39
|
|
|
38
40
|
export type ClipInfo = { name: string, duration: number, trackCount: number }
|
|
39
41
|
|
|
42
|
+
/** What `AnimationClip.from(clip, …)` derives from a clip. */
|
|
43
|
+
export type DeriveOptions = {
|
|
44
|
+
/** The clip MIRRORED — left ↔ right, the motion on the other side of the body (a stop that brakes on the
|
|
45
|
+
* left foot brakes on the right). Derived on the rig of the clip's own file (joints pair by name, the
|
|
46
|
+
* sagittal plane comes from the rest pose), so the result is the same on every model: its contacts,
|
|
47
|
+
* phase, root motion and heading are the mirrored ones, its name is `<name>_M`. Only clips from a GLB
|
|
48
|
+
* carry a rig; a curve-built clip cannot be mirrored. */
|
|
49
|
+
mirror?: boolean
|
|
50
|
+
/** Start of the window, seconds of the source (default 0). */
|
|
51
|
+
from?: number
|
|
52
|
+
/** End of the window, seconds of the source (default the clip's end). */
|
|
53
|
+
to?: number
|
|
54
|
+
}
|
|
55
|
+
|
|
40
56
|
const PATH = { position: 0, rotation: 1, scale: 2 } as const
|
|
41
57
|
const INTERP = { linear: 0, step: 1 } as const
|
|
42
58
|
|
|
@@ -90,8 +106,8 @@ export class AnimationClip {
|
|
|
90
106
|
|
|
91
107
|
/** Mark a moment of the clip (SECONDS from its start) with an event name: `kick.addEvent(0.4, 'hit')`
|
|
92
108
|
* → `anim.on('hit', (clip, layer) => …)` fires when the playhead crosses it, loops included.
|
|
93
|
-
* Events are part of the clip: every model playing it gets them; `
|
|
94
|
-
* the
|
|
109
|
+
* Events are part of the clip: every model playing it gets them; `AnimationClip.from(clip, { from, to })`
|
|
110
|
+
* keeps the ones inside the window, re-timed. Chainable. */
|
|
95
111
|
addEvent(time: number, name: string): this {
|
|
96
112
|
this._events.push({ t: Math.min(Math.max(0, time), this.duration), name })
|
|
97
113
|
this._events.sort((a, b) => a.t - b.t)
|
|
@@ -128,7 +144,7 @@ export class AnimationClip {
|
|
|
128
144
|
|
|
129
145
|
/** Build a clip from curves in code — no DCC needed. Keys are `[time, value]`; a track binds to the
|
|
130
146
|
* node of that name when the clip is used by an Animator (bones, or any child node). */
|
|
131
|
-
static
|
|
147
|
+
static fromCurves(def: ClipDef): AnimationClip {
|
|
132
148
|
const names: string[] = []
|
|
133
149
|
const data: number[] = [0, 0]
|
|
134
150
|
let trackCount = 0
|
|
@@ -153,30 +169,37 @@ export class AnimationClip {
|
|
|
153
169
|
if (def.duration !== undefined) duration = def.duration
|
|
154
170
|
data[1] = duration
|
|
155
171
|
const setId = _creator.createClipFromTracks(names.join("\n"), new Float32Array(data))
|
|
156
|
-
if (setId === 0) throw new Error("AnimationClip.
|
|
172
|
+
if (setId === 0) throw new Error("AnimationClip.fromCurves: invalid track data")
|
|
157
173
|
const info = readInfo(setId)[0] ?? { name: "clip", duration, trackCount }
|
|
158
174
|
return new AnimationClip(setId, 0, info)
|
|
159
175
|
}
|
|
160
176
|
|
|
161
|
-
/** A
|
|
162
|
-
* (`
|
|
163
|
-
* action,
|
|
177
|
+
/** A NEW clip derived from `clip`: its mirror (`{ mirror: true }` — the other side of the body), a window
|
|
178
|
+
* of it (`{ from, to }` seconds of the source, re-timed to 0; array-slice semantics), or both. Cut a
|
|
179
|
+
* too-long take down to the action, carve several sub-clips out of one packed timeline, get the
|
|
180
|
+
* left-footed stop from the right-footed one:
|
|
164
181
|
*
|
|
165
|
-
* clips: { Kick:
|
|
182
|
+
* clips: { Kick: AnimationClip.from(kick, { from: 0.2, to: 1.1 }), StopM: AnimationClip.from(stop, { mirror: true }) }
|
|
166
183
|
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
const st = Math.max(0,
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
184
|
+
* A window drops the keys outside it and interpolates exact boundary values in, so the clip starts and
|
|
185
|
+
* ends precisely on the source's pose at the cut points. Everything downstream (blend spaces, events'
|
|
186
|
+
* times, root motion, foot contacts, phase sync) sees a normal clip. */
|
|
187
|
+
static from(clip: AnimationClip, options: DeriveOptions): AnimationClip {
|
|
188
|
+
const mirror = options.mirror ?? false
|
|
189
|
+
const windowed = options.from !== undefined || options.to !== undefined
|
|
190
|
+
const st = windowed ? Math.max(0, options.from ?? 0) : -1
|
|
191
|
+
const e = windowed ? Math.min(options.to ?? clip.duration, clip.duration) : -1
|
|
192
|
+
const setId = _creator.deriveClip(clip._set, clip._index, mirror, st, e)
|
|
193
|
+
if (setId === 0) {
|
|
194
|
+
const what = windowed ? `bad range ${options.from ?? 0}–${options.to ?? clip.duration}` : "cannot mirror (no rig in the clip's file, or nothing to mirror)"
|
|
195
|
+
throw new Error(`AnimationClip.from: ${what} of '${clip.name}' (${clip.duration.toFixed(2)}s)`)
|
|
196
|
+
}
|
|
197
|
+
const info = readInfo(setId)[0] ?? { name: mirror ? `${clip.name}_M` : clip.name, duration: windowed ? e - st : clip.duration, trackCount: clip.trackCount }
|
|
177
198
|
const out = new AnimationClip(setId, 0, info)
|
|
178
199
|
// the engine re-timed the event TIMES; carry the names the same way
|
|
179
|
-
out._events =
|
|
200
|
+
out._events = windowed
|
|
201
|
+
? clip._events.filter((ev) => ev.t >= st - 1e-6 && ev.t <= e + 1e-6).map((ev) => ({ t: Math.min(Math.max(0, ev.t - st), e - st), name: ev.name }))
|
|
202
|
+
: clip._events.map((ev) => ({ ...ev }))
|
|
180
203
|
return out
|
|
181
204
|
}
|
|
182
205
|
|
|
@@ -5,15 +5,16 @@
|
|
|
5
5
|
//
|
|
6
6
|
// const hero = await Model.load(asset('./hero.glb'))
|
|
7
7
|
// hero.anim.playLoop('Idle') // the loop: what the character rests on
|
|
8
|
-
// await hero.anim.play('Jump', {
|
|
8
|
+
// await hero.anim.play('Jump', { fade: 0.2 }) // a one-shot over it, back to the loop after
|
|
9
9
|
//
|
|
10
10
|
// const loco = hero.anim.playLoop({ Idle: 0, Walk: 2, Run: 6 }) // a blend space as the loop
|
|
11
11
|
// loco.value = hero.controller.velocity.length // where we are on its axis
|
|
12
12
|
// const upper = hero.anim.addLayer({ mask: 'Spine1' })
|
|
13
13
|
// upper.playLoop('Aim') // arms aim, legs keep walking
|
|
14
14
|
//
|
|
15
|
-
// Every switch is an inertialized transition: the pose shown
|
|
16
|
-
// new source decays over `fade`
|
|
15
|
+
// Every switch is an inertialized transition: the pose shown (a loop, a one-shot, rest) does not fade
|
|
16
|
+
// away, its difference to the new source decays over `fade` — and no `fade` means a cut, never a
|
|
17
|
+
// default. A clip may be replaced at any time; a one-shot on a layer with no loop holds its last frame.
|
|
17
18
|
//
|
|
18
19
|
// The surface, top to bottom: clips → playing → root motion → the feet and the warp (`anim.feet`,
|
|
19
20
|
// `anim.warp` — locomotion's post-processing of the pose) → level of detail → engine-facing.
|