Last active
June 21, 2025 05:38
-
-
Save MurageKibicho/c3f30f47b3b2e9eeb11064c5f5be86b7 to your computer and use it in GitHub Desktop.
SDL Bare minimum to get mouse scroll zoom working with OpenGL and Emscripten
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#ifdef __EMSCRIPTEN__ | |
#include <emscripten.h> | |
#include <SDL.h> | |
#include <SDL_opengles2.h> | |
#else | |
#include <SLD2/SDL.h> | |
#include <SDL2/SDL_opengles2.h> | |
#endif | |
#include <stdlib.h> | |
#include "cglm/cglm.h" | |
#include "cglm/call.h" | |
//Run : emcc HelloTriangle.c -s USE_SDL=2 -s FULL_ES2=1 -s WASM=1 -Icglm/include -DCGLM_HEADER_ONLY -o HelloTriangle.html | |
//Preview : emrun HelloTriangle.html | |
typedef struct event_handler_struct *EventHandler; | |
typedef struct camera_struct *Camera; | |
struct camera_struct | |
{ | |
bool cameraUpdated; | |
bool windowResized; | |
int windowWidth; | |
int windowHeight; | |
GLfloat viewportX; | |
GLfloat viewportY; | |
GLfloat basePanX; | |
GLfloat basePanY; | |
GLfloat regPanX; | |
GLfloat regPanY; | |
GLfloat zoom; | |
GLfloat aspect; | |
GLfloat zoomMin; | |
GLfloat zoomMax; | |
}; | |
struct event_handler_struct | |
{ | |
//Window | |
SDL_Window *window; | |
Uint32 windowID; | |
//Mouse input | |
bool mouseButtonDown; | |
float mouseWheelDelta; | |
int mouseButtonDownX; | |
int mouseButtonDownY; | |
int mousePositionX; | |
int mousePositionY; | |
//Finger input | |
bool fingerDown; | |
int fingerDownX; | |
int fingerDownY; | |
int fingerDownID; | |
//Pinch input | |
bool pinchDown; | |
float pinchZoomDelta; | |
float pinchScale; | |
//Global Camera | |
Camera camera; | |
GLint shaderPan; | |
GLint shaderZoom; | |
GLint shaderAspect; | |
}; | |
float clamp(float val, float lo, float hi) { | |
return (val < lo) ? lo : ((val > hi) ? hi : val); | |
} | |
Camera CreateCamera() | |
{ | |
Camera camera = malloc(sizeof(struct camera_struct)); | |
camera->cameraUpdated = false; | |
camera->windowResized = false; | |
camera->windowWidth = 0; | |
camera->windowHeight = 0; | |
camera->viewportX = 0.0f; | |
camera->viewportY = 0.0f; | |
camera->basePanX = 0.0f; | |
camera->basePanY = 0.0f; | |
camera->regPanX = 0.0f; | |
camera->regPanY = 0.0f; | |
camera->zoom = 1.0f; | |
camera->aspect = 1.0f; | |
camera->zoomMin = 0.1f; | |
camera->zoomMax = 10.0f; | |
return camera; | |
} | |
void Camera_SetWindowSize(Camera camera, int windowWidth, int windowHeight) | |
{ | |
if(camera->windowWidth != windowWidth || camera->windowHeight != windowHeight) | |
{ | |
camera->windowResized = true; | |
camera->cameraUpdated = true; | |
camera->windowWidth = windowWidth; | |
camera->windowHeight = windowHeight; | |
camera->viewportX = (float)windowWidth; | |
camera->viewportY = (float)windowHeight; | |
camera->aspect = (float)windowWidth / (float) windowHeight; | |
} | |
} | |
void Camera_DeviceToWorldCoords(Camera camera, float deviceX, float deviceY, float *worldX, float *worldY) | |
{ | |
*worldX = deviceX / camera->zoom - camera->regPanX; | |
*worldY = deviceY / camera->aspect / camera->zoom - camera->regPanY; | |
} | |
void Camera_WindowToDeviceCoords(Camera camera, int windowX, int windowY, float *deviceX, float *deviceY) | |
{ | |
float normalizedWindowX = windowX / (float)camera->windowWidth; | |
float normalizedWindowY = windowY / (float)camera->windowHeight; | |
*deviceX = (normalizedWindowX - 0.5f) * 2.0f; | |
*deviceY = (1.0f - normalizedWindowY - 0.5f) * 2.0f; | |
} | |
void Camera_WindowToWorldCoords(Camera camera, int windowX, int windowY, float *worldX, float *worldY) | |
{ | |
float deviceX = 0.0f;float deviceY = 0.0f; | |
Camera_WindowToDeviceCoords(camera, windowX, windowY, &deviceX, &deviceY); | |
Camera_DeviceToWorldCoords(camera, windowX, windowY, &deviceX, &deviceY); | |
} | |
void Camera_SetZoomDelta(Camera camera, GLfloat zoomDelta) | |
{ | |
camera->zoom = clamp(camera->zoom + zoomDelta, camera->zoomMin, camera->zoomMax); | |
camera->cameraUpdated = true; | |
} | |
void Camera_SetPanDelta(Camera camera, GLfloat deltaWorldX, GLfloat deltaWorldY) | |
{ | |
camera->regPanX += deltaWorldX; | |
camera->regPanY += deltaWorldY; | |
camera->cameraUpdated = true; | |
} | |
void EventHandler_ResizeWindow(EventHandler eventHandler, int windowWidth, int windowHeight) | |
{ | |
//Set OpenGL viewport | |
glViewport(0, 0, windowWidth, windowHeight); | |
Camera_SetWindowSize(eventHandler->camera, windowWidth, windowHeight); | |
} | |
EventHandler CreateEventHandler(char *windowName, int windowWidth, int windowHeight) | |
{ | |
EventHandler eventHandler = malloc(sizeof(struct event_handler_struct)); | |
//Initialize SDL window | |
eventHandler->window = SDL_CreateWindow(windowName, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,windowWidth, windowHeight, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_SHOWN); | |
eventHandler->windowID = SDL_GetWindowID(eventHandler->window); | |
//Initialize mouse handlers | |
eventHandler->mouseButtonDown = false; | |
eventHandler->mouseWheelDelta = 0.05f; | |
eventHandler->mouseButtonDownX = 0; | |
eventHandler->mouseButtonDownY = 0; | |
eventHandler->mousePositionX = 0; | |
eventHandler->mousePositionY = 0; | |
//Initialize finger handlers | |
eventHandler->fingerDown = false; | |
eventHandler->fingerDownX = 0.0f; | |
eventHandler->fingerDownY = 0.0f; | |
eventHandler->fingerDownID = 0; | |
//Initialize pinch handlers | |
eventHandler->pinchDown = false; | |
eventHandler->pinchZoomDelta = 0.001f; | |
eventHandler->pinchScale = 8.0f; | |
//Create camera | |
eventHandler->camera = CreateCamera(); | |
eventHandler->shaderPan = 0; | |
eventHandler->shaderZoom = 0; | |
eventHandler->shaderAspect = 0; | |
//Create OpenGLES2 context inside SDL Window | |
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2); | |
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); | |
SDL_GL_SetSwapInterval(1); | |
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); | |
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); | |
//Create OpenGL Context | |
SDL_GLContext openGLContext = SDL_GL_CreateContext(eventHandler->window); | |
if(openGLContext == NULL){SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Could not create OpenGL Context: %s\n", SDL_GetError());exit(1);} | |
printf("INFO: GL version: %s\n", glGetString(GL_VERSION)); | |
//OpenGL Set Background Color to white | |
glClearColor(0.0f, 0.0f, 0.0f, 1.0f); | |
//Call GL Clear | |
glClear(GL_COLOR_BUFFER_BIT); | |
//Change windowWidth and windowHeight to OpenGL window's actual size in pixels | |
int newWidth = 0;int newHeight=0; | |
SDL_GL_GetDrawableSize(eventHandler->window, &newWidth, &newHeight); | |
windowWidth = newWidth;windowHeight = newHeight; | |
printf("INFO: GL window size = %dx%d\n", windowWidth, windowHeight); | |
EventHandler_ResizeWindow(eventHandler, windowWidth, windowHeight); | |
return eventHandler; | |
} | |
void EventHandler_ZoomMouse(EventHandler eventHandler, bool mouseWheelDown) | |
{ | |
float beforeZoomX = 0.0f;float beforeZoomY = 0.0f; | |
float afterZoomX = 0.0f;float afterZoomY = 0.0f; | |
Camera_WindowToWorldCoords(eventHandler->camera, eventHandler->mousePositionX, eventHandler->mousePositionY, &beforeZoomX, &beforeZoomY); | |
float zoomDelta = mouseWheelDown ? -eventHandler->mouseWheelDelta : eventHandler->mouseWheelDelta; | |
Camera_SetZoomDelta(eventHandler->camera, zoomDelta); | |
float deltaWorldX = afterZoomX - beforeZoomX; | |
float deltaWorldY = afterZoomY - beforeZoomY; | |
Camera_SetPanDelta(eventHandler->camera, deltaWorldX, deltaWorldY); | |
} | |
const GLchar* vertexSource = | |
"uniform vec2 pan; \n" | |
"uniform float zoom; \n" | |
"uniform float aspect; \n" | |
"attribute vec4 position; \n" | |
"varying vec3 color; \n" | |
"void main() \n" | |
"{ \n" | |
" gl_Position = vec4(position.xyz, 1.0); \n" | |
" gl_Position.xy += pan; \n" | |
" gl_Position.xy *= zoom; \n" | |
" gl_Position.y *= aspect; \n" | |
" color = gl_Position.xyz + vec3(0.5); \n" | |
"} \n"; | |
// Fragment/pixel shader | |
const GLchar* fragmentSource = | |
"precision mediump float; \n" | |
"varying vec3 color; \n" | |
"void main() \n" | |
"{ \n" | |
" gl_FragColor = vec4 ( color, 1.0 ); \n" | |
"} \n"; | |
void UpdateShader(EventHandler eventHandler) | |
{ | |
// Get the current shader program | |
GLuint shaderProgram; | |
glGetIntegerv(GL_CURRENT_PROGRAM, (GLint*)&shaderProgram); | |
// Get uniform locations (could cache these for better performance) | |
eventHandler->shaderPan = glGetUniformLocation(shaderProgram, "pan"); | |
eventHandler->shaderZoom = glGetUniformLocation(shaderProgram, "zoom"); | |
eventHandler->shaderAspect = glGetUniformLocation(shaderProgram, "aspect"); | |
// Create a vec2 for pan (since your camera stores pan components separately) | |
GLfloat pan[2] = {eventHandler->camera->regPanX, eventHandler->camera->regPanY}; | |
// Set the uniform values | |
glUniform2fv(eventHandler->shaderPan, 1, pan); | |
glUniform1f(eventHandler->shaderZoom , eventHandler->camera->zoom); | |
glUniform1f(eventHandler->shaderAspect, eventHandler->camera->aspect); | |
} | |
GLuint InitializeShader() | |
{ | |
// Create and compile vertex shader | |
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER); | |
glShaderSource(vertexShader, 1, &vertexSource, NULL); | |
glCompileShader(vertexShader); | |
//Check for compilation issues in vertex shader | |
GLint success; | |
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success); | |
if(!success) | |
{ | |
char compilationErrorString[512]; | |
glGetShaderInfoLog(vertexShader, 512, NULL, compilationErrorString); | |
printf("Vertex shader compilation failed: %s\n", compilationErrorString); | |
} | |
// Create and compile fragment shader | |
GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); | |
glShaderSource(fragmentShader, 1, &fragmentSource, NULL); | |
glCompileShader(fragmentShader); | |
//Check for compilation issues in fragmentShader | |
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success); | |
if(!success) | |
{ | |
char compilationErrorString[512]; | |
glGetShaderInfoLog(fragmentShader, 512, NULL, compilationErrorString); | |
printf("Fragment shader compilation failed: %s\n", compilationErrorString); | |
} | |
// Link vertex and fragment shader into shader program and use it | |
GLuint shaderProgram = glCreateProgram(); | |
glAttachShader(shaderProgram, vertexShader); | |
glAttachShader(shaderProgram, fragmentShader); | |
glLinkProgram(shaderProgram); | |
glUseProgram(shaderProgram); | |
// Cleanup: Detach and delete shaders (they're now part of the program) | |
glDetachShader(shaderProgram, vertexShader); | |
glDetachShader(shaderProgram, fragmentShader); | |
glDeleteShader(vertexShader); | |
glDeleteShader(fragmentShader); | |
return shaderProgram; | |
} | |
void InitializeTriangleGeometry(GLuint shaderProgram) | |
{ | |
// Create vertex buffer object and copy vertex data into it | |
GLuint vbo; | |
glGenBuffers(1, &vbo); | |
glBindBuffer(GL_ARRAY_BUFFER, vbo); | |
//Triangle Vertices | |
GLfloat vertices[] = | |
{ | |
0.0f, 0.5f, 0.0f, | |
-0.5f, -0.5f, 0.0f, | |
0.5f, -0.5f, 0.0f | |
}; | |
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); | |
// Specify the layout of the shader vertex data (positions only, 3 floats) | |
GLint posAttrib = glGetAttribLocation(shaderProgram, "position"); | |
glEnableVertexAttribArray(posAttrib); | |
glVertexAttribPointer(posAttrib, 3, GL_FLOAT, GL_FALSE, 0, 0); | |
} | |
void Redraw(SDL_Window *sdlWindow) | |
{ | |
// Clear screen | |
glClear(GL_COLOR_BUFFER_BIT); | |
// Draw the vertex buffer | |
glDrawArrays(GL_TRIANGLES, 0, 3); | |
// Swap front/back framebuffers | |
SDL_GL_SwapWindow(sdlWindow); | |
} | |
void ProcessEventsWithSDL(EventHandler eventHandler) | |
{ | |
SDL_Event event; | |
//Infinite loop to process Events | |
while(SDL_PollEvent(&event)) | |
{ | |
//Handle different event types using a switch | |
switch(event.type) | |
{ | |
//Handle quit | |
case SDL_QUIT: | |
exit(0); | |
break; | |
//Handle Resizing | |
case SDL_WINDOWEVENT: | |
if(event.window.windowID == eventHandler->windowID && event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) | |
{ | |
EventHandler_ResizeWindow(eventHandler, event.window.data1, event.window.data2); | |
} | |
break; | |
//Keyboard Events | |
case SDL_KEYDOWN: | |
printf("Key pressed: %s (SDL_Keycode: %d)\n", SDL_GetKeyName(event.key.keysym.sym),event.key.keysym.sym); | |
break; | |
case SDL_KEYUP: | |
printf("Key released: %s (SDL_Keycode: %d)\n", SDL_GetKeyName(event.key.keysym.sym),event.key.keysym.sym); | |
break; | |
//Mouse Events | |
case SDL_MOUSEMOTION: | |
printf("Mouse moved to (%d, %d)\n", event.motion.x, event.motion.y); | |
eventHandler->mousePositionX = event.motion.x; | |
eventHandler->mousePositionY = event.motion.y; | |
break; | |
case SDL_MOUSEBUTTONUP: | |
printf("Mouse button %d released\n", event.button.button); | |
break; | |
case SDL_MOUSEWHEEL: | |
printf("Mouse wheel scrolled: x=%d, y=%d\n", event.wheel.x, event.wheel.y); | |
printf("Zoom: %f, Pan: (%f, %f)\n", eventHandler->camera->zoom,eventHandler->camera->regPanX,eventHandler->camera->regPanY); | |
bool mouseWheelDown = (event.wheel.y < 0.0); | |
EventHandler_ZoomMouse(eventHandler, mouseWheelDown); | |
break; | |
//Touch Events | |
case SDL_FINGERDOWN: | |
printf("Finger %lld touched at (%.2f, %.2f)\n", event.tfinger.fingerId, event.tfinger.x, event.tfinger.y); | |
break; | |
case SDL_FINGERUP: | |
printf("Finger %lld lifted\n", event.tfinger.fingerId); | |
break; | |
case SDL_FINGERMOTION: | |
printf("Finger %lld moved to (%.2f, %.2f)\n", event.tfinger.fingerId, event.tfinger.x, event.tfinger.y); | |
break; | |
default: | |
break; | |
} | |
} | |
} | |
void MainLoop(void *mainLoopArgument) | |
{ | |
EventHandler eventHandler = *(EventHandler*)mainLoopArgument; | |
ProcessEventsWithSDL(eventHandler); | |
UpdateShader(eventHandler); | |
Redraw(eventHandler->window); | |
} | |
int main() | |
{ | |
if(SDL_Init(SDL_INIT_VIDEO) != 0){SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Could not Initialize Video: %s\n", SDL_GetError());exit(1);} | |
int windowWidth = 512; | |
int windowHeight= 512; | |
char *windowName = "HelloTriangle"; | |
EventHandler eventHandler = CreateEventHandler(windowName, windowWidth, windowHeight); | |
if(eventHandler == NULL){printf("Error: Could not create eventHandler\n");} | |
//Initialize shader and geometry | |
GLuint shaderProgram = InitializeShader(); | |
InitializeTriangleGeometry(shaderProgram); | |
//Start Emscripten main loop | |
void *mainLoopArgument = eventHandler; | |
#ifdef __EMSCRIPTEN__ | |
int fps = 0; | |
emscripten_set_main_loop_arg(MainLoop, &mainLoopArgument, fps, true); | |
#else | |
while(true) | |
{ | |
MainLoop(&mainLoopArgument); | |
} | |
#endif | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment