@driftengine/core 4.13.2 → 4.13.3

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 (32) hide show
  1. package/CHANGELOG.json +11 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.js +1 -0
  5. package/dist/render/backend/webgl2/renderer.d.ts +12 -0
  6. package/dist/render/backend/webgl2/renderer.js +47 -2
  7. package/dist/render/backend/webgpu/renderer.d.ts +9 -0
  8. package/dist/render/backend/webgpu/renderer.js +33 -1
  9. package/dist/render/shaders/flat/detail.d.ts +21 -0
  10. package/dist/render/shaders/flat/detail.js +74 -0
  11. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  12. package/dist/render/shaders/flat/glassTint.js +1 -0
  13. package/dist/render/shaders/flat/index.d.ts +5 -0
  14. package/dist/render/shaders/flat/index.js +3 -0
  15. package/dist/render/shaders/flat/main.d.ts +1 -1
  16. package/dist/render/shaders/flat/main.js +2 -0
  17. package/dist/render/shaders/generated/flat.wgsl.d.ts +144 -16
  18. package/dist/render/shaders/generated/flat.wgsl.js +183 -55
  19. package/dist/render/surfaceDetail.d.ts +79 -0
  20. package/dist/render/surfaceDetail.js +86 -0
  21. package/dist/render/surfaceTexture.d.ts +9 -0
  22. package/package.json +6 -6
  23. package/src/index.ts +2 -0
  24. package/src/render/backend/webgl2/renderer.ts +58 -2
  25. package/src/render/backend/webgpu/renderer.ts +42 -1
  26. package/src/render/shaders/flat/detail.ts +74 -0
  27. package/src/render/shaders/flat/glassTint.ts +3 -1
  28. package/src/render/shaders/flat/index.ts +8 -0
  29. package/src/render/shaders/flat/main.ts +2 -0
  30. package/src/render/shaders/generated/flat.wgsl.ts +183 -55
  31. package/src/render/surfaceDetail.ts +136 -0
  32. package/src/render/surfaceTexture.ts +9 -0
package/CHANGELOG.json CHANGED
@@ -1,4 +1,15 @@
1
1
  [
2
+ {
3
+ "version": "4.13.3",
4
+ "date": "2026-10-12",
5
+ "entries": [
6
+ {
7
+ "kind": "added",
8
+ "short": "A material can lay fine grain over its maps, a region at a time: up to sixteen detail normals and occlusions at their own repeats, chosen per texel: `SurfaceMaterial.detail`.",
9
+ "text": "`SurfaceMaterial.detail` lays a surface's finest grain over its base maps: up to sixteen regions, each a tangent-space detail normal map, and an occlusion, at its own repeat, offset and strengths, and a region map choosing which one each texel wears. A surface's grain, leather, a weave, brushed metal, a face's pores, is a map of a few hundred texels repeated tens of times across it, and baked into the base maps it is lost: a 256-texel detail tiled forty times over a 2048-texel map would want some ten thousand texels a side, so a consumer's characters looked smooth beside the surfaces they were authored with. Each texel reads its own region's detail alone, turned through a frame built on the normal the base maps made, and multiplies its occlusion into the surface's; the reads take explicit gradients, the base coordinates' times the region's repeat, since the region is the pixel's own. **Where the maps are, and why**: the lit stage had no texture binding to spare on either backend, so the detail maps are layers of the array read where a shading model's map is, and a material with a `modelMap` puts its detail layers in that array; the region map is a layer of the material's ORM array, one index a texel in its red, read exactly so a region's edge is sharp, which wants an uncompressed array. A material whose model-map slot holds another array, or that is placed by the world, is drawn without its detail, said once. Sixteen regions are thirty-three vectors: WebGL2 compiles them in only when a material first asks (`SURFACE_DETAIL`) and refuses them, said once, on a part with no room; on WebGPU every material's block grows from 1,024 bytes to 1,536. Measured on an RX 9070 XT (`demo/dev/detail.html`): where a material reads region 0 it draws exactly what one region with no region map draws, on WebGPU and on WebGL2 over Vulkan and over OpenGL; its other regions differ from it by 166,558 pixels; and the backends part by at most one level in a channel."
10
+ }
11
+ ]
12
+ },
2
13
  {
3
14
  "version": "4.13.2",
4
15
  "date": "2026-10-11",
package/README.md CHANGED
@@ -6,7 +6,7 @@ WebGL2 everywhere else, behind one API, and carries the fixed-step loop, the ren
6
6
  input, the scene graph, geometry, text and the whole of `@driftengine/physics`, which it re-exports.
7
7
  Everything else is a separate package a game adds when it reaches for it.
8
8
 
9
- **Cost: 924.2 KB gzipped, importing `createRenderer`.** Measured by `scripts/size-gate.test.mjs`,
9
+ **Cost: 929.4 KB gzipped, importing `createRenderer`.** Measured by `scripts/size-gate.test.mjs`,
10
10
  which fails the build if the figure drifts by more than 3%.
11
11
 
12
12
  The manual is at **[driftengine.dev/docs](https://driftengine.dev/docs)**, with an example running
package/dist/index.d.ts CHANGED
@@ -231,6 +231,8 @@ export { SurfaceTexture } from './render/surfaceTexture.ts';
231
231
  export type { SurfaceMaterial, SurfaceTextureOptions } from './render/surfaceTexture.ts';
232
232
  export type { SurfaceProjection } from './render/surfaceProjection.ts';
233
233
  export type { SurfaceLayers } from './render/surfaceLayers.ts';
234
+ export type { SurfaceDetail, SurfaceDetailRegion } from './render/surfaceDetail.ts';
235
+ export { MAX_DETAIL_REGIONS } from './render/surfaceDetail.ts';
234
236
  export type { SurfaceLayerLook } from './render/surfaceLayerLooks.ts';
235
237
  export { compressedFormatName, isCompressedSource, levelBytes, uploadsCompressed, } from './render/compressedSource.ts';
236
238
  export type { BlockFormat, CompressedTextureFormat, CompressedTextureSource, SurfaceSource, } from './render/compressedSource.ts';
package/dist/index.js CHANGED
@@ -287,6 +287,7 @@ export { MAX_POINT_LIGHTS, SURFACE_TEXTURE_UNIT } from './render/lightBudget.js'
287
287
  /* The width a light buffer can usefully be with `clusteredLights` on; `createPointLightBuffer`. */
288
288
  export { MAX_CLUSTERED_LIGHTS } from './render/clusteredLights.js';
289
289
  export { SurfaceTexture } from './render/surfaceTexture.js';
290
+ export { MAX_DETAIL_REGIONS } from './render/surfaceDetail.js';
290
291
  /*
291
292
  * Compressed textures — BC, ETC2 and EAC, ASTC: the source `createSurfaceTexture` takes as blocks,
292
293
  * the one question to ask before handing one over — whether this device takes its format
@@ -1300,6 +1300,13 @@ export declare class WebGL2Renderer implements RendererApi {
1300
1300
  /** Whether it has room for the layers' looks beside them, and whether the refusal was said. */
1301
1301
  private looksRoom;
1302
1302
  private warnedLooksRoom;
1303
+ /** `uDetail`, the material's tiled detail packed by `packSurfaceDetail`. */
1304
+ private readonly detailFloats;
1305
+ /** Whether the lit stage has room for detail on this part, asked once. See `detailOfMaterial`. */
1306
+ private detailRoom;
1307
+ private warnedDetailRoom;
1308
+ /** The refusals already said, each once. */
1309
+ private readonly warnedDetail;
1303
1310
  private warnedLayersModelMap;
1304
1311
  private warnedLayersTriplanar;
1305
1312
  private warnedOverlayRoom;
@@ -2199,6 +2206,11 @@ export declare class WebGL2Renderer implements RendererApi {
2199
2206
  * as they are, said once — which on a material sharing its arrays reads the wrong textures, so
2200
2207
  * the sentence names the way out.
2201
2208
  */
2209
+ /**
2210
+ * A material's detail, or null where it is refused: its slot holds another array, it is placed by
2211
+ * the world, or the lit stage has no room for its thirty-three vectors. Each said once.
2212
+ */
2213
+ private detailOfMaterial;
2202
2214
  private looksFit;
2203
2215
  /** Says once that a layered material's triplanar projection is laid on the horizontal plane. */
2204
2216
  private warnLayersTriplanar;
@@ -41,6 +41,7 @@ import { resolveTransmission } from '../../transmission.js';
41
41
  import { resolveOcclusionFade } from '../../occlusionFade.js';
42
42
  import { orthographicDepthSpan } from '../../lightMatrix.js';
43
43
  import { LAYER_FLOATS, layerMaskMap, layeredProjection, layersFitBeside, packSurfaceLayers, } from '../../surfaceLayers.js';
44
+ import { DETAIL_FLOATS, detailOf, detailRefusal, detailRefusalWarning, packSurfaceDetail, } from '../../surfaceDetail.js';
44
45
  import { LOOK_FLOATS, asksLayerLooks, packLayerLooks } from '../../surfaceLayerLooks.js';
45
46
  import { FILM_FRAG, FILM_VERT } from '../../shaders/film.js';
46
47
  import { SceneTarget } from '../../sceneTarget.js';
@@ -976,6 +977,13 @@ export class WebGL2Renderer {
976
977
  /** Whether it has room for the layers' looks beside them, and whether the refusal was said. */
977
978
  looksRoom = null;
978
979
  warnedLooksRoom = false;
980
+ /** `uDetail`, the material's tiled detail packed by `packSurfaceDetail`. */
981
+ detailFloats = new Float32Array(DETAIL_FLOATS);
982
+ /** Whether the lit stage has room for detail on this part, asked once. See `detailOfMaterial`. */
983
+ detailRoom = null;
984
+ warnedDetailRoom = false;
985
+ /** The refusals already said, each once. */
986
+ warnedDetail = new Set();
979
987
  warnedLayersModelMap = false;
980
988
  warnedLayersTriplanar = false;
981
989
  warnedOverlayRoom = false;
@@ -3307,11 +3315,18 @@ export class WebGL2Renderer {
3307
3315
  const modelMap = material?.modelMap ?? null;
3308
3316
  /* A layered material's mask, where it is a map, goes where a model's would: surfaceLayers.ts. */
3309
3317
  const layers = this.layersOf(material);
3310
- const boundMap = modelMap ?? layerMaskMap(layers);
3318
+ const taken = modelMap ?? layerMaskMap(layers);
3319
+ /* And its detail's array, where nothing else is read there: surfaceDetail.ts. */
3320
+ const detail = this.detailOfMaterial(material, taken);
3321
+ const boundMap = taken ?? detail?.maps ?? null;
3311
3322
  gl.activeTexture(gl.TEXTURE0 + MODEL_MAP_TEXTURE_UNIT);
3312
3323
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
3313
3324
  if (boundMap !== null)
3314
3325
  boundMap.bind(gl, MODEL_MAP_TEXTURE_UNIT);
3326
+ packSurfaceDetail(detail, (material?.orm ?? null) !== null, this.detailFloats);
3327
+ if (detail !== null)
3328
+ this.askLit('SURFACE_DETAIL');
3329
+ gl.uniform4fv(u['uDetail'] ?? null, this.detailFloats);
3315
3330
  packSurfaceLayers(layers, this.layerFloats);
3316
3331
  if (layers !== null)
3317
3332
  this.askLit('LAYERED');
@@ -3598,6 +3613,33 @@ export class WebGL2Renderer {
3598
3613
  * as they are, said once — which on a material sharing its arrays reads the wrong textures, so
3599
3614
  * the sentence names the way out.
3600
3615
  */
3616
+ /**
3617
+ * A material's detail, or null where it is refused: its slot holds another array, it is placed by
3618
+ * the world, or the lit stage has no room for its thirty-three vectors. Each said once.
3619
+ */
3620
+ detailOfMaterial(material, taken) {
3621
+ const detail = detailOf(material);
3622
+ if (detail === null)
3623
+ return null;
3624
+ const refusal = detailRefusal(detail, taken, (material?.projection ?? null) !== null);
3625
+ if (refusal !== null) {
3626
+ if (!this.warnedDetail.has(refusal)) {
3627
+ this.warnedDetail.add(refusal);
3628
+ console.warn(detailRefusalWarning(refusal));
3629
+ }
3630
+ return null;
3631
+ }
3632
+ this.detailRoom ??= this.switchFits('SURFACE_DETAIL');
3633
+ if (this.detailRoom)
3634
+ return detail;
3635
+ if (!this.warnedDetailRoom) {
3636
+ this.warnedDetailRoom = true;
3637
+ console.warn("Renderer: a material's detail is refused on this device. It takes thirty-three fragment " +
3638
+ 'uniform vectors the lit shader does not have, so the material is drawn without it. A ' +
3639
+ 'lower `maxLights` makes the room, or WebGPU has it.');
3640
+ }
3641
+ return null;
3642
+ }
3601
3643
  looksFit(layers) {
3602
3644
  if (!asksLayerLooks(layers))
3603
3645
  return false;
@@ -5595,6 +5637,7 @@ export class WebGL2Renderer {
5595
5637
  movingSun: asIf.MOVING_SUN ?? this.litOn.MOVING_SUN,
5596
5638
  layered: asIf.LAYERED ?? this.litOn.LAYERED,
5597
5639
  layerLooks: asIf.LAYER_LOOKS ?? this.litOn.LAYER_LOOKS,
5640
+ surfaceDetail: asIf.SURFACE_DETAIL ?? this.litOn.SURFACE_DETAIL,
5598
5641
  maxLights: budget.maxLights,
5599
5642
  maxAreaLights: budget.maxAreaLights,
5600
5643
  });
@@ -7454,9 +7497,10 @@ export class WebGL2Renderer {
7454
7497
  this.projection.fill(0);
7455
7498
  /* Nor light through from behind. See `SurfaceMaterial.diffuseTransmission`. */
7456
7499
  resolveTransmission(null, this.transmission);
7457
- /* Nor layers: one layer until a material asks for more. */
7500
+ /* Nor layers: one layer until a material asks for more. Nor detail. */
7458
7501
  this.layerFloats.fill(0);
7459
7502
  this.lookFloats.fill(0);
7503
+ this.detailFloats.fill(0);
7460
7504
  this.overlayMaps = null;
7461
7505
  this.overlayTexture = null;
7462
7506
  /* Nor an ambient of its own: the frame's, until a draw asks. See `setAmbientSH`. */
@@ -7601,6 +7645,7 @@ export class WebGL2Renderer {
7601
7645
  gl.uniform4fv(u['uWorldUv'] ?? null, this.projection);
7602
7646
  gl.uniform4fv(u['uLayers'] ?? null, this.layerFloats);
7603
7647
  gl.uniform4fv(u['uLayerLooks'] ?? null, this.lookFloats);
7648
+ gl.uniform4fv(u['uDetail'] ?? null, this.detailFloats);
7604
7649
  /* Neither refracting nor glass, which is every draw that does not say otherwise. See
7605
7650
  `bindSeeThrough` for the four numbers in the one vector. */
7606
7651
  gl.uniform4f(u['uSeeThrough'] ?? null, 0, 0, 0, 0);
@@ -1208,6 +1208,10 @@ export declare class WebGPURenderer implements RendererApi {
1208
1208
  */
1209
1209
  private readonly transmission;
1210
1210
  private warnedLayersModelMap;
1211
+ /** `uDetail`, the material's tiled detail packed by `packSurfaceDetail`. */
1212
+ private readonly detailFloats;
1213
+ /** The detail refusals already said, each once. See `detailOfMaterial`. */
1214
+ private readonly warnedDetail;
1211
1215
  private warnedLayersTriplanar;
1212
1216
  /** Its numbers, `uModelParams`, packed by `packModel`. */
1213
1217
  private readonly modelParams;
@@ -2112,6 +2116,11 @@ export declare class WebGPURenderer implements RendererApi {
2112
2116
  * A material's layers where they can be drawn: none asked, or a model's map in the mask's place,
2113
2117
  * said once, leave one layer.
2114
2118
  */
2119
+ /**
2120
+ * A material's detail, or null where it is refused: its slot holds another array, or it is placed
2121
+ * by the world. Each said once. This backend has room for its vectors wherever it runs.
2122
+ */
2123
+ private detailOfMaterial;
2115
2124
  private layersOf;
2116
2125
  /** Says once that a layered material's triplanar projection is laid on the horizontal plane. */
2117
2126
  private warnLayersTriplanar;
@@ -70,6 +70,7 @@ import { resolveTransmission } from '../../transmission.js';
70
70
  import { resolveOcclusionFade } from '../../occlusionFade.js';
71
71
  import { orthographicDepthSpan } from '../../lightMatrix.js';
72
72
  import { LAYER_FLOATS, layerMaskMap, layeredProjection, layersFitBeside, packSurfaceLayers, } from '../../surfaceLayers.js';
73
+ import { DETAIL_FLOATS, detailOf, detailRefusal, detailRefusalWarning, packSurfaceDetail, } from '../../surfaceDetail.js';
73
74
  import { LOOK_FLOATS, asksLayerLooks, packLayerLooks } from '../../surfaceLayerLooks.js';
74
75
  import { frameReflectionsRefused, resolveFrameReflections, } from '../../frameReflections.js';
75
76
  import { REFLECTION_SURFACE_FORMAT, ReflectionSurfacePass } from './reflectionSurfacePass.js';
@@ -2284,6 +2285,10 @@ export class WebGPURenderer {
2284
2285
  */
2285
2286
  transmission = new Float32Array([-1, -1, -1, 0]);
2286
2287
  warnedLayersModelMap = false;
2288
+ /** `uDetail`, the material's tiled detail packed by `packSurfaceDetail`. */
2289
+ detailFloats = new Float32Array(DETAIL_FLOATS);
2290
+ /** The detail refusals already said, each once. See `detailOfMaterial`. */
2291
+ warnedDetail = new Set();
2287
2292
  warnedLayersTriplanar = false;
2288
2293
  /** Its numbers, `uModelParams`, packed by `packModel`. */
2289
2294
  modelParams = new Float32Array(MODEL_PARAM_FLOATS);
@@ -2688,7 +2693,10 @@ export class WebGPURenderer {
2688
2693
  const ownModelMap = (material?.modelMap ?? null);
2689
2694
  /* A layered material's mask, where it is a map, goes where a model's would: surfaceLayers.ts. */
2690
2695
  const layers = this.layersOf(material);
2691
- const modelMap = ownModelMap ?? layerMaskMap(layers);
2696
+ const taken = ownModelMap ?? layerMaskMap(layers);
2697
+ /* And its detail's array, where nothing else is read there: surfaceDetail.ts. */
2698
+ const detail = this.detailOfMaterial(material, taken);
2699
+ const modelMap = taken ?? detail?.maps ?? null;
2692
2700
  this.materialModel = material?.model ?? null;
2693
2701
  const reflectivity = material?.reflectivity;
2694
2702
  const environmentGain = material?.environmentGain;
@@ -2772,6 +2780,11 @@ export class WebGPURenderer {
2772
2780
  f.set(this.lookFloats, this.materialField('uLayerLooks'));
2773
2781
  if (asksLayerLooks(layers))
2774
2782
  this.enableLit('LAYER_LOOKS');
2783
+ /* And its tiled detail, the first time a material lays any: detail.ts. */
2784
+ packSurfaceDetail(detail, orm !== null, this.detailFloats);
2785
+ f.set(this.detailFloats, this.materialField('uDetail'));
2786
+ if (detail !== null)
2787
+ this.enableLit('SURFACE_DETAIL');
2775
2788
  f.set(this.modelParams, this.materialField('uModelParams'));
2776
2789
  });
2777
2790
  /*
@@ -5142,6 +5155,23 @@ export class WebGPURenderer {
5142
5155
  * A material's layers where they can be drawn: none asked, or a model's map in the mask's place,
5143
5156
  * said once, leave one layer.
5144
5157
  */
5158
+ /**
5159
+ * A material's detail, or null where it is refused: its slot holds another array, or it is placed
5160
+ * by the world. Each said once. This backend has room for its vectors wherever it runs.
5161
+ */
5162
+ detailOfMaterial(material, taken) {
5163
+ const detail = detailOf(material);
5164
+ if (detail === null)
5165
+ return null;
5166
+ const refusal = detailRefusal(detail, taken, (material?.projection ?? null) !== null);
5167
+ if (refusal === null)
5168
+ return detail;
5169
+ if (!this.warnedDetail.has(refusal)) {
5170
+ this.warnedDetail.add(refusal);
5171
+ console.warn(detailRefusalWarning(refusal));
5172
+ }
5173
+ return null;
5174
+ }
5145
5175
  layersOf(material) {
5146
5176
  const layers = material?.layers ?? null;
5147
5177
  if (layers === null || layers.repeats.length === 0)
@@ -7312,6 +7342,8 @@ export class WebGPURenderer {
7312
7342
  /* Nor layers: one layer until a material asks for more. */
7313
7343
  f.fill(0, at('uLayers'), at('uLayers') + LAYER_FLOATS);
7314
7344
  f.fill(0, at('uLayerLooks'), at('uLayerLooks') + LOOK_FLOATS);
7345
+ /* Nor detail. */
7346
+ f.fill(0, at('uDetail'), at('uDetail') + DETAIL_FLOATS);
7315
7347
  f[at('uCutout')] = 0;
7316
7348
  f[at('uCutout') + 1] = 0;
7317
7349
  this.cutoutResolveStaged = 'hard';
@@ -0,0 +1,21 @@
1
+ /**
2
+ * A material's tiled detail over whatever its maps made: `SurfaceMaterial.detail`, packed into
3
+ * `uDetail` by `surfaceDetail.ts`.
4
+ *
5
+ * **Each texel wears its own region's detail alone.** The region is read exactly from the ORM
6
+ * array's layer the material names, at the base coordinates, so an index is never blended with its
7
+ * neighbour's at a region's edge; then that region's normal map is read at its repeat and offset,
8
+ * turned through a frame built on the normal the base maps made — the construction a layered
9
+ * material lays its layers over a mesh normal with — and its occlusion multiplied into the
10
+ * surface's. A strength scales the detail's slopes as a layer's look does.
11
+ *
12
+ * **Read with explicit gradients**, the base coordinates' times the region's repeat, because the
13
+ * region is the pixel's own: a read whose level is implicit is legal only in uniform control flow
14
+ * (AGENTS.md 2026-08-07), and across a region's edge the repeated coordinates jump, which would
15
+ * read a level as coarse as the jump. The frame and the gradients are taken first, under the
16
+ * uniform branch that calls this.
17
+ *
18
+ * **A lit switch, `SURFACE_DETAIL`**, off until a material first asks, declared after `LAYERED` and
19
+ * `LAYER_LOOKS` so each keeps its id. Compiled out, its thirty-three vectors are not declared.
20
+ */
21
+ export declare function detailGlsl(on: boolean): string;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * A material's tiled detail over whatever its maps made: `SurfaceMaterial.detail`, packed into
3
+ * `uDetail` by `surfaceDetail.ts`.
4
+ *
5
+ * **Each texel wears its own region's detail alone.** The region is read exactly from the ORM
6
+ * array's layer the material names, at the base coordinates, so an index is never blended with its
7
+ * neighbour's at a region's edge; then that region's normal map is read at its repeat and offset,
8
+ * turned through a frame built on the normal the base maps made — the construction a layered
9
+ * material lays its layers over a mesh normal with — and its occlusion multiplied into the
10
+ * surface's. A strength scales the detail's slopes as a layer's look does.
11
+ *
12
+ * **Read with explicit gradients**, the base coordinates' times the region's repeat, because the
13
+ * region is the pixel's own: a read whose level is implicit is legal only in uniform control flow
14
+ * (AGENTS.md 2026-08-07), and across a region's edge the repeated coordinates jump, which would
15
+ * read a level as coarse as the jump. The frame and the gradients are taken first, under the
16
+ * uniform branch that calls this.
17
+ *
18
+ * **A lit switch, `SURFACE_DETAIL`**, off until a material first asks, declared after `LAYERED` and
19
+ * `LAYER_LOOKS` so each keeps its id. Compiled out, its thirty-three vectors are not declared.
20
+ */
21
+ export function detailGlsl(on) {
22
+ if (!on) {
23
+ return /* glsl */ `
24
+ const bool SURFACE_DETAIL = false; // wgsl:override
25
+ bool detailed() { return false; }
26
+ void surfaceDetail(inout vec3 n, inout float occlusion, vec2 uv) {}
27
+ `;
28
+ }
29
+ return /* glsl */ `
30
+ /* Whether this program lays a material's detail. Off but where one has asked: detail.ts. */
31
+ const bool SURFACE_DETAIL = true; // wgsl:override
32
+ /*
33
+ * Thirty-three vectors, as surfaceDetail.ts packs them: the region count (x) and the ORM array's
34
+ * layer holding the region map, or -1 for none (y); then two a region, the normal map's layer (x),
35
+ * the occlusion's or -1 (y), the normal strength (z) and the occlusion strength (w); its repeats
36
+ * across and down (x y) and its offset (z w).
37
+ */
38
+ uniform vec4 uDetail[33]; // wgsl:material
39
+
40
+ bool detailed() { return uDetail[0].x > 0.0; }
41
+
42
+ /* The region a texel of the base maps is in, read exactly, held to the regions there are. */
43
+ int detailRegion(vec2 uv) {
44
+ int last = int(uDetail[0].x) - 1;
45
+ if (uDetail[0].y < 0.0) return 0;
46
+ ivec2 size = textureSize(uOrmMap, 0).xy;
47
+ ivec2 texel = clamp(ivec2(floor(fract(uv) * vec2(size))), ivec2(0), size - 1);
48
+ int region = int(texelFetch(uOrmMap, ivec3(texel, int(uDetail[0].y)), 0).r * 255.0 + 0.5);
49
+ return clamp(region, 0, last);
50
+ }
51
+
52
+ void surfaceDetail(inout vec3 n, inout float occlusion, vec2 uv) {
53
+ /* In uniform control flow, before anything that depends on the pixel's region. */
54
+ mat3 frame = tangentFrame(n, vWorldPos, uv, vTangent, vHasTangents);
55
+ vec2 dx = dFdx(uv);
56
+ vec2 dy = dFdy(uv);
57
+ int region = detailRegion(uv);
58
+ vec4 picks = uDetail[1 + region * 2];
59
+ vec4 place = uDetail[2 + region * 2];
60
+ vec2 at = uv * place.xy + place.zw;
61
+ vec2 gx = dx * place.xy;
62
+ vec2 gy = dy * place.xy;
63
+ vec3 d = textureGrad(uModelMap, vec3(at, picks.x), gx, gy).xyz * 2.0 - 1.0;
64
+ /* z rebuilt where none is stored, as the base map's is: normalMap.ts. */
65
+ d.z = d.z > 0.0 ? d.z : sqrt(max(0.0, 1.0 - dot(d.xy, d.xy)));
66
+ d = normalize(vec3(d.xy * picks.z, max(d.z, 1e-4)));
67
+ n = normalize(frame * d);
68
+ if (picks.y >= 0.0) {
69
+ float held = textureGrad(uModelMap, vec3(at, picks.y), gx, gy).r;
70
+ occlusion *= mix(1.0, held, picks.w);
71
+ }
72
+ }
73
+ `;
74
+ }
@@ -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' | 'LAYER_LOOKS';
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' | 'SURFACE_DETAIL';
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`. */
@@ -38,6 +38,7 @@ export function noLitSwitches() {
38
38
  MOVING_SUN: false,
39
39
  LAYERED: false,
40
40
  LAYER_LOOKS: false,
41
+ SURFACE_DETAIL: false,
41
42
  };
42
43
  }
43
44
  /**
@@ -145,6 +145,11 @@ export interface FlatShaderOptions {
145
145
  * \`uLayerLooks\` and all. True when absent; nothing without \`layered\`. See \`layerLooksGlsl\`.
146
146
  */
147
147
  readonly layerLooks?: boolean;
148
+ /**
149
+ * Whether it lays a material's tiled detail over its maps, \`uDetail\` and all
150
+ * (\`SurfaceMaterial.detail\`). True when absent. See \`detailGlsl\`.
151
+ */
152
+ readonly surfaceDetail?: boolean;
148
153
  /**
149
154
  * How many point lights this build declares room for. `MAX_POINT_LIGHTS` when absent.
150
155
  *
@@ -24,6 +24,7 @@ import { overlayGlsl } from './overlay.js';
24
24
  import { reflectionSurfaceGlsl } from './reflectionSurface.js';
25
25
  import { worldUvGlsl } from './worldUv.js';
26
26
  import { movingSunGlsl } from './movingSun.js';
27
+ import { detailGlsl } from './detail.js';
27
28
  import { layeredGlsl } from './layered.js';
28
29
  import { MAP_READS_GLSL } from './mapReads.js';
29
30
  import { CHANNEL_BEND, LIT_CHANNEL_ATTRIBUTE } from '../vertexChannel.js';
@@ -514,6 +515,8 @@ export function flatFrag(options) {
514
515
  worldUvGlsl(options.worldUvs ?? true),
515
516
  /* After the projection, so its switch takes the next id; the normal map below calls it. */
516
517
  layeredGlsl(options.layered ?? true, options.layerLooks ?? true),
518
+ /* After the layers, so its switch takes the next id; main calls it. See detail.ts. */
519
+ detailGlsl(options.surfaceDetail ?? true),
517
520
  /* Where each map is read, after both of the ways it can be read otherwise. See mapReads.ts. */
518
521
  MAP_READS_GLSL,
519
522
  /* After the frame and the projection it calls, unconditional for the same reason as the