lecodes-sdk 0.20.2 → 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 +17 -5
- package/dist/types/canvas/Canvas.d.ts +2 -0
- package/dist/types/gl/DecalSet.d.ts +48 -3
- package/dist/types/gl/Geometry.d.ts +12 -0
- package/dist/types/gl/Lightmap.d.ts +9 -0
- package/dist/types/gl/Material.d.ts +4 -0
- package/dist/types/gl/Mesh.d.ts +7 -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 +1 -1
- 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/{AudioSource.d.ts → audio/AudioSource.d.ts} +6 -6
- package/dist/types/gl/{AudioZone.d.ts → audio/AudioZone.d.ts} +4 -4
- package/dist/types/gl/{SceneAudio.d.ts → audio/SceneAudio.d.ts} +1 -1
- package/dist/types/gl/{NavAgent.d.ts → nav/NavAgent.d.ts} +4 -4
- package/dist/types/gl/{NavMesh.d.ts → nav/NavMesh.d.ts} +4 -4
- package/dist/types/gl/{CharacterController.d.ts → physics/CharacterController.d.ts} +5 -5
- package/dist/types/gl/{Physics.d.ts → physics/Physics.d.ts} +6 -5
- package/dist/types/gl/{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/{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 +28 -26
- package/dist/types/runtime/files.d.ts +24 -1
- package/dist/types/scene/defineScene.d.ts +10 -3
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/prompts/README.md +142 -142
- package/src/bridges.d.ts +1599 -1481
- package/src/canvas/Canvas.ts +21 -0
- package/src/compile/__tests__/compile.test.ts +11 -0
- package/src/compile/header.ts +6 -3
- package/src/compile/index.ts +1 -0
- package/src/compile/sceneEditor.ts +42 -1
- package/src/gl/DecalSet.ts +132 -5
- package/src/gl/Geometry.ts +348 -284
- package/src/gl/Lightmap.ts +32 -5
- package/src/gl/Material.ts +21 -0
- package/src/gl/Mesh.ts +120 -102
- 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 +1 -1
- 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/{AudioSource.ts → audio/AudioSource.ts} +7 -7
- package/src/gl/{AudioZone.ts → audio/AudioZone.ts} +75 -75
- package/src/gl/{SceneAudio.ts → audio/SceneAudio.ts} +2 -2
- package/src/gl/{NavAgent.ts → nav/NavAgent.ts} +5 -5
- package/src/gl/{NavMesh.ts → nav/NavMesh.ts} +8 -8
- package/src/gl/{CharacterController.ts → physics/CharacterController.ts} +5 -5
- package/src/gl/{Physics.ts → physics/Physics.ts} +12 -5
- package/src/gl/{Ragdoll.ts → physics/Ragdoll.ts} +8 -6
- package/src/gl/{Shape.ts → physics/Shape.ts} +3 -3
- package/src/gl/{Trigger.ts → physics/Trigger.ts} +45 -45
- package/src/gl/{physicsEvents.ts → physics/physicsEvents.ts} +1 -1
- package/src/gl/{Terrain.ts → terrain/Terrain.ts} +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 -224
- package/src/runtime/files.ts +32 -2
- package/src/scene/defineScene.ts +26 -10
- package/src/scene/level.ts +2 -2
- 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/Lightmap.ts
CHANGED
|
@@ -71,6 +71,13 @@ export type LightmapLoadOptions = {
|
|
|
71
71
|
* and its underside does not — the same shading the atlas gives the statics. Before that the volume
|
|
72
72
|
* was one flat 0.5·E on every face and this knob was the workaround. Kept as a trim. */
|
|
73
73
|
volumeBoost?: number
|
|
74
|
+
/** What an OCCLUDER-only static (a prop without lightmap UVs — traced into the receivers' atlas but
|
|
75
|
+
* carrying no rect of its own) does in real time. `"baked"` (default): its shadow is in the bake, so
|
|
76
|
+
* it stops casting and the shadow pass draws only the movers — on a level of thousands of foliage
|
|
77
|
+
* cards that is a third of the GPU frame. `"realtime"`: it keeps casting, which is the only way its
|
|
78
|
+
* shadow reaches OTHER non-receiver props (a rock under a tree); the honest fix for that is lightmap
|
|
79
|
+
* UVs on the props (`lecodes assets doctor --lightmap-uv`), which makes them receivers. */
|
|
80
|
+
occluderShadows?: "baked" | "realtime"
|
|
74
81
|
}
|
|
75
82
|
|
|
76
83
|
export type LightmapInfo = {
|
|
@@ -87,6 +94,8 @@ export type LightmapInfo = {
|
|
|
87
94
|
applied: number
|
|
88
95
|
total: number
|
|
89
96
|
missing: string[]
|
|
97
|
+
/** Occluder-only statics whose real-time shadow was switched off (`occluderShadows: "baked"`). */
|
|
98
|
+
occluders: number
|
|
90
99
|
}
|
|
91
100
|
|
|
92
101
|
export type LightmapFiles = {
|
|
@@ -98,7 +107,7 @@ export type LightmapFiles = {
|
|
|
98
107
|
volume?: string
|
|
99
108
|
}
|
|
100
109
|
|
|
101
|
-
import type { Terrain } from "./Terrain"
|
|
110
|
+
import type { Terrain } from "./terrain/Terrain"
|
|
102
111
|
|
|
103
112
|
type Entry = { key: string, model?: Model, mesh?: Mesh, terrain?: Terrain }
|
|
104
113
|
|
|
@@ -274,12 +283,29 @@ export class Lightmap {
|
|
|
274
283
|
// the light volume next: it claims every lightmap-material instance, the atlas rects below win back the statics
|
|
275
284
|
const volume = files.volume ? await Lightmap.loadVolume(files.volume, volumeScale, lightGamma, ambientScale, strength, aoStrength) : null
|
|
276
285
|
const rects = new Map<string, { st: number[], page: number }>()
|
|
277
|
-
|
|
278
|
-
|
|
286
|
+
// an OCCLUDER-only static (no lightmap UVs): the bake traced its shadow onto every receiver, so it
|
|
287
|
+
// has nothing left to cast in real time — the shadow pass then draws only the movers. Twig & Twine's
|
|
288
|
+
// 9 441 props (5 M alpha-masked spruce cards) cost the GPU a third of the frame as live casters.
|
|
289
|
+
const occluders = new Set<string>()
|
|
290
|
+
for (const inst of data.instances) {
|
|
291
|
+
if (inst.receiver && inst.st) rects.set(inst.key, { st: inst.st, page: inst.page ?? 0 })
|
|
292
|
+
else if (!inst.receiver) occluders.add(inst.key)
|
|
293
|
+
}
|
|
294
|
+
const bakedOccluders = (options.occluderShadows ?? "baked") === "baked"
|
|
295
|
+
let applied = 0, shadowless = 0
|
|
279
296
|
const missing: string[] = []
|
|
280
297
|
for (const e of Lightmap.entries) {
|
|
281
298
|
const r = rects.get(e.key)
|
|
282
|
-
if (!r || r.page >= textures.length) {
|
|
299
|
+
if (!r || r.page >= textures.length) {
|
|
300
|
+
if (occluders.has(e.key)) {
|
|
301
|
+
if (bakedOccluders) {
|
|
302
|
+
if (e.model) e.model.castShadows = false
|
|
303
|
+
else if (e.mesh) e.mesh.castShadows = false
|
|
304
|
+
shadowless++
|
|
305
|
+
}
|
|
306
|
+
} else missing.push(e.key)
|
|
307
|
+
continue
|
|
308
|
+
}
|
|
283
309
|
const st = r.st
|
|
284
310
|
const texture = textures[r.page]
|
|
285
311
|
const light = r.page < lightTextures.length ? lightTextures[r.page] : null
|
|
@@ -304,7 +330,8 @@ export class Lightmap {
|
|
|
304
330
|
// the lights the bake carries go dark: the atlas holds them for the statics, the volume for the movers
|
|
305
331
|
let lights = 0
|
|
306
332
|
if (lightTextures.length > 0 && data.lights?.length) lights = Lightmap.switchOffBaked(data.lights)
|
|
307
|
-
|
|
333
|
+
if (shadowless > 0) console.log(`[lightmap] ${shadowless} occluder-only static(s) cast no real-time shadow (baked); the shadow pass draws the movers only`)
|
|
334
|
+
const info: LightmapInfo = { size: data.size, texel: data.texel, pages, applied, total: rects.size, missing, lights, occluders: shadowless }
|
|
308
335
|
if (volume) info.volume = { dims: volume.dims, cell: volume.cell }
|
|
309
336
|
return info
|
|
310
337
|
}
|
package/src/gl/Material.ts
CHANGED
|
@@ -349,6 +349,22 @@ export class Material {
|
|
|
349
349
|
return m
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
+
/** `Material.lightmap()`'s masked twin (`blending: masked`, same shader and parameters): what the engine
|
|
353
|
+
* gives a lightmapped model's alpha-MASK materials. The cutoff comes from the glTF material. */
|
|
354
|
+
static lightmapMasked(): Material {
|
|
355
|
+
const id = Material.lightmapShading === "baked"
|
|
356
|
+
? _creatorUtils.fetchLocal("lightmap-baked-masked.filamat")
|
|
357
|
+
: Material.lightmapShading === "baked-lite"
|
|
358
|
+
? _creatorUtils.fetchLocal("lightmap-baked-lite-masked.filamat")
|
|
359
|
+
: _creatorUtils.fetchLocal("lightmap-masked.filamat")
|
|
360
|
+
const m = new Material({ _id: id } as any)
|
|
361
|
+
m.set("baseColorFactor", "#ffffffff").set("roughnessFactor", 1).set("metallicFactor", 0)
|
|
362
|
+
m.set("lightmapST", [ 1, 1, 0, 0 ]).set("ambientScale", 1).set("sunStrength", 1).set("hasNormalMap", 0)
|
|
363
|
+
m.set("bakedAo", 1).set("lightScale", 0)
|
|
364
|
+
m.set("probeMin", [ 0, 0, 0, 0 ]).set("probeInvSize", [ 0, 0, 0, 0 ])
|
|
365
|
+
return m
|
|
366
|
+
}
|
|
367
|
+
|
|
352
368
|
/** The terrain splat material (docs/terrain-plan.md §1.5): four albedo (+ normal-map) layers blended by
|
|
353
369
|
* a control map on the terrain's own grid, per-layer `tiling` (metres per repeat) / `roughness` /
|
|
354
370
|
* `normalScale` / `triplanar`, lightmap-aware (the same `lightmap` / `lightmapST` block as
|
|
@@ -367,6 +383,10 @@ export class Material {
|
|
|
367
383
|
* the GLB provider instantiates; the template itself never draws). */
|
|
368
384
|
static _lightmapTemplate(): Material { return (Material._lmTemplate ??= Material.lightmap()) }
|
|
369
385
|
private static _lmTemplate: Material | undefined
|
|
386
|
+
/** @internal The tier's MASKED twin, handed to the engine next to the template: a model's glTF MASK
|
|
387
|
+
* materials (foliage cards) take it instead of falling back to the ubershader's full lit path. */
|
|
388
|
+
static _lightmapMaskedTemplate(): Material { return (Material._lmMaskedTemplate ??= Material.lightmapMasked()) }
|
|
389
|
+
private static _lmMaskedTemplate: Material | undefined
|
|
370
390
|
|
|
371
391
|
static _lightmapShading: LightmapShading = "full"
|
|
372
392
|
/** Which shader lightmapped models take — a graphics-quality tier, engine-wide. `"full"` is filament's
|
|
@@ -381,6 +401,7 @@ export class Material {
|
|
|
381
401
|
if (mode === Material._lightmapShading) return
|
|
382
402
|
Material._lightmapShading = mode
|
|
383
403
|
Material._lmTemplate = undefined // the next load builds a template on the new shader
|
|
404
|
+
Material._lmMaskedTemplate = undefined
|
|
384
405
|
}
|
|
385
406
|
|
|
386
407
|
/** Shadow-catcher material (transparent except where shadows fall). */
|
package/src/gl/Mesh.ts
CHANGED
|
@@ -1,102 +1,120 @@
|
|
|
1
|
-
// A renderable 3D node from raw geometry — the primitive shapes (box/sphere/cylinder/plane) and
|
|
2
|
-
// Mesh.from for a custom Geometry. A GLB import is a different kind: see Model.load.
|
|
3
|
-
|
|
4
|
-
import { cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
5
|
-
import {
|
|
6
|
-
Geometry,
|
|
7
|
-
type
|
|
8
|
-
type
|
|
9
|
-
type
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
renderPriority
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
this.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
set
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
static
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
static
|
|
87
|
-
return apply(new Mesh(
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
1
|
+
// A renderable 3D node from raw geometry — the primitive shapes (box/sphere/cylinder/capsule/plane) and
|
|
2
|
+
// Mesh.from for a custom Geometry. A GLB import is a different kind: see Model.load.
|
|
3
|
+
|
|
4
|
+
import { cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
5
|
+
import {
|
|
6
|
+
Geometry,
|
|
7
|
+
type CapsuleOptions,
|
|
8
|
+
type CylinderOptions,
|
|
9
|
+
type PlaneOptions,
|
|
10
|
+
type SphereOptions,
|
|
11
|
+
} from "./Geometry"
|
|
12
|
+
import { Material } from "./Material"
|
|
13
|
+
import { Node } from "./Node"
|
|
14
|
+
|
|
15
|
+
/** Common transform/render options every primitive factory accepts. */
|
|
16
|
+
export type MeshOptions = {
|
|
17
|
+
material?: Material
|
|
18
|
+
position?: Vec3Like
|
|
19
|
+
eulerAngles?: Vec3Like
|
|
20
|
+
scale?: Vec3Like | number
|
|
21
|
+
name?: string
|
|
22
|
+
castShadows?: boolean
|
|
23
|
+
receiveShadows?: boolean
|
|
24
|
+
/** Coarse draw order, 0 (first) … 7 (last); default 4. See `Mesh.renderPriority`. */
|
|
25
|
+
renderPriority?: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class Mesh extends Node {
|
|
29
|
+
private _geometry?: Geometry
|
|
30
|
+
|
|
31
|
+
constructor(geometry?: Geometry, material?: Material) {
|
|
32
|
+
super()
|
|
33
|
+
if (geometry) {
|
|
34
|
+
const mat = material ?? Material.unlit()
|
|
35
|
+
this._geometry = geometry
|
|
36
|
+
this._materials = [ mat ]
|
|
37
|
+
_creator.setMesh(this.id, Material.idOf(mat), geometry.vertices, geometry.normals, geometry.indices, geometry.uv, geometry._meshKind, geometry.uv1, geometry.colors)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
override get geometry(): Geometry | undefined { return this._geometry }
|
|
42
|
+
/** Replace the geometry in place — the node, its transform and its material stay, the vertex and
|
|
43
|
+
* index buffers are rebuilt. What an editor overlay or a debug drawer redraws with. */
|
|
44
|
+
setGeometry(geometry: Geometry): this {
|
|
45
|
+
this._geometry = geometry
|
|
46
|
+
_creator.setMesh(this.id, Material.idOf(this._materials![0]!), geometry.vertices, geometry.normals, geometry.indices, geometry.uv, geometry._meshKind, geometry.uv1, geometry.colors)
|
|
47
|
+
// the host rebuilds the renderable for a new geometry (creator-gl createMesh destroys + builds), which
|
|
48
|
+
// resets its priority / culling / shadow flags to the defaults — an overlay redrawn every frame lost its
|
|
49
|
+
// renderPriority 7 after one frame and hid inside the model. What was set on this mesh is set again.
|
|
50
|
+
if (this._renderPriority !== undefined) this.renderPriority = this._renderPriority
|
|
51
|
+
if (this._culling !== undefined) this.culling = this._culling
|
|
52
|
+
if (this._castShadows !== undefined) this.castShadows = this._castShadows
|
|
53
|
+
if (this._receiveShadows !== undefined) this.receiveShadows = this._receiveShadows
|
|
54
|
+
return this
|
|
55
|
+
}
|
|
56
|
+
private _renderPriority?: number
|
|
57
|
+
private _culling?: boolean
|
|
58
|
+
private _castShadows?: boolean
|
|
59
|
+
private _receiveShadows?: boolean
|
|
60
|
+
|
|
61
|
+
/** A Mesh always carries a material (slot 0) — see Node.setMaterial for the slot API. */
|
|
62
|
+
override get material(): Material { return this._materials![0]! }
|
|
63
|
+
override set material(m: Material) { this.setMaterial(m, 0) }
|
|
64
|
+
|
|
65
|
+
set culling(v: boolean) { this._culling = v; _creator.setCulling(this.id, v) }
|
|
66
|
+
/** Coarse draw order within the frame: 0 draws first, 7 last, 4 is the default (Filament's
|
|
67
|
+
* renderable priority; within one priority opaque draws sort front-to-back, blended back-to-front).
|
|
68
|
+
* Something drawn over the scene with `Material.depthTest = false` goes in 7, so nothing drawn
|
|
69
|
+
* after it can cover it. Write-only; a host that predates the call ignores it. */
|
|
70
|
+
set renderPriority(v: number) {
|
|
71
|
+
this._renderPriority = v
|
|
72
|
+
if (_creator.setRenderPriority) _creator.setRenderPriority(this.id, Math.max(0, Math.min(7, Math.round(v))))
|
|
73
|
+
else if (!Mesh._warnedPriority) { Mesh._warnedPriority = true; console.warn("[mesh] this host has no setRenderPriority — renderPriority ignored") }
|
|
74
|
+
}
|
|
75
|
+
private static _warnedPriority = false
|
|
76
|
+
set castShadows(v: boolean) { this._castShadows = v; _creator.setCastShadows(this.id, v) }
|
|
77
|
+
set receiveShadows(v: boolean) { this._receiveShadows = v; _creator.setReceiveShadows(this.id, v) }
|
|
78
|
+
|
|
79
|
+
// --- factories ---
|
|
80
|
+
|
|
81
|
+
static box(options: MeshOptions & { size?: Vec3Like | number } = {}): Mesh {
|
|
82
|
+
// the size goes into the geometry (not a scale after): the lightmap chart is laid out per face area
|
|
83
|
+
return apply(new Mesh(Geometry.box(options.size ?? 1), options.material), options)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
static sphere(options: MeshOptions & SphereOptions = {}): Mesh {
|
|
87
|
+
return apply(new Mesh(Geometry.sphere(options), options.material), options)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
static cylinder(options: MeshOptions & CylinderOptions = {}): Mesh {
|
|
91
|
+
return apply(new Mesh(Geometry.cylinder(options), options.material), options)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A capsule along Y (`radius`, `length` between the cap centres) — a character's or a collider's shape in one mesh. */
|
|
95
|
+
static capsule(options: MeshOptions & CapsuleOptions = {}): Mesh {
|
|
96
|
+
return apply(new Mesh(Geometry.capsule(options), options.material), options)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
static plane(options: MeshOptions & PlaneOptions = {}): Mesh {
|
|
100
|
+
return apply(new Mesh(Geometry.plane(options), options.material), options)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Wrap a custom Geometry. */
|
|
104
|
+
static from(geometry: Geometry, options: MeshOptions = {}): Mesh {
|
|
105
|
+
return apply(new Mesh(geometry, options.material), options)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const sizeToVec = (s: Vec3Like | number): [number, number, number] => (typeof s === "number" ? [ s, s, s ] : [ cx(s), cy(s), cz(s) ])
|
|
110
|
+
|
|
111
|
+
const apply = (mesh: Mesh, o: MeshOptions): Mesh => {
|
|
112
|
+
if (o.position) mesh.position = o.position
|
|
113
|
+
if (o.eulerAngles) mesh.eulerAngles = o.eulerAngles
|
|
114
|
+
if (o.scale !== undefined) mesh.scale = typeof o.scale === "number" ? [ o.scale, o.scale, o.scale ] : o.scale
|
|
115
|
+
if (o.name !== undefined) mesh.name = o.name
|
|
116
|
+
if (o.castShadows !== undefined) mesh.castShadows = o.castShadows
|
|
117
|
+
if (o.receiveShadows !== undefined) mesh.receiveShadows = o.receiveShadows
|
|
118
|
+
if (o.renderPriority !== undefined) mesh.renderPriority = o.renderPriority
|
|
119
|
+
return mesh
|
|
120
|
+
}
|
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,7 +13,7 @@ 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 "./SceneAudio"
|
|
16
|
+
import { SceneAudio } from "./audio/SceneAudio"
|
|
17
17
|
import { attachControls, type ControlsHandle, type ControlsOptions } from "./controls"
|
|
18
18
|
import { Material } from "./Material"
|
|
19
19
|
import { Texture } from "./Texture"
|