summaryrefslogtreecommitdiff
path: root/ffmpeg-shadertoy
diff options
context:
space:
mode:
authorHunter Kvalevog <hunter@kvog.sh>2025-09-18 22:47:52 -0500
committerHunter Kvalevog <hunter@kvog.sh>2025-09-18 23:40:09 -0500
commitdaa1259c15f522dfa7ece15e3f9d09d437d4be18 (patch)
tree7edc6ad67a55c4dbf4311d6255e3d196c51c3252 /ffmpeg-shadertoy
parent172718cc6e0dcae569a94769b5444bca55e4d20c (diff)
ffmpeg-shadertoy
Diffstat (limited to 'ffmpeg-shadertoy')
-rw-r--r--ffmpeg-shadertoy/CMakeLists.txt9
-rw-r--r--ffmpeg-shadertoy/fs1.glsl9
-rw-r--r--ffmpeg-shadertoy/fs2.glsl11
-rw-r--r--ffmpeg-shadertoy/main.c441
-rw-r--r--ffmpeg-shadertoy/vs.glsl9
5 files changed, 479 insertions, 0 deletions
diff --git a/ffmpeg-shadertoy/CMakeLists.txt b/ffmpeg-shadertoy/CMakeLists.txt
new file mode 100644
index 0000000..9848606
--- /dev/null
+++ b/ffmpeg-shadertoy/CMakeLists.txt
@@ -0,0 +1,9 @@
+cmake_minimum_required(VERSION 3.15 FATAL_ERROR)
+project(ffmpeg-shadertoy C)
+
+set(DEMO_NEEDS_FFMPEG TRUE)
+set(DEMO_NEEDS_SDL3 TRUE)
+include("${CMAKE_CURRENT_LIST_DIR}/../common/c_cpp/CMakeLists.txt")
+
+add_executable(ffmpeg-shadertoy main.c)
+target_link_libraries(ffmpeg-shadertoy PRIVATE common) \ No newline at end of file
diff --git a/ffmpeg-shadertoy/fs1.glsl b/ffmpeg-shadertoy/fs1.glsl
new file mode 100644
index 0000000..837e270
--- /dev/null
+++ b/ffmpeg-shadertoy/fs1.glsl
@@ -0,0 +1,9 @@
+#version 330 core
+
+// https://www.shadertoy.com/howto#q1
+
+uniform vec4 iMouse;
+uniform vec3 iResolution;
+uniform float iTime;
+
+// keep the line break below
diff --git a/ffmpeg-shadertoy/fs2.glsl b/ffmpeg-shadertoy/fs2.glsl
new file mode 100644
index 0000000..ebdc9ec
--- /dev/null
+++ b/ffmpeg-shadertoy/fs2.glsl
@@ -0,0 +1,11 @@
+
+// keep the line break above
+
+out vec4 f_color;
+
+void main()
+{
+ vec4 color;
+ mainImage(color, gl_FragCoord.xy);
+ f_color = color;
+}
diff --git a/ffmpeg-shadertoy/main.c b/ffmpeg-shadertoy/main.c
new file mode 100644
index 0000000..015b93c
--- /dev/null
+++ b/ffmpeg-shadertoy/main.c
@@ -0,0 +1,441 @@
+// ShaderToy video renderer
+//
+// SPDX-License-Identifier: GPL-3.0
+//
+// Usage: ffmpeg-shadertoy -w <width> -h <height> -t <seconds> -fps <fps> -crf <crf> <input.glsl> <output.mp4>
+//
+// - No texture support
+//
+// ref: https://code.ffmpeg.org/FFmpeg/FFmpeg/src/branch/master/doc/examples/mux.c
+// ref: https://code.ffmpeg.org/FFmpeg/FFmpeg/src/branch/master/doc/examples/encode_video.c
+//
+// Changelog:
+// - 9/18/25: Initial release
+//
+
+#define GLAD_GL_IMPLEMENTATION
+#include "../common/c_cpp/thirdparty/glad33/glad33.h"
+
+#include <libavcodec/avcodec.h>
+#include <libavformat/avformat.h>
+#include <libavutil/opt.h>
+#include <libswscale/swscale.h>
+
+#include <SDL3/SDL.h>
+#include <SDL3/SDL_main.h>
+#include <SDL3/SDL_opengl.h>
+
+// Helper: Report errors via glGetError() after every OpenGL function call.
+// macOS does not support glDebugMessageCallback
+static void assert_gl_error(GLenum error, const char* expr, int line)
+{
+ if (error != GL_NO_ERROR) {
+ const char* error_string;
+ switch (error) {
+#define BIND_ERROR(name) case name: { error_string = #name; }; break;
+ BIND_ERROR(GL_INVALID_ENUM);
+ BIND_ERROR(GL_INVALID_VALUE);
+ BIND_ERROR(GL_INVALID_OPERATION);
+ BIND_ERROR(GL_INVALID_FRAMEBUFFER_OPERATION);
+ BIND_ERROR(GL_OUT_OF_MEMORY);
+#undef BIND_ERROR
+ default: SDL_assert(0 && "Invalid GL_ERROR enum");
+ }
+ SDL_Log("[Line #%u] %s caused %s\n", line, expr, error_string);
+ }
+}
+#define GL(expr) \
+ expr; \
+ for (GLenum _glcode; (_glcode = glGetError()) != GL_NO_ERROR; ) { \
+ assert_gl_error(_glcode, #expr, __LINE__); \
+ }
+
+int main(int argc, char* argv[])
+{
+ int a_w = 1280;
+ int a_h = 720;
+ int a_t = 10;
+ int a_fps = 30;
+ int a_crf = 20;
+ const char* a_src = NULL;
+ const char* a_dst = NULL;
+ for (int i = 1; i < argc; ++i) {
+ const char* arg = argv[i];
+ if (!SDL_strcasecmp(arg, "-w") && i + 1 < argc) {
+ a_w = SDL_atoi(argv[i + 1]);
+ i += 1;
+ }
+ else if (!SDL_strcasecmp(arg, "-h") && i + 1 < argc) {
+ a_h = SDL_atoi(argv[i + 1]);
+ i += 1;
+ }
+ else if (!SDL_strcasecmp(arg, "-t") && i + 1 < argc) {
+ a_t = SDL_atoi(argv[i + 1]);
+ i += 1;
+ }
+ else if (!SDL_strcasecmp(arg, "-fps") && i + 1 < argc) {
+ a_fps = SDL_atoi(argv[i + 1]);
+ i += 1;
+ }
+ else if (!SDL_strcasecmp(arg, "-crf") && i + 1 < argc) {
+ a_crf = SDL_atoi(argv[i + 1]);
+ i += 1;
+ }
+ else {
+ printf("a=%s\n", arg);
+ if (!a_src) {
+ a_src = argv[i];
+ }
+ else if (!a_dst) {
+ a_dst = argv[i];
+ }
+ else {
+ fprintf(stderr, "Unknown argument %s\n", arg);
+ return EXIT_FAILURE;
+ }
+ }
+ }
+
+ if (!a_src) {
+ fprintf(stderr, "No input file specified\n");
+ return EXIT_FAILURE;
+ }
+
+ if (!a_dst) {
+ fprintf(stderr, "No output file specified\n");
+ return EXIT_FAILURE;
+ }
+
+ printf("Rendering %s w=%d h=%d t=%d fps=%d to %s\n", a_src, a_w, a_h, a_t, a_fps, a_dst);
+
+ int ret = 0;
+
+ AVFormatContext* avfc = NULL;
+ if ((ret = avformat_alloc_output_context2(&avfc, NULL, NULL, a_dst)) < 0) {
+ fprintf(stderr, "Failed to allocate output context: %s\n", av_err2str(ret));
+ return EXIT_FAILURE;
+ }
+
+ // Always encode H.264 mp4, no matter what
+ const AVCodec* codec = avcodec_find_encoder(AV_CODEC_ID_H264);
+ if (!codec) {
+ fprintf(stderr, "No H.264 encoder is available\n");
+ return EXIT_FAILURE;
+ }
+
+ AVStream* avs = avformat_new_stream(avfc, codec);
+ if (!avs) {
+ fprintf(stderr, "Failed to create video stream\n");
+ return EXIT_FAILURE;
+ }
+
+ AVCodecContext* avcc = avcodec_alloc_context3(codec);
+ if (!avcc) {
+ fprintf(stderr, "Failed to allocate codec context\n");
+ return EXIT_FAILURE;
+ }
+
+ avs->time_base.num = 1;
+ avs->time_base.den = a_fps;
+
+ avcc->codec_id = codec->id;
+ avcc->width = a_w;
+ avcc->height = a_h;
+ avcc->time_base = avs->time_base;
+ avcc->gop_size = 2 * a_fps;
+ avcc->max_b_frames = 2;
+ avcc->pix_fmt = AV_PIX_FMT_YUV420P;
+
+ char crf_str[32];
+ snprintf(crf_str, sizeof(crf_str), "%d", a_crf);
+
+ av_opt_set(avcc->priv_data, "preset", "fast", 0);
+ av_opt_set(avcc->priv_data, "tune", "animation", 0);
+ av_opt_set(avcc->priv_data, "crf", crf_str, 0);
+
+ if (avfc->oformat->flags & AVFMT_GLOBALHEADER) {
+ avcc->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
+ }
+
+ if ((ret = avcodec_open2(avcc, codec, NULL)) < 0) {
+ fprintf(stderr, "Failed to open encoder: %s\n", av_err2str(ret));
+ return EXIT_FAILURE;
+ }
+
+ if ((ret = avcodec_parameters_from_context(avs->codecpar, avcc)) < 0) {
+ fprintf(stderr, "Failed to set stream codec parameters: %s\n", av_err2str(ret));
+ return EXIT_FAILURE;
+ }
+
+ if ((ret = avio_open(&avfc->pb, a_dst, AVIO_FLAG_WRITE)) < 0) {
+ fprintf(stderr, "Failed to open %s: %s\n", a_dst, av_err2str(ret));
+ return EXIT_FAILURE;
+ }
+
+ if ((ret = avformat_write_header(avfc, NULL)) < 0) {
+ fprintf(stderr, "Failed to write file header: %s\n", av_err2str(ret));
+ return EXIT_FAILURE;
+ }
+
+ AVPacket* pkt = av_packet_alloc();
+ if (!pkt) {
+ fprintf(stderr, "Failed to allocate packet\n");
+ return EXIT_FAILURE;
+ }
+
+ AVFrame* frame_src = av_frame_alloc();
+ AVFrame* frame_dst = av_frame_alloc();
+ if (!frame_src || !frame_dst) {
+ fprintf(stderr, "Failed to allocate frame\n");
+ return EXIT_FAILURE;
+ }
+
+ frame_src->format = AV_PIX_FMT_RGBA;
+ frame_src->width = a_w;
+ frame_src->height = a_h;
+ if ((ret = av_frame_get_buffer(frame_src, 32)) < 0) {
+ fprintf(stderr, "Failed to allocate src frame buffer: %s\n", av_err2str(ret));
+ return EXIT_FAILURE;
+ }
+ if ((ret = av_frame_make_writable(frame_src)) < 0) {
+ fprintf(stderr, "Failed to make src frame writable: %s\n", av_err2str(ret));
+ return EXIT_FAILURE;
+ }
+
+ frame_dst->format = AV_PIX_FMT_YUV420P;
+ frame_dst->width = a_w;
+ frame_dst->height = a_h;
+ if ((ret = av_frame_get_buffer(frame_dst, 32)) < 0) {
+ fprintf(stderr, "Failed to allocate dst frame buffer: %s\n", av_err2str(ret));
+ return EXIT_FAILURE;
+ }
+ if ((ret = av_frame_make_writable(frame_dst)) < 0) {
+ fprintf(stderr, "Failed to make dst frame writable: %s\n", av_err2str(ret));
+ return EXIT_FAILURE;
+ }
+
+ struct SwsContext* sws = sws_getContext(frame_src->width, frame_src->height, frame_src->format,
+ frame_src->width, frame_src->height, frame_dst->format,
+ 0, NULL, NULL, NULL);
+ if (!sws) {
+ fprintf(stderr, "Failed to get libswscale context\n");
+ return EXIT_FAILURE;
+ }
+
+ if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS)) {
+ fprintf(stderr, "Failed to initialize SDL: %s\n", SDL_GetError());
+ return EXIT_FAILURE;
+ }
+
+ SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
+ SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
+ SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
+ SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1);
+ SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 4);
+
+ SDL_Window* wnd = SDL_CreateWindow("ffmpeg-shadertoy", a_w, a_h, SDL_WINDOW_OPENGL);
+ if (!wnd) {
+ fprintf(stderr, "Failed to create window: %s\n", SDL_GetError());
+ return EXIT_FAILURE;
+ }
+
+ if (!SDL_GL_CreateContext(wnd)) {
+ fprintf(stderr, "Failed to create OpenGL context: %s\n", SDL_GetError());
+ return EXIT_FAILURE;
+ }
+ SDL_GL_SetSwapInterval(1);
+
+ if (!gladLoadGL(SDL_GL_GetProcAddress)) {
+ fprintf(stderr, "Failed to load OpenGL functions\n");
+ return EXIT_FAILURE;
+ }
+
+ const float vdata[] = {
+ -1.0f, 1.0f, -1.0f, 0.0f, 1.0f,
+ 1.0f, 1.0f, -1.0f, 1.0f, 1.0f,
+ 1.0f, -1.0f, -1.0f, 1.0f, 0.0f,
+ -1.0f, -1.0f, -1.0f, 0.0f, 0.0f,
+ };
+
+ const uint16_t idata[] = {
+ 0, 1, 2,
+ 0, 2, 3,
+ };
+
+ GLuint vao;
+ GL(glGenVertexArrays(1, &vao))
+ GL(glBindVertexArray(vao));
+
+ GLuint vbo;
+ GL(glGenBuffers(1, &vbo));
+ GL(glBindBuffer(GL_ARRAY_BUFFER, vbo));
+ GL(glBufferData(GL_ARRAY_BUFFER, sizeof(vdata), vdata, GL_STATIC_DRAW));
+
+ GLuint ibo;
+ GL(glGenBuffers(1, &ibo));
+ GL(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo));
+ GL(glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(idata), idata, GL_STATIC_DRAW));
+
+ GL(glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 5, (void*)(sizeof(float) * 0)));
+ GL(glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 5, (void*)(sizeof(float) * 3)));
+ GL(glEnableVertexAttribArray(0));
+ GL(glEnableVertexAttribArray(1));
+
+ const char* vs_src = SDL_LoadFile("vs.glsl", NULL);
+ if (!vs_src) {
+ fprintf(stderr, "Failed to load vertex shader: %s\n", SDL_GetError());
+ return EXIT_FAILURE;
+ }
+
+ GLuint vs = GL(glCreateShader(GL_VERTEX_SHADER));
+ GL(glShaderSource(vs, 1, &vs_src, NULL));
+ GL(glCompileShader(vs));
+ GLint vs_status = 0;
+ GL(glGetShaderiv(vs, GL_COMPILE_STATUS, &vs_status));
+ if (vs_status != GL_TRUE) {
+ char error[1024] = { 0 };
+ GL(glGetShaderInfoLog(vs, sizeof(error), NULL, error));
+ SDL_Log("Error compiling vertex shader: %s\n", error);
+ return EXIT_FAILURE;
+ }
+
+ const char* fs_src1 = SDL_LoadFile("fs1.glsl", NULL);
+ if (!fs_src1) {
+ fprintf(stderr, "Failed to load fragment shader: %s\n", SDL_GetError());
+ return EXIT_FAILURE;
+ }
+
+ const char* fs_src2 = SDL_LoadFile(a_src, NULL);
+ if (!fs_src2) {
+ fprintf(stderr, "Failed to load source shader: %s\n", SDL_GetError());
+ return EXIT_FAILURE;
+ }
+
+ const char* fs_src3 = SDL_LoadFile("fs2.glsl", NULL);
+ if (!fs_src3) {
+ fprintf(stderr, "Failed to load fragment shader: %s\n", SDL_GetError());
+ return EXIT_FAILURE;
+ }
+
+ const char* fs_src[] = {
+ fs_src1,
+ fs_src2,
+ fs_src3,
+ };
+
+ GLuint fs = GL(glCreateShader(GL_FRAGMENT_SHADER));
+ GL(glShaderSource(fs, 3, fs_src, NULL));
+ GL(glCompileShader(fs));
+ GLint fs_status = 0;
+ GL(glGetShaderiv(fs, GL_COMPILE_STATUS, &fs_status));
+ if (fs_status != GL_TRUE) {
+ char error[1024] = { 0 };
+ GL(glGetShaderInfoLog(fs, sizeof(error), NULL, error));
+ SDL_Log("Error compiling fragment shader: %s\n", error);
+ return EXIT_FAILURE;
+ }
+
+ GLuint prog = GL(glCreateProgram());
+ GL(glAttachShader(prog, vs));
+ GL(glAttachShader(prog, fs));
+ GL(glLinkProgram(prog));
+
+ GLint prog_status;
+ GL(glGetProgramiv(prog, GL_LINK_STATUS, &prog_status));
+ if (prog_status != GL_TRUE) {
+ char error[1024] = { 0 };
+ GL(glGetProgramInfoLog(prog, sizeof(error), NULL, error));
+ SDL_Log("Error linking program: %s\n", error);
+ return EXIT_FAILURE;
+ }
+
+ GL(glUseProgram(prog));
+
+ GL(glViewport(0, 0, a_w, a_h));
+
+ // Constant uniforms
+ GLint upos = -1;
+ if ((upos = glGetUniformLocation(prog, "iResolution")) >= 0) {
+ GL(glUniform3f(upos, (float)a_w, (float)a_h, 0.0f));
+ }
+
+ int64_t frame_pts = 0;
+
+ for (float t = 0; t <= (float)a_t; t += 1.0f / (float)a_fps) {
+ printf("t=%.2f/%.2f\n", t, (float)a_t);
+
+ SDL_Event evt;
+ while (SDL_PollEvent(&evt)) {
+ bool quit = false;
+ switch (evt.type) {
+ case SDL_EVENT_QUIT: {
+ quit = true;
+ } break;
+ case SDL_EVENT_KEY_DOWN: {
+ if (evt.key.key == SDLK_ESCAPE || evt.key.key == SDLK_Q) {
+ quit = true;
+ }
+ } break;
+ if (quit) {
+ exit(1);
+ }
+ }
+ }
+
+ // Verying uniforms
+ if ((upos = glGetUniformLocation(prog, "iTime")) >= 0) {
+ GL(glUniform1f(upos, t));
+ }
+
+ GL(glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, NULL));
+
+ GL(glReadPixels(0, 0, a_w, a_h, GL_RGBA, GL_UNSIGNED_BYTE, frame_src->data[0]));
+
+ // Invert frame. Could do this in vertex shader but that would break the preview.
+ const uint8_t* src_data0 = frame_src->data[0] + (a_h - 1) * frame_src->linesize[0];
+ const uint8_t* src_data[4] = { src_data0, NULL, NULL, NULL };
+ int src_lines[4] = { -frame_src->linesize[0], 0, 0, 0 };
+
+ sws_scale(sws, src_data, src_lines, 0, frame_src->height,
+ frame_dst->data, frame_dst->linesize);
+
+ frame_dst->pts = frame_pts++;
+
+ if ((ret = avcodec_send_frame(avcc, frame_dst)) < 0) {
+ fprintf(stderr, "Failed to send video frame to encoder: %s\n", av_err2str(ret));
+ return EXIT_FAILURE;
+ }
+
+ while (ret >= 0) {
+ if ((ret = avcodec_receive_packet(avcc, pkt)) < 0) {
+ if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
+ break;
+ }
+ else {
+ fprintf(stderr, "Error receiving packet from encoder: %s\n", av_err2str(ret));
+ return EXIT_FAILURE;
+ }
+ }
+
+ av_packet_rescale_ts(pkt, avcc->time_base, avs->time_base);
+
+ if ((ret = av_interleaved_write_frame(avfc, pkt)) < 0) {
+ fprintf(stderr, "Error writing packet: %s\n", av_err2str(ret));
+ return EXIT_FAILURE;
+ }
+ }
+
+ SDL_GL_SwapWindow(wnd);
+ }
+
+ if ((ret = av_write_trailer(avfc)) < 0) {
+ fprintf(stderr, "Failed to write file trailer: %s\n", av_err2str(ret));
+ return EXIT_FAILURE;
+ }
+
+ avio_closep(&avfc->pb);
+
+ SDL_DestroyWindow(wnd);
+
+ return EXIT_SUCCESS;
+}
diff --git a/ffmpeg-shadertoy/vs.glsl b/ffmpeg-shadertoy/vs.glsl
new file mode 100644
index 0000000..57bcc37
--- /dev/null
+++ b/ffmpeg-shadertoy/vs.glsl
@@ -0,0 +1,9 @@
+#version 330 core
+
+layout (location = 0) in vec3 p;
+layout (location = 1) in vec2 t;
+
+void main()
+{
+ gl_Position = vec4(p, 1.0f);
+}