package org.djutils.draw; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.util.Arrays; import org.djutils.draw.bounds.Bounds3d; import org.djutils.draw.point.Point3d; import org.junit.Test; /** * Transform3dTest.java. *

* Copyright (c) 2020-2021 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
* BSD-style license. See DJUTILS License. *

* @author Alexander Verbraeck * @author Peter Knoppers */ public class Transform3dTest { /** * Test the matrix / vector multiplication. */ @Test public void testMatrixMultiplication() { double[] mA = new double[] {5, 7, 9, 10, 2, 3, 3, 8, 8, 10, 2, 3, 3, 3, 4, 8}; double[] mB = new double[] {3, 10, 12, 18, 12, 1, 4, 9, 9, 10, 12, 2, 3, 12, 4, 10}; double[] mAmB = Transform3d.mulMatMat(mA, mB); double[] expected = new double[] {210, 267, 236, 271, 93, 149, 104, 149, 171, 146, 172, 268, 105, 169, 128, 169}; for (int i = 0; i < 16; i++) { if (mAmB[i] != expected[i]) { fail(String.format("difference MA x MB at %d: expected %f, was: %f", i, expected[i], mAmB[i])); } } double[] m = new double[] {1, 0, 2, 0, 0, 3, 0, 4, 0, 0, 5, 0, 6, 0, 0, 7}; double[] v = new double[] {2, 5, 1, 8}; double[] mv = Transform3d.mulMatVec(m, v); double[] ev = new double[] {4, 47, 5, 68}; for (int i = 0; i < 4; i++) { if (mv[i] != ev[i]) { fail(String.format("difference M x V at %d: expected %f, was: %f", i, ev[i], mv[i])); } } v = new double[] {1, 2, 3}; mv = Transform3d.mulMatVec3(m, v); ev = new double[] {7, 10, 15}; for (int i = 0; i < 3; i++) { if (mv[i] != ev[i]) { fail(String.format("difference M x V3 at %d: expected %f, was: %f", i, ev[i], mv[i])); } } } /** * Test that the constructor creates an Identity matrix. */ @Test public void testConstructor() { // TODO: decide whether the internal (flattened) matrix should be visible at all, or add a getter Transform3d t = new Transform3d(); assertEquals("matrix contians 16 values", 16, t.getMat().length); for (int row = 0; row < 4; row++) { for (int col = 0; col < 4; col++) { int e = row == col ? 1 : 0; assertEquals("Value in identity matrix matches", e, t.getMat()[4 * row + col], 0); } } } /** * Test the translate, scale, rotate, shear and reflect methods. */ @Test public void testTranslateScaleRotateShearAndReflect() { Transform3d t; // Test time grows (explodes) with the 6th power of the length of values. double[] values = new double[] {-100000, -100, -3, -1, -0.1, 0, 0.1, 1, 3, 100, 100000}; for (double dx : values) { for (double dy : values) { for (double dz : values) { // Translate defined with a double[] t = new Transform3d(); t.translate(dx, dy, dz); for (double px : values) { for (double py : values) { for (double pz : values) { Point3d p = t.transform(new Point3d(px, py, pz)); assertEquals("translated x matches", px + dx, p.x, 0.001); assertEquals("translated y matches", py + dy, p.y, 0.001); assertEquals("translated z matches", pz + dz, p.z, 0.001); double[] result = t.transform(new double[] {px, py, pz}); assertEquals("translated x matches", px + dx, result[0], 0.001); assertEquals("translated y matches", py + dy, result[1], 0.001); assertEquals("translated z matches", pz + dz, result[2], 0.001); } } } // Translate defined with a Point t = new Transform3d(); t.translate(new Point3d(dx, dy, dz)); for (double px : values) { for (double py : values) { for (double pz : values) { Point3d p = t.transform(new Point3d(px, py, pz)); assertEquals("translated x matches", px + dx, p.x, 0.001); assertEquals("translated y matches", py + dy, p.y, 0.001); assertEquals("translated z matches", pz + dz, p.z, 0.001); double[] result = t.transform(new double[] {px, py, pz}); assertEquals("translated x matches", px + dx, result[0], 0.001); assertEquals("translated y matches", py + dy, result[1], 0.001); assertEquals("translated z matches", pz + dz, result[2], 0.001); } } } // Scale t = new Transform3d(); t.scale(dx, dy, dz); for (double px : values) { for (double py : values) { for (double pz : values) { Point3d p = t.transform(new Point3d(px, py, pz)); assertEquals("scaled x matches", px * dx, p.x, 0.001); assertEquals("scaled y matches", py * dy, p.y, 0.001); assertEquals("scaled z matches", pz * dz, p.z, 0.001); double[] result = t.transform(new double[] {px, py, pz}); assertEquals("scaled x matches", px * dx, result[0], 0.001); assertEquals("scaled y matches", py * dy, result[1], 0.001); assertEquals("scaled z matches", pz * dz, result[2], 0.001); } } } // ShearXY t = new Transform3d(); t.shearXY(dx, dy); for (double px : values) { for (double py : values) { for (double pz : values) { Point3d p = t.transform(new Point3d(px, py, pz)); assertEquals("sheared x matches", px + pz * dx, p.x, 0.001); assertEquals("sheared y matches", py + pz * dy, p.y, 0.001); assertEquals("sheared z matches", pz, p.z, 0.001); double[] result = t.transform(new double[] {px, py, pz}); assertEquals("sheared x matches", px + pz * dx, result[0], 0.001); assertEquals("sheared y matches", py + pz * dy, result[1], 0.001); assertEquals("sheared z matches", pz, result[2], 0.001); } } } // ShearXZ t = new Transform3d(); t.shearXZ(dx, dz); for (double px : values) { for (double py : values) { for (double pz : values) { Point3d p = t.transform(new Point3d(px, py, pz)); assertEquals("sheared x matches", px + py * dx, p.x, 0.001); assertEquals("sheared y matches", py, p.y, 0.001); assertEquals("sheared z matches", pz + py * dz, p.z, 0.001); double[] result = t.transform(new double[] {px, py, pz}); assertEquals("sheared x matches", px + py * dx, result[0], 0.001); assertEquals("sheared y matches", py, result[1], 0.001); assertEquals("sheared z matches", pz + py * dz, result[2], 0.001); } } } // ShearYZ t = new Transform3d(); t.shearYZ(dy, dz); for (double px : values) { for (double py : values) { for (double pz : values) { Point3d p = t.transform(new Point3d(px, py, pz)); assertEquals("sheared x matches", px, p.x, 0.001); assertEquals("sheared y matches", py + px * dy, p.y, 0.001); assertEquals("sheared z matches", pz + px * dz, p.z, 0.001); double[] result = t.transform(new double[] {px, py, pz}); assertEquals("sheared x matches", px, result[0], 0.001); assertEquals("sheared y matches", py + px * dy, result[1], 0.001); assertEquals("sheared z matches", pz + px * dz, result[2], 0.001); } } } } // Rotate around Z (using dx as angle) t = new Transform3d(); t.rotZ(dx); double sine = Math.sin(dx); double cosine = Math.cos(dx); for (double px : values) { for (double py : values) { for (double pz : values) { Point3d p = t.transform(new Point3d(px, py, pz)); assertEquals("rotated x matches", px * cosine - py * sine, p.x, 0.001); assertEquals("rotated y matches", py * cosine + px * sine, p.y, 0.001); assertEquals("rotated z matches", pz, p.z, 0.001); double[] result = t.transform(new double[] {px, py, pz}); assertEquals("rotated x matches", px * cosine - py * sine, result[0], 0.001); assertEquals("rotated z matches", py * cosine + px * sine, result[1], 0.001); assertEquals("rotated z matches", pz, result[2], 0.001); } } } // Rotate around X (using dx as angle) t = new Transform3d(); t.rotX(dx); sine = Math.sin(dx); cosine = Math.cos(dx); for (double px : values) { for (double py : values) { for (double pz : values) { Point3d p = t.transform(new Point3d(px, py, pz)); assertEquals("rotated x matches", px, p.x, 0.001); assertEquals("rotated y matches", py * cosine - pz * sine, p.y, 0.001); assertEquals("rotated z matches", pz * cosine + py * sine, p.z, 0.001); double[] result = t.transform(new double[] {px, py, pz}); assertEquals("rotated x matches", px, result[0], 0.001); assertEquals("rotated z matches", py * cosine - pz * sine, result[1], 0.001); assertEquals("rotated z matches", pz * cosine + py * sine, result[2], 0.001); } } } // Rotate around Y (using dx as angle) t = new Transform3d(); t.rotY(dx); sine = Math.sin(dx); cosine = Math.cos(dx); for (double px : values) { for (double py : values) { for (double pz : values) { Point3d p = t.transform(new Point3d(px, py, pz)); assertEquals("rotated x matches", px * cosine + pz * sine, p.x, 0.001); assertEquals("rotated y matches", py, p.y, 0.001); assertEquals("rotated z matches", pz * cosine - px * sine, p.z, 0.001); double[] result = t.transform(new double[] {px, py, pz}); assertEquals("rotated x matches", px * cosine + pz * sine, result[0], 0.001); assertEquals("rotated z matches", py, result[1], 0.001); assertEquals("rotated z matches", pz * cosine - px * sine, result[2], 0.001); } } } } } // ReflectX t = new Transform3d(); t.reflectX(); for (double px : values) { for (double py : values) { for (double pz : values) { Point3d p = t.transform(new Point3d(px, py, pz)); assertEquals("x-reflected x matches", -px, p.x, 0.001); assertEquals("x-reflected y matches", py, p.y, 0.001); assertEquals("x-reflected z matches", pz, p.z, 0.001); double[] result = t.transform(new double[] {px, py, pz}); assertEquals("x-reflected x matches", -px, result[0], 0.001); assertEquals("x-reflected y matches", py, result[1], 0.001); assertEquals("x-reflected z matches", pz, result[2], 0.001); } } } // ReflectY t = new Transform3d(); t.reflectY(); for (double px : values) { for (double py : values) { for (double pz : values) { Point3d p = t.transform(new Point3d(px, py, pz)); assertEquals("y-reflected x matches", px, p.x, 0.001); assertEquals("y-reflected y matches", -py, p.y, 0.001); assertEquals("y-reflected z matches", pz, p.z, 0.001); double[] result = t.transform(new double[] {px, py, pz}); assertEquals("y-reflected x matches", px, result[0], 0.001); assertEquals("y-reflected y matches", -py, result[1], 0.001); assertEquals("y-reflected z matches", pz, result[2], 0.001); } } } // ReflectZ t = new Transform3d(); t.reflectZ(); for (double px : values) { for (double py : values) { for (double pz : values) { Point3d p = t.transform(new Point3d(px, py, pz)); assertEquals("z-reflected x matches", px, p.x, 0.001); assertEquals("z-reflected y matches", py, p.y, 0.001); assertEquals("z-reflected z matches", -pz, p.z, 0.001); double[] result = t.transform(new double[] {px, py, pz}); assertEquals("z-reflected x matches", px, result[0], 0.001); assertEquals("z-reflected y matches", py, result[1], 0.001); assertEquals("z-reflected z matches", -pz, result[2], 0.001); } } } } /** * Test the transform method. */ @Test public void transformTest() { Transform3d reflectionX = new Transform3d().reflectX(); Transform3d reflectionY = new Transform3d().reflectY(); Transform3d reflectionZ = new Transform3d().reflectZ(); // Test time explodes with the 6th power of the length of this array double[] values = new double[] {-30, 0, 0.07, 25}; for (double translateX : values) { for (double translateY : values) { for (double translateZ : values) { Transform3d translation = new Transform3d().translate(translateX, translateY, translateZ); for (double scaleX : values) { for (double scaleY : values) { for (double scaleZ : values) { Transform3d scaling = new Transform3d().scale(scaleX, scaleY, scaleZ); for (double angle : new double[] {-2, 0, 0.5}) { Transform3d rotationX = new Transform3d().rotX(angle); Transform3d rotationY = new Transform3d().rotY(angle); Transform3d rotationZ = new Transform3d().rotZ(angle); for (double shearA : values) { for (double shearB : values) { Transform3d t = new Transform3d().translate(translateX, translateY, translateZ) .scale(scaleX, scaleY, scaleZ).rotZ(angle).shearXY(shearA, shearB); Transform3d shearXY = new Transform3d().shearXY(shearA, shearB); Transform3d tReflectX = new Transform3d().reflectX().translate(translateX, translateY, translateZ) .scale(scaleX, scaleY, scaleZ).rotY(angle).shearYZ(shearA, shearB); Transform3d shearYZ = new Transform3d().shearYZ(shearA, shearB); Transform3d tReflectY = new Transform3d().reflectY().translate(translateX, translateY, translateZ) .scale(scaleX, scaleY, scaleZ).rotZ(angle).shearXZ(shearA, shearB); Transform3d shearXZ = new Transform3d().shearXZ(shearA, shearB); Transform3d tReflectZ = new Transform3d().reflectZ().translate(translateX, translateY, translateZ) .scale(scaleX, scaleY, scaleZ).rotX(angle).shearXY(shearA, shearB); for (double px : values) { for (double py : values) { for (double pz : values) { Point3d p = new Point3d(px, py, pz); Point3d tp = t.transform(p); Point3d chainP = translation.transform( scaling.transform(rotationZ.transform(shearXY.transform(p)))); assertEquals("X", chainP.x, tp.x, 0.0000001); assertEquals("Y", chainP.y, tp.y, 0.0000001); assertEquals("Z", chainP.z, tp.z, 0.0000001); tp = tReflectX.transform(p); Point3d chainPReflectX = reflectionX.transform(translation.transform( scaling.transform(rotationY.transform(shearYZ.transform(p))))); assertEquals("RX X", chainPReflectX.x, tp.x, 0.0000001); assertEquals("RX Y", chainPReflectX.y, tp.y, 0.0000001); assertEquals("RX Z", chainPReflectX.z, tp.z, 0.0000001); tp = tReflectY.transform(p); Point3d chainPReflectY = reflectionY.transform(translation.transform( scaling.transform(rotationZ.transform(shearXZ.transform(p))))); assertEquals("RY X", chainPReflectY.x, tp.x, 0.0000001); assertEquals("RY Y", chainPReflectY.y, tp.y, 0.0000001); assertEquals("RY Z", chainPReflectY.z, tp.z, 0.0000001); tp = tReflectZ.transform(p); Point3d chainPReflectZ = reflectionZ.transform(translation.transform( scaling.transform(rotationX.transform(shearXY.transform(p))))); assertEquals("RZ X", chainPReflectZ.x, tp.x, 0.0000001); assertEquals("RZ Y", chainPReflectZ.y, tp.y, 0.0000001); assertEquals("RZ Z", chainPReflectZ.z, tp.z, 0.0000001); } } } } } } } } } } } } } /** * Test transformation of a bounding box. */ @Test public void transformBounds3dTest() { double[] values = new double[] {-100, 0.1, 0, 0.1, 100}; double[] sizes = new double[] {0, 10, 100}; Transform3d t = new Transform3d().rotX(0.4).rotZ(0.8).rotY(-1.2).reflectX().scale(0.5, 1.5, 2.5).shearXY(2, 3) .translate(123, 456, 789); // System.out.println(t); for (double x : values) { for (double y : values) { for (double z : values) { for (double xSize : sizes) { for (double ySize : sizes) { for (double zSize : sizes) { Bounds3d bb = new Bounds3d(x, x + xSize, y, y + ySize, z, z + zSize); Point3d[] points = new Point3d[] {new Point3d(x, y, z), new Point3d(x + xSize, y, z), new Point3d(x, y + ySize, z), new Point3d(x + xSize, y + ySize, z), new Point3d(x, y, z + zSize), new Point3d(x + xSize, y, z + zSize), new Point3d(x, y + ySize, z + zSize), new Point3d(x + xSize, y + ySize, z + zSize)}; Point3d[] transformedPoints = new Point3d[8]; for (int i = 0; i < points.length; i++) { transformedPoints[i] = t.transform(points[i]); } Bounds3d expected = new Bounds3d(Arrays.stream(transformedPoints).iterator()); Bounds3d got = t.transform(bb); if (!got.equals(expected)) { System.err.println("oops"); t.transform(bb); } assertEquals("bb minX", expected.getMinX(), got.getMinX(), 0.0001); assertEquals("bb maxX", expected.getMaxX(), got.getMaxX(), 0.0001); assertEquals("bb minY", expected.getMinY(), got.getMinY(), 0.0001); assertEquals("bb maxY", expected.getMaxY(), got.getMaxY(), 0.0001); assertEquals("bb minZ", expected.getMinZ(), got.getMinZ(), 0.0001); assertEquals("bb maxZ", expected.getMaxZ(), got.getMaxZ(), 0.0001); } } } } } } } /** * Reproducible test of multiple transformations on a bounding box. */ @Test public void testBoundingBox3d() { Bounds3d bounds = new Bounds3d(-4, 4, -4, 4, -4, 4); // identical transformation Transform3d transform = new Transform3d(); Bounds3d b = transform.transform(bounds); testBounds3d(b, -4, 4, -4, 4, -4, 4); // translate x, y transform = new Transform3d(); transform.translate(20, 10, 0); b = transform.transform(bounds); testBounds3d(b, 20 - 4, 20 + 4, 10 - 4, 10 + 4, -4, 4); // translate x, y, z transform = new Transform3d(); transform.translate(-20, -10, -30); b = transform.transform(bounds); testBounds3d(b, -20 - 4, -20 + 4, -10 - 4, -10 + 4, -30 - 4, -30 + 4); // rotate 90 degrees (should be same) transform = new Transform3d(); transform.rotZ(Math.toRadians(90.0)); b = transform.transform(bounds); testBounds3d(b, -4, 4, -4, 4, -4, 4); // rotate 45 degrees in the XY-plane transform = new Transform3d(); transform.rotZ(Math.toRadians(45.0)); double d = 4.0 * Math.sqrt(2.0); b = transform.transform(bounds); testBounds3d(b, -d, d, -d, d, -4, 4); // rotate 45 degrees in the XY-plane and then translate to (10, 20) // note that to do FIRST rotation and THEN translation, the steps have to be built in the OPPOSITE order // since matrix multiplication operates from RIGHT to LEFT. transform = new Transform3d(); transform.translate(10, 20, 0); transform.rotZ(Math.toRadians(45.0)); b = transform.transform(bounds); testBounds3d(b, 10 - d, 10 + d, 20 - d, 20 + d, -4, 4); } /** * Check bounds values. * @param b Bounds3d; the box to test * @param minX double; expected value * @param maxX double; expected value * @param minY double; expected value * @param maxY double; expected value * @param minZ double; expected value * @param maxZ double; expected value */ private void testBounds3d(final Bounds3d b, final double minX, final double maxX, final double minY, final double maxY, final double minZ, final double maxZ) { assertEquals(minX, b.getMinX(), 0.001); assertEquals(maxX, b.getMaxX(), 0.001); assertEquals(minY, b.getMinY(), 0.001); assertEquals(maxY, b.getMaxY(), 0.001); assertEquals(minZ, b.getMinZ(), 0.001); assertEquals(maxZ, b.getMaxZ(), 0.001); } /** * Check that toString returns something descriptive. */ @Test public void toStringTest() { assertTrue("toString returns something descriptive", new Transform3d().toString().startsWith("Transform3d ")); } /** * Check what transform does to a unit vector. * @param args String[]; not used */ public static void main(final String[] args) { Point3d unitVector = new Point3d(1, 0, 0); double rotX = Math.toRadians(-55); double rotY = Math.toRadians(-65); double rotZ = Math.toRadians(-175); Transform3d transform = new Transform3d(); transform.rotZ(rotZ); System.out.println(transform.transform(unitVector)); transform.rotY(rotY); System.out.println(transform.transform(unitVector)); transform.rotX(rotX); Point3d rotated = transform.transform(unitVector); System.out.println(rotated); System.out.println("dirZ: " + Math.toDegrees(Math.atan2(rotated.y, rotated.x))); System.out.println( "dirY: " + Math.toDegrees(Math.atan2(-rotated.z, Math.sqrt(rotated.x * rotated.x + rotated.y * rotated.y))) + " == " + Math.toDegrees(Math.atan2(-rotated.z, Math.hypot(rotated.x, rotated.y)))); } }