@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
|
-
*
|
|
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
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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 [
|
|
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
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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
|
|
162
|
-
bufferBytesPerRow =
|
|
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
|
|
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);
|