@kitware/vtk.js 34.0.0-beta.1 → 34.0.0-beta.2

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.
@@ -1,4 +1,5 @@
1
1
  import { vtkAlgorithm } from './../../interfaces';
2
+ import { Nullable } from './../../types';
2
3
 
3
4
  /**
4
5
  *
@@ -15,67 +16,110 @@ export interface ITextureInitialValues {
15
16
 
16
17
  export interface vtkTexture extends vtkAlgorithm {
17
18
  /**
18
- *
19
+ * Returns the canvas used by the texture.
20
+ */
21
+ getCanvas(): Nullable<HTMLCanvasElement>;
22
+
23
+ /**
24
+ * Returns true if the texture is set to repeat at the edges.
19
25
  */
20
26
  getRepeat(): boolean;
21
27
 
22
28
  /**
23
- *
29
+ * Returns true if the texture is set to clamp at the edges.
24
30
  */
25
31
  getEdgeClamp(): boolean;
26
32
 
27
33
  /**
28
- *
34
+ * Returns true if the texture is set to interpolate between texels.
29
35
  */
30
36
  getInterpolate(): boolean;
31
37
 
32
38
  /**
33
- *
39
+ * Returns the image used by the texture.
40
+ */
41
+ getImage(): Nullable<HTMLImageElement>;
42
+
43
+ /**
44
+ * Returns an ImageBitmap object.
34
45
  */
35
- getImage(): any;
46
+ getImageBitmap(): Nullable<ImageBitmap>;
36
47
 
37
48
  /**
38
- *
49
+ * Returns true if the image is loaded.
39
50
  */
40
51
  getImageLoaded(): boolean;
41
52
 
42
53
  /**
43
- *
54
+ * Returns the input image data object.
55
+ */
56
+ getInputAsJsImageData(): Nullable<
57
+ ImageData | ImageBitmap | HTMLCanvasElement | HTMLImageElement
58
+ >;
59
+
60
+ /**
61
+ * Returns the current mip level of the texture.
44
62
  */
45
63
  getMipLevel(): number;
46
64
 
47
65
  /**
48
- *
49
- * @param repeat
50
- * @default false
66
+ * Returns true if the texture can be resized at run time.
67
+ * This is useful for dynamic textures that may change size based on user
68
+ * interaction or other factors.
51
69
  */
52
- setRepeat(repeat: boolean): boolean;
70
+ getResizable(): boolean;
71
+
72
+ /**
73
+ * Returns the canvas used by the texture.
74
+ */
75
+ setCanvas(canvas: HTMLCanvasElement): void;
53
76
 
54
77
  /**
55
- *
78
+ * Sets the texture to clamp at the edges.
56
79
  * @param edgeClamp
57
80
  * @default false
58
81
  */
59
82
  setEdgeClamp(edgeClamp: boolean): boolean;
60
83
 
61
84
  /**
62
- *
85
+ * Sets the texture to interpolate between texels.
63
86
  * @param interpolate
64
87
  * @default false
65
88
  */
66
89
  setInterpolate(interpolate: boolean): boolean;
67
90
 
68
91
  /**
69
- *
92
+ * Sets the image used by the texture.
70
93
  * @param image
71
94
  * @default null
72
95
  */
73
- setImage(image: any): void;
96
+ setImage(image: HTMLImageElement): void;
74
97
 
75
98
  /**
99
+ * Sets the image as an ImageBitmap object.
100
+ * Supported in WebGPU only.
101
+ * @param imageBitmap
102
+ */
103
+ setImageBitmap(imageBitmap: ImageBitmap): void;
104
+
105
+ /**
106
+ * Sets the input image data as a JavaScript ImageData object.
107
+ * @param imageData
108
+ */
109
+ setJsImageData(imageData: ImageData): void;
110
+
111
+ /**
112
+ * Sets the current mip level of the texture.
76
113
  * @param level
77
114
  */
78
115
  setMipLevel(level: number): boolean;
116
+
117
+ /**
118
+ * Sets the texture to repeat at the edges.
119
+ * @param repeat
120
+ * @default false
121
+ */
122
+ setRepeat(repeat: boolean): boolean;
79
123
  }
80
124
 
81
125
  /**
@@ -98,26 +142,34 @@ export function extend(
98
142
  export function newInstance(initialValues?: ITextureInitialValues): vtkTexture;
99
143
 
100
144
  /**
101
- * Method used to create mipmaps from given texture data. Works best with textures that have a
102
- * width and a height that are powers of two.
145
+ * Generates mipmaps for a given GPU texture using a compute shader.
146
+ *
147
+ * This function iteratively generates each mip level for the provided texture,
148
+ * using a bilinear downsampling compute shader implemented in WGSL. It creates
149
+ * the necessary pipeline, bind groups, and dispatches compute passes for each
150
+ * mip level.
103
151
  *
104
- * @param nativeArray the array of data to create mipmaps from.
105
- * @param width the width of the data
106
- * @param height the height of the data
107
- * @param level the level to which additional mipmaps are generated.
152
+ * @param {GPUDevice} device - The WebGPU device used to create resources and submit commands.
153
+ * @param {GPUTexture} texture - The GPU texture for which mipmaps will be generated.
154
+ * @param {number} mipLevelCount - The total number of mip levels to generate (including the base level).
108
155
  */
109
156
  export function generateMipmaps(
110
- nativeArray: any,
111
- width: number,
112
- height: number,
113
- level: number
157
+ device: any,
158
+ texture: any,
159
+ mipLevelCount: number
114
160
  ): Array<Uint8ClampedArray>;
115
161
 
116
162
  /**
117
- * vtkTexture is an image algorithm that handles loading and binding of texture maps.
118
- * It obtains its data from an input image data dataset type.
119
- * Thus you can create visualization pipelines to read, process, and construct textures.
120
- * Note that textures will only work if texture coordinates are also defined, and if the rendering system supports texture.
163
+ * vtkTexture is an image algorithm that handles loading and binding of texture
164
+ * maps. It obtains its data from an input image data dataset type. Thus you can
165
+ * create visualization pipelines to read, process, and construct textures. Note
166
+ * that textures will only work if texture coordinates are also defined, and if
167
+ * the rendering system supports texture.
168
+ *
169
+ * This class is used in both WebGL and WebGPU rendering backends, but the
170
+ * implementation details may vary. In WebGL, it uses HTMLImageElement and
171
+ * HTMLCanvasElement for textures, while in WebGPU, it uses HTMLImageElement,
172
+ * HTMLCanvasElement, and ImageBitmap.
121
173
  */
122
174
  export declare const vtkTexture: {
123
175
  newInstance: typeof newInstance;
@@ -1,5 +1,7 @@
1
1
  import { m as macro } from '../../macros2.js';
2
2
 
3
+ /* eslint-disable no-bitwise */
4
+
3
5
  // ----------------------------------------------------------------------------
4
6
  // vtkTexture methods
5
7
  // ----------------------------------------------------------------------------
@@ -23,11 +25,29 @@ function vtkTexture(publicAPI, model) {
23
25
  publicAPI.setInputConnection(null);
24
26
  model.image = null;
25
27
  model.canvas = null;
28
+ model.imageBitmap = null;
26
29
  }
27
30
  model.jsImageData = imageData;
28
31
  model.imageLoaded = true;
29
32
  publicAPI.modified();
30
33
  };
34
+ publicAPI.setImageBitmap = imageBitmap => {
35
+ if (model.imageBitmap === imageBitmap) {
36
+ return;
37
+ }
38
+
39
+ // clear other entries
40
+ if (imageBitmap !== null) {
41
+ publicAPI.setInputData(null);
42
+ publicAPI.setInputConnection(null);
43
+ model.image = null;
44
+ model.canvas = null;
45
+ model.jsImageData = null;
46
+ }
47
+ model.imageBitmap = imageBitmap;
48
+ model.imageLoaded = true;
49
+ publicAPI.modified();
50
+ };
31
51
  publicAPI.setCanvas = canvas => {
32
52
  if (model.canvas === canvas) {
33
53
  return;
@@ -38,6 +58,7 @@ function vtkTexture(publicAPI, model) {
38
58
  publicAPI.setInputData(null);
39
59
  publicAPI.setInputConnection(null);
40
60
  model.image = null;
61
+ model.imageBitmap = null;
41
62
  model.jsImageData = null;
42
63
  }
43
64
  model.canvas = canvas;
@@ -54,6 +75,7 @@ function vtkTexture(publicAPI, model) {
54
75
  publicAPI.setInputConnection(null);
55
76
  model.canvas = null;
56
77
  model.jsImageData = null;
78
+ model.imageBitmap = null;
57
79
  }
58
80
  model.image = image;
59
81
  model.imageLoaded = false;
@@ -86,13 +108,20 @@ function vtkTexture(publicAPI, model) {
86
108
  width = model.image.width;
87
109
  height = model.image.height;
88
110
  }
111
+ if (model.imageBitmap) {
112
+ width = model.imageBitmap.width;
113
+ height = model.imageBitmap.height;
114
+ }
89
115
  const dimensionality = (width > 1) + (height > 1) + (depth > 1);
90
116
  return dimensionality;
91
117
  };
92
118
  publicAPI.getInputAsJsImageData = () => {
93
119
  if (!model.imageLoaded || publicAPI.getInputData()) return null;
94
120
  if (model.jsImageData) {
95
- return model.jsImageData();
121
+ return model.jsImageData;
122
+ }
123
+ if (model.imageBitmap) {
124
+ return model.imageBitmap;
96
125
  }
97
126
  if (model.canvas) {
98
127
  const context = model.canvas.getContext('2d');
@@ -100,104 +129,159 @@ function vtkTexture(publicAPI, model) {
100
129
  return imageData;
101
130
  }
102
131
  if (model.image) {
103
- const canvas = document.createElement('canvas');
104
- canvas.width = model.image.width;
105
- canvas.height = model.image.height;
132
+ const width = model.image.width;
133
+ const height = model.image.height;
134
+ const canvas = new OffscreenCanvas(width, height);
106
135
  const context = canvas.getContext('2d');
107
- context.translate(0, canvas.height);
136
+ context.translate(0, height);
108
137
  context.scale(1, -1);
109
- context.drawImage(model.image, 0, 0, model.image.width, model.image.height);
110
- const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
138
+ context.drawImage(model.image, 0, 0, width, height);
139
+ const imageData = context.getImageData(0, 0, width, height);
111
140
  return imageData;
112
141
  }
113
142
  return null;
114
143
  };
115
144
  }
116
145
 
117
- // Use nativeArray instead of self
118
- const generateMipmaps = (nativeArray, width, height, level) => {
119
- // TODO: FIX UNEVEN TEXTURE MIP GENERATION:
120
- // When textures don't have standard ratios, higher mip levels
121
- // result in their color chanels getting messed up and shifting
122
- // 3x3 gaussian kernel
123
- const g3m = [1, 2, 1]; // eslint-disable-line
124
- const g3w = 4; // eslint-disable-line
125
-
126
- const kernel = g3m;
127
- const kernelWeight = g3w;
128
- const hs = nativeArray.length / (width * height); // TODO: support for textures with depth more than 1
129
- let currentWidth = width;
130
- let currentHeight = height;
131
- let imageData = nativeArray;
132
- const maps = [imageData];
133
- for (let i = 0; i < level; i++) {
134
- const oldData = [...imageData];
135
- currentWidth /= 2;
136
- currentHeight /= 2;
137
- imageData = new Uint8ClampedArray(currentWidth * currentHeight * hs);
138
- const vs = hs * currentWidth;
139
-
140
- // Scale down
141
- let shift = 0;
142
- for (let p = 0; p < imageData.length; p += hs) {
143
- if (p % vs === 0) {
144
- shift += 2 * hs * currentWidth;
146
+ /**
147
+ * Generates mipmaps for a given GPU texture using a compute shader.
148
+ *
149
+ * This function iteratively generates each mip level for the provided texture,
150
+ * using a bilinear downsampling compute shader implemented in WGSL. It creates
151
+ * the necessary pipeline, bind groups, and dispatches compute passes for each
152
+ * mip level.
153
+ *
154
+ * @param {GPUDevice} device - The WebGPU device used to create resources and submit commands.
155
+ * @param {GPUTexture} texture - The GPU texture for which mipmaps will be generated. Must be created with mip levels.
156
+ * @param {number} mipLevelCount - The total number of mip levels to generate (including the base level).
157
+ */
158
+ const generateMipmaps = (device, texture, mipLevelCount) => {
159
+ const computeShaderCode = `
160
+ @group(0) @binding(0) var inputTexture: texture_2d<f32>;
161
+ @group(0) @binding(1) var outputTexture: texture_storage_2d<rgba8unorm, write>;
162
+
163
+ @compute @workgroup_size(8, 8)
164
+ fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
165
+ let texelCoord = vec2<i32>(global_id.xy);
166
+ let outputSize = textureDimensions(outputTexture);
167
+
168
+ if (texelCoord.x >= i32(outputSize.x) || texelCoord.y >= i32(outputSize.y)) {
169
+ return;
145
170
  }
146
- for (let c = 0; c < hs; c++) {
147
- let sample = oldData[shift + c];
148
- sample += oldData[shift + hs + c];
149
- sample += oldData[shift - 2 * vs + c];
150
- sample += oldData[shift - 2 * vs + hs + c];
151
- sample /= 4;
152
- imageData[p + c] = sample;
171
+
172
+ let inputSize = textureDimensions(inputTexture);
173
+ let scale = vec2<f32>(inputSize) / vec2<f32>(outputSize);
174
+
175
+ // Compute the floating-point source coordinate
176
+ let srcCoord = (vec2<f32>(texelCoord) + 0.5) * scale - 0.5;
177
+
178
+ // Get integer coordinates for the four surrounding texels
179
+ let x0 = i32(floor(srcCoord.x));
180
+ let x1 = min(x0 + 1, i32(inputSize.x) - 1);
181
+ let y0 = i32(floor(srcCoord.y));
182
+ let y1 = min(y0 + 1, i32(inputSize.y) - 1);
183
+
184
+ // Compute the weights
185
+ let wx = srcCoord.x - f32(x0);
186
+ let wy = srcCoord.y - f32(y0);
187
+
188
+ // Fetch the four texels
189
+ let c00 = textureLoad(inputTexture, vec2<i32>(x0, y0), 0);
190
+ let c10 = textureLoad(inputTexture, vec2<i32>(x1, y0), 0);
191
+ let c01 = textureLoad(inputTexture, vec2<i32>(x0, y1), 0);
192
+ let c11 = textureLoad(inputTexture, vec2<i32>(x1, y1), 0);
193
+
194
+ // Bilinear interpolation
195
+ let color = mix(
196
+ mix(c00, c10, wx),
197
+ mix(c01, c11, wx),
198
+ wy
199
+ );
200
+
201
+ textureStore(outputTexture, texelCoord, color);
202
+ }
203
+ `;
204
+ const computeShader = device.createShaderModule({
205
+ code: computeShaderCode
206
+ });
207
+ const bindGroupLayout = device.createBindGroupLayout({
208
+ entries: [{
209
+ binding: 0,
210
+ // eslint-disable-next-line no-undef
211
+ visibility: GPUShaderStage.COMPUTE,
212
+ texture: {
213
+ sampleType: 'float'
153
214
  }
154
- shift += 2 * hs;
155
- }
156
-
157
- // Horizontal Pass
158
- let dataCopy = [...imageData];
159
- for (let p = 0; p < imageData.length; p += hs) {
160
- for (let c = 0; c < hs; c++) {
161
- let x = -(kernel.length - 1) / 2;
162
- let kw = kernelWeight;
163
- let value = 0.0;
164
- for (let k = 0; k < kernel.length; k++) {
165
- let index = p + c + x * hs;
166
- const lineShift = index % vs - (p + c) % vs;
167
- if (lineShift > hs) index += vs;
168
- if (lineShift < -hs) index -= vs;
169
- if (dataCopy[index]) {
170
- value += dataCopy[index] * kernel[k];
171
- } else {
172
- kw -= kernel[k];
173
- }
174
- x += 1;
175
- }
176
- imageData[p + c] = value / kw;
215
+ }, {
216
+ binding: 1,
217
+ // eslint-disable-next-line no-undef
218
+ visibility: GPUShaderStage.COMPUTE,
219
+ storageTexture: {
220
+ format: 'rgba8unorm',
221
+ access: 'write-only'
177
222
  }
178
- }
179
- // Vertical Pass
180
- dataCopy = [...imageData];
181
- for (let p = 0; p < imageData.length; p += hs) {
182
- for (let c = 0; c < hs; c++) {
183
- let x = -(kernel.length - 1) / 2;
184
- let kw = kernelWeight;
185
- let value = 0.0;
186
- for (let k = 0; k < kernel.length; k++) {
187
- const index = p + c + x * vs;
188
- if (dataCopy[index]) {
189
- value += dataCopy[index] * kernel[k];
190
- } else {
191
- kw -= kernel[k];
192
- }
193
- x += 1;
194
- }
195
- imageData[p + c] = value / kw;
223
+ }, {
224
+ binding: 2,
225
+ // eslint-disable-next-line no-undef
226
+ visibility: GPUShaderStage.COMPUTE,
227
+ sampler: {
228
+ type: 'filtering'
196
229
  }
230
+ }]
231
+ });
232
+ const pipelineLayout = device.createPipelineLayout({
233
+ bindGroupLayouts: [bindGroupLayout]
234
+ });
235
+ const pipeline = device.createComputePipeline({
236
+ label: 'ComputeMipmapPipeline',
237
+ layout: pipelineLayout,
238
+ compute: {
239
+ module: computeShader,
240
+ entryPoint: 'main'
197
241
  }
198
- maps.push(imageData);
242
+ });
243
+ const sampler = device.createSampler({
244
+ magFilter: 'linear',
245
+ minFilter: 'linear'
246
+ });
247
+
248
+ // Generate each mip level
249
+ for (let mipLevel = 1; mipLevel < mipLevelCount; mipLevel++) {
250
+ const srcView = texture.createView({
251
+ baseMipLevel: mipLevel - 1,
252
+ mipLevelCount: 1
253
+ });
254
+ const dstView = texture.createView({
255
+ baseMipLevel: mipLevel,
256
+ mipLevelCount: 1
257
+ });
258
+ const bindGroup = device.createBindGroup({
259
+ layout: pipeline.getBindGroupLayout(0),
260
+ entries: [{
261
+ binding: 0,
262
+ resource: srcView
263
+ }, {
264
+ binding: 1,
265
+ resource: dstView
266
+ }, {
267
+ binding: 2,
268
+ resource: sampler
269
+ }]
270
+ });
271
+ const commandEncoder = device.createCommandEncoder({
272
+ label: `MipmapGenerateCommandEncoder`
273
+ });
274
+ const computePass = commandEncoder.beginComputePass();
275
+ computePass.setPipeline(pipeline);
276
+ computePass.setBindGroup(0, bindGroup);
277
+ const mipWidth = Math.max(1, texture.width >> mipLevel);
278
+ const mipHeight = Math.max(1, texture.height >> mipLevel);
279
+ const workgroupsX = Math.ceil(mipWidth / 8);
280
+ const workgroupsY = Math.ceil(mipHeight / 8);
281
+ computePass.dispatchWorkgroups(workgroupsX, workgroupsY);
282
+ computePass.end();
283
+ device.queue.submit([commandEncoder.finish()]);
199
284
  }
200
- return maps;
201
285
  };
202
286
 
203
287
  // ----------------------------------------------------------------------------
@@ -208,6 +292,7 @@ const DEFAULT_VALUES = {
208
292
  image: null,
209
293
  canvas: null,
210
294
  jsImageData: null,
295
+ imageBitmap: null,
211
296
  imageLoaded: false,
212
297
  repeat: false,
213
298
  interpolate: false,
@@ -225,7 +310,7 @@ function extend(publicAPI, model) {
225
310
  // Build VTK API
226
311
  macro.obj(publicAPI, model);
227
312
  macro.algo(publicAPI, model, 6, 0);
228
- macro.get(publicAPI, model, ['canvas', 'image', 'jsImageData', 'imageLoaded', 'resizable']);
313
+ macro.get(publicAPI, model, ['canvas', 'image', 'jsImageData', 'imageBitmap', 'imageLoaded', 'resizable']);
229
314
  macro.setGet(publicAPI, model, ['repeat', 'edgeClamp', 'interpolate', 'mipLevel']);
230
315
  vtkTexture(publicAPI, model);
231
316
  }