lecodes-cli 0.18.0 → 0.18.1
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/README.md +1 -1
- package/dist/index.js +1222 -293
- package/package.json +4 -4
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk/compile/aspectMacro.ts +52 -8
- package/runtime/sdk/compile/assetMacro.ts +116 -15
- package/runtime/sdk/compile/bundler.ts +38 -3
- package/runtime/sdk/compile/index.ts +15 -0
- package/runtime/sdk/compile/sceneEditor.ts +11 -1
- package/runtime/sdk/compile/shaderSchema.ts +202 -0
- package/runtime/sdk/compile/shaderTargets.ts +42 -3
- package/runtime/sdk/core/Aspect.ts +512 -255
- package/runtime/sdk/core/compWrite.ts +42 -0
- package/runtime/sdk/core/fields.ts +1 -1
- package/runtime/sdk/core/time.ts +81 -0
- package/runtime/sdk/g2/Camera2D.ts +8 -1
- package/runtime/sdk/g2/CharacterController2D.ts +253 -53
- package/runtime/sdk/g2/Node2D.ts +80 -10
- package/runtime/sdk/g2/OneWay2D.ts +66 -0
- package/runtime/sdk/g2/Physics2D.ts +240 -30
- package/runtime/sdk/g2/Scene2D.ts +33 -1
- package/runtime/sdk/g2/Shape2D.ts +218 -22
- package/runtime/sdk/g2/Trigger2D.ts +42 -12
- package/runtime/sdk/g2/groups2d.ts +106 -0
- package/runtime/sdk/g2/loop.ts +15 -4
- package/runtime/sdk/gl/Camera.ts +1 -1
- package/runtime/sdk/gl/CameraPlace.ts +4 -3
- package/runtime/sdk/gl/CharacterController.ts +184 -56
- package/runtime/sdk/gl/Gearbox.ts +212 -0
- package/runtime/sdk/gl/Geometry.ts +70 -9
- package/runtime/sdk/gl/Light.ts +64 -2
- package/runtime/sdk/gl/Lightmap.ts +179 -0
- package/runtime/sdk/gl/Material.ts +25 -0
- package/runtime/sdk/gl/Mesh.ts +6 -23
- package/runtime/sdk/gl/Model.ts +16 -2
- package/runtime/sdk/gl/Node.ts +119 -39
- package/runtime/sdk/gl/Physics.ts +75 -24
- package/runtime/sdk/gl/Scene.ts +148 -5
- package/runtime/sdk/gl/Shape.ts +42 -3
- package/runtime/sdk/gl/Trigger.ts +1 -6
- package/runtime/sdk/gl/Vehicle.ts +276 -322
- package/runtime/sdk/gl/Wheel.ts +240 -0
- package/runtime/sdk/gl/scenarios.ts +291 -317
- package/runtime/sdk/inject.ts +186 -171
- package/runtime/sdk/scene/defineScene.ts +1227 -1172
- package/runtime/sdk/scene/gizmos.ts +148 -128
- package/runtime/sdk/scene/material.ts +188 -0
- package/runtime/sdk-types.json +1 -1
|
@@ -8,17 +8,23 @@
|
|
|
8
8
|
// crate.addEventListener('enter', other => …) // contact / trigger overlap began
|
|
9
9
|
//
|
|
10
10
|
// For a `dynamic` body, physics OWNS the transform — do NOT set node.position per frame; drive it via
|
|
11
|
-
// velocity / applyImpulse. Reading node.position is free (native already wrote it).
|
|
12
|
-
//
|
|
11
|
+
// velocity / applyImpulse. Reading node.position is free (native already wrote it). A `position` write
|
|
12
|
+
// is a TELEPORT that reaches the body (Shape routes it), which is how a kinematic body is placed too.
|
|
13
13
|
|
|
14
14
|
import { Aspect } from "../core/Aspect"
|
|
15
15
|
import type { FieldMeta } from "../core/fields"
|
|
16
|
+
import type { CompAxis, CompWriter } from "../core/compWrite"
|
|
16
17
|
import { Vec3, cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
17
18
|
import { nodeRegistry, type Node } from "./Node"
|
|
18
19
|
import { Shape } from "./Shape"
|
|
19
20
|
|
|
20
21
|
export type MotionType = "static" | "dynamic" | "kinematic"
|
|
21
22
|
const MOTION: Record<MotionType, number> = { static: 0, kinematic: 1, dynamic: 2 }
|
|
23
|
+
// Jolt's angular velocity is rad/s; every angle the SDK exposes is in degrees (like `eulerAngles`).
|
|
24
|
+
const DEG2RAD = Math.PI / 180
|
|
25
|
+
const RAD2DEG = 180 / Math.PI
|
|
26
|
+
// Scratch for velocity reads (one per module, never handed out) — a read allocates the Vec3 only.
|
|
27
|
+
const scratch = new Float32Array(3)
|
|
22
28
|
|
|
23
29
|
export interface PhysicsConfig {
|
|
24
30
|
/** Gravity in world units/s² (Y-up: down is negative). Default [0, -9.81, 0]. */
|
|
@@ -35,7 +41,14 @@ export interface RayHit {
|
|
|
35
41
|
fraction: number
|
|
36
42
|
}
|
|
37
43
|
|
|
38
|
-
|
|
44
|
+
/**
|
|
45
|
+
* What a body's friction already is the moment the host creates it. This is part of the ABI, not an
|
|
46
|
+
* assumption: bridges.d.ts states it, so every host owes us this value. Knowing it lets attach skip
|
|
47
|
+
* a bridge call per body whenever nobody named a surface.
|
|
48
|
+
*/
|
|
49
|
+
export const DEFAULT_FRICTION = 0.6
|
|
50
|
+
|
|
51
|
+
export class Physics extends Aspect<"physics", Node> implements CompWriter {
|
|
39
52
|
static readonly aspect = "physics"
|
|
40
53
|
|
|
41
54
|
/** Render interpolation of body transforms between the fixed 60 Hz steps (global; default on). Turn
|
|
@@ -60,11 +73,19 @@ export class Physics extends Aspect<"physics", Node> {
|
|
|
60
73
|
* that decides how well a car corners.
|
|
61
74
|
*
|
|
62
75
|
* Change it at runtime by re-configuring — `floor.aspect(Physics, { friction: 0.02 })` (an ice
|
|
63
|
-
* patch)
|
|
64
|
-
*
|
|
65
|
-
*
|
|
76
|
+
* patch) — or by assigning `floor.physics.friction = 0.02`; either way the setter pushes the new
|
|
77
|
+
* surface to the live body at once. (chisel keeps an instance setter whose name appears as an
|
|
78
|
+
* object-literal key anywhere in the bundle, so the config-object route survives tree-shaking
|
|
79
|
+
* even when no code reads `.friction`.)
|
|
66
80
|
*/
|
|
67
|
-
friction
|
|
81
|
+
get friction(): number { return this._friction }
|
|
82
|
+
set friction(v: number) {
|
|
83
|
+
this._friction = v
|
|
84
|
+
// Unconditional on a live body, unlike attach: it may be on its way BACK to the default from
|
|
85
|
+
// something else, and skipping that would leave the ice patch in place.
|
|
86
|
+
if (this._bodyId) _creator.physicsSetFriction?.(this._bodyId, v)
|
|
87
|
+
}
|
|
88
|
+
private _friction = DEFAULT_FRICTION
|
|
68
89
|
|
|
69
90
|
/** Scene-editor inspector: `motion` as a dropdown (a string default alone infers a text box). */
|
|
70
91
|
static fields: FieldMeta<Physics> = {
|
|
@@ -86,7 +107,9 @@ export class Physics extends Aspect<"physics", Node> {
|
|
|
86
107
|
}
|
|
87
108
|
const shapeId = shape._claim() // drop the pick-only body; reuse its shape
|
|
88
109
|
this._bodyId = _creator.physicsCreateBody(this.node.id, shapeId, MOTION[this.motion], this.mass, false, false)
|
|
89
|
-
|
|
110
|
+
// A fresh body already carries DEFAULT_FRICTION host-side, so only a NAMED surface has to
|
|
111
|
+
// travel — the common case costs no bridge call at all.
|
|
112
|
+
if (this._friction !== DEFAULT_FRICTION) _creator.physicsSetFriction?.(this._bodyId, this._friction)
|
|
90
113
|
shape._ownBody(this._bodyId)
|
|
91
114
|
}
|
|
92
115
|
|
|
@@ -99,26 +122,57 @@ export class Physics extends Aspect<"physics", Node> {
|
|
|
99
122
|
}
|
|
100
123
|
}
|
|
101
124
|
|
|
102
|
-
/** Re-configuring (`node.aspect(Physics, { friction })`) pushes the new surface to the live body. */
|
|
103
|
-
onReconfigure(): void { this._applyFriction() }
|
|
104
|
-
|
|
105
|
-
private _applyFriction(): void {
|
|
106
|
-
if (this._bodyId) _creator.physicsSetFriction?.(this._bodyId, this.friction)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
125
|
/** Native Jolt body id. 0 until attached, or if the build has no physics support. */
|
|
110
126
|
get id(): number { return this._bodyId }
|
|
111
127
|
|
|
112
|
-
/** Linear velocity in world units/second (fresh Vec3 on read
|
|
128
|
+
/** Linear velocity in world units/second (fresh Vec3 on read; `velocity.y = 5` written directly on
|
|
129
|
+
* the aspect is compiled to a component write — see `_writeComp`). */
|
|
113
130
|
get velocity(): Vec3 {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return new Vec3(
|
|
131
|
+
if (!this._bodyId) return new Vec3(0, 0, 0)
|
|
132
|
+
_creator.physicsGetLinearVelocity(this._bodyId, scratch)
|
|
133
|
+
return new Vec3(scratch[0], scratch[1], scratch[2])
|
|
117
134
|
}
|
|
118
135
|
set velocity(v: Vec3Like) {
|
|
119
136
|
if (this._bodyId) _creator.physicsSetLinearVelocity(this._bodyId, cx(v), cy(v), cz(v))
|
|
120
137
|
}
|
|
121
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Angular velocity — **degrees/second about each world axis** (the SDK's angle unit everywhere;
|
|
141
|
+
* the engine works in radians and converts here). Fresh Vec3 on read.
|
|
142
|
+
*
|
|
143
|
+
* This is the only handle on a body's spin, and placing an object usually needs it: a `position`
|
|
144
|
+
* write is a pure teleport, so a crate that was tumbling keeps tumbling at its new home. Putting
|
|
145
|
+
* something down is `body.velocity = [0,0,0]; body.angularVelocity = [0,0,0]; node.position = p`.
|
|
146
|
+
*/
|
|
147
|
+
get angularVelocity(): Vec3 {
|
|
148
|
+
if (!this._bodyId || !_creator.physicsGetAngularVelocity) return new Vec3(0, 0, 0)
|
|
149
|
+
_creator.physicsGetAngularVelocity(this._bodyId, scratch)
|
|
150
|
+
return new Vec3(scratch[0] * RAD2DEG, scratch[1] * RAD2DEG, scratch[2] * RAD2DEG)
|
|
151
|
+
}
|
|
152
|
+
set angularVelocity(v: Vec3Like) {
|
|
153
|
+
if (this._bodyId && _creator.physicsSetAngularVelocity) {
|
|
154
|
+
_creator.physicsSetAngularVelocity(this._bodyId, cx(v) * DEG2RAD, cy(v) * DEG2RAD, cz(v) * DEG2RAD)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** @internal chisel `comp_write` (see core/compWrite.ts): `body.velocity.y = v` / `body.angularVelocity.z
|
|
159
|
+
* = v` written directly on the aspect compile to a call here — one read into the scratch, one set, no
|
|
160
|
+
* vector. The list is compile-time data. */
|
|
161
|
+
static _comps = [ "velocity", "angularVelocity" ]
|
|
162
|
+
_writeComp(prop: string, axis: CompAxis, v: number): void {
|
|
163
|
+
if (!this._bodyId || axis === "w") return
|
|
164
|
+
const i = axis === "x" ? 0 : axis === "y" ? 1 : 2
|
|
165
|
+
if (prop === "velocity") {
|
|
166
|
+
_creator.physicsGetLinearVelocity(this._bodyId, scratch)
|
|
167
|
+
scratch[i] = v
|
|
168
|
+
_creator.physicsSetLinearVelocity(this._bodyId, scratch[0], scratch[1], scratch[2])
|
|
169
|
+
} else if (_creator.physicsGetAngularVelocity && _creator.physicsSetAngularVelocity) {
|
|
170
|
+
_creator.physicsGetAngularVelocity(this._bodyId, scratch)
|
|
171
|
+
scratch[i] = v * DEG2RAD
|
|
172
|
+
_creator.physicsSetAngularVelocity(this._bodyId, scratch[0], scratch[1], scratch[2])
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
122
176
|
/** Apply an instantaneous impulse (kg·m/s) and wake the body. */
|
|
123
177
|
applyImpulse(v: Vec3Like): this {
|
|
124
178
|
if (this._bodyId) _creator.physicsApplyImpulse(this._bodyId, cx(v), cy(v), cz(v))
|
|
@@ -138,11 +192,8 @@ export class Physics extends Aspect<"physics", Node> {
|
|
|
138
192
|
return this
|
|
139
193
|
}
|
|
140
194
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (this._bodyId) _creator.physicsSetBodyPosition(this._bodyId, cx(p), cy(p), cz(p))
|
|
144
|
-
return this
|
|
145
|
-
}
|
|
195
|
+
// No moveTo(): `node.position = p` IS the move. Shape hands the body id to the node, so a position
|
|
196
|
+
// write reaches the body — one way to place anything, whether or not physics is involved.
|
|
146
197
|
|
|
147
198
|
// --- world (static) -------------------------------------------------------------------------
|
|
148
199
|
/** Configure gravity / limits. Call once before creating bodies. */
|
package/runtime/sdk/gl/Scene.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// direct light + shadows.
|
|
7
7
|
|
|
8
8
|
import { Color, type ColorInput } from "../core/color"
|
|
9
|
-
import { _installAspectFrames } from "../core/Aspect"
|
|
9
|
+
import { _installAspectFrames, _installTimeScale, _attachSystem, _detachSystem, _detachAllSystems, type Aspect, type AspectCtor, type FieldOf, type TargetOf } from "../core/Aspect"
|
|
10
10
|
import { _bumpNavEpoch, _navEpoch, _navSupported, _setCurrent, Presentable, type PresentOptions } from "../ui/presentable"
|
|
11
11
|
import type { ClickEvent, TouchStartEvent } from "../runtime/touch"
|
|
12
12
|
import type { FetchResponse } from "../runtime/fetch"
|
|
@@ -18,6 +18,42 @@ import { Node, nodeRegistry } from "./Node"
|
|
|
18
18
|
import { glState } from "./state"
|
|
19
19
|
import { registerTouchEndEvent, registerTouchStartEvent } from "./touch"
|
|
20
20
|
|
|
21
|
+
export type AmbientOcclusionOptions = {
|
|
22
|
+
/** Strength of the darkening (default 1). */
|
|
23
|
+
intensity?: number
|
|
24
|
+
/** How far the occlusion reaches, in metres (default 0.3). */
|
|
25
|
+
radius?: number
|
|
26
|
+
/** Falloff contrast; >1 tightens it into the crease (default 1). */
|
|
27
|
+
power?: number
|
|
28
|
+
/** Sample count + filtering (default 'medium'). Not the buffer resolution — that stays half. */
|
|
29
|
+
quality?: "low" | "medium" | "high" | "ultra"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type FogOptions = {
|
|
33
|
+
/** A TINT on the in-scattered ambient, not an absolute colour: the engine multiplies it by the
|
|
34
|
+
* environment luminance, so white (the default) means "fog as bright as the ambient" and the fog
|
|
35
|
+
* brightens with `environmentIntensity`. Do NOT expect the same hex to look like it does in
|
|
36
|
+
* `skybox` — that one IS an absolute radiance and reads roughly an order of magnitude brighter.
|
|
37
|
+
* Tint towards the sky's hue; leave it white to sit at the ambient level. */
|
|
38
|
+
color?: ColorInput
|
|
39
|
+
/** Metres from the camera before the fog starts (default 0). */
|
|
40
|
+
start?: number
|
|
41
|
+
/** Extinction per metre at `height`; 0.01 ≈ clearly visible over ~100 m (default 0.01). */
|
|
42
|
+
density?: number
|
|
43
|
+
/** The fog's "sea level" in world Y (default 0). */
|
|
44
|
+
height?: number
|
|
45
|
+
/** How fast it thins with altitude, 1/m. 0 = uniform everywhere; higher = a ground-hugging layer
|
|
46
|
+
* you can see over (default 0). */
|
|
47
|
+
heightFalloff?: number
|
|
48
|
+
/** Cap on how opaque it can get, 0–1 — keeps far shapes readable (default 1). */
|
|
49
|
+
maxOpacity?: number
|
|
50
|
+
/** Metres after which fog stops applying; 0 = everywhere (default 0). */
|
|
51
|
+
cutoff?: number
|
|
52
|
+
/** Take the colour from the environment in the view direction, tinted by `color`, instead of a
|
|
53
|
+
* flat `color`. Convincing when the IBL is a real sky (default false). */
|
|
54
|
+
fromEnvironment?: boolean
|
|
55
|
+
}
|
|
56
|
+
|
|
21
57
|
export type SceneOptions = {
|
|
22
58
|
/** Image-based ambient lighting (default on). */
|
|
23
59
|
ibl?: boolean
|
|
@@ -31,8 +67,29 @@ export type SceneOptions = {
|
|
|
31
67
|
* saturation until very bright; `'linear'` clips each channel (what an engine without a
|
|
32
68
|
* tonemapper shows — saturated, Unity-without-post-processing look); `'filmic'` (Uncharted). */
|
|
33
69
|
toneMapping?: "aces" | "neutral" | "linear" | "filmic"
|
|
34
|
-
/**
|
|
35
|
-
|
|
70
|
+
/** The sky. Three forms:
|
|
71
|
+
* - a colour — a flat clear colour;
|
|
72
|
+
* - `{ texture }` — a KTX1 **cubemap**, the sharp `<name>_skybox.ktx` that filament's `cmgen`
|
|
73
|
+
* produces from an equirectangular .hdr/.exr. This is the one to use for a real sky. Pass an
|
|
74
|
+
* `asset('../assets/sky.ktx')` handle or a bare staged filename;
|
|
75
|
+
* - `'environment'` — reuse the scene's IBL cubemap. Cheapest (no second texture) but that file
|
|
76
|
+
* is prefiltered for roughness, so the sky comes out soft; fine as a fallback, not as the goal.
|
|
77
|
+
*
|
|
78
|
+
* Either texture form draws through filament's own skybox pass: a full-screen pass in device
|
|
79
|
+
* space after the opaque queue. No geometry, no meridian seam, no pole distortion — do NOT build
|
|
80
|
+
* a sky dome or a fullscreen equirect material by hand, both are strictly worse. */
|
|
81
|
+
skybox?: ColorInput | "environment" | { texture: string }
|
|
82
|
+
/** Screen-space ambient occlusion — the contact darkening in creases and where props meet the
|
|
83
|
+
* ground. Without it an IBL lights a crease exactly as brightly as an open face, so everything
|
|
84
|
+
* reads as pasted onto the floor rather than standing on it. `true` takes defaults tuned for
|
|
85
|
+
* human-scale props; `radius` is world-space metres and is the one knob that must follow the
|
|
86
|
+
* scene's scale (~0.3 for objects on a table, ~0.6-1 for a yard of crates and containers). */
|
|
87
|
+
ambientOcclusion?: boolean | AmbientOcclusionOptions
|
|
88
|
+
/** Distance fog / aerial perspective: distant geometry loses contrast so the eye reads depth, and
|
|
89
|
+
* the hard edge where a finite level ends against the skybox goes away. By default the fog applies
|
|
90
|
+
* at every distance — the skybox included — so the sky itself takes the fog colour and the horizon
|
|
91
|
+
* blends on its own; `cutoff` opts geometry beyond a distance back out. */
|
|
92
|
+
fog?: FogOptions
|
|
36
93
|
/** Multisample anti-aliasing: `true` = 4×, `2`/`4` = that many samples, `false` = off (FXAA
|
|
37
94
|
* takes over). Unset keeps the host default (desktop 4×, mobile/web off). The biggest single
|
|
38
95
|
* fill-rate cost after resolution — turn it down on big screens before anything else. */
|
|
@@ -45,6 +102,19 @@ export type SceneOptions = {
|
|
|
45
102
|
* (Filament dynamic resolution, sharpened upscale) down to `min` (default 0.5). Off by default:
|
|
46
103
|
* it makes the output frame-time dependent, so flow tests / headless renders never enable it. */
|
|
47
104
|
dynamicResolution?: boolean | { min?: number }
|
|
105
|
+
/** Anisotropic filtering, 1 (off) … 16. Default 2. What it buys is detail on surfaces seen at a
|
|
106
|
+
* GRAZING angle — a first-person weapon, a floor, a wall — where the sample footprint is far
|
|
107
|
+
* longer in one axis than the other and isotropic filtering has to pick a mip for the long one,
|
|
108
|
+
* several levels coarser than the short axis deserves. Measured on a weapon seen from the side
|
|
109
|
+
* (local contrast): 8.18 at 1× → 10.02 at 2× → 10.84 at 4× → 11.29 at 8×. The default is 2
|
|
110
|
+
* because it is where the curve is steepest per unit of memory bandwidth; raise it if you have
|
|
111
|
+
* the headroom.
|
|
112
|
+
*
|
|
113
|
+
* It is an ENGINE-WIDE default, not a property of this scene: a sampler is baked when its
|
|
114
|
+
* texture is bound, so this reaches the models a scene loads AFTER it (which is every model in a
|
|
115
|
+
* scene file — `env` is applied before the nodes build) and leaves already-loaded ones alone.
|
|
116
|
+
* On the desktop host `CREATOR_TEXTURE_ANISOTROPY` overrides it, for tuning without a rebuild. */
|
|
117
|
+
anisotropy?: number
|
|
48
118
|
}
|
|
49
119
|
|
|
50
120
|
// Route aspect update(dt) through the render-synced native phases (render() early = before physics,
|
|
@@ -63,11 +133,16 @@ const installRenderSyncedFrames = (): void => {
|
|
|
63
133
|
_creator.setLateUpdate(late)
|
|
64
134
|
return true
|
|
65
135
|
})
|
|
136
|
+
// Time.scale / Time.paused reach the engine's own clocks (physics, animators, particles) through
|
|
137
|
+
// setTimeScale; a host without it keeps running its sims at wall-clock speed (feature-detected).
|
|
138
|
+
_installTimeScale((scale) => { _creator.setTimeScale?.(scale) })
|
|
66
139
|
}
|
|
67
140
|
|
|
68
141
|
export class Scene implements Presentable {
|
|
69
142
|
/** @internal native scene handle. */
|
|
70
143
|
readonly _id: number
|
|
144
|
+
/** @internal the IBL intensity this scene was built with (Lightmap.load's ambient floor). */
|
|
145
|
+
_environmentIntensity = 20000
|
|
71
146
|
readonly camera: Camera
|
|
72
147
|
|
|
73
148
|
/** @internal touch listeners, read by the dispatch system. */
|
|
@@ -97,16 +172,22 @@ export class Scene implements Presentable {
|
|
|
97
172
|
}
|
|
98
173
|
|
|
99
174
|
if (options.ibl !== false) _creator.setDefaultIbl(this._id, options.environmentIntensity ?? 20000)
|
|
175
|
+
this._environmentIntensity = options.ibl === false ? 0 : (options.environmentIntensity ?? 20000)
|
|
100
176
|
if (options.bloom) _creator.setBloomOptions(this._id, true, options.bloomIntensity ?? 0.2, 1)
|
|
101
177
|
if (options.toneMapping !== undefined && options.toneMapping !== "aces") {
|
|
102
178
|
_creator.setToneMapping?.(this._id, { neutral: 1, linear: 2, filmic: 3 }[options.toneMapping] ?? 0)
|
|
103
179
|
}
|
|
104
|
-
if (options.skybox !== undefined)
|
|
180
|
+
if (options.skybox !== undefined) this.skybox = options.skybox
|
|
181
|
+
if (options.ambientOcclusion) this.setAmbientOcclusion(options.ambientOcclusion)
|
|
182
|
+
if (options.fog) this.setFog(options.fog)
|
|
105
183
|
if (options.antialias !== undefined) {
|
|
106
184
|
const a = options.antialias
|
|
107
185
|
// `false` must reach the host: desktop defaults to 4× MSAA, so an explicit off is a real change.
|
|
108
186
|
_creator.setSceneMultiSampleAntiAliasing(this._id, a !== false, typeof a === "number" ? a : 4)
|
|
109
187
|
}
|
|
188
|
+
// Before anything else in this constructor that could load a texture: the value has to be in
|
|
189
|
+
// place by the time assets bind their samplers.
|
|
190
|
+
if (options.anisotropy !== undefined) _creator.setTextureAnisotropy?.(options.anisotropy)
|
|
110
191
|
if (options.renderScale !== undefined || options.dynamicResolution) {
|
|
111
192
|
this.setRenderOptions(options.renderScale ?? 1, options.dynamicResolution ?? false)
|
|
112
193
|
}
|
|
@@ -125,7 +206,34 @@ export class Scene implements Presentable {
|
|
|
125
206
|
return this._material
|
|
126
207
|
}
|
|
127
208
|
|
|
128
|
-
set skybox(
|
|
209
|
+
set skybox(sky: ColorInput | "environment" | { texture: string }) {
|
|
210
|
+
if (sky === "environment") { _creator.setSkyboxFromEnvironment?.(this._id); return }
|
|
211
|
+
if (typeof sky === "object" && sky !== null && "texture" in sky) {
|
|
212
|
+
const src = sky.texture
|
|
213
|
+
// `asset()` hands back "id:<n>" (already staged); a bare name is resolved on the spot.
|
|
214
|
+
const id = src.startsWith("id:") ? Number(src.slice(3)) : _creatorUtils.fetchLocal(src)
|
|
215
|
+
_creator.setSkyboxTexture?.(this._id, id)
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
_creator.setSkybox(this._id, Color.toPackedRgb(sky))
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Runtime form of `ambientOcclusion` (a graphics-settings menu). `false` turns it off. */
|
|
222
|
+
setAmbientOcclusion(options: boolean | AmbientOcclusionOptions): void {
|
|
223
|
+
const o: AmbientOcclusionOptions = typeof options === "object" ? options : {}
|
|
224
|
+
const quality = { low: 0, medium: 1, high: 2, ultra: 3 }[o.quality ?? "medium"] ?? 1
|
|
225
|
+
_creator.setAmbientOcclusionOptions?.(this._id, options !== false, o.intensity ?? 1, o.radius ?? 0.3, o.power ?? 1, quality)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Runtime form of `fog` (weather, entering a building). `false` turns it off. */
|
|
229
|
+
setFog(options: FogOptions | false): void {
|
|
230
|
+
const o: FogOptions = options === false ? {} : options
|
|
231
|
+
_creator.setFogOptions?.(
|
|
232
|
+
this._id, options !== false, Color.toPackedRgb(o.color ?? "#ffffff"),
|
|
233
|
+
o.start ?? 0, o.density ?? 0.01, o.height ?? 0, o.heightFalloff ?? 0,
|
|
234
|
+
o.maxOpacity ?? 1, o.cutoff ?? 0, o.fromEnvironment ?? false,
|
|
235
|
+
)
|
|
236
|
+
}
|
|
129
237
|
|
|
130
238
|
setMaterialGlobalParameter(i: number, x: number, y: number, z: number, w: number): void {
|
|
131
239
|
_creator.setMaterialGlobalParameter(this._id, i, x, y, z, w)
|
|
@@ -143,6 +251,41 @@ export class Scene implements Presentable {
|
|
|
143
251
|
return this
|
|
144
252
|
}
|
|
145
253
|
|
|
254
|
+
// ---- systems: aspects of the scene (core/Aspect.ts `System`) ----
|
|
255
|
+
/** @internal class → instance, the authoritative store (named accessors mirror this). */
|
|
256
|
+
readonly _systems = new Map<Function, Aspect<any, any, any>>()
|
|
257
|
+
|
|
258
|
+
/** Attach (and configure) a system, or reconfigure it if already present. Returns the scene typed
|
|
259
|
+
* as now-having it (`scene.system(Hud).hud`). A `System<'x', Scene2D>` is rejected here. */
|
|
260
|
+
// `this: Self` (not `Self & Scene`): with the intersection TS infers Self = Scene and a chain
|
|
261
|
+
// `scene.system(A).system(B)` loses A's accessor from the result type.
|
|
262
|
+
system<Self extends TargetOf<A>, A extends Aspect<any, any, any>>(
|
|
263
|
+
this: Self,
|
|
264
|
+
ctor: AspectCtor<A>,
|
|
265
|
+
opts?: Partial<A>,
|
|
266
|
+
): Self & FieldOf<A> {
|
|
267
|
+
return _attachSystem(this as unknown as Scene, ctor, opts) as unknown as Self & FieldOf<A>
|
|
268
|
+
}
|
|
269
|
+
/** Safe access — undefined if the system isn't attached. */
|
|
270
|
+
get<A extends Aspect<any, any, any>>(ctor: AspectCtor<A>): A | undefined {
|
|
271
|
+
return this._systems.get(ctor) as A | undefined
|
|
272
|
+
}
|
|
273
|
+
/** Existence check AND type guard: inside `if (scene.has(Hud))`, `scene.hud` is present. */
|
|
274
|
+
has<A extends Aspect<any, any, any>>(ctor: AspectCtor<A>): this is this & FieldOf<A> {
|
|
275
|
+
return this._systems.has(ctor)
|
|
276
|
+
}
|
|
277
|
+
/** Detach a system (runs its onDetach). Chainable. */
|
|
278
|
+
removeSystem<A extends Aspect<any, any, any>>(ctor: AspectCtor<A>): this {
|
|
279
|
+
_detachSystem(this, ctor)
|
|
280
|
+
return this
|
|
281
|
+
}
|
|
282
|
+
/** Tear the scene down: every system detaches (last-attached first) and the scene closes. Nodes
|
|
283
|
+
* are yours — destroy the ones you own; the native scene object itself is not released. */
|
|
284
|
+
destroy(): void {
|
|
285
|
+
_detachAllSystems(this)
|
|
286
|
+
this.close()
|
|
287
|
+
}
|
|
288
|
+
|
|
146
289
|
createOverlay(options: SceneOptions = {}): Scene {
|
|
147
290
|
return new Scene({ ...options, _parent: this } as any)
|
|
148
291
|
}
|
package/runtime/sdk/gl/Shape.ts
CHANGED
|
@@ -98,6 +98,8 @@ export class Shape extends Aspect<"shape", Node> {
|
|
|
98
98
|
onDetach(): void {
|
|
99
99
|
if (this._bodyId) { _creator.physicsRemoveBody(this._bodyId); this._bodyId = 0 }
|
|
100
100
|
if (this._shapeId) { _creator.physicsDestroyShape(this._shapeId); this._shapeId = 0 }
|
|
101
|
+
this.node._xf = 0
|
|
102
|
+
this.node._xfKind = 0
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
/** Re-configuring (`node.aspect(Shape, { origin })`) rebuilds the geometry when a dimension, the
|
|
@@ -206,14 +208,20 @@ export class Shape extends Aspect<"shape", Node> {
|
|
|
206
208
|
|
|
207
209
|
// ---- body/shape lifetime --------------------------------------------------------------------
|
|
208
210
|
|
|
209
|
-
/** @internal Physics/Trigger take over: drop the pick body and hand back
|
|
211
|
+
/** @internal Physics/Trigger/CharacterController/Vehicle take over: drop the pick body and hand back
|
|
212
|
+
* the shape id. The node's transform owner goes with the body (the new owner sets its own). */
|
|
210
213
|
_claim(): number {
|
|
211
214
|
if (this._bodyId) { _creator.physicsRemoveBody(this._bodyId); this._bodyId = 0 }
|
|
215
|
+
this.node._xf = 0
|
|
216
|
+
this.node._xfKind = 0
|
|
212
217
|
return this._shapeId
|
|
213
218
|
}
|
|
214
|
-
/** @internal Physics/Trigger report the body they created, so Shape owns its lifetime + pickability.
|
|
219
|
+
/** @internal Physics/Trigger report the body they created, so Shape owns its lifetime + pickability.
|
|
220
|
+
* 0 = they are detaching and no longer own it. */
|
|
215
221
|
_ownBody(bodyId: number): void {
|
|
216
222
|
this._bodyId = bodyId
|
|
223
|
+
this.node._xf = bodyId // route node.position writes to the body (see Node._setOwnedPosition)
|
|
224
|
+
this.node._xfKind = bodyId ? 2 : 0
|
|
217
225
|
if (!this.raycast && bodyId) _creator.physicsSetPickable(bodyId, false)
|
|
218
226
|
}
|
|
219
227
|
/** @internal restore a pick-only body when Physics/Trigger detach but the Shape remains. */
|
|
@@ -223,6 +231,10 @@ export class Shape extends Aspect<"shape", Node> {
|
|
|
223
231
|
|
|
224
232
|
private _createPickBody(): void {
|
|
225
233
|
this._bodyId = _creator.physicsCreateBody(this.node.id, this._shapeId, 0, 0, false, /*pickOnly*/ true)
|
|
234
|
+
// Route position writes here too: the pick body is a static snapshot, so without this a moved node
|
|
235
|
+
// would keep taking its clicks at the old place.
|
|
236
|
+
this.node._xf = this._bodyId
|
|
237
|
+
this.node._xfKind = this._bodyId ? 2 : 0
|
|
226
238
|
if (!this.raycast && this._bodyId) _creator.physicsSetPickable(this._bodyId, false)
|
|
227
239
|
}
|
|
228
240
|
|
|
@@ -235,18 +247,45 @@ export class Shape extends Aspect<"shape", Node> {
|
|
|
235
247
|
private _rebuild(): void {
|
|
236
248
|
if (!this._shapeId) return // never attached, or a build without physics
|
|
237
249
|
const previous = this._shapeId
|
|
250
|
+
const previousSig = this._sig
|
|
238
251
|
const id = this._build()
|
|
239
252
|
if (!id) return // build failed — keep what works
|
|
253
|
+
|
|
254
|
+
// A CharacterController owns its shape and can REFUSE the swap: standing up under a pipe doesn't
|
|
255
|
+
// fit. Nothing is half-applied on refusal, so the fix is to stay UNCOMMITTED — leave `_sig` on the
|
|
256
|
+
// old geometry, and the next `node.aspect(Shape, …)` with the same dimensions sees a difference
|
|
257
|
+
// and tries again. That retry is the headroom test, and it is exact (the real capsule, not a ray).
|
|
258
|
+
const charId = this.node._xfKind === 1 ? this.node._xf : 0
|
|
259
|
+
if (charId) {
|
|
260
|
+
if (_creator.characterSetShape?.(charId, id)) {
|
|
261
|
+
this._shapeId = id
|
|
262
|
+
this._resizeRefused = false
|
|
263
|
+
_creator.physicsDestroyShape(previous)
|
|
264
|
+
} else {
|
|
265
|
+
_creator.physicsDestroyShape(id) // drop what we just built; the old collider stays live
|
|
266
|
+
this._sig = previousSig
|
|
267
|
+
this._resizeRefused = !!_creator.characterSetShape
|
|
268
|
+
if (!_creator.characterSetShape) {
|
|
269
|
+
console.warn(`Shape: this host can't resize a live CharacterController — "${this.node.name}" keeps its current collider`)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
|
|
240
275
|
this._shapeId = id
|
|
241
276
|
if (this._bodyId) {
|
|
242
277
|
if (_creator.physicsSetBodyShape) _creator.physicsSetBodyShape(this._bodyId, id, true)
|
|
243
278
|
else console.warn("Shape: this host can't re-shape a live body — the new dimensions apply from the next attach")
|
|
244
279
|
} else {
|
|
245
|
-
console.warn(`Shape: "${this.node.name}"'s geometry is owned by a
|
|
280
|
+
console.warn(`Shape: "${this.node.name}"'s geometry is owned by a Vehicle — re-attach it to pick up the new dimensions`)
|
|
246
281
|
}
|
|
247
282
|
_creator.physicsDestroyShape(previous)
|
|
248
283
|
}
|
|
249
284
|
|
|
285
|
+
/** @internal True while a requested resize has been refused — the fields say one thing, the engine
|
|
286
|
+
* is still using another. Surfaced as `controller.resizing`. */
|
|
287
|
+
_resizeRefused = false
|
|
288
|
+
|
|
250
289
|
private _build(): number {
|
|
251
290
|
this._autoOrigin = null
|
|
252
291
|
this._sig = this._signature()
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
// goal.addEventListener('exit', other => …)
|
|
10
10
|
|
|
11
11
|
import { Aspect } from "../core/Aspect"
|
|
12
|
-
import { cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
13
12
|
import { Shape } from "./Shape"
|
|
14
13
|
import { ensurePhysicsEvents } from "./physicsEvents"
|
|
15
14
|
import type { Node } from "./Node"
|
|
@@ -42,9 +41,5 @@ export class Trigger extends Aspect<"trigger", Node> {
|
|
|
42
41
|
}
|
|
43
42
|
}
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
moveTo(p: Vec3Like): this {
|
|
47
|
-
if (this._bodyId) _creator.physicsSetBodyPosition(this._bodyId, cx(p), cy(p), cz(p))
|
|
48
|
-
return this
|
|
49
|
-
}
|
|
44
|
+
// No moveTo(): `node.position = p` moves the node AND the sensor body it owns (see Node._xf).
|
|
50
45
|
}
|