How-to · SDK

Project a 3D cube with the GTE

Rotate fixed-point vertices, project them into screen coordinates, and draw a wireframe cube.

On this page

PS1 homebrew, running in your browser. This player runs the compiled PS1 executable in the PSoXide emulator.

Click to run inside the player.

Focus the game to use the keyboard. Arrows: D-pad. F / G / H / X: Cross / Circle / Square / Triangle. Enter: Start. Sound starts muted; use the speaker button in the game. No touch controls.

Build and try it

After setting up the SDK:

make disc EXAMPLE=hello-gte

Open build/examples/mipsel-sony-psx/release/hello-gte.cue. A white wireframe cube rotates against a dark background. It uses the PS1's Geometry Transformation Engine (GTE) for vertex projection, then the GPU for drawing lines.

Describe the cube

CUBE_VERTS contains eight corners. The example uses fixed-point values with 12 fractional bits: 0x1000 represents 1.0 and 0x0800 represents 0.5. CUBE_EDGES contains twelve pairs of vertex indices. Keeping shape data separate from drawing code makes it easy to replace the cube later.

Set up the projection

The example centres the view at (160, 120), chooses a projection distance and moves the object forward:

scene::set_screen_offset(160 << 16, 120 << 16);
scene::set_projection_plane(200);
scene::load_translation(Vec3I32::new(0, 0, 0x3000));

The screen offsets use 16 fractional bits, which is why they are shifted by 16. The vertex coordinates use a different scale. Follow the API's units rather than applying one fixed-point format everywhere.

Moving the cube away from the camera keeps its depth positive for the perspective divide. This small example is not a general near-plane clipping system.

Rotate and project each frame

let yaw = Mat3I16::rotate_y(frame.wrapping_mul(YAW_STEP));
let pitch = Mat3I16::rotate_x(frame.wrapping_mul(PITCH_STEP));
let rot = yaw.mul(&pitch);
scene::load_rotation(&rot);

These rotation helpers use 256 angle units per turn. The GTE transforms each vertex and returns its projected screen position:

let p = scene::project_vertex(*v);
projected[i] = (p.sx, p.sy);

The loop then connects the projected endpoints using LineMono primitives drawn with gpu.draw. Projection and rasterization are separate operations: the GTE gives you coordinates, and the GPU draws them.

Try a change

Set PITCH_STEP to 0 and leave YAW_STEP at 4 to see rotation around one axis. Then reduce YAW_STEP to 1. Rebuild and compare. Try changing the projection plane from 200 to 160: the cube should occupy less of the screen without changing its vertices.

Keep a positive translation large enough for the whole cube to stay in front of the view. Moving vertices through the camera without clipping can produce extreme or invalid projected coordinates.

Next: filled geometry

Replace edges with triangle faces and sort those faces before drawing. The ordering-table guide explains the submission step. A solid renderer also needs clipping and back-face handling; this wireframe example does not implement them.