This is buttery-smooth on my actual computer, but because the resolution is high, if I take a screen recording of it the video ends up a little choppy, so apologies for that. I’m going to add a proper video recording feature to it soon (since that’s actually its intended purpose rn) and then I’ll be able to post buttery-smooth videos of it as well. The technical environment is Vulkan + SDL on Linux.
Okay, I got video output working!!
Now I can demonstrate what it’s like to move around in this environment without the video stuttering. Hooray!! (Music is Glenn Gould playing Goldberg Variations 28.)
I’m pleased with the system I came up with for this. While you’re actually playing, the program records frames of video as the information sent to the GPU as opposed to the actual image data for each frame and saves it to a binary file. You can play these files back in realtime using the program, and you can also have the program output frames of video using one. The record files losslessly represent the video frames at a significant compression ratio—compressed with Gzip, the record file for the above fideo is 6.2M, whereas the frames generated from it are 7.1G, for a compression ratio of around 1200. There’s some low-hanging fruit I could go for in the record file format that could make it even smaller, too, but I’m waiting for the whole design of the codebase to settle out a little more before I start worrying about things like that (I did lots of refactoring before implementing this feature too and have some nicer abstractions to work with now).
Anyway, now that I have this, I can use this program for the purpose I had in mind when I started on it, which is finishing a polytopes-themed animated video. I have code for things other than the tesseract, I’m just using that because it makes for a nice test model (hasn’t it been purty too though?). We’ll see some other things shortly.
a precision platforming area i made for a secret side path. think super mario world momentum but everything is slower and floatier. oh and you bounce off walls.

a primary idea behind the game was to make a platformer where you fall more slowly when you hold jump, but sell that / make it more intuitive with a visual metaphor, hence the jetpack. it doesn’t give you enough thrust to overcome gravity, but it does slow your decent.
my sister, who rarely plays games, was pretty quick to pick up the controls so it feels like a success already.
Watching Trigun and i’m like “that 3rd person shooter I kinda don’t work on doesn’t need multiple guns, just one”
this is the truth. i’ve been mulling over making a small-arms fps with one Really Good Pistol as the only weapon. i think more games would benefit from this approach. especially indie stuff. that being said i’d probably froth if a big triple-a company announced that it was making a fully-funded fps with exactly one perfect firearm that they’d spent years meticulously perfecting.
on the last game I worked on I was in charge of the vehicle systems, which basically didn’t exist, and I looked at the team size and budget and was like “we are going to make one vehicle, but it’s going to be very good, because if we have 2-4 that suck it’ll be way worse” and while we didn’t ship the one vehicle that was in the game was pretty fully realized and worked in every scenario and playtest without much issue. I felt like I was underdelivering but in reality it was a very pragmatic move.
I kind of feel like both of these approaches imply games that could be fun. The one-implement approach is maybe relatively-more “action” while the multi-implement approach is maybe relatively-more “action RPG.” ![]()
yeah like how in the new zelda games combat is only really fun if you’re willing to chuck around all the little gewgaws you’ve been picking up along the way right?
Yeah like…I’ve only played BotW but at least in that game, I get the impression that there’s been a certain amount of controversy (as with any Zelda game I guess) as to how much it really ought to be considered “RPG” vs. “action-adventure” or whatever, but I think there’s definitely a sense at least where like, the way you’re continuously having to improvise builds based on what you find to some extent is rather reminiscent of the gameplay of traditional/classic roguelikes. If you had, like, only the Master Sword and no other equipment in BotW, I think the combat design would probably become much more purely about positioning, timing, learning the sword’s moveset frame-by-frame, etc. etc., as opposed to bringing in questions of what synergizes with what or how you could give yourself a kind of “meta” or strategic advantage in combat through actions outside the strict combat system or other things of that sort. Maybe to some extent, it’s a question of designing something closer to a single playstyle that the player learns and adapts to, as opposed to giving the player wide latitude in playstyle and having them find some kind of equilibrium with the game on that basis.
Trying to finish this GBStudio game and…there’s so little documentation online about how core features work…like sprite editing, or how the Shoot Em Up mode actually works. I’m figuring it out but it’s so tedious. Despite this, it’s a neat toolset.
stil tinkering, idk what i’m doing. just playing with areas and textures and painting and making little guys and daydreaming. i guess the good news is i’m a big recycler so even if i end up not using things in the current project it’ll probably end up somewhere


watched one of the roger moore james bonds as spy research which was p funny, i definitely get why they made alan partridge a bondhead bc they’re pretty much the same character. james bond seems like someone who has strong opinions about driving gloves. or like if you mussed up his car at all he’d do the angry dad thing of just silently glaring ahead with pursed lips the entire trip.
Spent a long time ideating about this HPL3 engine custom mod for Amnesia The Bunker project I’m working on, but I think I am actually out of that phase, or at least dipping out of it for a little while, and into a more practical design phase. Everything is still in pen and paper. But I have a scope, a narrative, some arbitrary design goals and some theoretical tools I want to deploy that should all culminate in an all around solid horror themed adventure puzzle game experience. I’ve paired this activity with another one that saw me drawing a dungeon layout for a ttrpg every week, which has been somewhat helpful, but maybe more like a sanity-bringing activity to toil with when it felt like the ideating phase was just too ephemeral to bring the satisfaction of doing “actual” work, which I honestly rely perhaps a good bit too much on to feel like a person. But now I am looking forward to actually having design problems to tackle. My next step is to design the puzzle that is blocking the player from leaving a rough excavation area at the lowest point of the bunker and getting access to other parts of the bunker above. And what I mean here by “puzzle” is the same thing i.e. as finding a locked door in RE and also finding the key that unlocks it while navigating the economy of resources through combat and avoiding enemies. So what I’m working with here is the beast enemy from the game, the resources available to fight or heal, the items required to get through the door and the terrain that you need to get through to reach them. How do I balance all these things to make an exciting and interesting horror puzzle to solve?
I really love these little characters. The Gregory Horror Show influence is shining through. Though the crazy tiling gives the environment this quality that they have entered not a haunted hotel but into the patterns of a Las Vegas casino carpet instead.
I still have a few more features/behaviors I want to add to this before I do the full render but I should probably step away from the computer for a bit and tend to everything else ![]()
I just got synchronization between the sound and graphics working properly like as of this video, so now I feel like I have it “all set up.” Next I want to convert the color to an HSV representation and rotate the hue of the colors based on which octave they’re in, which will illuminate harmonic relationships in the sound.
(This is ultimately towards a new music player for my website.)
Here’s one of the frames in 4K—there’s a fair amount of fun detail in there if you zoom in.
Oh, and I guess I should say also, this is output from a C++ program currently utilizing FFTW, libFLAC, and libpng. You pass it a FLAC file on the command line and it generates frames of a video like this. Here’s the totally unvarnished messy-and-rapidly-changing state of the currently-only C++ source file just for kicks
handle at your own risk etc. etc.
#include "kaiser.hpp"
#include <fftw3.h>
#include <FLAC++/decoder.h>
#define PNG_DEBUG 3
#include <libpng16/png.h>
#include <iostream>
#include <cstdint>
#include <cstddef>
#include <vector>
#include <array>
#include <stdexcept>
#include <sstream>
#define _USE_MATH_DEFINES
#include <cmath>
#include <utility>
#include <complex>
#include <cstring>
#include <iomanip>
#include <fstream>
#include <filesystem>
using std::cerr;
using std::uint32_t;
using std::uint64_t;
using std::int32_t;
using std::size_t;
using std::vector;
using std::array;
using std::runtime_error;
using std::stringstream;
using std::round;
using std::exchange;
using std::complex;
using std::abs;
using std::arg;
using std::memcpy;
using std::floor;
using std::pow;
using std::setw;
using std::setprecision;
using std::ofstream;
using std::filesystem::path;
using std::atan;
using std::round;
constexpr double len = 10.0;
constexpr unsigned time_incr = 400;
constexpr double time_incr_inv = 1.0/time_incr;
constexpr double freq_floor = 20.0;
constexpr double freq_ceil = 17000.0;
constexpr size_t edo = 220;
constexpr double brightness = 30.0;
constexpr png_byte phase_amt = 255;
constexpr png_byte bg_amt = 0;
//constexpr size_t octave_top = 13;
//constexpr size_t hz_pt_cnt = edo * octave_top;
template<typename T, size_t N>
class FFTWArr {
public:
static constexpr size_t bytes_sz() { return sizeof(T) * N; }
FFTWArr()
{
arr = static_cast<T*>(fftw_malloc(bytes_sz()));
}
FFTWArr(const FFTWArr&) =delete;
FFTWArr& operator=(const FFTWArr&) =delete;
FFTWArr(FFTWArr&& o)
: arr {exchange(o.arr, nullptr)}
{}
FFTWArr& operator=(FFTWArr&& o)
{
arr = exchange(o.arr, nullptr);
return *this;
}
~FFTWArr()
{
if (arr) {
fftw_free(arr);
}
}
T& operator[](size_t idx) { return arr[idx]; }
T* data() { return arr; }
constexpr size_t size() { return N; }
array<complex<T>, N/2 + 1> fft()
{
array<complex<T>, N/2 + 1> out;
FFTWArr<T, N> fftw_in;
FFTWArr<fftw_complex, N/2 + 1> fftw_out;
auto plan = fftw_plan_dft_r2c_1d(N, fftw_in.data(), fftw_out.data(), 0);
memcpy(fftw_in.data(), arr, bytes_sz());
fftw_execute(plan);
fftw_destroy_plan(plan);
memcpy(out.data(), fftw_out.data(), fftw_out.bytes_sz());
return out;
}
private:
T* arr = nullptr;
};
class FlacReader : public FLAC::Decoder::File {
public:
FlacReader(std::string fname);
virtual void metadata_callback(const ::FLAC__StreamMetadata* metadata);
virtual ::FLAC__StreamDecoderWriteStatus
write_callback(const ::FLAC__Frame* frame,
const FLAC__int32* const buffer[]);
virtual void error_callback(::FLAC__StreamDecoderErrorStatus status);
template<size_t winsz>
FFTWArr<double, winsz> seek(double pos)
{
long ndx = static_cast<long>(round(pos * sr)) - winsz/2;
FFTWArr<double, winsz> out;
for (vector<double>::size_type i = 0; i < winsz; ++i) {
auto srci = ndx + i;
if (srci > samps.size() - 1 || srci < 0) {
out[i] = 0.0;
} else {
out[i] = samps[srci] * kaiser[i];
//out[i] = samps[srci];
}
}
return out;
}
uint64_t total_samples;
uint32_t sr;
uint32_t channels;
uint32_t bps;
vector<double> samps;
private:
FlacReader(const FlacReader&);
FlacReader& operator=(const FlacReader&);
};
FlacReader::FlacReader(std::string fname)
: FLAC::Decoder::File()
{
FLAC__StreamDecoderInitStatus init_status = init(fname);
if (init_status != FLAC__STREAM_DECODER_INIT_STATUS_OK) {
stringstream msg;
msg << "ERROR: initializing decoder: "
<< FLAC__StreamDecoderInitStatusString[init_status]
<< "\n";
throw runtime_error(msg.str());
}
auto decode_ok = process_until_end_of_stream();
if (!decode_ok) {
stringstream msg;
msg << "ERROR: decoding: "
<< get_state().resolved_as_cstring(*this)
<< "\n";
throw runtime_error(msg.str());
}
}
void FlacReader::metadata_callback(const ::FLAC__StreamMetadata* metadata)
{
if (metadata->type == FLAC__METADATA_TYPE_STREAMINFO) {
total_samples = metadata->data.stream_info.total_samples;
sr = metadata->data.stream_info.sample_rate;
channels = metadata->data.stream_info.channels;
bps = metadata->data.stream_info.bits_per_sample;
}
}
double norm_24bit(int32_t samp)
{
double out;
if (samp > 0) {
out = static_cast<double>(samp) / 8388607.0;
} else {
out = static_cast<double>(samp) / 8388608.0;
}
return out;
}
::FLAC__StreamDecoderWriteStatus
FlacReader::write_callback(const ::FLAC__Frame* frame,
const FLAC__int32* const buffer[])
{
for(size_t i = 0; i < frame->header.blocksize; i++) {
auto left = buffer[0][i];
auto right = buffer[1][i];
auto avg = (norm_24bit(left) + norm_24bit(right)) / 2.0;
samps.push_back(avg);
}
return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
}
void FlacReader::error_callback(::FLAC__StreamDecoderErrorStatus status)
{
cerr << "Got error callback: "
<< FLAC__StreamDecoderErrorStatusString[status]
<< "\n";
}
template<typename T, size_t N>
complex<T> lerp_hz(const array<complex<T>, N>& arr, double hz)
{
if (hz >= N - 1 || hz < 0.0) {
return 0;
}
size_t bottom_ndx = floor(hz);
size_t top_ndx = bottom_ndx + 1;
double dist = hz - bottom_ndx;
auto bottom = arr[bottom_ndx];
auto top = arr[top_ndx];
return bottom + (top - bottom)*dist;
}
struct Vertex {
float x;
float y;
float mag;
float phs;
Vertex() = default;
Vertex(double x, double y, double mag, double phs)
: x {static_cast<float>(x)},
y {static_cast<float>(y)},
mag {static_cast<float>(mag)},
phs {static_cast<float>(phs)}
{};
template <typename T, typename U>
friend std::basic_ostream<T,U> &operator<<(std::basic_ostream<T,U> &o,
Vertex v)
{
o << "(x: " << v.x
<< ", y: " << v.y
<< ", mag: " << v.mag
<< ", phs: " << v.phs
<< ")\n";
return o;
}
template <typename T, typename U>
friend std::basic_ofstream<T,U> &operator<<(std::basic_ofstream<T,U> &o,
Vertex v)
{
o.write(reinterpret_cast<char*>(&v.x), sizeof(v.x));
o.write(reinterpret_cast<char*>(&v.y), sizeof(v.y));
o.write(reinterpret_cast<char*>(&v.mag), sizeof(v.mag));
o.write(reinterpret_cast<char*>(&v.phs), sizeof(v.phs));
return o;
}
};
struct PNGPix {
png_byte red;
png_byte green;
png_byte blue;
//png_byte alpha;
};
class Model {
public:
Model(size_t col_sz)
: col_sz {col_sz}
{}
template<typename T>
void push_back(complex<T> val)
{
auto row_ndx = vert_cnt++ % col_sz;
if (row_ndx == 0) {
verts.push_back(vector<Vertex>(col_sz));
}
verts.back()[row_ndx] = {
row_ndx / static_cast<double>(col_sz),
static_cast<double>(verts.size() - 1) * time_incr_inv,
//abs(val),
abs(val.real()),
arg(val),
};
}
const auto& cols() const { return verts; }
void write(path loc)
{
ofstream outf;
outf.open(loc, std::ios::out | std::ios::binary);
for (auto&& col : cols()) {
for (auto&& v : col) {
outf << v;
}
}
outf.close();
}
template <typename T, typename U>
friend std::basic_ostream<T,U> &operator<<(std::basic_ostream<T,U> &o,
Model m)
{
o << setw(7) << setprecision(7) << std::fixed;
for (auto&& col : m.cols()) {
for (auto&& v : col) {
o << v;
}
}
o << setw(0) << setprecision(0) << std::defaultfloat;
return o;
}
size_t width() const
{
return verts.size();
}
size_t height() const
{
return col_sz;
}
void write_png(const char* fname)
{
FILE* img = fopen(fname, "wb");
png_structp dest = png_create_write_struct(PNG_LIBPNG_VER_STRING,
NULL,
NULL,
NULL);
if (!dest) {
fputs("[write_png] png_create_write_struct failed", stderr);
exit(1);
}
png_infop img_inf = png_create_info_struct(dest);
if (!img_inf) {
fputs("[write_png] png_create_info_struct failed", stderr);
exit(1);
}
png_init_io(dest, img);
if (setjmp(png_jmpbuf(dest))) {
fputs("[write_png] writing png header failed", stderr);
exit(1);
}
const int bit_depth = 8;
const int color_type = PNG_COLOR_TYPE_RGB;
png_set_IHDR(dest,
img_inf,
width(),
height(),
bit_depth,
color_type,
PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT,
PNG_FILTER_TYPE_DEFAULT);
png_write_info(dest, img_inf);
if (setjmp(png_jmpbuf(dest))) {
fputs("[write_png] writing image data failed", stderr);
exit(1);
}
vector<PNGPix> row_cont(width());
for (size_t y = 0; y < height(); ++y) {
for (size_t x = 0; x < width(); ++x) {
auto vert = cols()[x][height() - y];
auto mag_scaled = atan(vert.mag / brightness) / M_PI_2;
auto mag_scaled_i = static_cast<png_byte>(mag_scaled * 255);
PNGPix px;
auto phase_comp = vert.phs * phase_amt * (0.2 + 0.8*mag_scaled) / M_PI;
auto bg_scale = bg_amt * mag_scaled;
px.green = mag_scaled_i;
px.blue = bg_scale;
px.red = bg_scale;
//px.alpha = mag_scaled_i;
//px.alpha = 255;
if (vert.phs > 0) {
px.blue += phase_comp;
} else {
px.red += -phase_comp;
}
//if (!(x % time_incr)) {
// px.green = 255 - px.green;
// px.blue = 255 - px.blue;
// px.red = 255 - px.red;
// //px.alpha = 255;
//}
row_cont[x] = px;
}
png_write_row(dest,
reinterpret_cast<png_const_bytep>(row_cont.data()));
}
if (setjmp(png_jmpbuf(dest))) {
fputs("[write_png] writing png tail failed", stderr);
exit(1);
}
png_write_end(dest, NULL);
fclose(img);
png_destroy_write_struct(&dest, &img_inf);
}
void write_png_frame(const char* fname,
double time,
size_t frame_width)
{
FILE* img = fopen(fname, "wb");
png_structp dest = png_create_write_struct(PNG_LIBPNG_VER_STRING,
NULL,
NULL,
NULL);
if (!dest) {
fputs("[write_png] png_create_write_struct failed", stderr);
exit(1);
}
png_infop img_inf = png_create_info_struct(dest);
if (!img_inf) {
fputs("[write_png] png_create_info_struct failed", stderr);
exit(1);
}
png_init_io(dest, img);
if (setjmp(png_jmpbuf(dest))) {
fputs("[write_png] writing png header failed", stderr);
exit(1);
}
const int bit_depth = 8;
const int color_type = PNG_COLOR_TYPE_RGB;
png_set_IHDR(dest,
img_inf,
frame_width,
height(),
bit_depth,
color_type,
PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT,
PNG_FILTER_TYPE_DEFAULT);
png_write_info(dest, img_inf);
if (setjmp(png_jmpbuf(dest))) {
fputs("[write_png] writing image data failed", stderr);
exit(1);
}
vector<PNGPix> row_cont(frame_width);
for (size_t y = 0; y < height(); ++y) {
for (size_t x = 0; x < frame_width; ++x) {
long col_ndx = static_cast<long>(x)
- (static_cast<double>(frame_width)/2)
+ round(time * time_incr);
if (col_ndx < 0) {
row_cont[x] = {};
continue;
}
auto vert = cols()[col_ndx][height() - y];
auto mag_scaled = atan(vert.mag / brightness) / M_PI_2;
auto mag_scaled_i = static_cast<png_byte>(mag_scaled * 255);
PNGPix px;
auto phase_comp = vert.phs * phase_amt * (0.2 + 0.8*mag_scaled) / M_PI;
auto bg_scale = bg_amt * mag_scaled;
px.green = mag_scaled_i;
px.blue = bg_scale;
px.red = bg_scale;
//px.alpha = mag_scaled_i;
//px.alpha = 255;
if (vert.phs > 0) {
px.blue += phase_comp;
} else {
px.red += -phase_comp;
}
//if (!(x % time_incr)) {
// px.green = 255 - px.green;
// px.blue = 255 - px.blue;
// px.red = 255 - px.red;
// //px.alpha = 255;
//}
row_cont[x] = px;
}
png_write_row(dest,
reinterpret_cast<png_const_bytep>(row_cont.data()));
}
if (setjmp(png_jmpbuf(dest))) {
fputs("[write_png] writing png tail failed", stderr);
exit(1);
}
png_write_end(dest, NULL);
fclose(img);
png_destroy_write_struct(&dest, &img_inf);
}
private:
size_t vert_cnt = 0;
size_t col_sz = 0;
vector<vector<Vertex>> verts;
};
int main(int argc, char* argv[])
{
if (argc != 2) {
cerr << "usage: " << argv[0] << " INFILE.flac" << "\n";
return 1;
}
FlacReader fr(argv[1]);
size_t min_hz_ndx = []{
size_t i = 0;
double val = 0;
while (val < freq_floor) {
val = pow(2, i++/static_cast<double>(edo));
}
return i;
}();
size_t max_hz_ndx = [min_hz_ndx]{
size_t i = min_hz_ndx;
double val = 0;
while (val < freq_ceil) {
val = pow(2, i++/static_cast<double>(edo));
}
return i;
}();
size_t col_sz = max_hz_ndx - min_hz_ndx;
Model model(col_sz);
for (size_t t = 0; t < len * time_incr; ++t) {
auto samps = fr.seek<32768>(t * time_incr_inv);
auto samps_fft = samps.fft();
vector<double> hz_pts;
for (size_t i = 0; i < col_sz; ++i) {
auto val = pow(2, (i + min_hz_ndx)/static_cast<double>(edo));
hz_pts.push_back(val);
}
for (size_t val_i = 0; val_i < hz_pts.size(); ++val_i) {
auto hz_pt = hz_pts[val_i];
auto val = lerp_hz(samps_fft, hz_pt);
model.push_back(val);
}
}
//model.write("spectr.bin");
constexpr double framerate = 30;
size_t max_frame = round(framerate * len);
for (size_t i = 0; i < max_frame; ++i) {
double dist = i / framerate;
stringstream s;
s << "/var/tmp/in_ram/"
<< std::fixed << std::setfill('0') << std::setw(6) << i
<< ".png";
model.write_png_frame(s.str().c_str(), dist, 3840);
}
return 0;
}
The build process also utilizes a short Python script which generates a C++ header file containing a Kaiser window, using SciPy:
from scipy import signal
window = signal.windows.kaiser(32768, beta=60)
head = """#ifndef k54dfaae6f6949e39bdddabe328a9c3f
#define k54dfaae6f6949e39bdddabe328a9c3f
#include <array>
constexpr std::array<double, 32768> kaiser {
"""
tail = """};
#endif
"""
f = open("kaiser.hpp", "w")
f.write(head)
for val in window.flat:
f.write(f' {val},\n')
f.write(tail)
f.close()
and here’s the simple little scratch Makefile as well just for the sake of completeness:
flacfft: main.cpp kaiser.hpp
g++ $(CPPFLAGS) -lfftw3 -lFLAC++ -lFLAC -I/usr/include/libpng16 -lpng16 -o $@ $^
kaiser.hpp: kaiser.py
python $^
.PHONY: clean
clean:
rm -rf flacfft kaiser.hpp
The resulting binary is 300k uncompressed
even with the code so messy as it is
To build and run (after putting the above files in a directory together and cding into it):
$ CPPFLAGS="-O2" make
$ ./flacfft birds_meowing.flac
I just realized, right now because it’s wantonly coupled to my environment it outputs frames to the hardcoded path /var/tmp/in_ram/, so if that’s not a valid path on your environment you’ll have to change it in the C++ code (it’s near the bottom
)
Hit “post” on that little GBStudio game I was working on










