Xlib 05: Single file,local shadertoy with hot reloading



asdfasdf

In last post we looked at doing some opengl and shaders with raw xlib. It was done in with single ‘.c’ file for simplicity. Here we will create almost the same thing but with autoreloading shaders on content change. There still will be just one ‘.c’ file but shaders will be split into their own files for easier editing.

The code as usual will be focused around simplicity and ignoring some error checking, speed optimization and etc.

Setup

Up till this post everything used just one file and we didn’t need to do any file management or seting up some build process. Here we will put all ‘.c’ code in one file 08_window_local_shadertoy.c and shader files will be in 08_shader.vert and 08_shader.frag. Names and extensions are arbitrary and you can use whatever you want but I chose these names for now.

In my own setup I have separate ‘src’ and ‘build’ neighboring folders. All binary files and intermediary files go to ‘build’ folder and all source code lives in ‘src’ folder. But instead of loading ‘../src/shader.vert’ from withing binary I chose to copy shader files from src folder to build folder on each build so that final binary can access shader files directory within its own location. Since this is a toy program to play with shaders it does not really matter how you organize this as we will be playing with fragment shader most of the time. So obvious solution was to put them as close as possible.

In the privous post I drew a triangle by defining vertices in VBO. Here don’t really care about vertices and want to play with all pixels with fragment shader. We still need to define some vertices to draw a rectrangle. For that we will use 6 points to draw 2 triangles that will cover whole sreen.

asdfasdf

float S = 1.0;
float Vertices[] = {
    -S,  S, 0.0f,
     S,  S, 0.0f,
    -S, -S, 0.0f,

     S,  S, 0.0f,
     S, -S, 0.0f,
    -S, -S, 0.0f,
};

unsigned int VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices), Vertices, GL_STATIC_DRAW);

Now we specify where are our shaders located. In this example I put them directly in code but if you plan to play with a lot of different shaders you should pass shader file names as parameter to binary and access them through argv. Here we use just simple char* variables.

char* VertexShaderPath = "08_shader.vert";
char* FragmentShaderPath = "08_shader_big.frag";

After that we need to load and compile our shaders. Then create a shader program and link them together. This time the code is moved to a separate function as we will need to use it more than once. Also shader code will be loaded from from disk instead of being part of the binary. The function take OldProgram as a parameter but we can just pass 0 for the first use as it is needed to clear old program when we request a new one.

u32 CompileProgramAndShaders(u32 OldProgram, char* VertexShaderPath, char* FragmentShaderPath) {
    file_result VertexFileResult = ReadEntireFile(VertexShaderPath);
    file_result FragmentFileResult = ReadEntireFile(FragmentShaderPath);

    const char* VertexShaderSource = VertexFileResult.Bytes;
    const char* FragmentShaderSource = FragmentFileResult.Bytes;

    unsigned int VertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(VertexShader, 1, &VertexShaderSource, NULL);
    glCompileShader(VertexShader);
    CheckShaderCompilation(VertexShader);

    unsigned int FragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(FragmentShader, 1, &FragmentShaderSource, NULL);
    glCompileShader(FragmentShader);
    CheckShaderCompilation(FragmentShader);

    unsigned int ShaderProgram = glCreateProgram();
    glAttachShader(ShaderProgram, VertexShader);
    glAttachShader(ShaderProgram, FragmentShader);
    glLinkProgram(ShaderProgram);

    CheckProgramCompilation(ShaderProgram);

    glDeleteShader(VertexShader);
    glDeleteShader(FragmentShader);

    if(OldProgram) {
        glDeleteProgram(OldProgram);
    }

    if(VertexFileResult.Bytes) { free(VertexFileResult.Bytes); }
    if(FragmentFileResult.Bytes) { free(FragmentFileResult.Bytes); }

    return ShaderProgram;
}

Here we use ReadEntireFile(…) as utility function to load all contents of a file and put it into file_result structure for convinience. It reads all bytes into memory and passes it back for later processing.

typedef struct file_result{
    u8* Bytes;
    u64 Size;
} file_result;

file_result ReadEntireFile(char* Path) {
    FILE* File = fopen(Path, "rb");
    file_result Result = {};

    if(File != 0) {
        struct stat FileStats = {};
        stat(Path, &FileStats);
        u64 FileSize = FileStats.st_size;

        if(FileSize > 0) {
            Result.Size = FileSize;
            Result.Bytes = (u8*)malloc(Result.Size + 1);
            fread(Result.Bytes, Result.Size, 1, File);
            Result.Bytes[Result.Size] = 0;
        } else {
            // TODO: Log
        }
        fclose(File);
    } else {
        fprintf(stderr, "Couldn't open file '%s'\n", Path);
    }

    return Result;
}

With this setup everything else remains the same. Nothing really changed much apart from now moving all shaders out of binary into their own files.

Here is basic vertex shader.

#version 330 core
layout (location = 0) in vec3 aPos;

void main()
{
   gl_Position = vec4(aPos.x, aPos.y, 0.0f, 1.0f);
}

And here is basic fragment shader with small animation.

out vec4 FragColor;
uniform float iTime;
uniform vec2 iResolution;

void main()
{
   vec2 uv = gl_FragCoord.xy / iResolution;
   uv.x += cos(iTime);
   FragColor = vec4(uv.x, uv.y, 0.0f, 1.0f);
}

Unfortunately this fragment shader will not work with our old C code as we are using some uniform variable like iTime and iResolution which are not avaible in our shader. These will need to be passed from C code to the shader. Passing iResolution is pretty straightforward as it is just a vec2 with width and height of our screen. In this example we will use just static screen width and height but a better approach would be to update them on window size change.

So first we ask for location for iResolution and then just fill it with values.

int ResultionLocation = glGetUniformLocation(ShaderProgram, "iResolution");
glUniform2f(ResultionLocation, WindowWidth, WindowHeight);

Passing time is similar but we will have to get that time.

int TimeLocation = glGetUniformLocation(ShaderProgram, "iTime");
float Time = GetTimeDouble(ProgramStartTime);
glUniform1f(TimeLocation, Time);

First isues with time is that we want to pass float and resolution of float is not enough to pass detailed time. So instead of passing current time we can pass (like shadertoy), time from the start of program execution. So somewhere before our main loop we just get passed time. We will be storing everything in struct timeval and converting them to float right before sending data to graphics card. struct timeval ProgramStartTime = GetTime();

So what does GetTimeDouble() does? It takes program start time and calculate the difference with current time and converts that to float.

double TimevalToDouble(struct timeval Time) {
    double Result = (double)Time.tv_sec + (double)Time.tv_usec / MICROSECONS_IN_SECOND;
    return Result;
}

float GetTimeDouble(struct timeval StartTime) {
    float Result = 0;
    struct timeval EndTime;
    gettimeofday(&EndTime, 0);

    struct timeval ResultTime = {};

    if(EndTime.tv_usec < StartTime.tv_usec) {
        ResultTime.tv_sec = EndTime.tv_sec - StartTime.tv_sec - 1;
        ResultTime.tv_usec = MICROSECONS_IN_SECOND - StartTime.tv_usec + EndTime.tv_usec;
    } else {
        ResultTime.tv_sec = EndTime.tv_sec - StartTime.tv_sec;
        ResultTime.tv_usec = EndTime.tv_usec - StartTime.tv_usec;
    }

    Result = (double)ResultTime.tv_sec + (double)ResultTime.tv_usec / MICROSECONS_IN_SECOND;
    Result = TimevalToDouble(ResultTime);

    return Result;
}

And with this out of the way we can actually run our program and see an animation of gradients.

Hot reloading

Even though now we don’t need to recompile our application each time we change something in a shader we still need to close the application and the open it to see the results. It is a bit annoying and not exactly fun way to play.

Let’s reload our shaders each time we change one of them with full shader recompilation and linking. To do that there are multiple ways to approach it with better way to do it with poll and inotify. It is relatively straighforward apparoch and already described how to do that for python in Poor mans autoreload server in single file python on linux. Even though it is in python it uses couple functions from libc which could be easily replicated here but I don’t want to introduct new concepts here.

Instead we will just super dump and wasteful apparoch of checking for file modification time every 16 milliseconds. At the begininning I thought we should do it each second but I found that my computer had zero issues with processing it 60 times a second so I decided to be piggy here as it is simple to explain.

time_t GetFileModificationTime(char* FilePath) {
    struct stat FileStats;
    stat(FilePath, &FileStats);
    time_t FileModificationTime = FileStats.st_mtime;
    return FileModificationTime;
    printf("%d\n", FileModificationTime);
}

int DidShaderFilesChange(char* VertexShaderPath, size_t* VertexOldTime, char* FragmentShaderPath, size_t* FragmentOldTime) {
    int Result = 0;
    time_t VertexTime = GetFileModificationTime(VertexShaderPath);
    time_t FragmentTime = GetFileModificationTime(FragmentShaderPath);

    if((VertexTime > *VertexOldTime) || (FragmentTime > *FragmentOldTime)) {
        Result = 1;
    }

    *VertexOldTime = VertexTime;
    *FragmentOldTime = FragmentTime;

    return Result;
}

So initially we store file modification time somewhere outside the main loop.

size_t VertexLastModificationTime = 0;
size_t FragmentLastModificationTime = 0;

And then inside the loop on each cycle we ask DidShaderFilesChange() if files modification time is newer compared to old store time. And if so we just call again CompileProgramAndShaders(…) which will do all the loading and linking of shaders.

if(DidShaderFilesChange(VertexShaderPath, &VertexLastModificationTime, FragmentShaderPath, &FragmentLastModificationTime)){
    printf("Changed\n");
    ShaderProgram = CompileProgramAndShaders(ShaderProgram, VertexShaderPath, FragmentShaderPath);
}

glUseProgram(ShaderProgram);

This is how it looks to work with this setup.

Some larger example from shadertoy

Next thing is was to try some larger example from shader to see if this approach could be used for larger examples. The issue is that tools like shadertoy use special variables, function and convention to allow short shader programs that fits into a twit. I don’t want to replacate all of its functionality but show a path to try to do it.

I chose a random shader from shadertoy that I liked. It pretty program from user kaiware007 and is available at https://www.shadertoy.com/view/Wt33Wf.

First of all I just copy pasted all code from that page into 08_shader_big.frag. Of course it didn’t work and gave a bunch of errors.

Initially let’s rename mainImage(…) to main() and remove all params and add version at the top of the file #version 330 core. Then add out variable fragColor and iTime and iResolution uniform variable.

out vec4 fragColor;
uniform float iTime;
uniform vec2 iResolution;

If you try to compile run it, it will error out saying that it needs fragCoord. For this we will need to change our vertex shader to look like this:

#version 330 core
layout (location = 0) in vec3 aPos;

uniform vec2 iResolution;
out vec4 fragCoord;

void main()
{
   gl_Position = vec4(aPos.x, aPos.y, 0.0f, 1.0f);
   fragCoord = vec4(
           gl_Position.x * iResolution.x,
           gl_Position.y * iResolution.y,
           0.0, 1.0);
}

What we did is we added fragCoord as an output variable and it is just vec4 with a mapping of normalized screen coordinates to actual screen size pixel positions. We could have done this in fragment shader but if we add it here we just add it one and never bother with it again. Now we just need to declare fragCoord input variable in vec4 fragCoord;

And this is pretty much it. It works!

Final Code

Here is a single file C code source: 08_window_local_shader_toy.c

#include <stdio.h>
#include <X11/Xlib.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/time.h>

// TODO: How to remove this?
#define GL_GLEXT_PROTOTYPES
#include <GL/glx.h>
#include <GL/glext.h>

#include <stdint.h>

typedef uint8_t u8;
typedef uint16_t u16;
typedef uint32_t u32;
typedef uint64_t u64;

#define MICROSECONS_IN_SECOND 1000000

static int DoubleBufferAttributes[] = {
    GLX_RGBA,
    GLX_RED_SIZE, 1,
    GLX_GREEN_SIZE, 1,
    GLX_BLUE_SIZE, 1,
    GLX_DEPTH_SIZE, 12,
    GLX_DOUBLEBUFFER,
    None,
};

void VerifyOrDie(int ResultStatus, const char *Message) {
    if(ResultStatus == 0) {
        fprintf(stderr, "%s", Message);
        exit(2);
    }
}

void CheckShaderCompilation(u32 Shader) {
    int ResultStatus;
    char Buffer[512];
    glGetShaderiv(Shader, GL_COMPILE_STATUS, &ResultStatus);

    if(ResultStatus == 0) {
        glGetShaderInfoLog(Shader, sizeof(Buffer), NULL, Buffer);
        printf("ERROR::SHADER::VERTEX::COMPILATION_FAILED -----------------------------------\n");
        printf("%s\n", Buffer);
    }
}

void CheckProgramCompilation(unsigned int Program) {
    int  ResultStatus;
    char Buffer[512];
    glGetProgramiv(Program, GL_COMPILE_STATUS, &ResultStatus);

    if(ResultStatus == 0)
    {
        glGetShaderInfoLog(Program, sizeof(Buffer), NULL, Buffer);
        printf("ERROR::PROGRAM::COMPILATION_FAILED\n");
        printf("%s\n", Buffer);
    }
}

typedef struct file_result{
    u8* Bytes;
    u64 Size;
} file_result;
file_result
ReadEntireFile(char* Path) {
    FILE* File = fopen(Path, "rb");
    file_result Result = {};

    if(File != 0) {
        struct stat FileStats = {};
        stat(Path, &FileStats);
        u64 FileSize = FileStats.st_size;

        if(FileSize > 0) {
            Result.Size = FileSize;
            Result.Bytes = (u8*)malloc(Result.Size + 1);
            fread(Result.Bytes, Result.Size, 1, File);
            Result.Bytes[Result.Size] = 0;
        } else {
            // TODO: Log
        }
        fclose(File);
    } else {
        fprintf(stderr, "Couldn't open file '%s'\n", Path);
    }

    return Result;
}

struct timeval GetTime() {
    struct timeval TimeValue;
    gettimeofday(&TimeValue, 0);
    return TimeValue;
}

double TimevalToDouble(struct timeval Time) {
    double Result = (double)Time.tv_sec + (double)Time.tv_usec / MICROSECONS_IN_SECOND;
    return Result;
}

float GetTimeDouble(struct timeval StartTime) {
    float Result = 0;
    struct timeval EndTime;
    gettimeofday(&EndTime, 0);

    struct timeval ResultTime = {};

    if(EndTime.tv_usec < StartTime.tv_usec) {
        ResultTime.tv_sec = EndTime.tv_sec - StartTime.tv_sec - 1;
        ResultTime.tv_usec = MICROSECONS_IN_SECOND - StartTime.tv_usec + EndTime.tv_usec;
    } else {
        ResultTime.tv_sec = EndTime.tv_sec - StartTime.tv_sec;
        ResultTime.tv_usec = EndTime.tv_usec - StartTime.tv_usec;
    }

    Result = (double)ResultTime.tv_sec + (double)ResultTime.tv_usec / MICROSECONS_IN_SECOND;
    Result = TimevalToDouble(ResultTime);

    return Result;
}

u32 CompileProgramAndShaders(u32 OldProgram, char* VertexShaderPath, char* FragmentShaderPath) {
    file_result VertexFileResult = ReadEntireFile(VertexShaderPath);
    file_result FragmentFileResult = ReadEntireFile(FragmentShaderPath);

    const char* VertexShaderSource = VertexFileResult.Bytes;
    const char* FragmentShaderSource = FragmentFileResult.Bytes;

    unsigned int VertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(VertexShader, 1, &VertexShaderSource, NULL);
    glCompileShader(VertexShader);
    CheckShaderCompilation(VertexShader);

    unsigned int FragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(FragmentShader, 1, &FragmentShaderSource, NULL);
    glCompileShader(FragmentShader);
    CheckShaderCompilation(FragmentShader);

    unsigned int ShaderProgram = glCreateProgram();
    glAttachShader(ShaderProgram, VertexShader);
    glAttachShader(ShaderProgram, FragmentShader);
    glLinkProgram(ShaderProgram);

    CheckProgramCompilation(ShaderProgram);

    glDeleteShader(VertexShader);
    glDeleteShader(FragmentShader);

    if(OldProgram) {
        glDeleteProgram(OldProgram);
    }

    if(VertexFileResult.Bytes) { free(VertexFileResult.Bytes); }
    if(FragmentFileResult.Bytes) { free(FragmentFileResult.Bytes); }

    /* glUseProgram(ShaderProgram); */

    return ShaderProgram;
}

time_t GetFileModificationTime(char* FilePath) {
    struct stat FileStats;
    stat(FilePath, &FileStats);
    time_t FileModificationTime = FileStats.st_mtime;
    return FileModificationTime;
    printf("%d\n", FileModificationTime);
}

int DidShaderFilesChange(char* VertexShaderPath, size_t* VertexOldTime, char* FragmentShaderPath, size_t* FragmentOldTime) {
    int Result = 0;
    time_t VertexTime = GetFileModificationTime(VertexShaderPath);
    time_t FragmentTime = GetFileModificationTime(FragmentShaderPath);

    if((VertexTime > *VertexOldTime) || (FragmentTime > *FragmentOldTime)) {
        Result = 1;
    }

    *VertexOldTime = VertexTime;
    *FragmentOldTime = FragmentTime;

    return Result;
}

int main()
{
    Display* MainDisplay = XOpenDisplay(0);
    int MainScreen = XDefaultScreen(MainDisplay);
    Window RootWindow = XDefaultRootWindow(MainDisplay);

    int Dummy;
    int ResultStatus = glXQueryExtension(MainDisplay, &Dummy, &Dummy);
    VerifyOrDie(ResultStatus != 0, "Error: X Server has not GLX extension\n");

    XVisualInfo* VisualInfo = glXChooseVisual(MainDisplay, MainScreen, DoubleBufferAttributes);
    VerifyOrDie(VisualInfo != 0, "glXChooseVisual returned 0");
    VerifyOrDie(VisualInfo->class == TrueColor, "No True COlor support. Cannot run program without it");


    GLXContext ShareList = None;
    int IsDirectRendering = True;
    GLXContext OpenGLContext = glXCreateContext(MainDisplay, VisualInfo, ShareList, IsDirectRendering);
    VerifyOrDie(OpenGLContext != 0, "ERROR: Couldn't create rendering context\n");


    int WindowX = 0;
    int WindowY = 0;
    int WindowWidth = 800;
    int WindowHeight = 600;
    /* int WindowWidth = 800; */
    /* int WindowHeight = 450; */
    int BorderWidth = 0;
    int WindowClass = InputOutput;
    int WindowDepth = VisualInfo->depth;
    Visual* WindowVisual = VisualInfo->visual;

    int AttributeValueMask = CWBackPixel | CWEventMask | CWColormap;

    XSetWindowAttributes WindowAttributes = {};
    WindowAttributes.colormap = XCreateColormap(MainDisplay, RootWindow, VisualInfo->visual, AllocNone);
    WindowAttributes.background_pixel = 0xffafe9af;
    WindowAttributes.event_mask = StructureNotifyMask | KeyPressMask | KeyReleaseMask | PointerMotionMask;

    Window MainWindow = XCreateWindow(MainDisplay, RootWindow, 
            WindowX, WindowY, WindowWidth, WindowHeight,
            BorderWidth, WindowDepth, WindowClass, WindowVisual,
            AttributeValueMask, &WindowAttributes);

    XStoreName(MainDisplay, MainWindow, "General app");

    glXMakeCurrent(MainDisplay, MainWindow, OpenGLContext);

    XMapWindow(MainDisplay, MainWindow);

    Atom WM_DELETE_WINDOW = XInternAtom(MainDisplay, "WM_DELETE_WINDOW", False);
    if(!XSetWMProtocols(MainDisplay, MainWindow, &WM_DELETE_WINDOW, 1)) {
        printf("Couldn't register WM_DELETE_WINDOW\n");
    }


    /* ------------------------------------------------------------------------------------------------------------ */
    /* ------------------------------------------------------------------------------------------------------------ */
    /* ------------------------------------------------------------------------------------------------------------ */


    float S = 1.0;
    float Vertices[] = {
        -S,  S, 0.0f,
         S,  S, 0.0f,
        -S, -S, 0.0f,

         S,  S, 0.0f,
         S, -S, 0.0f,
        -S, -S, 0.0f,
    };

    unsigned int VBO;
    glGenBuffers(1, &VBO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices), Vertices, GL_STATIC_DRAW);


    char* VertexShaderPath = "08_shader.vert";
    char* FragmentShaderPath = "08_shader_big.frag";

    u32 ShaderProgram = 0;
    ShaderProgram = CompileProgramAndShaders(ShaderProgram, VertexShaderPath, FragmentShaderPath);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);

    /* ----------------------------------------------------------------------- */


    /* ------------------------------------------------------------------------------------------------------------ */
    /* ------------------------------------------------------------------------------------------------------------ */
    /* ------------------------------------------------------------------------------------------------------------ */

    struct timeval ProgramStartTime = GetTime();
    struct timeval LastTime = ProgramStartTime;

    size_t VertexLastModificationTime = 0;
    size_t FragmentLastModificationTime = 0;

    int IsProgramRunning = 1;
    while(IsProgramRunning) {
        XEvent GeneralEvent = {};

        // NOTE: Process all events
        while(XPending(MainDisplay)) {
            XNextEvent(MainDisplay, &GeneralEvent);
            switch(GeneralEvent.type) {
                case ClientMessage: 
                {
                    IsProgramRunning = 0;
                } break;
            }
        }

        if(DidShaderFilesChange(VertexShaderPath, &VertexLastModificationTime, FragmentShaderPath, &FragmentLastModificationTime)){
            printf("Changed\n");
            ShaderProgram = CompileProgramAndShaders(ShaderProgram, VertexShaderPath, FragmentShaderPath);
        }

        glUseProgram(ShaderProgram);

        {
            glClearColor(0.0, 0.0, 0.2, 1.0);
            glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

            /* int TimeLocation = glGetUniformLocation(ShaderProgram, "time"); */
            /* int ResultionLocation = glGetUniformLocation(ShaderProgram, "resolution"); */

            int TimeLocation = glGetUniformLocation(ShaderProgram, "iTime");
            int ResultionLocation = glGetUniformLocation(ShaderProgram, "iResolution");
            int FrameRateLocation = glGetUniformLocation(ShaderProgram, "iFrameRate");
            int TimeDeltaLocation = glGetUniformLocation(ShaderProgram, "iTimeDelta");

            float Time = GetTimeDouble(ProgramStartTime);

            glUniform2f(ResultionLocation, WindowWidth, WindowHeight);
            glUniform1f(TimeLocation, Time);
            glUniform1f(FrameRateLocation, 60);
            glUniform1f(TimeDeltaLocation, 1/60);

            glDrawArrays(GL_TRIANGLES, 0, 6);

            glXSwapBuffers(MainDisplay, MainWindow);
        }

    }
}

And this is slightly changed code of final shader (I hope author don’t mind me posting it here directly). 08_shader_big.frag

#version 330 core

in vec4 fragCoord;

out vec4 fragColor;
uniform float iTime;
uniform vec2 iResolution;

float sun(vec2 uv, float battery)
{
 	float val = smoothstep(0.3, 0.29, length(uv));
 	float bloom = smoothstep(0.7, 0.0, length(uv));
    float cut = 3.0 * sin((uv.y + iTime * 0.2 * (battery + 0.02)) * 100.0) 
				+ clamp(uv.y * 14.0 + 1.0, -6.0, 6.0);
    cut = clamp(cut, 0.0, 1.0);
    return clamp(val * cut, 0.0, 1.0) + bloom * 0.6;
}

float grid(vec2 uv, float battery)
{
    vec2 size = vec2(uv.y, uv.y * uv.y * 0.2) * 0.01;
    uv += vec2(0.0, iTime * 4.0 * (battery + 0.05));
    uv = abs(fract(uv) - 0.5);
 	vec2 lines = smoothstep(size, vec2(0.0), uv);
 	lines += smoothstep(size * 5.0, vec2(0.0), uv) * 0.4 * battery;
    return clamp(lines.x + lines.y, 0.0, 3.0);
}

float dot2(in vec2 v ) { return dot(v,v); }

float sdTrapezoid( in vec2 p, in float r1, float r2, float he )
{
    vec2 k1 = vec2(r2,he);
    vec2 k2 = vec2(r2-r1,2.0*he);
    p.x = abs(p.x);
    vec2 ca = vec2(p.x-min(p.x,(p.y<0.0)?r1:r2), abs(p.y)-he);
    vec2 cb = p - k1 + k2*clamp( dot(k1-p,k2)/dot2(k2), 0.0, 1.0 );
    float s = (cb.x<0.0 && ca.y<0.0) ? -1.0 : 1.0;
    return s*sqrt( min(dot2(ca),dot2(cb)) );
}

float sdLine( in vec2 p, in vec2 a, in vec2 b )
{
    vec2 pa = p-a, ba = b-a;
    float h = clamp( dot(pa,ba)/dot(ba,ba), 0.0, 1.0 );
    return length( pa - ba*h );
}

float sdBox( in vec2 p, in vec2 b )
{
    vec2 d = abs(p)-b;
    return length(max(d,vec2(0))) + min(max(d.x,d.y),0.0);
}

float opSmoothUnion(float d1, float d2, float k){
	float h = clamp(0.5 + 0.5 * (d2 - d1) /k,0.0,1.0);
    return mix(d2, d1 , h) - k * h * ( 1.0 - h);
}

float sdCloud(in vec2 p, in vec2 a1, in vec2 b1, in vec2 a2, in vec2 b2, float w)
{
	//float lineVal1 = smoothstep(w - 0.0001, w, sdLine(p, a1, b1));
    float lineVal1 = sdLine(p, a1, b1);
    float lineVal2 = sdLine(p, a2, b2);
    vec2 ww = vec2(w*1.5, 0.0);
    vec2 left = max(a1 + ww, a2 + ww);
    vec2 right = min(b1 - ww, b2 - ww);
    vec2 boxCenter = (left + right) * 0.5;
    //float boxW = right.x - left.x;
    float boxH = abs(a2.y - a1.y) * 0.5;
    //float boxVal = sdBox(p - boxCenter, vec2(boxW, boxH)) + w;
    float boxVal = sdBox(p - boxCenter, vec2(0.04, boxH)) + w;
    
    float uniVal1 = opSmoothUnion(lineVal1, boxVal, 0.05);
    float uniVal2 = opSmoothUnion(lineVal2, boxVal, 0.05);
    
    return min(uniVal1, uniVal2);
}

/* void mainImage( out vec4 fragColor, in vec2 fragCoord ) */
void main()
{
    vec2 uv = (2.0 * fragCoord.xy - iResolution.xy)/iResolution.y;
    float battery = 1.0;
    //if (iMouse.x > 1.0 && iMouse.y > 1.0) battery = iMouse.y / iResolution.y;
    //else battery = 0.8;
    
    //if (abs(uv.x) < (9.0 / 16.0))
    {
        // Grid
        float fog = smoothstep(0.1, -0.02, abs(uv.y + 0.2));
        vec3 col = vec3(0.0, 0.1, 0.2);
        if (uv.y < -0.2)
        {
            uv.y = 3.0 / (abs(uv.y + 0.2) + 0.05);
            uv.x *= uv.y * 1.0;
            float gridVal = grid(uv, battery);
            col = mix(col, vec3(1.0, 0.5, 1.0), gridVal);
        }
        else
        {
            float fujiD = min(uv.y * 4.5 - 0.5, 1.0);
            uv.y -= battery * 1.1 - 0.51;
            
            vec2 sunUV = uv;
            vec2 fujiUV = uv;
            
            // Sun
            sunUV += vec2(0.75, 0.2);
            //uv.y -= 1.1 - 0.51;
            col = vec3(1.0, 0.2, 1.0);
            float sunVal = sun(sunUV, battery);
            
            col = mix(col, vec3(1.0, 0.4, 0.1), sunUV.y * 2.0 + 0.2);
            col = mix(vec3(0.0, 0.0, 0.0), col, sunVal);
            
            // fuji
            float fujiVal = sdTrapezoid( uv  + vec2(-0.75+sunUV.y * 0.0, 0.5), 1.75 + pow(uv.y * uv.y, 2.1), 0.2, 0.5);
            float waveVal = uv.y + sin(uv.x * 20.0 + iTime * 2.0) * 0.05 + 0.2;
            float wave_width = smoothstep(0.0,0.01,(waveVal));
            
            // fuji color
            col = mix( col, mix(vec3(0.0, 0.0, 0.25), vec3(1.0, 0.0, 0.5), fujiD), step(fujiVal, 0.0));
            // fuji top snow
            col = mix( col, vec3(1.0, 0.5, 1.0), wave_width * step(fujiVal, 0.0));
            // fuji outline
            col = mix( col, vec3(1.0, 0.5, 1.0), 1.0-smoothstep(0.0,0.01,abs(fujiVal)) );
            //col = mix( col, vec3(1.0, 1.0, 1.0), 1.0-smoothstep(0.03,0.04,abs(fujiVal)) );
            //col = vec3(1.0, 1.0, 1.0) *(1.0-smoothstep(0.03,0.04,abs(fujiVal)));
            
            // horizon color
            col += mix( col, mix(vec3(1.0, 0.12, 0.8), vec3(0.0, 0.0, 0.2), clamp(uv.y * 3.5 + 3.0, 0.0, 1.0)), step(0.0, fujiVal) );
            
            // cloud
            vec2 cloudUV = uv;
            cloudUV.x = mod(cloudUV.x + iTime * 0.1, 4.0) - 2.0;
            float cloudTime = iTime * 0.5;
            float cloudY = -0.5;
            float cloudVal1 = sdCloud(cloudUV, 
                                     vec2(0.1 + sin(cloudTime + 140.5)*0.1,cloudY), 
                                     vec2(1.05 + cos(cloudTime * 0.9 - 36.56) * 0.1, cloudY), 
                                     vec2(0.2 + cos(cloudTime * 0.867 + 387.165) * 0.1,0.25+cloudY), 
                                     vec2(0.5 + cos(cloudTime * 0.9675 - 15.162) * 0.09, 0.25+cloudY), 0.075);
            cloudY = -0.6;
            float cloudVal2 = sdCloud(cloudUV, 
                                     vec2(-0.9 + cos(cloudTime * 1.02 + 541.75) * 0.1,cloudY), 
                                     vec2(-0.5 + sin(cloudTime * 0.9 - 316.56) * 0.1, cloudY), 
                                     vec2(-1.5 + cos(cloudTime * 0.867 + 37.165) * 0.1,0.25+cloudY), 
                                     vec2(-0.6 + sin(cloudTime * 0.9675 + 665.162) * 0.09, 0.25+cloudY), 0.075);
            
            float cloudVal = min(cloudVal1, cloudVal2);
            
            //col = mix(col, vec3(1.0,1.0,0.0), smoothstep(0.0751, 0.075, cloudVal));
            col = mix(col, vec3(0.0, 0.0, 0.2), 1.0 - smoothstep(0.075 - 0.0001, 0.075, cloudVal));
            col += vec3(1.0, 1.0, 1.0)*(1.0 - smoothstep(0.0,0.01,abs(cloudVal - 0.075)));
        }

        col += fog * fog * fog;
        col = mix(vec3(col.r, col.r, col.r) * 0.5, col, battery * 0.7);

        fragColor = vec4(col,1.0);
    }
}

Demo

Conclusion

With this post I hope to conclude my posts related to creating windows with raw Xlib. Even I have couple more crazy ideas to write about but they won’t be anytime soon.

Apart from than we achieved a bad but still function local toy to play with shaders with just over 300 lines of C code. It is instantenous, fast and easy to play with. I think it is a good tool to learn basic shaders as you can easily play with shader code and if you need to look under the hood you can just look at a relatively small amount of code.

I hope it was as fun to your as it was fun for me to play with it.