using webgl aka shadertoy shaders with sdl2 and freebasic

Post your FreeBASIC source, examples, tips and tricks here. Please don’t post code without including an explanation.
Post Reply
thrive4
Posts: 79
Joined: Jun 25, 2021 15:32

using webgl aka shadertoy shaders with sdl2 and freebasic

Post by thrive4 »

description
This example is a slight twist to djpeters 'offline shadertoy'
the twist being the shader output is bound to an sdl window.

Also demonstrated is a hybrid usage of opengl with a regular
sdl window, some basic proportional placement and the pitfalls
with using multiple windows in sdl2.
Note: things do seem to have improved in sdl3 regrettably
I could not use sdl3 for this example.

snags
Well taking heed to the Chinese proverb;
man walk over mountain, trips over pebble,
webgl aka shadertoy shaders are not exactly
easy to utilize (similar to .mp3 or .avi for example)

This means you will need to do some creative
cut and pasting from shadertoy:
https://www.shadertoy.com
Which in turn means you will most likely have
to familiarize your self with:
https://www.shadertoy.com/howto

As mentioned earlier there is some funky behavior
when combining multiple windows.

navigation
f11 - fullscreen
esc - close app
arrow keys - navigate mouse pointer
return - select button (fake left mouse click)

left click lower right corner, under shader,
toggles size shader.

Compiling
Place sdl2.dll in the same folder as the code
https://github.com/libsdl-org/SDL/releases
curently v2.30.9 (windows 32bit)

also add the following three shaders, in the
same folder as the code, as seperate files
for demonstration purposes.

Save as 'theeye.gls'

Code: Select all

//courtesy https://www.shadertoy.com/view/llVczd
void mainImage( out vec4 f, in vec2 w ) {

	vec2  r = iResolution.xy, p = w - r*.5;
	float d = length(p) / r.y, c=1., x = pow(d, .1), y = atan(p.x, p.y) / 6.28;

	for (float i = 0.; i < 3.; ++i)
		c = min(c, length(fract(vec2(x - iTime*i*.003, fract(y + i*.003)*.10)*20.)*2.-1.));

	f = vec4(d+20.*c*d*d*(.6-d));
}
Save as 'aflowof33.gls'

Code: Select all

// courtesy https://www.shadertoy.com/view/td3Sz8
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
  vec2 p=(3.3*fragCoord.xy-iResolution.xy)/max(iResolution.x,iResolution.y);

  for(float i=3.3;i<33.;i++)
	{
        p+= .33/i * cos(i*p.yx+iTime*vec2(.33,.33)  + vec2(.33,3.3));
	}
	vec3 col=vec3(.33*sin(3.3*p.x)+.33,.33*sin(3.3*p.y)+.33,sin(3.3*p.x+3.3*p.y));
	fragColor=(3.3/(3.3-(3.3/3.3)))*vec4(col, 3.3);
}
Save as 'hextile.gls'

Code: Select all

// courtesy https://www.shadertoy.com/view/dtSXWV
void mainImage(out vec4 C, in vec2 U)
{
    vec2 R = iResolution.xy,
         v = (U+U-R)/R.y,                 // coords
         u = v*10./(1.-iMouse.y/R.y),     // coords with zoom & scale
         o = vec2(.5, .866),              // hex offset
         a = mod(u,   o+o) - o,           // grid 1
         b = mod(u-o, o+o) - o,           // grid 2
         h = dot(a,a) < dot(b,b) ? a : b, // combine grids for hex tile
         k = abs(h);

    float p = length(u-h) * 4.0 *iTime/60., // color & shadow pattern
          n = 6.2832; // pi2

    vec3 c = (1.-max(k.x, dot(k, o))*2.) // hex tile
           * (sin(p*n)/2.+1.) // brightness pattern
           * (cos(radians(vec3(-95, -135, 170)) + p*n)*.5+.5) // color
           * (2.-length(v)); // darken screen edges

    C = vec4(c, 1);
}
edit: cleanup code 19/11/2024
edit: ouch playback shader was reversed, time issue, now fixed 27/11/2024

main

Code: Select all

' tweaks to shadertoy related code based on
' https://www.freebasic.net/forum/viewtopic.php?t=24462&hilit=shadertoy
' by djpeters
#include once "SDL2/SDL.bi"
#include once "GL/gl.bi"
#include once "GL/glext.bi"
' dir function and provides constants to use for the attrib_mask parameter and dates
#include once "vbcompat.bi"

dim screenwidth  as integer = 1280
dim screenheight as integer = 720
dim fullscreen   as boolean = false
dim glfullscreen as boolean = false
dim desktopw     as integer
dim desktoph     as integer
dim desktopr     as integer
' get desktop info
ScreenInfo desktopw, desktoph,,,desktopr

' init hitbox button
dim boundbox as sdl_rect
dim offsety  as single = 0.325f

' init mouse feedback
Type mousecoord
    x  As integer
    y  As integer
End Type
dim mouse        as mousecoord
dim mousebutton  as string
Dim              As Integer x, y, buttons, buttonsup, res
mouse.x = 0
mouse.y = 0

' setup shadertoy
dim w2 as integer
dim h2 as integer

type vec3
  as GLfloat x,y,z
end type

' internal screen dimension gl shader
dim as vec3 v3

' define opengl proc
#define glDefine(n) dim shared as PFN##n##PROC n
' shader
glDefine(glCreateShader)
glDefine(glDeleteShader)
glDefine(glShaderSource)
glDefine(glCompileShader)
glDefine(glGetShaderiv)
' program
glDefine(glCreateProgram)
glDefine(glDeleteProgram)
glDefine(glAttachShader)
glDefine(glDetachShader)
glDefine(glLinkProgram)
glDefine(glGetProgramiv)
glDefine(glUseProgram)
' uniform
glDefine(glGetUniformLocation)
glDefine(glUniform1f)
glDefine(glUniform2f)
glDefine(glUniform3f)
glDefine(glUniform4f)
glDefine(glUniform1i)
#undef glDefine

type ShaderToy
    declare destructor
    declare function CompileFile(Filename as string) as boolean
    declare function CompileCode(Code as string) as boolean
    as GLuint FragmentShader
    as GLuint ProgramObject
end type
destructor ShaderToy
    if ProgramObject then
        glUseprogram(0)
        if FragmentShader  then
            glDetachShader(ProgramObject,FragmentShader)
            glDeleteShader(FragmentShader)
        end if
        glDeleteProgram(ProgramObject)
    end if
end destructor

dim as ShaderToy Shader
dim as integer mx,my,mb
dim as double  tStart = Timer()
dim as double  tLast  = tStart
dim as double  tNow   = Timer()
Dim As integer epoch
dim as string  filename

function ShaderToy.CompileCode(UserCode as string) as boolean
    dim as GLint logSize
    dim as GLint status
    dim as string FragmentProlog
    FragmentProlog & =!"uniform float     iGlobalTime;  // shader playback time (in seconds) deprecated in 2017\n"
    FragmentProlog & =!"uniform float     iTime;        // shader playback time (in seconds)\n"
    FragmentProlog & =!"uniform vec3      iResolution;  // viewport resolution (in pixels)\n"
    FragmentProlog & =!"uniform vec4      iMouse;       // mouse pixel coords. xy: current (if MLB down), zw: click\n"
    FragmentProlog & =!"uniform vec4      iDate;        // (year, month, day, time in seconds)\n"
    FragmentProlog & =!"uniform sampler2D iChannel0;\n"
    FragmentProlog & =!"uniform sampler2D iChannel1;\n"
    FragmentProlog & =!"uniform sampler2D iChannel2;\n"
    FragmentProlog & =!"uniform sampler2D iChannel3;\n"
    dim as string FragmentEpilog
    FragmentEpilog &= !"void main() {\n"
    FragmentEpilog &= !"  vec4 color;\n"
    FragmentEpilog &= !"  // call user shader\n"
    FragmentEpilog &= !"  mainImage(color, gl_FragCoord.xy);\n"
    FragmentEpilog &= !"  color.w = 1.0;\n"
    FragmentEpilog &= !"  gl_FragColor = color;\n"
    FragmentEpilog &= !"}\n"

    dim as string FragmentCode = FragmentProlog & UserCode & FragmentEpilog
    ' cleanup previous shader  
    glUseprogram(0)
    glDeleteProgram(ProgramObject)  : ProgramObject  = 0

    FragmentShader = glCreateShader(GL_FRAGMENT_SHADER)

    if FragmentShader=0 then
        'logentry("error", "glCreateShader(GL_FRAGMENT_SHADER) failed.")
        return false
    end if
    dim as GLchar ptr pCode=strptr(FragmentCode)
    glShaderSource (FragmentShader, 1, @pCode, NULL)
    glCompileShader(FragmentShader)
    glGetShaderiv  (FragmentShader, GL_COMPILE_STATUS, @status)
    if status = GL_FALSE then
        glGetShaderiv(FragmentShader, GL_INFO_LOG_LENGTH, @logSize)
        'logentry("error", "glCompileShader(FragmentShader) failed ")
        glDeleteShader(FragmentShader) : FragmentShader = 0
        return false
    end if

    ProgramObject = glCreateProgram()
    if ProgramObject = 0 then
        'logentry("error", "glCreateProgram() failed.")
        glDeleteShader(FragmentShader) : FragmentShader = 0
        return false
    end if
    glAttachShader(ProgramObject,FragmentShader)
    glLinkProgram (ProgramObject)
    glGetProgramiv(ProgramObject, GL_LINK_STATUS, @status)
    if (status = GL_FALSE) then
        glGetProgramiv(ProgramObject, GL_INFO_LOG_LENGTH, @logSize)
        'logentry("error", "glLinkProgram() failed.")
        glDeleteShader(FragmentShader) : FragmentShader = 0
        return false
    end if

    ' cleanup aka detach shader  
    glDeleteShader (FragmentShader) : FragmentShader = 0
    logSize   = 0

    return true

end function

function ShaderToy.CompileFile(filename as string) as boolean
    dim as string code
    var hFile = FreeFile()
    if open(filename,for input, as #hFile) then
        'logentry("error", filename + " shader path or file not found.")
        return false
    end if
    while not eof(hFile)
        dim as string aLine
        line input #hFile,aLine
        code &= aLine & !"\n"
    wend
    close #hFile
    return CompileCode(code)
end function

' setup sdl windows
SDL_Init(SDL_INIT_VIDEO)
' disable specific subsytems sdl
SDL_QuitSubSystem(SDL_INIT_AUDIO)
SDL_QuitSubSystem(SDL_INIT_HAPTIC)
' filter non used events
SDL_EventState(SDL_FINGERMOTION,    SDL_IGNORE)
SDL_EventState(SDL_FINGERDOWN,      SDL_IGNORE)
SDL_EventState(SDL_FINGERUP,        SDL_IGNORE)
SDL_EventState(SDL_MULTIGESTURE,    SDL_IGNORE)
SDL_EventState(SDL_DOLLARGESTURE,   SDL_IGNORE)
SDL_EventState(SDL_JOYBALLMOTION,   SDL_IGNORE)
SDL_EventState(SDL_DROPFILE,        SDL_IGNORE)
' set this to capture clicks normaly going to focused innerwindow
SDL_SetHint(SDL_HINT_MOUSE_FOCUS_CLICKTHROUGH, "1")
Dim glass   as SDL_Window ptr
Dim glglass as SDL_Window ptr
Dim         As SDL_Renderer Ptr renderer 
Dim         As SDL_Color backgrondcolor = (75, 85, 95, 0)
Dim         As SDL_Event event

initsdl:
if fullscreen then
    glass = SDL_CreateWindow("sdl2", null, null, screenwidth, screenheight, SDL_WINDOW_BORDERLESS)
else
    glass = SDL_CreateWindow("sdl2", 100, 100, screenwidth, screenheight, SDL_WINDOW_RESIZABLE)
end if

' create a window with an opengl context
if glfullscreen then
    glglass = SDL_CreateWindow("sdl2 opengl", null, null, screenwidth, screenheight, SDL_WINDOW_OPENGL _
                                or SDL_WINDOW_BORDERLESS or SDL_WINDOW_SHOWN)
else
    glglass = SDL_CreateWindow("sdl2 opengl", 100, 100, screenwidth * 0.35f, screenheight * 0.35f, SDL_WINDOW_OPENGL _
                                or SDL_WINDOW_BORDERLESS or SDL_WINDOW_SHOWN)
end if

' needed to force focus on regular sdl window
SDL_RaiseWindow(glass)
SDL_SetWindowInputFocus(glass)

renderer = SDL_CreateRenderer(glass, -1, SDL_RENDERER_ACCELERATED Or SDL_RENDERER_PRESENTVSYNC)
if (renderer = NULL) Then	
    'logentry("error", "sdl2 could not create renderer")
	SDL_Quit()
EndIf

' create opengl context bound to sdl
Dim As SDL_GLContext glContext = SDL_GL_CreateContext(glglass)
#define glProc(n) n = SDL_GL_GetProcAddress(#n) : if n = 0 then print "shadertoy opengl proc issue"
' shader
glProc(glCreateShader)
glProc(glDeleteShader)
glProc(glShaderSource)
glProc(glCompileShader)
glProc(glGetShaderiv)
' program
glProc(glCreateProgram)
glProc(glDeleteProgram)
glProc(glAttachShader)
glProc(glDetachShader)
glProc(glLinkProgram)
glProc(glGetProgramiv)
glProc(glUseProgram)
' uniform
glProc(glGetUniformLocation)
glProc(glUniform1f)
glProc(glUniform2f)
glProc(glUniform3f)
glProc(glUniform4f)
glProc(glUniform1i)
#undef glProc

' load shader code and compile it
if filename = "" then
    filename = exepath + "\theeye.gls"
end if
shader.CompileFile(filename)

' main
Dim running As Boolean = True

While running
    ' make sure gl window is on top
    SDL_RaiseWindow(glglass)
    ' keep gl window in place relative to regular sdl window
    SDL_GetWindowPosition(glass, @w2, @h2)

    if glfullscreen then
        sdl_setwindowposition(glglass, w2, h2)
        ' set vec3 iResolution
            v3.x = screenwidth
            v3.y = screenheight
            v3.z = v3.x/v3.y
    else
        SDL_SetWindowPosition(glglass,_
        ((w2 + screenwidth) - screenwidth * 0.35f) - (screenwidth * 0.05f),_
        ((h2 + screenheight) - screenheight * 0.25f) - (screenheight * 0.4f))
        ' set vec3 iResolution
            v3.x = screenwidth  * 0.35f
            v3.y = screenheight * 0.35f
            v3.z = v3.x/v3.y
    end if
    ' enable shader
    glUseProgram(Shader.ProgramObject)
    tNow = Timer()

    ' get uniforms locations in shader program
    var iGlobalTime = glGetUniformLocation(Shader.ProgramObject,"iGlobalTime")
    var iTime       = glGetUniformLocation(Shader.ProgramObject,"iTime")
    var iResolution = glGetUniformLocation(Shader.ProgramObject,"iResolution")
    var iMouse      = glGetUniformLocation(Shader.ProgramObject,"iMouse")
    var iDate       = glGetUniformLocation(Shader.ProgramObject,"iDate")
    glUniform3f(iResolution, v3.x, v3.y, v3.z)
    glUniform4f(idate, year(now), month(now), day(now), (hour(now) * 60 * 60) + (minute(now) * 60) + second(now) + (epoch - fix(epoch)))
    glUniform1f(iGlobalTime, tNow-tStart)
    glUniform1f(iTime,tNow - tStart)
    glRectf (-1.0, -1.0, 1.0, 1.0)

    ' Update the screen
    SDL_GL_SwapWindow(glglass)

    ' render main sdl window
    SDL_SetRenderDrawColor(renderer, backgrondcolor.r, backgrondcolor.g, backgrondcolor.b, backgrondcolor.a )
    
    SDL_RenderClear(renderer)
        SDL_SetRenderDrawColor(renderer, 255, 255, 255, 0)
        boundbox.x = screenwidth  * 0.015f
        boundbox.y = screenheight * offsety
        boundbox.w = 400
        boundbox.h = 100
        SDL_SetRenderDrawColor(renderer, 0, 255, 0, 0)
        SDL_RenderDrawRect(renderer, @boundbox)
        SDL_RenderFillRect(renderer, @boundbox)
        boundbox.y = screenheight * offsety + 120
        SDL_SetRenderDrawColor(renderer, 0, 0, 255, 0)
        SDL_RenderDrawRect(renderer, @boundbox)
        SDL_RenderFillRect(renderer, @boundbox)
        boundbox.y = screenheight * offsety + 240
        SDL_SetRenderDrawColor(renderer, 255, 0, 0, 0)
        SDL_RenderDrawRect(renderer, @boundbox)
        SDL_RenderFillRect(renderer, @boundbox)
    SDL_RenderPresent(renderer)


    While SDL_PollEvent(@event)
        select case event.type
            case SDL_KEYDOWN and event.key.keysym.sym = SDLK_ESCAPE
                running = False
            case SDL_WINDOWEVENT and event.window.event = SDL_WINDOWEVENT_CLOSE
                running = False
            case SDL_WINDOWEVENT and event.window.event = SDL_WINDOWEVENT_MINIMIZED
                SDL_HideWindow(glglass)
                SDL_MinimizeWindow(glass)
            case SDL_WINDOWEVENT and event.window.event = SDL_WINDOWEVENT_RESTORED
                SDL_ShowWindow(glglass)
                SDL_RestoreWindow(glass)
            case SDL_WINDOWEVENT and event.window.event = SDL_WINDOWEVENT_RESIZED
                SDL_GetWindowPosition(glass, @w2, @h2)
                sdl_setwindowposition(glglass, w2, h2)
            case SDL_KEYDOWN and event.key.keysym.sym = SDLK_F11
                SDL_GL_DeleteContext(glContext)
                SDL_DestroyRenderer(renderer)
                SDL_DestroyWindow(glass)
                SDL_DestroyWindow(glglass)
                select case fullscreen
                    case true
                        ' enable or disable mouse cursor in window
                        screenwidth  = 1280
                        screenheight = 720
                        fullscreen = false
                        goto initsdl
                    case false
                        screenwidth  = desktopw
                        screenheight = desktoph
                        fullscreen = true
                        sdl_setwindowposition(glglass, 0, 0)
                        goto initsdl
                end select
            ' navigation keyboard arrow keys and select
            case SDL_KEYDOWN and event.key.keysym.sym = SDLK_LEFT
                SDL_WarpMouseInWindow(glass, mouse.x - 50, mouse.y)
            case SDL_KEYDOWN and event.key.keysym.sym = SDLK_RIGHT
                SDL_WarpMouseInWindow(glass, mouse.x + 50, mouse.y)
            case SDL_KEYDOWN and event.key.keysym.sym = SDLK_DOWN
                SDL_WarpMouseInWindow(glass, mouse.x, mouse.y + 50)
            case SDL_KEYDOWN and event.key.keysym.sym = SDLK_UP
                SDL_WarpMouseInWindow(glass, mouse.x, mouse.y - 50)
            case SDL_KEYDOWN and event.key.keysym.sym = SDLK_RETURN
                ' fake mouse button left
                mousebutton = "left"
            case SDL_KEYUP and event.key.keysym.sym = SDLK_RETURN
                ' fake mouse button left
                mousebutton = "left"
                event.type = SDL_MOUSEBUTTONDOWN
                event.button.button = SDL_BUTTON_LEFT
                SDL_PushEvent(@event)
            case SDL_MOUSEMOTION
                mouse.x = event.motion.x 
                mouse.y = event.motion.y
                if (mouse.y > screenheight * offsety and mouse.y < boundbox.y + 340 and mouse.x < boundbox.x + 400)_ 
                    or (mouse.x > screenwidth * 0.35f and mouse.y > screenheight * 0.5f) then
                    SDL_SetCursor(SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_HAND))
                else
                    SDL_SetCursor(SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_ARROW))    
                end if
            case SDL_MOUSEBUTTONDOWN
                select case event.button.button
                    case SDL_BUTTON_LEFT
                        mousebutton = "left"
                        if (mouse.y > screenheight * offsety and mouse.y < boundbox.y + 100 and mouse.x < boundbox.x + 400) then
                            filename = exepath + "\hextile.gls"
                        end if
                        if (mouse.y > screenheight * offsety + 120 and mouse.y < boundbox.y + 220 and mouse.x < boundbox.x + 400) then
                            filename = exepath + "\aflowof33.gls"
                        end if
                        if (mouse.y > screenheight * offsety + 240 and mouse.y < boundbox.y + 340 and mouse.x < boundbox.x + 400) then
                            filename = exepath + "\theeye.gls"
                        end if
                        ' toggle fullscreen shader
                        if mouse.x > screenwidth * 0.35f and mouse.y > screenheight * 0.5f then
                            if glfullscreen then
                                glfullscreen = false
                            else
                                glfullscreen = true
                            end if
                            SDL_GL_DeleteContext(glContext)
                            SDL_DestroyRenderer(renderer)
                            SDL_DestroyWindow(glass)
                            SDL_DestroyWindow(glglass)
                            goto initsdl
                        end if
                        Shader.CompileFile(filename)
                    case SDL_BUTTON_MIDDLE
                        mousebutton = "middle"
                    case SDL_BUTTON_RIGHT
                        mousebutton = "right"
                    case else
                end select
        end select
    Wend
    SDL_SetWindowTitle(glass, "shadertoy sdl2 | coord: " &  mouse.x & ":" & mouse.y & " | button: " + mousebutton & " | file: " & filename)
    ' reduce cpu usage affects shader animation
    SDL_Delay(25)
Wend

' clean up
SDL_GL_DeleteContext(glContext)
SDL_DestroyRenderer(renderer)
SDL_DestroyWindow(glass)
SDL_DestroyWindow(glglass)
SDL_Quit()
end
Mucho kudos and courtesy to
djpeters: https://www.freebasic.net/forum/viewtop ... =shadertoy
shaders: gbirbilis, pik33 and ChunderFPV

A integrated example of the sdl shadertoy
code can be found at:
https://github.com/thrive4/app.fb.slideshow/releases
Post Reply