blob: d5b2350552ab73c538a10787575449e2addefba6 [file] [log] [blame]
// 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.
import {RenderView} from '../core/renderer.js';
import {InputRenderer} from '../nodes/input-renderer.js';
import {StatsViewer} from '../nodes/stats-viewer.js';
import {Node} from '../core/node.js';
import {vec3, quat} from '../math/gl-matrix.js';
export class WebXRView extends RenderView {
constructor(view, pose, layer) {
super(
view ? view.projectionMatrix : null,
view ? view.viewMatrix : null,
(layer && view) ? layer.getViewport(view) : null,
view ? view.eye : 'left'
);
}
}
export class Scene extends Node {
constructor() {
super();
this._timestamp = -1;
this._frameDelta = 0;
this._statsStanding = false;
this._stats = null;
this._statsEnabled = false;
this.enableStats(true); // Ensure the stats are added correctly by default.
this._inputRenderer = null;
this._resetInputEndFrame = true;
this._lastTimestamp = 0;
this._hoverFrame = 0;
this._hoveredNodes = [];
this.clear = true;
}
setRenderer(renderer) {
this._setRenderer(renderer);
}
loseRenderer() {
if (this._renderer) {
this._stats = null;
this._renderer = null;
this._inputRenderer = null;
}
}
get inputRenderer() {
if (!this._inputRenderer) {
this._inputRenderer = new InputRenderer();
this.addNode(this._inputRenderer);
}
return this._inputRenderer;
}
// Helper function that automatically adds the appropriate visual elements for
// all input sources.
updateInputSources(frame, refSpace) {
// FIXME: Check for the existence of the API first. This check should be
// removed once the input API is part of the official spec.
if (!frame.session.getInputSources) {
return;
}
let inputSources = frame.session.getInputSources();
let newHoveredNodes = [];
let lastHoverFrame = this._hoverFrame;
this._hoverFrame++;
for (let inputSource of inputSources) {
let inputPose = frame.getInputPose(inputSource, refSpace);
if (!inputPose) {
continue;
}
// Any time that we have a grip matrix, we'll render a controller.
if (inputPose.gripMatrix) {
this.inputRenderer.addController(inputPose.gripMatrix);
}
if (inputPose.targetRay) {
if (inputSource.targetRayMode == 'tracked-pointer') {
// If we have a pointer matrix and the pointer origin is the users
// hand (as opposed to their head or the screen) use it to render
// a ray coming out of the input device to indicate the pointer
// direction.
this.inputRenderer.addLaserPointer(inputPose.targetRay);
}
// If we have a pointer matrix we can also use it to render a cursor
// for both handheld and gaze-based input sources.
// Check and see if the pointer is pointing at any selectable objects.
let hitResult = this.hitTest(inputPose.targetRay);
if (hitResult) {
// Render a cursor at the intersection point.
this.inputRenderer.addCursor(hitResult.intersection);
if (hitResult.node._hoverFrameId != lastHoverFrame) {
hitResult.node.onHoverStart();
}
hitResult.node._hoverFrameId = this._hoverFrame;
newHoveredNodes.push(hitResult.node);
} else {
// Statically render the cursor 1 meters down the ray since we didn't
// hit anything selectable.
let cursorDistance = 1.0;
let cursorPos = vec3.fromValues(
inputPose.targetRay.origin.x,
inputPose.targetRay.origin.y,
inputPose.targetRay.origin.z
);
vec3.add(cursorPos, cursorPos, [
inputPose.targetRay.direction.x * cursorDistance,
inputPose.targetRay.direction.y * cursorDistance,
inputPose.targetRay.direction.z * cursorDistance,
]);
// let cursorPos = vec3.fromValues(0, 0, -1.0);
// vec3.transformMat4(cursorPos, cursorPos, inputPose.targetRay);
this.inputRenderer.addCursor(cursorPos);
}
}
}
for (let hoverNode of this._hoveredNodes) {
if (hoverNode._hoverFrameId != this._hoverFrame) {
hoverNode.onHoverEnd();
}
}
this._hoveredNodes = newHoveredNodes;
}
handleSelect(inputSource, frame, refSpace) {
let inputPose = frame.getInputPose(inputSource, refSpace);
if (!inputPose) {
return;
}
this.handleSelectPointer(inputPose.targetRay);
}
handleSelectPointer(targetRay) {
if (targetRay) {
// Check and see if the pointer is pointing at any selectable objects.
let hitResult = this.hitTest(targetRay);
if (hitResult) {
// Render a cursor at the intersection point.
hitResult.node.handleSelect();
}
}
}
enableStats(enable) {
if (enable == this._statsEnabled) {
return;
}
this._statsEnabled = enable;
if (enable) {
this._stats = new StatsViewer();
this._stats.selectable = true;
this.addNode(this._stats);
if (this._statsStanding) {
this._stats.translation = [0, 1.4, -0.75];
} else {
this._stats.translation = [0, -0.3, -0.5];
}
this._stats.scale = [0.3, 0.3, 0.3];
quat.fromEuler(this._stats.rotation, -45.0, 0.0, 0.0);
} else if (!enable) {
if (this._stats) {
this.removeNode(this._stats);
this._stats = null;
}
}
}
standingStats(enable) {
this._statsStanding = enable;
if (this._stats) {
if (this._statsStanding) {
this._stats.translation = [0, 1.4, -0.75];
} else {
this._stats.translation = [0, -0.3, -0.5];
}
this._stats.scale = [0.3, 0.3, 0.3];
quat.fromEuler(this._stats.rotation, -45.0, 0.0, 0.0);
}
}
draw(projectionMatrix, viewMatrix, eye) {
let view = new RenderView();
view.projectionMatrix = projectionMatrix;
view.viewMatrix = viewMatrix;
if (eye) {
view.eye = eye;
}
this.drawViewArray([view]);
}
/** Draws the scene into the base layer of the XRFrame's session */
drawXRFrame(xrFrame, pose) {
if (!this._renderer || !pose) {
return;
}
let gl = this._renderer.gl;
let session = xrFrame.session;
// Assumed to be a XRWebGLLayer for now.
let layer = session.renderState.baseLayer;
if (!gl) {
return;
}
gl.bindFramebuffer(gl.FRAMEBUFFER, layer.framebuffer);
if (this.clear) {
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
}
let views = [];
for (let view of pose.views) {
views.push(new WebXRView(view, pose, layer));
}
this.drawViewArray(views);
}
drawViewArray(views) {
// Don't draw when we don't have a valid context
if (!this._renderer) {
return;
}
this._renderer.drawViews(views, this);
}
startFrame() {
let prevTimestamp = this._timestamp;
this._timestamp = performance.now();
if (this._stats) {
this._stats.begin();
}
if (prevTimestamp >= 0) {
this._frameDelta = this._timestamp - prevTimestamp;
} else {
this._frameDelta = 0;
}
this._update(this._timestamp, this._frameDelta);
return this._frameDelta;
}
endFrame() {
if (this._inputRenderer && this._resetInputEndFrame) {
this._inputRenderer.reset();
}
if (this._stats) {
this._stats.end();
}
}
// Override to load scene resources on construction or context restore.
onLoadScene(renderer) {
return Promise.resolve();
}
}