Skip to content

Commit 24718b5

Browse files
committed
feat(WebGPU): add vtkSkybox
1 parent 88f7f94 commit 24718b5

5 files changed

Lines changed: 394 additions & 18 deletions

File tree

Sources/Rendering/WebGPU/Profiles/All.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import 'vtk.js/Sources/Rendering/WebGPU/Actor2D';
88
import 'vtk.js/Sources/Rendering/WebGPU/CubeAxesActor';
99
import 'vtk.js/Sources/Rendering/WebGPU/PolyDataMapper';
1010
import 'vtk.js/Sources/Rendering/WebGPU/PolyDataMapper2D';
11-
// import 'vtk.js/Sources/Rendering/WebGPU/Skybox';
11+
import 'vtk.js/Sources/Rendering/WebGPU/Skybox';
1212
import 'vtk.js/Sources/Rendering/WebGPU/ScalarBarActor';
1313
import 'vtk.js/Sources/Rendering/WebGPU/Texture';
1414

Sources/Rendering/WebGPU/Profiles/Geometry.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import 'vtk.js/Sources/Rendering/WebGPU/Actor2D';
88
import 'vtk.js/Sources/Rendering/WebGPU/CubeAxesActor';
99
import 'vtk.js/Sources/Rendering/WebGPU/PolyDataMapper';
1010
import 'vtk.js/Sources/Rendering/WebGPU/PolyDataMapper2D';
11-
// import 'vtk.js/Sources/Rendering/WebGPU/Skybox';
11+
import 'vtk.js/Sources/Rendering/WebGPU/Skybox';
1212
import 'vtk.js/Sources/Rendering/WebGPU/ScalarBarActor';
1313
import 'vtk.js/Sources/Rendering/WebGPU/Texture';
1414

Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
import { mat4 } from 'gl-matrix';
2+
3+
import macro from 'vtk.js/Sources/macros';
4+
import vtkDataArray from 'vtk.js/Sources/Common/Core/DataArray';
5+
import vtkViewNode from 'vtk.js/Sources/Rendering/SceneGraph/ViewNode';
6+
import vtkWebGPUFullScreenQuad from 'vtk.js/Sources/Rendering/WebGPU/FullScreenQuad';
7+
import vtkWebGPUShaderCache from 'vtk.js/Sources/Rendering/WebGPU/ShaderCache';
8+
import vtkWebGPUTexture from 'vtk.js/Sources/Rendering/WebGPU/Texture';
9+
import vtkWebGPUUniformBuffer from 'vtk.js/Sources/Rendering/WebGPU/UniformBuffer';
10+
11+
import { registerOverride } from 'vtk.js/Sources/Rendering/WebGPU/ViewNodeFactory';
12+
13+
const { VtkDataTypes } = vtkDataArray;
14+
const { vtkErrorMacro } = macro;
15+
16+
const backgroundFragTemplate = `
17+
//VTK::Renderer::Dec
18+
19+
//VTK::Mapper::Dec
20+
21+
//VTK::TCoord::Dec
22+
23+
//VTK::RenderEncoder::Dec
24+
25+
//VTK::IOStructs::Dec
26+
27+
@fragment
28+
fn main(
29+
//VTK::IOStructs::Input
30+
)
31+
//VTK::IOStructs::Output
32+
{
33+
var output: fragmentOutput;
34+
35+
var computedColor: vec4<f32> = textureSampleLevel(sbtexture, sbtextureSampler, input.tcoordVS, 0.0);
36+
37+
//VTK::RenderEncoder::Impl
38+
return output;
39+
}
40+
`;
41+
42+
const boxFragTemplate = `
43+
fn remapCubeCoord(dir: vec3<f32>) -> vec3<f32> {
44+
var tc = normalize(dir);
45+
if (abs(tc.z) < max(abs(tc.x), abs(tc.y))) {
46+
tc = vec3<f32>(1.0, 1.0, -1.0) * tc;
47+
} else {
48+
tc = vec3<f32>(-1.0, 1.0, 1.0) * tc;
49+
}
50+
return tc;
51+
}
52+
53+
//VTK::Renderer::Dec
54+
55+
//VTK::Mapper::Dec
56+
57+
//VTK::TCoord::Dec
58+
59+
//VTK::RenderEncoder::Dec
60+
61+
//VTK::IOStructs::Dec
62+
63+
@fragment
64+
fn main(
65+
//VTK::IOStructs::Input
66+
)
67+
//VTK::IOStructs::Output
68+
{
69+
var output: fragmentOutput;
70+
71+
let clipPos = vec4<f32>(input.vertexVC.xy, 1.0, 1.0);
72+
let worldPos = mapperUBO.IMCPCMatrix * clipPos;
73+
let direction = remapCubeCoord(worldPos.xyz / worldPos.w - mapperUBO.CameraPosition.xyz);
74+
var computedColor: vec4<f32> = textureSampleLevel(sbtexture, sbtextureSampler, direction, 0.0);
75+
76+
//VTK::RenderEncoder::Impl
77+
return output;
78+
}
79+
`;
80+
81+
const _imcpc = new Float64Array(16);
82+
83+
function getTextureFormat(dataArray) {
84+
const numComp = dataArray.getNumberOfComponents();
85+
let format = 'rgba';
86+
87+
if (numComp === 1) {
88+
format = 'r';
89+
} else if (numComp === 2) {
90+
format = 'rg';
91+
}
92+
93+
switch (dataArray.getDataType()) {
94+
case VtkDataTypes.UNSIGNED_CHAR:
95+
return `${format}8unorm`;
96+
case VtkDataTypes.FLOAT:
97+
case VtkDataTypes.UNSIGNED_INT:
98+
case VtkDataTypes.INT:
99+
case VtkDataTypes.DOUBLE:
100+
case VtkDataTypes.UNSIGNED_SHORT:
101+
case VtkDataTypes.SHORT:
102+
default:
103+
return `${format}16float`;
104+
}
105+
}
106+
107+
function vtkWebGPUSkybox(publicAPI, model) {
108+
model.classHierarchy.push('vtkWebGPUSkybox');
109+
110+
publicAPI.buildPass = (prepass) => {
111+
if (prepass) {
112+
model.WebGPURenderer =
113+
publicAPI.getFirstAncestorOfType('vtkWebGPURenderer');
114+
model.WebGPURenderWindow = model.WebGPURenderer?.getFirstAncestorOfType(
115+
'vtkWebGPURenderWindow'
116+
);
117+
118+
if (model.WebGPURenderer) {
119+
const renderer = model.WebGPURenderer.getRenderable();
120+
model.webgpuCamera = model.WebGPURenderer.getViewNodeFor(
121+
renderer.getActiveCamera(),
122+
model.webgpuCamera
123+
);
124+
}
125+
}
126+
};
127+
128+
publicAPI.queryPass = (prepass, renderPass) => {
129+
if (prepass) {
130+
if (!model.renderable || !model.renderable.getVisibility()) {
131+
return;
132+
}
133+
renderPass.incrementOpaqueActorCount();
134+
}
135+
};
136+
137+
publicAPI.ensureQuad = (device) => {
138+
if (model.quad) {
139+
return;
140+
}
141+
142+
model.quad = vtkWebGPUFullScreenQuad.newInstance();
143+
model.quad.setDevice(device);
144+
model.quad.setWebGPURenderer(model.WebGPURenderer);
145+
146+
model.UBO = vtkWebGPUUniformBuffer.newInstance({ label: 'mapperUBO' });
147+
model.UBO.addEntry('IMCPCMatrix', 'mat4x4<f32>');
148+
model.UBO.addEntry('CameraPosition', 'vec4<f32>');
149+
model.quad.setUBO(model.UBO);
150+
151+
const replaceSkyboxShaderPosition = (hash, pipeline) => {
152+
const vDesc = pipeline.getShaderDescription('vertex');
153+
vDesc.addBuiltinOutput('vec4<f32>', '@builtin(position) Position');
154+
if (!vDesc.hasOutput('vertexVC')) {
155+
vDesc.addOutput('vec4<f32>', 'vertexVC');
156+
}
157+
if (!vDesc.hasOutput('tcoordVS')) {
158+
vDesc.addOutput('vec2<f32>', 'tcoordVS');
159+
}
160+
161+
let code = vDesc.getCode();
162+
code = vtkWebGPUShaderCache.substitute(code, '//VTK::Position::Impl', [
163+
'output.tcoordVS = vec2<f32>(vertexBC.x * 0.5 + 0.5, 1.0 - vertexBC.y * 0.5 - 0.5);',
164+
'output.Position = vec4<f32>(vertexBC.x, vertexBC.y, 0.0, 1.0);',
165+
'output.vertexVC = vec4<f32>(vertexBC, 1.0);',
166+
]).result;
167+
vDesc.setCode(code);
168+
};
169+
model.quad
170+
.getShaderReplacements()
171+
.set('replaceShaderPosition', replaceSkyboxShaderPosition);
172+
};
173+
174+
publicAPI.getTexture = () => model.renderable.getTextures?.()?.[0] ?? null;
175+
176+
publicAPI.getCubeTextureHash = (texture) => {
177+
let hash = `${texture.getMTime()}-skybox-cube`;
178+
for (let i = 0; i < 6; i++) {
179+
const imageData = texture.getInputData(i);
180+
if (!imageData) {
181+
return null;
182+
}
183+
184+
hash += `-${imageData.getMTime()}`;
185+
const scalars = imageData.getPointData().getScalars();
186+
if (scalars) {
187+
hash += `-${scalars.getMTime()}`;
188+
}
189+
}
190+
return hash;
191+
};
192+
193+
publicAPI.getCubeTexture = (device, texture) => {
194+
const hash = publicAPI.getCubeTextureHash(texture);
195+
if (!hash) {
196+
return null;
197+
}
198+
199+
return device.getCachedObject(hash, () => {
200+
const firstImage = texture.getInputData(0);
201+
const firstScalars = firstImage.getPointData().getScalars();
202+
const dims = firstImage.getDimensions();
203+
const webgpuTexture = vtkWebGPUTexture.newInstance({
204+
label: 'sbtexture',
205+
});
206+
webgpuTexture.create(device, {
207+
width: dims[0],
208+
height: dims[1],
209+
depth: 6,
210+
dimension: '2d',
211+
format: getTextureFormat(firstScalars),
212+
});
213+
214+
for (let i = 0; i < 6; i++) {
215+
const imageData = texture.getInputData(i);
216+
const faceDims = imageData.getDimensions();
217+
const scalars = imageData.getPointData().getScalars();
218+
webgpuTexture.writeImageData({
219+
nativeArray: scalars.getData(),
220+
width: faceDims[0],
221+
height: faceDims[1],
222+
depth: 1,
223+
originZ: i,
224+
});
225+
}
226+
227+
return webgpuTexture;
228+
});
229+
};
230+
231+
publicAPI.updateTexture = (device) => {
232+
const texture = publicAPI.getTexture();
233+
if (!texture) {
234+
model.quad.setTextureViews([]);
235+
return false;
236+
}
237+
238+
if (model.renderable.getFormat() === 'background') {
239+
const webgpuTexture = device
240+
.getTextureManager()
241+
.getTextureForVTKTexture(texture, 'sbtexture');
242+
if (!webgpuTexture.getReady()) {
243+
model.quad.setTextureViews([]);
244+
return false;
245+
}
246+
247+
const tview = webgpuTexture.createView('sbtexture');
248+
const interpolate = texture.getInterpolate?.() ? 'linear' : 'nearest';
249+
tview.addSampler(device, {
250+
minFilter: interpolate,
251+
magFilter: interpolate,
252+
});
253+
model.quad.setTextureViews([tview]);
254+
return true;
255+
}
256+
257+
if (model.renderable.getFormat() === 'box') {
258+
const webgpuTexture = publicAPI.getCubeTexture(device, texture);
259+
if (!webgpuTexture?.getReady()) {
260+
model.quad.setTextureViews([]);
261+
return false;
262+
}
263+
264+
const tview = webgpuTexture.createView('sbtexture', {
265+
dimension: 'cube',
266+
});
267+
const interpolate = texture.getInterpolate?.() ? 'linear' : 'nearest';
268+
tview.addSampler(device, {
269+
addressModeU: 'clamp-to-edge',
270+
addressModeV: 'clamp-to-edge',
271+
addressModeW: 'clamp-to-edge',
272+
minFilter: interpolate,
273+
magFilter: interpolate,
274+
});
275+
model.quad.setTextureViews([tview]);
276+
return true;
277+
}
278+
279+
vtkErrorMacro(
280+
`Unsupported vtkSkybox format ${model.renderable.getFormat()}`
281+
);
282+
model.quad.setTextureViews([]);
283+
return false;
284+
};
285+
286+
publicAPI.updateUBO = (device) => {
287+
const camera = model.WebGPURenderer.getRenderable().getActiveCamera();
288+
const keyMats = model.webgpuCamera.getKeyMatrices(model.WebGPURenderer);
289+
const stabilizedCenter =
290+
model.WebGPURenderer.getStabilizedCenterByReference();
291+
mat4.copy(_imcpc, keyMats.pcsc);
292+
model.UBO.setArray('IMCPCMatrix', _imcpc);
293+
model.UBO.setArray('CameraPosition', [
294+
camera.getPositionByReference()[0] - stabilizedCenter[0],
295+
camera.getPositionByReference()[1] - stabilizedCenter[1],
296+
camera.getPositionByReference()[2] - stabilizedCenter[2],
297+
1.0,
298+
]);
299+
model.UBO.sendIfNeeded(device);
300+
};
301+
302+
publicAPI.opaquePass = (prepass) => {
303+
if (
304+
!prepass ||
305+
!model.renderable ||
306+
!model.renderable.getVisibility() ||
307+
model.WebGPURenderer?.getSelector()
308+
) {
309+
return;
310+
}
311+
312+
const device = model.WebGPURenderWindow.getDevice();
313+
publicAPI.ensureQuad(device);
314+
model.quad.setWebGPURenderer(model.WebGPURenderer);
315+
316+
const format = model.renderable.getFormat();
317+
model.quad.setPipelineHash(`skybox-${format}`);
318+
model.quad.setFragmentShaderTemplate(
319+
format === 'background' ? backgroundFragTemplate : boxFragTemplate
320+
);
321+
322+
if (!publicAPI.updateTexture(device)) {
323+
return;
324+
}
325+
326+
publicAPI.updateUBO(device);
327+
model.quad.prepareToDraw(model.WebGPURenderer.getRenderEncoder());
328+
model.quad.registerDrawCallback(model.WebGPURenderer.getRenderEncoder());
329+
};
330+
}
331+
332+
const DEFAULT_VALUES = {
333+
quad: null,
334+
UBO: null,
335+
webgpuCamera: null,
336+
WebGPURenderer: null,
337+
WebGPURenderWindow: null,
338+
};
339+
340+
export function extend(publicAPI, model, initialValues = {}) {
341+
Object.assign(model, DEFAULT_VALUES, initialValues);
342+
343+
vtkViewNode.extend(publicAPI, model, initialValues);
344+
345+
vtkWebGPUSkybox(publicAPI, model);
346+
}
347+
348+
export const newInstance = macro.newInstance(extend, 'vtkWebGPUSkybox');
349+
350+
export default { newInstance, extend };
351+
352+
registerOverride('vtkSkybox', newInstance);

0 commit comments

Comments
 (0)