;;; A triangle that follows the mouse pointer, spins while the left ;;; mouse button is held down, and quits on Escape (or on closing the ;;; window). ;;; ;;; SDL3 supplies the window, the GL context and the events. Everything ;;; drawn goes through an OpenGL 3.3 core-profile pipeline: a vertex and ;;; fragment shader, and one VAO/VBO holding a unit triangle. Where the ;;; triangle is and how far it has spun are passed in as uniforms, so ;;; the geometry itself is uploaded once and never touched again. ;;; ;;; Build, macOS: ;;; ./sexc example/sdl3-triangle.sex -o sdl3-triangle -- `pkg-config --cflags --libs sdl3` -framework OpenGL ;;; ;;; Build, Linux: ;;; ./sexc example/sdl3-triangle.sex -o sdl3-triangle -- `pkg-config --cflags --libs sdl3 gl` ;;; ;;; The GL header is the one platform difference, and #+ / #- picks it. ;;; Apple keeps the core-profile entry points in and ;;; links them through -framework OpenGL, which no pkg-config file ;;; describes; everywhere else the prototypes come from ;;; with GL_GLEXT_PROTOTYPES defined, and -lGL -- `pkg-config --libs ;;; gl' -- resolves them. SDL3/SDL_opengl.h is not the shortcut it ;;; looks like: on Apple it resolves to the 2.1 header, which has no ;;; glGenVertexArrays and so cannot bind the VAO this program needs. #+macosx (define GL-SILENCE-DEPRECATION 1) ; OpenGL is deprecated on macOS (include SDL3/SDL.h) #+macosx (include OpenGL/gl3.h) #-macosx (define GL-GLEXT-PROTOTYPES 1) #-macosx (include GL/gl.h) #-macosx (include GL/glext.h) (define WINDOW-WIDTH 800) (define WINDOW-HEIGHT 600) (define TRIANGLE-RADIUS 70.0) ; in window units (define SPIN-SPEED 3.0) ; radians per second ;;; The vertex shader does the whole transform: spin the unit triangle ;;; by u_angle, scale it to u_radius, move it to u_center, then convert ;;; from window coordinates to clip space. That way a frame only has to ;;; push four uniforms rather than rebuild any geometry. (var vertex-shader-src (* const char) " #version 330 core layout (location = 0) in vec2 a_pos; layout (location = 1) in vec3 a_color; uniform vec2 u_center; // triangle centre, in window units uniform vec2 u_viewport; // window size, same units as u_center uniform float u_angle; // current spin, in radians uniform float u_radius; // triangle size, in window units out vec3 v_color; void main() { float s = sin(u_angle); float c = cos(u_angle); vec2 spun = vec2(a_pos.x * c - a_pos.y * s, a_pos.x * s + a_pos.y * c); vec2 p = spun * u_radius + u_center; // Window coordinates have their origin top-left with y growing // downwards; clip space is centred with y growing upwards. gl_Position = vec4(p.x / u_viewport.x * 2.0 - 1.0, 1.0 - p.y / u_viewport.y * 2.0, 0.0, 1.0); v_color = a_color; } ") (var fragment-shader-src (* const char) " #version 330 core in vec3 v_color; out vec4 frag_color; void main() { frag_color = vec4(v_color, 1.0); } ") (fn compile-shader ((kind GLenum) (src (* const char))) GLuint (var shader GLuint (glCreateShader kind)) (glShaderSource shader 1 (& src) NULL) (glCompileShader shader) (var ok GLint 0) (glGetShaderiv shader GL-COMPILE-STATUS (& ok)) (if (== ok 0) (do (var info [char 1024]) (glGetShaderInfoLog shader 1024 NULL info) (SDL-Log "shader compilation failed: %s" info) (glDeleteShader shader) (return 0))) (return shader)) (fn make-program () GLuint (var vertex-shader GLuint (compile-shader GL-VERTEX-SHADER vertex-shader-src)) (var fragment-shader GLuint (compile-shader GL-FRAGMENT-SHADER fragment-shader-src)) (if (c-or (== vertex-shader 0) (== fragment-shader 0)) (do (glDeleteShader vertex-shader) (glDeleteShader fragment-shader) (return 0))) (var program GLuint (glCreateProgram)) (glAttachShader program vertex-shader) (glAttachShader program fragment-shader) (glLinkProgram program) ;; The shaders are only needed until the program is linked; the ;; program holds its own reference until then. (glDeleteShader vertex-shader) (glDeleteShader fragment-shader) (var ok GLint 0) (glGetProgramiv program GL-LINK-STATUS (& ok)) (if (== ok 0) (do (var info [char 1024]) (glGetProgramInfoLog program 1024 NULL info) (SDL-Log "program linking failed: %s" info) (glDeleteProgram program) (return 0))) (return program)) (pub fn main () int (if (! (SDL-Init SDL-INIT-VIDEO)) (do (SDL-Log "SDL_Init failed: %s" (SDL-GetError)) (return 1))) ;; Ask for core profile 3.3 before the window exists: these attributes ;; are read when the context is created. (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-DOUBLEBUFFER 1) (var window (* SDL-Window) (SDL-CreateWindow "Sex + SDL3 + OpenGL" WINDOW-WIDTH WINDOW-HEIGHT SDL-WINDOW-OPENGL)) (if (== window NULL) (do (SDL-Log "SDL_CreateWindow failed: %s" (SDL-GetError)) (SDL-Quit) (return 1))) (var gl-context SDL-GLContext (SDL-GL-CreateContext window)) (if (== gl-context NULL) (do (SDL-Log "SDL_GL_CreateContext failed: %s" (SDL-GetError)) (SDL-DestroyWindow window) (SDL-Quit) (return 1))) (SDL-GL-MakeCurrent window gl-context) (SDL-GL-SetSwapInterval 1) (var program GLuint (make-program)) (if (== program 0) (do (SDL-GL-DestroyContext gl-context) (SDL-DestroyWindow window) (SDL-Quit) (return 1))) ;; A unit triangle, two position components then three colour ;; components per vertex. The vertices sit on the unit circle at 90, ;; 210 and 330 degrees; the shader spins, scales and moves it. (var verts [GLfloat 15] #( 0.000 1.000 1.00 0.35 0.35 -0.866 -0.500 0.35 1.00 0.45 0.866 -0.500 0.40 0.50 1.00)) (var vao GLuint 0) (var vbo GLuint 0) (glGenVertexArrays 1 (& vao)) (glBindVertexArray vao) (glGenBuffers 1 (& vbo)) (glBindBuffer GL-ARRAY-BUFFER vbo) (glBufferData GL-ARRAY-BUFFER (sizeof verts) verts GL-STATIC-DRAW) (var stride GLsizei (cast (* 5 (sizeof GLfloat)) GLsizei)) (glVertexAttribPointer 0 2 GL-FLOAT GL-FALSE stride (cast 0 (* void))) (glEnableVertexAttribArray 0) (glVertexAttribPointer 1 3 GL-FLOAT GL-FALSE stride (cast (* 2 (sizeof GLfloat)) (* void))) (glEnableVertexAttribArray 1) (var u-center GLint (glGetUniformLocation program "u_center")) (var u-viewport GLint (glGetUniformLocation program "u_viewport")) (var u-angle GLint (glGetUniformLocation program "u_angle")) (var u-radius GLint (glGetUniformLocation program "u_radius")) (var running bool true) (var angle float 0.0) (var last-ticks Uint64 (SDL-GetTicks)) (var event SDL-Event) (while running (while (SDL-PollEvent (& event)) (switch (. event type) (case SDL-EVENT-QUIT (= running false)) (case SDL-EVENT-KEY-DOWN (if (== (. event key key) SDLK-ESCAPE) (= running false))))) ;; Seconds since the previous frame, so the spin rate does not ;; depend on how fast we happen to be rendering. (var now Uint64 (SDL-GetTicks)) (var dt float (/ (cast (- now last-ticks) float) 1000.0)) (= last-ticks now) (var mouse-x float 0.0) (var mouse-y float 0.0) (var buttons SDL-MouseButtonFlags (SDL-GetMouseState (& mouse-x) (& mouse-y))) (if (!= 0 (& buttons SDL-BUTTON-LMASK)) (= angle (+ angle (* SPIN-SPEED dt)))) ;; The viewport is in pixels, which is not the same as window units ;; on a HiDPI display; the mouse position is in window units, so the ;; shader needs that size rather than the pixel one. (var pixel-width int 0) (var pixel-height int 0) (SDL-GetWindowSizeInPixels window (& pixel-width) (& pixel-height)) (glViewport 0 0 pixel-width pixel-height) (var window-width int 0) (var window-height int 0) (SDL-GetWindowSize window (& window-width) (& window-height)) (glClearColor 0.06 0.06 0.09 1.0) (glClear GL-COLOR-BUFFER-BIT) (glUseProgram program) (glUniform2f u-center mouse-x mouse-y) (glUniform2f u-viewport (cast window-width float) (cast window-height float)) (glUniform1f u-angle angle) (glUniform1f u-radius TRIANGLE-RADIUS) (glBindVertexArray vao) (glDrawArrays GL-TRIANGLES 0 3) (SDL-GL-SwapWindow window)) (glDeleteVertexArrays 1 (& vao)) (glDeleteBuffers 1 (& vbo)) (glDeleteProgram program) (SDL-GL-DestroyContext gl-context) (SDL-DestroyWindow window) (SDL-Quit) (return 0))