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/Light.ts
CHANGED
|
@@ -67,6 +67,11 @@ export class Light extends Node {
|
|
|
67
67
|
_direction: [number, number, number] = [ 0, -1, 0 ]
|
|
68
68
|
/** The most recently created sun — what `Lightmap.load` uses unless told otherwise. */
|
|
69
69
|
static lastSun: Light | null = null
|
|
70
|
+
/** @internal the colour as created / last set (a settings menu recreates a sun from these). */
|
|
71
|
+
_color: ColorInput = 0xffffff
|
|
72
|
+
/** @internal sun shadow options as created (creation-time in the engine). */
|
|
73
|
+
_shadowsQuality = 1
|
|
74
|
+
_shadowDistance = 100
|
|
70
75
|
|
|
71
76
|
/** A directional sun light. */
|
|
72
77
|
static sun(options: SunOptions = {}): Light {
|
|
@@ -74,6 +79,9 @@ export class Light extends Node {
|
|
|
74
79
|
const [ dx, dy, dz ] = options.direction ?? [ 0.548267, -0.473983, -0.689016 ]
|
|
75
80
|
light._direction = [ dx, dy, dz ]
|
|
76
81
|
light._intensity = options.intensity ?? 100000
|
|
82
|
+
light._color = options.color ?? 0xffffff
|
|
83
|
+
light._shadowsQuality = options.shadowsQuality ?? 1
|
|
84
|
+
light._shadowDistance = options.shadowDistance ?? 100
|
|
77
85
|
Light.lastSun = light
|
|
78
86
|
_creator.createSunLight(
|
|
79
87
|
light.id, dx, dy, dz,
|
|
@@ -95,6 +103,7 @@ export class Light extends Node {
|
|
|
95
103
|
static point(options: PointOptions = {}): Light {
|
|
96
104
|
const light = new Light()
|
|
97
105
|
light._intensity = options.intensity ?? 1000
|
|
106
|
+
light._color = options.color ?? 0xffffff
|
|
98
107
|
light._baked = options.baked ?? true
|
|
99
108
|
Light._points.add(light)
|
|
100
109
|
_creator.createPointLight?.(
|
|
@@ -130,6 +139,13 @@ export class Light extends Node {
|
|
|
130
139
|
}
|
|
131
140
|
|
|
132
141
|
set color(value: ColorInput) {
|
|
142
|
+
this._color = value
|
|
133
143
|
_creator.setLightColor?.(this.id, Color.toPackedRgb(value))
|
|
134
144
|
}
|
|
145
|
+
get color(): ColorInput { return this._color }
|
|
146
|
+
/** Sun direction as created (the engine keeps it; a settings menu rebuilds a sun from it). */
|
|
147
|
+
get direction(): [number, number, number] { return this._direction }
|
|
148
|
+
/** Sun shadow options as created — creation-time in the engine, so a change means a new sun. */
|
|
149
|
+
get shadowsQuality(): number { return this._shadowsQuality }
|
|
150
|
+
get shadowDistance(): number { return this._shadowDistance }
|
|
135
151
|
}
|
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
|
|
|
@@ -241,7 +250,8 @@ export class Lightmap {
|
|
|
241
250
|
if (pageFiles.length < pages) {
|
|
242
251
|
console.warn(`Lightmap: the bake took ${pages} atlas pages but the scene lists ${pageFiles.length} — add the other page files (${(data.textures ?? []).slice(pageFiles.length).join(", ")}) to env.lightmap.texture`)
|
|
243
252
|
}
|
|
244
|
-
|
|
253
|
+
// baked lighting is exempt from Texture.maxSize: a capped atlas blurs every shadow edge
|
|
254
|
+
const textures = await Promise.all(pageFiles.slice(0, pages).map((f) => Texture.load(f, { fullSize: true })))
|
|
245
255
|
// the light atlas: the bake carries the point lights only when the scene passes their atlas
|
|
246
256
|
const lightFiles = list(files.light)
|
|
247
257
|
let lightTextures: Texture[] = []
|
|
@@ -252,7 +262,7 @@ export class Lightmap {
|
|
|
252
262
|
console.warn(`Lightmap: the bake carries ${data.lights?.length ?? 0} baked point light(s) but the scene passes no \`light\` atlas (${data.lightTextures[0]}) — the lamps stay real-time`)
|
|
253
263
|
} else {
|
|
254
264
|
if (lightFiles.length < pages) console.warn(`Lightmap: the light atlas has ${pages} pages but the scene lists ${lightFiles.length}`)
|
|
255
|
-
lightTextures = await Promise.all(lightFiles.slice(0, pages).map((f) => Texture.load(f)))
|
|
265
|
+
lightTextures = await Promise.all(lightFiles.slice(0, pages).map((f) => Texture.load(f, { fullSize: true })))
|
|
256
266
|
lightScale = (data.lightScale ?? 0) * (options.lightBoost ?? 1)
|
|
257
267
|
volumeScale = (data.lightScale ?? 0) * (options.volumeBoost ?? options.lightBoost ?? 1)
|
|
258
268
|
}
|
|
@@ -273,12 +283,29 @@ export class Lightmap {
|
|
|
273
283
|
// the light volume next: it claims every lightmap-material instance, the atlas rects below win back the statics
|
|
274
284
|
const volume = files.volume ? await Lightmap.loadVolume(files.volume, volumeScale, lightGamma, ambientScale, strength, aoStrength) : null
|
|
275
285
|
const rects = new Map<string, { st: number[], page: number }>()
|
|
276
|
-
|
|
277
|
-
|
|
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
|
|
278
296
|
const missing: string[] = []
|
|
279
297
|
for (const e of Lightmap.entries) {
|
|
280
298
|
const r = rects.get(e.key)
|
|
281
|
-
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
|
+
}
|
|
282
309
|
const st = r.st
|
|
283
310
|
const texture = textures[r.page]
|
|
284
311
|
const light = r.page < lightTextures.length ? lightTextures[r.page] : null
|
|
@@ -303,7 +330,8 @@ export class Lightmap {
|
|
|
303
330
|
// the lights the bake carries go dark: the atlas holds them for the statics, the volume for the movers
|
|
304
331
|
let lights = 0
|
|
305
332
|
if (lightTextures.length > 0 && data.lights?.length) lights = Lightmap.switchOffBaked(data.lights)
|
|
306
|
-
|
|
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 }
|
|
307
335
|
if (volume) info.volume = { dims: volume.dims, cell: volume.cell }
|
|
308
336
|
return info
|
|
309
337
|
}
|
package/src/gl/Material.ts
CHANGED
|
@@ -36,8 +36,49 @@ const applyUniform = (id: number, key: string, value: UniformValue): void => {
|
|
|
36
36
|
|
|
37
37
|
export type MaterialColorOptions = { color?: ColorInput, map?: Texture | Canvas | null }
|
|
38
38
|
|
|
39
|
+
/** How a stencil test / write on a material compares and what it writes. `test` runs against the
|
|
40
|
+
* scene's stencil buffer (`Scene.stencil` must be on); the ops say what the buffer gets when the
|
|
41
|
+
* fragment passes / fails the stencil test / fails the depth test (`replace` writes `ref`). */
|
|
42
|
+
export type StencilTest = "always" | "never" | "less" | "lessEqual" | "greater" | "greaterEqual" | "equal" | "notEqual"
|
|
43
|
+
export type StencilOp = "keep" | "zero" | "replace" | "increment" | "decrement" | "invert"
|
|
44
|
+
export type MaterialStencil = {
|
|
45
|
+
/** Write the stencil buffer at all. Default `false`. */
|
|
46
|
+
write?: boolean
|
|
47
|
+
/** The reference value, 0..255. Default 0. */
|
|
48
|
+
ref?: number
|
|
49
|
+
/** Default `"always"`. */
|
|
50
|
+
test?: StencilTest
|
|
51
|
+
onPass?: StencilOp
|
|
52
|
+
onFail?: StencilOp
|
|
53
|
+
onDepthFail?: StencilOp
|
|
54
|
+
readMask?: number
|
|
55
|
+
writeMask?: number
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Render state a material instance can override at run time (Filament keeps the defaults in the
|
|
59
|
+
* shader package; these are per-instance overrides on top). */
|
|
60
|
+
export type MaterialStateOptions = {
|
|
61
|
+
/** Test against the scene's depth buffer. `false` draws over everything already drawn — an
|
|
62
|
+
* editor gizmo, a marker that must never hide behind a wall. Pair it with a high
|
|
63
|
+
* `Mesh.renderPriority` so nothing drawn later covers it. Default `true`. */
|
|
64
|
+
depthTest?: boolean
|
|
65
|
+
/** Write to the depth buffer. Leave it on for something drawn over the scene whose own parts must
|
|
66
|
+
* still occlude each other (a gizmo's cone in front of its shaft). Unset = the shader's default
|
|
67
|
+
* (on for opaque, off for a blended one). */
|
|
68
|
+
depthWrite?: boolean
|
|
69
|
+
/** Draw both faces (no back-face culling): a plane seen from behind, a ribbon, cloth, a flat
|
|
70
|
+
* marker. Default `false` = the shader's culling (back faces dropped). */
|
|
71
|
+
doubleSided?: boolean
|
|
72
|
+
/** A stencil test / write for this material — see `MaterialStencil`. The selection-outline
|
|
73
|
+
* recipe: the object writes `{ write: true, ref: 1, onPass: "replace" }`, and a slightly larger
|
|
74
|
+
* copy of it draws with `{ test: "notEqual", ref: 1 }` + `depthTest: false` in `renderPriority` 7. */
|
|
75
|
+
stencil?: MaterialStencil
|
|
76
|
+
}
|
|
77
|
+
/** @deprecated the name before doubleSided / stencil joined it — the same type */
|
|
78
|
+
export type MaterialDepthOptions = MaterialStateOptions
|
|
79
|
+
|
|
39
80
|
/** `Material.unlit` options. */
|
|
40
|
-
export type UnlitMaterialOptions = MaterialColorOptions & {
|
|
81
|
+
export type UnlitMaterialOptions = MaterialColorOptions & MaterialStateOptions & {
|
|
41
82
|
/** Alpha-blend this material instead of drawing it opaque. Opaque is the default: a blended draw
|
|
42
83
|
* writes no depth, is sorted back-to-front and casts no shadow, which is rarely what a flat
|
|
43
84
|
* colour wants. Turn it on for anything that must show what is behind it — a glass pane, a
|
|
@@ -46,6 +87,27 @@ export type UnlitMaterialOptions = MaterialColorOptions & {
|
|
|
46
87
|
transparent?: boolean
|
|
47
88
|
}
|
|
48
89
|
|
|
90
|
+
/** `Material.decal` options — the projected-decal material a `DecalSet` draws with. */
|
|
91
|
+
export type DecalMaterialOptions = {
|
|
92
|
+
/** The atlas (a `DecalSet`'s `sheet` cuts it into cells). */
|
|
93
|
+
map?: Texture
|
|
94
|
+
/** A tangent-space normal atlas (same cell grid; +X = the image's right, +Y = its top; LINEAR —
|
|
95
|
+
* `Texture.fromPixels(…, { srgb: false })`). Turns the decal into a RELIEF decal: instead of
|
|
96
|
+
* painting a colour it bends the surface's lighting, so a footprint or a dent shows on any
|
|
97
|
+
* surface without a colour of its own. With `map` too, the colour multiplies in (a crater
|
|
98
|
+
* darkens by the map's alpha). The sun term applies in shadow as well (no shadow read). */
|
|
99
|
+
normalMap?: Texture
|
|
100
|
+
/** Relief strength — the normal map's xy scale. Default 1. */
|
|
101
|
+
bump?: number
|
|
102
|
+
/** Soft fraction (0..1) of the box's half depth at both ends. Default 0.3. */
|
|
103
|
+
edge?: number
|
|
104
|
+
/** Cosine of the surface angle past which the decal fades (0.3 ≈ 72°); 0 = project onto
|
|
105
|
+
* anything. Default 0.3. */
|
|
106
|
+
angleFade?: number
|
|
107
|
+
/** HDR boost of the image (0 = none). */
|
|
108
|
+
emissive?: number
|
|
109
|
+
}
|
|
110
|
+
|
|
49
111
|
/** `Material.particles` options — the default point-sprite material for particle systems. */
|
|
50
112
|
export type ParticlesMaterialOptions = {
|
|
51
113
|
/** Sprite texture — a single image or a flipbook sheet of frames. Unset = soft round dot. */
|
|
@@ -92,13 +154,16 @@ export const _sheetColsRows = (sheet: number | [number, number] | undefined): [n
|
|
|
92
154
|
}
|
|
93
155
|
|
|
94
156
|
/** `Material.lit` options — PBR scalars on top of the color/map pair. */
|
|
95
|
-
export type LitMaterialOptions = MaterialColorOptions & {
|
|
157
|
+
export type LitMaterialOptions = MaterialColorOptions & MaterialStateOptions & {
|
|
96
158
|
/** Perceptual roughness, 0 (mirror) … 1 (matte). Unset = the shader's default. */
|
|
97
159
|
roughness?: number
|
|
98
160
|
/** Metallic factor, 0 (dielectric) … 1 (metal). Unset = the shader's default. */
|
|
99
161
|
metallic?: number
|
|
100
162
|
}
|
|
101
163
|
|
|
164
|
+
/** `Material.lightmapShading` tiers — see the setter. */
|
|
165
|
+
export type LightmapShading = "full" | "baked" | "baked-lite"
|
|
166
|
+
|
|
102
167
|
export class Material {
|
|
103
168
|
/** @internal native material-instance handle. */
|
|
104
169
|
_id: number
|
|
@@ -133,6 +198,33 @@ export class Material {
|
|
|
133
198
|
if (this._colorKey) this.uniforms[this._colorKey] = Color.toHexString(c)
|
|
134
199
|
}
|
|
135
200
|
|
|
201
|
+
/** Depth test against the scene (write-only; see `MaterialDepthOptions`). A host that predates
|
|
202
|
+
* the call leaves the material as the shader has it. */
|
|
203
|
+
set depthTest(on: boolean) {
|
|
204
|
+
if (_creator.setMaterialDepthTest) _creator.setMaterialDepthTest(this._id, on)
|
|
205
|
+
else warnOnce("depthTest", "[material] this host has no setMaterialDepthTest — depthTest ignored")
|
|
206
|
+
}
|
|
207
|
+
/** Depth write (write-only; see `MaterialStateOptions`). */
|
|
208
|
+
set depthWrite(on: boolean) {
|
|
209
|
+
if (_creator.setMaterialDepthWrite) _creator.setMaterialDepthWrite(this._id, on)
|
|
210
|
+
else warnOnce("depthWrite", "[material] this host has no setMaterialDepthWrite — depthWrite ignored")
|
|
211
|
+
}
|
|
212
|
+
/** Both faces drawn (write-only; see `MaterialStateOptions`). */
|
|
213
|
+
set doubleSided(on: boolean) {
|
|
214
|
+
// 0 = no culling, 2 = back faces culled (the shader's usual default)
|
|
215
|
+
if (_creator.setMaterialCulling) _creator.setMaterialCulling(this._id, on ? 0 : 2)
|
|
216
|
+
else warnOnce("doubleSided", "[material] this host has no setMaterialCulling — doubleSided ignored")
|
|
217
|
+
}
|
|
218
|
+
/** The stencil test / write (write-only; `null` = back to none). See `MaterialStencil`. */
|
|
219
|
+
set stencil(s: MaterialStencil | null) {
|
|
220
|
+
if (!_creator.setMaterialStencil) { warnOnce("stencil", "[material] this host has no setMaterialStencil — stencil ignored"); return }
|
|
221
|
+
const o = s ?? {}
|
|
222
|
+
const clamp = (v: number | undefined, d: number): number => Math.max(0, Math.min(255, Math.round(v ?? d)))
|
|
223
|
+
_creator.setMaterialStencil(this._id, !!o.write, STENCIL_TEST.indexOf(o.test ?? "always"), clamp(o.ref, 0),
|
|
224
|
+
STENCIL_OP.indexOf(o.onPass ?? "keep"), STENCIL_OP.indexOf(o.onFail ?? "keep"), STENCIL_OP.indexOf(o.onDepthFail ?? "keep"),
|
|
225
|
+
clamp(o.readMask, 255), clamp(o.writeMask, 255))
|
|
226
|
+
}
|
|
227
|
+
|
|
136
228
|
// lit/unlit shade baseColor × baseColorMap (factor×map; engines default both to white), so
|
|
137
229
|
// removing the map means resetting baseColorMap to the white 1×1 — no flag to clear. (Materials
|
|
138
230
|
// used to carry a `useBaseColorMap` bool for shaders that branched instead of multiplying; the
|
|
@@ -158,6 +250,7 @@ export class Material {
|
|
|
158
250
|
if (options.map) m.map = options.map
|
|
159
251
|
if (options.roughness !== undefined) m.uniforms.roughness = options.roughness
|
|
160
252
|
if (options.metallic !== undefined) m.uniforms.metallic = options.metallic
|
|
253
|
+
applyDepth(m, options)
|
|
161
254
|
return m
|
|
162
255
|
}
|
|
163
256
|
|
|
@@ -177,6 +270,7 @@ export class Material {
|
|
|
177
270
|
m._colorKey = "baseColor"
|
|
178
271
|
if (options.color !== undefined) m.color = options.color
|
|
179
272
|
if (options.map) m.map = options.map
|
|
273
|
+
applyDepth(m, options)
|
|
180
274
|
return m
|
|
181
275
|
}
|
|
182
276
|
|
|
@@ -205,6 +299,28 @@ export class Material {
|
|
|
205
299
|
return m
|
|
206
300
|
}
|
|
207
301
|
|
|
302
|
+
/** Projected-decal material (`DecalSet`): samples the atlas where the decal's box meets the opaque
|
|
303
|
+
* scene behind it. Blended, no depth write, no shadows — the engine keeps the scene depth bound
|
|
304
|
+
* while a set with live decals is on screen. */
|
|
305
|
+
static decal(options: DecalMaterialOptions = {}): Material {
|
|
306
|
+
// Both fetchLocal literals inline so the compiler's preload header captures the shader names.
|
|
307
|
+
const m = options.normalMap
|
|
308
|
+
? new Material({ _id: _creatorUtils.fetchLocal("decal-relief.filamat") } as any)
|
|
309
|
+
: new Material({ _id: _creatorUtils.fetchLocal("decal.filamat") } as any)
|
|
310
|
+
m.uniforms.time = 0
|
|
311
|
+
m.uniforms.edge = options.edge ?? 0.3
|
|
312
|
+
m.uniforms.angleFade = options.angleFade ?? 0.3
|
|
313
|
+
if (options.normalMap) {
|
|
314
|
+
m.uniforms.normalMap = options.normalMap
|
|
315
|
+
m.uniforms.bump = options.bump ?? 1
|
|
316
|
+
m.uniforms.useColor = options.map ? 1 : 0
|
|
317
|
+
} else {
|
|
318
|
+
m.uniforms.emissive = options.emissive ?? 0
|
|
319
|
+
}
|
|
320
|
+
if (options.map) m.map = options.map
|
|
321
|
+
return m
|
|
322
|
+
}
|
|
323
|
+
|
|
208
324
|
/** Material that samples a VideoPlayer's texture. */
|
|
209
325
|
static video(map?: Texture): Material {
|
|
210
326
|
const m = new Material({ _id: _creatorUtils.fetchLocal("video.filamat") } as any)
|
|
@@ -218,8 +334,13 @@ export class Material {
|
|
|
218
334
|
* `metallicFactor`) plus `lightmap`, `lightmapST`, `ambientScale`, `sunStrength` (the sun / IBL terms come from
|
|
219
335
|
* filament's per-frame uniforms inside the shader). */
|
|
220
336
|
static lightmap(): Material {
|
|
221
|
-
// Inline
|
|
222
|
-
const
|
|
337
|
+
// Inline literals: the compiler's preload header captures the shader names from them.
|
|
338
|
+
const id = Material.lightmapShading === "baked"
|
|
339
|
+
? _creatorUtils.fetchLocal("lightmap-baked.filamat")
|
|
340
|
+
: Material.lightmapShading === "baked-lite"
|
|
341
|
+
? _creatorUtils.fetchLocal("lightmap-baked-lite.filamat")
|
|
342
|
+
: _creatorUtils.fetchLocal("lightmap.filamat")
|
|
343
|
+
const m = new Material({ _id: id } as any)
|
|
223
344
|
m.set("baseColorFactor", "#ffffffff").set("roughnessFactor", 1).set("metallicFactor", 0)
|
|
224
345
|
m.set("lightmapST", [ 1, 1, 0, 0 ]).set("ambientScale", 1).set("sunStrength", 1).set("hasNormalMap", 0)
|
|
225
346
|
// lightScale 0 = no baked point lights until Lightmap.load binds a light atlas / volume
|
|
@@ -228,6 +349,22 @@ export class Material {
|
|
|
228
349
|
return m
|
|
229
350
|
}
|
|
230
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
|
+
|
|
231
368
|
/** The terrain splat material (docs/terrain-plan.md §1.5): four albedo (+ normal-map) layers blended by
|
|
232
369
|
* a control map on the terrain's own grid, per-layer `tiling` (metres per repeat) / `roughness` /
|
|
233
370
|
* `normalScale` / `triplanar`, lightmap-aware (the same `lightmap` / `lightmapST` block as
|
|
@@ -246,6 +383,26 @@ export class Material {
|
|
|
246
383
|
* the GLB provider instantiates; the template itself never draws). */
|
|
247
384
|
static _lightmapTemplate(): Material { return (Material._lmTemplate ??= Material.lightmap()) }
|
|
248
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
|
|
390
|
+
|
|
391
|
+
static _lightmapShading: LightmapShading = "full"
|
|
392
|
+
/** Which shader lightmapped models take — a graphics-quality tier, engine-wide. `"full"` is filament's
|
|
393
|
+
* lit path over the baked atlas (IBL specular, real-time point lights such as a muzzle flash, sun
|
|
394
|
+
* shadows on dynamic objects). `"baked"` keeps the baked light and an approximated ambient but drops
|
|
395
|
+
* the lit path: ~40 % cheaper per pixel on a fill-bound GPU. `"baked-lite"` is that minus the normal,
|
|
396
|
+
* metallic/roughness and occlusion map reads (the factors stand in, the atlas keeps the baked AO;
|
|
397
|
+
* emissive still glows): +20 % more at 1080p on the same GPU. Read when a lightmapped model LOADS, so set
|
|
398
|
+
* it before the level (a settings menu applies it on the next level load), like `Texture.maxSize`. */
|
|
399
|
+
static get lightmapShading(): LightmapShading { return Material._lightmapShading }
|
|
400
|
+
static set lightmapShading(mode: LightmapShading) {
|
|
401
|
+
if (mode === Material._lightmapShading) return
|
|
402
|
+
Material._lightmapShading = mode
|
|
403
|
+
Material._lmTemplate = undefined // the next load builds a template on the new shader
|
|
404
|
+
Material._lmMaskedTemplate = undefined
|
|
405
|
+
}
|
|
249
406
|
|
|
250
407
|
/** Shadow-catcher material (transparent except where shadows fall). */
|
|
251
408
|
static shadow(color: ColorInput = "#000000aa"): Material {
|
|
@@ -277,3 +434,15 @@ export class Material {
|
|
|
277
434
|
/** @internal */
|
|
278
435
|
static idOf(material: Material): number { return material._id }
|
|
279
436
|
}
|
|
437
|
+
|
|
438
|
+
const applyDepth = (m: Material, o: MaterialStateOptions): void => {
|
|
439
|
+
if (o.depthTest !== undefined) m.depthTest = o.depthTest
|
|
440
|
+
if (o.depthWrite !== undefined) m.depthWrite = o.depthWrite
|
|
441
|
+
if (o.doubleSided !== undefined) m.doubleSided = o.doubleSided
|
|
442
|
+
if (o.stencil !== undefined) m.stencil = o.stencil
|
|
443
|
+
}
|
|
444
|
+
/** the wire order of the stencil enums (creator-gl material.cpp switches on the same numbers) */
|
|
445
|
+
const STENCIL_TEST: StencilTest[] = [ "always", "never", "less", "lessEqual", "greater", "greaterEqual", "equal", "notEqual" ]
|
|
446
|
+
const STENCIL_OP: StencilOp[] = [ "keep", "zero", "replace", "increment", "decrement", "invert" ]
|
|
447
|
+
const warned = new Set<string>()
|
|
448
|
+
const warnOnce = (key: string, msg: string): void => { if (!warned.has(key)) { warned.add(key); console.warn(msg) } }
|
package/src/gl/Mesh.ts
CHANGED
|
@@ -1,83 +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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
override get
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
+
}
|