|
| 1 | +import 'vtk.js/Sources/favicon'; |
| 2 | + |
| 3 | +import 'vtk.js/Sources/Rendering/Profiles/Volume'; |
| 4 | +import 'vtk.js/Sources/Rendering/Profiles/Geometry'; |
| 5 | +import macro from 'vtk.js/Sources/macros'; |
| 6 | +import vtkColorTransferFunction from 'vtk.js/Sources/Rendering/Core/ColorTransferFunction'; |
| 7 | +import vtkFullScreenRenderWindow from 'vtk.js/Sources/Rendering/Misc/FullScreenRenderWindow'; |
| 8 | +import vtkPiecewiseFunction from 'vtk.js/Sources/Common/DataModel/PiecewiseFunction'; |
| 9 | +import vtkVolume from 'vtk.js/Sources/Rendering/Core/Volume'; |
| 10 | +import vtkVolumeMapper from 'vtk.js/Sources/Rendering/Core/VolumeMapper'; |
| 11 | +import vtkLight from 'vtk.js/Sources/Rendering/Core/Light'; |
| 12 | +import vtkXMLImageDataReader from 'vtk.js/Sources/IO/XML/XMLImageDataReader'; |
| 13 | +import HttpDataAccessHelper from 'vtk.js/Sources/IO/Core/DataAccessHelper/HttpDataAccessHelper'; |
| 14 | +import vtkVolumeController from 'vtk.js/Sources/Interaction/UI/VolumeController'; |
| 15 | +import vtkBoundingBox from 'vtk.js/Sources/Common/DataModel/BoundingBox'; |
| 16 | +import vtkFPSMonitor from 'vtk.js/Sources/Interaction/UI/FPSMonitor'; |
| 17 | + |
| 18 | +import vtkActor from 'vtk.js/Sources/Rendering/Core/Actor'; |
| 19 | +import vtkSphereSource from 'vtk.js/Sources/Filters/Sources/SphereSource'; |
| 20 | +import vtkMapper from 'vtk.js/Sources/Rendering/Core/Mapper'; |
| 21 | + |
| 22 | +import controlPanel from './controller.html'; |
| 23 | + |
| 24 | +// ---------------------------------------------------------------------------- |
| 25 | +// Show loading progress bar |
| 26 | +// ---------------------------------------------------------------------------- |
| 27 | +const rootBody = document.querySelector('body'); |
| 28 | +const myContainer = rootBody; |
| 29 | + |
| 30 | +const fpsMonitor = vtkFPSMonitor.newInstance(); |
| 31 | +const progressContainer = document.createElement('div'); |
| 32 | +myContainer.appendChild(progressContainer); |
| 33 | + |
| 34 | +const progressCallback = (progressEvent) => { |
| 35 | + if (progressEvent.lengthComputable) { |
| 36 | + const percent = Math.floor( |
| 37 | + (100 * progressEvent.loaded) / progressEvent.total |
| 38 | + ); |
| 39 | + progressContainer.innerHTML = `Loading ${percent}%`; |
| 40 | + } else { |
| 41 | + progressContainer.innerHTML = macro.formatBytesToProperUnit( |
| 42 | + progressEvent.loaded |
| 43 | + ); |
| 44 | + } |
| 45 | +}; |
| 46 | + |
| 47 | +// ---------------------------------------------------------------------------- |
| 48 | +// Main function to set up and render volume |
| 49 | +// ---------------------------------------------------------------------------- |
| 50 | +function createVolumeShadowViewer(rootContainer, fileContents) { |
| 51 | + // Container content and style |
| 52 | + const background = [0, 0, 0]; |
| 53 | + const containerStyle = { height: '100%', width: '100%' }; |
| 54 | + const controlPanelStyle = { |
| 55 | + position: 'absolute', |
| 56 | + left: '5px', |
| 57 | + top: '210px', |
| 58 | + backgroundColor: 'white', |
| 59 | + borderRadius: '5px', |
| 60 | + listStyle: 'none', |
| 61 | + padding: '5px 10px', |
| 62 | + margin: '0', |
| 63 | + display: 'block', |
| 64 | + border: 'solid 1px black', |
| 65 | + maxWidth: 'calc(100% - 70px)', |
| 66 | + maxHeight: 'calc(100% - 60px)', |
| 67 | + overflow: 'auto', |
| 68 | + }; |
| 69 | + const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({ |
| 70 | + background, |
| 71 | + containerStyle, |
| 72 | + rootContainer, |
| 73 | + controlPanelStyle, |
| 74 | + }); |
| 75 | + fullScreenRenderer.addController(controlPanel); |
| 76 | + |
| 77 | + const renderer = fullScreenRenderer.getRenderer(); |
| 78 | + renderer.setTwoSidedLighting(false); |
| 79 | + const renderWindow = fullScreenRenderer.getRenderWindow(); |
| 80 | + |
| 81 | + // FPS monitor |
| 82 | + const fpsElm = fpsMonitor.getFpsMonitorContainer(); |
| 83 | + fpsElm.style.position = 'absolute'; |
| 84 | + fpsElm.style.left = '10px'; |
| 85 | + fpsElm.style.bottom = '10px'; |
| 86 | + fpsElm.style.background = 'rgba(255,255,255,0.5)'; |
| 87 | + fpsElm.style.borderRadius = '5px'; |
| 88 | + fpsMonitor.setContainer(rootContainer); |
| 89 | + fpsMonitor.setRenderWindow(renderWindow); |
| 90 | + |
| 91 | + // Actor and mapper pipeline |
| 92 | + const vtiReader = vtkXMLImageDataReader.newInstance(); |
| 93 | + vtiReader.parseAsArrayBuffer(fileContents); |
| 94 | + const source = vtiReader.getOutputData(0); |
| 95 | + |
| 96 | + const actor = vtkVolume.newInstance(); |
| 97 | + const mapper = vtkVolumeMapper.newInstance(); |
| 98 | + |
| 99 | + actor.setMapper(mapper); |
| 100 | + mapper.setInputData(source); |
| 101 | + |
| 102 | + // Add one positional light |
| 103 | + const bounds = actor.getBounds(); |
| 104 | + const center = [ |
| 105 | + (bounds[1] - bounds[0]) / 2.0, |
| 106 | + (bounds[3] - bounds[2]) / 2.0, |
| 107 | + (bounds[5] - bounds[4]) / 2.0, |
| 108 | + ]; |
| 109 | + renderer.removeAllLights(); |
| 110 | + const light = vtkLight.newInstance(); |
| 111 | + const lightPos = [center[0] + 300, center[1] + 50, center[2] - 50]; |
| 112 | + light.setPositional(true); |
| 113 | + light.setLightType('SceneLight'); |
| 114 | + light.setPosition(lightPos); |
| 115 | + light.setFocalPoint(center); |
| 116 | + light.setColor(1, 1, 1); |
| 117 | + light.setIntensity(1.0); |
| 118 | + light.setConeAngle(50.0); |
| 119 | + renderer.addLight(light); |
| 120 | + |
| 121 | + // Set up sample distance and initialize volume shadow related paramters |
| 122 | + const sampleDistance = |
| 123 | + 0.7 * |
| 124 | + Math.sqrt( |
| 125 | + source |
| 126 | + .getSpacing() |
| 127 | + .map((v) => v * v) |
| 128 | + .reduce((a, b) => a + b, 0) |
| 129 | + ); |
| 130 | + mapper.setSampleDistance(sampleDistance / 2.5); |
| 131 | + mapper.setComputeNormalFromOpacity(false); |
| 132 | + mapper.setGlobalIlluminationReach(0.0); |
| 133 | + mapper.setVolumetricScatteringBlending(0.0); |
| 134 | + mapper.setVolumeShadowSamplingDistFactor(5.0); |
| 135 | + |
| 136 | + // Add transfer function |
| 137 | + const lookupTable = vtkColorTransferFunction.newInstance(); |
| 138 | + const piecewiseFunction = vtkPiecewiseFunction.newInstance(); |
| 139 | + actor.getProperty().setRGBTransferFunction(0, lookupTable); |
| 140 | + actor.getProperty().setScalarOpacity(0, piecewiseFunction); |
| 141 | + |
| 142 | + // Set actor properties |
| 143 | + actor.getProperty().setInterpolationTypeToLinear(); |
| 144 | + actor |
| 145 | + .getProperty() |
| 146 | + .setScalarOpacityUnitDistance( |
| 147 | + 0, |
| 148 | + vtkBoundingBox.getDiagonalLength(source.getBounds()) / |
| 149 | + Math.max(...source.getDimensions()) |
| 150 | + ); |
| 151 | + actor.getProperty().setGradientOpacityMinimumValue(0, 0); |
| 152 | + const dataArray = |
| 153 | + source.getPointData().getScalars() || source.getPointData().getArrays()[0]; |
| 154 | + const dataRange = dataArray.getRange(); |
| 155 | + actor |
| 156 | + .getProperty() |
| 157 | + .setGradientOpacityMaximumValue(0, (dataRange[1] - dataRange[0]) * 0.05); |
| 158 | + actor.getProperty().setShade(true); |
| 159 | + actor.getProperty().setUseGradientOpacity(0, false); |
| 160 | + actor.getProperty().setGradientOpacityMinimumOpacity(0, 0.0); |
| 161 | + actor.getProperty().setGradientOpacityMaximumOpacity(0, 1.0); |
| 162 | + actor.getProperty().setAmbient(0.0); |
| 163 | + actor.getProperty().setDiffuse(2.0); |
| 164 | + actor.getProperty().setSpecular(0.0); |
| 165 | + actor.getProperty().setSpecularPower(0.0); |
| 166 | + actor.getProperty().setUseLabelOutline(false); |
| 167 | + actor.getProperty().setLabelOutlineThickness(2); |
| 168 | + renderer.addActor(actor); |
| 169 | + |
| 170 | + // Control UI for sample distance, transfer function, and shadow on/off |
| 171 | + const controllerWidget = vtkVolumeController.newInstance({ |
| 172 | + size: [400, 150], |
| 173 | + rescaleColorMap: true, |
| 174 | + }); |
| 175 | + const isBackgroundDark = background[0] + background[1] + background[2] < 1.5; |
| 176 | + const useShadow = 1; |
| 177 | + controllerWidget.setContainer(rootContainer); |
| 178 | + controllerWidget.setupContent( |
| 179 | + renderWindow, |
| 180 | + actor, |
| 181 | + isBackgroundDark, |
| 182 | + useShadow |
| 183 | + ); |
| 184 | + |
| 185 | + fullScreenRenderer.setResizeCallback(({ width, _height }) => { |
| 186 | + if (width > 414) { |
| 187 | + controllerWidget.setSize(400, 150); |
| 188 | + } else { |
| 189 | + controllerWidget.setSize(width - 14, 150); |
| 190 | + } |
| 191 | + controllerWidget.render(); |
| 192 | + fpsMonitor.update(); |
| 193 | + }); |
| 194 | + |
| 195 | + // Add sliders to tune volume shadow effect |
| 196 | + function updateVSB(e) { |
| 197 | + const vsb = Number(e.target.value); |
| 198 | + mapper.setVolumetricScatteringBlending(vsb); |
| 199 | + renderWindow.render(); |
| 200 | + } |
| 201 | + function updateGlobalReach(e) { |
| 202 | + const gir = Number(e.target.value); |
| 203 | + mapper.setGlobalIlluminationReach(gir); |
| 204 | + renderWindow.render(); |
| 205 | + } |
| 206 | + function updateSD(e) { |
| 207 | + const sd = Number(e.target.value); |
| 208 | + mapper.setVolumeShadowSamplingDistFactor(sd); |
| 209 | + renderWindow.render(); |
| 210 | + } |
| 211 | + function updateAT(e) { |
| 212 | + const at = Number(e.target.value); |
| 213 | + mapper.setAnisotropy(at); |
| 214 | + renderWindow.render(); |
| 215 | + } |
| 216 | + const el = document.querySelector('.volumeBlending'); |
| 217 | + el.setAttribute('min', 0); |
| 218 | + el.setAttribute('max', 1); |
| 219 | + el.setAttribute('value', 0.0); |
| 220 | + el.addEventListener('input', updateVSB); |
| 221 | + const gr = document.querySelector('.globalReach'); |
| 222 | + gr.setAttribute('min', 0); |
| 223 | + gr.setAttribute('max', 1); |
| 224 | + gr.setAttribute('value', 0); |
| 225 | + gr.addEventListener('input', updateGlobalReach); |
| 226 | + const sd = document.querySelector('.samplingDist'); |
| 227 | + sd.setAttribute('min', 1); |
| 228 | + sd.setAttribute('max', 10); |
| 229 | + sd.setAttribute('value', 5); |
| 230 | + sd.addEventListener('input', updateSD); |
| 231 | + const at = document.querySelector('.anisotropy'); |
| 232 | + at.setAttribute('min', -1.0); |
| 233 | + at.setAttribute('max', 1.0); |
| 234 | + at.setAttribute('value', 0.0); |
| 235 | + at.addEventListener('input', updateAT); |
| 236 | + |
| 237 | + // Add toggle for density gradient versus scalar gradient |
| 238 | + let isDensity = false; |
| 239 | + const buttonID = document.querySelector('.text2'); |
| 240 | + function toggleDensityNormal() { |
| 241 | + isDensity = !isDensity; |
| 242 | + mapper.setComputeNormalFromOpacity(isDensity); |
| 243 | + buttonID.innerText = `(${isDensity ? 'on' : 'off'})`; |
| 244 | + renderWindow.render(); |
| 245 | + } |
| 246 | + |
| 247 | + // Render a sphere to represent light position, if light is positional |
| 248 | + if (light.getPositional()) { |
| 249 | + const sphereSource = vtkSphereSource.newInstance(); |
| 250 | + const actorSphere = vtkActor.newInstance({ |
| 251 | + position: lightPos, |
| 252 | + scale: [2, 2, 2], |
| 253 | + }); |
| 254 | + const mapperSphere = vtkMapper.newInstance(); |
| 255 | + mapperSphere.setInputConnection(sphereSource.getOutputPort()); |
| 256 | + |
| 257 | + actorSphere.getProperty().setColor([1, 0, 0]); |
| 258 | + actorSphere.getProperty().setLighting(false); |
| 259 | + actorSphere.setMapper(mapperSphere); |
| 260 | + actorSphere.setUseBounds(false); |
| 261 | + renderer.addActor(actorSphere); |
| 262 | + } |
| 263 | + |
| 264 | + // Camera and first render |
| 265 | + renderer.resetCamera(); |
| 266 | + renderWindow.render(); |
| 267 | + |
| 268 | + // Make some variables global so that you can inspect and |
| 269 | + // modify objects in your browser's developer console: |
| 270 | + global.source = vtiReader; |
| 271 | + global.mapper = mapper; |
| 272 | + global.actor = actor; |
| 273 | + global.renderer = renderer; |
| 274 | + global.renderWindow = renderWindow; |
| 275 | + global.toggleDensityNormal = toggleDensityNormal; |
| 276 | + global.updateVSB = updateVSB; |
| 277 | + global.updateAT = updateAT; |
| 278 | + global.updateGlobalReach = updateGlobalReach; |
| 279 | + global.updateSD = updateSD; |
| 280 | +} |
| 281 | + |
| 282 | +// ---------------------------------------------------------------------------- |
| 283 | +// Read volume and render |
| 284 | +// ---------------------------------------------------------------------------- |
| 285 | +HttpDataAccessHelper.fetchBinary( |
| 286 | + 'https://data.kitware.com/api/v1/item/59de9dc98d777f31ac641dc1/download', |
| 287 | + { |
| 288 | + progressCallback, |
| 289 | + } |
| 290 | +).then((binary) => { |
| 291 | + myContainer.removeChild(progressContainer); |
| 292 | + createVolumeShadowViewer(myContainer, binary); |
| 293 | +}); |
0 commit comments