Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor / p5xr consolidation / Fix AR mode #215

Merged
merged 11 commits into from
Jul 29, 2024
14 changes: 14 additions & 0 deletions examples/ar/hello-cube/example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
function preload() {
createARCanvas();
}

function setup() {
describe("A cube waiting to be seen");
}

function draw() {
push();
translate(0, 0, -0.4);
box(0.1, 0.1, 0.1);
pop();
}
17 changes: 17 additions & 0 deletions examples/ar/hello-cube/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!doctype html>
<html>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1, user-scalable=no'>
<meta name='mobile-web-app-capable' content='yes'>
<meta name='apple-mobile-web-app-capable' content='yes'>

<title>AR EXAMPLE #1 : Hello Cube</title>
<script src='../../../node_modules/p5/lib/p5.js'></script>
<script src='../../../dist/p5xr.min.js'></script>
</head>
<body>
<header></header>
<script src="example.js"></script>
</body>
</html>
6 changes: 2 additions & 4 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,9 @@ window.p5xr = {
* @section VR
* @category Initialization
*/
p5.prototype.createVRCanvas = function (xrButton) {
p5.prototype.createVRCanvas = function () {
noLoop();
p5xr.instance = new p5vr(xrButton);
p5xr.instance.__initVR();
p5xr.instance = new p5vr();
};

/**
Expand All @@ -46,7 +45,6 @@ p5.prototype.createVRCanvas = function (xrButton) {
p5.prototype.createARCanvas = function () {
noLoop();
p5xr.instance = new p5ar();
p5xr.instance.initAR();
};

/**
Expand Down
192 changes: 163 additions & 29 deletions src/p5xr/core/p5xr.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ import p5xrInput from './p5xrInput';
*
* @constructor
*
* @param {Object} [options={}] - Configuration options for the XR session
* @param {Array<"anchors" | "bounded-floor" | "depth-sensing" | "dom-overlay" |
* "hand-tracking" | "hit-test" | "layers" | "light-estimation" | "local" |
* "local-floor" | "secondary-views" | "unbounded" | "viewer">} [options.requiredFeatures=[]] - Required features
* @param {Object} [options={}] - Configuration options for the XR session
* @param {Array<"anchors" | "bounded-floor" | "depth-sensing" | "dom-overlay" |
* "hand-tracking" | "hit-test" | "layers" | "light-estimation" | "local" |
* "local-floor" | "secondary-views" | "unbounded" | "viewer">} [options.optionalFeatures=[]] - Optional features
*
* @property mode {"inline" | "immersive-ar" | "immersive-vr"} WebXR session mode
* @property vrDevice {XRDevice} the current VR compatible device
* @property vrSession {XRSession} the current VR session
* @property vrFrameOfRef {XRFrameOfReference} the current VR frame of reference
Expand All @@ -16,19 +26,28 @@ import p5xrInput from './p5xrInput';
* @property curClearColor {Color} background clear color set by global `setVRBackgroundColor`
*/
export default class p5xr {
constructor(xrButton) {
constructor(options = {}) {
const {
requiredFeatures = [],
optionalFeatures = [],
} = options;

this.xrDevice = null;
this.xrButton = xrButton || null;
this.isVR = null;
this.mode = 'inline';
this.hasImmersive = null;
this.isImmersive = false;
this.xrSession = null;
this.xrRefSpace = null;
this.xrViewerSpace = null;
this.xrHitTestSource = null;
this.frame = null;
this.gl = null;
this.curClearColor = color(255, 255, 255);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious about this one!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, silly mistake

this.curClearColor = color(256, 255, 255);
this.viewer = new p5xrViewer();

this.requiredFeatures = requiredFeatures;
this.optionalFeatures = optionalFeatures;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This interface might be interesting to offer flexibility from the user's perspective in the future.

}

/**
Expand Down Expand Up @@ -125,17 +144,135 @@ export default class p5xr {
// WebXR availabilty
if (navigator?.xr) {
console.log('XR Available');
const mode = this.isVR ? 'VR' : 'AR';
const session = this.isVR ? 'immersive-vr' : 'immersive-ar';
const supported = await navigator.xr.isSessionSupported(session);
const supported = await navigator.xr.isSessionSupported(this.mode);
this.hasImmersive = supported;
this.xrButton.setAvailable(supported, mode);
this.xrButton.setAvailable(supported, this.mode);
} else {
console.log('XR Not Available');
this.xrButton.disable();
}
}

/**
* Helper function to reset XR and GL, should be called between
* ending an XR session and starting a new XR session
* @method resetXR
*/
resetXR() {
this.xrDevice = null;
this.xrSession = null;
this.xrRefSpace = null;
this.xrViewerSpace = null;
this.xrHitTestSource = null;
this.gl = null;
this.frame = null;
}

/**
* `navigator.xr.requestSession()` must be called within a user gesture event.
* @private
* @ignore
*/
__onXRButtonClicked() {
if (this.hasImmersive) {
console.log(`Requesting session with mode: ${this.mode}`);
this.isImmersive = true;
this.resetXR();
navigator.xr
.requestSession(this.mode, {
requiredFeatures: this.requiredFeatures,
optionalFeatures: this.optionalFeatures,
})
.then(this.__startSketch.bind(this))
.catch((error) => {
console.error(`An error occured activating ${this.mode}: ${error}`);
});
} else {
this.xrButton.hide();
}
}

/**
* This is where the actual p5 canvas is first created, and
* the GL rendering context is accessed by p5vr.
* The current XRSession also gets a frame of reference and
* base rendering layer. <br>
* @param {XRSession}
* @private
* @ignore
*/
__startSketch(session) {
this.xrSession = session;
this.canvas = p5.instance.canvas;
this.canvas.style.visibility = 'visible';

if (typeof window.setup === 'function') {
window.setup();
p5.instance._millisStart = window.performance.now();
}
// const refSpaceRequest = this.isImmersive ? 'local' : 'viewer';
// this.xrSession.requestReferenceSpace(refSpaceRequest).then((refSpace) => {
// this.xrRefSpace = refSpace;
// // Inform the session that we're ready to begin drawing.
// this.xrSession.requestAnimationFrame(this.__onXRFrame.bind(this));
// if (!this.isImmersive) {
// this.xrSession.updateRenderState({
// baseLayer: new XRWebGLLayer(this.xrSession, this.gl),
// inlineVerticalFieldOfView: 70 * (Math.PI / 180),
// });
// this.addInlineViewListeners(this.canvas);
// }
// });
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a fan of either commenting to explain a commented out chunk or just deleting the deprecated code

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm putting this back, this is necessary for inline mode to work

this.__onRequestSession();
}

/**
* Requests a reference space and makes the p5's WebGL layer XR compatible.
* @private
* @ignore
*/
__onRequestSession() {
p5.instance._renderer._curCamera.cameraType = 'custom';
const refSpaceRequest = this.isImmersive ? 'local' : 'viewer';

this.gl = this.canvas.getContext(p5.instance.webglVersion);
this.gl
.makeXRCompatible()
.then(() => {
// Use the p5's WebGL context to create a XRWebGLLayer and set it as the
// sessions baseLayer. This allows any content rendered to the layer to
// be displayed on the XRDevice;
this.xrSession.updateRenderState({
baseLayer: new XRWebGLLayer(this.xrSession, this.gl),
});
// TODO : need better way to handle feature-specific actions
if (this.requiredFeatures.includes('hit-test')) {
this.xrSession.requestReferenceSpace('viewer').then((refSpace) => {
this.xrViewerSpace = refSpace;
this.xrSession
.requestHitTestSource({ space: this.xrViewerSpace })
.then((hitTestSource) => {
this.xrHitTestSource = hitTestSource;
});
});
}

// Get a frame of reference, which is required for querying poses.
// 'local' places the initial pose relative to initial location of viewer
// 'viewer' is only for inline experiences and only allows rotation
this.xrSession
.requestReferenceSpace(refSpaceRequest)
.then((refSpace) => {
this.xrRefSpace = refSpace;
// Request initial animation frame
this.xrSession.requestAnimationFrame(this.__onXRFrame.bind(this));
});
})
.catch((e) => {
console.log(e);
});
}

/**
* This is the method that is attached to the event that announces
* availability of a new frame. The next animation frame is requested here,
Expand Down Expand Up @@ -180,6 +317,20 @@ export default class p5xr {
this.clearVR();
}

const context = window;
const userCalculate = context.calculate;
if (this.viewer.pose.views.length > 1) {
if (typeof userCalculate === 'function') {
userCalculate();
}
const now = window.performance.now();
p5.instance.deltaTime = now - p5.instance._lastFrameTime;
p5.instance._frameRate = 1000.0 / p5.instance.deltaTime;
p5.instance._setProperty('deltaTime', p5.instance.deltaTime);
p5.instance._lastFrameTime = now;
context._setProperty('frameCount', context.frameCount + 1);
}

let i = 0;
for (const view of this.viewer.pose.views) {
this.viewer.view = view;
Expand All @@ -189,7 +340,7 @@ export default class p5xr {
viewport.x,
viewport.y,
viewport.width * scaleFactor,
viewport.height * scaleFactor
viewport.height * scaleFactor,
);
this.__updateViewport(viewport);

Expand Down Expand Up @@ -219,28 +370,11 @@ export default class p5xr {
* @private
* @ignore
*/
__drawEye(eyeIndex) {
__drawEye() {
const context = window;
const userSetup = context.setup;
const userDraw = context.draw;
const userCalculate = context.calculate;

if (this.isVR) {
if (eyeIndex === 0) {
if (typeof userCalculate === 'function') {
userCalculate();
}
const now = window.performance.now();
p5.instance.deltaTime = now - p5.instance._lastFrameTime;
p5.instance._frameRate = 1000.0 / p5.instance.deltaTime;
p5.instance._setProperty('deltaTime', p5.instance.deltaTime);
p5.instance._lastFrameTime = now;
context._setProperty('frameCount', context.frameCount + 1);
}
} else {
// Scale is much smaller in AR
scale(0.01);
}
// 2D Mode should use graphics object
if (!p5.instance._renderer.isP3D) {
console.error('Sketch does not have 3D Renderer');
Expand Down Expand Up @@ -312,9 +446,9 @@ export default class p5xr {
*/
printUnsupportedMessage() {
console.warn(
'Your browser/hardware does not work with AR Mode currently. This is' +
' undergoing heavy development currently.' +
'You may be able to fix this by enabling WebXR flags in Chrome.'
'Your browser/hardware does not work with AR Mode currently. This is'
+ ' undergoing heavy development currently.'
+ 'You may be able to fix this by enabling WebXR flags in Chrome.',
);
}

Expand Down
7 changes: 4 additions & 3 deletions src/p5xr/core/p5xrButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -422,18 +422,19 @@ class p5xrButton {
* Set button state based on mode support
*/
setAvailable(isAvailable, mode) {
const displayMode = mode.slice(-2).toUpperCase();
if (isAvailable) {
const msg = `Enter ${mode}`;
const msg = `Enter ${displayMode}`;
this.setTitle(msg);
this.setTooltip(msg);
this.enable();
console.log(`${mode} supported`);
this.setDevice(true);
} else if (mode === 'VR') {
} else if (displayMode === 'VR') {
console.log('VR not supported. Falling back to inline mode.');
this.hide();
} else {
const msg = `${mode} not supported`;
const msg = `${displayMode} not supported`;
this.setTitle(msg);
this.setTooltip(msg);
this.disable();
Expand Down
Loading
Loading