topical media & game development
graphic-o3d-samples-o3djs-picking.js / js
/*
* Copyright 2009, Google Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
@fileoverview This file contains functions for implementing picking.
It puts them in the "picking" module on the o3djs object.
This example shows one way to implement picking. Because O3D is shader
agnostic we can't handle picking automatically since we have no way of
knowing what the developer is going to do with their shaders. On the other
hand, we can provide various functions that make it possible to do your own
picking. Only you know which objects are pickable and which are not. For
example if you are making an RTS game, only you would know that units are
pickable but ground and explosions are not and that neither is your HUD.
Similarly, only you would know how your shaders manipulate the vertices
passed to them.
It's possible that someone, maybe us, will create an engine to use o3d
that given a bunch of restrictions and flags on the data it accepts can
do picking in a more automatic way but that is not the goal of the o3d
api. Its goal is to provide a LOW-LEVEL shader agnostic API.
o3djs.provide('o3djs.picking');
o3djs.require('o3djs.math');
o3djs.require('o3djs.dump');
A Module for picking.
@namespace
o3djs.picking = o3djs.picking || {};
Creates a new PickInfo.
parameter: {!o3djs.picking.ShapeInfo} shapeInfo The ShapeInfo that
was picked.
parameter: {!o3d.RayIntersectionInfo} rayIntersectionInfo Information
about the pick.
parameter: {!o3djs.math.Vector3} worldIntersectionPosition
world position of intersection.
returns: {!o3djs.picking.PickInfo} The new PickInfo.
o3djs.picking.createPickInfo = function(shapeInfo,
rayIntersectionInfo,
worldIntersectionPosition) {
return new o3djs.picking.PickInfo(shapeInfo,
rayIntersectionInfo,
worldIntersectionPosition);
};
Creates a new ShapeInfo.
parameter: {!o3d.Shape} shape Shape to keep info about.
parameter: {!o3d.Transform} parent Parent transform of the shape.
returns: {!o3djs.picking.ShapeInfo} The new ShapeInfo.
o3djs.picking.createShapeInfo = function(shape, parent) {
return new o3djs.picking.ShapeInfo(shape, parent);
};
Creates a new TransformInfo.
parameter: {!o3d.Transform} transform Transform to keep info about.
parameter: {o3d.Transform} parent Parent transform of the transform.
Can be null.
returns: {!o3djs.picking.TransformInfo} The new TransformInfo.
o3djs.picking.createTransformInfo = function(transform, parent) {
return new o3djs.picking.TransformInfo(transform, parent);
};
Convert a pixel position relative to the top left corner of the client area
into the corresponding ray through the frustum in world space.
parameter: {number} clientXPosition x position relative to client area.
parameter: {number} clientYPosition y position relative to client area.
parameter: {!o3djs.math.Matrix4} view View matrix to transform with.
parameter: {!o3djs.math.Matrix4} projection Projection matrix to transform
with.
parameter: {number} clientWidth width of client area.
parameter: {number} clientHeight height of client area.
returns: {Ray} ray in world space.
o3djs.picking.clientPositionToWorldRayEx = function(clientXPosition,
clientYPosition,
view,
projection,
clientWidth,
clientHeight) {
// compute the world position of a ray going through the view frustum
var inverseViewProjectionMatrix = o3djs.math.inverse(
o3djs.math.matrix4.composition(projection, view));
// normScreenX, normScreenY are in frustum coordinates.
var normScreenX = clientXPosition / (clientWidth * 0.5) - 1;
var normScreenY = -(clientYPosition / (clientHeight * 0.5) - 1);
// Apply inverse view-projection matrix to get the ray in world coordinates.
return {
near: o3djs.math.matrix4.transformPoint(
inverseViewProjectionMatrix, [normScreenX, normScreenY, 0]),
far: o3djs.math.matrix4.transformPoint(
inverseViewProjectionMatrix, [normScreenX, normScreenY, 1])
};
};
Convert a pixel position relative to the top left corner of the client area
into the corresponding ray through the frustum in world space.
parameter: {number} clientXPosition x position relative to client area.
parameter: {number} clientYPosition y position relative to client area.
parameter: {!o3d.DrawContext} drawContext DrawContext to get view and
projection matrices from.
parameter: {number} clientWidth width of client area.
parameter: {number} clientHeight height of client area.
returns: {Ray} ray in world space.
o3djs.picking.clientPositionToWorldRay = function(clientXPosition,
clientYPosition,
drawContext,
clientWidth,
clientHeight) {
return o3djs.picking.clientPositionToWorldRayEx(
clientXPosition,
clientYPosition,
drawContext.view,
drawContext.projection,
clientWidth,
clientHeight);
};
A local dump function so we can easily comment it out.
parameter: {string} msg Message to dump.
o3djs.picking.dprint = function(msg) {
//o3djs.dump.dump(msg);
};
A local dump function so we can easily comment it out.
parameter: {string} label Label to print before value.
parameter: {!o3djs.math.Vector3} float3 Value to print.
parameter: {string} prefix optional prefix for indenting.
o3djs.picking.dprintPoint3 = function(label, float3, prefix) {
//o3djs.dump.dumpPoint3(label, float3, prefix);
};
A local dump function so we can easily comment it out.
parameter: {string} label Label to put in front of dump.
parameter: {!o3d.BoundingBox} boundingBox BoundingBox to dump.
parameter: {string} opt_prefix optional prefix for indenting.
o3djs.picking.dprintBoundingBox = function(label,
boundingBox,
opt_prefix) {
//o3djs.dump.dumpBoundingBox(label, boundingBox, opt_prefix);
};
A local dump function so we can easily comment it out.
parameter: {string} label Label to print before value.
parameter: {!o3d.RayIntersectionInfo} rayIntersectionInfo Value to print.
o3djs.picking.dumpRayIntersectionInfo = function(label,
rayIntersectionInfo) {
o3djs.picking.dprint(label + ' : valid = ' +
rayIntersectionInfo.valid + ' : intersected = ' +
rayIntersectionInfo.intersected);
if (rayIntersectionInfo.intersected) {
o3djs.picking.dprint(
' : pos: ' +
rayIntersectionInfo.position[0] + ', ' +
rayIntersectionInfo.position[1] + ', ' +
rayIntersectionInfo.position[2] + ', ');
}
o3djs.picking.dprint('\n');
};
Creates a new PickInfo. Used to return picking information.
@constructor
parameter: {!o3djs.picking.ShapeInfo} shapeInfo The ShapeInfo that
was picked.
parameter: {!o3d.RayIntersectionInfo} rayIntersectionInfo Information
about the pick.
parameter: {!o3djs.math.Vector3} worldIntersectionPosition world position of
intersection.
o3djs.picking.PickInfo = function(shapeInfo,
rayIntersectionInfo,
worldIntersectionPosition) {
The ShapeInfo that was picked.
@type {!o3djs.picking.ShapeInfo}
this.shapeInfo = shapeInfo;
Information about the pick.
@type {!o3d.RayInterstionInfo}
this.rayIntersectionInfo = rayIntersectionInfo;
The worldIntersectionPosition world position of intersection.
@type {!o3djs.math.Vector3}
this.worldIntersectionPosition = worldIntersectionPosition
};
Creates a new ShapeInfo. Used to store information about Shapes.
@constructor
parameter: {!o3d.Shape} shape Shape to keep info about.
parameter: {!o3d.Transform} parent Parent transform of the shape.
o3djs.picking.ShapeInfo = function(shape, parent) {
The Shape for this ShapeInfo
@type {!o3d.Shape}
this.shape = shape;
The parent Transform of this Shape.
@type {!o3d.Transform}
this.parent = parent;
The bounding box for this Shape
@type {o3d.BoundingBox}
this.boundingBox = null;
this.update();
};
Gets the BoundingBox of the Shape in this ShapeInfo.
returns: {!o3d.BoundingBox} The Shape's BoundingBox.
o3djs.picking.ShapeInfo.prototype.getBoundingBox = function() {
return this.boundingBox;
};
Updates the BoundingBox of the Shape in this ShapeInfo.
o3djs.picking.ShapeInfo.prototype.update = function() {
var elements = this.shape.elements;
if (elements.length > 0) {
this.boundingBox = elements[0].getBoundingBox(0);
for (var ee = 1; ee < elements.length; ee++) {
this.boundingBox = this.boundingBox.add(elements[ee].getBoundingBox(0));
}
}
};
Attempts to "pick" this Shape by checking for the intersection of a ray
in world space to the triangles this shape uses.
parameter: {Ray} worldRay A ray in world space to pick against.
returns: {o3djs.picking.PickInfo} Information about the picking.
null if the ray did not intersect any triangles.
o3djs.picking.ShapeInfo.prototype.pick = function(worldRay) {
var worldMatrix = this.parent.transform.getUpdatedWorldMatrix()
var inverseWorldMatrix = o3djs.math.inverse(worldMatrix);
var relativeNear = o3djs.math.matrix4.transformPoint(
inverseWorldMatrix, worldRay.near);
var relativeFar = o3djs.math.matrix4.transformPoint(
inverseWorldMatrix, worldRay.far);
var rayIntersectionInfo =
this.boundingBox.intersectRay(relativeNear,
relativeFar);
o3djs.picking.dumpRayIntersectionInfo('SHAPE(box): ' + this.shape.name,
rayIntersectionInfo);
if (rayIntersectionInfo.intersected) {
var elements = this.shape.elements;
for (var e = 0; e < elements.length; e++) {
var element = elements[e];
rayIntersectionInfo = element.intersectRay(
0,
o3djs.base.o3d.State.CULL_CCW,
relativeNear,
relativeFar);
o3djs.picking.dumpRayIntersectionInfo(
'SHAPE(tris): ' + this.shape.name + ' : element ' + element.name,
rayIntersectionInfo);
// TODO: get closest element not just first element.
if (rayIntersectionInfo.intersected) {
var worldIntersectionPosition = o3djs.math.matrix4.transformPoint(
worldMatrix, rayIntersectionInfo.position);
return o3djs.picking.createPickInfo(this,
rayIntersectionInfo,
worldIntersectionPosition);
}
}
}
return null;
};
Dumps info about a ShapeInfo
parameter: {string} prefix optional prefix for indenting.
o3djs.picking.ShapeInfo.prototype.dump = function(prefix) {
o3djs.picking.dprint(prefix + 'SHAPE: ' + this.shape.name + '\n');
o3djs.picking.dprintPoint3('bb min',
this.boundingBox.minExtent,
prefix + ' ');
o3djs.picking.dprintPoint3('bb max',
this.boundingBox.maxExtent,
prefix + ' ');
};
Creates a new TransformInfo. Used to store information about Transforms.
@constructor
parameter: {!o3d.Transform} transform Transform to keep info about.
parameter: {o3d.Transform} parent Parent transform of the transform.
Can be null.
o3djs.picking.TransformInfo = function(transform, parent) {
this.childTransformInfos = {}; // Object of TransformInfo by id
this.shapeInfos = {}; // Object of ShapeInfo by id
this.transform = transform;
this.parent = parent;
this.boundingBox = null;
};
Gets the BoundingBox of the Transform in this TransformInfo.
returns: {!o3d.BoundingBox} The Transform's BoundingBox.
o3djs.picking.TransformInfo.prototype.getBoundingBox = function() {
return this.boundingBox;
};
Updates the shape and child lists for this TransformInfo and recomputes its
BoundingBox.
o3djs.picking.TransformInfo.prototype.update = function() {
var newChildTransformInfos = {};
var newShapeInfos = {};
var children = this.transform.children;
for (var c = 0; c < children.length; c++) {
var child = children[c];
var transformInfo = this.childTransformInfos[child.clientId];
if (!transformInfo) {
transformInfo = o3djs.picking.createTransformInfo(child, this);
} else {
// clear the boundingBox so we'll regenerate it.
transformInfo.boundingBox = null;
}
transformInfo.update();
newChildTransformInfos[child.clientId] = transformInfo;
}
var shapes = this.transform.shapes;
for (var s = 0; s < shapes.length; s++) {
var shape = shapes[s];
var shapeInfo = this.shapeInfos[shape.clientId];
if (!shapeInfo) {
shapeInfo = o3djs.picking.createShapeInfo(shape, this);
} else {
// unless the vertices or elements change there is no need to
// recompute this.
// shape.update();
}
newShapeInfos[shape.clientId] = shapeInfo;
}
// o3djs.picking.dprint(
// 'num Children: ' + children.length + '\n');
// o3djs.picking.dprint(
// 'num Shapes: ' + shapes.length + '\n');
this.childTransformInfos = newChildTransformInfos;
this.shapeInfos = newShapeInfos;
var boundingBox = null;
for (var key in newShapeInfos) {
var shapeInfo = newShapeInfos[key];
var box = shapeInfo.getBoundingBox().mul(this.transform.localMatrix);
if (!boundingBox) {
boundingBox = box;
} else if (box) {
boundingBox = boundingBox.add(box);
}
}
for (var key in newChildTransformInfos) {
var transformInfo = newChildTransformInfos[key];
// Note: If there is no shape at the leaf on this branch
// there will be no bounding box.
var box = transformInfo.getBoundingBox();
if (!boundingBox) {
boundingBox = box.mul(this.transform.localMatrix);
} else if (box) {
boundingBox = boundingBox.add(box.mul(this.transform.localMatrix));
}
}
this.boundingBox = boundingBox;
};
Attempts to "pick" this TransformInfo by checking for the intersection of a
ray in world space to the boundingbox of the TransformInfo. If intesection
is succesful recursively calls its children and shapes to try to find
a single Shape that is hit by the ray.
parameter: {Ray} worldRay A ray in world space to pick against.
returns: {o3djs.picking.PickInfo} Information about the picking.
null if the ray did not intersect any triangles.
o3djs.picking.TransformInfo.prototype.pick = function(worldRay) {
if (this.boundingBox) {
var inverseWorldMatrix = o3djs.math.matrix4.identity();
if (this.parent) {
inverseWorldMatrix = o3djs.math.inverse(
this.parent.transform.getUpdatedWorldMatrix());
}
var relativeNear =
o3djs.math.matrix4.transformPoint(inverseWorldMatrix, worldRay.near);
var relativeFar =
o3djs.math.matrix4.transformPoint(inverseWorldMatrix, worldRay.far);
var rayIntersectionInfo =
this.boundingBox.intersectRay(relativeNear, relativeFar);
o3djs.picking.dumpRayIntersectionInfo(
'TRANSFORM(box): ' + this.transform.name, rayIntersectionInfo);
if (rayIntersectionInfo.intersected) {
var closestPickInfo = null;
var minDistance = -1;
for (var key in this.childTransformInfos) {
var transformInfo = this.childTransformInfos[key];
var pickInfo = transformInfo.pick(worldRay);
if (pickInfo) {
// is this closer than the last one?
var distance = o3djs.math.lengthSquared(
o3djs.math.sub(worldRay.near,
pickInfo.worldIntersectionPosition));
if (!closestPickInfo || distance < minDistance) {
minDistance = distance;
closestPickInfo = pickInfo;
}
}
}
for (var key in this.shapeInfos) {
var shapeInfo = this.shapeInfos[key];
var pickInfo = shapeInfo.pick(worldRay);
if (pickInfo) {
// is this closer than the last one?
var distance = o3djs.math.lengthSquared(
o3djs.math.sub(worldRay.near,
pickInfo.worldIntersectionPosition));
if (!closestPickInfo || distance < minDistance) {
minDistance = distance;
closestPickInfo = pickInfo;
}
}
}
return closestPickInfo;
}
}
return null;
};
Dumps info about a TransformInfo
parameter: {string} prefix optional prefix for indenting.
o3djs.picking.TransformInfo.prototype.dump = function(prefix) {
prefix = prefix || '';
o3djs.picking.dprint(prefix + 'TRANSFORM: ' + this.transform.name +
'\n');
if (this.boundingBox) {
o3djs.picking.dprintPoint3('bb min',
this.boundingBox.minExtent,
prefix + ' ');
o3djs.picking.dprintPoint3('bb max',
this.boundingBox.maxExtent,
prefix + ' ');
} else {
o3djs.picking.dprint(prefix + ' bb *NA*\n');
}
o3djs.picking.dprint(prefix + '--Shapes--\n');
for (var key in this.shapeInfos) {
var shapeInfo = this.shapeInfos[key];
shapeInfo.dump(prefix + ' ');
}
o3djs.picking.dprint(prefix + '--Children--\n');
for (var key in this.childTransformInfos) {
var transformInfo = this.childTransformInfos[key];
transformInfo.dump(prefix + ' ');
}
};
(C) Æliens
20/2/2008
You may not copy or print any of this material without explicit permission of the author or the publisher.
In case of other copyright issues, contact the author.