Skip to content

Commit a4f3558

Browse files
authored
Merge pull request #257 from dmliao/library-webxr-support
Add WebXR capturing functionality to Spector.js, and WebXR sample
2 parents f424c5d + 296c8ba commit a4f3558

14 files changed

+273
-9
lines changed

package-lock.json

+13
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,17 @@
3434
"test": "echo \"Error: no test specified\" && exit 1"
3535
},
3636
"devDependencies": {
37-
"ts-loader": "^9.3.1",
37+
"@types/webxr": "^0.5.1",
3838
"concat-cli": "^4.0.0",
3939
"css-loader": "^6.7.1",
4040
"exports-loader": "^4.0.0",
4141
"http-server": "^14.1.1",
4242
"livereload": "^0.9.3",
43-
"sass": "^1.53.0",
4443
"npm-run-all": "^4.1.5",
44+
"sass": "^1.53.0",
4545
"sass-loader": "^13.0.2",
4646
"style-loader": "^3.3.1",
47+
"ts-loader": "^9.3.1",
4748
"tslint": "^6.1.3",
4849
"typescript": "^4.7.4",
4950
"webpack": "^5.73.0",

sample/assets/js/injectSpector.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
var spector = new SPECTOR.Spector();
1+
var spectorOptions = {};
2+
if ((window.location + "").indexOf("webxr") !== -1) {
3+
spectorOptions.enableXRCapture = true;
4+
}
5+
6+
var spector = new SPECTOR.Spector(spectorOptions);
27
window.spector = spector;
38
spector.displayUI();
49

sample/js/webxr.js

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
let renderCanvas = document.getElementById('renderCanvas');
2+
3+
let createScene = async function () {
4+
let scene = new BABYLON.Scene(engine);
5+
let camera = new BABYLON.FreeCamera("camera1", new BABYLON.Vector3(0, 5, -10), scene);
6+
camera.setTarget(BABYLON.Vector3.Zero());
7+
camera.attachControl(renderCanvas, true);
8+
let light = new BABYLON.HemisphericLight("light1", new BABYLON.Vector3(0, 1, 0), scene);
9+
light.intensity = 0.7;
10+
let sphere = BABYLON.MeshBuilder.CreateSphere("sphere1", { segments: 16, diameter: 2 }, scene);
11+
sphere.position.y = 1;
12+
13+
const env = scene.createDefaultEnvironment();
14+
15+
// here we add XR support
16+
const xr = await scene.createDefaultXRExperienceAsync({
17+
floorMeshes: [env.ground],
18+
});
19+
20+
// we can't access the Spector UI while in WebXR, so we take a capture whenever the trigger
21+
// is pressed instead.
22+
let previousTriggerPressed = false;
23+
xr.input.onControllerAddedObservable.add((controller) => {
24+
let triggerComponent = controller.gamepadController.components["xr-standard-trigger"];
25+
triggerComponent.onButtonStateChanged.add((stateObject) => {
26+
if (stateObject.pressed && !previousTriggerPressed) {
27+
spector.captureXRContext();
28+
}
29+
previousTriggerPressed = stateObject.pressed;
30+
});
31+
});
32+
33+
return scene;
34+
};
35+
36+
let engine = new BABYLON.Engine(renderCanvas);
37+
createScene(engine, renderCanvas).then((scene) => {
38+
39+
engine.runRenderLoop(function () {
40+
scene.render();
41+
});
42+
43+
});

src/backend/spies/timeSpy.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,18 @@ export class TimeSpy {
6262
this.speedRatio = ratio;
6363
}
6464

65+
public static getRequestAnimationFrameFunctionNames(): string[] {
66+
return [...TimeSpy.requestAnimationFrameFunctions];
67+
}
68+
6569
public addRequestAnimationFrameFunctionName(functionName: string): void {
6670
TimeSpy.requestAnimationFrameFunctions.push(functionName);
6771
}
6872

73+
public getSpiedScope() {
74+
return this.spiedScope;
75+
}
76+
6977
public setSpiedScope(spiedScope: { [name: string]: any }): void {
7078
this.spiedScope = spiedScope;
7179
}
@@ -97,8 +105,7 @@ export class TimeSpy {
97105
});
98106
}
99107
}
100-
101-
private spyRequestAnimationFrame(functionName: string, owner: any): void {
108+
public spyRequestAnimationFrame(functionName: string, owner: any): void {
102109
// Needs both this.
103110
// tslint:disable-next-line
104111
const self = this;

src/backend/spies/xrSpy.ts

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { XRSessionSpector } from "../../polyfill/XRSessionSpector";
2+
import { XRWebGLBindingSpector } from "../../polyfill/XRWebGLBindingSpector";
3+
import { XRWebGLLayerSpector } from "../../polyfill/XRWebGLLayerSpector";
4+
import { OriginFunctionHelper } from "../utils/originFunctionHelper";
5+
import { TimeSpy } from "./timeSpy";
6+
7+
export class XRSpy {
8+
public currentXRSession: XRSessionSpector | undefined;
9+
private timeSpy: TimeSpy;
10+
constructor(timeSpy: TimeSpy) {
11+
this.timeSpy = timeSpy;
12+
this.init();
13+
}
14+
15+
public spyXRSession(session: XRSessionSpector) {
16+
if (this.currentXRSession) {
17+
this.unspyXRSession();
18+
}
19+
for (const Spy of TimeSpy.getRequestAnimationFrameFunctionNames()) {
20+
OriginFunctionHelper.resetOriginFunction(this.timeSpy.getSpiedScope(), Spy);
21+
}
22+
this.timeSpy.spyRequestAnimationFrame("requestAnimationFrame", session);
23+
this.currentXRSession = session;
24+
}
25+
26+
public unspyXRSession() {
27+
if (!this.currentXRSession) {
28+
return;
29+
}
30+
31+
OriginFunctionHelper.resetOriginFunction(this.currentXRSession, "requestAnimationFrame");
32+
this.currentXRSession = undefined;
33+
// listen to the regular frames again.
34+
for (const Spy of TimeSpy.getRequestAnimationFrameFunctionNames()) {
35+
this.timeSpy.spyRequestAnimationFrame(Spy, this.timeSpy.getSpiedScope());
36+
}
37+
}
38+
39+
private init(): void {
40+
if (!navigator.xr) {
41+
return;
42+
}
43+
44+
(window as any).XRWebGLLayer = XRWebGLLayerSpector;
45+
(window as any).XRWebGLBinding = XRWebGLBindingSpector;
46+
47+
// polyfill request session so Spector gets access to the session object.
48+
const existingRequestSession = navigator.xr.requestSession;
49+
Object.defineProperty(navigator.xr, "requestSessionInternal", { writable: true });
50+
(navigator.xr as any).requestSessionInternal = existingRequestSession;
51+
52+
const newRequestSession = (
53+
sessionMode: XRSessionMode,
54+
sessionInit?: any
55+
): Promise<XRSession> => {
56+
const modifiedSessionPromise = (mode: XRSessionMode, init?: any): Promise<XRSession> => {
57+
return (navigator.xr as any).requestSessionInternal(mode, init).then((session: XRSession) => {
58+
// listen to the XR Session here! When we do that, we'll stop listening to window.requestAnimationFrame
59+
// and start listening to session.requestAnimationFrame
60+
61+
// Feed the gl context through the session
62+
const spectorSession = session as XRSessionSpector;
63+
spectorSession._updateRenderState = session.updateRenderState;
64+
spectorSession.updateRenderState = async (
65+
renderStateInit?: XRRenderStateInit
66+
): Promise<void> => {
67+
if (renderStateInit.baseLayer) {
68+
const polyfilledBaseLayer =
69+
renderStateInit.baseLayer as XRWebGLLayerSpector;
70+
spectorSession.glContext = polyfilledBaseLayer.getContext();
71+
}
72+
73+
if (renderStateInit.layers) {
74+
for (const layer of renderStateInit.layers) {
75+
const layerAny: any = layer;
76+
if (layerAny.glContext) {
77+
spectorSession.glContext = layerAny.glContext;
78+
}
79+
}
80+
}
81+
return spectorSession._updateRenderState(renderStateInit);
82+
};
83+
84+
this.spyXRSession(spectorSession);
85+
session.addEventListener("end", () => {
86+
this.unspyXRSession();
87+
});
88+
return Promise.resolve(session);
89+
});
90+
};
91+
return modifiedSessionPromise(sessionMode, sessionInit);
92+
};
93+
94+
95+
Object.defineProperty(navigator.xr, "requestSession", { writable: true });
96+
(navigator.xr as any).requestSession = newRequestSession;
97+
}
98+
99+
}

src/backend/states/parameterState.ts

+5
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ export abstract class ParameterState extends BaseState {
6565

6666
for (const parameter of this.parameters[version - 1]) {
6767
const value = this.readParameterFromContext(parameter);
68+
if (value === null || value === undefined) {
69+
const stringValue = this.stringifyParameterValue(value, parameter);
70+
this.currentState[parameter.constant.name] = stringValue;
71+
continue;
72+
}
6873
const tag = WebGlObjects.getWebGlObjectTag(value);
6974
if (tag) {
7075
this.currentState[parameter.constant.name] = tag;

src/backend/utils/originFunctionHelper.ts

+17
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,23 @@ export class OriginFunctionHelper {
1616
object[originFunctionName] = object[functionName];
1717
}
1818

19+
public static resetOriginFunction(object: any, functionName: string): any {
20+
if (!object) {
21+
return;
22+
}
23+
24+
if (!object[functionName]) {
25+
return;
26+
}
27+
const originFunctionName = this.getOriginFunctionName(functionName);
28+
if (!object[originFunctionName]) {
29+
return;
30+
}
31+
32+
object[functionName] = object[originFunctionName];
33+
delete object[originFunctionName];
34+
}
35+
1936
public static storePrototypeOriginFunction(object: any, functionName: string): void {
2037
if (!object) {
2138
return;

src/backend/utils/readPixelsHelper.ts

+6
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ export class ReadPixelsHelper {
3333
// Empty error list.
3434
gl.getError();
3535

36+
// If type is UNSIGNED_NORMALIZED, we passed in a component type that isn't a pixel format type.
37+
// So we have to convert it to a valid pixel format type.
38+
if (type === WebGlConstants.UNSIGNED_NORMALIZED.value) {
39+
type = WebGlConstants.UNSIGNED_BYTE.value;
40+
}
41+
3642
// prepare destination storage.
3743
const size = width * height * 4;
3844
let pixels: ArrayBufferView;

src/polyfill/XRSessionSpector.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export interface XRSessionSpector extends XRSession {
2+
glContext: WebGLRenderingContext | WebGL2RenderingContext;
3+
4+
_updateRenderState: typeof XRSession.prototype.updateRenderState;
5+
}

src/polyfill/XRWebGLBindingSpector.ts

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export class XRWebGLBindingSpector extends XRWebGLBinding {
2+
private glContext: WebGLRenderingContext | WebGL2RenderingContext;
3+
constructor(session: XRSession, context: WebGLRenderingContext) {
4+
super(session, context);
5+
this.glContext = context;
6+
}
7+
8+
public createProjectionLayer(init?: XRProjectionLayerInit): XRProjectionLayer {
9+
const layer = super.createProjectionLayer(init);
10+
(layer as any).glContext = this.glContext;
11+
return layer;
12+
}
13+
}

src/polyfill/XRWebGLLayerSpector.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export class XRWebGLLayerSpector extends XRWebGLLayer {
2+
private glContext: WebGLRenderingContext | WebGL2RenderingContext;
3+
constructor(
4+
session: XRSession,
5+
context: WebGLRenderingContext | WebGL2RenderingContext,
6+
layerInit?: XRWebGLLayerInit
7+
) {
8+
super(session, context, layerInit);
9+
this.glContext = context;
10+
}
11+
12+
public getContext() {
13+
return this.glContext;
14+
}
15+
}

0 commit comments

Comments
 (0)