summaryrefslogtreecommitdiffstats
path: root/game/client/world/voxel_atlas.cc
blob: 101874707e384755afce0c89ac125635cd411c3c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
#include "client/pch.hh"

#include "client/world/voxel_atlas.hh"

#include "core/math/constexpr.hh"
#include "core/math/crc64.hh"
#include "core/resource/image.hh"
#include "core/resource/resource.hh"

struct AtlasPlane final {
    std::unordered_map<std::size_t, std::size_t> lookup;
    std::vector<world::AtlasStrip> strips;
    std::size_t layer_count_max;
    std::size_t layer_count;
    std::size_t plane_id;
    GLuint gl_texture;
};

static int atlas_width;
static int atlas_height;
static std::size_t atlas_count;
static std::vector<AtlasPlane> planes;

// Certain animated and varied voxels just double their
// textures (see the "default" texture part in VoxelInfoBuilder::build)
// so there could either be six UNIQUE atlas strips or only one
// https://crypto.stackexchange.com/questions/55162/best-way-to-hash-two-values-into-one
static std::size_t vector_hash(const std::vector<std::string>& strings)
{
    std::size_t source = 0;
    for(const std::string& str : strings)
        source += math::crc64(str);
    return math::crc64(&source, sizeof(source));
}

static void plane_setup(AtlasPlane& plane)
{
    glGenTextures(1, &plane.gl_texture);
    glBindTexture(GL_TEXTURE_2D_ARRAY, plane.gl_texture);
    glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, atlas_width, atlas_height, plane.layer_count_max, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR);
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_REPEAT);
}

static world::AtlasStrip* plane_lookup(AtlasPlane& plane, std::size_t hash_value)
{
    const auto it = plane.lookup.find(hash_value);

    if(it != plane.lookup.cend()) {
        return &plane.strips[it->second];
    }

    return nullptr;
}

static world::AtlasStrip* plane_new_strip(AtlasPlane& plane, const std::vector<std::string>& paths, std::size_t hash_value)
{
    world::AtlasStrip strip = {};
    strip.offset = plane.layer_count;
    strip.plane = plane.plane_id;

    glBindTexture(GL_TEXTURE_2D_ARRAY, plane.gl_texture);

    for(std::size_t i = 0; i < paths.size(); ++i) {
        if(auto image = resource::load<Image>(paths[i].c_str(), IMAGE_LOAD_FLIP)) {
            if((image->size.x != atlas_width) || (image->size.y != atlas_height)) {
                spdlog::warn("atlas: {}: size mismatch", paths[i]);
                continue;
            }

            const std::size_t offset = strip.offset + i;
            glTexSubImage3D(
                GL_TEXTURE_2D_ARRAY, 0, 0, 0, offset, image->size.x, image->size.y, 1, GL_RGBA, GL_UNSIGNED_BYTE, image->pixels);
        }
    }

    plane.layer_count += paths.size();

    const std::size_t index = plane.strips.size();
    plane.lookup.emplace(hash_value, index);
    plane.strips.push_back(std::move(strip));
    return &plane.strips[index];
}

void world::voxel_atlas::create(int width, int height, std::size_t count)
{
    GLint max_plane_layers;

    atlas_width = 1 << math::log2(width);
    atlas_height = 1 << math::log2(height);

    // Clipping this at OpenGL 4.5 limit of 2048 is important due to
    // how voxel quad meshes are packed in memory: each texture index is
    // confined in 11 bits so having bigger atlas planes makes no sense;
    glGetIntegerv(GL_MAX_ARRAY_TEXTURE_LAYERS, &max_plane_layers);
    max_plane_layers = math::clamp(max_plane_layers, 256, 2048);

    for(long i = count; i > 0L; i -= max_plane_layers) {
        AtlasPlane plane = {};
        plane.plane_id = planes.size();
        plane.layer_count_max = math::min<std::size_t>(max_plane_layers, i);
        plane.layer_count = 0;

        const std::size_t save_id = plane.plane_id;
        planes.push_back(std::move(plane));
        plane_setup(planes[save_id]);
    }

    spdlog::debug("voxel_atlas: count={}", count);
    spdlog::debug("voxel_atlas: atlas_size=[{}x{}]", atlas_width, atlas_height);
    spdlog::debug("voxel_atlas: max_plane_layers={}", max_plane_layers);
}

void world::voxel_atlas::destroy(void)
{
    for(const AtlasPlane& plane : planes)
        glDeleteTextures(1, &plane.gl_texture);
    atlas_width = 0;
    atlas_height = 0;
    planes.clear();
}

std::size_t world::voxel_atlas::plane_count(void)
{
    return planes.size();
}

GLuint world::voxel_atlas::plane_texture(std::size_t plane_id)
{
    if(plane_id < planes.size()) {
        return planes[plane_id].gl_texture;
    }
    else {
        return 0;
    }
}

void world::voxel_atlas::generate_mipmaps(void)
{
    for(const AtlasPlane& plane : planes) {
        glBindTexture(GL_TEXTURE_2D_ARRAY, plane.gl_texture);
        glGenerateMipmap(GL_TEXTURE_2D_ARRAY);
    }
}

world::AtlasStrip* world::voxel_atlas::find_or_load(const std::vector<std::string>& paths)
{
    const std::size_t hash_value = vector_hash(paths);

    for(AtlasPlane& plane : planes) {
        if(AtlasStrip* strip = plane_lookup(plane, hash_value)) {
            return strip;
        }

        continue;
    }

    for(AtlasPlane& plane : planes) {
        if((plane.layer_count + paths.size()) <= plane.layer_count_max) {
            return plane_new_strip(plane, paths, hash_value);
        }

        continue;
    }

    return nullptr;
}

world::AtlasStrip* world::voxel_atlas::find(const std::vector<std::string>& paths)
{
    const std::size_t hash_value = vector_hash(paths);

    for(AtlasPlane& plane : planes) {
        if(AtlasStrip* strip = plane_lookup(plane, hash_value)) {
            return strip;
        }

        continue;
    }

    return nullptr;
}