Improving Vector3D
The Vector3D
class debuted in Flash Player 10.0 as Adobe’s official implementation of, well, a 3D mathematical vector (not the pseudo-Array
class Vector
). Weirdly, it has a w
component and is therefore technically a 4D vector, but its API inconsistently make use of the fourth dimension. There are also strange oversights, inefficiencies, and functionality it really should have always had. Read on for my custom Vector3D
derivative—Vector3DExt
—that fixes all of these problems by extending and improving on the original.
The first (and most complex) improvement to make to Vector3D
is to implement alternative functions for crossProduct
, a commonly-used operation that returns its result as a newly-allocated Vector3D
. It would be really nice to avoid this allocation since garbage collection is very expensive. So, let’s introduce two new functions to make object re-use much easier:
crossProductIntoFast
crossProductIntoSafe
Both of these functions take the cross product of one vector with another vector and store result in a third vector, hence the word “into” in the function names. The reason there are two functions is that temporary variables are required if you want to store the result in one of the input vectors. The “fast” version is for when you want to reap a minor performance gain by storing the result in a third, unique vector. Otherwise, use the “safe” version.
Continuing with the theme of storing the result in another vector, I’ve added some “into” variants to existing functions that normally overwrite the x
, y
, and z
components of the vector you call them on:
projectInto
normalizeInto
negateInto
incrementByInto
decrementByInto
scaleByInto
Next there is a whole raft of functions that don’t use the w
component and are therefore unsuitable for many purposes. I’ve added functions for each of these and named them with “4D” to indicate that the fourth dimension (w
) is used. There are also “into” variations of many of them.
add4D
subtract4D
setTo4D
incrementBy4D
incrementBy4DInto
decrementBy4D
decrementBy4DInto
toString4D
project4D
project4DInto
scaleBy4D
scaleBy4DInto
normalize4D
normalize4DInto
negate4D
negate4DInto
dotProduct4D
distance4D
As a matter of housekeeping, there are some functions that return a new Vector3D
that are overridden to do do the exact same thing but instead return a Vector3DExt
. Simply cast these to access all of the new functionality:
add
clone
crossProduct
subtract
Lastly, there are functions and constants I just plain wanted and so I added these:
copyFrom4D
toStringExponential
toString4DExponential
toStringFixed
toString4DFixed
toStringPrecision
toString4DPrecision
get is3DVector
get is3DPoint
get isValid
static const ORIGIN
static const ZERO_VECTOR
static const NEGATIVE_X_AXIS
static const NEGATIVE_Y_AXIS
static const NEGATIVE_Z_AXIS
One word of caution: the above constants are just like the constants in Vector3D
(X_AXIS
, Y_AXIS
, Z_AXIS
) in that they can be modified. You can therefore redefine the X axis to be the Y axis by simply calling Vector3D.X_AXIS.setTo(0,1,0)
or the origin (0, 0, 0) to be (1,2,3) by calling Vector3DExt.ORIGIN.setTo(1,2,3)
. If you need to modify these, make a copy with clone
, copyFrom4D
, or Flash 11’s new copyFrom
.
Without further ado, here is the source code for Vector3DExt
: (Spot a bug? Something to add? Post a comment!)
/* The MIT License Copyright (c) 2011 Jackson Dunstan 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. */ package { import flash.geom.Vector3D; /** * A Vector3D with extended functionality * @author Jackson Dunstan (jacksondunstan.com) */ public class Vector3DExt extends Vector3D { /** The origin point in 3D. That is (0, 0, 0, 1) */ static public const ORIGIN:Vector3DExt = new Vector3DExt(0.0, 0.0, 0.0, 1.0); /** The zero vector. That is (0, 0, 0, 0). */ static public const ZERO_VECTOR:Vector3DExt = new Vector3DExt(0.0, 0.0, 0.0, 0.0); /** The negative X axis. That is (-1, 0, 0, 0). */ static public const NEGATIVE_X_AXIS:Vector3DExt = new Vector3DExt(-1.0, 0.0, 0.0, 0.0); /** The negative Y axis. That is (0, -1, 0, 0). */ static public const NEGATIVE_Y_AXIS:Vector3DExt = new Vector3DExt(0.0, -1.0, 0.0, 0.0); /** The negative Z axis. That is (0, 0, -1, 0). */ static public const NEGATIVE_Z_AXIS:Vector3DExt = new Vector3DExt(0.0, 0.0, -1.0, 0.0); /** * The the vector, optionally with some or all initial components * @param x (optional) X component. Defaults to zero. * @param y (optional) Y component. Defaults to zero. * @param z (optional) Z component. Defaults to zero. * @param w (optional) W component. Defaults to zero. */ public function Vector3DExt( x:Number = 0.0, y:Number = 0.0, z:Number = 0.0, w:Number = 0.0 ) { super(x, y, z, w); } /** * Check if this vector is a 3D vector. A 3D vector must have a W * component of zero. This is not true for 3D points (W=1) or * true in general for 4D vectors and points(W not always 0). * @return If this vector is a 3D vector */ public function get is3DVector(): Boolean { return w == 0.0; } /** * Check if this vector is a 3D point. A 3D point must have a W * component of one. This is not true for 3D vectors (W=0) or * true in general for 4D vectors and points (W not always 0). * @return If this vector is a 3D point */ public function get is3DPoint(): Boolean { return w == 1.0; } /** * Check if this vector is valid and therefore does not contain any * component that is NaN * @return If this vector is valid */ public function get isValid(): Boolean { // A Number does not equal itself only when it is NaN. This is // therefore an optimization to calling the global isNaN() function. return x==x && y==y && z==z && w==w; } /** * This version return a Vector3DExt * @inheritDoc */ override public function add(a:Vector3D): Vector3D { return new Vector3DExt( x + a.x, y + a.y, z + a.z, 0.0 ); } /** * This version return a Vector3DExt * @inheritDoc */ public function add4D(a:Vector3D): Vector3D { return new Vector3DExt( x + a.x, y + a.y, z + a.z, w + a.w ); } /** * This version returns a Vector3DExt * @inheritDoc */ override public function clone(): Vector3D { return new Vector3DExt(x, y, z, w); } /** * Copy the components of the given vector to this vector. This version * includes W. * @param sourceVector3D Vector to copy from. Must not be null. * @throws TypeError If 'sourceVector3D' is null */ public function copyFrom4D(sourceVector3D:Vector3D): void { x = sourceVector3D.x; y = sourceVector3D.y; z = sourceVector3D.z; w = sourceVector3D.w; } /** * This version returns a Vector3DExt * @inheritDoc */ override public function crossProduct(a:Vector3D): Vector3D { return new Vector3DExt( y*a.z - a.y*z, z*a.x - a.z*x, x*a.y - a.x*y, 0.0 ); } /** * Compute the cross product of this vector with another and store the * result in a third vector which must NOT be this vector or the given * vector to compute the cross product with or the components of the * resulting vector will be incorrect. The W component of 'into' * is set to zero regardless. * @param a Vector to compute the cross product with. Must not be null, * this vector, or 'into'. * @param into Vector to store the result in. Must not be null, this * vector, or 'a'. * @throws TypeError If either 'a' or 'into' is null */ public function crossProductIntoFast(a:Vector3D, into:Vector3D): void { into.x = y*a.z - a.y*z; into.y = z*a.x - a.z*x; into.z = x*a.y - a.x*y; into.w = 0.0; } /** * Compute the cross product of this vector with another and store the * result in a third vector which can be any non-null vector. The W * component of 'into' is set to zero. * @param a Vector to compute the cross product with. Must not be null. * May be this vector, 'into', or any other non-null vector. * @param into Vector to store the result in. Must not be null. May be * this vector, 'a', or any other non-null vector. * @throws TypeError If either 'a' or 'into' is null */ public function crossProductIntoSafe(a:Vector3D, into:Vector3D): void { var resultX:Number = y*a.z - a.y*z; var resultY:Number = z*a.x - a.z*x; var resultZ:Number = x*a.y - a.x*y; into.x = resultX; into.y = resultY; into.z = resultZ; into.w = 0.0; } /** * Subtract component-wise a given vector from this vector and store * the result in a third vector. The W component of 'into' is copied * from this vector without subtraction. * @param a Vector to subtract from this vector. Must not be null. * @param into Vector to store the result in. Must not be null. * @throws TypeError If either 'a' or 'into' is null */ public function decrementByInto(a:Vector3D, into:Vector3D): void { into.x = x - a.x; into.y = y - a.y; into.z = z - a.z; into.w = w; } /** * Subtract component-wise a given vector from this vector. This * version includes the W component. * @param a Vector to subtract from this vector. Must not be null. * @throws TypeError If 'a' is null */ public function decrementBy4D(a:Vector3D): void { x -= a.x; y -= a.y; z -= a.z; w -= a.w; } /** * Subtract component-wise a given vector from this vector and store * the result in a third vector. This version includes the W component. * @param a Vector to subtract from this vector. Must not be null. * @throws TypeError If either 'a' or 'into' is null */ public function decrementBy4DInto(a:Vector3D, into:Vector3D): void { into.x = x - a.x; into.y = y - a.y; into.z = z - a.z; into.w = w - a.w; } /** * Compute the distance between two 4D points and therefore includes * the W component. * @param pt1 First vector. Must not be null. * @param pt2 Second vector. Must not be null. * @return The distance between the given two points in 4D * @throws TypeError If either 'pt1' or 'pt2' is null */ static public function distance4D(pt1:Vector3D, pt2:Vector3D): Number { // Compute the vector from pt1 to pt2 var dX:Number = pt1.x - pt2.x; var dY:Number = pt1.y - pt2.y; var dZ:Number = pt1.z - pt2.z; var dW:Number = pt1.w - pt2.w; var dot:Number = dX*dX + dY*dY + dZ*dZ + dW*dW; // Distance is the magnitude or zero when the vectors are identical return dot > 0 ? Math.sqrt(dot) : 0; } /** * Compute the dot product of this vector and another vector. This * version includes the W component. * @param a Vector to compute the dot product with. Must not be null. * @return The dot product of this vector and 'a' * @throws TypeError if 'a' is null */ public function dotProduct4D(a:Vector3D): Number { return x*a.x + y*a.y + z*a.z + w*a.w; } /** * Add component-wise a given vector from this vector and store * the result in a third vector. The W component of 'into' is copied * from this vector without addition. * @param a Vector to add to this vector. Must not be null. * @param into Vector to store the result in. Must not be null. * @throws TypeError If either 'a' or 'into' is null */ public function incrementByInto(a:Vector3D, into:Vector3D): void { into.x = x + a.x; into.y = y + a.y; into.z = z + a.z; into.w = w; } /** * Add component-wise a given vector from this vector. This * version includes the W component. * @param a Vector to add to this vector. Must not be null. * @throws TypeError If 'a' is null */ public function incrementBy4D(a:Vector3D): void { x += a.x; y += a.y; z += a.z; w += a.w; } /** * Add component-wise a given vector from this vector and store * the result in a third vector. This version includes the W component. * @param a Vector to add to this vector. Must not be null. * @throws TypeError If either 'a' or 'into' is null */ public function incrementBy4DInto(a:Vector3D, into:Vector3D): void { into.x = x + a.x; into.y = y + a.y; into.z = z + a.z; into.w = w + a.w; } /** * Negate this vector and store the result in another vector. The * W component is copied from this vector without negation. * @param into Vector to store the result in. Must not be null. * @throws TypeError If 'into' is null. */ public function negateInto(into:Vector3D): void { into.x = -x; into.y = -y; into.z = -z; into.w = w; } /** * Negate this vector. This version includes the W component. */ public function negate4D(): void { x = -x; y = -y; z = -z; w = -w; } /** * Negate this vector and store the result in another vector. This * version includes the W component. * @param into Vector to store the result in. Must not be null. * @throws TypeError If 'into' is null */ public function negate4DInto(into:Vector3D): void { into.x = -x; into.y = -y; into.z = -z; into.w = -w; } /** * Normalize this vector (such that its 3D magnitude/length is one) and * store the result in another vector. If the X, Y, and W components * are all zero, no modification is made. The W component is copied * from this vector to 'into' without modification and is not * considered when computing the magnitude/length of this vector for * normalization of the X, Y, and Z components. * @param into Vector to store the result in. Must not be null. * @return The 3D magnitude/length of this vector before normalization * or zero if the X, Y, and Z components were all zero. W is * ignored when computing this value. * @throws TypeError If 'into' is null */ public function normalizeInto(into:Vector3D): Number { var dot:Number = x*x + y*y + z*z; if (dot > 0) { var mag:Number = Math.sqrt(dot); into.x = x / mag; into.y = y / mag; into.z = z / mag; into.w = w; return mag; } return 0.0; } /** * Normalize this vector (such that it's 4D magnitude/length is one). * If the X, Y, Z, and W components are all zero, no modification is * made. This version includes the W component. * @return The 4D magnitude of this vector before normalization or zero * if the X, Y, Z, and W components were all zero */ public function normalize4D(): Number { var dot:Number = x*x + y*y + z*z + w*w; if (dot > 0) { var mag:Number = Math.sqrt(dot); x /= mag; y /= mag; z /= mag; w /= mag; return mag; } return 0.0; } /** * Normalize this vector (such that it's 4D magnitude/length is one) * and store the result in another vector. If the X, Y, Z, and W * components are all zero, no modification is made to 'into'. This * version includes the W component. * @param into Vector to store the result in. Must not be null. * @return The 4D magnitude of this vector before normalization or zero * if the X, Y, Z, and W components were all zero * @throws TypeError If 'into' is null */ public function normalize4DInto(into:Vector3D): Number { var dot:Number = x*x + y*y + z*z + w*w; if (dot > 0) { var mag:Number = Math.sqrt(dot); into.x = x / mag; into.y = y / mag; into.z = z / mag; into.w = w / mag; return mag; } return 0.0; } /** * Divide component-wise this vector's components by this vector's W * component and store the result in another vector. If this vector's * W component is zero, the X, Y, and Z components set to 'into' will * be Infinity. W is copied from this vector to 'into' without * modification. * @param into Vector to store the result in. Must not be null. * @throws TypeError If 'into' is null */ public function projectInto(into:Vector3D): void { into.x = x / w; into.y = y / w; into.z = z / w; into.w = w; } /** * Divide component-wise this vector's components by this vector's W * component. If this vector's W component is zero, its X, Y, and Z * components will be set to Infinity. The W component of this vector * is always set to one. * @param into Vector to store the result in. Must not be null. * @throws TypeError If 'into' is null */ public function project4D(): void { x /= w; y /= w; z /= w; w = 1.0; } /** * Divide component-wise this vector's components by this vector's W * component and store the result in another vector. If this vector's * W component is zero, the X, Y, and Z components set to 'into' will * be Infinity. The W component of 'into' is always set to one. * @param into Vector to store the result in. Must not be null. * @throws TypeError If 'into' is null */ public function project4DInto(into:Vector3D): void { into.x = x / w; into.y = y / w; into.z = z / w; into.w = 1.0; } /** * Multiply the X, Y, and Z components of this vector by a given * value and store the result in another vector. The W component of * this vector is copied without modification. * @param s Value to multiply by * @param into Vector to store the result in * @throws TypeError If 'into' is null */ public function scaleByInto(s:Number, into:Vector3D): void { into.x = x * s; into.y = y * s; into.z = z * s; into.w = w; } /** * Multiply the X, Y, and Z components of this vector by a given * value * @param s Value to multiply by */ public function scaleBy4D(s:Number): void { x *= s; y *= s; z *= s; w *= s; } /** * Multiply the X, Y, Z, and W components of this vector by a given * value and store the result in another vector * @param s Value to multiply by * @param into Vector to store the result in * @throws TypeError If 'into' is null */ public function scaleBy4DInto(s:Number, into:Vector3D): void { into.x = x * s; into.y = y * s; into.z = z * s; into.w = w * s; } /** * Set all four components of this vector * @param xa X component of this vector * @param ya Y component of this vector * @param za Z component of this vector * @param wa W component of this vector */ public function setTo4D(xa:Number, ya:Number, za:Number, wa:Number): void { x = xa; y = ya; z = za; w = wa; } /** * This version return a Vector3DExt * @inheritDoc */ override public function subtract(a:Vector3D): Vector3D { return new Vector3DExt( x - a.x, y - a.y, z - a.z, 0.0 ); } /** * Subtract a vector component-wise from this vector and return a new * vector. This version includes the W component. The original vector * is not modified. * @param a Vector to subtract from this vector. Must not be null. * @return A new vector whose components are the difference between * this vector and the given vector * @throws TypeError If 'a' is null */ public function subtract4D(a:Vector3D): Vector3DExt { return new Vector3DExt( x - a.x, y - a.y, z - a.z, w - a.w ); } /** * Get a string representation of this vector. This version includes * the W component. * @return A string representation of this vector including the W * component */ public function toString4D(): String { return "Vector3DExt(" + x + ", " + y + ", " + z + ", " + w + ")"; } /** * Get a string representation of this vector. This version does NOT * include the W component. All components of the vector are formatted * using Number.toExponential(). * @param fractionDigits An integer between 0 and 20, inclusive, that * represents the desired number of decimal * places. * @return A string representation of this vector NOT including the W * component * @throws RangeError If the fractionDigits argument is outside the * range 0 to 20. */ public function toStringExponential(fractionDigits:uint): String { return "Vector3DExt(" + x.toExponential(fractionDigits) + ", " + y.toExponential(fractionDigits) + ", " + z.toExponential(fractionDigits) + ")"; } /** * Get a string representation of this vector. This version includes * the W component. All components of the vector are formatted using * Number.toExponential(). * @param fractionDigits An integer between 0 and 20, inclusive, that * represents the desired number of decimal * places. * @return A string representation of this vector including the W * component * @throws RangeError If the fractionDigits argument is outside the * range 0 to 20. */ public function toString4DExponential(fractionDigits:uint): String { return "Vector3DExt(" + x.toExponential(fractionDigits) + ", " + y.toExponential(fractionDigits) + ", " + z.toExponential(fractionDigits) + ", " + w.toExponential(fractionDigits) + ")"; } /** * Get a string representation of this vector. This version does NOT * include the W component. All components of the vector are formatted * using Number.toFixed(). * @param fractionDigits An integer between 0 and 20, inclusive, that * represents the desired number of decimal * places. * @return A string representation of this vector NOT including the W * component * @throws RangeError If the fractionDigits argument is outside the * range 0 to 20. */ public function toStringFixed(fractionDigits:uint): String { return "Vector3DExt(" + x.toFixed(fractionDigits) + ", " + y.toFixed(fractionDigits) + ", " + z.toFixed(fractionDigits) + ")"; } /** * Get a string representation of this vector. This version includes * the W component. All components of the vector are formatted using * Number.toFixed(). * @param fractionDigits An integer between 0 and 20, inclusive, that * represents the desired number of decimal * places. * @return A string representation of this vector including the W * component * @throws RangeError If the fractionDigits argument is outside the * range 0 to 20. */ public function toString4DFixed(fractionDigits:uint): String { return "Vector3DExt(" + x.toFixed(fractionDigits) + ", " + y.toFixed(fractionDigits) + ", " + z.toFixed(fractionDigits) + ", " + w.toFixed(fractionDigits) + ")"; } /** * Get a string representation of this vector. This version does NOT * include the W component. All components of the vector are formatted * using Number.toPrecision(). * @param precision An integer between 1 and 21, inclusive, that * represents the desired number of digits to * represent in the resulting string. * @return A string representation of this vector NOT including the W * component * @throws RangeError If the precision argument is outside the range 1 * to 21. */ public function toStringPrecision(precision:uint): String { return "Vector3DExt(" + x.toPrecision(precision) + ", " + y.toPrecision(precision) + ", " + z.toPrecision(precision) + ")"; } /** * Get a string representation of this vector. This version includes * the W component. All components of the vector are formatted * using Number.toPrecision(). * @param precision An integer between 1 and 21, inclusive, that * represents the desired number of digits to * represent in the resulting string. * @return A string representation of this vector including the W * component * @throws RangeError If the precision argument is outside the range 1 * to 21. */ public function toString4DPrecision(precision:uint): String { return "Vector3DExt(" + x.toPrecision(precision) + ", " + y.toPrecision(precision) + ", " + z.toPrecision(precision) + ", " + w.toPrecision(precision) + ")"; } } }
#1 by emrah on June 27th, 2011 ·
i hate this, another adobe problem.
#2 by li on June 28th, 2011 ·
Looks awesome. Have you done any benchmarks comparing this with the regular Vector3D?
#3 by jackson on June 28th, 2011 ·
Nope, but I’d be interested in hearing what you’d like to see compared. Remember that except for a few overrides, all the functions in
Vector3DExt
are new.#4 by Breakdance McFunkypants on November 28th, 2011 ·
Wonderful work. I’ve recently been invited to betatest Jean-Philippe Auclair’s “TheMiner” which is the sequel to his mind-blowing FlashPreloadProfiler. I’ve been using it on a Stage3D game I wrote, which, though it renders 200,000 polies and thousands of spaceships and particles at a smooth 60fps, is sadly creating and destroying nearly 10,000 Vector3Ds every second. Why? Adobe’s silly decision to return NEW objects on common math functions like crossproduct, etc. I’ve put great time and effort into never creating any NEW Vector3Ds during runtime render loops, and use single static temp vars for common operations. Hopefully, replacing the lousy Adobe Vector3D class with yours, I won’t cause so many GC issues.
Request: a torough benchmark of this Vector3Dext vs Vector3D with regard to speed and *most importantly* GC hiccups, ram usage, etc in game-like scenarios, where you might be rendering 5000 different moving, rotating meshes using Stage3D. My goal is constant ram usage without any memory leaks or GC hiccups for high performance 3d game engines.
It might take me a couple weeks, but when I get the chance to replace the built-in Vector3D with your version, I will report back with some stats of my own.
Keep up the fantastic work. You are my hero!
#5 by jackson on November 28th, 2011 ·
Glad to hear you’re finding it useful. :)
As for the benchmark, it’s important to remember that
Vector3DExt
is aVector3D
because it extends it. The performance gains will come, as you emphatically point out, from the functions that avoid allocating newVector3D
instances. Those gains won’t be specific toVector3DExt
though; you could get them by not allocating any number of types of objects. For more on this subject, check out my article Hidden Object Allocations.