From 163c8e8a3fef58b797c9094086d155be00d570c5 Mon Sep 17 00:00:00 2001 From: Lizzy Fleckenstein Date: Sun, 4 Aug 2024 22:06:56 +0200 Subject: [PATCH] implement texture atlas --- src/client/client_config.c | 12 ++++ src/client/client_config.h | 2 + src/client/client_node.c | 47 +++++++------- src/client/client_node.h | 10 +-- src/client/game.c | 1 + src/client/terrain_gfx.c | 7 ++- src/client/texture.c | 125 +++++++++++++++++++++++++++++++++++++ src/client/texture.h | 19 ++++++ 8 files changed, 195 insertions(+), 28 deletions(-) diff --git a/src/client/client_config.c b/src/client/client_config.c index a777cea..9e38399 100644 --- a/src/client/client_config.c +++ b/src/client/client_config.c @@ -9,6 +9,8 @@ struct ClientConfig client_config = { .meshgen_threads = 4, .swap_mouse_buttons = true, .texture_batching = true, + .atlas_size = 1024, + .atlas_mipmap = 4, }; static ConfigEntry config_entries[] = { @@ -47,6 +49,16 @@ static ConfigEntry config_entries[] = { .key = "texture_batching", .value = &client_config.texture_batching, }, + { + .type = CONFIG_UINT, + .key = "atlas_size", + .value = &client_config.atlas_size, + }, + { + .type = CONFIG_UINT, + .key = "atlas_mipmap", + .value = &client_config.atlas_mipmap, + }, }; __attribute__((constructor)) static void client_config_init() diff --git a/src/client/client_config.h b/src/client/client_config.h index 0e4adf7..9cb29d8 100644 --- a/src/client/client_config.h +++ b/src/client/client_config.h @@ -11,6 +11,8 @@ extern struct ClientConfig { unsigned int meshgen_threads; bool swap_mouse_buttons; bool texture_batching; + unsigned int atlas_size; + unsigned int atlas_mipmap; } client_config; #endif diff --git a/src/client/client_node.c b/src/client/client_node.c index 76eb738..485b82d 100644 --- a/src/client/client_node.c +++ b/src/client/client_node.c @@ -1,13 +1,15 @@ #include +#include #include "client/client.h" +#include "client/client_config.h" #include "client/client_node.h" #include "common/color.h" #include "common/environment.h" #include "common/node.h" #include "common/perlin.h" -#define TILES_SIMPLE(path) {.paths = {path, NULL, NULL, NULL, NULL, NULL}, .indices = {0, 0, 0, 0, 0, 0}, .textures = {NULL}} -#define TILES_NONE {.paths = {NULL}, .indices = {0}, .textures = {NULL}} +#define TILES_SIMPLE(path) {.paths = {path, NULL, NULL, NULL, NULL, NULL}, .indices = {0, 0, 0, 0, 0, 0}, .textures = {}} +#define TILES_NONE {.paths = {NULL}, .indices = {0}, .textures = {}} static void render_grass(NodeArgsRender *args) { @@ -30,8 +32,10 @@ static void render_grass(NodeArgsRender *args) static void render_stone(NodeArgsRender *args) { + /* args->vertex.cube.textureCoordinates.x += noise2d(args->pos.x, args->pos.z, 0, seed + OFFSET_TEXTURE_OFFSET_S); args->vertex.cube.textureCoordinates.y += noise2d(args->pos.x, args->pos.z, 0, seed + OFFSET_TEXTURE_OFFSET_T); + */ } static void render_color(NodeArgsRender *args) @@ -44,7 +48,6 @@ ClientNodeDef client_node_def[COUNT_NODE] = { { .tiles = TILES_SIMPLE(ASSET_PATH "textures/unknown.png"), .visibility = VISIBILITY_SOLID, - .mipmap = true, .render = NULL, .pointable = true, .selection_color = {1.0f, 1.0f, 1.0f}, @@ -54,7 +57,6 @@ ClientNodeDef client_node_def[COUNT_NODE] = { { .tiles = TILES_NONE, .visibility = VISIBILITY_NONE, - .mipmap = true, .render = NULL, .pointable = false, .selection_color = {1.0f, 1.0f, 1.0f}, @@ -64,7 +66,6 @@ ClientNodeDef client_node_def[COUNT_NODE] = { { .tiles = TILES_SIMPLE(ASSET_PATH "textures/grass.png"), .visibility = VISIBILITY_SOLID, - .mipmap = true, .render = &render_grass, .pointable = true, .selection_color = {1.0f, 1.0f, 1.0f}, @@ -74,7 +75,6 @@ ClientNodeDef client_node_def[COUNT_NODE] = { { .tiles = TILES_SIMPLE(ASSET_PATH "textures/dirt.png"), .visibility = VISIBILITY_SOLID, - .mipmap = true, .render = NULL, .pointable = true, .selection_color = {1.0f, 1.0f, 1.0f}, @@ -84,7 +84,6 @@ ClientNodeDef client_node_def[COUNT_NODE] = { { .tiles = TILES_SIMPLE(ASSET_PATH "textures/stone.png"), .visibility = VISIBILITY_SOLID, - .mipmap = true, .render = &render_stone, .pointable = true, .selection_color = {1.0f, 1.0f, 1.0f}, @@ -94,7 +93,6 @@ ClientNodeDef client_node_def[COUNT_NODE] = { { .tiles = TILES_SIMPLE(ASSET_PATH "textures/snow.png"), .visibility = VISIBILITY_SOLID, - .mipmap = true, .render = NULL, .pointable = true, .selection_color = {0.1f, 0.5f, 1.0f}, @@ -105,10 +103,9 @@ ClientNodeDef client_node_def[COUNT_NODE] = { .tiles = { .paths = {ASSET_PATH "textures/oak_wood.png", ASSET_PATH "textures/oak_wood_top.png", NULL, NULL, NULL, NULL}, .indices = {0, 0, 0, 0, 1, 1}, - .textures = {NULL}, + .textures = {}, }, .visibility = VISIBILITY_SOLID, - .mipmap = true, .render = &render_color, .pointable = true, .selection_color = {1.0f, 1.0f, 1.0f}, @@ -118,7 +115,6 @@ ClientNodeDef client_node_def[COUNT_NODE] = { { .tiles = TILES_SIMPLE(ASSET_PATH "textures/oak_leaves.png"), .visibility = VISIBILITY_SOLID, - .mipmap = true, .render = &render_color, .pointable = true, .selection_color = {1.0f, 1.0f, 1.0f}, @@ -129,10 +125,9 @@ ClientNodeDef client_node_def[COUNT_NODE] = { .tiles = { .paths = {ASSET_PATH "textures/pine_wood.png", ASSET_PATH "textures/pine_wood_top.png", NULL, NULL, NULL, NULL}, .indices = {0, 0, 0, 0, 1, 1}, - .textures = {NULL}, + .textures = {}, }, .visibility = VISIBILITY_SOLID, - .mipmap = true, .render = &render_color, .pointable = true, .selection_color = {1.0f, 1.0f, 1.0f}, @@ -142,7 +137,6 @@ ClientNodeDef client_node_def[COUNT_NODE] = { { .tiles = TILES_SIMPLE(ASSET_PATH "textures/pine_leaves.png"), .visibility = VISIBILITY_CLIP, - .mipmap = true, .render = &render_color, .pointable = true, .selection_color = {1.0f, 1.0f, 1.0f}, @@ -153,10 +147,9 @@ ClientNodeDef client_node_def[COUNT_NODE] = { .tiles = { .paths = {ASSET_PATH "textures/palm_wood.png", ASSET_PATH "textures/palm_wood_top.png", NULL, NULL, NULL, NULL}, .indices = {0, 0, 0, 0, 1, 1}, - .textures = {NULL}, + .textures = {}, }, .visibility = VISIBILITY_SOLID, - .mipmap = true, .render = &render_color, .pointable = true, .selection_color = {1.0f, 1.0f, 1.0f}, @@ -166,7 +159,6 @@ ClientNodeDef client_node_def[COUNT_NODE] = { { .tiles = TILES_SIMPLE(ASSET_PATH "textures/palm_leaves.png"), .visibility = VISIBILITY_SOLID, - .mipmap = true, .render = &render_color, .pointable = true, .selection_color = {1.0f, 1.0f, 1.0f}, @@ -176,7 +168,6 @@ ClientNodeDef client_node_def[COUNT_NODE] = { { .tiles = TILES_SIMPLE(ASSET_PATH "textures/sand.png"), .visibility = VISIBILITY_SOLID, - .mipmap = true, .render = NULL, .pointable = true, .selection_color = {1.0f, 1.0f, 1.0f}, @@ -186,7 +177,6 @@ ClientNodeDef client_node_def[COUNT_NODE] = { { .tiles = TILES_SIMPLE(ASSET_PATH "textures/water.png"), .visibility = VISIBILITY_BLEND, - .mipmap = true, .render = NULL, .pointable = false, .selection_color = {1.0f, 1.0f, 1.0f}, @@ -196,7 +186,6 @@ ClientNodeDef client_node_def[COUNT_NODE] = { { .tiles = TILES_SIMPLE(ASSET_PATH "textures/lava.png"), .visibility = VISIBILITY_BLEND, - .mipmap = true, .render = NULL, .pointable = false, .selection_color = {1.0f, 1.0f, 1.0f}, @@ -206,7 +195,6 @@ ClientNodeDef client_node_def[COUNT_NODE] = { { .tiles = TILES_SIMPLE(ASSET_PATH "textures/vulcano_stone.png"), .visibility = VISIBILITY_SOLID, - .mipmap = true, .render = NULL, .pointable = true, .selection_color = {1.0f, 1.0f, 1.0f}, @@ -214,19 +202,25 @@ ClientNodeDef client_node_def[COUNT_NODE] = { }, }; +Texture client_node_atlas; + void client_node_init() { + TextureAtlas atlas = texture_atlas_create( + client_config.atlas_size, client_config.atlas_size, 4, + (client_config.atlas_mipmap && client_config.mipmap) ? client_config.atlas_mipmap : 1); + for (NodeType node = 0; node < COUNT_NODE; node++) { ClientNodeDef *def = &client_node_def[node]; if (def->visibility != VISIBILITY_NONE) { - Texture *textures[6]; + TextureSlice textures[6]; for (int i = 0; i < 6; i++) { char *path = def->tiles.paths[i]; if (path) - textures[i] = texture_load(path, def->mipmap); + textures[i] = texture_atlas_add(&atlas, path); else break; } @@ -235,6 +229,13 @@ void client_node_init() def->tiles.textures[i] = textures[def->tiles.indices[i]]; } } + + client_node_atlas = texture_atlas_upload(&atlas); +} + +void client_node_deinit() +{ + texture_destroy(&client_node_atlas); } void client_node_delete(TerrainNode *node) diff --git a/src/client/client_node.h b/src/client/client_node.h index a2f88ee..345a1d6 100644 --- a/src/client/client_node.h +++ b/src/client/client_node.h @@ -21,12 +21,11 @@ typedef struct { typedef struct { struct { - char *paths[6]; // input - int indices[6]; // input - Texture *textures[6]; // output + char *paths[6]; // input + int indices[6]; // input + TextureSlice textures[6]; // output } tiles; NodeVisibility visibility; - bool mipmap; void (*render)(NodeArgsRender *args); bool pointable; v3f32 selection_color; @@ -34,7 +33,10 @@ typedef struct { } ClientNodeDef; extern ClientNodeDef client_node_def[]; +extern Texture client_node_atlas; + void client_node_init(); +void client_node_deinit(); void client_node_delete(TerrainNode *node); void client_node_deserialize(TerrainNode *node, Blob buffer); diff --git a/src/client/game.c b/src/client/game.c index 8366976..b384502 100644 --- a/src/client/game.c +++ b/src/client/game.c @@ -125,5 +125,6 @@ void game(Flag *gfx_init) interact_deinit(); client_item_deinit(); client_inventory_deinit(); + client_node_deinit(); } diff --git a/src/client/terrain_gfx.c b/src/client/terrain_gfx.c index b0cffc9..98dc30c 100644 --- a/src/client/terrain_gfx.c +++ b/src/client/terrain_gfx.c @@ -159,7 +159,12 @@ static inline void render_node(ChunkRenderData *data, v3s32 offset) if (def->render) def->render(&args); - model_batch_add_vertex(batch, def->tiles.textures[args.f]->txo, &args.vertex); + TextureSlice *texture = &def->tiles.textures[args.f]; + v2f32 *tcoord = &args.vertex.cube.textureCoordinates; + tcoord->x = texture->tex_coord_x + tcoord->x * texture->tex_coord_w; + tcoord->y = texture->tex_coord_y + tcoord->y * texture->tex_coord_h; + + model_batch_add_vertex(batch, client_node_atlas.txo, &args.vertex); } } } diff --git a/src/client/texture.c b/src/client/texture.c index a531d42..14d1409 100644 --- a/src/client/texture.c +++ b/src/client/texture.c @@ -182,3 +182,128 @@ void texture_destroy(Texture *texture) { glDeleteTextures(1, &texture->txo); GL_DEBUG } + +TextureAtlas texture_atlas_create(int width, int height, int channels, size_t mipmap_levels) +{ + TextureAtlas atlas = { + .texture = { 0, width, height, channels }, + .mipmap_levels = mipmap_levels, + .data = malloc(sizeof(unsigned char *) * mipmap_levels), + .alloc = { + .x = 0, + .y = 0, + .row_height = 0, + }, + }; + + for (size_t i = 0; i < atlas.mipmap_levels; i++) { + atlas.data[i] = calloc(width * height, channels); + + width /= 2; + height /= 2; + + if (width == 0 || height == 0) + atlas.mipmap_levels = i; + } + + return atlas; +} + +TextureSlice texture_atlas_add(TextureAtlas *atlas, char *path) +{ + int width, height, channels; + + unsigned char *data = stbi_load(path, &width, &height, &channels, 0); + if (!data) { + fprintf(stderr, "[error] failed to load texture %s\n", path); + abort(); + } + + if (channels != atlas->texture.channels) { + fprintf(stderr, "[error] invalid number of channels in %s, expected %d got %d\n", + path, atlas->texture.channels, channels); + abort(); + } + + if (atlas->mipmap_levels > 1 && ((width & (width - 1)) != 0 || (height & (height - 1)) != 0)) { + fprintf(stderr, "[error] texture %s dimensions are not powers of two\n", path); + abort(); + } + + if (width > atlas->texture.width || height > atlas->texture.height) { + fprintf(stderr, "[error] texture %s does not fit into atlas\n", path); + abort(); + } + + if ((int) atlas->alloc.x + width >= atlas->texture.width) { + atlas->alloc.x = 0; + atlas->alloc.y += atlas->alloc.row_height; + atlas->alloc.row_height = 0; + } + + if ((int) atlas->alloc.y + height >= atlas->texture.height) { + fprintf(stderr, "[error] texture %s has no space left in atlas\n", path); + abort(); + } + + unsigned int x = atlas->alloc.x; + unsigned int y = atlas->alloc.y; + + atlas->alloc.x += width; + if ((int) atlas->alloc.row_height < height) + atlas->alloc.row_height = height; + + for (size_t i = 0; i < atlas->mipmap_levels; i++) { + unsigned int mm_width = width >> i; + unsigned int mm_height = height >> i; + + if (mm_width == 0) mm_width = 1; + if (mm_height == 0) mm_height = 1; + + stbir_resize_uint8( + data, width, height, 0, + atlas->data[i] + ((atlas->texture.width >> i) * (y >> i) + (x >> i)) * channels, + mm_width, mm_height, (atlas->texture.width >> i) * channels, + channels + ); + } + + stbi_image_free(data); + + return (TextureSlice) { + .tex_coord_x = ((GLfloat) x) / ((GLfloat) atlas->texture.width), + .tex_coord_y = ((GLfloat) y) / ((GLfloat) atlas->texture.height), + .tex_coord_w = ((GLfloat) width) / ((GLfloat) atlas->texture.width), + .tex_coord_h = ((GLfloat) height) / ((GLfloat) atlas->texture.height), + .width = width, + .height = height, + }; +} + +Texture texture_atlas_upload(TextureAtlas *atlas) +{ + glGenTextures(1, &atlas->texture.txo); GL_DEBUG + glBindTexture(GL_TEXTURE_2D, atlas->texture.txo); GL_DEBUG + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, atlas->mipmap_levels > 1 + ? GL_NEAREST_MIPMAP_NEAREST : GL_NEAREST); GL_DEBUG + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); GL_DEBUG + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); GL_DEBUG + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); GL_DEBUG + + if (atlas->mipmap_levels > 1) { + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0); GL_DEBUG + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, atlas->mipmap_levels-1); GL_DEBUG + } + + for (size_t i = 0; i < atlas->mipmap_levels; i++) { + glTexImage2D(GL_TEXTURE_2D, i, GL_RGBA, atlas->texture.width >> i, atlas->texture.height >> i, + 0, GL_RGBA, GL_UNSIGNED_BYTE, atlas->data[i]); GL_DEBUG + stbi_image_free(atlas->data[i]); + } + + free(atlas->data); + atlas->data = NULL; + + return atlas->texture; +} diff --git a/src/client/texture.h b/src/client/texture.h index 25111f7..cd16811 100644 --- a/src/client/texture.h +++ b/src/client/texture.h @@ -9,9 +9,28 @@ typedef struct { int width, height, channels; } Texture; +typedef struct { + GLfloat tex_coord_x, tex_coord_y; + GLfloat tex_coord_w, tex_coord_h; + unsigned int width, height; +} TextureSlice; + +typedef struct { + Texture texture; + size_t mipmap_levels; + unsigned char **data; + struct { + unsigned int x, y, row_height; + } alloc; +} TextureAtlas; + Texture *texture_load(char *path, bool mipmap); Texture *texture_load_cubemap(char *path, bool linear_filter); void texture_upload(Texture *texture, unsigned char *data, GLenum format, bool mipmap); void texture_destroy(Texture *texture); +TextureAtlas texture_atlas_create(int width, int height, int channels, size_t mipmap_levels); +TextureSlice texture_atlas_add(TextureAtlas *atlas, char *path); +Texture texture_atlas_upload(TextureAtlas *atlas); + #endif // _TEXTURE_H_