View Javadoc
1   package org.djutils.draw;
2   
3   import java.util.Arrays;
4   import java.util.Iterator;
5   
6   import org.djutils.draw.bounds.Bounds3d;
7   import org.djutils.draw.point.Point3d;
8   
9   /**
10   * Transform3d contains a MUTABLE transformation object that can transform points (x,y,z) based on e.g, rotation and
11   * translation. It uses an affine transform matrix that can be built up from different components (translation, rotation,
12   * scaling, reflection, shearing).
13   * <p>
14   * Copyright (c) 2020-2022 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
15   * BSD-style license. See <a href="https://djutils.org/docs/current/djutils/licenses.html">DJUTILS License</a>.
16   * </p>
17   * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
18   * @author <a href="https://www.tudelft.nl/pknoppers">Peter Knoppers</a>
19   */
20  public class Transform3d implements Cloneable
21  {
22      /** The 4x4 transformation matrix, initialized as the Identity matrix. */
23      private double[] mat = new double[] {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1};
24  
25      /**
26       * Multiply a 4x4 matrix (stored as a 16-value array by row) with a 4-value vector.
27       * @param m double[]; the matrix
28       * @param v double[]; the vector
29       * @return double[4]; the result of m x v
30       */
31      protected static double[] mulMatVec(final double[] m, final double[] v)
32      {
33          double[] result = new double[4];
34          for (int i = 0; i < 4; i++)
35          {
36              result[i] = m[4 * i] * v[0] + m[4 * i + 1] * v[1] + m[4 * i + 2] * v[2] + m[4 * i + 3] * v[3];
37          }
38          return result;
39      }
40  
41      /**
42       * Multiply a 4x4 matrix (stored as a 16-value array by row) with a 3-value vector and a 1 for the 4th value.
43       * @param m double[]; the matrix
44       * @param v double[]; the vector
45       * @return double[3]; the result of m x (v1, v2, v3, 1), with the last value left out
46       */
47      protected static double[] mulMatVec3(final double[] m, final double[] v)
48      {
49          double[] result = new double[3];
50          for (int i = 0; i < 3; i++)
51          {
52              result[i] = m[4 * i] * v[0] + m[4 * i + 1] * v[1] + m[4 * i + 2] * v[2] + m[4 * i + 3];
53          }
54          return result;
55      }
56  
57      /**
58       * Multiply a 4x4 matrix (stored as a 16-value array by row) with another 4x4-matrix.
59       * @param m1 double[]; the first matrix
60       * @param m2 double[]; the second matrix
61       * @return double[16]; the result of m1 x m2
62       */
63      protected static double[] mulMatMat(final double[] m1, final double[] m2)
64      {
65          double[] result = new double[16];
66          for (int i = 0; i < 4; i++)
67          {
68              for (int j = 0; j < 4; j++)
69              {
70                  result[4 * i + j] =
71                          m1[4 * i] * m2[j] + m1[4 * i + 1] * m2[j + 4] + +m1[4 * i + 2] * m2[j + 8] + m1[4 * i + 3] * m2[j + 12];
72              }
73          }
74          return result;
75      }
76  
77      /**
78       * Get a safe copy of the affine transformation matrix.
79       * @return double[]; a safe copy of the affine transformation matrix
80       */
81      public double[] getMat()
82      {
83          return this.mat.clone();
84      }
85  
86      /**
87       * Transform coordinates by a vector (tx, ty, tz). Note that to carry out multiple operations, the steps have to be built in
88       * the OPPOSITE order since matrix multiplication operates from RIGHT to LEFT.
89       * @param tx double; the translation value for the x-coordinates
90       * @param ty double; the translation value for the y-coordinates
91       * @param tz double; the translation value for the z-coordinates
92       * @return Transform3d; the new transformation matrix after applying this transform
93       */
94      public Transform3d translate(final double tx, final double ty, final double tz)
95      {
96          if (tx == 0.0 && ty == 0.0 && tz == 0.0)
97          {
98              return this;
99          }
100         this.mat = mulMatMat(this.mat, new double[] {1, 0, 0, tx, 0, 1, 0, ty, 0, 0, 1, tz, 0, 0, 0, 1});
101         return this;
102     }
103 
104     /**
105      * Translate coordinates by a the x, y, and z values contained in a Point. Note that to carry out multiple operations, the
106      * steps have to be built in the OPPOSITE order since matrix multiplication operates from RIGHT to LEFT.
107      * @param point Point3d; the point containing the x, y, and z translation values
108      * @return Transform3d; the new transformation matrix after applying this transform
109      */
110     public Transform3d translate(final Point3d point)
111     {
112         if (point.x == 0.0 && point.y == 0.0 && point.z == 0.0)
113         {
114             return this;
115         }
116         this.mat = mulMatMat(this.mat, new double[] {1, 0, 0, point.x, 0, 1, 0, point.y, 0, 0, 1, point.z, 0, 0, 0, 1});
117         return this;
118     }
119 
120     /**
121      * Scale all coordinates with a factor for x, y, and z. A scale factor of 1 leaves the coordinate unchanged. Note that to
122      * carry out multiple operations, the steps have to be built in the OPPOSITE order since matrix multiplication operates from
123      * RIGHT to LEFT.
124      * @param sx double; the scale factor for the x-coordinates
125      * @param sy double; the scale factor for the y-coordinates
126      * @param sz double; the scale factor for the z-coordinates
127      * @return Transform3d; the new transformation matrix after applying this transform
128      */
129     public Transform3d scale(final double sx, final double sy, final double sz)
130     {
131         if (sx == 1.0 && sy == 1.0 && sz == 1.0)
132         {
133             return this;
134         }
135         this.mat = mulMatMat(this.mat, new double[] {sx, 0, 0, 0, 0, sy, 0, 0, 0, 0, sz, 0, 0, 0, 0, 1});
136         return this;
137     }
138 
139     /**
140      * The Euler rotation around the x-axis with an angle in radians. Note that to carry out multiple operations, the steps have
141      * to be built in the OPPOSITE order since matrix multiplication operates from RIGHT to LEFT.
142      * @param angle double; the angle to rotate the coordinates with with around the x-axis
143      * @return Transform3d; the new transformation matrix after applying this transform
144      */
145     public Transform3d rotX(final double angle)
146     {
147         if (angle == 0.0)
148         {
149             return this;
150         }
151         double c = Math.cos(angle);
152         double s = Math.sin(angle);
153         this.mat = mulMatMat(this.mat, new double[] {1, 0, 0, 0, 0, c, -s, 0, 0, s, c, 0, 0, 0, 0, 1});
154         return this;
155     }
156 
157     /**
158      * The Euler rotation around the y-axis with an angle in radians. Note that to carry out multiple operations, the steps have
159      * to be built in the OPPOSITE order since matrix multiplication operates from RIGHT to LEFT.
160      * @param angle double; the angle to rotate the coordinates with with around the y-axis
161      * @return Transform3d; the new transformation matrix after applying this transform
162      */
163     public Transform3d rotY(final double angle)
164     {
165         if (angle == 0.0)
166         {
167             return this;
168         }
169         double c = Math.cos(angle);
170         double s = Math.sin(angle);
171         this.mat = mulMatMat(this.mat, new double[] {c, 0, s, 0, 0, 1, 0, 0, -s, 0, c, 0, 0, 0, 0, 1});
172         return this;
173     }
174 
175     /**
176      * The Euler rotation around the z-axis with an angle in radians. Note that to carry out multiple operations, the steps have
177      * to be built in the OPPOSITE order since matrix multiplication operates from RIGHT to LEFT.
178      * @param angle double; the angle to rotate the coordinates with with around the z-axis
179      * @return Transform3d; the new transformation matrix after applying this transform
180      */
181     public Transform3d rotZ(final double angle)
182     {
183         if (angle == 0.0)
184         {
185             return this;
186         }
187         double c = Math.cos(angle);
188         double s = Math.sin(angle);
189         this.mat = mulMatMat(this.mat, new double[] {c, -s, 0, 0, s, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1});
190         return this;
191     }
192 
193     /**
194      * The xy-shear leaves the xy-coordinate plane for z=0 untouched. Coordinates on z=1 are translated by a vector (sx, sy, 0).
195      * Coordinates for points with other z-values are translated by a vector (z.sx, z.sy, 0), where z is the z-coordinate of the
196      * point. Note that to carry out multiple operations, the steps have to be built in the OPPOSITE order since matrix
197      * multiplication operates from RIGHT to LEFT.
198      * @param sx double; the shear factor in the x-direction for z=1
199      * @param sy double; the shear factor in the y-direction for z=1
200      * @return Transform3d; the new transformation matrix after applying this transform
201      */
202     public Transform3d shearXY(final double sx, final double sy)
203     {
204         if (sx == 0.0 && sy == 0.0)
205         {
206             return this;
207         }
208         this.mat = mulMatMat(this.mat, new double[] {1, 0, sx, 0, 0, 1, sy, 0, 0, 0, 1, 0, 0, 0, 0, 1});
209         return this;
210     }
211 
212     /**
213      * The yz-shear leaves the yz-coordinate plain for x=0 untouched. Coordinates on x=1 are translated by a vector (0, sy, sz).
214      * Coordinates for points with other x-values are translated by a vector (0, x.sy, x.sz), where x is the x-coordinate of the
215      * point. Note that to carry out multiple operations, the steps have to be built in the OPPOSITE order since matrix
216      * multiplication operates from RIGHT to LEFT.
217      * @param sy double; the shear factor in the y-direction for x=1
218      * @param sz double; the shear factor in the z-direction for x=1
219      * @return Transform3d; the new transformation matrix after applying this transform
220      */
221     public Transform3d shearYZ(final double sy, final double sz)
222     {
223         if (sy == 0.0 && sz == 0.0)
224         {
225             return this;
226         }
227         this.mat = mulMatMat(this.mat, new double[] {1, 0, 0, 0, sy, 1, 0, 0, sz, 0, 1, 0, 0, 0, 0, 1});
228         return this;
229     }
230 
231     /**
232      * The xz-shear leaves the xz-coordinate plain for y=0 untouched. Coordinates on y=1 are translated by a vector (sx, 0, sz).
233      * Coordinates for points with other y-values are translated by a vector (y.sx, 0, y.sz), where y is the y-coordinate of the
234      * point. Note that to carry out multiple operations, the steps have to be built in the OPPOSITE order since matrix
235      * multiplication operates from RIGHT to LEFT.
236      * @param sx double; the shear factor in the y-direction for y=1
237      * @param sz double; the shear factor in the z-direction for y=1
238      * @return Transform3d; the new transformation matrix after applying this transform
239      */
240     public Transform3d shearXZ(final double sx, final double sz)
241     {
242         if (sx == 0.0 && sz == 0.0)
243         {
244             return this;
245         }
246         this.mat = mulMatMat(this.mat, new double[] {1, sx, 0, 0, 0, 1, 0, 0, 0, sz, 1, 0, 0, 0, 0, 1});
247         return this;
248     }
249 
250     /**
251      * The reflection of the x-coordinate, by mirroring it in the yz-plane (the plane with x=0). Note that to carry out multiple
252      * operations, the steps have to be built in the OPPOSITE order since matrix multiplication operates from RIGHT to LEFT.
253      * @return Transform3d; the new transformation matrix after applying this transform
254      */
255     public Transform3d reflectX()
256     {
257         this.mat = mulMatMat(this.mat, new double[] {-1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1});
258         return this;
259     }
260 
261     /**
262      * The reflection of the y-coordinate, by mirroring it in the xz-plane (the plane with y=0). Note that to carry out multiple
263      * operations, the steps have to be built in the OPPOSITE order since matrix multiplication operates from RIGHT to LEFT.
264      * @return Transform3d; the new transformation matrix after applying this transform
265      */
266     public Transform3d reflectY()
267     {
268         this.mat = mulMatMat(this.mat, new double[] {1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1});
269         return this;
270     }
271 
272     /**
273      * The reflection of the z-coordinate, by mirroring it in the xy-plane (the plane with z=0). Note that to carry out multiple
274      * operations, the steps have to be built in the OPPOSITE order since matrix multiplication operates from RIGHT to LEFT.
275      * @return Transform3d; the new transformation matrix after applying this transform
276      */
277     public Transform3d reflectZ()
278     {
279         this.mat = mulMatMat(this.mat, new double[] {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1});
280         return this;
281     }
282 
283     /**
284      * Apply the stored transform on the xyz-vector and return the transformed vector. For speed reasons, no checks on correct
285      * size of the vector is done.
286      * @param xyz double[]; double[3] the provided vector
287      * @return double[3]; the transformed vector
288      */
289     public double[] transform(final double[] xyz)
290     {
291         return mulMatVec3(this.mat, xyz);
292     }
293 
294     /**
295      * Apply the stored transform on the provided point and return a point with the transformed coordinate.
296      * @param point Point3d; the point to be transformed
297      * @return Point3d; a point with the transformed coordinates
298      */
299     public Point3d transform(final Point3d point)
300     {
301         return new Point3d(mulMatVec3(this.mat, new double[] {point.x, point.y, point.z}));
302     }
303 
304     /**
305      * Apply the stored transform on the provided point and return a point with the transformed coordinate.
306      * @param pointIterator Iterator&lt;Point3d&gt;; generates the points to be transformed
307      * @return Iterator&lt;Point3d&gt;; an iterator that will generator all transformed points
308      */
309     public Iterator<Point3d> transform(final Iterator<Point3d> pointIterator)
310     {
311         return new Iterator<Point3d>()
312         {
313 
314             @Override
315             public boolean hasNext()
316             {
317                 return pointIterator.hasNext();
318             }
319 
320             @Override
321             public Point3d next()
322             {
323                 return transform(pointIterator.next());
324             }
325         };
326     }
327 
328     /**
329      * Apply the stored transform on the provided Bounds3d and return a new Bounds3d with the bounds of the transformed
330      * coordinates. All 8 corner points have to be transformed, since we do not know which of the 8 points will result in the
331      * lowest and highest x, y, and z coordinates.
332      * @param boundingBox Bounds3d; the bounds to be transformed
333      * @return Bounds3d; the new bounds based on the transformed coordinates
334      */
335     public Bounds3d transform(final Bounds3d boundingBox)
336     {
337         return new Bounds3d(transform(boundingBox.getPoints()));
338     }
339 
340     /** {@inheritDoc} */
341     @Override
342     public int hashCode()
343     {
344         final int prime = 31;
345         int result = 1;
346         result = prime * result + Arrays.hashCode(this.mat);
347         return result;
348     }
349 
350     /** {@inheritDoc} */
351     @Override
352     @SuppressWarnings("checkstyle:needbraces")
353     public boolean equals(final Object obj)
354     {
355         if (this == obj)
356             return true;
357         if (obj == null)
358             return false;
359         if (getClass() != obj.getClass())
360             return false;
361         Transform3d other = (Transform3d) obj;
362         if (!Arrays.equals(this.mat, other.mat))
363             return false;
364         return true;
365     }
366 
367     /** {@inheritDoc} */
368     @Override
369     public String toString()
370     {
371         return "Transform3d [mat=" + Arrays.toString(this.mat) + "]";
372     }
373 
374 }