add support for 2d textures in sprite shader - fixes #386

Sun, 20 Apr 2025 15:41:16 +0200

author
Mike Becker <universe@uap-core.de>
date
Sun, 20 Apr 2025 15:41:16 +0200
changeset 88
6234b7ea48f3
parent 87
874a02a683c5
child 89
e1f682a8a145

add support for 2d textures in sprite shader - fixes #386

DEPENDENCIES file | annotate | diff | comparison | revisions
configure file | annotate | diff | comparison | revisions
make/project.xml file | annotate | diff | comparison | revisions
shader/sprite_frag.glsl file | annotate | diff | comparison | revisions
shader/sprite_vtx.glsl file | annotate | diff | comparison | revisions
src/ascension/2d/sprite.h file | annotate | diff | comparison | revisions
src/ascension/glcontext.h file | annotate | diff | comparison | revisions
src/ascension/shader.h file | annotate | diff | comparison | revisions
src/ascension/texture.h file | annotate | diff | comparison | revisions
src/context.c file | annotate | diff | comparison | revisions
src/font.c file | annotate | diff | comparison | revisions
src/glcontext.c file | annotate | diff | comparison | revisions
src/shader.c file | annotate | diff | comparison | revisions
src/sprite.c file | annotate | diff | comparison | revisions
src/texture.c file | annotate | diff | comparison | revisions
test/snake/snake.c file | annotate | diff | comparison | revisions
test/snake/textures/ship.png file | annotate | diff | comparison | revisions
--- a/DEPENDENCIES	Sat Apr 19 19:30:46 2025 +0200
+++ b/DEPENDENCIES	Sun Apr 20 15:41:16 2025 +0200
@@ -1,3 +1,3 @@
-Fedora: dnf install SDL2-devel SDL2_ttf-devel glew-devel
-Arch: pacman -Sy sdl2 sdl2_ttf glew pkgconf
-FreeBSD: pkg install sdl2 sdl2_ttf glew pkgconf
+Fedora: dnf install SDL2-devel SDL2_ttf-devel SDL2_image-devel glew-devel
+Arch: pacman -Sy sdl2 sdl2_ttf sdl2_image glew pkgconf
+FreeBSD: pkg install sdl2 sdl2_ttf sdl2_image glew pkgconf
--- a/configure	Sat Apr 19 19:30:46 2025 +0200
+++ b/configure	Sun Apr 20 15:41:16 2025 +0200
@@ -326,6 +326,30 @@
     dep_checked_sdl2=1
     return 0
 }
+dependency_error_sdl2_image()
+{
+    print_check_msg "$dep_checked_sdl2_image" "checking for sdl2_image... "
+    # dependency sdl2_image
+    while true
+    do
+        if [ -z "$PKG_CONFIG" ]; then
+            break
+        fi
+        if test_pkg_config "SDL2_image" "" "" "" ; then
+            TEMP_CFLAGS="$TEMP_CFLAGS `"$PKG_CONFIG" --cflags SDL2_image`"
+            TEMP_LDFLAGS="$TEMP_LDFLAGS `"$PKG_CONFIG" --libs SDL2_image`"
+        else
+            break
+        fi
+        print_check_msg "$dep_checked_sdl2_image" "yes\n"
+        dep_checked_sdl2_image=1
+        return 1
+    done
+
+    print_check_msg "$dep_checked_sdl2_image" "no\n"
+    dep_checked_sdl2_image=1
+    return 0
+}
 dependency_error_glew()
 {
     print_check_msg "$dep_checked_glew" "checking for glew... "
@@ -433,6 +457,10 @@
     DEPENDENCIES_FAILED="$DEPENDENCIES_FAILED sdl2 "
     ERROR=1
 fi
+if dependency_error_sdl2_image; then
+    DEPENDENCIES_FAILED="$DEPENDENCIES_FAILED sdl2_image "
+    ERROR=1
+fi
 if dependency_error_glew; then
     DEPENDENCIES_FAILED="$DEPENDENCIES_FAILED glew "
     ERROR=1
--- a/make/project.xml	Sat Apr 19 19:30:46 2025 +0200
+++ b/make/project.xml	Sun Apr 20 15:41:16 2025 +0200
@@ -21,6 +21,10 @@
 		<pkgconfig>SDL2_ttf</pkgconfig>
 	</dependency>
 
+    <dependency name="sdl2_image">
+        <pkgconfig>SDL2_image</pkgconfig>
+    </dependency>
+
 	<dependency name="glew">
 		<pkgconfig>glew</pkgconfig>
 	</dependency>
--- a/shader/sprite_frag.glsl	Sat Apr 19 19:30:46 2025 +0200
+++ b/shader/sprite_frag.glsl	Sun Apr 20 15:41:16 2025 +0200
@@ -2,9 +2,11 @@
 
 layout(location = 0) out vec4 diffuse;
 in vec2 texcoord;
+in vec2 uvcoord;
 
-uniform sampler2DRect tex;
+uniform sampler2D uv_tex;
+uniform sampler2DRect rect_tex;
 
 void main(void) {
-    diffuse = texture(tex, texcoord);
+    diffuse = texture(rect_tex, texcoord) * texture(uv_tex, uvcoord);
 }
--- a/shader/sprite_vtx.glsl	Sat Apr 19 19:30:46 2025 +0200
+++ b/shader/sprite_vtx.glsl	Sun Apr 20 15:41:16 2025 +0200
@@ -2,6 +2,7 @@
 
 layout(location = 0) in vec2 position;
 out vec2 texcoord;
+out vec2 uvcoord;
 
 uniform mat4 projection;
 uniform mat4 view;
@@ -13,5 +14,6 @@
     // apply depth
     pos.z = depth / -1024.0;
     gl_Position = pos;
+    uvcoord = position;
     texcoord = vec2(model[0].x, model[1].y)*position;
 }
--- a/src/ascension/2d/sprite.h	Sat Apr 19 19:30:46 2025 +0200
+++ b/src/ascension/2d/sprite.h	Sun Apr 20 15:41:16 2025 +0200
@@ -36,6 +36,28 @@
     AscTexture tex;
 } AscSprite;
 
+struct asc_sprite_create_args {
+    AscTexture texture;
+    int x;
+    int y;
+    /**
+     * Optional width for re-scaling.
+     * If zero, the texture width will be used.
+     */
+    unsigned width;
+    /**
+     * Optional height for re-scaling.
+     * If zero, the texture height will be used.
+     */
+    unsigned height;
+    bool opaque;
+};
+
+AscSceneNode *asc_sprite_create(struct asc_sprite_create_args args);
+
+#define asc_sprite(...) \
+    asc_sprite_create((struct asc_sprite_create_args) { __VA_ARGS__ })
+
 void asc_sprite_draw(AscSprite const *sprite);
 
 #endif //ASCENSION_SPRITE_H
--- a/src/ascension/glcontext.h	Sat Apr 19 19:30:46 2025 +0200
+++ b/src/ascension/glcontext.h	Sun Apr 20 15:41:16 2025 +0200
@@ -32,6 +32,7 @@
 
 #include "primitives.h"
 #include "shader.h"
+#include "texture.h"
 
 typedef struct AscGLContextSettings {
     int gl_major_version;
@@ -49,11 +50,17 @@
     struct {
         AscShaderSprite sprite;
     } shader;
+    struct {
+        AscTexture empty_1x1_2d;
+        AscTexture empty_1x1_rect;
+    } textures;
 } AscGLContext;
 
-#define ASC_PRIMITIVES_PLANE (&asc_active_window->glctx.primitives.plane)
+#define ASC_PRIMITIVES asc_active_window->glctx.primitives
 
-#define ASC_SHADER_SPRITE (&asc_active_window->glctx.shader.sprite)
+#define ASC_DEFAULT_SHADER asc_active_window->glctx.shader
+
+#define ASC_DEFAULT_TEXTURES asc_active_window->glctx.textures
 
 __attribute__((__nonnull__, __warn_unused_result__))
 bool asc_gl_context_initialize(
--- a/src/ascension/shader.h	Sat Apr 19 19:30:46 2025 +0200
+++ b/src/ascension/shader.h	Sun Apr 20 15:41:16 2025 +0200
@@ -48,7 +48,8 @@
 typedef struct AscShaderSprite {
     AscShaderProgram program;
     int depth;
-    int tex;
+    int rect_tex;
+    int uv_tex;
 } AscShaderSprite;
 
 /**
--- a/src/ascension/texture.h	Sat Apr 19 19:30:46 2025 +0200
+++ b/src/ascension/texture.h	Sun Apr 20 15:41:16 2025 +0200
@@ -35,12 +35,15 @@
 struct AscTexture {
     unsigned target;
     unsigned tex_id;
+    unsigned width;
+    unsigned height;
 };
 
 #define asc_texture_uninitialized(tex) ((tex)->tex_id == 0)
 
 enum asc_texture_target {
-    ASC_TEXTURE_RECTANGLE
+    ASC_TEXTURE_RECTANGLE,
+    ASC_TEXTURE_2D
 };
 
 enum asc_texture_min_filter {
@@ -70,6 +73,10 @@
 
 #define asc_texture_init_rectangle(tex) \
     asc_texture_init(tex, ASC_TEXTURE_RECTANGLE, \
+        ASC_TEXTURE_MIN_FILTER_NEAREST, ASC_TEXTURE_MAG_FILTER_NEAREST)
+
+#define asc_texture_init_2d(tex) \
+    asc_texture_init(tex, ASC_TEXTURE_2D, \
         ASC_TEXTURE_MIN_FILTER_LINEAR, ASC_TEXTURE_MAG_FILTER_LINEAR)
 
 __attribute__((__nonnull__))
@@ -78,4 +85,7 @@
 __attribute__((__nonnull__))
 void asc_texture_from_surface(AscTexture *tex, SDL_Surface const *surface);
 
+__attribute__((__nonnull__))
+void asc_texture_from_file(AscTexture *tex, const char *name);
+
 #endif //ASCENSION_TEXTURE_H
--- a/src/context.c	Sat Apr 19 19:30:46 2025 +0200
+++ b/src/context.c	Sun Apr 20 15:41:16 2025 +0200
@@ -31,6 +31,7 @@
 
 #include <SDL2/SDL.h>
 #include <SDL2/SDL_ttf.h>
+#include <SDL2/SDL_image.h>
 
 #include <time.h>
 
@@ -76,12 +77,13 @@
     );
 
     // initialize SDL
+    const int supported_img_flags = IMG_INIT_PNG | IMG_INIT_JPG;
     if (SDL_Init(SDL_INIT_VIDEO) < 0) {
         asc_error(SDL_GetError());
-    } else {
-        if (TTF_Init() < 0) {
-            asc_error(TTF_GetError());
-        }
+    } else if (TTF_Init() < 0) {
+        asc_error(TTF_GetError());
+    } else if (IMG_Init(supported_img_flags) != supported_img_flags) {
+        asc_error(IMG_GetError());
     }
     SDL_ClearError();
     asc_context.total_nanos = asc_nanos();
@@ -99,8 +101,8 @@
     asc_font_cache_destroy();
 
     // quit SDL
-    if (TTF_WasInit())
-        TTF_Quit();
+    IMG_Quit();
+    TTF_Quit();
     SDL_Quit();
 
     // destroy the error buffer
--- a/src/font.c	Sat Apr 19 19:30:46 2025 +0200
+++ b/src/font.c	Sun Apr 20 15:41:16 2025 +0200
@@ -90,6 +90,7 @@
     struct asc_font_cache_entry entry;
     entry.font = font;
     cxmutstr fpath = asc_filesystem_combine_paths(cx_strcast(asc_context.font_path), cx_str(asc_font_filename(font.style)));
+    asc_dprintf("Load font from %" CX_PRIstr, CX_SFMT(fpath));
     entry.ttf = TTF_OpenFont(fpath.ptr, font.size);
     cx_strfree(&fpath);
     if (entry.ttf == NULL) {
--- a/src/glcontext.c	Sat Apr 19 19:30:46 2025 +0200
+++ b/src/glcontext.c	Sun Apr 20 15:41:16 2025 +0200
@@ -74,6 +74,34 @@
     asc_shader_program_destroy(&ctx->shader.sprite.program);
 }
 
+static int asc_texture_initialize_predefined(AscGLContext *ctx) {
+    asc_texture_init_rectangle(&ctx->textures.empty_1x1_rect);
+    asc_texture_init_2d(&ctx->textures.empty_1x1_2d);
+
+    // Create a 1x1 surface with 32-bit RGBA format
+    SDL_Surface* white1x1 = SDL_CreateRGBSurface(
+        0, 1, 1, 32,
+        0xFF000000, 0x00FF0000, 0x0000FF00, 0x000000FF);
+    if (white1x1 == NULL) {
+        // TODO: merge error messages once asc_error allows formatting
+        asc_error("Failed to create surface");
+        asc_error(SDL_GetError());
+        return 1;
+    }
+    SDL_FillRect(white1x1, NULL, 0xFFFFFFFF);
+
+    // Initialize the empty textures with the white surface
+    asc_texture_from_surface(&ctx->textures.empty_1x1_2d, white1x1);
+    asc_texture_from_surface(&ctx->textures.empty_1x1_rect, white1x1);
+
+    return asc_error_catch_all_gl();
+}
+
+static void asc_texture_destroy_predefined(AscGLContext *ctx) {
+    asc_texture_destroy(&ctx->textures.empty_1x1_2d);
+    asc_texture_destroy(&ctx->textures.empty_1x1_rect);
+}
+
 bool asc_gl_context_initialize(
         AscGLContext *ctx,
         SDL_Window *window,
@@ -109,6 +137,11 @@
             return false;
         }
 
+        if (asc_texture_initialize_predefined(ctx)) {
+            asc_error("Initializing predefined textures failed");
+            return false;
+        }
+
         return true;
     } else {
         asc_error(glewGetErrorString(err));
@@ -121,6 +154,7 @@
     if (ctx->glctx == NULL) return;
     SDL_GL_MakeCurrent(ctx->window, ctx->glctx);
 
+    asc_texture_destroy_predefined(ctx);
     asc_shader_destroy_predefined(ctx);
     asc_primitives_destroy(ctx);
 
--- a/src/shader.c	Sat Apr 19 19:30:46 2025 +0200
+++ b/src/shader.c	Sun Apr 20 15:41:16 2025 +0200
@@ -135,7 +135,8 @@
         return 1;
     }
     sprite->depth = glGetUniformLocation(sprite->program.id, "depth");
-    sprite->tex = glGetUniformLocation(sprite->program.id, "texture");
+    sprite->rect_tex = glGetUniformLocation(sprite->program.id, "rect_tex");
+    sprite->uv_tex = glGetUniformLocation(sprite->program.id, "uv_tex");
     asc_shader_free_codes(codes);
     return 0;
 }
@@ -162,6 +163,7 @@
         return 0;
     }
     cxmutstr fpath = asc_filesystem_combine_paths(cx_strcast(asc_context.shader_path), cx_str(filename));
+    asc_dprintf("Load shader code from %" CX_PRIstr, CX_SFMT(fpath));
     FILE *f = fopen(fpath.ptr, "r");
     cx_strfree(&fpath);
     if (f == NULL) return -1;
--- a/src/sprite.c	Sat Apr 19 19:30:46 2025 +0200
+++ b/src/sprite.c	Sun Apr 20 15:41:16 2025 +0200
@@ -32,20 +32,52 @@
 
 #include <GL/glew.h>
 
+static void asc_sprite_free(AscSceneNode *node) {
+    free(node);
+}
+
+AscSceneNode *asc_sprite_create(struct asc_sprite_create_args args) {
+    AscSprite *sprite = calloc(1, sizeof(AscSprite));
+
+    // special sprite parameters
+    sprite->tex = args.texture;
+
+    // basic node parameters
+    AscSceneNode *node = (AscSceneNode *) sprite;
+    node->render_group = args.opaque
+                             ? ASC_RENDER_GROUP_SPRITE_OPAQUE
+                             : ASC_RENDER_GROUP_SPRITE_BLEND;
+    node->free_func = asc_sprite_free;
+
+    node->position.x = (float) args.x;
+    node->position.y = (float) args.y;
+
+    node->scale.x = (float) (args.width == 0 ? args.texture.width : args.width);
+    node->scale.y = (float) (args.height == 0 ? args.texture.height : args.height);
+
+    return node;
+}
+
 void asc_sprite_draw(AscSprite const *node) {
     // Obtain shader
-    AscShaderSprite *shader = ASC_SHADER_SPRITE;
+    AscShaderSprite *shader = &ASC_DEFAULT_SHADER.sprite;
 
     // Upload model matrix
     glUniformMatrix4fv(shader->program.model, 1,
                        GL_FALSE, node->data.world_transform);
 
     // Bind texture
-    asc_texture_bind(&node->tex, shader->tex, 0);
+    if (node->tex.target == GL_TEXTURE_RECTANGLE) {
+        asc_texture_bind(&node->tex, shader->rect_tex, 0);
+        asc_texture_bind(&ASC_DEFAULT_TEXTURES.empty_1x1_2d, shader->uv_tex, 1);
+    } else {
+        asc_texture_bind(&ASC_DEFAULT_TEXTURES.empty_1x1_rect, shader->rect_tex, 0);
+        asc_texture_bind(&node->tex, shader->uv_tex, 1);
+    }
 
     // Apply depth
     glUniform1f(shader->depth, (float)(node->data.depth));
 
     // Draw mesh
-    asc_mesh_draw_triangle_strip(ASC_PRIMITIVES_PLANE);
+    asc_mesh_draw_triangle_strip(&ASC_PRIMITIVES.plane);
 }
--- a/src/texture.c	Sat Apr 19 19:30:46 2025 +0200
+++ b/src/texture.c	Sun Apr 20 15:41:16 2025 +0200
@@ -25,11 +25,14 @@
  * POSSIBILITY OF SUCH DAMAGE.
  */
 
+#include "ascension/context.h"
 #include "ascension/texture.h"
 #include "ascension/error.h"
+#include "ascension/filesystem.h"
 
 #include <assert.h>
 #include <GL/glew.h>
+#include <SDL2/SDL_image.h>
 
 void asc_texture_bind(AscTexture const *tex, int uniform_location, int unit) {
     glActiveTexture(GL_TEXTURE0 + unit);
@@ -48,16 +51,62 @@
         asc_dprintf("Texture address: %"PRIxPTR, (uintptr_t) tex);
         return;
     }
+    tex->width = surface->w;
+    tex->height = surface->h;
     glBindTexture(tex->target,tex->tex_id);
     glPixelStorei(GL_UNPACK_ROW_LENGTH,
                   surface->pitch / surface->format->BytesPerPixel);
-    glTexImage2D(tex->target, 0, GL_RGBA,
+
+    // Determine the format and internal format based on the SDL surface format
+    GLint internal_format;
+    GLenum format;
+    switch (surface->format->format) {
+        case SDL_PIXELFORMAT_RGB24:
+            internal_format = GL_RGB8;
+            format = GL_RGB;
+            break;
+        case SDL_PIXELFORMAT_BGR24:
+            internal_format = GL_RGB8;
+            format = GL_BGR;
+            break;
+        case SDL_PIXELFORMAT_RGBA32:
+        case SDL_PIXELFORMAT_ABGR32:
+            internal_format = GL_RGBA8;
+            format = GL_RGBA;
+            break;
+        case SDL_PIXELFORMAT_ARGB32:
+        case SDL_PIXELFORMAT_BGRA32:
+            internal_format = GL_RGBA8;
+            format = GL_BGRA;
+            break;
+        default:
+            // TODO: add more output once asc_error allows format strings
+            asc_error("Unsupported pixel format.");
+            return;
+    }
+    glTexImage2D(tex->target, 0, internal_format,
                  surface->w, surface->h,
-                 0, GL_BGRA,
+                 0, format,
                  GL_UNSIGNED_BYTE, surface->pixels);
+    // TODO: replace catch all with proper error handling for this single call
     asc_error_catch_all_gl();
 }
 
+void asc_texture_from_file(AscTexture *tex, const char *name) {
+    cxmutstr filepath = asc_filesystem_combine_paths(cx_strcast(asc_context.texture_path), cx_str(name));
+    asc_dprintf("Load texture from %" CX_PRIstr, CX_SFMT(filepath));
+    SDL_Surface *image = IMG_Load(filepath.ptr);
+    cx_strfree(&filepath);
+    if (image == NULL) {
+        asc_error("Failed to load texture.");
+        asc_error(IMG_GetError());
+        return;
+    }
+    asc_texture_from_surface(tex, image);
+    asc_dprintf("Free temporary surface %"PRIxPTR, (uintptr_t) image);
+    SDL_FreeSurface(image);
+}
+
 void asc_texture_init(
         AscTexture *tex,
         enum asc_texture_target target,
@@ -65,7 +114,8 @@
         enum asc_texture_mag_filter mag_filter
 ) {
     static const GLenum texture_targets[] = {
-            GL_TEXTURE_RECTANGLE
+            GL_TEXTURE_RECTANGLE,
+            GL_TEXTURE_2D,
     };
     static const GLint texture_filters[] = {
             GL_NEAREST,
--- a/test/snake/snake.c	Sat Apr 19 19:30:46 2025 +0200
+++ b/test/snake/snake.c	Sun Apr 20 15:41:16 2025 +0200
@@ -75,6 +75,33 @@
     asc_add_ui_node(node);
 }
 
+static void free_spaceship(AscSceneNode *node) {
+    asc_texture_destroy(&((AscSprite*)node)->tex);
+    free(node);
+}
+
+static void create_spaceship(void) {
+    // TODO: textures should be passed by pointers (and probably ref-counted)
+    AscTexture texture;
+    asc_texture_init_2d(&texture);
+    asc_texture_from_file(&texture, "ship.png");
+    AscSceneNode *sprite = asc_sprite(
+        .texture = texture,
+        .x = 250,
+        .y = 300,
+        .width = 64,
+        .height = 64
+    );
+
+    // TODO: add to 2D scene instead of UI scene, once we have one
+    asc_add_ui_node(sprite);
+
+    // TODO: remove this hack by refactoring how textures work
+    sprite->free_func = free_spaceship;
+
+    // TODO: return something
+}
+
 int main(int argc, char** argv) {
     asc_context_initialize();
     if (asc_has_error()) {
@@ -100,6 +127,9 @@
     create_fps_counter();
     create_score_counter();
 
+    // create spaceship
+    create_spaceship();
+
     // Main Loop
     do {
         // quit application on any error
Binary file test/snake/textures/ship.png has changed

mercurial