|
| 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