Sub Pixel Rendering

written by Eclipzer

Copyright (c) Quinton Roberts 2006.

What's A Sub Pixel?

A sub-pixel is a pixel that can be defined using decimal coordinates and rendered accordingly. The implications of this are visually quite dramatic. With the ability to be rendered at decimal intervals, sub-pixels can be positioned exactly where you want them instead of at a pixel approximation. This rendering improves the visual quality of images.

sine wave sampled at 0.5 pixel increments
left: pixel rendering
right: sub-pixel rendering

This wave is generated by sampling a sine wave at 0.5 pixel increments. The left side of the wave is rendered using pixels, where as the right side uses sub-pixels. The sub-pixels are able to more closely approximate the curve, producing a smoother looking image, because they take into account the fractional portion of the samples. Pixels on the other hand, force the samples to whole number values, creating a loss of information (the fractional portion of the sample). This loss of information is visually evident in the wave on the left, where "jaggies" are consistently displayed throughout the length of the wave.

Losing Information

Let's use an example sample function to demonstrate, numerically, this loss of information.

  ysample = 50 sin(x°) 'sample function

  x       = 95.5°
  ysample = 50 sin(95.5°) = 49.7698099184 'sampled value (high precision)

  ysub-pixel = 49.76981
  ytruncate  = 49
  yround     = 50

  Psub-pixel(95.5, 49.76981)
  Ptruncate (95, 49)
  Pround    (100, 50)

Since physical pixels only exist as integer values, our x,y coordinates must be integer values. This is normally achieved through truncation or rounding. In either case we end up losing information (0.5 from our x-value and 0.76981 from our y-value). With sub-pixels, we keep the fractional information intact and use it to construct a more accurate image.

Conceptual Rendering Method

To render a sub-pixel we actually 'map' it to four physical pixels. By determining how much area the sub-pixel covers in each of these pixels, we can calculate how intense the color of the sub-pixel is for each pixel.

sub-pixel displayed on a pixel grid sub-pixel "mapped" to four physical pixels pixel representation of a sub-pixel

Pixel Color Equation

Using the above method, we can deduce that the equation used to determine the color of each pixel of a sub-pixel is:

  Color = Area x Intensity

or

  Colorpixel = Areasub-pixel x Colorsub-pixel

Let's explore why this equation makes sense. If the area is one then the sub-pixel lies fully within this pixel and the pixel color would be the same as that of the sub-pixel.

  Cpixel = Asub-pixel x Csub-pixel = 1 x Csub-pixel = Csub-pixel

  Cpixel = Csub-pixel

If the area is zero then the sub-pixel lies completely outside this pixel the pixel color would be zero.

  Cpixel = Asub-pixel x Csub-pixel = 0 x Csub-pixel = 0

  Cpixel = 0

Computing Pixel Areas

We tend to think of pixels as points, which is why we describe them using a single coordinate. With sub-pixels we see that our pixel is better thought of as a square possessing dimension (length, height and area). For this reason, we need to create more points from our original data. In essence, we must interpret our original data in terms of four coordinates describing a sub-pixel square instead of a single coordinate describing a pixel point.

sub-pixel corner coordinates
Sub-Pixel Coordinate Definitions:
  x1          'left edge (original x-coordinate)
  y1          'top edge (original y-coordinate)
  x2 = x1 + 1 'right edge
  y2 = y1 + 1 'bottom edge

Sub-Pixel Corner Coordinates:
  p1(x1, y1) 'top-left
  p2(x2, y1) 'top-right
  p3(x1, y2) 'bottom-left
  p4(x2, y2) 'bottom-right

We must also consider the lines that divide the sub-pixel into the four areas we need to calculate.

sub-pixel divided by lines x=x0 and y=y0
Sub-Pixel Division Lines:

  x0 = truncate(x2) 'remove precision of x-coordinate (decimal portion)
  y0 = truncate(y2) 'remove precision of y-coordinate (decimal portion)

With our sub-pixel definitions now in place, we can begin to calculate areas used to determine our four pixel colors.

sub-pixel divided into four seperate areas

Area = Length x Height = (x2 - x1)(y2 - y1) 'general area equation

  A1 = (x0-x1)(y0-y1)    A2 = (x2-x0)(y0-y1) 
  A3 = (x0-x1)(y2-y0)    A4 = (x2-x0)(y2-y0)

Define Common Quantities:

  ax1 = x0-x1    ax2 = x2-x0
  ay1 = y0-y1    ay2 = y2-y0

Simplify By Substitution:

  A1 = (ax1)(ay1)    A2 = (ax2)(ay1)
  A3 = (ax1)(ay2)    A4 = (ax2)(ay2)

Computing Pixel Color From Area

Once we've calculated our areas we can use them to determine our four pixel colors. Because the color on a computer screen is quantified using three different color components (RGB) we must first break our color into these components and then apply our pixel color equation to each one. These new components are then used to construct each of our four pixel colors.

Compute Pixel (Pn) RGB Components (CPn = An x Csub-pixel) where n=1,2,3,4:

  CPn = (RPn,GPn,BPn) 'pixel (Pn) color composed of RGB values

  RPn = An x Rsub-pixel
  GPn = An x Gsub-pixel
  BPn = An x Bsub-pixel

What About The Background Color?

The last thing we need to consider is background color. Currently, if our sub-pixel lies completely outside one of our four pixels, then that pixel's color is zero. This normally indicates the color black. But what if our background isn't black, or our sub-pixel is simply being rendered over another image altogether? To handle this situation, we just need to blend our background color with our sub-pixel color. We do this with the standard alpha blending equation, where our calculated area acts as our alpha value.

  Color = Alpha x (Color1 - Color2) + Color2

or

  Coloralpha = Alpha x (Colorsub-pixel - Colorbackground) + Colorbackground

  RPn = An x (Rsub-pixel - Rbackground) + Rbackground
  GPn = An x (Gsub-pixel - Gbackground) + Gbackground
  BPn = An x (Bsub-pixel - Bbackground) + Bbackground

sub-pixel rendered over green background

Putting It All Together

Psuedo-Code (Note: the "!" symbol refers to a single-precision variable):

  Draw_SubPixel (x!,y!,c)

  ' Define sub-pixel attributes
    x = truncate(x!):  x0 = x+1:  x1! = x!:  x2! = x1!+1
    y = truncate(y!):  y0 = y+1:  y1! = y!:  y2! = y1!+1

  ' Determine area lengths
    ax1! = x0 - x1!:  ax2! = x2! - x0
    ay1! = y0 - y1!:  ay2! = y2! - y0

  ' Calculate areas
    a1! = ax1!*ay1!:  a2! = ax2!*ay1!
    a3! = ax1!*ay2!:  a4! = ax2!*ay2!

  ' Determine 4 background colors
    bkg1 = pixel_color(x  , y)
    bkg2 = pixel_color(x+1, y)
    bkg3 = pixel_color(x  , y+1)
    bkg4 = pixel_color(x+1, y+1)

  ' Determine RBG components of background colors
    r1 = r_component(bkg1):  r2 = r_component(bkg2):  r3 = r_component(bkg3):  r4 = r_component(bkg4)
    g1 = g_component(bkg1):  g2 = r_component(bkg2):  g3 = r_component(bkg3):  g4 = r_component(bkg4)
    b1 = b_component(bkg1):  b2 = r_component(bkg2):  b3 = r_component(bkg3):  b4 = r_component(bkg4)

  ' Determine RGB components of sub-pixel color
    r0 = r_component(c)
    g0 = g_component(c)
    b0 = b_component(c)

  ' Calculate  RGB components of sub-pixel's 4 pixels
    c1 = color(a1!*(r0 - r1) + r1, a1!*(g0 - g1) + g1, a1!*(b0 - b1) + b1)
    c2 = color(a2!*(r0 - r2) + r2, a2!*(g0 - g2) + g2, a2!*(b0 - b2) + b2)
    c3 = color(a3!*(r0 - r3) + r3, a3!*(g0 - g3) + g3, a3!*(b0 - b3) + b3)
    c4 = color(a4!*(r0 - r4) + r4, a4!*(g0 - g4) + g4, a4!*(b0 - b4) + b4)

  ' Draw 4 pixels
    draw_pixel x  , y  , c1
    draw_pixel x+1, y  , c2
    draw_pixel x  , y+1, c3
    draw_pixel x+1, y+1, c4

--Eclipzer