So a while back I wrote a post about transforming Revit’s internal coordinates to the Project/Survey coordinate systems. While it was a good stab, it wasn’t the best implementation.. I was wildly unfamiliar with Transforms back then and saw them as this mysterious thing with things called basis vectors and other weird stuff. Soooo, I tried doing the conversions between internal, project and shared spaces via individual transformations… Translate, Rotate etc. Heh Heh… well, while it worked in most cases, it didn’t in some cases, most notably when the Project Base Point was moved unclipped… <sigh>

Well, a few years later I am much better versed in Transformations (you could say I have transformed! :P), mostly because I have to use them daily in our Unity and Revit development and also because I have studied them a little in my degree. They are far less scary than they seem.

Transforms in general…

Basically, (and skipping ALL the math – this will not be a detailed explanation of Transforms) a Transform is a way of describing the Translation, Rotation and Scale of something relative to some coordinate/reference system. To keep things simple, I will be looking at 3D affine transformations as most people who use 3D applications will have some basic understanding of the concept of Translate, Rotate, Scale. 3D Transforms are generally written as a 4×4 Matrix (4 rows, 4 columns) and are called Transformation Matrices. Matrices have some very cool properties which make computation easy as well as being nice containers for the data and they are heavily used in computer graphics. Anyway, Let’s take the following Transform…

T1 = [(0, 0, 0), (0, 0, 0), (1, 1, 1)]

Where…

T = [(Translation), (Rotation), (Scale)]

Which in 4×4 Matrix form is…

T = \begin{bmatrix} tx & ty & tz & 0 \\ rx & ry & rz & 0 \\ sx & sy & sz & 0  \\0 & 0 & 0 & 1 \end{bmatrix}

If we were to apply T1 to the point P1 = (9, 8, 7), then the point would remain where it is. It is not Translated, Rotated or Scaled. So P2 = (9, 8, 7).

Now, take the Transform…

T2 = [(1, 2, 3), (0, 0, 0), (1, 1, 1)]

If we apply T2 to the point P1 this would translate the position to P2 = (10, 10, 10). That is, the point P1 has moved 1 unit in the X axis, 2 units in the Y axis and 3 units in the Z axis. So, we have just moved/translated the point, or in other words, we have just Transformed the point. The same could be said for Rotations and Scale, the Transform simply holds the values for these Transform operations and when you apply them to a point, the operations are applied to the point.

Now we have covered the very simplest of concepts, we should next discuss the inverse of a Transform as we will need it later in the code. Well, it is pretty much as it sounds, it reverses (or inverts) the transformations. Let’s take P2, if we apply the inverse of T2, then we Transform the point back to P1 = (9, 8, 7). That simple!

There is one final thing to talk about when dealing with Transforms and that is how they are applied to the object. There are two ways, which are Active and Passive. Active transformations are applied to the object, so literally moving the object in the coordinate system. Whereas, Passive transformations leave the object where it is, but instead transform the coordinate system.

Active Transform Example – Any time we move an element in Revit, we are applying an active transformation to the Element… this concept we are very used to from using the Move, Rotate, Scale and Mirror commands.

Passive Transform Example – One example, at least for visualising passive transformations, would be when we switch a plan view between True/Project North (where the Project Base Point angle to True North is not equal to zero). All the geometry appears to move and rotate, but Revit certainly does not move all the geometry, instead it probably locks the Cameras Transform to the active coordinate system… so, when the coordinate system changes the camera with it. So, this would be a passive transformation of sorts because the coordinates of the geometry are still the same internal coordinates, but coordinate system itself changes (at last visually).

Passive Transformation

There are a lot of things you can do with Transforms, and I am certainly only covering the bare minimum here for context otherwise this post will get very, VERY long and there is already plenty on Transforms (whole chapters of books) out there by actual experts if this really tickles your fancy. Instead, let’s use what we just discussed to show how this works in the context of some 3D application. For this, let’s use Revit as I should really correct the mess of my initial post and code…

Revit Transforms…

Revit, just like any 3D application, requires heavy use of Transforms for all manner of things from Rendering to Projection to Positioning. In this particular case, we will look at Positioning and the location of Elements with respect to their Internal, Project and Shared position.

Before we do though, let’s quickly talk about how Revit works and how it positions things.

Families and Family Instances…

For anyone familiar with the Revit API, you might know that Elements are often described as FamilyInstance and FamilySymbols, this is equivalent to you non-familiar to the Revit API as the Element Instance and the Family/Element Type respectively. We will call them Family Instance and Family Type to avoid confusion in terminology.

As a Revit user, when you place a Family Instance in Revit, you are working in a plane of some kind (Level, Reference Plane etc) and you usually just point and click where you want your Family Instance and it magically gets placed exactly (in most cases) where you wanted it. But behind the scenes, what (or what I believe) is happening when you create a Family Instance is that you are actually kind of making a copy of the Family Type, which has zero Transformations, and then Revit applies the Transformations to this copy to give you the Family Instance in the location you wanted.

To put this in another way, imagine you have a cube family and the cube family has its origin at the centre of the cube in the Family Editor. Now, imagine you placed this cube at 0, 0, 0 with no rotation… well, this is the default position the Family Type has. If we create another cube instance at some arbitrary distance from origin and rotation, the Family Types Transform is not changed, instead the Family Instance copies (well sort of copies – it actually references) the Family Type and applies the Transform. See below gallery for this in images…

As you can see from the above imagery, in the first image, we have 3 boxes modelled in Revit. First is placed at origin with no translation or rotation, second with a translation, and third with a translation and rotation. In the second image though, we only see one box, this is the Family Type Geometry for all 3 boxes in Revit which are all in the same place, I have also drawn axes that represent the Transforms as Coordinate Systems – this is the family instance transformation. In the third and final image, we apply the Transformations to the Family Type Geometry and we get representations of the Family Instance geometry. Here is the script to do this if you want to try it out yourself…

import clr

clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript import Geometry as geom

clr.AddReference("RevitServices")
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
doc =  DocumentManager.Instance.CurrentDBDocument

clr.AddReference("RevitNodes")
import Revit
clr.ImportExtensions(Revit.Elements)
clr.ImportExtensions(Revit.GeometryConversion)

clr.AddReference("RevitAPI")
from Autodesk.Revit.DB import *

##### Definitions #####

"""
Ensure a list of Objects
"""
def tolist(obj1):
	if hasattr(obj1,"__iter__"): return obj1
	else: return [obj1]

"""
Get the Family Instants Type Geometry.
NOTE: We are only getting the first valid Solid. Multi-Solid Elements are not handled.
"""
def GetTypeGeometry(e):
	fs = doc.GetElement(e.GetTypeId())
	gElem = fs.get_Geometry(Options())
	
	for gObj in gElem:
		if gObj.GetType() == Solid:
			sld = gObj.ToProtoType()
			if sld != None:
				return sld
	return None

"""
Get the Family Instances Total Transform as Dynamo CoordinateSystem.
"""
def GetInstanceTransform(e):
	return TransformToCS(e.GetTotalTransform())

"""
Helper function to convert from Revit Transform to Dynamo CoordinateSystem.
"""
def TransformToCS(t):
	return geom.CoordinateSystem.ByOriginVectors(t.Origin.ToPoint(), t.BasisX.ToVector(), t.BasisY.ToVector(), t.BasisZ.ToVector())

##### Inputs #####

# List of Elements...
elems = tolist(UnwrapElement(IN[0]))

##### Main Script #####

solids = [GetTypeGeometry(e) for e in elems]

transforms = [GetInstanceTransform(e) for e in elems]

transformedSolids = [geom.Geometry.Transform(s, t) for s,t in zip(solids, transforms)]

##### Output #####

OUT = solids, transforms, transformedSolids

Coordinate Systems and why Revit has them…

Cool.. well, that is Revit’s internal coordinate system and how all the Elements are transformed with respect to the internal coordinate system. But what about project/shared coordinates?!?! Well, this also needs a quick explanation too… see, this post is getting long and I totally skipped all the nerdy stuff!

As you may already know, Revit has 3 defined coordinate Systems, two of which we are all aware of (Project and Shared) and a third, which we just discussed, but was a mystery to some until Revit 2020 when they introduced the Internal Origin and those that did not know already were like “Not another base point?!?”. Since we have covered the Internal Coordinate System and briefly what Transforms are, this should make a whole lot more sense. Project and Shared coordinates are simply just Transformations!

Yup, you can pretend to be surprised if you are not, but they way it works is like this… when you want to get some spot coordinate for some position on some element, what you are doing is getting a point in Revit’s internal coordinate system and applying Transformations. Depending on what type of coordinate base you are using, you get back different values. These values are calculated based on the Project/Survey Points Transform. This is really neat.. and I’ll explain why…

3D graphics are expensive! And so are long numbers. Have you ever had that message in Revit where it has a complete hissy fit about your geometry being beyond like 20km from the origin or some jargon? Or maybe you have created some points in Revit via Dynamo in real world coordinates. Well, your geometry goes a little nuts. You can usually pan, zoom and orbit like a pro, but now your geometry start jumping all over the shop and you mumble to yourself how shite Revit is and how you don’t get this nonsense in Autocad (we’ve all been there). Well, what you are seeing is likely the result of floating point precision errors because the vertex values that describe this geometry is out of its range if you will and values cannot be calculated accurately due to rounding, the bigger the number, the bigger the error. Revit, and pretty much every 3d application, has this issue. Here is a cool demo I found that highlights this issue precisely…


Probably why we don’t simply model in real world coordinates, but instead when getting coordinates, the point is transformed from internal to whatever coordinate system you want. Let’s try it ourselves…

Applying Transformations…

So, now I have only the die hard readers left or lazy people who skipped my best efforts to go off on a tangent, here is a better way to convert from the internal coordinate system to Project and Shared coordinate systems and using Transforms…

##### Imports #####

import clr

clr.AddReference("RevitServices")
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
doc =  DocumentManager.Instance.CurrentDBDocument

clr.AddReference("RevitNodes")
import Revit
clr.ImportExtensions(Revit.Elements)
clr.ImportExtensions(Revit.GeometryConversion)

clr.AddReference("RevitAPI")
from Autodesk.Revit.DB import *

##### Definitions #####

def tolist(obj1):
	if hasattr(obj1,"__iter__"): return obj1
	else: return [obj1]

"""
Gets the Active Project Locations (survey points) Transform. 
"""
def GetSurveyTransform():
	return doc.ActiveProjectLocation.GetTotalTransform()

"""
Get the Project Base Points Transform.
"""
def GetProjectTransform():
	basePtLoc = next((l for l in FilteredElementCollector(doc).OfClass(ProjectLocation).WhereElementIsNotElementType().ToElements() if l.Name == "Project"), None)
	
	return basePtLoc.GetTotalTransform()

"""
Applies the inverse transformation of the given Transform to the given point.
"""
def ApplyInverseTransformation(t, pt):
	return t.Inverse.OfPoint(pt.ToXyz()).ToPoint()

"""
Dynamo hates points that are in a galaxy far, far away and WILL moan with shared coordinates as it can't draw them properly. So this converts the points into numerical array form, so [x, y, z]
"""
def PointToNumberArray(pt):
	return [pt.X, pt.Y, pt.Z]

##### Inputs #####

# List of Points.These should be in Revits internal coordinate space.
pts = tolist(IN[0])

# Should we convert to numerical array form.
asNumerical = tolist(IN[1])[0]

# Force asNumerical to False if not True
if not asNumerical == True:
	asNumerical = False

##### Main Code #####

# We get the Transform first so we don't have to do this for each point. Faster this way.
srvTrans = GetSurveyTransform()
# Then do the conversion...
srvPts = [ApplyInverseTransformation(srvTrans, pt) for pt in pts]

# Same for project transformations...
projTrans = GetProjectTransform()
projPts = [ApplyInverseTransformation(projTrans, pt) for pt in pts]

##### Output #####

# Return Transformed points as numerical array instead of actual points...
if asNumerical:
	OUT = [PointToNumberArray(p) for p in pts], [PointToNumberArray(p) for p in projPts], [PointToNumberArray(p) for p in srvPts]
# Return Transformed points as points...
else:
	OUT = pts, projPts, srvPts

Much cleaner than my original solution! Ha! And it doesn’t matter if Project Base Point is unclipped either.

Note: To convert from Base Point Coordinates to Internal, you just need to apply the Transform to the point directly instead of inverting…

basePointTransform.OfPoint(pt.ToXyz()).ToPoint()

Further Reading…

If you are new to some of this stuff I would certainly check out Khan Academy as they have some really good videos on basic/intermediate mathematics…

Linear Algebra

Full Geometry Course

Also, check out the Dynamo/Grasshopper primers, they are great resources on basic mathematics with some excellent examples and imagery.

Finally, Daniel Shiffman has an excellent, albeit rather quirky, YouTube channel called Coding Train for processing and some of his stuff could be transferrable to other applications, he also has a free book Nature Of Code you can download or buy as hardcover which has some neat algorithms in it and basic explanations for simple geometry.

If you really wanna go to town and the mathematics of computer graphics/physics really interests you, here are some book recommendations. These are not for the faint hearted, but if you have an itch to learn how to create or simply understand more about what is happening under the hood of 3d Applications/Gaming Engines, then these are great resources to your book shelf – especially if you are a Game Developer. These all cover some degree of basic Maths like Vectors, Matrices, Transforms, Planes, intersections, linear/quadratic algebra and coordinate systems etc, but from that point they do ramp up in complexity quite quickly and cover topics like projection matrices, camera frustrums, rendering etc etc …

Realtime Collision Detection – Great for intersections and projections, but covers some basic stuff well.

Game Physics – A seriously well written book will a lot in it. This is about physics, so the math does get pretty edgy, quite a bit of calculus. Will take up half your bookshelf.

Mathematics for 3D Game Programming and Computer Graphics – Great all-round math book. Least scary one IMO.

Phyically Based Rendering – This is an awesome book on how raytracers work and how to build one. Maths is pretty complex and doesn’t really ease into it like the others. But still an interesting book.