Skip to content
Tech News
← Back to articles

Divide by Depth for Instant 3D

read original more articles
Why This Matters

This article breaks down the core mathematical trick behind 3D-to-2D projection—dividing x and y coordinates by depth (z)—that underlies virtually all real-time graphics and game engines. Understanding this simple formula demystifies how cameras, perspective, and scaling work under the hood, which matters for developers who want to move beyond high-level game frameworks to build custom rendering and creative visual effects.

Key Takeaways

When I first started working on games, I used high level frameworks that would give you a Camera that just kind of worked. But as I learned more and wanted to apply more creative techniques, I struggled to even have the vocabulary to search for what I wanted to do, and only after writing lower level graphics code did I understand cameras are based around kind of really simple math.

In the excellent One Formula That Demystifies 3D Graphics (via @tsoding), we’re presented with the following:

( x , y , z ) x ' = x / z y ' = y / z

In essence, if we say \(y\) is up and \(z\) is forward, 3D coordinates \((x, y, z)\) can be projected into 2D coordinates \((x', y')\) by dividing \(x\) and \(y\) by \(z\). For example, if we have a series of 3D points that only vary in depth, as depth increases, their projected positions get closer to vanishing point \((0,0)\):

\[\begin{array}{c|c} (x,y,z) & (x',y') \\ \hline (2,1,2) & (1,0.5) \\ (2,1,4) & (0.5,0.25) \\ (2,1,8) & (0.25,0.125) \end{array}\]

This can be demonstrated by a ball that moves and scales with depth as it orbits the camera’s up axis, offset along the \(z\) axis by forward :

// slider:1 5 const float forward = 2 . 0 ; vec3 camera_pos = vec3 ( sin ( iTime ), 0 ., cos ( iTime ) + forward ); vec2 projected_pos = camera_pos . xy / camera_pos . z ; float radius = 0 . 5 / camera_pos . z ; void mainImage ( out vec4 c , vec2 p ) { vec2 uv = ( p * 2 . 0 - iResolution . xy ) / iResolution . y ; // slider:1 5 const float forward = 2 . 0 ; vec3 camera_pos = vec3 ( sin ( iTime ), 0 ., cos ( iTime ) + forward ); vec2 projected_pos = camera_pos . xy / camera_pos . z ; float radius = 0 . 5 / camera_pos . z ; c = iForeground * step ( length ( uv - projected_pos ), radius ); } Show full shader

Using the same principle, we can even draw more sophisticated “geometry” in the same way!

// slider: 2 5 const float z = 2 . 5 ; float d = 0 . 0 ; for ( int i = 0 ; i < 12 ; i ++ ) { vec3 a = r * c [ e [ i ]. x ] + fwd * z , b = r * c [ e [ i ]. y ] + fwd * z ; d = max ( d , line ( uv , a . xy / a . z , b . xy / b . z )); } fragColor = iForeground * d ; float sdSegment ( vec2 point , vec2 start , vec2 end ) { vec2 offset = point - start ; vec2 segment = end - start ; float along = clamp ( dot ( offset , segment ) / max ( dot ( segment , segment ), 1e-8 ), 0 . 0 , 1 . 0 ); return length ( offset - segment * along ); } float line ( vec2 point , vec2 start , vec2 end ) { float width = fwidth ( point . y ) * 1 . 5 ; return smoothstep ( width , 0 . 0 , sdSegment ( point , start , end )); } void mainImage ( out vec4 fragColor , in vec2 fragCoord ) { vec2 uv = ( fragCoord * 2 . 0 - iResolution . xy ) / iResolution . y ; const vec3 c [ 8 ] = vec3 []( vec3 ( - 1 ., - 1 ., - 1 .), vec3 ( - 1 ., - 1 ., 1 .), vec3 ( - 1 ., 1 ., - 1 .), vec3 ( - 1 ., 1 ., 1 .), vec3 ( 1 ., - 1 ., - 1 .), vec3 ( 1 ., - 1 ., 1 .), vec3 ( 1 ., 1 ., - 1 .), vec3 ( 1 ., 1 ., 1 .) ); const ivec2 e [ 12 ] = ivec2 []( ivec2 ( 0 , 1 ), ivec2 ( 0 , 2 ), ivec2 ( 0 , 4 ), ivec2 ( 1 , 3 ), ivec2 ( 1 , 5 ), ivec2 ( 2 , 3 ), ivec2 ( 2 , 6 ), ivec2 ( 3 , 7 ), ivec2 ( 4 , 5 ), ivec2 ( 4 , 6 ), ivec2 ( 5 , 7 ), ivec2 ( 6 , 7 ) ); mat3 r = mat3 ( cos ( iTime ), 0 ., sin ( iTime ), 0 ., 1 ., 0 ., - sin ( iTime ), 0 ., cos ( iTime ) ); const vec3 fwd = vec3 ( 0 . 0 , 0 . 0 , 1 . 0 ); // slider: 2 5 const float z = 2 . 5 ; float d = 0 . 0 ; for ( int i = 0 ; i < 12 ; i ++ ) { vec3 a = r * c [ e [ i ]. x ] + fwd * z , b = r * c [ e [ i ]. y ] + fwd * z ; d = max ( d , line ( uv , a . xy / a . z , b . xy / b . z )); } fragColor = iForeground * d ; } Show full shader

... continue reading