Skip to content

Commit 206d645

Browse files
authored
Merge pull request #2470 from Kitware/volumeShadowExample
docs(volume shadow): add example light and shadow ray example
2 parents 8490c07 + ca07f48 commit 206d645

File tree

6 files changed

+401
-4
lines changed

6 files changed

+401
-4
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<div>
2+
<button onClick="toggleDensityNormal()">Toggle Density Normal: <i class="text2">(off)</i></button>
3+
</div>
4+
<table>
5+
<tr>
6+
<td>Surface to Volume Blending</td>
7+
<td>
8+
<input class='volumeBlending' type="range" min="0.0" max="1.0" step="0.05" value="0.0" />
9+
</td>
10+
</tr>
11+
<tr>
12+
<td>G Illumination Reach </td>
13+
<td>
14+
<input class='globalReach' type="range" min="0.0" max="1.0" step="0.05" value="0.0" />
15+
</td>
16+
</tr>
17+
<tr>
18+
<td>VS Sample Dist</td>
19+
<td>
20+
<input class='samplingDist' type="range" min="1.0" max="10.0" step="1.0" value="5.0" />
21+
</td>
22+
</tr>
23+
<tr>
24+
<td>Anisotropy</td>
25+
<td>
26+
<input class='anisotropy' type="range" min="-1.0" max="1.0" step="0.05" value="0.0" />
27+
</td>
28+
</tr>
29+
</table>
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
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+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
2+
### Volume Mapper Light and Shadow
3+
4+
This example demonstrates experimental light and shadow features for vtk.js. Secondary shadow rays is relatively expensive to render in terms of computation and memory. We recommend initializing this example with less processor demanding parameters. Please turn on WebGL2 on your target browser.
5+
6+
## Widgets included in this example
7+
8+
- Standard [vtkVolumeController](https://kitware.github.io/vtk-js/api/Interaction_UI_VolumeController.html) widget, also seen in [VolumeViewer](https://kitware.github.io/vtk-js/examples/VolumeViewer.html) example.
9+
- Controll panel with parameters related to shadow and lighting
10+
* **Toggle Density Normal**: if on, normal is calculated based on the changes of opacity value. Otherwise it is based on scalar value.
11+
* **Surface to Volume Blending**: at 0.0, shadow effect is created using phong surface model (local gradient); at 1.0, shadow effect is created using secondary shadow ray (global shadow); any value in between blends these two effects. In terms of speed, blending is slower than secondary shadow ray, and secondary shadow ray is slower than surface rendering.
12+
* **G Illumination Reach**: at 0.0, length of shadow ray equals to 1 unit of sampling distance; at 1.0, length of shadow ray equals to maximum possible value, i.e., it traverse through the entire volume; values in between interpolates between the minimum and maximum. Longer shadow ray means slower rendering.
13+
* **VS Sample Dist**: represents sample distance multiplier. At 1.0, shadow ray sample distance equals to primary ray sample distance; at 2.0, shadow ray sample distance is 2 times primary ray sample distance; and so on. Greater multipler means faster rendering, but also less accurate shadow effect.
14+
* **Anistropy**: at -1.0, light scatters backward; at 0.0, light scatters uniformly; at 1.0, light scatters forward.
15+
16+
Shadow and light effects are only rendered if shadow is turned on, and with at least one light source present.
17+
18+
Documentation on these experimental features are not updated yet.
19+
20+

0 commit comments

Comments
 (0)