@driftengine/core 4.11.0 → 4.12.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 (77) hide show
  1. package/CHANGELOG.json +72 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +4 -2
  4. package/dist/index.js +1 -1
  5. package/dist/render/ambientOcclusionPass.d.ts +3 -2
  6. package/dist/render/ambientOcclusionPass.js +4 -2
  7. package/dist/render/backend/api.d.ts +21 -1
  8. package/dist/render/backend/webgl2/renderer.d.ts +61 -5
  9. package/dist/render/backend/webgl2/renderer.js +153 -21
  10. package/dist/render/backend/webgpu/renderer.d.ts +54 -1
  11. package/dist/render/backend/webgpu/renderer.js +246 -89
  12. package/dist/render/instances.d.ts +13 -1
  13. package/dist/render/instances.js +10 -0
  14. package/dist/render/occlusionFade.d.ts +21 -0
  15. package/dist/render/occlusionFade.js +27 -0
  16. package/dist/render/sceneTarget.d.ts +2 -0
  17. package/dist/render/sceneTarget.js +1 -1
  18. package/dist/render/shaders/ambientOcclusion.js +12 -1
  19. package/dist/render/shaders/flat/areaLight.d.ts +1 -1
  20. package/dist/render/shaders/flat/areaLight.js +1 -1
  21. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  22. package/dist/render/shaders/flat/glassTint.js +1 -0
  23. package/dist/render/shaders/flat/index.d.ts +5 -0
  24. package/dist/render/shaders/flat/index.js +10 -3
  25. package/dist/render/shaders/flat/layerLooks.d.ts +16 -0
  26. package/dist/render/shaders/flat/layerLooks.js +116 -0
  27. package/dist/render/shaders/flat/layered.d.ts +19 -9
  28. package/dist/render/shaders/flat/layered.js +169 -37
  29. package/dist/render/shaders/flat/main.d.ts +1 -1
  30. package/dist/render/shaders/flat/main.js +17 -5
  31. package/dist/render/shaders/flat/models.js +1 -1
  32. package/dist/render/shaders/flat/normalMap.d.ts +1 -1
  33. package/dist/render/shaders/flat/normalMap.js +11 -2
  34. package/dist/render/shaders/flat/preamble.js +6 -0
  35. package/dist/render/shaders/generated/ambientOcclusion.wgsl.d.ts +6 -1
  36. package/dist/render/shaders/generated/ambientOcclusion.wgsl.js +6 -1
  37. package/dist/render/shaders/generated/flat.wgsl.d.ts +176 -48
  38. package/dist/render/shaders/generated/flat.wgsl.js +215 -87
  39. package/dist/render/surfaceLayerLooks.d.ts +80 -0
  40. package/dist/render/surfaceLayerLooks.js +83 -0
  41. package/dist/render/surfaceLayers.d.ts +128 -19
  42. package/dist/render/surfaceLayers.js +73 -13
  43. package/dist/render/surfaceTexture.d.ts +13 -4
  44. package/dist/render/transmission.d.ts +21 -0
  45. package/dist/render/transmission.js +21 -0
  46. package/dist/ui/splash.d.ts +42 -2
  47. package/dist/ui/splash.js +78 -13
  48. package/package.json +6 -6
  49. package/scripts/browser.d.mts +5 -0
  50. package/scripts/browser.mjs +68 -14
  51. package/scripts/look.d.mts +81 -0
  52. package/scripts/look.mjs +380 -0
  53. package/src/index.ts +4 -0
  54. package/src/render/ambientOcclusionPass.ts +4 -1
  55. package/src/render/backend/api.ts +31 -0
  56. package/src/render/backend/webgl2/renderer.ts +170 -22
  57. package/src/render/backend/webgpu/renderer.ts +379 -139
  58. package/src/render/instances.ts +18 -1
  59. package/src/render/occlusionFade.ts +29 -0
  60. package/src/render/sceneTarget.ts +3 -0
  61. package/src/render/shaders/ambientOcclusion.ts +12 -1
  62. package/src/render/shaders/flat/areaLight.ts +1 -1
  63. package/src/render/shaders/flat/glassTint.ts +3 -1
  64. package/src/render/shaders/flat/index.ts +15 -3
  65. package/src/render/shaders/flat/layerLooks.ts +116 -0
  66. package/src/render/shaders/flat/layered.ts +170 -37
  67. package/src/render/shaders/flat/main.ts +17 -5
  68. package/src/render/shaders/flat/models.ts +1 -1
  69. package/src/render/shaders/flat/normalMap.ts +11 -2
  70. package/src/render/shaders/flat/preamble.ts +6 -0
  71. package/src/render/shaders/generated/ambientOcclusion.wgsl.ts +6 -1
  72. package/src/render/shaders/generated/flat.wgsl.ts +215 -87
  73. package/src/render/surfaceLayerLooks.ts +155 -0
  74. package/src/render/surfaceLayers.ts +187 -23
  75. package/src/render/surfaceTexture.ts +13 -4
  76. package/src/render/transmission.ts +27 -0
  77. package/src/ui/splash.ts +83 -14
@@ -7,4 +7,4 @@
7
7
  *
8
8
  * Depth zero and after the models, whose `modelLight` it calls; see `flatFrag`.
9
9
  */
10
- export declare const AREA_LIGHT_GLSL = "\n/**\n * The rectangle's signed form factor at this fragment: its corners **wound so a surface on the\n * emitting side sees them counter-clockwise**, which is what makes it positive there. The natural\n * order \u2014 minus, plus, plus, minus \u2014 winds the other way, and every one-sided light then clamped to\n * zero and the frame was black. The sign is the sidedness: a one-sided rectangle takes the positive\n * part and a two-sided one the magnitude.\n */\nfloat areaSignedForm(vec3 n, vec3 centre, vec3 right, vec3 up, vec2 halfSize) {\n vec3 c0 = centre - right * halfSize.x + up * halfSize.y;\n vec3 c1 = centre + right * halfSize.x + up * halfSize.y;\n vec3 c2 = centre + right * halfSize.x - up * halfSize.y;\n vec3 c3 = centre - right * halfSize.x - up * halfSize.y;\n return quadFormFactor(n, vWorldPos, c0, c1, c2, c3);\n}\n\n/**\n * How far along one of a rectangle's axes a fragment still sees it past the barn door on that axis's\n * positive edge: `p` the fragment's place along the axis, `height` its height above the\n * rectangle's plane, `halfSize` the rectangle's half extent, and the door's tip `tipOut` beyond\n * the edge and `tipUp` above the plane. Points of the rectangle past the answer are hidden.\n *\n * Above the tip, the tip's shadow on the plane as seen from the fragment is the limit. Below it, a\n * fragment beside the door and outside its line is hidden from all of it, and one inside the door's\n * reach is hidden from none of it.\n */\nfloat barnDoorLimit(float p, float height, float halfSize, float tipOut, float tipUp) {\n float tipAt = halfSize + tipOut;\n if (height > tipUp) return (height * tipAt - tipUp * p) / (height - tipUp);\n if (p > halfSize && height * tipOut < tipUp * (p - halfSize)) return -1e9;\n return 1e9;\n}\n\n/**\n * The part of a rectangle a fragment sees past its barn doors, written into `centre` and\n * `halfSize` as the smaller rectangle that is left, or false where the doors hide all of it.\n * `doors` is the cosine of their angle from the normal and their length; a length of 0 is no doors.\n * Each axis is clipped by its own two doors, which is what makes this four projections and a min.\n */\nbool areaBarnDoors(inout vec3 centre, vec3 right, vec3 up, inout vec2 halfSize, vec2 doors) {\n if (doors.y <= 0.0) return true;\n vec3 toFragment = vWorldPos - centre;\n float height = dot(toFragment, cross(right, up));\n /* Behind the emitting side: nothing to clip, and the form factor already answers it. */\n if (height <= 0.0) return true;\n float tipOut = doors.y * sqrt(max(1.0 - doors.x * doors.x, 0.0));\n float tipUp = doors.y * doors.x;\n vec2 p = vec2(dot(toFragment, right), dot(toFragment, up));\n vec2 hi = vec2(\n min(halfSize.x, barnDoorLimit(p.x, height, halfSize.x, tipOut, tipUp)),\n min(halfSize.y, barnDoorLimit(p.y, height, halfSize.y, tipOut, tipUp))\n );\n vec2 lo = vec2(\n max(-halfSize.x, -barnDoorLimit(-p.x, height, halfSize.x, tipOut, tipUp)),\n max(-halfSize.y, -barnDoorLimit(-p.y, height, halfSize.y, tipOut, tipUp))\n );\n if (hi.x <= lo.x || hi.y <= lo.y) return false;\n centre += right * (0.5 * (lo.x + hi.x)) + up * (0.5 * (lo.y + hi.y));\n halfSize = 0.5 * (hi - lo);\n return true;\n}\n\n/**\n * What one rectangle of colour `color` and form factor `form` adds to the lamps' two sums, open\n * and shadowed by `occl` and `glass`.\n *\n * **A model answers a rectangle as it answers a lamp at its centre**, carried at the rectangle's\n * own energy: the diffuse by the exact form factor over the centre's cosine, the specular by the\n * form factor. An approximation, stated: a model's lobe is not integrated over the rectangle as the\n * standard lobe is, so a glossy model under a large softbox shows a lobe from its centre rather than\n * its shape.\n *\n * **The standard lobe is integrated over the rectangle**, rather than sampled at one point on it.\n * What stood here once handed the closest point on the rectangle to `sphereLobe` and scaled it by\n * `form`, the *diffuse* form factor. Measured against a brute-force integral by\n * `scripts/areaSpecular.mjs`, that lost the reflection rather than blurring it: on polished metal\n * facing a softbox it returned 0.000233 where the integral is 0.9207, and head-on on a rough surface\n * it was 3.7 times too bright. Worst 287% either way; this is 40.1% where the light is what the\n * surface reflects and 79.0% for one overhead. `quadCoverage` says what fraction of the specular\n * lobe the rectangle covers and the environment BRDF says how much the surface returns, the split\n * the probe path uses. **`dfg.x + dfg.y` and not `envSpecularEnergy`**, deliberately: the\n * compensation returns the multiply-scattered share, and every direct lobe in this shader is\n * single-scattering, so adding it here returns energy the light never had. Measured at 211%\n * against 40.1%.\n */\nvoid areaLightAdd(\n vec3 centre,\n vec3 right,\n vec3 up,\n vec2 halfSize,\n vec3 color,\n float form,\n float occl,\n vec3 glass,\n vec3 n,\n vec3 albedo,\n float metal,\n vec3 specColor,\n float roughness,\n bool modelled,\n inout vec3 lampOpen,\n inout vec3 lampShadowed\n) {\n if (modelled) {\n vec3 areaL = normalize(centre - vWorldPos);\n modelLight(areaL, 0.0, 0.0);\n vec3 areaModel = (mDiffuse * (form / max(dot(n, areaL), 1e-3)) + mSpecular * form) * color;\n lampOpen += areaModel;\n lampShadowed += areaModel * occl * glass;\n return;\n }\n /* Held back with the point lamps', and shadowed by the same term it always was. */\n vec3 areaDiffuse = albedo * color * form * (1.0 - metal);\n lampOpen += areaDiffuse;\n lampShadowed += areaDiffuse * occl * glass;\n\n if (vSpecular > 0.0 || metal > 0.0) {\n vec3 c0 = centre - right * halfSize.x + up * halfSize.y;\n vec3 c1 = centre + right * halfSize.x + up * halfSize.y;\n vec3 c2 = centre + right * halfSize.x - up * halfSize.y;\n vec3 c3 = centre - right * halfSize.x - up * halfSize.y;\n vec3 toEyeArea = normalize(uCameraPos - vWorldPos);\n float coverage = quadCoverage(n, vWorldPos, toEyeArea, roughness, c0, c1, c2, c3);\n if (coverage > 0.0) {\n vec2 areaDfg = envBrdfApprox(max(dot(n, toEyeArea), 0.0), roughness);\n vec3 areaSpec = specColor * areaDfg.x + vec3(areaDfg.y);\n vec3 areaHighlight = areaSpec * color * coverage;\n lampOpen += areaHighlight;\n lampShadowed += areaHighlight * occl * glass;\n }\n }\n}\n";
10
+ export declare const AREA_LIGHT_GLSL = "\n/**\n * The rectangle's signed form factor at this fragment: its corners **wound so a surface on the\n * emitting side sees them counter-clockwise**, which is what makes it positive there. The natural\n * order \u2014 minus, plus, plus, minus \u2014 winds the other way, and every one-sided light then clamped to\n * zero and the frame was black. The sign is the sidedness: a one-sided rectangle takes the positive\n * part and a two-sided one the magnitude.\n */\nfloat areaSignedForm(vec3 n, vec3 centre, vec3 right, vec3 up, vec2 halfSize) {\n vec3 c0 = centre - right * halfSize.x + up * halfSize.y;\n vec3 c1 = centre + right * halfSize.x + up * halfSize.y;\n vec3 c2 = centre + right * halfSize.x - up * halfSize.y;\n vec3 c3 = centre - right * halfSize.x - up * halfSize.y;\n return quadFormFactor(n, vWorldPos, c0, c1, c2, c3);\n}\n\n/**\n * How far along one of a rectangle's axes a fragment still sees it past the barn door on that axis's\n * positive edge: `p` the fragment's place along the axis, `height` its height above the\n * rectangle's plane, `halfSize` the rectangle's half extent, and the door's tip `tipOut` beyond\n * the edge and `tipUp` above the plane. Points of the rectangle past the answer are hidden.\n *\n * Above the tip, the tip's shadow on the plane as seen from the fragment is the limit. Below it, a\n * fragment beside the door and outside its line is hidden from all of it, and one inside the door's\n * reach is hidden from none of it.\n */\nfloat barnDoorLimit(float p, float height, float halfSize, float tipOut, float tipUp) {\n float tipAt = halfSize + tipOut;\n if (height > tipUp) return (height * tipAt - tipUp * p) / (height - tipUp);\n if (p > halfSize && height * tipOut < tipUp * (p - halfSize)) return -1e9;\n return 1e9;\n}\n\n/**\n * The part of a rectangle a fragment sees past its barn doors, written into `centre` and\n * `halfSize` as the smaller rectangle that is left, or false where the doors hide all of it.\n * `doors` is the cosine of their angle from the normal and their length; a length of 0 is no doors.\n * Each axis is clipped by its own two doors, which is what makes this four projections and a min.\n */\nbool areaBarnDoors(inout vec3 centre, vec3 right, vec3 up, inout vec2 halfSize, vec2 doors) {\n if (doors.y <= 0.0) return true;\n vec3 toFragment = vWorldPos - centre;\n float height = dot(toFragment, cross(right, up));\n /* Behind the emitting side: nothing to clip, and the form factor already answers it. */\n if (height <= 0.0) return true;\n float tipOut = doors.y * sqrt(max(1.0 - doors.x * doors.x, 0.0));\n float tipUp = doors.y * doors.x;\n vec2 p = vec2(dot(toFragment, right), dot(toFragment, up));\n vec2 hi = vec2(\n min(halfSize.x, barnDoorLimit(p.x, height, halfSize.x, tipOut, tipUp)),\n min(halfSize.y, barnDoorLimit(p.y, height, halfSize.y, tipOut, tipUp))\n );\n vec2 lo = vec2(\n max(-halfSize.x, -barnDoorLimit(-p.x, height, halfSize.x, tipOut, tipUp)),\n max(-halfSize.y, -barnDoorLimit(-p.y, height, halfSize.y, tipOut, tipUp))\n );\n if (hi.x <= lo.x || hi.y <= lo.y) return false;\n centre += right * (0.5 * (lo.x + hi.x)) + up * (0.5 * (lo.y + hi.y));\n halfSize = 0.5 * (hi - lo);\n return true;\n}\n\n/**\n * What one rectangle of colour `color` and form factor `form` adds to the lamps' two sums, open\n * and shadowed by `occl` and `glass`.\n *\n * **A model answers a rectangle as it answers a lamp at its centre**, carried at the rectangle's\n * own energy: the diffuse by the exact form factor over the centre's cosine, the specular by the\n * form factor. An approximation, stated: a model's lobe is not integrated over the rectangle as the\n * standard lobe is, so a glossy model under a large softbox shows a lobe from its centre rather than\n * its shape.\n *\n * **The standard lobe is integrated over the rectangle**, rather than sampled at one point on it.\n * What stood here once handed the closest point on the rectangle to `sphereLobe` and scaled it by\n * `form`, the *diffuse* form factor. Measured against a brute-force integral by\n * `scripts/areaSpecular.mjs`, that lost the reflection rather than blurring it: on polished metal\n * facing a softbox it returned 0.000233 where the integral is 0.9207, and head-on on a rough surface\n * it was 3.7 times too bright. Worst 287% either way; this is 40.1% where the light is what the\n * surface reflects and 79.0% for one overhead. `quadCoverage` says what fraction of the specular\n * lobe the rectangle covers and the environment BRDF says how much the surface returns, the split\n * the probe path uses. **`dfg.x + dfg.y` and not `envSpecularEnergy`**, deliberately: the\n * compensation returns the multiply-scattered share, and every direct lobe in this shader is\n * single-scattering, so adding it here returns energy the light never had. Measured at 211%\n * against 40.1%.\n */\nvoid areaLightAdd(\n vec3 centre,\n vec3 right,\n vec3 up,\n vec2 halfSize,\n vec3 color,\n float form,\n float occl,\n vec3 glass,\n vec3 n,\n vec3 albedo,\n float metal,\n vec3 specColor,\n float roughness,\n bool modelled,\n inout vec3 lampOpen,\n inout vec3 lampShadowed\n) {\n if (modelled) {\n vec3 areaL = normalize(centre - vWorldPos);\n modelLight(areaL, 0.0, 0.0);\n vec3 areaModel = (mDiffuse * (form / max(dot(n, areaL), 1e-3)) + mSpecular * form) * color;\n lampOpen += areaModel;\n lampShadowed += areaModel * occl * glass;\n return;\n }\n /* Held back with the point lamps', and shadowed by the same term it always was. */\n vec3 areaDiffuse = albedo * color * form * (1.0 - metal);\n lampOpen += areaDiffuse;\n lampShadowed += areaDiffuse * occl * glass;\n\n if (surfaceSpecular > 0.0 || metal > 0.0) {\n vec3 c0 = centre - right * halfSize.x + up * halfSize.y;\n vec3 c1 = centre + right * halfSize.x + up * halfSize.y;\n vec3 c2 = centre + right * halfSize.x - up * halfSize.y;\n vec3 c3 = centre - right * halfSize.x - up * halfSize.y;\n vec3 toEyeArea = normalize(uCameraPos - vWorldPos);\n float coverage = quadCoverage(n, vWorldPos, toEyeArea, roughness, c0, c1, c2, c3);\n if (coverage > 0.0) {\n vec2 areaDfg = envBrdfApprox(max(dot(n, toEyeArea), 0.0), roughness);\n vec3 areaSpec = specColor * areaDfg.x + vec3(areaDfg.y);\n vec3 areaHighlight = areaSpec * color * coverage;\n lampOpen += areaHighlight;\n lampShadowed += areaHighlight * occl * glass;\n }\n }\n}\n";
@@ -123,7 +123,7 @@ void areaLightAdd(
123
123
  lampOpen += areaDiffuse;
124
124
  lampShadowed += areaDiffuse * occl * glass;
125
125
 
126
- if (vSpecular > 0.0 || metal > 0.0) {
126
+ if (surfaceSpecular > 0.0 || metal > 0.0) {
127
127
  vec3 c0 = centre - right * halfSize.x + up * halfSize.y;
128
128
  vec3 c1 = centre + right * halfSize.x + up * halfSize.y;
129
129
  vec3 c2 = centre + right * halfSize.x - up * halfSize.y;
@@ -8,7 +8,7 @@ export declare function glassTintGlsl(glassShadows: boolean): string;
8
8
  * The lit stage's switches, by the name of the constant each is: the decision both backends make
9
9
  * about which optional code a lit program carries. See `litSwitchesGlsl`.
10
10
  */
11
- export type LitSwitch = 'GLASS_SHADOWS' | 'CLUSTERED_LIGHTS' | 'LIGHT_FIXTURES' | 'SURFACE_EFFECTS' | 'DRIFT_LIGHT' | 'PHYSICAL_SPECULAR' | 'SURFACE_OVERLAY' | 'REFLECTION_MAPS' | 'WORLD_UVS' | 'MOVING_SUN' | 'LAYERED';
11
+ export type LitSwitch = 'GLASS_SHADOWS' | 'CLUSTERED_LIGHTS' | 'LIGHT_FIXTURES' | 'SURFACE_EFFECTS' | 'DRIFT_LIGHT' | 'PHYSICAL_SPECULAR' | 'SURFACE_OVERLAY' | 'REFLECTION_MAPS' | 'WORLD_UVS' | 'MOVING_SUN' | 'LAYERED' | 'LAYER_LOOKS';
12
12
  /** Every switch off: what a renderer starts from before its profile and its content say otherwise. */
13
13
  export declare function noLitSwitches(): Record<LitSwitch, boolean>;
14
14
  /** Which of the lit stage's optional features a build reads. See `litSwitchesGlsl`. */
@@ -37,6 +37,7 @@ export function noLitSwitches() {
37
37
  WORLD_UVS: false,
38
38
  MOVING_SUN: false,
39
39
  LAYERED: false,
40
+ LAYER_LOOKS: false,
40
41
  };
41
42
  }
42
43
  /**
@@ -140,6 +140,11 @@ export interface FlatShaderOptions {
140
140
  * True when absent. See \`layeredGlsl\`.
141
141
  */
142
142
  readonly layered?: boolean;
143
+ /**
144
+ * Whether a layered material's layers are picked from shared arrays and given looks of their own,
145
+ * \`uLayerLooks\` and all. True when absent; nothing without \`layered\`. See \`layerLooksGlsl\`.
146
+ */
147
+ readonly layerLooks?: boolean;
143
148
  /**
144
149
  * How many point lights this build declares room for. `MAX_POINT_LIGHTS` when absent.
145
150
  *
@@ -18,6 +18,7 @@ import { modelsGlsl } from './models.js';
18
18
  import { AREA_LIGHT_GLSL } from './areaLight.js';
19
19
  import { NORMAL_MAP_GLSL } from './normalMap.js';
20
20
  import { DRIFT_LIGHT_GLSL } from './driftLight.js';
21
+ import { LIGHTMAP_OPACITY_STEPS } from '../../instances.js';
21
22
  import { SURFACE_EFFECTS_GLSL } from './surfaceEffects.js';
22
23
  import { overlayGlsl } from './overlay.js';
23
24
  import { reflectionSurfaceGlsl } from './reflectionSurface.js';
@@ -341,10 +342,16 @@ void main() {
341
342
  vAlpha = aInstanceTint.w;
342
343
  vThickness = 1.0;
343
344
  if (LIGHTMAP_REGIONS) {
345
+ /*
346
+ * The opacity's transparency rides the U offset as whole steps of 2, the offset itself under 1
347
+ * (\`MeshInstances.lightmapRegions\`): taken off, and none where the instance is opaque.
348
+ */
349
+ float steps = floor(aInstanceTint.z * 0.5);
350
+ float offsetU = aInstanceTint.z - 2.0 * steps;
344
351
  /* Out of the lane's \`-1 - uv\`, through the region, and back into it. See lightmap.ts. */
345
- vGrain = -1.0 - ((-1.0 - aGrain) * aInstanceTint.x + aInstanceTint.z);
352
+ vGrain = -1.0 - ((-1.0 - aGrain) * aInstanceTint.x + offsetU);
346
353
  vRelief = -1.0 - ((-1.0 - aRelief) * aInstanceTint.y + aInstanceTint.w);
347
- vAlpha = 1.0;
354
+ vAlpha = 1.0 - steps / ${LIGHTMAP_OPACITY_STEPS.toFixed(1)};
348
355
  }
349
356
  #else
350
357
  vSkyDirect = aChannel.y;
@@ -506,7 +513,7 @@ export function flatFrag(options) {
506
513
  /* After the reflection maps, so its switch takes the next id; main calls it. worldUv.ts. */
507
514
  worldUvGlsl(options.worldUvs ?? true),
508
515
  /* After the projection, so its switch takes the next id; the normal map below calls it. */
509
- layeredGlsl(options.layered ?? true),
516
+ layeredGlsl(options.layered ?? true, options.layerLooks ?? true),
510
517
  /* Where each map is read, after both of the ways it can be read otherwise. See mapReads.ts. */
511
518
  MAP_READS_GLSL,
512
519
  /* After the frame and the projection it calls, unconditional for the same reason as the
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Which layer of the shared arrays each of a layered material's layers reads, and how each looks:
3
+ * `uLayerLooks`, packed by `surfaceLayerLooks.ts`, read by `layered.ts`.
4
+ *
5
+ * **Every helper is the identity where the switch is off**, written as a branch on `LAYER_LOOKS`
6
+ * itself because the generated WGSL carries one string for both values of an override: layer `i`
7
+ * reads layer `i`, the maps beyond the layers start at the layer count, a tint is white, a range is
8
+ * 0 to 1, a normal is as stored, a specular is the surface's own, a repeat down is the repeat
9
+ * across, and nothing is occluded — exactly what `layered.ts` did before this existed, which is what
10
+ * a layered material that asks for none of it has to keep drawing.
11
+ *
12
+ * **A lit switch, `LAYER_LOOKS`**, off until a material first asks and declared after `LAYERED`, so
13
+ * every switch before it keeps its id. It is only ever compiled beside `LAYERED`, whose `uLayers`
14
+ * it reads.
15
+ */
16
+ export declare function layerLooksGlsl(on: boolean): string;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Which layer of the shared arrays each of a layered material's layers reads, and how each looks:
3
+ * `uLayerLooks`, packed by `surfaceLayerLooks.ts`, read by `layered.ts`.
4
+ *
5
+ * **Every helper is the identity where the switch is off**, written as a branch on `LAYER_LOOKS`
6
+ * itself because the generated WGSL carries one string for both values of an override: layer `i`
7
+ * reads layer `i`, the maps beyond the layers start at the layer count, a tint is white, a range is
8
+ * 0 to 1, a normal is as stored, a specular is the surface's own, a repeat down is the repeat
9
+ * across, and nothing is occluded — exactly what `layered.ts` did before this existed, which is what
10
+ * a layered material that asks for none of it has to keep drawing.
11
+ *
12
+ * **A lit switch, `LAYER_LOOKS`**, off until a material first asks and declared after `LAYERED`, so
13
+ * every switch before it keeps its id. It is only ever compiled beside `LAYERED`, whose `uLayers`
14
+ * it reads.
15
+ */
16
+ export function layerLooksGlsl(on) {
17
+ if (!on) {
18
+ return /* glsl */ `
19
+ const bool LAYER_LOOKS = false; // wgsl:override
20
+ float layerArray(int i) { return float(i); }
21
+ float layerExtras() { return uLayers[1].y; }
22
+ vec3 layerTint(int i) { return vec3(1.0); }
23
+ vec2 layerRanges(int i, vec2 roughMetal) { return roughMetal; }
24
+ vec3 layerStrengthen(int i, vec3 texel) { return texel; }
25
+ float layerSpecular(int i, float own) { return own; }
26
+ float layerRepeatDown(int i, float across) { return across; }
27
+ float layerOcclusionStrength() { return 0.0; }
28
+ bool layerOcclusionAmbient() { return false; }
29
+ vec2 layerOcclusionRange() { return vec2(0.0, 1.0); }
30
+ `;
31
+ }
32
+ return /* glsl */ `
33
+ /* Whether layers are picked from shared arrays and given looks of their own: layerLooks.ts. */
34
+ const bool LAYER_LOOKS = true; // wgsl:override
35
+ /*
36
+ * Fifteen vectors, as surfaceLayerLooks.ts packs them: the array layer each of the five reads, base
37
+ * first (floats 0 to 4); where the maps beyond the layers start (5), the mesh occlusion's strength
38
+ * (6) and whether it darkens the ambient light rather than the colour (7); then two vectors a
39
+ * layer, base first: its tint (x y z) and normal strength (w), then its roughness range (x to y)
40
+ * and metalness range (z to w); then each layer's specular, -1 for the surface's own (48 to 52), its
41
+ * repeat down (53 to 57), and the occlusion's range (58 to 59).
42
+ */
43
+ uniform vec4 uLayerLooks[15]; // wgsl:material
44
+
45
+ /* Float k of the fifteen vectors. */
46
+ float layerLook(int k) {
47
+ return uLayerLooks[k / 4][k % 4];
48
+ }
49
+
50
+ /* The layer of the albedo, normal and ORM arrays a material's layer i reads. */
51
+ float layerArray(int i) {
52
+ if (LAYER_LOOKS) return layerLook(i);
53
+ return float(i);
54
+ }
55
+
56
+ /* Where the maps beyond the layers start in each array. */
57
+ float layerExtras() {
58
+ if (LAYER_LOOKS) return uLayerLooks[1].y;
59
+ return uLayers[1].y;
60
+ }
61
+
62
+ vec3 layerTint(int i) {
63
+ if (LAYER_LOOKS) return uLayerLooks[2 + i * 2].xyz;
64
+ return vec3(1.0);
65
+ }
66
+
67
+ /* The ORM's roughness and metalness, each spread over the layer's own range. */
68
+ vec2 layerRanges(int i, vec2 roughMetal) {
69
+ if (LAYER_LOOKS) {
70
+ vec4 range = uLayerLooks[3 + i * 2];
71
+ return vec2(mix(range.x, range.y, roughMetal.x), mix(range.z, range.w, roughMetal.y));
72
+ }
73
+ return roughMetal;
74
+ }
75
+
76
+ /*
77
+ * A tangent-space texel bent further or less by the layer's strength: its x and y scaled, its z
78
+ * kept, so 0 lies flat and a strength above 1 steepens every slope the map holds. z is held above
79
+ * zero so a texel lying exactly flat on its surface cannot normalise to nothing at strength 0.
80
+ */
81
+ vec3 layerStrengthen(int i, vec3 texel) {
82
+ if (LAYER_LOOKS) return normalize(vec3(texel.xy * uLayerLooks[2 + i * 2].w, max(texel.z, 1e-4)));
83
+ return texel;
84
+ }
85
+
86
+ /* The layer's specular, or the surface's own where it names none. */
87
+ float layerSpecular(int i, float own) {
88
+ if (LAYER_LOOKS) {
89
+ float named = layerLook(48 + i);
90
+ return named < 0.0 ? own : named;
91
+ }
92
+ return own;
93
+ }
94
+
95
+ /* The layer's repeat down, which is its repeat across unless it gave two. */
96
+ float layerRepeatDown(int i, float across) {
97
+ if (LAYER_LOOKS) return layerLook(53 + i);
98
+ return across;
99
+ }
100
+
101
+ float layerOcclusionStrength() {
102
+ if (LAYER_LOOKS) return uLayerLooks[1].z;
103
+ return 0.0;
104
+ }
105
+
106
+ bool layerOcclusionAmbient() {
107
+ if (LAYER_LOOKS) return uLayerLooks[1].w > 0.5;
108
+ return false;
109
+ }
110
+
111
+ vec2 layerOcclusionRange() {
112
+ if (LAYER_LOOKS) return uLayerLooks[14].zw;
113
+ return vec2(0.0, 1.0);
114
+ }
115
+ `;
116
+ }
@@ -2,18 +2,28 @@
2
2
  * A material blended from layers by a mask: `SurfaceMaterial.layers`, packed into `uLayers` by
3
3
  * `surfaceLayers.ts`.
4
4
  *
5
- * **Layer `i` is layer `i` of the material's arrays**, each read at the mesh's coordinates times its
6
- * own repeat; the mask is layer 0 of the map a shading model would read, at the mesh's coordinates.
7
- * Each channel lays a layer over everything before it — red layer 1, green 2, blue 3, alpha 4 — so a
8
- * layer's share is its channel times one less each later channel, and the base keeps what none of
9
- * them took. Colour, roughness and occlusion are blended by those shares, and the normal map's
10
- * tangent-space texels too, before the frame turns them into the world.
5
+ * **Layer `i` is layer `i` of the material's arrays** — or the layer `layerLooks.ts` picks, with the
6
+ * tint, ranges and normal strength it gives each — each read at its own repeat of where the
7
+ * layers are laid: the mesh's coordinates, or the world's horizontal plane where the material has a
8
+ * projection (`worldUv.ts`). The mask's channels weigh layers 1 to 4 — red, green, blue, alpha — and
9
+ * come from a map read where a shading model's would be, from the ORM array's layer past the layers,
10
+ * or from the vertex colour; an added mask placed as the layers are raises one layer's weight, and
11
+ * one layer may be weighed by how much the surface faces up instead of by its channel.
12
+ *
13
+ * **Two ways to combine them.** Laid over, each layer covers everything before it, so a layer's
14
+ * share is its weight times one less each later weight, and the base keeps what none took. Summed,
15
+ * the base is mixed toward each layer times its weight by the weights' sum held to 1, so each
16
+ * layer's share is its weight times that and the base keeps the rest. The shares are worked out once
17
+ * a pixel, in `layeredBegin`; colour, roughness and occlusion are blended by them, and the normal
18
+ * map's tangent-space texels too, before the frame turns them into the world — around a normal read
19
+ * at the mesh's own coordinates, where the material lays one under the layers.
11
20
  *
12
21
  * **Every layer present is read, whatever its share.** A read whose level is implicit is legal only
13
22
  * in uniform control flow (AGENTS.md 2026-08-07), so the loop is bounded by the layer count, a
14
23
  * uniform, and never skips on a share, which is the pixel's own.
15
24
  *
16
- * **A lit switch, `LAYERED`**, off until a material first asks, declared last so every switch before
17
- * it keeps its id. Compiled out, its two vectors are not declared and its calls are constants.
25
+ * **A lit switch, `LAYERED`**, off until a material first asks, declared after every switch before
26
+ * it so each keeps its id. Compiled out, its four vectors are not declared and its calls are
27
+ * constants. `looks` compiles `LAYER_LOOKS` in beside it, and means nothing without it.
18
28
  */
19
- export declare function layeredGlsl(on: boolean): string;
29
+ export declare function layeredGlsl(on: boolean, looks?: boolean): string;
@@ -1,106 +1,238 @@
1
+ import { layerLooksGlsl } from './layerLooks.js';
1
2
  /**
2
3
  * A material blended from layers by a mask: `SurfaceMaterial.layers`, packed into `uLayers` by
3
4
  * `surfaceLayers.ts`.
4
5
  *
5
- * **Layer `i` is layer `i` of the material's arrays**, each read at the mesh's coordinates times its
6
- * own repeat; the mask is layer 0 of the map a shading model would read, at the mesh's coordinates.
7
- * Each channel lays a layer over everything before it — red layer 1, green 2, blue 3, alpha 4 — so a
8
- * layer's share is its channel times one less each later channel, and the base keeps what none of
9
- * them took. Colour, roughness and occlusion are blended by those shares, and the normal map's
10
- * tangent-space texels too, before the frame turns them into the world.
6
+ * **Layer `i` is layer `i` of the material's arrays** — or the layer `layerLooks.ts` picks, with the
7
+ * tint, ranges and normal strength it gives each — each read at its own repeat of where the
8
+ * layers are laid: the mesh's coordinates, or the world's horizontal plane where the material has a
9
+ * projection (`worldUv.ts`). The mask's channels weigh layers 1 to 4 — red, green, blue, alpha — and
10
+ * come from a map read where a shading model's would be, from the ORM array's layer past the layers,
11
+ * or from the vertex colour; an added mask placed as the layers are raises one layer's weight, and
12
+ * one layer may be weighed by how much the surface faces up instead of by its channel.
13
+ *
14
+ * **Two ways to combine them.** Laid over, each layer covers everything before it, so a layer's
15
+ * share is its weight times one less each later weight, and the base keeps what none took. Summed,
16
+ * the base is mixed toward each layer times its weight by the weights' sum held to 1, so each
17
+ * layer's share is its weight times that and the base keeps the rest. The shares are worked out once
18
+ * a pixel, in `layeredBegin`; colour, roughness and occlusion are blended by them, and the normal
19
+ * map's tangent-space texels too, before the frame turns them into the world — around a normal read
20
+ * at the mesh's own coordinates, where the material lays one under the layers.
11
21
  *
12
22
  * **Every layer present is read, whatever its share.** A read whose level is implicit is legal only
13
23
  * in uniform control flow (AGENTS.md 2026-08-07), so the loop is bounded by the layer count, a
14
24
  * uniform, and never skips on a share, which is the pixel's own.
15
25
  *
16
- * **A lit switch, `LAYERED`**, off until a material first asks, declared last so every switch before
17
- * it keeps its id. Compiled out, its two vectors are not declared and its calls are constants.
26
+ * **A lit switch, `LAYERED`**, off until a material first asks, declared after every switch before
27
+ * it so each keeps its id. Compiled out, its four vectors are not declared and its calls are
28
+ * constants. `looks` compiles `LAYER_LOOKS` in beside it, and means nothing without it.
18
29
  */
19
- export function layeredGlsl(on) {
30
+ export function layeredGlsl(on, looks = false) {
20
31
  if (!on) {
21
32
  return /* glsl */ `
22
33
  const bool LAYERED = false; // wgsl:override
23
34
  bool layered() { return false; }
24
35
  void layeredBegin() {}
36
+ vec3 layeredColor(vec3 color) { return color; }
25
37
  vec4 layeredAlbedo() { return vec4(1.0); }
26
38
  vec3 layeredOrm() { return vec3(1.0); }
27
39
  vec3 layeredTexel() { return vec3(0.0, 0.0, 1.0); }
40
+ bool layeredMeshNormal() { return false; }
41
+ vec3 layeredMeshTexel() { return vec3(0.0, 0.0, 1.0); }
28
42
  bool layeredGlows() { return false; }
29
43
  vec3 layeredEmissive() { return vec3(0.0); }
44
+ float layeredAmbient() { return 1.0; }
45
+ float layeredSpecular(float own) { return own; }
30
46
  `;
31
47
  }
32
48
  return /* glsl */ `
33
49
  /* Whether this program blends a material's layers. Off but where one has asked: layered.ts. */
34
50
  const bool LAYERED = true; // wgsl:override
35
- /* The five repeats, base first (x y z w, then x), the count (y) and the glowing layer, or -1 (z). */
36
- uniform vec4 uLayers[2]; // wgsl:material
37
-
38
- /* The mask's channels that lay a layer, zero past the count: read once, in layeredBegin. */
39
- vec4 lMask;
51
+ /*
52
+ * Four vectors, as surfaceLayers.ts packs them: the five repeats, base first (x y z w, then x); the
53
+ * count (y), the glowing layer or -1 (z) and where the mask is, 0 a map, 1 the ORM array, 2 the
54
+ * vertex colour (w); the blend, 0 over and 1 summed (x), whether a mesh normal is laid under the
55
+ * layers (y), the added mask's layer or -1 (z) and its repeat (w); its intensity (x), the facing
56
+ * layer or -1 (y), its bias (z) and its sharpness (w).
57
+ */
58
+ uniform vec4 uLayers[4]; // wgsl:material
59
+ ${layerLooksGlsl(looks)}
60
+ /* Each layer's share of this point, the base's and layers 1 to 4's: worked out once, in layeredBegin. */
61
+ float lShareBase;
62
+ vec4 lShare;
63
+ /* What an occlusion read at the mesh's coordinates leaves of this point, 1 where none: layeredBegin. */
64
+ float lMeshOcclusion;
40
65
 
41
66
  bool layered() {
42
67
  return uLayers[1].y > 0.5;
43
68
  }
44
69
 
45
- /* The mask, at the mesh's coordinates, where a model's map is. Under a branch on uniforms. */
46
- void layeredBegin() {
47
- vec4 m = texture(uModelMap, vec3(vUv.xy, 0.0));
48
- float n = uLayers[1].y;
49
- lMask = vec4(n > 1.5 ? m.r : 0.0, n > 2.5 ? m.g : 0.0, n > 3.5 ? m.b : 0.0, n > 4.5 ? m.a : 0.0);
70
+ float layerRepeat(int i) {
71
+ return i < 4 ? uLayers[0][i] : uLayers[1].x;
50
72
  }
51
73
 
52
- /* Layer i's share of this point: its channel, less every later layer's. */
53
- float layerShare(int i) {
54
- float keep = 1.0;
55
- for (int j = 3; j >= 0; j--) {
56
- if (j + 1 == i) return lMask[j] * keep;
57
- keep *= 1.0 - lMask[j];
74
+ /* Where the layers are laid: the world's horizontal plane under a projection, the mesh's otherwise. */
75
+ vec2 layerPlace() {
76
+ if (WORLD_UVS && projected()) return planarAt();
77
+ return vUv.xy;
78
+ }
79
+
80
+ vec3 layerAt(int i) {
81
+ float across = layerRepeat(i);
82
+ return vec3(layerPlace() * vec2(across, layerRepeatDown(i, across)), layerArray(i));
83
+ }
84
+
85
+ /* The weights as the material keeps them: the mask's channels, at the mesh's own coordinates. */
86
+ vec4 layerWeights() {
87
+ float source = uLayers[1].w;
88
+ if (source > 1.5) return vec4(vColor, 0.0);
89
+ if (source > 0.5) return texture(uOrmMap, vec3(vUv.xy, layerExtras()));
90
+ return texture(uModelMap, vec3(vUv.xy, 0.0));
91
+ }
92
+
93
+ /* Which of layers 1 to 4 a packed layer number names, as a vector of one 1: none for -1. */
94
+ vec4 layerPick(float layer) {
95
+ return vec4(equal(vec4(layer), vec4(1.0, 2.0, 3.0, 4.0)));
96
+ }
97
+
98
+ /*
99
+ * The weights, changed where the material asks and zero past the count, and every share from them.
100
+ * Under a branch on uniforms, as its reads are.
101
+ */
102
+ void layeredBegin() {
103
+ /*
104
+ * The occlusion at the mesh's coordinates, once: the ORM array's layer past the layers and past
105
+ * any mask and added mask it carries, spread over its range and taken at its strength.
106
+ */
107
+ lMeshOcclusion = 1.0;
108
+ float strength = layerOcclusionStrength();
109
+ if (strength > 0.0) {
110
+ float at = layerExtras() + (abs(uLayers[1].w - 1.0) < 0.5 ? 1.0 : 0.0) + (uLayers[2].z > 0.5 ? 1.0 : 0.0);
111
+ vec2 range = layerOcclusionRange();
112
+ float occlusion = mix(range.x, range.y, texture(uOrmMap, vec3(vUv.xy, at)).r);
113
+ lMeshOcclusion = mix(1.0, occlusion, strength);
114
+ }
115
+ vec4 m = layerWeights();
116
+ if (uLayers[2].z > 0.5) {
117
+ /* The ORM array's layer past the layers, and past the mask where the array carries that too. */
118
+ float at = layerExtras() + (abs(uLayers[1].w - 1.0) < 0.5 ? 1.0 : 0.0);
119
+ float added = texture(uOrmMap, vec3(layerPlace() * uLayers[2].w, at)).r * uLayers[3].x;
120
+ m = mix(m, clamp(m + added, 0.0, 1.0), layerPick(uLayers[2].z));
58
121
  }
59
- return keep;
122
+ if (uLayers[3].y > 0.5) {
123
+ float up = normalize(vNormal).y * 0.5 + 0.5;
124
+ float facing = clamp(uLayers[3].z + uLayers[3].w * up, 0.0, 1.0);
125
+ m = mix(m, vec4(facing), layerPick(uLayers[3].y));
126
+ }
127
+ float n = uLayers[1].y;
128
+ m *= vec4(n > 1.5 ? 1.0 : 0.0, n > 2.5 ? 1.0 : 0.0, n > 3.5 ? 1.0 : 0.0, n > 4.5 ? 1.0 : 0.0);
129
+ if (uLayers[2].x > 0.5) {
130
+ float taken = min(m.x + m.y + m.z + m.w, 1.0);
131
+ lShare = m * taken;
132
+ lShareBase = 1.0 - taken;
133
+ return;
134
+ }
135
+ /* Each over everything before it: a layer keeps its weight of what every later one leaves. */
136
+ float keep = 1.0;
137
+ lShare.w = m.w * keep;
138
+ keep *= 1.0 - m.w;
139
+ lShare.z = m.z * keep;
140
+ keep *= 1.0 - m.z;
141
+ lShare.y = m.y * keep;
142
+ keep *= 1.0 - m.y;
143
+ lShare.x = m.x * keep;
144
+ keep *= 1.0 - m.x;
145
+ lShareBase = keep;
60
146
  }
61
147
 
62
- float layerRepeat(int i) {
63
- return i < 4 ? uLayers[0][i] : uLayers[1].x;
148
+ float layerShare(int i) {
149
+ if (i == 0) return lShareBase;
150
+ return lShare[i - 1];
64
151
  }
65
152
 
66
- vec3 layerAt(int i) {
67
- return vec3(vUv.xy * layerRepeat(i), float(i));
153
+ /* The vertex colour's part in the albedo: none where it is the mask instead. */
154
+ vec3 layeredColor(vec3 color) {
155
+ return uLayers[1].w > 1.5 ? vec3(1.0) : color;
68
156
  }
69
157
 
70
- /* Colour by the shares, and the base layer's coverage, which a cutout reads. */
158
+ /*
159
+ * Colour by the shares, each layer's tinted, and the base layer's coverage, which a cutout reads;
160
+ * then darkened by the occlusion at the mesh's coordinates, where it is the colour that takes it.
161
+ */
71
162
  vec4 layeredAlbedo() {
72
163
  vec4 sum = vec4(0.0);
73
164
  for (int i = 0; i < 5; i++) {
74
165
  if (float(i) >= uLayers[1].y) break;
75
166
  vec4 t = texture(uAlbedo, layerAt(i));
76
- sum.rgb += t.rgb * layerShare(i);
167
+ sum.rgb += t.rgb * layerTint(i) * layerShare(i);
77
168
  if (i == 0) sum.a = t.a;
78
169
  }
170
+ if (!layerOcclusionAmbient()) sum.rgb *= lMeshOcclusion;
79
171
  return sum;
80
172
  }
81
173
 
174
+ /* What the ambient light keeps: the occlusion at the mesh's coordinates, where it takes it. */
175
+ float layeredAmbient() {
176
+ return layerOcclusionAmbient() ? lMeshOcclusion : 1.0;
177
+ }
178
+
179
+ /*
180
+ * The specular by the shares, each layer's own or the surface's where it names none. The surface's
181
+ * own, untouched, where no material has asked for looks: shares summing to one in floating point
182
+ * are not quite one, and a layered material that names nothing must draw exactly as before.
183
+ */
184
+ float layeredSpecular(float own) {
185
+ if (LAYER_LOOKS) {
186
+ float sum = 0.0;
187
+ for (int i = 0; i < 5; i++) {
188
+ if (float(i) >= uLayers[1].y) break;
189
+ sum += layerSpecular(i, own) * layerShare(i);
190
+ }
191
+ return sum;
192
+ }
193
+ return own;
194
+ }
195
+
196
+ /* Occlusion, roughness and metalness by the shares, each layer's spread over its own ranges. */
82
197
  vec3 layeredOrm() {
83
198
  vec3 sum = vec3(0.0);
84
199
  for (int i = 0; i < 5; i++) {
85
200
  if (float(i) >= uLayers[1].y) break;
86
- sum += texture(uOrmMap, layerAt(i)).rgb * layerShare(i);
201
+ vec3 t = texture(uOrmMap, layerAt(i)).rgb;
202
+ t.yz = layerRanges(i, t.yz);
203
+ sum += t * layerShare(i);
87
204
  }
88
205
  return sum;
89
206
  }
90
207
 
91
- /* The tangent-space texels blended, each with its z rebuilt where a two-channel map stored none. */
208
+ /* A tangent-space texel off the normal array, its z rebuilt where a two-channel map stored none. */
209
+ vec3 layerNormalTexel(vec3 at) {
210
+ vec3 t = texture(uNormalMap, at).xyz * 2.0 - 1.0;
211
+ t.z = t.z > 0.0 ? t.z : sqrt(max(0.0, 1.0 - dot(t.xy, t.xy)));
212
+ return t;
213
+ }
214
+
215
+ /* The tangent-space texels blended by the shares. */
92
216
  vec3 layeredTexel() {
93
217
  vec3 sum = vec3(0.0);
94
218
  for (int i = 0; i < 5; i++) {
95
219
  if (float(i) >= uLayers[1].y) break;
96
- vec3 t = texture(uNormalMap, layerAt(i)).xyz * 2.0 - 1.0;
97
- t.z = t.z > 0.0 ? t.z : sqrt(max(0.0, 1.0 - dot(t.xy, t.xy)));
98
- sum += t * layerShare(i);
220
+ sum += layerStrengthen(i, layerNormalTexel(layerAt(i))) * layerShare(i);
99
221
  }
100
222
  float len = length(sum);
101
223
  return len > 1e-5 ? sum / len : vec3(0.0, 0.0, 1.0);
102
224
  }
103
225
 
226
+ /* Whether a normal read at the mesh's own coordinates lies under the layers. */
227
+ bool layeredMeshNormal() {
228
+ return uLayers[2].y > 0.5;
229
+ }
230
+
231
+ /* That normal's texel: the normal array's layer past the layers, at the mesh's coordinates. */
232
+ vec3 layeredMeshTexel() {
233
+ return layerNormalTexel(vec3(vUv.xy, layerExtras()));
234
+ }
235
+
104
236
  /* Whether the emissive map belongs to one layer rather than to the whole surface. */
105
237
  bool layeredGlows() {
106
238
  return uLayers[1].z > -0.5;