Pixels vs vectors

Rasterization and anti-aliasing

1,641 words9 min read

Computer graphics must bridge two fundamentally different worlds: the mathematical perfection of continuous shapes and the discrete reality of pixel grids. Vector graphics describe shapes with equations - infinitely smooth, infinitely scalable, existing in an idealized geometric space. But to display them on a screen or print them on paper, we must rasterize: convert those perfect curves into a grid of colored squares.

This conversion is the central challenge of computer graphics rendering. A circle is a precise mathematical object - the set of all points equidistant from a center. But a screen is a rectangular array of tiny lights that can only be on or off (or various intensity levels). There is no pixel that represents 'the edge of a circle.' We must approximate, and the quality of that approximation determines whether graphics look crisp and professional or jagged and amateurish.

The techniques developed to solve this problem - rasterization algorithms, anti-aliasing methods, subpixel rendering - represent decades of research and engineering. Understanding them illuminates not just how graphics work but also the fundamental tension between mathematical ideals and physical reality that pervades computing.

The rasterization problem

Consider a simple task: drawing a circle on a 100x100 pixel grid. The mathematical circle is a perfect, continuous curve defined by the equation x² + y² = r². Every point on this curve satisfies the equation exactly. But pixels are squares arranged in a grid - there is no such thing as half a pixel or a curved pixel edge. How do we decide which pixels to light up?

The naive approach is to color a pixel if its center falls inside the shape. For each pixel, compute whether the center point satisfies the inside/outside test. This produces a recognizable circle, but one with jagged, stair-stepped edges - the dreaded 'jaggies' or aliasing artifacts. The stair-stepping is particularly noticeable on diagonal lines and curves, where the pixel grid and shape boundary are maximally misaligned.

Rasterization & Anti-Aliasing

Binary rasterization: pixels are either fully on or fully off, creating jagged edges.

Interactive rasterization demonstration. Adjust resolution and anti-aliasing to see how they affect the rendered appearance of vector shapes.

Why does this happen? The mathematical shape contains information at infinite resolution - every point on the curve is precisely defined. But we are sampling this infinite-resolution signal onto a finite grid. In signal processing terms, we are violating the Nyquist-Shannon sampling theorem: the shape contains frequency components higher than our sampling rate can capture. The result is aliasing - false patterns that appear in the sampled signal.

Drawing lines: Bresenham's algorithm

Before tackling curves, consider the simpler problem of drawing a straight line. Given endpoints (x₀, y₀) and (x₁, y₁), which pixels should be lit? The mathematical line passes through infinitely many points; we need to choose a discrete approximation that looks straight to human eyes.

The simplest approach computes the line's slope m = Δy/Δx, then for each x position, calculates y = y₀ + m(x - x₀) and rounds to the nearest integer. This works but involves floating-point multiplication and rounding for every pixel - expensive operations in the 1960s when these algorithms were developed.

Bresenham's line algorithm (1962) achieves the same result using only integer addition and comparison. It tracks an error term representing the distance from the ideal line to the current pixel position. At each step, it chooses whether to stay on the current row or move to the next based on which choice minimizes error. The algorithm is elegant, fast, and produces optimal results - the closest possible discrete approximation to the ideal line.

// Bresenham's line algorithm - integer arithmetic only
function drawLine(x0: number, y0: number, x1: number, y1: number): Point[] {
  const dx = Math.abs(x1 - x0)
  const dy = Math.abs(y1 - y0)
  const sx = x0 < x1 ? 1 : -1
  const sy = y0 < y1 ? 1 : -1
  let err = dx - dy
  const points: Point[] = []
  
  while (true) {
    points.push({ x: x0, y: y0 })
    if (x0 === x1 && y0 === y1) break
    
    const e2 = 2 * err
    if (e2 > -dy) { err -= dy; x0 += sx }
    if (e2 < dx) { err += dx; y0 += sy }
  }
  
  return points
}

Anti-aliasing: the continuous solution

Binary rasterization (pixel is either on or off) cannot escape jaggies. The solution is to allow intermediate values - gray pixels that represent partial coverage. A pixel half-covered by a black shape on a white background would be colored 50% gray. This creates a visual gradient at edges that our eyes interpret as smoothness, even though the underlying pixels are still square.

How do we determine coverage? The most straightforward approach is supersampling: subdivide each pixel into a grid of subpixels (say, 4x4), test each subpixel against the shape, and set the pixel's intensity based on the fraction of subpixels that fall inside. 16x supersampling means 16 tests per pixel - expensive but effective.

// 4x4 supersampling anti-aliasing
function supersample(shape: Shape, x: number, y: number): number {
  let coverage = 0
  const samples = 4
  
  for (let sy = 0; sy < samples; sy++) {
    for (let sx = 0; sx < samples; sx++) {
      // Sample at subpixel center
      const sampleX = x + (sx + 0.5) / samples
      const sampleY = y + (sy + 0.5) / samples
      
      if (shape.contains(sampleX, sampleY)) {
        coverage++
      }
    }
  }
  
  return coverage / (samples * samples) // 0.0 to 1.0
}

Supersampling is conceptually simple but computationally expensive. Rendering at 4x4 supersampling requires 16 times the work of binary rasterization. More sophisticated approaches compute coverage analytically - calculating the exact area of intersection between the pixel square and the shape. This is mathematically elegant but requires solving geometric intersection problems for each pixel-shape pair.

Modern GPUs use multi-sample anti-aliasing (MSAA), which supersamples only the edge pixels where aliasing is visible. Interior pixels that are fully covered need only a single sample. This dramatically reduces the performance cost while maintaining edge quality. Temporal anti-aliasing (TAA) spreads samples across multiple frames, using motion vectors to accumulate information over time.

Subpixel rendering

LCD displays offer an intriguing opportunity for higher resolution. Each pixel is actually three colored stripes - red, green, and blue - arranged horizontally. By controlling these subpixels independently, we can achieve three times the horizontal resolution for certain content, particularly black-on-white text.

Microsoft's ClearType technology popularized subpixel rendering for text. Instead of treating each pixel as a single unit, the rasterizer computes separate coverage values for each RGB subpixel. The result looks sharper than traditional anti-aliasing because edge positions can be defined at ⅓-pixel precision. The eye integrates the color fringes at edges, perceiving a smooth gray boundary rather than the colored subpixels.

GPU rasterization

Modern GPUs contain dedicated hardware for rasterization, processing billions of primitives per second. But GPUs rasterize triangles, not arbitrary shapes. Complex shapes must first be tessellated - decomposed into triangles that approximate the original shape. A circle might become 64 or 128 triangles; a complex path might require thousands.

The GPU's rasterizer processes each triangle in parallel, determining which pixels are covered and computing interpolated values (colors, texture coordinates) for each covered pixel. Edge handling uses coverage masks to enable MSAA. The entire process happens at extraordinary speed because it is massively parallel - every pixel can be processed independently.

For 2D graphics, the tessellation step is often the bottleneck. Tesselating complex paths with many curves requires significant CPU work before the GPU can begin rendering. Libraries like NanoVG and Pathfinder have developed techniques to move more of this work to the GPU, treating path rendering as a GPU compute problem rather than a fixed-function rasterization problem.

Signed distance fields

A newer approach to rendering shapes stores them not as outlines but as distance fields. For each point in a 2D grid, store the signed distance to the nearest edge: negative inside the shape, positive outside. These distance fields can be stored in textures and rendered on GPUs with automatic anti-aliasing derived from texture filtering.

The key insight is that the distance field contains enough information to reconstruct edges at any resolution. When rendering, check if the interpolated distance value is near zero - that is where the edge lies. The rate of change of the distance field indicates edge direction, enabling proper anti-aliasing without explicit edge detection.

Signed distance fields excel at rendering text and icons that need to be displayed at many sizes. A single 64x64 SDF texture can render readable glyphs from 8 pixels to 200+ pixels, with smooth anti-aliased edges at every size. They also enable effects like outlines, drop shadows, and glow that would be expensive to compute from outline data.

// Fragment shader for SDF rendering
uniform sampler2D sdfTexture;
uniform float smoothing; // Typically 0.25/fontSize

void main() {
  float distance = texture2D(sdfTexture, texCoord).a;
  float alpha = smoothstep(0.5 - smoothing, 0.5 + smoothing, distance);
  gl_FragColor = vec4(color.rgb, color.a * alpha);
}

When pixels win

Vectors are not always superior to rasters. Some images - photographs, paintings, film frames, detailed textures - are inherently raster data captured by sensors or created with raster tools. Converting them to vectors would either lose information or create impractically large files. A photograph of a forest cannot meaningfully be represented as mathematical shapes.

Performance also favors rasters in many situations. Once rasterized, an image can be displayed by simply copying pixels to the screen - no computation required. Vector rendering must be performed every frame, every time the view changes. For static content at known resolutions, pre-rasterized images are faster to display.

The practical solution is to use each representation where it excels: vectors for text, icons, logos, and UI elements that must scale and animate; rasters for photographs, complex illustrations, and textures. Modern applications mix both freely, compositing vector and raster layers into final images that combine the best properties of each.

The pixel-vector divide is a fundamental duality in digital imaging, reflecting the deeper tension between continuous mathematics and discrete computation. Every graphics system must navigate this divide, making tradeoffs between precision and performance, scalability and efficiency. Understanding both sides of the divide - and when to use each - is essential knowledge for anyone working with digital graphics.

How Things Work - A Visual Guide to Technology