Before I started work on creating a vector game engine to be used on displays like oscilloscopes, I had watched a really fantastic explanation of how to create graphics for laser displays by Seb Lee-Delisle:
In this video, Seb talked about the challenges of working with a laser that takes time to accelerate and decelerate as it moves around the screen. Draw sorting and gradually changing the laser’s position when moves between shapes are two techniques that he used when recreating Asteroids for a laser projector. When approaching my new oscilloscope project, I kept these ideas in my back pocket as solutions to problems I expected to have.
Overshooting and Oscillations
Sure enough, I did have some problems with controlling the electron beam of my oscilloscope vector display. When moving from one position to another which is very far away, the electron beam would overshoot and oscillate before finally settling into the place I wanted it to land:
For context, my current setup and game engine uses an audio DAC that is running at 192 kHz with a variable video game framerate. My current project targets 80 fps, but it could go much higher. 80 fps with 192,000 samples per second means that I can move the electron beam 2400 times per frame. If I need to move the electron beam more times per frame, that frame will take longer to draw and the framerate will drop. If it drops too low, you can start to see flickering, like an old CRT monitor running at a low refresh rate. The electron beam moves extremely quickly — faster than 192,000th of a second! So I need to gradually step along the path I am drawing. This effectively “slows down” the electron beam’s movement when drawing a shape. This means that 2400 samples per frame is not very much to work with!
Getting back to the problem of the electron beam overshooting and oscillating, I started first with the solution of draw sorting. I figured that if I could make the electron beam move in a more optimized path that it could help reduce the distance that the beam needed to travel between shapes and thus reduce the overshooting and oscillation at the start of drawing a new shape.
I implemented a very quick prototype algorithm that simply looked for the next shape that was closest to the current electron beam position every time it finished drawing a shape. This had some immediate drawbacks that I didn’t expect! Although, it’s possible this method could help with the overshooting, I found that it caused a much bigger problem: the scene started flickering and jittering as I moved the camera around!
The reason for this flicker and jitter was immediately obvious to me — the refresh rate of each shape was varying between frames! Here’s an example of a worst case scenario that would cause this problem: Let’s say, after sorting all the shapes, the draw order of objects is:
[A, B, C, D, E, F, G]. But then the player moves the camera and the scene changes such that the draw order after sorting is something like
[B, C, D, E, F, G, A]. While object A was drawn first in one frame, it was drawn last in the next frame. Without sorting, object A would have been drawn once every 1/80th of a second, but now it needed to wait a full 1/40th of a second before being re-drawn to the screen. This behaviour causes flicker and can make objects look like they are jittering as a camera pans across the scene.
This experiment has made it very clear that I must always draw shapes in the same order with a vector display to ensure that their refresh rate is kept somewhat constant!
Blanking is a term used to describe the time when a display will “blank” (stop drawing an image) for a short period of time while it prepares draw at a different position, usually on the opposite side of the screen. The idea of blanking is important and valuable to both raster and vector displays. With my current oscilloscope, I do not have the ability to turn off the electron beam, but I can dedicate some additional time to give the electron beam a chance to settle in its new position before starting to draw. Here’s what it looks like if I pause on the new draw position for 7 samples (7/192,000th of a second) before continuing to draw the new shape:
You can see that this definitely helps. Now each line is being fully drawn, though the initial oscillations as the beam settles on the new location can still be seen.
Exponential Deceleration During Blanking
There was one last trick that I kept in my back pocket: changing the acceleration of the the electron between points, during blanking. I fiddled with some different tween functions, and settled on a 7 frame blanking with a quadratic ease out:
Ta-da! This is now looking pretty crisp. In the future, I plan to change the number of blanking samples based on the distance between the two shapes. If it’s a small distance, there’s no point in spending a full 7 samples during this blanking time.
Oscilloscope Z-Input & Blanking
I mentioned previously that I was unable to turn the electron beam off and on while moving between shapes. This isn’t entirely true: my oscilloscope does have a “z-input” that can be used for blanking. But unfortunately, I found that it is AC-coupled. This means that the z-input is only able to detect changes in voltage, rather than read the DC voltage directly like my x and y-inputs. My game engine has actually supported changing the brightness of samples through a “z-input” and a third audio channel since the beginning, but I will need to get a new oscilloscope with a DC-coupled z-input that I can use for full-featured blanking.
My vector game engine’s source code is available on Github under the MIT license. It’s pretty rough and I intend to use it primarily for my own experiments, but you’re welcome to peek around and check out the progress. I plan to continue to post here from time to time with updates, but for the latest news on the project, check out my Twitter feed.