Skip to content

Instantly share code, notes, and snippets.

@aduffey
Last active March 27, 2024 02:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aduffey/d7468b8068d6124641ff0762c2b373e8 to your computer and use it in GitHub Desktop.
Save aduffey/d7468b8068d6124641ff0762c2b373e8 to your computer and use it in GitHub Desktop.
Scanline simulation math

To model a scanline, we can first start by modeling a CRT's spot and then model its translation across the screen.

What should we use to model the spot? Something based on a two-dimensional gaussian ($e^{-x^2}e^{-y^2}$) is the obvious answer, but that has a couple of downsides:

  1. A gaussian extends to infinity on the $x$ axis, so we would have to window or truncate it.
  2. Two gaussians overlapping form higher peaks than each one by itself. Imagine two scanlines next to each other. Near the peaks of the individual gaussians would be slightly taller peaks of their sum. If this was our spot model, we would have to deal with these by rescaling the gaussians or the sums to be within the range of 0 to 1. See overlap-gaussian.png for a visual example.

Neither of these problems is insurmountable, but they make the math a bit more annoying. Instead, we can use a raised cosine:

$$ g(x) = \begin{cases} \frac{1}{2} + \frac{1}{2} \cos(\pi x) & \text{if } x \in [-1,1]\\ 0 & \text{otherwise} \end{cases} $$

When two adjacent raised cosines are added together, the sum will never be above the peaks as long as they are at least half of their widths apart. I believe I got this idea from crt-lottes-fast, although that shader uses a different way of calculating scanlines. See overlap-cosine.png for a visual example.

We can generalize this to two dimensions:

$$ g(x, y) = \begin{cases} \frac{1}{4} (1 + \cos(\pi x)) (1 + \cos(\pi y)) & \text{if } x \in [-1,1] \text{ and } y \in [-1,1]\\ 0 & \text{otherwise} \end{cases} $$

Compare the images below: spot_gaussian.png and spot_cosine.png. The cosine spot is slightly squared off in comparison but is pretty similar.

The size and intensity of the spot also varies. We can consider $s$ to be the spot's intensity, with a range of 0 at full black to 1 at full white. The spot should:

  • Have an integral over its bounds equal to $s$ so that its overall brightness varies linearly.
  • Have a width $\sigma(s)$ that increases as the brightness increases. In other words, the spot should get larger as its intensity increases.

We can satisfy both of these constraints:

$$ g(x, y, s) = \begin{cases} \frac{s}{4 \sigma(s)} (1 + \cos(\frac{\pi x}{\sigma(s)})) (1 + \cos(\frac{\pi y}{\sigma(s)})) & \text{if } x \in [\frac{-1}{\sigma(s)},\frac{1}{\sigma(s)}] \text{ and } y \in [\frac{-1}{\sigma(s)},\frac{1}{\sigma(s)}] \\ 0 & \text{otherwise} \end{cases} $$

Now, imagine the spot being scanned across the screen horizontally over time. The areas it lights up are integrated by our eyes (helped by the persistence of the phosphor) to look like lines. We can model a scanline with an integral and the addition of a time element:

$$ \int_{t_0}^{t_1} \frac{s(t)}{4 \sigma(s(t))} (1 + \cos(\frac{\pi (x-t)}{\sigma(s(t))})) (1 + \cos(\frac{\pi y}{\sigma(s(t))})) \ \mathrm{d}t $$

(Note that the bounds of the spot function are omitted for visual clarity.)

We essentially scan the spot over the screen between time $t_0$ and $t_1$ (the start and end of the scanline), adding up all the light that was produced. This is similar to a convolution. The result of our integral is the sum of the light at a given point $(x,y)$, where $x=0$ is the start of the scanline and $y=0$ is the center of the scanline. Units are scanline widths (the distance from the center of one scanline to the center of the next).

For $\sigma(s)$, a square root bounded by a maximum, $\sigma_{max}$, and minimum, $\sigma_{min}$ (as a proportion of the maximum), seems appropriate:

$$ \sigma(s) = \sigma_{max} ((1 - \sigma_{min}) \sqrt{s} + \sigma_{min}) $$

I would like to measure an actual CRT to check how accurate to reality this width function is, but my JVC is currently relegated to the garage.

So now with all of this together, our scanline integral is clearly not practical to solve analytically. That's fine, because we don't actually have an analytic representation of $s(t)$ anyways. Instead we have a series of equally-spaced samples from $s(t)$. We can use these samples to estimate the integral using the rectangle rule.

For any given point on the screen, we can estimate its value by adding the estimates from the two nearest scanlines. As long as $\sigma_{max} \le 1$, no other scanline will contribute to its brightness.

Note that if we had instead projected our spot to two dimensions as $\frac{1}{2} + \frac{1}{2} \cos(\pi \sqrt{x^2 + y^2})$, adjacent, full-brightness scanlines would not add to 1 and would instead have peaks. To keep this property through the integral, we need to keep the $x$ and $y$ components separable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment