Skip to content
This repository was archived by the owner on Apr 29, 2023. It is now read-only.
Jim edited this page Apr 2, 2023 · 216 revisions

πŸ„ cow.cpp

βœ‹ FAQ's

  • to return to the top of the docs, press the Home key on your keyboard
  • ignore anything prefixed with an underscore (i.e., call soup_draw(...), not _soup_draw(...))
  • doing things marked with this emoji βš” can be a bit dangerous if you don't understand exactly what you're doing
  • doing things marked with this emoji πŸ›‘ can be a bit safer :)
  • for help while cow is running, especially if you forget the hotkeys, just press ? on the keyboard (don't forget to hold shift)
  • real is a typedef for double
  • vecX usually means "this can be either a vec2 or a vec3"
  • void foo(real x, real y, real z = 0.0); means "you can call this function without specifying z and the default argument (in this case 0.0) will be filled in for you"; i.e., foo(2.0, 1.0) is equivalent to foo(2.0, 1.0, 0.0);
  • unless otherwise specified, angles are in radians
  • coordinate systems are always right-handed
  • the world y-axis points up
  • good luck; have fun; make moovies -jim

πŸ‘‹ hello

here is an empty cow.cpp app

void app() {
    // ... (setup)
    while (cow_begin_frame()) {
        // ... (loop)
    }
}

and here is the same app with some comments

void app() {
    // hi!--i'm an "app"
    // code *here* (before the loop) runs only once; good place to...
    // - set up a camera
    // - allocate memory
    // - precompute anything that won't change during the app
    while (cow_begin_frame()) {
        // hello!--this is the app's "loop", which runs over and over until the user quits
        // code *here* (inside the loop) is run once per frame; good place to...
        // - update whatever stuff you stored in memory before
        // - draw stuff!
        // - interact with stuff!
        // - immediate mode graphical user interface!
    }
}

a small app might look something like this

  • don't panic, experiment!
void app() {
    Camera2D camera = { 5.0 };
    vec2 foo[] = { { -1.0, -1.0 }, { -1.0, 1.0 }, { 1.0, 1.0 }, { 1.0, -1.0 } };
    bool bar = false;
    while (cow_begin_frame()) {
        camera_move(&camera);
        mat4 PV = camera_get_PV(&camera);
        gui_checkbox("bar", &bar);
        widget_drag(PV, 4, foo);
        soup_draw(PV, SOUP_QUADS, 4, foo, NULL, (!bar) ? monokai.red : monokai.blue);
    }
}

to run a couple apps sequentially, we can do this...

// void app_A() {
//     while (cow_begin_frame()) {
//         printf("A\n");
//     }
// }
// 
// void app_B() {
//     while (cow_begin_frame()) {
//         printf("B\n");
//     }
// }
// 
// int main() {
//     _cow_init();
//     _cow_reset();
//     app_A();
//     _cow_reset();
//     app_B();
// }

...but you will probably prefer the ✨APP api✨, which uses macro magic to give us menu controls (press ? while running to explore)

  • APPS { ... } is a main menu block
  • APP(name); is an app, which should be a function void name() { ... } with a while (cow_begin_frame()) { ... } loop inside it
void app_A() {
    while (cow_begin_frame()) {
        printf("A\n");
    }
}

void app_B() {
    while (cow_begin_frame()) {
        printf("B\n");
    }
}

int main() {
    APPS {
        APP(app_A);
        APP(app_B);
    }
}

note: the syntax must be followed carefully; don't mix and match with _cow_init();, _cow_reset();; don't write app_A(); instead of APP(app_A);

πŸ“ defines

some useful macros (written here as if they were functions)

void ASSERT(bool condition);
  • ASSERT(condition); crashes the program is condition is false
PI
TAU
HUGE_VAL
TINY_VAL
  • PI is $\pi$
  • TAU is $2\pi$
  • HUGE_VAL is big real number
  • TINY_VAL is a tiny real number
real RAD(real degrees);
real DEG(real radians);
  • RAD(theta) converts from degrees -> radians; returns $\frac{\pi}{180.0}*\theta$
  • DEG(theta) converts from radians -> degrees; returns $\frac{180.0}{\pi}*\theta$
real ABS(real a);
real SGN(real a);
real MIN(real a, real b);
real MAX(real a, real b);
real AVG(real a, real b);
  • ABS(a) return $|a|$
  • SGN(a) returns $\text{sgn}(a)$
  • MIN(a, b) returns $\min(a,b)$
  • MAX(a, b) returns $\max(a,b)$
  • AVG(a, b) return $(a + b)/2$
bool IS_ZERO(real a);
bool ARE_EQUAL(real a, real b);
bool IS_BETWEEN(real p, real a, real b);
  • IS_ZERO(a) returns whether a is approximately zero
  • ARE_EQUAL(a, b) returns whether a and b are approximately equal
  • IS_BETWEEN(p, a, b) returns whether p is approximately in the interval $[a, b]$
real LERP(real t, real a, real b);
vecX LERP(real t, vecX a, vecX b);
real INVERSE_LERP(real p, real a, real b);
real LINEAR_REMAP(real p, real a, real b, real c, real d);
real CLAMP(real p, real a, real b);
  • LERP(t, a, b) returns $\mathbf{p} = (1-t)*\mathbf{a} + t*\mathbf{b}$ i.e. "linearly interpolates" aka "lerps" between a and b
  • INVERSE_LERP(p, a, b) returns $t = \frac{p - a}{b - a}$
  • LINEAR_REMAP(p, a, b, c, d) returns LERP(INVERSE_LERP(p, a, b), c, d)
  • CLAMP(p, a, b) returns $\min\left(\max(p,a),b\right),$ i.e. "clamps" $p$ to the range $[a, b]$
// convert from Fahrenheit -> Celsius
real celsius = LINEAR_REMAP(fahrenheit, 32.0, 212.0, 0.0, 100.0);
// enforce "theta must be between -90 deg and +90 deg"
theta = CLAMP(theta, -RAD(90), RAD(90));

βš” int _COUNT_OF(T fixed_size_array[]);

  • _COUNT_OF(foo) returns the number of elements in fixed size array foo; use at your own risk

πŸ’ͺ sbuff

simple std::vector alternative

template <typename T> struct StretchyBuffer {
    int length;
    int _capacity;
    T *data;
    T &operator [](int index) { return data[index]; }
};
template <typename T> void sbuff_push_back(StretchyBuffer<T> *buffer, T element);
template <typename T> void sbuff_free(StretchyBuffer<T> *buffer);
template <typename T> void sbuff_insert(StretchyBuffer<T> *buffer, int i, T element);
template <typename T> void sbuff_delete(StretchyBuffer<T> *buffer, int i);
  • int length is the number of elements currently stored in the stretchy buffer
  • sbuff_push_back(&buffer, element); appends (a copy of) element to buffer's internal data array
  • sbuff_free(&buffer); frees buffer's internal data array and zeroes out the struct
  • sbuff_insert(&buffer, i, element); inserts element at index i and moves the rest of the elements over
  • sbuff_delete(&buffer, i); deletes the element at index i and moves the rest of the elements over πŸ›‘ don't call this method while iterating over a StretchyBuffer!
// print "42" in an incredibly roundabout fashion
StretchyBuffer<int> numbers = {};
sbuff_push_back(&numbers, 4);
sbuff_push_back(&numbers, 2);
ASSERT(numbers.length == 2);
printf("%d%d\n", numbers[0], numbers[1]);
sbuff_free(&numbers);

🎲 random

real random_real(real lower_bound, real upper_bound);
real random_sign();
  • random_real(lower_bound, upper_bound) returns a random real in the lower_bound to upper_bound range
  • random_sign() returns either 1.0 or -1.0 with equal probability

🌎 globals

all of cow.cpp's "state" that you might need to access directly is bundled up in this single struct

note: this data is understood to be read-only

struct Globals {
    mat4 NDC_from_Screen;
    mat4 Identity; // NDC_from_NDC
  • globals.NDC_from_Screen is the transform that maps from screen coordinates (i.e. pixel coordinates; good for writing GUI) to normalized device coordinates aka NDC (what OpenGL needs to draw something)
  • globals.Identity is the 4x4 identity matrix πŸ€·β€β™‚οΈ; it is also the matrix that takes us from NDC to NDC
    bool key_pressed[];
    bool key_held[];
    bool key_released[];
    bool key_shift_held;
  • globals.key_pressed[key] is whether that key was just pressed; key can be 'a', 'b', ..., '0', '1', ..., COW_KEY_SPACE, COW_KEY_TAB, COW_KEY_ARROW_LEFT, COW_KEY_ARROW_RIGHT, COW_KEY_ESCAPE, COW_KEY_BACKSPACE, COW_KEY_ENTER
  • globals.key_held[key] is whether that key is currently being held
  • globals.key_released[key] is whether that key was just released
  • globals.key_shift_held is whether shift is currently held
    bool mouse_left_pressed;
    bool mouse_left_held;
    bool mouse_left_released;
    bool mouse_right_pressed;
    bool mouse_right_held;
    bool mouse_right_released;
    real mouse_wheel_offset;
    vec2 mouse_position_Screen;
    vec2 mouse_position_NDC;
    vec2 mouse_change_in_position_Screen;
    vec2 mouse_change_in_position_NDC;
    int _mouse_owner;
};
Globals globals;
  • globals.mouse_left_pressed is whether the left mouse button was just pressed
  • globals.mouse_wheel_offset is how far (i believe in "clicks") the mouse wheel was scrolled since last frame; this is a signed quantity
  • globals.mouse_position_Screen is the position of the mouse in screen coordinates
  • globals.mouse_position_NDC is the position of the mouse in normalized device coordinates
  • globals.mouse_change_in_position_Screen is the change in the mouse's position since last frame in screen coordinates
  • globals.mouse_change_in_position_NDC is the change in the mouse's position since last frame in normalized device coordinates
  • globals._mouse_owner is used internally, and says which part of cow.cpp currently "owns" the mouse input; it can be COW_MOUSE_OWNER_NONE, COW_MOUSE_OWNER_GUI, COW_MOUSE_OWNER_WIDGET

🐭 mouse

get the mouse position (and change in position) in world coordinates

vec2 mouse_get_position(mat4 PV);
vec2 mouse_get_change_in_position(mat4 PV);
  • mouse_get_position(PV) returns the mouse's position in world coordinates, given matrix $\mathbf{PV}$ (typically from a Camera2D)
  • mouse_get_change_in_position(PV) returns the mouse's change in position since the last frame in world coordinates, given matrix $\mathbf{PV}$
vec2 s_mouse = mouse_get_position(PV);
soup_draw(PV, SOUP_POINTS, 1, &s_mouse, NULL, monokai.red);

πŸŽ₯ camera

a simple 2D camera and 3D perspective/orthographic orbit camera

struct Camera2D {
    real screen_height_World;
    real o_x;
    real o_y;
};
  • real screen_height_World is the height of the screen in world coordinates
  • real o_x is the x-coordinate of the camera's position
  • real o_y is the y-coordinate of the camera's position
struct Camera3D {
    union {
        real persp_distance_to_origin;
        real ortho_screen_height_World;
    };
    real angle_of_view;
    real theta;
    real phi;
    real o_x;
    real o_y;

note: to use an ortho camera, set angle_of_view to 0

  • real persp_distance_to_origin is the distance from a perspective camera's (world) position to the world origin
  • real ortho_screen_height_World is the height of the screen in world coordinates for an ortho camera
  • real angle_of_view is the angle of view for a perspective camera; set to 0 to use an ortho camera
  • real theta is the camera's yaw
  • real phi is the camera's pitch
  • real o_x is analogous to the same field in a Camera2D (though hard to summarize in words)
  • real o_y is analogous to the same field in a Camera2D (though hard to summarize in words)
void camera_move(CameraXD *camera);
void camera_attach_to_gui(CameraXD *camera);
mat4 camera_get_C(CameraXD *camera);
mat4 camera_get_V(CameraXD *camera);
mat4 camera_get_P(CameraXD *camera);
mat4 camera_get_PV(CameraXD *camera);

note: CameraXD means the function can be called on a Camera2D or a Camera3D

  • camera_move(camera); lets you move the camera using mouse controls; rotate by holding the left mouse button, pan by holding right mouse button, zoom with the mouse wheel

  • camera_attach_to_gui(camera); reads out the camera to the gui with sliders

  • camera_get_C(camera) returns the camera matrix

$$\mathbf{C}=\begin{bmatrix}\mathbf{x}&\mathbf{y}&\mathbf{z}&\mathbf{o}\\0&0&0&1\end{bmatrix}$$
  • camera_get_V(camera) returns the view matrix $\mathbf{V}=\mathbf{C}^{-1}$
  • camera_get_P(camera) returns the projection matrix $\mathbf{P}$
  • camera_get_PV(camera) returns $\mathbf{P}\mathbf{V}$
Camera2D camera = { 10.0 };
while (cow_begin_frame()) {
    camera_move(&camera);
    mat4 PV = camera_get_PV(&camera);
    // ...
}
Camera3D camera = { 10.0, RAD(45) };
while (cow_begin_frame()) {
    camera_move(&camera);
    mat4 PV = camera_get_PV(&camera);
    // ...
}

🧐 gui

immediate mode gui; thanks Casey Muratori!

void gui_printf(const char *format, ...);
void gui_readout(char *name, int *variable);
void gui_readout(char *name, real *variable);
bool gui_button(char *name, int hotkey = '\0');
void gui_checkbox(char *name, bool *variable, int hotkey = '\0');
void gui_slider(
    char *name,
    int *variable,
    int lower_bound,
    int upper_bound,
    int decrement_hotkey = '\0',
    int increment_hotkey = '\0',
    bool loop = false);
void gui_slider(
    char *name,
    real *variable,
    real lower_bound,
    real upper_bound,
    bool readout_in_degrees = false);
  • gui_printf(...); follows the syntax of printf(...) but prints a line to the gui instead of to the terminal
  • gui_readout(name, variable); prints a variable to the gui so you can inspect it
  • gui_button(name, hotkey) returns true iff you just pressed the corresponding button or hotkey
  • gui_checkbox(name, variable, hotkey); lets you toggle the boolean variable true / false
  • gui_slider(name, variable, lower_bound, upper_bound, decrement_hotkey, increment_hotkey, loop); lets you scrub the integer variable in the inclusive lower_bound to upper_bound range either with the mouse or using decrement_hotkey and increment_hotkey; if loop is true, then `variable will wrap around (instead of being clamped) when hotkey'd outside of the range
  • gui_slider(name, variable, lower_bound, upper_bound, readout_in_degrees); lets you scrub the real variable in the lower_bound to upper_bound range using the mouse; if variable is an angle in radians, you can pass true for readout_in_degrees in order to read it out in degrees
bool paused = false;
real time = 0.0;
while (cow_begin_frame()) {
    gui_checkbox("paused", &paused, 'p');
    gui_readout("time", &time);
    if (gui_button("reset")) {
        time = 0.0;
    }

    if (!paused) {
        time += .0167;
    }
}
real t = 0.0;
int map = 0;
while (cow_begin_frame()) {
    gui_slider("t", &t, 0.0, 1.0);
    gui_slider("map", &map, 0, 1, 'j', 'k', true);

    vec3 color = (map == 0) ? color_plasma(t) : color_rainbow_swirl(t);

    eso_begin(globals.Identity, SOUP_QUADS);
    eso_color(color);
    eso_vertex(-1, -1);
    eso_vertex(-1,  1);
    eso_vertex( 1,  1);
    eso_vertex( 1, -1);
    eso_end();
}

🎨 color

monokai is the codebase's unofficial color palette; here are the colors as vec3's

monokai.red
monokai.orange
monokai.yellow
monokai.green
monokai.blue
monokai.purple
monokai.brown
monokai.white
monokai.gray
monokai.black

we also have a few useful functions that return colors

vec3 color_kelly(int i);
vec3 color_plasma(real t);
vec3 color_rainbow_swirl(real t);
  • color_kelly(i) returns the i-th (not counting black or white) kelly color; there are 20 kelly colors: yellow, purple, ...; i is wrapped around if outside the range
  • color_plasma(real t) returns a color from the plasma colormap; t is clamped to the range $[0, 1]$
  • color_rainbow_swirl(real t) returns a color from a silly cyclic rainbow colormap

🍲 soup

triangle-soup, "point-soup" (point cloud), line...soup? the possibilities are truly endless

void soup_draw(
    mat4 PVM,
    int primitive,
    int num_vertices,
    vecX *vertex_positions, // X is 2 (XY) or 3 (XYZ)
    vecY *vertex_colors,    // Y is 3 (RGB) or 4 (RGBA)
    vecY color_if_vertex_colors_is_NULL,
    real size_in_pixels = 0,
    bool use_world_units_instead_of_pixels = false, 
    bool force_draw_on_top = false);
  • mat4 PVM is the transform matrix
  • int primitive can be SOUP_POINTS, SOUP_LINES, SOUP_LINE_STRIP, SOUP_LINE_LOOP, SOUP_TRIANGLES, SOUP_TRIANGLE_FAN, SOUP_TRIANGLE_STRIP, SOUP_OUTLINED_TRIANGLES SOUP_QUADS, SOUP_OUTLINED_QUADS
  • int num_vertices is the number of vertices
  • vecX *vertex_positions are the positions of those vertices
  • vecY *vertex_colors are the colors of those vertices; pass NULL to draw all vertices the same color
  • vecY color_if_vertex_colors_is_NULL is the color used for drawing if vertex_colors is NULL
  • real size_in_pixels is the size of the primitive in pixels; ignored when drawing triangles and quads; pass 0 to use default size
  • bool use_world_units_instead_of_pixels changes the interpretation of size_in_pixels to be world units
  • bool force_draw_on_top is whether you want to force the primitives to be drawn on top of everything else
int num_vertices = 4;
vec2 *foo = (vec2 *) calloc(num_vertices, sizeof(vec2));
int k = 0;
foo[k++] = { -1.0, -1.0 };
foo[k++] = { -1.0,  1.0 };
foo[k++] = {  1.0,  1.0 };
foo[k++] = {  1.0, -1.0 };
soup_draw(PV, SOUP_QUADS, num_vertices, foo, NULL, monokai.red);
free(foo);
vec2 foo[] = { { -1.0, -1.0 }, { -1.0, 1.0 }, { 1.0, 1.0 }, { 1.0, -1.0 } };
soup_draw(PV, SOUP_QUADS, _COUNT_OF(foo), foo, NULL, monokai.red);
StretchyBuffer<vec2> vertex_positions = {};
sbuff_push_back(&vertex_positions, { -1.0, -1.0 });
sbuff_push_back(&vertex_positions, { -1.0,  1.0 });
sbuff_push_back(&vertex_positions, {  1.0,  1.0 });
sbuff_push_back(&vertex_positions, {  1.0, -1.0 });
soup_draw(PV, SOUP_POINTS, vertex_positions.length, vertex_positions.data, NULL, monokai.red);
vec2 s = { 1.0, 1.0 };
soup_draw(PV, SOUP_POINTS, 1, &s, NULL, monokai.red);

πŸ₯« eso

"easy soup"; it's a wrapper around soup_draw(...) that handles memory for you

  • "eso" rhymes with "miso" :)
void eso_begin(mat4 PVM, int primitive, real size_in_pixels = 0, bool use_world_units_instead_of_pixels = false, bool force_draw_on_top = false);
void eso_color(real r, real g, real b, real a = 1.0);
void eso_color(vec3 rgb, real a = 1.0);
void eso_vertex(real x, real y, real z = 0.0);
void eso_vertex(vec2 xy);
void eso_vertex(vec3 xyz);
void eso_end();

note: eso_color(...) and eso_vertex(...) have a multiple (equivalent) signatures; call whichever version is most convenient for your data

  • call eso_begin(...); to start a drawing
  • call eso_color(...); to set the color used by subsequent calls to eso_vertex(...)
  • call eso_vertex(...); to draw a vertex
  • call eso_end(...); to finish the drawing

note: to actually draw something, you must call eso_begin(...); first, followed by at least one call to eso_color(...);, followed by at least one call to eso_vertex(...); followed by a single call to eso_end(...); πŸ™‚πŸ‘

// draw a box (2D)
eso_begin(PV, SOUP_QUADS);
eso_color(1.0, 0.0, 0.0);
eso_vertex(-1.0, -1.0);
eso_vertex(-1.0,  1.0);
eso_vertex( 1.0,  1.0);
eso_vertex( 1.0, -1.0);
eso_end();
// draw some axes :)
vec3 o = { 0.0, 0.0, 0.0 };
vec3 x = { 0.1, 0.0, 0.0 };
vec3 y = { 0.0, 0.1, 0.0 };
vec3 z = { 0.0, 0.0, 0.1 };
eso_begin(PV, SOUP_LINES);
eso_color(monokai.red);
eso_vertex(o);
eso_vertex(o + x);
eso_color(monokai.green);
eso_vertex(o);
eso_vertex(o + y);
eso_color(monokai.blue);
eso_vertex(o);
eso_vertex(o + z);
eso_end();

πŸ‡ mesh

general-purpose function for the very basic drawing of indexed triangle meshes (Phong shading or a single texture; can also do matcap)

void mesh_draw(
    mat4 P,
    mat4 V,
    mat4 M,
    int num_triangles,
    int3 *triangle_indices,
    int num_vertices,
    vec3 *vertex_positions,
    vec3 *vertex_normals,
    vec3 *vertex_colors,
    vec3 color_if_vertex_colors_is_NULL = { 1.0, 1.0, 1.0 },
    vec2 *vertex_texCoords = NULL,
    char *texture_filename = NULL);
  • mat4 P is a projection matrix $\mathbf{P}$
  • mat4 V is a view matrix $\mathbf{V}$; pass $\mathbf{I}$ (i.e. globals.Identity) if doing matcap shading
  • mat4 M is a model matrix $\mathbf{M}$; pass $\mathbf{VM}$ if doing matcap shading
  • int num_triangles is the number of triangles in the indexed triangle mesh
  • int3 *triangle_indices are the indices of those triangles
  • int num_vertices is the number of vertices
  • vec3 *vertex_positions are the positions of those vertices
  • vec3 *vertex_normals are the normals of those vertices; pass NULL to not use lighting
  • vec3 *vertex_colors are the colors of those vertices; pass NULL to draw all vertices the same color
  • vec3 color_if_vertex_colors_is_NULL is the color used for drawing if vertex_colors is NULL
  • vec3 *vertex_texture_coordinates are the texture coordinates of those vertices; pass NULL to not use texture
  • char *texture_filename is the filename of the texture used if vertex_texture_coordinates is not NULL; if using a texture you created in code, pass the name of that texture
mesh_draw(
    P,
    V,
    M,
    num_triangles,
    triangle_indices,
    num_vertices,
    vertex_positions,
    vertex_normals,
    NULL,
    monokai.blue);

πŸ“š library

a couple convenient structs and some builtin meshes (with easy-to-use draw methods)

this is 3D soup

struct Soup3D {
    int primitive;
    int num_vertices;
    vec3 *vertex_positions;
    vec3 *vertex_colors;

    void draw(...);
};

void Soup3D::draw(
    mat4 PVM,
    vec3 color_if_vertex_colors_is_NULL = { 1.0, 0.0, 1.0 },
    real size_in_pixels = 0,
    bool force_draw_on_top = false);

this is a 3D indexed triangle mesh

struct IndexedTriangleMesh3D {
    int num_vertices;
    int num_triangles;
    vec3 *vertex_positions;
    vec3 *vertex_normals;
    vec3 *vertex_colors;
    int3 *triangle_indices;
    vec2 *vertex_texture_coordinates;
    char *texture_filename;

    void draw(...);
};

void IndexedTriangleMesh3D::draw(
    mat4 P,
    mat4 V,
    mat4 M,
    vec3 color_if_vertex_colors_is_NULL = { 1.0, 0.0, 1.0 },
    char *texture_filename_if_texture_filename_is_NULL = NULL);

here are the builtin meshes

// Soup3D
library.soups.axes // has vertex colors!--x-axis is red, y-axis is green, z-axis is blue
library.soups.tet
library.soups.box  // [-1.0, 1.0]^3
library.soups.bunny
library.soups.teapot

// IndexedTriangleMesh3D
library.meshes.triangle
library.meshes.square   // [-1.0, 1.0]^2; has texture coordinates!
library.meshes.box      // [-1.0, 1.0]^3;
library.meshes.cone     // unit radius and height; base is centered at origin (on xz-plane)
library.meshes.cylinder // unit radius and height; base is centered at origin (on xz-plane)
library.meshes.sphere   // unit radius; centered at origin
library.meshes.bunny
library.meshes.teapot

here is some simple usage code

library.soups.axes.draw(PV);
library.meshes.bunny.draw(P, V, globals.Identity, monokai.blue);
library.meshes.bunny.draw(P, globals.Identity, V * M, {}, "codebase/matcap.png");

πŸ–Ό texture

API for creating textures in code (as opposed to loading them from a file) TODO (NEXT VERSION): i, j should be x, y NOT y, x like they are now

struct Texture {
    char *name;
    int width;
    int height;
    int number_of_channels;
    u8 *data;
};

Texture texture_create(char *texture_name, int width, int height, int number_of_channels);
  • texture_create(...) returns a mysterious Texture struct you need to hold onto and pass around
  • texture_name is a unique name for your texture; should be a NULL-terminated string
  • width is the width of the texture in pixels
  • height is the height of the texture in pixels
  • number_of_channels is the number of channels per pixel; 4 for RGBA, 3 for RGB, 1 for R (red)
void texture_set_pixel(Texture *texture, int i, int j, real r, real g = 0.0, real b = 0.0, real a = 1.0);
void texture_set_pixel(Texture *texture, int i, int j, vec3 rgb, real a = 1.0);
void texture_get_pixel(Texture *texture, int i, int j, real *r, real *g = NULL, real *b = NULL, real *a = NULL);
  • texture_set_pixel(texture, i, j, ...); sets the color of the $(i, j)$-th pixel; color values should be between 0.0 and 1.0
  • texture_get_pixel(texture, i, j, ...); gets the color of the $(i, j)$-th pixel; color values will be between 0.0 and 1.0
void texture_sync_to_GPU(Texture *texture);
  • texture_sync_to_GPU(texture); syncs your changes to the texture (typically made with texture_set_pixel(...);) so everything is up to date when you call mesh_draw(..., texture.name);
// read out an entry of a (1 channel) depth buffer
real current_depth;
texture_get_pixel(depth_buffer, i, j, &current_depth);
void eg_texture() {
    Camera2D camera = { 3.0 };
    Texture texture = texture_create("shader toy tribute act", 16, 16, 3);
    bool paused = false;
    real time = 0.0;
    while (cow_begin_frame()) {
        camera_move(&camera);
        mat4 P = camera_get_P(&camera);
        mat4 V = camera_get_V(&camera);

        gui_checkbox("paused", &paused, 'p');

        if (!paused) {
            for (int i = 0; i < texture.height; ++i) {
                real v = real(i) / (texture.height - 1);
                for (int j = 0; j < texture.width; ++j) {
                    real u = real(j) / (texture.width - 1);
                    texture_set_pixel(&texture, i, j, V3(0.5) + 0.5 * V3(cos(time + u), cos(time + v + 2.0), cos(time + u + 4.0)));
                }
            }
            texture_sync_to_GPU(&texture);

            time += 0.0167;
        }
        library.meshes.square.draw(P, V, globals.Identity, {}, texture.name);
    }
}

πŸ”  text

basic billboarded text; thank Sean Barrett for stb_easy_font.h!

void text_draw(
    mat4 PV,
    char *text,
    vecX position_World,
    vecY color = { 1.0, 1.0, 1.0 },
    real font_size_in_pixels = 0,
    vec2 nudge_in_pixels = { 0.0, 0.0 },
    bool force_draw_on_top = false);
  • mat4 PV; is the transform matrix
  • char *text; is a NULL-terminated string
  • vecX *position_World; is the (top left) position in world coordinates
  • vecY color; is the color of the text
  • real font_size_in_pixels; is the font size
  • vec2 nudge_in_pixels; is how much the text will be nudged in screen coordinates
  • bool force_draw_on_top; is whether you want to force the text to be drawn on top of everything else
text_draw(globals.NDC_from_Screen, "<- mouse", globals.mouse_position_Screen); 

πŸ”Š sound

sound and music; thank Randy Gaul for cute_sound.h!

  • does not work on Ubuntu πŸ˜”
  • only supports 16-bit wav files
void sound_attach_to_gui();
void sound_play_sound(char *filename);
void sound_loop_music(char *filename);
void sound_stop_all();
  • sound_attach_to_gui(); adds sound controls to the GUI
  • sound_play_sound(filename); plays a sound once
  • sound_loop_music(filename); loops a music track forever
  • sound_stop_all(); stops playing all sounds and music
void eg_sound() {
    sound_loop_music("codebase/music.wav");
    while (cow_begin_frame()) {
        if (gui_button("play sound.wav", ' ')) {
            sound_play_sound("codebase/sound.wav");
        }
        sound_attach_to_gui();
    }
}

πŸ•Ά shader

shader wrappers that are easy to use (relatively speaking); if you want to do something avant garde, please read learnopengl.com

Shader shader_create(
    char *vertex_shader_source,
    int num_vertex_attributes,
    char *fragment_shader_source,
    char *geometry_shader_source = NULL);
  • returns a mysterious struct you need to hold onto and pass around
  • each source should be a NULL-terminated string; supplying a geometry shader is optional
  • num_vertex_attributes should be the number of layout (location = i) in vecX foo;-lines in vertex_shader_source
void shader_set_uniform(Shader *shader, char *name, T value);
  • specifies the value of uniform variable named name for the shader program corresponding to shader_program_ID
  • the type T of value can be bool, int, real, vec2, vec3, or mat4
  • the type must match (i.e. a vec2 in your C++ code should correspond to a vec2 in your GLSL code); the exceptition is a real in your C++ code should correspond to a float in your GLSL code
void shader_pass_vertex_attribute(Shader *shader, int num_vertices, vecX *vertex_attribute);
  • pass a vertex_attribute for all num_vertices vertices to the shader
  • you need to pass attributes in the same order that they are laid out in your vertex shader (see sample code)
  • the total number of times you call this function per draw must match the num_attributes you supplied when you created the shader
void shader_draw(Shader *shader, int num_triangles, int3 *triangle_indices) ;
  • draw; hopefully, something shows up πŸ™ƒ
  • num_triangles is the number of triangles in the indexed triangle mesh you are drawing
  • triangle_indices is their indices.
void eg_shader() {
    char *vertex_shader_source = R""(
        #version 330 core
        uniform mat4 transform;
        layout (location = 0) in vec3 vertex_position;
        layout (location = 1) in vec3 vertex_normal;
        out vec3 color;
        void main() {
            color = vertex_normal;
            gl_Position = transform * vec4(vertex_position, 1.0);
        }
    )"";

    char *fragment_shader_source = R""(
        #version 330 core
        in vec3 color;
        out vec4 fragColor;
        void main() {
            fragColor = vec4(color, 1.0);
        }
    )"";

    Shader shader = shader_create(vertex_shader_source, 2, fragment_shader_source);

    IndexedTriangleMesh3D mesh = meshlib.mesh_bunny;
    int num_vertices       = mesh.num_vertices;
    vec3 *vertex_positions = mesh.vertex_positions;
    vec3 *vertex_normals   = mesh.vertex_normals;
    int num_triangles      = mesh.num_triangles;
    int3 *triangle_indices = mesh.triangle_indices;

    Camera3D camera = { 5.0 };
    while (cow_begin_frame()) {
        camera_move(&camera);
        shader_set_uniform(&shader, "transform", camera_get_PV(&camera));
        shader_pass_vertex_attribute(&shader, num_vertices, vertex_positions);
        shader_pass_vertex_attribute(&shader, num_vertices, vertex_normals);
        shader_draw(&shader, num_triangles, triangle_indices);
    }
}

🟧 window

some little wrappers around GLFW

vec2 window_get_size();
void window_set_size(vec2 size);
void window_set_position(vec2 position);
void window_set_clear_color(real r, real g, real b, real a = 1.0);
void window_set_clear_color(vec3 rgb, real a = 1.0);
void window_set_floating(bool floating);
void window_set_decorated(bool decorated);
  • the size of the window is in pixels
  • the position of the window is in pixels
  • the window is cleared to the (internally stored) clear color as part of cow_begin_frame()
  • a "floating" window is always above any other windows
  • a "decorated" window has a top ribbon with the window title and buttons to minimize, maximize, and close
void window_pointer_lock();
void window_pointer_unlock();
bool window_pointer_is_locked();
  • window_pointer_lock(); "eats" your mouse cursor (useful for FPS games) until window_point_unlock(); is called

πŸ”΄ recorder

screen capture; thanks Jon Olick for jo_mpeg.h!--and Wladislav Artsimovich for the C port :3

  • press ? for hotkey and tweaks
  • captures are automatically saved to the recordings directory inside the codebase
  • e.g. VLC can play the captures

🐌 snail.cpp

smol and slow linalg library

FAQ

the snail types vec2, vec3, vec4, mat2, mat3, mat4 are all plain old data structs (with some fancy pants operator overloading and things)

therefore, code like this

vec2 a = { 1.0, 2.0 };
vec2 b = a + { 5.0, 3.0 }; // does NOT compile

will trigger compile errors like this

! error C2059: syntax error: '{'
! error C2143: syntax error: missing ';' before...

you must use a "constructor" if you are both creating and using a snail type on one line

vec2 a = { 1.0, 2.0 };
vec2 b = a + V2(5.0, 3.0); // does compile :)

πŸ›‘ if this section didn't make 100% sense, i recommend using V2(...), V3(...), and friends every single time you create a snail vector

// nothing wrong with this :)
vec2 a = V2(1.0, 2.0);
vec2 b = a + V2(5.0, 3.0);

accessors

the entries of a vecX v can be accessed in multiple ways

v.x,  v.y,  v.y,  v.w  // dot accessors
v[0], v[1], v[2], v[3] // square bracket accessors

the entries of a matX M can be accessed like this

M(row, column)

βš” if you #define SNAIL_I_SOLEMNLY_SWEAR_I_AM_UP_TO_NO_GOOD before including snail.cpp you can also access like this

v.r,  v.g,  v.b,  v.a; v.xy; v.xyz; v.data[0], v.data[1], ...
M.data[0], M.data[1], ...

"constructors"

typical

vec2 V2(real x, real y);
vec3 V3(real x, real y, real z);
vec4 V4(real x, real y, real z, real w);
mat2 M2(real xx, real xy, real yx, real yy);
mat3 M3(...);
mat4 M4(...);

note: matrix constructors expect entries in row-major order

go up a dimension

vec3 V3(Vec<2> xy, real z);
vec4 V4(Vec<3> xyz, real w);

all entries are the same

vec2 V2(real s);
vec3 V3(real s);
vec4 V4(real s);

horizontal stack (i.e., specify columns)

mat2 hstack(vec2 col0, vec2 col1);
mat3 hstack(vec3 col0, vec3 col1, vec3 col2);
mat4 hstack(vec4 col0, vec4 col1, vec4 col2, vec4 col3);

homogeneous transform

mat4 M4_Identity();
mat4 M4_Translation(real x, real y, real z = 0.0);
mat4 M4_Translation(vec2 xy);
mat4 M4_Translation(vec3 xyz);
mat4 M4_Scaling(real x, real y, real z = 1.0);
mat4 M4_Scaling(real s);
mat4 M4_Scaling(vec2 xy);
mat4 M4_Scaling(vec3 xyz);
mat4 M4_RotationAboutXAxis(real angle);
mat4 M4_RotationAboutYAxis(real angle);
mat4 M4_RotationAboutZAxis(real angle);
mat4 M4_RotationAxisAngle(vec3 axis, real angle);

special

mat4 M4_xyzo(vec3 x, vec3 y, vec3 z, vec3 o);
$$\begin{bmatrix}\mathbf{x}&\mathbf{y}&\mathbf{z}&\mathbf{o}\\0&0&0&1\end{bmatrix}\$$$

vector addition, subtraction, and scalar multiplication

vec3 a = ...;
vec3 b = ...;
real r = ...;
vec3 c = r * a;
c += b;

matrix-vector multiplication

$\mathbf{b} \leftarrow \mathbf{A} \mathbf{x}$

mat3 A = ...;
vec3 x = ...;
vec3 b = A * x; // NOT the same as x * A

solving a matrix-vector equation

solve $\mathbf{A} \mathbf{x} = \mathbf{b}$ for $\mathbf{x}$

  • $\mathbf{x} \leftarrow \mathbf{A}^{-1} \mathbf{b}$
mat3 A = ...;
vec3 b = ...;
vec3 x = inverse(A) * b;

vector operations

real dot(vecX a, vecX b);
matX outer(vecX a, vecX b);
real cross(vec2 a, vec2 b); // "cross product"
vec3 cross(vec3 a, vec3 b); // actual cross product
  • dot(a, b) returns $\mathbf{a} \cdot \mathbf{b}=\mathbf{a}^T \mathbf{b}$
  • outer(a, b) returns $\mathbf{a} \mathbf{b}^T$
  • cross(a, b) returns $\mathbf{a} \times \mathbf{b}$; for vec2's assumes a $0$ $z$-component and returns the $z$-component of the result
real norm(vecX a);
real squaredNorm(vecX a);
vecX normalized(vecX a);
  • norm(a) returns $|\mathbf{a}|$, i.e. the length of $\mathbf{a}$
  • squaredNorm(a) returns $|\mathbf{a}|^2$
  • normalized(a) returns $\hat{\mathbf{a}} = \mathbf{a} / |\mathbf{a}|$

homogenous transforms

vecX transformPoint(mat4 M, vecX p);
vecX transformVector(mat4 M, vecX v);
vecX transformNormal(mat4 M, vecX n);
  • p can be a vec3 or a vec2; in the vec2 case we temporarily augment { p.x, p.y } into { p.x, p.y, 0.0 }; the same is true of v and n
  • transformPoint(M, p) performs
$$\mathbf{M} \begin{bmatrix}p_x\\p_y\\p_z\\1.0\end{bmatrix}$$

and divides by the $w$-coordinate of the result

  • transformVector(M, v) performs
$$\mathbf{M} \begin{bmatrix}v_x\\v_y\\v_z\\0.0\end{bmatrix}$$
  • transformNormal(M, n) performs
$$\mathbf{M}^{-T} \begin{bmatrix}n_x\\n_y\\n_z\\0.0\end{bmatrix}$$

component-wise operations

vecX cwiseAbs(vecX a);
vecX cwiseMin(vecX a, vecX b);
vecX cwiseMax(vecX a, vecX b);
vecX cwiseProduct(vecX a, vecX b);

matrix functions

matX inverse(matX M);
matX transpose(matX M);
real determinant(matX M);

pretty printing

pprint(vecX a);
pprint(matX M);

int2 and int3

int2 and int3 are similar to vec2 and vec3, e.g.,

int3 foo = { 0, 1, 2 };
foo[1] = 3;

however,

  • they store int's instead of real's
  • they have no fun functions defined for them
  • their "dot accessors" are foo.i, foo.j, foo.k instead of foo.x, foo.y, foo.z