Download unify/tools/assetgen.cpp from Snapkitty/repoverse: direct link, hf CLI and curl.
- Browser
- Download file 10.6 kB
-
https://huggingface.co/Snapkitty/repoverse/resolve/main/unify/tools/assetgen.cpp
- Command line
-
hf download hf://Snapkitty/repoverse/unify/tools/assetgen.cpp
-
curl -L -o assetgen.cpp https://huggingface.co/Snapkitty/repoverse/resolve/main/unify/tools/assetgen.cpp
10.6 kB
| // Generates the demo game's binary assets (original pixel art, SFX, streamed music) so | |
| // the repository ships no third-party media. Deterministic: same output every run. | |
| // unify_assetgen <assets-dir> | |
| using namespace unify; | |
| static void px(Image& im, int x, int y, Color c) { | |
| if (x < 0 || y < 0 || x >= im.width || y >= im.height) return; | |
| uint8_t* p = im.at(x, y); | |
| p[0] = c.r; p[1] = c.g; p[2] = c.b; p[3] = c.a; | |
| } | |
| static void rect(Image& im, int x, int y, int w, int h, Color c) { for (int j = 0; j < h; j++) for (int i = 0; i < w; i++) px(im, x + i, y + j, c); } | |
| static void disc(Image& im, float cx, float cy, float r, Color c) { | |
| for (int y = 0; y < im.height; y++) for (int x = 0; x < im.width; x++) | |
| if ((x + 0.5f - cx) * (x + 0.5f - cx) + (y + 0.5f - cy) * (y + 0.5f - cy) <= r * r) px(im, x, y, c); | |
| } | |
| // Player: 16x24, a small robot courier. `pose`: 0/1 idle breathe, 2..5 run cycle, 6 jump, 7 fall. | |
| static Image player(int pose) { | |
| Image im(16, 24); | |
| const Color body{64, 156, 255, 255}, dark{30, 70, 150, 255}, visor{240, 250, 255, 255}, eye{20, 20, 40, 255}, boot{40, 40, 52, 255}; | |
| int bob = pose == 1 ? 1 : 0; | |
| rect(im, 3, 3 + bob, 10, 9, body); // head | |
| rect(im, 4, 5 + bob, 8, 4, visor); // visor | |
| rect(im, 6, 6 + bob, 1, 2, eye); | |
| rect(im, 9, 6 + bob, 1, 2, eye); | |
| rect(im, 7, 1 + bob, 2, 2, dark); // antenna | |
| rect(im, 4, 12 + bob, 8, 7, body); // torso | |
| rect(im, 5, 14 + bob, 6, 1, dark); | |
| int lf = 0, rf = 0; // leg offsets | |
| if (pose >= 2 && pose <= 5) { static const int cyc[4][2] = {{-2, 2}, {0, 0}, {2, -2}, {0, 0}}; lf = cyc[pose - 2][0]; rf = cyc[pose - 2][1]; } | |
| if (pose == 6) { lf = -1; rf = 1; } | |
| rect(im, 5 + lf, 19, 2, 4, dark); | |
| rect(im, 9 + rf, 19, 2, 4, dark); | |
| rect(im, 4 + lf, 22, 3, 2, boot); | |
| rect(im, 9 + rf, 22, 3, 2, boot); | |
| if (pose == 6) { rect(im, 1, 9, 3, 2, body); rect(im, 12, 9, 3, 2, body); } // arms up | |
| else if (pose == 7) { rect(im, 1, 13, 3, 2, body); rect(im, 12, 13, 3, 2, body); } // arms out | |
| else { rect(im, 2, 13 + bob, 2, 5, body); rect(im, 12, 13 + bob, 2, 5, body); } | |
| return im; | |
| } | |
| static Image tile(bool top) { | |
| Image im(16, 16, {120, 84, 52, 255}); | |
| for (int i = 0; i < 16; i++) for (int j = 0; j < 16; j++) if (((i * 7 + j * 13) % 11) == 0) px(im, i, j, {98, 66, 40, 255}); | |
| if (top) { rect(im, 0, 0, 16, 4, {86, 190, 90, 255}); for (int i = 0; i < 16; i += 3) px(im, i, 4, {86, 190, 90, 255}); rect(im, 0, 0, 16, 1, {140, 230, 130, 255}); } | |
| return im; | |
| } | |
| static Image crate() { | |
| Image im(16, 16, {176, 122, 60, 255}); | |
| rect(im, 0, 0, 16, 2, {120, 80, 36, 255}); rect(im, 0, 14, 16, 2, {120, 80, 36, 255}); | |
| rect(im, 0, 0, 2, 16, {120, 80, 36, 255}); rect(im, 14, 0, 2, 16, {120, 80, 36, 255}); | |
| for (int i = 2; i < 14; i++) { px(im, i, i, {120, 80, 36, 255}); px(im, i, 15 - i, {120, 80, 36, 255}); } | |
| return im; | |
| } | |
| static Image coin(int f) { | |
| Image im(12, 12); | |
| float w = std::fabs(std::cos(f * 3.14159f / 4)) * 5 + 1; | |
| for (int y = 0; y < 12; y++) for (int x = 0; x < 12; x++) { | |
| float dx = (x + 0.5f - 6) / w, dy = (y + 0.5f - 6) / 5.5f; | |
| if (dx * dx + dy * dy <= 1) px(im, x, y, dx * dx + dy * dy < 0.4f ? Color{255, 236, 120, 255} : Color{232, 176, 40, 255}); | |
| } | |
| return im; | |
| } | |
| static Image ball() { Image im(16, 16); disc(im, 8, 8, 7.5f, {230, 70, 90, 255}); disc(im, 6, 5, 2.5f, {255, 180, 190, 255}); return im; } | |
| static Image spark() { Image im(4, 4); rect(im, 1, 0, 2, 4, {255, 250, 200, 255}); rect(im, 0, 1, 4, 2, {255, 250, 200, 255}); return im; } | |
| static Image flag(int f) { | |
| Image im(16, 32); | |
| rect(im, 2, 0, 2, 32, {220, 220, 230, 255}); | |
| for (int y = 0; y < 10; y++) for (int x = 0; x < 10; x++) { | |
| int wave = int(std::round(std::sin((x + f * 2) * 0.8) * 1.2)); | |
| px(im, 4 + x, 2 + y + wave, (x / 2 + y / 2) % 2 ? Color{40, 40, 40, 255} : Color{250, 250, 250, 255}); | |
| } | |
| return im; | |
| } | |
| static Image platform() { | |
| Image im(48, 12, {90, 100, 128, 255}); | |
| rect(im, 0, 0, 48, 2, {150, 170, 210, 255}); | |
| for (int x = 2; x < 48; x += 8) rect(im, x, 5, 4, 3, {60, 66, 90, 255}); | |
| return im; | |
| } | |
| static Image sky() { | |
| Image im(16, 64); | |
| for (int y = 0; y < 64; y++) { float t = y / 63.0f; rect(im, 0, y, 16, 1, {uint8_t(40 + 50 * t), uint8_t(60 + 90 * t), uint8_t(120 + 100 * t), 255}); } | |
| return im; | |
| } | |
| static Image cloud() { | |
| Image im(32, 16); | |
| disc(im, 9, 10, 6, {245, 248, 255, 255}); disc(im, 17, 7, 7, {245, 248, 255, 255}); disc(im, 24, 10, 6, {245, 248, 255, 255}); | |
| rect(im, 8, 10, 18, 6, {245, 248, 255, 255}); | |
| return im; | |
| } | |
| // ---------------------------------------------------------------------------------- audio | |
| static std::vector<int16_t> render_sfx(float seconds, float (*fn)(float t, float dur)) { | |
| int n = int(seconds * 44100); | |
| std::vector<int16_t> pcm(size_t(n) * 2); | |
| for (int i = 0; i < n; i++) { | |
| float v = fn(i / 44100.0f, seconds); | |
| int16_t s = int16_t(std::lround(std::max(-1.0f, std::min(1.0f, v)) * 20000)); | |
| pcm[size_t(i) * 2] = pcm[size_t(i) * 2 + 1] = s; | |
| } | |
| return pcm; | |
| } | |
| static float square(float phase) { return std::fmod(phase, 1.0f) < 0.5f ? 1.0f : -1.0f; } | |
| static std::vector<int16_t> render_music(int* loop_start, int* loop_end) { | |
| // 8 bars of a looping chiptune at 120 BPM (16 s). Loop points skip the 2-bar intro. | |
| const float bpm = 120, beat = 60 / bpm; | |
| const int bars = 10, total = int(bars * 4 * beat * 44100); | |
| std::vector<float> l(size_t(total), 0), r(size_t(total), 0); | |
| static const int bass[8] = {45, 45, 41, 41, 43, 43, 40, 40}; | |
| static const int melody[32] = {69, 72, 76, 72, 74, 72, 69, 67, 65, 69, 72, 69, 67, 71, 74, 71, | |
| 69, 72, 76, 79, 77, 76, 74, 72, 71, 72, 74, 76, 72, 71, 69, -1}; | |
| auto note_freq = [](int n) { return 440.0f * std::pow(2.0f, (n - 69) / 12.0f); }; | |
| for (int i = 0; i < total; i++) { | |
| float t = i / 44100.0f; | |
| int step = int(t / (beat / 2)); // eighth notes | |
| int bar = int(t / (4 * beat)); | |
| float in_step = std::fmod(t, beat / 2) / (beat / 2); | |
| float env = std::exp(-in_step * 3); | |
| float b = square(t * note_freq(bass[(bar * 2 + (step % 8) / 4) % 8] - 12)) * 0.18f * (0.6f + 0.4f * env); | |
| float m = 0; | |
| if (bar >= 2) { | |
| int n = melody[step % 32]; | |
| if (n > 0) m = square(t * note_freq(n)) * 0.12f * env + square(t * note_freq(n) * 1.005f) * 0.05f * env; | |
| } | |
| float hat = (step % 2 == 1 && in_step < 0.08f) ? (float((i * 1103515245u + 12345u) >> 16 & 0xFF) / 128.0f - 1) * 0.06f * (1 - in_step / 0.08f) : 0; | |
| l[size_t(i)] = b + m * 0.8f + hat; | |
| r[size_t(i)] = b + m + hat * 0.7f; | |
| } | |
| std::vector<int16_t> pcm(size_t(total) * 2); | |
| for (int i = 0; i < total; i++) { | |
| pcm[size_t(i) * 2] = int16_t(std::lround(std::max(-1.0f, std::min(1.0f, l[size_t(i)])) * 26000)); | |
| pcm[size_t(i) * 2 + 1] = int16_t(std::lround(std::max(-1.0f, std::min(1.0f, r[size_t(i)])) * 26000)); | |
| } | |
| *loop_start = int(2 * 4 * beat * 44100); | |
| *loop_end = total; | |
| return pcm; | |
| } | |
| int main(int argc, char** argv) { | |
| if (argc < 2) { std::fprintf(stderr, "usage: unify_assetgen <assets-dir>\n"); return 2; } | |
| std::string root = argv[1]; | |
| for (const char* d : {"textures", "audio", "music", "saves"}) std::filesystem::create_directories(root + "/" + d); | |
| std::vector<std::pair<std::string, Image>> sprites; | |
| const char* poses[8] = {"player_idle_0", "player_idle_1", "player_run_0", "player_run_1", "player_run_2", "player_run_3", "player_jump", "player_fall"}; | |
| for (int i = 0; i < 8; i++) sprites.push_back({poses[i], player(i)}); | |
| sprites.push_back({"tile_top", tile(true)}); | |
| sprites.push_back({"tile", tile(false)}); | |
| sprites.push_back({"crate", crate()}); | |
| for (int i = 0; i < 4; i++) sprites.push_back({"coin_" + std::to_string(i), coin(i)}); | |
| sprites.push_back({"ball", ball()}); | |
| sprites.push_back({"spark", spark()}); | |
| sprites.push_back({"flag_0", flag(0)}); | |
| sprites.push_back({"flag_1", flag(1)}); | |
| sprites.push_back({"platform", platform()}); | |
| sprites.push_back({"sky", sky()}); | |
| sprites.push_back({"cloud", cloud()}); | |
| std::vector<std::pair<std::string, const Image*>> items; | |
| for (auto& s : sprites) items.push_back({s.first, &s.second}); | |
| Image atlas_img; | |
| Atlas atlas; | |
| if (!pack_atlas(items, 128, atlas_img, atlas)) { std::fprintf(stderr, "atlas too small\n"); return 1; } | |
| auto qoi = encode_qoi(atlas_img); | |
| FILE* f = std::fopen((root + "/textures/sprites.qoi").c_str(), "wb"); | |
| std::fwrite(qoi.data(), 1, qoi.size(), f); | |
| std::fclose(f); | |
| f = std::fopen((root + "/textures/sprites.atlas").c_str(), "w"); | |
| for (size_t i = 1; i < atlas.regions.size(); i++) { | |
| const auto& r = atlas.regions[i]; | |
| std::fprintf(f, "%s %d %d %d %d\n", r.name.c_str(), r.rect.x, r.rect.y, r.rect.w, r.rect.h); | |
| } | |
| std::fclose(f); | |
| write_wav(root + "/audio/jump.wav", render_sfx(0.18f, [](float t, float d) { return square(t * (300 + 900 * t / d)) * 0.5f * (1 - t / d); }), 2, 44100); | |
| write_wav(root + "/audio/coin.wav", render_sfx(0.25f, [](float t, float d) { return square(t * (t < 0.07f ? 988.0f : 1319.0f)) * 0.4f * (1 - t / d); }), 2, 44100); | |
| write_wav(root + "/audio/land.wav", render_sfx(0.12f, [](float t, float d) { | |
| float noise = float((uint32_t(t * 44100) * 2654435761u) >> 20 & 0xFFF) / 2048.0f - 1; | |
| return noise * 0.35f * (1 - t / d) + std::sin(t * 2 * 3.14159f * 90) * 0.4f * (1 - t / d); }), 2, 44100); | |
| write_wav(root + "/audio/bounce.wav", render_sfx(0.15f, [](float t, float d) { return std::sin(t * 2 * 3.14159f * (220 + 440 * (1 - t / d))) * 0.6f * (1 - t / d); }), 2, 44100); | |
| write_wav(root + "/audio/win.wav", render_sfx(0.9f, [](float t, float d) { | |
| static const float notes[] = {523.25f, 659.25f, 783.99f, 1046.5f}; | |
| int i = std::min(3, int(t / 0.15f)); | |
| return square(t * notes[i]) * 0.35f * (i == 3 ? 1 - (t - 0.45f) / (d - 0.45f) : 1.0f); }), 2, 44100); | |
| int ls, le; | |
| auto music = render_music(&ls, &le); | |
| write_wav(root + "/music/theme.wav", music, 2, 44100, ls, le); | |
| std::printf("assets written to %s (%zu sprites, atlas %dx%d, music %.1fs)\n", root.c_str(), sprites.size(), atlas_img.width, atlas_img.height, music.size() / 2 / 44100.0); | |
| return 0; | |
| } | |