Rendering Loop - Sixth 3D
Table of Contents
1. Rendering loop
The rendering loop is the heart of the engine, continuously generating frames on a dedicated background thread. It orchestrates the entire rendering pipeline from 3D world space to pixels on screen.
1.1. Main loop structure
The render thread runs continuously in a dedicated daemon thread:
while (renderThreadRunning) { ensureThatViewIsUpToDate(); // Render one frame maintainTargetFps(); // Sleep if needed }
The thread is a daemon, so it automatically stops when the JVM exits. You can stop it explicitly with ViewPanel.stop().
1.2. Frame rate control
The engine supports two modes:
- Target FPS mode: Set with setFrameRate(int). The thread sleeps between frames to maintain the target rate. If rendering takes longer than the frame interval, the engine catches up naturally without sleeping.
- Unlimited mode: Set
setFrameRate(0)or negative. No sleeping — renders as fast as possible. Useful for benchmarking.
1.3. Frame listeners
Before each frame, the engine notifies all registered FrameListeners:
viewPanel.addFrameListener((panel, deltaMs) -> {
// Update animations, physics, game logic
shape.rotate(0.01);
return true; // true = force repaint
});
Frame listeners can trigger repaints by returning true. Built-in listeners include:
- Camera — handles keyboard/mouse navigation
- InputManager — processes input events
2. Rendering phases
Each frame goes through 6 phases. Open the Developer Tools panel (F12) to see these phases logged in real-time:
2.1. Phase 1: Clear canvas
The pixel buffer is filled with the background color (default: black).
Arrays.fill(pixels, 0, width * height, backgroundColorRgb);
This is a simple Arrays.fill operation — very fast, single-threaded.
2.2. Phase 2: Transform shapes
All shapes are transformed from world space to screen space:
- Build camera-relative transform (inverse of camera position/rotation)
- For each shape:
- Apply camera transform
- Project 3D → 2D (perspective projection)
- Calculate
onScreenZfor depth sorting - Queue for rendering
This is single-threaded but very fast — just math, no pixel operations.
2.3. Phase 3: Sort shapes
Shapes are sorted by onScreenZ (depth) in descending order:
Collections.sort(queuedShapes, (a, b) -> Double.compare(b.onScreenZ, a.onScreenZ));
Back-to-front sorting is essential for correct transparency and occlusion. Shapes further from the camera are painted first.
2.4. Phase 4: Paint shapes (multi-threaded)
The screen is divided into 8 horizontal segments, each rendered by a separate thread:
Each thread:
- Gets a
SegmentRenderingContextwith Y-bounds (minY, maxY) - Iterates all shapes and paints pixels within its Y-range
- Clips triangles/lines at segment boundaries
- Detects mouse hits (before clipping)
A CountDownLatch waits for all 8 threads to complete before proceeding.
Why 8 segments? This matches the typical core count of modern CPUs.
The fixed thread pool (Executors.newFixedThreadPool(8)) avoids the
overhead of creating threads per frame.
2.5. Phase 5: Combine mouse results
During painting, each segment tracks which shape is under the mouse cursor. Since all segments paint the same shapes (just different Y-ranges), they should all report the same hit. Phase 5 takes the first non-null result:
for (SegmentRenderingContext ctx : segmentContexts) { if (ctx.getSegmentMouseHit() != null) { renderingContext.setCurrentObjectUnderMouseCursor(ctx.getSegmentMouseHit()); break; } }
2.6. Phase 6: Blit to screen
The rendered BufferedImage is copied to the screen using
BufferStrategy for tear-free page-flipping:
do { Graphics2D g = bufferStrategy.getDrawGraphics(); g.drawImage(renderingContext.bufferedImage, 0, 0, null); g.dispose(); } while (bufferStrategy.contentsRestored()); bufferStrategy.show(); Toolkit.getDefaultToolkit().sync();
The do-while loop handles the case where the OS recreates the back
buffer (common during window resizing). Since our offscreen
BufferedImage still has the correct pixels, we only need to re-blit,
not re-render.
3. Smart repaint skipping
The engine avoids unnecessary rendering:
viewRepaintNeededflag: Set totrueonly when something changes- Frame listeners can return
falseto skip repaint - Resizing, component events, and explicit
repaintDuringNextViewUpdate()calls set the flag
This means a static scene consumes almost zero CPU — the render thread just spins checking the flag.
4. Rendering context
The RenderingContext holds all state for a single frame:
| Field | Purpose |
|---|---|
pixels[] |
Raw pixel buffer (int[] in RGB format) |
bufferedImage |
Java2D wrapper around pixels |
graphics |
Graphics2D for text, lines, shapes |
width, height |
Screen dimensions |
centerCoordinate |
Screen center (for projection) |
projectionScale |
Perspective scale factor |
frameNumber |
Monotonically increasing frame counter |
A new context is created when the window is resized. Otherwise, the
same context is reused — prepareForNewFrameRendering() just resets
per-frame state like mouse tracking.