@kitware/vtk.js 34.0.0-beta.1 → 34.0.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.
@@ -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
  }