gtk/demos/gtk-demo/gtkshadertoy.c
Alexander Larsson 0092a08dfc gtk-demo: Add shadertoy demo
This adds a small demo of using OpenGL shaders, it renders a quad
over the entire widget with a custom fragment shader. The coordinates
and the uniform names are compatible with the ones on shadertoy.com
(although some features, like texture inputs are missing currently).

The default shader in the demo is
https://www.shadertoy.com/view/wsjBD3 which is CC0, so it is
redistributable by Gtk+ (most other shaders are CC-BY-NC-SA which
isn't obviously compatible). I also added a set of buttons loading
a few other CC0 shaders I found.
2020-09-08 09:56:25 +02:00

525 lines
15 KiB
C

#include <math.h>
#include <stdlib.h>
#include <string.h>
#include <epoxy/gl.h>
#include "gtkshadertoy.h"
const char *default_image_shader =
"void mainImage(out vec4 fragColor, in vec2 fragCoord) {\n"
" // Normalized pixel coordinates (from 0 to 1)\n"
" vec2 uv = fragCoord/iResolution.xy;\n"
"\n"
" // Time varying pixel color\n"
" vec3 col = 0.5 + 0.5*cos(iTime+uv.xyx+vec3(0,2,4));\n"
"\n"
" if (distance(iMouse.xy, fragCoord.xy) <= 10.0) {\n"
" col = vec3(0.0);\n"
" }\n"
"\n"
" // Output to screen\n"
" fragColor = vec4(col,1.0);\n"
"}\n";
const char *shadertoy_vertex_shader =
"#version 150 core\n"
"\n"
"uniform vec3 iResolution;\n"
"\n"
"in vec2 position;\n"
"out vec2 fragCoord;\n"
"\n"
"void main() {\n"
" gl_Position = vec4(position, 0.0, 1.0);\n"
"\n"
" // Convert from OpenGL coordinate system (with origin in center\n"
" // of screen) to Shadertoy/texture coordinate system (with origin\n"
" // in lower left corner)\n"
" fragCoord = (gl_Position.xy + vec2(1.0)) / vec2(2.0) * iResolution.xy;\n"
"}\n";
const char *fragment_prefix =
"#version 150 core\n"
"\n"
"uniform vec3 iResolution; // viewport resolution (in pixels)\n"
"uniform float iTime; // shader playback time (in seconds)\n"
"uniform float iTimeDelta; // render time (in seconds)\n"
"uniform int iFrame; // shader playback frame\n"
"uniform float iChannelTime[4]; // channel playback time (in seconds)\n"
"uniform vec3 iChannelResolution[4]; // channel resolution (in pixels)\n"
"uniform vec4 iMouse; // mouse pixel coords. xy: current (if MLB down), zw: click\n"
"uniform sampler2D iChannel0;\n"
"uniform sampler2D iChannel1;\n"
"uniform sampler2D iChannel2;\n"
"uniform sampler2D iChannel3;\n"
"uniform vec4 iDate; // (year, month, day, time in seconds)\n"
"uniform float iSampleRate; // sound sample rate (i.e., 44100)\n"
"\n"
"in vec2 fragCoord;\n"
"out vec4 fragColor;\n";
// Fragment shader suffix
const char *fragment_suffix =
" void main() {\n"
" mainImage(fragColor, fragCoord);\n"
" }\n";
typedef struct {
char *image_shader;
gboolean image_shader_dirty;
gboolean error_set;
/* Vertex buffers */
GLuint vao;
GLuint buffer;
/* Active program */
GLuint program;
/* Location of uniforms for program */
GLuint resolution_location;
GLuint time_location;
GLuint timedelta_location;
GLuint frame_location;
GLuint mouse_location;
/* Current uniform values */
float resolution[3];
float time;
float timedelta;
float mouse[4];
int frame;
/* Animation data */
gint64 first_frame_time;
gint64 first_frame;
guint tick;
} GtkShadertoyPrivate;
G_DEFINE_TYPE_WITH_PRIVATE (GtkShadertoy, gtk_shadertoy, GTK_TYPE_GL_AREA)
static gboolean gtk_shadertoy_render (GtkGLArea *area,
GdkGLContext *context);
static void gtk_shadertoy_reshape (GtkGLArea *area,
int width,
int height);
static void gtk_shadertoy_realize (GtkWidget *widget);
static void gtk_shadertoy_unrealize (GtkWidget *widget);
static gboolean gtk_shadertoy_tick (GtkWidget *widget,
GdkFrameClock *frame_clock,
gpointer user_data);
GtkWidget *
gtk_shadertoy_new (void)
{
return g_object_new (gtk_shadertoy_get_type (), NULL);
}
static void
drag_begin_cb (GtkGestureDrag *drag,
double x,
double y,
gpointer user_data)
{
GtkShadertoy *shadertoy = GTK_SHADERTOY (user_data);
GtkShadertoyPrivate *priv = gtk_shadertoy_get_instance_private (shadertoy);
int height = gtk_widget_get_height (GTK_WIDGET (shadertoy));
int scale = gtk_widget_get_scale_factor (GTK_WIDGET (shadertoy));
priv->mouse[0] = x * scale;
priv->mouse[1] = (height - y) * scale;
priv->mouse[2] = priv->mouse[0];
priv->mouse[3] = priv->mouse[1];
}
static void
drag_update_cb (GtkGestureDrag *drag,
double dx,
double dy,
gpointer user_data)
{
GtkShadertoy *shadertoy = GTK_SHADERTOY (user_data);
GtkShadertoyPrivate *priv = gtk_shadertoy_get_instance_private (shadertoy);
int width = gtk_widget_get_width (GTK_WIDGET (shadertoy));
int height = gtk_widget_get_height (GTK_WIDGET (shadertoy));
int scale = gtk_widget_get_scale_factor (GTK_WIDGET (shadertoy));
double x, y;
gtk_gesture_drag_get_start_point (drag, &x, &y);
x += dx;
y += dy;
if (x >= 0 && x < width &&
y >= 0 && y < height)
{
priv->mouse[0] = x * scale;
priv->mouse[1] = (height - y) * scale;
}
}
static void
drag_end_cb (GtkGestureDrag *drag,
gdouble dx,
gdouble dy,
gpointer user_data)
{
GtkShadertoy *shadertoy = GTK_SHADERTOY (user_data);
GtkShadertoyPrivate *priv = gtk_shadertoy_get_instance_private (shadertoy);
priv->mouse[2] = -priv->mouse[2];
priv->mouse[3] = -priv->mouse[3];
}
static void
gtk_shadertoy_init (GtkShadertoy *shadertoy)
{
GtkShadertoyPrivate *priv = gtk_shadertoy_get_instance_private (shadertoy);
GtkGesture *drag;
priv->image_shader = g_strdup (default_image_shader);
priv->tick = gtk_widget_add_tick_callback (GTK_WIDGET (shadertoy), gtk_shadertoy_tick, shadertoy, NULL);
drag = gtk_gesture_drag_new ();
gtk_widget_add_controller (GTK_WIDGET (shadertoy), GTK_EVENT_CONTROLLER (drag));
g_signal_connect (drag, "drag-begin", (GCallback)drag_begin_cb, shadertoy);
g_signal_connect (drag, "drag-update", (GCallback)drag_update_cb, shadertoy);
g_signal_connect (drag, "drag-end", (GCallback)drag_end_cb, shadertoy);
}
static void
gtk_shadertoy_finalize (GObject *obj)
{
GtkShadertoy *shadertoy = GTK_SHADERTOY (obj);
GtkShadertoyPrivate *priv = gtk_shadertoy_get_instance_private (shadertoy);
gtk_widget_remove_tick_callback (GTK_WIDGET (shadertoy), priv->tick);
g_free (priv->image_shader);
G_OBJECT_CLASS (gtk_shadertoy_parent_class)->finalize (obj);
}
static void
gtk_shadertoy_class_init (GtkShadertoyClass *klass)
{
GTK_GL_AREA_CLASS (klass)->render = gtk_shadertoy_render;
GTK_GL_AREA_CLASS (klass)->resize = gtk_shadertoy_reshape;
GTK_WIDGET_CLASS (klass)->realize = gtk_shadertoy_realize;
GTK_WIDGET_CLASS (klass)->unrealize = gtk_shadertoy_unrealize;
G_OBJECT_CLASS (klass)->finalize = gtk_shadertoy_finalize;
}
/* new window size or exposure */
static void
gtk_shadertoy_reshape (GtkGLArea *area, int width, int height)
{
GtkShadertoyPrivate *priv = gtk_shadertoy_get_instance_private ((GtkShadertoy *) area);
priv->resolution[0] = width;
priv->resolution[1] = height;
priv->resolution[2] = 1.0; /* screen aspect ratio */
/* Set the viewport */
glViewport (0, 0, (GLint) width, (GLint) height);
}
static GLuint
create_shader (int type,
const char *src,
GError **error)
{
GLuint shader;
int status;
shader = glCreateShader (type);
glShaderSource (shader, 1, &src, NULL);
glCompileShader (shader);
glGetShaderiv (shader, GL_COMPILE_STATUS, &status);
if (status == GL_FALSE)
{
int log_len;
char *buffer;
glGetShaderiv (shader, GL_INFO_LOG_LENGTH, &log_len);
buffer = g_malloc (log_len + 1);
glGetShaderInfoLog (shader, log_len, NULL, buffer);
g_set_error (error, GDK_GL_ERROR, GDK_GL_ERROR_COMPILATION_FAILED,
"Compile failure in %s shader:\n%s",
type == GL_VERTEX_SHADER ? "vertex" : "fragment",
buffer);
g_free (buffer);
glDeleteShader (shader);
return 0;
}
return shader;
}
static gboolean
init_shaders (GtkShadertoy *shadertoy,
const char *vertex_source,
const char *fragment_source,
GError **error)
{
GtkShadertoyPrivate *priv = gtk_shadertoy_get_instance_private (shadertoy);
GLuint vertex, fragment;
GLuint program = 0;
int status;
gboolean res = TRUE;
vertex = create_shader (GL_VERTEX_SHADER, vertex_source, error);
if (vertex == 0)
return FALSE;
fragment = create_shader (GL_FRAGMENT_SHADER, fragment_source, error);
if (fragment == 0)
{
glDeleteShader (vertex);
return FALSE;
}
program = glCreateProgram ();
glAttachShader (program, vertex);
glAttachShader (program, fragment);
glLinkProgram (program);
glGetProgramiv (program, GL_LINK_STATUS, &status);
if (status == GL_FALSE)
{
int log_len;
char *buffer;
glGetProgramiv (program, GL_INFO_LOG_LENGTH, &log_len);
buffer = g_malloc (log_len + 1);
glGetProgramInfoLog (program, log_len, NULL, buffer);
g_set_error (error, GDK_GL_ERROR, GDK_GL_ERROR_LINK_FAILED,
"Linking failure:\n%s", buffer);
res = FALSE;
g_free (buffer);
glDeleteProgram (program);
goto out;
}
if (priv->program != 0)
glDeleteProgram (priv->program);
priv->program = program;
priv->resolution_location = glGetUniformLocation (program, "iResolution");
priv->time_location = glGetUniformLocation (program, "iTime");
priv->timedelta_location = glGetUniformLocation (program, "iTimeDelta");
priv->frame_location = glGetUniformLocation (program, "iFrame");
priv->mouse_location = glGetUniformLocation (program, "iMouse");
glDetachShader (program, vertex);
glDetachShader (program, fragment);
out:
/* These are now owned by the program and can be deleted */
glDeleteShader (vertex);
glDeleteShader (fragment);
return res;
}
static void
gtk_shadertoy_realize_shader (GtkShadertoy *shadertoy)
{
GtkShadertoyPrivate *priv = gtk_shadertoy_get_instance_private (shadertoy);
char *fragment_shader;
GError *error = NULL;
fragment_shader = g_strconcat (fragment_prefix, priv->image_shader, fragment_suffix, NULL);
if (!init_shaders (shadertoy, shadertoy_vertex_shader, fragment_shader, &error))
{
priv->error_set = TRUE;
gtk_gl_area_set_error (GTK_GL_AREA (shadertoy), error);
g_error_free (error);
}
g_free (fragment_shader);
/* Start new shader at time zero */
priv->first_frame_time = 0;
priv->first_frame = 0;
priv->image_shader_dirty = FALSE;
}
static gboolean
gtk_shadertoy_render (GtkGLArea *area,
GdkGLContext *context)
{
GtkShadertoy *shadertoy = GTK_SHADERTOY (area);
GtkShadertoyPrivate *priv = gtk_shadertoy_get_instance_private (shadertoy);
if (gtk_gl_area_get_error (area) != NULL)
return FALSE;
if (priv->image_shader_dirty)
gtk_shadertoy_realize_shader (shadertoy);
/* Clear the viewport */
glClearColor (0.0, 0.0, 0.0, 1.0);
glClear (GL_COLOR_BUFFER_BIT);
glUseProgram (priv->program);
/* Update uniforms */
if (priv->resolution_location != -1)
glUniform3fv (priv->resolution_location, 1, priv->resolution);
if (priv->time_location != -1)
glUniform1f (priv->time_location, priv->time);
if (priv->timedelta_location != -1)
glUniform1f (priv->timedelta_location, priv->timedelta);
if (priv->frame_location != -1)
glUniform1i (priv->frame_location, priv->frame);
if (priv->mouse_location != -1)
glUniform4fv (priv->mouse_location, 1, priv->mouse);
/* Use the vertices in our buffer */
glBindBuffer (GL_ARRAY_BUFFER, priv->buffer);
glEnableVertexAttribArray (0);
glVertexAttribPointer (0, 4, GL_FLOAT, GL_FALSE, 0, 0);
glDrawArrays (GL_TRIANGLES, 0, 6);
/* We finished using the buffers and program */
glDisableVertexAttribArray (0);
glBindBuffer (GL_ARRAY_BUFFER, 0);
glUseProgram (0);
/* Flush the contents of the pipeline */
glFlush ();
return TRUE;
}
const char *
gtk_shadertoy_get_image_shader (GtkShadertoy *shadertoy)
{
GtkShadertoyPrivate *priv = gtk_shadertoy_get_instance_private (shadertoy);
return priv->image_shader;
}
void
gtk_shadertoy_set_image_shader (GtkShadertoy *shadertoy,
const char *shader)
{
GtkShadertoyPrivate *priv = gtk_shadertoy_get_instance_private (shadertoy);
g_free (priv->image_shader);
priv->image_shader = g_strdup (shader);
/* Don't override error we didn't set it ourselves */
if (priv->error_set)
{
gtk_gl_area_set_error (GTK_GL_AREA (shadertoy), NULL);
priv->error_set = FALSE;
}
priv->image_shader_dirty = TRUE;
}
static void
gtk_shadertoy_realize (GtkWidget *widget)
{
GtkGLArea *glarea = GTK_GL_AREA (widget);
GtkShadertoy *shadertoy = GTK_SHADERTOY (widget);
GtkShadertoyPrivate *priv = gtk_shadertoy_get_instance_private (shadertoy);
/* Draw two triangles across whole screen */
const GLfloat vertex_data[] = {
-1.0f, -1.0f, 0.f, 1.f,
-1.0f, 1.0f, 0.f, 1.f,
1.0f, 1.0f, 0.f, 1.f,
-1.0f, -1.0f, 0.f, 1.f,
1.0f, 1.0f, 0.f, 1.f,
1.0f, -1.0f, 0.f, 1.f,
};
GTK_WIDGET_CLASS (gtk_shadertoy_parent_class)->realize (widget);
gtk_gl_area_make_current (glarea);
if (gtk_gl_area_get_error (glarea) != NULL)
return;
glGenVertexArrays (1, &priv->vao);
glBindVertexArray (priv->vao);
glGenBuffers (1, &priv->buffer);
glBindBuffer (GL_ARRAY_BUFFER, priv->buffer);
glBufferData (GL_ARRAY_BUFFER, sizeof (vertex_data), vertex_data, GL_STATIC_DRAW);
glBindBuffer (GL_ARRAY_BUFFER, 0);
gtk_shadertoy_realize_shader (shadertoy);
}
static void
gtk_shadertoy_unrealize (GtkWidget *widget)
{
GtkGLArea *glarea = GTK_GL_AREA (widget);
GtkShadertoyPrivate *priv = gtk_shadertoy_get_instance_private ((GtkShadertoy *) widget);
gtk_gl_area_make_current (glarea);
if (gtk_gl_area_get_error (glarea) == NULL)
{
if (priv->buffer != 0)
glDeleteBuffers (1, &priv->buffer);
if (priv->vao != 0)
glDeleteVertexArrays (1, &priv->vao);
if (priv->program != 0)
glDeleteProgram (priv->program);
}
GTK_WIDGET_CLASS (gtk_shadertoy_parent_class)->unrealize (widget);
}
static gboolean
gtk_shadertoy_tick (GtkWidget *widget,
GdkFrameClock *frame_clock,
gpointer user_data)
{
GtkShadertoy *shadertoy = GTK_SHADERTOY (widget);
GtkShadertoyPrivate *priv = gtk_shadertoy_get_instance_private (shadertoy);
gint64 frame_time;
gint64 frame;
float previous_time;
frame = gdk_frame_clock_get_frame_counter (frame_clock);
frame_time = gdk_frame_clock_get_frame_time (frame_clock);
if (priv->first_frame_time == 0)
{
priv->first_frame_time = frame_time;
priv->first_frame = frame;
previous_time = 0;
}
else
previous_time = priv->time;
priv->time = (frame_time - priv->first_frame_time) / 1000000.0f;
priv->frame = frame - priv->first_frame;
priv->timedelta = priv->time - previous_time;
gtk_widget_queue_draw (widget);
return G_SOURCE_CONTINUE;
}