diff --git a/core/src/avm2/globals/flash/geom/Matrix3D.as b/core/src/avm2/globals/flash/geom/Matrix3D.as index b1151179cffb..e38d6198c3fc 100644 --- a/core/src/avm2/globals/flash/geom/Matrix3D.as +++ b/core/src/avm2/globals/flash/geom/Matrix3D.as @@ -1,9 +1,85 @@ +/** + * The Matrix3D class represents a transformation matrix that determines the position and + * orientation of a three-dimensional (3D) display object. The matrix can perform + * transformation functions including translation (repositioning along the x, y, and z + * axes), rotation, and scaling (resizing). The Matrix3D class can also perform + * perspective projection, which maps points from the 3D coordinate space to a + * two-dimensional (2D) view. + * + * A single matrix can combine multiple transformations and apply them at once to a 3D + * display object. For example, a matrix can be applied to 3D coordinates to perform a + * rotation followed by a translation. + * + * When you explicitly set the `z` property or any of the rotation or scaling properties + * of a display object, a corresponding Matrix3D object is automatically created. + * + * You can access a 3D display object's Matrix3D object through the `transform.matrix3d` + * property. 2D objects do not have a Matrix3D object. + * + * The value of the `z` property of a 2D object is zero and the value of its `matrix3D` + * property is `null`. + * + * **Note:** If the same Matrix3D object is assigned to two different display objects, a + * runtime error is thrown. + * + * The Matrix3D class uses a 4x4 square matrix: a table of four rows and columns of + * numbers that hold the data for the transformation. The first three rows of the matrix + * hold data for each 3D axis (x,y,z). The translation information is in the last column. + * The orientation and scaling data are in the first three columns. The scaling factors + * are the diagonal numbers in the first three columns. Here is a representation of + * Matrix3D elements: + * + * scalex shearYX shearZX perspectiveX + * shearXY scaleY shearZY perspectiveY + * shearXZ shearYZ scaleZ perspectiveZ + * transitionx transitionY transitionZ perspectiveS + * + * You don't need to understand matrix mathematics to use the Matrix3D class. It offers + * specific methods that simplify the task of transformation and projection, such as the + * `appendTranslation()`, `appendRotation()`, or `interpolateTo()` methods. You also can + * use the `decompose()` and `recompose()` methods or the `rawData` property to access + * the underlying matrix elements. + * + * Display objects cache their axis rotation properties to have separate rotation for + * each axis and to manage the different combinations of rotations. When a method of a + * Matrix3D object is called to transform a display object, the rotation cache of the + * object is invalidated. + */ // Based on the MIT-licensed OpenFL code https://github.com/openfl/openfl/blob/develop/src/openfl/geom/Matrix3D.hx package flash.geom { - import __ruffle__.stub_method; + + import flash.geom.Orientation3D; public class Matrix3D { + + private static var _correct:int = 0; // Apply scale skew perspective to interpolate methods + // I would recommend setting this to 1 by default + + /** + * Gets or sets the value of the 'correct' property. + * This property is used to apply scale, skew, and perspective to interpolate methods. + * You can call it globally with Matrix3d.correct = true; to force correct behaviour + * + * @param value Boolean - The value to set for the 'correct' property. + * If true, the state is considered correct; if false, incorrect. + * @return Boolean - Returns true if the state is correct (non-zero), otherwise false. + */ + public static function get correct():Boolean { + return _correct != 0; + } + + public static function set correct(value:Boolean):void { + _correct = value ? 1 : 0; + } + + /** + * A Vector of 16 Numbers, where every four elements is a column of a 4x4 matrix. + * + * An exception is thrown if the `rawData` property is set to a matrix that is not + * invertible. The Matrix3D object must be invertible. If a non-invertible matrix is + * needed, create a subclass of the Matrix3D object. + */ // The 4x4 matrix data, stored in column-major order // This is never null. [Ruffle(InternalSlot)] @@ -21,28 +97,122 @@ package flash.geom { public function Matrix3D(v:Vector. = null) { if (v != null && v.length == 16) { - this._rawData = v.AS3::concat(); - } else { + //this._rawData = v.AS3::concat(); + this.rawData = v; // use setter... must be tested if much slower... + } + else { this.identity(); } } + /** + * Converts the current matrix to an identity or unit matrix. An identity matrix has + * a value of one for the elements on the main diagonal and a value of zero for all + * other elements. The result is a matrix where the rawData value is + * 1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1 and the rotation setting is set to + * `Vector3D(0,0,0)`, the position or translation setting is set to `Vector3D(0,0,0)`, + * and the scale is set to `Vector3D(1,1,1)`. Here is a representation of an + * identity matrix. + * + * 1, 0, 0, 0, + * 0, 1, 0, 0, + * 0, 0, 1, 0, + * 0, 0, 0, 1 + * + * An object transformed by applying an identity matrix performs no transformation. + * In other words, if a matrix is multiplied by an identity matrix, the result is a + * matrix that is the same as (identical to) the original matrix. + */ public function identity():void { // Note that every 4 elements is a *column*, not a row this._rawData = new [ - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1 - ]; + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ]; } + /** + * Appends an incremental translation, a repositioning along the x, y, and z axes, + * to a Matrix3D object. When the Matrix3D object is applied to a display object, + * the matrix performs the translation changes after other transformations in the + * Matrix3D object. + * + * The translation is defined as a set of three incremental changes along the + * three axes (x,y,z). When the transformation is applied to a display object, the + * display object moves from it current location along the x, y, and z axes as + * specified by the parameters. To make sure that the translation only affects a + * specific axis, set the other parameters to zero. A zero parameter means no change + * along the specific axis. + * + * The translation changes are not absolute. They are relative to the current + * position and orientation of the matrix. To make an absolute change to the + * transformation matrix, use the recompose() method. The order of transformation + * also matters. A translation followed by a rotation transformation produces a + * different effect than a rotation followed by a translation. + * + * @param x An incremental translation along the x axis. + * @param y An incremental translation along the y axis. + * @param z An incremental translation along the z axis. + */ public function appendTranslation(x:Number, y:Number, z:Number):void { this._rawData[12] += x; this._rawData[13] += y; this._rawData[14] += z; } + /** + * Appends an incremental rotation to a Matrix3D object. When the Matrix3D object is + * applied to a display object, the matrix performs the rotation after other + * transformations in the Matrix3D object. + * + * The display object's rotation is defined by an axis, an incremental degree of + * rotation around the axis, and an optional pivot point for the center of the + * object's rotation. The axis can be any general direction. The common axes are the + * `X_AXIS (Vector3D(1,0,0))`, `Y_AXIS (Vector3D(0,1,0))`, and + * `Z_AXIS (Vector3D(0,0,1))`. In aviation terminology, the rotation about the y axis + * is called yaw. The rotation about the x axis is called pitch. The rotation about + * the z axis is called roll. + * + * The order of transformation matters. A rotation followed by a translation + * transformation produces a different effect than a translation followed by a + * rotation transformation. + * + * The rotation effect is not absolute. It is relative to the current position and + * orientation. To make an absolute change to the transformation matrix, use the + * `recompose()` method. The `appendRotation()` method is also different from the + * axis rotation property of the display object, such as `rotationX` property. The + * `rotation` property is always performed before any translation, whereas the + * `appendRotation()` method is performed relative to what is already in the matrix. + * To make sure that you get a similar effect as the display object's axis rotation + * property, use the `prependRotation()` method, which performs the rotation before + * other transformations in the matrix. + * + * When the `appendRotation()` method's transformation is applied to a Matrix3D object + * of a display object, the cached rotation property values of the display object + * are invalidated. + * + * One way to have a display object rotate around a specific point relative to its + * location is to set the translation of the object to the specified point, rotate + * the object using the `appendRotation()` method, and translate the object back to + * the original position. In the following example, the myObject 3D display object + * makes a y-axis rotation around the coordinate (10,10,0). + * + * ``` + * myObject.z = 1; + * myObject.transform.matrix3D.appendTranslation(10,10,0); + * myObject.transform.matrix3D.appendRotation(1, Vector3D.Y_AXIS); + * myObject.transform.matrix3D.appendTranslation(-10,-10,0); + * ``` + * + * @param degrees The degree of the rotation. + * @param axis The axis or direction of rotation. The usual axes are the + * `X_AXIS (Vector3D(1,0,0))`, `Y_AXIS (Vector3D(0,1,0))`, and + * `Z_AXIS (Vector3D(0,0,1))`. This vector should have a length of one. + * @param pivotPoint A point that determines the center of an object's rotation. + * The default pivot point for an object is its registration point. + */ public function appendRotation(degrees:Number, axis:Vector3D, pivotPoint:Vector3D = null):void { var tx:Number, ty:Number, tz:Number; tx = ty = tz = 0; @@ -74,24 +244,36 @@ package flash.geom { var ccos = 1 - cos; var m = new Matrix3D(); - var d = m.rawData; - d[0] = x2 + (y2 + z2) * cos; - d[1] = x * y * ccos + z * sin; - d[2] = x * z * ccos - y * sin; - d[4] = x * y * ccos - z * sin; - d[5] = y2 + (x2 + z2) * cos; - d[6] = y * z * ccos + x * sin; - d[8] = x * z * ccos + y * sin; - d[9] = y * z * ccos - x * sin; - d[10] = z2 + (x2 + y2) * cos; - d[12] = (tx * (y2 + z2) - x * (ty * y + tz * z)) * ccos + (ty * z - tz * y) * sin; - d[13] = (ty * (x2 + z2) - y * (tx * x + tz * z)) * ccos + (tz * x - tx * z) * sin; - d[14] = (tz * (x2 + y2) - z * (tx * x + ty * y)) * ccos + (tx * y - ty * x) * sin; - m.rawData = d; + var mr = m.rawData; + mr[0] = x2 + (y2 + z2) * cos; + mr[1] = x * y * ccos + z * sin; + mr[2] = x * z * ccos - y * sin; + + mr[4] = x * y * ccos - z * sin; + mr[5] = y2 + (x2 + z2) * cos; + mr[6] = y * z * ccos + x * sin; + + mr[8] = x * z * ccos + y * sin; + mr[9] = y * z * ccos - x * sin; + mr[10] = z2 + (x2 + y2) * cos; + + mr[12] = (tx * (y2 + z2) - x * (ty * y + tz * z)) * ccos + (ty * z - tz * y) * sin; + mr[13] = (ty * (x2 + z2) - y * (tx * x + tz * z)) * ccos + (tz * x - tx * z) * sin; + mr[14] = (tz * (x2 + y2) - z * (tx * x + ty * y)) * ccos + (tx * y - ty * x) * sin; + m.rawData = mr; this.append(m); } + /** + * Copies all of the vector data from the source vector object into the calling + * Matrix3D object. The optional `index` parameter allows you to select any starting + * slot in the vector. + * + * @param vector The vector object from which to copy the data. + * @param index + * @param transpose + */ [API("674")] public function copyRawDataFrom(vector:Vector., index:uint = 0, transpose:Boolean = false):void { if (transpose) { @@ -109,6 +291,12 @@ package flash.geom { } } + /** + * Copies specific row of the calling Matrix3D object into the Vector3D object. + * + * @param row The row from which to copy the data from. + * @param vector3D The Vector3D object to copy the data into. + */ // Based on https://github.com/openfl/openfl/blob/develop/src/openfl/geom/Matrix3D.hx#L542C1-L573 [API("674")] public function copyRowTo(row:uint, vector3D:Vector3D):void { @@ -118,67 +306,98 @@ package flash.geom { switch (row) { case 0: - vector3D.x = rawData[0]; - vector3D.y = rawData[4]; - vector3D.z = rawData[8]; - vector3D.w = rawData[12]; + vector3D.x = this._rawData[0]; + vector3D.y = this._rawData[4]; + vector3D.z = this._rawData[8]; + vector3D.w = this._rawData[12]; break; case 1: - vector3D.x = rawData[1]; - vector3D.y = rawData[5]; - vector3D.z = rawData[9]; - vector3D.w = rawData[13]; + vector3D.x = this._rawData[1]; + vector3D.y = this._rawData[5]; + vector3D.z = this._rawData[9]; + vector3D.w = this._rawData[13]; break; case 2: - vector3D.x = rawData[2]; - vector3D.y = rawData[6]; - vector3D.z = rawData[10]; - vector3D.w = rawData[14]; + vector3D.x = this._rawData[2]; + vector3D.y = this._rawData[6]; + vector3D.z = this._rawData[10]; + vector3D.w = this._rawData[14]; break; case 3: - vector3D.x = rawData[3]; - vector3D.y = rawData[7]; - vector3D.z = rawData[11]; - vector3D.w = rawData[15]; + vector3D.x = this._rawData[3]; + vector3D.y = this._rawData[7]; + vector3D.z = this._rawData[11]; + vector3D.w = this._rawData[15]; break; } } + /** + * Copies a Vector3D object into specific row of the calling Matrix3D object. + * + * @param row The row from which to copy the data to. + * @param vector3D The Vector3D object from which to copy the data. + */ // Based on https://github.com/openfl/openfl/blob/develop/src/openfl/geom/Matrix3D.hx#L504-L534 [API("674")] public function copyRowFrom(row:uint, vector3D:Vector3D):void { if (row > 3) { throw new ArgumentError("Error #2004: One of the parameters is invalid.", 2004); } - + switch (row) { case 0: - _rawData[0] = vector3D.x; - _rawData[4] = vector3D.y; - _rawData[8] = vector3D.z; - _rawData[12] = vector3D.w; + this._rawData[0] = vector3D.x; + this._rawData[4] = vector3D.y; + this._rawData[8] = vector3D.z; + this._rawData[12] = vector3D.w; break; case 1: - _rawData[1] = vector3D.x; - _rawData[5] = vector3D.y; - _rawData[9] = vector3D.z; - _rawData[13] = vector3D.w; + this._rawData[1] = vector3D.x; + this._rawData[5] = vector3D.y; + this._rawData[9] = vector3D.z; + this._rawData[13] = vector3D.w; break; case 2: - _rawData[2] = vector3D.x; - _rawData[6] = vector3D.y; - _rawData[10] = vector3D.z; - _rawData[14] = vector3D.w; + this._rawData[2] = vector3D.x; + this._rawData[6] = vector3D.y; + this._rawData[10] = vector3D.z; + this._rawData[14] = vector3D.w; break; case 3: - _rawData[3] = vector3D.x; - _rawData[7] = vector3D.y; - _rawData[11] = vector3D.z; - _rawData[15] = vector3D.w; + this._rawData[3] = vector3D.x; + this._rawData[7] = vector3D.y; + this._rawData[11] = vector3D.z; + this._rawData[15] = vector3D.w; break; } } + /** + * Uses the transformation matrix without its translation elements to transform a + * Vector3D object from one space coordinate to another. The returned Vector3D + * object holds the new coordinates after the rotation and scaling transformations + * have been applied. If the `deltaTransformVector()` method applies a matrix that + * only contains a translation transformation, the returned Vector3D is the same as + * the original Vector3D object. + * + * You can use the `deltaTransformVector()` method to have a display object in one + * coordinate space respond to the rotation transformation of a second display + * object. The object does not copy the rotation; it only changes its position to + * reflect the changes in the rotation. For example, to use the display.Graphics + * API for drawing a rotating 3D display object, you must map the object's rotating + * coordinates to a 2D point. First, retrieve the object's 3D coordinates after each + * rotation, using the `deltaTransformVector()` method. Next, apply the display + * object's `local3DToGlobal()` method to translate the 3D coordinates to 2D points. + * You can then use the 2D points to draw the rotating 3D object. + * + * **Note:** This method automatically sets the `w` component of the passed Vector3D + * to 0.0. + * + * @param v A Vector3D object holding the coordinates that are going to be + * transformed. + * @return Vector3D A Vector3D object with the transformed coordinates. + */ public function deltaTransformVector(v:Vector3D):Vector3D { var x:Number = this._rawData[0] * v.x + this._rawData[4] * v.y + this._rawData[8] * v.z; var y:Number = this._rawData[1] * v.x + this._rawData[5] * v.y + this._rawData[9] * v.z; @@ -187,6 +406,31 @@ package flash.geom { return new Vector3D(x, y, z, w); } + /** + * Uses the transformation matrix without its translation elements to transform a + * Vector3D object from one space coordinate to another. The returned Vector3D + * object holds the new coordinates after the rotation and scaling transformations + * have been applied. If the `deltaTransformVector()` method applies a matrix that + * only contains a translation transformation, the returned Vector3D is the same as + * the original Vector3D object. + * + * You can use the `deltaTransformVector()` method to have a display object in one + * coordinate space respond to the rotation transformation of a second display + * object. The object does not copy the rotation; it only changes its position to + * reflect the changes in the rotation. For example, to use the display.Graphics + * API for drawing a rotating 3D display object, you must map the object's rotating + * coordinates to a 2D point. First, retrieve the object's 3D coordinates after each + * rotation, using the `deltaTransformVector()` method. Next, apply the display + * object's `local3DToGlobal()` method to translate the 3D coordinates to 2D points. + * You can then use the 2D points to draw the rotating 3D object. + * + * **Note:** This method automatically sets the `w` component of the passed Vector3D + * to 0.0. + * + * @param v A Vector3D object holding the coordinates that are going to be + * transformed. + * @return Vector3D A Vector3D object with the transformed coordinates. + */ public function transformVector(v:Vector3D):Vector3D { var x:Number = this._rawData[0] * v.x + this._rawData[4] * v.y + this._rawData[8] * v.z + this._rawData[12]; var y:Number = this._rawData[1] * v.x + this._rawData[5] * v.y + this._rawData[9] * v.z + this._rawData[13]; @@ -198,7 +442,7 @@ package flash.geom { public function transformVectors(vin:Vector., vout:Vector.):void { if (vin == null) { throw new TypeError("Error #2007: Parameter vin must be non-null.", 2007); -} + } if (vout == null) { throw new TypeError("Error #2007: Parameter vout must be non-null.", 2007); } @@ -221,22 +465,45 @@ package flash.geom { } } + /** + * Converts the current Matrix3D object to a matrix where the rows and columns are + * swapped. For example, if the current Matrix3D object's rawData contains the + * following 16 numbers, `1,2,3,4,11,12,13,14,21,22,23,24,31,32,33,34`, the + * `transpose()` method reads every four elements as a row and turns the rows into + * columns. The result is a matrix with the rawData of: + * `1,11,21,31,2,12,22,32,3,13,23,33,4,14,24,34`. + * + * The `transpose()` method replaces the current matrix with a transposed matrix. + * If you want to transpose a matrix without altering the current matrix, first copy + * the current matrix by using the `clone()` method and then apply the `transpose()` + * method to the copy. + * + * An orthogonal matrix is a square matrix whose transpose is equal to its inverse. + */ public function transpose():void { // Make a copy - var oRawData = this._rawData.AS3::concat(); - this._rawData[1] = oRawData[4]; - this._rawData[2] = oRawData[8]; - this._rawData[3] = oRawData[12]; - this._rawData[4] = oRawData[1]; - this._rawData[6] = oRawData[9]; - this._rawData[7] = oRawData[13]; - this._rawData[8] = oRawData[2]; - this._rawData[9] = oRawData[6]; - this._rawData[11] = oRawData[14]; - this._rawData[12] = oRawData[3]; - this._rawData[13] = oRawData[7]; - this._rawData[14] = oRawData[11]; + var mr = this.rawData; + this._rawData[1] = mr[4]; + this._rawData[2] = mr[8]; + this._rawData[3] = mr[12]; + this._rawData[4] = mr[1]; + this._rawData[6] = mr[9]; + this._rawData[7] = mr[13]; + this._rawData[8] = mr[2]; + this._rawData[9] = mr[6]; + this._rawData[11] = mr[14]; + this._rawData[12] = mr[3]; + this._rawData[13] = mr[7]; + this._rawData[14] = mr[11]; } + + /** + * A Vector of 16 Numbers, where every four elements is a column of a 4x4 matrix. + * + * An exception is thrown if the `rawData` property is set to a matrix that is not + * invertible. The Matrix3D object must be invertible. If a non-invertible matrix is + * needed, create a subclass of the Matrix3D object. + */ public function append(lhs:Matrix3D):void { var m111:Number = this._rawData[0], m121:Number = this._rawData[4], @@ -292,35 +559,181 @@ package flash.geom { this._rawData[15] = m141 * m214 + m142 * m224 + m143 * m234 + m144 * m244; } + /** + * Appends an incremental scale change along the x, y, and z axes to a Matrix3D + * object. When the Matrix3D object is applied to a display object, the matrix + * performs the scale changes after other transformations in the Matrix3D object. + * The default scale factor is (1.0, 1.0, 1.0). + * + * The scale is defined as a set of three incremental changes along the three axes + * (x,y,z). You can multiply each axis with a different number. When the scale + * changes are applied to a display object, the object's size increases or decreases. + * For example, setting the x, y, and z axes to two doubles the size of the object, + * while setting the axes to 0.5 halves the size. To make sure that the scale + * transformation only affects a specific axis, set the other parameters to one. A + * parameter of one means no scale change along the specific axis. + * + * The `appendScale()` method can be used for resizing as well as for managing + * distortions, such as stretch or contract of a display object, or for zooming in + * and out on a location. Scale transformations are automatically performed during a + * display object's rotation and translation. + * + * The order of transformation matters. A resizing followed by a translation + * transformation produces a different effect than a translation followed by a + * resizing transformation. + * + * @param xScale A multiplier used to scale the object along the x axis. + * @param yScale A multiplier used to scale the object along the y axis. + * @param zScale A multiplier used to scale the object along the z axis. + */ // Based on https://github.com/openfl/openfl/blob/971a4c9e43b5472fd84d73920a2b7c1b3d8d9257/src/openfl/geom/Matrix3D.hx#L307 public function appendScale(xScale:Number, yScale:Number, zScale:Number):void { this.append(new Matrix3D(Vector.([ - xScale, 0.0, 0.0, 0.0, 0.0, yScale, 0.0, 0.0, 0.0, 0.0, zScale, 0.0, 0.0, 0.0, 0.0, 1.0 - ]))); + xScale, 0, 0, 0, + 0, yScale, 0, 0, + 0, 0, zScale, 0, + 0, 0, 0, 1 + ]))); } + /** + * Prepends an incremental translation, a repositioning along the x, y, and z axes, + * to a Matrix3D object. When the Matrix3D object is applied to a display object, + * the matrix performs the translation changes before other transformations in the + * Matrix3D object. + * + * Translation specifies the distance the display object moves from its current + * location along the x, y, and z axes. The `prependTranslation()` method sets the + * translation as a set of three incremental changes along the three axes (x,y,z). + * To have a translation change only a specific axis, set the other parameters to + * zero. A zero parameter means no change along the specific axis. + * + * The translation changes are not absolute. The effect is object-relative, + * relative to the frame of reference of the original position and orientation. + * To make an absolute change to the transformation matrix, use the `recompose()` + * method. The order of transformation also matters. A translation followed by a + * rotation transformation produces a different effect than a rotation followed by + * a translation transformation. When prependTranslation() is used, the display + * object continues to move in the direction it is facing, regardless of the other + * transformations. For example, if a display object was facing toward a positive x + * axis, it continues to move in the direction specified by the + * `prependTranslation()` method, regardless of how the object has been rotated. To + * make translation changes occur after other transformations, use the + * `appendTranslation()` method. + * + * @param x An incremental translation along the x axis. + * @param y An incremental translation along the y axis. + * @param z An incremental translation along the z axis. + */ public function prependTranslation(x:Number, y:Number, z:Number):void { var m = new Matrix3D(); m.position = new Vector3D(x, y, z); this.prepend(m); } + /** + * Prepends an incremental rotation to a Matrix3D object. When the Matrix3D object is + * applied to a display object, the matrix performs the rotation before other + * transformations in the Matrix3D object. + * + * The display object's rotation is defined by an axis, an incremental degree of + * rotation around the axis, and an optional pivot point for the center of the + * object's rotation. The axis can be any general direction. The common axes are the + * `X_AXIS (Vector3D(1,0,0))`, `Y_AXIS (Vector3D(0,1,0))`, and + * `Z_AXIS (Vector3D(0,0,1))`. In aviation terminology, the rotation about the y + * axis is called yaw. The rotation about the x axis is called pitch. The rotation + * about the z axis is called roll. + * + * The order of transformation matters. A rotation followed by a translation + * transformation produces a different effect than a translation followed by a + * rotation. + * + * The rotation effect is not absolute. The effect is object-relative, relative to + * the frame of reference of the original position and orientation. To make an + * absolute change to the transformation, use the `recompose()` method. + * + * When the `prependRotation()` method's transformation is applied to a Matrix3D + * object of a display object, the cached rotation property values of the display + * object are invalidated. + * + * One way to have a display object rotate around a specific point relative to its + * location is to set the translation of the object to the specified point, rotate + * the object using the `prependRotation()` method, and translate the object back to + * the original position. In the following example, the `myObject` 3D display object + * makes a y-axis rotation around the coordinate (10,10,0). + * + * ``` + * myObject.z = 1; + * myObject.transform.matrix3D.prependTranslation(10,10,0); + * myObject.transform.matrix3D.prependRotation(1, Vector3D.Y_AXIS); + * myObject.transform.matrix3D.prependTranslation(-10,-10,0); + * ``` + * + * @param degrees The degree of rotation. + * @param axis The axis or direction of rotation. The usual axes are the + * `X_AXIS (Vector3D(1,0,0))`, `Y_AXIS (Vector3D(0,1,0))`, and + * `Z_AXIS (Vector3D(0,0,1))`. This vector should have a length of one. + * @param pivotPoint A point that determines the center of rotation. The default + * pivot point for an object is its registration point. + */ public function prependRotation(degrees:Number, axis:Vector3D, pivotPoint:Vector3D = null):void { var m = new Matrix3D(); m.appendRotation(degrees, axis, pivotPoint); this.prepend(m); } + /** + * A Vector3D object that holds the position, the 3D coordinate (x,y,z) of a display + * object within the transformation's frame of reference. The `position` property + * provides immediate access to the translation vector of the display object's + * matrix without needing to decompose and recompose the matrix. + * + * With the `position` property, you can get the translation elements of the + * transformation matrix. + */ public function get position():Vector3D { - return new Vector3D(_rawData[12], _rawData[13], _rawData[14]); + return new Vector3D(this._rawData[12], this._rawData[13], this._rawData[14]); } + /** + * A Vector3D object that holds the position, the 3D coordinate (x,y,z) of a display + * object within the transformation's frame of reference. The `position` property + * provides immediate access to the translation vector of the display object's + * matrix without needing to decompose and recompose the matrix. + * + * With the `position` property, you set the translation elements of the + * transformation matrix. + */ public function set position(val:Vector3D):void { this._rawData[12] = val.x; this._rawData[13] = val.y; this._rawData[14] = val.z; } + /** + * Prepends a matrix by multiplying the current Matrix3D object by another Matrix3D + * object. The result combines both matrix transformations. + * + * Matrix multiplication is different from matrix addition. Matrix multiplication is + * not commutative. In other words, A times B is not equal to B times A. With the + * `prepend()` method, the multiplication happens from the right side, meaning the `rhs` + * Matrix3D object is on the right side of the multiplication operator. + * + * ``` + * thisMatrix = thisMatrix * rhs + * ``` + * + * The modifications made by `prepend()` method are object-space-relative. In other + * words, they are always relative to the object's initial frame of reference. + * + * The `prepend()` method replaces the current matrix with the prepended matrix. If + * you want to prepend two matrixes without altering the current matrix, first copy + * the current matrix by using the `clone()` method and then apply the `prepend()` + * method to the copy. + * + * @param rhs A right-hand-side of the matrix by which the current Matrix3D is + * multiplied. + */ public function prepend(rhs:Matrix3D):void { var m111:Number = rhs._rawData[0], m121:Number = rhs._rawData[4], @@ -376,18 +789,61 @@ package flash.geom { this._rawData[15] = m141 * m214 + m142 * m224 + m143 * m234 + m144 * m244; } + /** + * Prepends an incremental scale change along the x, y, and z axes to a Matrix3D + * object. When the Matrix3D object is applied to a display object, the matrix + * performs the scale changes before other transformations in the Matrix3D + * object. The changes are object-relative, relative to the frame of reference of + * the original position and orientation. The default scale factor is (1.0, 1.0, 1.0). + * + * The scale is defined as a set of three incremental changes along the three + * axes (x,y,z). You can multiply each axis with a different number. When the + * scale changes are applied to a display object, the object's size increases or + * decreases. For example, setting the x, y, and z axes to two doubles the size of + * the object, while setting the axes to 0.5 halves the size. To make sure that the + * scale transformation only affects a specific axis, set the other parameters to + * one. A parameter of one means no scale change along the specific axis. + * + * The `prependScale()` method can be used for resizing as well as for managing + * distortions, such as stretch or contract of a display object. It can also be used + * for zooming in and out on a location. Scale transformations are automatically + * performed during a display object's rotation and translation. + * + * The order of transformation matters. A resizing followed by a translation + * transformation produces a different effect than a translation followed by a + * resizing transformation. + * + * @param xScale A multiplier used to scale the object along the x axis. + * @param yScale A multiplier used to scale the object along the y axis. + * @param zScale A multiplier used to scale the object along the z axis. + */ public function prependScale(xScale:Number, yScale:Number, zScale:Number):void { var m = new Matrix3D(); m.appendScale(xScale, yScale, zScale); this.prepend(m); } + /** + * Copies all of the matrix data from the source Matrix3D object into the calling + * Matrix3D object. + * + * @param sourceMatrix3D The Matrix3D object from which to copy the data. + */ [API("674")] public function copyFrom(other:Matrix3D):void { // This makes a copy of other.rawData this._rawData = other.rawData; } + /** + * Copies all of the matrix data from the calling Matrix3D object into the + * provided vector. The optional index parameter allows you to select any target + * starting slot in the vector. + * + * @param vector The vector object to which to copy the data. + * @param index + * @param transpose + */ [API("674")] public function copyRawDataTo(vector:Vector., index:uint = 0, transpose:Boolean = false):void { if (transpose) { @@ -395,7 +851,7 @@ package flash.geom { } for (var i = 0; i < rawData.length; i++) { - vector[i + index] = _rawData[i]; + vector[i + index] = this._rawData[i]; } if (transpose) { @@ -403,71 +859,643 @@ package flash.geom { } } + /** + * Returns a new Matrix3D object that is an exact copy of the current Matrix3D object. + * + * @return A new Matrix3D object that is an exact copy of the current Matrix3D + * object. + */ public function clone():Matrix3D { return new Matrix3D(this.rawData); } + /** + * @param other + */ public function copyToMatrix3D(other:Matrix3D):void { - other.rawData = rawData; + other.rawData = this.rawData; } + /** + * Rotates the display object so that it faces a specified position. This method + * allows for an in-place modification to the orientation. The forward direction + * vector of the display object (the at Vector3D object) points at the specified + * world-relative position. The display object's up direction is specified with the + * up Vector3D object. + * + * The `pointAt()` method invalidates the cached rotation property value of the + * display object. The method decomposes the display object's matrix and modifies the + * rotation elements to have the object turn to the specified position. It then + * recomposes (updates) the display object's matrix, which performs the + * transformation. If the object is pointing at a moving target, such as a moving + * object's position, then with each subsequent call, the method has the object + * rotate toward the moving target. + * + * **Note:** If you use the `Matrix3D.pointAt()` method without setting the + * optional parameters, a target object does not face the specified world-relative + * position by default. You need to set the values for at to the -y-axis (0,-1,0) + * and up to the -z axis (0,0,-1). ** and where does it point to ??? + * + * @param pos The world-relative position of the target object. World-relative + * defines the object's transformation relative to the world space and coordinates, + * where all objects are positioned. + * @param at The object-relative vector that defines where the display object is + * pointing. Object-relative defines the object's transformation relative to the + * object space, the object's own frame of reference and coordinate system. Default + * value is the +y axis (0,1,0). + * @param up The object-relative vector that defines "up" for the display object. + * If the object is drawn looking down from above, the +z axis is its "up" vector. + * Object-relative defines the object's transformation relative to the object space, + * the object's own frame of reference and coordinate system. Default value is the + * +z-axis (0,0,1). + */ public function pointAt(pos:Vector3D, at:Vector3D = null, up:Vector3D = null):void { - stub_method("flash.geom.Matrix3D", "pointAt"); + if (at == null) { + at = new Vector3D(0, 0, 1); // Default aiming direction (forward along the Z axis) + } + + if (up == null) { + up = new Vector3D(0, 1, 0); // Default Up direction (up along the Y axis) + } + + // if the method is called with pos only, so that at and up is null + // flash does something i don't understand... + // it should use these vectors here => Default At and Default Up + // no idea what values are in use... when setting at and up everything looks really fine... + + // if up and at makes no sense flash return a identity matrix + // if hope it does this only for 0,1,2 4,5,6 8,9,10 + + //trace('args', pos, at, up); + + // Decompose the current matrix into components + var components:Vector. = this.decompose(); // 0 = translation, 1 = rotation, 2 = scale + var translation:Vector3D = components[0]; + var scale:Vector3D = components[2]; + +if (0) { + scale.x = Matrix3D.helperCustomRound(scale.x); + scale.y = Matrix3D.helperCustomRound(scale.y); + scale.z = Matrix3D.helperCustomRound(scale.z); +} + // we want our eye + var eye:Vector3D = pos.subtract(translation); // don't normalize eye afterwards... + if (at.z <= 0) { + eye.negate(); + } + //trace('eye', eye) + + // flash does here something that is not clear... if at has x,y other then 0 + // there is an additional transformation on the vector * logic unknown... + + // we should work here with normalized at and up, leave the args untouched... + var atNorm:Vector3D = at.clone(); + var upNorm:Vector3D = up.clone(); + + // .w is irrelavant when calling normalize ? looks so... Vector3D.normalize() ignores .w + atNorm.normalize(); + upNorm.normalize(); + + // Calculate the Z axis (the direction to the target) + var zAxis:Vector3D = atNorm.subtract(eye); + zAxis.normalize(); + + // the axis must be orthogonal + + // Clone the Up vector + var upVector:Vector3D = upNorm.clone(); + + if (1) { // not sure if needed... + // Adjust the Up vector to ensure it's orthogonal to the Z axis + var dirProjection:Vector3D = zAxis.clone(); + dirProjection.scaleBy(upVector.dotProduct(zAxis)); // Project upVector onto the zAxis + + upVector = upVector.subtract(dirProjection); // Subtract projection from upVector to make it orthogonal + + // Check if the Up vector is valid, otherwise create a default orthogonal vector + if (upVector.length > 0) { + upVector.normalize(); // Normalize if it's valid + } else { + // Create an orthogonal vector if Up and direction are too similar + if (zAxis.x != 0) { + upVector = new Vector3D(-zAxis.y, zAxis.x, 0); // Example orthogonal vector in XY plane + } else { + upVector = new Vector3D(1, 0, 0); // Fallback orthogonal vector + } + //trace('upVector'); + } + } + + // Calculate the X-axis (right vector, by cross product of Up and Z-axis) + var xAxis:Vector3D = upVector.crossProduct(zAxis); + xAxis.normalize(); + + // Calculate the Y-axis (Up vector, by cross product of Z-axis and X-axis) + var yAxis:Vector3D = zAxis.crossProduct(xAxis); + yAxis.normalize(); + yAxis.negate(); // bring it to flash 3d space + + // we should use recompose here... but then we need a quaternion + // see + // https://www.w3.org/TR/css-transforms-2/#matrix-interpolation + + var mr:Vector. = this.rawData; + + // we leave perspective untouched... [3,7,11,15] + // and translation [12,13,14] is allready set... no need to set it again + + mr[0] = xAxis.x; + mr[1] = xAxis.y; + mr[2] = xAxis.z; + //mr[3] = 0; + + mr[4] = yAxis.x; + mr[5] = yAxis.y; + mr[6] = yAxis.z; + //mr[7] = 0; + + mr[8] = zAxis.x; + mr[9] = zAxis.y; + mr[10] = zAxis.z; + //mr[11] = 0; + + //mr[15] = 1; + + // Apply skew (if provided) + if (components.length > 3) { + var skew:Vector3D = components[3]; + //trace('skew', skew); + + // Apply XY skew + mr[4] += mr[0] * skew.x; + mr[5] += mr[1] * skew.x; + mr[6] += mr[2] * skew.x; + + // Apply XZ skew + mr[8] += mr[0] * skew.y; + mr[9] += mr[1] * skew.y; + mr[10] += mr[2] * skew.y; + + // Apply YZ skew + mr[8] += mr[4] * skew.z; + mr[9] += mr[5] * skew.z; + mr[10] += mr[6] * skew.z; + } + + // Apply scale to X, Y, Z axes + for (var i:int = 0; i < 3; i++) { + mr[i] *= scale.x; // X-axis + mr[4 + i] *= scale.y; // Y-axis + mr[8 + i] *= scale.z; // Z-axis + } + + this.rawData = mr; } + /** + * Interpolates this matrix towards the translation, rotation, and scale + * transformations of the target matrix. + * + * The `interpolateTo()` method avoids the unwanted results that can occur when + * using methods such as the display object's axis rotation properties. The + * `interpolateTo()` method invalidates the cached value of the rotation property of + * the display object and converts the orientation elements of the display object's + * matrix to a quaternion before interpolation. This method guarantees the shortest, + * most efficient path for the rotation. It also produces a smooth, gimbal-lock-free + * rotation. A gimbal lock can occur when using Euler Angles, where each axis is + * handled independently. During the rotation around two or more axes, the axes can + * become aligned, leading to unexpected results. Quaternion rotation avoids the + * gimbal lock. + * + * **Note:** In case of interpolation, the scaling value of the matrix will reset and + * the matrix will be normalized. (which is wrong) + * + * Consecutive calls to the `interpolateTo()` method can produce the effect of a + * display object starting quickly and then slowly approaching another display + * object. For example, if the percent parameter is set to 0.1, the display object + * moves ten percent toward the target object specified by the `toMat` parameter. + * On subsequent calls or in subsequent frames, the object moves ten percent of the + * remaining 90 percent, then ten percent of the remaining distance, and continues + * until it reaches the target. + * + * @param toMat The target Matrix3D object. + * @param percent A value between 0 and 1 that determines the location of the + * display object relative to the target. The closer the value is to 1.0, the closer + * the display object is to its current position. The closer the value is to 0, the + * closer the display object is to the target. + */ + public function interpolateTo(toMat:Matrix3D, percent:Number):void { + // beware this logic is different to method interpolate, 0 means we are at the toMat + // so we start here with the toMat... + var m:Matrix3D = Matrix3D.interpolate(toMat, new Matrix3D(this.rawData), percent); + this.rawData = m.rawData; + } + + private static function helperCustomRound (value:Number):Number { + var integerPart:Number = Math.floor(value); + var decimalPart:Number = value - integerPart; + var firstSixDecimals:Number = Math.floor(decimalPart * 1000000); + + if (firstSixDecimals == 999999) { + return Math.ceil(value); + } else if (firstSixDecimals == 0) { + return Math.floor(value); + } + + return value; + } + + /** + * Interpolates the translation, rotation, and scale transformation of one matrix + * toward those of the target matrix. + * + * The `interpolate()` method avoids some of the unwanted results that can occur + * when using methods such as the display object's axis rotation properties. The + * `interpolate()` method invalidates the cached value of the rotation property of + * the display object and converts the orientation elements of the display object's + * matrix to a quaternion before interpolation. This method guarantees the shortest, + * most efficient path for the rotation. It also produces a smooth, gimbal-lock-free + * rotation. A gimbal lock can occur when using Euler Angles, where each axis is + * handled independently. During the rotation around two or more axes, the axes can + * become aligned, leading to unexpected results. Quaternion rotation avoids the + * gimbal lock. + * + * Consecutive calls to the `interpolate()` method can produce the effect of a + * display object starting quickly and then slowly approaching another display + * object. For example, if you set the `thisMat` parameter to the returned Matrix3D + * object, the `toMat` parameter to the target display object's associated Matrix3D + * object, and the `percent` parameter to 0.1, the display object moves ten percent + * toward the target object. On subsequent calls or in subsequent frames, the object + * moves ten percent of the remaining 90 percent, then ten percent of the remaining + * distance, and continues until it reaches the target. + * + * @param thisMat The Matrix3D object that is to be interpolated. + * @param toMat The target Matrix3D object. + * @param percent A value between 0 and 1 that determines the percent the + * `thisMat` Matrix3D object is interpolated toward the target Matrix3D object. + * @return A Matrix3D object with elements that place the values of the matrix + * between the original matrix and the target matrix. When the returned matrix is + * applied to the this display object, the object moves the specified percent closer + * to the target object. + */ + public static function interpolate(thisMat:Matrix3D, toMat:Matrix3D, percent:Number):Matrix3D { + + var debug:Boolean = false; + + // implemented a 'static function correct' with default value false + // to keep it compatible to older projects relying on the former logic... + + var method:String = Orientation3D.QUATERNION; + + var decomposedA:Vector., decomposedB:Vector.; + + // i pretty sure, this will be slow, decompose might take too much time... + // simple speed up, create a small cache for the last 10 matrix mr which stores the + // decompose data for thisMat and toMat + // useless when interpolating many different mr + + decomposedA = thisMat.decompose(method); + decomposedB = toMat.decompose(method); + + // 0 = translation, 1 = rotation, 2 = scale + // 3 = skew, 4 = perspective * own implementation + //trace('skew', decomposedA[3], decomposedB[3]); + //trace('pers', decomposedA[4], decomposedB[4]); + +if (0) { + decomposedA[2].x = Matrix3D.helperCustomRound(decomposedA[2].x); + decomposedA[2].y = Matrix3D.helperCustomRound(decomposedA[2].y); + decomposedA[2].z = Matrix3D.helperCustomRound(decomposedA[2].z); + + decomposedB[2].x = Matrix3D.helperCustomRound(decomposedB[2].x); + decomposedB[2].y = Matrix3D.helperCustomRound(decomposedB[2].y); + decomposedB[2].z = Matrix3D.helperCustomRound(decomposedB[2].z); +} + if (debug) trace('scales', decomposedA[2], decomposedB[2]); + + var v0:Vector3D = decomposedA[1]; + var v1:Vector3D = decomposedB[1]; + + var dot:Number = v0.x * v1.x + v0.y * v1.y + v0.z * v1.z + v0.w * v1.w; // we reuse this later below... + var cosOmega:Number = dot; + + if (cosOmega < 0) { + cosOmega = -cosOmega; + } + + var k0:Number, k1:Number; + + if (cosOmega > 0.9999 || percent == 0 || percent == 1) { + // If the quaternions are nearly identical, perform a linear interpolation + k0 = 1 - percent; + k1 = percent; + if (debug) trace('linear'); + } else { + // Otherwise, use spherical linear interpolation (Slerp) + var sinOmega:Number = Math.sqrt(1 - cosOmega * cosOmega); + var omega:Number = Math.atan2(sinOmega, cosOmega); + var oneOverSinOmega:Number = 1 / sinOmega; + k0 = Math.sin((1 - percent) * omega) * oneOverSinOmega; + k1 = Math.sin(percent * omega) * oneOverSinOmega; + if (debug) trace('slerp'); + } + if (debug) trace('k0', k0, 'k1', k1); + + // beware flash does not use slerp everywhere... + // so far i've checked the logic it's only used for the quaternion + + var tx:Number, ty:Number, tz:Number; + // pretty sure this is what flash does... lerp... yes... + tx = decomposedA[0].x * (1 - percent) + decomposedB[0].x * percent; + ty = decomposedA[0].y * (1 - percent) + decomposedB[0].y * percent; + tz = decomposedA[0].z * (1 - percent) + decomposedB[0].z * percent; + + var x:Number, y:Number, z:Number, w:Number; + + if (debug) trace('v0', v0, v0.w); // for the first vector * rotation + if (debug) trace('v1', v1, v1.w); // for the second vector * rotation + + // If the angle is greater than 180 degrees, negate the quaternion to take the shorter path + if (dot < 0.0) { + // all parts of the quaternion must be touched... don't use .negate(); .w is not touched... + if (debug) trace('quaternion v1 inverted'); + v1.x = -v1.x; + v1.y = -v1.y; + v1.z = -v1.z; + v1.w = -v1.w; + } + + x = v0.x * k0 + v1.x * k1; + y = v0.y * k0 + v1.y * k1; + z = v0.z * k0 + v1.z * k1; + w = v0.w * k0 + v1.w * k1; + + // Normalization of the quaternion, otherwise we could get a not wanted scale effects + if (1) { + var magnitude:Number = Math.sqrt(x * x + y * y + z * z + w * w); + x /= magnitude; + y /= magnitude; + z /= magnitude; + w /= magnitude; + } + + var scale = new Vector3D(); + // lerp... yes... + scale.x = decomposedA[2].x * (1 - percent) + decomposedB[2].x * percent; + scale.y = decomposedA[2].y * (1 - percent) + decomposedB[2].y * percent; + scale.z = decomposedA[2].z * (1 - percent) + decomposedB[2].z * percent; + +if (0) { + scale.x = Matrix3D.helperCustomRound(scale.x); + scale.y = Matrix3D.helperCustomRound(scale.y); + scale.z = Matrix3D.helperCustomRound(scale.z); +} + if (debug) trace('scale', scale); // not in use by flash... + + var skew:Vector3D = new Vector3D(0, 0, 0); + if (decomposedA.length > 3 && decomposedB.length > 3) { // if provided + // lerp + skew.x = decomposedA[3].x * (1 - percent) + decomposedB[3].x * percent; + skew.y = decomposedA[3].y * (1 - percent) + decomposedB[3].y * percent; + skew.z = decomposedA[3].z * (1 - percent) + decomposedB[3].z * percent; + } + if (debug) trace('skew', skew); // not in use by flash... + + var perspective:Vector3D = new Vector3D(0, 0, 0, 1); + if (decomposedA.length > 4 && decomposedB.length > 4) { // if provided + // lerp + perspective.x = decomposedA[4].x * (1 - percent) + decomposedB[4].x * percent; + perspective.y = decomposedA[4].y * (1 - percent) + decomposedB[4].y * percent; + perspective.z = decomposedA[4].z * (1 - percent) + decomposedB[4].z * percent; + perspective.w = decomposedA[4].w * (1 - percent) + decomposedB[4].w * percent; + + // should we normalize ??? + } + if (debug) trace('perspective', perspective, perspective.w); // not in use by flash... + + + // let recompose do our work + var m:Matrix3D = new Matrix3D(); + var vec = new Vector.([]); + + vec.push(new Vector3D(tx, ty, tz)); // translation + vec.push(new Vector3D(x, y, z, w)); // rotation + if (Matrix3D.correct) { + vec.push(scale); // scale + vec.push(skew); // skew + vec.push(perspective); // perspective + } else { + vec.push(new Vector3D(1, 1, 1)); // scale + } + + m.recompose(vec, method); + + if (debug) trace('mr', m.rawData); + + return m; + +/* + // double code not needed anymore... + + var mr:Vector. = new Vector.(16, true); + + // recompose matrix raw with quaternion (without 12,13,14 this is basically quaternion to matrix) + mr[0] = (1 - 2 * y * y - 2 * z * z); + mr[1] = (2 * x * y + 2 * w * z); + mr[2] = (2 * x * z - 2 * w * y); + mr[3] = 0; + mr[4] = (2 * x * y - 2 * w * z); + mr[5] = (1 - 2 * x * x - 2 * z * z); + mr[6] = (2 * y * z + 2 * w * x); + mr[7] = 0; + mr[8] = (2 * x * z + 2 * w * y); + mr[9] = (2 * y * z - 2 * w * x); + mr[10] = (1 - 2 * x * x - 2 * y * y); + mr[11] = 0; + mr[12] = tx; + mr[13] = ty; + mr[14] = tz; + mr[15] = 1; + + // note: handle scale, skew and perspective + // skew and perspective is available in extended decompose now + // just do a simple lerp for skew and perspective * done... but not tested... + + if (Matrix3D.correct) { + + // Apply skew * tbd testing + + // Apply XY skew + mr[4] += mr[0] * skew.x; + mr[5] += mr[1] * skew.x; + mr[6] += mr[2] * skew.x; + + // Apply XZ skew + mr[8] += mr[0] * skew.y; + mr[9] += mr[1] * skew.y; + mr[10] += mr[2] * skew.y; + + // Apply YZ skew + mr[8] += mr[4] * skew.z; + mr[9] += mr[5] * skew.z; + mr[10] += mr[6] * skew.z; + + if (debug) trace('skew mr', mr); + + // Apply scale + for (var i:int = 0; i < 3; i++) { + mr[i] *= scale.x; // X-axis + mr[4 + i] *= scale.y; // Y-axis + mr[8 + i] *= scale.z; // Z-axis + } + + if (debug) trace('scaled mr', mr); + + // Apply perspective * tbd testing + + mr[3] = perspective.x; + mr[7] = perspective.y; + mr[11] = perspective.z; + mr[15] = perspective.w; + + if (debug) trace('perspective mr', mr); + + + } + + // ??? should we avoid 0 scales like in recompose ??? + + var m:Matrix3D = new Matrix3D(mr); + + return m; +*/ + } + + /** + * Sets the transformation matrix's translation, rotation, and scale settings. Unlike + * the incremental changes made by the display object's rotation properties or + * Matrix3D object's rotation methods, the changes made by `recompose()` method are + * absolute changes. The `recompose()` method overwrites the matrix's transformation. + * + * To modify the matrix's transformation with an absolute parent frame of reference, + * retrieve the settings with the decompose() method and make the appropriate + * changes. You can then set the Matrix3D object to the modified transformation + * using the `recompose()` method. + * + * The `recompose()` method's parameter specifies the orientation style that was + * used for the transformation. The default orientation is eulerAngles, which defines + * the orientation with three separate angles of rotation for each axis. The + * rotations occur consecutively and do not change the axis of each other. The + * display object's axis rotation properties perform Euler Angles orientation style + * transformation. The other orientation style options are axisAngle and quaternion. + * The Axis Angle orientation uses the combination of an axis and an angle to + * determine the orientation. The axis around which the object is rotated is a unit + * vector that represents a direction. The angle represents the magnitude of the + * rotation about the vector. The direction also determines where a display object + * is facing and the angle determines which way is up. The `appendRotation()` and + * `prependRotation()` methods use the Axis Angle orientation. The quaternion + * orientation uses complex numbers and the fourth element of a vector. An + * orientation is represented by the three axes of rotation (x,y,z) and an angle of + * rotation (w). The interpolate() method uses quaternion. + * + * @param components A Vector of three Vector3D objects that replace the Matrix3D + * object's translation[0], rotation[1], scale[2], and optional skew[3] and perspective[4] elements + * @param orientationStyle An optional parameter that determines the orientation + * style used for the matrix transformation. The three types of orientation styles + * are eulerAngles (constant `EULER_ANGLES`), axisAngle (constant `AXIS_ANGLE`), and + * quaternion (constant `QUATERNION`). For additional information on the different + * orientation style, see the geom.Orientation3D class. + * @return Returns `false` if any of the Vector3D elements of the components + * Vector do not exist or are `null`. + */ // Based on OpenFL: https://github.com/openfl/openfl/blob/971a4c9e43b5472fd84d73920a2b7c1b3d8d9257/src/openfl/geom/Matrix3D.hx#L1437 public function recompose(components:Vector., orientationStyle:String = "eulerAngles"):Boolean { - checkOrientation(orientationStyle); + + if (!(orientationStyle == Orientation3D.AXIS_ANGLE || orientationStyle == Orientation3D.EULER_ANGLES || orientationStyle == Orientation3D.QUATERNION)) { + throw new Error("Error #2187: Invalid orientation style " + orientationStyle + ". Value must be one of 'Orientation3D.EULER_ANGLES', 'Orientation3D.AXIS_ANGLE', or 'Orientation3D.QUATERNION'.", 2187); + } + + var rot:Vector3D = components[1]; if (orientationStyle == Orientation3D.QUATERNION) { - // Flash throws exceptions from 'recompose' certain values of 'components', + // Flash throws exceptions from 'recompose' certain values of 'components' // which we need to reproduce. See the 'matrix3d_compose' test - stub_method("flash.geom.Matrix3D", "recompose", "Orientation3D.QUATERNION"); + + // Check if .w is defined * not sure if this ever happens... just to be sure... + if (isNaN(rot.w)) { + throw new Error("Error #2187: Invalid orientation style " + orientationStyle + ". Invalid quaternion: 'w' component is undefined."); + } + + // Normalize the quaternion (if it's not already normalized) + var length:Number = Math.sqrt(rot.x * rot.x + rot.y * rot.y + rot.z * rot.z + rot.w * rot.w); + + // If the length is zero or not valid, the quaternion is invalid + if (length == 0) { + throw new Error("Error #2187: Invalid orientation style " + orientationStyle + ". Invalid quaternion: zero length."); + } + + // If the quaternion is not normalized, normalize it + if (length != 1) { + rot.x /= length; + rot.y /= length; + rot.z /= length; + rot.w /= length; + } } + // RUFFLE - unlike in OpenFL, we continue on even if some of the 'scale' components are 0 if (components.length < 3) { return false; } - identity(); + this.identity(); // if anything goes wrong, should we stay with the identity matrix ?? + + // 0 = translation, 1 = rotation, 2 = scale + var tx:Number = components[0].x; + var ty:Number = components[0].y; + var tz:Number = components[0].z; + + // rotation see above... + + var scale = new Vector3D(); + scale.x = components[2].x; + scale.y = components[2].y; + scale.z = components[2].z; - var scale = []; - scale[0] = scale[1] = scale[2] = components[2].x; - scale[4] = scale[5] = scale[6] = components[2].y; - scale[8] = scale[9] = scale[10] = components[2].z; + var mr:Vector. = this.rawData; switch (orientationStyle) { case Orientation3D.EULER_ANGLES: - var cx = Math.cos(components[1].x); - var cy = Math.cos(components[1].y); - var cz = Math.cos(components[1].z); - var sx = Math.sin(components[1].x); - var sy = Math.sin(components[1].y); - var sz = Math.sin(components[1].z); - - _rawData[0] = cy * cz * scale[0]; - _rawData[1] = cy * sz * scale[1]; - _rawData[2] = -sy * scale[2]; - _rawData[3] = 0; - _rawData[4] = (sx * sy * cz - cx * sz) * scale[4]; - _rawData[5] = (sx * sy * sz + cx * cz) * scale[5]; - _rawData[6] = sx * cy * scale[6]; - _rawData[7] = 0; - _rawData[8] = (cx * sy * cz + sx * sz) * scale[8]; - _rawData[9] = (cx * sy * sz - sx * cz) * scale[9]; - _rawData[10] = cx * cy * scale[10]; - _rawData[11] = 0; - _rawData[12] = components[0].x; - _rawData[13] = components[0].y; - _rawData[14] = components[0].z; - _rawData[15] = 1; + var cx:Number = Math.cos(rot.x); + var cy:Number = Math.cos(rot.y); + var cz:Number = Math.cos(rot.z); + var sx:Number = Math.sin(rot.x); + var sy:Number = Math.sin(rot.y); + var sz:Number = Math.sin(rot.z); + + mr[0] = cy * cz; + mr[1] = cy * sz; + mr[2] = -sy; + mr[3] = 0; + mr[4] = (sx * sy * cz - cx * sz); + mr[5] = (sx * sy * sz + cx * cz); + mr[6] = sx * cy; + mr[7] = 0; + mr[8] = (cx * sy * cz + sx * sz); + mr[9] = (cx * sy * sz - sx * cz); + mr[10] = cx * cy; + mr[11] = 0; + mr[12] = tx; + mr[13] = ty; + mr[14] = tz; + mr[15] = 1; break; - + case Orientation3D.QUATERNION: default: - var x = components[1].x; - var y = components[1].y; - var z = components[1].z; - var w = components[1].w; + var x:Number = rot.x; + var y:Number = rot.y; + var z:Number = rot.z; + var w:Number = rot.w; if (orientationStyle == Orientation3D.AXIS_ANGLE) { x *= Math.sin(w / 2); @@ -476,39 +1504,79 @@ package flash.geom { w = Math.cos(w / 2); } - _rawData[0] = (1 - 2 * y * y - 2 * z * z) * scale[0]; - _rawData[1] = (2 * x * y + 2 * w * z) * scale[1]; - _rawData[2] = (2 * x * z - 2 * w * y) * scale[2]; - _rawData[3] = 0; - _rawData[4] = (2 * x * y - 2 * w * z) * scale[4]; - _rawData[5] = (1 - 2 * x * x - 2 * z * z) * scale[5]; - _rawData[6] = (2 * y * z + 2 * w * x) * scale[6]; - _rawData[7] = 0; - _rawData[8] = (2 * x * z + 2 * w * y) * scale[8]; - _rawData[9] = (2 * y * z - 2 * w * x) * scale[9]; - _rawData[10] = (1 - 2 * x * x - 2 * y * y) * scale[10]; - _rawData[11] = 0; - _rawData[12] = components[0].x; - _rawData[13] = components[0].y; - _rawData[14] = components[0].z; - _rawData[15] = 1; + mr[0] = (1 - 2 * y * y - 2 * z * z); + mr[1] = (2 * x * y + 2 * w * z); + mr[2] = (2 * x * z - 2 * w * y); + mr[3] = 0; + mr[4] = (2 * x * y - 2 * w * z); + mr[5] = (1 - 2 * x * x - 2 * z * z); + mr[6] = (2 * y * z + 2 * w * x); + mr[7] = 0; + mr[8] = (2 * x * z + 2 * w * y); + mr[9] = (2 * y * z - 2 * w * x); + mr[10] = (1 - 2 * x * x - 2 * y * y); + mr[11] = 0; + mr[12] = tx; + mr[13] = ty; + mr[14] = tz; + mr[15] = 1; } - if (components[2].x == 0) { - _rawData[0] = 1e-15; + // note the order is extremly important, skew must be applied before scale + + // Apply skew (if provided) + if (components.length > 3) { + var skew:Vector3D = components[3]; + + // Apply XY skew + mr[4] += mr[0] * skew.x; + mr[5] += mr[1] * skew.x; + mr[6] += mr[2] * skew.x; + + // Apply XZ skew + mr[8] += mr[0] * skew.y; + mr[9] += mr[1] * skew.y; + mr[10] += mr[2] * skew.y; + + // Apply YZ skew + mr[8] += mr[4] * skew.z; + mr[9] += mr[5] * skew.z; + mr[10] += mr[6] * skew.z; } - if (components[2].y == 0) { - _rawData[5] = 1e-15; + // Apply scale + for (var i:int = 0; i < 3; i++) { + mr[i] *= scale.x; // X-axis + mr[4 + i] *= scale.y; // Y-axis + mr[8 + i] *= scale.z; // Z-axis } - if (components[2].z == 0) { - _rawData[10] = 1e-15; + // Apply perspective (if provided) + if (components.length > 4) { + var perspective:Vector3D = components[4]; + + mr[3] = perspective.x; + mr[7] = perspective.y; + mr[11] = perspective.z; + mr[15] = perspective.w; } - return !(components[2].x == 0 || components[2].y == 0 || components[2].y == 0); + // Avoid 0 scales + if (scale.x == 0) mr[0] = 1e-15; + if (scale.y == 0) mr[5] = 1e-15; + if (scale.z == 0) mr[10] = 1e-15; + + this.rawData = mr; + + return !(scale.x == 0 || scale.y == 0 || scale.y == 0); } + /** + * Copies specific column of the calling Matrix3D object into the Vector3D object. + + * @param column The column from which to copy the data. + * @param vector3D The destination Vector3D object of the copy. + */ [API("674")] public function copyColumnTo(column:uint, vector3D:Vector3D):void { if (column > 3) { @@ -516,35 +1584,41 @@ package flash.geom { } switch (column) { case 0: - vector3D.x = _rawData[0]; - vector3D.y = _rawData[1]; - vector3D.z = _rawData[2]; - vector3D.w = _rawData[3]; + vector3D.x = this._rawData[0]; + vector3D.y = this._rawData[1]; + vector3D.z = this._rawData[2]; + vector3D.w = this._rawData[3]; break; case 1: - vector3D.x = _rawData[4]; - vector3D.y = _rawData[5]; - vector3D.z = _rawData[6]; - vector3D.w = _rawData[7]; + vector3D.x = this._rawData[4]; + vector3D.y = this._rawData[5]; + vector3D.z = this._rawData[6]; + vector3D.w = this._rawData[7]; break; case 2: - vector3D.x = _rawData[8]; - vector3D.y = _rawData[9]; - vector3D.z = _rawData[10]; - vector3D.w = _rawData[11]; + vector3D.x = this._rawData[8]; + vector3D.y = this._rawData[9]; + vector3D.z = this._rawData[10]; + vector3D.w = this._rawData[11]; break; case 3: - vector3D.x = _rawData[12]; - vector3D.y = _rawData[13]; - vector3D.z = _rawData[14]; - vector3D.w = _rawData[15]; + vector3D.x = this._rawData[12]; + vector3D.y = this._rawData[13]; + vector3D.z = this._rawData[14]; + vector3D.w = this._rawData[15]; break; } } + /** + * Copies a Vector3D object into specific column of the calling Matrix3D object. + * + * @param column The destination column of the copy. + * @param vector3D The Vector3D object from which to copy the data. + */ [API("674")] public function copyColumnFrom(column:uint, vector3D:Vector3D):void { if (column > 3) { @@ -552,47 +1626,127 @@ package flash.geom { } switch (column) { case 0: - _rawData[0] = vector3D.x; - _rawData[1] = vector3D.y; - _rawData[2] = vector3D.z; - _rawData[3] = vector3D.w; + this._rawData[0] = vector3D.x; + this._rawData[1] = vector3D.y; + this._rawData[2] = vector3D.z; + this._rawData[3] = vector3D.w; break; case 1: - _rawData[4] = vector3D.x; - _rawData[5] = vector3D.y; - _rawData[6] = vector3D.z; - _rawData[7] = vector3D.w; + this._rawData[4] = vector3D.x; + this._rawData[5] = vector3D.y; + this._rawData[6] = vector3D.z; + this._rawData[7] = vector3D.w; break; case 2: - _rawData[8] = vector3D.x; - _rawData[9] = vector3D.y; - _rawData[10] = vector3D.z; - _rawData[11] = vector3D.w; + this._rawData[8] = vector3D.x; + this._rawData[9] = vector3D.y; + this._rawData[10] = vector3D.z; + this._rawData[11] = vector3D.w; break; case 3: - _rawData[12] = vector3D.x; - _rawData[13] = vector3D.y; - _rawData[14] = vector3D.z; - _rawData[15] = vector3D.w; + this._rawData[12] = vector3D.x; + this._rawData[13] = vector3D.y; + this._rawData[14] = vector3D.z; + this._rawData[15] = vector3D.w; break; } } + /** + * Returns the transformation matrix's translation, rotation, and scale settings as + * a Vector of three Vector3D objects. The first Vector3D object holds the + * translation elements. The second Vector3D object holds the rotation elements. + * The third Vector3D object holds the scale elements. + * + * Some Matrix3D methods, such as the `interpolateTo()` method, automatically + * decompose and recompose the matrix to perform their transformation. + * + * To modify the matrix's transformation with an absolute parent frame of reference, + * retrieve the settings with the `decompose()` method and make the appropriate + * changes. You can then set the Matrix3D object to the modified transformation + * using the `recompose()` method. + * + * The `decompose()` method's parameter specifies the orientation style that is + * meant to be used for the transformation. The default orientation is `eulerAngles`, + * which defines the orientation with three separate angles of rotation for each + * axis. The rotations occur consecutively and do not change the axis of each other. + * The display object's axis rotation properties perform Euler Angles orientation + * style transformation. The other orientation style options are `axisAngle` and + * `quaternion`. The Axis Angle orientation uses a combination of an axis and an + * angle to determine the orientation. The axis around which the object is rotated + * is a unit vector that represents a direction. The angle represents the magnitude + * of the rotation about the vector. The direction also determines where a display + * object is facing and the angle determines which way is up. The `appendRotation()` + * and `prependRotation()` methods use the Axis Angle orientation. The `quaternion` + * orientation uses complex numbers and the fourth element of a vector. The three + * axes of rotation (x,y,z) and an angle of rotation (w) represent the orientation. + * The `interpolate()` method uses quaternion. + * + * @param orientationStyle An optional parameter that determines the orientation + * style used for the matrix transformation. The three types of orientation style + * are `eulerAngles` (constant `EULER_ANGLES`), `axisAngle` (constant `AXIS_ANGLE`), + * and `quaternion` (constant `QUATERNION`). For additional information on the + * different orientation style, see the geom.Orientation3D class. + * @return A Vector of three Vector3D objects, each holding the translation, + * rotation, and scale settings, respectively. + */ public function decompose(orientationStyle:String = "eulerAngles"):Vector. { - checkOrientation(orientationStyle); - var vec = new Vector.([]); - var m = clone(); - var mr = m.rawData; + if (!(orientationStyle == Orientation3D.AXIS_ANGLE || orientationStyle == Orientation3D.EULER_ANGLES || orientationStyle == Orientation3D.QUATERNION)) { + throw new Error("Error #2187: Invalid orientation style " + orientationStyle + ". Value must be one of 'Orientation3D.EULER_ANGLES', 'Orientation3D.AXIS_ANGLE', or 'Orientation3D.QUATERNION'.", 2187); + } + + var mr = this.rawData; var pos = new Vector3D(mr[12], mr[13], mr[14]); mr[12] = 0; mr[13] = 0; mr[14] = 0; + var perspective = new Vector3D(0, 0, 0, 1); + + if (mr[3] != 0 || mr[7] != 0 || mr[11] != 0) { + + if (1) { + + perspective.x = mr[3]; + perspective.y = mr[7]; + perspective.z = mr[11]; + perspective.w = mr[15]; + + mr[3] = 0; + mr[7] = 0; + mr[11] = 0; + mr[15] = 1; + + } else { + + var rhs = new Vector3D(mr[3], mr[7], mr[11], mr[15]); + + // Remove the perspective values from the matrix + mr[3] = 0; + mr[7] = 0; + mr[11] = 0; + mr[15] = 1; // Set the Homogeneous value to 1 + + // Invert the matrix + var inversePerspectiveMatrix = this.clone(); // clone needed here... + inversePerspectiveMatrix.invert(); + + // Transpose the inverted matrix + var transposedInverse = inversePerspectiveMatrix.transpose(); + + // Calculate the perspective by multiplying by the transposed matrix + perspective = transposedInverse.transformVector(rhs); + + } + } + + // note the order is extremly important, scale must be removed before skew + var scale = new Vector3D(); scale.x = Math.sqrt(mr[0] * mr[0] + mr[1] * mr[1] + mr[2] * mr[2]); @@ -613,6 +1767,25 @@ package flash.geom { mr[9] /= scale.z; mr[10] /= scale.z; + var skew = new Vector3D(0, 0, 0); + // Calculate skew.x (XY scissor factor) + skew.x = mr[0] * mr[4] + mr[1] * mr[5] + mr[2] * mr[6]; + mr[4] -= mr[0] * skew.x; // Remove the XY scissor factor from the Y axis + mr[5] -= mr[1] * skew.x; + mr[6] -= mr[2] * skew.x; + + // Calculate skew.y (XZ scissor factor) + skew.y = mr[0] * mr[8] + mr[1] * mr[9] + mr[2] * mr[10]; + mr[8] -= mr[0] * skew.y; // Remove the XZ scissor factor from the Z axis + mr[9] -= mr[1] * skew.y; + mr[10] -= mr[2] * skew.y; + + // Calculate skew.z (YZ scissor factor) + skew.z = mr[4] * mr[8] + mr[5] * mr[9] + mr[6] * mr[10]; + mr[8] -= mr[4] * skew.z; // Remove the YZ scissor factor from the Z axis + mr[9] -= mr[5] * skew.z; + mr[10] -= mr[6] * skew.z; + var rot = new Vector3D(); switch (orientationStyle) { @@ -678,13 +1851,43 @@ package flash.geom { break; } + var vec = new Vector.([]); + vec.push(pos); vec.push(rot); vec.push(scale); + vec.push(skew); + vec.push(perspective); return vec; } + /** + * Inverts the current matrix. An inverted matrix is the same size as the original + * but performs the opposite transformation of the original matrix. For example, if + * the original matrix has an object rotate around the x axis in one direction, the + * inverse of the matrix will have the object rotate around the axis in the opposite + * direction. Applying an inverted matrix to an object undoes the transformation + * performed by the original matrix. If a matrix is multiplied by its inverse + * matrix, the result is an identity matrix. + * + * An inverse of a matrix can be used to divide one matrix by another. The way to + * divide matrix A by matrix B is to multiply matrix A by the inverse of matrix B. + * The inverse matrix can also be used with a camera space. When the camera moves in + * the world space, the object in the world needs to move in the opposite direction + * to transform from the world view to the camera or view space. For example, if the + * camera moves closer, the objects becomes bigger. In other words, if the camera + * moves down the world z axis, the object moves up world z axis. + * + * The `invert()` method replaces the current matrix with an inverted matrix. If you + * want to invert a matrix without altering the current matrix, first copy the + * current matrix by using the clone() method and then apply the `invert()` method + * to the copy. + * + * The Matrix3D object must be invertible. + + * @return Returns `true` if the matrix was successfully inverted. + */ public function invert():Boolean { var d = determinant; var invertable = Math.abs(d) > 0.00000000001; @@ -692,60 +1895,64 @@ package flash.geom { if (invertable) { d = 1 / d; - var m11:Number = _rawData[0]; - var m21:Number = _rawData[4]; - var m31:Number = _rawData[8]; - var m41:Number = _rawData[12]; - var m12:Number = _rawData[1]; - var m22:Number = _rawData[5]; - var m32:Number = _rawData[9]; - var m42:Number = _rawData[13]; - var m13:Number = _rawData[2]; - var m23:Number = _rawData[6]; - var m33:Number = _rawData[10]; - var m43:Number = _rawData[14]; - var m14:Number = _rawData[3]; - var m24:Number = _rawData[7]; - var m34:Number = _rawData[11]; - var m44:Number = _rawData[15]; - - _rawData[0] = d * (m22 * (m33 * m44 - m43 * m34) - m32 * (m23 * m44 - m43 * m24) + m42 * (m23 * m34 - m33 * m24)); - _rawData[1] = -d * (m12 * (m33 * m44 - m43 * m34) - m32 * (m13 * m44 - m43 * m14) + m42 * (m13 * m34 - m33 * m14)); - _rawData[2] = d * (m12 * (m23 * m44 - m43 * m24) - m22 * (m13 * m44 - m43 * m14) + m42 * (m13 * m24 - m23 * m14)); - _rawData[3] = -d * (m12 * (m23 * m34 - m33 * m24) - m22 * (m13 * m34 - m33 * m14) + m32 * (m13 * m24 - m23 * m14)); - _rawData[4] = -d * (m21 * (m33 * m44 - m43 * m34) - m31 * (m23 * m44 - m43 * m24) + m41 * (m23 * m34 - m33 * m24)); - _rawData[5] = d * (m11 * (m33 * m44 - m43 * m34) - m31 * (m13 * m44 - m43 * m14) + m41 * (m13 * m34 - m33 * m14)); - _rawData[6] = -d * (m11 * (m23 * m44 - m43 * m24) - m21 * (m13 * m44 - m43 * m14) + m41 * (m13 * m24 - m23 * m14)); - _rawData[7] = d * (m11 * (m23 * m34 - m33 * m24) - m21 * (m13 * m34 - m33 * m14) + m31 * (m13 * m24 - m23 * m14)); - _rawData[8] = d * (m21 * (m32 * m44 - m42 * m34) - m31 * (m22 * m44 - m42 * m24) + m41 * (m22 * m34 - m32 * m24)); - _rawData[9] = -d * (m11 * (m32 * m44 - m42 * m34) - m31 * (m12 * m44 - m42 * m14) + m41 * (m12 * m34 - m32 * m14)); - _rawData[10] = d * (m11 * (m22 * m44 - m42 * m24) - m21 * (m12 * m44 - m42 * m14) + m41 * (m12 * m24 - m22 * m14)); - _rawData[11] = -d * (m11 * (m22 * m34 - m32 * m24) - m21 * (m12 * m34 - m32 * m14) + m31 * (m12 * m24 - m22 * m14)); - _rawData[12] = -d * (m21 * (m32 * m43 - m42 * m33) - m31 * (m22 * m43 - m42 * m23) + m41 * (m22 * m33 - m32 * m23)); - _rawData[13] = d * (m11 * (m32 * m43 - m42 * m33) - m31 * (m12 * m43 - m42 * m13) + m41 * (m12 * m33 - m32 * m13)); - _rawData[14] = -d * (m11 * (m22 * m43 - m42 * m23) - m21 * (m12 * m43 - m42 * m13) + m41 * (m12 * m23 - m22 * m13)); - _rawData[15] = d * (m11 * (m22 * m33 - m32 * m23) - m21 * (m12 * m33 - m32 * m13) + m31 * (m12 * m23 - m22 * m13)); + var m11:Number = this._rawData[0]; + var m21:Number = this._rawData[4]; + var m31:Number = this._rawData[8]; + var m41:Number = this._rawData[12]; + var m12:Number = this._rawData[1]; + var m22:Number = this._rawData[5]; + var m32:Number = this._rawData[9]; + var m42:Number = this._rawData[13]; + var m13:Number = this._rawData[2]; + var m23:Number = this._rawData[6]; + var m33:Number = this._rawData[10]; + var m43:Number = this._rawData[14]; + var m14:Number = this._rawData[3]; + var m24:Number = this._rawData[7]; + var m34:Number = this._rawData[11]; + var m44:Number = this._rawData[15]; + + this._rawData[0] = d * (m22 * (m33 * m44 - m43 * m34) - m32 * (m23 * m44 - m43 * m24) + m42 * (m23 * m34 - m33 * m24)); + this._rawData[1] = -d * (m12 * (m33 * m44 - m43 * m34) - m32 * (m13 * m44 - m43 * m14) + m42 * (m13 * m34 - m33 * m14)); + this._rawData[2] = d * (m12 * (m23 * m44 - m43 * m24) - m22 * (m13 * m44 - m43 * m14) + m42 * (m13 * m24 - m23 * m14)); + this._rawData[3] = -d * (m12 * (m23 * m34 - m33 * m24) - m22 * (m13 * m34 - m33 * m14) + m32 * (m13 * m24 - m23 * m14)); + this._rawData[4] = -d * (m21 * (m33 * m44 - m43 * m34) - m31 * (m23 * m44 - m43 * m24) + m41 * (m23 * m34 - m33 * m24)); + this._rawData[5] = d * (m11 * (m33 * m44 - m43 * m34) - m31 * (m13 * m44 - m43 * m14) + m41 * (m13 * m34 - m33 * m14)); + this._rawData[6] = -d * (m11 * (m23 * m44 - m43 * m24) - m21 * (m13 * m44 - m43 * m14) + m41 * (m13 * m24 - m23 * m14)); + this._rawData[7] = d * (m11 * (m23 * m34 - m33 * m24) - m21 * (m13 * m34 - m33 * m14) + m31 * (m13 * m24 - m23 * m14)); + this._rawData[8] = d * (m21 * (m32 * m44 - m42 * m34) - m31 * (m22 * m44 - m42 * m24) + m41 * (m22 * m34 - m32 * m24)); + this._rawData[9] = -d * (m11 * (m32 * m44 - m42 * m34) - m31 * (m12 * m44 - m42 * m14) + m41 * (m12 * m34 - m32 * m14)); + this._rawData[10] = d * (m11 * (m22 * m44 - m42 * m24) - m21 * (m12 * m44 - m42 * m14) + m41 * (m12 * m24 - m22 * m14)); + this._rawData[11] = -d * (m11 * (m22 * m34 - m32 * m24) - m21 * (m12 * m34 - m32 * m14) + m31 * (m12 * m24 - m22 * m14)); + this._rawData[12] = -d * (m21 * (m32 * m43 - m42 * m33) - m31 * (m22 * m43 - m42 * m23) + m41 * (m22 * m33 - m32 * m23)); + this._rawData[13] = d * (m11 * (m32 * m43 - m42 * m33) - m31 * (m12 * m43 - m42 * m13) + m41 * (m12 * m33 - m32 * m13)); + this._rawData[14] = -d * (m11 * (m22 * m43 - m42 * m23) - m21 * (m12 * m43 - m42 * m13) + m41 * (m12 * m23 - m22 * m13)); + this._rawData[15] = d * (m11 * (m22 * m33 - m32 * m23) - m21 * (m12 * m33 - m32 * m13) + m31 * (m12 * m23 - m22 * m13)); } return invertable; } + /** + * A Number that determines whether a matrix is invertible. + * + * A Matrix3D object must be invertible. You can use the `determinant` property to make + * sure that a Matrix3D object is invertible. If determinant is zero, an inverse of + * the matrix does not exist. For example, if an entire row or column of a matrix is + * zero or if two rows or columns are equal, the determinant is zero. Determinant is + * also used to solve a series of equations. + * + * Only a square matrix, like the Matrix3D class, has a determinant. + */ public function get determinant():Number { - return 1 * ((_rawData[0] * _rawData[5] - _rawData[4] * _rawData[1]) * (_rawData[10] * _rawData[15] - _rawData[14] * _rawData[11]) - - (_rawData[0] * _rawData[9] - _rawData[8] * _rawData[1]) * (_rawData[6] * _rawData[15] - _rawData[14] * _rawData[7]) - + (_rawData[0] * _rawData[13] - _rawData[12] * _rawData[1]) * (_rawData[6] * _rawData[11] - _rawData[10] * _rawData[7]) - + (_rawData[4] * _rawData[9] - _rawData[8] * _rawData[5]) * (_rawData[2] * _rawData[15] - _rawData[14] * _rawData[3]) - - (_rawData[4] * _rawData[13] - _rawData[12] * _rawData[5]) * (_rawData[2] * _rawData[11] - _rawData[10] * _rawData[3]) - + (_rawData[8] * _rawData[13] - _rawData[12] * _rawData[9]) * (_rawData[2] * _rawData[7] - _rawData[6] * _rawData[3])); + mr = this.rawData; + return 1 * ((mr[0] * mr[5] - mr[4] * mr[1]) * (mr[10] * mr[15] - mr[14] * mr[11]) + - (mr[0] * mr[9] - mr[8] * mr[1]) * (mr[6] * mr[15] - mr[14] * mr[7]) + + (mr[0] * mr[13] - mr[12] * mr[1]) * (mr[6] * mr[11] - mr[10] * mr[7]) + + (mr[4] * mr[9] - mr[8] * mr[5]) * (mr[2] * mr[15] - mr[14] * mr[3]) + - (mr[4] * mr[13] - mr[12] * mr[5]) * (mr[2] * mr[11] - mr[10] * mr[3]) + + (mr[8] * mr[13] - mr[12] * mr[9]) * (mr[2] * mr[7] - mr[6] * mr[3])); } } } - -import flash.geom.Orientation3D; - -function checkOrientation(orientationStyle:String) { - if (!(orientationStyle == Orientation3D.AXIS_ANGLE || orientationStyle == Orientation3D.EULER_ANGLES || orientationStyle == Orientation3D.QUATERNION)) { - throw new Error("Error #2187: Invalid orientation style " + orientationStyle + ". Value must be one of 'Orientation3D.EULER_ANGLES', 'Orientation3D.AXIS_ANGLE', or 'Orientation3D.QUATERNION'.", 2187); - } -} diff --git a/core/src/avm2/globals/flash/geom/PerspectiveProjection.as b/core/src/avm2/globals/flash/geom/PerspectiveProjection.as index a65cb790bb1c..a79955b0c0ed 100644 --- a/core/src/avm2/globals/flash/geom/PerspectiveProjection.as +++ b/core/src/avm2/globals/flash/geom/PerspectiveProjection.as @@ -1,44 +1,196 @@ package flash.geom { - import __ruffle__.stub_constructor; - import __ruffle__.stub_getter; - import __ruffle__.stub_method; - import __ruffle__.stub_setter; + import flash.geom.Matrix3D; import flash.geom.Point; + /** + * The `PerspectiveProjection` class provides an easy way to assign or modify the perspective + * transformations of a display object and all of its children. For more complex or custom + * perspective transformations, use the `Matrix3D` class. While the `PerspectiveProjection` class + * provides basic three-dimensional presentation properties, the `Matrix3D` class provides more + * detailed control over the three-dimensional presentation of display objects. + * + * Projection is a way of representing a three-dimensional object in a two-dimensional space, + * like a cube projected onto a computer screen. Perspective projection uses a viewing frustum + * (a rectangular pyramid) to model and project a three-dimensional world and its objects on the screen. + * The viewing frustum becomes increasingly wider as it moves further from the origin of the viewpoint. + * The origin of the viewpoint could be a camera or the eyes of an observer facing the screen. + * The projected perspective produces the illusion of three dimensions with depth and distance, + * where the objects closer to the screen appear larger than the objects farther from the screen. + * + * ![Frustum viewing area](/images/frustum.jpg) + * + * A default `PerspectiveProjection` object is a framework defined for perspective transformation of + * the root object, based on the field of view and aspect ratio (dimensions) of the stage. + * The projection center, the vanishing point, is set to the center of the stage, which means the + * three-dimensional display objects disappear toward the center of the stage as they move + * back in the z axis. The default viewpoint is at point (0,0) looking down the positive z axis. + * The y-axis points down toward the bottom of the screen. You can gain access to the root display + * object's perspective projection settings and change the field of view and projection center + * properties of the perspectiveProjection property through the root object's `DisplayObject.transform` + * property. + * + * You can also set a different perspective projection setting for a display object through the parent's + * perspective projection. First, create a `PerspectiveProjection` object and set its `fieldOfView` and + * projectionCenter properties. Next, assign the `PerspectiveProjection` object to the parent display + * object using the `DisplayObject.transform` property. The specified projection matrix and transformation + * will then apply to all the display object's three-dimensional children. + * + * tbd: not tested, just playing arround... + */ public class PerspectiveProjection { - // Getters are stubbed with what seem to be Flash's default values + + private var stage:Object; // tbd: we need access to current stage + + private var _focalLength:Number; + private var _fieldOfView:Number; + private var _matrix3D:Matrix3D; + + private var _projectionCenter:Point; + + private var _znear:Number = 0.1; // Default value + private var _zfar:Number = 1000; // Default value + + private var TO_RADIAN:Number = 0.01745329251994329577; // (Math.PI / 180) + public function PerspectiveProjection() { - stub_constructor("flash.geom.PerspectiveProjection"); + + this.stage = { + stageWidth: 800, // tbd: fix me + stageHeight: 600 // tbd: fix me + }; + + // we set here our basic values for the private variables... + this._focalLength = 0; + this.fieldOfView = 55; // we store in degree * flash default also calcs the _focalLength + this._matrix3D = new Matrix3D(); // per default set to an identity matrix... + this._projectionCenter = new Point(stage.stageWidth / 2, stage.stageHeight / 2); + } + /** + * Specifies an angle, as a degree between 0 and 180, for the field of view in three dimensions. + * This value determines how strong the perspective transformation and distortion apply to a + * three-dimensional display object with a non-zero z-coordinate. + * + * A degree close to 0 means that the screen's two-dimensional x- and y-coordinates are roughly + * the same as the three-dimensional x-, y-, and z-coordinates with little or no distortion. + * In other words, for a small angle, a display object moving down the z axis appears to stay + * near the same size and moves little. + * + * A value close to 180 degrees results in a fisheye lens effect: positions with a z value smaller + * than 0 are magnified, while positions with a z value larger than 0 are minimized. With a + * large angle, a display object moving down the z axis appears to change size quickly and moves + * a great distance. If the field of view is set to 0 or 180, nothing is seen on the screen. + */ public function get fieldOfView():Number { - stub_getter("flash.geom.PerspectiveProjection", "fieldOfView"); - return 55; + return this._fieldOfView; } public function set fieldOfView(value:Number) { - stub_setter("flash.geom.PerspectiveProjection", "fieldOfView"); + + if (value <= 0 || value >= 180) { + throw new Error("PerspectiveProjection fieldOfView must be between 0 and 180 degrees."); + } + + this._fieldOfView = value; + //this._focalLength = stage.stageWidth * (1.0 / Math.tan(this._fieldOfView * TO_RADIAN * 0.5)); + + // influenced by both the field of view and the aspect ratio of the stage. + // the aspect ratio is the ratio of width to height (i.e., stageWidth / stageHeight) + //var effectiveHeight:Number = 2 * this._znear * Math.tan(this._fieldOfView * TO_RADIAN * 0.5); + //var effectiveWidth:Number = effectiveHeight * (stage.stageWidth / stage.stageHeight); + //this._focalLength = effectiveWidth; + + this._focalLength = 2 * this._znear * Math.tan(this._fieldOfView * TO_RADIAN * 0.5) * (stage.stageWidth / stage.stageHeight); + + return this._fieldOfView; // return degree } + /** + * The distance between the eye or the viewpoint's origin (0,0,0) and the display object located + * in the z axis. During the perspective transformation, the `focalLength` is calculated dynamically + * using the angle of the field of view and the stage's aspect ratio (stage width divided by stage height). + */ public function get focalLength():Number { - stub_getter("flash.geom.PerspectiveProjection", "focalLength"); - return 480.25; + return this._focalLength; } public function set focalLength(value:Number) { - stub_setter("flash.geom.PerspectiveProjection", "focalLength"); + this._focalLength = value; } + /** + * A two-dimensional point representing the center of the projection, the vanishing point for the display object. + * + * The projectionCenter property is an offset to the default registration point that is the upper left of the stage, + * point (0,0). The default projection transformation center is in the middle of the stage, which means the + * three-dimensional display objects disappear toward the center of the stage as they move backwards in the z axis. + */ public function get projectionCenter():Point { - stub_getter("flash.geom.PerspectiveProjection", "projectionCenter"); - return new Point(250, 250); + return this._projectionCenter; } public function set projectionCenter(value:Point) { - stub_setter("flash.geom.PerspectiveProjection", "projectionCenter"); + this._projectionCenter = value; + } + + /** + * Sets the near and far clipping planes for the perspective projection. + * + * The near and far planes define the depth range of the viewing frustum, which + * determines the visible depth of the 3D scene. Any objects positioned closer than + * the near plane or farther than the far plane will not be rendered. + * + * The `znear` value must be greater than 0, and the `zfar` value must be greater + * than `znear` to maintain a valid viewing frustum. + * + * @param znear The near clipping plane distance, which must be greater than 0. + * @param zfar The far clipping plane distance, which must be greater than the `znear` value. + * + * @throws Error if `znear` is less than or equal to 0, or if `zfar` is less than or equal to `znear`. + */ + public function setZPlanes(znear:Number, zfar:Number):void { + if (znear <= 0) { + throw new Error("PerspectiveProjection znear must be greater than 0."); + } + if (zfar <= znear) { + throw new Error("PerspectiveProjection zfar must be greater than znear."); + } + + this._znear = znear; + this._zfar = zfar; } + /** + * Returns the underlying Matrix3D object of the display object. + * + * A display object, like the root object, can have a `PerspectiveProjection` object without needing a `Matrix3D` + * property defined for its transformations. In fact, use either a `PerspectiveProjection` or a `Matrix3D` object + * to specify the perspective transformation. If when using the `PerspectiveProjection` object, a `Matrix3D` + * object was needed, the `toMatrix3D()` method can retrieve the underlying `Matrix3D` object of the display object. + * For example, the `toMatrix3D()` method can be used with the `Utils3D.projectVectors()` method. + * @return The underlying `Matrix3D` object. + */ public function toMatrix3D():Matrix3D { - stub_method("flash.geom.PerspectiveProjection", "toMatrix3D"); - return new Matrix3D(); + + var aspectRatio:Number = stage.stageWidth / stage.stageHeight; + var f:Number = 1.0 / Math.tan(this._fieldOfView * TO_RADIAN * 0.5); // focal length calculated from fieldOfView + + var mr = this._matrix3D.rawData; + + mr[0] = f / aspectRatio; // scales X coordinates according to aspect ratio + mr[5] = f; // scales Y coordinates + mr[10] = (this._zfar + this._znear) / (this._znear - this._zfar); // Z-scaling for frustum layers + mr[11] = -1; // Perspective transformation for the Z coordinate + + // also see https://github.com/openfl/openfl/pull/2712 + mr[12] = ((projectionCenter.x * 2) / stage.stageWidth) - 1; // Shift in X + mr[13] = ((projectionCenter.y * 2) / stage.stageHeight) - 1; // Shift in Y + + mr[14] = (2 * this._zfar * this._znear) / (this._znear - this._zfar); // Offset for the Z coordinate + mr[15] = 0; // Last element set to 0 to ensure homogeneity + + this._matrix3D.rawData = mr; + + return this._matrix3D; } } -} \ No newline at end of file +} diff --git a/core/src/avm2/globals/flash/geom/Utils3D.as b/core/src/avm2/globals/flash/geom/Utils3D.as index 123274a8b015..b470699c26af 100644 --- a/core/src/avm2/globals/flash/geom/Utils3D.as +++ b/core/src/avm2/globals/flash/geom/Utils3D.as @@ -1,5 +1,81 @@ package flash.geom { public class Utils3D { + + + /** + * Interpolates the orientation of an object toward a position. The `pointTowards()` + * method combines the functionality of the `Matrix3D.pointAt()` and + * `Matrix3D.interpolateTo()` methods. + * + * The `pointTowards()` method allows for in-place modification to the orientation. + * It decomposes the Matrix3D of the display object and replaces the rotation + * elements by ones that make a percent turn toward the position of the target. The + * object can make an incremental turn toward the target while still moving in its + * own direction. The consecutive calls to the `pointTowards()` followed by a + * translation method can produce the animation of an object chasing or following a + * moving target. First point the object a percent point toward the target, then + * incrementally move the object along an axis. + * + * @param percent A Number between 0 and 1 that incrementally turns the object + * toward the target. + * @param mat The Matrix3D property of the object that is transformed. + * @param pos The world-relative position of the target object. World-relative + * defines the transformation of the object relative to the world space and + * coordinates, where all objects are positioned. + * @param at The object-relative vector that defines where the display object is + * pointing. Object-relative defines the transformation of the object relative to the + * object space, the object's own frame of reference and coordinate system. Default + * value is (0,0,-1). + * @param up The object-relative vector that defines "up" for the display object. + * If the object is drawn looking down from the above, the +z axis is its "up" + * vector. Object-relative defines the transformation of the object relative to the + * object space, the object's own frame of reference and coordinate system. Default + * value is (0,-1,0). + * @return A modified version of the Matrix3D object specified in the second + * parameter. To transform the display object using the `pointTowards()` method, set + * the Matrix3D property of the display object to the returned Matrix3D object. + */ + public static function pointTowards(percent:Number, mat:Matrix3D, pos:Vector3D, at:Vector3D = null, up:Vector3D = null):Matrix3D { + + // beware the default at and up is different to pointAt + if (at == null) { + at = new Vector3D(0, 0, -1); // Default aiming direction + } + + if (up == null) { + up = new Vector3D(0, -1, 0); // Default Up direction + } + + // we should not change the arg mat + var thisMat:Matrix3D = mat.clone(); // we use this as inital matrix + + var toMat:Matrix3D = mat.clone(); + toMat.pointAt(pos, at, up); + + return Matrix3D.interpolate(thisMat, toMat, percent); + //stub_method("flash.geom.Utils3D", "pointTowards"); + + } + + /** + * Using a projection Matrix3D object, projects a Vector3D object from one space + * coordinate to another. The `projectVector()` method is like the + * `Matrix3D.transformVector()` method except that the `projectVector()` method + * divides the x, y, and z elements of the original Vector3D object by the + * projection depth value. The depth value is the distance from the eye to the + * Vector3D object in view or eye space. The default value for this distance is the + * value of the z element. + * + * @param m A projection Matrix3D object that implements the projection + * transformation. If a display object has a PerspectiveProjection object, you can + * use the `perspectiveProjection.toMatrix()` method to produce a projection Matrix3D + * object that applies to the children of the display object. For more advance + * projections, use the `matrix3D.rawData` property to create a custom projection + * matrix. There is no built-in Matrix3D method for creating a projection Matrix3D + * object. + * @param v The Vector3D object that is projected to a new space coordinate. + * @return A new Vector3D with a transformed space coordinate. + */ public static function projectVector(m:Matrix3D, v:Vector3D):Vector3D { var projected = m.transformVector(v); projected.x /= projected.w; @@ -9,9 +85,39 @@ package flash.geom { return projected; } + /** + * Using a projection Matrix3D object, projects a Vector of three-dimensional space + * coordinates (`verts`) to a Vector of two-dimensional space coordinates + * (`projectedVerts`). The projected Vector object should be pre-allocated before it + * is used as a parameter. + * + * The `projectVectors()` method also sets the t value of the uvt data. You should + * pre-allocate a Vector that can hold the uvts data for each projected Vector set of + * coordinates. Also specify the u and v values of the uvt data. The uvt data is a + * Vector of normalized coordinates used for texture mapping. In UV coordinates, + * (0,0) is the upper left of the bitmap, and (1,1) is the lower right of the bitmap. + * + * This method can be used in conjunction with the `Graphics.drawTriangles()` method + * and the GraphicsTrianglePath class. + * + * @param m A projection Matrix3D object that implements the projection + * transformation. You can produce a projection Matrix3D object using the + * `Matrix3D.rawData` property. + * @param verts A Vector of Floats, where every three Floats represent the x, y, + * and z coordinates of a three-dimensional space, like `Vector3D(x,y,z)`. + * @param projectedVerts A vector of Floats, where every two Floats represent a + * projected two-dimensional coordinate, like Point(x,y). You should pre-allocate the + * Vector. The `projectVectors()` method fills the values for each projected point. + * @param uvts A vector of Floats, where every three Floats represent the u, v, + * and t elements of the uvt data. The u and v are the texture coordinate for each + * projected point. The t value is the projection depth value, the distance from + * the eye to the Vector3D object in the view or eye space. You should pre-allocate + * the Vector and specify the u and v values. The projectVectors method fills the t + * value for each projected point. + */ // Based on https://github.com/openfl/openfl/blob/971a4c9e43b5472fd84d73920a2b7c1b3d8d9257/src/openfl/geom/Utils3D.hx public static function projectVectors(m:Matrix3D, verts:Vector., projectedVerts:Vector., uvts:Vector.):void { - var n = m.rawData; + var mr = m.rawData; var x, y, z, w; var x1, y1, z1, w1; @@ -32,9 +138,9 @@ package flash.geom { z = verts[i + 2]; w = 1; - x1 = x * n[0] + y * n[4] + z * n[8] + w * n[12]; - y1 = x * n[1] + y * n[5] + z * n[9] + w * n[13]; - w1 = x * n[3] + y * n[7] + z * n[11] + w * n[15]; + x1 = x * mr[0] + y * mr[4] + z * mr[8] + w * mr[12]; + y1 = x * mr[1] + y * mr[5] + z * mr[9] + w * mr[13]; + w1 = x * mr[3] + y * mr[7] + z * mr[11] + w * mr[15]; projectedVerts[j] = x1 / w1; projectedVerts[j + 1] = y1 / w1; @@ -46,4 +152,4 @@ package flash.geom { } } } -} \ No newline at end of file +}