Skip to content

Commit 08e9ad8

Browse files
committed
feat(XR): Add base WebXR AR support to OpenGL RenderWindow
1 parent f971e0f commit 08e9ad8

File tree

3 files changed

+140
-17
lines changed

3 files changed

+140
-17
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<button class='arbutton' style="width: 100%">Start AR</button>

Examples/Geometry/AR/index.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import 'vtk.js/Sources/favicon';
2+
3+
// Load the rendering pieces we want to use (for both WebGL and WebGPU)
4+
import 'vtk.js/Sources/Rendering/Profiles/Geometry';
5+
6+
import vtkActor from 'vtk.js/Sources/Rendering/Core/Actor';
7+
import vtkCalculator from 'vtk.js/Sources/Filters/General/Calculator';
8+
import vtkConeSource from 'vtk.js/Sources/Filters/Sources/ConeSource';
9+
import vtkFullScreenRenderWindow from 'vtk.js/Sources/Rendering/Misc/FullScreenRenderWindow';
10+
import vtkMapper from 'vtk.js/Sources/Rendering/Core/Mapper';
11+
import { AttributeTypes } from 'vtk.js/Sources/Common/DataModel/DataSetAttributes/Constants';
12+
import { FieldDataTypes } from 'vtk.js/Sources/Common/DataModel/DataSet/Constants';
13+
14+
// Force DataAccessHelper to have access to various data source
15+
import 'vtk.js/Sources/IO/Core/DataAccessHelper/HtmlDataAccessHelper';
16+
import 'vtk.js/Sources/IO/Core/DataAccessHelper/HttpDataAccessHelper';
17+
import 'vtk.js/Sources/IO/Core/DataAccessHelper/JSZipDataAccessHelper';
18+
19+
import controlPanel from './controller.html';
20+
21+
// ----------------------------------------------------------------------------
22+
// Standard rendering code setup
23+
// ----------------------------------------------------------------------------
24+
25+
const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance({
26+
background: [0, 0, 0, 255],
27+
});
28+
const renderer = fullScreenRenderer.getRenderer();
29+
const renderWindow = fullScreenRenderer.getRenderWindow();
30+
31+
// ----------------------------------------------------------------------------
32+
// Example code
33+
// ----------------------------------------------------------------------------
34+
// create a filter on the fly, sort of cool, this is a random scalars
35+
// filter we create inline, for a simple cone you would not need
36+
// this
37+
// ----------------------------------------------------------------------------
38+
39+
const coneSource = vtkConeSource.newInstance({ height: 100, radius: 50 });
40+
const filter = vtkCalculator.newInstance();
41+
42+
filter.setInputConnection(coneSource.getOutputPort());
43+
44+
filter.setFormula({
45+
getArrays: (inputDataSets) => ({
46+
input: [],
47+
output: [
48+
{
49+
location: FieldDataTypes.CELL,
50+
name: 'Random',
51+
dataType: 'Float32Array',
52+
attribute: AttributeTypes.SCALARS,
53+
},
54+
],
55+
}),
56+
evaluate: (arraysIn, arraysOut) => {
57+
const [scalars] = arraysOut.map((d) => d.getData());
58+
for (let i = 0; i < scalars.length; i++) {
59+
scalars[i] = Math.random();
60+
}
61+
},
62+
});
63+
64+
const mapper = vtkMapper.newInstance();
65+
mapper.setInputConnection(filter.getOutputPort());
66+
67+
const actor = vtkActor.newInstance();
68+
actor.setMapper(mapper);
69+
actor.setPosition(0.0, 0.0, -20.0);
70+
71+
renderer.addActor(actor);
72+
renderer.resetCamera();
73+
renderWindow.render();
74+
75+
// -----------------------------------------------------------
76+
// UI control handling
77+
// -----------------------------------------------------------
78+
79+
fullScreenRenderer.addController(controlPanel);
80+
const arbutton = document.querySelector('.arbutton');
81+
82+
const SESSION_IS_AR = true;
83+
arbutton.addEventListener('click', (e) => {
84+
if (arbutton.textContent === 'Start AR') {
85+
fullScreenRenderer.setBackground([0, 0, 0, 0]);
86+
fullScreenRenderer.getApiSpecificRenderWindow().startXR(SESSION_IS_AR);
87+
arbutton.textContent = 'Exit AR';
88+
} else {
89+
fullScreenRenderer.setBackground([0, 0, 0, 255]);
90+
fullScreenRenderer.getApiSpecificRenderWindow().stopXR(SESSION_IS_AR);
91+
arbutton.textContent = 'Start AR';
92+
}
93+
});
94+
95+
// -----------------------------------------------------------
96+
// Make some variables global so that you can inspect and
97+
// modify objects in your browser's developer console:
98+
// -----------------------------------------------------------
99+
100+
global.source = coneSource;
101+
global.mapper = mapper;
102+
global.actor = actor;
103+
global.renderer = renderer;
104+
global.renderWindow = renderWindow;

Sources/Rendering/OpenGL/RenderWindow/index.js

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -271,22 +271,26 @@ function vtkOpenGLRenderWindow(publicAPI, model) {
271271

272272
// Request an XR session on the user device with WebXR,
273273
// typically in response to a user request such as a button press
274-
publicAPI.startXR = () => {
274+
publicAPI.startXR = (isAR) => {
275275
if (navigator.xr === undefined) {
276276
throw new Error('WebXR is not available');
277277
}
278278

279-
if (!navigator.xr.isSessionSupported('immersive-vr')) {
280-
throw new Error('VR display is not available');
279+
model.xrSessionIsAR = isAR;
280+
const sessionType = isAR ? 'immersive-ar' : 'immersive-vr';
281+
if (!navigator.xr.isSessionSupported(sessionType)) {
282+
if (isAR) {
283+
throw new Error('Device does not support AR session');
284+
} else {
285+
throw new Error('VR display is not available');
286+
}
281287
}
282288
if (model.xrSession === null) {
283-
navigator.xr
284-
.requestSession('immersive-vr')
285-
.then(publicAPI.enterXR, () => {
286-
throw new Error('Failed to create VR session!');
287-
});
289+
navigator.xr.requestSession(sessionType).then(publicAPI.enterXR, () => {
290+
throw new Error('Failed to create XR session!');
291+
});
288292
} else {
289-
throw new Error('VR Session already exists!');
293+
throw new Error('XR Session already exists!');
290294
}
291295
};
292296

@@ -340,7 +344,9 @@ function vtkOpenGLRenderWindow(publicAPI, model) {
340344
model.xrSession = null;
341345
}
342346

343-
publicAPI.setSize(...model.oldCanvasSize);
347+
if (model.oldCanvasSize !== undefined) {
348+
publicAPI.setSize(...model.oldCanvasSize);
349+
}
344350

345351
// Reset to default canvas
346352
const ren = model.renderable.getRenderers()[0];
@@ -362,6 +368,12 @@ function vtkOpenGLRenderWindow(publicAPI, model) {
362368

363369
if (xrPose) {
364370
const gl = publicAPI.get3DContext();
371+
372+
if (model.xrSessionIsAR) {
373+
gl.canvas.width = model.oldCanvasSize[0];
374+
gl.canvas.height = model.oldCanvasSize[1];
375+
}
376+
365377
const glLayer = xrSession.renderState.baseLayer;
366378
gl.bindFramebuffer(gl.FRAMEBUFFER, glLayer.framebuffer);
367379
gl.clear(gl.COLOR_BUFFER_BIT);
@@ -375,13 +387,18 @@ function vtkOpenGLRenderWindow(publicAPI, model) {
375387
const viewport = glLayer.getViewport(view);
376388

377389
gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height);
378-
if (view.eye === 'left') {
379-
ren.setViewport(0, 0, 0.5, 1.0);
380-
} else if (view.eye === 'right') {
381-
ren.setViewport(0.5, 0, 1.0, 1.0);
382-
} else {
383-
// No handling for non-eye viewport
384-
return;
390+
391+
// TODO: Appropriate handling for AR passthrough on HMDs
392+
// with two eyes will require further investigation.
393+
if (!model.xrSessionIsAR) {
394+
if (view.eye === 'left') {
395+
ren.setViewport(0, 0, 0.5, 1.0);
396+
} else if (view.eye === 'right') {
397+
ren.setViewport(0.5, 0, 1.0, 1.0);
398+
} else {
399+
// No handling for non-eye viewport
400+
return;
401+
}
385402
}
386403

387404
ren
@@ -1131,6 +1148,7 @@ const DEFAULT_VALUES = {
11311148
defaultToWebgl2: true, // attempt webgl2 on by default
11321149
activeFramebuffer: null,
11331150
xrSession: null,
1151+
xrSessionIsAR: false,
11341152
xrReferenceSpace: null,
11351153
imageFormat: 'image/png',
11361154
useOffScreen: false,

0 commit comments

Comments
 (0)