A vertex travels through several coordinate spaces on its way to the screen — local, world, view, and clip. Understanding these spaces and the matrices between them is the key to 3D rendering and gameplay.
A model’s vertices start in local space (relative to the model’s own origin), get placed into world space by the model matrix, are moved relative to the camera by the view matrix into view space, and are finally projected into clip space by the projection matrix. Knowing which space you are in — and which matrix converts between them — is essential to both rendering and gameplay math.
The journey of a vertex to the screen:
LOCAL space vertex relative to the model's own origin
| * Model matrix (position/rotate/scale the object)
WORLD space vertex placed in the shared world
| * View matrix (move everything relative to the camera)
VIEW space vertex relative to the camera (camera at origin)
| * Projection matrix (apply perspective / orthographic)
CLIP space -> the GPU clips + maps to the screen
The famous "MVP" = Projection * View * Model. That single product takes a
local vertex all the way to clip space.Local (or object) space is where a model is authored — a character’s vertices relative to their own feet. The model matrix transforms those into world space, the shared arena where objects have absolute positions and can interact. You constantly convert between them: a bullet spawns in the gun’s local space but must be placed and tracked in world space.
import numpy as np
# a sword's tip in the PLAYER's local space (relative to the player):
tip_local = np.array([0, 2, 0, 1]) # 2 units "up" the sword
# the player's model matrix places local points into the WORLD:
def transform(M, p): return M @ p
player_M = np.array([[1,0,0, 10], # player at world (10, 0, 5)
[0,1,0, 0],
[0,0,1, 5],
[0,0,0, 1]])
tip_world = transform(player_M, tip_local) # where the tip is IN THE WORLD
print(tip_world[:3]) # [10, 2, 5]
# spawn effects/bullets in local space, then convert to world to place them.The view matrix is the inverse of the camera’s world transform: instead of moving the camera, you move the entire world so the camera sits at the origin looking down an axis. This is a mental flip worth internalizing — "the camera never moves; the world moves around it." Building a view matrix from a camera position and target ("look-at") is a standard operation.
import numpy as np
def look_at(eye, target, up=(0,1,0)):
eye, target, up = map(np.array, (eye, target, up))
f = (target - eye); f = f / np.linalg.norm(f) # forward
r = np.cross(f, up); r = r / np.linalg.norm(r) # right
u = np.cross(r, f) # true up
# view = rotate the world by (r,u,-f) then translate by -eye
return f, r, u
# an engine's camera.look_at(target) builds this for you.
# key idea: the VIEW matrix is the camera's INVERSE — move the WORLD so the
# camera is at the origin. "The camera doesn't move; the world moves past it."The projection matrix maps view space into clip space and decides how depth looks. Perspective projection makes distant objects smaller (3D games, a sense of depth), while orthographic projection keeps sizes constant regardless of distance (2D games, UI, strategy views, CAD). Choosing the right projection is a fundamental decision that shapes how your game reads.
PERSPECTIVE projection
distant things get SMALLER (foreshortening) -> depth + realism
defined by: field of view, aspect ratio, near/far planes
use for: most 3D games, first/third person
ORTHOGRAPHIC projection
size is CONSTANT regardless of distance -> no foreshortening
defined by: a box (left/right/top/bottom/near/far)
use for: 2D games, UI/HUD, isometric strategy, CAD, map views
Same scene, different feel. The projection matrix is the last step before
the GPU turns 3D vertices into 2D screen positions.