Skip to main content
cover image · 1280×720
Blog Post

Handheld Virtual Donut

We render your hand-held virtual donut

JE
jeremybobbin
@jeremybobbin·Apr 22, 2026· 3 min read·arduino
3 views
Handheld Virtual Donut

So I have an Arduino mega here & a gyroscope.

complete

At first, I wanted to make a digital level, like this:

level

With a pixel matrix like this:

matrix

But I couldn't find the right library & I didn't feel like reading through the component spec.

I had remembered seeing renderings of rotating donuts, as an introduction to graphics programming, and figured it'd be cool to see the donut's orientation reflected by the gyroscope.

I found this article which discusses the math - I just copied the code & "ported it to UNIX"(made it easier to use in a UNIX environment). Here is the code

c
#include <math.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

#define THETA_SPACING 0.07f
#define PHI_SPACING 0.02f

#define PI 3.14159265358979323846
#define R1 1.0f
#define R2 2.0f
#define K2 5.0f

int main (int argc, char **argv) {
	char *s;
	int n;

	float K1, A, B;

	char *output;
	float *zbuffer;

	struct winsize w;
	ioctl(STDOUT_FILENO, TIOCGWINSZ, &w);

	if ((s = getenv("LINES"))) {
		w.ws_row = atoi(s);
	}

	if ((s = getenv("COLUMNS"))) {
		w.ws_col = atoi(s);
	}

	K1 = w.ws_col*K2*3/(8*(R1+R2));

	output = malloc(sizeof(char)*w.ws_row*w.ws_col);
	zbuffer = malloc(sizeof(float)*w.ws_row*w.ws_col);

	A = 0;
	B = 0;

	if (argc >= 2) {
		A = strtof(argv[1], NULL);
	}

	if (argc >= 3) {
		B = strtof(argv[2], NULL);
	}

	memset(output, ' ', sizeof(char)*w.ws_row*w.ws_col);
	memset(zbuffer, 0, sizeof(float)*w.ws_row*w.ws_col);

	for (float theta = 0; theta < 2*PI; theta += THETA_SPACING) {
		for(float phi = 0; phi < 2*PI; phi += PHI_SPACING) {

			float circlex = R2 + R1*cos(theta);
			float circley = R1*sin(theta);

			float x = circlex*(cos(B)*cos(phi) + sin(A)*sin(B)*sin(phi)) - circley*cos(A)*sin(B);
			float y = circlex*(sin(B)*cos(phi) - sin(A)*cos(B)*sin(phi)) + circley*cos(A)*cos(B);
			float z = K2 + cos(A)*circlex*sin(phi) + circley*sin(A);

			int xp = (int) (w.ws_col/2 + K1*(1/z)*x);
			int yp = (int) (w.ws_row/2 - K1*(1/z)*y);
			if (xp < 0 || yp < 0 || xp > w.ws_col || yp > w.ws_row) {
				continue;
			}
			
			float L = cos(phi)*cos(theta)*sin(B) - cos(A)*cos(theta)*sin(phi) - sin(A)*sin(theta) + cos(B)*(cos(A)*sin(theta) - cos(theta)*sin(A)*sin(phi));
			if (L > 0) {
				if((1/z) > zbuffer[xp+(yp)*w.ws_col]) {
					zbuffer[xp+(yp)*w.ws_col] = (1/z);
					if (xp > 0 && yp > 0) {
						output[xp+((yp)*w.ws_col)] = ".,-~:;=!*#$@"[(int)(L*8)];
					}
				}
			}
		}
	}

	for (int j = 0; j < w.ws_row; j++) {
		for (int i = 0; i < w.ws_col; i++) {
			putchar(output[i+(j*w.ws_col)]);
		}
		putchar('\n');
	}
}

You can compile it(gcc -lm donut.c -o donut) & pass it up to two numbers as arguments like this: ./donut 1 1.5 and you should see the following:

donut

Since it's a donut, I only care about its roll & pitch - not its yaw. A donut looks the same, even if you rotate it N-degrees.

roll-pitch-yaw

Since we're constantly accelerating upwards, we can use X & Y acceleration values from the gyroscope to estimate the gyroscope's orientation towards the ground. Here's the code to print the X & Y acceleration to the console:

c
#include <Wire.h>

int i;
char buf[128];
int acc[3], gyro[3], temperature;

void setup() {
	Serial.begin(9600);
	i = 0;

	Wire.beginTransmission(0x68); 
	Wire.write(0x6B);  
	Wire.write(0x00);
	Wire.endTransmission(); 
	                                      
	Wire.beginTransmission(0x68); 
	Wire.write(0x1C);   
	Wire.write(0x10); 
	Wire.endTransmission(); 
	                                      
	Wire.beginTransmission(0x68);
	Wire.write(0x1B);
	Wire.write(0x08); 
	Wire.endTransmission(); 
}

void loop() {
	Wire.beginTransmission(0x68);  
	Wire.write(0x3B);
	Wire.endTransmission(); 
	Wire.requestFrom(0x68, 14);    

	acc[0] = Wire.read()<<8|Wire.read();                                  
	acc[1] = Wire.read()<<8|Wire.read();                                  
	acc[2] = Wire.read()<<8|Wire.read();                                  
	temperature = Wire.read()<<8|Wire.read();                                   
	gyro[0] = Wire.read()<<8|Wire.read();
	gyro[1] = Wire.read()<<8|Wire.read();
	gyro[2] = Wire.read()<<8|Wire.read();

	sprintf(buf, "%4.2f %4.2f\n", ((float)acc[0])/3000, ((float)acc[1])/3000);
	if (i%10 == 0) {
		Serial.print(buf);
	}

	delay(10);
	i++;
}

Sadly, since Arduino's printf-functions do not handle floats by default, you may see output like the following:

? ?
? ?
? ?
? ?
? ?
? ?
...

I gave it the linker flags -Wl,-u,vfprintf -lprintf_flt and it worked properly:

0.23 -0.04
0.08 0.12
0.33 0.39
0.25 0.44
-0.34 0.62
0.26 0.13
1.18 -1.47
...

All that's left is to write a little shell to compose the two programs:

bash
arduino-cli monitor --fqbn $FQBN -p /dev/ttyACM0 |
	while read a b; do
		clear
		./donut "$a" "$b"
		printf "%0.2f %0.2f" "$a" "$b"
	done

And there you have it! Here's a video demonstration:

YouTube
Filed under
arduino
JE
Written by
jeremybobbin
@jeremybobbin
Related Posts

Comments

No comments yet. Be the first!