font rendering

main
Brett 2024-11-13 15:24:58 -05:00
parent ed6f36020e
commit f75cbbb10c
7 changed files with 212 additions and 158 deletions

View File

@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.25) cmake_minimum_required(VERSION 3.25)
include(FetchContent) include(FetchContent)
set(BLT_GRAPHICS_VERSION 1.0.10) set(BLT_GRAPHICS_VERSION 1.0.11)
set(BLT_GRAPHICS_TEST_VERSION 0.0.1) set(BLT_GRAPHICS_TEST_VERSION 0.0.1)
project(BLT_WITH_GRAPHICS VERSION ${BLT_GRAPHICS_VERSION}) project(BLT_WITH_GRAPHICS VERSION ${BLT_GRAPHICS_VERSION})

0
build_emscript.sh Normal file → Executable file
View File

View File

@ -40,23 +40,25 @@ namespace blt::gfx::font
class font_face_t class font_face_t
{ {
public: public:
explicit font_face_t(std::string_view path); explicit font_face_t(std::string_view path, std::string_view font_name);
explicit font_face_t(const blt::u8* data, blt::size_t size, bool compressed = false); explicit font_face_t(std::string_view font_name, const blt::u8* data, blt::size_t size, bool compressed = false);
void set_pixel_sizes(blt::u32 width, blt::u32 height) const; void set_pixel_sizes(blt::u32 width, blt::u32 height) const;
[[nodiscard]] std::optional<int> load_char(FT_ULong c) const; [[nodiscard]] std::optional<int> load_char(FT_ULong c) const;
[[nodiscard]] FT_Face* get() const [[nodiscard]] FT_Face* get() const
{ { return face.get(); }
return face.get();
} [[nodiscard]] std::string_view get_name() const
{ return font_name; }
private: private:
// decompressed data ownership // decompressed data ownership
std::shared_ptr<blt::u8[]> internal_uncompressed_data; std::shared_ptr<blt::u8[]> internal_uncompressed_data;
std::shared_ptr<FT_Face> face; std::shared_ptr<FT_Face> face;
std::string font_name;
}; };
struct font_file_t struct font_file_t

View File

@ -53,14 +53,14 @@ namespace blt::gfx
added_font_results_t add_font(const font::font_file_t& file, blt::u64 start, float size); added_font_results_t add_font(const font::font_file_t& file, blt::u64 start, float size);
bounded_t& get_glyph(blt::u64 c, float size) bounded_t& get_glyph(blt::u64 c, std::string_view font)
{ return glyphs.at(size).at(c); } { return glyphs.at(font).at(c); }
[[nodiscard]] const bounded_t& get_glyph(blt::u64 c, float size) const [[nodiscard]] const bounded_t& get_glyph(blt::u64 c, std::string_view font) const
{ return glyphs.at(size).at(c); } { return glyphs.at(font).at(c); }
private: private:
blt::hashmap_t<float, blt::hashmap_t<blt::u64, bounded_t>> glyphs; blt::hashmap_t<std::string, blt::hashmap_t<blt::u64, bounded_t>> glyphs;
blt::u32 used_width = 0; blt::u32 used_width = 0;
blt::u32 used_height = 0; blt::u32 used_height = 0;
blt::u32 padding; blt::u32 padding;
@ -78,45 +78,41 @@ namespace blt::gfx
struct bounded_font_t struct bounded_font_t
{ {
std::unique_ptr<font_texture_atlas> atlas; font_texture_atlas* atlas;
// float min_size, max_size; blt::u64 min_char, max_char;
// blt::u64 min_char, max_char;
std::vector<size_bounds_t> size_limits;
explicit bounded_font_t(std::unique_ptr<font_texture_atlas> atlas): atlas(std::move(atlas)) bounded_font_t(font_texture_atlas* atlas, u64 minChar, u64 maxChar): atlas(atlas), min_char(minChar), max_char(maxChar)
{} {}
// bounded_font_t(std::unique_ptr<font_texture_atlas> atlas, float minSize, float maxSize, u64 minChar, u64 maxChar): // replace with font -> char range. use scaling.
// atlas(std::move(atlas)), min_size(minSize), max_size(maxSize), min_char(minChar), max_char(maxChar)
// {}
}; };
public: public:
explicit font_generator_t(blt::i32 dimensions, blt::u32 padding, const std::vector<float>& sizes_to_generate); explicit font_generator_t(blt::i32 dimensions, blt::u32 padding, float gen_size);
void add_font(const font::font_file_t& file); void add_font(const font::font_file_t& file);
inline font_texture_atlas& get_texture(float size, blt::u64 c) inline font_texture_atlas& get_texture(const std::string& font, blt::u64 c)
{ {
for (auto& atlas : atlases) for (auto& atlas : font_associations[font])
{ {
BLT_TRACE("(%f %ld), %f %f, %ld %ld", size, c, atlas.min_size, atlas.max_size, atlas.min_char, atlas.max_char);
if (!(size >= atlas.min_size && size <= atlas.max_size))
continue;
if (c >= atlas.min_char && c <= atlas.max_char) if (c >= atlas.min_char && c <= atlas.max_char)
return *atlas.atlas; return *atlas.atlas;
} }
throw std::runtime_error("Character not found inside atlases at this size"); throw std::runtime_error("Character not found inside atlases with font '" + font + "'");
} }
[[nodiscard]] blt::i32 get_dimensions() const [[nodiscard]] blt::i32 get_dimensions() const
{ return dimensions; } { return dimensions; }
[[nodiscard]] float get_generated_size() const
{ return gen_size; }
private: private:
blt::i32 dimensions; blt::i32 dimensions;
blt::u32 padding; blt::u32 padding;
std::vector<bounded_font_t> atlases; std::vector<std::unique_ptr<font_texture_atlas>> atlases;
// doesn't make sense to store this. blt::hashmap_t<std::string, std::vector<bounded_font_t>> font_associations;
std::vector<float> sizes_to_generate; float gen_size;
}; };
class font_renderer_t class font_renderer_t
@ -126,9 +122,9 @@ namespace blt::gfx
{ {
friend font_renderer_t; friend font_renderer_t;
public: public:
explicit compiled_text_t(font_generator_t& generator); compiled_text_t& setText(std::string_view str, float size, std::string_view font = "__default");
void change_text(std::string_view str, float size); compiled_text_t& recompile();
compiled_text_t& setPosition(const blt::vec2f& pos) compiled_text_t& setPosition(const blt::vec2f& pos)
{ {
@ -136,9 +132,22 @@ namespace blt::gfx
return *this; return *this;
} }
compiled_text_t& setScale(const blt::vec2f& s) compiled_text_t& setBounds(const blt::vec2f& limit)
{ {
scale = s; bounds = limit;
return *this;
}
compiled_text_t& setUnrestrictedBounds()
{
bounds = {};
return *this;
}
compiled_text_t& setSize(float size)
{
current_size = size;
scale = {size / generator.get_generated_size(), size / generator.get_generated_size()};
return *this; return *this;
} }
@ -154,12 +163,6 @@ namespace blt::gfx
return *this; return *this;
} }
compiled_text_t& setScale(float x, float y)
{
scale = {x, y};
return *this;
}
compiled_text_t& setColor(float r, float g, float b, float a = 1) compiled_text_t& setColor(float r, float g, float b, float a = 1)
{ {
color = {r, g, b, a}; color = {r, g, b, a};
@ -172,6 +175,9 @@ namespace blt::gfx
return *this; return *this;
} }
[[nodiscard]] const vec2f& getBounds() const
{ return bounds; }
[[nodiscard]] const vec2f& getPosition() const [[nodiscard]] const vec2f& getPosition() const
{ return position; } { return position; }
@ -185,6 +191,8 @@ namespace blt::gfx
{ return z_index; } { return z_index; }
private: private:
explicit compiled_text_t(font_generator_t& generator);
void draw(); void draw();
struct text_render_info_t struct text_render_info_t
@ -199,10 +207,10 @@ namespace blt::gfx
}; };
font_generator_t& generator; font_generator_t& generator;
std::string contents; std::string contents, font;
vertex_array_t vao{}; vertex_array_t vao{};
std::vector<text_render_info_t> renders; std::vector<text_render_info_t> renders;
blt::vec2f position; blt::vec2f position, bounds;
blt::vec2f scale = {1, 1}; blt::vec2f scale = {1, 1};
blt::vec4 color = blt::make_color(1, 1, 1); blt::vec4 color = blt::make_color(1, 1, 1);
float z_index = 0; float z_index = 0;
@ -212,9 +220,9 @@ namespace blt::gfx
public: public:
explicit font_renderer_t(); explicit font_renderer_t();
void create_default(blt::i32 dimensions = 0); void create_default(float gen_size = 250, blt::i32 dimensions = 0);
void create(const std::vector<float>& generated_font_sizes, blt::i32 dimensions = 0); void create(float gen_size, blt::i32 dimensions = 0);
void cleanup(); void cleanup();
@ -223,11 +231,11 @@ namespace blt::gfx
generator->add_font(file); generator->add_font(file);
} }
void add_default_font(std::string_view path); void add_default_font(std::string_view path, std::string_view name = "__default");
void add_default_font(const blt::u8* data, blt::size_t size, bool compressed = false); void add_default_font(const blt::u8* data, blt::size_t size, bool compressed = false, std::string_view name = "__default");
compiled_text_t* create_text(std::string_view str, float size); compiled_text_t* create_text(std::string_view str, float size, std::string_view font = "__default");
void destroy_text(compiled_text_t* text); void destroy_text(compiled_text_t* text);

0
resources/fonts/a.out Normal file → Executable file
View File

View File

@ -208,8 +208,9 @@ namespace blt::gfx::font
FT_Done_FreeType(*lib); FT_Done_FreeType(*lib);
} }
font_face_t::font_face_t(std::string_view path) font_face_t::font_face_t(std::string_view path, std::string_view name)
{ {
font_name = name;
face = std::shared_ptr<FT_Face>(new FT_Face{}, [](FT_Face* ptr) { face = std::shared_ptr<FT_Face>(new FT_Face{}, [](FT_Face* ptr) {
FT_Done_Face(*ptr); FT_Done_Face(*ptr);
delete ptr; delete ptr;
@ -224,8 +225,9 @@ namespace blt::gfx::font
} }
} }
font_face_t::font_face_t(const blt::u8* data, blt::size_t size, bool compressed) font_face_t::font_face_t(std::string_view name, const blt::u8* data, blt::size_t size, bool compressed)
{ {
font_name = name;
face = std::shared_ptr<FT_Face>(new FT_Face{}, [](FT_Face* ptr) { face = std::shared_ptr<FT_Face>(new FT_Face{}, [](FT_Face* ptr) {
FT_Done_Face(*ptr); FT_Done_Face(*ptr);
delete ptr; delete ptr;

View File

@ -19,6 +19,7 @@
#include <blt/gfx/renderer/shaders/2d_font.vert> #include <blt/gfx/renderer/shaders/2d_font.vert>
#include <blt/gfx/renderer/shaders/2d_font.frag> #include <blt/gfx/renderer/shaders/2d_font.frag>
#include <blt/gfx/shader.h> #include <blt/gfx/shader.h>
#include <cctype>
// TODO: signed distance fonts // TODO: signed distance fonts
//#include <msdfgen.h> //#include <msdfgen.h>
@ -85,7 +86,7 @@ namespace blt::gfx
upload(face->glyph->bitmap.buffer, GL_RED, 0, static_cast<blt::i32>(begin_width), static_cast<blt::i32>(begin_height), upload(face->glyph->bitmap.buffer, GL_RED, 0, static_cast<blt::i32>(begin_width), static_cast<blt::i32>(begin_height),
static_cast<blt::i32>(width), static_cast<blt::i32>(height), GL_UNSIGNED_BYTE); static_cast<blt::i32>(width), static_cast<blt::i32>(height), GL_UNSIGNED_BYTE);
glyphs[size].insert(std::pair{i, bounded_t{ glyphs[file.font.get_name()].insert(std::pair{i, bounded_t{
font::glyph_t{ font::glyph_t{
{width, height}, {width, height},
{face->glyph->bitmap_left, face->glyph->bitmap_top}, {face->glyph->bitmap_left, face->glyph->bitmap_top},
@ -98,52 +99,36 @@ namespace blt::gfx
return {-1ul, min_index_used, max_index_used}; return {-1ul, min_index_used, max_index_used};
} }
font_generator_t::font_generator_t(blt::i32 dimensions, blt::u32 padding, const std::vector<float>& sizes_to_generate): font_generator_t::font_generator_t(blt::i32 dimensions, blt::u32 padding, float gen_size):
dimensions(dimensions), padding(padding), sizes_to_generate(sizes_to_generate) dimensions(dimensions), padding(padding), gen_size(gen_size)
{} {}
void font_generator_t::add_font(const font::font_file_t& file) void font_generator_t::add_font(const font::font_file_t& file)
{ {
glPixelStorei(GL_UNPACK_ALIGNMENT, 1); glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
float min_size = std::numeric_limits<float>::max(); auto& vec = atlases;
float max_size = 0;
for (const auto& size : sizes_to_generate) if (atlases.empty())
atlases.emplace_back(std::make_unique<font_texture_atlas>(dimensions, padding));
auto& font_vec = font_associations[file.font.get_name()];
blt::u64 start_index = file.character_min_index;
while (start_index != -1ul)
{ {
min_size = std::min(size, min_size); // add font and associate results with a font file
max_size = std::max(size, max_size); auto results = atlases.back()->add_font(file, start_index, gen_size);
auto& vec = atlases; font_vec.emplace_back(atlases.back().get(), results.min_index_used, results.max_index_used);
// didn't fit, need to push new atlas
if (vec.empty()) if (results.last_inserted_index != -1ul)
vec.emplace_back(std::make_unique<font_texture_atlas>(dimensions, padding), min_size, max_size, file.character_min_index, 0); vec.emplace_back(std::make_unique<font_texture_atlas>(dimensions, padding));
start_index = results.last_inserted_index;
blt::u64 start_index = file.character_min_index;
while (start_index != -1ul)
{
auto results = vec.back().atlas->add_font(file, start_index, size);
vec.back().min_char = results.min_index_used;
vec.back().max_char = results.max_index_used;
vec.back().min_size = min_size;
vec.back().max_size = max_size;
if (results.last_inserted_index != -1ul)
{
vec.emplace_back(std::make_unique<font_texture_atlas>(dimensions, padding), min_size, max_size, start_index, 0);
min_size = size;
max_size = size;
}
start_index = results.last_inserted_index;
// BLT_INFO("%f, %ld", size, start_index);
}
} }
glPixelStorei(GL_UNPACK_ALIGNMENT, 4); glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
// BLT_TRACE(atlases.size());
} }
extern float square_vertices[20];
extern const unsigned int square_indices[6];
font_renderer_t::font_renderer_t() = default; font_renderer_t::font_renderer_t() = default;
void font_renderer_t::create(const std::vector<float>& generated_font_sizes, blt::i32 dimensions) void font_renderer_t::create(float gen_size, blt::i32 dimensions)
{ {
font_shader = shader_t::make_unique(shader_2d_font_vert, shader_2d_font_frag); font_shader = shader_t::make_unique(shader_2d_font_vert, shader_2d_font_frag);
font_shader->bind(); font_shader->bind();
@ -156,7 +141,14 @@ namespace blt::gfx
dimensions = std::min(size, 2048); dimensions = std::min(size, 2048);
BLT_INFO("Max texture size %d, font renderer will use size %d", size, dimensions); BLT_INFO("Max texture size %d, font renderer will use size %d", size, dimensions);
} }
generator = std::make_unique<font_generator_t>(dimensions, 2, generated_font_sizes); generator = std::make_unique<font_generator_t>(dimensions, 2, gen_size);
}
void font_renderer_t::create_default(float gen_size, blt::i32 dimensions)
{
create(gen_size, dimensions);
add_default_font(reinterpret_cast<const blt::u8*>(font::default_font_compressed_data), font::default_font_compressed_size, true);
} }
void font_renderer_t::cleanup() void font_renderer_t::cleanup()
@ -173,7 +165,7 @@ namespace blt::gfx
blt::gfx::font::cleanup(); blt::gfx::font::cleanup();
} }
font_renderer_t::compiled_text_t* font_renderer_t::create_text(std::string_view str, float size) font_renderer_t::compiled_text_t* font_renderer_t::create_text(std::string_view str, float size, std::string_view font)
{ {
std::unique_ptr<font_renderer_t::compiled_text_t> ptr; std::unique_ptr<font_renderer_t::compiled_text_t> ptr;
if (!cached_text_objects.empty()) if (!cached_text_objects.empty())
@ -182,9 +174,9 @@ namespace blt::gfx
cached_text_objects.erase(cached_text_objects.begin()); cached_text_objects.erase(cached_text_objects.begin());
} else } else
{ {
ptr = std::make_unique<compiled_text_t>(*generator); ptr = std::unique_ptr<compiled_text_t>(new compiled_text_t{*generator});
} }
ptr->change_text(str, size); ptr->setText(str, size, font);
auto index = added_texts.size(); auto index = added_texts.size();
added_texts.push_back(std::move(ptr)); added_texts.push_back(std::move(ptr));
text_ptr_to_index[added_texts.back().get()] = index; text_ptr_to_index[added_texts.back().get()] = index;
@ -218,90 +210,30 @@ namespace blt::gfx
} }
} }
void font_renderer_t::create_default(blt::i32 dimensions) void font_renderer_t::add_default_font(std::string_view path, std::string_view name)
{ {
create({9, 11, 12, 13, 14, 16, 18, 24, 32, 36, 40, 48, 52, 64, 72, 96, 106, 250}, dimensions); font::font_face_t default_font_face{path, name};
add_default_font(reinterpret_cast<const blt::u8*>(font::default_font_compressed_data), font::default_font_compressed_size, true);
}
void font_renderer_t::add_default_font(std::string_view path)
{
font::font_face_t default_font_face{path};
font::font_file_t default_font{default_font_face, 0, 128}; font::font_file_t default_font{default_font_face, 0, 128};
add_font(default_font); add_font(default_font);
} }
void font_renderer_t::add_default_font(const blt::u8* data, blt::size_t size, bool compressed) void font_renderer_t::add_default_font(const blt::u8* data, blt::size_t size, bool compressed, std::string_view name)
{ {
font::font_face_t default_font_face{data, size, compressed}; font::font_face_t default_font_face{name, data, size, compressed};
font::font_file_t default_font{default_font_face, 0, 128}; font::font_file_t default_font{default_font_face, 0, 128};
add_font(default_font); add_font(default_font);
} }
void font_renderer_t::compiled_text_t::change_text(std::string_view str, float size) font_renderer_t::compiled_text_t& font_renderer_t::compiled_text_t::setText(std::string_view str, float size, std::string_view f)
{ {
if (str == contents) if (str == contents)
return; return *this;
contents = str; contents = str;
current_size = size; font = f;
static std::vector<float> vertices; setSize(size);
vertices.clear(); recompile();
renders.clear(); return *this;
font_texture_atlas* last_texture = nullptr;
blt::size_t draw_start = 0;
blt::size_t draw_count = 0;
float x = 0;
// TODO: parker UTF8
for (const auto& c : str)
{
auto& texture = generator.get_texture(size, c);
if (last_texture == nullptr)
last_texture = &texture;
else if (last_texture != &texture)
{
renders.emplace_back(last_texture, draw_start, draw_count);
draw_start += draw_count;
draw_count = 0;
}
auto& ch = texture.get_glyph(c, size);
auto dims = static_cast<float>(generator.get_dimensions());
float x_pos = x + static_cast<float>(ch.glyph.bearing.x());
float y_pos = -static_cast<float>(ch.glyph.size.y() - ch.glyph.bearing.y());
auto w = static_cast<float>(ch.glyph.size.x());
auto h = static_cast<float>(ch.glyph.size.y());
auto texture_min_x = static_cast<float>(ch.min.x()) / dims;
auto texture_min_y = static_cast<float>(ch.min.y()) / dims;
auto texture_max_x = static_cast<float>(ch.max.x()) / dims;
auto texture_max_y = static_cast<float>(ch.max.y()) / dims;
// BLT_TRACE("%c: %f | xy[%f %f] wh[%f %f] | min[%f %f] max[%f %f] |", c, x, x_pos, y_pos, w, h, texture_min_x, texture_min_y, texture_max_x, texture_max_y);
std::array<float, 6 * 4> local_vert = {
x_pos, y_pos + h, texture_min_x, texture_min_y,
x_pos, y_pos, texture_min_x, texture_max_y,
x_pos + w, y_pos, texture_max_x, texture_max_y,
x_pos, y_pos + h, texture_min_x, texture_min_y,
x_pos + w, y_pos, texture_max_x, texture_max_y,
x_pos + w, y_pos + h, texture_max_x, texture_min_y
};
vertices.insert(vertices.end(), local_vert.begin(), local_vert.end());
draw_count += 6;
x += static_cast<float>(ch.glyph.advance >> 6);
}
// BLT_TRACE("size: %ld %ld %ld", draw_count, vertices.size(), vertices.size() / 4);
renders.emplace_back(last_texture, draw_start, draw_count);
vao.getBuffer(0).update(static_cast<long>(vertices.size() * sizeof(float)), vertices.data());
} }
font_renderer_t::compiled_text_t::compiled_text_t(font_generator_t& generator): generator(generator) font_renderer_t::compiled_text_t::compiled_text_t(font_generator_t& generator): generator(generator)
@ -330,5 +262,115 @@ namespace blt::gfx
} }
} }
struct word_t
{
std::vector<float> vertices;
float x = 0;
void apply(std::vector<float>& out)
{
for (float v : vertices)
out.push_back(v + x);
vertices.clear();
x = 0;
}
};
struct position_tracker_t
{
float global_x = 0, global_y = 0;
void newline(float line_size)
{
global_y -= line_size;
global_x = 0;
}
void advance(blt::u64 size)
{
global_x += static_cast<float>(size >> 6);
}
};
font_renderer_t::compiled_text_t& font_renderer_t::compiled_text_t::recompile()
{
static std::vector<float> vertices;
static word_t current_word;
vertices.clear();
renders.clear();
font_texture_atlas* last_texture = nullptr;
blt::size_t draw_start = 0;
blt::size_t draw_count = 0;
position_tracker_t current_pos;
// TODO: parker UTF8
for (const auto& c : contents)
{
// std::cout << "C: " << c << " || " << static_cast<int>(c) << std::endl;
if (c == '\n')
{
current_pos.newline(generator.get_generated_size());
continue;
}
auto& texture = generator.get_texture(std::string(font), c);
auto& ch = texture.get_glyph(c, font);
// if (std::isblank(c))
// {
// if (bounds.x() > 0 && current_pos.global_x + current_word.x > bounds.x())
// current_pos.newline(generator.get_generated_size());
//
// }
if (std::isspace(c))
current_word.apply(vertices);
if (last_texture == nullptr)
last_texture = &texture;
else if (last_texture != &texture)
{
renders.emplace_back(last_texture, draw_start, draw_count);
draw_start += draw_count;
draw_count = 0;
}
auto dims = static_cast<float>(generator.get_dimensions());
float x_pos = current_pos.global_x + static_cast<float>(ch.glyph.bearing.x());
float y_pos = current_pos.global_y - static_cast<float>(ch.glyph.size.y() - ch.glyph.bearing.y());
auto w = static_cast<float>(ch.glyph.size.x());
auto h = static_cast<float>(ch.glyph.size.y());
auto texture_min_x = static_cast<float>(ch.min.x()) / dims;
auto texture_min_y = static_cast<float>(ch.min.y()) / dims;
auto texture_max_x = static_cast<float>(ch.max.x()) / dims;
auto texture_max_y = static_cast<float>(ch.max.y()) / dims;
// BLT_TRACE("%c: %f | xy[%f %f] wh[%f %f] | min[%f %f] max[%f %f] |", c, x, x_pos, y_pos, w, h, texture_min_x, texture_min_y, texture_max_x, texture_max_y);
std::array<float, 6 * 4> local_vert = {
x_pos, y_pos + h, texture_min_x, texture_min_y,
x_pos, y_pos, texture_min_x, texture_max_y,
x_pos + w, y_pos, texture_max_x, texture_max_y,
x_pos, y_pos + h, texture_min_x, texture_min_y,
x_pos + w, y_pos, texture_max_x, texture_max_y,
x_pos + w, y_pos + h, texture_max_x, texture_min_y
};
current_word.vertices.insert(current_word.vertices.end(), local_vert.begin(), local_vert.end());
draw_count += 6;
current_pos.advance(ch.glyph.advance);
}
current_word.apply(vertices);
// BLT_TRACE("size: %ld %ld %ld", draw_count, vertices.size(), vertices.size() / 4);
renders.emplace_back(last_texture, draw_start, draw_count);
vao.getBuffer(0).update(static_cast<long>(vertices.size() * sizeof(float)), vertices.data());
return *this;
}
} }