Twitter  Facebook  YouTube  E-Mail  RSS
The One Man MMO Project
The story of a lone developer's quest to build an online world :: MMO programming, design, and industry commentary
Pixel Font Monochrome CRT Shader
By Robert Basler on 2020-11-02 21:35:20
Homepage: onemanmmo.com email:one at onemanmmo dot com

I want Space Mines III to look like it is playing on a 24x24 text screen displayed on the ten inch black and white CRT I played on as a kid. That is a little tricky because that CRT is long gone. Finding reference pictures of old CRT's was surprisingly difficult, they were either blurry or modern image editor reproductions. Luckily I found several good photos on vintage-computer.com.

CRT shaders are tricky beasts because LCD's don't really have the resolution to simulate a CRT properly. There are lots of CRT shaders on the internet, but they are designed to simulate visual aberrations and the color masks of various types of color monitors. I couldn't find a single monochrome CRT shader.

The code for rendering the CRT is pretty simple, it is rendered in three passes of a fullscreen quad with an orthographic projection. Hardcore render people would get that down to a single triangle, I couldn't be bothered. The shader for the first pass is included here, the other two shaders discussed below can be found easily on the internet.

The game sends all the info about the characters to render on the screen as a single bitmap (crttexture) split into three sections. The left third is the foreground color for the letter (fontForeground), the middle third is the background color (fontBackground), the right third stores the character's index into the font in the red channel (fontCharacter). I didn't bother to make the bitmap a power-of-two size (which video cards prefer) the bitmap is a simple multiple of the CRT dimension in characters (crtSize).

#version 150
uniform sampler2D crttexture;
uniform sampler2D font;
uniform vec2 crtSize;
in vec2 texCoord0;
out vec4 fragColor;

void main (void)
{
float s = texCoord0.s / 3.0;
vec4 fontForeground = texture( crttexture, vec2( s, texCoord0.t ) );
vec4 fontBackground = texture( crttexture, vec2( s + ( 1.0 / 3.0 ), texCoord0.t ) );
float fontCharacter = texture( crttexture, vec2( s + ( 2.0 / 3.0 ), texCoord0.t ) ).r * 255.0;


The shader then figures out the crt cell coordinates on the screen, and the font cell within the font texture as well as the texel it needs for the current fragment.

    vec2 crtCellCoords = mod( texCoord0 * crtSize, 1.0 );
vec2 fontCellCoords0 = vec2( floor( mod( fontCharacter, 16.0 ) ) / 16.0, floor( fontCharacter / 16.0 ) / 16.0 );
vec2 fontCellCoords1 = fontCellCoords0 + vec2( 1.0 / 16.0, 1.0 / 16.0 );
vec2 fontCoords = mix( fontCellCoords0, fontCellCoords1, crtCellCoords );
float fontTexel = texture( font, fontCoords ).r;


The font texture is a full color black and white bitmap, but I only need the red channel.

SuperboardIIFont.png
[16x16 character grid of 8x8 pixel characters.]


The pixel font I'm using is an original reconstruction of the font from the Ohio Scientific Superboard II.

CRT1_Lo.png
[Rectangular pixels.]


This was my first attempt. It simply fills each pixel area with the foreground or background color. It works fine, but I wanted a more CRT-ey look. I tried various methods with dimming pixel lines to simulate the CRT look, but none of them were successful. To render a rectangle for each pixel, I used the following code:

    fragColor = fontTexel < 0.5 ? fontForeground : fontBackground;


CRT2_Lo.png
[Circles with soft edges.]


From simple squares, the obvious next thing to try was circles. They look alright and there were certainly lots of monitors that looked like this, but the Superboard II was not one of them. This shader produces horizonal lines with vertical gaps which don't look much like the neat horizontal lines in my reference CRT images. To render a soft-edged circle for each pixel, I replaced the fragColor calculation above with the following shader code:

    if( fontTexel >= 0.5 )
{
fragColor = fontBackground;
}
else
{
vec2 pixelResolution = crtSize * 8;
vec2 pixel = mod( texCoord0, 1.0 / pixelResolution ) * pixelResolution;
vec2 m = pixel - vec2( 0.5, 0.5 );
float radius = sqrt(m.x * m.x + m.y * m.y);
const float MIN_RADIUS = 0.3;
const float MAX_RADIUS = 0.5;
if( radius < MIN_RADIUS )
{
fragColor = fontForeground;
}
else if (radius > MAX_RADIUS )
{
fragColor = fontBackground;
}
else
{
fragColor = mix( fontForeground, fontBackground, ( radius - MIN_RADIUS ) / ( MAX_RADIUS - MIN_RADIUS ) );
}
}


CRT3_Lo.png
[Ellipses with soft edges.]


Once I tried circles, ellipses seemed like the obvious solution to the vertical gaps. The ellipses are actually slightly oversized horizontally so that the contact area with the pixel on either side is slightly larger. The ellipses have RADIUS_X and RADIUS_Y where an ellipse that stays inside it's pixel area will have a value of 0.5 or less. To try to get the CRT pixel smearing I added BLEND_MIN and BLEND_MAX to give the ellipses soft edges, but it still wasn't quite enough.

    if( fontTexel >= 0.5 )
{
fragColor = fontBackground;
}
else
{
// Radius of ellipse. Values greater than .5 blend to next pixel.
const float RADIUS_X = 0.55;
const float RADIUS_Y = 0.45;
vec2 pixelResolution = crtSize * 8;
vec2 pixel = mod( texCoord0, 1.0 / pixelResolution ) * pixelResolution;
vec2 m = pixel - vec2( 0.5, 0.5 );
float radius = ( ( m.x * m.x ) / ( RADIUS_X * RADIUS_X ) ) + ( ( m.y * m.y ) / ( RADIUS_Y * RADIUS_Y ) );
// Blend from foreground to background color to blur edges of ellipse.
const float BLEND_MIN = 0.7;
const float BLEND_MAX = 1.0;
if ( radius < BLEND_MIN )
{
fragColor = fontForeground;
}
else if ( radius > BLEND_MAX )
{
fragColor = fontBackground;
}
else
{
fragColor = mix( fontForeground, fontBackground, ( radius - BLEND_MIN ) / ( BLEND_MAX - BLEND_MIN ) );
}
}


CRT4_Lo.png
[Ellipses with blur.]


That was pretty close, but to get it the rest of the way, I added a two pass (horizontal/vertical) five-tap blur. It blurs the pixels together slightly and gives the pixels nice soft edges. I realized after seeing this version that the Superboard II actually did have elliptical pixels rather than the rounded-rectangle pixels of later IBM PC's. This looks a lot like I remember.

As a bonus for those players who may have grown up with amber or green CRT's (I actually had all three) you can select black and white by pressing F1, amber by pressing F2 or green by pressing F3 in Space Mines III.

New Comment

Cookie Warning

We were unable to retrieve our cookie from your web browser. If pressing F5 once to reload this page does not get rid of this message, please read this to learn more.

You will not be able to post until you resolve this problem.

Comment (You can use HTML, but please double-check web link URLs and HTML tags!)
Your Name
Homepage (optional, don't include http://)
Email (optional, but automatically spam protected so please do)
How many minutes in an hour? (What's this?)

  Admin Log In



[Home] [Blog] [Video] [Shop] [Press Kit] [About]
Terms Of Use & Privacy Policy