@driftengine/core 4.9.0 → 4.10.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 (135) hide show
  1. package/CHANGELOG.json +62 -11
  2. package/README.md +43 -38
  3. package/dist/index.d.ts +3 -1
  4. package/dist/index.js +4 -3
  5. package/dist/render/ambientHarmonics.d.ts +1 -1
  6. package/dist/render/ambientHarmonics.js +1 -1
  7. package/dist/render/areaLights.d.ts +4 -4
  8. package/dist/render/areaLights.js +1 -1
  9. package/dist/render/backend/api.d.ts +10 -3
  10. package/dist/render/backend/select.js +11 -5
  11. package/dist/render/backend/webgl2/boneAnimations.d.ts +29 -0
  12. package/dist/render/backend/webgl2/boneAnimations.js +80 -0
  13. package/dist/render/backend/webgl2/instanced.d.ts +5 -1
  14. package/dist/render/backend/webgl2/instanced.js +13 -1
  15. package/dist/render/backend/webgl2/renderer.d.ts +90 -2
  16. package/dist/render/backend/webgl2/renderer.js +253 -24
  17. package/dist/render/backend/webgl2/skinPaletteTexture.d.ts +5 -3
  18. package/dist/render/backend/webgl2/skinPaletteTexture.js +6 -4
  19. package/dist/render/backend/webgpu/boneAnimations.d.ts +34 -0
  20. package/dist/render/backend/webgpu/boneAnimations.js +76 -0
  21. package/dist/render/backend/webgpu/buffers.d.ts +19 -4
  22. package/dist/render/backend/webgpu/buffers.js +18 -11
  23. package/dist/render/backend/webgpu/compressedUpload.d.ts +0 -1
  24. package/dist/render/backend/webgpu/compressedUpload.js +53 -8
  25. package/dist/render/backend/webgpu/depthPass.d.ts +59 -3
  26. package/dist/render/backend/webgpu/depthPass.js +49 -14
  27. package/dist/render/backend/webgpu/drawCommand.d.ts +18 -4
  28. package/dist/render/backend/webgpu/drawCommand.js +10 -3
  29. package/dist/render/backend/webgpu/flatPass.d.ts +85 -42
  30. package/dist/render/backend/webgpu/flatPass.js +136 -20
  31. package/dist/render/backend/webgpu/instanced.d.ts +14 -1
  32. package/dist/render/backend/webgpu/instanced.js +24 -1
  33. package/dist/render/backend/webgpu/materialStore.d.ts +101 -0
  34. package/dist/render/backend/webgpu/materialStore.js +265 -0
  35. package/dist/render/backend/webgpu/pipelineCache.d.ts +6 -0
  36. package/dist/render/backend/webgpu/pipelineCache.js +7 -0
  37. package/dist/render/backend/webgpu/renderer.d.ts +152 -35
  38. package/dist/render/backend/webgpu/renderer.js +787 -142
  39. package/dist/render/backend/webgpu/skinPaletteRing.d.ts +7 -3
  40. package/dist/render/backend/webgpu/skinPaletteRing.js +13 -7
  41. package/dist/render/backend/webgpu/skinPaletteTexture.d.ts +6 -0
  42. package/dist/render/backend/webgpu/skinPaletteTexture.js +7 -1
  43. package/dist/render/backend/webgpu/staticBundles.d.ts +125 -0
  44. package/dist/render/backend/webgpu/staticBundles.js +241 -0
  45. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +10 -3
  46. package/dist/render/backend/webgpu/surfaceTexturePass.js +20 -2
  47. package/dist/render/backend/webgpu/uniformRing.d.ts +21 -0
  48. package/dist/render/backend/webgpu/uniformRing.js +9 -1
  49. package/dist/render/bloomChain.d.ts +3 -3
  50. package/dist/render/boneAnimation.d.ts +92 -0
  51. package/dist/render/boneAnimation.js +133 -0
  52. package/dist/render/compressedSource.d.ts +33 -13
  53. package/dist/render/compressedSource.js +70 -25
  54. package/dist/render/glCompressed.js +83 -0
  55. package/dist/render/instances.d.ts +26 -0
  56. package/dist/render/lightBudget.d.ts +10 -0
  57. package/dist/render/lightBudget.js +10 -0
  58. package/dist/render/lightmap.d.ts +1 -1
  59. package/dist/render/mesh.d.ts +5 -0
  60. package/dist/render/mesh.js +6 -0
  61. package/dist/render/pointLightSelection.d.ts +4 -4
  62. package/dist/render/screenSpaceReflection.d.ts +2 -1
  63. package/dist/render/shaders/boneAnimation.d.ts +19 -0
  64. package/dist/render/shaders/boneAnimation.js +63 -0
  65. package/dist/render/shaders/depth.d.ts +7 -0
  66. package/dist/render/shaders/depth.js +35 -10
  67. package/dist/render/shaders/flat/index.d.ts +6 -0
  68. package/dist/render/shaders/flat/index.js +27 -5
  69. package/dist/render/shaders/flat/main.d.ts +1 -1
  70. package/dist/render/shaders/flat/main.js +15 -5
  71. package/dist/render/shaders/flat/models.js +1 -1
  72. package/dist/render/shaders/flat/overlay.js +1 -1
  73. package/dist/render/shaders/flat/pointShadow.js +2 -2
  74. package/dist/render/shaders/flat/preamble.js +38 -24
  75. package/dist/render/shaders/generated/depth.wgsl.d.ts +93 -0
  76. package/dist/render/shaders/generated/depth.wgsl.js +93 -0
  77. package/dist/render/shaders/generated/flat.wgsl.d.ts +3077 -2903
  78. package/dist/render/shaders/generated/flat.wgsl.js +3100 -2925
  79. package/dist/render/shaders/outputTransform.d.ts +7 -0
  80. package/dist/render/shaders/outputTransform.js +28 -14
  81. package/dist/render/shaders/vertexChannel.d.ts +7 -0
  82. package/dist/render/shaders/vertexChannel.js +26 -0
  83. package/dist/render/staticDraws.d.ts +47 -0
  84. package/dist/render/staticDraws.js +43 -0
  85. package/dist/render/surfaceTexture.d.ts +38 -7
  86. package/dist/render/surfaceTexture.js +56 -17
  87. package/package.json +6 -6
  88. package/src/index.ts +8 -2
  89. package/src/render/ambientHarmonics.ts +1 -1
  90. package/src/render/areaLights.ts +4 -4
  91. package/src/render/backend/api.ts +13 -3
  92. package/src/render/backend/select.ts +10 -5
  93. package/src/render/backend/webgl2/boneAnimations.ts +105 -0
  94. package/src/render/backend/webgl2/instanced.ts +20 -1
  95. package/src/render/backend/webgl2/renderer.ts +315 -24
  96. package/src/render/backend/webgl2/skinPaletteTexture.ts +6 -4
  97. package/src/render/backend/webgpu/boneAnimations.ts +97 -0
  98. package/src/render/backend/webgpu/buffers.ts +42 -16
  99. package/src/render/backend/webgpu/compressedUpload.ts +59 -9
  100. package/src/render/backend/webgpu/depthPass.ts +51 -10
  101. package/src/render/backend/webgpu/drawCommand.ts +22 -4
  102. package/src/render/backend/webgpu/flatPass.ts +187 -14
  103. package/src/render/backend/webgpu/instanced.ts +32 -1
  104. package/src/render/backend/webgpu/materialStore.ts +276 -0
  105. package/src/render/backend/webgpu/pipelineCache.ts +7 -0
  106. package/src/render/backend/webgpu/renderer.ts +928 -155
  107. package/src/render/backend/webgpu/skinPaletteRing.ts +13 -7
  108. package/src/render/backend/webgpu/skinPaletteTexture.ts +7 -1
  109. package/src/render/backend/webgpu/staticBundles.ts +363 -0
  110. package/src/render/backend/webgpu/surfaceTexturePass.ts +29 -3
  111. package/src/render/backend/webgpu/uniformRing.ts +10 -1
  112. package/src/render/bloomChain.ts +3 -3
  113. package/src/render/boneAnimation.ts +223 -0
  114. package/src/render/compressedSource.ts +121 -29
  115. package/src/render/glCompressed.ts +83 -0
  116. package/src/render/instances.ts +27 -0
  117. package/src/render/lightBudget.ts +11 -0
  118. package/src/render/lightmap.ts +1 -1
  119. package/src/render/mesh.ts +6 -0
  120. package/src/render/pointLightSelection.ts +4 -4
  121. package/src/render/screenSpaceReflection.ts +2 -1
  122. package/src/render/shaders/boneAnimation.ts +63 -0
  123. package/src/render/shaders/depth.ts +45 -10
  124. package/src/render/shaders/flat/index.ts +33 -5
  125. package/src/render/shaders/flat/main.ts +15 -5
  126. package/src/render/shaders/flat/models.ts +1 -1
  127. package/src/render/shaders/flat/overlay.ts +1 -1
  128. package/src/render/shaders/flat/pointShadow.ts +2 -2
  129. package/src/render/shaders/flat/preamble.ts +38 -24
  130. package/src/render/shaders/generated/depth.wgsl.ts +95 -0
  131. package/src/render/shaders/generated/flat.wgsl.ts +3100 -2925
  132. package/src/render/shaders/outputTransform.ts +30 -14
  133. package/src/render/shaders/vertexChannel.ts +27 -0
  134. package/src/render/staticDraws.ts +95 -0
  135. package/src/render/surfaceTexture.ts +94 -27
@@ -48,3 +48,10 @@ export declare const HIGHLIGHT_SHOULDER_GLSL = "\nvec3 highlightShoulder(vec3 c)
48
48
  * a branch that costs one compare.
49
49
  */
50
50
  export declare const OUTPUT_TRANSFORM_GLSL = "\n/** 0 none, 1 sRGB, 2 ACES then sRGB, 3 the highlight shoulder then sRGB. See renderQuality.ts. */\nuniform int uOutputTransform;\n/** Scales the scene into the tone curve. 1 is the reference grade. See renderQuality.ts. */\nuniform float uOutputExposure;\n\n/*\n * ACES: the reference fit, not the cheap one.\n *\n * This was Narkowicz's single rational function, on the argument that the difference from\n * the reference curve is invisible next to the gain. That argument holds for a look and\n * fails for a *match*, which is what this option is for \u2014 aces exists so a world ported\n * from three.js reads the same, and three.js applies the full fit: into AP1 through\n * ACESInputMat, the RRT and ODT together, then back out through ACESOutputMat.\n *\n * The two curves are not a shade apart. Narkowicz is markedly brighter through the\n * midtones, which is exactly where an interior scene lives, and it is a large part of a\n * measured ~1.7x that survived correcting the colour space and the missing Lambert pi.\n * An approximation of a standard is the wrong thing to hold when the standard is the\n * specification.\n */\nvec3 rrtAndOdtFit(vec3 v) {\n vec3 a = v * (v + 0.0245786) - 0.000090537;\n vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081;\n return a / b;\n}\n\nvec3 acesFilmic(vec3 x) {\n /*\n * Exposure first, because a tone curve is only defined relative to one.\n *\n * The reference RRT and ODT are graded for a dark cinema. Every renderer that ships ACES\n * scales its input to suit whatever it is actually shown on, and the factor each one\n * picks is a *look*, not a standard - so this engine takes no position and reads it from\n * the caller. Neutral by default, which is the reference grade unmodified. three.js's own\n * pick is 1 / 0.6, for a consumer matching one.\n */\n x *= uOutputExposure;\n // Columns, as GLSL reads them: sRGB -> XYZ -> D65_2_D60 -> AP1 -> RRT_SAT.\n const mat3 ACES_INPUT = mat3(\n 0.59719, 0.07600, 0.02840,\n 0.35458, 0.90834, 0.13383,\n 0.04823, 0.01566, 0.83777\n );\n // ODT_SAT -> XYZ -> D60_2_D65 -> sRGB.\n const mat3 ACES_OUTPUT = mat3(\n 1.60475, -0.10208, -0.00327,\n -0.53108, 1.10813, -0.07276,\n -0.07367, -0.00605, 1.07602\n );\n return clamp(ACES_OUTPUT * rrtAndOdtFit(ACES_INPUT * x), 0.0, 1.0);\n}\n\n/*\n * Linear to sRGB. The piecewise form rather than a plain pow: the linear toe near black\n * is where banding lives in an 8-bit buffer, and the cheap approximation puts it in the\n * wrong place.\n */\nvec3 linearToSrgb(vec3 c) {\n vec3 low = c * 12.92;\n vec3 high = 1.055 * pow(max(c, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055;\n return mix(high, low, step(c, vec3(0.0031308)));\n}\n\n\nvec3 highlightShoulder(vec3 c) {\n float m = max(c.r, max(c.g, c.b));\n if (m <= 0.8) return c;\n float e = m - 0.8;\n return c * ((0.8 + 0.2 * e / (e + 0.2)) / m);\n}\n\nvec3 applyOutputTransform(vec3 c) {\n if (uOutputTransform == 0) return c;\n if (uOutputTransform == 2) c = acesFilmic(c);\n if (uOutputTransform == 3) c = highlightShoulder(c * uOutputExposure);\n return linearToSrgb(c);\n}\n";
51
+ /**
52
+ * The same, for the lit stage, whose two uniforms live in its material block: a draw that opts out
53
+ * of the grade sets them, so they change with the material and not with the pass. See
54
+ * `hoistUniformBlock` in `scripts/wgsl/transform.mjs`. Text otherwise identical, so WebGL2 runs
55
+ * the same program it always has.
56
+ */
57
+ export declare const LIT_OUTPUT_TRANSFORM_GLSL = "\n/** 0 none, 1 sRGB, 2 ACES then sRGB, 3 the highlight shoulder then sRGB. See renderQuality.ts. */\nuniform int uOutputTransform; // wgsl:material\n/** Scales the scene into the tone curve. 1 is the reference grade. See renderQuality.ts. */\nuniform float uOutputExposure; // wgsl:material\n\n/*\n * ACES: the reference fit, not the cheap one.\n *\n * This was Narkowicz's single rational function, on the argument that the difference from\n * the reference curve is invisible next to the gain. That argument holds for a look and\n * fails for a *match*, which is what this option is for \u2014 aces exists so a world ported\n * from three.js reads the same, and three.js applies the full fit: into AP1 through\n * ACESInputMat, the RRT and ODT together, then back out through ACESOutputMat.\n *\n * The two curves are not a shade apart. Narkowicz is markedly brighter through the\n * midtones, which is exactly where an interior scene lives, and it is a large part of a\n * measured ~1.7x that survived correcting the colour space and the missing Lambert pi.\n * An approximation of a standard is the wrong thing to hold when the standard is the\n * specification.\n */\nvec3 rrtAndOdtFit(vec3 v) {\n vec3 a = v * (v + 0.0245786) - 0.000090537;\n vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081;\n return a / b;\n}\n\nvec3 acesFilmic(vec3 x) {\n /*\n * Exposure first, because a tone curve is only defined relative to one.\n *\n * The reference RRT and ODT are graded for a dark cinema. Every renderer that ships ACES\n * scales its input to suit whatever it is actually shown on, and the factor each one\n * picks is a *look*, not a standard - so this engine takes no position and reads it from\n * the caller. Neutral by default, which is the reference grade unmodified. three.js's own\n * pick is 1 / 0.6, for a consumer matching one.\n */\n x *= uOutputExposure;\n // Columns, as GLSL reads them: sRGB -> XYZ -> D65_2_D60 -> AP1 -> RRT_SAT.\n const mat3 ACES_INPUT = mat3(\n 0.59719, 0.07600, 0.02840,\n 0.35458, 0.90834, 0.13383,\n 0.04823, 0.01566, 0.83777\n );\n // ODT_SAT -> XYZ -> D60_2_D65 -> sRGB.\n const mat3 ACES_OUTPUT = mat3(\n 1.60475, -0.10208, -0.00327,\n -0.53108, 1.10813, -0.07276,\n -0.07367, -0.00605, 1.07602\n );\n return clamp(ACES_OUTPUT * rrtAndOdtFit(ACES_INPUT * x), 0.0, 1.0);\n}\n\n/*\n * Linear to sRGB. The piecewise form rather than a plain pow: the linear toe near black\n * is where banding lives in an 8-bit buffer, and the cheap approximation puts it in the\n * wrong place.\n */\nvec3 linearToSrgb(vec3 c) {\n vec3 low = c * 12.92;\n vec3 high = 1.055 * pow(max(c, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055;\n return mix(high, low, step(c, vec3(0.0031308)));\n}\n\n\nvec3 highlightShoulder(vec3 c) {\n float m = max(c.r, max(c.g, c.b));\n if (m <= 0.8) return c;\n float e = m - 0.8;\n return c * ((0.8 + 0.2 * e / (e + 0.2)) / m);\n}\n\nvec3 applyOutputTransform(vec3 c) {\n if (uOutputTransform == 0) return c;\n if (uOutputTransform == 2) c = acesFilmic(c);\n if (uOutputTransform == 3) c = highlightShoulder(c * uOutputExposure);\n return linearToSrgb(c);\n}\n";
@@ -46,20 +46,8 @@ vec3 highlightShoulder(vec3 c) {
46
46
  return c * ((0.8 + 0.2 * e / (e + 0.2)) / m);
47
47
  }
48
48
  `;
49
- /**
50
- * `uOutputTransform` and `uOutputExposure`, the fit, and `applyOutputTransform`.
51
- *
52
- * Declares its own two uniforms, so including it is the whole of what a fragment stage has to
53
- * do besides calling it. Both are ints and floats rather than defines because the choice is a
54
- * caller's, per draw, and a permutation per output transform would double a pipeline count for
55
- * a branch that costs one compare.
56
- */
57
- export const OUTPUT_TRANSFORM_GLSL = `
58
- /** 0 none, 1 sRGB, 2 ACES then sRGB, 3 the highlight shoulder then sRGB. See renderQuality.ts. */
59
- uniform int uOutputTransform;
60
- /** Scales the scene into the tone curve. 1 is the reference grade. See renderQuality.ts. */
61
- uniform float uOutputExposure;
62
-
49
+ /** The fit and `applyOutputTransform`, below whichever declaration of the two uniforms. */
50
+ const OUTPUT_TRANSFORM_BODY_GLSL = `
63
51
  /*
64
52
  * ACES: the reference fit, not the cheap one.
65
53
  *
@@ -126,3 +114,29 @@ vec3 applyOutputTransform(vec3 c) {
126
114
  return linearToSrgb(c);
127
115
  }
128
116
  `;
117
+ /**
118
+ * `uOutputTransform` and `uOutputExposure`, the fit, and `applyOutputTransform`.
119
+ *
120
+ * Declares its own two uniforms, so including it is the whole of what a fragment stage has to
121
+ * do besides calling it. Both are ints and floats rather than defines because the choice is a
122
+ * caller's, per draw, and a permutation per output transform would double a pipeline count for
123
+ * a branch that costs one compare.
124
+ */
125
+ export const OUTPUT_TRANSFORM_GLSL = `
126
+ /** 0 none, 1 sRGB, 2 ACES then sRGB, 3 the highlight shoulder then sRGB. See renderQuality.ts. */
127
+ uniform int uOutputTransform;
128
+ /** Scales the scene into the tone curve. 1 is the reference grade. See renderQuality.ts. */
129
+ uniform float uOutputExposure;
130
+ ${OUTPUT_TRANSFORM_BODY_GLSL}`;
131
+ /**
132
+ * The same, for the lit stage, whose two uniforms live in its material block: a draw that opts out
133
+ * of the grade sets them, so they change with the material and not with the pass. See
134
+ * `hoistUniformBlock` in `scripts/wgsl/transform.mjs`. Text otherwise identical, so WebGL2 runs
135
+ * the same program it always has.
136
+ */
137
+ export const LIT_OUTPUT_TRANSFORM_GLSL = `
138
+ /** 0 none, 1 sRGB, 2 ACES then sRGB, 3 the highlight shoulder then sRGB. See renderQuality.ts. */
139
+ uniform int uOutputTransform; // wgsl:material
140
+ /** Scales the scene into the tone curve. 1 is the reference grade. See renderQuality.ts. */
141
+ uniform float uOutputExposure; // wgsl:material
142
+ ${OUTPUT_TRANSFORM_BODY_GLSL}`;
@@ -18,6 +18,13 @@
18
18
  * 4.1 KB across five, so a `#if` here would buy nothing measurable and cost a variant axis.
19
19
  */
20
20
  export declare const CHANNEL_ATTRIBUTE = "\nlayout(location = 13) in vec4 aChannel;\n\n/*\n * The frame's wind, already converted by `resolveScatterDeform` \u2014 a normalised direction, metres\n * of bend, a gust amplitude and a scaled clock.\n *\n * **The same numbers the scatter batch reads, and that is the rule rather than an economy.** A\n * second conversion here would be a second wind however identical its inputs, and the failure is\n * not localised: it shows up as a scene that does not cohere, a canopy leaning one way while the\n * grass beneath it leans another, with no single element looking wrong. See \"One wind, sampled\n * once\" in AGENTS.md.\n */\nuniform vec2 uWindDirection;\nuniform float uWindSpeed;\nuniform float uWindGust;\nuniform float uWindTime;\n/* Spatial frequency of the travelling gust, per metre. */\nuniform vec2 uWindSpatialPhase;\n";
21
+ /**
22
+ * The same, for the lit stage, whose wind lives in its view block: the frame's and not the draw's,
23
+ * so a draw's own block does not change when the wind does. See `hoistUniformBlock` in
24
+ * `scripts/wgsl/transform.mjs`. Text otherwise identical, so WebGL2 runs the same program it always
25
+ * has; the depth stage keeps the unmarked chunk, having no view block.
26
+ */
27
+ export declare const LIT_CHANNEL_ATTRIBUTE = "\nlayout(location = 13) in vec4 aChannel;\n\n/*\n * The frame's wind, already converted by `resolveScatterDeform` \u2014 a normalised direction, metres\n * of bend, a gust amplitude and a scaled clock.\n *\n * **The same numbers the scatter batch reads, and that is the rule rather than an economy.** A\n * second conversion here would be a second wind however identical its inputs, and the failure is\n * not localised: it shows up as a scene that does not cohere, a canopy leaning one way while the\n * grass beneath it leans another, with no single element looking wrong. See \"One wind, sampled\n * once\" in AGENTS.md.\n */\nuniform vec2 uWindDirection; // wgsl:view\nuniform float uWindSpeed; // wgsl:view\nuniform float uWindGust; // wgsl:view\nuniform float uWindTime; // wgsl:view\n/* Spatial frequency of the travelling gust, per metre. */\nuniform vec2 uWindSpatialPhase; // wgsl:view\n";
21
28
  /**
22
29
  * Where the wind puts a vertex, given how much of it that vertex takes.
23
30
  *
@@ -37,6 +37,32 @@ uniform float uWindTime;
37
37
  /* Spatial frequency of the travelling gust, per metre. */
38
38
  uniform vec2 uWindSpatialPhase;
39
39
  `;
40
+ /**
41
+ * The same, for the lit stage, whose wind lives in its view block: the frame's and not the draw's,
42
+ * so a draw's own block does not change when the wind does. See `hoistUniformBlock` in
43
+ * `scripts/wgsl/transform.mjs`. Text otherwise identical, so WebGL2 runs the same program it always
44
+ * has; the depth stage keeps the unmarked chunk, having no view block.
45
+ */
46
+ export const LIT_CHANNEL_ATTRIBUTE = `
47
+ layout(location = 13) in vec4 aChannel;
48
+
49
+ /*
50
+ * The frame's wind, already converted by \`resolveScatterDeform\` — a normalised direction, metres
51
+ * of bend, a gust amplitude and a scaled clock.
52
+ *
53
+ * **The same numbers the scatter batch reads, and that is the rule rather than an economy.** A
54
+ * second conversion here would be a second wind however identical its inputs, and the failure is
55
+ * not localised: it shows up as a scene that does not cohere, a canopy leaning one way while the
56
+ * grass beneath it leans another, with no single element looking wrong. See "One wind, sampled
57
+ * once" in AGENTS.md.
58
+ */
59
+ uniform vec2 uWindDirection; // wgsl:view
60
+ uniform float uWindSpeed; // wgsl:view
61
+ uniform float uWindGust; // wgsl:view
62
+ uniform float uWindTime; // wgsl:view
63
+ /* Spatial frequency of the travelling gust, per metre. */
64
+ uniform vec2 uWindSpatialPhase; // wgsl:view
65
+ `;
40
66
  /**
41
67
  * Where the wind puts a vertex, given how much of it that vertex takes.
42
68
  *
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Draws that do not change between frames, captured once from an enumeration and replayed into any
3
+ * colour pass: what `createStaticDraws` keeps, and the one rule both backends replay it by.
4
+ *
5
+ * **Captured, not referenced.** An enumeration is a closure over the caller's state, written to
6
+ * allocate nothing, so it hands a sink its scratch: the matrix it fills for every entry and the
7
+ * count it rewrites for the next batch. A list kept across frames copies both when it is made, so
8
+ * what it draws is what the enumeration said then. Meshes, batches and materials are held by
9
+ * reference: they are the caller's, and disposing one a list still holds is the caller's error.
10
+ *
11
+ * **Rigid meshes and instanced batches only.** A skinned mesh is posed every frame and a scatter
12
+ * batch bends with every frame's wind, so neither can be kept, and both are refused by name rather
13
+ * than captured in the pose they happened to have. A caller draws them each frame as it always has.
14
+ *
15
+ * What a backend does with the list is its own: WebGL2 replays the entries every frame through
16
+ * the scene-caster path, and WebGPU records them once into a render bundle per view. See
17
+ * `RendererApi.createStaticDraws` for what a caller is promised, which is the same on both.
18
+ */
19
+ import type { InstancedHandle, MeshHandle } from './backend/api.ts';
20
+ import type { MeshInstances } from './instances.ts';
21
+ import type { SceneCasterMaterial, ShadowCasterSink, ShadowCasters } from './shadowCasters.ts';
22
+ /** A list from `createStaticDraws`. Opaque but for its size. */
23
+ export interface StaticDrawsHandle {
24
+ /** How many entries it holds: meshes and batches, a batch counting once. */
25
+ readonly draws: number;
26
+ }
27
+ /** One captured entry. */
28
+ export type StaticEntry = {
29
+ readonly kind: 'mesh';
30
+ readonly mesh: MeshHandle;
31
+ readonly model: Float32Array;
32
+ readonly material: SceneCasterMaterial;
33
+ } | {
34
+ readonly kind: 'instanced';
35
+ readonly batch: InstancedHandle;
36
+ readonly data: MeshInstances;
37
+ readonly material: SceneCasterMaterial;
38
+ };
39
+ /** What both backends hold: the entries, and whether the list has been disposed. */
40
+ export interface StaticDrawsList extends StaticDrawsHandle {
41
+ readonly entries: readonly StaticEntry[];
42
+ disposed: boolean;
43
+ }
44
+ /** Enumerate `casters` once into a list. Allocates: it runs when the list is made, not per frame. */
45
+ export declare function captureStaticDraws(casters: ShadowCasters): StaticDrawsList;
46
+ /** Hand every entry to `sink`, in the order it was captured. */
47
+ export declare function replayStaticDraws(list: StaticDrawsList, sink: ShadowCasterSink): void;
@@ -0,0 +1,43 @@
1
+ /** Enumerate `casters` once into a list. Allocates: it runs when the list is made, not per frame. */
2
+ export function captureStaticDraws(casters) {
3
+ const entries = [];
4
+ const sink = {
5
+ mesh: (mesh, model, material) => {
6
+ entries.push({
7
+ kind: 'mesh',
8
+ mesh,
9
+ model: new Float32Array(model),
10
+ material: material ?? null,
11
+ });
12
+ },
13
+ instanced: (batch, data, material) => {
14
+ /* The batch's placements are on the device already; the count is the caller's scratch. */
15
+ entries.push({ kind: 'instanced', batch, data: { ...data }, material: material ?? null });
16
+ },
17
+ skinnedMesh: () => {
18
+ throw new Error('createStaticDraws: a skinned mesh is posed every frame, so a list kept across frames ' +
19
+ 'cannot hold one. Draw it each frame, with drawMesh or drawSceneCasters.');
20
+ },
21
+ scatter: () => {
22
+ throw new Error('createStaticDraws: a scatter batch bends with every frame’s wind, so a list kept across ' +
23
+ 'frames cannot hold one. Draw it each frame with drawScatter.');
24
+ },
25
+ };
26
+ casters(sink);
27
+ return {
28
+ entries,
29
+ get draws() {
30
+ return entries.length;
31
+ },
32
+ disposed: false,
33
+ };
34
+ }
35
+ /** Hand every entry to `sink`, in the order it was captured. */
36
+ export function replayStaticDraws(list, sink) {
37
+ for (const entry of list.entries) {
38
+ if (entry.kind === 'mesh')
39
+ sink.mesh(entry.mesh, entry.model, entry.material);
40
+ else
41
+ sink.instanced?.(entry.batch, entry.data, entry.material);
42
+ }
43
+ }
@@ -9,7 +9,7 @@
9
9
  * so a merged block wearing forty facades is one draw.
10
10
  */
11
11
  import type { SurfaceModel } from './surfaceModel.ts';
12
- import type { CompressedTextureFormat, SurfaceSource } from './compressedSource.ts';
12
+ import type { CompressedTextureFormat, CompressedTextureSource, SurfaceSource } from './compressedSource.ts';
13
13
  import type { SurfaceLayerEffect } from './surfaceEffects.ts';
14
14
  import type { CutoutMode } from './cutoutDither.ts';
15
15
  import type { LightmapTexels } from './lightmap.ts';
@@ -107,7 +107,7 @@ export interface SurfaceMaterial<Texture = SurfaceTexture> {
107
107
  * which is every light's default too, so a scene that names no channel is lit as it always was.
108
108
  *
109
109
  * A point or spot light shades this surface only where its `PointLightSet.lightChannels` and this
110
- * share a bit — Unreal's lighting channels. A character's own key and rim lights on channel 2,
110
+ * share a bit — lighting channels. A character's own key and rim lights on channel 2,
111
111
  * and the character's materials on 3, light the character and leave the floor around it as the
112
112
  * stage's lights alone have it. **What it does not reach**: the sun, the sky and the probes, area
113
113
  * lights, DriftLight's volume and the GPU-driven pipeline light every surface whatever its mask.
@@ -141,6 +141,22 @@ export interface SurfaceMaterial<Texture = SurfaceTexture> {
141
141
  * which is the opposite of what the caller asked for.
142
142
  */
143
143
  occlusionStrength?: number;
144
+ /**
145
+ * How much of the environment this material's draws mirror, 0 to 1: the number
146
+ * `setSurfaceReflectivity` sets for every draw, stated by the material for its own. Absent, its
147
+ * draws wear the setter's, as every material's did before this existed.
148
+ *
149
+ * **Here so a static list can carry it.** A list keeps each entry's material and none of the
150
+ * renderer's state between draws (`createStaticDraws`), so a stage whose batches each mirror their
151
+ * own share of a baked reflection says so in the material it records. What it gives up is nothing a
152
+ * setter could do: one called after this material is set is ignored for the material's draws.
153
+ */
154
+ reflectivity?: number;
155
+ /**
156
+ * How bright the environment this material's draws reflect is: `setEnvironmentGain`'s number,
157
+ * stated by the material for its own, and absent the setter's. Held non-negative.
158
+ */
159
+ environmentGain?: number;
144
160
  /**
145
161
  * Scales the emissive map, per channel. Default 1.
146
162
  *
@@ -164,14 +180,14 @@ export interface SurfaceMaterial<Texture = SurfaceTexture> {
164
180
  modelMap?: Texture | null;
165
181
  /**
166
182
  * Whether a lamp's and the sun's highlight on this surface is GGX's own, `π · D · Vis · F · N·L`,
167
- * as Unreal and this engine's skin and eye shade it, rather than the engine's lobe scaled to a
168
- * peak of one. **False by default**, every surface as it was.
183
+ * as physically based renderers and this engine's skin and eye shade it, rather than the engine's
184
+ * lobe scaled to a peak of one. **False by default**, every surface as it was.
169
185
  *
170
186
  * The peak-normalised lobe is a look control: the specular attribute says how strong a highlight
171
187
  * is and roughness how wide, apart. Physical, they are entangled as they are in a real surface —
172
188
  * the peak is `1 / (4 α²)` of the light head-on, so a polished surface's highlight is many times
173
189
  * the look's and a rough one's lower and broader — and the specular attribute is read as the
174
- * reflectance at normal incidence, F0: Unreal's `0.08 × Specular`, 0.04 at its default. Fresnel
190
+ * reflectance at normal incidence, F0: `0.08 × Specular`, 0.04 at its default. Fresnel
175
191
  * then brightens every surface toward grazing, not only a metal. A rectangle's highlight is the
176
192
  * lobe integrated over it with the attribute as F0 already, so it is unchanged either way.
177
193
  *
@@ -275,7 +291,9 @@ export declare class SurfaceTexture {
275
291
  /** The per-layer effects table, or null for a texture given none. See `surfaceEffects.ts`. */
276
292
  private effectsTable;
277
293
  /** The block format a compressed texture holds, or null for one uploaded from images. */
278
- private readonly blockFormat;
294
+ private blockFormat;
295
+ /** How the texture is sampled, kept so blocks swapped in by `update` are sampled the same way. */
296
+ private readonly sampling;
279
297
  /** Level 0's size, which `flatLayer` copies; kept as `update` changes it. */
280
298
  private width;
281
299
  private height;
@@ -303,7 +321,20 @@ export declare class SurfaceTexture {
303
321
  * Not a hot path: it re-uploads the whole image and regenerates the chain. A caller
304
322
  * doing this every frame at any size wants to know that it costs what it costs.
305
323
  */
306
- update(gl: WebGL2RenderingContext, source: TexImageSource): void;
324
+ update(gl: WebGL2RenderingContext, source: TexImageSource | CompressedTextureSource, compressed?: readonly CompressedTextureFormat[]): void;
325
+ /**
326
+ * Blocks in place of the image, behind the same handle: **a new GL object** rather than the
327
+ * image's levels respecified, because a chain shorter than the image's would leave the levels
328
+ * past it allocated, and immutable storage could not be respecified at all. Sampled as the image
329
+ * was, and refused before anything changes where the context does not take the format.
330
+ */
331
+ private swapToBlocks;
332
+ /**
333
+ * Wrap, filters and anisotropy on the bound texture. Nearest where the caller said their pixels
334
+ * are the subject; see `filter`. The mip chain is still blended between levels, because that is
335
+ * minification and this option is about magnification.
336
+ */
337
+ private applySampling;
307
338
  /**
308
339
  * Attach the only layer as the bound framebuffer's colour: a scene capture's target, drawn into
309
340
  * by `captureScene`. Engine-internal, as `bind` is.
@@ -1,5 +1,5 @@
1
1
  import { layerSize, refuseArrayUpdate, sourceSize } from './textureSource.js';
2
- import { compressedLayers, planBlocks, refuseBlockUpdate } from './compressedSource.js';
2
+ import { compressedLayers, isCompressedSource, planBlocks, refuseBlockUpdate, } from './compressedSource.js';
3
3
  import { uploadCompressedArray } from './glCompressed.js';
4
4
  import { SURFACE_EFFECT_TEXELS, packSurfaceEffects } from './surfaceEffects.js';
5
5
  import { isLightmapTexels } from './lightmap.js';
@@ -25,6 +25,8 @@ export class SurfaceTexture {
25
25
  effectsTable = null;
26
26
  /** The block format a compressed texture holds, or null for one uploaded from images. */
27
27
  blockFormat;
28
+ /** How the texture is sampled, kept so blocks swapped in by `update` are sampled the same way. */
29
+ sampling;
28
30
  /** Level 0's size, which `flatLayer` copies; kept as `update` changes it. */
29
31
  width = 0;
30
32
  height = 0;
@@ -101,23 +103,14 @@ export class SurfaceTexture {
101
103
  this.storage = this.srgb ? gl.SRGB8_ALPHA8 : gl.RGBA8;
102
104
  uploadImages(gl, sources, this.srgb);
103
105
  }
104
- gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, wrap);
105
- gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, wrap);
106
- /* Nearest where the caller said their pixels are the subject; see `filter`. The mip chain is
107
- still blended between levels, because that is minification and this option is about
108
- magnification. */
109
- const nearest = options.filter === 'nearest';
110
- gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, nearest ? gl.NEAREST : gl.LINEAR);
111
- gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, this.mipmapped
112
- ? nearest
113
- ? gl.NEAREST_MIPMAP_LINEAR
114
- : gl.LINEAR_MIPMAP_LINEAR
115
- : nearest
116
- ? gl.NEAREST
117
- : gl.LINEAR);
106
+ this.sampling = {
107
+ wrap,
108
+ nearest: options.filter === 'nearest',
109
+ anisotropy: options.anisotropy ?? DEFAULT_ANISOTROPY,
110
+ };
111
+ this.applySampling(gl);
118
112
  if (this.mipmapped && this.blockFormat === null)
119
113
  gl.generateMipmap(gl.TEXTURE_2D_ARRAY);
120
- applyAnisotropy(gl, options.anisotropy ?? DEFAULT_ANISOTROPY);
121
114
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, null);
122
115
  /* Half floats, nearest, clamped: a table read by texelFetch, whose every whole number is under
123
116
  what a half holds exactly. */
@@ -155,10 +148,14 @@ export class SurfaceTexture {
155
148
  * Not a hot path: it re-uploads the whole image and regenerates the chain. A caller
156
149
  * doing this every frame at any size wants to know that it costs what it costs.
157
150
  */
158
- update(gl, source) {
151
+ update(gl, source, compressed = []) {
159
152
  if (this.texture === null)
160
153
  return;
161
154
  refuseArrayUpdate(this.layers);
155
+ if (isCompressedSource(source)) {
156
+ this.swapToBlocks(gl, source, compressed);
157
+ return;
158
+ }
162
159
  refuseBlockUpdate(this.blockFormat);
163
160
  const { width, height } = sourceSize(source);
164
161
  this.width = width;
@@ -171,6 +168,48 @@ export class SurfaceTexture {
171
168
  gl.generateMipmap(gl.TEXTURE_2D_ARRAY);
172
169
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, null);
173
170
  }
171
+ /**
172
+ * Blocks in place of the image, behind the same handle: **a new GL object** rather than the
173
+ * image's levels respecified, because a chain shorter than the image's would leave the levels
174
+ * past it allocated, and immutable storage could not be respecified at all. Sampled as the image
175
+ * was, and refused before anything changes where the context does not take the format.
176
+ */
177
+ swapToBlocks(gl, source, compressed) {
178
+ const plan = planBlocks([source], this.srgb, compressed);
179
+ const texture = gl.createTexture();
180
+ if (texture === null)
181
+ throw new Error('SurfaceTexture: createTexture failed');
182
+ this.dropFlat(gl);
183
+ if (this.texture !== null)
184
+ gl.deleteTexture(this.texture);
185
+ this.texture = texture;
186
+ this.blockFormat = source.format;
187
+ this.width = source.width;
188
+ this.height = source.height;
189
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, texture);
190
+ uploadCompressedArray(gl, plan.name, [source], this.mipmapped ? plan.levels : 1);
191
+ this.applySampling(gl);
192
+ gl.bindTexture(gl.TEXTURE_2D_ARRAY, null);
193
+ }
194
+ /**
195
+ * Wrap, filters and anisotropy on the bound texture. Nearest where the caller said their pixels
196
+ * are the subject; see `filter`. The mip chain is still blended between levels, because that is
197
+ * minification and this option is about magnification.
198
+ */
199
+ applySampling(gl) {
200
+ const { wrap, nearest, anisotropy } = this.sampling;
201
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_S, wrap);
202
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_WRAP_T, wrap);
203
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, nearest ? gl.NEAREST : gl.LINEAR);
204
+ gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, this.mipmapped
205
+ ? nearest
206
+ ? gl.NEAREST_MIPMAP_LINEAR
207
+ : gl.LINEAR_MIPMAP_LINEAR
208
+ : nearest
209
+ ? gl.NEAREST
210
+ : gl.LINEAR);
211
+ applyAnisotropy(gl, anisotropy);
212
+ }
174
213
  /**
175
214
  * Attach the only layer as the bound framebuffer's colour: a scene capture's target, drawn into
176
215
  * by `captureScene`. Engine-internal, as `bind` is.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driftengine/core",
3
- "version": "4.9.0",
3
+ "version": "4.10.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.9.0",
33
- "@driftengine/physics": "4.9.0",
32
+ "@driftengine/drft": "4.10.0",
33
+ "@driftengine/physics": "4.10.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.9.0",
71
- "@driftengine/network": "4.9.0",
72
- "@driftengine/texture": "4.9.0"
70
+ "@driftengine/nav": "4.10.0",
71
+ "@driftengine/network": "4.10.0",
72
+ "@driftengine/texture": "4.10.0"
73
73
  }
74
74
  }
package/src/index.ts CHANGED
@@ -568,6 +568,10 @@ export type {
568
568
  ShadowCasterSink,
569
569
  ShadowCasters,
570
570
  } from './render/shadowCasters.ts';
571
+ /* What `createStaticDraws` hands back, so a consumer can hold one in a field it declares. */
572
+ export type { StaticDrawsHandle } from './render/staticDraws.ts';
573
+ /* A bone animation's clip as a caller hands it over, and the handle `createBoneAnimation` returns. */
574
+ export type { BoneAnimationClip, BoneAnimationHandle } from './render/boneAnimation.ts';
571
575
  export type { Atmosphere, UnderwaterAtmosphere } from './render/atmosphere.ts';
572
576
  /*
573
577
  * Reading an image's texels back exactly, which no canvas and no `VideoFrame` will do.
@@ -660,12 +664,14 @@ export { MAX_CLUSTERED_LIGHTS } from './render/clusteredLights.ts';
660
664
  export { SurfaceTexture } from './render/surfaceTexture.ts';
661
665
  export type { SurfaceMaterial, SurfaceTextureOptions } from './render/surfaceTexture.ts';
662
666
  /*
663
- * BC compressed textures: the source `createSurfaceTexture` takes as blocks, and the one question to
664
- * ask before handing one over — whether this device takes its format (`renderer.compressedFormats`).
667
+ * Compressed textures — BC, ETC2 and EAC, ASTC: the source `createSurfaceTexture` takes as blocks,
668
+ * the one question to ask before handing one over — whether this device takes its format
669
+ * (`renderer.compressedFormats`) — and the bytes a level of it holds, for a reader checking a file.
665
670
  */
666
671
  export {
667
672
  compressedFormatName,
668
673
  isCompressedSource,
674
+ levelBytes,
669
675
  uploadsCompressed,
670
676
  } from './render/compressedSource.ts';
671
677
  export type {
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * **Why a draw needs its own.** The engine's diffuse ambient is one answer for a whole frame — the
6
6
  * sky-and-ground gradient, or the probe grid's irradiance — so everything standing in one place is
7
- * lit alike. A stage with a baked volume of indirect light (Unreal's volumetric lightmap) lights a
7
+ * lit alike. A stage with a baked volume of indirect light (a volumetric lightmap) lights a
8
8
  * character by sampling that volume where the character stands; a consumer that samples it each
9
9
  * frame hands the result here, around that character's draws, and the character takes the light of
10
10
  * the spot it is in rather than the frame's average.
@@ -94,14 +94,14 @@ export interface AreaLightSource {
94
94
  * others. **Read only where the rectangle is shaded through the froxel table** — the ones past
95
95
  * `maxAreaLights` with `clusteredLights` on; the fixed four light everything they face, as they
96
96
  * always have. Absent, it is where the rectangle's irradiance on its axis falls to a thousandth
97
- * of a scene unit, and never inside the rectangle itself. Unreal's `AttenuationRadius`, in metres.
97
+ * of a scene unit, and never inside the rectangle itself. An attenuation radius, in metres.
98
98
  */
99
99
  range?: number;
100
100
  /**
101
101
  * Barn doors: a flap hinged at each of the rectangle's four edges, standing this many degrees from
102
102
  * its normal. 90, or absent, folds them flat and they hide nothing; smaller closes them, so the
103
103
  * light narrows to the opening they leave — a rectangle at 50° throws a beam rather than filling
104
- * its hemisphere. Unreal's `BarnDoorAngle`, whose default of 88° is nearly flat.
104
+ * its hemisphere. 88° is the usual default, nearly flat.
105
105
  *
106
106
  * What a fragment sees past the doors is the part of the rectangle no door's tip hides, one axis
107
107
  * at a time, and the rectangle then shades as that smaller rectangle, its highlight included.
@@ -111,7 +111,7 @@ export interface AreaLightSource {
111
111
  * rectangle's back face has none.
112
112
  */
113
113
  barnDoorAngle?: number;
114
- /** The barn doors' length in metres. Absent, 0.2: Unreal's default of 20 cm. */
114
+ /** The barn doors' length in metres. Absent, 0.2: the usual default of 20 cm. */
115
115
  barnDoorLength?: number;
116
116
  }
117
117
 
@@ -150,7 +150,7 @@ export function createAreaLightBuffer(capacity: number = MAX_AREA_LIGHTS): AreaL
150
150
  };
151
151
  }
152
152
 
153
- /** Unreal's default barn door length, in metres. See `AreaLightSource.barnDoorLength`. */
153
+ /** The usual default barn door length, in metres. See `AreaLightSource.barnDoorLength`. */
154
154
  export const DEFAULT_BARN_DOOR_LENGTH = 0.2;
155
155
 
156
156
  /**
@@ -8,7 +8,7 @@ import type { Renderer, TranslucentMeshOptions } from './webgl2/renderer.ts';
8
8
  import type { InstancedOptions, MeshInstances } from '../instances.ts';
9
9
  import type { SurfaceMaterial } from '../surfaceTexture.ts';
10
10
  import type { SurfaceOverlay } from '../surfaceOverlay.ts';
11
- import type { SurfaceSource } from '../compressedSource.ts';
11
+ import type { CompressedTextureSource, SurfaceSource } from '../compressedSource.ts';
12
12
  import type { LightmapPage } from '../lightmap.ts';
13
13
  import type { DeviceParticles } from '../particlePool.ts';
14
14
  import type { ClothBindingData } from '../clothBindingData.ts';
@@ -415,12 +415,22 @@ export type RendererApi = Omit<
415
415
  ): boolean;
416
416
 
417
417
  /**
418
- * Replace a texture's pixels, keeping the GPU object and its sampler state.
418
+ * Replace a texture's pixels, keeping the handle and its sampler state.
419
419
  *
420
420
  * The binding a draw loop already holds stays valid, so the swap is a swap rather than a
421
421
  * rebuild. Not a hot path: it re-uploads the whole image and rebuilds the mip chain.
422
+ *
423
+ * **Blocks may replace an image**, at the format's own size and with the chain they carry: the
424
+ * same picture at a fraction of the memory, which is how a loader that first showed a decoded
425
+ * image hands the device the blocks it encoded afterwards. Read in the colour space the texture
426
+ * was made with, and refused as `createSurfaceTexture` refuses them where the device does not take
427
+ * that format (`compressedFormats`). **The way back is refused**: a texture holding blocks takes
428
+ * no image, since nothing could say what its chain should become.
422
429
  */
423
- updateSurfaceTexture(texture: SurfaceTextureHandle, source: TexImageSource): void;
430
+ updateSurfaceTexture(
431
+ texture: SurfaceTextureHandle,
432
+ source: TexImageSource | CompressedTextureSource,
433
+ ): void;
424
434
 
425
435
  disposeSurfaceTexture(texture: SurfaceTextureHandle): void;
426
436
 
@@ -248,12 +248,17 @@ async function acquireWebGpu(
248
248
  features.push('timestamp-query');
249
249
  }
250
250
  /*
251
- * BC compressed textures, on the same terms: asked for wherever the adapter offers it, because
252
- * a device without it still draws — the loader decodes for it — and one with it samples a BC7
253
- * texture at a byte a texel instead of four. See `compressedUpload.ts`.
251
+ * The three block families, on the same terms: each asked for wherever the adapter offers it,
252
+ * because a device without one still draws — the loader decodes, or encodes ETC2, for it — and
253
+ * one with it samples a texture at a byte a texel or half of one instead of four. Desktops
254
+ * offer BC; phones ETC2 and ASTC. See `compressedUpload.ts`.
254
255
  */
255
- if (adapter.features?.has('texture-compression-bc') === true) {
256
- features.push('texture-compression-bc');
256
+ for (const family of [
257
+ 'texture-compression-bc',
258
+ 'texture-compression-etc2',
259
+ 'texture-compression-astc',
260
+ ] as const) {
261
+ if (adapter.features?.has(family) === true) features.push(family);
257
262
  }
258
263
  const descriptor: GPUDeviceDescriptor = {};
259
264
  if (Object.keys(required).length > 0) descriptor.requiredLimits = required;