XXIIVV
Pitch Yaw Roll picture
20P12 — Pitch Yaw Roll

Moogle is a 3D wireframe toolkit.

Moogle is a minimal 3D wireframe tool designed to be used alongside its companion tool Nasu, both can export to the chr_format. It offers a handful of basic geometry drawing functions, it was written in ANSI C, and was inspired by Graf3DScene.

Moogle was first written on Plan9, the original Plan9 C implementation is available here.

moogle.c

To control the window size and default colors, edit the values defined at the top of the file itself. Press , E to export a .chr file, press R to render a .bmp file, and press h to toggle tile guides. To learn more, visit the repository.

cc -std=c89 -Wall moogle.c -L/usr/local/lib -lSDL2 -lm -o moogle

The following code is a single-file implementation written in 700 lines of ANSI C, the only dependecy is SDL2.

#include <SDL2/SDL.h>
#include <stdio.h>
#include <math.h>

/* 
Copyright (c) 2020 Devine Lu Linvega

Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE.
*/

#define HOR 32
#define VER 16
#define PAD 8
#define ZOOM 2
#define color1 0x000000
#define color2 0x72DEC2
#define color3 0xFFFFFF
#define color4 0x444444
#define color0 0x111111

#define VLIMIT 128
#define ELIMIT 128
#define MLIMIT 128

#define SZ (HOR * VER * 16)
#define PI 3.14159265358979323846

typedef struct {
	int x, y;
} Point;

typedef struct {
	double x, y;
} Point2d;

typedef struct {
	double x, y, z;
} Point3d;

typedef struct {
	int a, b;
} Edge;

typedef struct {
	int verticeslen, edgeslen, color;
	Point3d position, vertices[VLIMIT];
	Edge edges[ELIMIT];
} Mesh;

typedef struct {
	int len;
	Point3d origin;
	Mesh meshes[MLIMIT];
} Scene;

typedef enum {
	ISOMETRIC,
	PERSPECTIVE
} Projection;

typedef struct {
	double pitch, yaw, roll, range;
	Projection projection;
} Camera;

int colors[] = {color1, color2, color3, color4, color0};
int WIDTH = 8 * HOR + PAD * 2;
int HEIGHT = 8 * VER + PAD * 2;
int FPS = 30, GUIDES = 1;
SDL_Window *gWindow = NULL;
SDL_Renderer *gRenderer = NULL;
SDL_Texture *gTexture = NULL;
Uint32 *pixels;
unsigned char chrbuf[SZ];

Scene scn;
Camera cam;

/* helpers */

Point
Pt(int x, int y)
{
	Point p;
	p.x = x;
	p.y = y;
	return p;
}

Point2d
Pt2d(double x, double y)
{
	Point2d p;
	p.x = x;
	p.y = y;
	return p;
}

Point3d
Pt3d(double x, double y, double z)
{
	Point3d p;
	p.x = x;
	p.y = y;
	p.z = z;
	return p;
}

Mesh
Ms3d(double x, double y, double z)
{
	Mesh m;
	m.position = Pt3d(x, y, z);
	m.verticeslen = 0;
	m.edgeslen = 0;
	m.color = 1;
	return m;
}

Scene
Sc3d(double x, double y, double z)
{
	Scene s;
	s.origin = Pt3d(x, y, z);
	s.len = 0;
	return s;
}

Camera
Cm3d(double pitch, double yaw, double roll)
{
	Camera c;
	c.pitch = pitch;
	c.yaw = yaw;
	c.roll = roll;
	c.projection = PERSPECTIVE;
	c.range = 50;
	return c;
}

/* geometry */

double
rad2deg(double rad)
{
	return rad * (180 / PI);
}

double
deg2rad(double deg)
{
	return deg * (PI / 180);
}

double
ptangledeg(Point2d a, Point2d b)
{
	return rad2deg(atan2(b.y - a.y, b.x - a.x));
}

double
ptangle(Point2d a, Point2d b)
{
	return atan2(b.y - a.y, b.x - a.x);
}

double
ptdistance(Point2d a, Point2d b)
{
	return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}

Point2d
rotpt2d(Point2d c, Point2d p0, double deg)
{
	double a = ptangle(c, p0) + deg2rad(deg);
	double r = ptdistance(c, p0);
	return Pt2d(c.x + r * cos(a), c.y + r * sin(a));
}

Point3d
addpt3d(Point3d *a, Point3d *b)
{
	return Pt3d(a->x + b->x, a->y + b->y, a->z + b->z);
}

/* scene */

Point3d *
setvertex(Point3d *v, double x, double y, double z)
{
	v->x = x;
	v->y = y;
	v->z = z;
	return v;
}

Point3d *
addvertex(Mesh *m, double x, double y, double z)
{
	if(m->verticeslen == VLIMIT) {
		printf("Warning: Reached vertex limit\n");
		return NULL;
	}
	return setvertex(&m->vertices[m->verticeslen++], x, y, z);
}

Edge *
setedge(Edge *e, int v0, int v1)
{
	e->a = v0;
	e->b = v1;
	return e;
}

Edge *
addedge(Mesh *m, int v0, int v1)
{
	if(m->edgeslen == VLIMIT) {
		printf("Warning: Reached edge limit\n");
		return NULL;
	}
	return setedge(&m->edges[m->edgeslen++], v0, v1);
}

Mesh *
addmesh(Scene *s, Mesh m)
{
	if(s->len == MLIMIT) {
		printf("Warning: Reached mesh limit\n");
		return NULL;
	}
	s->meshes[s->len] = m;
	s->len++;
	return &s->meshes[s->len - 1];
}

Point3d *
rotverx(Point3d *o, Point3d *v, double angle)
{
	Point2d r = rotpt2d(Pt2d(o->y, o->z), Pt2d(v->y, v->z), angle);
	v->y = r.x;
	v->z = r.y;
	return v;
}

Point3d *
rotvery(Point3d *o, Point3d *v, double angle)
{
	Point2d r = rotpt2d(Pt2d(o->x, o->z), Pt2d(v->x, v->z), angle);
	v->x = r.x;
	v->z = r.y;
	return v;
}

Point3d *
rotverz(Point3d *o, Point3d *v, double angle)
{
	Point2d r = rotpt2d(Pt2d(o->x, o->y), Pt2d(v->x, v->y), angle);
	v->x = r.x;
	v->y = r.y;
	return v;
}

Point3d *
rotvertex(Point3d *o, Point3d *v, double pitch, double yaw, double roll)
{
	if(pitch)
		rotverx(o, v, pitch);
	if(yaw)
		rotvery(o, v, yaw);
	if(roll)
		rotverz(o, v, roll);
	return v;
}

Point
project(Camera *c, Point3d v)
{
	double r;
	if(c->projection == ISOMETRIC)
		return Pt(
			(WIDTH / 2) + v.x * (10 - c->range / 10),
			(HEIGHT / 2) + v.y * (10 - c->range / 10));
	r = 200 / (v.z + c->range);
	return Pt(WIDTH / 2 + r * v.x, HEIGHT / 2 + r * v.y);
}

/* transforms */

Mesh *
translate(Mesh *m, double x, double y, double z)
{
	int i;
	for(i = 0; i < m->verticeslen; i++)
		setvertex(&m->vertices[i],
			m->vertices[i].x + x,
			m->vertices[i].y + y,
			m->vertices[i].z + z);
	return m;
}

Mesh *
scale(Mesh *m, double x, double y, double z)
{
	int i;
	for(i = 0; i < m->verticeslen; i++)
		setvertex(&m->vertices[i],
			m->vertices[i].x * x,
			m->vertices[i].y * y,
			m->vertices[i].z * z);
	return m;
}

Mesh *
rotate(Mesh *m, double pitch, double yaw, double roll)
{
	int i;
	for(i = 0; i < m->verticeslen; i++)
		rotvertex(&m->position, &m->vertices[i], pitch, yaw, roll);
	return m;
}

Mesh *
color(Mesh *m, int color)
{
	m->color = color;
	return m;
}

Mesh *
extrude(Mesh *m, double depth)
{
	int i, vl = m->verticeslen, el = m->edgeslen;
	for(i = 0; i < vl; i++) {
		addvertex(m,
			m->vertices[i].x,
			m->vertices[i].y,
			m->vertices[i].z + depth);
		addedge(m, i, i + vl);
	}
	for(i = 0; i < el; i++)
		addedge(m,
			m->edges[i].a + vl,
			m->edges[i].b + vl);
	return m;
}

Mesh *
symmetry(Mesh *m, double x, double y, double z)
{
	int i, el, vl = m->verticeslen;
	for(i = 0; i < vl; i++)
		addvertex(m,
			m->vertices[i].x * x,
			m->vertices[i].y * y,
			m->vertices[i].z * z);
	el = m->edgeslen;
	for(i = 0; i < el; i++)
		addedge(m,
			el + 1 + m->edges[i].a,
			el + 1 + m->edges[i].b);
	return m;
}

/* Shapes */

Mesh *
addpoly(Mesh *m, double x, double y, double z, double radius, int segs)
{
	int i, offset = m->verticeslen;
	for(i = 0; i < segs; i++) {
		addvertex(m,
			x + radius * cos(2 * PI * i / segs),
			y + radius * sin(2 * PI * i / segs),
			z);
		addedge(m, offset + i, offset + (i + 1) % segs);
	}
	return m;
}

Mesh *
addpolygon(Scene *s, double radius, int segs)
{
	Mesh m = Ms3d(0, 0, 0);
	addpoly(&m, 0, 0, 0, radius, segs);
	return addmesh(s, m);
}

Mesh *
addpyramid(Scene *s, double radius, int segs, double depth)
{
	int i;
	Mesh m = Ms3d(0, 0, 0);
	addpoly(&m, 0, 0, depth / 2, radius, segs);
	addvertex(&m, 0, 0, -depth / 2);
	for(i = 0; i < segs; i++)
		addedge(&m, i, segs);
	return addmesh(s, m);
}

Mesh *
addfrustum(Scene *s, double radius, int segs, double depth, double cap)
{
	int i;
	Mesh m = Ms3d(0, 0, 0);
	addpoly(&m, 0, 0, depth / 2, radius, segs);
	addpoly(&m, 0, 0, -depth / 2, cap, segs);
	for(i = 0; i < segs; i++)
		addedge(&m, i, segs + i);
	return addmesh(s, m);
}

Mesh *
addprism(Scene *s, double radius, int segs, double depth)
{
	Mesh m = Ms3d(0, 0, 0);
	addpoly(&m, 0, 0, -depth / 2, radius, segs);
	extrude(&m, depth);
	return addmesh(s, m);
}

/* chr */

int
rowchr(int x, int y)
{
	return (y % 8) + ((x / 8 + y / 8 * HOR) * 16);
}

void
putchr(int x, int y, int color)
{
	int r = rowchr(x, y), px = x % 8;
	if(r < 0 || r > SZ - 8)
		return;
	if(!color) {
		chrbuf[r] &= ~(1UL << (7 - px));
		chrbuf[r + 8] &= ~(1UL << (7 - px));
	} else if(color == 2) {
		chrbuf[r] |= 1UL << (7 - px);
		chrbuf[r + 8] &= ~(1UL << (7 - px));
	} else if(color == 1) {
		chrbuf[r] &= ~(1UL << (7 - px));
		chrbuf[r + 8] |= 1UL << (7 - px);
	} else if(color == 3) {
		chrbuf[r] |= 1UL << (7 - px);
		chrbuf[r + 8] |= 1UL << (7 - px);
	}
}

void
newchr(void)
{
	int i;
	for(i = 0; i < SZ; ++i)
		chrbuf[i] = 0x00;
}

void
line(Point p0, Point p1, int color)
{
	double dx = abs(p1.x - p0.x), sx = p0.x < p1.x ? 1 : -1;
	double dy = -abs(p1.y - p0.y), sy = p0.y < p1.y ? 1 : -1;
	double err = dx + dy, e2;
	for(;;) {
		putchr(p0.x, p0.y, color);
		if(p0.x == p1.x && p0.y == p1.y)
			break;
		e2 = 2 * err;
		if(e2 >= dy) {
			err += dy;
			p0.x += sx;
		}
		if(e2 <= dx) {
			err += dx;
			p0.y += sy;
		}
	}
}

/* draw */

void
draw(Uint32 *dst)
{
	int b, i, j, id = 0;
	for(b = 0; b < SZ; b += 16)
		for(i = 0; i < 8; i++)
			for(j = 7; j >= 0; j--) {
				int ch1 = chrbuf[b + i];
				int ch2 = chrbuf[b + i + 8];
				int color = ((ch1 >> j) & 0x1) + (((ch2 >> j) & 0x1) << 1);
				int ti = id / 64;
				int px = (ti / (HOR * VER)) * (8 * HOR) + (ti % HOR) * 8 + (id % 8);
				int py = ((ti / HOR) * 8) + ((id % 64) / 8);
				dst[(py + PAD) * WIDTH + (px + PAD)] = colors[color];
				id++;
			}
	SDL_UpdateTexture(gTexture, NULL, dst, WIDTH * sizeof(Uint32));
	SDL_RenderClear(gRenderer);
	SDL_RenderCopy(gRenderer, gTexture, NULL, NULL);
	SDL_RenderPresent(gRenderer);
}

void
widget(Scene *s, Camera *c)
{
	Point3d a = Pt3d(0, 0, 0), x = Pt3d(5, 0, 0), y = Pt3d(0, 5, 0), z = Pt3d(0, 0, 5);
	rotvertex(&s->origin, &x, c->pitch, c->yaw, c->roll);
	rotvertex(&s->origin, &y, c->pitch, c->yaw, c->roll);
	rotvertex(&s->origin, &z, c->pitch, c->yaw, c->roll);
	line(project(c, a), project(c, x), 1);
	line(project(c, a), project(c, y), 2);
	line(project(c, a), project(c, z), 3);
}

void
render(Scene *s, Camera *c)
{
	int i, j;
	newchr();
	for(i = 0; i < s->len; i++) {
		Mesh *m = &s->meshes[i];
		for(j = 0; j < m->edgeslen; j++) {
			Edge *edge = &m->edges[j];
			Point3d a = addpt3d(&m->vertices[edge->a], &m->position);
			Point3d b = addpt3d(&m->vertices[edge->b], &m->position);
			rotvertex(&s->origin, &a, c->pitch, c->yaw, c->roll);
			rotvertex(&s->origin, &b, c->pitch, c->yaw, c->roll);
			line(
				project(c, addpt3d(&s->origin, &a)),
				project(c, addpt3d(&s->origin, &b)),
				m->color);
		}
	}
	if(GUIDES)
		widget(s, c);
	draw(pixels);
}

void
orient(Scene *s, Camera *c, double pitch, double yaw, double roll)
{
	c->pitch = pitch;
	c->yaw = yaw;
	c->roll = roll;
	render(s, c);
}

int
error(char *msg, const char *err)
{
	printf("Error %s: %s\n", msg, err);
	return 0;
}

void
toggleprojection(Scene *s, Camera *c)
{
	c->projection = c->projection == ISOMETRIC ? PERSPECTIVE : ISOMETRIC;
	render(s, c);
}

void
toggleguides(void)
{
	GUIDES = !GUIDES;
	render(&scn, &cam);
}

void
modpitch(int mod)
{
	cam.pitch += mod;
	render(&scn, &cam);
}

void
modyaw(int mod)
{
	cam.yaw += mod;
	render(&scn, &cam);
}

void
modroll(int mod)
{
	cam.roll += mod;
	render(&scn, &cam);
}

void
modrange(int mod)
{
	int res = cam.range + mod;
	if(res > 0 && res < 90)
		cam.range = res;
	render(&scn, &cam);
}

void
exportchr()
{
	FILE *f = fopen("moogle-export.chr", "wb");
	if(!fwrite(chrbuf, sizeof(chrbuf), 1, f))
		error("Save", "Invalid output file");
	fclose(f);
	puts("Exported moogle-export.chr");
}

void
renderbmp(void)
{
	SDL_Surface *surface = SDL_GetWindowSurface(gWindow);
	GUIDES = 0;
	draw(pixels);
	SDL_RenderReadPixels(gRenderer,
		NULL,
		SDL_PIXELFORMAT_ARGB8888,
		surface->pixels,
		surface->pitch);
	SDL_SaveBMP(surface, "moogle-render.bmp");
	SDL_FreeSurface(surface);
	puts("Rendered moogle-render.bmp");
}

void
quit(void)
{
	free(pixels);
	SDL_DestroyTexture(gTexture);
	gTexture = NULL;
	SDL_DestroyRenderer(gRenderer);
	gRenderer = NULL;
	SDL_DestroyWindow(gWindow);
	gWindow = NULL;
	SDL_Quit();
	exit(0);
}

void
dokey(SDL_Event *event)
{
	switch(event->key.keysym.sym) {
	case SDLK_TAB: toggleprojection(&scn, &cam); break;
	case SDLK_e: exportchr(); break;
	case SDLK_r: renderbmp(); break;
	case SDLK_h: toggleguides(); break;
	case SDLK_UP:
	case SDLK_w: modpitch(3.0); break;
	case SDLK_LEFT: modroll(-3.0); break;
	case SDLK_a: modyaw(-3.0); break;
	case SDLK_DOWN:
	case SDLK_s: modpitch(-3.0); break;
	case SDLK_RIGHT: modroll(-3.0); break;
	case SDLK_d: modyaw(3.0); break;
	case SDLK_z: modrange(3.0); break;
	case SDLK_x: modrange(-3.0); break;
	}
}

int
init(void)
{
	int i, j;
	if(SDL_Init(SDL_INIT_VIDEO) < 0)
		return error("Init", SDL_GetError());
	gWindow = SDL_CreateWindow("Moogle",
		SDL_WINDOWPOS_UNDEFINED,
		SDL_WINDOWPOS_UNDEFINED,
		WIDTH * ZOOM,
		HEIGHT * ZOOM,
		SDL_WINDOW_SHOWN);
	if(gWindow == NULL)
		return error("Window", SDL_GetError());
	gRenderer = SDL_CreateRenderer(gWindow, -1, 0);
	if(gRenderer == NULL)
		return error("Renderer", SDL_GetError());
	gTexture = SDL_CreateTexture(gRenderer,
		SDL_PIXELFORMAT_ARGB8888,
		SDL_TEXTUREACCESS_STATIC,
		WIDTH,
		HEIGHT);
	if(gTexture == NULL)
		return error("Texture", SDL_GetError());
	pixels = (Uint32 *)malloc(WIDTH * HEIGHT * sizeof(Uint32));
	if(pixels == NULL)
		return error("Pixels", "Failed to allocate memory");
	for(i = 0; i < HEIGHT; i++)
		for(j = 0; j < WIDTH; j++)
			pixels[i * WIDTH + j] = color1;
	return 1;
}

int
main(void)
{
	int ticknext = 0;

	scn = Sc3d(0, 0, 0);
	cam = Cm3d(120, 20, 0);

	if(!init())
		return error("Init", "Failure");

	addfrustum(&scn, 12, 8, 10, 8);
	color(addprism(&scn, 12, 8, 10), 2);

	newchr();
	render(&scn, &cam);

	while(1) {
		int tick = SDL_GetTicks();
		SDL_Event event;
		if(tick < ticknext)
			SDL_Delay(ticknext - tick);
		ticknext = tick + (1000 / FPS);
		while(SDL_PollEvent(&event) != 0)
			if(event.type == SDL_QUIT)
				quit();
			else if(event.type == SDL_KEYDOWN)
				dokey(&event);
			else if(event.type == SDL_WINDOWEVENT)
				if(event.window.event == SDL_WINDOWEVENT_EXPOSED)
					draw(pixels);
	}
	quit();
	return 0;
}
— Submit an edit to moogle.c.txt(735 lines)

Structures

Here's some additional primitives that I use to build scenes in Moogle.

Mesh*
addbox(Scene* s, double width, double height, double depth)
{
	int i;
	Mesh m = Ms3d(0, 0, 0);
	addvertex(&m, width / 2, height / 2, -depth / 2);
	addvertex(&m, -width / 2, height / 2, -depth / 2);
	addvertex(&m, -width / 2, -height / 2, -depth / 2);
	addvertex(&m, width / 2, -height / 2, -depth / 2);
	for(i = 0; i < 4; i++)
		addedge(&m, i, (i + 1) % 4);
	extrude(&m, depth);
	return addmesh(s, m);
}

Mesh*
addplane(Scene* s, double width, double height, double xsegs, double ysegs)
{
	int ix, iy;
	Mesh m = Ms3d(0, 0, 0);
	for(ix = 0; ix < xsegs + 1; ix++) {
		addvertex(&m, ix * (width / xsegs) - width / 2, height / 2, 0);
		addvertex(&m, ix * (width / xsegs) - width / 2, -height / 2, 0);
		addedge(&m, ix * 2, ix * 2 + 1);
	}
	for(iy = 0; iy < ysegs + 1; iy++) {
		addvertex(&m, width / 2, iy * (height / ysegs) - height / 2, 0);
		addvertex(&m, -width / 2, iy * (height / ysegs) - height / 2, 0);
		addedge(&m, ix * 2 + iy * 2, ix * 2 + iy * 2 + 1);
	}
	return addmesh(s, m);
}

Mesh*
addarc(Scene* s, double radius, double segs, double angle)
{
	int i;
	double arc = angle / 360;
	Mesh m = Ms3d(0, 0, 0);
	for(i = 0; i < segs + 1; i++) {
		addvertex(&m,
		          radius * cos(2 * PI * i * arc / segs),
		          radius * sin(2 * PI * i * arc / segs),
		          0);
		if(i < segs)
			addedge(&m, i, i + 1);
	}
	return addmesh(s, m);
}

Mesh*
addsphere(Scene* s, double radius, double xsegs, double zsegs)
{
	int i;
	Mesh m = Ms3d(0, 0, 0);
	for(i = 1; i < zsegs + 1; i++)
		addpoly(&m, 0, 0,
		        radius * cos(PI * i / zsegs),
		        radius * sin(PI * i / zsegs),
		        xsegs);
	return addmesh(s, m);
}

Mesh*
addline(Scene* s, double ax, double ay, double az, double bx, double by, double bz)
{
	Mesh m = Ms3d(0, 0, 0);
	addvertex(&m, ax, ay, az);
	addvertex(&m, bx, by, bz);
	addedge(&m, 0, 1);
	return addmesh(s, m);
}
— Submit an edit to moogle-structures.c.txt(72 lines)
Moogle Spheroid picture
20P01 — Moogle Spheroid

incoming(8): neauismetica graf3dscene dotgrid nasu computer defunct meta identity

Last update on 20U03, edited 8 times. +41/52fh-----+