Commit a033d326 authored by Piotr Bialecki's avatar Piotr Bialecki Committed by Commit Bot

AR sample: implement synchronous hittest in JS using detected planes

This adds one more AR sample page that relies on incubated plane
detection API to implement synchronous hit test purely in JavaScript.

Change-Id: I7d98fb5f656eb4068facf83b4de8ac9a7b49f334
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1597276Reviewed-by: default avatarBrandon Jones <bajones@chromium.org>
Commit-Queue: Piotr Bialecki <bialpio@chromium.org>
Cr-Commit-Position: refs/heads/master@{#658283}
parent d4821d12
...@@ -329,9 +329,9 @@ export class Node { ...@@ -329,9 +329,9 @@ export class Node {
return; return;
} }
let index = this._renderPrimitives._instances.indexOf(primitive); let index = this._renderPrimitives.indexOf(primitive);
if (index > -1) { if (index > -1) {
this._renderPrimitives._instances.splice(index, 1); this._renderPrimitives.splice(index, 1);
index = primitive._instances.indexOf(this); index = primitive._instances.indexOf(this);
if (index > -1) { if (index > -1) {
......
...@@ -585,6 +585,13 @@ export class Renderer { ...@@ -585,6 +585,13 @@ export class Renderer {
return renderPrimitive; return renderPrimitive;
} }
removeRenderPrimitive(renderPrimitive) {
let index = this._renderPrimitives[renderPrimitive._material._renderOrder].indexOf(renderPrimitive);
if(index > -1) {
this._renderPrimitives[renderPrimitive._material._renderOrder].splice(index, 1);
}
}
createMesh(primitive, material) { createMesh(primitive, material) {
let meshNode = new Node(); let meshNode = new Node();
meshNode.addRenderPrimitive(this.createRenderPrimitive(primitive, material)); meshNode.addRenderPrimitive(this.createRenderPrimitive(primitive, material));
......
...@@ -25,17 +25,20 @@ import {PrimitiveStream} from '../geometry/primitive-stream.js'; ...@@ -25,17 +25,20 @@ import {PrimitiveStream} from '../geometry/primitive-stream.js';
const GL = WebGLRenderingContext; // For enums const GL = WebGLRenderingContext; // For enums
const SHADOW_SEGMENTS = 32; const SHADOW_SEGMENTS = 32;
const SHADOW_GROUND_OFFSET = 0.01;
const SHADOW_CENTER_ALPHA = 0.7; const DEFAULT_SHADOW_GROUND_OFFSET = 0.01;
const SHADOW_INNER_ALPHA = 0.3; const DEFAULT_SHADOW_INNER_ALPHA = 0.3;
const SHADOW_OUTER_ALPHA = 0.0; const DEFAULT_SHADOW_CENTER_ALPHA = 0.7;
const SHADOW_INNER_RADIUS = 0.6; const DEFAULT_SHADOW_OUTER_ALPHA = 0.0;
const SHADOW_OUTER_RADIUS = 1.0; const DEFAULT_SHADOW_INNER_RADIUS = 0.6;
const DEFAULT_SHADOW_OUTER_RADIUS = 1.0;
class DropShadowMaterial extends Material { class DropShadowMaterial extends Material {
constructor() { constructor(options = {}) {
super(); super();
this.baseColor = this.defineUniform('baseColor', options.baseColor);
this.state.blend = true; this.state.blend = true;
this.state.blendFuncSrc = GL.ONE; this.state.blendFuncSrc = GL.ONE;
this.state.blendFuncDst = GL.ONE_MINUS_SRC_ALPHA; this.state.blendFuncDst = GL.ONE_MINUS_SRC_ALPHA;
...@@ -64,15 +67,29 @@ class DropShadowMaterial extends Material { ...@@ -64,15 +67,29 @@ class DropShadowMaterial extends Material {
return ` return `
varying float vShadow; varying float vShadow;
uniform vec3 baseColor;
vec4 fragment_main() { vec4 fragment_main() {
return vec4(0.0, 0.0, 0.0, vShadow); return vec4(baseColor, vShadow);
}`; }`;
} }
} }
export class DropShadowNode extends Node { export class DropShadowNode extends Node {
constructor(iconTexture, callback) { constructor(options = {}) {
super(); super();
if(!options.baseColor)
options.baseColor = [0,0,0];
this.material = new DropShadowMaterial(options);
this.SHADOW_INNER_RADIUS = options.shadow_inner_radius || DEFAULT_SHADOW_INNER_RADIUS;
this.SHADOW_OUTER_RADIUS = options.shadow_outer_radius || DEFAULT_SHADOW_OUTER_RADIUS;
this.SHADOW_GROUND_OFFSET = options.shadow_ground_offset || DEFAULT_SHADOW_GROUND_OFFSET;
this.SHADOW_INNER_ALPHA = options.shadow_inner_alpha || DEFAULT_SHADOW_INNER_ALPHA;
this.SHADOW_CENTER_ALPHA = options.shadow_center_alpha || DEFAULT_SHADOW_CENTER_ALPHA;
this.SHADOW_OUTER_ALPHA = options.shadow_outer_alpha || DEFAULT_SHADOW_OUTER_ALPHA;
} }
onRendererChanged(renderer) { onRendererChanged(renderer) {
...@@ -81,7 +98,7 @@ export class DropShadowNode extends Node { ...@@ -81,7 +98,7 @@ export class DropShadowNode extends Node {
stream.startGeometry(); stream.startGeometry();
// Shadow center // Shadow center
stream.pushVertex(0, SHADOW_GROUND_OFFSET, 0, SHADOW_CENTER_ALPHA); stream.pushVertex(0, this.SHADOW_GROUND_OFFSET, 0, this.SHADOW_CENTER_ALPHA);
let segRad = ((Math.PI * 2.0) / SHADOW_SEGMENTS); let segRad = ((Math.PI * 2.0) / SHADOW_SEGMENTS);
...@@ -92,8 +109,18 @@ export class DropShadowNode extends Node { ...@@ -92,8 +109,18 @@ export class DropShadowNode extends Node {
let rad = i * segRad; let rad = i * segRad;
let x = Math.cos(rad); let x = Math.cos(rad);
let y = Math.sin(rad); let y = Math.sin(rad);
stream.pushVertex(x * SHADOW_INNER_RADIUS, SHADOW_GROUND_OFFSET, y * SHADOW_INNER_RADIUS, SHADOW_INNER_ALPHA);
stream.pushVertex(x * SHADOW_OUTER_RADIUS, SHADOW_GROUND_OFFSET, y * SHADOW_OUTER_RADIUS, SHADOW_OUTER_ALPHA); stream.pushVertex(
x * this.SHADOW_INNER_RADIUS,
this.SHADOW_GROUND_OFFSET,
y * this.SHADOW_INNER_RADIUS,
this.SHADOW_INNER_ALPHA);
stream.pushVertex(
x * this.SHADOW_OUTER_RADIUS,
this.SHADOW_GROUND_OFFSET,
y * this.SHADOW_OUTER_RADIUS,
this.SHADOW_OUTER_ALPHA);
if (i > 0) { if (i > 0) {
// Inner circle // Inner circle
...@@ -113,7 +140,7 @@ export class DropShadowNode extends Node { ...@@ -113,7 +140,7 @@ export class DropShadowNode extends Node {
stream.endGeometry(); stream.endGeometry();
let shadowPrimitive = stream.finishPrimitive(renderer); let shadowPrimitive = stream.finishPrimitive(renderer);
this._shadowRenderPrimitive = renderer.createRenderPrimitive(shadowPrimitive, new DropShadowMaterial()); this._shadowRenderPrimitive = renderer.createRenderPrimitive(shadowPrimitive, this.material);
this.addRenderPrimitive(this._shadowRenderPrimitive); this.addRenderPrimitive(this._shadowRenderPrimitive);
} }
} }
// 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 {GeometryBuilderBase} from '../geometry/primitive-stream.js';
const GL = WebGLRenderingContext; // For enums
class PlaneMaterial extends Material {
constructor(options = {}) {
super();
this.baseColor = this.defineUniform('baseColor', options.baseColor);
this.renderOrder = RENDER_ORDER.TRANSPARENT;
this.state.blend = true;
this.state.blendFuncSrc = GL.ONE;
this.state.blendFuncDst = GL.ONE_MINUS_SRC_ALPHA;
this.state.depthFunc = GL.LEQUAL;
this.state.depthMask = false;
this.state.cullFace = false;
}
get materialName() {
return 'PLANE';
}
get vertexSource() {
return `
attribute vec3 POSITION;
attribute vec3 NORMAL;
varying vec3 vLight;
const vec3 lightDir = vec3(0.75, 0.5, 1.0);
const vec3 ambientColor = vec3(0.5, 0.5, 0.5);
const vec3 lightColor = vec3(0.75, 0.75, 0.75);
vec4 vertex_main(mat4 proj, mat4 view, mat4 model) {
vec3 normalRotated = vec3(model * vec4(NORMAL, 0.0));
float lightFactor = max(dot(normalize(lightDir), normalRotated), 0.0);
vLight = ambientColor + (lightColor * lightFactor);
return proj * view * model * vec4(POSITION, 1.0);
}`;
}
get fragmentSource() {
return `
precision mediump float;
uniform vec4 baseColor;
varying vec3 vLight;
vec4 fragment_main() {
return vec4(vLight, 1.0) * baseColor;
}`;
}
}
export class PlaneNode extends Node {
constructor(options = {}) {
super();
if(!options.polygon)
throw new Error(`Plane polygon must be specified.`);
if(!options.baseColor)
throw new Error(`Plane base color must be specified.`);
this.baseColor = options.baseColor;
this.polygon = options.polygon;
this._material = new PlaneMaterial({baseColor : options.baseColor});
this._renderer = null;
}
createPlanePrimitive(polygon) {
// TODO: create new builder class for planes
let planeBuilder = new GeometryBuilderBase();
planeBuilder.primitiveStream.startGeometry();
let numVertices = polygon.length;
let firstVertex = planeBuilder.primitiveStream.nextVertexIndex;
polygon.forEach(vertex => {
planeBuilder.primitiveStream.pushVertex(vertex.x, vertex.y, vertex.z);
});
for(let i = 0; i < numVertices - 2; i++) {
planeBuilder.primitiveStream.pushTriangle(firstVertex, firstVertex + i + 1, firstVertex + i + 2);
}
planeBuilder.primitiveStream.endGeometry();
return planeBuilder.finishPrimitive(this._renderer);
}
onRendererChanged(renderer) {
if(!this.polygon)
throw new Error(`Polygon is not set on a plane where it should be!`);
this._renderer = renderer;
this.planeNode = this._renderer.createRenderPrimitive(
this.createPlanePrimitive(this.polygon), this._material);
this.addRenderPrimitive(this.planeNode);
this.polygon = null;
return this.waitForComplete();
}
onPlaneChanged(polygon) {
if(this.polygon)
throw new Error(`Polygon is set on a plane where it shouldn't be!`);
let updatedPrimitive = this.createPlanePrimitive(polygon);
this.planeNode.setPrimitive(updatedPrimitive);
return this.planeNode.waitForComplete();
}
}
// 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} from '../core/material.js';
import {Node} from '../core/node.js';
import {Primitive, PrimitiveAttribute} from '../core/primitive.js';
const GL = WebGLRenderingContext; // For enums
class RayMaterial extends Material {
constructor(options = { baseColor : [1, 0, 0, 1] }) {
super();
this.baseColor = this.defineUniform('baseColor', options.baseColor);
}
get materialName() {
return 'RAY_MATERIAL';
}
get vertexSource() {
return `
attribute vec3 POSITION;
vec4 vertex_main(mat4 proj, mat4 view, mat4 model) {
return proj * view * model * vec4(POSITION, 1.0);
}`;
}
get fragmentSource() {
return `
precision mediump float;
uniform vec4 baseColor;
vec4 fragment_main() {
return baseColor;
}`;
}
}
export class RayNode extends Node {
constructor(options = {}) {
super();
if(typeof options.direction === 'undefined')
this._ray_direction = [0, 0, -5];
else
this._ray_direction = options.direction;
this.material = new RayMaterial(options);
}
onRendererChanged(renderer) {
this.ray_direction = this._ray_direction;
}
get ray_direction() {
return this._ray_direction;
}
set ray_direction(ray_direction) {
if (this._ray_direction) {
this.clearRenderPrimitives();
}
this._ray_direction = ray_direction;
let verts = [];
let indices = [];
verts.push(0, 0, 0);
indices.push(0);
verts.push(ray_direction[0], ray_direction[1], ray_direction[2]);
indices.push(1);
let vertexBuffer = this._renderer.createRenderBuffer(GL.ARRAY_BUFFER, new Float32Array(verts));
let indexBuffer = this._renderer.createRenderBuffer(GL.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices));
let attribs = [
new PrimitiveAttribute('POSITION', vertexBuffer, 3, GL.FLOAT, 12, 0),
];
let primitive = new Primitive(attribs, indices.length, GL.LINES);
primitive.setIndexBuffer(indexBuffer);
let renderPrimitive = this._renderer.createRenderPrimitive(primitive, this.material);
this.addRenderPrimitive(renderPrimitive);
}
}
// 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 {hitTest, filterHitTestResults} from './hit-test.js';
const hittest_json = `
[
{
"polygon": [
{
"x": -0.7949525117874146,
"y": 0,
"z": 0.09085708856582642,
"w": 1
},
{
"x": -0.7388008236885071,
"y": 0,
"z": 0.2163567692041397,
"w": 1
},
{
"x": -0.7257306575775146,
"y": 0,
"z": 0.24235095083713531,
"w": 1
},
{
"x": -0.6986311078071594,
"y": 0,
"z": 0.2608765661716461,
"w": 1
},
{
"x": -0.18674542009830475,
"y": 0,
"z": 0.49622973799705505,
"w": 1
},
{
"x": -0.059895534068346024,
"y": 0,
"z": 0.5397075414657593,
"w": 1
},
{
"x": 0.0341365709900856,
"y": 0,
"z": 0.5527538657188416,
"w": 1
},
{
"x": 0.28332310914993286,
"y": 0,
"z": 0.5463775992393494,
"w": 1
},
{
"x": 0.7734386324882507,
"y": 0,
"z": 0.5031757950782776,
"w": 1
},
{
"x": 0.8223313689231873,
"y": 0,
"z": 0.25600069761276245,
"w": 1
},
{
"x": 0.7012030482292175,
"y": 0,
"z": -0.04883747920393944,
"w": 1
},
{
"x": 0.4847647547721863,
"y": 0,
"z": -0.3673478960990906,
"w": 1
},
{
"x": 0.4113020598888397,
"y": 0,
"z": -0.4683343172073364,
"w": 1
},
{
"x": 0.30164092779159546,
"y": 0,
"z": -0.5527538657188416,
"w": 1
},
{
"x": -0.2830299437046051,
"y": 0,
"z": -0.5527538657188416,
"w": 1
},
{
"x": -0.6636397242546082,
"y": 0,
"z": -0.3377974033355713,
"w": 1
},
{
"x": -0.7396656274795532,
"y": 0,
"z": -0.285207599401474,
"w": 1
},
{
"x": -0.7742070555686951,
"y": 0,
"z": -0.1881212741136551,
"w": 1
},
{
"x": -0.8223313689231873,
"y": 0,
"z": 0.01371713262051344,
"w": 1
},
{
"x": -0.8136139512062073,
"y": 0,
"z": 0.03997987508773804,
"w": 1
},
{
"x": -0.8045119643211365,
"y": 0,
"z": 0.06563511490821838,
"w": 1
}
],
"pose": {
"0": 0.3692772388458252,
"1": 0,
"2": -0.9293193221092224,
"3": 0,
"4": 0,
"5": 1,
"6": 0,
"7": 0,
"8": 0.9293193221092224,
"9": 0,
"10": 0.3692772388458252,
"11": 0,
"12": 0.28132984042167664,
"13": -1.115093469619751,
"14": -1.0961706638336182,
"15": 1
}
}
]`;
const ray_json = `{
"origin": {
"x": 0.03104975074529648,
"y": -0.02061435580253601,
"z": -0.06608150154352188,
"w": 1
},
"direction": {
"x": -0.22517916560173035,
"y": -0.6192044019699097,
"z": -0.7522501945495605,
"w": 0
}
}`;
function make_point(array) {
return { x : array[0], y : array[1], z : array[2], w : array[3] };
}
function run_test(points_array, point, results_callback) {
const polygon = points_array.map(make_point);
const point_on_plane = make_point(point);
const result = filterHitTestResults(
[
{
plane : { polygon : polygon },
point_on_plane : point_on_plane
}
]
);
results_callback(result);
}
function test1() {
console.info("-------------------- running test1 -------------------- ");
let polygon = [
[-1, 0, -1, 1],
[-1, 0, 1, 1],
[ 1, 0, 1, 1],
[ 1, 0, -1, 1],
];
run_test(polygon, [0,0,0,1], result => {
if(result.length != 1) {
console.error("Expected one result!");
debugger;
}
});
run_test(polygon, [2,0,0,1], result => {
if(result.length != 0) {
console.error("Expected no results!");
debugger;
}
});
run_test(polygon, [1.01,0,0,1], result => {
if(result.length != 0) {
console.error("Expected no results!");
debugger;
}
});
console.info("-------------------- test1 finished -------------------- ");
}
class MockedPlane {
constructor(pose, polygon) {
this._pose = pose;
this._polygon = polygon;
}
getPose(frame_of_reference_ignored) {
return { transform : { matrix : this._pose } };
}
get polygon() {
return this._polygon;
}
}
function test2() {
let planes_raw = JSON.parse(hittest_json);
let ray_raw = JSON.parse(ray_json);
let planes = planes_raw.map(plane_raw => new MockedPlane(plane_raw.pose, plane_raw.polygon));
let ray = new XRRay(ray_raw.origin, ray_raw.direction);
const result = hitTest(ray, planes, null);
const result_filtered = filterHitTestResults(result);
if(result_filtered.length != 0) {
console.error("Expected no results!");
debugger;
}
}
export function runUnitTests() {
console.info("-------------------- running unittests -------------------- ");
test1();
//test2();
console.info("-------------------- run finished -------------------- ");
}
// 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.
let neg = function(vector) {
return {x : -vector.x, y : -vector.y, z : -vector.z, w : vector.w};
}
let sub = function(lhs, rhs) {
if(!((lhs.w == 1 && rhs.w == 1) || (lhs.w == 1 && rhs.w == 0) || (lhs.w == 0 && rhs.w == 0)))
console.error("only point - point, point - line or line - line subtraction is allowed");
return {x : lhs.x - rhs.x, y : lhs.y - rhs.y, z : lhs.z - rhs.z, w : lhs.w - rhs.w};
}
let add = function(lhs, rhs) {
if(!((lhs.w == 0 && rhs.w == 1) || (lhs.w == 1 && rhs.w == 0)))
console.error("only line + point or point + line addition is allowed");
return {x : lhs.x + rhs.x, y : lhs.y + rhs.y, z : lhs.z + rhs.z, w : lhs.w + rhs.w};
}
let mul = function(vector, scalar) {
return {x : vector.x * scalar, y : vector.y * scalar, z : vector.z * scalar, w : vector.w};
}
// |matrix| - Float32Array, |input| - point-like dict (must have x, y, z, w)
export function transform_point_by_matrix (matrix, input) {
return {
x : matrix[0] * input.x + matrix[4] * input.y + matrix[8] * input.z + matrix[12] * input.w,
y : matrix[1] * input.x + matrix[5] * input.y + matrix[9] * input.z + matrix[13] * input.w,
z : matrix[2] * input.x + matrix[6] * input.y + matrix[10] * input.z + matrix[14] * input.w,
w : matrix[3] * input.x + matrix[7] * input.y + matrix[11] * input.z + matrix[15] * input.w,
};
}
// |point| - point-like dict (must have x, y, z, w)
let normalize_perspective = function(point) {
if(point.w == 0 || point.w == 1) return point;
return {
x : point.x / point.w,
y : point.y / point.w,
z : point.z / point.w,
w : 1
};
}
let dotProduct = function(lhs, rhs) {
return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z;
}
let crossProduct = function(lhs, rhs) {
return {
x : lhs.y * rhs.z - lhs.z * rhs.y,
y : lhs.z * rhs.x - lhs.x * rhs.z,
z : lhs.x * rhs.y - lhs.y * rhs.x,
w : 0
}
}
let length = function(vector) {
return Math.sqrt(dotProduct(vector, vector));
}
let normalize = function(vector) {
const l = length(vector);
return mul(vector, 1.0/l);
}
let calculateHitMatrix = function(ray_vector, plane_normal, point) {
// projection of ray_vector onto a plane
const ray_vector_projection = sub(ray_vector, mul(plane_normal, dotProduct(ray_vector, plane_normal)));
// new coordinate system axes
const y = plane_normal;
const z = normalize(neg(ray_vector_projection));
const x = normalize(crossProduct(y, z));
let hitMatrix = new Float32Array(16);
hitMatrix[0] = x.x;
hitMatrix[1] = x.y;
hitMatrix[2] = x.z;
hitMatrix[3] = 0;
hitMatrix[4] = y.x;
hitMatrix[5] = y.y;
hitMatrix[6] = y.z;
hitMatrix[7] = 0;
hitMatrix[8] = z.x;
hitMatrix[9] = z.y;
hitMatrix[10] = z.z;
hitMatrix[11] = 0;
hitMatrix[12] = point.x;
hitMatrix[13] = point.y;
hitMatrix[14] = point.z;
hitMatrix[15] = 1;
return hitMatrix;
}
// single plane hit test - doesn't take into account the plane's polygon
function hitTestPlane(ray, plane, frameOfReference) {
const plane_pose = plane.getPose(frameOfReference);
const plane_normal = transform_point_by_matrix(
plane_pose.transform.matrix, {x : 0, y : 1.0, z : 0, w : 0});
const plane_center = normalize_perspective(
transform_point_by_matrix(
plane_pose.transform.matrix, {x : 0, y : 0, z : 0, w : 1.0}));
const ray_origin = ray.origin;
const ray_vector = ray.direction;
const numerator = dotProduct( sub(plane_center, ray_origin), plane_normal);
const denominator = dotProduct(ray_vector, plane_normal);
if(denominator < 0.0001 && denominator > -0.0001) {
// parallel planes
if(numerator < 0.0001 && numerator > -0.0001) {
// contained in the plane
console.debug("Ray contained in the plane", plane);
return { plane : plane };
} else {
// no hit
console.debug("No hit", plane);
return null;
}
} else {
// single point of intersection
const d = numerator / denominator;
if(d < 0) {
// no hit - plane-line intersection exists but not for half-line
console.debug("No hit", d, plane);
return null;
} else {
const point = add(ray_origin, mul(ray_vector, d)); // hit test point coordinates in frameOfReference
let point_on_plane = transform_point_by_matrix(plane_pose.transform.inverse.matrix, point); // hit test point coodinates relative to plane pose
console.assert(Math.abs(point_on_plane.y) < 0.0001, "Incorrect Y coordinate of mapped point");
let hitMatrix = calculateHitMatrix(ray_vector, plane_normal, point);
return {
distance : d,
plane : plane,
ray : ray,
point : point,
point_on_plane : point_on_plane,
hitMatrix : hitMatrix,
pose_matrix : plane_pose.transform.matrix
};
}
}
console.error("Should never reach here");
return null;
}
// multiple planes hit test
export function hitTest(ray, planes, frameOfReference) {
const hit_test_results = planes.map(plane => hitTestPlane(ray, plane, frameOfReference));
// throw away all strange results (no intersection with plane, ray lies on plane)
let hit_test_results_with_points = hit_test_results.filter(
maybe_plane => maybe_plane && typeof maybe_plane.point != "undefined");
// sort results by distance
hit_test_results_with_points.sort((l, r) => l.distance - r.distance);
// throw away the ones that don't fall within polygon bounds (except the bottommost plane)
// convert hittest results to something that the caller expects
return hit_test_results_with_points;
}
function simplifyPolygon(polygon) {
let result = [];
let previous_point = polygon[polygon.length - 1];
for(let i = 0; i < polygon.length; ++i) {
const current_point = polygon[i];
const segment = sub(current_point, previous_point);
if(length(segment) < 0.001) {
continue;
}
result.push(current_point);
previous_point = current_point;
}
return result;
}
export function extendPolygon(polygon) {
return polygon.map(vertex => {
let center_to_vertex_normal = normalize(vertex);
center_to_vertex_normal.w = 0;
const addition = mul(center_to_vertex_normal, 0.2);
return add(vertex, addition);
});
}
// 2d "cross product" of 3d points lying on a 2d plane with Y = 0
let crossProduct2d = function(lhs, rhs) {
return lhs.x * rhs.z - lhs.z * rhs.x;
}
// Filters hit test results to keep only the planes for which the used ray falls
// within their polygon. Optionally, we can keep the last horizontal plane that
// was hit.
export function filterHitTestResults(hitTestResults,
keep_last_plane = false,
simplify_planes = false,
use_enlarged_polygon = false) {
console.assert(!(simplify_planes && use_enlarged_polygon), "Wait that's illegal.")
let result = hitTestResults.filter(hitTestResult => {
let polygon = simplify_planes ? simplifyPolygon(hitTestResult.plane.polygon)
: hitTestResult.plane.polygon;
polygon = use_enlarged_polygon ? hitTestResult.plane.extended_polygon : polygon;
const hit_test_point = hitTestResult.point_on_plane;
// Check if the point is on the same side from all the segments:
// - if yes, then it's in the polygon
// - if no, then it's outside of the polygon
// This works only for convex polygons.
let side = 0; // unknown, 1 = right, 2 = left
let previous_point = polygon[polygon.length - 1];
for(let i = 0; i < polygon.length; ++i) {
const current_point = polygon[i];
const line_segment = sub(current_point, previous_point);
const segment_direction = normalize(line_segment);
const turn_segment = sub(hit_test_point, current_point);
const turn_direction = normalize(turn_segment);
const cosine_ray_segment = crossProduct2d(segment_direction, turn_direction);
if(side == 0) {
if(cosine_ray_segment > 0) {
side = 1;
} else {
side = 2;
}
} else {
if(cosine_ray_segment > 0 && side == 2) return false;
if(cosine_ray_segment < 0 && side == 1) return false;
}
previous_point = current_point;
}
return true;
});
if(keep_last_plane && hitTestResults.length > 0) {
const last_horizontal_plane_result = hitTestResults.slice().reverse().find(
element => {
return element.plane.orientation == "Horizontal";
});
if(last_horizontal_plane_result
&& result.findIndex(element => element === last_horizontal_plane_result) == -1) {
result.push(last_horizontal_plane_result);
}
}
return result;
}
...@@ -109,6 +109,11 @@ ...@@ -109,6 +109,11 @@
{ title: 'AR Hit Test', category: 'Phone AR', { title: 'AR Hit Test', category: 'Phone AR',
path: 'phone-ar-hit-test.html', path: 'phone-ar-hit-test.html',
description: 'Demonstrates using the Hit Test API to place virtual objects on real-world surfaces.' }, description: 'Demonstrates using the Hit Test API to place virtual objects on real-world surfaces.' },
{ title: 'AR Plane Detection & Hit-Test', category: 'Phone AR',
path: 'phone-ar-plane-detection.html',
description: 'Demonstrates using the Plane Detection feature, including implementation of' +
'synchronous hit test in JavaScript leveraging obtained plane data.' },
]; ];
let mainElement = document.getElementById("main"); let mainElement = document.getElementById("main");
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment