@driftengine/core 4.13.1 → 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 (78) hide show
  1. package/CHANGELOG.json +42 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +4 -1
  4. package/dist/index.js +1 -0
  5. package/dist/render/backend/api.d.ts +63 -8
  6. package/dist/render/backend/webgl2/pointSkinFeedback.d.ts +23 -0
  7. package/dist/render/backend/webgl2/pointSkinFeedback.js +159 -0
  8. package/dist/render/backend/webgl2/renderer.d.ts +33 -0
  9. package/dist/render/backend/webgl2/renderer.js +88 -4
  10. package/dist/render/backend/webgpu/buffers.d.ts +10 -2
  11. package/dist/render/backend/webgpu/buffers.js +66 -16
  12. package/dist/render/backend/webgpu/clothTextures.d.ts +6 -3
  13. package/dist/render/backend/webgpu/clothTextures.js +14 -11
  14. package/dist/render/backend/webgpu/drawCommand.js +3 -3
  15. package/dist/render/backend/webgpu/gpuClothSet.js +1 -0
  16. package/dist/render/backend/webgpu/groupShelf.d.ts +31 -0
  17. package/dist/render/backend/webgpu/groupShelf.js +54 -0
  18. package/dist/render/backend/webgpu/pointSkinPass.d.ts +27 -0
  19. package/dist/render/backend/webgpu/pointSkinPass.js +104 -0
  20. package/dist/render/backend/webgpu/renderer.d.ts +40 -7
  21. package/dist/render/backend/webgpu/renderer.js +151 -48
  22. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +1 -1
  23. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +7 -2
  24. package/dist/render/backend/webgpu/shaders/pointSkin.wgsl.d.ts +13 -0
  25. package/dist/render/backend/webgpu/shaders/pointSkin.wgsl.js +57 -0
  26. package/dist/render/backend/webgpu/vertexFormats.js +1 -1
  27. package/dist/render/lightBudget.d.ts +6 -0
  28. package/dist/render/lightBudget.js +6 -0
  29. package/dist/render/mesh.d.ts +19 -5
  30. package/dist/render/mesh.js +39 -8
  31. package/dist/render/pointSkin.d.ts +55 -0
  32. package/dist/render/pointSkin.js +121 -0
  33. package/dist/render/rendererHarness.js +6 -0
  34. package/dist/render/shader.d.ts +3 -1
  35. package/dist/render/shader.js +6 -1
  36. package/dist/render/shaders/flat/detail.d.ts +21 -0
  37. package/dist/render/shaders/flat/detail.js +74 -0
  38. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  39. package/dist/render/shaders/flat/glassTint.js +1 -0
  40. package/dist/render/shaders/flat/index.d.ts +5 -0
  41. package/dist/render/shaders/flat/index.js +3 -0
  42. package/dist/render/shaders/flat/main.d.ts +1 -1
  43. package/dist/render/shaders/flat/main.js +2 -0
  44. package/dist/render/shaders/generated/flat.wgsl.d.ts +144 -16
  45. package/dist/render/shaders/generated/flat.wgsl.js +183 -55
  46. package/dist/render/surfaceDetail.d.ts +79 -0
  47. package/dist/render/surfaceDetail.js +86 -0
  48. package/dist/render/surfaceTexture.d.ts +9 -0
  49. package/dist/render/vertexPacking.d.ts +16 -4
  50. package/dist/render/vertexPacking.js +10 -3
  51. package/package.json +6 -6
  52. package/src/index.ts +4 -0
  53. package/src/render/backend/api.ts +70 -7
  54. package/src/render/backend/webgl2/pointSkinFeedback.ts +173 -0
  55. package/src/render/backend/webgl2/renderer.ts +121 -4
  56. package/src/render/backend/webgpu/buffers.ts +90 -16
  57. package/src/render/backend/webgpu/clothTextures.ts +14 -11
  58. package/src/render/backend/webgpu/drawCommand.ts +3 -3
  59. package/src/render/backend/webgpu/gpuClothSet.ts +1 -0
  60. package/src/render/backend/webgpu/groupShelf.ts +74 -0
  61. package/src/render/backend/webgpu/pointSkinPass.ts +115 -0
  62. package/src/render/backend/webgpu/renderer.ts +192 -83
  63. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +7 -2
  64. package/src/render/backend/webgpu/shaders/pointSkin.wgsl.ts +58 -0
  65. package/src/render/backend/webgpu/vertexFormats.ts +1 -1
  66. package/src/render/lightBudget.ts +7 -0
  67. package/src/render/mesh.ts +49 -7
  68. package/src/render/pointSkin.ts +178 -0
  69. package/src/render/rendererHarness.ts +6 -0
  70. package/src/render/shader.ts +5 -0
  71. package/src/render/shaders/flat/detail.ts +74 -0
  72. package/src/render/shaders/flat/glassTint.ts +3 -1
  73. package/src/render/shaders/flat/index.ts +8 -0
  74. package/src/render/shaders/flat/main.ts +2 -0
  75. package/src/render/shaders/generated/flat.wgsl.ts +183 -55
  76. package/src/render/surfaceDetail.ts +136 -0
  77. package/src/render/surfaceTexture.ts +9 -0
  78. package/src/render/vertexPacking.ts +25 -5
package/CHANGELOG.json CHANGED
@@ -1,4 +1,46 @@
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
+ },
13
+ {
14
+ "version": "4.13.2",
15
+ "date": "2026-10-11",
16
+ "entries": [
17
+ {
18
+ "kind": "added",
19
+ "short": "A mesh stored as weighted points in its joints' own spaces is posed on the device, so the main thread hands over the joints and nothing else: `createPointSkin` and `posePointSkin`.",
20
+ "text": "`createPointSkin(mesh, data)` and `posePointSkin(skin, matrices)` pose a dynamic mesh on the device from a format some models are stored in: each vertex up to four points, each in the space of the joint it follows, with a weight each, and a normal in the space of its first joint (`PointSkinData`). Posed, a vertex is the weighted sum of each point moved by its joint, which ordinary skinning cannot express, since it moves one bind-pose position by a blend of joints; such a rig was posed on the main thread and its positions uploaded every frame, measured by a consumer at 38% of the main thread's drawing time. WebGPU poses in a compute pass and WebGL2 by transform feedback, writing the mesh's own positions and normals, so every pass draws it as any dynamic mesh, its shadow and its motion included, and a skinned vertex stage that has no attribute left for four more points is untouched. The joints are each joint's transform into the mesh's model space, sixteen floats a joint with no inverse bind. The mesh's bounds become a box around each joint's reach, scaled by the joint's largest axis and by the most a vertex's weights sum to, so it is culled as it moves without the posed vertices coming back to the CPU. Measured on an RX 9070 XT (`demo/dev/pointSkin.html`): the device's pose draws the picture the same pose computed on the CPU draws, to the pixel on WebGPU and within 6 pixels on WebGL2, and differs from the rest pose by 70,792; under reconstruction the two part by 743 pixels a level deep, the rounding of single against double precision, where a motion fault parts them by 43,629."
21
+ },
22
+ {
23
+ "kind": "added",
24
+ "short": "A mesh's colours can be rewritten in place: a mesh made `{ dynamicColors: true }` takes `updateMeshColors`, so ground shaded again every frame is no longer made again.",
25
+ "text": "`updateMeshColors(mesh, colors)` rewrites a mesh's colours, three linear floats a vertex, on a mesh created with `{ dynamicColors: true }`, which keeps them as floats in a buffer of their own. Nothing else moves: not the positions, not the bounds, and no motion vector. `updateMesh` rewrites positions and normals only, so a surface whose colour changes while it stays where it is, such as ground darkened with its distance from a moving camera, was made again as a new mesh nearly every frame: a consumer measured 295 of 301 frames measured, a fifth of the drawing's main-thread time. The option is independent of `dynamic`, and costs twelve bytes a vertex where sixteen-bit colours cost six or eight and one colour every vertex shares costs nothing. On WebGPU the colours are the mesh's last vertex buffer, so every other buffer stays where it was, and every pipeline that draws such a mesh, the shadow pass's included, reads them there. Measured on an RX 9070 XT (`demo/dev/meshColors.html`): colours rewritten in place draw the picture a mesh made with the same colours draws, to the pixel on both backends, drawn alone and instanced."
26
+ },
27
+ {
28
+ "kind": "added",
29
+ "short": "A skinned cloth can be drawn back toward its skinned shape, so styled hair keeps the shape it was made in at rest and swings out of it as the body moves: `pull`, a stiffness.",
30
+ "text": "`SkinnedClothParameters.pull` draws every free particle of a garment back toward where its skinning puts it, as a stiffness: metres a second squared for every metre away. Applied first among the limits, after the constraints, each substep keeping `1 / (1 + pull · h²)` of a particle's offset, which is a compliant constraint's step, so the shape it holds against gravity is one length at every step: a particle at rest under gravity sits `9.81 / pull` metres below its skinned place. The limits could not do this, since a max distance is a sphere a particle may fall anywhere inside, so styled hair pinned at its roots fell to its edge and hung over the face. **A stiffness rather than a share of the offset kept a second as `damping` is, which is what was asked for**: a share scaled by the step holds a sag proportional to it, measured on hair authored to keep 0.7 of its offset a 1/60 step at 6.36, 3.49 and 1.83 mm at 1/60, 1/120 and 1/240, where the stiffness holds 6.36 mm at all three. A share `k` kept a step `s` is `(1 / k - 1) / s²` and keeps exactly `k` at that step. The same on the device and on the CPU: with a pull, the device's solve parts from the CPU's by 0.001 mm, no more than the CPU parts from itself nudged a tenth of a micrometre a frame (`demo/dev/skinnedCloth.html?parity=1&pull=400`)."
31
+ },
32
+ {
33
+ "kind": "added",
34
+ "short": "An Android app can be held the way its game is played: `\"android\": { \"orientation\": \"sensorLandscape\" }` in drift.package.json, written into the activity.",
35
+ "text": "`drift.package.json`'s `android.orientation` is written into the activity's `android:screenOrientation`, so an app starts the right way round before any of the game's code runs, which a `lockOrientation` taken at run time cannot do. It takes every value Android documents (`landscape`, `sensorLandscape`, `portrait`, `sensorPortrait`, `userLandscape`, `fullSensor` and the rest) and refuses one it does not when the manifest is read, naming the field, since Android ignores an attribute value it does not know and the build would quietly be the phone's choice again. `unspecified`, the default, leaves the copied manifest exactly as it was. Until now the template's `unspecified` was the only answer, so a game held sideways built from a copy of the packager's templates, copied again from each release. `drift-package doctor` prints it beside the permissions and the cleartext policy."
36
+ },
37
+ {
38
+ "kind": "fixed",
39
+ "short": "A skinned draw that follows cloth binds the groups it made before, where WebGPU made them again nearly every frame: 24 a frame for two characters, and none now.",
40
+ "text": "A skinned draw's bind group was kept one a palette slot and material and made again whenever a resource in it differed from the last draw's. A garment's particles take turns between two textures, a step each, so a draw following cloth found the other texture every frame; and several meshes following one cloth bind one particle texture each with a binding of its own, so they replaced each other's group draw by draw. A consumer counted 24 groups made a frame for two characters, and none with cloth off. Each key now keeps a shelf of up to sixteen groups, each found again by the four resources it was built with, so a steady garment's draws make none; the cast shadows' groups, which missed the same way, keep a shelf a palette slot. And the views a draw of one binding binds with a set of particles are kept for every binding rather than for the last, which made two objects a draw as several meshes took turns. Measured on an RX 9070 XT (`demo/dev/skinnedCloth.html`): the garment, the garment under reconstruction and three garments stepped as one draw the picture a build that makes every group again draws, to the pixel, where binding last step's particles moves 18,321 pixels."
41
+ }
42
+ ]
43
+ },
2
44
  {
3
45
  "version": "4.13.1",
4
46
  "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: 920.9 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
@@ -155,8 +155,9 @@ export { BLOOM_LEVELS } from './render/bloomChain.ts';
155
155
  export type { BloomResponse } from './render/bloomChain.ts';
156
156
  export { probeDevice } from './render/backend/probe.ts';
157
157
  export type { ProbeVerdict } from './render/backend/probe.ts';
158
- export type { BoltHandle, CausticsHandle, ClothBindingHandle, ClothParticlesHandle, FlockHandle, IncrementalMeshHandle, LineHandle, MeshHandle, MeshOptions, PrepareOptions, RenderBackend, RendererApi, ParticleHandle, PlumeHandle, ScatterHandle, InstancedHandle, SdfTextHandle, SurfaceTextureHandle, TextHandle, WaterHandle, WindStreakHandle, } from './render/backend/api.ts';
158
+ export type { BoltHandle, CausticsHandle, ClothBindingHandle, PointSkinHandle, ClothParticlesHandle, FlockHandle, IncrementalMeshHandle, LineHandle, MeshHandle, MeshOptions, PrepareOptions, RenderBackend, RendererApi, ParticleHandle, PlumeHandle, ScatterHandle, InstancedHandle, SdfTextHandle, SurfaceTextureHandle, TextHandle, WaterHandle, WindStreakHandle, } from './render/backend/api.ts';
159
159
  export type { ClothBindingData } from './render/clothBindingData.ts';
160
+ export type { PointSkinData } from './render/pointSkin.ts';
160
161
  export { anisotropicModel, eyeModel, hairModel, lightmapModel, skinModel, } from './render/surfaceModel.ts';
161
162
  export type { AnisotropicModel, EyeModel, HairModel, LightmapModel, SkinModel, SurfaceModel, SurfaceModelKind, } from './render/surfaceModel.ts';
162
163
  export { DEFAULT_FRAME_REFLECTIONS } from './render/frameReflections.ts';
@@ -230,6 +231,8 @@ export { SurfaceTexture } from './render/surfaceTexture.ts';
230
231
  export type { SurfaceMaterial, SurfaceTextureOptions } from './render/surfaceTexture.ts';
231
232
  export type { SurfaceProjection } from './render/surfaceProjection.ts';
232
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';
233
236
  export type { SurfaceLayerLook } from './render/surfaceLayerLooks.ts';
234
237
  export { compressedFormatName, isCompressedSource, levelBytes, uploadsCompressed, } from './render/compressedSource.ts';
235
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
@@ -12,6 +12,7 @@ import type { CompressedTextureSource, SurfaceSource } from '../compressedSource
12
12
  import type { LightmapPage } from '../lightmap.ts';
13
13
  import type { DeviceParticles } from '../particlePool.ts';
14
14
  import type { ClothBindingData } from '../clothBindingData.ts';
15
+ import type { PointSkinData } from '../pointSkin.ts';
15
16
  import type { TextStyle } from '../textLayout.ts';
16
17
  import type { SdfFont } from '../sdfFont.ts';
17
18
  import type { SdfTextStyle } from '../sdfTextLayout.ts';
@@ -49,6 +50,12 @@ export interface MeshHandle {
49
50
  */
50
51
  export interface ClothBindingHandle {
51
52
  }
53
+ /**
54
+ * A dynamic mesh's point skin on the device, from `createPointSkin`: its vertices as weighted points
55
+ * in its joints' spaces. Opaque likewise.
56
+ */
57
+ export interface PointSkinHandle {
58
+ }
52
59
  /** One character's cloth particles on the device, from `createClothParticles`. Opaque likewise. */
53
60
  export interface ClothParticlesHandle {
54
61
  }
@@ -190,15 +197,24 @@ export interface MeshOptions {
190
197
  /**
191
198
  * Whether this mesh's positions will be rewritten with `updateMesh`.
192
199
  *
193
- * Costs a `DYNAMIC_DRAW` hint on WebGL2 and a CPU-side copy of the interleaved vertex data on
194
- * WebGPU, which interleaves every attribute into one buffer and so cannot patch a position
195
- * without the rest of the vertex in hand. Both are paid only where a consumer asks.
200
+ * Costs a `DYNAMIC_DRAW` hint on WebGL2, and on WebGPU two buffers of its own for the positions
201
+ * and the normals beside the rest of the vertex, bound with it on every draw, and floats for the
202
+ * normals where they might have been sixteen bits. Paid only where a consumer asks.
196
203
  */
197
204
  readonly dynamic?: boolean;
205
+ /**
206
+ * Whether this mesh's colours will be rewritten with `updateMeshColors`.
207
+ *
208
+ * Costs the colours as floats in a buffer of their own, twelve bytes a vertex where they might
209
+ * have been sixteen bits or, for one colour every vertex shares, nothing at all. Independent of
210
+ * `dynamic`: a surface shaded again as a camera moves rewrites its colours every frame and its
211
+ * positions never, and should pay for neither the positions' buffers nor their motion.
212
+ */
213
+ readonly dynamicColors?: boolean;
198
214
  }
199
215
  export type RendererApi = Omit<{
200
216
  [K in keyof Renderer]: Renderer[K];
201
- }, 'gpuTimer' | 'createMesh' | 'createMeshIncremental' | 'updateMesh' | 'drawMesh' | 'disposeMesh' | 'drawTranslucentMesh' | 'drawLightVolume' | 'drawFilm' | 'createScatter' | 'drawScatter' | 'uploadScatter' | 'disposeScatter' | 'createInstanced' | 'uploadInstanced' | 'drawInstanced' | 'drawTranslucentInstanced' | 'disposeInstanced' | 'createWater' | 'drawWater' | 'disposeWater' | 'createPlumes' | 'drawPlumes' | 'disposePlumes' | 'setPlumeScale' | 'createCaustics' | 'drawCaustics' | 'disposeCaustics' | 'createParticles' | 'drawParticles' | 'drawDeviceParticles' | 'disposeParticles' | 'createWindStreaks' | 'drawWindStreaks' | 'disposeWindStreaks' | 'createFlock' | 'drawFlock' | 'disposeFlock' | 'createBolts' | 'drawBolts' | 'disposeBolts' | 'createLines' | 'drawLines' | 'disposeLines' | 'createText' | 'setText' | 'setPlate' | 'drawText' | 'textWidth' | 'disposeText' | 'createSdfText' | 'setSdfText' | 'drawSdfText' | 'disposeSdfText' | 'createSurfaceTexture' | 'createSurfaceTextureArray' | 'createLightmap' | 'createSceneCapture' | 'captureScene' | 'updateSurfaceTexture' | 'disposeSurfaceTexture' | 'setSurfaceTexture' | 'setMaterial' | 'prepareMesh' | 'prepareInstanced' | 'setSurfaceOverlay' | 'createClothBinding' | 'createClothParticles' | 'updateClothParticles' | 'setCloth' | 'disposeClothBinding' | 'disposeClothParticles'> & {
217
+ }, 'gpuTimer' | 'createMesh' | 'createMeshIncremental' | 'updateMesh' | 'updateMeshColors' | 'drawMesh' | 'disposeMesh' | 'drawTranslucentMesh' | 'drawLightVolume' | 'drawFilm' | 'createScatter' | 'drawScatter' | 'uploadScatter' | 'disposeScatter' | 'createInstanced' | 'uploadInstanced' | 'drawInstanced' | 'drawTranslucentInstanced' | 'disposeInstanced' | 'createWater' | 'drawWater' | 'disposeWater' | 'createPlumes' | 'drawPlumes' | 'disposePlumes' | 'setPlumeScale' | 'createCaustics' | 'drawCaustics' | 'disposeCaustics' | 'createParticles' | 'drawParticles' | 'drawDeviceParticles' | 'disposeParticles' | 'createWindStreaks' | 'drawWindStreaks' | 'disposeWindStreaks' | 'createFlock' | 'drawFlock' | 'disposeFlock' | 'createBolts' | 'drawBolts' | 'disposeBolts' | 'createLines' | 'drawLines' | 'disposeLines' | 'createText' | 'setText' | 'setPlate' | 'drawText' | 'textWidth' | 'disposeText' | 'createSdfText' | 'setSdfText' | 'drawSdfText' | 'disposeSdfText' | 'createSurfaceTexture' | 'createSurfaceTextureArray' | 'createLightmap' | 'createSceneCapture' | 'captureScene' | 'updateSurfaceTexture' | 'disposeSurfaceTexture' | 'setSurfaceTexture' | 'setMaterial' | 'prepareMesh' | 'prepareInstanced' | 'setSurfaceOverlay' | 'createClothBinding' | 'createClothParticles' | 'updateClothParticles' | 'setCloth' | 'disposeClothBinding' | 'disposeClothParticles' | 'createPointSkin' | 'posePointSkin' | 'disposePointSkin'> & {
202
218
  /**
203
219
  * A mesh's cloth binding: which simulation triangle each of its vertices follows, where on it and
204
220
  * by how much (`ClothBindingData`). The mesh must be skinned — a binding blends each vertex
@@ -222,6 +238,29 @@ export type RendererApi = Omit<{
222
238
  setCloth(binding: ClothBindingHandle | null, particles?: ClothParticlesHandle | null): void;
223
239
  disposeClothBinding(binding: ClothBindingHandle): void;
224
240
  disposeClothParticles(particles: ClothParticlesHandle): void;
241
+ /**
242
+ * A dynamic mesh posed on the device from weighted points in its joints' own spaces
243
+ * (`PointSkinData`): each vertex the weighted sum of up to four points, each moved by its joint,
244
+ * and a normal turned by its first joint. Posed into the mesh's own positions and normals, so
245
+ * every pass draws it as any dynamic mesh, motion vectors included. See `pointSkin.ts`.
246
+ *
247
+ * **For a rig whose vertices are stored that way**, which ordinary skinning cannot pose: it moves
248
+ * one bind-pose position by a blend of joints. Such a rig was posed on the main thread and
249
+ * uploaded every frame, reported at 38% of the main thread's drawing time.
250
+ *
251
+ * The mesh must be `{ dynamic: true }` and have the skin's vertex count; any other is refused in
252
+ * words, as is a joint past 511.
253
+ */
254
+ createPointSkin(mesh: MeshHandle, data: PointSkinData): PointSkinHandle;
255
+ /**
256
+ * Pose a skin: `matrices` holds each joint's transform into the mesh's model space, sixteen floats
257
+ * a joint, column-major, at least as many joints as the skin names. **Once a frame, before the
258
+ * mesh's draws**: a second pose in a frame reaches every draw of it on WebGPU, as a second
259
+ * `updateMesh` does. The mesh's bounds become a box around every joint's reach, so it is culled
260
+ * as it moves without the posed vertices ever coming back to the CPU.
261
+ */
262
+ posePointSkin(skin: PointSkinHandle, matrices: Float32Array): void;
263
+ disposePointSkin(skin: PointSkinHandle): void;
225
264
  /**
226
265
  * What the following draws lay over their surfaces beyond their materials — a rim of light, a
227
266
  * dissolve with a glowing edge, wrinkle normals by region — or null for none, which every pass
@@ -581,16 +620,32 @@ export type RendererApi = Omit<{
581
620
  * quads with a matrix each, which caps the resolution at the number of draw calls a consumer
582
621
  * can afford. A consumer found it by trying to hang a banner.
583
622
  *
584
- * **Only positions and normals.** Colour, emissive and the rest describe what a surface *is*
585
- * and do not change because it bent; rewriting them every frame would be uploading unchanged
586
- * bytes. The vertex *count* is fixed: the index buffer, the pipeline and every other attribute
587
- * are sized against it, and an update of a different size is refused naming both.
623
+ * **Only positions and normals.** Emissive and the rest describe what a surface *is* and do not
624
+ * change because it bent; rewriting them every frame would be uploading unchanged bytes. Colour
625
+ * is the exception a consumer found, a surface shaded again every frame without moving, and it
626
+ * has `updateMeshColors` of its own rather than a place here, so a rewrite of either sends only
627
+ * what changed. The vertex *count* is fixed: the index buffer, the pipeline and every other
628
+ * attribute are sized against it, and an update of a different size is refused naming both.
588
629
  *
589
630
  * The mesh must have been created with `{ dynamic: true }`. That is not ceremony — it decides
590
631
  * the buffer hint on one backend and what has to be kept on the other, and a mesh that never
591
632
  * deforms should pay for neither.
592
633
  */
593
634
  updateMesh(mesh: MeshHandle, positions: Float32Array, normals?: Float32Array): void;
635
+ /**
636
+ * Rewrite a mesh's colours, three floats a vertex, linear as `MeshData.colors` is.
637
+ *
638
+ * **For a surface whose colour changes while it stays where it is**: ground shaded with its
639
+ * distance from a moving camera, a heat map, a tile lit by a game's own rules. Without it such a
640
+ * surface is made again as a new mesh every frame, which is a buffer allocated and let go each
641
+ * frame on both backends and every pipeline it draws with looked up again.
642
+ *
643
+ * The mesh must have been created with `{ dynamicColors: true }`, which gives the colours a buffer
644
+ * of their own; any other is refused in words. Nothing else moves: not the positions, not the
645
+ * bounds, and no motion vector, since a colour does not move. A different vertex count is refused
646
+ * naming both.
647
+ */
648
+ updateMeshColors(mesh: MeshHandle, colors: Float32Array): void;
594
649
  /** Draw a handle this renderer made. Passing one from the other backend is a caller error. */
595
650
  drawMesh(mesh: MeshHandle, model: ReadonlyMat4, depthLayer?: number, tint?: Vec3 | null,
596
651
  /**
@@ -0,0 +1,23 @@
1
+ import type { Mesh } from '../../mesh.ts';
2
+ import { type PointSkinData, type PointSkinReach } from '../../pointSkin.ts';
3
+ /** The program and feedback object every skin on a context poses with, made at the first skin. */
4
+ export interface PointSkinPose {
5
+ readonly program: WebGLProgram;
6
+ readonly feedback: WebGLTransformFeedback;
7
+ }
8
+ export declare function createPointSkinPose(gl: WebGL2RenderingContext): PointSkinPose;
9
+ export declare class GlPointSkin {
10
+ readonly mesh: Mesh;
11
+ readonly reach: PointSkinReach;
12
+ private readonly count;
13
+ private readonly vao;
14
+ private readonly buffers;
15
+ private readonly joints;
16
+ private readonly width;
17
+ constructor(gl: WebGL2RenderingContext, mesh: Mesh, data: PointSkinData);
18
+ /** One buffer, read as each `[location, size, stride, offset]` says. */
19
+ private attach;
20
+ /** Pose every vertex into the mesh's buffers now, and undo every binding it made. */
21
+ pose(gl: WebGL2RenderingContext, pose: PointSkinPose, matrices: Float32Array): void;
22
+ dispose(gl: WebGL2RenderingContext): void;
23
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * A point skin on WebGL2: posed by transform feedback, a vertex stage run once a vertex with the
3
+ * rasteriser off, its two outputs captured into the dynamic mesh's own position and normal buffers.
4
+ * What a point skin is, and why it poses into a mesh, is `pointSkin.ts`; the same arithmetic in the
5
+ * same order is `backend/webgpu/shaders/pointSkin.wgsl.ts`.
6
+ *
7
+ * **Every binding it makes it undoes**, because it runs between a frame's draws: the program, the
8
+ * vertex array, the joint texture on its own unit, the feedback buffers and the discard. A buffer
9
+ * left bound for feedback while a draw reads it as vertices is an error WebGL2 raises at the draw.
10
+ */
11
+ import { POINT_SKIN_TEXTURE_UNIT } from '../../lightBudget.js';
12
+ import { reachOf } from '../../pointSkin.js';
13
+ import { compileProgram } from '../../shader.js';
14
+ const POSE_VERT = /* glsl */ `#version 300 es
15
+ precision highp float;
16
+ precision highp int;
17
+ layout(location = 0) in vec4 aNamed;
18
+ layout(location = 1) in vec4 aWeights;
19
+ layout(location = 2) in vec3 aPoint0;
20
+ layout(location = 3) in vec3 aPoint1;
21
+ layout(location = 4) in vec3 aPoint2;
22
+ layout(location = 5) in vec3 aPoint3;
23
+ layout(location = 6) in vec3 aNormal;
24
+ uniform highp sampler2D uJoints;
25
+ out vec3 vPosition;
26
+ out vec3 vNormal;
27
+
28
+ mat4 jointAt(float joint) {
29
+ int at = int(joint) * 4;
30
+ return mat4(
31
+ texelFetch(uJoints, ivec2(at, 0), 0),
32
+ texelFetch(uJoints, ivec2(at + 1, 0), 0),
33
+ texelFetch(uJoints, ivec2(at + 2, 0), 0),
34
+ texelFetch(uJoints, ivec2(at + 3, 0), 0)
35
+ );
36
+ }
37
+
38
+ void main() {
39
+ vec3 p = vec3(0.0);
40
+ if (aWeights.x != 0.0) p += aWeights.x * (jointAt(aNamed.x) * vec4(aPoint0, 1.0)).xyz;
41
+ if (aWeights.y != 0.0) p += aWeights.y * (jointAt(aNamed.y) * vec4(aPoint1, 1.0)).xyz;
42
+ if (aWeights.z != 0.0) p += aWeights.z * (jointAt(aNamed.z) * vec4(aPoint2, 1.0)).xyz;
43
+ if (aWeights.w != 0.0) p += aWeights.w * (jointAt(aNamed.w) * vec4(aPoint3, 1.0)).xyz;
44
+ vec3 raw = mat3(jointAt(aNamed.x)) * aNormal;
45
+ float l = length(raw);
46
+ vNormal = l > 0.0 ? raw / l : raw;
47
+ vPosition = p;
48
+ gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
49
+ }
50
+ `;
51
+ const POSE_FRAG = /* glsl */ `#version 300 es
52
+ precision mediump float;
53
+ out vec4 colour;
54
+ void main() {
55
+ colour = vec4(0.0);
56
+ }
57
+ `;
58
+ export function createPointSkinPose(gl) {
59
+ const program = compileProgram(gl, POSE_VERT, POSE_FRAG, 'pointSkin.pose', [
60
+ 'vPosition',
61
+ 'vNormal',
62
+ ]);
63
+ gl.useProgram(program);
64
+ gl.uniform1i(gl.getUniformLocation(program, 'uJoints'), POINT_SKIN_TEXTURE_UNIT);
65
+ gl.useProgram(null);
66
+ const feedback = gl.createTransformFeedback();
67
+ if (feedback === null)
68
+ throw new Error('createTransformFeedback failed');
69
+ return { program, feedback };
70
+ }
71
+ export class GlPointSkin {
72
+ mesh;
73
+ reach;
74
+ count;
75
+ vao;
76
+ buffers = [];
77
+ joints;
78
+ width;
79
+ constructor(gl, mesh, data) {
80
+ this.mesh = mesh;
81
+ this.reach = reachOf(data);
82
+ this.count = data.weights.length / 4;
83
+ const vao = gl.createVertexArray();
84
+ if (vao === null)
85
+ throw new Error('createVertexArray failed');
86
+ this.vao = vao;
87
+ gl.bindVertexArray(vao);
88
+ this.attach(gl, data.joints, [[0, 4, 16, 0]]);
89
+ this.attach(gl, data.weights, [[1, 4, 16, 0]]);
90
+ this.attach(gl, data.points, [
91
+ [2, 3, 48, 0],
92
+ [3, 3, 48, 12],
93
+ [4, 3, 48, 24],
94
+ [5, 3, 48, 36],
95
+ ]);
96
+ this.attach(gl, data.normals, [[6, 3, 12, 0]]);
97
+ gl.bindVertexArray(null);
98
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
99
+ /* One row, four texels a joint, as the skin palette is. */
100
+ this.width = Math.max(1, this.reach.joints * 4);
101
+ const joints = gl.createTexture();
102
+ if (joints === null)
103
+ throw new Error('createTexture failed');
104
+ this.joints = joints;
105
+ gl.activeTexture(gl.TEXTURE0 + POINT_SKIN_TEXTURE_UNIT);
106
+ gl.bindTexture(gl.TEXTURE_2D, joints);
107
+ gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA32F, this.width, 1);
108
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
109
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
110
+ gl.bindTexture(gl.TEXTURE_2D, null);
111
+ }
112
+ /** One buffer, read as each `[location, size, stride, offset]` says. */
113
+ attach(gl, values, reads) {
114
+ const buffer = gl.createBuffer();
115
+ if (buffer === null)
116
+ throw new Error('createBuffer failed');
117
+ this.buffers.push(buffer);
118
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
119
+ gl.bufferData(gl.ARRAY_BUFFER, values, gl.STATIC_DRAW);
120
+ for (const [location, size, stride, offset] of reads) {
121
+ gl.enableVertexAttribArray(location);
122
+ gl.vertexAttribPointer(location, size, gl.FLOAT, false, stride, offset);
123
+ }
124
+ }
125
+ /** Pose every vertex into the mesh's buffers now, and undo every binding it made. */
126
+ pose(gl, pose, matrices) {
127
+ const streams = this.mesh.streams;
128
+ if (streams === null)
129
+ return;
130
+ gl.useProgram(pose.program);
131
+ gl.bindVertexArray(this.vao);
132
+ gl.activeTexture(gl.TEXTURE0 + POINT_SKIN_TEXTURE_UNIT);
133
+ gl.bindTexture(gl.TEXTURE_2D, this.joints);
134
+ if (this.reach.joints > 0) {
135
+ gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, this.width, 1, gl.RGBA, gl.FLOAT, matrices, 0);
136
+ }
137
+ gl.enable(gl.RASTERIZER_DISCARD);
138
+ gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, pose.feedback);
139
+ gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, streams.positions);
140
+ gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, streams.normals);
141
+ gl.beginTransformFeedback(gl.POINTS);
142
+ gl.drawArrays(gl.POINTS, 0, this.count);
143
+ gl.endTransformFeedback();
144
+ gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null);
145
+ gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 1, null);
146
+ gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);
147
+ gl.bindBuffer(gl.TRANSFORM_FEEDBACK_BUFFER, null);
148
+ gl.disable(gl.RASTERIZER_DISCARD);
149
+ gl.bindTexture(gl.TEXTURE_2D, null);
150
+ gl.bindVertexArray(null);
151
+ gl.useProgram(null);
152
+ }
153
+ dispose(gl) {
154
+ for (const buffer of this.buffers)
155
+ gl.deleteBuffer(buffer);
156
+ gl.deleteTexture(this.joints);
157
+ gl.deleteVertexArray(this.vao);
158
+ }
159
+ }
@@ -85,6 +85,8 @@ import type { FlockParams } from '../../flockRenderer.ts';
85
85
  import { InstancedBatch } from './instanced.ts';
86
86
  import type { BoneAnimationClip, BoneAnimationHandle } from '../../boneAnimation.ts';
87
87
  import type { InstancedOptions, MeshInstances } from '../../instances.ts';
88
+ import { type PointSkinData } from '../../pointSkin.ts';
89
+ import { GlPointSkin } from './pointSkinFeedback.ts';
88
90
  /** Reused each frame: which shader light slot each bound cubemap serves. */
89
91
  export type { ShadowCasterSink, ShadowCasters } from '../../shadowCasters.ts';
90
92
  /** Lighting and atmosphere for a frame. The game decides what goes in it. */
@@ -1298,6 +1300,13 @@ export declare class WebGL2Renderer implements RendererApi {
1298
1300
  /** Whether it has room for the layers' looks beside them, and whether the refusal was said. */
1299
1301
  private looksRoom;
1300
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;
1301
1310
  private warnedLayersModelMap;
1302
1311
  private warnedLayersTriplanar;
1303
1312
  private warnedOverlayRoom;
@@ -1869,6 +1878,25 @@ export declare class WebGL2Renderer implements RendererApi {
1869
1878
  * buffer is the same buffer. Rebinding here would cost a state change for nothing.
1870
1879
  */
1871
1880
  updateMesh(mesh: Mesh, positions: Float32Array, normals?: Float32Array): void;
1881
+ /** The program and feedback object every point skin poses with, made at the first one. */
1882
+ private pointSkinPose;
1883
+ /**
1884
+ * A dynamic mesh posed on the device from weighted points in its joints' spaces, by transform
1885
+ * feedback. Refused in words on a mesh that is not dynamic, whose buffers are not hinted for a
1886
+ * rewrite every frame.
1887
+ */
1888
+ createPointSkin(mesh: Mesh, data: PointSkinData): GlPointSkin;
1889
+ /**
1890
+ * Pose a skin now: `matrices` holds each joint's transform into the mesh's model space, sixteen
1891
+ * floats, column-major. A command in the stream, so the draws after it read this pose.
1892
+ */
1893
+ posePointSkin(skin: GlPointSkin, matrices: Float32Array): void;
1894
+ disposePointSkin(skin: GlPointSkin): void;
1895
+ /**
1896
+ * Rewrite a mesh's colours, on a mesh made `dynamicColors`. The VAO is left alone for the reason
1897
+ * `updateMesh` gives: the buffer is the same buffer.
1898
+ */
1899
+ updateMeshColors(mesh: Mesh, colors: Float32Array): void;
1872
1900
  /**
1873
1901
  * Compile the skinned flat program, once, on the first rigged mesh.
1874
1902
  *
@@ -2178,6 +2206,11 @@ export declare class WebGL2Renderer implements RendererApi {
2178
2206
  * as they are, said once — which on a material sharing its arrays reads the wrong textures, so
2179
2207
  * the sentence names the way out.
2180
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;
2181
2214
  private looksFit;
2182
2215
  /** Says once that a layered material's triplanar projection is laid on the horizontal plane. */
2183
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';
@@ -134,6 +135,8 @@ import { CUTOUT_RESOLVE_CODE, resolveCutout } from '../../cutoutDither.js';
134
135
  import { materialHasMaps, noteMapsWithoutUvs } from '../../mapsWithoutUvs.js';
135
136
  import { VIEW_MODEL_DEPTH_SHARE, viewModelDepthRange } from '../../viewModel.js';
136
137
  import { noLitSwitches } from '../../shaders/flat/glassTint.js';
138
+ import { poseBounds, validatePointSkin, validatePose, } from '../../pointSkin.js';
139
+ import { GlPointSkin, createPointSkinPose } from './pointSkinFeedback.js';
137
140
  /**
138
141
  * How many frames `firstFrameSettled` will ask before it stops asking.
139
142
  *
@@ -974,6 +977,13 @@ export class WebGL2Renderer {
974
977
  /** Whether it has room for the layers' looks beside them, and whether the refusal was said. */
975
978
  looksRoom = null;
976
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();
977
987
  warnedLayersModelMap = false;
978
988
  warnedLayersTriplanar = false;
979
989
  warnedOverlayRoom = false;
@@ -2584,7 +2594,7 @@ export class WebGL2Renderer {
2584
2594
  */
2585
2595
  const drawn = withLightmapUvs(data);
2586
2596
  this.warmVariants(drawn);
2587
- return new Mesh(this.gl, drawn, options.dynamic === true);
2597
+ return new Mesh(this.gl, drawn, options.dynamic === true, false, options.dynamicColors === true);
2588
2598
  }
2589
2599
  /**
2590
2600
  * Geometry in, handle out — but the geometry lands over as many frames as the caller gives it.
@@ -2598,7 +2608,7 @@ export class WebGL2Renderer {
2598
2608
  createMeshIncremental(data, options = {}) {
2599
2609
  const drawn = withLightmapUvs(data);
2600
2610
  this.warmVariants(drawn);
2601
- const { mesh, upload } = createMeshIncremental(this.gl, drawn, options.dynamic === true);
2611
+ const { mesh, upload } = createMeshIncremental(this.gl, drawn, options.dynamic === true, options.dynamicColors === true);
2602
2612
  return {
2603
2613
  mesh,
2604
2614
  upload: {
@@ -2641,6 +2651,43 @@ export class WebGL2Renderer {
2641
2651
  updateMesh(mesh, positions, normals) {
2642
2652
  mesh.update(this.gl, positions, normals);
2643
2653
  }
2654
+ /** The program and feedback object every point skin poses with, made at the first one. */
2655
+ pointSkinPose = null;
2656
+ /**
2657
+ * A dynamic mesh posed on the device from weighted points in its joints' spaces, by transform
2658
+ * feedback. Refused in words on a mesh that is not dynamic, whose buffers are not hinted for a
2659
+ * rewrite every frame.
2660
+ */
2661
+ createPointSkin(mesh, data) {
2662
+ if (mesh.streams === null) {
2663
+ throw new Error('a point skin poses a mesh created with { dynamic: true }, whose positions and normals ' +
2664
+ 'are rewritten every frame. Say so at `createMesh`.');
2665
+ }
2666
+ validatePointSkin(data, mesh.vertexCount);
2667
+ this.pointSkinPose ??= createPointSkinPose(this.gl);
2668
+ return new GlPointSkin(this.gl, mesh, data);
2669
+ }
2670
+ /**
2671
+ * Pose a skin now: `matrices` holds each joint's transform into the mesh's model space, sixteen
2672
+ * floats, column-major. A command in the stream, so the draws after it read this pose.
2673
+ */
2674
+ posePointSkin(skin, matrices) {
2675
+ validatePose(matrices, skin.reach);
2676
+ if (this.contextLost || this.pointSkinPose === null)
2677
+ return;
2678
+ poseBounds(matrices, skin.reach, skin.mesh.bounds);
2679
+ skin.pose(this.gl, this.pointSkinPose, matrices);
2680
+ }
2681
+ disposePointSkin(skin) {
2682
+ skin.dispose(this.gl);
2683
+ }
2684
+ /**
2685
+ * Rewrite a mesh's colours, on a mesh made `dynamicColors`. The VAO is left alone for the reason
2686
+ * `updateMesh` gives: the buffer is the same buffer.
2687
+ */
2688
+ updateMeshColors(mesh, colors) {
2689
+ mesh.updateColors(this.gl, colors);
2690
+ }
2644
2691
  /**
2645
2692
  * Compile the skinned flat program, once, on the first rigged mesh.
2646
2693
  *
@@ -3268,11 +3315,18 @@ export class WebGL2Renderer {
3268
3315
  const modelMap = material?.modelMap ?? null;
3269
3316
  /* A layered material's mask, where it is a map, goes where a model's would: surfaceLayers.ts. */
3270
3317
  const layers = this.layersOf(material);
3271
- 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;
3272
3322
  gl.activeTexture(gl.TEXTURE0 + MODEL_MAP_TEXTURE_UNIT);
3273
3323
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
3274
3324
  if (boundMap !== null)
3275
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);
3276
3330
  packSurfaceLayers(layers, this.layerFloats);
3277
3331
  if (layers !== null)
3278
3332
  this.askLit('LAYERED');
@@ -3559,6 +3613,33 @@ export class WebGL2Renderer {
3559
3613
  * as they are, said once — which on a material sharing its arrays reads the wrong textures, so
3560
3614
  * the sentence names the way out.
3561
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
+ }
3562
3643
  looksFit(layers) {
3563
3644
  if (!asksLayerLooks(layers))
3564
3645
  return false;
@@ -5556,6 +5637,7 @@ export class WebGL2Renderer {
5556
5637
  movingSun: asIf.MOVING_SUN ?? this.litOn.MOVING_SUN,
5557
5638
  layered: asIf.LAYERED ?? this.litOn.LAYERED,
5558
5639
  layerLooks: asIf.LAYER_LOOKS ?? this.litOn.LAYER_LOOKS,
5640
+ surfaceDetail: asIf.SURFACE_DETAIL ?? this.litOn.SURFACE_DETAIL,
5559
5641
  maxLights: budget.maxLights,
5560
5642
  maxAreaLights: budget.maxAreaLights,
5561
5643
  });
@@ -7415,9 +7497,10 @@ export class WebGL2Renderer {
7415
7497
  this.projection.fill(0);
7416
7498
  /* Nor light through from behind. See `SurfaceMaterial.diffuseTransmission`. */
7417
7499
  resolveTransmission(null, this.transmission);
7418
- /* Nor layers: one layer until a material asks for more. */
7500
+ /* Nor layers: one layer until a material asks for more. Nor detail. */
7419
7501
  this.layerFloats.fill(0);
7420
7502
  this.lookFloats.fill(0);
7503
+ this.detailFloats.fill(0);
7421
7504
  this.overlayMaps = null;
7422
7505
  this.overlayTexture = null;
7423
7506
  /* Nor an ambient of its own: the frame's, until a draw asks. See `setAmbientSH`. */
@@ -7562,6 +7645,7 @@ export class WebGL2Renderer {
7562
7645
  gl.uniform4fv(u['uWorldUv'] ?? null, this.projection);
7563
7646
  gl.uniform4fv(u['uLayers'] ?? null, this.layerFloats);
7564
7647
  gl.uniform4fv(u['uLayerLooks'] ?? null, this.lookFloats);
7648
+ gl.uniform4fv(u['uDetail'] ?? null, this.detailFloats);
7565
7649
  /* Neither refracting nor glass, which is every draw that does not say otherwise. See
7566
7650
  `bindSeeThrough` for the four numbers in the one vector. */
7567
7651
  gl.uniform4f(u['uSeeThrough'] ?? null, 0, 0, 0, 0);