| <!doctype html> |
| <!-- |
| Copyright 2018 The Immersive Web Community Group |
| |
| Permission is hereby granted, free of charge, to any person obtaining a copy of |
| this software and associated documentation files (the "Software"), to deal in |
| the Software without restriction, including without limitation the rights to |
| use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of |
| the Software, and to permit persons to whom the Software is furnished to do so, |
| subject to the following conditions: |
| |
| The above copyright notice and this permission notice shall be included in all |
| copies or substantial portions of the Software. |
| |
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS |
| FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR |
| COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER |
| IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
| CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| --> |
| <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'> |
| <link rel='icon' type='image/png' sizes='32x32' href='../favicon-32x32.png'> |
| <link rel='icon' type='image/png' sizes='96x96' href='../favicon-96x96.png'> |
| <link rel='stylesheet' href='../css/common.css'> |
| |
| <title>Cube Sea</title> |
| |
| <script src='../js/third-party/dat.gui.min.js'></script> |
| </head> |
| <body> |
| <header> |
| <details id='details-panel' open> |
| <summary>Cube Sea</summary> |
| <p> |
| All hail the cube sea! It's not very interesting to look at, but it's |
| fantastic for testing! |
| <a class="back" href="./">Back</a> |
| </p> |
| </details> |
| </header> |
| <script type="module"> |
| import {WebXRButton} from '../js/util/webxr-button.js'; |
| import {Scene} from '../js/render/scenes/scene.js'; |
| import {Renderer, createWebGLContext} from '../js/render/core/renderer.js'; |
| import {CubeSeaNode} from '../js/render/nodes/cube-sea.js'; |
| import {Gltf2Node} from '../js/render/nodes/gltf2.js'; |
| import {InlineViewerHelper} from '../js/util/inline-viewer-helper.js'; |
| import {QueryArgs} from '../js/util/query-args.js'; |
| |
| // If requested, use the polyfill to provide support for mobile devices |
| // and devices which only support WebVR. |
| import WebXRPolyfill from '../js/third-party/webxr-polyfill/build/webxr-polyfill.module.js'; |
| if (QueryArgs.getBool('usePolyfill', true)) { |
| let polyfill = new WebXRPolyfill(); |
| } |
| |
| // Helper methods for saving dat.gui-edited parameters in the URL hash. |
| // Only non-default values are saved. |
| var paramTypes = {}; |
| var paramValues = {}; |
| var paramDefaults = {}; |
| function getBoolParam(name, optDefault) { |
| paramTypes[name] = 'bool'; |
| if (!optDefault) optDefault = false; |
| paramDefaults[name] = optDefault; |
| let val = QueryArgs.getBool(name, optDefault); |
| paramValues[name] = val; |
| return val; |
| } |
| function getIntParam(name, optDefault) { |
| paramTypes[name] = 'int'; |
| paramDefaults[name] = optDefault; |
| let val = QueryArgs.getInt(name, optDefault); |
| paramValues[name] = val; |
| return val; |
| } |
| function getFloatParam(name, optDefault) { |
| paramTypes[name] = 'float'; |
| paramDefaults[name] = optDefault; |
| let val = QueryArgs.getFloat(name, optDefault); |
| paramValues[name] = val; |
| return val; |
| } |
| function getStringParam(name, optDefault) { |
| paramTypes[name] = 'string'; |
| paramDefaults[name] = optDefault; |
| let val = QueryArgs.getString(name, optDefault); |
| paramValues[name] = val; |
| return val; |
| } |
| function updateHash() { |
| let pairs = []; |
| for (let rawKey in paramTypes) { |
| let rawVal = paramValues[rawKey]; |
| if (rawVal == paramDefaults[rawKey]) |
| continue; |
| let type = paramTypes[rawKey]; |
| let keyString = encodeURIComponent(rawKey); |
| let valString; |
| if (type == 'bool') { |
| valString = rawVal ? '1' : '0'; |
| } else if (type == 'float') { |
| valString = encodeURIComponent((rawVal+0).toFixed(3)); |
| } else { |
| valString = encodeURIComponent(rawVal); |
| } |
| pairs.push(keyString + '=' + valString); |
| } |
| let newHash = '#' + pairs.join('&'); |
| document.location.hash = newHash; |
| } |
| function getParamSaver(name, optApplyFunc) { |
| if (!(name in paramTypes)) |
| throw new Error('unknown param name: ' + name); |
| return (value) => { |
| paramValues[name] = value; |
| updateHash(); |
| if (optApplyFunc) { |
| optApplyFunc(value); |
| } |
| }; |
| } |
| |
| let cubeSea = new CubeSeaNode({ |
| imageUrl: '../media/textures/cube-sea.png', |
| |
| // Number and size of the static cubes. Use the larger |
| // cube count from heavyGpu to avoid inconsistent defaults. |
| cubeCount: getIntParam('cubeCount', 12), |
| cubeScale: getFloatParam('cubeScale'), |
| |
| // If true, use a very heavyweight shader to stress the GPU. |
| heavyGpu: getBoolParam('heavyGpu'), |
| |
| // Draw only half the world cubes. Helps test variable render cost |
| // when combined with heavyGpu. |
| halfOnly: getBoolParam('halfOnly'), |
| |
| // Automatically spin the world cubes. Intended for automated testing, |
| // not recommended for viewing in a headset. |
| autoRotate: getBoolParam('autorotate'), |
| }); |
| |
| let appSettings = { |
| // Framebuffer scale override. 1 = default |
| framebufferScale: getFloatParam('framebufferScale', 1), |
| |
| // Immersive AR mode |
| arMode: getBoolParam('arMode', false), |
| |
| // Use an inline canvas for "magic window" mode? |
| magicWindow: getBoolParam('magicWindow', true), |
| |
| // Don't produce frames at all. Simulates a broken app. |
| noFrames: getBoolParam('noFrames', false), |
| |
| // Only submit every Nth presentation frame. The first frame is |
| // rendered, so a large value can be used to simulate a splash screen. |
| submitInterval: getIntParam('submitInterval', 1), |
| |
| // Busy wait for the specified time to simulate CPU intensive JS. |
| simulatedWorkTimeMs: getFloatParam('workTime', 0), |
| |
| antialias: getBoolParam('antialias', true), |
| }; |
| |
| function updateCubeSea() { |
| cubeSea.rebuildCubes() |
| } |
| |
| // Add GUI fields to edit URL parameters. The dat.gui name (2nd arg) |
| // is the name of the dict parameter. The getParamSaver name is the |
| // name of the hash parameter which may be different. |
| let gui = new dat.GUI({ autoPlace: false }); |
| var folderScene = gui.addFolder('Scene options'); |
| folderScene.add(cubeSea, 'cubeCount', 4, 14, 1).onFinishChange( |
| getParamSaver('cubeCount', updateCubeSea)); |
| folderScene.add(cubeSea, 'cubeScale', 0.2, 2.0, 0.05).onFinishChange( |
| getParamSaver('cubeScale', updateCubeSea)); |
| folderScene.add(cubeSea, 'halfOnly').onFinishChange( |
| getParamSaver('halfOnly', updateCubeSea)); |
| // heavyGpu currently only gets read on startup. |
| folderScene.add(cubeSea, 'heavyGpu').onFinishChange( |
| getParamSaver('heavyGpu', () => { window.location.reload(false); })); |
| folderScene.add(appSettings, 'simulatedWorkTimeMs', 0, 20, 1).onFinishChange( |
| getParamSaver('workTime')); |
| folderScene.open(); |
| |
| var folderRender = gui.addFolder('Rendering options'); |
| folderRender.add(appSettings, 'framebufferScale', 0, 2, 0.25).onFinishChange( |
| getParamSaver('framebufferScale')); |
| folderRender.add(appSettings, 'antialias').onFinishChange( |
| getParamSaver('antialias')); |
| folderRender.add(appSettings, 'arMode').onFinishChange( |
| getParamSaver('arMode', () => { window.location.reload(false); })); |
| folderRender.add(appSettings, 'magicWindow').onFinishChange( |
| getParamSaver('magicWindow', () => { window.location.reload(false); })); |
| folderRender.open(); |
| |
| var folderTest = gui.addFolder('Regression testing'); |
| folderTest.add(appSettings, 'noFrames').onFinishChange( |
| getParamSaver('noFrames')); |
| folderTest.add(appSettings, 'submitInterval').onFinishChange( |
| getParamSaver('submitInterval')); |
| folderTest.add(cubeSea, 'autoRotate').onFinishChange( |
| getParamSaver('autorotate', updateCubeSea)); |
| // Don't auto-open this folder, it's not very interesting for interactive use. |
| |
| gui.close(); |
| |
| let detailsPanel = document.getElementById('details-panel'); |
| detailsPanel.appendChild(gui.domElement); |
| detailsPanel.appendChild(document.createElement('br')); |
| |
| // TODO: Test variables that still need to be implemented, assuming |
| // they are applicable to WebXR. |
| /* |
| // Use 1024x1024 per eye instead of the recommended base render |
| // resolution. |
| var standardSize = getBoolParam('standardSize', false); |
| |
| // Support for automated latency testing. |
| var latencyPatch = getBoolParam('latencyPatch', false); |
| var pureFlickerApp = getBoolParam('pureFlickerApp', false); |
| */ |
| |
| // XR globals. |
| let xrButton = null; |
| let xrImmersiveRefSpace = null; |
| let inlineViewerHelper = null; |
| |
| // WebGL scene globals. |
| let gl = null; |
| let renderer = null; |
| let scene = new Scene(); |
| scene.addNode(cubeSea); |
| |
| function initXR() { |
| let buttonOptions = { |
| onRequestSession: onRequestSession, |
| onEndSession: onEndSession |
| }; |
| if (appSettings.arMode) { |
| buttonOptions.textEnterXRTitle = "START AR"; |
| buttonOptions.textXRNotFoundTitle = "AR NOT FOUND"; |
| buttonOptions.textExitXRTitle = "EXIT AR"; |
| } |
| xrButton = new WebXRButton(buttonOptions); |
| document.querySelector('header').appendChild(xrButton.domElement); |
| |
| let xrMode = appSettings.arMode ? 'immersive-ar' : 'immersive-vr'; |
| if (navigator.xr) { |
| navigator.xr.isSessionSupported(xrMode).then((supported) => { |
| xrButton.enabled = supported; |
| }); |
| |
| // Set up "magic window" mode. |
| if (appSettings.magicWindow) { |
| navigator.xr.requestSession('inline').then(onSessionStarted); |
| } |
| } |
| } |
| |
| function initGL() { |
| if (gl) |
| return; |
| |
| gl = createWebGLContext({ |
| xrCompatible: true |
| }); |
| if (appSettings.magicWindow) { |
| document.body.appendChild(gl.canvas); |
| |
| function onResize() { |
| gl.canvas.width = gl.canvas.clientWidth * window.devicePixelRatio; |
| gl.canvas.height = gl.canvas.clientHeight * window.devicePixelRatio; |
| } |
| window.addEventListener('resize', onResize); |
| onResize(); |
| } |
| |
| // Set up a non-black clear color so that we can see if something renders wrong. |
| gl.clearColor(0.1, 0.2, 0.3, 1.0); |
| |
| renderer = new Renderer(gl); |
| scene.setRenderer(renderer); |
| } |
| |
| function onRequestSession() { |
| let xrMode = appSettings.arMode ? 'immersive-ar' : 'immersive-vr'; |
| return navigator.xr.requestSession(xrMode).then((session) => { |
| xrButton.setSession(session); |
| session.isImmersive = true; |
| onSessionStarted(session); |
| }); |
| } |
| |
| function onSessionStarted(session) { |
| session.addEventListener('end', onSessionEnded); |
| |
| initGL(); |
| scene.inputRenderer.useProfileControllerMeshes(session); |
| |
| let glLayer = new XRWebGLLayer(session, gl, { |
| framebufferScaleFactor: appSettings.framebufferScale, |
| antialias: appSettings.antialias |
| }); |
| session.updateRenderState({ baseLayer: glLayer }); |
| |
| let refSpaceType = session.isImmersive ? 'local' : 'viewer'; |
| session.requestReferenceSpace(refSpaceType).then((refSpace) => { |
| if (session.isImmersive) { |
| xrImmersiveRefSpace = refSpace; |
| } else { |
| inlineViewerHelper = new InlineViewerHelper(gl.canvas, refSpace); |
| } |
| session.requestAnimationFrame(onXRFrame); |
| }); |
| } |
| |
| function onEndSession(session) { |
| session.end(); |
| } |
| |
| function onSessionEnded(event) { |
| xrButton.setSession(null); |
| } |
| |
| let frameNum = 0; |
| let tmp = 0; |
| |
| function onXRFrame(t, frame) { |
| let session = frame.session; |
| let refSpace = session.isImmersive ? |
| xrImmersiveRefSpace : |
| inlineViewerHelper.referenceSpace; |
| let pose = frame.getViewerPose(refSpace); |
| |
| frameNum++; |
| |
| scene.startFrame(); |
| |
| if (appSettings.simulatedWorkTimeMs > 0) { |
| // Simulate a heavy Javascript workload |
| let start = Date.now(); |
| while (Date.now() - start < appSettings.simulatedWorkTimeMs) { |
| // Spin our wheels doing useless BS :) |
| tmp = Math.sin(Math.cos(Math.tan(tmp))); |
| } |
| } |
| |
| session.requestAnimationFrame(onXRFrame); |
| |
| // Simulate a poorly designed app that doesn't provide frames. |
| if (appSettings.noFrames) { |
| scene.endFrame(); |
| return; |
| } |
| |
| // Simulate an app that's only submitting on a subset of animation |
| // frames. This seems to be legal according to the WebVR spec? |
| // frameNum=0 is submitted, so setting submitInterval to a large |
| // number can simulate a splash screen. |
| if (appSettings.submitInterval && frameNum % appSettings.submitInterval > 0) { |
| return; |
| } |
| |
| scene.clear = !appSettings.arMode; |
| |
| scene.updateInputSources(frame, refSpace); |
| |
| scene.drawXRFrame(frame, pose); |
| |
| scene.endFrame(); |
| } |
| |
| // Start the XR application. |
| initXR(); |
| </script> |
| </body> |
| </html> |