@luma.gl/engine 9.2.6 → 9.3.0-alpha.11

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 (198) hide show
  1. package/dist/animation-loop/animation-loop.d.ts +11 -5
  2. package/dist/animation-loop/animation-loop.d.ts.map +1 -1
  3. package/dist/animation-loop/animation-loop.js +83 -47
  4. package/dist/animation-loop/animation-loop.js.map +1 -1
  5. package/dist/animation-loop/make-animation-loop.d.ts.map +1 -1
  6. package/dist/animation-loop/make-animation-loop.js +8 -1
  7. package/dist/animation-loop/make-animation-loop.js.map +1 -1
  8. package/dist/animation-loop/request-animation-frame.d.ts.map +1 -1
  9. package/dist/animation-loop/request-animation-frame.js +23 -6
  10. package/dist/animation-loop/request-animation-frame.js.map +1 -1
  11. package/dist/compute/computation.d.ts +3 -7
  12. package/dist/compute/computation.d.ts.map +1 -1
  13. package/dist/compute/computation.js +16 -13
  14. package/dist/compute/computation.js.map +1 -1
  15. package/dist/compute/swap.d.ts +2 -0
  16. package/dist/compute/swap.d.ts.map +1 -1
  17. package/dist/compute/swap.js +10 -5
  18. package/dist/compute/swap.js.map +1 -1
  19. package/dist/debug/debug-framebuffer.d.ts +9 -4
  20. package/dist/debug/debug-framebuffer.d.ts.map +1 -1
  21. package/dist/debug/debug-framebuffer.js +91 -45
  22. package/dist/debug/debug-framebuffer.js.map +1 -1
  23. package/dist/dist.dev.js +2767 -1344
  24. package/dist/dist.min.js +326 -211
  25. package/dist/dynamic-texture/dynamic-texture.d.ts +102 -0
  26. package/dist/dynamic-texture/dynamic-texture.d.ts.map +1 -0
  27. package/dist/dynamic-texture/dynamic-texture.js +558 -0
  28. package/dist/dynamic-texture/dynamic-texture.js.map +1 -0
  29. package/dist/dynamic-texture/texture-data.d.ts +144 -0
  30. package/dist/dynamic-texture/texture-data.d.ts.map +1 -0
  31. package/dist/dynamic-texture/texture-data.js +208 -0
  32. package/dist/dynamic-texture/texture-data.js.map +1 -0
  33. package/dist/geometries/cone-geometry.d.ts +3 -1
  34. package/dist/geometries/cone-geometry.d.ts.map +1 -1
  35. package/dist/geometries/cone-geometry.js.map +1 -1
  36. package/dist/geometries/cube-geometry.js +7 -7
  37. package/dist/geometries/cube-geometry.js.map +1 -1
  38. package/dist/geometries/cylinder-geometry.d.ts +2 -1
  39. package/dist/geometries/cylinder-geometry.d.ts.map +1 -1
  40. package/dist/geometries/cylinder-geometry.js.map +1 -1
  41. package/dist/geometries/ico-sphere-geometry.js +3 -1
  42. package/dist/geometries/ico-sphere-geometry.js.map +1 -1
  43. package/dist/geometry/gpu-geometry.d.ts.map +1 -1
  44. package/dist/geometry/gpu-geometry.js +11 -3
  45. package/dist/geometry/gpu-geometry.js.map +1 -1
  46. package/dist/index.cjs +2620 -1267
  47. package/dist/index.cjs.map +4 -4
  48. package/dist/index.d.ts +20 -6
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +12 -4
  51. package/dist/index.js.map +1 -1
  52. package/dist/material/material-factory.d.ts +73 -0
  53. package/dist/material/material-factory.d.ts.map +1 -0
  54. package/dist/material/material-factory.js +111 -0
  55. package/dist/material/material-factory.js.map +1 -0
  56. package/dist/material/material.d.ts +84 -0
  57. package/dist/material/material.d.ts.map +1 -0
  58. package/dist/material/material.js +176 -0
  59. package/dist/material/material.js.map +1 -0
  60. package/dist/model/model.d.ts +47 -16
  61. package/dist/model/model.d.ts.map +1 -1
  62. package/dist/model/model.js +148 -71
  63. package/dist/model/model.js.map +1 -1
  64. package/dist/model/split-uniforms-and-bindings.d.ts +4 -3
  65. package/dist/model/split-uniforms-and-bindings.d.ts.map +1 -1
  66. package/dist/model/split-uniforms-and-bindings.js +2 -2
  67. package/dist/model/split-uniforms-and-bindings.js.map +1 -1
  68. package/dist/models/billboard-texture-model.d.ts +8 -5
  69. package/dist/models/billboard-texture-model.d.ts.map +1 -1
  70. package/dist/models/billboard-texture-model.js +79 -25
  71. package/dist/models/billboard-texture-model.js.map +1 -1
  72. package/dist/models/billboard-texture-module.d.ts +1 -1
  73. package/dist/models/billboard-texture-module.js +1 -1
  74. package/dist/models/clip-space.js +7 -7
  75. package/dist/models/directional-light-model.d.ts +7 -0
  76. package/dist/models/directional-light-model.d.ts.map +1 -0
  77. package/dist/models/directional-light-model.js +23 -0
  78. package/dist/models/directional-light-model.js.map +1 -0
  79. package/dist/models/light-model-utils.d.ts +69 -0
  80. package/dist/models/light-model-utils.d.ts.map +1 -0
  81. package/dist/models/light-model-utils.js +395 -0
  82. package/dist/models/light-model-utils.js.map +1 -0
  83. package/dist/models/point-light-model.d.ts +7 -0
  84. package/dist/models/point-light-model.d.ts.map +1 -0
  85. package/dist/models/point-light-model.js +22 -0
  86. package/dist/models/point-light-model.js.map +1 -0
  87. package/dist/models/spot-light-model.d.ts +7 -0
  88. package/dist/models/spot-light-model.d.ts.map +1 -0
  89. package/dist/models/spot-light-model.js +23 -0
  90. package/dist/models/spot-light-model.js.map +1 -0
  91. package/dist/modules/picking/color-picking.d.ts +5 -9
  92. package/dist/modules/picking/color-picking.d.ts.map +1 -1
  93. package/dist/modules/picking/color-picking.js +122 -115
  94. package/dist/modules/picking/color-picking.js.map +1 -1
  95. package/dist/modules/picking/index-picking.d.ts +4 -4
  96. package/dist/modules/picking/index-picking.d.ts.map +1 -1
  97. package/dist/modules/picking/index-picking.js +36 -16
  98. package/dist/modules/picking/index-picking.js.map +1 -1
  99. package/dist/modules/picking/legacy-color-picking.d.ts +26 -0
  100. package/dist/modules/picking/legacy-color-picking.d.ts.map +1 -0
  101. package/dist/modules/picking/legacy-color-picking.js +7 -0
  102. package/dist/modules/picking/legacy-color-picking.js.map +1 -0
  103. package/dist/modules/picking/picking-manager.d.ts +29 -3
  104. package/dist/modules/picking/picking-manager.d.ts.map +1 -1
  105. package/dist/modules/picking/picking-manager.js +188 -41
  106. package/dist/modules/picking/picking-manager.js.map +1 -1
  107. package/dist/modules/picking/picking-uniforms.d.ts +13 -12
  108. package/dist/modules/picking/picking-uniforms.d.ts.map +1 -1
  109. package/dist/modules/picking/picking-uniforms.js +27 -14
  110. package/dist/modules/picking/picking-uniforms.js.map +1 -1
  111. package/dist/modules/picking/picking.d.ts +25 -0
  112. package/dist/modules/picking/picking.d.ts.map +1 -0
  113. package/dist/modules/picking/picking.js +18 -0
  114. package/dist/modules/picking/picking.js.map +1 -0
  115. package/dist/passes/get-fragment-shader.js +12 -27
  116. package/dist/passes/get-fragment-shader.js.map +1 -1
  117. package/dist/passes/shader-pass-renderer.d.ts +5 -7
  118. package/dist/passes/shader-pass-renderer.d.ts.map +1 -1
  119. package/dist/passes/shader-pass-renderer.js +16 -42
  120. package/dist/passes/shader-pass-renderer.js.map +1 -1
  121. package/dist/scenegraph/group-node.d.ts +5 -0
  122. package/dist/scenegraph/group-node.d.ts.map +1 -1
  123. package/dist/scenegraph/group-node.js +12 -0
  124. package/dist/scenegraph/group-node.js.map +1 -1
  125. package/dist/scenegraph/model-node.d.ts +2 -2
  126. package/dist/scenegraph/model-node.d.ts.map +1 -1
  127. package/dist/scenegraph/model-node.js.map +1 -1
  128. package/dist/scenegraph/scenegraph-node.d.ts +1 -1
  129. package/dist/scenegraph/scenegraph-node.d.ts.map +1 -1
  130. package/dist/scenegraph/scenegraph-node.js +23 -15
  131. package/dist/scenegraph/scenegraph-node.js.map +1 -1
  132. package/dist/shader-inputs.d.ts +9 -7
  133. package/dist/shader-inputs.d.ts.map +1 -1
  134. package/dist/shader-inputs.js +90 -13
  135. package/dist/shader-inputs.js.map +1 -1
  136. package/dist/utils/buffer-layout-order.d.ts.map +1 -1
  137. package/dist/utils/buffer-layout-order.js +12 -2
  138. package/dist/utils/buffer-layout-order.js.map +1 -1
  139. package/dist/utils/shader-module-utils.d.ts +7 -0
  140. package/dist/utils/shader-module-utils.d.ts.map +1 -0
  141. package/dist/utils/shader-module-utils.js +46 -0
  142. package/dist/utils/shader-module-utils.js.map +1 -0
  143. package/package.json +6 -6
  144. package/src/animation-loop/animation-loop.ts +89 -50
  145. package/src/animation-loop/make-animation-loop.ts +14 -5
  146. package/src/animation-loop/request-animation-frame.ts +32 -6
  147. package/src/compute/computation.ts +32 -17
  148. package/src/compute/swap.ts +13 -7
  149. package/src/debug/debug-framebuffer.ts +139 -61
  150. package/src/dynamic-texture/dynamic-texture.ts +730 -0
  151. package/src/dynamic-texture/texture-data.ts +336 -0
  152. package/src/{async-texture/texture-setters.ts.disabled → dynamic-texture/texture-data.ts.disabled} +1 -1
  153. package/src/geometries/cone-geometry.ts +6 -1
  154. package/src/geometries/cube-geometry.ts +7 -7
  155. package/src/geometries/cylinder-geometry.ts +5 -1
  156. package/src/geometries/ico-sphere-geometry.ts +3 -1
  157. package/src/geometry/gpu-geometry.ts +11 -3
  158. package/src/index.ts +38 -8
  159. package/src/material/material-factory.ts +157 -0
  160. package/src/material/material.ts +254 -0
  161. package/src/model/model.ts +196 -93
  162. package/src/model/split-uniforms-and-bindings.ts +8 -6
  163. package/src/models/billboard-texture-model.ts +90 -29
  164. package/src/models/billboard-texture-module.ts +1 -1
  165. package/src/models/clip-space.ts +7 -7
  166. package/src/models/directional-light-model.ts +32 -0
  167. package/src/models/light-model-utils.ts +587 -0
  168. package/src/models/point-light-model.ts +31 -0
  169. package/src/models/spot-light-model.ts +32 -0
  170. package/src/modules/picking/color-picking.ts +123 -122
  171. package/src/modules/picking/index-picking.ts +36 -16
  172. package/src/modules/picking/legacy-color-picking.ts +8 -0
  173. package/src/modules/picking/picking-manager.ts +252 -50
  174. package/src/modules/picking/picking-uniforms.ts +39 -24
  175. package/src/modules/picking/picking.ts +22 -0
  176. package/src/passes/get-fragment-shader.ts +12 -27
  177. package/src/passes/shader-pass-renderer.ts +25 -48
  178. package/src/scenegraph/group-node.ts +16 -0
  179. package/src/scenegraph/model-node.ts +2 -2
  180. package/src/scenegraph/scenegraph-node.ts +27 -16
  181. package/src/shader-inputs.ts +167 -26
  182. package/src/utils/buffer-layout-order.ts +18 -2
  183. package/src/utils/shader-module-utils.ts +65 -0
  184. package/dist/async-texture/async-texture.d.ts +0 -166
  185. package/dist/async-texture/async-texture.d.ts.map +0 -1
  186. package/dist/async-texture/async-texture.js +0 -386
  187. package/dist/async-texture/async-texture.js.map +0 -1
  188. package/dist/factories/pipeline-factory.d.ts +0 -37
  189. package/dist/factories/pipeline-factory.d.ts.map +0 -1
  190. package/dist/factories/pipeline-factory.js +0 -181
  191. package/dist/factories/pipeline-factory.js.map +0 -1
  192. package/dist/factories/shader-factory.d.ts +0 -22
  193. package/dist/factories/shader-factory.d.ts.map +0 -1
  194. package/dist/factories/shader-factory.js +0 -88
  195. package/dist/factories/shader-factory.js.map +0 -1
  196. package/src/async-texture/async-texture.ts +0 -551
  197. package/src/factories/pipeline-factory.ts +0 -224
  198. package/src/factories/shader-factory.ts +0 -103
@@ -0,0 +1,730 @@
1
+ // luma.gl, MIT license
2
+ // Copyright (c) vis.gl contributors
3
+
4
+ import type {
5
+ TextureProps,
6
+ SamplerProps,
7
+ TextureView,
8
+ Device,
9
+ TextureFormat,
10
+ TextureReadOptions
11
+ } from '@luma.gl/core';
12
+
13
+ import {Buffer, Texture, Sampler, log} from '@luma.gl/core';
14
+
15
+ // import {loadImageBitmap} from '../application-utils/load-file';
16
+ import {uid} from '../utils/uid';
17
+ import {
18
+ // cube constants
19
+ type TextureCubeFace,
20
+ TEXTURE_CUBE_FACE_MAP,
21
+ // texture slice/mip data types
22
+ type TextureSubresource,
23
+ // props (dimension + data)
24
+ type TextureDataProps,
25
+ type TextureDataAsyncProps,
26
+ // combined data for different texture types
27
+ type Texture1DData,
28
+ type Texture2DData,
29
+ type Texture3DData,
30
+ type TextureArrayData,
31
+ type TextureCubeArrayData,
32
+ type TextureCubeData,
33
+ // Helpers
34
+ getTextureSizeFromData,
35
+ resolveTextureImageFormat,
36
+ getTexture1DSubresources,
37
+ getTexture2DSubresources,
38
+ getTexture3DSubresources,
39
+ getTextureCubeSubresources,
40
+ getTextureArraySubresources,
41
+ getTextureCubeArraySubresources
42
+ } from './texture-data';
43
+
44
+ /**
45
+ * Properties for a dynamic texture
46
+ */
47
+ export type DynamicTextureProps = Omit<TextureProps, 'data' | 'mipLevels' | 'width' | 'height'> &
48
+ TextureDataAsyncProps & {
49
+ /** Generate mipmaps after creating textures and setting data */
50
+ mipmaps?: boolean;
51
+ /** nipLevels can be set to 'auto' to generate max number of mipLevels */
52
+ mipLevels?: number | 'auto';
53
+ /** Width - can be auto-calculated when initializing from ExternalImage */
54
+ width?: number;
55
+ /** Height - can be auto-calculated when initializing from ExternalImage */
56
+ height?: number;
57
+ };
58
+
59
+ /**
60
+ * Dynamic Textures
61
+ *
62
+ * - Mipmaps - DynamicTexture can generate mipmaps for textures (WebGPU does not provide built-in mipmap generation).
63
+ *
64
+ * - Texture initialization and updates - complex textures (2d array textures, cube textures, 3d textures) need multiple images
65
+ * `DynamicTexture` provides an API that makes it easy to provide the required data.
66
+ *
67
+ * - Texture resizing - Textures are immutable in WebGPU, meaning that they cannot be resized after creation.
68
+ * DynamicTexture provides a `resize()` method that internally creates a new texture with the same parameters
69
+ * but a different size.
70
+ *
71
+ * - Async image data initialization - It is often very convenient to be able to initialize textures with promises
72
+ * returned by image or data loading functions, as it allows a callback-free linear style of programming.
73
+ *
74
+ * @note GPU Textures are quite complex objects, with many subresources and modes of usage.
75
+ * The `DynamicTexture` class allows luma.gl to provide some support for working with textures
76
+ * without accumulating excessive complexity in the core Texture class which is designed as an immutable nature of GPU resource.
77
+ */
78
+ export class DynamicTexture {
79
+ readonly device: Device;
80
+ readonly id: string;
81
+
82
+ /** Props with defaults resolved (except `data` which is processed separately) */
83
+ props: Readonly<Required<DynamicTextureProps>>;
84
+
85
+ /** Created resources */
86
+ private _texture: Texture | null = null;
87
+ private _sampler: Sampler | null = null;
88
+ private _view: TextureView | null = null;
89
+
90
+ /** Ready when GPU texture has been created and data (if any) uploaded */
91
+ readonly ready: Promise<Texture>;
92
+ isReady = false;
93
+ destroyed = false;
94
+
95
+ private resolveReady: (t: Texture) => void = () => {};
96
+ private rejectReady: (error: Error) => void = () => {};
97
+
98
+ get texture(): Texture {
99
+ if (!this._texture) throw new Error('Texture not initialized yet');
100
+ return this._texture;
101
+ }
102
+ get sampler(): Sampler {
103
+ if (!this._sampler) throw new Error('Sampler not initialized yet');
104
+ return this._sampler;
105
+ }
106
+ get view(): TextureView {
107
+ if (!this._view) throw new Error('View not initialized yet');
108
+ return this._view;
109
+ }
110
+
111
+ get [Symbol.toStringTag]() {
112
+ return 'DynamicTexture';
113
+ }
114
+ toString(): string {
115
+ const width = this._texture?.width ?? this.props.width ?? '?';
116
+ const height = this._texture?.height ?? this.props.height ?? '?';
117
+ return `DynamicTexture:"${this.id}":${width}x${height}px:(${this.isReady ? 'ready' : 'loading...'})`;
118
+ }
119
+
120
+ constructor(device: Device, props: DynamicTextureProps) {
121
+ this.device = device;
122
+
123
+ const id = uid('dynamic-texture');
124
+ // NOTE: We avoid holding on to data to make sure it can be garbage collected.
125
+ const originalPropsWithAsyncData = props;
126
+ this.props = {...DynamicTexture.defaultProps, id, ...props, data: null};
127
+ this.id = this.props.id;
128
+
129
+ this.ready = new Promise<Texture>((resolve, reject) => {
130
+ this.resolveReady = resolve;
131
+ this.rejectReady = reject;
132
+ });
133
+
134
+ this.initAsync(originalPropsWithAsyncData);
135
+ }
136
+
137
+ /** @note Fire and forget; caller can await `ready` */
138
+ async initAsync(originalPropsWithAsyncData: DynamicTextureProps): Promise<void> {
139
+ try {
140
+ // TODO - Accept URL string for 2D: turn into ExternalImage promise
141
+ // const dataProps =
142
+ // typeof props.data === 'string' && (props.dimension ?? '2d') === '2d'
143
+ // ? ({dimension: '2d', data: loadImageBitmap(props.data)} as const)
144
+ // : {};
145
+
146
+ const propsWithSyncData = await this._loadAllData(originalPropsWithAsyncData);
147
+ this._checkNotDestroyed();
148
+ const subresources = propsWithSyncData.data
149
+ ? getTextureSubresources({
150
+ ...propsWithSyncData,
151
+ width: originalPropsWithAsyncData.width,
152
+ height: originalPropsWithAsyncData.height,
153
+ format: originalPropsWithAsyncData.format
154
+ })
155
+ : [];
156
+ const userProvidedFormat =
157
+ 'format' in originalPropsWithAsyncData && originalPropsWithAsyncData.format !== undefined;
158
+ const userProvidedUsage =
159
+ 'usage' in originalPropsWithAsyncData && originalPropsWithAsyncData.usage !== undefined;
160
+
161
+ // Deduce size when not explicitly provided
162
+ // TODO - what about depth?
163
+ const deduceSize = (): {width: number; height: number} => {
164
+ if (this.props.width && this.props.height) {
165
+ return {width: this.props.width, height: this.props.height};
166
+ }
167
+
168
+ const size = getTextureSizeFromData(propsWithSyncData);
169
+ if (size) {
170
+ return size;
171
+ }
172
+
173
+ return {width: this.props.width || 1, height: this.props.height || 1};
174
+ };
175
+
176
+ const size = deduceSize();
177
+ if (!size || size.width <= 0 || size.height <= 0) {
178
+ throw new Error(`${this} size could not be determined or was zero`);
179
+ }
180
+
181
+ // Normalize caller-provided subresources into one validated mip chain description.
182
+ const textureData = analyzeTextureSubresources(this.device, subresources, size, {
183
+ format: userProvidedFormat ? originalPropsWithAsyncData.format : undefined
184
+ });
185
+ const resolvedFormat = textureData.format ?? this.props.format;
186
+
187
+ // Create a minimal TextureProps and validate via `satisfies`
188
+ const baseTextureProps = {
189
+ ...this.props,
190
+ ...size,
191
+ format: resolvedFormat,
192
+ mipLevels: 1, // temporary; updated below
193
+ data: undefined
194
+ } satisfies TextureProps;
195
+
196
+ if (this.device.isTextureFormatCompressed(resolvedFormat) && !userProvidedUsage) {
197
+ baseTextureProps.usage = Texture.SAMPLE | Texture.COPY_DST;
198
+ }
199
+
200
+ // Explicit mip arrays take ownership of the mip chain; otherwise we may auto-generate it.
201
+ const shouldGenerateMipmaps =
202
+ this.props.mipmaps &&
203
+ !textureData.hasExplicitMipChain &&
204
+ !this.device.isTextureFormatCompressed(resolvedFormat);
205
+
206
+ if (this.device.type === 'webgpu' && shouldGenerateMipmaps) {
207
+ const requiredUsage =
208
+ this.props.dimension === '3d'
209
+ ? Texture.SAMPLE | Texture.STORAGE | Texture.COPY_DST | Texture.COPY_SRC
210
+ : Texture.SAMPLE | Texture.RENDER | Texture.COPY_DST | Texture.COPY_SRC;
211
+ baseTextureProps.usage |= requiredUsage;
212
+ }
213
+
214
+ // Compute mip levels (auto clamps to max)
215
+ const maxMips = this.device.getMipLevelCount(baseTextureProps.width, baseTextureProps.height);
216
+ const desired = textureData.hasExplicitMipChain
217
+ ? textureData.mipLevels
218
+ : this.props.mipLevels === 'auto'
219
+ ? maxMips
220
+ : Math.max(1, Math.min(maxMips, this.props.mipLevels ?? 1));
221
+
222
+ const finalTextureProps: TextureProps = {...baseTextureProps, mipLevels: desired};
223
+
224
+ this._texture = this.device.createTexture(finalTextureProps);
225
+ this._sampler = this.texture.sampler;
226
+ this._view = this.texture.view;
227
+
228
+ // Upload data if provided
229
+ if (textureData.subresources.length) {
230
+ this._setTextureSubresources(textureData.subresources);
231
+ }
232
+
233
+ if (this.props.mipmaps && !textureData.hasExplicitMipChain && !shouldGenerateMipmaps) {
234
+ log.warn(`${this} skipping auto-generated mipmaps for compressed texture format`)();
235
+ }
236
+
237
+ if (shouldGenerateMipmaps) {
238
+ this.generateMipmaps();
239
+ }
240
+
241
+ this.isReady = true;
242
+ this.resolveReady(this.texture);
243
+
244
+ log.info(0, `${this} created`)();
245
+ } catch (e) {
246
+ const err = e instanceof Error ? e : new Error(String(e));
247
+ this.rejectReady(err);
248
+ }
249
+ }
250
+
251
+ destroy(): void {
252
+ if (this._texture) {
253
+ this._texture.destroy();
254
+ this._texture = null;
255
+ this._sampler = null;
256
+ this._view = null;
257
+ }
258
+ this.destroyed = true;
259
+ }
260
+
261
+ generateMipmaps(): void {
262
+ if (this.device.type === 'webgl') {
263
+ this.texture.generateMipmapsWebGL();
264
+ } else if (this.device.type === 'webgpu') {
265
+ this.device.generateMipmapsWebGPU(this.texture);
266
+ } else {
267
+ log.warn(`${this} mipmaps not supported on ${this.device.type}`);
268
+ }
269
+ }
270
+
271
+ /** Set sampler or create one from props */
272
+ setSampler(sampler: Sampler | SamplerProps = {}): void {
273
+ this._checkReady();
274
+ const s = sampler instanceof Sampler ? sampler : this.device.createSampler(sampler);
275
+ this.texture.setSampler(s);
276
+ this._sampler = s;
277
+ }
278
+
279
+ /**
280
+ * Copies texture contents into a GPU buffer and waits until the copy is complete.
281
+ * The caller owns the returned buffer and must destroy it when finished.
282
+ */
283
+ async readBuffer(options: TextureReadOptions = {}): Promise<Buffer> {
284
+ if (!this.isReady) {
285
+ await this.ready;
286
+ }
287
+
288
+ const width = options.width ?? this.texture.width;
289
+ const height = options.height ?? this.texture.height;
290
+ const depthOrArrayLayers = options.depthOrArrayLayers ?? this.texture.depth;
291
+ const layout = this.texture.computeMemoryLayout({width, height, depthOrArrayLayers});
292
+
293
+ const buffer = this.device.createBuffer({
294
+ byteLength: layout.byteLength,
295
+ usage: Buffer.COPY_DST | Buffer.MAP_READ
296
+ });
297
+
298
+ this.texture.readBuffer(
299
+ {
300
+ ...options,
301
+ width,
302
+ height,
303
+ depthOrArrayLayers
304
+ },
305
+ buffer
306
+ );
307
+
308
+ const fence = this.device.createFence();
309
+ await fence.signaled;
310
+ fence.destroy();
311
+
312
+ return buffer;
313
+ }
314
+
315
+ /** Reads texture contents back to CPU memory. */
316
+ async readAsync(options: TextureReadOptions = {}): Promise<ArrayBuffer> {
317
+ if (!this.isReady) {
318
+ await this.ready;
319
+ }
320
+
321
+ const width = options.width ?? this.texture.width;
322
+ const height = options.height ?? this.texture.height;
323
+ const depthOrArrayLayers = options.depthOrArrayLayers ?? this.texture.depth;
324
+ const layout = this.texture.computeMemoryLayout({width, height, depthOrArrayLayers});
325
+
326
+ const buffer = await this.readBuffer(options);
327
+ const data = await buffer.readAsync(0, layout.byteLength);
328
+ buffer.destroy();
329
+ return data.buffer;
330
+ }
331
+
332
+ /**
333
+ * Resize by cloning the underlying immutable texture.
334
+ * Does not copy contents; caller may need to re-upload and/or regenerate mips.
335
+ */
336
+ resize(size: {width: number; height: number}): boolean {
337
+ this._checkReady();
338
+
339
+ if (size.width === this.texture.width && size.height === this.texture.height) {
340
+ return false;
341
+ }
342
+ const prev = this.texture;
343
+ this._texture = prev.clone(size);
344
+ this._sampler = this.texture.sampler;
345
+ this._view = this.texture.view;
346
+
347
+ prev.destroy();
348
+ log.info(`${this} resized`);
349
+ return true;
350
+ }
351
+
352
+ /** Convert cube face label to texture slice index. Index can be used with `setTexture2DData()`. */
353
+ getCubeFaceIndex(face: TextureCubeFace): number {
354
+ const index = TEXTURE_CUBE_FACE_MAP[face];
355
+ if (index === undefined) throw new Error(`Invalid cube face: ${face}`);
356
+ return index;
357
+ }
358
+
359
+ /** Convert cube face label to texture slice index. Index can be used with `setTexture2DData()`. */
360
+ getCubeArrayFaceIndex(cubeIndex: number, face: TextureCubeFace): number {
361
+ return 6 * cubeIndex + this.getCubeFaceIndex(face);
362
+ }
363
+
364
+ /** @note experimental: Set multiple mip levels (1D) */
365
+ setTexture1DData(data: Texture1DData): void {
366
+ this._checkReady();
367
+ if (this.texture.props.dimension !== '1d') {
368
+ throw new Error(`${this} is not 1d`);
369
+ }
370
+ const subresources = getTexture1DSubresources(data);
371
+ this._setTextureSubresources(subresources);
372
+ }
373
+
374
+ /** @note experimental: Set multiple mip levels (2D), optionally at `z`, slice (depth/array level) index */
375
+ setTexture2DData(lodData: Texture2DData, z: number = 0): void {
376
+ this._checkReady();
377
+ if (this.texture.props.dimension !== '2d') {
378
+ throw new Error(`${this} is not 2d`);
379
+ }
380
+
381
+ const subresources = getTexture2DSubresources(z, lodData);
382
+ this._setTextureSubresources(subresources);
383
+ }
384
+
385
+ /** 3D: multiple depth slices, each may carry multiple mip levels */
386
+ setTexture3DData(data: Texture3DData): void {
387
+ if (this.texture.props.dimension !== '3d') {
388
+ throw new Error(`${this} is not 3d`);
389
+ }
390
+ const subresources = getTexture3DSubresources(data);
391
+ this._setTextureSubresources(subresources);
392
+ }
393
+
394
+ /** 2D array: multiple layers, each may carry multiple mip levels */
395
+ setTextureArrayData(data: TextureArrayData): void {
396
+ if (this.texture.props.dimension !== '2d-array') {
397
+ throw new Error(`${this} is not 2d-array`);
398
+ }
399
+ const subresources = getTextureArraySubresources(data);
400
+ this._setTextureSubresources(subresources);
401
+ }
402
+
403
+ /** Cube: 6 faces, each may carry multiple mip levels */
404
+ setTextureCubeData(data: TextureCubeData): void {
405
+ if (this.texture.props.dimension !== 'cube') {
406
+ throw new Error(`${this} is not cube`);
407
+ }
408
+ const subresources = getTextureCubeSubresources(data);
409
+ this._setTextureSubresources(subresources);
410
+ }
411
+
412
+ /** Cube array: multiple cubes (faces×layers), each face may carry multiple mips */
413
+ setTextureCubeArrayData(data: TextureCubeArrayData): void {
414
+ if (this.texture.props.dimension !== 'cube-array') {
415
+ throw new Error(`${this} is not cube-array`);
416
+ }
417
+ const subresources = getTextureCubeArraySubresources(data);
418
+ this._setTextureSubresources(subresources);
419
+ }
420
+
421
+ /** Sets multiple mip levels on different `z` slices (depth/array index) */
422
+ private _setTextureSubresources(subresources: TextureSubresource[]): void {
423
+ // If user supplied multiple mip levels, warn if auto-mips also requested
424
+ // if (lodArray.length > 1 && this.props.mipmaps !== false) {
425
+ // log.warn(
426
+ // `Texture ${this.id}: provided multiple LODs and also requested mipmap generation.`
427
+ // )();
428
+ // }
429
+
430
+ for (const subresource of subresources) {
431
+ const {z, mipLevel} = subresource;
432
+ switch (subresource.type) {
433
+ case 'external-image':
434
+ const {image, flipY} = subresource;
435
+ this.texture.copyExternalImage({image, z, mipLevel, flipY});
436
+ break;
437
+ case 'texture-data':
438
+ const {data, textureFormat} = subresource;
439
+ if (textureFormat && textureFormat !== this.texture.format) {
440
+ throw new Error(
441
+ `${this} mip level ${mipLevel} uses format "${textureFormat}" but texture format is "${this.texture.format}"`
442
+ );
443
+ }
444
+ this.texture.writeData(data.data, {
445
+ x: 0,
446
+ y: 0,
447
+ z,
448
+ width: data.width,
449
+ height: data.height,
450
+ depthOrArrayLayers: 1,
451
+ mipLevel
452
+ });
453
+ break;
454
+ default:
455
+ throw new Error('Unsupported 2D mip-level payload');
456
+ }
457
+ }
458
+ }
459
+
460
+ // ------------------ helpers ------------------
461
+
462
+ /** Recursively resolve all promises in data structures */
463
+ private async _loadAllData(props: TextureDataAsyncProps): Promise<TextureDataProps> {
464
+ const syncData = await awaitAllPromises(props.data);
465
+ const dimension = (props.dimension ?? '2d') as TextureDataProps['dimension'];
466
+ return {dimension, data: syncData ?? null} as TextureDataProps;
467
+ }
468
+
469
+ private _checkNotDestroyed() {
470
+ if (this.destroyed) {
471
+ log.warn(`${this} already destroyed`);
472
+ }
473
+ }
474
+
475
+ private _checkReady() {
476
+ if (!this.isReady) {
477
+ log.warn(`${this} Cannot perform this operation before ready`);
478
+ }
479
+ }
480
+
481
+ static defaultProps: Required<DynamicTextureProps> = {
482
+ ...Texture.defaultProps,
483
+ dimension: '2d',
484
+ data: null,
485
+ mipmaps: false
486
+ };
487
+ }
488
+
489
+ type TextureSubresourceAnalysis = {
490
+ readonly subresources: TextureSubresource[];
491
+ readonly mipLevels: number;
492
+ readonly format?: TextureFormat;
493
+ readonly hasExplicitMipChain: boolean;
494
+ };
495
+
496
+ // Flatten dimension-specific texture data into one list of uploadable subresources.
497
+ function getTextureSubresources(
498
+ props: TextureDataProps & Partial<Pick<TextureProps, 'width' | 'height' | 'format'>>
499
+ ): TextureSubresource[] {
500
+ if (!props.data) {
501
+ return [];
502
+ }
503
+
504
+ const baseLevelSize =
505
+ props.width && props.height ? {width: props.width, height: props.height} : undefined;
506
+ const textureFormat = 'format' in props ? props.format : undefined;
507
+
508
+ switch (props.dimension) {
509
+ case '1d':
510
+ return getTexture1DSubresources(props.data);
511
+ case '2d':
512
+ return getTexture2DSubresources(0, props.data, baseLevelSize, textureFormat);
513
+ case '3d':
514
+ return getTexture3DSubresources(props.data);
515
+ case '2d-array':
516
+ return getTextureArraySubresources(props.data);
517
+ case 'cube':
518
+ return getTextureCubeSubresources(props.data);
519
+ case 'cube-array':
520
+ return getTextureCubeArraySubresources(props.data);
521
+ default:
522
+ throw new Error(`Unhandled dimension ${(props as TextureDataProps).dimension}`);
523
+ }
524
+ }
525
+
526
+ // Resolve a consistent texture format and the longest mip chain valid across all slices.
527
+ function analyzeTextureSubresources(
528
+ device: Device,
529
+ subresources: TextureSubresource[],
530
+ size: {width: number; height: number},
531
+ options: {format?: TextureFormat}
532
+ ): TextureSubresourceAnalysis {
533
+ if (subresources.length === 0) {
534
+ return {
535
+ subresources,
536
+ mipLevels: 1,
537
+ format: options.format,
538
+ hasExplicitMipChain: false
539
+ };
540
+ }
541
+
542
+ const groupedSubresources = new Map<number, TextureSubresource[]>();
543
+ for (const subresource of subresources) {
544
+ const group = groupedSubresources.get(subresource.z) ?? [];
545
+ group.push(subresource);
546
+ groupedSubresources.set(subresource.z, group);
547
+ }
548
+
549
+ const hasExplicitMipChain = subresources.some(subresource => subresource.mipLevel > 0);
550
+ let resolvedFormat = options.format;
551
+ let resolvedMipLevels = Number.POSITIVE_INFINITY;
552
+ const validSubresources: TextureSubresource[] = [];
553
+
554
+ for (const [z, sliceSubresources] of groupedSubresources) {
555
+ // Validate each slice independently, then keep only the mip levels that are valid everywhere.
556
+ const sortedSubresources = [...sliceSubresources].sort(
557
+ (left, right) => left.mipLevel - right.mipLevel
558
+ );
559
+ const baseLevel = sortedSubresources[0];
560
+ if (!baseLevel || baseLevel.mipLevel !== 0) {
561
+ throw new Error(`DynamicTexture: slice ${z} is missing mip level 0`);
562
+ }
563
+
564
+ const baseSize = getTextureSubresourceSize(device, baseLevel);
565
+ if (baseSize.width !== size.width || baseSize.height !== size.height) {
566
+ throw new Error(
567
+ `DynamicTexture: slice ${z} base level dimensions ${baseSize.width}x${baseSize.height} do not match expected ${size.width}x${size.height}`
568
+ );
569
+ }
570
+
571
+ const baseFormat = getTextureSubresourceFormat(baseLevel);
572
+ if (baseFormat) {
573
+ if (resolvedFormat && resolvedFormat !== baseFormat) {
574
+ throw new Error(
575
+ `DynamicTexture: slice ${z} base level format "${baseFormat}" does not match texture format "${resolvedFormat}"`
576
+ );
577
+ }
578
+ resolvedFormat = baseFormat;
579
+ }
580
+
581
+ const mipLevelLimit =
582
+ resolvedFormat && device.isTextureFormatCompressed(resolvedFormat)
583
+ ? // Block-compressed formats cannot have mips smaller than a single compression block.
584
+ getMaxCompressedMipLevels(device, baseSize.width, baseSize.height, resolvedFormat)
585
+ : device.getMipLevelCount(baseSize.width, baseSize.height);
586
+
587
+ let validMipLevelsForSlice = 0;
588
+ for (
589
+ let expectedMipLevel = 0;
590
+ expectedMipLevel < sortedSubresources.length;
591
+ expectedMipLevel++
592
+ ) {
593
+ const subresource = sortedSubresources[expectedMipLevel];
594
+ // Stop at the first gap so callers can provide extra trailing data without breaking creation.
595
+ if (!subresource || subresource.mipLevel !== expectedMipLevel) {
596
+ break;
597
+ }
598
+ if (expectedMipLevel >= mipLevelLimit) {
599
+ break;
600
+ }
601
+
602
+ const subresourceSize = getTextureSubresourceSize(device, subresource);
603
+ const expectedWidth = Math.max(1, baseSize.width >> expectedMipLevel);
604
+ const expectedHeight = Math.max(1, baseSize.height >> expectedMipLevel);
605
+ if (subresourceSize.width !== expectedWidth || subresourceSize.height !== expectedHeight) {
606
+ break;
607
+ }
608
+
609
+ const subresourceFormat = getTextureSubresourceFormat(subresource);
610
+ if (subresourceFormat) {
611
+ if (!resolvedFormat) {
612
+ resolvedFormat = subresourceFormat;
613
+ }
614
+ // Later mip levels must stay on the same format as the validated base level.
615
+ if (subresourceFormat !== resolvedFormat) {
616
+ break;
617
+ }
618
+ }
619
+
620
+ validMipLevelsForSlice++;
621
+ validSubresources.push(subresource);
622
+ }
623
+
624
+ resolvedMipLevels = Math.min(resolvedMipLevels, validMipLevelsForSlice);
625
+ }
626
+
627
+ const mipLevels = Number.isFinite(resolvedMipLevels) ? Math.max(1, resolvedMipLevels) : 1;
628
+
629
+ return {
630
+ // Keep every slice trimmed to the same mip count so the texture shape stays internally consistent.
631
+ subresources: validSubresources.filter(subresource => subresource.mipLevel < mipLevels),
632
+ mipLevels,
633
+ format: resolvedFormat,
634
+ hasExplicitMipChain
635
+ };
636
+ }
637
+
638
+ // Read the per-level format using the transitional textureFormat -> format fallback rules.
639
+ function getTextureSubresourceFormat(subresource: TextureSubresource): TextureFormat | undefined {
640
+ if (subresource.type !== 'texture-data') {
641
+ return undefined;
642
+ }
643
+ return subresource.textureFormat ?? resolveTextureImageFormat(subresource.data);
644
+ }
645
+
646
+ // Resolve dimensions from either raw bytes or external-image subresources.
647
+ function getTextureSubresourceSize(
648
+ device: Device,
649
+ subresource: TextureSubresource
650
+ ): {width: number; height: number} {
651
+ switch (subresource.type) {
652
+ case 'external-image':
653
+ return device.getExternalImageSize(subresource.image);
654
+ case 'texture-data':
655
+ return {width: subresource.data.width, height: subresource.data.height};
656
+ default:
657
+ throw new Error('Unsupported texture subresource');
658
+ }
659
+ }
660
+
661
+ // Count the mip levels that stay at or above one compression block in each dimension.
662
+ function getMaxCompressedMipLevels(
663
+ device: Device,
664
+ baseWidth: number,
665
+ baseHeight: number,
666
+ format: TextureFormat
667
+ ): number {
668
+ const {blockWidth = 1, blockHeight = 1} = device.getTextureFormatInfo(format);
669
+ let mipLevels = 1;
670
+ for (let mipLevel = 1; ; mipLevel++) {
671
+ const width = Math.max(1, baseWidth >> mipLevel);
672
+ const height = Math.max(1, baseHeight >> mipLevel);
673
+ if (width < blockWidth || height < blockHeight) {
674
+ break;
675
+ }
676
+ mipLevels++;
677
+ }
678
+ return mipLevels;
679
+ }
680
+
681
+ // HELPERS
682
+
683
+ /** Resolve all promises in a nested data structure */
684
+ async function awaitAllPromises(x: any): Promise<any> {
685
+ x = await x;
686
+ if (Array.isArray(x)) {
687
+ return await Promise.all(x.map(awaitAllPromises));
688
+ }
689
+ if (x && typeof x === 'object' && x.constructor === Object) {
690
+ const object: Record<string, any> = x;
691
+ const values = await Promise.all(Object.values(object).map(awaitAllPromises));
692
+ const keys = Object.keys(object);
693
+ const resolvedObject: Record<string, any> = {};
694
+ for (let i = 0; i < keys.length; i++) {
695
+ resolvedObject[keys[i]] = values[i];
696
+ }
697
+ return resolvedObject;
698
+ }
699
+ return x;
700
+ }
701
+
702
+ // /** @note experimental: Set multiple mip levels (2D), optionally at `z`, slice (depth/array level) index */
703
+ // setTexture2DData(lodData: Texture2DData, z: number = 0): void {
704
+ // this._checkReady();
705
+
706
+ // const lodArray = this._normalizeTexture2DData(lodData);
707
+
708
+ // // If user supplied multiple mip levels, warn if auto-mips also requested
709
+ // if (lodArray.length > 1 && this.props.mipmaps !== false) {
710
+ // log.warn(
711
+ // `Texture ${this.id}: provided multiple LODs and also requested mipmap generation.`
712
+ // )();
713
+ // }
714
+
715
+ // for (let mipLevel = 0; mipLevel < lodArray.length; mipLevel++) {
716
+ // const imageData = lodArray[mipLevel];
717
+ // if (this.device.isExternalImage(imageData)) {
718
+ // this.texture.copyExternalImage({image: imageData, z, mipLevel, flipY: true});
719
+ // } else if (this._isTextureImageData(imageData)) {
720
+ // this.texture.copyImageData({data: imageData.data, z, mipLevel});
721
+ // } else {
722
+ // throw new Error('Unsupported 2D mip-level payload');
723
+ // }
724
+ // }
725
+ // }
726
+
727
+ // /** Normalize 2D layer payload into an array of mip-level items */
728
+ // private _normalizeTexture2DData(data: Texture2DData): (TextureImageData | ExternalImage)[] {
729
+ // return Array.isArray(data) ? data : [data];
730
+ // }