Initial version of sprite renderer code and fixed bugs related to shaders, camera and editor

dev
Shariq Shah 8 years ago
parent 3c8cd097c3
commit 33ae972f04
  1. 13
      README.md
  2. 14
      assets/shaders/sprite.frag
  3. 17
      assets/shaders/sprite.vert
  4. 12
      src/common/log.c
  5. 1
      src/common/log.h
  6. 16
      src/libsymmetry/camera.c
  7. 39
      src/libsymmetry/editor.c
  8. 37
      src/libsymmetry/game.c
  9. 1
      src/libsymmetry/gl_load.h
  10. 36
      src/libsymmetry/renderer.c
  11. 3
      src/libsymmetry/renderer.h
  12. 64
      src/libsymmetry/shader.c
  13. 9
      src/libsymmetry/shader.h
  14. 137
      src/libsymmetry/sprite.c
  15. 43
      src/libsymmetry/sprite.h

@ -155,10 +155,13 @@
- ## TODO
- Fix bugs with sprite batch renderer not working with projection matrix
- Implement necessary changes to run Soloud on linux
- Get rid of pkg-confg and system-installed SDL2 dependancy on linux and instead put custom compiled SDL libs in third_party similar to how we're handling it in windows
- Add fallback shader
- Implement Game States
- Store Materials in new format supported by parser
- Add model description file which has the same syntax supported by parser and modify old blender exporter to conform to new standards
- Implement sound/listener loading from scene file
- Finish loading scene from file
- Update makefiles to be able to compile the code in it's current state
- Find a solution for the asset import/export situation by either updating the blender exporter or adding assimp as dependancy
- Fix bugs with sound sources not updating
@ -331,3 +334,9 @@
* Changed Config to read/write using new Parser and Parser_Objects
* Implemented Reading/Writing keybindings using new parser object
* Replaced OpenAL with Soloud with SDL2 backend
* Implemented sound/listener loading from scene file
* Finished loading scene from file
* Initial implementation of immediate-mode batched sprite render
* Fixed bugs with shader include file pre-processor
* Fixed bugs with editor's camera property viewer
* Fixed bugs related to changing camera projection

@ -0,0 +1,14 @@
//include version.glsl
in vec2 uv;
in vec4 color;
uniform sampler2D sampler;
out vec4 frag_color;
void main()
{
frag_color = color * texture(sampler, uv);
//frag_color = vec4(0, 1, 0, 1);
}

@ -0,0 +1,17 @@
//include version.glsl
uniform mat4 mvp;
in vec3 vPosition;
in vec2 vUV;
in vec4 vColor;
out vec2 uv;
out vec4 color;
void main()
{
gl_Position = mvp * vec4(vPosition, 1.0);
uv = vUV;
color = vColor;
}

@ -74,6 +74,18 @@ void log_to_stdout(const char* message, ...)
printf("\n%s", COL_RESET);
}
void log_raw(const char* str, ...)
{
va_list console_list, file_list;
va_start(console_list, str);
va_copy(file_list, console_list);
vfprintf(log_file, str, file_list);
vprintf(str, console_list);
va_end(console_list);
va_end(file_list);
fflush(log_file);
}
void log_message(const char* message, ...)
{
printf("%sMSG : ", COL_DEFAULT);

@ -9,6 +9,7 @@ void log_message(const char* message, ...);
void log_warning(const char* message, ...);
void log_error(const char* context, const char* error, ...);
void log_to_stdout(const char* message, ...); /* Only use when logging is not initialized */
void log_raw(const char* str, ...);
FILE* log_file_handle_get(void);
void log_file_handle_set(FILE* file);

@ -5,6 +5,7 @@
#include "../common/array.h"
#include "framebuffer.h"
#include "texture.h"
#include "game.h"
#include "bounding_volumes.h"
#include "../common/utils.h"
@ -44,8 +45,8 @@ void camera_create(struct Entity* entity, int width, int height)
camera->farz = 1000.f;
camera->nearz = 0.1f;
camera->fov = 60.f;
camera->ortho = 0;
camera->resizeable = 1;
camera->ortho = false;
camera->resizeable = true;
float aspect_ratio = (float)width / (float)height;
camera->aspect_ratio = aspect_ratio <= 0.f ? (4.f / 3.f) : aspect_ratio;
mat4_identity(&camera->view_mat);
@ -62,7 +63,10 @@ void camera_update_view_proj(struct Entity* entity)
{
struct Camera* camera = &entity->camera;
mat4_identity(&camera->view_proj_mat);
mat4_mul(&camera->view_proj_mat, &camera->proj_mat, &camera->view_mat);
if(camera->ortho)
mat4_mul(&camera->view_proj_mat, &camera->view_proj_mat, &camera->proj_mat);
else
mat4_mul(&camera->view_proj_mat, &camera->proj_mat, &camera->view_mat);
update_frustum(camera);
}
@ -92,7 +96,11 @@ void camera_update_proj(struct Entity* entity)
}
else
{
mat4_ortho(&camera->proj_mat, -1, 1, -1, 1, camera->nearz, camera->farz);
int width, height;
struct Game_State* game_state = game_state_get();
platform->window.get_size(game_state->window, &width, &height);
mat4_ortho(&camera->proj_mat, 0, width, height, 0, camera->nearz, camera->farz);
}
camera_update_view_proj(entity);
}

@ -373,19 +373,48 @@ void editor_update(float dt)
{
if(nk_tree_push(context, NK_TREE_TAB, "Camera", NK_MAXIMIZED))
{
bool update = false;
struct Camera* camera = &entity->camera;
nk_layout_row_dynamic(context, row_height, 2);
nk_label(context, "Aspect Ratio", NK_TEXT_ALIGN_LEFT); nk_labelf(context, NK_TEXT_ALIGN_RIGHT, "%.5f", camera->aspect_ratio);
if(!camera->ortho)
{
nk_layout_row_dynamic(context, row_height, 1);
float new_fov = nk_propertyf(context, "Fov", 30.f, camera->fov, 90.f, 0.1f, 1.f);
if(new_fov != camera->fov)
{
camera->fov = new_fov;
update = true;
}
nk_layout_row_dynamic(context, row_height, 2);
nk_label(context, "Aspect Ratio", NK_TEXT_ALIGN_LEFT); nk_labelf(context, NK_TEXT_ALIGN_RIGHT, "%.5f", camera->aspect_ratio);
}
nk_layout_row_dynamic(context, row_height, 1); nk_label(context, "Clear Color", NK_TEXT_ALIGN_CENTERED);
nk_layout_row_dynamic(context, row_height, 1);
nk_property_float(context, "Fov", 45.f, &camera->fov, 90.f, 1.f, 5.f);
editor_widget_color_combov4(context, &camera->clear_color, 200, 300);
nk_layout_row_dynamic(context, row_height, 1);
nk_property_float(context, "NearZ", 0.01f, &camera->nearz, camera->farz, 0.1f, 1.f);
float new_near_z = nk_propertyf(context, "NearZ", -FLT_MAX, camera->nearz, camera->farz, 0.1f, 1.f);
if(new_near_z != camera->nearz)
{
camera->nearz = new_near_z;
update = true;
}
nk_layout_row_dynamic(context, row_height, 1);
nk_property_float(context, "FarZ", camera->nearz, &camera->farz, FLT_MAX, 1.f, 5.f);
float new_far_z = nk_propertyf(context, "FarZ", camera->nearz, camera->farz, FLT_MAX, 0.1f, 2.f);
if(new_far_z != camera->farz)
{
camera->farz = new_far_z;
update = true;
}
if(update)
{
if(!camera->ortho) camera_update_view(entity);
camera_update_proj(entity);
}
nk_tree_pop(context);
}

@ -24,6 +24,7 @@
#include "gl_load.h"
#include "gui.h"
#include "editor.h"
#include "sprite.h"
#include "../common/parser.h"
#include "../common/hashmap.h"
#include "../common/variant.h"
@ -216,6 +217,12 @@ void scene_setup(void)
struct Entity* player = entity_find("player");
game_state->player_node = player->id;
/*struct Camera* camera = &player->camera;
camera->ortho = true;
camera->farz = 1.f;
camera->nearz = -1.f;
camera_update_proj(player);*/
/*struct Entity* suz = entity_find("Suzanne");
struct Entity* sound_ent = scene_add_as_child("Sound_Ent", ET_SOUND_SOURCE, suz->id);
struct Sound_Source* sound_source = &sound_ent->sound_source;
@ -363,6 +370,36 @@ void debug(float dt)
transform_rotate(model, &x_axis, 25.f * dt, TS_WORLD);
vec3 amount = {0, 0, -5 * dt};
transform_translate(model, &amount, TS_LOCAL);
struct Sprite_Batch* batch = get_batch();
sprite_batch_begin(batch);
static struct Sprite sprite;
sprite.vertices[0].pos.x = 0.f; sprite.vertices[0].pos.y = 0.f; sprite.vertices[0].pos.z = 0.f;
sprite.vertices[1].pos.x = 1.f; sprite.vertices[1].pos.y = 0.f; sprite.vertices[1].pos.z = 0.f;
sprite.vertices[2].pos.x = 0.f; sprite.vertices[2].pos.y = 1.f; sprite.vertices[2].pos.z = 0.f;
sprite.vertices[3].pos.x = 0.f; sprite.vertices[3].pos.y = 1.f; sprite.vertices[3].pos.z = 0.f;
sprite.vertices[4].pos.x = 1.f; sprite.vertices[4].pos.y = 1.f; sprite.vertices[4].pos.z = 0.f;
sprite.vertices[5].pos.x = 1.f; sprite.vertices[5].pos.y = 0.f; sprite.vertices[5].pos.z = 0.f;
sprite.vertices[0].uv.x = 0.f; sprite.vertices[0].uv.y = 0.f;
sprite.vertices[1].uv.x = 1.f; sprite.vertices[1].uv.y = 0.f;
sprite.vertices[2].uv.x = 0.f; sprite.vertices[2].uv.y = 1.f;
sprite.vertices[3].uv.x = 0.f; sprite.vertices[3].uv.y = 1.f;
sprite.vertices[4].uv.x = 1.f; sprite.vertices[4].uv.y = 1.f;
sprite.vertices[5].uv.x = 1.f; sprite.vertices[5].uv.y = 0.f;
sprite.vertices[0].color.x = 0.f; sprite.vertices[0].color.y = 0.f; sprite.vertices[0].color.z = 0.f; sprite.vertices[0].color.w = 1.f;
sprite.vertices[1].color.x = 1.f; sprite.vertices[1].color.y = 0.f; sprite.vertices[1].color.z = 0.f; sprite.vertices[1].color.w = 1.f;
sprite.vertices[2].color.x = 0.f; sprite.vertices[2].color.y = 1.f; sprite.vertices[2].color.z = 0.f; sprite.vertices[2].color.w = 1.f;
sprite.vertices[3].color.x = 1.f; sprite.vertices[3].color.y = 1.f; sprite.vertices[3].color.z = 0.f; sprite.vertices[3].color.w = 1.f;
sprite.vertices[4].color.x = 1.f; sprite.vertices[4].color.y = 1.f; sprite.vertices[4].color.z = 0.f; sprite.vertices[4].color.w = 1.f;
sprite.vertices[5].color.x = 1.f; sprite.vertices[5].color.y = 1.f; sprite.vertices[5].color.z = 0.f; sprite.vertices[5].color.w = 1.f;
sprite_batch_add_sprite(batch, &sprite);
sprite_batch_end(batch);
}
bool run(void)

@ -8,7 +8,6 @@
#else
#include <SDL2/SDL_opengl.h>
#define SYMMETRY_GL_LIST \
/* ret, name, params */ \

@ -18,6 +18,7 @@
#include "geometry.h"
#include "material.h"
#include "editor.h"
#include "sprite.h"
#include "../common/variant.h"
#include "../common/common.h"
@ -38,6 +39,8 @@ static int num_culled = 0, num_rendered = 0, num_indices = 0;
static int num_culled_slot = -1, num_rendered_slot = -1, num_indices_slot = -1;
static struct Sprite_Batch* sprite_batch = NULL;
void on_framebuffer_size_change(int width, int height);
void renderer_init(void)
@ -117,6 +120,16 @@ void renderer_init(void)
num_culled_slot = editor_debugvar_slot_create("Culled Geom", VT_INT);
num_rendered_slot = editor_debugvar_slot_create("Rendered Geom", VT_INT);
num_indices_slot = editor_debugvar_slot_create("Total Indices", VT_INT);
sprite_batch = malloc(sizeof(*sprite_batch));
if(!sprite_batch)
{
log_error("renderer:init", "Failed to allocated sprite batch");
}
else
{
sprite_batch_create(sprite_batch, "sprite_map.tga", "sprite.vert", "sprite.frag", GL_TRIANGLES);
}
}
void renderer_draw(struct Entity* active_viewer)
@ -391,11 +404,29 @@ void renderer_draw(struct Entity* active_viewer)
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
}
/* Render 2D stuff */
shader_bind(sprite_batch->shader);
{
static mat4 ortho_mat;
mat4_identity(&ortho_mat);
/*int width, height;
struct Game_State* game_state = game_state_get();
platform->window.get_size(game_state->window, &width, &height);
mat4_ortho(&ortho_mat, 0, width, height, 0, -1.f, 1.f);*/
shader_set_uniform_mat4(sprite_batch->shader, "mvp", &ortho_mat);
sprite_batch_render(sprite_batch);
}
shader_unbind();
/* Render UI */
gui_render(NK_ANTI_ALIASING_ON);
}
void renderer_cleanup(void)
{
free(sprite_batch);
gui_cleanup();
geom_remove(quad_geo);
framebuffer_remove(def_fbo);
@ -447,6 +478,11 @@ int renderer_check_glerror(const char* context)
return error;
}
struct Sprite_Batch * get_batch(void)
{
return sprite_batch;
}
void renderer_debug_draw_enabled(bool enabled)
{
struct Hashmap* cvars = platform->config.get();

@ -32,6 +32,7 @@ struct Render_Settings
};
struct Entity;
struct Sprite_Batch;
void renderer_settings_get(struct Render_Settings* settings);
void renderer_settings_set(const struct Render_Settings* settings);
@ -42,4 +43,6 @@ void renderer_clearcolor_set(float r, float g, float b, float a);
void renderer_debug_draw_enabled(bool enabled);
int renderer_check_glerror(const char* context);
struct Sprite_Batch* get_batch(void);
#endif

@ -13,28 +13,24 @@
#include <string.h>
#include <assert.h>
// Constants for locations of attributes inside all shaders
const int POSITION_LOC = 0;
const int NORMAL_LOC = 1;
const int UV_LOC = 2;
const int COLOR_LOC = 3;
static uint* shader_list;
static int* empty_indices;
#define MAX_INCLUDE_LINE_LEN 256
void debug_print_shader(const char* shaderText)
{
size_t len = strlen(shaderText);
int line_count = 1;
printf("%d. ", line_count);
log_raw("%d. ", line_count);
for(uint i = 0; i < len; i++)
{
if(shaderText[i] != '\n')
printf("%c", shaderText[i]);
log_raw("%c", shaderText[i]);
else
printf("\n%d. ", ++line_count);
log_raw("\n%d. ", ++line_count);
}
printf("\n END_DEBUG_PRINT\n\n");
log_raw("\n END_DEBUG_PRINT\n\n");
}
char* run_preprocessor(char* shader_text)
@ -42,40 +38,32 @@ char* run_preprocessor(char* shader_text)
char* include_loc = strstr(shader_text, "//include");
if(include_loc)
{
char* line_end = strchr(include_loc, '\n');
int line_size = line_end - include_loc;
char* inc_line = malloc((sizeof(char) * line_size) + 1);
strncpy(inc_line, include_loc, line_size);
inc_line[line_size] = '\0';
char inc_line[MAX_INCLUDE_LINE_LEN];
memset(inc_line, '\0', MAX_INCLUDE_LINE_LEN);
char fmt_str[64];
snprintf(fmt_str, 64, "//include %%%d[^\r\n]", MAX_INCLUDE_LINE_LEN);
sscanf(shader_text, fmt_str, inc_line);
char* filename = strtok(inc_line, " ");
while(filename)
{
filename = strtok(NULL, " ");
if(filename)
{
char* path = str_new("shaders/");
path = str_concat(path, filename);
char* file = platform->file.read(DIRT_INSTALL, path, "r", NULL);
char* shader_copy = str_new(shader_text);
char* temp = realloc(shader_text, (strlen(shader_text) + strlen(file) + 2));
if(temp)
char* file_contents = platform->file.read(DIRT_INSTALL, path, "rb", NULL);
if(file_contents)
{
shader_text = temp;
strcpy(shader_text, file);
strcat(shader_text, shader_copy);
char* shader_text_new = str_new("%s\n%s", file_contents, shader_text);
free(shader_text);
free(file_contents);
free(path);
shader_text = shader_text_new;
}
else
{
log_warning("Realloc failed in Shader::run_preprocessor");
}
free(path);
free(shader_copy);
free(file);
}
filename = strtok(NULL, " ");
}
free(inc_line);
}
return shader_text;
}
@ -96,8 +84,8 @@ int shader_create(const char* vert_shader_name, const char* frag_shader_name)
GLuint vert_shader = glCreateShader(GL_VERTEX_SHADER);
GLuint frag_shader = glCreateShader(GL_FRAGMENT_SHADER);
char* vert_source = platform->file.read(DIRT_INSTALL, vs_path, "r", NULL);
char* frag_source = platform->file.read(DIRT_INSTALL, fs_path, "r", NULL);
char* vert_source = platform->file.read(DIRT_INSTALL, vs_path, "rb", NULL);
char* frag_source = platform->file.read(DIRT_INSTALL, fs_path, "rb", NULL);
assert(vert_source != NULL);
assert(frag_source != NULL);
@ -164,10 +152,10 @@ int shader_create(const char* vert_shader_name, const char* frag_shader_name)
glAttachShader(program, frag_shader);
// Bind attribute locations
glBindAttribLocation(program, POSITION_LOC, "vPosition");
glBindAttribLocation(program, NORMAL_LOC, "vNormal");
glBindAttribLocation(program, UV_LOC, "vUV");
glBindAttribLocation(program, COLOR_LOC, "vColor");
glBindAttribLocation(program, AL_POSITION, "vPosition");
glBindAttribLocation(program, AL_NORMAL, "vNormal");
glBindAttribLocation(program, AL_UV, "vUV");
glBindAttribLocation(program, AL_COLOR, "vColor");
renderer_check_glerror("shader:create");
glLinkProgram(program);

@ -3,6 +3,15 @@
#include "../common/linmath.h"
// Constants for locations of attributes inside all shaders
enum Attribute_Location
{
AL_POSITION = 0,
AL_NORMAL = 1,
AL_UV = 2,
AL_COLOR = 3
};
enum Uniform_Type
{
UT_FLOAT = 0,

@ -0,0 +1,137 @@
#include "sprite.h"
#include "texture.h"
#include "shader.h"
#include "renderer.h"
#include "../common/log.h"
#include "../common/array.h"
#include "gl_load.h"
#include <assert.h>
static int sprite_count = 0;
void sprite_init(void)
{
}
void sprite_cleanup(void)
{
}
void sprite_batch_create(struct Sprite_Batch* batch, const char* texture_name, const char* vert_shader, const char* frag_shader, int draw_mode)
{
assert(batch);
memset(&batch->sprites[0], '\0', sizeof(struct Sprite) * SPRITE_BATCH_SIZE);
glGenVertexArrays(1, &batch->vao);
glBindVertexArray(batch->vao);
glGenBuffers(1, &batch->vbo);
glBindBuffer(GL_ARRAY_BUFFER, batch->vbo);
glBufferData(GL_ARRAY_BUFFER,
sizeof(struct Vertex) * MAX_SPRITE_VERTICES * SPRITE_BATCH_SIZE,
NULL,
GL_STREAM_DRAW);
renderer_check_glerror("sprite_batch_create:glBufferData");
// Position
glVertexAttribPointer(AL_POSITION, 3, GL_FLOAT, GL_FALSE, sizeof(struct Vertex), 0);
renderer_check_glerror("sprite_batch_create:glVertexAttribPointer");
glEnableVertexAttribArray(AL_POSITION);
renderer_check_glerror("sprite_batch_create:glEnableVertexAttribPointer");
// Uvs
glVertexAttribPointer(AL_UV, 2, GL_FLOAT, GL_FALSE, sizeof(struct Vertex), sizeof(vec3));
renderer_check_glerror("sprite_batch_create:glVertexAttribPointer");
glEnableVertexAttribArray(AL_UV);
renderer_check_glerror("sprite_batch_create:glEnableVertexAttribPointer");
// Color
glVertexAttribPointer(AL_COLOR, 4, GL_FLOAT, GL_FALSE, sizeof(struct Vertex), sizeof(vec3) + sizeof(vec2));
renderer_check_glerror("sprite_batch_create:glVertexAttribPointer");
glEnableVertexAttribArray(AL_COLOR);
renderer_check_glerror("sprite_batch_create:glEnableVertexAttribPointer");
//glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
int texture = texture_create_from_file(texture_name, TU_DIFFUSE);
if(texture < 0)
{
log_error("sprite_batch:create", "Failed to find texture '%s' when creating sprite batch", texture_name);
texture = texture_create_from_file("default.tga", TU_DIFFUSE);
}
batch->texture = texture;
int shader = shader_create(vert_shader, frag_shader);
if(shader < -1)
{
log_error("sprite_batch:create", "Failed to create shader from '%s'/'%s' sprite batch", vert_shader, frag_shader);
shader = shader_create("default.vert", "default.frag");
}
batch->shader = shader;
batch->draw_mode = draw_mode;
}
void sprite_batch_begin(struct Sprite_Batch* batch)
{
assert(batch);
batch->current_sprite_count = 0;
}
void sprite_batch_add_sprite(struct Sprite_Batch* batch, struct Sprite* sprite)
{
assert(batch);
if(batch->current_sprite_count < SPRITE_BATCH_SIZE)
{
batch->current_sprite_count++;
memcpy(&batch->sprites[batch->current_sprite_count - 1], sprite, sizeof(struct Sprite));
}
else
{
log_error("sprite_batch:add_sprite", "Batch full");
}
}
void sprite_batch_add_sprite_new(struct Sprite_Batch* batch, int texture, struct Vertex vertices[MAX_SPRITE_VERTICES])
{
assert(batch);
if(batch->current_sprite_count < SPRITE_BATCH_SIZE)
{
batch->current_sprite_count++;
memcpy(&batch->sprites[batch->current_sprite_count - 1], &vertices[0], sizeof(struct Sprite));
}
else
{
log_error("sprite_batch:add_sprite", "Batch full");
}
}
void sprite_batch_end(struct Sprite_Batch* batch)
{
assert(batch);
glBindBuffer(GL_ARRAY_BUFFER, batch->vbo);
glBufferSubData(GL_ARRAY_BUFFER,
0,
sizeof(struct Vertex) * MAX_SPRITE_VERTICES * batch->current_sprite_count,
&batch->sprites[0]);
renderer_check_glerror("sprite_batch_end:glBufferSubData");
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
void sprite_batch_render(struct Sprite_Batch* batch)
{
assert(batch);
texture_bind(batch->texture);
glBindVertexArray(batch->vao);
glDrawArrays(batch->draw_mode, 0, MAX_SPRITE_VERTICES * batch->current_sprite_count);
renderer_check_glerror("sprite_batch_render:glDrawArrays");
glBindVertexArray(0);
texture_unbind(batch->texture);
}

@ -0,0 +1,43 @@
#ifndef SPRITE_H
#define SPRITE_H
#include "../common/linmath.h"
#include "../common/num_types.h"
#define SPRITE_BATCH_SIZE 1024
#define MAX_SPRITE_VERTICES 6
struct Vertex
{
vec3 pos;
vec2 uv;
vec4 color;
};
struct Sprite
{
struct Vertex vertices[MAX_SPRITE_VERTICES];
};
struct Sprite_Batch
{
int texture;
int shader;
struct Sprite sprites[SPRITE_BATCH_SIZE];
int draw_mode;
uint vao;
uint vbo;
int current_sprite_count;
};
void sprite_init(void);
void sprite_cleanup(void);
void sprite_batch_create(struct Sprite_Batch* batch, const char* texture_name, const char* vert_shader, const char* frag_shader, int draw_mode);
void sprite_batch_begin(struct Sprite_Batch* batch);
void sprite_batch_add_sprite(struct Sprite_Batch* batch, struct Sprite* sprite);
void sprite_batch_add_sprite_new(struct Sprite_Batch* batch, int texture, struct Vertex vertices[MAX_SPRITE_VERTICES]);
void sprite_batch_end(struct Sprite_Batch* batch);
void sprite_batch_render(struct Sprite_Batch* batch);
#endif
Loading…
Cancel
Save