shithub: wipeout

ref: b2e3ae80dd3e5f152cdf35bf58473b8d6cef0ef4
dir: /src/render_gl.c/

View raw version

// macOS
#if defined(__APPLE__) && defined(__MACH__)
	#include <OpenGL/gl.h>
	#include <OpenGL/glext.h>
	
	#define glGenVertexArrays glGenVertexArraysAPPLE
	#define glBindVertexArray glBindVertexArrayAPPLE
	#define glDeleteVertexArrays glDeleteVertexArraysAPPLE

// Linux
#elif defined(__unix__)
	#include <GL/glew.h>
	
// WINDOWS
#else
	#include <windows.h>

	#define GL3_PROTOTYPES 1
	#include <glew.h>
	#pragma comment(lib, "glew32.lib")

	#include <gl/GL.h>
	#pragma comment(lib, "opengl32.lib")
#endif



#include "libs/stb_image_write.h"

#include "system.h"
#include "render.h"
#include "mem.h"
#include "utils.h"


#define ATLAS_SIZE 64
#define ATLAS_GRID 32
#define ATLAS_BORDER 16

#define RENDER_TRIS_BUFFER_CAPACITY 2048
#define TEXTURES_MAX 1024


#if defined(__EMSCRIPTEN__) || defined(USE_GLES2)
	// WebGL (GLES) needs the `precision` to be set, wheras OpenGL 2 
	// doesn't like that...
	#define SHADER_SOURCE(...) "precision highp float;" #__VA_ARGS__

	// WebGL1 only allows for a 16 bit depth buffer attachment, so 
	// we sacrifice a bit of the near plane to get more precision
	// further out
	#define NEAR_PLANE 128.0
	#define FAR_PLANE (RENDER_FADEOUT_FAR)
	#define RENDER_DEPTH_BUFFER_INTERNAL_FORMAT GL_DEPTH_COMPONENT16
#else
	#define SHADER_SOURCE(...) #__VA_ARGS__

	#define NEAR_PLANE 16.0
	#define FAR_PLANE (RENDER_FADEOUT_FAR)
	#define RENDER_DEPTH_BUFFER_INTERNAL_FORMAT GL_DEPTH_COMPONENT24
#endif
	

typedef struct {
	vec2i_t offset;
	vec2i_t size;
} render_texture_t;

uint16_t RENDER_NO_TEXTURE;

#define use_program(SHADER) \
	glUseProgram((SHADER)->program); \
	glBindVertexArray((SHADER)->vao);

#define bind_va_f(index, container, member, start) \
	glVertexAttribPointer( \
		index, member_size(container, member)/sizeof(float), GL_FLOAT, false, \
		sizeof(container), \
		(GLvoid*)(offsetof(container, member) + start) \
	)

#define bind_va_color(index, container, member, start) \
	glVertexAttribPointer( \
		index, 4,  GL_UNSIGNED_BYTE, true, \
		sizeof(container), \
		(GLvoid*)(offsetof(container, member) + start) \
	)


static GLuint compile_shader(GLenum type, const char *source) {
	GLuint shader = glCreateShader(type);
	glShaderSource(shader, 1, &source, NULL);
	glCompileShader(shader);
	
	GLint success;
	glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
	if (!success) {
		int log_written;
		char log[256];
		glGetShaderInfoLog(shader, 256, &log_written, log);
		die("Error compiling shader: %s\n", log);
	}
	return shader;
}

static GLuint create_program(const char *vs_source, const char *fs_source) {
	GLuint vs = compile_shader(GL_VERTEX_SHADER, vs_source);
	GLuint fs = compile_shader(GL_FRAGMENT_SHADER, fs_source);

	GLuint program = glCreateProgram();
	glAttachShader(program, vs);
	glAttachShader(program, fs);
	glLinkProgram(program);
	glUseProgram(program);
	return program;
}



// -----------------------------------------------------------------------------
// Main game shaders

static const char * const SHADER_GAME_VS = SHADER_SOURCE(
	attribute vec3 pos;
	attribute vec2 uv;
	attribute vec4 color;

	varying vec4 v_color;
	varying vec2 v_uv;
	uniform mat4 view;
	uniform mat4 model;
	uniform mat4 projection;
	uniform vec2 screen;
	uniform vec3 camera_pos;
	uniform vec2 fade;
	uniform float time;
	
	void main() {
		gl_Position = projection * view * model * vec4(pos, 1.0);
		gl_Position.xy += screen.xy * gl_Position.w;
		v_color = color;
		v_color.a *= smoothstep(
			fade.y, fade.x, // fadeout far, near
			length(vec4(camera_pos, 1.0) - model * vec4(pos, 1.0))
		);
		v_uv = uv / 2048.0; // ATLAS_GRID * ATLAS_SIZE
	}
);

static const char * const SHADER_GAME_FS = SHADER_SOURCE(
	varying vec4 v_color;
	varying vec2 v_uv;
	uniform sampler2D texture;

	void main() {
		vec4 tex_color = texture2D(texture, v_uv);
		vec4 color = tex_color * v_color;
		if (color.a == 0.0) {
			discard;
		}
		color.rgb = color.rgb * 2.0;
		gl_FragColor = color;
	}
);

typedef struct {
	GLuint program;
	GLuint vao;
	struct {
		GLuint view;
		GLuint model;
		GLuint projection;
		GLuint screen;
		GLuint camera_pos;
		GLuint fade;
		GLuint time;
	} uniform;
	struct {
		GLuint pos;
		GLuint uv;
		GLuint color;
	} attribute;
} prg_game_t;

prg_game_t *shader_game_init() {
	prg_game_t *s = mem_bump(sizeof(prg_game_t));
	
	s->program = create_program(SHADER_GAME_VS, SHADER_GAME_FS);

	s->uniform.view = glGetUniformLocation(s->program, "view");
	s->uniform.model = glGetUniformLocation(s->program, "model");
	s->uniform.projection = glGetUniformLocation(s->program, "projection");
	s->uniform.screen = glGetUniformLocation(s->program, "screen");
	s->uniform.camera_pos = glGetUniformLocation(s->program, "camera_pos");
	s->uniform.fade = glGetUniformLocation(s->program, "fade");

	s->attribute.pos = glGetAttribLocation(s->program, "pos");
	s->attribute.uv = glGetAttribLocation(s->program, "uv");
	s->attribute.color = glGetAttribLocation(s->program, "color");

	glGenVertexArrays(1, &s->vao);
	glBindVertexArray(s->vao);

	glEnableVertexAttribArray(s->attribute.pos);
	glEnableVertexAttribArray(s->attribute.uv);
	glEnableVertexAttribArray(s->attribute.color);

	bind_va_f(s->attribute.pos, vertex_t, pos, 0);
	bind_va_f(s->attribute.uv, vertex_t, uv, 0);
	bind_va_color(s->attribute.color, vertex_t, color, 0);

	return s;
}


// -----------------------------------------------------------------------------
// POST Effect shaders

static const char * const SHADER_POST_VS = SHADER_SOURCE(
	attribute vec3 pos;
	attribute vec2 uv;

	varying vec2 v_uv;

	uniform mat4 projection;
	uniform vec2 screen_size;
	uniform float time;
	
	void main() {
		gl_Position = projection * vec4(pos, 1.0);
		v_uv = uv;
	}
);

static const char * const SHADER_POST_FS_DEFAULT = SHADER_SOURCE(
	varying vec2 v_uv;

	uniform sampler2D texture;
	uniform vec2 screen_size;

	void main() {
		gl_FragColor = texture2D(texture, v_uv);
	}
);

// CRT effect based on https://www.shadertoy.com/view/Ms23DR 
// by https://github.com/mattiasgustavsson/
static const char * const SHADER_POST_FS_CRT = SHADER_SOURCE(
	varying vec2 v_uv;

	uniform float time;
	uniform sampler2D texture;
	uniform vec2 screen_size;

	vec2 curve(vec2 uv) {
		uv = (uv - 0.5) * 2.0;
		uv *= 1.1;	
		uv.x *= 1.0 + pow((abs(uv.y) / 5.0), 2.0);
		uv.y *= 1.0 + pow((abs(uv.x) / 4.0), 2.0);
		uv  = (uv / 2.0) + 0.5;
		uv =  uv *0.92 + 0.04;
		return uv;
	}

	void main(){
		vec2 uv = curve(gl_FragCoord.xy / screen_size);
		vec3 color;
		float x =  sin(0.3*time+uv.y*21.0)*sin(0.7*time+uv.y*29.0)*sin(0.3+0.33*time+uv.y*31.0)*0.0017;

		color.r = texture2D(texture, vec2(x+uv.x+0.001,uv.y+0.001)).x+0.05;
		color.g = texture2D(texture, vec2(x+uv.x+0.000,uv.y-0.002)).y+0.05;
		color.b = texture2D(texture, vec2(x+uv.x-0.002,uv.y+0.000)).z+0.05;
		color.r += 0.08*texture2D(texture, 0.75*vec2(x+0.025, -0.027)+vec2(uv.x+0.001,uv.y+0.001)).x;
		color.g += 0.05*texture2D(texture, 0.75*vec2(x+-0.022, -0.02)+vec2(uv.x+0.000,uv.y-0.002)).y;
		color.b += 0.08*texture2D(texture, 0.75*vec2(x+-0.02, -0.018)+vec2(uv.x-0.002,uv.y+0.000)).z;

		color = clamp(color*0.6+0.4*color*color*1.0,0.0,1.0);

		float vignette = (0.0 + 1.0*16.0*uv.x*uv.y*(1.0-uv.x)*(1.0-uv.y));
		color *= vec3(pow(vignette, 0.25));

		color *= vec3(0.95,1.05,0.95);
		color *= 2.8;

		float scanlines = clamp( 0.35+0.35*sin(3.5*time+uv.y*screen_size.y*1.5), 0.0, 1.0);
		
		float s = pow(scanlines,1.7);
		color = color * vec3(0.4+0.7*s);

		color *= 1.0+0.01*sin(110.0*time);
		if (uv.x < 0.0 || uv.x > 1.0)
			color *= 0.0;
		if (uv.y < 0.0 || uv.y > 1.0)
			color *= 0.0;
		
		color*=1.0-0.65*vec3(clamp((mod(gl_FragCoord.x, 2.0)-1.0)*2.0,0.0,1.0));
		gl_FragColor = vec4(color,1.0);
	}
);

typedef struct {
	GLuint program;
	GLuint vao;
	struct {
		GLuint projection;
		GLuint screen_size;
		GLuint time;
	} uniform;
	struct {
		GLuint pos;
		GLuint uv;
	} attribute;
} prg_post_t;

void shader_post_general_init(prg_post_t *s) {
	s->uniform.projection = glGetUniformLocation(s->program, "projection");
	s->uniform.screen_size = glGetUniformLocation(s->program, "screen_size");
	s->uniform.time = glGetUniformLocation(s->program, "time");

	s->attribute.pos = glGetAttribLocation(s->program, "pos");
	s->attribute.uv = glGetAttribLocation(s->program, "uv");

	glGenVertexArrays(1, &s->vao);
	glBindVertexArray(s->vao);

	glEnableVertexAttribArray(s->attribute.pos);
	glEnableVertexAttribArray(s->attribute.uv);

	bind_va_f(s->attribute.pos, vertex_t, pos, 0);
	bind_va_f(s->attribute.uv, vertex_t, uv, 0);
}

prg_post_t *shader_post_default_init() {
	prg_post_t *s = mem_bump(sizeof(prg_post_t));
	s->program = create_program(SHADER_POST_VS, SHADER_POST_FS_DEFAULT);	
	shader_post_general_init(s);
	return s;
}

prg_post_t *shader_post_crt_init() {
	prg_post_t *s = mem_bump(sizeof(prg_post_t));
	s->program = create_program(SHADER_POST_VS, SHADER_POST_FS_CRT);	
	shader_post_general_init(s);
	return s;
}



// -----------------------------------------------------------------------------

static GLuint vbo;

static tris_t tris_buffer[RENDER_TRIS_BUFFER_CAPACITY];
static uint32_t tris_len = 0;

static vec2i_t screen_size;
static vec2i_t backbuffer_size;

static uint32_t atlas_map[ATLAS_SIZE] = {0};
static GLuint atlas_texture = 0;
static render_blend_mode_t blend_mode = RENDER_BLEND_NORMAL;

static mat4_t projection_mat_2d = mat4_identity();
static mat4_t projection_mat_bb = mat4_identity();
static mat4_t projection_mat_3d = mat4_identity();
static mat4_t sprite_mat = mat4_identity();
static mat4_t view_mat = mat4_identity();


static render_texture_t textures[TEXTURES_MAX];
static uint32_t textures_len = 0;
static bool texture_mipmap_is_dirty = false;

static render_resolution_t render_res;
static GLuint backbuffer = 0;
static GLuint backbuffer_texture = 0;
static GLuint backbuffer_depth_buffer = 0;

prg_game_t *prg_game;
prg_post_t *prg_post;
prg_post_t *prg_post_effects[NUM_RENDER_POST_EFFCTS] = {};


static void render_flush();


// static void gl_message_callback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, const void *userParam) {
// 	puts(message);
// }

void render_init(vec2i_t screen_size) {	
	#if defined(__APPLE__) && defined(__MACH__)
		// OSX
		// (nothing to do here)
	#else
		// Windows, Linux
		glewExperimental = GL_TRUE;
		glewInit();
	#endif

	// glEnable(GL_DEBUG_OUTPUT);
	// glDebugMessageCallback(gl_message_callback, NULL);
	// glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, NULL, GL_TRUE);


	// Atlas Texture

	glGenTextures(1, &atlas_texture);
	glBindTexture(GL_TEXTURE_2D, atlas_texture);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, RENDER_USE_MIPMAPS ? GL_LINEAR_MIPMAP_LINEAR : GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

	float anisotropy = 0;
	glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &anisotropy);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, anisotropy);

	uint32_t tw = ATLAS_SIZE * ATLAS_GRID;
	uint32_t th = ATLAS_SIZE * ATLAS_GRID;
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, tw, th, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
	printf("atlas texture %5d\n", atlas_texture);
	

	// Tris buffer

	glGenBuffers(1, &vbo);
	glBindBuffer(GL_ARRAY_BUFFER, vbo);


	// Post Shaders

	prg_post_effects[RENDER_POST_NONE] = shader_post_default_init();
	prg_post_effects[RENDER_POST_CRT] = shader_post_crt_init();
	render_set_post_effect(RENDER_POST_NONE);

	// Game shader

	prg_game = shader_game_init();
	use_program(prg_game);

	render_set_view(vec3(0, 0, 0), vec3(0, 0, 0));
	render_set_model_mat(&mat4_identity());

	glEnable(GL_CULL_FACE);
	glEnable(GL_BLEND);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);


	// Create white texture

	rgba_t white_pixels[4] = {
		rgba(128,128,128,255), rgba(128,128,128,255),
		rgba(128,128,128,255), rgba(128,128,128,255)
	};
	RENDER_NO_TEXTURE = render_texture_create(2, 2, white_pixels);


	// Backbuffer

	render_res = RENDER_RES_NATIVE;
	render_set_screen_size(screen_size);
}

void render_cleanup() {
	// TODO
}


static mat4_t render_setup_2d_projection_mat(vec2i_t size) {
	float near = -1;
	float far = 1;
	float left = 0;
	float right = size.x;
	float bottom = size.y;
	float top = 0;
	float lr = 1 / (left - right);
	float bt = 1 / (bottom - top);
	float nf = 1 / (near - far);
	return mat4(
		-2 * lr,  0,  0,  0,
		0,  -2 * bt,  0,  0,
		0,        0,  2 * nf,    0, 
		(left + right) * lr, (top + bottom) * bt, (far + near) * nf, 1
	);
}

static mat4_t render_setup_3d_projection_mat(vec2i_t size) {
	// wipeout has a horizontal fov of 90deg, but we want the fov to be fixed 
	// for the vertical axis, so that widescreen displays just have a wider 
	// view. For the original 4/3 aspect ratio this equates to a vertical fov
	// of 73.75deg.
	float aspect = (float)size.x / (float)size.y;
	float fov = (73.75 / 180.0) * 3.14159265358;
	float f = 1.0 / tan(fov / 2);
	float nf = 1.0 / (NEAR_PLANE - FAR_PLANE);
	return mat4(
		f / aspect, 0, 0, 0,
		0, f, 0, 0, 
		0, 0, (FAR_PLANE + NEAR_PLANE) * nf, -1, 
		0, 0, 2 * FAR_PLANE * NEAR_PLANE * nf, 0
	);
}

void render_set_screen_size(vec2i_t size) {
	screen_size = size;
	projection_mat_bb = render_setup_2d_projection_mat(screen_size);

	render_set_resolution(render_res);
}


void render_set_resolution(render_resolution_t res) {
	render_res = res;

	if (res == RENDER_RES_NATIVE) {
		backbuffer_size = screen_size;
	}
	else {
		float aspect = (float)screen_size.x / (float)screen_size.y;
		if (res == RENDER_RES_240P) {
			backbuffer_size = vec2i(240.0 * aspect, 240);
		}
		else if (res == RENDER_RES_480P) {
			backbuffer_size = vec2i(480.0 * aspect, 480);	
		}
		else {
			die("Invalid resolution: %d", res);
		}
	}

	if (!backbuffer) {
		glGenTextures(1, &backbuffer_texture);	
		glGenFramebuffers(1, &backbuffer);
		glGenRenderbuffers(1, &backbuffer_depth_buffer);
	}
	
	glBindTexture(GL_TEXTURE_2D, backbuffer_texture);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, backbuffer_size.x, backbuffer_size.y, 0, GL_RGB, GL_UNSIGNED_BYTE, 0);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	
	glBindFramebuffer(GL_FRAMEBUFFER, backbuffer);
	glBindRenderbuffer(GL_RENDERBUFFER, backbuffer_depth_buffer);	
	glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, backbuffer_depth_buffer);
	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, backbuffer_texture, 0);
	
	glBindRenderbuffer(GL_RENDERBUFFER, backbuffer_depth_buffer);
	glRenderbufferStorage(GL_RENDERBUFFER, RENDER_DEPTH_BUFFER_INTERNAL_FORMAT, backbuffer_size.x, backbuffer_size.y);

	projection_mat_2d = render_setup_2d_projection_mat(backbuffer_size);
	projection_mat_3d = render_setup_3d_projection_mat(backbuffer_size);


	// Use nearest texture min filter for 240p and 480p
	glBindTexture(GL_TEXTURE_2D, atlas_texture);
	if (res == RENDER_RES_NATIVE) {
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, RENDER_USE_MIPMAPS ? GL_LINEAR_MIPMAP_LINEAR : GL_LINEAR);
	}
	else {
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	}
	glViewport(0, 0, backbuffer_size.x, backbuffer_size.y);
}

void render_set_post_effect(render_post_effect_t post) {
	error_if(post < 0 || post > NUM_RENDER_POST_EFFCTS, "Invalid post effect %d", post);
	prg_post = prg_post_effects[post];
}

vec2i_t render_size() {
	return backbuffer_size;
}

void render_frame_prepare() {
	use_program(prg_game);
	glBindFramebuffer(GL_FRAMEBUFFER, backbuffer);
	glViewport(0, 0, backbuffer_size.x, backbuffer_size.y);

	glBindTexture(GL_TEXTURE_2D, atlas_texture);
	glUniform2f(prg_game->uniform.screen, 0, 0);
	glEnable(GL_DEPTH_TEST);
	glDepthMask(true);
	glDisable(GL_POLYGON_OFFSET_FILL);
	glClearColor(0, 0, 0, 1);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glEnable(GL_DEPTH_TEST); 
}

void render_frame_end() {
	render_flush();

	use_program(prg_post);

	glBindFramebuffer(GL_FRAMEBUFFER, 0);
	glViewport(0, 0, screen_size.x, screen_size.y);
	glBindTexture(GL_TEXTURE_2D, backbuffer_texture);
	glUniformMatrix4fv(prg_post->uniform.projection, 1, false, projection_mat_bb.m);
	glUniform1f(prg_post->uniform.time, system_cycle_time());
	glUniform2f(prg_post->uniform.screen_size, screen_size.x, screen_size.y);

	glClearColor(0, 0, 0, 1);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	rgba_t white = rgba(128,128,128,255);
	tris_buffer[tris_len++] = (tris_t){
		.vertices = {
			{.pos = {0, screen_size.y, 0}, .uv = {0, 0}, .color = white},
			{.pos = {screen_size.x, 0, 0}, .uv = {1, 1}, .color = white},
			{.pos = {0, 0, 0}, .uv = {0, 1}, .color = white},
		}
	};
	tris_buffer[tris_len++] = (tris_t){
		.vertices = {
			{.pos = {screen_size.x, screen_size.y, 0}, .uv = {1, 0}, .color = white},
			{.pos = {screen_size.x, 0, 0}, .uv = {1, 1}, .color = white},
			{.pos = {0, screen_size.y, 0}, .uv = {0, 0}, .color = white},
		}
	};

	render_flush();
}

void render_flush() {
	if (tris_len == 0) {
		return;
	}

	if (texture_mipmap_is_dirty) {
		glGenerateMipmap(GL_TEXTURE_2D);
		texture_mipmap_is_dirty = false;
	}

	glBindBuffer(GL_ARRAY_BUFFER, vbo);
	glBufferData(GL_ARRAY_BUFFER, sizeof(tris_t) * tris_len, tris_buffer, GL_DYNAMIC_DRAW);
	glDrawArrays(GL_TRIANGLES, 0, tris_len * 3);
	tris_len = 0;
}


void render_set_view(vec3_t pos, vec3_t angles) {
	render_flush();
	render_set_depth_write(true);
	render_set_depth_test(true);

	view_mat = mat4_identity();
	mat4_set_translation(&view_mat, vec3(0, 0, 0));
	mat4_set_roll_pitch_yaw(&view_mat, vec3(angles.x, -angles.y + M_PI, angles.z + M_PI));
	mat4_translate(&view_mat, vec3_inv(pos));
	mat4_set_yaw_pitch_roll(&sprite_mat, vec3(-angles.x, angles.y - M_PI, 0));

	render_set_model_mat(&mat4_identity());

	render_flush();
	glUniformMatrix4fv(prg_game->uniform.view, 1, false, view_mat.m);
	glUniformMatrix4fv(prg_game->uniform.projection, 1, false, projection_mat_3d.m);
	glUniform3f(prg_game->uniform.camera_pos, pos.x, pos.y, pos.z);
	glUniform2f(prg_game->uniform.fade, RENDER_FADEOUT_NEAR, RENDER_FADEOUT_FAR);
}

void render_set_view_2d() {
	render_flush();
	render_set_depth_test(false);
	render_set_depth_write(false);

	render_set_model_mat(&mat4_identity());
	glUniform3f(prg_game->uniform.camera_pos, 0, 0, 0);
	glUniformMatrix4fv(prg_game->uniform.view, 1, false, mat4_identity().m);
	glUniformMatrix4fv(prg_game->uniform.projection, 1, false, projection_mat_2d.m);
}

void render_set_model_mat(mat4_t *m) {
	render_flush();
	glUniformMatrix4fv(prg_game->uniform.model, 1, false, m->m);
}

void render_set_depth_write(bool enabled) {
	render_flush();
	glDepthMask(enabled);
}

void render_set_depth_test(bool enabled) {
	render_flush();
	if (enabled) {
		glEnable(GL_DEPTH_TEST);
	}
	else {
		glDisable(GL_DEPTH_TEST); 
	}
}

void render_set_depth_offset(float offset) {
	render_flush();
	if (offset == 0) {
		glDisable(GL_POLYGON_OFFSET_FILL);
		return;	
	}

	glEnable(GL_POLYGON_OFFSET_FILL);
	glPolygonOffset(offset, 1.0);
}

void render_set_screen_position(vec2_t pos) {
	render_flush();
	glUniform2f(prg_game->uniform.screen, pos.x, -pos.y);
}

void render_set_blend_mode(render_blend_mode_t new_mode) {
	if (new_mode == blend_mode) {
		return;
	}
	render_flush();

	blend_mode = new_mode;
	if (blend_mode == RENDER_BLEND_NORMAL) {
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
	}
	else if (blend_mode == RENDER_BLEND_LIGHTER) {
		glBlendFunc(GL_SRC_ALPHA, GL_ONE);
	}
}

void render_set_cull_backface(bool enabled) {
	render_flush();
	if (enabled) {
		glEnable(GL_CULL_FACE);
	}
	else {
		glDisable(GL_CULL_FACE);
	}
}




vec3_t render_transform(vec3_t pos) {
	return vec3_transform(vec3_transform(pos, &view_mat), &projection_mat_3d);
}

void render_push_tris(tris_t tris, uint16_t texture_index) {
	error_if(texture_index >= textures_len, "Invalid texture %d", texture_index);
	
	if (tris_len >= RENDER_TRIS_BUFFER_CAPACITY) {
		render_flush();
	}

	render_texture_t *t = &textures[texture_index];

	for (int i = 0; i < 3; i++) {
		tris.vertices[i].uv.x += t->offset.x;
		tris.vertices[i].uv.y += t->offset.y;
	}
	tris_buffer[tris_len++] = tris;
}

void render_push_sprite(vec3_t pos, vec2i_t size, rgba_t color, uint16_t texture_index) {
	error_if(texture_index >= textures_len, "Invalid texture %d", texture_index);

	vec3_t p1 = vec3_add(pos, vec3_transform(vec3(-size.x * 0.5, -size.y * 0.5, 0), &sprite_mat));
	vec3_t p2 = vec3_add(pos, vec3_transform(vec3( size.x * 0.5, -size.y * 0.5, 0), &sprite_mat));
	vec3_t p3 = vec3_add(pos, vec3_transform(vec3(-size.x * 0.5,  size.y * 0.5, 0), &sprite_mat));
	vec3_t p4 = vec3_add(pos, vec3_transform(vec3( size.x * 0.5,  size.y * 0.5, 0), &sprite_mat));

	render_texture_t *t = &textures[texture_index];
	render_push_tris((tris_t){
		.vertices = {
			{
				.pos = p1,
				.uv = {0, 0},
				.color = color
			},
			{
				.pos = p2,
				.uv = {0 + t->size.x ,0},
				.color = color
			},
			{
				.pos = p3,
				.uv = {0, 0 + t->size.y},
				.color = color
			},
		}
	}, texture_index);
	render_push_tris((tris_t){
		.vertices = {
			{
				.pos = p3,
				.uv = {0, 0 + t->size.y},
				.color = color
			},
			{
				.pos = p2,
				.uv = {0 + t->size.x, 0},
				.color = color
			},
			{
				.pos = p4,
				.uv = {0 + t->size.x, 0 + t->size.y},
				.color = color
			},
		}
	}, texture_index);
}

void render_push_2d(vec2i_t pos, vec2i_t size, rgba_t color, uint16_t texture_index) {
	render_push_2d_tile(pos, vec2i(0, 0), render_texture_size(texture_index), size, color, texture_index);
}

void render_push_2d_tile(vec2i_t pos, vec2i_t uv_offset, vec2i_t uv_size, vec2i_t size, rgba_t color, uint16_t texture_index) {
	error_if(texture_index >= textures_len, "Invalid texture %d", texture_index);
	render_push_tris((tris_t){
		.vertices = {
			{
				.pos = {pos.x, pos.y + size.y, 0},
				.uv = {uv_offset.x , uv_offset.y + uv_size.y},
				.color = color
			},
			{
				.pos = {pos.x + size.x, pos.y, 0},
				.uv = {uv_offset.x +  uv_size.x, uv_offset.y},
				.color = color
			},
			{
				.pos = {pos.x, pos.y, 0},
				.uv = {uv_offset.x , uv_offset.y},
				.color = color
			},
		}
	}, texture_index);

	render_push_tris((tris_t){
		.vertices = {
			{
				.pos = {pos.x + size.x, pos.y + size.y, 0},
				.uv = {uv_offset.x + uv_size.x, uv_offset.y + uv_size.y},
				.color = color
			},
			{
				.pos = {pos.x + size.x, pos.y, 0},
				.uv = {uv_offset.x + uv_size.x, uv_offset.y},
				.color = color
			},
			{
				.pos = {pos.x, pos.y + size.y, 0},
				.uv = {uv_offset.x , uv_offset.y + uv_size.y},
				.color = color
			},
		}
	}, texture_index);
}


uint16_t render_texture_create(uint32_t tw, uint32_t th, rgba_t *pixels) {
	error_if(textures_len >= TEXTURES_MAX, "TEXTURES_MAX reached");

	uint32_t bw = tw + ATLAS_BORDER * 2;
	uint32_t bh = th + ATLAS_BORDER * 2;

	// Find a position in the atlas for this texture (with added border)
	uint32_t grid_width = (bw + ATLAS_GRID - 1) / ATLAS_GRID;
	uint32_t grid_height = (bh + ATLAS_GRID - 1) / ATLAS_GRID;
	uint32_t grid_x = 0;
	uint32_t grid_y = ATLAS_SIZE - grid_height + 1;

	for (uint32_t cx = 0; cx < ATLAS_SIZE - grid_width; cx++) {
		if (atlas_map[cx] >= grid_y) {
			continue;
		}

		uint32_t cy = atlas_map[cx];
		bool is_best = true;

		for (uint32_t bx = cx; bx < cx + grid_width; bx++) {
			if (atlas_map[bx] >= grid_y) {
				is_best = false;
				cx = bx;
				break;
			}
			if (atlas_map[bx] > cy) {
				cy = atlas_map[bx];
			}
		}
		if (is_best) {
			grid_y = cy;
			grid_x = cx;
		}
	}

	error_if(grid_y + grid_height > ATLAS_SIZE, "Render atlas ran out of space");

	for (uint32_t cx = grid_x; cx < grid_x + grid_width; cx++) {
		atlas_map[cx] = grid_y + grid_height;
	}

	// Add the border pixels for this texture
	rgba_t *pb = mem_temp_alloc(sizeof(rgba_t) * bw * bh);

	if (tw && th) {
		// Top border
		for (int32_t y = 0; y < ATLAS_BORDER; y++) {
			memcpy(pb + bw * y + ATLAS_BORDER, pixels, tw * sizeof(rgba_t));
		}

		// Bottom border
		for (int32_t y = 0; y < ATLAS_BORDER; y++) {
			memcpy(pb + bw * (bh - ATLAS_BORDER + y) + ATLAS_BORDER, pixels + tw * (th-1), tw * sizeof(rgba_t));
		}
		
		// Left border
		for (int32_t y = 0; y < bh; y++) {
			for (int32_t x = 0; x < ATLAS_BORDER; x++) {
				pb[y * bw + x] = pixels[clamp(y-ATLAS_BORDER, 0, th-1) * tw];
			}
		}

		// Right border
		for (int32_t y = 0; y < bh; y++) {
			for (int32_t x = 0; x < ATLAS_BORDER; x++) {
				pb[y * bw + x + bw - ATLAS_BORDER] = pixels[tw - 1 + clamp(y-ATLAS_BORDER, 0, th-1) * tw];
			}
		}

		// Texture
		for (int32_t y = 0; y < th; y++) {
			memcpy(pb + bw * (y + ATLAS_BORDER) + ATLAS_BORDER, pixels + tw * y, tw * sizeof(rgba_t));
		}
	}

	uint32_t x = grid_x * ATLAS_GRID;
	uint32_t y = grid_y * ATLAS_GRID;
	glBindTexture(GL_TEXTURE_2D, atlas_texture);
	glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, bw, bh, GL_RGBA, GL_UNSIGNED_BYTE, pb);
	mem_temp_free(pb);


	texture_mipmap_is_dirty = RENDER_USE_MIPMAPS;
	uint16_t texture_index = textures_len;
	textures_len++;
	textures[texture_index] = (render_texture_t){ {x + ATLAS_BORDER, y + ATLAS_BORDER}, {tw, th} };

	printf("inserted atlas texture (%3dx%3d) at (%3d,%3d)\n", tw, th, grid_x, grid_y);
	return texture_index;
}

vec2i_t render_texture_size(uint16_t texture_index) {
	error_if(texture_index >= textures_len, "Invalid texture %d", texture_index);
	return textures[texture_index].size;
}

void render_texture_replace_pixels(int16_t texture_index, rgba_t *pixels) {
	error_if(texture_index >= textures_len, "Invalid texture %d", texture_index);

	render_texture_t *t = &textures[texture_index];
	glBindTexture(GL_TEXTURE_2D, atlas_texture);
	glTexSubImage2D(GL_TEXTURE_2D, 0, t->offset.x, t->offset.y, t->size.x, t->size.y, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
}

uint16_t render_textures_len() {
	return textures_len;
}

void render_textures_reset(uint16_t len) {
	error_if(len > textures_len, "Invalid texture reset len %d >= %d", len, textures_len);
	render_flush();

	textures_len = len;
	clear(atlas_map);

	// Clear completely and recreate the default white texture
	if (len == 0) {
		rgba_t white_pixels[4] = {
			rgba(128,128,128,255), rgba(128,128,128,255),
			rgba(128,128,128,255), rgba(128,128,128,255)
		};
		RENDER_NO_TEXTURE = render_texture_create(2, 2, white_pixels);
		return;
	}

	// Replay all texture grid insertions up to the reset len
	for (int i = 0; i < textures_len; i++) {
		uint32_t grid_x = (textures[i].offset.x - ATLAS_BORDER) / ATLAS_GRID;
		uint32_t grid_y = (textures[i].offset.y - ATLAS_BORDER) / ATLAS_GRID;
		uint32_t grid_width = (textures[i].size.x + ATLAS_BORDER * 2 + ATLAS_GRID - 1) / ATLAS_GRID;
		uint32_t grid_height = (textures[i].size.y + ATLAS_BORDER * 2 + ATLAS_GRID - 1) / ATLAS_GRID;
		for (uint32_t cx = grid_x; cx < grid_x + grid_width; cx++) {
			atlas_map[cx] = grid_y + grid_height;
		}
	}
}

void render_textures_dump(const char *path) {
	int width = ATLAS_SIZE * ATLAS_GRID;
	int height = ATLAS_SIZE * ATLAS_GRID;
	rgba_t *pixels = malloc(sizeof(rgba_t) * width * height);
	glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
	stbi_write_png(path, width, height, 4, pixels, 0);
	free(pixels);
}