lecodes-cli 0.20.2 → 2.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.
Files changed (154) hide show
  1. package/README.md +56 -57
  2. package/dist/index.js +11052 -7592
  3. package/package.json +14 -8
  4. package/runtime/materials/decal-relief.mat +12 -1
  5. package/runtime/materials/decal.mat +17 -3
  6. package/runtime/materials/lightmap.mat +117 -138
  7. package/runtime/materials/particles-quad.mat +6 -2
  8. package/runtime/materials/terrain-lightmap.mat +123 -0
  9. package/runtime/materials/terrain.mat +36 -55
  10. package/runtime/sdk-types.json +1 -1
  11. package/runtime/web/VERSION.json +5 -0
  12. package/runtime/web/assets/camera-5XOM8eHl.js +1 -0
  13. package/runtime/web/assets/creator-2d-D9WKYM6h.js +1 -0
  14. package/runtime/web/assets/creator-2d-ukvEmNEp.wasm +0 -0
  15. package/runtime/web/assets/creator-full-BBXzQlhq.js +1 -0
  16. package/runtime/web/assets/creator-full-D2dSDdDj.wasm +0 -0
  17. package/runtime/web/assets/creator-full.js-BYswGsqR.symbols +8124 -0
  18. package/runtime/web/assets/creator-ui-DIc92ion.wasm +0 -0
  19. package/runtime/web/assets/creator-ui-_1Z_AX2M.js +1 -0
  20. package/runtime/web/assets/geolocation-BjTKN9_P.js +1 -0
  21. package/runtime/web/assets/inspect-C7x78PNs.js +2 -0
  22. package/runtime/web/assets/map-CyBxOSV0.js +1 -0
  23. package/runtime/web/assets/map-XzoOFt0E.js +2 -0
  24. package/runtime/web/assets/mapImpl-bxvQ2h1Z.js +823 -0
  25. package/runtime/web/assets/maplibre-gl-worker-B7LXXUkR.js +8 -0
  26. package/runtime/web/assets/materials-uri4FvPE.bin +0 -0
  27. package/runtime/web/assets/materials-world-CQWt8TAm.bin +0 -0
  28. package/runtime/web/assets/qr-scanner-CK_cYoJR.js +1 -0
  29. package/runtime/web/assets/uberarchive-DLH-kBhM.bin +0 -0
  30. package/runtime/web/index.html +36 -0
  31. package/runtime/web/player.js +21 -0
  32. package/runtime/web-page/index.html +41 -0
  33. package/runtime/web-page/page.css +1 -0
  34. package/runtime/web-page/page.js +31 -0
  35. package/src/api.ts +1 -302
  36. package/src/cli/args.ts +37 -0
  37. package/src/cli/command.ts +113 -0
  38. package/src/cli/errors.ts +13 -0
  39. package/src/cli/help.ts +158 -0
  40. package/src/cli/index.ts +7 -0
  41. package/src/cli/output.ts +102 -0
  42. package/src/cli/run.ts +106 -0
  43. package/src/commands/{appAndroid.ts → app/android.ts} +454 -43
  44. package/src/commands/{appDesktop.ts → app/desktop.ts} +64 -33
  45. package/src/commands/{appDesktopMac.ts → app/desktopMac.ts} +34 -27
  46. package/src/commands/{appIcon.ts → app/icon.ts} +11 -8
  47. package/src/commands/{app.ts → app/index.ts} +400 -290
  48. package/src/commands/{appShared.ts → app/shared.ts} +43 -44
  49. package/src/commands/{appTemplatesAndroid.ts → app/templates/android.ts} +72 -39
  50. package/src/commands/{appTemplatesGradlew.ts → app/templates/gradlew.ts} +1 -1
  51. package/src/commands/{appTemplates.ts → app/templates/ios.ts} +2 -2
  52. package/src/commands/assets.ts +23 -21
  53. package/src/commands/clone.ts +48 -51
  54. package/src/commands/compile.ts +76 -89
  55. package/src/commands/create.ts +46 -47
  56. package/src/commands/design/comments.ts +361 -0
  57. package/src/commands/design/context.ts +101 -0
  58. package/src/commands/design/index.ts +391 -0
  59. package/src/commands/design/snapshot.ts +134 -0
  60. package/src/commands/{designTemplates.ts → design/templates.ts} +4 -3
  61. package/src/commands/desktop.ts +94 -82
  62. package/src/commands/dev.ts +260 -182
  63. package/src/commands/diff.ts +39 -51
  64. package/src/commands/index.ts +64 -0
  65. package/src/commands/{init.ts → init/index.ts} +195 -191
  66. package/src/commands/{projectTemplates.ts → init/templates.ts} +64 -21
  67. package/src/commands/install.ts +80 -67
  68. package/src/commands/lightmap.ts +543 -252
  69. package/src/commands/link.ts +87 -94
  70. package/src/commands/login.ts +25 -23
  71. package/src/commands/navmesh.ts +169 -139
  72. package/src/commands/plugin.ts +46 -0
  73. package/src/commands/pn.ts +201 -244
  74. package/src/commands/pull.ts +49 -56
  75. package/src/commands/push.ts +48 -54
  76. package/src/commands/render.ts +176 -111
  77. package/src/commands/scene.ts +63 -55
  78. package/src/commands/serve.ts +58 -0
  79. package/src/commands/{shaders.ts → shaders/index.ts} +61 -42
  80. package/src/commands/{shadersNew.ts → shaders/new.ts} +3 -3
  81. package/src/commands/shared.ts +79 -0
  82. package/src/commands/status.ts +22 -26
  83. package/src/commands/test.ts +94 -63
  84. package/src/commands/thumbs.ts +185 -175
  85. package/src/commands/update/index.ts +226 -0
  86. package/src/commands/{types.ts → update/types.ts} +31 -22
  87. package/src/compile/collect.ts +5 -5
  88. package/src/compile/collectLocal.ts +3 -3
  89. package/src/compile/designCompile.ts +4 -4
  90. package/src/compile/headlessBundle.ts +16 -12
  91. package/src/compile/nativeStack.ts +1 -1
  92. package/src/compile/projectCompile.ts +12 -4
  93. package/src/compile/sceneCompile.ts +14 -26
  94. package/src/compile/screenEntry.ts +123 -127
  95. package/src/compile/shaders.ts +3 -3
  96. package/src/{lecodes-3d-editor.d.ts → declarations/lecodes-3d-editor.d.ts} +4 -0
  97. package/src/{lecodes-assets.d.ts → declarations/lecodes-assets.d.ts} +1 -1
  98. package/src/{lecodes-renderer.d.ts → declarations/lecodes-headless.d.ts} +3 -3
  99. package/src/dev/androidDev.ts +1 -1
  100. package/src/dev/clientTemplates.ts +18 -14
  101. package/src/dev/devServer.ts +78 -41
  102. package/src/dev/iosDev.ts +180 -0
  103. package/src/dev/webRunner.ts +83 -15
  104. package/src/{cmgenTool.ts → hosts/cmgenTool.ts} +2 -2
  105. package/src/{desktopRenderer.ts → hosts/desktopRenderer.ts} +104 -32
  106. package/src/{desktopScript.ts → hosts/desktopScript.ts} +151 -36
  107. package/src/{distRoot.ts → hosts/distRoot.ts} +9 -5
  108. package/src/{matcTool.ts → hosts/matcTool.ts} +2 -2
  109. package/src/{peerInstall.ts → hosts/peerInstall.ts} +7 -7
  110. package/src/{peers.ts → hosts/peers.ts} +2 -2
  111. package/src/{releases.ts → hosts/releases.ts} +1 -1
  112. package/src/index.ts +35 -480
  113. package/src/platform/api.ts +302 -0
  114. package/src/{browserAuth.ts → platform/browserAuth.ts} +1 -1
  115. package/src/{config.ts → platform/config.ts} +1 -1
  116. package/src/{serverDiff.ts → platform/serverDiff.ts} +3 -3
  117. package/src/plugin/contract.ts +438 -0
  118. package/src/plugin/emitKotlin.ts +357 -0
  119. package/src/plugin/emitSdk.ts +259 -0
  120. package/src/plugin/emitSwift.ts +336 -0
  121. package/src/plugin/emitWeb.ts +383 -0
  122. package/src/plugin/generate.ts +157 -0
  123. package/src/plugin/lines.ts +28 -0
  124. package/src/plugin/lower.ts +231 -0
  125. package/src/plugin/manifest.ts +192 -0
  126. package/src/{projectEnv.ts → project/env.ts} +33 -2
  127. package/src/{ignore.ts → project/ignore.ts} +164 -163
  128. package/src/{localFiles.ts → project/localFiles.ts} +1 -1
  129. package/src/{manifest.ts → project/manifest.ts} +1 -1
  130. package/src/{project.ts → project/materialize.ts} +2 -2
  131. package/src/project/paths.ts +20 -0
  132. package/src/{textDiff.ts → project/textDiff.ts} +1 -1
  133. package/src/{types.ts → project/types.ts} +0 -0
  134. package/runtime/materials/lightmap-baked-lite.mat +0 -175
  135. package/runtime/materials/lightmap-baked.mat +0 -176
  136. package/runtime/scene-harness.json +0 -1
  137. package/runtime/web/assets/__vite-browser-external-BIHI7g3E.js +0 -1
  138. package/runtime/web/assets/basis-C64VHDVD.js +0 -1
  139. package/runtime/web/assets/basis_transcoder-VXdx5NbI.wasm +0 -0
  140. package/runtime/web/assets/createViewerLite-Ct_PZdof.js +0 -852
  141. package/runtime/web/assets/draco-BiISTFcR.js +0 -118
  142. package/runtime/web/assets/draco_decoder-DsQ12WqX.wasm +0 -0
  143. package/runtime/web/assets/index-BMt7AnC5.js +0 -2
  144. package/runtime/web/assets/mapViewImpl-B2JcES8l.js +0 -810
  145. package/runtime/web/assets/maplibre-gl-worker-CJfwIrte.js +0 -8
  146. package/runtime/web/assets/worker-Caf-yYEI.js +0 -2
  147. package/runtime/web/embed.html +0 -32
  148. package/runtime/web/embed.js +0 -171
  149. package/src/commands/design.ts +0 -846
  150. package/src/commands/update.ts +0 -211
  151. package/src/util.ts +0 -146
  152. /package/src/{lecodes-design.d.ts → declarations/lecodes-design.d.ts} +0 -0
  153. /package/src/{qrcode-terminal.d.ts → declarations/qrcode-terminal.d.ts} +0 -0
  154. /package/src/{designMeta.ts → project/designMeta.ts} +0 -0
package/package.json CHANGED
@@ -1,23 +1,26 @@
1
1
  {
2
2
  "name": "lecodes-cli",
3
- "version": "0.20.2",
3
+ "version": "2.0.0",
4
4
  "dependencies": {
5
- "@letary/chisel": "^0.10.0",
5
+ "@letary/chisel": "^0.13.0",
6
6
  "jimp": "^1.6.1",
7
- "lecodes-sdk": "0.20.2"
7
+ "lecodes-sdk": "2.0.0"
8
8
  },
9
9
  "devDependencies": {
10
10
  "lecodes-design": "^0.9.0",
11
- "lecodes-renderer": "0.20.2",
11
+ "lecodes-headless": "2.0.0",
12
12
  "qrcode-terminal": "^0.12.0",
13
13
  "lecodes-3d-editor": "^0.7.0",
14
14
  "lecodes-assets": "0.2.1",
15
15
  "lecodes-creator-nav": "0.19.0",
16
- "lecodes-viewer-lite": "0.20.2"
16
+ "lecodes-gl-lite": "2.0.0",
17
+ "lecodes-web-frame": "2.0.0",
18
+ "lecodes-web-player": "2.0.0",
19
+ "lecodes-web-shared": "2.0.0"
17
20
  },
18
21
  "peerDependencies": {
19
22
  "lecodes-design": "^0.9.0",
20
- "lecodes-renderer": "^0.20.2",
23
+ "lecodes-headless": "^2.0.0",
21
24
  "lecodes-3d-editor": "^0.7.0",
22
25
  "lecodes-assets": "^0.2.1"
23
26
  },
@@ -43,7 +46,7 @@
43
46
  "lecodes-design": {
44
47
  "optional": true
45
48
  },
46
- "lecodes-renderer": {
49
+ "lecodes-headless": {
47
50
  "optional": true
48
51
  },
49
52
  "lecodes-3d-editor": {
@@ -57,9 +60,12 @@
57
60
  "icon": "bun scripts/make-icon.ts",
58
61
  "vendor-runtime": "bun scripts/vendor-runtime.ts",
59
62
  "sync-peers": "bun scripts/sync-peer-versions.ts",
60
- "build": "bun scripts/vendor-runtime.ts && bun build src/index.ts --target=node --outfile dist/index.js --external @letary/chisel --external lecodes-renderer --external lecodes-design --external lecodes-3d-editor --external lecodes-assets --banner=\"#!/usr/bin/env node\"",
63
+ "build": "bun scripts/vendor-runtime.ts && bun build src/index.ts --target=node --outfile dist/index.js --external @letary/chisel --external lecodes-headless --external lecodes-design --external lecodes-3d-editor --external lecodes-assets --banner=\"#!/usr/bin/env node\"",
61
64
  "build:bin": "bun scripts/build-bin.ts",
62
65
  "typecheck": "tsc --noEmit",
66
+ "test": "bun test",
67
+ "docs": "bun scripts/gen-docs.ts",
68
+ "docs:check": "bun scripts/gen-docs.ts --check",
63
69
  "dev": "bun run src/index.ts",
64
70
  "prepublishOnly": "bun run sync-peers && bun run build"
65
71
  },
@@ -121,6 +121,12 @@ fragment {
121
121
  dot(variable_inv1.xyz, sw) + variable_inv1.w,
122
122
  dot(variable_inv2.xyz, sw) + variable_inv2.w);
123
123
  if (any(greaterThan(abs(local), vec3(0.5)))) discard;
124
+ // The caps: a trail segment's mitred joints. Each end edge is tilted by `cap` (unit slope)
125
+ // about the corner on the side where it stays put, cutting into the box on the other side —
126
+ // so two neighbouring segments meet on one shared line, no overlap and no wedge of a gap.
127
+ highp float capS = variable_rect.z, capE = variable_rect.w;
128
+ if (local.y < -0.5 + local.x * capS + 0.5 * abs(capS)) discard;
129
+ if (local.y > 0.5 + local.x * capE - 0.5 * abs(capE)) discard;
124
130
 
125
131
  float4 tint = getColor();
126
132
  alpha = tint.a;
@@ -156,7 +162,12 @@ fragment {
156
162
  if (edge > 0.0) alpha *= saturate((0.5 - abs(local.z)) / (0.5 * edge));
157
163
 
158
164
  highp vec2 uv = vec2(local.x + 0.5, 0.5 - local.y);
159
- highp vec2 tuv = mix(variable_rect.xy, variable_rect.zw, uv);
165
+ // The atlas rect, packed two 11-bit values to a float (decals.h) to make room for the caps.
166
+ highp float p0 = floor(variable_rect.x + 0.5), p1 = floor(variable_rect.y + 0.5);
167
+ highp float u0i = floor(p0 / 2048.0), u1i = floor(p1 / 2048.0);
168
+ highp vec2 rect0 = vec2(u0i, p0 - u0i * 2048.0) / 2047.0;
169
+ highp vec2 rect1 = vec2(u1i, p1 - u1i * 2048.0) / 2047.0;
170
+ highp vec2 tuv = mix(rect0, rect1, uv);
160
171
 
161
172
  // The bent normal: the map's xy along the decal's right / up, z along the surface normal.
162
173
  vec3 T = normalize(variable_inv0.xyz);
@@ -11,8 +11,11 @@
11
11
  // Per-vertex inputs (DECAL_VERTEX_FLOATS in decals.h, one interleaved buffer, 8 corners per decal):
12
12
  // POSITION — the box corner in the SET's space (the renderable's model space)
13
13
  // CUSTOM0..2 — rows of the 3x4 inverse frame: unit box ← set space
14
- // CUSTOM3 — atlas rect u0 v0 u1 v1 (v0 = the cell's top, image top = +Y of the box)
15
- // COLOR — tint rgba (a = opacity)
14
+ // CUSTOM3 — pack(u0,v0), pack(u1,v1) (the atlas rect, two 11-bit 0..1 values per float;
15
+ // v0 = the cell's top, image top = +Y of the box), capStart, capEnd (tilt of the
16
+ // bottom / top edge in unit space — a trail segment's mitred joints, 0 = square)
17
+ // COLOR — tint rgba (a = opacity, already × the gradient end at each corner: −Y corners
18
+ // carry alphaStart, +Y ones alphaEnd, so a trail darkens along its length)
16
19
  // CUSTOM4 — born, life, fadeIn, fadeOut (seconds on the `time` clock; life 0 = forever)
17
20
  // Uniforms:
18
21
  // baseColorMap — the atlas
@@ -122,6 +125,12 @@ fragment {
122
125
  dot(variable_inv1.xyz, sw) + variable_inv1.w,
123
126
  dot(variable_inv2.xyz, sw) + variable_inv2.w);
124
127
  if (any(greaterThan(abs(local), vec3(0.5)))) discard;
128
+ // The caps: a trail segment's mitred joints. Each end edge is tilted by `cap` (unit slope)
129
+ // about the corner on the side where it stays put, cutting into the box on the other side —
130
+ // so two neighbouring segments meet on one shared line, no overlap and no wedge of a gap.
131
+ highp float capS = variable_rect.z, capE = variable_rect.w;
132
+ if (local.y < -0.5 + local.x * capS + 0.5 * abs(capS)) discard;
133
+ if (local.y > 0.5 + local.x * capE - 0.5 * abs(capE)) discard;
125
134
 
126
135
  float4 tint = getColor();
127
136
  alpha = tint.a;
@@ -161,7 +170,12 @@ fragment {
161
170
  if (edge > 0.0) alpha *= saturate((0.5 - abs(local.z)) / (0.5 * edge));
162
171
 
163
172
  highp vec2 uv = vec2(local.x + 0.5, 0.5 - local.y);
164
- highp vec2 tuv = mix(variable_rect.xy, variable_rect.zw, uv);
173
+ // The atlas rect, packed two 11-bit values to a float (decals.h) to make room for the caps.
174
+ highp float p0 = floor(variable_rect.x + 0.5), p1 = floor(variable_rect.y + 0.5);
175
+ highp float u0i = floor(p0 / 2048.0), u1i = floor(p1 / 2048.0);
176
+ highp vec2 rect0 = vec2(u0i, p0 - u0i * 2048.0) / 2047.0;
177
+ highp vec2 rect1 = vec2(u1i, p1 - u1i * 2048.0) / 2047.0;
178
+ highp vec2 tuv = mix(rect0, rect1, uv);
165
179
  float4 tex = texture(materialParams_baseColorMap, tuv);
166
180
  alpha *= tex.a;
167
181
  if (alpha <= 0.002) discard;
@@ -1,40 +1,89 @@
1
- // lightmap — a lit PBR material that multiplies a BAKED shadow mask into the result and adds the
2
- // BAKED direct light of the level's point lights (docs/lightmap-plan.md §3). gltfio hands GLB
3
- // primitives this material instead of the ubershader when the SDK loads a model with
4
- // `lightmap: true` (LightmapMaterialProvider in creator-gl/src/glb.cpp), so the base-colour
5
- // parameters use gltfio's own names and the AssetLoader fills them itself. Mesh receivers
6
- // (Mesh.plane) get it from the SDK directly.
1
+ // lightmap — the material of a BAKED static (creator-bake, phase 1). gltfio hands a GLB's primitives this
2
+ // material instead of the ubershader when the SDK loads a model with `lightmap: true` (the provider in
3
+ // creator-gl/src/glb.cpp), so the glTF-side parameters use gltfio's own names and the AssetLoader fills
4
+ // them itself. Mesh receivers get it from the SDK directly.
7
5
  //
8
- // Atlas channels: R = sun visibility, G = ambient occlusion. G goes to ambientOcclusion, which
9
- // filament applies to the IBL term exactly (diffuse + specular AO). R attenuates the sun through
10
- // postLightingColor (multiply) — it scales the WHOLE lit result, so `k` keeps the ambient share
11
- // alive in shadow: k = (ambient·G + sun·NoL·vis) / (ambient·G + sun·NoL), diffuse-exact.
12
- // The real-time sun still lights this surface, so dynamic casters keep their shadow-map shadows.
6
+ // A static takes ALL of its diffuse light from the atlas: `lightmapLight` holds the irradiance E in lux
7
+ // (BC6H, times lightScale) that the bake traced onto the surface — the sun, the sky and the lamps
8
+ // together — and the pixel's diffuse is albedo x E / pi, Filament's own punctual diffuse. Nothing
9
+ // real-time is added on top: the engine keeps the sun and the lamps off these renderables (light channels,
10
+ // lightmapApply) and Filament's IBL — diffuse and specular — is switched off exactly (ambientOcclusion = 0,
11
+ // specularAmbientOcclusion simple). The SPECULAR comes from the bake too: with the level's REFLECTION PROBES
12
+ // bound (phase 4, `lightmapProbe` + the probe map under the aux page: prefiltered radiance traced from points of the
13
+ // level, creator-bake/src/reflect.h) it is the split-sum response to the radiance the probes this texel SEES (baked)
14
+ // hold along the reflection — blended per pixel, parallax-corrected by each probe's room box, at the material's
15
+ // roughness — held down to this texel's own light where the probes see more than the surface does (a specular
16
+ // occlusion from the bake, see the fragment); without them, the baked light as light from all over plus the
17
+ // sky's SH radiance along the reflection through the baked sky visibility. No
18
+ // highlight from the baked direction (fireflies, see the fragment). Plus the glTF emissive. So the picture on
19
+ // a static is the bake, and a wrong picture is a wrong bake or a wrong material, never a shading estimate.
13
20
  //
14
- // The LIGHT atlas (lightmapLight, same rects) holds the point lights' direct irradiance E as
15
- // sRGB(E / lightScale): the bake traced colour × candela × filament's own falloff window × NoL ×
16
- // shadow rays, so the baked lamp matches the real-time one it replaces (the SDK switches the
17
- // real-time light off once the bake applies). It reaches the pixel as emissive = diffuseColor ×
18
- // E / π, exposure-weighted — the diffuse term filament's own punctual path would have produced.
19
- // No specular from baked lights (v1).
21
+ // The aux atlas (`lightmapAux`, RGBA8 linear, the same rects): RGB = share x d (x 0.5 + 0.5) — the
22
+ // directional SHARE of the baked light times its direction d — and A = sky visibility (phase 3,
23
+ // creator-bake/src/direction.h). The
24
+ // bake split every texel's light into an ambient part (uniform over the hemisphere) and one direction, so
25
+ // a normal map's shading normal ns gets the irradiance those two would give it:
26
+ // E(ns) = E x [ (1 - share) (1 + n.ns) / 2 + share max(0, d.ns) / (d.n) ]
27
+ // exact for both parts, and E itself where the normal is flat. Under a grazing light (d.n < 0.1) the bake
28
+ // fades the share out and the slope is capped. `reconstructIrradiance` in direction.h is the same formula;
29
+ // bake-check tests it.
20
30
  //
21
- // Sampler budget: feature level 1 allows 9 per material (16 minus filament's 7 for a lit shader);
22
- // this one uses all 9 — nothing else can be added without dropping one.
31
+ // DEBUG VIEWS (`debugMode`, level-wide through lightmapDebug): the material writes DATA to the pixel
32
+ // and turns the lit path off (black base colour, no reflectance), so what is on screen is the number —
33
+ // look at it under `toneMapping: 'linear'` for a faithful reading.
34
+ // 1 lighting only: a white diffuse material under the camera's exposure 2 albedo (base colour, linear)
35
+ // 3 geometric normal (x 0.5 + 0.5) 4 shading normal (the normal map applied)
36
+ // 5 the directional share (0 = ambient, 1 = one direction) 6 sky visibility
37
+ // 7 the atlas: a texel checker, tinted per instance 8 the baked light's direction (x 0.5 + 0.5)
38
+ // 9 false-colour lux: log10 over [debugParams.y, debugParams.z] 10 relief: E(ns) / E, grey 0.5 = 1 (flat)
39
+ // 13 probe: the radiance the specular reflects (the probe along the reflection at the material's roughness, occluded;
40
+ // or the sky + baked light without a probe), under the camera's exposure like view 1
41
+ // 14 the probe map: grey = the probes' share Wp of the reflection, tinted by the texel's probe pair (magenta = no entry)
42
+ // 15 the sky's path of the reflection alone 16 the probes' path alone, after the occlusion (black = no probe)
43
+ // On desktop both atlases are fetched bicubically (textureBicubic: a cubic B-spline through four bilinear taps) —
44
+ // the texel grid stops showing on the diagonals of a shadow's edge; the bake's gutters (4 texels, chart-aware fill)
45
+ // cover the wider 4x4 footprint. Mobile and iOS take the plain bilinear fetch (matc -TLIGHTMAP_FETCH, materials.json);
46
+ // macOS (`materials-macos`) takes the desktop flags: the same MOBILE shader model as iOS, the desktop's quality.
47
+ //
48
+ // PRECISION: everything in LUX is `highp`, explicitly. The mobile shader model (matc -p mobile: the `materials` and
49
+ // `materials-ios` targets) makes mediump the fragment default, and Filament's Metal path turns mediump into 16-bit
50
+ // half (filamat GLSLPostProcessor: CreateConvertRelaxedToHalfPass) — max 65504. A sunlit texel is 100 000-180 000 lux
51
+ // once the atlas is multiplied back by lightScale, so on macOS / iOS every baked static went infinite (flat white,
52
+ // and bloom smeared it over the frame: the yard, 2026-09-17) while the desktop target (highp by default) was right.
53
+ // The parameters in lux are `precision : high` for the same reason. Ratios and exposed values are O(1) and may stay.
54
+ // The same for ATLAS COORDINATES, TEXEL INDICES, PROBE INDICES and WORLD POSITIONS: a half has 11 mantissa bits, so a
55
+ // uv near 1 steps by 1/2048 (two texels of a 4096 page — the irradiance read as blocks on macOS, 2026-09-17), an
56
+ // integer above 2048 cannot be odd (the texel checker, view 7, went flat), and 65534 / 65535 (the sky / none probe
57
+ // sentinels) are not representable at all. `luv`, the bicubic taps, mapU16 and the probe ids are highp for that.
23
58
  material {
24
59
  name : lightmap,
25
60
  shadingModel : lit,
26
61
  doubleSided : true,
27
- // glTF's UV origin is top-left, like gltfio's ubershader (base.mat.in): no flip — filament's
28
- // default (true) would mirror both the props' base colour and the atlas rects vertically.
62
+ // opaque or masked: matc -TBLENDING (materials.json `variants`) — the masked twin is what the
63
+ // provider hands a glTF MASK material; BLEND materials (glass) are not lightmapped at all
64
+ blending : ${BLENDING},
65
+ // glTF's UV origin is top-left, like gltfio's ubershader (base.mat.in): no flip
29
66
  flipUV : false,
30
67
  requires : [ uv0, uv1 ],
31
- postLightingBlending : multiply,
68
+ // Filament's own specular occlusion, fed ambientOcclusion = 0: the IBL — diffuse AND specular — is off
69
+ // on a baked surface. Occluding the specular through f0 alone (2026-09-16, phase 1) leaked the sky at
70
+ // grazing angles: Schlick's (1 - NoV)^5 term does not care about f0, so a basement floor mirrored the
71
+ // sky IBL whenever the camera looked along it. The sky reflection is the material's own now (skyRadianceSH
72
+ // x skyVis in the fragment); sharp interior reflections need local probes (a later phase).
73
+ specularAmbientOcclusion : simple,
32
74
  parameters : [
33
75
  { type : float4, name : baseColorFactor },
34
76
  { type : sampler2d, name : baseColorMap },
35
77
  { type : float, name : metallicFactor },
36
78
  { type : float, name : roughnessFactor },
37
79
  { type : float3, name : emissiveFactor },
80
+ // KHR_materials_emissive_strength (gltfio sets it, 1 without the extension): an HDR panel is factor 1 x strength 10
81
+ { type : float, name : emissiveStrength },
82
+ // THE LEVEL'S RADIANCE OF EMISSION 1.0, cd / m2 (the bake's emissiveNits, level-wide): over 0 the glow is drawn
83
+ // SCENE-referred - emission x nits through the camera's exposure, the luminance the bake lit the room with - so a
84
+ // panel looks as bright as the light it gives. 0 = screen-referred as before (emission 1.0 = white at any exposure;
85
+ // an imported strength of 10 then blooms over the whole ceiling).
86
+ { type : float, name : emissiveNits, precision : high },
38
87
  // set unconditionally by gltfio's AssetLoader (a missing uniform is a PANIC, not a no-op)
39
88
  { type : float, name : normalScale },
40
89
  { type : float, name : aoStrength },
@@ -43,139 +92,69 @@ material {
43
92
  { type : sampler2d, name : normalMap },
44
93
  { type : float, name : hasNormalMap },
45
94
  // glTF metal-rough (B = metallic, G = roughness), occlusion (R) and emissive maps on UV0 —
46
- // bound by the AssetLoader by name; absent = the shared 1x1 white, which is each map's
47
- // identity under its factor (metallic 0 x 1, roughness x 1, AO 1, emissive 0 x 1)
95
+ // bound by the AssetLoader by name; absent = the shared 1x1 white, each map's identity
48
96
  { type : sampler2d, name : metallicRoughnessMap },
49
97
  { type : sampler2d, name : occlusionMap },
50
98
  { type : sampler2d, name : emissiveMap },
51
- { type : sampler2d, name : lightmap },
52
- // the point lights' baked irradiance, sRGB(E / lightScale) on the same rects; lightScale 0 = none
99
+ // the bake: irradiance (lux / lightScale) and the aux page, on uv1 * lightmapST.xy + lightmapST.zw
53
100
  { type : sampler2d, name : lightmapLight },
54
- { type : float, name : lightScale },
55
- // the light store's curve: texel = sRGB(pow(E / lightScale, 1 / lightGamma)). 0/1 = the linear v2
56
- // store, 2 = the square-root store bakes write since 2026-08-31 (headroom for the 1/d² peaks)
57
- { type : float, name : lightGamma },
58
- // uv1 * st.xy + st.zw = the instance's rectangle of the atlas
59
- { type : float4, name : lightmapST },
60
- // multiplier on the ambient (IBL) share in the shadow math: 1 = filament's own shadow
61
- // darkness, >1 lighter shadows, <1 deeper
62
- { type : float, name : ambientScale },
63
- // 1 = baked sun shadows at full strength, 0 = R ignored (AO only)
64
- { type : float, name : sunStrength },
65
- // how much of the baked ambient occlusion applies: 1 = all of it, 0 = none (an interior lit
66
- // by its ambient probe alone can need less than the geometric truth)
67
- { type : float, name : bakedAo },
68
- // The light VOLUME for dynamic objects (docs/lightmap-plan.md §9): the same channels on a 3D
69
- // grid over the level, sampled at the fragment's world position + probeMin.w along the normal.
70
- // probeInvSize.w = the grid's depth nz (>= 1) selects it over the atlas (creator-gl binds it; an
71
- // atlas rect resets it to 0). probeLight = the point lights' irradiance as an AMBIENT CUBE: six
72
- // face slabs (+X −X +Y −Y +Z −Z) stacked along z, depth nz * 6, blended by the normal's n²
73
- { type : sampler3d, name : probeVolume },
74
- { type : sampler3d, name : probeLight },
75
- { type : float4, name : probeMin },
76
- { type : float4, name : probeInvSize }
101
+ { type : sampler2d, name : lightmapAux },
102
+ { type : float4, name : lightmapST, precision : high },
103
+ // the level's reflection probes (phase 4, creator-bake/src/reflect.h): the octahedral ATLAS of every probe's
104
+ // roughness levels (radiance in lux / lightScale per steradian; level l = perceptual roughness l / (levels - 1)).
105
+ // WHICH probes a pixel reflects is baked: under every aux page sits its PROBE MAP (rows past the square page,
106
+ // read by texelFetch — see probeGather). probeParams = (bound, levels - 1, oct = level 0's tile edge, probes
107
+ // per atlas row); probeGrid.xyz / probeGridN.xyz = the origin / size of the range the map's 16-bit probe records
108
+ // are shares of. probeBall = k + 1 shows probe k alone, unoccluded (the debug mirror balls). Level-wide.
109
+ { type : sampler2d, name : lightmapProbe },
110
+ { type : float4, name : probeParams },
111
+ { type : float4, name : probeGrid, precision : high },
112
+ { type : float4, name : probeGridN, precision : high },
113
+ { type : float, name : probeBall },
114
+ // A SMALL STATIC the atlas left out (the bake's minSize; creator-bake/src/objects.h): no rect — its light was baked
115
+ // where it stands, as an AMBIENT CUBE: objectCube[+x -x +y -y +z -z] = (the irradiance rgb in lux a surface facing
116
+ // that way gets at that side of the object — sun, sky, lamps, bounce — and its sky visibility). objectProbes =
117
+ // (probe a, probe b, a's weight — what the probe map's byte holds for a texel; 65535 none, 65534 sky — and 1 = this
118
+ // mode is on). The atlas samplers stay bound to one of the level's pages: the probes' records sit under its aux image.
119
+ { type : float4[6], name : objectCube, precision : high },
120
+ { type : float4, name : objectProbes, precision : high },
121
+ // A MOVER'S SUN SHADOW on the baked surface: sunDir = TO the sun (w = 1: the aux pages end in the SUN MAP - its
122
+ // levels' rows, level l of edge s = S >> l in s / 4 rows: texel (x, y) = channel x & 3 of pixel ((x >> 2) + (y & 3) s / 4,
123
+ // base(l) + (y >> 2)), the texel's baked share of the sun's disc; lightmap-shading.fs sunMapAt), sunLight = the sun's
124
+ // illuminance in lux, rgb, .w = the sun map's level count (the shader picks the level by the pixel's footprint). Where the real-time
125
+ // shadow map (movers only: the baked statics cast nothing) says "in shadow", the sun's part leaves the baked
126
+ // light - no double shadow: a texel the bake already shaded has no sun to lose.
127
+ { type : float4, name : sunDir },
128
+ { type : float4, name : sunLight, precision : high },
129
+ { type : float, name : lightScale, precision : high },
130
+ { type : float, name : debugMode },
131
+ { type : float4, name : debugParams }
77
132
  ]
78
133
  }
79
134
 
80
135
  fragment {
136
+ #include "lightmap-shading.fs"
137
+
81
138
  void material(inout MaterialInputs material) {
139
+ vec3 n = getWorldGeometricNormalVector();
140
+ vec3 ns = n;
82
141
  if (materialParams.hasNormalMap > 0.5) {
83
142
  vec3 tn = texture(materialParams_normalMap, getUV0()).xyz * 2.0 - 1.0;
84
143
  tn.xy *= materialParams.normalScale;
85
144
  material.normal = tn;
145
+ ns = normalize(getWorldTangentFrame() * tn);
86
146
  }
87
147
  prepareMaterial(material);
88
148
 
89
- material.baseColor = texture(materialParams_baseColorMap, getUV0()) * materialParams.baseColorFactor;
149
+ vec4 base = texture(materialParams_baseColorMap, getUV0()) * materialParams.baseColorFactor;
90
150
  vec2 mr = texture(materialParams_metallicRoughnessMap, getUV0()).bg;
91
- material.metallic = materialParams.metallicFactor * mr.x;
92
- material.roughness = materialParams.roughnessFactor * mr.y;
93
- vec3 glow = texture(materialParams_emissiveMap, getUV0()).rgb * materialParams.emissiveFactor;
94
- float occ = 1.0 + materialParams.aoStrength * (texture(materialParams_occlusionMap, getUV0()).r - 1.0);
95
-
96
- vec3 n = getWorldGeometricNormalVector();
97
- vec3 ns = getWorldNormalVector(); // the shading normal (normal map applied)
98
- vec3 luma = vec3(0.2126, 0.7152, 0.0722);
99
- float lg = materialParams.lightGamma > 0.0 ? materialParams.lightGamma : 1.0;
100
- vec2 lm;
101
- vec3 baked; // the point lights' irradiance E(n) on this pixel, physical units
102
- vec3 l; // the direction that light is taken to arrive from (the specular term below)
103
- if (materialParams.probeInvSize.w > 0.5) {
104
- // a dynamic object: the light volume at this pixel (trilinear, half a cell off the surface)
105
- vec3 p = getUserWorldPosition() + n * materialParams.probeMin.w;
106
- vec3 uvw = (p - materialParams.probeMin.xyz) * materialParams.probeInvSize.xyz;
107
- lm = texture(materialParams_probeVolume, uvw).rg;
108
- // the ambient cube: the face slab of each axis' sign, z clamped to the slab's own cell
109
- // centres so the trilinear fetch never blends into the neighbouring face; decode each face
110
- // (the store is a square root), then blend in linear irradiance by n²
111
- float nz = materialParams.probeInvSize.w;
112
- float wz = clamp(uvw.z, 0.5 / nz, 1.0 - 0.5 / nz);
113
- vec3 fx = vec3(uvw.xy, ((ns.x >= 0.0 ? 0.0 : 1.0) + wz) / 6.0);
114
- vec3 fy = vec3(uvw.xy, ((ns.y >= 0.0 ? 2.0 : 3.0) + wz) / 6.0);
115
- vec3 fz = vec3(uvw.xy, ((ns.z >= 0.0 ? 4.0 : 5.0) + wz) / 6.0);
116
- vec3 ex = pow(texture(materialParams_probeLight, fx).rgb, vec3(lg));
117
- vec3 ey = pow(texture(materialParams_probeLight, fy).rgb, vec3(lg));
118
- vec3 ez = pow(texture(materialParams_probeLight, fz).rgb, vec3(lg));
119
- vec3 w2 = ns * ns;
120
- baked = (w2.x * ex + w2.y * ey + w2.z * ez) * materialParams.lightScale;
121
- // the dominant direction within the normal's octant: each face's energy along its axis
122
- vec3 dirE = vec3(sign(ns.x) * dot(ex, luma), sign(ns.y) * dot(ey, luma), sign(ns.z) * dot(ez, luma));
123
- l = dot(dirE, dirE) > 1e-12 ? normalize(dirE) : ns;
124
- } else {
125
- vec2 luv = getUV1() * materialParams.lightmapST.xy + materialParams.lightmapST.zw;
126
- lm = texture(materialParams_lightmap, luv).rg;
127
- baked = pow(texture(materialParams_lightmapLight, luv).rgb, vec3(lg)) * materialParams.lightScale;
128
- l = ns; // the atlas keeps no direction: a static's lamp light is taken along its normal
129
- }
130
- lm.g = mix(1.0, lm.g, materialParams.bakedAo);
131
- material.ambientOcclusion = lm.g * occ;
132
-
133
- // Baked point lights on the pixel, exposed here (emissive.w = 0 adds it as-is) so the glTF
134
- // emissive can share the slot. Diffuse: albedo × (1 − metallic) × E / π — filament's own punctual
135
- // diffuse. Specular: the same E through a GGX lobe from `l`, so METALS take baked light too — their
136
- // diffuse is zero, and a rough steel table used to render darker than the floor it stands on because
137
- // only the ambient probe reached it. L_out = f_spec · E_⊥ · NoL, and E(n) = E_⊥ · NoL is what the
138
- // store holds. The lobe is kept no tighter than roughness 0.5: the stored E is light gathered from
139
- // a room, not one bulb, and a mirror-sharp lobe would paint it as a highlight over the whole surface.
140
- vec3 diffuseColor = material.baseColor.rgb * (1.0 - material.metallic);
141
- vec3 specBrdf;
142
- {
143
- vec3 v = getWorldViewVector();
144
- vec3 h = normalize(v + l);
145
- float NoL = saturate(dot(ns, l));
146
- float NoV = max(dot(ns, v), 1e-4);
147
- float NoH = saturate(dot(ns, h));
148
- float LoH = saturate(dot(l, h));
149
- float pr = max(material.roughness, 0.5);
150
- float a2 = pr * pr;
151
- a2 *= a2;
152
- float dd = NoH * NoH * (a2 - 1.0) + 1.0;
153
- float D = a2 / (PI * dd * dd);
154
- float V = 0.5 / max(NoL * sqrt(NoV * NoV * (1.0 - a2) + a2) + NoV * sqrt(NoL * NoL * (1.0 - a2) + a2), 1e-4);
155
- vec3 f0 = mix(vec3(0.04), material.baseColor.rgb, material.metallic);
156
- vec3 F = f0 + (1.0 - f0) * pow(1.0 - LoH, 5.0);
157
- specBrdf = D * V * F;
158
- }
159
- material.emissive = vec4(glow + (diffuseColor / PI + specBrdf) * baked * getExposure(), 0.0);
151
+ float metallic = materialParams.metallicFactor * mr.x;
152
+ float roughness = materialParams.roughnessFactor * mr.y;
153
+ // highp, and nits x exposure FIRST (the PRECISION note in the header): the camera's own reading of emission 1.0 is
154
+ // 1.2 x 2^EV100 = 39 000 nits outdoors - times a strength of 10 it is past a half's 65 504 before the exposure
155
+ highp vec3 glow = texture(materialParams_emissiveMap, getUV0()).rgb * (materialParams.emissiveFactor * materialParams.emissiveStrength);
156
+ if (materialParams.emissiveNits > 0.0) glow *= (materialParams.emissiveNits * getExposure());
160
157
 
161
- float vis = mix(1.0, lm.r, materialParams.sunStrength);
162
- // The ambient share, from the SAME per-frame uniforms filament shades this pixel with
163
- // (frameUniforms: the directional light + the IBL's SH, both exposure-premultiplied), so
164
- // the floor is exactly the IBL-only brightness filament's own shadow map would leave.
165
- vec3 sh = frameUniforms.iblSH[0].xyz
166
- + frameUniforms.iblSH[1].xyz * n.y + frameUniforms.iblSH[2].xyz * n.z + frameUniforms.iblSH[3].xyz * n.x
167
- + frameUniforms.iblSH[4].xyz * (n.y * n.x) + frameUniforms.iblSH[5].xyz * (n.y * n.z)
168
- + frameUniforms.iblSH[6].xyz * (3.0 * n.z * n.z - 1.0) + frameUniforms.iblSH[7].xyz * (n.z * n.x)
169
- + frameUniforms.iblSH[8].xyz * (n.x * n.x - n.y * n.y);
170
- // diffuse terms in filament's units: IBL = albedo * E_sh * iblLuminance; sun = albedo * (color * intensity) * NoL / PI
171
- float amb = max(dot(max(sh, 0.0), luma), 0.0) * frameUniforms.iblLuminance * materialParams.ambientScale * lm.g;
172
- float nol = max(dot(n, frameUniforms.lightDirection), 0.0);
173
- float sun = dot(frameUniforms.lightColorIntensity.rgb, luma) * frameUniforms.lightColorIntensity.w * nol / PI;
174
- // the post-lighting multiply also scales the emissive slot the baked lamps ride in, so their
175
- // share (same units: exposed irradiance / π) sits on both sides — a lamp in the sun's shadow
176
- // is not dimmed by it
177
- float lamp = dot(baked, luma) * (getExposure() / PI);
178
- float k = (amb + sun * vis + lamp) / max(amb + sun + lamp, 1e-6);
179
- material.postLightingColor = vec4(k, k, k, 1.0);
158
+ lightmapShade(material, n, ns, base, metallic, roughness, glow);
180
159
  }
181
160
  }
@@ -88,8 +88,12 @@ vertex {
88
88
  float3 x = normalize(cross(y, camZ));
89
89
  offset = x * corner.x * c0.x + y * corner.y * (c0.x + materialParams.stretch * speed * yl);
90
90
  } else {
91
- // ribbon: extrude sideways from the strip tangent; corner.x carries u along the trail
92
- float3 x = cross(aux, camZ);
91
+ // ribbon: extrude sideways across the strip; corner.x carries u along the trail.
92
+ // c0.y (the billboard's rotation, meaningless here) says which kind of aux this is:
93
+ // 0 — the strip TANGENT, crossed with the view so the strip faces the camera
94
+ // 1 — the width axis already resolved on the CPU (an oriented / blade-aligned strip,
95
+ // blended back toward the camera as it goes edge-on; it owes the view nothing)
96
+ float3 x = c0.y > 0.5 ? aux : cross(aux, camZ);
93
97
  float xl = length(x);
94
98
  x = xl > 1e-4 ? x / xl : camX;
95
99
  offset = x * corner.y * c0.x;
@@ -0,0 +1,123 @@
1
+ // terrain-lightmap — THE BAKED TERRAIN: terrain.mat's surface (four splat layers blended by the control map, see its
2
+ // header) shaded like lightmap.mat (its header is the model: the whole diffuse light is the baked irradiance atlas on
3
+ // UV1, no real-time sun, no diffuse IBL; the sun's share comes back out where a mover's real-time shadow falls). The
4
+ // shading is lightmap-shading.fs, the very code lightmap.mat runs — a terrain next to a baked wall cannot drift from it.
5
+ //
6
+ // UV1 is the terrain's global unit square (creator-gl/src/terrain.cpp): ONE atlas rect for the whole terrain, which
7
+ // lightmapApply addresses through `lightmapST` on the terrain's single material instance.
8
+ //
9
+ // WHO USES IT: a scene file's `terrain:` node that is a lightmap static (sdk scene/defineScene.ts) is created with this
10
+ // material instead of terrain.mat, like a model is loaded with lightmap.mat. Without a bound bake it is black, like
11
+ // any lightmapped static.
12
+ //
13
+ // NO REFLECTION PROBES (LIGHTMAP_NO_PROBES): feature level 1 gives a material 9 samplers and the layers take 7 of them,
14
+ // the atlas 2. Ground is rough and mostly under the sky: it reflects the sky's path (SH radiance x the baked sky
15
+ // visibility), which is what lightmap.mat does wherever no probe is seen.
16
+ material {
17
+ name : terrain_lightmap,
18
+ shadingModel : lit,
19
+ // geometric specular AA (Kaplanyan): the ground is a 1024² normal map at ×1.5–2 tiled every 2 m under a
20
+ // 100 000 lux sun — without this every minified texel is a sub-pixel mirror and the sand sparkles
21
+ // (Mountain, 2026-09-23; A/B with normalScale 0 showed nothing but the normal map does it)
22
+ specularAntiAliasing : true,
23
+ blending : ${BLENDING},
24
+ flipUV : false,
25
+ requires : [ uv0, uv1 ],
26
+ specularAmbientOcclusion : simple,
27
+ parameters : [
28
+ // ---- the surface: terrain.mat's
29
+ { type : sampler2d, name : control },
30
+ { type : sampler2d, name : albedo0 },
31
+ { type : sampler2d, name : albedo1 },
32
+ { type : sampler2d, name : albedo2 },
33
+ { type : sampler2d, name : albedo3 },
34
+ { type : sampler2d, name : normals01 },
35
+ { type : sampler2d, name : normals23 },
36
+ { type : float4, name : tiling },
37
+ { type : float4, name : roughness },
38
+ { type : float4, name : normalScale },
39
+ { type : float4, name : triplanar },
40
+ { type : float2, name : gridSize },
41
+ { type : float, name : cellSize },
42
+ { type : float3, name : tint },
43
+ // ---- the bake: lightmap.mat's (the comments are there), without the probe atlas
44
+ { type : sampler2d, name : lightmapLight },
45
+ { type : sampler2d, name : lightmapAux },
46
+ { type : float4, name : lightmapST, precision : high },
47
+ { type : float4, name : probeParams },
48
+ { type : float4, name : probeGrid, precision : high },
49
+ { type : float4, name : probeGridN, precision : high },
50
+ { type : float, name : probeBall },
51
+ { type : float4[6], name : objectCube, precision : high },
52
+ { type : float4, name : objectProbes, precision : high },
53
+ { type : float4, name : sunDir },
54
+ { type : float4, name : sunLight, precision : high },
55
+ { type : float, name : lightScale, precision : high },
56
+ { type : float, name : debugMode },
57
+ { type : float4, name : debugParams }
58
+ ]
59
+ }
60
+
61
+ fragment {
62
+ #define LIGHTMAP_NO_PROBES
63
+ #include "lightmap-shading.fs"
64
+
65
+ vec4 triplanarSample(sampler2D tex, highp vec3 p, vec3 bw, float s) {
66
+ return texture(tex, p.zy / s) * bw.x + texture(tex, p.xz / s) * bw.y + texture(tex, p.xy / s) * bw.z;
67
+ }
68
+
69
+ // `slot` 0 = this layer's normal XY sits in the pack's RG, 1 = in its BA
70
+ void splatLayer(sampler2D alb, sampler2D pack, float slot, float tiling, float nscale, float tri, float w,
71
+ highp vec2 uv, highp vec3 wp, vec3 bw, float rf, inout vec3 albedo, inout vec3 nrm, inout float rough) {
72
+ if (w <= 0.0) return;
73
+ float s = max(tiling, 1e-3);
74
+ if (tri > 0.5) {
75
+ vec4 c = triplanarSample(alb, wp, bw, s);
76
+ albedo += c.rgb * w;
77
+ rough += rf * c.a * w;
78
+ nrm += vec3(0.0, 0.0, 1.0) * w;
79
+ } else {
80
+ highp vec2 luv = uv / s;
81
+ vec4 c = texture(alb, luv);
82
+ albedo += c.rgb * w;
83
+ rough += rf * c.a * w;
84
+ if (nscale > 0.0) {
85
+ vec4 p = texture(pack, luv);
86
+ vec2 xy = (slot > 0.5 ? p.ba : p.rg) * 2.0 - 1.0;
87
+ xy *= nscale;
88
+ float z = sqrt(max(1.0 - dot(xy, xy), 0.0));
89
+ nrm += vec3(xy, z) * w;
90
+ } else {
91
+ nrm += vec3(0.0, 0.0, 1.0) * w;
92
+ }
93
+ }
94
+ }
95
+
96
+ void material(inout MaterialInputs material) {
97
+ // metres and world positions are highp (lightmap.mat's PRECISION note: a half steps by centimetres at 100 m)
98
+ highp vec2 uv = getUV0();
99
+ highp vec2 cuv = (uv / max(materialParams.cellSize, 1e-4) + 0.5) / max(materialParams.gridSize, vec2(1.0));
100
+ vec4 w = texture(materialParams_control, cuv);
101
+ float ws = w.r + w.g + w.b + w.a;
102
+ w = ws > 1e-4 ? w / ws : vec4(1.0, 0.0, 0.0, 0.0);
103
+
104
+ highp vec3 wp = getUserWorldPosition();
105
+ vec3 gn = getWorldGeometricNormalVector();
106
+ vec3 bw = abs(gn);
107
+ bw = bw / max(bw.x + bw.y + bw.z, 1e-4);
108
+
109
+ vec3 albedo = vec3(0.0);
110
+ vec3 nrm = vec3(0.0);
111
+ float rough = 0.0;
112
+ splatLayer(materialParams_albedo0, materialParams_normals01, 0.0, materialParams.tiling.x, materialParams.normalScale.x, materialParams.triplanar.x, w.r, uv, wp, bw, materialParams.roughness.x, albedo, nrm, rough);
113
+ splatLayer(materialParams_albedo1, materialParams_normals01, 1.0, materialParams.tiling.y, materialParams.normalScale.y, materialParams.triplanar.y, w.g, uv, wp, bw, materialParams.roughness.y, albedo, nrm, rough);
114
+ splatLayer(materialParams_albedo2, materialParams_normals23, 0.0, materialParams.tiling.z, materialParams.normalScale.z, materialParams.triplanar.z, w.b, uv, wp, bw, materialParams.roughness.z, albedo, nrm, rough);
115
+ splatLayer(materialParams_albedo3, materialParams_normals23, 1.0, materialParams.tiling.w, materialParams.normalScale.w, materialParams.triplanar.w, w.a, uv, wp, bw, materialParams.roughness.w, albedo, nrm, rough);
116
+ vec3 tn = normalize(nrm);
117
+ material.normal = tn;
118
+ vec3 ns = normalize(getWorldTangentFrame() * tn);
119
+ prepareMaterial(material);
120
+
121
+ lightmapShade(material, gn, ns, vec4(albedo * materialParams.tint, 1.0), 0.0, rough, vec3(0.0));
122
+ }
123
+ }