@driftengine/core 4.10.0 → 4.11.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 (140) hide show
  1. package/CHANGELOG.json +71 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +4 -3
  4. package/dist/index.js +1 -1
  5. package/dist/render/backend/webgl2/renderer.d.ts +99 -2
  6. package/dist/render/backend/webgl2/renderer.js +361 -42
  7. package/dist/render/backend/webgpu/buffers.d.ts +5 -0
  8. package/dist/render/backend/webgpu/buffers.js +98 -48
  9. package/dist/render/backend/webgpu/clothKernels.d.ts +8 -0
  10. package/dist/render/backend/webgpu/clothKernels.js +76 -0
  11. package/dist/render/backend/webgpu/clothOps.d.ts +39 -0
  12. package/dist/render/backend/webgpu/clothOps.js +78 -0
  13. package/dist/render/backend/webgpu/clothSetLayout.d.ts +25 -0
  14. package/dist/render/backend/webgpu/clothSetLayout.js +84 -0
  15. package/dist/render/backend/webgpu/clothSetPack.d.ts +33 -0
  16. package/dist/render/backend/webgpu/clothSetPack.js +144 -0
  17. package/dist/render/backend/webgpu/drawCommand.d.ts +2 -0
  18. package/dist/render/backend/webgpu/drawCommand.js +1 -0
  19. package/dist/render/backend/webgpu/flatPass.d.ts +1 -1
  20. package/dist/render/backend/webgpu/flatPass.js +2 -1
  21. package/dist/render/backend/webgpu/gpuClothSet.d.ts +80 -0
  22. package/dist/render/backend/webgpu/gpuClothSet.js +462 -0
  23. package/dist/render/backend/webgpu/latePass.js +1 -0
  24. package/dist/render/backend/webgpu/motionPass.js +15 -6
  25. package/dist/render/backend/webgpu/pipelineCache.d.ts +22 -1
  26. package/dist/render/backend/webgpu/pipelineCache.js +39 -1
  27. package/dist/render/backend/webgpu/postPass.d.ts +1 -1
  28. package/dist/render/backend/webgpu/renderer.d.ts +48 -0
  29. package/dist/render/backend/webgpu/renderer.js +207 -27
  30. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.d.ts +9 -2
  31. package/dist/render/backend/webgpu/shaders/clothSolve.wgsl.js +121 -47
  32. package/dist/render/backend/webgpu/staticBundles.js +2 -1
  33. package/dist/render/backend/webgpu/vertexFormats.d.ts +22 -0
  34. package/dist/render/backend/webgpu/vertexFormats.js +97 -0
  35. package/dist/render/globalMediumPass.d.ts +2 -0
  36. package/dist/render/globalMediumPass.js +1 -0
  37. package/dist/render/indexWidth.d.ts +17 -0
  38. package/dist/render/indexWidth.js +23 -0
  39. package/dist/render/lightMatrix.d.ts +11 -0
  40. package/dist/render/lightMatrix.js +14 -0
  41. package/dist/render/mesh.d.ts +9 -0
  42. package/dist/render/mesh.js +38 -14
  43. package/dist/render/renderQuality.d.ts +30 -0
  44. package/dist/render/renderQuality.js +5 -0
  45. package/dist/render/rendererHarness.d.ts +7 -0
  46. package/dist/render/rendererHarness.js +12 -0
  47. package/dist/render/shader.d.ts +15 -0
  48. package/dist/render/shader.js +41 -0
  49. package/dist/render/shaders/flat/directionalShadow.js +84 -10
  50. package/dist/render/shaders/flat/glassTint.d.ts +1 -1
  51. package/dist/render/shaders/flat/glassTint.js +3 -0
  52. package/dist/render/shaders/flat/index.d.ts +15 -0
  53. package/dist/render/shaders/flat/index.js +15 -2
  54. package/dist/render/shaders/flat/layered.d.ts +19 -0
  55. package/dist/render/shaders/flat/layered.js +115 -0
  56. package/dist/render/shaders/flat/main.d.ts +1 -1
  57. package/dist/render/shaders/flat/main.js +43 -16
  58. package/dist/render/shaders/flat/mapReads.d.ts +10 -0
  59. package/dist/render/shaders/flat/mapReads.js +38 -0
  60. package/dist/render/shaders/flat/movingSun.d.ts +21 -0
  61. package/dist/render/shaders/flat/movingSun.js +46 -0
  62. package/dist/render/shaders/flat/normalMap.d.ts +6 -2
  63. package/dist/render/shaders/flat/normalMap.js +14 -3
  64. package/dist/render/shaders/flat/overlay.d.ts +3 -2
  65. package/dist/render/shaders/flat/overlay.js +41 -19
  66. package/dist/render/shaders/flat/preamble.js +23 -5
  67. package/dist/render/shaders/flat/worldUv.d.ts +22 -0
  68. package/dist/render/shaders/flat/worldUv.js +113 -0
  69. package/dist/render/shaders/generated/flat.wgsl.d.ts +768 -368
  70. package/dist/render/shaders/generated/flat.wgsl.js +791 -391
  71. package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +8 -3
  72. package/dist/render/shaders/generated/globalMedium.wgsl.js +8 -3
  73. package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +15 -10
  74. package/dist/render/shaders/generated/lightVolume.wgsl.js +16 -11
  75. package/dist/render/shaders/globalMedium.js +9 -1
  76. package/dist/render/shaders/lightVolume.js +9 -1
  77. package/dist/render/shaders/tangentFrame.d.ts +1 -1
  78. package/dist/render/shaders/tangentFrame.js +16 -3
  79. package/dist/render/skinnedClothRun.d.ts +28 -0
  80. package/dist/render/skinnedClothRun.js +103 -35
  81. package/dist/render/surfaceLayers.d.ts +39 -0
  82. package/dist/render/surfaceLayers.js +26 -0
  83. package/dist/render/surfaceOverlay.d.ts +16 -3
  84. package/dist/render/surfaceOverlay.js +11 -6
  85. package/dist/render/surfaceProjection.d.ts +34 -0
  86. package/dist/render/surfaceProjection.js +30 -0
  87. package/dist/render/surfaceTexture.d.ts +30 -0
  88. package/dist/render/vertexPacking.d.ts +48 -0
  89. package/dist/render/vertexPacking.js +68 -0
  90. package/package.json +6 -6
  91. package/src/index.ts +4 -2
  92. package/src/render/backend/webgl2/renderer.ts +408 -55
  93. package/src/render/backend/webgpu/buffers.ts +153 -54
  94. package/src/render/backend/webgpu/clothKernels.ts +86 -0
  95. package/src/render/backend/webgpu/clothOps.ts +86 -0
  96. package/src/render/backend/webgpu/clothSetLayout.ts +102 -0
  97. package/src/render/backend/webgpu/clothSetPack.ts +197 -0
  98. package/src/render/backend/webgpu/drawCommand.ts +3 -0
  99. package/src/render/backend/webgpu/flatPass.ts +26 -28
  100. package/src/render/backend/webgpu/gpuClothSet.ts +564 -0
  101. package/src/render/backend/webgpu/latePass.ts +1 -0
  102. package/src/render/backend/webgpu/motionPass.ts +18 -7
  103. package/src/render/backend/webgpu/pipelineCache.ts +46 -7
  104. package/src/render/backend/webgpu/renderer.ts +274 -48
  105. package/src/render/backend/webgpu/shaders/clothSolve.wgsl.ts +121 -47
  106. package/src/render/backend/webgpu/staticBundles.ts +2 -1
  107. package/src/render/backend/webgpu/vertexFormats.ts +113 -0
  108. package/src/render/globalMediumPass.ts +7 -0
  109. package/src/render/indexWidth.ts +27 -0
  110. package/src/render/lightMatrix.ts +15 -0
  111. package/src/render/mesh.ts +66 -14
  112. package/src/render/renderQuality.ts +38 -0
  113. package/src/render/rendererHarness.ts +15 -0
  114. package/src/render/shader.ts +59 -0
  115. package/src/render/shaders/flat/directionalShadow.ts +84 -10
  116. package/src/render/shaders/flat/glassTint.ts +7 -1
  117. package/src/render/shaders/flat/index.ts +30 -2
  118. package/src/render/shaders/flat/layered.ts +115 -0
  119. package/src/render/shaders/flat/main.ts +43 -16
  120. package/src/render/shaders/flat/mapReads.ts +38 -0
  121. package/src/render/shaders/flat/movingSun.ts +46 -0
  122. package/src/render/shaders/flat/normalMap.ts +14 -3
  123. package/src/render/shaders/flat/overlay.ts +41 -19
  124. package/src/render/shaders/flat/preamble.ts +23 -5
  125. package/src/render/shaders/flat/worldUv.ts +113 -0
  126. package/src/render/shaders/generated/flat.wgsl.ts +791 -391
  127. package/src/render/shaders/generated/globalMedium.wgsl.ts +8 -3
  128. package/src/render/shaders/generated/lightVolume.wgsl.ts +16 -11
  129. package/src/render/shaders/globalMedium.ts +9 -1
  130. package/src/render/shaders/lightVolume.ts +9 -1
  131. package/src/render/shaders/tangentFrame.ts +16 -3
  132. package/src/render/skinnedClothRun.ts +138 -38
  133. package/src/render/surfaceLayers.ts +57 -0
  134. package/src/render/surfaceOverlay.ts +24 -7
  135. package/src/render/surfaceProjection.ts +57 -0
  136. package/src/render/surfaceTexture.ts +30 -0
  137. package/src/render/vertexPacking.ts +109 -0
  138. package/dist/render/backend/webgpu/gpuSkinnedCloth.d.ts +0 -74
  139. package/dist/render/backend/webgpu/gpuSkinnedCloth.js +0 -452
  140. package/src/render/backend/webgpu/gpuSkinnedCloth.ts +0 -502
@@ -11,74 +11,142 @@
11
11
  * millisecond a frame on the main thread, and 4,500 are twenty — a garment that size wants the
12
12
  * device. The device solver ran 4,500 particles in 0.65 ms a frame, most of it the dispatches
13
13
  * rather than the particles (`demo/dev/skinnedCloth.html?bench=1`).
14
+ *
15
+ * **Several garments step as one with `createSkinnedClothSet`**: one pass, one submit and one
16
+ * garment's dispatches a step for all of them, where a garment each was a pass, a submit and its
17
+ * own dispatches each. They must share a step, substeps, iterations and most steps a frame.
14
18
  */
15
19
  import { SkinnedCloth } from '@driftengine/physics';
16
20
  import { computeHostOf } from './backend/webgpu/computeHost.js';
17
- import { GpuSkinnedCloth } from './backend/webgpu/gpuSkinnedCloth.js';
21
+ import { GpuClothSet } from './backend/webgpu/gpuClothSet.js';
18
22
  /** A garment for `renderer`, from a set-up checked by name. */
19
23
  export function createSkinnedCloth(renderer, setup) {
20
24
  const host = computeHostOf(renderer);
21
- return host === null ? new CpuCloth(renderer, setup) : new DeviceCloth(host, setup);
25
+ const set = host === null ? new CpuClothSet(renderer, [setup]) : new DeviceClothSet(host, [setup]);
26
+ return new OneGarment(set);
27
+ }
28
+ /** A set of one, as a solver: what `createSkinnedCloth` hands back. */
29
+ class OneGarment {
30
+ set;
31
+ particles;
32
+ runsOn;
33
+ constructor(set) {
34
+ this.set = set;
35
+ this.particles = set.particles[0];
36
+ this.runsOn = set.runsOn;
37
+ }
38
+ step(globals, model, dt) {
39
+ this.set.setPose(0, globals, model);
40
+ this.set.step(dt);
41
+ }
42
+ setWind(x, y, z) {
43
+ this.set.setWind(0, x, y, z);
44
+ }
45
+ reset() {
46
+ this.set.reset(0);
47
+ }
48
+ dispose() {
49
+ this.set.dispose();
50
+ }
51
+ /** The particles where the last step left them: the parity check's door. */
52
+ read() {
53
+ return this.set.read(0);
54
+ }
22
55
  }
23
- class DeviceCloth {
56
+ /**
57
+ * Garments for `renderer` stepped together, from set-ups checked by name: one pass, one submit and
58
+ * one garment's dispatches a step on the device, whatever the count. They must agree on `step`,
59
+ * `substeps`, `iterations` and `maxSteps`, and are refused by the first they disagree on.
60
+ */
61
+ export function createSkinnedClothSet(renderer, setups) {
62
+ const host = computeHostOf(renderer);
63
+ return host === null ? new CpuClothSet(renderer, setups) : new DeviceClothSet(host, setups);
64
+ }
65
+ class DeviceClothSet {
24
66
  host;
25
67
  runsOn = 'device';
26
68
  particles;
27
- cloth;
28
- constructor(host, setup) {
69
+ set;
70
+ constructor(host, setups) {
29
71
  this.host = host;
30
- this.cloth = new GpuSkinnedCloth(host.device(), setup);
31
- this.particles = this.cloth.particles;
72
+ this.set = new GpuClothSet(host.device(), setups);
73
+ const particles = [];
74
+ for (let g = 0; g < this.set.size; g++)
75
+ particles.push(this.set.particles(g));
76
+ this.particles = particles;
32
77
  }
33
- step(globals, model, dt) {
78
+ setPose(g, globals, model) {
34
79
  if (this.host.lost())
35
80
  return;
36
- this.cloth.advance(globals, model, dt, this.host.changeFrame());
81
+ this.set.setPose(g, globals, model);
37
82
  }
38
- setWind(x, y, z) {
39
- this.cloth.setWind(x, y, z);
83
+ setWind(g, x, y, z) {
84
+ this.set.setWind(g, x, y, z);
40
85
  }
41
- reset() {
86
+ reset(g) {
87
+ if (this.host.lost())
88
+ return;
89
+ this.set.reset(g);
90
+ }
91
+ step(dt) {
42
92
  if (this.host.lost())
43
93
  return;
44
- this.cloth.reset();
94
+ this.set.step(dt, this.host.changeFrame());
45
95
  }
46
96
  /** Its buffers at once — its work is submitted — and its textures with the frame's. */
47
97
  dispose() {
48
- this.cloth.dispose();
49
- this.host.retire(this.cloth.particles.textures);
98
+ this.set.dispose();
99
+ for (let g = 0; g < this.set.size; g++)
100
+ this.host.retire(this.set.particles(g).textures);
50
101
  }
51
- /** The particles where the last step left them: the parity check's door, waits on the device. */
52
- read() {
53
- return this.cloth.read();
102
+ /** Garment `g`'s particles where its last step left them: the parity check's, waits on the device. */
103
+ read(g) {
104
+ return this.set.read(g);
54
105
  }
55
106
  }
56
- class CpuCloth {
107
+ class CpuClothSet {
57
108
  renderer;
58
109
  runsOn = 'cpu';
59
110
  particles;
60
- cloth;
61
- /** The frame's particles at `alpha`, before they go up. */
111
+ cloths;
112
+ /** Each garment's particles at `alpha`, before they go up. */
62
113
  staging;
63
- constructor(renderer, setup) {
114
+ constructor(renderer, setups) {
64
115
  this.renderer = renderer;
65
- this.cloth = new SkinnedCloth(setup);
66
- this.particles = renderer.createClothParticles(this.cloth.count);
67
- this.staging = new Float32Array(this.cloth.count * 3);
116
+ this.cloths = setups.map((setup) => new SkinnedCloth(setup));
117
+ this.particles = this.cloths.map((cloth) => renderer.createClothParticles(cloth.count));
118
+ this.staging = this.cloths.map((cloth) => new Float32Array(cloth.count * 3));
68
119
  }
69
- step(globals, model, dt) {
70
- this.cloth.setPose(globals, model);
71
- this.cloth.advance(dt);
72
- this.cloth.interpolate(this.cloth.alpha, this.staging);
73
- this.renderer.updateClothParticles(this.particles, this.staging);
120
+ setPose(g, globals, model) {
121
+ this.at(g).setPose(globals, model);
74
122
  }
75
- setWind(x, y, z) {
76
- this.cloth.setWind(x, y, z);
123
+ setWind(g, x, y, z) {
124
+ this.at(g).setWind(x, y, z);
77
125
  }
78
- reset() {
79
- this.cloth.reset();
126
+ reset(g) {
127
+ this.at(g).reset();
128
+ }
129
+ step(dt) {
130
+ for (let g = 0; g < this.cloths.length; g++) {
131
+ const cloth = this.cloths[g];
132
+ const staging = this.staging[g];
133
+ cloth.advance(dt);
134
+ cloth.interpolate(cloth.alpha, staging);
135
+ this.renderer.updateClothParticles(this.particles[g], staging);
136
+ }
80
137
  }
81
138
  dispose() {
82
- this.renderer.disposeClothParticles(this.particles);
139
+ for (const particles of this.particles)
140
+ this.renderer.disposeClothParticles(particles);
141
+ }
142
+ /** Garment `g`'s particles where its last step left them, as the device set reads its own. */
143
+ read(g) {
144
+ return Promise.resolve(new Float32Array(this.at(g).positions));
145
+ }
146
+ at(g) {
147
+ const cloth = this.cloths[g];
148
+ if (cloth === undefined)
149
+ throw new Error(`skinned cloth set: no garment ${g}`);
150
+ return cloth;
83
151
  }
84
152
  }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * A material blended from layers by a mask: `SurfaceMaterial.layers`, and the two vectors
3
+ * `shaders/flat/layered.ts` reads it from.
4
+ */
5
+ /** The most layers a material blends: a base and one for each of the mask's four channels. */
6
+ export declare const MAX_SURFACE_LAYERS = 5;
7
+ /**
8
+ * Up to five layers, each its own colour, normal and roughness at its own repeat, blended by a mask.
9
+ *
10
+ * **Layer `i` is layer `i` of the material's arrays**: `albedo`, `normal` and `orm` each an array
11
+ * whose layers are the material's layers in order, read at the mesh's coordinates times
12
+ * `repeats[i]`, so rock can repeat forty times across a cliff while the moss on it repeats three.
13
+ * **The mask** is read at the mesh's own coordinates and lays each layer over those before it: red
14
+ * lays layer 1 over the base, green layer 2 over that, blue layer 3, alpha layer 4. Its layer 0 is
15
+ * read, from where a shading model's map goes, which is why a layered material carries no
16
+ * `modelMap`. `emissiveLayer` gives the material's emissive map to one layer, glowing where that
17
+ * layer shows and nowhere it is covered; absent, the map glows as on any material.
18
+ *
19
+ * **What it gives up**: every layer's maps are read wherever any shows, three reads a layer, so five
20
+ * layers are fifteen reads where one material is three; layers share an array's size and format; a
21
+ * cutout reads the base layer's alpha; and the mask is sampled linear, so it is uploaded as data
22
+ * (`colorSpace: 'linear'`).
23
+ */
24
+ export interface SurfaceLayers<Texture> {
25
+ /** The weights, red to alpha laying layers 1 to 4, read at the mesh's coordinates. */
26
+ readonly mask: Texture;
27
+ /** Each layer's repeats across the mesh's coordinates, base first: one to five of them. */
28
+ readonly repeats: readonly number[];
29
+ /** The layer the emissive map glows on, 0 to 4. Absent: everywhere, as on any material. */
30
+ readonly emissiveLayer?: number;
31
+ }
32
+ /** Floats `packSurfaceLayers` writes: two vectors. */
33
+ export declare const LAYER_FLOATS = 8;
34
+ /**
35
+ * `layers` into `out`: the five repeats (1 for a layer not given), then how many layers there are
36
+ * and which one glows (−1 for none); zeros, no layers, where there are none. A repeat that is not a
37
+ * positive number is 1, so a layer is never laid at a scale that collapses it to a point.
38
+ */
39
+ export declare function packSurfaceLayers<Texture>(layers: SurfaceLayers<Texture> | null | undefined, out: Float32Array): void;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * A material blended from layers by a mask: `SurfaceMaterial.layers`, and the two vectors
3
+ * `shaders/flat/layered.ts` reads it from.
4
+ */
5
+ /** The most layers a material blends: a base and one for each of the mask's four channels. */
6
+ export const MAX_SURFACE_LAYERS = 5;
7
+ /** Floats `packSurfaceLayers` writes: two vectors. */
8
+ export const LAYER_FLOATS = 8;
9
+ /**
10
+ * `layers` into `out`: the five repeats (1 for a layer not given), then how many layers there are
11
+ * and which one glows (−1 for none); zeros, no layers, where there are none. A repeat that is not a
12
+ * positive number is 1, so a layer is never laid at a scale that collapses it to a point.
13
+ */
14
+ export function packSurfaceLayers(layers, out) {
15
+ out.fill(0);
16
+ if (layers === null || layers === undefined || layers.repeats.length === 0)
17
+ return;
18
+ const count = Math.min(layers.repeats.length, MAX_SURFACE_LAYERS);
19
+ for (let i = 0; i < MAX_SURFACE_LAYERS; i++) {
20
+ const repeat = i < count ? layers.repeats[i] : 1;
21
+ out[i] = Number.isFinite(repeat) && repeat > 0 ? repeat : 1;
22
+ }
23
+ out[5] = count;
24
+ const glow = layers.emissiveLayer;
25
+ out[6] = glow !== undefined && Number.isInteger(glow) && glow >= 0 && glow < count ? glow : -1;
26
+ }
@@ -27,12 +27,24 @@ export interface OverlayRegion {
27
27
  * raised to a contrast, times a noise laid in screen space, a slow pulse and a mask, in a colour.
28
28
  * Added to what the surface emits, and not gated on the night as emission is: it is a glow the
29
29
  * surface wears, not light it gives off.
30
+ *
31
+ * **Or, with `mode: 'blend'`, a colour the surface is pulled toward**, lit like the rest of it: the
32
+ * base colour mixed toward the rim's by the edge times `alpha`, before any light reads it, so the
33
+ * rim is bright where light falls and dark where none does. The colour is `colour` times
34
+ * `intensity`, each channel held at 1 as a base colour is, so a colour of 8 red is a red of 1 and
35
+ * not a glow; `alpha` is the weight, which may pass 1 to saturate the edge sooner.
30
36
  */
31
37
  export interface SurfaceRim {
32
38
  readonly colour: Vec3;
33
- /** How bright, 0 for none. Multiplied with `alpha`. */
39
+ /**
40
+ * How bright, 0 for none, multiplied with `alpha`. Where the rim blends it scales `colour`
41
+ * instead, so 0 there is a rim of black, and `alpha` alone says how much: 0 for none.
42
+ */
34
43
  readonly intensity: number;
44
+ /** The rim's weight, 1 unless given. */
35
45
  readonly alpha?: number;
46
+ /** `'add'`, the default, lays the rim on as glow; `'blend'` pulls the base colour toward it, lit. */
47
+ readonly mode?: 'add' | 'blend';
36
48
  /** The edge's exponent: larger keeps the rim nearer the silhouette. 1.5 unless given. */
37
49
  readonly falloff?: number;
38
50
  /**
@@ -118,10 +130,11 @@ export declare const OVERLAY_FLOATS: number;
118
130
  * no overlay. A region with a zero scale is an image not given, and its term reads as none — a
119
131
  * noise of 1, a mask of 0, no wrinkle — and every region is one without `maps`. Fifteen vectors:
120
132
  *
121
- * - 0: rim colour, rim strength (intensity times alpha; 0 is no rim)
133
+ * - 0: rim colour, rim strength (intensity times alpha; 0 is no rim). A blending rim: the colour
134
+ * times intensity, each channel held at 1, and the alpha alone
122
135
  * - 1: falloff, upward lean, contrast, mask weight
123
136
  * - 2: the rim's noise region; 3: its scroll along x and y, its tiling, the pulse's rate
124
- * - 4: the pulse's low and high (1 and 1 where none)
137
+ * - 4: the pulse's low and high (1 and 1 where none), and the mode: 0 adds, 1 blends
125
138
  * - 5: the rim's mask region
126
139
  * - 6: the dissolve's noise region (a zero scale is no dissolve); 7: its tiling, threshold, edge
127
140
  * - 8: the edge's light and its strength; 9: the laid-over colour and its amount
@@ -14,10 +14,11 @@ function region(out, at, r) {
14
14
  * no overlay. A region with a zero scale is an image not given, and its term reads as none — a
15
15
  * noise of 1, a mask of 0, no wrinkle — and every region is one without `maps`. Fifteen vectors:
16
16
  *
17
- * - 0: rim colour, rim strength (intensity times alpha; 0 is no rim)
17
+ * - 0: rim colour, rim strength (intensity times alpha; 0 is no rim). A blending rim: the colour
18
+ * times intensity, each channel held at 1, and the alpha alone
18
19
  * - 1: falloff, upward lean, contrast, mask weight
19
20
  * - 2: the rim's noise region; 3: its scroll along x and y, its tiling, the pulse's rate
20
- * - 4: the pulse's low and high (1 and 1 where none)
21
+ * - 4: the pulse's low and high (1 and 1 where none), and the mode: 0 adds, 1 blends
21
22
  * - 5: the rim's mask region
22
23
  * - 6: the dissolve's noise region (a zero scale is no dissolve); 7: its tiling, threshold, edge
23
24
  * - 8: the edge's light and its strength; 9: the laid-over colour and its amount
@@ -31,10 +32,14 @@ export function packSurfaceOverlay(overlay, out) {
31
32
  const images = overlay.maps !== undefined;
32
33
  const rim = overlay.rim;
33
34
  if (rim !== undefined) {
34
- out[0] = finite(rim.colour[0], 0);
35
- out[1] = finite(rim.colour[1], 0);
36
- out[2] = finite(rim.colour[2], 0);
37
- out[3] = Math.max(0, finite(rim.intensity, 0) * finite(rim.alpha, 1));
35
+ const blend = rim.mode === 'blend';
36
+ const scale = blend ? finite(rim.intensity, 0) : 1;
37
+ for (let c = 0; c < 3; c++) {
38
+ const channel = finite(rim.colour[c], 0) * scale;
39
+ out[c] = blend ? Math.min(Math.max(channel, 0), 1) : channel;
40
+ }
41
+ out[3] = Math.max(0, (blend ? 1 : finite(rim.intensity, 0)) * finite(rim.alpha, 1));
42
+ out[18] = blend ? 1 : 0;
38
43
  out[4] = Math.max(0, finite(rim.falloff, 1.5));
39
44
  out[5] = Math.max(0, finite(rim.upward, 0.4));
40
45
  out[6] = Math.max(0, finite(rim.contrast, 1));
@@ -0,0 +1,34 @@
1
+ /**
2
+ * A material's maps placed by where a point is in the world: `SurfaceMaterial.projection`, and the
3
+ * one vector `shaders/flat/worldUv.ts` reads it from.
4
+ */
5
+ /**
6
+ * Texture coordinates from the world rather than from the mesh.
7
+ *
8
+ * **`'planar'`** lays the maps across the two horizontal axes, `x` and `z`: ground whose mesh
9
+ * carries flat coordinates, or none worth reading, shows its texture at `scale` repeats a metre.
10
+ * **`'triplanar'`** reads each map on the three planes across `x`, `y` and `z` and blends them by
11
+ * how squarely the surface faces each, the normal's components raised to `sharpness`, so walls and
12
+ * rock wear a texture without stretching. See `worldUv.ts` for the normal map's blend.
13
+ *
14
+ * **What it gives up**: the world's coordinates do not move with a mesh, so a moving mesh slides
15
+ * under its texture; triplanar reads every map three times; and the material's `uScale`, `vScale`
16
+ * and offsets, which are the mesh's coordinates', do not apply.
17
+ */
18
+ export interface SurfaceProjection {
19
+ readonly kind: 'planar' | 'triplanar';
20
+ /** Repeats a metre: 2 lays a texture every half metre. */
21
+ readonly scale: number;
22
+ /** How sharply triplanar's planes meet: the exponent on the normal. 4 unless given. */
23
+ readonly sharpness?: number;
24
+ }
25
+ /** Floats `packSurfaceProjection` writes: one vector. */
26
+ export declare const PROJECTION_FLOATS = 4;
27
+ /**
28
+ * `projection` into `out`: its kind (0 none, 1 planar, 2 triplanar), its repeats a metre and its
29
+ * sharpness; zeros for none. A scale that is not finite and above zero projects nothing, so a
30
+ * material is never drawn at a scale that collapses its texture to a point.
31
+ */
32
+ export declare function packSurfaceProjection(projection: SurfaceProjection | null | undefined, out: Float32Array): void;
33
+ /** Whether a material projects its maps at all. */
34
+ export declare function projects(projection: SurfaceProjection | null | undefined): boolean;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * A material's maps placed by where a point is in the world: `SurfaceMaterial.projection`, and the
3
+ * one vector `shaders/flat/worldUv.ts` reads it from.
4
+ */
5
+ /** Floats `packSurfaceProjection` writes: one vector. */
6
+ export const PROJECTION_FLOATS = 4;
7
+ /**
8
+ * `projection` into `out`: its kind (0 none, 1 planar, 2 triplanar), its repeats a metre and its
9
+ * sharpness; zeros for none. A scale that is not finite and above zero projects nothing, so a
10
+ * material is never drawn at a scale that collapses its texture to a point.
11
+ */
12
+ export function packSurfaceProjection(projection, out) {
13
+ out.fill(0);
14
+ if (projection === null || projection === undefined)
15
+ return;
16
+ const scale = projection.scale;
17
+ if (!Number.isFinite(scale) || scale <= 0)
18
+ return;
19
+ out[0] = projection.kind === 'triplanar' ? 2 : 1;
20
+ out[1] = scale;
21
+ const sharpness = projection.sharpness ?? 4;
22
+ out[2] = Number.isFinite(sharpness) ? Math.min(Math.max(sharpness, 1), 64) : 4;
23
+ }
24
+ /** Whether a material projects its maps at all. */
25
+ export function projects(projection) {
26
+ return (projection !== null &&
27
+ projection !== undefined &&
28
+ Number.isFinite(projection.scale) &&
29
+ projection.scale > 0);
30
+ }
@@ -8,6 +8,8 @@
8
8
  * array samples exactly as a 2D texture does. A mesh names its layer per vertex (`MeshData.layers`),
9
9
  * so a merged block wearing forty facades is one draw.
10
10
  */
11
+ import type { SurfaceLayers } from './surfaceLayers.ts';
12
+ import type { SurfaceProjection } from './surfaceProjection.ts';
11
13
  import type { SurfaceModel } from './surfaceModel.ts';
12
14
  import type { CompressedTextureFormat, CompressedTextureSource, SurfaceSource } from './compressedSource.ts';
13
15
  import type { SurfaceLayerEffect } from './surfaceEffects.ts';
@@ -196,6 +198,34 @@ export interface SurfaceMaterial<Texture = SurfaceTexture> {
196
198
  * and an 8-bit frame clips the brighter peak without `hdrScene`.
197
199
  */
198
200
  physicalSpecular?: boolean;
201
+ /**
202
+ * Texture coordinates from where a point is in the world rather than from the mesh: `'planar'`
203
+ * across the horizontal axes for ground, `'triplanar'` on three planes for walls and rock, at so
204
+ * many repeats a metre. Absent, the mesh's, as every material's were. **A lit switch**, compiled in
205
+ * the first time a material asks, so a scene that never asks pays nothing. See
206
+ * `surfaceProjection.ts` for what it gives up.
207
+ */
208
+ projection?: SurfaceProjection | null;
209
+ /**
210
+ * How much light from behind a thin surface lets through, 0 to 1: a banner lit from behind, a
211
+ * leaf against the sun, a lampshade. The light falling on the far side — the sun, lamps, area
212
+ * lights, DriftLight — reaches the eye through the surface, coloured by its own colour; a metal
213
+ * lets none through. 0, the default, is every material as it was. Two-sided or not: a one-sided
214
+ * surface seen from its front shows the light behind it as well.
215
+ *
216
+ * **What it gives up**: a thin surface, not a volume, so a thick one lets through as much as a
217
+ * sheet; the light behind is not blurred by the surface; and none of it on glass, which lets
218
+ * light through by its own rule (`TranslucentMeshOptions.glass`).
219
+ */
220
+ diffuseTransmission?: number;
221
+ /**
222
+ * Up to five layers blended by a mask, each at its own repeat: `albedo`, `normal` and `orm` arrays
223
+ * whose layers are the material's, and a mask laying each over the ones before it. Absent, the
224
+ * material is one layer, as every material was. **A lit switch**, compiled in the first time a
225
+ * material asks. The mask is read where `modelMap` goes, so a material carries one or the other.
226
+ * See `surfaceLayers.ts` for what it gives up.
227
+ */
228
+ layers?: SurfaceLayers<Texture> | null;
199
229
  }
200
230
  export interface SurfaceTextureOptions {
201
231
  /**
@@ -0,0 +1,48 @@
1
+ import type { MeshData } from '@driftengine/drft';
2
+ /**
3
+ * How each of a mesh's vertex attributes travels to the device, decided once from its data and
4
+ * bound by each backend: as floats, as sixteen-bit fixed point, or as one value every vertex
5
+ * shares.
6
+ *
7
+ * **Only what reads back as it went, or near enough that no picture can tell.** One value every
8
+ * vertex shares is exact, and is a constant rather than a stream: a colour or an emissive naming
9
+ * no difference between vertices costs nothing a vertex, where both were always stored per vertex.
10
+ * A unit direction — a normal, a tangent and its handedness — in sixteen-bit signed fixed point is
11
+ * within 1/32767 a component, a few thousandths of a degree: measured against floats, 12 pixels of
12
+ * 921,600 moved in one published scene, by 2 of 255 on average, and none in the other eight.
13
+ * Colours and skin weights inside [0, 1] in sixteen-bit unsigned fixed point are within 1/65535. A
14
+ * textured vertex with a tangent frame, white and not glowing, goes from 64 bytes to 36 on WebGPU.
15
+ *
16
+ * **What it gives up.** Eight-bit normals would save four bytes more and are not taken: a normal in
17
+ * steps of 1/127 can band a highlight. **Texture coordinates stay floats**, in any range: a
18
+ * coordinate on a tile's far edge rounds past it in sixteen-bit fixed point, by 1/131,070, and a
19
+ * pixel there samples the neighbouring tile of an atlas — measured as 29 pixels moved by 62 of 255
20
+ * in the voxel scene, every one at a face's edge — and a half float, which steps by a 2048th near
21
+ * 1.0, is worse. A dynamic mesh's normals stay floats, because each rewrite would have to be packed
22
+ * on the CPU. What would make this wrong is a value outside its range, which the range check sends
23
+ * to floats rather than clamping, or a shader reading an attribute as something other than the
24
+ * value it carries.
25
+ */
26
+ /** Floats; sixteen-bit fixed point, signed or not; or one value for every vertex. */
27
+ export type AttributePacking = 'float' | 'snorm16' | 'unorm16' | 'constant';
28
+ /** The attributes this decides for. Every other one travels as floats. */
29
+ export declare const PACKED_ATTRIBUTES: readonly ['normals', 'tangents', 'colors', 'emissive', 'weights', 'weights2'];
30
+ export type PackedAttribute = (typeof PACKED_ATTRIBUTES)[number];
31
+ export type VertexPacking = Readonly<Record<PackedAttribute, AttributePacking>>;
32
+ /** The choice for this mesh. `dynamic` is a mesh whose positions and normals are rewritten. */
33
+ export declare function vertexPackingOf(data: MeshData, dynamic: boolean): VertexPacking;
34
+ /**
35
+ * What 1 is in each fixed point, the values WebGPU's `snorm16` and `unorm16` and WebGL2's
36
+ * normalised `SHORT` and `UNSIGNED_SHORT` divide by. A value is stored rounded to nearest, so 1, 0
37
+ * and -1 come back exactly.
38
+ */
39
+ export declare const SNORM16_ONE = 32767;
40
+ export declare const UNORM16_ONE = 65535;
41
+ /** One value in the fixed point whose 1 is `one`, rounded to nearest: the only place it is. */
42
+ export declare function toFixed16(value: number, one: number): number;
43
+ /** An attribute's values in its fixed point, tightly, for a backend reading separate arrays. */
44
+ export declare function fixedArray(values: Float32Array, packing: 'snorm16' | 'unorm16'): Int16Array | Uint16Array;
45
+ /** How `name` travels under this packing: floats for an attribute it does not decide for. */
46
+ export declare function packingFor(packing: VertexPacking, name: string): AttributePacking;
47
+ /** The value every vertex shares, for an attribute packed as `'constant'`. */
48
+ export declare function sharedValue(values: Float32Array, width: number): number[];
@@ -0,0 +1,68 @@
1
+ /** The attributes this decides for. Every other one travels as floats. */
2
+ export const PACKED_ATTRIBUTES = [
3
+ 'normals',
4
+ 'tangents',
5
+ 'colors',
6
+ 'emissive',
7
+ 'weights',
8
+ 'weights2',
9
+ ];
10
+ /** The choice for this mesh. `dynamic` is a mesh whose positions and normals are rewritten. */
11
+ export function vertexPackingOf(data, dynamic) {
12
+ return {
13
+ normals: dynamic ? 'float' : fixedOrFloat(data.normals, -1),
14
+ tangents: fixedOrFloat(data.tangents, -1),
15
+ colors: shared(data.colors, 3) ? 'constant' : fixedOrFloat(data.colors, 0),
16
+ emissive: shared(data.emissive, 1) ? 'constant' : 'float',
17
+ weights: fixedOrFloat(data.weights, 0),
18
+ weights2: fixedOrFloat(data.weights2, 0),
19
+ };
20
+ }
21
+ /**
22
+ * What 1 is in each fixed point, the values WebGPU's `snorm16` and `unorm16` and WebGL2's
23
+ * normalised `SHORT` and `UNSIGNED_SHORT` divide by. A value is stored rounded to nearest, so 1, 0
24
+ * and -1 come back exactly.
25
+ */
26
+ export const SNORM16_ONE = 32767;
27
+ export const UNORM16_ONE = 65535;
28
+ /** One value in the fixed point whose 1 is `one`, rounded to nearest: the only place it is. */
29
+ export function toFixed16(value, one) {
30
+ return Math.round(value * one);
31
+ }
32
+ /** An attribute's values in its fixed point, tightly, for a backend reading separate arrays. */
33
+ export function fixedArray(values, packing) {
34
+ const out = packing === 'snorm16' ? new Int16Array(values.length) : new Uint16Array(values.length);
35
+ const one = packing === 'snorm16' ? SNORM16_ONE : UNORM16_ONE;
36
+ for (let at = 0; at < values.length; at++)
37
+ out[at] = toFixed16(values[at], one);
38
+ return out;
39
+ }
40
+ /** How `name` travels under this packing: floats for an attribute it does not decide for. */
41
+ export function packingFor(packing, name) {
42
+ return packing[name] ?? 'float';
43
+ }
44
+ /** The value every vertex shares, for an attribute packed as `'constant'`. */
45
+ export function sharedValue(values, width) {
46
+ return Array.from(values.subarray(0, width));
47
+ }
48
+ /** Fixed point where every value lies in [low, 1]; floats otherwise, and for a NaN. */
49
+ function fixedOrFloat(values, low) {
50
+ if (values === undefined)
51
+ return 'float';
52
+ for (let at = 0; at < values.length; at++) {
53
+ const value = values[at];
54
+ if (!(value >= low && value <= 1))
55
+ return 'float';
56
+ }
57
+ return low < 0 ? 'snorm16' : 'unorm16';
58
+ }
59
+ /** Whether every vertex holds the first vertex's value. Not for a mesh of none. */
60
+ function shared(values, width) {
61
+ if (values.length < width)
62
+ return false;
63
+ for (let at = width; at < values.length; at++) {
64
+ if (values[at] !== values[at % width])
65
+ return false;
66
+ }
67
+ return true;
68
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driftengine/core",
3
- "version": "4.10.0",
3
+ "version": "4.11.0",
4
4
  "description": "The runtime of DriftEngine, a WebGPU game engine with a WebGL2 fallback: renderer, loop, input, cameras, scene graph and physics",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -29,8 +29,8 @@
29
29
  ],
30
30
  "sideEffects": false,
31
31
  "dependencies": {
32
- "@driftengine/drft": "4.10.0",
33
- "@driftengine/physics": "4.10.0",
32
+ "@driftengine/drft": "4.11.0",
33
+ "@driftengine/physics": "4.11.0",
34
34
  "gl-matrix": "^3.4.3"
35
35
  },
36
36
  "author": "Drift Technologies",
@@ -67,8 +67,8 @@
67
67
  "prepack": "node ../../scripts/build.mjs @driftengine/core"
68
68
  },
69
69
  "devDependencies": {
70
- "@driftengine/nav": "4.10.0",
71
- "@driftengine/network": "4.10.0",
72
- "@driftengine/texture": "4.10.0"
70
+ "@driftengine/nav": "4.11.0",
71
+ "@driftengine/network": "4.11.0",
72
+ "@driftengine/texture": "4.11.0"
73
73
  }
74
74
  }
package/src/index.ts CHANGED
@@ -539,8 +539,8 @@ export type { LightmapPage, LightmapRegion } from './render/lightmap.ts';
539
539
  * A skinned cloth solved where the renderer can: compute on WebGPU, the CPU under WebGL2. A function
540
540
  * rather than a renderer method so only a game that imports it carries the solver.
541
541
  */
542
- export { createSkinnedCloth } from './render/skinnedClothRun.ts';
543
- export type { SkinnedClothSolver } from './render/skinnedClothRun.ts';
542
+ export { createSkinnedCloth, createSkinnedClothSet } from './render/skinnedClothRun.ts';
543
+ export type { SkinnedClothSet, SkinnedClothSolver } from './render/skinnedClothRun.ts';
544
544
 
545
545
  /**
546
546
  * What a frame asked of a backend, against the ceilings that backend imposes.
@@ -652,6 +652,7 @@ export type {
652
652
  DirectionalShadowDepthLayers,
653
653
  GlassShadows,
654
654
  OutputTransform,
655
+ PipelineCompile,
655
656
  RenderQuality,
656
657
  RenderQualityOptions,
657
658
  ShadowFilterTaps,
@@ -663,6 +664,7 @@ export { MAX_POINT_LIGHTS, SURFACE_TEXTURE_UNIT } from './render/lightBudget.ts'
663
664
  export { MAX_CLUSTERED_LIGHTS } from './render/clusteredLights.ts';
664
665
  export { SurfaceTexture } from './render/surfaceTexture.ts';
665
666
  export type { SurfaceMaterial, SurfaceTextureOptions } from './render/surfaceTexture.ts';
667
+ export type { SurfaceProjection } from './render/surfaceProjection.ts';
666
668
  /*
667
669
  * Compressed textures — BC, ETC2 and EAC, ASTC: the source `createSurfaceTexture` takes as blocks,
668
670
  * the one question to ask before handing one over — whether this device takes its format