Skip to content

Commit cb82ace

Browse files
committed
feat(WebGPU): add MSAA (multi-sample anti-aliasing) support
Add configurable sampleCount to the WebGPU render pipeline: - RenderWindow: expose setSampleCount()/getSampleCount() (default: 1) - Texture: support sampleCount in create(), resize(), resizeToMatch() - RenderEncoder: add resolveTextureViews for MSAA resolve targets - OpaquePass: create multisampled color/depth textures when sampleCount > 1, with a resolved (1-sample) color texture for downstream sampling - RenderWindow.getPixelsAsync: use resolved texture for readback when MSAA active Usage: const gpuRenderWindow = vtkWebGPURenderWindow.newInstance(); gpuRenderWindow.setSampleCount(4); // Enable 4x MSAA
1 parent 948a897 commit cb82ace

File tree

6 files changed

+248
-38
lines changed

6 files changed

+248
-38
lines changed

Sources/Rendering/WebGPU/OpaquePass/index.js

Lines changed: 84 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,50 +22,94 @@ function vtkWebGPUOpaquePass(publicAPI, model) {
2222
model._currentParent = viewNode;
2323

2424
const device = viewNode.getDevice();
25+
const sampleCount = viewNode.getSampleCount ? viewNode.getSampleCount() : 1;
26+
27+
// If sampleCount changed since last render, tear down and recreate
28+
if (model.renderEncoder && model._currentSampleCount !== sampleCount) {
29+
model.renderEncoder = null;
30+
model.colorTexture = null;
31+
model.depthTexture = null;
32+
model.resolveColorTexture = null;
33+
model._resolveColorTextureView = null;
34+
}
2535

2636
if (!model.renderEncoder) {
27-
publicAPI.createRenderEncoder();
37+
publicAPI.createRenderEncoder(sampleCount);
38+
model._currentSampleCount = sampleCount;
39+
40+
const width = viewNode.getCanvas().width;
41+
const height = viewNode.getCanvas().height;
42+
43+
// Color texture — multisampled when sampleCount > 1
2844
model.colorTexture = vtkWebGPUTexture.newInstance({
2945
label: 'opaquePassColor',
3046
});
47+
/* eslint-disable no-undef */
48+
/* eslint-disable no-bitwise */
3149
model.colorTexture.create(device, {
32-
width: viewNode.getCanvas().width,
33-
height: viewNode.getCanvas().height,
50+
width,
51+
height,
3452
format: 'rgba16float',
35-
/* eslint-disable no-undef */
36-
/* eslint-disable no-bitwise */
53+
sampleCount,
3754
usage:
3855
GPUTextureUsage.RENDER_ATTACHMENT |
39-
GPUTextureUsage.TEXTURE_BINDING |
40-
GPUTextureUsage.COPY_SRC,
56+
(sampleCount === 1
57+
? GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC
58+
: 0),
4159
});
4260
const ctView = model.colorTexture.createView('opaquePassColorTexture');
4361
model.renderEncoder.setColorTextureView(0, ctView);
4462

63+
// When MSAA is active, create a resolve target (1-sample) for
64+
// downstream passes that need to sample the color result
65+
if (sampleCount > 1) {
66+
model.resolveColorTexture = vtkWebGPUTexture.newInstance({
67+
label: 'opaquePassResolveColor',
68+
});
69+
model.resolveColorTexture.create(device, {
70+
width,
71+
height,
72+
format: 'rgba16float',
73+
usage:
74+
GPUTextureUsage.RENDER_ATTACHMENT |
75+
GPUTextureUsage.TEXTURE_BINDING |
76+
GPUTextureUsage.COPY_SRC,
77+
});
78+
model._resolveColorTextureView = model.resolveColorTexture.createView(
79+
'opaquePassColorTexture'
80+
);
81+
const resolveView = model._resolveColorTextureView;
82+
model.renderEncoder.setResolveTextureView(0, resolveView);
83+
}
84+
85+
// Depth texture — also multisampled
4586
model.depthFormat = 'depth32float';
4687
model.depthTexture = vtkWebGPUTexture.newInstance({
4788
label: 'opaquePassDepth',
4889
});
4990
model.depthTexture.create(device, {
50-
width: viewNode.getCanvas().width,
51-
height: viewNode.getCanvas().height,
91+
width,
92+
height,
5293
format: model.depthFormat,
94+
sampleCount,
5395
usage:
5496
GPUTextureUsage.RENDER_ATTACHMENT |
55-
GPUTextureUsage.TEXTURE_BINDING |
56-
GPUTextureUsage.COPY_SRC,
97+
(sampleCount === 1
98+
? GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC
99+
: 0),
57100
});
101+
/* eslint-enable no-undef */
102+
/* eslint-enable no-bitwise */
58103
const dView = model.depthTexture.createView('opaquePassDepthTexture');
59104
model.renderEncoder.setDepthTextureView(dView);
60105
} else {
61-
model.colorTexture.resize(
62-
viewNode.getCanvas().width,
63-
viewNode.getCanvas().height
64-
);
65-
model.depthTexture.resize(
66-
viewNode.getCanvas().width,
67-
viewNode.getCanvas().height
68-
);
106+
const width = viewNode.getCanvas().width;
107+
const height = viewNode.getCanvas().height;
108+
model.colorTexture.resize(width, height);
109+
model.depthTexture.resize(width, height);
110+
if (model.resolveColorTexture) {
111+
model.resolveColorTexture.resize(width, height);
112+
}
69113
}
70114

71115
model.renderEncoder.attachTextureViews();
@@ -74,18 +118,30 @@ function vtkWebGPUOpaquePass(publicAPI, model) {
74118
renNode.traverse(publicAPI);
75119
};
76120

77-
publicAPI.getColorTextureView = () =>
78-
model.renderEncoder.getColorTextureViews()[0];
121+
// When MSAA is active, downstream passes must sample from the resolved
122+
// (1-sample) texture, not the multisampled one
123+
publicAPI.getColorTextureView = () => {
124+
if (model._resolveColorTextureView) {
125+
return model._resolveColorTextureView;
126+
}
127+
return model.renderEncoder.getColorTextureViews()[0];
128+
};
79129

80130
publicAPI.getDepthTextureView = () =>
81131
model.renderEncoder.getDepthTextureView();
82132

83-
publicAPI.createRenderEncoder = () => {
133+
publicAPI.createRenderEncoder = (sampleCount = 1) => {
84134
model.renderEncoder = vtkWebGPURenderEncoder.newInstance({
85135
label: 'OpaquePass',
86136
});
87137
// default settings are fine for this
88138
model.renderEncoder.setPipelineHash('op');
139+
// Set multisample state in pipeline settings when MSAA is active
140+
if (sampleCount > 1) {
141+
const settings = model.renderEncoder.getPipelineSettings();
142+
settings.multisample = { count: sampleCount };
143+
model.renderEncoder.setPipelineSettings(settings);
144+
}
89145
};
90146
}
91147

@@ -97,6 +153,7 @@ const DEFAULT_VALUES = {
97153
renderEncoder: null,
98154
colorTexture: null,
99155
depthTexture: null,
156+
resolveColorTexture: null,
100157
};
101158

102159
// ----------------------------------------------------------------------------
@@ -107,7 +164,11 @@ export function extend(publicAPI, model, initialValues = {}) {
107164
// Build VTK API
108165
vtkRenderPass.extend(publicAPI, model, initialValues);
109166

110-
macro.get(publicAPI, model, ['colorTexture', 'depthTexture']);
167+
macro.get(publicAPI, model, [
168+
'colorTexture',
169+
'depthTexture',
170+
'resolveColorTexture',
171+
]);
111172

112173
// Object methods
113174
vtkWebGPUOpaquePass(publicAPI, model);

Sources/Rendering/WebGPU/OrderIndependentTranslucentPass/index.js

Lines changed: 94 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,15 @@ fn main(
3636
}
3737
`;
3838

39+
// ----------------------------------------------------------------------------
40+
3941
function vtkWebGPUOrderIndependentTranslucentPass(publicAPI, model) {
4042
// Set our className
4143
model.classHierarchy.push('vtkWebGPUOrderIndependentTranslucentPass');
4244

43-
// this pass implements a forward rendering pipeline
44-
// if both volumes and opaque geometry are present
45-
// it will mix the two together by capturing a zbuffer
46-
// first
45+
// This pass implements a forward rendering pipeline for translucent geometry.
46+
// It uses order-independent transparency (OIT) with weighted blended
47+
// compositing, reading the opaque depth buffer for depth testing.
4748
publicAPI.traverse = (renNode, viewNode) => {
4849
if (model.deleted) {
4950
return;
@@ -53,46 +54,112 @@ function vtkWebGPUOrderIndependentTranslucentPass(publicAPI, model) {
5354
model._currentParent = viewNode;
5455

5556
const device = viewNode.getDevice();
57+
const sampleCount = viewNode.getSampleCount ? viewNode.getSampleCount() : 1;
58+
59+
// If sampleCount changed, tear down
60+
if (
61+
model.translucentRenderEncoder &&
62+
model._currentSampleCount !== sampleCount
63+
) {
64+
model.translucentRenderEncoder = null;
65+
model.translucentColorTexture = null;
66+
model.translucentAccumulateTexture = null;
67+
model.translucentResolveColorTexture = null;
68+
model.translucentResolveAccumulateTexture = null;
69+
model._resolveColorView = null;
70+
model._resolveAccumulateView = null;
71+
}
5672

5773
if (!model.translucentRenderEncoder) {
58-
publicAPI.createRenderEncoder();
74+
publicAPI.createRenderEncoder(sampleCount);
5975
publicAPI.createFinalEncoder();
76+
model._currentSampleCount = sampleCount;
6077
model.translucentColorTexture = vtkWebGPUTexture.newInstance({
6178
label: 'translucentPassColor',
6279
});
6380
model.translucentColorTexture.create(device, {
6481
width: viewNode.getCanvas().width,
6582
height: viewNode.getCanvas().height,
6683
format: 'rgba16float',
84+
sampleCount,
6785
/* eslint-disable no-undef */
6886
/* eslint-disable no-bitwise */
6987
usage:
70-
GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
88+
GPUTextureUsage.RENDER_ATTACHMENT |
89+
(sampleCount === 1 ? GPUTextureUsage.TEXTURE_BINDING : 0),
7190
});
7291
const v1 = model.translucentColorTexture.createView('oitpColorTexture');
7392
model.translucentRenderEncoder.setColorTextureView(0, v1);
7493

94+
// Resolve color texture for MSAA
95+
if (sampleCount > 1) {
96+
model.translucentResolveColorTexture = vtkWebGPUTexture.newInstance({
97+
label: 'translucentPassResolveColor',
98+
});
99+
model.translucentResolveColorTexture.create(device, {
100+
width: viewNode.getCanvas().width,
101+
height: viewNode.getCanvas().height,
102+
format: 'rgba16float',
103+
usage:
104+
GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
105+
});
106+
model._resolveColorView =
107+
model.translucentResolveColorTexture.createView('oitpColorTexture');
108+
model.translucentRenderEncoder.setResolveTextureView(
109+
0,
110+
model._resolveColorView
111+
);
112+
}
113+
75114
model.translucentAccumulateTexture = vtkWebGPUTexture.newInstance({
76115
label: 'translucentPassAccumulate',
77116
});
78117
model.translucentAccumulateTexture.create(device, {
79118
width: viewNode.getCanvas().width,
80119
height: viewNode.getCanvas().height,
81120
format: 'r16float',
121+
sampleCount,
82122
/* eslint-disable no-undef */
83123
/* eslint-disable no-bitwise */
84124
usage:
85-
GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
125+
GPUTextureUsage.RENDER_ATTACHMENT |
126+
(sampleCount === 1 ? GPUTextureUsage.TEXTURE_BINDING : 0),
86127
});
87128
const v2 =
88129
model.translucentAccumulateTexture.createView('oitpAccumTexture');
89130
model.translucentRenderEncoder.setColorTextureView(1, v2);
131+
132+
// Resolve accumulate texture for MSAA
133+
if (sampleCount > 1) {
134+
model.translucentResolveAccumulateTexture =
135+
vtkWebGPUTexture.newInstance({
136+
label: 'translucentPassResolveAccumulate',
137+
});
138+
model.translucentResolveAccumulateTexture.create(device, {
139+
width: viewNode.getCanvas().width,
140+
height: viewNode.getCanvas().height,
141+
format: 'r16float',
142+
usage:
143+
GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
144+
});
145+
model._resolveAccumulateView =
146+
model.translucentResolveAccumulateTexture.createView(
147+
'oitpAccumTexture'
148+
);
149+
model.translucentRenderEncoder.setResolveTextureView(
150+
1,
151+
model._resolveAccumulateView
152+
);
153+
}
90154
model.fullScreenQuad = vtkWebGPUFullScreenQuad.newInstance();
91155
model.fullScreenQuad.setDevice(viewNode.getDevice());
92156
model.fullScreenQuad.setPipelineHash('oitpfsq');
93-
model.fullScreenQuad.setTextureViews(
94-
model.translucentRenderEncoder.getColorTextureViews()
95-
);
157+
// Use resolved textures for the full screen quad if MSAA is on
158+
const views =
159+
sampleCount > 1
160+
? [model._resolveColorView, model._resolveAccumulateView]
161+
: model.translucentRenderEncoder.getColorTextureViews();
162+
model.fullScreenQuad.setTextureViews(views);
96163
model.fullScreenQuad.setFragmentShaderTemplate(oitpFragTemplate);
97164
} else {
98165
model.translucentColorTexture.resizeToMatch(
@@ -101,6 +168,14 @@ function vtkWebGPUOrderIndependentTranslucentPass(publicAPI, model) {
101168
model.translucentAccumulateTexture.resizeToMatch(
102169
model.colorTextureView.getTexture()
103170
);
171+
if (model.translucentResolveColorTexture) {
172+
model.translucentResolveColorTexture.resizeToMatch(
173+
model.colorTextureView.getTexture()
174+
);
175+
model.translucentResolveAccumulateTexture.resizeToMatch(
176+
model.colorTextureView.getTexture()
177+
);
178+
}
104179
}
105180

106181
model.translucentRenderEncoder.setDepthTextureView(model.depthTextureView);
@@ -129,10 +204,16 @@ function vtkWebGPUOrderIndependentTranslucentPass(publicAPI, model) {
129204
model.translucentAccumulateTexture,
130205
];
131206

132-
publicAPI.createRenderEncoder = () => {
207+
publicAPI.createRenderEncoder = (sampleCount = 1) => {
133208
model.translucentRenderEncoder = vtkWebGPURenderEncoder.newInstance({
134209
label: 'translucentRender',
135210
});
211+
// Set multisample state if needed
212+
if (sampleCount > 1) {
213+
const settings = model.translucentRenderEncoder.getPipelineSettings();
214+
settings.multisample = { count: sampleCount };
215+
model.translucentRenderEncoder.setPipelineSettings(settings);
216+
}
136217
const rDesc = model.translucentRenderEncoder.getDescription();
137218
rDesc.colorAttachments = [
138219
{
@@ -261,6 +342,8 @@ function vtkWebGPUOrderIndependentTranslucentPass(publicAPI, model) {
261342
const DEFAULT_VALUES = {
262343
colorTextureView: null,
263344
depthTextureView: null,
345+
translucentResolveColorTexture: null,
346+
translucentResolveAccumulateTexture: null,
264347
};
265348

266349
// ----------------------------------------------------------------------------

Sources/Rendering/WebGPU/RenderEncoder/index.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,13 @@ function vtkWebGPURenderEncoder(publicAPI, model) {
9696
model.colorTextureViews[idx] = view;
9797
};
9898

99+
publicAPI.setResolveTextureView = (idx, view) => {
100+
if (model.resolveTextureViews[idx] === view) {
101+
return;
102+
}
103+
model.resolveTextureViews[idx] = view;
104+
};
105+
99106
publicAPI.activateBindGroup = (bg) => {
100107
const device = model.boundPipeline.getDevice();
101108
const midx = model.boundPipeline.getBindGroupLayoutCount(bg.getLabel());
@@ -126,6 +133,14 @@ function vtkWebGPURenderEncoder(publicAPI, model) {
126133
model.description.colorAttachments[i].view =
127134
model.colorTextureViews[i].getHandle();
128135
}
136+
// MSAA: set resolveTarget if a resolve texture view is provided
137+
if (model.resolveTextureViews[i]) {
138+
model.description.colorAttachments[i].resolveTarget =
139+
model.resolveTextureViews[i].getHandle();
140+
// When using MSAA, the multisampled texture is transient;
141+
// store only into the resolve target
142+
model.description.colorAttachments[i].storeOp = 'discard';
143+
}
129144
}
130145
if (model.depthTextureView) {
131146
model.description.depthStencilAttachment.view =
@@ -225,6 +240,7 @@ export function extend(publicAPI, model, initialValues = {}) {
225240
};
226241

227242
model.colorTextureViews = [];
243+
model.resolveTextureViews = [];
228244

229245
macro.get(publicAPI, model, ['boundPipeline', 'colorTextureViews']);
230246

0 commit comments

Comments
 (0)