Skip to main content

Renderer

The Renderer class is responsible for the main render loop, iterating over all pixels and producing the final image. It includes support for anti-aliasing through supersampling.

Definition

cpp/include/Renderer.h
enum class AALevel {
NONE = 0, // 1 sample per pixel
AA_2X = 1, // 2×2 = 4 samples per pixel
AA_4X = 2 // 4×4 = 16 samples per pixel
};

class Renderer {
public:
int width; // Render width in pixels
int height; // Render height in pixels
AALevel antiAliasing; // Anti-aliasing level

Renderer() : width(512), height(512), antiAliasing(AALevel::NONE) {}

void setAntiAliasing(int level);
int getAntiAliasing() const;
int getSamplesPerPixel() const;

std::vector<uint8_t> render(Scene& scene);
};

Render Loop

Without Anti-Aliasing

for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
// Convert pixel to normalized coordinates [-1, 1]
float u = (2.0f * x / width - 1.0f);
float v = (1.0f - 2.0f * y / height);

// Generate ray through pixel center
Ray ray = scene.camera.getRay(u, v);

// Trace ray and get color
Vec3 color = scene.traceRay(ray, 0);

// Write to buffer
buffer[index] = color;
}
}

With Anti-Aliasing (Supersampling)

int gridSize = getSampleGridSize();  // 2 for 2×2, 4 for 4×4
float subpixelSize = 1.0f / gridSize;
float invSamples = 1.0f / (gridSize * gridSize);

for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
Vec3 colorAccum(0, 0, 0);

// Stratified sampling with jitter
for (int sy = 0; sy < gridSize; ++sy) {
for (int sx = 0; sx < gridSize; ++sx) {
// Random jitter within subpixel
float jitterX = random(); // 0 to 1
float jitterY = random();

// Subpixel position
float subX = (sx + jitterX) * subpixelSize;
float subY = (sy + jitterY) * subpixelSize;

// Convert to NDC
float u = (2.0f * (x + subX) / width - 1.0f);
float v = (1.0f - 2.0f * (y + subY) / height);

Ray ray = scene.camera.getRay(u, v);
colorAccum = colorAccum + scene.traceRay(ray, 0);
}
}

// Average all samples
buffer[index] = (colorAccum * invSamples).clamp();
}
}

Anti-Aliasing Explained

The Aliasing Problem

Without AA, each pixel samples only the center, causing jagged edges:

Actual edge:          Sampled (no AA):

╱ ████
╱ ████
╱ ████
╱ ████

Smooth diagonal Stair-stepped (jaggies)

Supersampling Solution

Sample multiple points per pixel and average:

2×2 Supersampling:         4×4 Supersampling:

┌───┬───┐ ┌─┬─┬─┬─┐
│ ● │ ● │ │●│●│●│●│
├───┼───┤ ├─┼─┼─┼─┤
│ ● │ ● │ │●│●│●│●│
└───┴───┘ ├─┼─┼─┼─┤
│●│●│●│●│
4 samples ├─┼─┼─┼─┤
│●│●│●│●│
└─┴─┴─┴─┘

16 samples

Jittered Sampling

Regular grid sampling can cause patterns. Adding random jitter breaks these up:

// Without jitter: visible grid patterns
float subX = (sx + 0.5f) * subpixelSize;

// With jitter: smooth results
float jitterX = dist(rng); // Random 0-1
float subX = (sx + jitterX) * subpixelSize;

Anti-Aliasing Levels

LevelGridSamples/PixelQualityPerformance
Off1×11Baseline
2×22×24Good~4× slower
4×44×416Excellent~16× slower

Coordinate Systems

Pixel Coordinates

  • x: 0 to width-1 (left to right)
  • y: 0 to height-1 (top to bottom)

Normalized Device Coordinates (NDC)

  • u: -1 to 1 (left to right)
  • v: -1 to 1 (bottom to top)
float u = (2.0f * x / width - 1.0f);   // Map [0, width] to [-1, 1]
float v = (1.0f - 2.0f * y / height); // Map [0, height] to [1, -1]

Note: v is flipped because screen Y increases downward, but world Y increases upward.

Buffer Format

The output is a flat array of bytes in RGBA order:

Buffer layout:
[R₀, G₀, B₀, A₀, R₁, G₁, B₁, A₁, R₂, ...]

Pixel at (x, y):
index = (y * width + x) * 4
R = buffer[index + 0]
G = buffer[index + 1]
B = buffer[index + 2]
A = buffer[index + 3]

JavaScript Integration

Setting Anti-Aliasing

// 0 = Off, 1 = 2×2, 2 = 4×4
wasmModule.setAntiAliasing(1);

Calling render()

const pixelVector = wasmModule.render(resolution, resolution);

Copying to JavaScript Array

const size = pixelVector.size();
const pixelData = new Uint8ClampedArray(size);

for (let i = 0; i < size; i++) {
pixelData[i] = pixelVector.get(i);
}

Memory Cleanup

// IMPORTANT: Free C++ memory to prevent leaks
pixelVector.delete();

Drawing to Canvas

const imageData = new ImageData(pixelData, resolution, resolution);
ctx.putImageData(imageData, 0, 0);

Performance Considerations

Resolution Impact

ResolutionPixelsRelative Time
256×25665,536
512×512262,144
768×768589,824
1024×10241,048,57616×

Time scales quadratically with resolution.

Combined Impact (Resolution × AA)

ResolutionNo AA2×2 AA4×4 AA
25616×
51216×64×
76836×144×

Tip: Use lower resolution with 4×4 AA for smooth results without excessive render time.

Optimization Flags

The build script uses -O3 for maximum optimization:

emcc core.cpp -O3 ...

This enables:

  • Loop unrolling
  • Function inlining
  • SIMD vectorization (where possible)
  • Dead code elimination

API Reference

// Set anti-aliasing level (0=Off, 1=2×2, 2=4×4)
setAntiAliasing(level: number): void

// Get current AA level
getAntiAliasing(): number

// Get samples per pixel for current AA level
getSamplesPerPixel(): number

Complete Flow

JavaScript:                    C++ (WASM):

render(512, 512) ─────────────► Renderer::render()

├── for y in 0..height
│ └── for x in 0..width
│ ├── for sy in 0..gridSize (AA)
│ │ └── for sx in 0..gridSize
│ │ ├── getRay(u, v)
│ │ └── traceRay() → accumulate
│ ├── average samples
│ └── buffer[index] = color

pixelVector ◄─────────────────────── return buffer

├── Copy to Uint8ClampedArray
├── pixelVector.delete()

└── ctx.putImageData()