3D Transformations – Part 1 Matrices

Transformations are fundamental to working with 3D scenes and something that can be frequently confusing to those that haven’t worked in 3D before. In this, the first of two articles I will show you how to encode 3D transformations as a single 4×4 matrix which you can then pass into the appropriate RealityServer command to position, orient and scale objects in your scene. In a second part I will dive into a newer method of specifying transformations in RealityServer called SRT transformations which also allows for the easy animation of objects.

Math and Notes

There is a bit of math in this article since we are dealing with matrices. A basic knowledge of vectors and matrices is assumed, however you don’t need to have done a full blown vector calculus course to understand how to make practical use of the information here. If you are so inclined however there is a lot of great information online, Google is your friend.

Also just a quick note, for angles expressed in this article we will be using radians rather than degrees. While it is common for user interfaces to present angles in degrees, the built in trigonometric functions of most programming languages expect angles in radians. You can easily code the conversion if needed using the following formula.

  \text{angle in radians} = \text{angle in degrees} \cdot \frac {\pi} {180^\circ}

Another important thing to remember is that RealityServer uses a right handed coordinate system. This dictates which way rotations will proceed around an axis. In our examples we will be assuming Z is our up axis. However this is a completely arbitrary choice and you can easily use Y as your up axis if you like (or any other axis for that matter). This only affects how you setup your camera. Up-ness is a topic for another article however.

In RealityServer, all transformations are encoded as a world to object space affine transformation encoded as a 4×4 matrix in row major order. Since the transform is from world to object space it may be the inverse of what you commonly see in other 3D applications. By default, when instances of objects are created they are assigned the default transformation matrix which is the identity matrix. This has no effect on the object at all.

  M =   \begin{bmatrix}  1 & 0 & 0 & 0 \\  0 & 1 & 0 & 0 \\  0 & 0 & 1 & 0 \\  0 & 0 & 0 & 1  \end{bmatrix}

We will be setting the values in this matrix in order to translate, rotate and scale our object. Note that object transformations can be nested along the scene graph. So if you had an instance which translated an object 3 units in the x direction which is contained within a group that is translated another 2 units in the x direction then the object will be translated by 5 units in the end.

If are already working with an application that uses transformation matrices but they are in object to world space form, you can usually simply invert the matrix before use in RealityServer. It if is also in column major rather than row major form then you will also need to transpose the matrix before inverting it. If building your matrices from scratch then if you follow this article you should be fine.

The examples in this article are the raw JSON-RPC command sequences that get sent to RealityServer. If you have not done so already please read the article on Exploring the RealityServer JSON-RPC API.

Starting Point

Throughout this article we will be applying transformations to a simple object in a test scene I have created. I’ll use a box which is 4 units long, 2 units high and 1 unit thick. The large grid areas on the floor are 1 unit in size and the small grid areas are 0.2 units. You can see our initial scene with our test object in it at its initial position to the right (it has the identity transformation applied to it).

First I will cover the three fundamental elements of transformations – translation, rotation and scaling. I’ll show you how to create matrices to represent each of these and then how to combine all three operations into a single matrix using matrix multiplication.

Even if you do not understand the mathematics involved you should still be able to follow along well enough to code transformations into your application by following the examples.

Test Scene

Test Object in Initial Position

Only the code for actually setting the transformations will be shown in the rest of this article, however if you wish to follow along with the same scene, here is the full code for creating the image above. The box object is called exBoxInstance if you want to perform operations on it.

Scene Creation Commands

[
	{"jsonrpc": "2.0", "method": "create_scope", "params": {
		"scope_name" : "exScope"
	}, "id": 1001},
	{"jsonrpc": "2.0", "method": "use_scope", "params": {
		"scope_name" : "exScope"
	}, "id": 1002},
	{"jsonrpc": "2.0", "method": "create_scene", "params": {
		"scene_name" : "exScene"
	}, "id": 1003},
	{"jsonrpc": "2.0", "method": "create_element", "params": {
		"element_name" : "exOptions",
		"element_type" : "Options"
	}, "id": 1004},
	{"jsonrpc": "2.0", "method": "scene_set_options", "params": {
		"scene_name" : "exScene",
		"options" : "exOptions"
	}, "id": 1005},
	{"jsonrpc": "2.0", "method": "create_element", "params": {
		"element_name" : "exRootGroup",
		"element_type" : "Group"
	}, "id": 1006},
	{"jsonrpc": "2.0", "method": "scene_set_rootgroup", "params": {
		"scene_name" : "exScene",
		"group" : "exRootGroup"
	}, "id": 1007},
	{"jsonrpc": "2.0", "method": "create_element", "params": {
		"element_name" : "exCamera",
		"element_type" : "Camera"
	}, "id": 1008},
	{"jsonrpc": "2.0", "method": "camera_set_resolution", "params": {
		"camera_name" : "exCamera",
		"resolution" : { "x" : 380, "y" : 380 }
	}, "id": 1009},
	{"jsonrpc": "2.0", "method": "camera_set_aspect", "params": {
		"camera_name" : "exCamera",
		"aspect" : 1.0
	}, "id": 1010},
	{"jsonrpc": "2.0", "method": "create_element", "params": {
		"element_name" : "exCameraInstance",
		"element_type" : "Instance"
	}, "id": 1011},
	{"jsonrpc": "2.0", "method": "instance_set_world_to_obj", "params": {
		"instance_name" : "exCameraInstance",
		"transform" : {
			"xx": 1,
			"xy": 0,
			"xz": 0,
			"xw": 0,
			"yx": 0,
			"yy": 0.5070201265633939,
			"yz": -0.86193421515776947,
			"yw": 0,
			"zx": 0,
			"zy": 0.86193421515776969,
			"zz": 0.50702012656339379,
			"zw": 0,
			"wx": -2,
			"wy": -0.25351006328169756,
			"wz": -9.4305743540791251,
			"ww": 1
		}
	}, "id": 1012},
	{"jsonrpc": "2.0", "method": "instance_attach", "params": {
		"instance_name" : "exCameraInstance",
		"item_name" : "exCamera"
	}, "id": 1013},
	{"jsonrpc": "2.0", "method": "group_attach", "params": {
		"group_name" : "exRootGroup",
		"item_name" : "exCameraInstance"
	}, "id": 1014},
	{"jsonrpc": "2.0", "method": "scene_set_camera_instance", "params": {
		"scene_name" : "exScene",
		"camera_instance" : "exCameraInstance"
	}, "id": 1015},
	{"jsonrpc": "2.0", "method": "element_set_attributes", "params": {
		"create" : true,
		"element_name" : "exCamera",
		"attributes" : {
			"tm_tonemapper" : {
				"type" : "String",
				"value" : "mia_exposure_photographic"
			},
			"mip_cm2_factor" : {
				"type" : "Float32",
				"value" : 1.0
			},
			"mip_film_iso" : {
				"type" : "Float32",
				"value" : 300.0
			},
			"mip_camera_shutter" : {
				"type" : "Float32",
				"value" : 0.05
			},
			"mip_f_number" : {
				"type" : "Float32",
				"value" : 1.75
			},
			"mip_gamma" : {
				"type" : "Float32",
				"value" : 2.2
			}
		}
	}, "id": 1016},
	{"jsonrpc": "2.0", "method": "generate_box", "params": {
		"name" : "exBox",
		"length" : 4.0,
		"width" : 1.0,
		"height" : 2.0
	}, "id": 1017},
	{"jsonrpc": "2.0", "method": "element_set_attribute", "params": {
		"element_name" : "exBox",
		"attribute_type" : "Boolean",
		"attribute_name" : "visible",
		"attribute_value" : true,
		"create" : true
	}, "id": 1018},
	{"jsonrpc": "2.0", "method": "create_element", "params": {
		"element_name" : "exBoxInstance",
		"element_type" : "Instance"
	}, "id": 1019},
	{"jsonrpc": "2.0", "method": "instance_attach", "params": {
		"instance_name" : "exBoxInstance",
		"item_name" : "exBox"
	}, "id": 1020},
	{"jsonrpc": "2.0", "method": "group_attach", "params": {
		"group_name" : "exRootGroup",
		"item_name" : "exBoxInstance"
	}, "id": 1021},
	{"jsonrpc": "2.0", "method": "import_scene_elements", "params": {
		"filename": "${shader}/material_examples/architectural.mdl"
	}, "id": 1022},
	{"jsonrpc": "2.0", "method": "create_material_instance_from_definition", "params": {
		"arguments": {
			"diffuse" : { "r" : 0.5, "g" : 0.5, "b" : 0.7 },
			"reflectivity" : 0.0
		},
		"material_definition_name": "mdl::material_examples::architectural::architectural",
		"material_name": "exBoxMaterial"
	}, "id": 1023},
	{"jsonrpc": "2.0", "method": "instance_set_material", "params": {
		"instance_name" : "exBoxInstance",
		"material_name" : "exBoxMaterial"
	}, "id": 1024},
	{"jsonrpc": "2.0", "method": "generate_mesh", "params": {
		"name" : "exPlane",
		"mesh" : {
			"vectors" : {
				"points" : [
					{"x" : -50.0, "y" : -50.0, "z": 0.0},
					{"x" : -50.0, "y" : 50.00, "z": 0.0},
					{"x" : 50.00, "y" : 50.00, "z": 0.0},
					{"x" : 50.00, "y" : -50.0, "z": 0.0}
				],
				"normals" : [
					{"x" : 0.0, "y" : 0.0, "z": 1.0}
				],
				"uvs" : [
					{"x" : 0.00, "y" : 0.00},
					{"x" : 0.00, "y" : 10.0},
					{"x" : 10.0, "y" : 10.0},
					{"x" : 10.0, "y" : 0.00}
				]
			},
			"vertices" : [
				{"v" : 0, "n": 0, "t": 0},
				{"v" : 1, "n": 0, "t": 1},
				{"v" : 2, "n": 0, "t": 2},
				{"v" : 3, "n": 0, "t": 3}
			],
			"polygons" : [
				[0, 1, 2, 3]
			]
		}
	}, "id": 1025},
	{"jsonrpc": "2.0", "method": "element_set_attribute", "params": {
		"element_name" : "exPlane",
		"attribute_type" : "Boolean",
		"attribute_name" : "visible",
		"attribute_value" : true,
		"create" : true
	}, "id": 1026},
	{"jsonrpc": "2.0", "method": "create_element", "params": {
		"element_name" : "exPlaneInstance",
		"element_type" : "Instance"
	}, "id": 1027},
	{"jsonrpc": "2.0", "method": "instance_attach", "params": {
		"instance_name" : "exPlaneInstance",
		"item_name" : "exPlane"
	}, "id": 1028},
	{"jsonrpc": "2.0", "method": "group_attach", "params": {
		"group_name" : "exRootGroup",
		"item_name" : "exPlaneInstance"
	}, "id": 1029},
	{"jsonrpc": "2.0", "method": "create_material_instance_from_definition", "params": {
		"arguments": {
			"diffuse" : { "r" : 0.7, "g" : 0.7, "b" : 0.7 },
			"reflectivity" : 0.0
		},
		"material_definition_name": "mdl::material_examples::architectural::architectural",
		"material_name": "exPlaneMaterial"
	}, "id": 1030},
	{"jsonrpc": "2.0", "method": "material_attach_texture_to_argument", "params": {
		"material_name" : "exPlaneMaterial",
		"argument_name" : "diffuse",
		"texture_name" : "scenes/meyemII/images/metaslgrid09.png",
		"texture_options" : {},
		"create_from_file" : true
	}, "id": 1031},	
	{"jsonrpc": "2.0", "method": "instance_set_material", "params": {
		"instance_name" : "exPlaneInstance",
		"material_name" : "exPlaneMaterial"
	}, "id": 1032},
	{"jsonrpc": "2.0", "method": "import_scene_elements", "params": {
		"filename": "${shader}/material_examples/lights_photometric.mdl"
	}, "id": 1033},

	{"jsonrpc": "2.0", "method": "create_element", "params": {
		"element_name" : "exRectLight",
		"element_type" : "Light"
	}, "id": 1034},
	{"jsonrpc": "2.0", "method": "light_set_area_shape", "params": {
		"light_name" : "exRectLight",
		"area_shape" : "rectangle"
	}, "id": 1035},
	{"jsonrpc": "2.0", "method": "light_set_area_size", "params": {
		"light_name" : "exRectLight",
		"area_size" : {"x": 8.0, "y" : 8.0}
	}, "id": 1036},
	{"jsonrpc": "2.0", "method": "create_material_instance_from_definition", "params": {
		"material_definition_name" : "mdl::material_examples::lights_photometric::diffuse_area_light",
		"material_name" : "exRectLightShader",
		"arguments" : {
			"flux" : 156.25
		}
	}, "id": 1037},
	{"jsonrpc": "2.0", "method": "element_set_attribute", "params": {
		"element_name" : "exRectLight",
		"attribute_type" : "Ref",
		"attribute_name" : "material",
		"attribute_value" : "exRectLightShader",
		"create" : true
	}, "id": 1038},
	{"jsonrpc": "2.0", "method": "create_element", "params": {
		"element_name" : "exRectLightInstance",
		"element_type" : "Instance"
	}, "id": 1039},
	{"jsonrpc": "2.0", "method": "instance_attach", "params": {
		"instance_name" : "exRectLightInstance",
		"item_name" : "exRectLight"
	}, "id": 1040},
	{"jsonrpc": "2.0", "method": "instance_set_world_to_obj", "params": {
		"instance_name" : "exRectLightInstance",
		"transform" : {
			"xx": 0.70710678118654735,
			"xy": 0,
			"xz": 0.70710678118654746,
			"xw": 0,
			"yx": 0.0,
			"yy": 1.0,
			"yz": 0.0,
			"yw": 0.0,
			"zx": -0.70710678118654746,
			"zy": 0,
			"zz": 0.70710678118654768,
			"zw": 0,
			"wx": -4.2426406871192839,
			"wy": -1,
			"wz": -9.8994949366116654,
			"ww": 1
		}
	}, "id": 1041},
	{"jsonrpc": "2.0", "method": "group_attach", "params": {
		"group_name" : "exRootGroup",
		"item_name" : "exRectLightInstance"
	}, "id": 1042},

	{"jsonrpc": "2.0", "method": "export_scene", "params": {
		"scene_name" : "exScene",
		"filename" : "scenes/exScene.mi"
	}, "id": 8001},
	{"jsonrpc": "2.0", "method": "element_set_attributes", "params": {
		"element_name" : "exOptions",
		"create" : true,
		"attributes" : {
			"progressive_rendering_max_samples" : {
				"type" : "Sint32",
				"value" : 2500
			},
			"progressive_rendering_max_time" : {
				"type" : "Sint32",
				"value" : 1000
			},
			"progressive_rendering_quality_enabled" : {
				"type" : "Boolean",
				"value" : false
			}
		}
	}, "id": 9001},
	{"jsonrpc": "2.0", "method": "render", "params": {
		"scene_name" : "exScene",
		"renderer" : "iray",
		"render_context_options" : {
			"scheduler_mode" : {
				"type" : "String",
				"value" : "batch"
			}
		}
	}, "id": 9002},
	{"jsonrpc": "2.0", "method": "delete_scope", "params": {
		"scope_name" : "exScope"
	}, "id": 9003}
]

Translation

Let’s start with the simplest type of transformation, translations. This is what moves your objects around your scene. When used alone these are very simple and only affect three values in our transformation matrix.

  T(v_x,v_y,v_z) =   \begin{bmatrix}  1 & 0 & 0 & 0 \\  0 & 1 & 0 & 0 \\  0 & 0 & 1 & 0 \\  -v_x & -v_y & -v_z & 1  \end{bmatrix}

So for example, if you want to move your object 1 unit in the x direction, -2 units in the y direction and 0.5 units in the z direction we would use the following matrix shown to the right. We can set this using the code below.

  T(1,-2,0.5) =   \begin{bmatrix}  1 & 0 & 0 & 0 \\  0 & 1 & 0 & 0 \\  0 & 0 & 1 & 0 \\  -1 & 2 & -0.5 & 1  \end{bmatrix}
{
  "jsonrpc": "2.0",
  "method": "instance_set_world_to_obj",
  "params": {
    "instance_name" : "exBoxInstance",
    "transform" : {
      "xx": 1, "xy": 0, "xz": 0, "xw": 0,
      "yx": 0, "yy": 1, "yz": 0, "yw": 0,
      "zx": 0, "zy": 0, "zz": 1, "zw": 0,
      "wx": -1, "wy": 2, "wz": -0.5, "ww": 1
    }
  },
  "id": 1000
}
Transformed

Translation

Original

Original

Rotations

Rotations are quite a bit more complex. For this we need to compute a standard 3×3 rotation matrix which will become part of our 4×4 matrix. Again though, when used alone, it is relatively straight forward to setup. Let’s first show how to compute rotation matrices for each of the standard orthogonal axis.

X Axis

We’ll start with the x axis (1,0,0) and rotating around that. To the right you can see the general form of the matrix required for this. Notice the rotation part takes up the top left 3 x 3 positions.

  R_x(\theta) =   \begin{bmatrix}  1 & 0 & 0 & 0 \\  0 & cos(-\theta) & sin(-\theta) & 0 \\  0 & -sin(-\theta) & cos(-\theta) & 0 \\  0 & 0 & 0 & 1  \end{bmatrix}

Let’s do a quick example. We’ll rotate 45° in the x axis. So that will be 0.7853981634 radians. Remember we must negate our angles before passing them to our trigonometric functions. Some quick math and we get the matrix to the right. The command for applying this is below.

  R_x(0.7853981634) =   \begin{bmatrix}  1 & 0 & 0 & 0 \\  0 & 0.7071067811865476 & -0.7071067811865475 & 0 \\  0 & 0.7071067811865475 & 0.7071067811865476 & 0 \\  0 & 0 & 0 & 1  \end{bmatrix}
{
  "jsonrpc": "2.0",
  "method": "instance_set_world_to_obj",
  "params": {
    "instance_name" : "exBoxInstance",
    "transform" : {
      "xx": 1,
      "xy": 0,
      "xz": 0,
      "xw": 0,
      "yx": 0,
      "yy": 0.7071067811865476,
      "yz": -0.7071067811865475,
      "yw": 0,
      "zx": 0,
      "zy": 0.7071067811865475,
      "zz": 0.7071067811865476,
      "zw": 0,
      "wx": 0,
      "wy": 0,
      "wz": 0,
      "ww": 1
    }
  },
  "id": 1000
}
Transformed

Rotation X

Original

Original

Y Axis

Let’s move onto the y axis (0,1,0) and rotating around that. To the right you can see the general form of the matrix required for this. Notice the rotation part takes up the top left 3 x 3 positions as before.

  R_y(\theta) =   \begin{bmatrix}  cos(-\theta) & 0 & -sin(-\theta) & 0 \\  0 & 1 & 0 & 0 \\  sin(-\theta) & 0 & cos(-\theta) & 0 \\  0 & 0 & 0 & 1  \end{bmatrix}

Let’s do another quick example. We’ll rotate -90° in the y axis. So that will be -1.5707963268 radians. A bit more math and we get the following matrix. The command for applying this is below.

  R_y(-1.5707963268) =   \begin{bmatrix}  0 & 0 & -1 & 0 \\  0 & 1 & 0 & 0 \\  1 & 0 & 0 & 0 \\  0 & 0 & 0 & 1  \end{bmatrix}
{
  "jsonrpc": "2.0",
  "method": "instance_set_world_to_obj",
  "params": {
    "instance_name" : "exBoxInstance",
    "transform" : {
        "xx": 0, "xy": 0, "xz": -1, "xw": 0,
        "yx": 0, "yy": 1, "yz": 0, "yw": 0,
        "zx": 1, "zy": 0, "zz": 0, "zw": 0,
        "wx": 0, "wy": 0, "wz": 0, "ww": 1
    }
  },
  "id": 1000
}
Transformed

Rotation Y

Original

Original

Z Axis

Finally we’ll do the z axis (0,0,1) rotation. To the right you can see the general form of the matrix required for this. Notice the rotation part takes up the top left 3 x 3 positions as before.

  R_z(\theta) =   \begin{bmatrix}  cos(-\theta) & sin(-\theta) & 0 & 0 \\  -sin(-\theta) & cos(-\theta) & 0 & 0 \\  0 & 0 & 1 & 0 \\  0 & 0 & 0 & 1  \end{bmatrix}

One more angle, we’ll rotate 90° in the z axis this time. So that will be 1.5707963268 radians. The command for applying this is below.

  R_z(-1.5707963268) =   \begin{bmatrix}  0 & -1 & 0 & 0 \\  1 & 0 & 0 & 0 \\  0 & 0 & 1 & 0 \\  0 & 0 & 0 & 1  \end{bmatrix}
{
  "jsonrpc": "2.0",
  "method": "instance_set_world_to_obj",
  "params": {
    "instance_name" : "exBoxInstance",
    "transform" : {
        "xx": 0, "xy": -1, "xz": 0, "xw": 0,
        "yx": 1, "yy": 0, "yz": 0, "yw": 0,
        "zx": 0, "zy": 0, "zz": 1, "zw": 0,
        "wx": 0, "wy": 0, "wz": 0, "ww": 1
    }
  },
  "id": 1000
}
Transformed

trans_05_rotation_z

Original

Original

Arbitary Axis

Rotating around the standard orthogonal axis is probably the most common rotation operation. The individual rotations above can be combined into a single operation using matrix multiplication (more on that later). However what about rotation around an arbitary axis? This is also a very common operation (for example if you want to roll a camera around its axis). For this we need a more complex general matrix shown below.

  R_{xyz}(x,y,z,\theta) =   \begin{bmatrix}  1 - cos(-\theta) \cdot x^2 + cos(-\theta) &  1 - cos(-\theta) \cdot x \cdot y + sin(-\theta) \cdot z &  1 - cos(-\theta) \cdot x \cdot z - sin(-\theta) \cdot y &  0 \\  1 - cos(-\theta) \cdot x \cdot y - sin(-\theta) \cdot z &  1 - cos(-\theta) \cdot y^2 + cos(-\theta) &  1 - cos(-\theta) \cdot y \cdot z + sin(-\theta) \cdot x &  0 \\  1 - cos(-\theta) \cdot x \cdot z + sin(-\theta) \cdot y &  1 - cos(-\theta) \cdot y \cdot z - sin(-\theta) \cdot x &  1 - cos(-\theta) \cdot z^2 + cos(-\theta) &  0 \\  0 &  0 &  0 &  1  \end{bmatrix}

To use this type of rotation we need to specify both and angle and an axis about which to rotate. Let’s try rotating -90° around the (0.577350269, 0.577350269, 0.577350269) axis. So that’s -1.5707963268 radians giving us the following matrix.

  R_{xyz}(0.57,0.57,0.57,-1.5707963268) =   \begin{bmatrix}  0.3333333331143723 & 0.9106836021143723 & -0.24401693588562773 & 0 \\  -0.24401693588562773 & 0.3333333331143723 & 0.9106836021143723 & 0 \\  0.9106836021143723 & -0.24401693588562773 & 0.3333333331143723 & 0 \\  0 & 0 & 0 & 1  \end{bmatrix}

If you want to see code for how to do this, which actually looks much simpler than the maths here you can take a look at the example JavaScript matrix class near the end of this article. You can easily port this to any language.

{
  "jsonrpc": "2.0",
  "method": "instance_set_world_to_obj",
  "params": {
    "instance_name" : "exBoxInstance",
    "transform" : {
        "xx": 0.3333333331143723,
        "xy": 0.9106836021143723,
        "xz": -0.24401693588562773,
        "xw": 0,
        "yx": -0.24401693588562773,
        "yy": 0.3333333331143723,
        "yz": 0.9106836021143723,
        "yw": 0,
        "zx": 0.9106836021143723,
        "zy": -0.24401693588562773,
        "zz": 0.3333333331143723,
        "zw": 0,
        "wx": 0,
        "wy": 0,
        "wz": 0,
        "ww": 1
    }
  },
  "id": 1000
}
Transformed

Rotation Arbitary

Original

Original

Scaling

The last fundament transformation type is scaling, making our object larger or smaller in a given axis. Fortunately this one is very simple. To the right is the general form of the scaling matrix we need.

  S(v_x,v_y,v_z) =   \begin{bmatrix}  ^1/_{v_x} & 0 & 0 & 0 \\  0 & ^1/_{v_y} & 0 & 0 \\  0 & 0 & ^1/_{v_z} & 0 \\  0 & 0 & 0 & 1  \end{bmatrix}

For a quick example let’s scale by 1.25 in the x axis, 4 in the y axis and 0.5 in the z axis. This is obviously a non-uniform scaling. Note that negative scales will mirror your object. Here is our matrix on the right.

  S(1.25,5,0.5) =   \begin{bmatrix}  0.8 & 0 & 0 & 0 \\  0 & 0.25 & 0 & 0 \\  0 & 0 & 2 & 0 \\  0 & 0 & 0 & 1  \end{bmatrix}
{
  "jsonrpc": "2.0",
  "method": "instance_set_world_to_obj",
  "params": {
    "instance_name" : "exBoxInstance",
    "transform" : {
        "xx": 0.8, "xy": 0, "xz": 0, "xw": 0,
        "yx": 0, "yy": 0.25, "yz": 0, "yw": 0,
        "zx": 0, "zy": 0, "zz": 2, "zw": 0,
        "wx": 0, "wy": 0, "wz": 0, "ww": 1
    }
  },
  "id": 1000
}
Transformed

Scaling

Original

Original

Combining Transformations

So, we can do all of the fundamental transformations now, however in the examples above we are only doing a single operation. What if we want to move, rotate and scale our object? For this we need to combine our individual transformation matrices into a single 4×4 matrix. We do this with matrix multiplication. For matrix multiplication, let’s identify the individual elements of our matrix as follows.

   \begin{bmatrix}  11 & 12 & 13 & 14 \\  21 & 22 & 23 & 24 \\  31 & 32 & 33 & 34 \\  41 & 42 & 43 & 44  \end{bmatrix}

Using these identifiers, we can multiply matrix A by matrix B as follows. Note that order is important when performing matrix multiplication, so ABC ≠ CBA, unlike multiplying numbers. Here is how we do the multiplication.

  A \cdot B =  \begin{bmatrix}  A_{11} \cdot B_{11} + A_{12} \cdot B_{21} + A_{13} \cdot B_{31} + A_{14} \cdot B_{41} &  A_{11} \cdot B_{12} + A_{12} \cdot B_{22} + A_{13} \cdot B_{32} + A_{14} \cdot B_{42} &  A_{11} \cdot B_{13} + A_{12} \cdot B_{23} + A_{13} \cdot B_{33} + A_{14} \cdot B_{43} &  A_{11} \cdot B_{14} + A_{12} \cdot B_{24} + A_{13} \cdot B_{34} + A_{14} \cdot B_{44} &  \\  A_{21} \cdot B_{11} + A_{22} \cdot B_{21} + A_{23} \cdot B_{31} + A_{24} \cdot B_{41} &  A_{21} \cdot B_{12} + A_{22} \cdot B_{22} + A_{23} \cdot B_{32} + A_{24} \cdot B_{42} &  A_{21} \cdot B_{13} + A_{22} \cdot B_{23} + A_{23} \cdot B_{33} + A_{24} \cdot B_{43} &  A_{21} \cdot B_{14} + A_{22} \cdot B_{24} + A_{23} \cdot B_{34} + A_{24} \cdot B_{44} &  \\  A_{31} \cdot B_{11} + A_{32} \cdot B_{21} + A_{33} \cdot B_{31} + A_{34} \cdot B_{41} &  A_{31} \cdot B_{12} + A_{32} \cdot B_{22} + A_{33} \cdot B_{32} + A_{34} \cdot B_{42} &  A_{31} \cdot B_{13} + A_{32} \cdot B_{23} + A_{33} \cdot B_{33} + A_{34} \cdot B_{43} &  A_{31} \cdot B_{14} + A_{32} \cdot B_{24} + A_{33} \cdot B_{34} + A_{34} \cdot B_{44} &  \\  A_{41} \cdot B_{11} + A_{42} \cdot B_{21} + A_{43} \cdot B_{31} + A_{44} \cdot B_{41} &  A_{41} \cdot B_{12} + A_{42} \cdot B_{22} + A_{43} \cdot B_{32} + A_{44} \cdot B_{42} &  A_{41} \cdot B_{13} + A_{42} \cdot B_{23} + A_{43} \cdot B_{33} + A_{44} \cdot B_{43} &  A_{41} \cdot B_{14} + A_{42} \cdot B_{24} + A_{43} \cdot B_{34} + A_{44} \cdot B_{44}  \end{bmatrix}

Ok, that’s pretty crazy but of course you will have this in a nice matrix multiplication function in your code, so you can easily multiply any number of matrices together. Now, when combining transformation matrices you must multiply them in the reverse order to that in which you want them applied. This is critical, you will get completely the wrong answer otherwise. So, if we want to scale our object, then rotate it and then translate it. We must multiply the translation matrix by the rotation matrix by the scaling matrix, in that order.

You can multiply together as many transformation matrices as you like, including multiple matrices of the same type of transformation. So it’s perfectly valid to perform three rotations, followed by two translations, followed by another rotation followed by a scaling operation. Or any other combination you can think of, however pay close attention to how the order of your transformations affects the results.

Transformation Library

Using all of the principles explained in this article, here then is a simple, self contained JavaScript matrix library, based in part on the code we ship with RealityServer in our client library. However the code below can be used completely independently from RealityServer if you wish. With it you can build tools to create suitable input for the instance_set_world_to_obj command.

JavaScript Matrix Library

/**
 * This file defines the Matrix4x4 class.
 * @file Matrix4x4.js
 */

/**
 * Generic class for representing 4x4 matrices.
 * @constructor
 * @param {Object} matrix - An object with the initial values for the Matrix4x4. 
 * Can be either an Object or Matrix4x4.
 */
var Matrix4x4 = function(matrix) {    
    if (matrix) {
        this.set_from_object(matrix);
    } else {
        this.set_identity();
    }
}

/**
 * xx component of the matrix.
 * @type {Number}
 * @public
 */
Matrix4x4.prototype.xx;

/**
 * xy component of the matrix.
 * @type {Number}
 * @public
 */
Matrix4x4.prototype.xy;

/**
 * xz component of the matrix.
 * @type {Number}
 * @public
 */
Matrix4x4.prototype.xz;

/**
 * xw component of the matrix.
 * @type {Number}
 * @public
 */
Matrix4x4.prototype.xw;

/**
 * yx component of the matrix.
 * @type {Number}
 * @public
 */
Matrix4x4.prototype.yx;

/**
 * yy component of the matrix.
 * @type {Number}
 * @public
 */
Matrix4x4.prototype.yy;

/**
 * yz component of the matrix.
 * @type {Number}
 * @public
 */
Matrix4x4.prototype.yz;

/**
 * yw component of the matrix.
 * @type {Number}
 * @public
 */
Matrix4x4.prototype.yw;

/**
 * zx component of the matrix.
 * @type {Number}
 * @public
 */
Matrix4x4.prototype.zx;

/**
 * zy component of the matrix.
 * @type {Number}
 * @public
 */
Matrix4x4.prototype.zy;

/**
 * zz component of the matrix.
 * @type {Number}
 * @public
 */
Matrix4x4.prototype.zz;

/**
 * zw component of the matrix.
 * @type {Number}
 * @public
 */
Matrix4x4.prototype.zw;

/**
 * wx component of the matrix.
 * @type {Number}
 * @public
 */
Matrix4x4.prototype.wx;

/**
 * wy component of the matrix.
 * @type {Number}
 * @public
 */
Matrix4x4.prototype.wy;

/**
 * wz component of the matrix.
 * @type {Number}
 * @public
 */
Matrix4x4.prototype.wz;

/**
 * ww component of the matrix.
 * @type {Number}
 * @public
 */
Matrix4x4.prototype.ww;

/**
 * xx component of the matrix.
 * @type {Number}
 * @public
 */

/**
 * Set matrix components from object
 * @param {Object} obj - Object with each component of the matrix, all components must be present.
 * @public
 */
Matrix4x4.prototype.set_from_object = function(obj) {

    this.xx = obj.xx; this.xy = obj.xy; this.xz = obj.xz; this.xw = obj.xw;
    this.yx = obj.yx; this.yy = obj.yy; this.yz = obj.yz; this.yw = obj.yw;
    this.zx = obj.zx; this.zy = obj.zy; this.zz = obj.zz; this.zw = obj.zw;
    this.wx = obj.wx; this.wy = obj.wy; this.wz = obj.wz; this.ww = obj.ww;

}

/**
 * Set all matrix components to 0.
 * @public
 */
Matrix4x4.prototype.clear = function() {
    this.xx = this.xy = this.xz = this.xw =
    this.yx = this.yy = this.yz = this.yw =
    this.zx = this.zy = this.zz = this.zw =
    this.wx = this.wy = this.wz = this.ww = 0;
}

/**
 * Set matrix to the identity matrix.
 * @public
 */
Matrix4x4.prototype.set_identity = function() {
    this.clear();
    this.xx = this.yy = this.zz = this.ww = 1;
}

/**
 * Sets this matrix to a rotation matrix.
 * @param {Number} axis - The vector to rotate around.
 * @param {Number} angle - The angle to rotate in radians.
 * @public
 */
Matrix4x4.prototype.set_rotation = function(axis, angle) {
    this.set_identity();

    var c = Math.cos(angle);
    var s = Math.sin(angle);
    var t = 1-c;
    var X = axis.x;
    var Y = axis.y;
    var Z = axis.z;

    this.xx = t * X * X + c;
    this.xy = t * X * Y + s * Z;
    this.xz = t * X * Z - s * Y;

    this.yx = t * X * Y - s * Z;
    this.yy = t * Y * Y + c;
    this.yz = t * Y * Z + s * X;

    this.zx = t * X * Z + s * Y;
    this.zy = t * Y * Z - s * X;
    this.zz = t * Z * Z + c;
}

/**
 * Sets this matrix to a rotation matrix.
 * @param {Number} x - Scaling factor in the x axis.
 * @param {Number} y - Scaling factor in the y axis.
 * @param {Number} z - Scaling factor in the z axis.
 * @public
 */
Matrix4x4.prototype.set_scaling = function(x, y, z) {
    this.set_identity();

    this.xx = x;
    this.yy = y;
    this.zz = z;
}

/**
 * Sets the translation elements of this matrix while leaving the 
 * rest of the matrix untouched. 
 * @param {Number} x - Translation amount in the x axis.
 * @param {Number} y - Translation amount in the y axis.
 * @param {Number} z - Translation amount in the z axis.
 */ 
Matrix4x4.prototype.set_translation_elements = function(x, y, z) {
    this.wx = x;
    this.wy = y;
    this.wz = z;
}

/**
 * Sets this matrix to the dot product between this matrix and the 
 * matrix specified by rhs.
 * @param {Matrix4x4} matrix - The matrix on the right hand side of the dot product.
 */ 
Matrix4x4.prototype.multiply = function(matrix) {

    var _mat = new Matrix4x4(this);

    this.xx = _mat.xx * matrix.xx + _mat.xy * matrix.yx + _mat.xz * matrix.zx + _mat.xw * matrix.wx;
    this.xy = _mat.xx * matrix.xy + _mat.xy * matrix.yy + _mat.xz * matrix.zy + _mat.xw * matrix.wy;
    this.xz = _mat.xx * matrix.xz + _mat.xy * matrix.yz + _mat.xz * matrix.zz + _mat.xw * matrix.wz;
    this.xw = _mat.xx * matrix.xw + _mat.xy * matrix.yw + _mat.xz * matrix.zw + _mat.xw * matrix.ww;
    this.yx = _mat.yx * matrix.xx + _mat.yy * matrix.yx + _mat.yz * matrix.zx + _mat.yw * matrix.wx;
    this.yy = _mat.yx * matrix.xy + _mat.yy * matrix.yy + _mat.yz * matrix.zy + _mat.yw * matrix.wy;
    this.yz = _mat.yx * matrix.xz + _mat.yy * matrix.yz + _mat.yz * matrix.zz + _mat.yw * matrix.wz;
    this.yw = _mat.yx * matrix.xw + _mat.yy * matrix.yw + _mat.yz * matrix.zw + _mat.yw * matrix.ww;
    this.zx = _mat.zx * matrix.xx + _mat.zy * matrix.yx + _mat.zz * matrix.zx + _mat.zw * matrix.wx;
    this.zy = _mat.zx * matrix.xy + _mat.zy * matrix.yy + _mat.zz * matrix.zy + _mat.zw * matrix.wy;
    this.zz = _mat.zx * matrix.xz + _mat.zy * matrix.yz + _mat.zz * matrix.zz + _mat.zw * matrix.wz;
    this.zw = _mat.zx * matrix.xw + _mat.zy * matrix.yw + _mat.zz * matrix.zw + _mat.zw * matrix.ww;
    this.wx = _mat.wx * matrix.xx + _mat.wy * matrix.yx + _mat.wz * matrix.zx + _mat.ww * matrix.wx;
    this.wy = _mat.wx * matrix.xy + _mat.wy * matrix.yy + _mat.wz * matrix.zy + _mat.ww * matrix.wy;
    this.wz = _mat.wx * matrix.xz + _mat.wy * matrix.yz + _mat.wz * matrix.zz + _mat.ww * matrix.wz;
    this.ww = _mat.wx * matrix.xw + _mat.wy * matrix.yw + _mat.wz * matrix.zw + _mat.ww * matrix.ww;
}

/**
 * Returns a deep copy of this matrix.
 * @return {Matrix4x4} A deep copy of this matrix.
 */ 
Matrix4x4.prototype.clone = function() {
    return new Matrix4x4(this);
}

/**
 * Returns a pretty print string representation of the matrix.
 * @return {String} Pretty printed string of the matrix.
 */ 
Matrix4x4.prototype.to_string = function() {
    return '{\n'
        + '\t"xx": ' + this.xx + ', "xy": ' + this.xy + ', "xz": ' + this.xz + ', "xw": ' + this.xw + ',\n'
        + '\t"yx": ' + this.yx + ', "yy": ' + this.yy + ', "yz": ' + this.yz + ', "yw": ' + this.yw + ',\n'
        + '\t"zx": ' + this.zx + ', "zy": ' + this.zy + ', "zz": ' + this.zz + ', "zw": ' + this.zw + ',\n'
        + '\t"wx": ' + this.wx + ', "wy": ' + this.wy + ', "wz": ' + this.wz + ', "ww": ' + this.ww + '\n'
        +'}';
}

module.exports = Matrix4x4;

Note that this matrix class is generic and just works directly on the values provided. To be used with RealityServer you need to negate values used for translations, negate angles used for rotations and invert values used for scaling. As an example see this simple test code.

JavaScript Matrix Library Tests

var Matrix4x4 = require('./Matrix4x4.js');

var test_translation_matrix = new Matrix4x4();
var test_translation_vector = {x: 1.5, y: -2.5, z: -0.2};
test_translation_matrix.set_translation_elements(
	-test_translation_vector.x,
	-test_translation_vector.y,
	-test_translation_vector.z
);
console.info("Translation elements set to 1.5, -2.5, 0.2:");
console.info(test_translation_matrix.to_string());

var test_rotation_matrix = new Matrix4x4();
var test_rotation_angle = 45;
var test_rotation_axis = {x: 0, y: 0, z: 1};
test_rotation_matrix.set_rotation(test_rotation_axis, test_rotation_angle * (Math.PI / 180.0));
console.info("Rotation matrix for 45 degree rotation around 0, 0, 1 axis:");
console.info(test_rotation_matrix.to_string());

var test_scaling_matrix = new Matrix4x4();
test_scaling_vector = {x: 2.0, y: 0.4, z: 10};
test_scaling_matrix.set_scaling(
	1 / test_scaling_vector.x,
	1 / test_scaling_vector.y,
	1 / test_scaling_vector.z
);
console.info("Scaling matrix by 2.0, 0.4, 10:");
console.info(test_scaling_matrix.to_string());

var test_rotate_then_translate = test_translation_matrix.clone();
test_rotate_then_translate.multiply(test_rotation_matrix);
console.info("Rotate then translate (translate matrix x rotation matrix):");
console.info(test_rotate_then_translate.to_string());

var test_rotate_then_translate_then_scale = test_rotate_then_translate.clone();
test_rotate_then_translate_then_scale.multiply(test_scaling_matrix);
console.info("Scale then rotate then translate (translate matrix x rotation matrix x scale matrix):");
console.info(test_rotate_then_translate_then_scale.to_string());

The above code uses Node.js module style but is easily adapted for use in browsers or other places. It can also obviously be easily ported to any other language.

Going Further

That covers the essentials of setting up transformation matrices in RealityServer (and likely also a wide range of other 3D tools that use affine transformation matrices). While there is some complexity involved, once you have a nice library for working with your transformations it is all quite simple. Often our new users know about transformation matrices but are not sure about the format used for those in RealityServer, the above hopefully clarifies this. If not or if you have any other questions, don’t hesitate to contact us for more information.

Also, keep an eye out for part 2 of this article where I will be diving into the newer SRT transformation type in RealityServer and how it can help with easily animating your objects.

Paul Arden

Paul Arden has worked in the Computer Graphics industry for over 20 years, co-founding the architectural visualisation practice Luminova out of university before moving to mental images and NVIDIA to manage the Cloud-based rendering solution, RealityServer, now managed by migenius where Paul serves as CEO.

More Posts - LinkedIn

Articles Tutorials
Get in Touch