Sun, 20 Apr 2025 15:41:16 +0200
add support for 2d textures in sprite shader - fixes #386
--- 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