Desmos Wiki (Unofficial)

Guide to 3D

Making something 3D in Desmos may initially seem like a daunting task. However, it's ultimately far simpler than it looks once you break it down. This guide will explain how Desmos 3D works and how one can implement their own 3D engine. The focus will be on readability rather than achieving maximum performance.

This guide is for making 3D engines in the Desmos 2D calculator! #

Now that Desmos 3D is out, it's important to mention that this guide is specifically geared toward making 3D engines in Desmos's 2D graphing calculator. If you're looking for info on how to use the 3D calculator, you won't find that in this article.

Some notes on style #

Throughout this article, I'll be using very long variable names. This is to prioritize clarity of explanation over conciseness. You probably shouldn't do this to the same extent I do here in your own graphs.

What are 3D Scenes Made Of? #

Before diving into making an actual 3D engine, it's important to consider what 3D scenes are actually made of. 3D scenes are usually made up of 3D meshes. 3D meshes are nothing more than a group of many, many triangles in 3D space. These triangles are themselves made up of the following components:

  1. A collection of 3D points, where each point is called a vertex. Keep in mind that these will have to be split into three lists (a list of x-values, a list of y-values, and a list of z-values), because Desmos currently does not support 3D points.
  2. A collection of numbers that determine which points make up the vertices of which triangles. For instance, these may say that a triangle in a mesh is formed from the 3rd, 4th, and 7th vertex. These numbers are called indices.

A 2D Example #

To better understand the concept of a mesh, here is a 2D analog of a mesh, where 2D vertices are being linked together into triangles to create a square:

In this case, we have a unit square with one corner at and another at . The first triangle is made up of the 1st, 2nd, and 4th vertices, while the second triangle is made up of the 2nd, 3rd, and 4th vertices.

You might notice the seemingly strange choice to use three lists for the indices (, , and ). Using three index lists makes indexing easier because each triangle uses three indices. If we interleaved them all in a single list then we'd have to do slow, annoying indexing math to untangle the triangles' indices.

How do we make this 3D? #

How can we expand this to 3D? As stated earlier, we'll need to represent the 3D vertices as three different lists (just to be clear, these are different from the three index lists I just described). We'll need a list for the X-coordinates, one for the Y-coordinates, and a third for Z. Let's pick the vertices for a cube, where one corner is at and the other is at .

To actually render this, we'll need to convert these vertices from 3D coordinates to 2D coordinates, and then render them with the same technique we used to render the 2D square in the section above.

Projection #

"Projection" in this case refers to the process of converting coordinates in 3D space to 2D coordinates on the screen. We'll use a perspective projection, which is the same kind of projection that you use to see.

In a perspective projection, things appear smaller the farther they are away from the camera. How much smaller? Compare this to your own eyesight— if you have an object 1 meter away from your eyes, and then you move it so that it's 2 meters away, it appears half as large. If you move it 3 meters away, it appears one-third as large. If you move it 4 meters away, it appears one-fourth as large. Also note that the further away it gets, the closer it appears to the "center" of your field of vision.

In other words, the apparent size— and proximity to your field of view's center— is inversely proportional to the depth of an object. This formalizes the intuition of farther away things being smaller.

A Short Note on Coordinate Systems #

For this guide, we'll be using a Y-up, Right-Handed coordinate system. By "Y-up" we mean that the Y-axis is the "up" direction, and by "Right-Handed" we mean that the coordinate system follows the right-hand rule: Positive X is right, positive Y is up, and positive Z is toward the camera. This is why all the Z-values in the cube are negative— we want it to be in front of the camera so it displays properly.

Putting it all together #

So, given all this, since positive Z is forward and negative Z is backward, we can perform a perspective projection— converting from 3D to 2D, in other words— by taking the X and Y components and dividing them by the negative of the Z component. If we do this, we get the vertices of a cube:

Adding the Indices and Polygons #

Now that we've got a bunch of 2D points, we can now get the indices of the 12 triangles that make up the 6 square faces of the cube and join them together with polygons, exactly like we did before with the 2D square:

Making it Colorful #

Now that we've got the cube working, let's make it fully opaque, get rid of the lines, and make every triangle a separate color. We'll also hide the points.

Wait... that doesn't look right! Faraway faces are being drawn in front of close-up faces!

Depth Sorting (Painter's Algorithm) #

Lists of polygons in Desmos are drawn in the same order in which they are placed in the list. As a consequence, polygons later in a list will be drawn on top of polygons earlier in a list. Consider the following example:

The blue triangle is drawn on top of the red polygon because it's later in the list.

We can take advantage of this phenomenon to render the 3D scene properly by sorting the list of polygons. How do we sort a list of polygons? In the Graphing Calculator, sorting a list of polygons might seem impossible because polygons can't be compared by Desmos's built-in function. However, we can get around this by using two-argument sort, which uses the ordering of its second argument to sort its first argument.

For instance, is because it's as if Desmos thinks it's sorting , when in reality the list that's actually changing is . We can take advantage of this technique by using the list of polygons as our first argument and the depths of our polygons multiplied by -1 as the second argument. Because sorts in ascending order, more negative depths— which represent farther-away polygons, since, again we're multiplying by -1— will appear first, allowing closer polygons to appear later on in the list and thus show up on top. The end result is that nearby polygons render on top of faraway ones.

Quick Aside: What do I mean by 'depths'? #

When you hear the word "depth", you might immediately think that I mean "Z-value." However, if we were to use Z-values as the depths for our depth sorting, it would work, but it wouldn't work particularly well. Instead, we should use the euclidean distance from the camera to a given polygon. It'd be difficult to explain why this works better without creating a few long paragraphs that bog down this article, so if you're curious as to why Z-values don't work as well for depths, try implementing a renderer that uses Z-values as depths and compare it to one that uses distances.

Depth Sorting (continued) #

The thing is, we can't just use the distances of the vertices to the camera directly. This won't work because these distances are per-vertex, while we're trying to sort polygons, which don't correspond to vertices 1:1, and don't even really have a definite, exact distance to the camera. We can solve this by finding the average distance to the camera of the three vertices of a polygon.

Note that we'll also have to sort the colors in this exact same way, so that they always correspond to the same polygon.

Okay, there we go, that looks better! But it's also kind of boring— we're viewing the cube from head-on, so all we can see is a square. Let's make the cube viewable from other positions and angles.

Coordinate Systems #

Before we continue, I think it's important that you know the various different coordinate systems that come into play when 3D rendering. This will make it far easier to communicate ideas and avoid accidentally tripping over yourself. There are few errors more frustrating than using a variable representing the wrong coordinate system, because without proper naming conventions, these variables can be exceedingly difficult to distinguish from one another.

Consider also reading this excellent article at LearnOpenGL on coordinate systems, though do keep in mind that it contains quite a bit of OpenGL-specific information that won't be as useful here.

Screen Space #

Screen Space is arguably the simplest of the coordinate systems. In the context of Desmos, Screen Space refers to the graph coordinates on your screen. That is, if you were to plot a point in Desmos, its actual position on the graph would be its actual position in screen space.

Now, just to be clear, this is somewhat different from the "Screen Space" you might see in computer graphics, which refers to a coordinate system where one corner of the screen is defined as (0, 0) and the opposite corner is defined as (width of screen in pixels, height of screen in pixels). So if you had a 1920x1080 monitor, screen space would range from 0 to 1920 on the x-axis and 0 to 1080 on the y-axis.

Model Space #

Model space is the coordinate system defined by a 3D model's raw data. That is, if you read a 3D model file and spat out the vertex positions without doing anything to them (no translation, no rotation, no scaling, etc.), those vertices would be said to be in model space.

World Space #

If you were to then take that model (with its model space coordinates) and move it around in the 3D world— for instance, if you moved a video game character around— then the vertex positions after being moved are said to be in world space.

Depending on what you're doing, and especially if your scene is stationary, you might not need to bother with world space.

View Space #

Now that we have a 3D model positioned somewhere in the world, we have to take into account the fact that it's being viewed from a specific location. Why should changing the view have any effect on the coordinate space? Here's why:

Consider what happens when you walk ten meters forward. The rest of the world appears to move ten meters backward. This is how cameras in (rasterization-based) 3D renderers work— the camera doesn't move forward, the world moves backward. The universe literally moves, revolves, and scales around the camera to give the illusion of movement. And because this involves shifting the world-space vertices around, we need a new coordinate system. And that coordinate system is called View Space, because it represents the coordinates from the standpoint of the camera's view.

Translation (Moving Around in Space) #

Now that we know this, we can make the camera move. I've stuffed most of this graph into an "Internals" folder for clarity's sake, and added three variables— , , and — to represent the camera's position in space. Because we want to have the world move in the opposite direction of the camera's motion, we subtract each camera position variable from the respective model space vertex variable. If you open the "Internals" folder, you might notice that we're skipping straight from model space to view space, skipping world space. This is fine in this case because we aren't moving, rotating, or scaling the cube at all. Because there's no transformation, model space and world space for the cube are one and the same.

Rotation #

Rotation is a huge can of worms. There are quite a few different techniques you could use, including:

However, for the sake of our collective sanities, I'm going to stick with one of the simplest rotation schemes I can think of: a simple, first-person camera rotation controller— the kind you would probably see in an FPS game or in Minecraft, where you can pan the camera left and right and tilt it up and down.

2D Rotation #

In order to get this working, we first have to understand 2D rotation. In short, if you have a point and want to rotate it by an angle about the origin, to get a point , you can use the following formulas to get and .

3D Rotation #

Now that we know how to rotate in 2D, we can use this information to figure out how to rotate in 3D.

The same ideas from the "translation" section apply to rotation as well: To rotate the camera to the right, it has to appear as if the entire world is rotating to the left. Speaking more generally, to perform some rotation, the world has to rotate in the opposite direction by the same angle.

Panning #

When you pan a camera, you rotate it left and/or right. This is the view from above:

A camera, viewed from above, panning left and right

Notice how, from above, panning a camera just looks like a regular old 2D rotation? As a result, we can use the 2D rotation formula from above to rotate the camera and produce a new set of coordinates somewhere between world space and view space. Keep in mind, however, that instead of rotating the X and Y axes, we're rotating the X and Z axes, because this is being viewed from top-down, so make sure to adjust the formula accordingly. Also remember that the coordinates we're using as input to the 2D rotation formula should have already been translated in the manner described in the last section.

Tilting #

Tilt follows a very similar pattern.

A camera, viewed from the side, tilting up and down

This is yet another 2D rotation. However, instead of rotating the X and Y axes, we're rotating the Y and Z axes. Since tilting occurs after panning, this part takes the translated and panned coordinates as input. What we have as output is our view space coordinates.

Putting it All Together #

Let's add rotation to what we've made so far. I've put everything related to the implementation of rotation and translation in the "Rotation and Translation" folder. Try dragging the point to rotate the scene.