@kitware/vtk.js 33.1.1 → 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.
@@ -208,16 +208,15 @@ async function createPropertyFromGLTFMaterial(model, material, actor) {
208
208
  property.setDiffuseTexture(diffuseTex);
209
209
  }
210
210
  }
211
+
212
+ // Handle metallic-roughness texture (metallicRoughnessTexture)
211
213
  if (pbr.metallicRoughnessTexture) {
212
214
  pbr.metallicRoughnessTexture.extensions;
213
215
  const tex = pbr.metallicRoughnessTexture.texture;
214
216
  const sampler = tex.sampler;
215
- const metallicImage = await loadImage(tex.source, 'b');
216
- const metallicTex = createVTKTextureFromGLTFTexture(metallicImage, sampler);
217
- property.setMetallicTexture(metallicTex);
218
- const roughnessImage = await loadImage(tex.source, 'g');
219
- const roughnessTex = createVTKTextureFromGLTFTexture(roughnessImage, sampler);
220
- property.setRoughnessTexture(roughnessTex);
217
+ const rmImage = await loadImage(tex.source);
218
+ const rmTex = createVTKTextureFromGLTFTexture(rmImage, sampler);
219
+ property.setRMTexture(rmTex);
221
220
  }
222
221
 
223
222
  // Handle ambient occlusion texture (occlusionTexture)
@@ -1,5 +1,5 @@
1
1
  import { vtkObject } from './../../interfaces';
2
- import { RGBColor } from './../../types';
2
+ import { Nullable, RGBColor } from './../../types';
3
3
  import { Interpolation, Representation, Shading } from './Property/Constants';
4
4
  import { vtkTexture } from './Texture';
5
5
 
@@ -14,6 +14,8 @@ export interface IPropertyInitialValues {
14
14
  normalTexture?: vtkTexture;
15
15
  ambientOcclusionTexture?: vtkTexture;
16
16
  emissionTexture?: vtkTexture;
17
+ RMTexture?: vtkTexture;
18
+ ORMTexture?: vtkTexture;
17
19
  edgeColor?: RGBColor;
18
20
  ambient?: number;
19
21
  diffuse?: number;
@@ -216,32 +218,42 @@ export interface vtkProperty extends vtkObject {
216
218
  /**
217
219
  * Get the diffuse texture.
218
220
  */
219
- getDiffuseTexture(): vtkTexture;
221
+ getDiffuseTexture(): Nullable<vtkTexture>;
220
222
 
221
223
  /**
222
224
  * Get the metallic texture.
223
225
  */
224
- getMetallicTexture(): vtkTexture;
226
+ getMetallicTexture(): Nullable<vtkTexture>;
227
+
228
+ /**
229
+ * Get the roughness & metallic texture.
230
+ */
231
+ getRMTexture(): Nullable<vtkTexture>;
232
+
233
+ /**
234
+ * Get the occlusion, roughness & metallic texture.
235
+ */
236
+ getORMTexture(): Nullable<vtkTexture>;
225
237
 
226
238
  /**
227
239
  * Get the roughness texture.
228
240
  */
229
- getRoughnessTexture(): vtkTexture;
241
+ getRoughnessTexture(): Nullable<vtkTexture>;
230
242
 
231
243
  /**
232
244
  * Get the normal texture.
233
245
  */
234
- getNormalTexture(): vtkTexture;
246
+ getNormalTexture(): Nullable<vtkTexture>;
235
247
 
236
248
  /**
237
249
  * Get the ambient occlusion texture.
238
250
  */
239
- getAmbientOcclusionTexture(): vtkTexture;
251
+ getAmbientOcclusionTexture(): Nullable<vtkTexture>;
240
252
 
241
253
  /**
242
254
  * Get the emission texture.
243
255
  */
244
- getEmissionTexture(): vtkTexture;
256
+ getEmissionTexture(): Nullable<vtkTexture>;
245
257
 
246
258
  /**
247
259
  * Set the ambient lighting coefficient.
@@ -500,6 +512,18 @@ export interface vtkProperty extends vtkObject {
500
512
  */
501
513
  setRoughnessTexture(roughnessTexture: vtkTexture): boolean;
502
514
 
515
+ /**
516
+ * Set the roughness & metallic texture.
517
+ * @param {vtkTexture} RMTexture
518
+ */
519
+ setRMTexture(RMTexture: vtkTexture): boolean;
520
+
521
+ /**
522
+ * Set the occlusion, roughness & metallic texture.
523
+ * @param {vtkTexture} ORMTexture
524
+ */
525
+ setORMTexture(ORMTexture: vtkTexture): boolean;
526
+
503
527
  /**
504
528
  * Set the normal texture.
505
529
  * @param {vtkTexture} normalTexture
@@ -94,7 +94,9 @@ const DEFAULT_VALUES = {
94
94
  lineWidth: 1,
95
95
  lighting: true,
96
96
  shading: false,
97
- materialName: null
97
+ materialName: null,
98
+ ORMTexture: null,
99
+ RMTexture: null
98
100
  };
99
101
 
100
102
  // ----------------------------------------------------------------------------
@@ -105,7 +107,7 @@ function extend(publicAPI, model) {
105
107
 
106
108
  // Build VTK API
107
109
  macro.obj(publicAPI, model);
108
- macro.setGet(publicAPI, model, ['lighting', 'interpolation', 'ambient', 'diffuse', 'metallic', 'roughness', 'normalStrength', 'emission', 'baseIOR', 'specular', 'specularPower', 'opacity', 'edgeVisibility', 'lineWidth', 'pointSize', 'backfaceCulling', 'frontfaceCulling', 'representation', 'diffuseTexture', 'metallicTexture', 'roughnessTexture', 'normalTexture', 'ambientOcclusionTexture', 'emissionTexture']);
110
+ macro.setGet(publicAPI, model, ['lighting', 'interpolation', 'ambient', 'diffuse', 'metallic', 'roughness', 'normalStrength', 'emission', 'baseIOR', 'specular', 'specularPower', 'opacity', 'edgeVisibility', 'lineWidth', 'pointSize', 'backfaceCulling', 'frontfaceCulling', 'representation', 'diffuseTexture', 'metallicTexture', 'roughnessTexture', 'normalTexture', 'ambientOcclusionTexture', 'emissionTexture', 'ORMTexture', 'RMTexture']);
109
111
  macro.setGetArray(publicAPI, model, ['ambientColor', 'specularColor', 'diffuseColor', 'edgeColor'], 3);
110
112
 
111
113
  // Object methods
@@ -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
  // ----------------------------------------------------------------------------
@@ -42,13 +42,14 @@ function vtkWebGPUBuffer(publicAPI, model) {
42
42
  bufferSubData(model.device.getHandle(), model.handle, 0, data.buffer);
43
43
  };
44
44
  publicAPI.createAndWrite = (data, usage) => {
45
+ const paddedSize = Math.ceil(data.byteLength / 4) * 4;
45
46
  model.handle = model.device.getHandle().createBuffer({
46
- size: data.byteLength,
47
+ size: paddedSize,
47
48
  usage,
48
49
  mappedAtCreation: true,
49
50
  label: model.label
50
51
  });
51
- model.sizeInBytes = data.byteLength;
52
+ model.sizeInBytes = paddedSize;
52
53
  model.usage = usage;
53
54
  new Uint8Array(model.handle.getMappedRange()).set(new Uint8Array(data.buffer)); // memcpy
54
55
  model.handle.unmap();
@@ -595,44 +595,65 @@ function vtkWebGPUCellArrayMapper(publicAPI, model) {
595
595
  const fDesc = pipeline.getShaderDescription('fragment');
596
596
  code = fDesc.getCode();
597
597
  const actor = model.WebGPUActor.getRenderable();
598
+ const property = actor.getProperty();
598
599
  const checkDims = texture => {
599
600
  if (!texture) return false;
600
601
  const dims = texture.getDimensionality();
601
602
  return dims === numComp;
602
603
  };
603
604
  const usedTextures = [];
604
- if (actor.getProperty().getDiffuseTexture?.()?.getImageLoaded() || actor.getTextures()[0] || model.colorTexture) {
605
+ const diffuseTexture = property.getDiffuseTexture?.();
606
+ if (diffuseTexture?.getImageLoaded() || actor.getTextures()[0] || model.colorTexture) {
605
607
  if (
606
608
  // Chained or statements here are questionable
607
- checkDims(actor.getProperty().getDiffuseTexture?.()) || checkDims(actor.getTextures()[0]) || checkDims(model.colorTexture)) {
609
+ checkDims(diffuseTexture) || checkDims(actor.getTextures()[0]) || checkDims(model.colorTexture)) {
608
610
  usedTextures.push('_diffuseMap = textureSample(DiffuseTexture, DiffuseTextureSampler, input.tcoordVS);');
609
611
  }
610
612
  }
611
- if (actor.getProperty().getRoughnessTexture?.()?.getImageLoaded()) {
612
- if (checkDims(actor.getProperty().getRoughnessTexture())) {
613
- usedTextures.push('_roughnessMap = textureSample(RoughnessTexture, RoughnessTextureSampler, input.tcoordVS);');
613
+ const ormTexture = property.getORMTexture?.();
614
+ const rmTexture = property.getRMTexture?.();
615
+ const roughnessTexture = property.getRoughnessTexture?.();
616
+ const metallicTexture = property.getMetallicTexture?.();
617
+ const ambientOcclusionTexture = property.getAmbientOcclusionTexture?.();
618
+ const emissionTexture = property.getEmissionTexture?.();
619
+ const normalTexture = property.getNormalTexture?.();
620
+
621
+ // ORM texture support: if present, sample R/G/B for AO/Roughness/Metallic
622
+ if (ormTexture?.getImageLoaded()) {
623
+ if (checkDims(ormTexture)) {
624
+ usedTextures.push('_ambientOcclusionMap = textureSample(ORMTexture, ORMTextureSampler, input.tcoordVS).rrra;', '_roughnessMap = textureSample(ORMTexture, ORMTextureSampler, input.tcoordVS).ggga;', '_metallicMap = textureSample(ORMTexture, ORMTextureSampler, input.tcoordVS).bbba;');
614
625
  }
615
- }
616
- if (actor.getProperty().getMetallicTexture?.()?.getImageLoaded()) {
617
- if (checkDims(actor.getProperty().getMetallicTexture())) {
618
- usedTextures.push('_metallicMap = textureSample(MetallicTexture, MetallicTextureSampler, input.tcoordVS);');
626
+ } else if (rmTexture?.getImageLoaded()) {
627
+ if (checkDims(rmTexture)) {
628
+ usedTextures.push('_roughnessMap = textureSample(RMTexture, RMTextureSampler, input.tcoordVS).ggga;', '_metallicMap = textureSample(RMTexture, RMTextureSampler, input.tcoordVS).bbba;');
619
629
  }
620
- }
621
- if (actor.getProperty().getNormalTexture?.()?.getImageLoaded()) {
622
- if (checkDims(actor.getProperty().getNormalTexture())) {
623
- usedTextures.push('_normalMap = textureSample(NormalTexture, NormalTextureSampler, input.tcoordVS);');
630
+ } else {
631
+ if (roughnessTexture?.getImageLoaded()) {
632
+ if (checkDims(roughnessTexture)) {
633
+ usedTextures.push('_roughnessMap = textureSample(RoughnessTexture, RoughnessTextureSampler, input.tcoordVS);');
634
+ }
624
635
  }
625
- }
626
- if (actor.getProperty().getAmbientOcclusionTexture?.()?.getImageLoaded()) {
627
- if (checkDims(actor.getProperty().getAmbientOcclusionTexture())) {
628
- usedTextures.push('_ambientOcclusionMap = textureSample(AmbientOcclusionTexture, AmbientOcclusionTextureSampler, input.tcoordVS);');
636
+ if (metallicTexture?.getImageLoaded()) {
637
+ if (checkDims(metallicTexture)) {
638
+ usedTextures.push('_metallicMap = textureSample(MetallicTexture, MetallicTextureSampler, input.tcoordVS);');
639
+ }
640
+ }
641
+ if (ambientOcclusionTexture?.getImageLoaded()) {
642
+ if (checkDims(ambientOcclusionTexture)) {
643
+ usedTextures.push('_ambientOcclusionMap = textureSample(AmbientOcclusionTexture, AmbientOcclusionTextureSampler, input.tcoordVS);');
644
+ }
629
645
  }
630
646
  }
631
- if (actor.getProperty().getEmissionTexture?.()?.getImageLoaded()) {
632
- if (checkDims(actor.getProperty().getEmissionTexture())) {
647
+ if (emissionTexture?.getImageLoaded()) {
648
+ if (checkDims(emissionTexture)) {
633
649
  usedTextures.push('_emissionMap = textureSample(EmissionTexture, EmissionTextureSampler, input.tcoordVS);');
634
650
  }
635
651
  }
652
+ if (normalTexture?.getImageLoaded()) {
653
+ if (checkDims(normalTexture)) {
654
+ usedTextures.push('_normalMap = textureSample(NormalTexture, NormalTextureSampler, input.tcoordVS);');
655
+ }
656
+ }
636
657
  code = vtkWebGPUShaderCache.substitute(code, '//VTK::TCoord::Impl', usedTextures).result;
637
658
  fDesc.setCode(code);
638
659
  };
@@ -863,6 +884,14 @@ function vtkWebGPUCellArrayMapper(publicAPI, model) {
863
884
  const pair = ['Diffuse', model.colorTexture];
864
885
  textures.push(pair);
865
886
  }
887
+ if (actor.getProperty().getORMTexture?.()) {
888
+ const pair = ['ORM', actor.getProperty().getORMTexture()];
889
+ textures.push(pair);
890
+ }
891
+ if (actor.getProperty().getRMTexture?.()) {
892
+ const pair = ['RM', actor.getProperty().getRMTexture()];
893
+ textures.push(pair);
894
+ }
866
895
  if (actor.getProperty().getRoughnessTexture?.()) {
867
896
  const pair = ['Roughness', actor.getProperty().getRoughnessTexture()];
868
897
  textures.push(pair);
@@ -144,7 +144,7 @@ function vtkForwardPass(publicAPI, model) {
144
144
  dstFactor: 'one-minus-src-alpha'
145
145
  },
146
146
  alpha: {
147
- srcfactor: 'one',
147
+ srcFactor: 'one',
148
148
  dstFactor: 'one-minus-src-alpha'
149
149
  }
150
150
  }
@@ -156,7 +156,7 @@ function vtkWebGPUOrderIndependentTranslucentPass(publicAPI, model) {
156
156
  dstFactor: 'one'
157
157
  },
158
158
  alpha: {
159
- srcfactor: 'one',
159
+ srcFactor: 'one',
160
160
  dstFactor: 'one'
161
161
  }
162
162
  }
@@ -168,7 +168,7 @@ function vtkWebGPUOrderIndependentTranslucentPass(publicAPI, model) {
168
168
  dstFactor: 'one-minus-src'
169
169
  },
170
170
  alpha: {
171
- srcfactor: 'one',
171
+ srcFactor: 'one',
172
172
  dstFactor: 'one-minus-src-alpha'
173
173
  }
174
174
  }
@@ -209,7 +209,7 @@ function vtkWebGPUOrderIndependentTranslucentPass(publicAPI, model) {
209
209
  dstFactor: 'one-minus-src-alpha'
210
210
  },
211
211
  alpha: {
212
- srcfactor: 'one',
212
+ srcFactor: 'one',
213
213
  dstFactor: 'one-minus-src-alpha'
214
214
  }
215
215
  }
@@ -190,7 +190,7 @@ function extend(publicAPI, model) {
190
190
  dstFactor: 'one-minus-src-alpha'
191
191
  },
192
192
  alpha: {
193
- srcfactor: 'one',
193
+ srcFactor: 'one',
194
194
  dstFactor: 'one-minus-src-alpha'
195
195
  }
196
196
  }
@@ -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);
@@ -424,7 +424,7 @@ function vtkWebGPUVolumePass(publicAPI, model) {
424
424
  operation: 'max'
425
425
  },
426
426
  alpha: {
427
- srcfactor: 'one',
427
+ srcFactor: 'one',
428
428
  dstFactor: 'one',
429
429
  operation: 'max'
430
430
  }
@@ -438,7 +438,7 @@ function vtkWebGPUVolumePass(publicAPI, model) {
438
438
  operation: 'min'
439
439
  },
440
440
  alpha: {
441
- srcfactor: 'one',
441
+ srcFactor: 'one',
442
442
  dstFactor: 'one',
443
443
  operation: 'min'
444
444
  }
@@ -516,7 +516,7 @@ function vtkWebGPUVolumePass(publicAPI, model) {
516
516
  dstFactor: 'one-minus-src-alpha'
517
517
  },
518
518
  alpha: {
519
- srcfactor: 'one',
519
+ srcFactor: 'one',
520
520
  dstFactor: 'one-minus-src-alpha'
521
521
  }
522
522
  }
@@ -549,7 +549,7 @@ function vtkWebGPUVolumePass(publicAPI, model) {
549
549
  dstFactor: 'one-minus-src-alpha'
550
550
  },
551
551
  alpha: {
552
- srcfactor: 'one',
552
+ srcFactor: 'one',
553
553
  dstFactor: 'one-minus-src-alpha'
554
554
  }
555
555
  }
@@ -590,7 +590,7 @@ function vtkWebGPUVolumePass(publicAPI, model) {
590
590
  dstFactor: 'one-minus-src-alpha'
591
591
  },
592
592
  alpha: {
593
- srcfactor: 'one',
593
+ srcFactor: 'one',
594
594
  dstFactor: 'one-minus-src-alpha'
595
595
  }
596
596
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kitware/vtk.js",
3
- "version": "33.1.1",
3
+ "version": "33.2.1",
4
4
  "description": "Visualization Toolkit for the Web",
5
5
  "keywords": [
6
6
  "3d",