2020-03-05 06:47:24 +00:00
|
|
|
var webxr = {
|
|
|
|
$state: {},
|
|
|
|
|
|
|
|
webxr_init: function(offset, msaa) {
|
|
|
|
if (!navigator.xr) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-08-16 06:31:20 +00:00
|
|
|
state.sessions = {};
|
|
|
|
state.session = null;
|
2020-03-05 06:47:24 +00:00
|
|
|
state.frame = null;
|
|
|
|
state.clipNear = .1;
|
|
|
|
state.clipFar = 1000.0;
|
|
|
|
state.renderCallback = null;
|
|
|
|
state.renderUserdata = null;
|
2020-08-16 06:31:20 +00:00
|
|
|
state.camera = Module._malloc(264 /* sizeof(Camera) */);
|
|
|
|
state.boundsGeometry = 0; /* NULL */
|
|
|
|
state.boundsGeometryCount = 0;
|
|
|
|
|
|
|
|
var mappings = {
|
|
|
|
'oculus-touch-left': [0, 3, null, 1, null, null, null, 4, 5],
|
|
|
|
'oculus-touch-right': [0, 3, null, 1, null, 4, 5, null, null],
|
|
|
|
'valve-index': [0, 3, 2, 1, null, 4, null, 4, null],
|
|
|
|
'microsoft-mixed-reality': [0, 3, 2, 1],
|
|
|
|
'htc-vive': [0, null, 2, 1],
|
|
|
|
'generic-trigger': [0],
|
|
|
|
'generic-trigger-touchpad': [0, null, 2],
|
|
|
|
'generic-trigger-thumbstick': [0, 3],
|
|
|
|
'generic-trigger-touchpad-thumbstick': [0, 3, 2],
|
|
|
|
'generic-trigger-squeeze': [0, null, null, 1],
|
|
|
|
'generic-trigger-squeeze-touchpad': [0, null, 2, 1],
|
|
|
|
'generic-trigger-squeeze-touchpad-thumbstick': [0, 3, 2, 1],
|
|
|
|
'generic-trigger-squeeze-thumbstick': [0, 3, null, 1],
|
|
|
|
'generic-hand-select': [0],
|
|
|
|
};
|
|
|
|
|
|
|
|
function startSession(mode, options) {
|
|
|
|
return navigator.xr.requestSession(mode, options).then(function(session) {
|
|
|
|
var spaces = {
|
|
|
|
'inline': ['viewer'],
|
|
|
|
'immersive-vr': ['bounded-floor', 'local-floor']
|
|
|
|
};
|
|
|
|
|
|
|
|
// This is confusing but it basically keeps trying to request successive reference spaces
|
|
|
|
// until one succeeds. $space is a promise that resolves to a reference space
|
|
|
|
var $space = spaces[mode].reduce(function(chain, spaceType) {
|
|
|
|
return chain.catch(function() {
|
|
|
|
session.spaceType = spaceType;
|
|
|
|
return session.requestReferenceSpace(spaceType);
|
|
|
|
});
|
|
|
|
}, Promise.reject());
|
|
|
|
|
|
|
|
session.inputSources = [];
|
|
|
|
|
|
|
|
return $space.then(function(space) {
|
|
|
|
state.session = session;
|
|
|
|
state.sessions[mode] = session;
|
|
|
|
session.layer = new XRWebGLLayer(session, Module.preinitializedWebGLContext);
|
|
|
|
session.updateRenderState({
|
|
|
|
baseLayer: session.layer,
|
|
|
|
inlineVerticalFieldOfView: mode === 'inline' ? (67.0 * Math.PI / 180.0) : undefined
|
|
|
|
});
|
|
|
|
|
|
|
|
if (session.spaceType.includes('floor')) {
|
|
|
|
session.space = space;
|
|
|
|
} else {
|
|
|
|
session.space = space.getOffsetReferenceSpace(new XRRigidTransform({ y: -offset }));
|
2020-03-05 06:47:24 +00:00
|
|
|
}
|
|
|
|
|
2020-08-16 06:31:20 +00:00
|
|
|
session.framebufferId = 0;
|
2020-03-05 06:47:24 +00:00
|
|
|
|
2020-08-16 06:31:20 +00:00
|
|
|
if (session.layer.framebuffer) {
|
|
|
|
session.framebufferId = GL.getNewId(GL.framebuffers);
|
|
|
|
GL.framebuffers[session.framebufferId] = session.layer.framebuffer;
|
2020-03-05 06:47:24 +00:00
|
|
|
}
|
|
|
|
|
2020-08-16 06:31:20 +00:00
|
|
|
var sizeof_CanvasFlags = 16;
|
|
|
|
var flags = Module.stackAlloc(sizeof_CanvasFlags);
|
|
|
|
HEAPU8.fill(0, flags, flags + sizeof_CanvasFlags); // memset(&flags, 0, sizeof(CanvasFlags));
|
|
|
|
HEAPU8[flags + 12] = mode === 'inline' ? 0 : 1; // flags.stereo
|
|
|
|
var width = session.layer.framebufferWidth;
|
|
|
|
var height = session.layer.framebufferHeight;
|
|
|
|
session.canvas = Module['_lovrCanvasCreateFromHandle'](width, height, flags, session.framebufferId, 0, 0, 1, true);
|
|
|
|
Module.stackRestore(flags);
|
|
|
|
|
|
|
|
session.animationFrame = session.requestAnimationFrame(function onFrame(t, frame) {
|
|
|
|
session.animationFrame = session.requestAnimationFrame(onFrame);
|
|
|
|
session.displayTime = t;
|
|
|
|
session.frame = frame;
|
|
|
|
session.viewer = frame.getViewerPose(session.space);
|
|
|
|
|
|
|
|
if (!state.renderCallback) return;
|
|
|
|
|
|
|
|
var views = session.viewer.views;
|
|
|
|
var stereo = views.length > 1;
|
|
|
|
var matrices = (state.camera + 8) >> 2;
|
|
|
|
HEAPU8[state.camera + 0] = stereo; // camera.stereo = stereo
|
|
|
|
HEAPU32[(state.camera + 4) >> 2] = session.canvas; // camera.canvas = session.canvas
|
|
|
|
HEAPF32.set(views[0].transform.inverse.matrix, matrices + 0);
|
|
|
|
HEAPF32.set(views[0].projectionMatrix, matrices + 32);
|
|
|
|
if (stereo) {
|
|
|
|
HEAPF32.set(views[1].transform.inverse.matrix, matrices + 16);
|
|
|
|
HEAPF32.set(views[1].projectionMatrix, matrices + 48);
|
|
|
|
}
|
|
|
|
|
|
|
|
Module['_lovrGraphicsSetCamera'](state.camera, true);
|
|
|
|
Module['dynCall_vi'](state.renderCallback, state.renderUserdata);
|
|
|
|
Module['_lovrGraphicsSetCamera'](0, false);
|
|
|
|
});
|
|
|
|
|
|
|
|
session.addEventListener('inputsourceschange', function(event) {
|
|
|
|
session.inputSources.forEach(function(inputSource, i) {
|
|
|
|
if (event.removed.includes(inputSource)) {
|
|
|
|
session.inputSources[i] = null;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
event.added.forEach(function(inputSource) {
|
|
|
|
if (inputSource.handedness === 'left') {
|
|
|
|
session.inputSources[1 /* DEVICE_HAND_LEFT */] = inputSource;
|
|
|
|
} else if (inputSource.handedness === 'right') {
|
|
|
|
session.inputSources[2 /* DEVICE_HAND_RIGHT */] = inputSource;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (var i = 0; i < inputSource.profiles.length; i++) {
|
|
|
|
var profile = inputSource.profiles[i];
|
|
|
|
|
|
|
|
// So far Oculus touch controllers are the only "meaningfully handed" controllers
|
|
|
|
// If more appear then a more general approach should be used
|
|
|
|
if (profile === 'oculus-touch') {
|
|
|
|
profile = profile + '-' + inputSource.handedness;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (mappings[profile]) {
|
|
|
|
inputSource.mapping = mappings[profile];
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
session.addEventListener('end', function() {
|
|
|
|
delete state.sessions[session.mode];
|
|
|
|
|
|
|
|
if (session.canvas) {
|
|
|
|
Module['_lovrCanvasDestroy'](session.canvas);
|
|
|
|
Module._free(session.canvas - 4);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (session.framebufferId) {
|
|
|
|
GL.framebuffers[session.framebufferId].name = 0;
|
|
|
|
GL.framebuffers[session.framebufferId] = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the immersive session ends (for any reason), switch back to the inline session
|
|
|
|
if (session.mode === 'immersive-vr') {
|
|
|
|
state.session = state.sessions.inline;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return session;
|
2020-03-05 06:47:24 +00:00
|
|
|
});
|
|
|
|
});
|
2020-08-16 06:31:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Module.lovr = Module.lovr || {};
|
|
|
|
Module.lovr.enterVR = function() {
|
|
|
|
return startSession('immersive-vr', {
|
|
|
|
requiredFeatures: ['local-floor'],
|
|
|
|
optionalFeatures: ['bounded-floor']
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
Module.lovr.exitVR = function() {
|
|
|
|
return (state.session && state.session.mode === 'immersive-vr') ? state.session.end() : Promise.resolve();
|
|
|
|
};
|
|
|
|
|
|
|
|
startSession('inline');
|
2020-03-05 06:47:24 +00:00
|
|
|
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
|
|
|
|
webxr_destroy: function() {
|
2020-08-16 06:31:20 +00:00
|
|
|
for (mode in state.sessions) {
|
|
|
|
state.sessions[mode].end();
|
2020-03-05 06:47:24 +00:00
|
|
|
}
|
|
|
|
|
2020-08-16 06:31:20 +00:00
|
|
|
Module._free(state.camera|0);
|
|
|
|
Module._free(state.boundsGeometry|0);
|
2020-03-05 06:47:24 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
webxr_getName: function(name, size) {
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
|
|
|
|
webxr_getOriginType: function() {
|
2020-08-16 06:31:20 +00:00
|
|
|
if (!state.session) return 0;
|
|
|
|
return state.session.spaceType.includes('floor') ? 1 /* ORIGIN_FLOOR */ : 0 /* ORIGIN_HEAD */;
|
2020-03-05 06:47:24 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
webxr_getDisplayTime: function() {
|
2020-08-16 06:31:20 +00:00
|
|
|
return state.session ? (state.session.displayTime / 1000.0) : 0;
|
2020-03-05 06:47:24 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
webxr_getDisplayDimensions: function(width, height) {
|
2020-08-16 06:31:20 +00:00
|
|
|
if (state.session) {
|
|
|
|
HEAPU32[width >> 2] = state.session.layer.framebufferWidth;
|
|
|
|
HEAPU32[height >> 2] = state.session.layer.framebufferHeight;
|
|
|
|
} else {
|
|
|
|
HEAPU32[width >> 2] = 0;
|
|
|
|
HEAPU32[height >> 2] = 0;
|
|
|
|
}
|
2020-03-05 06:47:24 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
webxr_getDisplayFrequency: function() {
|
|
|
|
return 0.0;
|
|
|
|
},
|
|
|
|
|
|
|
|
webxr_getDisplayMask: function() {
|
|
|
|
return 0; /* NULL */
|
|
|
|
},
|
|
|
|
|
|
|
|
webxr_getViewCount: function() {
|
2020-08-16 06:31:20 +00:00
|
|
|
if (!state.session) {
|
2020-03-05 06:47:24 +00:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2020-08-16 06:31:20 +00:00
|
|
|
return state.session.viewer.views.length;
|
2020-03-05 06:47:24 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
webxr_getViewPose: function(index, position, orientation) {
|
2020-08-16 06:31:20 +00:00
|
|
|
if (!state.session || !state.session.viewer) {
|
2020-03-05 06:47:24 +00:00
|
|
|
return false;
|
|
|
|
}
|
2020-08-16 06:31:20 +00:00
|
|
|
|
|
|
|
var view = state.session.viewer.views[index];
|
|
|
|
if (state.session.viewer.views[index]) {
|
|
|
|
var transform = view.transform;
|
|
|
|
HEAPF32[position >> 2 + 0] = transform.position.x;
|
|
|
|
HEAPF32[position >> 2 + 1] = transform.position.y;
|
|
|
|
HEAPF32[position >> 2 + 2] = transform.position.z;
|
|
|
|
HEAPF32[position >> 2 + 3] = transform.position.w;
|
|
|
|
HEAPF32[orientation >> 2 + 0] = transform.orientation.x;
|
|
|
|
HEAPF32[orientation >> 2 + 1] = transform.orientation.y;
|
|
|
|
HEAPF32[orientation >> 2 + 2] = transform.orientation.z;
|
|
|
|
HEAPF32[orientation >> 2 + 3] = transform.orientation.w;
|
2020-03-05 06:47:24 +00:00
|
|
|
return true;
|
|
|
|
}
|
2020-08-16 06:31:20 +00:00
|
|
|
|
2020-03-05 06:47:24 +00:00
|
|
|
return false;
|
|
|
|
},
|
|
|
|
|
|
|
|
webxr_getViewAngles: function(index, left, right, up, down) {
|
2020-08-16 06:31:20 +00:00
|
|
|
return false; // TODO
|
2020-03-05 06:47:24 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
webxr_getClipDistance: function(clipNear, clipFar) {
|
|
|
|
HEAPF32[clipNear >> 2] = state.clipNear;
|
|
|
|
HEAPF32[clipFar >> 2] = state.clipFar;
|
|
|
|
},
|
|
|
|
|
|
|
|
webxr_setClipDistance: function(clipNear, clipFar) {
|
2020-08-16 06:31:20 +00:00
|
|
|
if (!state.session) return;
|
2020-03-05 06:47:24 +00:00
|
|
|
state.clipNear = clipNear;
|
|
|
|
state.clipFar = clipFar;
|
|
|
|
state.session.updateRenderState({
|
|
|
|
clipNear: clipNear,
|
|
|
|
clipFar: clipFar
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
webxr_getBoundsDimensions: function(width, depth) {
|
2020-08-16 06:31:20 +00:00
|
|
|
HEAPF32[width >> 2] = 0.0; // Unsupported, see #557
|
2020-03-05 06:47:24 +00:00
|
|
|
HEAPF32[depth >> 2] = 0.0;
|
|
|
|
},
|
|
|
|
|
|
|
|
webxr_getBoundsGeometry: function(count) {
|
2020-08-16 06:31:20 +00:00
|
|
|
if (!state.session || !(state.session.space instanceof XRBoundedReferenceSpace)) {
|
|
|
|
return 0; /* NULL */
|
|
|
|
}
|
|
|
|
|
|
|
|
var points = state.session.space.boundsGeometry;
|
|
|
|
|
|
|
|
if (state.boundsGeometryCount < points.length) {
|
|
|
|
Module._free(state.boundsGeometry|0);
|
|
|
|
state.boundsGeometry = Module._malloc(4 * 4 * points.length);
|
|
|
|
if (state.boundsGeometry === 0) {
|
|
|
|
return state.boundsGeometry;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (var i = 0; i < points.length; i++) {
|
|
|
|
HEAPF32.set(points[i], state.boundsGeometry + 4 * i);
|
|
|
|
}
|
|
|
|
|
|
|
|
return state.boundsGeometry;
|
2020-03-05 06:47:24 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
webxr_getPose: function(device, position, orientation) {
|
2020-08-16 06:31:20 +00:00
|
|
|
if (!state.session || !state.session.viewer) return false;
|
|
|
|
|
|
|
|
if (device === 0 /* DEVICE_HEAD */) {
|
|
|
|
var transform = state.session.viewer.transform;
|
|
|
|
HEAPF32[position >> 2 + 0] = transform.position.x;
|
|
|
|
HEAPF32[position >> 2 + 1] = transform.position.y;
|
|
|
|
HEAPF32[position >> 2 + 2] = transform.position.z;
|
|
|
|
HEAPF32[position >> 2 + 3] = transform.position.w;
|
|
|
|
HEAPF32[orientation >> 2 + 0] = transform.orientation.x;
|
|
|
|
HEAPF32[orientation >> 2 + 1] = transform.orientation.y;
|
|
|
|
HEAPF32[orientation >> 2 + 2] = transform.orientation.z;
|
|
|
|
HEAPF32[orientation >> 2 + 3] = transform.orientation.w;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (state.session.inputSources[device]) {
|
|
|
|
var inputSource = state.session.inputSources[device];
|
|
|
|
var space = inputSource.gripSpace || inputSource.targetRaySpace;
|
|
|
|
var transform = state.session.frame.getPose(space, state.session.space).transform;
|
|
|
|
HEAPF32[position >> 2 + 0] = transform.position.x;
|
|
|
|
HEAPF32[position >> 2 + 1] = transform.position.y;
|
|
|
|
HEAPF32[position >> 2 + 2] = transform.position.z;
|
|
|
|
HEAPF32[position >> 2 + 3] = transform.position.w;
|
|
|
|
HEAPF32[orientation >> 2 + 0] = transform.orientation.x;
|
|
|
|
HEAPF32[orientation >> 2 + 1] = transform.orientation.y;
|
|
|
|
HEAPF32[orientation >> 2 + 2] = transform.orientation.z;
|
|
|
|
HEAPF32[orientation >> 2 + 3] = transform.orientation.w;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
2020-03-05 06:47:24 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
webxr_getVelocity: function(device, velocity, angularVelocity) {
|
2020-08-16 06:31:20 +00:00
|
|
|
return false; // Unsupported, see #619
|
2020-03-05 06:47:24 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
webxr_isDown: function(device, button, down, changed) {
|
2020-08-16 06:31:20 +00:00
|
|
|
if (!state.session) return false;
|
|
|
|
|
|
|
|
var inputSource = state.session.inputSources[device];
|
|
|
|
if (!inputSource || !inputSource.gamepad || !inputSource.mapping || !inputSource.mapping[button]) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
HEAPU32[down >> 2] = inputSource.gamepad.buttons[inputSource.mapping[button]].pressed ? 1 : 0;
|
|
|
|
HEAPU32[changed >> 2] = 0; // TODO
|
|
|
|
return true;
|
2020-03-05 06:47:24 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
webxr_isTouched: function(device, button, touched) {
|
2020-08-16 06:31:20 +00:00
|
|
|
if (!state.session) return false;
|
|
|
|
|
|
|
|
var inputSource = state.session.inputSources[device];
|
|
|
|
if (!inputSource || !inputSource.gamepad || !inputSource.mapping || !inputSource.mapping[button]) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
HEAPU32[touched >> 2] = inputSource.gamepad.buttons[inputSource.mapping[button]].touched ? 1 : 0;
|
|
|
|
return true;
|
2020-03-05 06:47:24 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
webxr_getAxis: function(device, axis, value) {
|
2020-08-16 06:31:20 +00:00
|
|
|
if (!state.session) return false;
|
|
|
|
|
|
|
|
var inputSource = state.session.inputSources[device];
|
|
|
|
if (!inputSource || !inputSource.gamepad || !inputSource.mapping) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (axis) {
|
|
|
|
// These 1D axes are queried as buttons in the Gamepad API
|
|
|
|
// The DeviceAxis enumerants match the DeviceButton ones, so they're interchangeable
|
|
|
|
case 0: /* AXIS_TRIGGER */
|
|
|
|
case 3: /* AXIS_GRIP */
|
|
|
|
if (inputSource.mapping[axis]) {
|
|
|
|
HEAPF32[value >> 2] = inputSource.gamepad.buttons[inputSource.mapping[axis]].value;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
|
|
|
|
case 1: /* AXIS_THUMBSTICK */
|
|
|
|
HEAPF32[value >> 2 + 0] = inputSource.gamepad.axes[2];
|
|
|
|
HEAPF32[value >> 2 + 1] = inputSource.gamepad.axes[3];
|
|
|
|
return true;
|
|
|
|
|
|
|
|
case 2: /* AXIS_TOUCHPAD */
|
|
|
|
HEAPF32[value >> 2 + 0] = inputSource.gamepad.axes[0];
|
|
|
|
HEAPF32[value >> 2 + 1] = inputSource.gamepad.axes[1];
|
|
|
|
return true;
|
|
|
|
|
|
|
|
default:
|
|
|
|
return false;
|
|
|
|
}
|
2020-03-05 06:47:24 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
webxr_vibrate: function(device, strength, duration, frequency) {
|
2020-08-16 06:31:20 +00:00
|
|
|
if (!state.session) return false;
|
|
|
|
|
|
|
|
var inputSource = state.session.inputSources[device];
|
|
|
|
if (!inputSource || !inputSource.gamepad || !inputSource.gamepad.hapticActuators || !inputSource.gamepad.hapticActuators[0]) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Not technically an official WebXR feature, but widely supported
|
|
|
|
inputSource.gamepad.hapticActuators[0].pulse(strength, duration * 1000);
|
|
|
|
return true;
|
2020-03-05 06:47:24 +00:00
|
|
|
},
|
|
|
|
|
2020-08-02 23:25:51 +00:00
|
|
|
webxr_newModelData: function(device, animated) {
|
2020-08-16 06:31:20 +00:00
|
|
|
return 0; /* NULL */
|
2020-03-05 06:47:24 +00:00
|
|
|
},
|
|
|
|
|
2020-08-02 23:25:51 +00:00
|
|
|
webxr_animate: function(device, model) {
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
|
2020-03-05 06:47:24 +00:00
|
|
|
webxr_renderTo: function(callback, userdata) {
|
|
|
|
state.renderCallback = callback;
|
|
|
|
state.renderUserdata = userdata;
|
|
|
|
},
|
|
|
|
|
|
|
|
webxr_update: function(dt) {
|
|
|
|
//
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
autoAddDeps(webxr, '$state');
|
|
|
|
mergeInto(LibraryManager.library, webxr);
|