blob: beb6e69f79e03aa09598a358d7b578e85b6a984f [file] [edit]
// 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 {Material, RENDER_ORDER} from '../core/material.js';
import {Node} from '../core/node.js';
import {Primitive, PrimitiveAttribute} from '../core/primitive.js';
import {DataTexture} from '../core/texture.js';
import {Gltf2Node} from '../nodes/gltf2.js';
// This library matches XRInputSource profiles to available controller models for us.
import { fetchProfile } from 'https://cdn.jsdelivr.net/npm/@webxr-input-profiles/motion-controllers@1.0/dist/motion-controllers.module.js';
// The path of the CDN the sample will fetch controller models from.
const DEFAULT_PROFILES_PATH = 'https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets@1.0/dist/profiles';
const GL = WebGLRenderingContext; // For enums
// Laser texture data, 48x1 RGBA (not premultiplied alpha). This represents a
// "cross section" of the laser beam with a bright core and a feathered edge.
// Borrowed from Chromium source code.
const LASER_TEXTURE_DATA = new Uint8Array([
0xff, 0xff, 0xff, 0x01, 0xff, 0xff, 0xff, 0x02, 0xbf, 0xbf, 0xbf, 0x04, 0xcc, 0xcc, 0xcc, 0x05,
0xdb, 0xdb, 0xdb, 0x07, 0xcc, 0xcc, 0xcc, 0x0a, 0xd8, 0xd8, 0xd8, 0x0d, 0xd2, 0xd2, 0xd2, 0x11,
0xce, 0xce, 0xce, 0x15, 0xce, 0xce, 0xce, 0x1a, 0xce, 0xce, 0xce, 0x1f, 0xcd, 0xcd, 0xcd, 0x24,
0xc8, 0xc8, 0xc8, 0x2a, 0xc9, 0xc9, 0xc9, 0x2f, 0xc9, 0xc9, 0xc9, 0x34, 0xc9, 0xc9, 0xc9, 0x39,
0xc9, 0xc9, 0xc9, 0x3d, 0xc8, 0xc8, 0xc8, 0x41, 0xcb, 0xcb, 0xcb, 0x44, 0xee, 0xee, 0xee, 0x87,
0xfa, 0xfa, 0xfa, 0xc8, 0xf9, 0xf9, 0xf9, 0xc9, 0xf9, 0xf9, 0xf9, 0xc9, 0xfa, 0xfa, 0xfa, 0xc9,
0xfa, 0xfa, 0xfa, 0xc9, 0xf9, 0xf9, 0xf9, 0xc9, 0xf9, 0xf9, 0xf9, 0xc9, 0xfa, 0xfa, 0xfa, 0xc8,
0xee, 0xee, 0xee, 0x87, 0xcb, 0xcb, 0xcb, 0x44, 0xc8, 0xc8, 0xc8, 0x41, 0xc9, 0xc9, 0xc9, 0x3d,
0xc9, 0xc9, 0xc9, 0x39, 0xc9, 0xc9, 0xc9, 0x34, 0xc9, 0xc9, 0xc9, 0x2f, 0xc8, 0xc8, 0xc8, 0x2a,
0xcd, 0xcd, 0xcd, 0x24, 0xce, 0xce, 0xce, 0x1f, 0xce, 0xce, 0xce, 0x1a, 0xce, 0xce, 0xce, 0x15,
0xd2, 0xd2, 0xd2, 0x11, 0xd8, 0xd8, 0xd8, 0x0d, 0xcc, 0xcc, 0xcc, 0x0a, 0xdb, 0xdb, 0xdb, 0x07,
0xcc, 0xcc, 0xcc, 0x05, 0xbf, 0xbf, 0xbf, 0x04, 0xff, 0xff, 0xff, 0x02, 0xff, 0xff, 0xff, 0x01,
]);
const LASER_LENGTH = 1.0;
const LASER_DIAMETER = 0.01;
const LASER_FADE_END = 0.535;
const LASER_FADE_POINT = 0.5335;
const LASER_DEFAULT_COLOR = [1.0, 1.0, 1.0, 0.25];
const CURSOR_RADIUS = 0.004;
const CURSOR_SHADOW_RADIUS = 0.007;
const CURSOR_SHADOW_INNER_LUMINANCE = 0.5;
const CURSOR_SHADOW_OUTER_LUMINANCE = 0.0;
const CURSOR_SHADOW_INNER_OPACITY = 0.75;
const CURSOR_SHADOW_OUTER_OPACITY = 0.0;
const CURSOR_OPACITY = 0.9;
const CURSOR_SEGMENTS = 16;
const CURSOR_DEFAULT_COLOR = [1.0, 1.0, 1.0, 1.0];
const CURSOR_DEFAULT_HIDDEN_COLOR = [0.5, 0.5, 0.5, 0.25];
const DEFAULT_RESET_OPTIONS = {
controllers: true,
lasers: true,
cursors: true,
};
class LaserMaterial extends Material {
constructor() {
super();
this.renderOrder = RENDER_ORDER.ADDITIVE;
this.state.cullFace = false;
this.state.blend = true;
this.state.blendFuncSrc = GL.ONE;
this.state.blendFuncDst = GL.ONE;
this.state.depthMask = false;
this.laser = this.defineSampler('diffuse');
this.laser.texture = new DataTexture(LASER_TEXTURE_DATA, 48, 1);
this.laserColor = this.defineUniform('laserColor', LASER_DEFAULT_COLOR);
}
get materialName() {
return 'INPUT_LASER';
}
get vertexSource() {
return `
attribute vec3 POSITION;
attribute vec2 TEXCOORD_0;
varying vec2 vTexCoord;
vec4 vertex_main(mat4 proj, mat4 view, mat4 model) {
vTexCoord = TEXCOORD_0;
return proj * view * model * vec4(POSITION, 1.0);
}`;
}
get fragmentSource() {
return `
precision mediump float;
uniform vec4 laserColor;
uniform sampler2D diffuse;
varying vec2 vTexCoord;
const float fadePoint = ${LASER_FADE_POINT};
const float fadeEnd = ${LASER_FADE_END};
vec4 fragment_main() {
vec2 uv = vTexCoord;
float front_fade_factor = 1.0 - clamp(1.0 - (uv.y - fadePoint) / (1.0 - fadePoint), 0.0, 1.0);
float back_fade_factor = clamp((uv.y - fadePoint) / (fadeEnd - fadePoint), 0.0, 1.0);
vec4 color = laserColor * texture2D(diffuse, vTexCoord);
float opacity = color.a * front_fade_factor * back_fade_factor;
return vec4(color.rgb * opacity, opacity);
}`;
}
}
const CURSOR_VERTEX_SHADER = `
attribute vec4 POSITION;
varying float vLuminance;
varying float vOpacity;
vec4 vertex_main(mat4 proj, mat4 view, mat4 model) {
vLuminance = POSITION.z;
vOpacity = POSITION.w;
// Billboarded, constant size vertex transform.
vec4 screenPos = proj * view * model * vec4(0.0, 0.0, 0.0, 1.0);
screenPos /= screenPos.w;
screenPos.xy += POSITION.xy;
return screenPos;
}`;
const CURSOR_FRAGMENT_SHADER = `
precision mediump float;
uniform vec4 cursorColor;
varying float vLuminance;
varying float vOpacity;
vec4 fragment_main() {
vec3 color = cursorColor.rgb * vLuminance;
float opacity = cursorColor.a * vOpacity;
return vec4(color * opacity, opacity);
}`;
// Cursors are drawn as billboards that always face the camera and are rendered
// as a fixed size no matter how far away they are.
class CursorMaterial extends Material {
constructor() {
super();
this.renderOrder = RENDER_ORDER.ADDITIVE;
this.state.cullFace = false;
this.state.blend = true;
this.state.blendFuncSrc = GL.ONE;
this.state.depthMask = false;
this.cursorColor = this.defineUniform('cursorColor', CURSOR_DEFAULT_COLOR);
}
get materialName() {
return 'INPUT_CURSOR';
}
get vertexSource() {
return CURSOR_VERTEX_SHADER;
}
get fragmentSource() {
return CURSOR_FRAGMENT_SHADER;
}
}
class CursorHiddenMaterial extends Material {
constructor() {
super();
this.renderOrder = RENDER_ORDER.ADDITIVE;
this.state.cullFace = false;
this.state.blend = true;
this.state.blendFuncSrc = GL.ONE;
this.state.depthFunc = GL.GEQUAL;
this.state.depthMask = false;
this.cursorColor = this.defineUniform('cursorColor', CURSOR_DEFAULT_HIDDEN_COLOR);
}
// TODO: Rename to "program_name"
get materialName() {
return 'INPUT_CURSOR_2';
}
get vertexSource() {
return CURSOR_VERTEX_SHADER;
}
get fragmentSource() {
return CURSOR_FRAGMENT_SHADER;
}
}
export class InputRenderer extends Node {
constructor() {
super();
this._maxInputElements = 32;
this._controllers = null;
this._lasers = null;
this._cursors = null;
this._activeControllers = 0;
this._activeLasers = 0;
this._activeCursors = 0;
this._blurred = false;
}
onRendererChanged(renderer) {
this._controllers = null;
this._lasers = null;
this._cursors = null;
this._activeControllers = 0;
this._activeLasers = 0;
this._activeCursors = 0;
}
useProfileControllerMeshes(session) {
// As input sources are connected if they are tracked-pointer devices
// look up which meshes should be associated with their profile and
// load as the controller model for that hand.
session.addEventListener('inputsourceschange', (event) => {
for (let inputSource of event.added) {
if (inputSource.targetRayMode == 'tracked-pointer') {
fetchProfile(inputSource, DEFAULT_PROFILES_PATH).then(({profile, assetPath}) => {
this.setControllerMesh(new Gltf2Node({url: assetPath}), inputSource.handedness, inputSource.profiles[0]);
});
}
}
});
session.addEventListener('visibilitychange', (event) => {
// remove hand controller while blurred
if (event.session.visibilityState === 'visible-blurred') {
this._blurred = true;
} else if (event.session.visibilityState === 'visible') {
this._blurred = false;
}
});
}
setControllerMesh(controllerNode, handedness = 'right', profile = '') {
if (!this._controllers) {
this._controllers = {};
}
this._controllers[profile + "_" + handedness] = { nodes: [controllerNode], activeCount: 0 };
controllerNode.visible = false;
// FIXME: Temporary fix to initialize for cloning.
this.addNode(controllerNode);
}
addController(gripMatrix, handedness = 'right', profile = '') {
if (!this._controllers || this._blurred) { return; }
let controller = this._controllers[profile + "_" + handedness];
if (!controller) { return; }
let controllerNode = null;
if (controller.activeCount < controller.nodes.length) {
controllerNode = controller.nodes[controller.activeCount];
} else {
controllerNode = controller.nodes[0].clone();
this.addNode(controllerNode);
controller.nodes.push(controllerNode);
}
controller.activeCount = (controller.activeCount + 1) % this._maxInputElements;
controllerNode.matrix = gripMatrix;
controllerNode.visible = true;
}
hideController(handedness = 'right', profile = '') {
if (!this._controllers) { return; }
let controller = this._controllers[profile + "_" + handedness];
if (!controller) { return; }
controllerNode.visible = true;
}
addLaserPointer(rigidTransform) {
if (this._blurred) { return; }
// Create the laser pointer mesh if needed.
if (!this._lasers && this._renderer) {
this._lasers = [this._createLaserMesh()];
this.addNode(this._lasers[0]);
}
let laser = null;
if (this._activeLasers < this._lasers.length) {
laser = this._lasers[this._activeLasers];
} else {
laser = this._lasers[0].clone();
this.addNode(laser);
this._lasers.push(laser);
}
this._activeLasers = (this._activeLasers + 1) % this._maxInputElements;
laser.matrix = rigidTransform.matrix;
laser.visible = true;
}
addCursor(cursorPos) {
if (this._blurred) { return; }
// Create the cursor mesh if needed.
if (!this._cursors && this._renderer) {
this._cursors = [this._createCursorMesh()];
this.addNode(this._cursors[0]);
}
let cursor = null;
if (this._activeCursors < this._cursors.length) {
cursor = this._cursors[this._activeCursors];
} else {
cursor = this._cursors[0].clone();
this.addNode(cursor);
this._cursors.push(cursor);
}
this._activeCursors = (this._activeCursors + 1) % this._maxInputElements;
cursor.translation = cursorPos;
cursor.visible = true;
}
reset(options) {
if (!options) {
options = DEFAULT_RESET_OPTIONS;
}
if (this._controllers && options.controllers) {
for (let handedness in this._controllers) {
let controller = this._controllers[handedness];
controller.activeCount = 0;
for (let controllerNode of controller.nodes) {
controllerNode.visible = false;
}
}
}
if (this._lasers && options.lasers) {
for (let laser of this._lasers) {
laser.visible = false;
}
this._activeLasers = 0;
}
if (this._cursors && options.cursors) {
for (let cursor of this._cursors) {
cursor.visible = false;
}
this._activeCursors = 0;
}
}
_createLaserMesh() {
let gl = this._renderer._gl;
let lr = LASER_DIAMETER * 0.5;
let ll = LASER_LENGTH;
// Laser is rendered as cross-shaped beam
let laserVerts = [
// X Y Z U V
0.0, lr, 0.0, 0.0, 1.0,
0.0, lr, -ll, 0.0, 0.0,
0.0, -lr, 0.0, 1.0, 1.0,
0.0, -lr, -ll, 1.0, 0.0,
lr, 0.0, 0.0, 0.0, 1.0,
lr, 0.0, -ll, 0.0, 0.0,
-lr, 0.0, 0.0, 1.0, 1.0,
-lr, 0.0, -ll, 1.0, 0.0,
0.0, -lr, 0.0, 0.0, 1.0,
0.0, -lr, -ll, 0.0, 0.0,
0.0, lr, 0.0, 1.0, 1.0,
0.0, lr, -ll, 1.0, 0.0,
-lr, 0.0, 0.0, 0.0, 1.0,
-lr, 0.0, -ll, 0.0, 0.0,
lr, 0.0, 0.0, 1.0, 1.0,
lr, 0.0, -ll, 1.0, 0.0,
];
let laserIndices = [
0, 1, 2, 1, 3, 2,
4, 5, 6, 5, 7, 6,
8, 9, 10, 9, 11, 10,
12, 13, 14, 13, 15, 14,
];
let laserVertexBuffer = this._renderer.createRenderBuffer(gl.ARRAY_BUFFER, new Float32Array(laserVerts));
let laserIndexBuffer = this._renderer.createRenderBuffer(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(laserIndices));
let laserIndexCount = laserIndices.length;
let laserAttribs = [
new PrimitiveAttribute('POSITION', laserVertexBuffer, 3, gl.FLOAT, 20, 0),
new PrimitiveAttribute('TEXCOORD_0', laserVertexBuffer, 2, gl.FLOAT, 20, 12),
];
let laserPrimitive = new Primitive(laserAttribs, laserIndexCount);
laserPrimitive.setIndexBuffer(laserIndexBuffer);
let laserMaterial = new LaserMaterial();
let laserRenderPrimitive = this._renderer.createRenderPrimitive(laserPrimitive, laserMaterial);
let meshNode = new Node();
meshNode.addRenderPrimitive(laserRenderPrimitive);
return meshNode;
}
_createCursorMesh() {
let gl = this._renderer._gl;
// Cursor is a circular white dot with a dark "shadow" skirt around the edge
// that fades from black to transparent as it moves out from the center.
// Cursor verts are packed as [X, Y, Luminance, Opacity]
let cursorVerts = [];
let cursorIndices = [];
let segRad = (2.0 * Math.PI) / CURSOR_SEGMENTS;
// Cursor center
for (let i = 0; i < CURSOR_SEGMENTS; ++i) {
let rad = i * segRad;
let x = Math.cos(rad);
let y = Math.sin(rad);
cursorVerts.push(x * CURSOR_RADIUS, y * CURSOR_RADIUS, 1.0, CURSOR_OPACITY);
if (i > 1) {
cursorIndices.push(0, i-1, i);
}
}
let indexOffset = CURSOR_SEGMENTS;
// Cursor Skirt
for (let i = 0; i < CURSOR_SEGMENTS; ++i) {
let rad = i * segRad;
let x = Math.cos(rad);
let y = Math.sin(rad);
cursorVerts.push(x * CURSOR_RADIUS, y * CURSOR_RADIUS,
CURSOR_SHADOW_INNER_LUMINANCE, CURSOR_SHADOW_INNER_OPACITY);
cursorVerts.push(x * CURSOR_SHADOW_RADIUS, y * CURSOR_SHADOW_RADIUS,
CURSOR_SHADOW_OUTER_LUMINANCE, CURSOR_SHADOW_OUTER_OPACITY);
if (i > 0) {
let idx = indexOffset + (i * 2);
cursorIndices.push(idx-2, idx-1, idx);
cursorIndices.push(idx-1, idx+1, idx);
}
}
let idx = indexOffset + (CURSOR_SEGMENTS * 2);
cursorIndices.push(idx-2, idx-1, indexOffset);
cursorIndices.push(idx-1, indexOffset+1, indexOffset);
let cursorVertexBuffer = this._renderer.createRenderBuffer(gl.ARRAY_BUFFER, new Float32Array(cursorVerts));
let cursorIndexBuffer = this._renderer.createRenderBuffer(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(cursorIndices));
let cursorIndexCount = cursorIndices.length;
let cursorAttribs = [
new PrimitiveAttribute('POSITION', cursorVertexBuffer, 4, gl.FLOAT, 16, 0),
];
let cursorPrimitive = new Primitive(cursorAttribs, cursorIndexCount);
cursorPrimitive.setIndexBuffer(cursorIndexBuffer);
let cursorMaterial = new CursorMaterial();
let cursorHiddenMaterial = new CursorHiddenMaterial();
// Cursor renders two parts: The bright opaque cursor for areas where it's
// not obscured and a more transparent, darker version for areas where it's
// behind another object.
let cursorRenderPrimitive = this._renderer.createRenderPrimitive(cursorPrimitive, cursorMaterial);
let cursorHiddenRenderPrimitive = this._renderer.createRenderPrimitive(cursorPrimitive, cursorHiddenMaterial);
let meshNode = new Node();
meshNode.addRenderPrimitive(cursorRenderPrimitive);
meshNode.addRenderPrimitive(cursorHiddenRenderPrimitive);
return meshNode;
}
}