@kitware/vtk.js 33.2.0 → 33.2.1

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.
@@ -98,19 +98,21 @@ export function extend(
98
98
  export function newInstance(initialValues?: ITextureInitialValues): vtkTexture;
99
99
 
100
100
  /**
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.
101
+ * Generates mipmaps for a given GPU texture using a compute shader.
103
102
  *
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.
103
+ * This function iteratively generates each mip level for the provided texture,
104
+ * using a bilinear downsampling compute shader implemented in WGSL. It creates
105
+ * the necessary pipeline, bind groups, and dispatches compute passes for each
106
+ * mip level.
107
+ *
108
+ * @param {GPUDevice} device - The WebGPU device used to create resources and submit commands.
109
+ * @param {GPUTexture} texture - The GPU texture for which mipmaps will be generated.
110
+ * @param {number} mipLevelCount - The total number of mip levels to generate (including the base level).
108
111
  */
109
112
  export function generateMipmaps(
110
- nativeArray: any,
111
- width: number,
112
- height: number,
113
- level: number
113
+ device: any,
114
+ texture: any,
115
+ mipLevelCount: number
114
116
  ): Array<Uint8ClampedArray>;
115
117
 
116
118
  /**
@@ -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
  // ----------------------------------------------------------------------------
@@ -114,90 +116,142 @@ function vtkTexture(publicAPI, model) {
114
116
  };
115
117
  }
116
118
 
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;
119
+ /**
120
+ * Generates mipmaps for a given GPU texture using a compute shader.
121
+ *
122
+ * This function iteratively generates each mip level for the provided texture,
123
+ * using a bilinear downsampling compute shader implemented in WGSL. It creates
124
+ * the necessary pipeline, bind groups, and dispatches compute passes for each
125
+ * mip level.
126
+ *
127
+ * @param {GPUDevice} device - The WebGPU device used to create resources and submit commands.
128
+ * @param {GPUTexture} texture - The GPU texture for which mipmaps will be generated. Must be created with mip levels.
129
+ * @param {number} mipLevelCount - The total number of mip levels to generate (including the base level).
130
+ */
131
+ const generateMipmaps = (device, texture, mipLevelCount) => {
132
+ const computeShaderCode = `
133
+ @group(0) @binding(0) var inputTexture: texture_2d<f32>;
134
+ @group(0) @binding(1) var outputTexture: texture_storage_2d<rgba8unorm, write>;
135
+
136
+ @compute @workgroup_size(8, 8)
137
+ fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
138
+ let texelCoord = vec2<i32>(global_id.xy);
139
+ let outputSize = textureDimensions(outputTexture);
140
+
141
+ if (texelCoord.x >= i32(outputSize.x) || texelCoord.y >= i32(outputSize.y)) {
142
+ return;
145
143
  }
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;
144
+
145
+ let inputSize = textureDimensions(inputTexture);
146
+ let scale = vec2<f32>(inputSize) / vec2<f32>(outputSize);
147
+
148
+ // Compute the floating-point source coordinate
149
+ let srcCoord = (vec2<f32>(texelCoord) + 0.5) * scale - 0.5;
150
+
151
+ // Get integer coordinates for the four surrounding texels
152
+ let x0 = i32(floor(srcCoord.x));
153
+ let x1 = min(x0 + 1, i32(inputSize.x) - 1);
154
+ let y0 = i32(floor(srcCoord.y));
155
+ let y1 = min(y0 + 1, i32(inputSize.y) - 1);
156
+
157
+ // Compute the weights
158
+ let wx = srcCoord.x - f32(x0);
159
+ let wy = srcCoord.y - f32(y0);
160
+
161
+ // Fetch the four texels
162
+ let c00 = textureLoad(inputTexture, vec2<i32>(x0, y0), 0);
163
+ let c10 = textureLoad(inputTexture, vec2<i32>(x1, y0), 0);
164
+ let c01 = textureLoad(inputTexture, vec2<i32>(x0, y1), 0);
165
+ let c11 = textureLoad(inputTexture, vec2<i32>(x1, y1), 0);
166
+
167
+ // Bilinear interpolation
168
+ let color = mix(
169
+ mix(c00, c10, wx),
170
+ mix(c01, c11, wx),
171
+ wy
172
+ );
173
+
174
+ textureStore(outputTexture, texelCoord, color);
175
+ }
176
+ `;
177
+ const computeShader = device.createShaderModule({
178
+ code: computeShaderCode
179
+ });
180
+ const bindGroupLayout = device.createBindGroupLayout({
181
+ entries: [{
182
+ binding: 0,
183
+ // eslint-disable-next-line no-undef
184
+ visibility: GPUShaderStage.COMPUTE,
185
+ texture: {
186
+ sampleType: 'float'
153
187
  }
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;
188
+ }, {
189
+ binding: 1,
190
+ // eslint-disable-next-line no-undef
191
+ visibility: GPUShaderStage.COMPUTE,
192
+ storageTexture: {
193
+ format: 'rgba8unorm',
194
+ access: 'write-only'
177
195
  }
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;
196
+ }, {
197
+ binding: 2,
198
+ // eslint-disable-next-line no-undef
199
+ visibility: GPUShaderStage.COMPUTE,
200
+ sampler: {
201
+ type: 'filtering'
196
202
  }
203
+ }]
204
+ });
205
+ const pipelineLayout = device.createPipelineLayout({
206
+ bindGroupLayouts: [bindGroupLayout]
207
+ });
208
+ const pipeline = device.createComputePipeline({
209
+ layout: pipelineLayout,
210
+ compute: {
211
+ module: computeShader,
212
+ entryPoint: 'main'
197
213
  }
198
- maps.push(imageData);
214
+ });
215
+ const sampler = device.createSampler({
216
+ magFilter: 'linear',
217
+ minFilter: 'linear'
218
+ });
219
+
220
+ // Generate each mip level
221
+ for (let mipLevel = 1; mipLevel < mipLevelCount; mipLevel++) {
222
+ const srcView = texture.createView({
223
+ baseMipLevel: mipLevel - 1,
224
+ mipLevelCount: 1
225
+ });
226
+ const dstView = texture.createView({
227
+ baseMipLevel: mipLevel,
228
+ mipLevelCount: 1
229
+ });
230
+ const bindGroup = device.createBindGroup({
231
+ layout: pipeline.getBindGroupLayout(0),
232
+ entries: [{
233
+ binding: 0,
234
+ resource: srcView
235
+ }, {
236
+ binding: 1,
237
+ resource: dstView
238
+ }, {
239
+ binding: 2,
240
+ resource: sampler
241
+ }]
242
+ });
243
+ const commandEncoder = device.createCommandEncoder();
244
+ const computePass = commandEncoder.beginComputePass();
245
+ computePass.setPipeline(pipeline);
246
+ computePass.setBindGroup(0, bindGroup);
247
+ const mipWidth = Math.max(1, texture.width >> mipLevel);
248
+ const mipHeight = Math.max(1, texture.height >> mipLevel);
249
+ const workgroupsX = Math.ceil(mipWidth / 8);
250
+ const workgroupsY = Math.ceil(mipHeight / 8);
251
+ computePass.dispatchWorkgroups(workgroupsX, workgroupsY);
252
+ computePass.end();
253
+ device.queue.submit([commandEncoder.finish()]);
199
254
  }
200
- return maps;
201
255
  };
202
256
 
203
257
  // ----------------------------------------------------------------------------
@@ -67,6 +67,11 @@ function vtkWebGPUTexture(publicAPI, model) {
67
67
  texture: model.handle,
68
68
  premultipliedAlpha: true
69
69
  }, [model.width, model.height, model.depth]);
70
+
71
+ // Generate mipmaps on GPU if needed
72
+ if (publicAPI.getDimensionality() !== 3 && model.mipLevel > 0) {
73
+ vtkTexture.generateMipmaps(model.device.getHandle(), model.handle, model.mipLevel + 1);
74
+ }
70
75
  model.ready = true;
71
76
  return;
72
77
  }
@@ -80,35 +85,48 @@ function vtkWebGPUTexture(publicAPI, model) {
80
85
  }
81
86
  const tDetails = vtkWebGPUTypes.getDetailsFromTextureFormat(model.format);
82
87
  let bufferBytesPerRow = model.width * tDetails.stride;
83
- const fixAll = (arr, height, depth) => {
88
+ const alignTextureData = (arr, height, depth) => {
84
89
  // bytesPerRow must be a multiple of 256 so we might need to rebuild
85
90
  // the data here before passing to the buffer. e.g. if it is unorm8x4 then
86
91
  // we need to have width be a multiple of 64
87
- const inWidthInBytes = arr.length / (height * depth) * arr.BYTES_PER_ELEMENT;
88
-
89
- // is this a half float texture?
92
+ // Check if the texture is half float
90
93
  const halfFloat = tDetails.elementSize === 2 && tDetails.sampleType === 'float';
94
+ const bytesPerElement = arr.BYTES_PER_ELEMENT;
95
+ const inWidthInBytes = arr.length / (height * depth) * bytesPerElement;
96
+
97
+ // No changes needed if not half float and already aligned
98
+ if (!halfFloat && inWidthInBytes % 256 === 0) {
99
+ return [arr, inWidthInBytes];
100
+ }
101
+
102
+ // Calculate dimensions for the new buffer
103
+ const inWidth = inWidthInBytes / bytesPerElement;
104
+ const outBytesPerElement = tDetails.elementSize;
105
+ const outWidthInBytes = 256 * Math.floor((inWidth * outBytesPerElement + 255) / 256);
106
+ const outWidth = outWidthInBytes / outBytesPerElement;
91
107
 
92
- // if we need to copy the data
93
- if (halfFloat || inWidthInBytes % 256) {
94
- const inArray = arr;
95
- const inWidth = inWidthInBytes / inArray.BYTES_PER_ELEMENT;
96
- const outBytesPerElement = tDetails.elementSize;
97
- const outWidthInBytes = 256 * Math.floor((inWidth * outBytesPerElement + 255) / 256);
98
- const outWidth = outWidthInBytes / outBytesPerElement;
99
- const outArray = macro.newTypedArray(halfFloat ? 'Uint16Array' : inArray.constructor.name, outWidth * height * depth);
100
- for (let v = 0; v < height * depth; v++) {
101
- if (halfFloat) {
102
- for (let i = 0; i < inWidth; i++) {
103
- outArray[v * outWidth + i] = HalfFloat.toHalf(inArray[v * inWidth + i]);
104
- }
105
- } else {
106
- outArray.set(inArray.subarray(v * inWidth, (v + 1) * inWidth), v * outWidth);
108
+ // Create the output array
109
+ const outArray = macro.newTypedArray(halfFloat ? 'Uint16Array' : arr.constructor.name, outWidth * height * depth);
110
+
111
+ // Copy and convert data when needed
112
+ const totalRows = height * depth;
113
+ if (halfFloat) {
114
+ for (let v = 0; v < totalRows; v++) {
115
+ const inOffset = v * inWidth;
116
+ const outOffset = v * outWidth;
117
+ for (let i = 0; i < inWidth; i++) {
118
+ outArray[outOffset + i] = HalfFloat.toHalf(arr[inOffset + i]);
107
119
  }
108
120
  }
109
- return [outArray, outWidthInBytes];
121
+ } else if (outWidth === inWidth) {
122
+ // If the output width is the same as input, just copy
123
+ outArray.set(arr);
124
+ } else {
125
+ for (let v = 0; v < totalRows; v++) {
126
+ outArray.set(arr.subarray(v * inWidth, (v + 1) * inWidth), v * outWidth);
127
+ }
110
128
  }
111
- return [arr, inWidthInBytes];
129
+ return [outArray, outWidthInBytes];
112
130
  };
113
131
  if (req.nativeArray) {
114
132
  nativeArray = req.nativeArray;
@@ -126,48 +144,43 @@ function vtkWebGPUTexture(publicAPI, model) {
126
144
  }
127
145
  const cmdEnc = model.device.createCommandEncoder();
128
146
  if (publicAPI.getDimensionality() !== 3) {
129
- // Non-3D, supports mipmaps
130
- const mips = vtkTexture.generateMipmaps(nativeArray, model.width, model.height, model.mipLevel);
131
- let currentWidth = model.width;
132
- let currentHeight = model.height;
133
- for (let m = 0; m <= model.mipLevel; m++) {
134
- const fix = fixAll(mips[m], currentHeight, 1);
135
- bufferBytesPerRow = fix[1];
136
- const buffRequest = {
137
- dataArray: req.dataArray ? req.dataArray : null,
138
- nativeArray: fix[0],
139
- /* eslint-disable no-undef */
140
- usage: BufferUsage.Texture
141
- /* eslint-enable no-undef */
142
- };
147
+ // Non-3D
148
+ // First, upload the base mip level (level 0)
149
+ const ret = alignTextureData(nativeArray, model.height, 1);
150
+ bufferBytesPerRow = ret[1];
151
+ const buffRequest = {
152
+ dataArray: req.dataArray ? req.dataArray : null,
153
+ nativeArray: ret[0],
154
+ usage: BufferUsage.Texture
155
+ };
156
+ const buff = model.device.getBufferManager().getBuffer(buffRequest);
157
+ cmdEnc.copyBufferToTexture({
158
+ buffer: buff.getHandle(),
159
+ offset: 0,
160
+ bytesPerRow: bufferBytesPerRow,
161
+ rowsPerImage: model.height
162
+ }, {
163
+ texture: model.handle,
164
+ mipLevel: 0
165
+ }, [model.width, model.height, 1]);
143
166
 
144
- const buff = model.device.getBufferManager().getBuffer(buffRequest);
145
- cmdEnc.copyBufferToTexture({
146
- buffer: buff.getHandle(),
147
- offset: 0,
148
- bytesPerRow: bufferBytesPerRow,
149
- rowsPerImage: currentHeight
150
- }, {
151
- texture: model.handle,
152
- mipLevel: m
153
- }, [currentWidth, currentHeight, 1]);
154
- currentWidth /= 2;
155
- currentHeight /= 2;
156
- }
167
+ // Submit the base level upload
157
168
  model.device.submitCommandEncoder(cmdEnc);
169
+
170
+ // Generate remaining mip levels on GPU
171
+ if (model.mipLevel > 0) {
172
+ vtkTexture.generateMipmaps(model.device.getHandle(), model.handle, model.mipLevel + 1);
173
+ }
158
174
  model.ready = true;
159
175
  } else {
160
176
  // 3D, no mipmaps
161
- const fix = fixAll(nativeArray, model.height, model.depth);
162
- bufferBytesPerRow = fix[1];
177
+ const ret = alignTextureData(nativeArray, model.height, model.depth);
178
+ bufferBytesPerRow = ret[1];
163
179
  const buffRequest = {
164
180
  dataArray: req.dataArray ? req.dataArray : null,
165
- /* eslint-disable no-undef */
166
181
  usage: BufferUsage.Texture
167
- /* eslint-enable no-undef */
168
182
  };
169
-
170
- buffRequest.nativeArray = fix[0];
183
+ buffRequest.nativeArray = ret[0];
171
184
  const buff = model.device.getBufferManager().getBuffer(buffRequest);
172
185
  cmdEnc.copyBufferToTexture({
173
186
  buffer: buff.getHandle(),
@@ -2,6 +2,7 @@ import { m as macro } from '../../macros2.js';
2
2
  import vtkDataArray from '../../Common/Core/DataArray.js';
3
3
  import vtkWebGPUTexture from './Texture.js';
4
4
 
5
+ /* eslint-disable no-bitwise */
5
6
  const {
6
7
  VtkDataTypes
7
8
  } = vtkDataArray;
@@ -69,6 +70,7 @@ function vtkWebGPUTextureManager(publicAPI, model) {
69
70
  req.height = req.image.height;
70
71
  req.depth = 1;
71
72
  req.format = 'rgba8unorm';
73
+ req.usage = GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING;
72
74
  }
73
75
 
74
76
  // fill in based on js imageData
@@ -79,6 +81,7 @@ function vtkWebGPUTextureManager(publicAPI, model) {
79
81
  req.format = 'rgba8unorm';
80
82
  req.flip = true;
81
83
  req.nativeArray = req.jsImageData.data;
84
+ req.usage = GPUTextureUsage.STORAGE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING;
82
85
  }
83
86
  if (req.canvas) {
84
87
  req.width = req.canvas.width;
@@ -116,7 +119,7 @@ function vtkWebGPUTextureManager(publicAPI, model) {
116
119
  // get a texture or create it if not cached.
117
120
  // this is the main entry point
118
121
  publicAPI.getTexture = req => {
119
- // if we have a source the get/create/cache the texture
122
+ // if we have a source then get/create/cache the texture
120
123
  if (req.hash) {
121
124
  // if a matching texture already exists then return it
122
125
  return model.device.getCachedObject(req.hash, _createTexture, req);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kitware/vtk.js",
3
- "version": "33.2.0",
3
+ "version": "33.2.1",
4
4
  "description": "Visualization Toolkit for the Web",
5
5
  "keywords": [
6
6
  "3d",