diff --git a/src/dusk/display/display.c b/src/dusk/display/display.c index 3c29fdac..1f371793 100644 --- a/src/dusk/display/display.c +++ b/src/dusk/display/display.c @@ -22,7 +22,7 @@ #include "util/memory.h" #include "util/string.h" #include "asset/asset.h" -#include "display/shader/shaderunlit.h" +#include "display/shader/shaderlist.h" #include "time/time.h" display_t DISPLAY = { 0 }; @@ -53,31 +53,8 @@ errorret_t displayInit(void) { errorChain(screenInit()); // Setup initial shader with default values - mat4 view, proj, model; - glm_lookat( - (vec3){ 0.0f, 0.0f, 1.0f }, - (vec3){ 0.0f, 0.0f, 0.0f }, - (vec3){ 0.0f, 1.0f, 0.0f }, - view - ); - glm_perspective( - glm_rad(45.0f), - SCREEN.aspect, - 0.1f, - 100.0f, - proj - ); - - glm_mat4_identity(model); - - errorChain(shaderInit(&SHADER_UNLIT, &SHADER_UNLIT_DEFINITION)); - errorChain(shaderBind(&SHADER_UNLIT)); - errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_PROJECTION, proj)); - errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_VIEW, view)); - errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_MODEL, model)); - errorChain(shaderSetTexture(&SHADER_UNLIT, SHADER_UNLIT_TEXTURE, NULL)); - errorChain(shaderSetColor(&SHADER_UNLIT, SHADER_UNLIT_COLOR, COLOR_WHITE)); + errorChain(shaderListInit()); errorOk(); } @@ -119,7 +96,7 @@ errorret_t displaySetState(displaystate_t state) { } errorret_t displayDispose(void) { - errorChain(shaderDispose(&SHADER_UNLIT)); + errorChain(shaderListDispose()); errorChain(spriteBatchDispose()); screenDispose(); errorChain(textDispose()); diff --git a/src/dusk/display/shader/CMakeLists.txt b/src/dusk/display/shader/CMakeLists.txt index 5ebe3ca8..3ec11731 100644 --- a/src/dusk/display/shader/CMakeLists.txt +++ b/src/dusk/display/shader/CMakeLists.txt @@ -7,5 +7,6 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC shader.c + shaderlist.c shaderunlit.c ) \ No newline at end of file diff --git a/src/dusk/display/shader/shader.c b/src/dusk/display/shader/shader.c index a4d0ae78..92c6fb09 100644 --- a/src/dusk/display/shader/shader.c +++ b/src/dusk/display/shader/shader.c @@ -8,6 +8,7 @@ #include "shader.h" #include "shadermaterial.h" #include "assert/assert.h" +#include "log/log.h" shader_t *bound = NULL; @@ -57,6 +58,7 @@ errorret_t shaderSetColor( ) { assertNotNull(shader, "Shader cannot be null"); assertStrLenMin(name, 1, "Uniform name cannot be empty"); + assertTrue(bound == shader, "Shader must be bound."); errorChain(shaderSetColorPlatform(shader, name, color)); errorOk(); } diff --git a/src/dusk/display/shader/shaderlist.c b/src/dusk/display/shader/shaderlist.c new file mode 100644 index 00000000..9a816298 --- /dev/null +++ b/src/dusk/display/shader/shaderlist.c @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "shaderlist.h" +#include "display/screen/screen.h" +#include "assert/assert.h" + +shaderlistdef_t SHADER_LIST_DEFS[SHADER_LIST_SHADER_COUNT] = { + [SHADER_LIST_SHADER_UNLIT] = { + .shader = &SHADER_UNLIT, + .definition = &SHADER_UNLIT_DEFINITION + }, +}; + +errorret_t shaderListInit() { + mat4 view, proj, model; + glm_lookat( + (vec3){ 0.0f, 0.0f, 1.0f }, + (vec3){ 0.0f, 0.0f, 0.0f }, + (vec3){ 0.0f, 1.0f, 0.0f }, + view + ); + + glm_perspective( + glm_rad(45.0f), + SCREEN.aspect, + 0.1f, + 100.0f, + proj + ); + + glm_mat4_identity(model); + + for(shaderlistshader_t i = 0; i < SHADER_LIST_SHADER_COUNT; i++) { + if(i == SHADER_LIST_SHADER_NULL) { + continue; + } + + assertNotNull( + SHADER_LIST_DEFS[i].shader, "Shader cannot be null" + ); + assertNotNull( + SHADER_LIST_DEFS[i].definition, "Shader definition cannot be null" + ); + + errorChain(shaderInit( + SHADER_LIST_DEFS[i].shader, SHADER_LIST_DEFS[i].definition + )); + errorChain(shaderBind(SHADER_LIST_DEFS[i].shader)); + errorChain(shaderSetMatrix( + SHADER_LIST_DEFS[i].shader, SHADER_UNLIT_PROJECTION, proj + )); + errorChain(shaderSetMatrix( + SHADER_LIST_DEFS[i].shader, SHADER_UNLIT_VIEW, view + )); + errorChain(shaderSetMatrix( + SHADER_LIST_DEFS[i].shader, SHADER_UNLIT_MODEL, model + )); + errorChain(shaderSetTexture( + SHADER_LIST_DEFS[i].shader, SHADER_UNLIT_TEXTURE, NULL + )); + errorChain(shaderSetColor( + SHADER_LIST_DEFS[i].shader, SHADER_UNLIT_COLOR, COLOR_WHITE + )); + } + + errorOk(); +} + +errorret_t shaderListDispose(void) { + for(shaderlistshader_t i = 0; i < SHADER_LIST_SHADER_COUNT; i++) { + if(i == SHADER_LIST_SHADER_NULL) { + continue; + } + + assertNotNull( + SHADER_LIST_DEFS[i].shader, "Shader cannot be null" + ); + + errorChain(shaderDispose(SHADER_LIST_DEFS[i].shader)); + } + errorOk(); +} diff --git a/src/dusk/display/shader/shaderlist.h b/src/dusk/display/shader/shaderlist.h new file mode 100644 index 00000000..e7556362 --- /dev/null +++ b/src/dusk/display/shader/shaderlist.h @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "display/shader/shader.h" +#include "display/shader/shaderunlit.h" + +typedef enum { + SHADER_LIST_SHADER_NULL, + + SHADER_LIST_SHADER_UNLIT, + + SHADER_LIST_SHADER_COUNT +} shaderlistshader_t; + +typedef struct { + shader_t *shader; + shaderdefinition_t *definition; +} shaderlistdef_t; + +extern shaderlistdef_t SHADER_LIST_DEFS[SHADER_LIST_SHADER_COUNT]; + +/** + * Initializes all default shaders and uploads the initial view, projection, + * and model matrices to each. + * + * @return Error state. + */ +errorret_t shaderListInit(); + +/** + * Disposes all default shaders. + * + * @return Error state. + */ +errorret_t shaderListDispose(void); diff --git a/src/dusk/display/shader/shadermaterial.h b/src/dusk/display/shader/shadermaterial.h index 293d2bb3..e01e0cc4 100644 --- a/src/dusk/display/shader/shadermaterial.h +++ b/src/dusk/display/shader/shadermaterial.h @@ -6,7 +6,7 @@ */ #pragma once -#include "display/shader/shaderunlit.h" +#include "display/shader/shaderlist.h" typedef union shadermaterial_u { shaderunlitmaterial_t unlit; diff --git a/src/dusk/display/spritebatch/spritebatch.c b/src/dusk/display/spritebatch/spritebatch.c index 591beb4f..8094cfae 100644 --- a/src/dusk/display/spritebatch/spritebatch.c +++ b/src/dusk/display/spritebatch/spritebatch.c @@ -9,14 +9,13 @@ #include "assert/assert.h" #include "util/memory.h" #include "util/math.h" -#include "display/shader/shaderunlit.h" +#include "display/shader/shadermaterial.h" meshvertex_t SPRITEBATCH_VERTICES[SPRITEBATCH_VERTEX_COUNT]; spritebatch_t SPRITEBATCH; errorret_t spriteBatchInit() { memoryZero(&SPRITEBATCH, sizeof(spritebatch_t)); - errorChain(meshInit( &SPRITEBATCH.mesh, QUAD_PRIMITIVE_TYPE, @@ -28,137 +27,100 @@ errorret_t spriteBatchInit() { errorret_t spriteBatchBuffer( const spritebatchsprite_t *sprites, - const uint32_t count + const uint32_t count, + shader_t *shader, + const shadermaterial_t material ) { - for(uint32_t i = 0; i < count; i++) { - const spritebatchsprite_t *s = &sprites[i]; + assertNotNull(sprites, "Sprites cannot be null"); + assertTrue(count > 0, "Count must be greater than zero"); + assertNotNull(shader, "Shader cannot be null"); - // if(s->texture != SPRITEBATCH.currentTexture) { - // errorChain(spriteBatchFlush()); - // } + // Did the shader or material data change? + if(shader != SPRITEBATCH.shader) { + errorChain(spriteBatchFlush()); + SPRITEBATCH.shader = shader; + SPRITEBATCH.material = material; + } else if(memoryCompare( + &material, &SPRITEBATCH.material, sizeof(shadermaterial_t) + ) != 0) { + // Did the material data change? + errorChain(spriteBatchFlush()); + SPRITEBATCH.shader = shader; + SPRITEBATCH.material = material; + } - if(SPRITEBATCH.spriteCount >= SPRITEBATCH_SPRITES_MAX_PER_FLUSH) { - errorChain(spriteBatchFlush()); - } + // Buffer the vertices. + for(uint32_t i = 0; i < count; i++ ){ + spritebatchsprite_t sprite = sprites[i]; meshvertex_t *v = &SPRITEBATCH_VERTICES[ (SPRITEBATCH.spriteCount + (SPRITEBATCH.spriteFlush * SPRITEBATCH_SPRITES_MAX_PER_FLUSH)) * QUAD_VERTEX_COUNT ]; + + // Buffer the quad + v[0].pos[0] = sprite.min[0]; + v[0].pos[1] = sprite.min[1]; + v[0].pos[2] = sprite.min[2]; - v[0].pos[0] = s->min[0]; v[0].pos[1] = s->min[1]; v[0].pos[2] = s->min[2]; - v[0].uv[0] = s->uvMin[0]; v[0].uv[1] = s->uvMin[1]; + v[0].uv[0] = sprite.uvMin[0]; + v[0].uv[1] = sprite.uvMin[1]; - v[1].pos[0] = s->max[0]; v[1].pos[1] = s->min[1]; v[1].pos[2] = s->min[2]; - v[1].uv[0] = s->uvMax[0]; v[1].uv[1] = s->uvMin[1]; - v[2].pos[0] = s->max[0]; v[2].pos[1] = s->max[1]; v[2].pos[2] = s->min[2]; - v[2].uv[0] = s->uvMax[0]; v[2].uv[1] = s->uvMax[1]; + v[1].pos[0] = sprite.max[0]; + v[1].pos[1] = sprite.min[1]; + v[1].pos[2] = sprite.min[2]; - v[3].pos[0] = s->min[0]; v[3].pos[1] = s->min[1]; v[3].pos[2] = s->min[2]; - v[3].uv[0] = s->uvMin[0]; v[3].uv[1] = s->uvMin[1]; + v[1].uv[0] = sprite.uvMax[0]; + v[1].uv[1] = sprite.uvMin[1]; - v[4].pos[0] = s->max[0]; v[4].pos[1] = s->max[1]; v[4].pos[2] = s->min[2]; - v[4].uv[0] = s->uvMax[0]; v[4].uv[1] = s->uvMax[1]; - v[5].pos[0] = s->min[0]; v[5].pos[1] = s->max[1]; v[5].pos[2] = s->min[2]; - v[5].uv[0] = s->uvMin[0]; v[5].uv[1] = s->uvMax[1]; + v[2].pos[0] = sprite.max[0]; + v[2].pos[1] = sprite.max[1]; + v[2].pos[2] = sprite.max[2]; - #if MESH_ENABLE_COLOR - for(uint8_t vi = 0; vi < QUAD_VERTEX_COUNT; vi++) { - v[vi].color = s->color; - } - #endif + v[2].uv[0] = sprite.uvMax[0]; + v[2].uv[1] = sprite.uvMax[1]; + + v[3].pos[0] = sprite.min[0]; + v[3].pos[1] = sprite.min[1]; + v[3].pos[2] = sprite.min[2]; + + v[3].uv[0] = sprite.uvMin[0]; + v[3].uv[1] = sprite.uvMin[1]; + + + v[4].pos[0] = sprite.max[0]; + v[4].pos[1] = sprite.max[1]; + v[4].pos[2] = sprite.max[2]; + + v[4].uv[0] = sprite.uvMax[0]; + v[4].uv[1] = sprite.uvMax[1]; + + + v[5].pos[0] = sprite.min[0]; + v[5].pos[1] = sprite.max[1]; + v[5].pos[2] = sprite.max[2]; + + v[5].uv[0] = sprite.uvMin[0]; + v[5].uv[1] = sprite.uvMax[1]; + + // Do we need to flush? SPRITEBATCH.spriteCount++; + if(SPRITEBATCH.spriteCount >= SPRITEBATCH_SPRITES_MAX_PER_FLUSH) { + errorChain(spriteBatchFlush()); + } } + errorOk(); } -errorret_t spriteBatchBufferSprite(const spritebatchsprite_t *sprite) { - return spriteBatchBuffer(sprite, 1); -} - -errorret_t spriteBatchPushv( - const float_t *minX, - const float_t *minY, - const float_t *maxX, - const float_t *maxY, - const float_t *z, - #if MESH_ENABLE_COLOR - const color_t *color, - #endif - const float_t *u0, - const float_t *v0, - const float_t *u1, - const float_t *v1, - const size_t count -) { - for(size_t i = 0; i < count; i++) { - spritebatchsprite_t sprite; - sprite.min[0] = minX[i]; sprite.min[1] = minY[i]; sprite.min[2] = z[i]; - sprite.max[0] = maxX[i]; sprite.max[1] = maxY[i]; sprite.max[2] = z[i]; - sprite.uvMin[0] = u0[i]; sprite.uvMin[1] = v0[i]; - sprite.uvMax[0] = u1[i]; sprite.uvMax[1] = v1[i]; - sprite.texture = SPRITEBATCH.currentTexture; - #if MESH_ENABLE_COLOR - sprite.color = color[i]; - #endif - errorChain(spriteBatchBuffer(&sprite, 1)); - } - errorOk(); -} - -errorret_t spriteBatchPush( - const float_t minX, - const float_t minY, - const float_t maxX, - const float_t maxY, - #if MESH_ENABLE_COLOR - const color_t color, - #endif - const float_t u0, - const float_t v0, - const float_t u1, - const float_t v1 -) { - spritebatchsprite_t sprite; - sprite.min[0] = minX; sprite.min[1] = minY; sprite.min[2] = 0; - sprite.max[0] = maxX; sprite.max[1] = maxY; sprite.max[2] = 0; - sprite.uvMin[0] = u0; sprite.uvMin[1] = v0; - sprite.uvMax[0] = u1; sprite.uvMax[1] = v1; - sprite.texture = SPRITEBATCH.currentTexture; - #if MESH_ENABLE_COLOR - sprite.color = color; - #endif - return spriteBatchBuffer(&sprite, 1); -} - -errorret_t spriteBatchPushZ( - const vec3 min, - const vec3 max, - #if MESH_ENABLE_COLOR - const color_t color, - #endif - const vec2 uvMin, - const vec2 uvMax -) { - spritebatchsprite_t sprite; - sprite.min[0] = min[0]; sprite.min[1] = min[1]; sprite.min[2] = min[2]; - sprite.max[0] = max[0]; sprite.max[1] = max[1]; sprite.max[2] = max[2]; - sprite.uvMin[0] = uvMin[0]; sprite.uvMin[1] = uvMin[1]; - sprite.uvMax[0] = uvMax[0]; sprite.uvMax[1] = uvMax[1]; - sprite.texture = SPRITEBATCH.currentTexture; - #if MESH_ENABLE_COLOR - sprite.color = color; - #endif - return spriteBatchBuffer(&sprite, 1); -} - void spriteBatchClear() { SPRITEBATCH.spriteCount = 0; SPRITEBATCH.spriteFlush = 0; - SPRITEBATCH.currentTexture = NULL; + SPRITEBATCH.shader = NULL; + memoryZero(&SPRITEBATCH.material, sizeof(shadermaterial_t)); } errorret_t spriteBatchFlush() { @@ -171,6 +133,9 @@ errorret_t spriteBatchFlush() { SPRITEBATCH.spriteFlush * SPRITEBATCH_SPRITES_MAX_PER_FLUSH * QUAD_VERTEX_COUNT ); + + errorChain(shaderBind(SPRITEBATCH.shader)); + errorChain(shaderSetMaterial(SPRITEBATCH.shader, &SPRITEBATCH.material)); errorChain(meshFlush(&SPRITEBATCH.mesh, vertexOffset, vertexCount)); errorChain(meshDraw(&SPRITEBATCH.mesh, vertexOffset, vertexCount)); @@ -186,4 +151,4 @@ errorret_t spriteBatchFlush() { errorret_t spriteBatchDispose() { errorChain(meshDispose(&SPRITEBATCH.mesh)); errorOk(); -} \ No newline at end of file +} diff --git a/src/dusk/display/spritebatch/spritebatch.h b/src/dusk/display/spritebatch/spritebatch.h index 0da6dba6..a4279d72 100644 --- a/src/dusk/display/spritebatch/spritebatch.h +++ b/src/dusk/display/spritebatch/spritebatch.h @@ -8,6 +8,7 @@ #pragma once #include "display/mesh/quad.h" #include "display/texture/texture.h" +#include "display/shader/shadermaterial.h" #define SPRITEBATCH_SPRITES_MAX 512 #define SPRITEBATCH_VERTEX_COUNT (SPRITEBATCH_SPRITES_MAX * QUAD_VERTEX_COUNT) @@ -21,146 +22,66 @@ typedef struct { vec3 max; vec2 uvMin; vec2 uvMax; - texture_t *texture; -#if MESH_ENABLE_COLOR - color_t color; -#endif } spritebatchsprite_t; typedef struct { mesh_t mesh; int32_t spriteCount; int32_t spriteFlush; - texture_t *currentTexture; + + shader_t *shader; + shadermaterial_t material; } spritebatch_t; -// Have to define these seperately because of alignment in certain platforms. +// Have to define these separately because of alignment on certain platforms. extern meshvertex_t SPRITEBATCH_VERTICES[SPRITEBATCH_VERTEX_COUNT]; extern spritebatch_t SPRITEBATCH; /** - * Initializes a sprite batch. + * Initializes the global sprite batch and its internal mesh buffer. * - * @return An error code indicating success or failure. + * @return Error state. */ errorret_t spriteBatchInit(); /** - * Buffers an array of sprites. Flushes automatically when the texture changes - * or the per-flush limit is reached. + * Lowest-level buffer function. Writes sprites into the internal vertex buffer. + * Flushes automatically when the per-flush capacity is reached. Does not + * modify material state — call spriteBatchSetState or use a high-level push + * function before buffering. * * @param sprites Pointer to the sprite array. * @param count Number of sprites to buffer. - * @return An error code indicating success or failure. + * @param shader Shader to use when flushing. + * @param material Material information passed to the shader when flushing. + * @return Error state. */ errorret_t spriteBatchBuffer( const spritebatchsprite_t *sprites, - const uint32_t count + const uint32_t count, + shader_t *shader, + const shadermaterial_t material ); /** - * Convenience wrapper — buffers a single sprite. - * - * @param sprite The sprite to buffer. - * @return An error code indicating success or failure. - */ -errorret_t spriteBatchBufferSprite(const spritebatchsprite_t *sprite); - -/** - * Pushes multiple sprites to the batch using parallel arrays of values. - * - * @param minX Array of minimum x coordinates. - * @param minY Array of minimum y coordinates. - * @param maxX Array of maximum x coordinates. - * @param maxY Array of maximum y coordinates. - * @param z Array of z coordinates. - * @param color Array of colors (if enabled). - * @param u0 Array of u0 texture coordinates. - * @param v0 Array of v0 texture coordinates. - * @param u1 Array of u1 texture coordinates. - * @param v1 Array of v1 texture coordinates. - * @param count Number of sprites to push. - * @return An error code indicating success or failure. - */ -errorret_t spriteBatchPushv( - const float_t *minX, - const float_t *minY, - const float_t *maxX, - const float_t *maxY, - const float_t *z, -#if MESH_ENABLE_COLOR - const color_t *color, -#endif - const float_t *u0, - const float_t *v0, - const float_t *u1, - const float_t *v1, - const size_t count -); - -/** - * Pushes a 2D sprite (z=0) to the batch. - * - * @param minX The minimum x coordinate of the sprite. - * @param minY The minimum y coordinate of the sprite. - * @param maxX The maximum x coordinate of the sprite. - * @param maxY The maximum y coordinate of the sprite. - * @param color The color to tint the sprite with. - * @param u0 Texture u coordinate for the top-left corner. - * @param v0 Texture v coordinate for the top-left corner. - * @param u1 Texture u coordinate for the bottom-right corner. - * @param v1 Texture v coordinate for the bottom-right corner. - * @return An error code indicating success or failure. - */ -errorret_t spriteBatchPush( - const float_t minX, - const float_t minY, - const float_t maxX, - const float_t maxY, - #if MESH_ENABLE_COLOR - const color_t color, - #endif - const float_t u0, - const float_t v0, - const float_t u1, - const float_t v1 -); - -/** - * Pushes a 3D sprite to the batch. - * - * @param min The minimum (x,y,z) coordinate of the sprite. - * @param max The maximum (x,y,z) coordinate of the sprite. - * @param color The color to tint the sprite with. - * @param uvMin Texture coordinate for the top-left corner. - * @param uvMax Texture coordinate for the bottom-right corner. - * @return An error code indicating success or failure. - */ -errorret_t spriteBatchPushZ( - const vec3 min, - const vec3 max, - #if MESH_ENABLE_COLOR - const color_t color, - #endif - const vec2 uvMin, - const vec2 uvMax -); - -/** - * Clears the sprite batch. Calling flush after this renders nothing. + * Resets sprite and flush counters and clears the current material state. + * Calling spriteBatchFlush after this renders nothing. */ void spriteBatchClear(); /** - * Flushes the sprite batch, rendering all queued sprites. + * Uploads and draws all buffered sprites. If a material type has been set via + * spriteBatchSetState or spriteBatchCheckState, the shader is bound and the + * material is applied first. If matType is NULL the caller is responsible for + * having the correct shader already bound. Does nothing if the buffer is empty. * - * @return An error code indicating success or failure. + * @return Error state. */ errorret_t spriteBatchFlush(); /** - * Disposes of the sprite batch, freeing any allocated resources. + * Disposes of the sprite batch and frees its internal mesh buffer. * - * @return An error code indicating success or failure. + * @return Error state. */ errorret_t spriteBatchDispose(); diff --git a/src/dusk/display/text/text.c b/src/dusk/display/text/text.c index 7177cd13..f935b327 100644 --- a/src/dusk/display/text/text.c +++ b/src/dusk/display/text/text.c @@ -34,15 +34,12 @@ errorret_t textDispose(void) { errorOk(); } -errorret_t textDrawChar( - const float_t x, - const float_t y, - const char_t c, - #if MESH_ENABLE_COLOR - const color_t color, - #endif - font_t *font +spritebatchsprite_t textGetSprite( + const vec2 pos, const char_t c, const font_t *font ) { + assertNotNull(font, "Font cannot be NULL"); + + // Change char from ASCII to a tile index. int32_t tileIndex = (int32_t)(c) - TEXT_CHAR_START; if(tileIndex < 0 || tileIndex >= font->tileset->tileCount) { tileIndex = ((int32_t)'@') - TEXT_CHAR_START; @@ -52,19 +49,22 @@ errorret_t textDrawChar( "Character is out of bounds for font tiles" ); + // Create sprite. vec4 uv; tilesetTileGetUV(font->tileset, tileIndex, uv); - errorChain(spriteBatchPush( - x, y, - x + font->tileset->tileWidth, - y + font->tileset->tileHeight, - #if MESH_ENABLE_COLOR - color, - #endif - uv[0], uv[1], uv[2], uv[3] - )); - errorOk(); + spritebatchsprite_t sprite; + sprite.min[0] = pos[0]; + sprite.min[1] = pos[1]; + sprite.min[2] = 0.0f; + sprite.max[0] = pos[0] + font->tileset->tileWidth; + sprite.max[1] = pos[1] + font->tileset->tileHeight; + sprite.max[2] = 0.0f; + sprite.uvMin[0] = uv[0]; + sprite.uvMin[1] = uv[1]; + sprite.uvMax[0] = uv[2]; + sprite.uvMax[1] = uv[3]; + return sprite; } errorret_t textDraw( @@ -76,6 +76,14 @@ errorret_t textDraw( ) { assertNotNull(text, "Text cannot be NULL"); + spritebatchsprite_t sprite; + shadermaterial_t material = { + .unlit = { + .color = color, + .texture = font->texture + } + }; + float_t posX = x; float_t posY = y; @@ -100,13 +108,8 @@ errorret_t textDraw( continue; } - errorChain(textDrawChar( - posX, posY, c, - #if MESH_ENABLE_COLOR - color, - #endif - font - )); + sprite = textGetSprite((vec2){posX, posY}, c, font); + errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material)); posX += font->tileset->tileWidth; } errorOk(); diff --git a/src/dusk/display/text/text.h b/src/dusk/display/text/text.h index 166eeeb7..a62bf3bd 100644 --- a/src/dusk/display/text/text.h +++ b/src/dusk/display/text/text.h @@ -8,6 +8,7 @@ #pragma once #include "asset/asset.h" #include "display/text/font.h" +#include "display/spritebatch/spritebatch.h" #define TEXT_CHAR_START '!' @@ -28,23 +29,17 @@ errorret_t textInit(void); errorret_t textDispose(void); /** - * Draws a single character at the specified position. + * Builds a sprite for a single character at the given position. * - * @param x The x-coordinate to draw the character at. - * @param y The y-coordinate to draw the character at. - * @param c The character to draw. - * @param color The color to draw the character in. - * @param font Font to use for rendering. - * @return Either an error or success result. + * @param pos The (x, y) position of the character in screen/world space. + * @param c The character to build a sprite for. + * @param font Font to use for tile lookup. + * @return The populated sprite ready for spriteBatchBuffer. */ -errorret_t textDrawChar( - const float_t x, - const float_t y, +spritebatchsprite_t textGetSprite( + const vec2 pos, const char_t c, - #if MESH_ENABLE_COLOR - const color_t color, - #endif - font_t *font + const font_t *font ); /** diff --git a/src/dusk/entity/component/display/entityrenderable.c b/src/dusk/entity/component/display/entityrenderable.c index 387171a7..74de17af 100644 --- a/src/dusk/entity/component/display/entityrenderable.c +++ b/src/dusk/entity/component/display/entityrenderable.c @@ -7,22 +7,12 @@ #include "entityrenderable.h" #include "entity/entitymanager.h" +#include "display/shader/shadermaterial.h" #include "display/shader/shaderunlit.h" +#include "display/display.h" #include "display/mesh/cube.h" -#include "display/spritebatch/spritebatch.h" - -errorret_t entityRenderableDrawDefault( - const entityid_t entityId, - const componentid_t componentId, - void *user -) { - (void)entityId; - (void)componentId; - (void)user; - errorChain(shaderSetColor(&SHADER_UNLIT, SHADER_UNLIT_COLOR, COLOR_WHITE)); - errorChain(shaderSetTexture(&SHADER_UNLIT, SHADER_UNLIT_TEXTURE, NULL)); - return meshDraw(&CUBE_MESH_SIMPLE, 0, -1); -} +#include "util/memory.h" +#include "assert/assert.h" void entityRenderableInit( const entityid_t entityId, @@ -31,15 +21,44 @@ void entityRenderableInit( entityrenderable_t *r = componentGetData( entityId, componentId, COMPONENT_TYPE_RENDERABLE ); - r->draw = entityRenderableDrawDefault; - r->drawUser = NULL; + memoryZero(r, sizeof(entityrenderable_t)); + r->type = ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL; + r->data.material.shaderType = SHADER_LIST_SHADER_UNLIT; + r->data.material.material.unlit.color = COLOR_WHITE; + r->data.material.meshes[0] = &CUBE_MESH_SIMPLE; + r->data.material.meshOffsets[0] = 0; + r->data.material.meshCounts[0] = -1; + r->data.material.meshCount = 1; + r->data.material.state.flags = DISPLAY_STATE_FLAG_DEPTH_TEST; } void entityRenderableDispose( const entityid_t entityId, const componentid_t componentId ) { + +} +void entityRenderableSetType( + const entityid_t entityId, + const componentid_t componentId, + const entityrenderabletype_t type +) { + entityrenderable_t *r = componentGetData( + entityId, componentId, COMPONENT_TYPE_RENDERABLE + ); + r->type = type; +} + +void entityRenderableSetPriority( + const entityid_t entityId, + const componentid_t componentId, + const int8_t priority +) { + entityrenderable_t *r = componentGetData( + entityId, componentId, COMPONENT_TYPE_RENDERABLE + ); + r->priority = priority; } void entityRenderableSetDraw( @@ -52,11 +71,13 @@ void entityRenderableSetDraw( ), void *user ) { + assertNotNull(draw, "Draw callback cannot be null"); entityrenderable_t *r = componentGetData( entityId, componentId, COMPONENT_TYPE_RENDERABLE ); - r->draw = draw; - r->drawUser = user; + r->type = ENTITY_RENDERABLE_TYPE_CUSTOM; + r->data.custom.draw = draw; + r->data.custom.drawUser = user; } errorret_t entityRenderableDraw( @@ -66,5 +87,42 @@ errorret_t entityRenderableDraw( entityrenderable_t *r = componentGetData( entityId, componentId, COMPONENT_TYPE_RENDERABLE ); - return r->draw(entityId, componentId, r->drawUser); + + switch(r->type) { + case ENTITY_RENDERABLE_TYPE_SPRITEBATCH: { + const entityrenderablespritebatch_t *sb = &r->data.spritebatch; + errorChain(displaySetState((displaystate_t){ + .flags = DISPLAY_STATE_FLAG_BLEND + })); + spriteBatchClear(); + shadermaterial_t mat; + memoryZero(&mat, sizeof(shadermaterial_t)); + mat.unlit.texture = sb->texture; + mat.unlit.color = COLOR_WHITE; + errorChain(spriteBatchBuffer( + sb->sprites, sb->spriteCount, + SHADER_LIST_DEFS[SHADER_LIST_SHADER_UNLIT].shader, mat + )); + return spriteBatchFlush(); + } + + case ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL: { + const entityrenderablematerial_t *m = &r->data.material; + errorChain(displaySetState(m->state)); + shader_t *shader = SHADER_LIST_DEFS[m->shaderType].shader; + assertNotNull(shader, "Shader cannot be null for material type"); + errorChain(shaderBind(shader)); + errorChain(shaderSetMaterial(shader, &m->material)); + for(uint8_t i = 0; i < m->meshCount; i++) { + errorChain(meshDraw(m->meshes[i], m->meshOffsets[i], m->meshCounts[i])); + } + errorOk(); + } + + case ENTITY_RENDERABLE_TYPE_CUSTOM: + return r->data.custom.draw(entityId, componentId, r->data.custom.drawUser); + + default: + assertUnreachable("Invalid renderable type"); + } } diff --git a/src/dusk/entity/component/display/entityrenderable.h b/src/dusk/entity/component/display/entityrenderable.h index a0a36a55..1d87243a 100644 --- a/src/dusk/entity/component/display/entityrenderable.h +++ b/src/dusk/entity/component/display/entityrenderable.h @@ -10,6 +10,32 @@ #include "display/mesh/mesh.h" #include "display/shader/shadermaterial.h" #include "display/spritebatch/spritebatch.h" +#include "display/displaystate.h" + +#define ENTITY_RENDERABLE_SPRITEBATCH_SPRITES_MAX 64 +#define ENTITY_RENDERABLE_MESHES_MAX 8 + +typedef enum { + ENTITY_RENDERABLE_TYPE_CUSTOM = 0, + ENTITY_RENDERABLE_TYPE_SPRITEBATCH, + ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL, +} entityrenderabletype_t; + +typedef struct { + spritebatchsprite_t sprites[ENTITY_RENDERABLE_SPRITEBATCH_SPRITES_MAX]; + uint32_t spriteCount; + texture_t *texture; +} entityrenderablespritebatch_t; + +typedef struct { + mesh_t *meshes[ENTITY_RENDERABLE_MESHES_MAX]; + int32_t meshOffsets[ENTITY_RENDERABLE_MESHES_MAX]; + int32_t meshCounts[ENTITY_RENDERABLE_MESHES_MAX]; + uint8_t meshCount; + shaderlistshader_t shaderType; + shadermaterial_t material; + displaystate_t state; +} entityrenderablematerial_t; typedef struct { errorret_t (*draw)( @@ -18,12 +44,30 @@ typedef struct { void *user ); void *drawUser; +} entityrenderablecustom_t; + +typedef union entityrenderabledata_u { + entityrenderablespritebatch_t spritebatch; + entityrenderablematerial_t material; + entityrenderablecustom_t custom; +} entityrenderabledata_t; + +typedef struct { + entityrenderabletype_t type; + entityrenderabledata_t data; + + /** + * Render priority. 0 = auto (derived from type/flags). Higher values render + * later (on top of lower values). Range: [-128..127] with 0 is auto. + */ + int8_t priority; } entityrenderable_t; /** * Initializes the entity renderable component. Defaults to - * ENTITY_RENDERABLE_TYPE_MATERIAL, the unlit shader, white color, no mesh. - * + * ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL with the unlit shader, a white cube, + * and depth-test enabled. + * * @param entityId The entity to initialize the component for. * @param componentId The renderable component of the entity. */ @@ -33,8 +77,8 @@ void entityRenderableInit( ); /** - * Disposes the entity renderable component, freeing any callback user data. - * + * Disposes the entity renderable component. + * * @param entityId The entity to dispose the component for. * @param componentId The renderable component of the entity. */ @@ -44,11 +88,40 @@ void entityRenderableDispose( ); /** - * Sets the draw callback for the entity's renderable component. + * Sets the rendering type for the renderable component. Resets type-specific + * data to zero. + * + * @param entityId The entity to configure. + * @param componentId The renderable component. + * @param type The rendering type to use. + */ +void entityRenderableSetType( + const entityid_t entityId, + const componentid_t componentId, + const entityrenderabletype_t type +); + +/** + * Sets the render priority. 0 = auto (derived from type/flags). Higher values + * render later (on top). Use non-zero to force ordering. + * + * @param entityId The entity to configure. + * @param componentId The renderable component. + * @param priority The priority value, or 0 for auto. + */ +void entityRenderableSetPriority( + const entityid_t entityId, + const componentid_t componentId, + const int8_t priority +); + +/** + * Sets the draw callback, switching the type to ENTITY_RENDERABLE_TYPE_CUSTOM. * * @param entityId The entity to configure. * @param componentId The renderable component of the entity. * @param draw The draw callback to assign. + * @param user Userdata passed to the callback. */ void entityRenderableSetDraw( const entityid_t entityId, diff --git a/src/dusk/scene/CMakeLists.txt b/src/dusk/scene/CMakeLists.txt index cf90daa8..cd98f773 100644 --- a/src/dusk/scene/CMakeLists.txt +++ b/src/dusk/scene/CMakeLists.txt @@ -7,6 +7,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC scene.c + scenerenderpipeline.c ) # Subdirectories diff --git a/src/dusk/scene/overworld/overworldground.c b/src/dusk/scene/overworld/overworldground.c index 93bf5e01..591d335f 100644 --- a/src/dusk/scene/overworld/overworldground.c +++ b/src/dusk/scene/overworld/overworldground.c @@ -7,40 +7,21 @@ #include "overworldground.h" #include "entity/entitymanager.h" -#include "entity/component/display/entityrenderable.h" #include "entity/component/physics/entityphysics.h" -#include "display/mesh/plane.h" -#include "display/shader/shaderunlit.h" #define OVERWORLD_GROUND_SIZE 20.0f -static errorret_t overworldGroundDraw( - const entityid_t entityId, - const componentid_t componentId, - void *user -) { - (void)entityId; - (void)componentId; - (void)user; - errorChain(shaderSetColor(&SHADER_UNLIT, SHADER_UNLIT_COLOR, COLOR_MAGENTA)); - errorChain(shaderSetTexture(&SHADER_UNLIT, SHADER_UNLIT_TEXTURE, NULL)); - return meshDraw(&PLANE_MESH_SIMPLE, 0, -1); -} - void overworldGroundAdd(overworldground_t *ground) { ground->entityId = entityManagerAdd(); ground->posCompId = entityAddComponent( ground->entityId, COMPONENT_TYPE_POSITION ); - componentid_t renderComp = entityAddComponent( - ground->entityId, COMPONENT_TYPE_RENDERABLE - ); + (void)entityAddComponent(ground->entityId, COMPONENT_TYPE_RENDERABLE); vec3 pos = { -OVERWORLD_GROUND_SIZE, 0.0f, -OVERWORLD_GROUND_SIZE }; vec3 scale = { OVERWORLD_GROUND_SIZE * 2.0f, 1.0f, OVERWORLD_GROUND_SIZE * 2.0f }; entityPositionSetLocalPosition(ground->entityId, ground->posCompId, pos); entityPositionSetLocalScale(ground->entityId, ground->posCompId, scale); - entityRenderableSetDraw(ground->entityId, renderComp, overworldGroundDraw, NULL); // Separate physics entity centered on the finite ground area. // The visual entity's position is its corner {-size, 0, -size}, not its diff --git a/src/dusk/scene/overworld/overworldscene.c b/src/dusk/scene/overworld/overworldscene.c index 0f1d5ac5..733c19b6 100644 --- a/src/dusk/scene/overworld/overworldscene.c +++ b/src/dusk/scene/overworld/overworldscene.c @@ -11,9 +11,15 @@ #include "overworldnpc.h" #include "console/console.h" #include "entity/entitymanager.h" +#include "entity/component/display/entityrenderable.h" +#include "display/shader/shaderlist.h" #include "entity/component/overworld/entityinteractable.h" #include "entity/component/overworld/entityoverworldcamera.h" +#include "display/mesh/cube.h" +#include "display/mesh/plane.h" +#include "display/displaystate.h" #include "scene/scene.h" +#include "assert/assert.h" #define OVERWORLD (SCENE.data.overworld) @@ -26,14 +32,72 @@ static void overworldSceneNpcInteract( consolePrint("NPC interacted with!"); } +void overworldSceneConfigureShaderMaterial( + const entityid_t entityId, + const color_t color, + mesh_t *mesh +) { + assertNotNull(mesh, "Mesh cannot be null"); + componentid_t renderComp = entityGetComponent( + entityId, COMPONENT_TYPE_RENDERABLE + ); + entityRenderableSetType(entityId, renderComp, ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL); + entityrenderable_t *r = componentGetData( + entityId, renderComp, COMPONENT_TYPE_RENDERABLE + ); + r->data.material.shaderType = SHADER_LIST_SHADER_UNLIT; + r->data.material.material.unlit.color = color; + r->data.material.material.unlit.texture = NULL; + r->data.material.state.flags = DISPLAY_STATE_FLAG_DEPTH_TEST; + r->data.material.meshes[0] = mesh; + r->data.material.meshOffsets[0] = 0; + r->data.material.meshCounts[0] = -1; + r->data.material.meshCount = 1; +} + +void overworldSceneConfigureSprite( + const entityid_t entityId, + const color_t color, + texture_t *texture +) { + componentid_t renderComp = entityGetComponent( + entityId, COMPONENT_TYPE_RENDERABLE + ); + entityRenderableSetType(entityId, renderComp, ENTITY_RENDERABLE_TYPE_SPRITEBATCH); + entityrenderable_t *r = componentGetData( + entityId, renderComp, COMPONENT_TYPE_RENDERABLE + ); + r->data.spritebatch.texture = texture; + r->data.spritebatch.spriteCount = 1; + + r->data.spritebatch.sprites[0].min[0] = -0.5f; + r->data.spritebatch.sprites[0].min[1] = -0.0f; + r->data.spritebatch.sprites[0].min[2] = -0.5f; + + r->data.spritebatch.sprites[0].max[0] = 0.5f; + r->data.spritebatch.sprites[0].max[1] = 0.0f; + r->data.spritebatch.sprites[0].max[2] = 0.5f; + + r->data.spritebatch.sprites[0].uvMin[0] = 0.0f; + r->data.spritebatch.sprites[0].uvMin[1] = 0.0f; + r->data.spritebatch.sprites[0].uvMax[0] = 1.0f; + r->data.spritebatch.sprites[0].uvMax[1] = 1.0f; +} + void overworldSceneInit(void) { consolePrint("Overworld scene initialized"); overworldGroundAdd(&OVERWORLD.ground); + overworldSceneConfigureShaderMaterial( + OVERWORLD.ground.entityId, COLOR_MAGENTA, &PLANE_MESH_SIMPLE + ); + overworldPlayerAdd(&OVERWORLD.player); + overworldSceneConfigureSprite(OVERWORLD.player.entityId, COLOR_GREEN, NULL); vec3 npcPos = { 3.0f, 0.5f, 3.0f }; overworldNpcAdd(&OVERWORLD.npc, npcPos); + overworldSceneConfigureSprite(OVERWORLD.npc.entityId, COLOR_BLUE, NULL); OVERWORLD.cameraEntityId = entityManagerAdd(); (void)entityAddComponent(OVERWORLD.cameraEntityId, COMPONENT_TYPE_POSITION); @@ -48,7 +112,7 @@ void overworldSceneInit(void) { entityoverworldcamera_t *camData = entityOverworldCameraGet( OVERWORLD.cameraEntityId, OVERWORLD.cameraOverworldCompId ); - glm_vec3_copy((vec3){ 0.5f, 0.5f, 0.5f }, camData->targetOffset); + glm_vec3_zero(camData->targetOffset); glm_vec3_copy((vec3){ 0.0f, 0.0f, 5.0f }, camData->eyeOffset); camData->scale = 32.0f; entityInteractableSetCallback( diff --git a/src/dusk/scene/overworld/overworldscene.h b/src/dusk/scene/overworld/overworldscene.h index 826e2f4b..2bb7e7fe 100644 --- a/src/dusk/scene/overworld/overworldscene.h +++ b/src/dusk/scene/overworld/overworldscene.h @@ -8,6 +8,9 @@ #pragma once #include "error/error.h" #include "entity/entitybase.h" +#include "display/color.h" +#include "display/mesh/mesh.h" +#include "display/texture/texture.h" #include "scene/overworld/overworldplayer.h" #include "scene/overworld/overworldground.h" #include "scene/overworld/overworldnpc.h" @@ -20,6 +23,48 @@ typedef struct { overworldnpc_t npc; } overworldscene_t; +/** + * Configures a SHADER_MATERIAL renderable on an entity with the unlit shader, + * depth testing enabled, no blending, and a single mesh. + * + * @param entityId The entity to configure. + * @param color Unlit diffuse color. + * @param mesh Mesh to render. + */ +void overworldSceneConfigureShaderMaterial( + const entityid_t entityId, + const color_t color, + mesh_t *mesh +); + +/** + * Configures a SPRITEBATCH renderable on an entity as a single 1x1 billboard + * sprite centered at the entity's origin. + * + * @param entityId The entity to configure. + * @param color Per-vertex sprite tint color. + * @param texture Texture to use, or NULL for untextured. + */ +void overworldSceneConfigureSprite( + const entityid_t entityId, + const color_t color, + texture_t *texture +); + +/** + * Initializes the overworld scene, spawning all entities and configuring + * the camera, player, ground, and NPC. + */ void overworldSceneInit(void); + +/** + * Updates the overworld scene each frame. + * + * @return Error state. + */ errorret_t overworldSceneUpdate(void); + +/** + * Disposes the overworld scene, invalidating all entity and component IDs. + */ void overworldSceneDispose(void); diff --git a/src/dusk/scene/scene.c b/src/dusk/scene/scene.c index 195de3b0..82c796e6 100644 --- a/src/dusk/scene/scene.c +++ b/src/dusk/scene/scene.c @@ -10,13 +10,12 @@ #include "time/time.h" #include "display/screen/screen.h" #include "entity/entitymanager.h" -#include "entity/component/display/entityrenderable.h" #include "display/shader/shaderunlit.h" -#include "display/spritebatch/spritebatch.h" -#include "display/screen/screen.h" +#include "display/display.h" #include "console/console.h" #include "util/string.h" #include "ui/ui.h" +#include "scene/scenerenderpipeline.h" scenefuncs_t SCENE_FUNCTIONS[SCENE_TYPE_COUNT] = { { 0 }, @@ -69,63 +68,13 @@ errorret_t sceneUpdate(void) { } errorret_t sceneRender(void) { - mat4 proj, view, model, ident; + mat4 proj, view, ident; glm_mat4_identity(ident); - errorChain(shaderBind(&SHADER_UNLIT)); - - // Render scene. - entityid_t camera = entityCameraGetCurrent(); - entityid_t entities[ENTITY_COUNT_MAX]; - componentid_t components[ENTITY_COUNT_MAX]; - entityid_t entCount = componentGetEntitiesWithComponent( - COMPONENT_TYPE_RENDERABLE, - entities, components - ); - - // Anything to render? - if(camera != ENTITY_ID_INVALID && entCount > 0) { - componentid_t camPos = entityGetComponent(camera,COMPONENT_TYPE_POSITION); - componentid_t camCam = entityGetComponent(camera,COMPONENT_TYPE_CAMERA); - - // View - if(camPos == COMPONENT_ID_INVALID) { - errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_VIEW, ident)); - } else { - entityPositionGetTransform(camera, camPos, view); - errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_VIEW, view)); - } - - // Projection - assertTrue( - camCam != COMPONENT_ID_INVALID, - "Current camera does not have a camera component?" - ); - entityCameraGetProjection(camera, camCam, proj); - errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_PROJECTION, proj)); - - // For each ent. - for(entityid_t i = 0; i < entCount; i++) { - entityid_t entityId = entities[i]; - componentid_t renderableComp = components[i]; - - // Has position? - componentid_t posComp = entityGetComponent( - entityId, COMPONENT_TYPE_POSITION - ); - if(posComp == COMPONENT_ID_INVALID) { - errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_MODEL, ident)); - } else { - entityPositionGetTransform(entityId, posComp, model); - errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_MODEL, model)); - } - - // Render. - errorChain(entityRenderableDraw(entityId, renderableComp)); - } - } + errorChain(sceneRenderPipeline(entityCameraGetCurrent())); // UI Rendering + errorChain(shaderBind(&SHADER_UNLIT)); errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_MODEL, ident)); glm_ortho( @@ -143,7 +92,7 @@ errorret_t sceneRender(void) { view ); errorChain(shaderSetMatrix(&SHADER_UNLIT, SHADER_UNLIT_VIEW, view)); - + errorChain(displaySetState((displaystate_t){ .flags = DISPLAY_STATE_FLAG_BLEND })); diff --git a/src/dusk/scene/scenerenderpipeline.c b/src/dusk/scene/scenerenderpipeline.c new file mode 100644 index 00000000..42114417 --- /dev/null +++ b/src/dusk/scene/scenerenderpipeline.c @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "scenerenderpipeline.h" +#include "entity/entitymanager.h" +#include "entity/component/display/entityposition.h" +#include "entity/component/display/entitycamera.h" +#include "display/shader/shaderlist.h" +#include "display/shader/shaderunlit.h" +#include "display/displaystate.h" +#include "assert/assert.h" + +int8_t sceneRenderPipelineGetPriority(const entityrenderable_t *r) { + assertNotNull(r, "Renderable cannot be null"); + if(r->priority != 0) return r->priority; + switch(r->type) { + case ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL: + return (r->data.material.state.flags & DISPLAY_STATE_FLAG_BLEND) + ? (int8_t)10 : (int8_t)-10; + case ENTITY_RENDERABLE_TYPE_SPRITEBATCH: + return (int8_t)100; + case ENTITY_RENDERABLE_TYPE_CUSTOM: + default: + return (int8_t)0; + } +} + +int_t sceneRenderPipelineCompare(const void *a, const void *b) { + assertNotNull(a, "Entry a cannot be null"); + assertNotNull(b, "Entry b cannot be null"); + const scenerenderpipelineentry_t *ea = (const scenerenderpipelineentry_t *)a; + const scenerenderpipelineentry_t *eb = (const scenerenderpipelineentry_t *)b; + return (int_t)ea->effectivePriority - (int_t)eb->effectivePriority; +} + +shader_t *sceneRenderPipelineGetShader(const entityrenderable_t *r) { + assertNotNull(r, "Renderable cannot be null"); + switch(r->type) { + case ENTITY_RENDERABLE_TYPE_SPRITEBATCH: + return SHADER_LIST_DEFS[SHADER_LIST_SHADER_UNLIT].shader; + case ENTITY_RENDERABLE_TYPE_SHADER_MATERIAL: + return SHADER_LIST_DEFS[r->data.material.shaderType].shader; + case ENTITY_RENDERABLE_TYPE_CUSTOM: + default: + return SHADER_LIST_DEFS[SHADER_LIST_SHADER_UNLIT].shader; + } +} + +errorret_t sceneRenderPipeline(const entityid_t cameraId) { + mat4 proj, view, model, ident; + glm_mat4_identity(ident); + + entityid_t entities[ENTITY_COUNT_MAX]; + componentid_t components[ENTITY_COUNT_MAX]; + entityid_t entCount = componentGetEntitiesWithComponent( + COMPONENT_TYPE_RENDERABLE, entities, components + ); + + if(cameraId == ENTITY_ID_INVALID || entCount == 0) errorOk(); + + scenerenderpipelineentry_t pipeline[ENTITY_COUNT_MAX]; + for(entityid_t i = 0; i < entCount; i++) { + entityrenderable_t *r = componentGetData( + entities[i], components[i], COMPONENT_TYPE_RENDERABLE + ); + pipeline[i].entityId = entities[i]; + pipeline[i].componentId = components[i]; + pipeline[i].effectivePriority = sceneRenderPipelineGetPriority(r); + } + sort(pipeline, (size_t)entCount, sizeof(scenerenderpipelineentry_t), sceneRenderPipelineCompare); + + componentid_t camPos = entityGetComponent(cameraId, COMPONENT_TYPE_POSITION); + componentid_t camCam = entityGetComponent(cameraId, COMPONENT_TYPE_CAMERA); + assertTrue( + camCam != COMPONENT_ID_INVALID, + "Current camera does not have a camera component?" + ); + + if(camPos == COMPONENT_ID_INVALID) { + glm_mat4_copy(ident, view); + } else { + entityPositionGetTransform(cameraId, camPos, view); + } + entityCameraGetProjection(cameraId, camCam, proj); + + for(shaderlistshader_t si = SHADER_LIST_SHADER_NULL + 1; si < SHADER_LIST_SHADER_COUNT; si++) { + shader_t *s = SHADER_LIST_DEFS[si].shader; + assertNotNull(s, "Shader in list cannot be null"); + errorChain(shaderBind(s)); + errorChain(shaderSetMatrix(s, SHADER_UNLIT_VIEW, view)); + errorChain(shaderSetMatrix(s, SHADER_UNLIT_PROJECTION, proj)); + } + + for(entityid_t i = 0; i < entCount; i++) { + entityid_t eid = pipeline[i].entityId; + componentid_t cid = pipeline[i].componentId; + + entityrenderable_t *r = componentGetData(eid, cid, COMPONENT_TYPE_RENDERABLE); + shader_t *s = sceneRenderPipelineGetShader(r); + + componentid_t posComp = entityGetComponent(eid, COMPONENT_TYPE_POSITION); + if(posComp == COMPONENT_ID_INVALID) { + errorChain(shaderBind(s)); + errorChain(shaderSetMatrix(s, SHADER_UNLIT_MODEL, ident)); + } else { + entityPositionGetTransform(eid, posComp, model); + errorChain(shaderBind(s)); + errorChain(shaderSetMatrix(s, SHADER_UNLIT_MODEL, model)); + } + + errorChain(entityRenderableDraw(eid, cid)); + } + + errorChain(displaySetState((displaystate_t){ .flags = 0 })); + errorOk(); +} diff --git a/src/dusk/scene/scenerenderpipeline.h b/src/dusk/scene/scenerenderpipeline.h new file mode 100644 index 00000000..4dfc979c --- /dev/null +++ b/src/dusk/scene/scenerenderpipeline.h @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "entity/entitybase.h" +#include "entity/component/display/entityrenderable.h" +#include "display/shader/shader.h" +#include "util/sort.h" + +typedef struct { + entityid_t entityId; + componentid_t componentId; + int8_t effectivePriority; +} scenerenderpipelineentry_t; + +/** + * Returns the effective render priority for a renderable. When the renderable's + * explicit priority is non-zero that value is returned directly; otherwise an + * automatic value is derived from the renderable type and display state flags. + * + * @param r The renderable component data. + * @return Effective priority: lower renders first, higher renders last. + */ +int8_t sceneRenderPipelineGetPriority(const entityrenderable_t *r); + +/** + * sortcompare_t comparator for scenerenderpipelineentry_t. Compares by + * effectivePriority ascending so lower-priority entries sort first. + * + * @param a Pointer to the first scenerenderpipelineentry_t. + * @param b Pointer to the second scenerenderpipelineentry_t. + * @return Negative, zero, or positive. + */ +int_t sceneRenderPipelineCompare(const void *a, const void *b); + +/** + * Returns the shader that will be used to render the given renderable. + * SPRITEBATCH and CUSTOM default to SHADER_UNLIT; SHADER_MATERIAL uses the + * shader indexed by the material's shaderType field in SHADER_LIST_DEFS. + * + * @param r The renderable component data. + * @return Pointer to the shader, never NULL. + */ +shader_t *sceneRenderPipelineGetShader(const entityrenderable_t *r); + +/** + * Renders all entities with renderable components in priority order. + * Lower effective priority renders first (behind); higher renders last (on top). + * When priority is 0 the effective value is derived from the renderable's type + * and flags; otherwise the explicit priority is used directly. + * + * @param cameraId The entity ID of the active camera, or ENTITY_ID_INVALID. + * @return Error state. + */ +errorret_t sceneRenderPipeline(const entityid_t cameraId); diff --git a/src/dusk/ui/uiframe.c b/src/dusk/ui/uiframe.c index 60c60196..222379be 100644 --- a/src/dusk/ui/uiframe.c +++ b/src/dusk/ui/uiframe.c @@ -26,97 +26,126 @@ errorret_t uiFrameDraw( assertNotNull(frame, "frame must not be NULL"); assertNotNull(frame->texture, "frame texture must not be NULL"); - errorChain(shaderSetTexture( - &SHADER_UNLIT, SHADER_UNLIT_TEXTURE, frame->texture - )); - + shadermaterial_t material = { + .unlit = { + .color = COLOR_WHITE, + .texture = frame->texture + } + }; + spritebatchsprite_t sprites[9]; float_t tileW = (float_t)frame->tileset.tileWidth; float_t tileH = (float_t)frame->tileset.tileHeight; - vec4 uv; tilesetPositionGetUV(&frame->tileset, 0, 0, uv); - errorChain(spriteBatchPush( - x, y, x + tileW, y + tileH, - #if MESH_ENABLE_COLOR - COLOR_WHITE, - #endif - uv[0], uv[1], uv[2], uv[3] - )); + sprites[0].min[0] = x; + sprites[0].min[1] = y; + sprites[0].min[2] = 0.0f; + sprites[0].max[0] = x + tileW; + sprites[0].max[1] = y + tileH; + sprites[0].max[2] = 0.0f; + sprites[0].uvMin[0] = uv[0]; + sprites[0].uvMin[1] = uv[1]; + sprites[0].uvMax[0] = uv[2]; + sprites[0].uvMax[1] = uv[3]; tilesetPositionGetUV(&frame->tileset, 1, 0, uv); - errorChain(spriteBatchPush( - x + tileW, y, x + width - tileW, y + tileH, - #if MESH_ENABLE_COLOR - COLOR_WHITE, - #endif - uv[0], uv[1], uv[2], uv[3] - )); + sprites[1].min[0] = x + tileW; + sprites[1].min[1] = y; + sprites[1].min[2] = 0.0f; + sprites[1].max[0] = x + width - tileW; + sprites[1].max[1] = y + tileH; + sprites[1].max[2] = 0.0f; + sprites[1].uvMin[0] = uv[0]; + sprites[1].uvMin[1] = uv[1]; + sprites[1].uvMax[0] = uv[2]; + sprites[1].uvMax[1] = uv[3]; tilesetPositionGetUV(&frame->tileset, 2, 0, uv); - errorChain(spriteBatchPush( - x + width - tileW, y, x + width, y + tileH, - #if MESH_ENABLE_COLOR - COLOR_WHITE, - #endif - uv[0], uv[1], uv[2], uv[3] - )); + sprites[2].min[0] = x + width - tileW; + sprites[2].min[1] = y; + sprites[2].min[2] = 0.0f; + sprites[2].max[0] = x + width; + sprites[2].max[1] = y + tileH; + sprites[2].max[2] = 0.0f; + sprites[2].uvMin[0] = uv[0]; + sprites[2].uvMin[1] = uv[1]; + sprites[2].uvMax[0] = uv[2]; + sprites[2].uvMax[1] = uv[3]; tilesetPositionGetUV(&frame->tileset, 0, 1, uv); - errorChain(spriteBatchPush( - x, y + tileH, x + tileW, y + height - tileH, - #if MESH_ENABLE_COLOR - COLOR_WHITE, - #endif - uv[0], uv[1], uv[2], uv[3] - )); + sprites[3].min[0] = x; + sprites[3].min[1] = y + tileH; + sprites[3].min[2] = 0.0f; + sprites[3].max[0] = x + tileW; + sprites[3].max[1] = y + height - tileH; + sprites[3].max[2] = 0.0f; + sprites[3].uvMin[0] = uv[0]; + sprites[3].uvMin[1] = uv[1]; + sprites[3].uvMax[0] = uv[2]; + sprites[3].uvMax[1] = uv[3]; tilesetPositionGetUV(&frame->tileset, 1, 1, uv); - errorChain(spriteBatchPush( - x + tileW, y + tileH, x + width - tileW, y + height - tileH, - #if MESH_ENABLE_COLOR - COLOR_WHITE, - #endif - uv[0], uv[1], uv[2], uv[3] - )); + sprites[4].min[0] = x + tileW; + sprites[4].min[1] = y + tileH; + sprites[4].min[2] = 0.0f; + sprites[4].max[0] = x + width - tileW; + sprites[4].max[1] = y + height - tileH; + sprites[4].max[2] = 0.0f; + sprites[4].uvMin[0] = uv[0]; + sprites[4].uvMin[1] = uv[1]; + sprites[4].uvMax[0] = uv[2]; + sprites[4].uvMax[1] = uv[3]; tilesetPositionGetUV(&frame->tileset, 2, 1, uv); - errorChain(spriteBatchPush( - x + width - tileW, y + tileH, x + width, y + height - tileH, - #if MESH_ENABLE_COLOR - COLOR_WHITE, - #endif - uv[0], uv[1], uv[2], uv[3] - )); + sprites[5].min[0] = x + width - tileW; + sprites[5].min[1] = y + tileH; + sprites[5].min[2] = 0.0f; + sprites[5].max[0] = x + width; + sprites[5].max[1] = y + height - tileH; + sprites[5].max[2] = 0.0f; + sprites[5].uvMin[0] = uv[0]; + sprites[5].uvMin[1] = uv[1]; + sprites[5].uvMax[0] = uv[2]; + sprites[5].uvMax[1] = uv[3]; tilesetPositionGetUV(&frame->tileset, 0, 2, uv); - errorChain(spriteBatchPush( - x, y + height - tileH, x + tileW, y + height, - #if MESH_ENABLE_COLOR - COLOR_WHITE, - #endif - uv[0], uv[1], uv[2], uv[3] - )); + sprites[6].min[0] = x; + sprites[6].min[1] = y + height - tileH; + sprites[6].min[2] = 0.0f; + sprites[6].max[0] = x + tileW; + sprites[6].max[1] = y + height; + sprites[6].max[2] = 0.0f; + sprites[6].uvMin[0] = uv[0]; + sprites[6].uvMin[1] = uv[1]; + sprites[6].uvMax[0] = uv[2]; + sprites[6].uvMax[1] = uv[3]; tilesetPositionGetUV(&frame->tileset, 1, 2, uv); - errorChain(spriteBatchPush( - x + tileW, y + height - tileH, x + width - tileW, y + height, - #if MESH_ENABLE_COLOR - COLOR_WHITE, - #endif - uv[0], uv[1], uv[2], uv[3] - )); + sprites[7].min[0] = x + tileW; + sprites[7].min[1] = y + height - tileH; + sprites[7].min[2] = 0.0f; + sprites[7].max[0] = x + width - tileW; + sprites[7].max[1] = y + height; + sprites[7].max[2] = 0.0f; + sprites[7].uvMin[0] = uv[0]; + sprites[7].uvMin[1] = uv[1]; + sprites[7].uvMax[0] = uv[2]; + sprites[7].uvMax[1] = uv[3]; tilesetPositionGetUV(&frame->tileset, 2, 2, uv); - errorChain(spriteBatchPush( - x + width - tileW, y + height - tileH, x + width, y + height, - #if MESH_ENABLE_COLOR - COLOR_WHITE, - #endif - uv[0], uv[1], uv[2], uv[3] - )); + sprites[8].min[0] = x + width - tileW; + sprites[8].min[1] = y + height - tileH; + sprites[8].min[2] = 0.0f; + sprites[8].max[0] = x + width; + sprites[8].max[1] = y + height; + sprites[8].max[2] = 0.0f; + sprites[8].uvMin[0] = uv[0]; + sprites[8].uvMin[1] = uv[1]; + sprites[8].uvMax[0] = uv[2]; + sprites[8].uvMax[1] = uv[3]; - errorOk(); + return spriteBatchBuffer(sprites, 9, &SHADER_UNLIT, material); } void uiFrameDispose(uiframe_t *frame) { diff --git a/src/dusk/ui/uifullbox.c b/src/dusk/ui/uifullbox.c index 1108e07e..2229fae8 100644 --- a/src/dusk/ui/uifullbox.c +++ b/src/dusk/ui/uifullbox.c @@ -58,16 +58,14 @@ errorret_t uiFullboxDraw(uifullbox_t *fullbox) { errorChain(shaderSetColor(&SHADER_UNLIT, SHADER_UNLIT_COLOR, color)); #endif - errorChain(spriteBatchPush( - 0.0f, 0.0f, - (float_t)SCREEN.width, (float_t)SCREEN.height, - #if MESH_ENABLE_COLOR - color, - #endif - 0.0f, 0.0f, 1.0f, 1.0f - )); - errorChain(spriteBatchFlush()); - errorOk(); + spritebatchsprite_t sprite; + shadermaterial_t material = { + .unlit = { + .color = color, + .texture = &TEXTURE_WHITE + } + }; + return spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, material); } void uiFullboxTransition( diff --git a/src/dusk/ui/uitextbox.c b/src/dusk/ui/uitextbox.c index 7652c417..a815c769 100644 --- a/src/dusk/ui/uitextbox.c +++ b/src/dusk/ui/uitextbox.c @@ -12,6 +12,7 @@ #include "event/event.h" #include "display/screen/screen.h" #include "display/texture/texture.h" +#include "display/text/text.h" #include "display/spritebatch/spritebatch.h" #include "display/shader/shaderunlit.h" @@ -216,15 +217,12 @@ errorret_t uiTextboxDraw(void) { )); errorChain(spriteBatchFlush()); - errorChain(shaderSetTexture( - &SHADER_UNLIT, SHADER_UNLIT_TEXTURE, UI_TEXTBOX.font->texture - )); - #if MESH_ENABLE_COLOR - #else - errorChain(shaderSetColor( - &SHADER_UNLIT, SHADER_UNLIT_COLOR, UI_TEXTBOX.textColor - )); - #endif + shadermaterial_t textMaterial = { + .unlit = { + .color = UI_TEXTBOX.textColor, + .texture = UI_TEXTBOX.font->texture + } + }; float_t frameTileW = (float_t)UI_TEXTBOX.frame.tileset.tileWidth; float_t frameTileH = (float_t)UI_TEXTBOX.frame.tileset.tileHeight; @@ -242,21 +240,17 @@ errorret_t uiTextboxDraw(void) { for(int32_t li = pageFirst; li < pageLast && charsLeft > 0; li++) { uitextboxline_t *line = &UI_TEXTBOX.lines[li]; int32_t visible = line->count < charsLeft ? line->count : charsLeft; - float_t lineY = contentY; - lineY += (float_t)(li - pageFirst) * (fontH + UI_TEXTBOX_LINE_SPACING); + float_t lineY = contentY + (float_t)(li - pageFirst) * (fontH + UI_TEXTBOX_LINE_SPACING); for(int32_t ci = 0; ci < visible; ci++) { char_t c = UI_TEXTBOX.text[line->start + ci]; if(c == ' ') continue; - errorChain(textDrawChar( - contentX + (float_t)ci * fontW, - lineY, + spritebatchsprite_t sprite = textGetSprite( + (vec2){ contentX + (float_t)ci * fontW, lineY }, c, - #if MESH_ENABLE_COLOR - UI_TEXTBOX.textColor, - #endif UI_TEXTBOX.font - )); + ); + errorChain(spriteBatchBuffer(&sprite, 1, &SHADER_UNLIT, textMaterial)); } charsLeft -= visible;