lecodes-sdk 2.4.9 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/global.d.ts +35 -0
- package/dist/types/core/env.d.ts +25 -0
- package/dist/types/core/particlesConfig.d.ts +196 -0
- package/dist/types/g2/Font2D.d.ts +19 -0
- package/dist/types/g2/Material2D.d.ts +24 -0
- package/dist/types/g2/Node2D.d.ts +6 -5
- package/dist/types/g2/Particles2D.d.ts +169 -0
- package/dist/types/g2/Path2D.d.ts +95 -0
- package/dist/types/g2/Physics2D.d.ts +1 -1
- package/dist/types/g2/Scene2D.d.ts +15 -17
- package/dist/types/g2/Sprite.d.ts +12 -1
- package/dist/types/g2/SpriteSheet.d.ts +4 -1
- package/dist/types/g2/Text2D.d.ts +96 -0
- package/dist/types/g2/Tilemap.d.ts +8 -0
- package/dist/types/g2/defineScene2d.d.ts +69 -11
- package/dist/types/gl/Foliage.d.ts +0 -7
- package/dist/types/gl/Lightmap.d.ts +101 -2
- package/dist/types/gl/Material.d.ts +21 -15
- package/dist/types/gl/Model.d.ts +8 -3
- package/dist/types/gl/Particles.d.ts +3 -135
- package/dist/types/gl/Statics.d.ts +56 -0
- package/dist/types/gl/staticsFormat.d.ts +101 -0
- package/dist/types/inject.d.ts +11 -3
- package/dist/types/runtime/env.d.ts +10 -0
- package/dist/types/runtime/envValues.d.ts +8 -0
- package/dist/types/scene/defineScene.d.ts +17 -12
- package/dist/types/server/env.d.ts +14 -0
- package/dist/types/server/inject.d.ts +1 -0
- package/dist/types/ui/fonts.d.ts +4 -0
- package/dist/types/version.d.ts +1 -1
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/src/bridges/2d.d.ts +65 -5
- package/src/bridges/gl.d.ts +34 -15
- package/src/compile/bakeMacro.ts +3 -0
- package/src/compile/compileProject.ts +25 -4
- package/src/compile/envCompile.ts +74 -0
- package/src/compile/fontMacro.ts +11 -6
- package/src/compile/index.ts +6 -0
- package/src/compile/resourceSlots.ts +1 -0
- package/src/compile/serverSplit.ts +47 -4
- package/src/core/env.ts +47 -0
- package/src/core/particlesConfig.ts +422 -0
- package/src/g2/Font2D.ts +87 -0
- package/src/g2/Material2D.ts +64 -0
- package/src/g2/Node2D.ts +10 -6
- package/src/g2/Particles2D.ts +337 -0
- package/src/g2/Path2D.ts +215 -0
- package/src/g2/Physics2D.ts +1 -1
- package/src/g2/Scene2D.ts +16 -22
- package/src/g2/Sprite.ts +23 -4
- package/src/g2/SpriteSheet.ts +9 -5
- package/src/g2/Text2D.ts +178 -0
- package/src/g2/Tilemap.ts +10 -0
- package/src/g2/defineScene2d.ts +108 -21
- package/src/g2/groups2d.ts +2 -2
- package/src/gl/Foliage.ts +1 -16
- package/src/gl/Lightmap.ts +79 -26
- package/src/gl/Material.ts +51 -21
- package/src/gl/Model.ts +19 -5
- package/src/gl/Particles.ts +22 -361
- package/src/gl/Statics.ts +301 -0
- package/src/gl/nav/NavMesh.ts +4 -1
- package/src/gl/staticsFormat.ts +240 -0
- package/src/inject.ts +18 -3
- package/src/runtime/env.ts +13 -0
- package/src/runtime/envValues.ts +8 -0
- package/src/runtime/fetch.ts +18 -0
- package/src/scene/defineScene.ts +107 -27
- package/src/server/env.ts +25 -0
- package/src/server/host.ts +6 -0
- package/src/server/inject.ts +2 -1
- package/src/ui/fonts.ts +46 -0
- package/src/version.ts +1 -1
package/src/gl/Lightmap.ts
CHANGED
|
@@ -77,7 +77,7 @@ export type LightmapOptions = {
|
|
|
77
77
|
* what is under it by the solid angle it fills, with no picture. Dense canvas: `{ through: 0.08, diffuse: 0.25 }`. */
|
|
78
78
|
transmit?: Record<string, LightmapTransmit>
|
|
79
79
|
/** FOR THE BAKE: CUBES BY NAME — regular expressions (ECMAScript, case-sensitive) searched in every record's key
|
|
80
|
-
* (`<node key>@<ordinal>`: `"SM_pipe"` takes every pipe node, `"@1$"` the second
|
|
80
|
+
* (`<node key>@<ordinal>`: `"SM_pipe"` takes every pipe node, `"@1$"` the second node primitive of each): a record that matches
|
|
81
81
|
* leaves the atlas like a small or thin one — an occluder lit by ambient cubes baked where it stands — whatever its size
|
|
82
82
|
* or charts. Added to the CLI's `--cube` patterns (both apply). */
|
|
83
83
|
cube?: string[]
|
|
@@ -121,6 +121,9 @@ export type LightmapFiles = {
|
|
|
121
121
|
/** THE PACKED LIGHTMAP UV1 (`<stem>-uv1.bin`): the bake packs every mesh's lightmap charts into a square of their own
|
|
122
122
|
* and its rects map that uv1 — the engine swaps it into the models' TEXCOORD_1 before the rects go on. */
|
|
123
123
|
uv1?: string
|
|
124
|
+
/** THE STATIC LEVEL CACHE (`<stem>.static`, gl/Statics.ts): the snapshot `lecodes static` wrote beside the bake; the macro
|
|
125
|
+
* stages it when it exists, and `load()` rebuilds the level's statics from it when its digest still matches. */
|
|
126
|
+
statics?: string
|
|
124
127
|
}
|
|
125
128
|
|
|
126
129
|
/** What `bake('<path>.bake', options)` compiles to — the files of the bake and the level's options in one literal:
|
|
@@ -142,6 +145,7 @@ export type LightmapDebugParams = {
|
|
|
142
145
|
|
|
143
146
|
/** What `Lightmap.probe` answers: the baked texel under a screen point. */
|
|
144
147
|
export type LightmapProbe = {
|
|
148
|
+
/** the bake's record (`<key>@<ordinal>`) whose rect holds the texel; "" when none does */
|
|
145
149
|
key: string
|
|
146
150
|
entity: number
|
|
147
151
|
node: string
|
|
@@ -239,9 +243,9 @@ type BakeConfig = {
|
|
|
239
243
|
* from `from` to `to` along it) — one for a small one, round(length / objectSlab) for a long one — and per slab per side
|
|
240
244
|
* of its box (+x -x +y -y +z -z) the irradiance rgb in lux + the sky visibility; probes = a, b, a's weight (-1 = none,
|
|
241
245
|
* -2 = the sky) */
|
|
242
|
-
type BakeObjectLight = { at: number[], axis: number, from: number, to: number, cubes: number[][][], probes: number[] }
|
|
246
|
+
export type BakeObjectLight = { at: number[], axis: number, from: number, to: number, cubes: number[][][], probes: number[] }
|
|
243
247
|
/** a RECORD of the .bake: a registered node's key, or `<key>@<ordinal>` for one mesh of a model (creator-bake atlas.h) */
|
|
244
|
-
type BakeInstance = { key: string, receiver: boolean, small?: boolean, light?: BakeObjectLight, page?: number, rect?: number[], st?: number[] }
|
|
248
|
+
export type BakeInstance = { key: string, receiver: boolean, small?: boolean, light?: BakeObjectLight, page?: number, rect?: number[], st?: number[] }
|
|
245
249
|
type BakeProbes = {
|
|
246
250
|
file: string, size: number, levels: number, count: number, oct: number, tile: number[], perRow: number, atlas: number[],
|
|
247
251
|
spacing: number, layout: "rooms" | "grid",
|
|
@@ -251,8 +255,8 @@ type BakeProbes = {
|
|
|
251
255
|
/** per probe: its open share of the sphere (0 = a room, over 0.15 = under the sky) */
|
|
252
256
|
sky: number[],
|
|
253
257
|
}
|
|
254
|
-
type BakeData = {
|
|
255
|
-
version: number, size: number, pages: number, texel: number, lightScale: number,
|
|
258
|
+
export type BakeData = {
|
|
259
|
+
version: number, size: number, pages: number, texel: number, lightScale: number, texels?: number,
|
|
256
260
|
light: string[], aux: string[], direct?: string[], indirect?: string[],
|
|
257
261
|
probes?: BakeProbes | null,
|
|
258
262
|
/** the light grid (creator-bake volume.h): its file and lattice */
|
|
@@ -263,6 +267,8 @@ type BakeData = {
|
|
|
263
267
|
instances: BakeInstance[],
|
|
264
268
|
}
|
|
265
269
|
type PageKind = "light" | "direct" | "indirect"
|
|
270
|
+
/** What `Lightmap.prepare` hands `apply`: the bake read and its pages loaded. */
|
|
271
|
+
export type LightmapPrepared = { def: LightmapDef, data: BakeData, n: number, light: Texture[], aux: Texture[], probes: number, probeAtlas?: Texture, t: number[] }
|
|
266
272
|
type Bound = {
|
|
267
273
|
data: BakeData, rects: Map<string, BakeInstance>, byEntity: Map<number, Entry>, files: LightmapFiles, pages: number,
|
|
268
274
|
aux: Texture[], light: Texture[], direct?: Texture[], indirect?: Texture[],
|
|
@@ -434,9 +440,21 @@ export class Lightmap {
|
|
|
434
440
|
* to (a scene file's `env.lightmap`). Resolves to null when nothing was applied (no bake yet, a host without the
|
|
435
441
|
* feature, bake mode). */
|
|
436
442
|
private static scene: Scene | null = null
|
|
437
|
-
|
|
443
|
+
/** `load` in two halves (the static level cache binds records as it clones, so the pages must be in BEFORE the
|
|
444
|
+
* statics build): `prepare` reads the bake, swaps the uv1 in, loads the pages / probes and sets the level-wide
|
|
445
|
+
* options; `apply` binds the rects of the statics the SDK registered, then the light grid. */
|
|
446
|
+
static async prepare(scene: Scene, def: LightmapDef): Promise<LightmapPrepared | null> {
|
|
438
447
|
Lightmap.scene = scene
|
|
439
448
|
const bake = bakeConfig()
|
|
449
|
+
// a def built by hand the old way — `{ data, light: asset('…-light.ktx2'), aux }`: the pages are LISTS and `path` the
|
|
450
|
+
// bake's target (only the bake needs it), both of them bake()'s; without this a string page fails as "not a function"
|
|
451
|
+
// on its .map, a missing path on its .lastIndexOf
|
|
452
|
+
if (!Array.isArray(def.light) || !Array.isArray(def.aux) || (bake && typeof def.path !== "string")) {
|
|
453
|
+
const msg = `${String(def.path ?? def.data)} is not what bake() compiles to — pass \`bake('<path>.bake')\`: it reads every page off the manifest`
|
|
454
|
+
if (bake) console.error(`[lightmap] error: ${msg}`)
|
|
455
|
+
else console.warn(`Lightmap: ${msg}; rendering without the bake`)
|
|
456
|
+
return null
|
|
457
|
+
}
|
|
440
458
|
if (bake) { Lightmap.bake(bake, def); return null }
|
|
441
459
|
if (!_creator.lightmapApply) {
|
|
442
460
|
if (!Lightmap.warned) { Lightmap.warned = true; console.warn("Lightmap: this host has no lightmap support — rendering without the bake") }
|
|
@@ -444,6 +462,7 @@ export class Lightmap {
|
|
|
444
462
|
}
|
|
445
463
|
if (def.data === null) { console.warn(`Lightmap: no bake at ${def.path} yet — run \`lecodes lightmap bake\``); return null }
|
|
446
464
|
let data: BakeData
|
|
465
|
+
const tJson = Date.now()
|
|
447
466
|
try {
|
|
448
467
|
const resp = await fetch(def.data, { useOnce: true })
|
|
449
468
|
if (resp.status >= 400) { console.warn(`Lightmap: no bake at ${def.path} (HTTP ${resp.status}) — run \`lecodes lightmap bake\``); return null }
|
|
@@ -456,7 +475,9 @@ export class Lightmap {
|
|
|
456
475
|
console.warn(`Lightmap: ${def.path} is not a creator-bake file (version ${String(data.version)}) — rebake with \`lecodes lightmap bake\``)
|
|
457
476
|
return null
|
|
458
477
|
}
|
|
478
|
+
const tUv1 = Date.now()
|
|
459
479
|
if (!(await Lightmap.loadUv1(data, def))) return null // the rects map the packed uv1: it goes in first
|
|
480
|
+
const tPages = Date.now()
|
|
460
481
|
// the pages the manifest names, in order (bake() listed them off it); both atlases are DATA and exempt from
|
|
461
482
|
// Texture.maxSize (a capped atlas blurs every shadow edge)
|
|
462
483
|
const n = Math.min(data.pages, def.light.length, def.aux.length)
|
|
@@ -485,6 +506,24 @@ export class Lightmap {
|
|
|
485
506
|
}
|
|
486
507
|
}
|
|
487
508
|
console.log(`[lightmap] bound ${n} page(s) of ${data.size} at ${(data.texel * 100).toFixed(2)} cm (${data.mips ?? 1} level(s), sun map ${data.sunMapLevels ?? (data.sunMap ? 1 : 0)}), lightScale ${lightScale}${(def.lightScale ?? 1) !== 1 ? ` (bake ${data.lightScale} x ${def.lightScale})` : ""}${probes > 0 ? `, ${probes} reflection probe(s)` : ""}`)
|
|
509
|
+
return { def, data, n, light, aux, probes, probeAtlas, t: [ tJson, tUv1, tPages, Date.now() ] }
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** The bake's page texture ids, light / aux interleaved per page — what the static level cache binds with. */
|
|
513
|
+
static pageIds(p: LightmapPrepared): Uint32Array {
|
|
514
|
+
const out = new Uint32Array(p.n * 2)
|
|
515
|
+
for (let i = 0; i < p.n; i++) { out[i * 2] = p.light[i]._h.id; out[i * 2 + 1] = p.aux[i]._h.id }
|
|
516
|
+
return out
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** @internal The bake the level is bound to (after `apply`), for the static level cache's snapshot. */
|
|
520
|
+
static _bakeData(): BakeData | null { return Lightmap.bound?.data ?? null }
|
|
521
|
+
|
|
522
|
+
static async apply(p: LightmapPrepared): Promise<LightmapInfo | null> {
|
|
523
|
+
if (!_creator.lightmapApply) return null // prepare checked; the narrowing is per function
|
|
524
|
+
const { def, data, n, light, aux, probes, probeAtlas } = p
|
|
525
|
+
const [ tJson, tUv1, tPages, tPagesDone ] = p.t
|
|
526
|
+
const tApply = Date.now()
|
|
488
527
|
const rects = new Map<string, BakeInstance>()
|
|
489
528
|
for (const inst of data.instances) if (inst.receiver && inst.st && inst.page !== undefined) rects.set(inst.key, inst)
|
|
490
529
|
// the ones the bake left out for their size: occluders there, movers here — not a rect gone missing
|
|
@@ -499,8 +538,9 @@ export class Lightmap {
|
|
|
499
538
|
// a bake from before the slabs (`light.cube`, one cube): not read — its small statics stay unlit until it is rebaked
|
|
500
539
|
const lit = (l?: BakeObjectLight): l is BakeObjectLight => !!l && Array.isArray(l.cubes)
|
|
501
540
|
if (data.instances.some((i) => i.small && i.light && !lit(i.light))) console.warn(`Lightmap: ${def.path} has small statics without \`light.cubes\` (baked before the slabbed object light) — they stay unlit; rebake with \`lecodes lightmap bake\``)
|
|
502
|
-
// THE BAKE'S UNIT IS THE MESH: a model's primitives are records of their own, `<key>@<ordinal>` (ordinal = the
|
|
503
|
-
// index over the file's
|
|
541
|
+
// THE BAKE'S UNIT IS THE NODE'S MESH: a model's node primitives are records of their own, `<key>@<ordinal>` (ordinal = the
|
|
542
|
+
// primitive's index over the file's walked nodes) - a rect, or a small static's light, each; the host binds record k to
|
|
543
|
+
// that node's primitive (and its LOD twins')
|
|
504
544
|
const parts = Lightmap.recordsOf(data.instances)
|
|
505
545
|
let smallLit = 0
|
|
506
546
|
const byEntity = new Map<number, Entry>()
|
|
@@ -547,10 +587,18 @@ export class Lightmap {
|
|
|
547
587
|
if (small.size) console.log(`[lightmap] ${small.size} small, thin or cube-marked record(s) are not in the atlas (the bake's minTexels / thinTexels / cube): ${smallLit} lit by the light baked where they stand`)
|
|
548
588
|
if (missing.length) console.warn(`Lightmap: ${missing.length} static(s) or mesh record(s) have no rect in the bake (${missing.slice(0, 5).join(", ")}${missing.length > 5 ? "…" : ""}) — rebake`)
|
|
549
589
|
Lightmap.bound = { data, rects, byEntity, files: def, pages: n, aux, light, probes, probeAtlas, showing: "light" }
|
|
590
|
+
const tVolume = Date.now()
|
|
550
591
|
await Lightmap.loadVolume(data, def)
|
|
592
|
+
console.log(`[lightmap] load: json ${tUv1 - tJson}, uv1 ${tPages - tUv1}, pages ${tPagesDone - tPages}, apply ${tVolume - tApply} (${applied + smallLit} of ${Lightmap.entries.length} statics, ${data.instances.length} records), volume ${Date.now() - tVolume} ms`)
|
|
551
593
|
return { size: data.size, texel: data.texel, pages: n, applied, total: rects.size, missing }
|
|
552
594
|
}
|
|
553
595
|
|
|
596
|
+
/** Bind a bake to the level: the statics registered so far take their rects, the movers the light grid. */
|
|
597
|
+
static async load(scene: Scene, def: LightmapDef): Promise<LightmapInfo | null> {
|
|
598
|
+
const p = await Lightmap.prepare(scene, def)
|
|
599
|
+
return p ? Lightmap.apply(p) : null
|
|
600
|
+
}
|
|
601
|
+
|
|
554
602
|
/** The bake's `<key>@k` records by their node key: a model's meshes (k = the primitive's ordinal in its file). */
|
|
555
603
|
private static recordsOf(instances: BakeInstance[]): Map<string, Map<number, BakeInstance>> {
|
|
556
604
|
const out = new Map<string, Map<number, BakeInstance>>()
|
|
@@ -636,31 +684,36 @@ export class Lightmap {
|
|
|
636
684
|
/** The view names in `lightmapDebug`'s order — for a knob that cycles them. */
|
|
637
685
|
static get debugModes(): readonly LightmapDebugMode[] { return DEBUG_MODES }
|
|
638
686
|
|
|
639
|
-
/** The baked texel under a screen point (logical px): its
|
|
687
|
+
/** The baked texel under a screen point (logical px): its record key, page and texel column / row,
|
|
640
688
|
* so the value can be looked up in the bake's debug layers (`lecodes lightmap inspect`). null when
|
|
641
689
|
* nothing baked is under the point or the host has no triangle pick. */
|
|
642
690
|
static probe(screenX: number, screenY: number): LightmapProbe | null {
|
|
643
691
|
const b = Lightmap.bound
|
|
644
692
|
if (!b || !_creator.pickTriangle) return null
|
|
645
|
-
|
|
646
|
-
//
|
|
647
|
-
//
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
}
|
|
653
|
-
if (!hit) { console.log(`[probe]
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
if (!hit.uv1) { console.log(`[probe] ${entry.key}: the hit primitive carries no TEXCOORD_1`); return null }
|
|
660
|
-
const st = inst.st
|
|
693
|
+
// ONE PICK OVER THE SCENE (entity 0: the engine skips what is out of the scene or not drawn - a template, a LOD level
|
|
694
|
+
// not shown) and THE RECT FROM THE ENGINE: under the static level cache the statics have no JS node and no registry
|
|
695
|
+
// entry, the host bound their rects itself, so the hit's own material answers (lightmapTexture + lightmapST) and the
|
|
696
|
+
// record is the receiver whose rect on that page holds the point (rects never overlap: atlas.h)
|
|
697
|
+
const hit = _creator.pickTriangle(0, screenX, screenY)
|
|
698
|
+
if (!hit) { console.log(`[probe] nothing under ${screenX.toFixed(0)},${screenY.toFixed(0)}`); return null }
|
|
699
|
+
const where = `node "${hit.nodeName}" at ${hit.point.map((c) => c.toFixed(2)).join(",")}`
|
|
700
|
+
if (!hit.uv1) { console.log(`[probe] ${where}: the hit primitive carries no TEXCOORD_1`); return null }
|
|
701
|
+
if (!hit.lightmapST || !hit.lightmapTexture) { console.log(`[probe] ${where}: draws with no rect of the bake (a mover, or a static the bake left out)`); return null }
|
|
702
|
+
const sets: Texture[][] = [ b.light, b.direct ?? [], b.indirect ?? [] ]
|
|
703
|
+
let page = -1
|
|
704
|
+
for (const set of sets) { page = set.findIndex((t) => t._h.id === hit.lightmapTexture); if (page >= 0) break }
|
|
705
|
+
if (page < 0) { console.log(`[probe] ${where}: its light page (texture ${hit.lightmapTexture}) is not of the bound bake`); return null }
|
|
706
|
+
const st = hit.lightmapST
|
|
661
707
|
const u = hit.uv1[0] * st[0] + st[2], v = hit.uv1[1] * st[1] + st[3]
|
|
708
|
+
let key = ""
|
|
709
|
+
for (const inst of b.data.instances) {
|
|
710
|
+
const r = inst.st
|
|
711
|
+
if (!inst.receiver || inst.page !== page || !r) continue
|
|
712
|
+
if (u >= r[2] && u <= r[2] + r[0] && v >= r[3] && v <= r[3] + r[1]) { key = inst.key; break }
|
|
713
|
+
}
|
|
714
|
+
if (key === "") console.log(`[probe] ${where}: no record of the bake holds page ${page} texel ${Math.floor(u * b.data.size)},${Math.floor(v * b.data.size)} (a rebind?)`)
|
|
662
715
|
return {
|
|
663
|
-
key
|
|
716
|
+
key, entity: hit.entity, node: hit.nodeName, material: hit.material, page,
|
|
664
717
|
texel: [ Math.floor(u * b.data.size), Math.floor(v * b.data.size) ],
|
|
665
718
|
uv1: [ hit.uv1[0], hit.uv1[1] ], world: [ hit.point[0], hit.point[1], hit.point[2] ], distance: hit.distance,
|
|
666
719
|
}
|
package/src/gl/Material.ts
CHANGED
|
@@ -450,24 +450,48 @@ export class Material {
|
|
|
450
450
|
* materials (foliage cards) take it instead of falling back to the ubershader's full lit path. */
|
|
451
451
|
static _lightmapMaskedTemplate(): Material { return (Material._lmMaskedTemplate ??= Material.lightmapMasked()) }
|
|
452
452
|
private static _lmMaskedTemplate: Material | undefined
|
|
453
|
-
/** The FOLIAGE tier (`Model.load(…, { foliage
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
453
|
+
/** The FOLIAGE tier's BARK (`Model.load(…, { foliage })`, see `Foliage`): the opaque material a vegetation model's
|
|
454
|
+
* trunks, limbs and stems take — lit by the sky and the real-time sun, swaying in the wind, bending away from
|
|
455
|
+
* benders, thinning out with distance, with a per-copy tint. glTF parameters like `Material.lightmap()` plus
|
|
456
|
+
* `wind` / `sway` / `fade` / `benders`, which the engine writes itself. */
|
|
457
457
|
static foliage(): Material {
|
|
458
458
|
const m = Material._builtin(_creator.builtinMaterial("foliage"), "foliage")
|
|
459
459
|
Material._glbDefaults(m)
|
|
460
460
|
return m
|
|
461
461
|
}
|
|
462
462
|
|
|
463
|
-
/** `
|
|
464
|
-
*
|
|
465
|
-
|
|
466
|
-
|
|
463
|
+
/** The CARDS of a crown (`Model.load(…, { foliage: 'tree' })`): what a vegetation model's alpha-MASK materials take
|
|
464
|
+
* — leaves and needles lit per vertex on the crown's hull normal by the sky and the real-time sun, with the
|
|
465
|
+
* sun's shadow per pixel. Ground cover takes `Material.grass()` instead. */
|
|
466
|
+
static foliageCards(): Material {
|
|
467
|
+
const m = Material._builtin(_creator.builtinMaterial("foliage-cards"), "foliage-cards")
|
|
467
468
|
Material._glbDefaults(m)
|
|
468
469
|
return m
|
|
469
470
|
}
|
|
470
471
|
|
|
472
|
+
/** GROUND COVER (`Model.load(…, { foliage: 'grass' })`): the cards of grass, ferns, heath — the plant that IS the
|
|
473
|
+
* ground's surface. The same wind as `Material.foliage()`, lit by the terrain it stands on: the ground's baked
|
|
474
|
+
* ambient and sun share under it, the soil's colour at its base. A crown (`'tree'`) reads nothing from the ground. */
|
|
475
|
+
static grass(): Material {
|
|
476
|
+
const m = Material._builtin(_creator.builtinMaterial("grass"), "grass")
|
|
477
|
+
Material._glbDefaults(m)
|
|
478
|
+
return m
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** A tree's far IMPOSTOR (`Model.load(…, { foliage: 'tree' })`): the material of the camera-facing quad
|
|
482
|
+
* lecodes-assets bakes into a tree's GLB as its last LOD level — an atlas of views of the tree (a grid of azimuths ×
|
|
483
|
+
* elevation rows, or a hemi-octahedron; albedo, normal, baked sky visibility) lit per pixel like the crown's cards.
|
|
484
|
+
* The engine picks it for the material whose extras carry `lecodes.impostor` and writes `impostorGrid` /
|
|
485
|
+
* `impostor` from them. */
|
|
486
|
+
static foliageImpostor(): Material {
|
|
487
|
+
const m = Material._builtin(_creator.builtinMaterial("foliage-impostor"), "foliage-impostor")
|
|
488
|
+
Material._glbDefaults(m)
|
|
489
|
+
// the quad shadows itself from its own atlas (the frame nearest the sun); matc has no parameter defaults, and the
|
|
490
|
+
// GLB path (glb.cpp impostorFromExtras) writes the same 1 — 0 turns the term off
|
|
491
|
+
m.set("impostorSelfShadow", 1)
|
|
492
|
+
return m
|
|
493
|
+
}
|
|
494
|
+
|
|
471
495
|
/** the glTF-side identity values every provider-handed material starts from */
|
|
472
496
|
private static _glbDefaults(m: Material): void {
|
|
473
497
|
m.set("baseColorFactor", "#ffffffff").set("roughnessFactor", 1).set("metallicFactor", 0).set("hasNormalMap", 0)
|
|
@@ -485,20 +509,26 @@ export class Material {
|
|
|
485
509
|
return Material._folTemplate
|
|
486
510
|
}
|
|
487
511
|
private static _folTemplate: Material | null | undefined
|
|
488
|
-
/**
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
static
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
512
|
+
/** @internal a crown's cards (foliage-cards.mat), the masked twin handed with the bark */
|
|
513
|
+
static _foliageCardsTemplate(): Material | null {
|
|
514
|
+
if (Material._folCardsTemplate === undefined) Material._folCardsTemplate = Material._glbTemplate(_creator.builtinMaterial("foliage-cards"))
|
|
515
|
+
return Material._folCardsTemplate
|
|
516
|
+
}
|
|
517
|
+
private static _folCardsTemplate: Material | null | undefined
|
|
518
|
+
/** @internal the ground cover's masked twin (grass.mat); null where the host carries no grass material — the
|
|
519
|
+
* crown's cards then (an older host) */
|
|
520
|
+
static _grassTemplate(): Material | null {
|
|
521
|
+
if (Material._grassTpl === undefined) Material._grassTpl = Material._glbTemplate(_creator.builtinMaterial("grass")) ?? Material._foliageCardsTemplate()
|
|
522
|
+
return Material._grassTpl
|
|
523
|
+
}
|
|
524
|
+
private static _grassTpl: Material | null | undefined
|
|
525
|
+
/** @internal a tree's far impostor (foliage-impostor.mat), handed with the bark and the cards; null where the host
|
|
526
|
+
* carries no such material — `Model.load` then arms the pair alone */
|
|
527
|
+
static _foliageImpostorTemplate(): Material | null {
|
|
528
|
+
if (Material._folImpostorTpl === undefined) Material._folImpostorTpl = Material._glbTemplate(_creator.builtinMaterial("foliage-impostor"))
|
|
529
|
+
return Material._folImpostorTpl
|
|
500
530
|
}
|
|
501
|
-
private static
|
|
531
|
+
private static _folImpostorTpl: Material | null | undefined
|
|
502
532
|
private static _glbTemplate(h: Handle | null): Material | null {
|
|
503
533
|
if (!h) return null
|
|
504
534
|
const m = new Material(h)
|
package/src/gl/Model.ts
CHANGED
|
@@ -20,7 +20,7 @@ export interface TrianglePick {
|
|
|
20
20
|
mesh: string
|
|
21
21
|
primitive: number
|
|
22
22
|
material: string
|
|
23
|
-
/** the primitive's index over its file's
|
|
23
|
+
/** the primitive's index over its file's walked nodes' primitives: the lightmap bake's record `<key>@<ordinal>` */
|
|
24
24
|
lightmapOrdinal: number
|
|
25
25
|
/** triangle index within the primitive (index-buffer order), and whether the ray came from behind */
|
|
26
26
|
triangle: number
|
|
@@ -34,6 +34,12 @@ export interface TrianglePick {
|
|
|
34
34
|
vertices: { index: number, bind: [number, number, number], world: [number, number, number], bones: { name: string, weight: number }[] }[]
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/** What a vegetation model is lit as (`Model.load`'s `foliage`): a CROWN or GROUND COVER; false = not foliage. */
|
|
38
|
+
export type FoliageKind = false | "tree" | "grass"
|
|
39
|
+
/** @internal a scene node's `foliage` field as the kind `Model.load` takes (`true` = a crown) */
|
|
40
|
+
export const foliageKindOf = (f: boolean | "tree" | "grass" | { kind?: "tree" | "grass" } | undefined): FoliageKind =>
|
|
41
|
+
(!f ? false : typeof f === "object" ? f.kind ?? "tree" : f === "grass" ? "grass" : "tree")
|
|
42
|
+
|
|
37
43
|
export class Model extends Node {
|
|
38
44
|
/** The model's Animator — always present, its clip table = the GLB's embedded clips. Add to it
|
|
39
45
|
* with `model.anim.addClip(name, clip)` (external clips), `model.anim.addLayer()`; set its fields
|
|
@@ -135,8 +141,11 @@ export class Model extends Node {
|
|
|
135
141
|
* the standard shader, lit real-time (movers). Hosts without lightmap support ignore it. */
|
|
136
142
|
lightmap?: boolean
|
|
137
143
|
/** Vegetation: load through the FOLIAGE tier (see `Foliage`) — wind, touch bending, distance fade,
|
|
138
|
-
* per-copy tint.
|
|
139
|
-
|
|
144
|
+
* per-copy tint. `'tree'` (= `true`): a CROWN — a volume in the air, lit by the sky and the real-time sun
|
|
145
|
+
* through the shadow map. `'grass'`: GROUND COVER — grass, ferns, heath: the plant that is the ground's
|
|
146
|
+
* surface, lit by the terrain's baked light under it and tinted by its soil at the base. Hosts without the
|
|
147
|
+
* tier fall back to the standard shader. */
|
|
148
|
+
foliage?: boolean | "tree" | "grass"
|
|
140
149
|
} = {},
|
|
141
150
|
): Promise<Model> {
|
|
142
151
|
if (typeof source === "string") {
|
|
@@ -152,9 +161,14 @@ export class Model extends Node {
|
|
|
152
161
|
let foliage = false
|
|
153
162
|
if (foliageWanted) {
|
|
154
163
|
const fol = Material._foliageTemplate()
|
|
155
|
-
const folMasked = fol && Material.
|
|
164
|
+
const folMasked = fol && (options.foliage === "grass" ? Material._grassTemplate() : Material._foliageCardsTemplate())
|
|
156
165
|
foliage = !!fol && !!folMasked
|
|
157
|
-
|
|
166
|
+
// a tree also arms its far impostor (the `_LOD<k>` quad lecodes-assets bakes), when the host carries the material
|
|
167
|
+
const impostor = fol && folMasked && options.foliage !== "grass" ? Material._foliageImpostorTemplate() : null
|
|
168
|
+
if (fol && folMasked) {
|
|
169
|
+
if (impostor) _creator.setNextGlbLightmapped!(Material.idOf(fol), Material.idOf(folMasked), Material.idOf(impostor))
|
|
170
|
+
else _creator.setNextGlbLightmapped!(Material.idOf(fol), Material.idOf(folMasked))
|
|
171
|
+
}
|
|
158
172
|
}
|
|
159
173
|
const lightmapped = !foliage && !!options.lightmap && !!_creator.setNextGlbLightmapped
|
|
160
174
|
if (lightmapped) _creator.setNextGlbLightmapped!(Material.idOf(Material._lightmapTemplate()), Material.idOf(Material._lightmapMaskedTemplate()))
|