#include <psemek/gfx/renderer/deferred.hpp>

#ifndef PSEMEK_GLES

#include <psemek/gfx/program.hpp>
#include <psemek/gfx/framebuffer.hpp>
#include <psemek/gfx/texture.hpp>
#include <psemek/gfx/mesh.hpp>
#include <psemek/gfx/error.hpp>
#include <psemek/gfx/effect/blur.hpp>
#include <psemek/gfx/effect/overlay.hpp>

#include <psemek/random/generator.hpp>
#include <psemek/random/uniform_hemiball.hpp>
#include <psemek/random/uniform_sphere.hpp>

#include <psemek/math/homogeneous.hpp>
#include <psemek/math/gram_schmidt.hpp>
#include <psemek/math/translation.hpp>
#include <psemek/math/mesh.hpp>
#include <psemek/math/contains.hpp>

#include <psemek/cg/convex_hull_2d/graham.hpp>
#include <psemek/cg/body/frustum.hpp>
#include <psemek/cg/body/box.hpp>
#include <psemek/cg/convex/separation.hpp>

#include <psemek/util/to_string.hpp>
#include <psemek/util/hash.hpp>
#include <psemek/util/hash_table.hpp>
#include <psemek/util/exception.hpp>

#include <map>

namespace psemek::gfx
{

	static char const g_buffer_pass_common[] =
R"(#version 330

const uint O_UNIFORM_COLOR  = 1u << 0;
const uint O_TEXTURE_COLOR  = 1u << 1;
const uint O_TRANSPARENT    = 1u << 2;
const uint O_LIT            = 1u << 3;
const uint O_CASTS_SHADOW   = 1u << 4;
const uint O_PRE_TRANSFORM  = 1u << 5;
const uint O_POST_TRANSFORM = 1u << 6;
const uint O_INSTANCED      = 1u << 7;
const uint O_BLOOMING       = 1u << 8;
const uint O_BUMP_MAP       = 1u << 9;

uniform uint u_flag_mask;
)";

	static char const g_buffer_pass_vs[] =
R"(

uniform mat4 u_camera_transform;
uniform mat4x3 u_pre_transform;
uniform mat4x3 u_post_transform;

layout (location = 0) in vec4 in_position;
layout (location = 1) in vec4 in_color;
layout (location = 2) in vec2 in_texcoord;
layout (location = 3) in vec3 in_normal;

layout (location = 4) in mat3x4 in_instance_transform;

out vec3 position;
out vec4 color;
out vec2 texcoord;
out vec3 normal;

void main()
{
	vec4 pos = in_position;
	vec3 n = in_normal;

	if ((u_flag_mask & O_PRE_TRANSFORM) != 0u)
	{
		pos = vec4(u_pre_transform * pos, 1.0);
		n = u_pre_transform * vec4(n, 0.0);
	}

	if ((u_flag_mask & O_INSTANCED) != 0u)
	{
		pos = vec4(transpose(in_instance_transform) * pos, 1.0);
		n = transpose(in_instance_transform) * vec4(n, 0.0);
	}

	if ((u_flag_mask & O_POST_TRANSFORM) != 0u)
	{
		pos = vec4(u_post_transform * pos, 1.0);
		n = u_post_transform * vec4(n, 0.0);
	}

	position = pos.xyz;

	gl_Position = u_camera_transform * pos;

	color = in_color;
	texcoord = in_texcoord;
	normal = n;
}
)";

	static char const g_buffer_pass_fs[] =
R"(

uniform vec4 u_color;
uniform sampler2D u_texture;
uniform sampler2D u_bump_texture;
uniform vec2 u_material;
uniform float u_max_intensity;

in vec3 position;
in vec4 color;
in vec2 texcoord;
in vec3 normal;

layout (location = 0) out vec3 out0;
layout (location = 1) out vec4 out1;
layout (location = 2) out uint out2;
layout (location = 3) out vec2 out3;

uint pack_normal(vec3 n)
{
	uint face = 0u;

	float a0, a1;

	vec3 an = abs(n);

	if (an.x > an.y)
	{
		if (an.x > an.z)
		{
			bool p = (n.x > 0.0);
			face = p ? 1u : 6u;
			a0 = (p ? n.y : n.z) / an.x;
			a1 = (p ? n.z : n.y) / an.x;
		}
		else
		{
			bool p = (n.z > 0.0);
			face = p ? 4u : 3u;
			a0 = (p ? n.x : n.y) / an.z;
			a1 = (p ? n.y : n.x) / an.z;
		}
	}
	else
	{
		if (an.y > an.z)
		{
			bool p = (n.y > 0.0);
			face = p ? 2u : 5u;
			a0 = (p ? n.z : n.x) / an.y;
			a1 = (p ? n.x : n.z) / an.y;
		}
		else
		{
			bool p = (n.z > 0.0);
			face = p ? 4u : 3u;
			a0 = (p ? n.x : n.y) / an.z;
			a1 = (p ? n.y : n.x) / an.z;
		}
	}

	uint v0 = uint((a0 * 0.5 + 0.5) * float(1 << 15));
	uint v1 = uint((a1 * 0.5 + 0.5) * float(1 << 14));

	return (face << 29u) | (v1 << 15u) | v0;
}

void main()
{
	vec4 albedo;
	if ((u_flag_mask & O_TEXTURE_COLOR) != 0u)
	{
		vec4 base_color = texture(u_texture, texcoord);
		if ((u_flag_mask & O_UNIFORM_COLOR) != 0u)
			albedo = u_color * base_color;
		else
			albedo = base_color;
	}
	else
	{
		if ((u_flag_mask & O_UNIFORM_COLOR) != 0u)
			albedo = u_color;
		else
			albedo = color;
	}

	vec3 n = normal;
	if ((u_flag_mask & O_BUMP_MAP) != 0u)
	{
		mat2 dtdf;
		dtdf[0] = dFdx(texcoord);
		dtdf[1] = dFdy(texcoord);

		mat2x3 dpdf;
		dpdf[0] = dFdx(position);
		dpdf[1] = dFdy(position);

		mat2x3 dpdt = dpdf * inverse(dtdf);

		vec3 z = n;
		vec3 x = normalize(dpdt[0]);
		vec3 y = normalize(dpdt[1]);

		vec3 v = texture(u_bump_texture, texcoord).xyz * 2.0 - vec3(1.0);

		n = normalize(v.x * x + v.y * y + v.z * z);
	}

	out0 = position;
	out1 = vec4(albedo.rgb / u_max_intensity, (u_flag_mask & O_LIT) != 0u ? 1.f : 0.5f);
	out2 = pack_normal(normalize(n));
	out3 = u_material;
}
)";

static char const shadow_builder_vs[] =
R"(
uniform mat4 u_light_transform;
uniform mat4x3 u_pre_transform;
uniform mat4x3 u_post_transform;

layout (location = 0) in vec4 in_position;

layout (location = 4) in mat3x4 in_instance_transform;

void main()
{
	vec4 pos = in_position;

	if ((u_flag_mask & O_PRE_TRANSFORM) != 0u)
	{
		pos = vec4(u_pre_transform * pos, 1.0);
	}

	if ((u_flag_mask & O_INSTANCED) != 0u)
	{
		pos = vec4(transpose(in_instance_transform) * pos, 1.0);
	}

	if ((u_flag_mask & O_POST_TRANSFORM) != 0u)
	{
		pos = vec4(u_post_transform * pos, 1.0);
	}

	gl_Position = u_light_transform * pos;
}
)";

static char const shadow_builder_gs[] =
R"(#version 330

uniform mat4 u_layer_transform[6];

layout (triangles) in;
layout (triangle_strip, max_vertices = 18) out;

void main()
{
	for (int face = 0; face < 6; ++face)
	{
		gl_Layer = face;
		for (int i = 0; i < 3; ++i)
		{
			gl_Position = u_layer_transform[face] * gl_in[i].gl_Position;
			EmitVertex();
		}
		EndPrimitive();
	}
}
)";

static char const shadow_builder_fs[] =
R"(#version 330
void main(){}
)";

	static char const fullscreen_vs[] =
R"(#version 330

const vec4 vertices[6] = vec4[6](
	vec4(-1.0, -1.0, 0.0, 1.0),
	vec4( 1.0, -1.0, 0.0, 1.0),
	vec4( 1.0,  1.0, 0.0, 1.0),

	vec4(-1.0, -1.0, 0.0, 1.0),
	vec4( 1.0,  1.0, 0.0, 1.0),
	vec4(-1.0,  1.0, 0.0, 1.0)
);

out vec2 texcoord;

void main()
{
	gl_Position = vertices[gl_VertexID];

	texcoord = vertices[gl_VertexID].xy * 0.5 + vec2(0.5);
}
)";

	static char const screen_vs[] =
R"(#version 330

layout (location = 0) in vec2 in_position;

out vec2 texcoord;

void main()
{
	gl_Position = vec4(in_position, 0.0, 1.0);

	texcoord = in_position * 0.5 + vec2(0.5);
}
)";


	static char const light_common[] =
R"(#version 330

uniform sampler2D u_g0;
uniform sampler2D u_g1;
uniform usampler2D u_g2;
uniform sampler2D u_g3;


in vec2 texcoord;

out vec4 out_color;

vec3 unpack_normal(uint v)
{
	uint v0 = v & ((1u << 15) - 1u);
	uint v1 = (v >> 15) & ((1u << 14) - 1u);
	uint face = (v >> 29) & 7u;

	float a0 = 2.0 * float(v0) / float(1 << 15) - 1.0;
	float a1 = 2.0 * float(v1) / float(1 << 14) - 1.0;

	if (face == 1u)
	{
		return normalize(vec3(1.0, a0, a1));
	}
	else if (face == 2u)
	{
		return normalize(vec3(a1, 1.0, a0));
	}
	else if (face == 3u)
	{
		return normalize(vec3(a1, a0, -1.0));
	}
	else if (face == 4u)
	{
		return normalize(vec3(a0, a1, 1.0));
	}
	else if (face == 5u)
	{
		return normalize(vec3(a0, -1.0, a1));
	}
	else if (face == 6u)
	{
		return normalize(vec3(-1.0, a1, a0));
	}

	return vec3(0.0, 0.0, 0.0);
}

)";

	static char const ambient_pass_fs[] =
R"(

uniform vec3 u_ambient;
uniform sampler2D u_ssao_texture;
uniform int u_use_ssao;

uniform float u_max_intensity;

void main()
{
	vec4 albedo = texture(u_g1, texcoord);

	vec3 color;

	if (albedo.a < 0.25)
		discard;
	else if (albedo.a < 0.75)
		color = albedo.rgb;
	else if (u_use_ssao == 1)
		color = albedo.rgb * u_ambient * texture(u_ssao_texture, texcoord).r;
	else
		color = albedo.rgb * u_ambient;

	out_color = vec4(color, 1.0);
}
)";

	static char const directional_light_pass_fs[] =
R"(

uniform vec3 u_light_direction;
uniform vec3 u_light_color;

#define MAX_CASCADES 8

uniform mat4 u_light_transform[MAX_CASCADES];
uniform sampler2DArrayShadow u_shadow;
uniform int u_shadowed;
uniform int u_cascades;

uniform vec3 u_camera_position;

uniform float u_max_intensity;

void main()
{
	vec4 albedo = texture(u_g1, texcoord);
	if (albedo.a < 0.5)
	{
		out_color = vec4(0.0);
		return;
	}

	vec3 position = texture(u_g0, texcoord).xyz;
	vec3 normal = unpack_normal(texture(u_g2, texcoord).r);
	vec2 material = texture(u_g3, texcoord).xy;

	vec3 view = normalize(u_camera_position - position);

	float d = dot(u_light_direction, normal);

	vec3 refl = 2.0 * normal * d - u_light_direction;

	float l = max(0.0, d) + pow(max(0.0, dot(view, refl)), material.y) * material.x;

	vec3 color = l * albedo.rgb * u_light_color;

	if (u_shadowed != 0 && d > 0.0)
	{
		for (int cascade = 0; cascade < MAX_CASCADES; ++cascade)
		{
			if (cascade >= u_cascades) break;

			vec4 shadow_space = u_light_transform[cascade] * vec4(position, 1.0);

			vec3 tc = shadow_space.xyz / shadow_space.w;
			tc = tc * 0.5 + vec3(0.5);

			if (tc.x >= 0.0 && tc.x <= 1.0 && tc.y >= 0.0 && tc.y <= 1.0 && tc.z >= 0.0)
			{
//				int l = cascade + 1;
//				color = vec3(float(l & 1), float(l & 2) / 2.0, float(l & 4) / 4.0);
//				break;

				float s = 0.0;
				for (int tx = -2; tx <= 2; ++tx)
				{
					for (int ty = -2; ty <= 2; ++ty)
					{
						float d = 1.0 / 1024.0 / pow(2.0, float(cascade));
						s += texture(u_shadow, vec4(tc.x + float(tx) * d, tc.y + float(ty) * d, float(cascade), tc.z)) / 25.0;
					}
				}

				color *= s;
				break;
			}
		}
	}

	out_color = vec4(color, 1.0);
}
)";

	static char const point_light_pass_fs[] =
R"(

uniform vec3 u_light_position;
uniform vec3 u_light_color;
uniform vec3 u_light_attenuation;

uniform samplerCubeShadow u_shadow;
uniform int u_shadowed;
uniform vec3 u_shadow_far_negative;
uniform vec3 u_shadow_far_positive;
uniform float u_shadow_near;

uniform vec3 u_camera_position;

uniform float u_max_intensity;

void main()
{
	vec4 albedo = texture(u_g1, texcoord);
	if (albedo.a < 0.5)
	{
		out_color = vec4(0.0);
		return;
	}

	vec3 position = texture(u_g0, texcoord).xyz;
	vec3 normal = unpack_normal(texture(u_g2, texcoord).r);
	vec2 material = texture(u_g3, texcoord).xy;

	vec3 view = normalize(u_camera_position - position);

	vec3 light = u_light_position - position;

	float r = length(light);

	vec3 light_n = light / r;

	float d = dot(light_n, normal);

	vec3 refl = 2.0 * normal * d - light_n;

	float l = max(0.0, d) + pow(max(0.0, dot(view, refl)), material.y) * material.x;

	vec3 color = l * albedo.rgb * u_light_color / (u_light_attenuation.x + r * (u_light_attenuation.y + r * u_light_attenuation.z));

	if (u_shadowed != 0 && d > 0.0)
	{
		vec3 dir = -light;

		float far;
		float near = u_shadow_near;

		vec3 adir = abs(dir);

		float d = max(adir.x, max(adir.y, adir.z));

		if (d == adir.x)
		{
			if (dir.x > 0.0)
				far = u_shadow_far_positive.x;
			else
				far = u_shadow_far_negative.x;
		}
		else if (d == adir.y)
		{
			if (dir.y > 0.0)
				far = u_shadow_far_positive.y;
			else
				far = u_shadow_far_negative.y;
		}
		else
		{
			if (dir.z > 0.0)
				far = u_shadow_far_positive.z;
			else
				far = u_shadow_far_negative.z;
		}

		float A = (far + near) / (far - near);
		float B = - 2.0 * far * near / (far - near);

		d = (A * d + B) / d;

		float v = texture(u_shadow, vec4(dir, d * 0.5 + 0.5));

		color *= v;
	}

	out_color = vec4(color, 1.0);
}
)";

	static char const transparent_pass_vs[] =
R"(

uniform mat4 u_camera_transform;
uniform mat4x3 u_pre_transform;
uniform mat4x3 u_post_transform;

layout (location = 0) in vec4 in_position;
layout (location = 1) in vec4 in_color;
layout (location = 2) in vec2 in_texcoord;
layout (location = 3) in vec3 in_normal;

layout (location = 4) in mat3x4 in_instance_transform;

out vec4 color;
out vec2 texcoord;

void main()
{
	vec4 pos = in_position;
	vec3 n = in_normal;

	if ((u_flag_mask & O_PRE_TRANSFORM) != 0u)
	{
		pos = vec4(u_pre_transform * pos, 1.0);
		n = u_pre_transform * vec4(n, 0.0);
	}

	if ((u_flag_mask & O_INSTANCED) != 0u)
	{
		pos = vec4(transpose(in_instance_transform) * pos, 1.0);
		n = transpose(in_instance_transform) * vec4(n, 0.0);
	}

	if ((u_flag_mask & O_POST_TRANSFORM) != 0u)
	{
		pos = vec4(u_post_transform * pos, 1.0);
		n = u_post_transform * vec4(n, 0.0);
	}

	gl_Position = u_camera_transform * pos;

	color = in_color;
	texcoord = in_texcoord;
}
)";

	static char const transparent_pass_fs[] =
R"(

uniform vec4 u_color;
uniform sampler2D u_texture;
uniform float u_max_intensity;

in vec4 color;
in vec2 texcoord;

layout (location = 0) out vec4 out_color;

void main()
{
	vec4 albedo;
	if ((u_flag_mask & O_TEXTURE_COLOR) != 0u)
	{
		vec4 base_color = texture(u_texture, texcoord);
		if ((u_flag_mask & O_UNIFORM_COLOR) != 0u)
			albedo = u_color * base_color;
		else
			albedo = base_color;
	}
	else
	{
		if ((u_flag_mask & O_UNIFORM_COLOR) != 0u)
			albedo = u_color;
		else
			albedo = color;
	}

	out_color = vec4(albedo.rgb / u_max_intensity, albedo.a);
}
)";

	static char const ssao_pass_fs[] =
R"(

uniform sampler2D u_ssao_rotation;
uniform vec2 u_ssao_rotation_step;

uniform float u_ssao_radius;

uniform vec3 u_ssao_kernel[32];

uniform mat4 u_view;
uniform mat4 u_transform;

void main()
{
	vec4 albedo = texture(u_g1, texcoord);
	if (albedo.a < 0.5)
	{
		out_color = vec4(1.0);
		return;
	}

	vec3 position = texture(u_g0, texcoord).xyz;
	vec3 normal = unpack_normal(texture(u_g2, texcoord).r);

	vec3 tangent = texture(u_ssao_rotation, texcoord * u_ssao_rotation_step).xyz;
	tangent = normalize(tangent - normal * dot(normal, tangent));
	vec3 bitangent = cross(normal, tangent);

	mat3 tbn = mat3(tangent, bitangent, normal);

	float occlusion = 0.0;
	for (int i = 0; i < 32; ++i)
	{
		vec3 sample = position + (tbn * u_ssao_kernel[i]) * u_ssao_radius;

		vec4 clip = u_transform * vec4(sample, 1.0);
		clip.xy /= clip.w;
		clip.xy = clip.xy * 0.5 + vec2(0.5);

		vec3 pos = texture(u_g0, clip.xy).xyz;

		float w = length(pos - position) / u_ssao_radius;

		if (w <= 1.0 && dot(pos - position, normal) >= 0.f && (u_view * vec4(pos, 1.0)).z > (u_view * vec4(sample, 1.0)).z)
			occlusion += 1.0 / 32.0;
	}

	out_color = vec4(1.0 - occlusion);
}
)";

	char const occlusion_pass_vs[] =
R"(#version 330

uniform mat4 u_camera_transform;
uniform vec3 u_box_min;
uniform vec3 u_box_max;

vec3 vertices[8] = vec3[8](
	vec3(0.0, 0.0, 0.0),
	vec3(1.0, 0.0, 0.0),
	vec3(0.0, 1.0, 0.0),
	vec3(1.0, 1.0, 0.0),
	vec3(0.0, 0.0, 1.0),
	vec3(1.0, 0.0, 1.0),
	vec3(0.0, 1.0, 1.0),
	vec3(1.0, 1.0, 1.0)
	);

ivec3 faces[12] = ivec3[12](
	// -Z
	ivec3(0, 2, 1),
	ivec3(1, 2, 3),
	// +Z
	ivec3(4, 5, 6),
	ivec3(6, 5, 7),
	// -Y
	ivec3(0, 1, 4),
	ivec3(4, 1, 5),
	// +Y
	ivec3(2, 6, 3),
	ivec3(6, 7, 3),
	// -X
	ivec3(0, 4, 2),
	ivec3(2, 4, 6),
	// +X
	ivec3(1, 3, 5),
	ivec3(3, 7, 5)
	);

void main()
{
	ivec3 face = faces[gl_VertexID / 3];
	int i = gl_VertexID % 3;
	vec3 pos = vertices[(i == 0) ? face.x : (i == 1) ? face.y : face.z];

	gl_Position = u_camera_transform * vec4(pos * (u_box_max - u_box_min) + u_box_min, 1.0);
}
)";

	char const occlusion_pass_fs[] =
R"(#version 330
void main(){}
)";

	static std::size_t bbox_to_screen_fan(math::matrix<float, 4, 4> const & camera_transform, math::box<float, 3> const & b, math::point<float, 2> * result)
	{
		math::point<float, 2> bbox_corners_screen[8];
		auto bbox_corners_screen_end = bbox_corners_screen;

		bool need_clipping = false;
		for (int z = 0; z < 2; ++z)
		{
			for (int y = 0; y < 2; ++y)
			{
				for (int x = 0; x < 2; ++x)
				{
					math::point<float, 3> p;
					p[0] = math::lerp<float>(b[0], x);
					p[1] = math::lerp<float>(b[1], y);
					p[2] = math::lerp<float>(b[2], z);

					auto q = camera_transform * math::homogeneous(p);

					if (q[2] < -q[3] || q[2] > q[3])
					{
						need_clipping = true;
						break;
					}

					*bbox_corners_screen_end++ = {q[0] / q[3], q[1] / q[3]};
				}
			}
		}

		if (need_clipping)
		{
			*result++ = {-1.f, -1.f};
			*result++ = { 1.f, -1.f};
			*result++ = { 1.f,  1.f};
			*result++ = {-1.f,  1.f};
			return 4;
		}

		math::point<float, 2> * bbox_hull_screen_it[8];
		auto bbox_hull_size = cg::graham_convex_hull(bbox_corners_screen, bbox_corners_screen_end, bbox_hull_screen_it) - bbox_hull_screen_it;

		for (std::size_t i = 0; i < bbox_hull_size; ++i)
			result[i] = *bbox_hull_screen_it[i];

		return bbox_hull_size;
	}

	struct deferred_renderer::impl
	{
		deferred_renderer::position_mode position_mode = deferred_renderer::position_mode::float32;
		bool position_mode_changed = true;

		gfx::program occlusion_pass_program{occlusion_pass_vs, occlusion_pass_fs};
		gfx::program g_buffer_pass_program{std::string(g_buffer_pass_common) + g_buffer_pass_vs, std::string(g_buffer_pass_common) + g_buffer_pass_fs};
		gfx::program ambient_pass_program{fullscreen_vs, std::string(light_common) + ambient_pass_fs};
		gfx::program directional_light_pass_program{screen_vs, std::string(light_common) + directional_light_pass_fs};
		gfx::program point_light_pass_program{screen_vs, std::string(light_common) + point_light_pass_fs};
		gfx::program shadow_builder_program{std::string(g_buffer_pass_common) + shadow_builder_vs, shadow_builder_fs};
		gfx::program cubemap_shadow_builder_program{std::string(g_buffer_pass_common) + shadow_builder_vs, shadow_builder_gs, shadow_builder_fs};
		gfx::program transparent_pass_program{std::string(g_buffer_pass_common) + transparent_pass_vs, std::string(g_buffer_pass_common) + transparent_pass_fs};
		gfx::program bloom_pass_program{std::string(g_buffer_pass_common) + transparent_pass_vs, std::string(g_buffer_pass_common) + transparent_pass_fs};
		gfx::program ssao_pass_program{fullscreen_vs, std::string(light_common) + ssao_pass_fs};

		// G-buffer attachments:
		// 0 - position (rbg)
		// 1 - albedo (rgb), lit (a)
		// 2 - normal (packed)
		// 3 - material.diffuse (r), material.specular (g), material.shininess (b)

		gfx::framebuffer g_framebuffer;
		gfx::framebuffer occlusion_framebuffer;
		gfx::framebuffer bg_framebuffer;
		gfx::texture_2d g_buffer_texture[4];
		gfx::texture_2d g_buffer_depth;

		// Only albedo & depth attached
		gfx::framebuffer transparent_framebuffer;

		std::optional<math::vector<std::size_t, 2>> g_buffer_size;

		gfx::framebuffer directional_shadow_framebuffer;
		gfx::texture_2d_array directional_shadow_texture;

		gfx::framebuffer point_shadow_framebuffer;
		gfx::texture_cubemap point_shadow_texture;

		std::map<std::pair<int, float>, hblur> hblur_container;
		std::map<std::pair<int, float>, vblur> vblur_container;
		overlay bloom_overlay;

		std::optional<math::vector<std::size_t, 2>> bloom_size;
		std::optional<std::size_t> bloom_downsample;

		// 0: original, unblurred bloom
		// 1: horizontally blurred bloom
		// 2: fully blurred bloom
		gfx::framebuffer bloom_framebuffer[3];
		gfx::texture_2d bloom_texture[3];

		gfx::texture_2d ssao_rotation_texture;

		std::optional<math::vector<std::size_t, 2>> ssao_size;
		std::optional<std::size_t> ssao_downsample;

		// 0: original, unblurred ssao
		// 1: horizontally blurred ssao
		// 2: fully blurred ssao
		gfx::framebuffer ssao_framebuffer[3];
		gfx::texture_2d ssao_texture[3];

		hblur ssao_hblur{5, 2.f};
		vblur ssao_vblur{5, 2.f};

		gfx::mesh screen_mesh;

		std::vector<GLuint> queries;
		gfx::array box_array;
		std::optional<math::matrix<float, 4, 4>> prev_transform;
	};

	deferred_renderer::deferred_renderer()
		: pimpl_{make_impl()}
	{
		impl().g_buffer_pass_program.bind();
		impl().g_buffer_pass_program["u_texture"] = 0;
		impl().g_buffer_pass_program["u_bump_texture"] = 1;

		for (std::size_t i = 0; i < 4; ++i)
		{
			impl().g_buffer_texture[i].nearest_filter();
		}
		impl().g_buffer_depth.nearest_filter();

		impl().ambient_pass_program.bind();
		impl().ambient_pass_program["u_g0"] = 0;
		impl().ambient_pass_program["u_g1"] = 1;
		impl().ambient_pass_program["u_g2"] = 2;
		impl().ambient_pass_program["u_g3"] = 3;
		impl().ambient_pass_program["u_ssao_texture"] = 4;

		impl().directional_light_pass_program.bind();
		impl().directional_light_pass_program["u_g0"] = 0;
		impl().directional_light_pass_program["u_g1"] = 1;
		impl().directional_light_pass_program["u_g2"] = 2;
		impl().directional_light_pass_program["u_g3"] = 3;
		impl().directional_light_pass_program["u_shadow"] = 4;

		impl().point_light_pass_program.bind();
		impl().point_light_pass_program["u_g0"] = 0;
		impl().point_light_pass_program["u_g1"] = 1;
		impl().point_light_pass_program["u_g2"] = 2;
		impl().point_light_pass_program["u_g3"] = 3;
		impl().point_light_pass_program["u_shadow"] = 4;

		impl().directional_shadow_texture.linear_filter();
		impl().directional_shadow_texture.clamp();
		gl::TexParameteri(impl().directional_shadow_texture.target, gl::TEXTURE_COMPARE_MODE, gl::COMPARE_REF_TO_TEXTURE);
		gl::TexParameteri(impl().directional_shadow_texture.target, gl::TEXTURE_COMPARE_FUNC, gl::LEQUAL);

		impl().point_shadow_texture.linear_filter();
		impl().point_shadow_texture.clamp();
		gl::TexParameteri(impl().point_shadow_texture.target, gl::TEXTURE_COMPARE_MODE, gl::COMPARE_REF_TO_TEXTURE);
		gl::TexParameteri(impl().point_shadow_texture.target, gl::TEXTURE_COMPARE_FUNC, gl::LEQUAL);

		for (std::size_t i = 0; i < 3; ++i)
		{
			impl().bloom_texture[i].linear_filter();
			impl().bloom_texture[i].clamp();
		}

		{
			// params are random
			random::generator rng{0xaa8e8eabull, 0x44a700e7ull};

			{
				random::uniform_hemiball_vector_distribution<float, 3> d{math::vector{0.f, 0.f, 1.f}};
				std::vector<math::vector<float, 3>> ssao_kernel(32);
				for (auto & v : ssao_kernel)
					v = d(rng);

				impl().ssao_pass_program.bind();
				for (std::size_t i = 0; i < ssao_kernel.size(); ++i)
				{
					impl().ssao_pass_program[util::to_string("u_ssao_kernel[", i, "]").data()] = ssao_kernel[i];
				}
			}

			{
				random::uniform_sphere_vector_distribution<float, 2> d;

				basic_pixmap<math::vector<float, 2>> pm({8, 8});
				for (auto & v : pm)
					v = d(rng);

				impl().ssao_rotation_texture.load(pm);
				impl().ssao_rotation_texture.nearest_filter();
				impl().ssao_rotation_texture.repeat();
			}
		}

		for (std::size_t i = 0; i < 3; ++i)
		{
			impl().ssao_texture[i].linear_filter();
			impl().ssao_texture[i].clamp();
			gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_SWIZZLE_G, gl::RED);
			gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_SWIZZLE_B, gl::RED);
			gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_SWIZZLE_A, gl::RED);
		}

		impl().ssao_pass_program.bind();
		impl().ssao_pass_program["u_g0"] = 0;
		impl().ssao_pass_program["u_g1"] = 1;
		impl().ssao_pass_program["u_g2"] = 2;
		impl().ssao_pass_program["u_g3"] = 3;
		impl().ssao_pass_program["u_ssao_rotation"] = 4;

		impl().screen_mesh.setup<math::point<float, 2>>();
	}

	deferred_renderer::~deferred_renderer() = default;

	void deferred_renderer::set_position_mode(position_mode mode)
	{
		impl().position_mode = mode;
		impl().position_mode_changed = true;
	}

	static std::uint32_t const O_UNIFORM_COLOR  = 1 << 0;
	static std::uint32_t const O_TEXTURE_COLOR  = 1 << 1;
	static std::uint32_t const O_TRANSPARENT    = 1 << 2;
	static std::uint32_t const O_LIT            = 1 << 3;
	static std::uint32_t const O_CASTS_SHADOW   = 1 << 4;
	static std::uint32_t const O_PRE_TRANSFORM  = 1 << 5;
	static std::uint32_t const O_POST_TRANSFORM = 1 << 6;
	static std::uint32_t const O_INSTANCED      = 1 << 7;
	static std::uint32_t const O_BLOOMING       = 1 << 8;
	static std::uint32_t const O_BUMP_MAP       = 1 << 9;

	std::uint32_t mask(deferred_renderer::object const & o)
	{
		std::uint32_t m = 0;
		if (o.mat->color) m |= O_UNIFORM_COLOR;
		if (o.mat->texture) m |= O_TEXTURE_COLOR;
		if (o.mat->transparent) m |= O_TRANSPARENT;
		if (o.mat->lit) m |= O_LIT;
		if (o.mat->casts_shadow) m |= O_CASTS_SHADOW;
		if (o.pre_transform) m |= O_PRE_TRANSFORM;
		if (o.post_transform) m |= O_POST_TRANSFORM;
		if (o.mesh->is_instanced()) m |= O_INSTANCED;
		if (o.mat->blooming) m |= O_BLOOMING;
		if (o.mat->bump_map) m |= O_BUMP_MAP;
		return m;
	}

	void deferred_renderer::render(std::vector<object> const & objects, render_target const & target, options const & opts)
	{
		// Allocate query objects

		std::size_t const bins_total = opts.grid_size[0] * opts.grid_size[1] * opts.grid_size[2];

		if (impl().queries.size() < bins_total)
		{
			auto old_size = impl().queries.size();
			impl().queries.resize(bins_total);
			gl::GenQueries(bins_total - old_size, impl().queries.data() + old_size);
		}

		// Get camera info

		assert(opts.camera);
		auto const camera_transform = opts.camera->transform();
		auto const camera_position = opts.camera->position();
		auto const camera_direction = opts.camera->direction();
		auto const camera_clip_planes = opts.camera->clip_planes();

		cg::frustum<float, 3> camera_frustum(camera_transform);

		// Sort objects by mask & compute bbox

		struct objects_bucket
		{
			std::vector<std::size_t> objects;
			std::size_t first_visible;
		};

		struct bin
		{
			util::hash_map<std::tuple<std::uint32_t, material const *>, objects_bucket> buckets;
			math::box<float, 3> bbox;
			bool contains_near_clip = false;
			float camera_separation;
		};

		math::box<float, 3> all_bbox;
		std::vector<float> camera_distance(objects.size());

		for (std::size_t i = 0; i < objects.size(); ++i)
		{
			auto const & o = objects[i];
			all_bbox |= o.bbox;

			float dist = -std::numeric_limits<float>::infinity();
			for (int c = 0; c < 8; ++c)
			{
				dist = std::max(dist, dot(o.bbox.corner(c & 1, (c & 2) >> 1, (c & 4) >> 1) - camera_position, camera_direction));
			}

			camera_distance[i] = dist;
		}

		util::ndarray<bin, 3> bins({opts.grid_size[0], opts.grid_size[1], opts.grid_size[2]});

		math::box<float, 3> lit_bbox;
		math::box<float, 3> casts_shadow_bbox;

		for (std::size_t i = 0; i < objects.size(); ++i)
		{
			auto const & o = objects[i];
			assert(o.mesh);

			if (o.mat->lit && o.mat->transparent)
				throw util::exception("Materials that are both lit & transparent are not supported");

			if (o.mat->lit && o.mat->blooming)
				throw util::exception("Materials that are both lit & blooming are not supported");

			if (o.mat->casts_shadow && o.mat->transparent)
				throw util::exception("Transparent objects cannot cast shadow");

			auto c = o.bbox.center() - all_bbox.corner(0, 0, 0);

			int bx = math::clamp(std::floor(c[0] / all_bbox[0].length() * opts.grid_size[0]), {0, opts.grid_size[0] - 1});
			int by = math::clamp(std::floor(c[1] / all_bbox[1].length() * opts.grid_size[1]), {0, opts.grid_size[1] - 1});
			int bz = math::clamp(std::floor(c[2] / all_bbox[2].length() * opts.grid_size[2]), {0, opts.grid_size[2] - 1});

			bin & b = bins(bx, by, bz);

			b.bbox |= o.bbox;
			b.buckets[std::tuple{mask(o), o.mat}].objects.push_back(i);

			if (o.mat->lit) lit_bbox |= o.bbox;
			if (o.mat->casts_shadow) casts_shadow_bbox |= o.bbox;
		}

		for (auto & b : bins)
		{
			b.bbox = math::expand(b.bbox, b.bbox.dimensions() / 64.f);

			b.camera_separation = cg::separation(camera_frustum, cg::box{b.bbox}).distance;

			b.contains_near_clip = true;
			for (int i = 0; i < 4; ++i)
				b.contains_near_clip &= math::contains(b.bbox, camera_frustum.vertices[i]);

			for (auto & p : b.buckets)
			{
				std::sort(p.second.objects.begin(), p.second.objects.end(), [&](auto i, auto j){ return camera_distance[i] < camera_distance[j]; });
				p.second.first_visible = std::partition_point(p.second.objects.begin(), p.second.objects.end(), [&](auto i){ return camera_distance[i] < 0.f; }) - p.second.objects.begin();
			}
		}

		bool use_occlusion = false;

		auto render_all = [&](auto & program, auto && predicate, bool clip_by_camera = false)
		{
			for (int bi = 0; bi < bins_total; ++bi)
			{
				auto const & b = bins.data()[bi];

				if (clip_by_camera && b.camera_separation > 0.f) continue;

				bool const bin_use_occlusion = clip_by_camera && !b.contains_near_clip && !b.buckets.empty() && use_occlusion;

				if (bin_use_occlusion)
					gl::BeginConditionalRender(impl().queries[bi], gl::QUERY_NO_WAIT);

				for (auto const & p : b.buckets)
				{
					if (p.second.objects.empty()) continue;

					std::uint32_t mask = std::get<0>(p.first);

					if (!predicate(mask)) continue;

					material const * mat = std::get<1>(p.first);

					program["u_flag_mask"] = mask;

					if (mask & O_UNIFORM_COLOR)
						program["u_color"] = *(mat->color);

					if (mask & O_TEXTURE_COLOR)
					{
						gl::ActiveTexture(gl::TEXTURE0);
						mat->texture->bind();
					}

					if (mask & O_BUMP_MAP)
					{
						gl::ActiveTexture(gl::TEXTURE1);
						mat->bump_map->bind();
					}

					program["u_material"] = math::vector<float, 2>{mat->specular.intensity, mat->specular.shininess};

					for (std::size_t i = clip_by_camera ? p.second.first_visible : 0; i < p.second.objects.size(); ++i)
					{
						std::size_t id = p.second.objects[i];

						auto const & o = objects[id];

						if (mask & O_PRE_TRANSFORM)
							program["u_pre_transform"] = *o.pre_transform;

						if (mask & O_POST_TRANSFORM)
							program["u_post_transform"] = *o.post_transform;

						o.mesh->draw();
					}
				}

				if (bin_use_occlusion)
					gl::EndConditionalRender();
			}
		};

		// Resize g-buffer if needed

		auto const buffer_size = math::cast<std::size_t>(target.viewport.dimensions());
		bool const buffer_size_changed = !impl().g_buffer_size || *impl().g_buffer_size != buffer_size;
		if (buffer_size_changed || impl().position_mode_changed)
		{
			if (impl().position_mode == position_mode::float16)
				impl().g_buffer_texture[0].load<math::vector<gfx::float16, 3>>(buffer_size);
			else if (impl().position_mode == position_mode::float32)
				impl().g_buffer_texture[0].load<math::vector<float, 3>>(buffer_size);

			if (buffer_size_changed)
			{
				impl().g_buffer_texture[1].load<math::vector<std::uint16_t, 4>>(buffer_size);
				impl().g_buffer_texture[2].load<gfx::integer<std::uint32_t>>(buffer_size);
				impl().g_buffer_texture[3].load<math::vector<gfx::float16, 2>>(buffer_size);
				impl().g_buffer_depth.load<gfx::depth24_pixel>(buffer_size);
			}

			for (std::size_t i = 0; i < 4; ++i)
			{
				impl().g_framebuffer.color(impl().g_buffer_texture[i], i);
			}

			impl().g_framebuffer.depth(impl().g_buffer_depth);
			impl().occlusion_framebuffer.depth(impl().g_buffer_depth);

			impl().bg_framebuffer.color(impl().g_buffer_texture[1]);

			impl().g_framebuffer.assert_complete();
			impl().occlusion_framebuffer.assert_complete();
			impl().bg_framebuffer.assert_complete();

			impl().transparent_framebuffer.color(impl().g_buffer_texture[1]);
			impl().transparent_framebuffer.depth(impl().g_buffer_depth);
			impl().transparent_framebuffer.assert_complete();

			impl().g_buffer_size = buffer_size;
			impl().position_mode_changed = false;
		}

		bool const bloom_size_changed = opts.bloom && (!impl().bloom_size || *impl().bloom_size != buffer_size || !impl().bloom_downsample || *impl().bloom_downsample != opts.bloom->downsample);
		if (bloom_size_changed)
		{
			impl().bloom_texture[0].load<gfx::color_rgba>(buffer_size);
			impl().bloom_framebuffer[0].color(impl().bloom_texture[0]);
			impl().bloom_framebuffer[0].depth(impl().g_buffer_depth);
			impl().bloom_framebuffer[0].assert_complete();

			impl().bloom_texture[1].load<gfx::color_rgba>({buffer_size[0] / opts.bloom->downsample, buffer_size[1]});
			impl().bloom_framebuffer[1].color(impl().bloom_texture[1]);
			impl().bloom_framebuffer[1].assert_complete();

			impl().bloom_texture[2].load<gfx::color_rgba>(buffer_size / opts.bloom->downsample);
			impl().bloom_framebuffer[2].color(impl().bloom_texture[2]);
			impl().bloom_framebuffer[2].assert_complete();

			impl().bloom_size = buffer_size;
			impl().bloom_downsample = opts.bloom->downsample;
		}

		bool const ssao_size_changed = opts.ssao && (!impl().ssao_size || *impl().ssao_size != buffer_size || !impl().ssao_downsample || *impl().ssao_downsample != opts.ssao->downsample);
		if (ssao_size_changed)
		{
			for (std::size_t i = 0; i < 3; ++i)
			{
				impl().ssao_texture[i].load<std::uint8_t>(buffer_size / opts.ssao->downsample);
				impl().ssao_framebuffer[i].color(impl().ssao_texture[i]);
				impl().ssao_framebuffer[i].assert_complete();
			}

			impl().ssao_size = buffer_size;
			impl().ssao_downsample = opts.ssao->downsample;
		}

		std::optional<std::pair<std::size_t, float>> hblur_params;
		std::optional<std::pair<std::size_t, float>> vblur_params;

		if (opts.bloom)
		{
			hblur_params = std::pair{opts.bloom->size, opts.bloom->sigma};
			if (impl().hblur_container.count(*hblur_params) == 0)
			{
				auto params = std::tuple{hblur_params->first, hblur_params->second};
				impl().hblur_container.emplace(std::piecewise_construct, params, params);
			}

			vblur_params = std::pair{opts.bloom->size, opts.bloom->sigma};
			if (impl().vblur_container.count(*vblur_params) == 0)
			{
				auto params = std::tuple{vblur_params->first, vblur_params->second};
				impl().vblur_container.emplace(std::piecewise_construct, params, params);
			}
		}

		// Occlusion pre-pass

		if (!buffer_size_changed && impl().prev_transform)
		{
			use_occlusion = true;

			impl().occlusion_framebuffer.bind();

			gl::Viewport(0, 0, target.viewport[0].length(), target.viewport[1].length());

			gl::DepthMask(gl::FALSE);
			gl::Enable(gl::DEPTH_TEST);
			gl::DepthFunc(gl::LEQUAL);
			gl::Enable(gl::DEPTH_CLAMP);

			gl::Disable(gl::CULL_FACE);

			impl().occlusion_pass_program.bind();
			impl().occlusion_pass_program["u_camera_transform"] = *impl().prev_transform;
			impl().box_array.bind();

			for (int bi = 0; bi < bins_total; ++bi)
			{
				auto & b = bins.data()[bi];

				if (b.buckets.empty()) continue;
				if (b.contains_near_clip) continue;
				if (b.camera_separation > 0.f) continue;

				impl().occlusion_pass_program["u_box_min"] = math::vector{b.bbox[0].min, b.bbox[1].min, b.bbox[2].min};
				impl().occlusion_pass_program["u_box_max"] = math::vector{b.bbox[0].max, b.bbox[1].max, b.bbox[2].max};

				gl::BeginQuery(gl::ANY_SAMPLES_PASSED, impl().queries[bi]);
				gl::DrawArrays(gl::TRIANGLES, 0, 36);
				gl::EndQuery(gl::ANY_SAMPLES_PASSED);
			}

			gl::DepthMask(gl::TRUE);
			gl::Disable(gl::DEPTH_CLAMP);
		}

		// Setup g-buffer

		impl().g_framebuffer.bind();

		gl::Viewport(0, 0, target.viewport[0].length(), target.viewport[1].length());

		GLenum g_draw_buffers[4] { gl::COLOR_ATTACHMENT0, gl::COLOR_ATTACHMENT1, gl::COLOR_ATTACHMENT2, gl::COLOR_ATTACHMENT3 };
		gl::DrawBuffers(4, g_draw_buffers);
		float buffer_1_clear[4] { 0.f, 0.f, 0.f, 0.f };
		if (opts.clear_color)
		{
			buffer_1_clear[0] = (*opts.clear_color)[0] / opts.max_intensity;
			buffer_1_clear[1] = (*opts.clear_color)[1] / opts.max_intensity;
			buffer_1_clear[2] = (*opts.clear_color)[2] / opts.max_intensity;
			buffer_1_clear[3] = 0.f;
		}

		gl::ClearBufferfv(gl::COLOR, 1, buffer_1_clear);

		if (opts.background_generator)
		{
			impl().bg_framebuffer.bind();

			gl::Enable(gl::DEPTH_TEST);
			gl::DepthFunc(gl::LEQUAL);
			gl::Disable(gl::BLEND);

			opts.background_generator();

			impl().g_framebuffer.bind();
		}

		gl::ClearDepth(1.f);
		gl::Clear(gl::DEPTH_BUFFER_BIT);

		gl::Enable(gl::DEPTH_TEST);
		gl::DepthFunc(gl::LEQUAL);

		gl::Disable(gl::BLEND);

		gl::Enable(gl::CULL_FACE);
		gl::CullFace(gl::BACK);

		// Render to g-buffer

		impl().g_buffer_pass_program.bind();
		impl().g_buffer_pass_program["u_camera_transform"] = camera_transform;
		impl().g_buffer_pass_program["u_max_intensity"] = opts.max_intensity;

		render_all(impl().g_buffer_pass_program, [](auto mask){ return !(mask & O_TRANSPARENT); }, true);

		// Render unlit transparent objects

		gl::DrawBuffer(gl::COLOR_ATTACHMENT0);

		impl().transparent_framebuffer.bind();
		impl().transparent_pass_program.bind();
		impl().transparent_pass_program["u_camera_transform"] = camera_transform;
		impl().transparent_pass_program["u_max_intensity"] = 1.f;

		gl::Enable(gl::BLEND);
		gl::BlendFuncSeparate(gl::SRC_ALPHA, gl::ONE_MINUS_SRC_ALPHA, gl::ZERO, gl::ONE);

		gl::DepthMask(gl::FALSE);

		render_all(impl().transparent_pass_program, [](auto mask){ return (mask & O_TRANSPARENT); }, true);

		// Render bloom

		if (opts.bloom)
		{
			impl().bloom_framebuffer[0].bind();

			gl::ClearColor(0.f, 0.f, 0.f, 0.f);
			gl::Clear(gl::COLOR_BUFFER_BIT);

			gl::Enable(gl::DEPTH_TEST);
			gl::DepthFunc(gl::LEQUAL);

			gl::Disable(gl::BLEND);

			impl().bloom_pass_program.bind();
			impl().bloom_pass_program["u_camera_transform"] = camera_transform;
			impl().bloom_pass_program["u_max_intensity"] = opts.max_intensity;

			render_all(impl().bloom_pass_program, [](auto mask){ return (mask & O_BLOOMING); }, true);
		}

		// Render unlit transparent objects to bloom

		if (opts.bloom)
		{
			impl().transparent_pass_program.bind();
			impl().transparent_pass_program["u_camera_transform"] = camera_transform;
			impl().transparent_pass_program["u_max_intensity"] = 1.f;

			gl::Enable(gl::BLEND);
			gl::BlendFuncSeparate(gl::SRC_ALPHA, gl::ONE_MINUS_SRC_ALPHA, gl::ZERO, gl::ONE);

			render_all(impl().transparent_pass_program, [](auto mask){ return (mask & O_TRANSPARENT); }, true);
		}

		gl::Disable(gl::BLEND);
		gl::DepthMask(gl::TRUE);

		// Apply horizontal blur to bloom

		if (opts.bloom)
		{
			render_target target;
			target.framebuffer = &impl().bloom_framebuffer[1];
			target.draw_buffer = gl::COLOR_ATTACHMENT0;
			target.viewport = {{{0, impl().bloom_texture[1].width()}, {0, impl().bloom_texture[1].height()}}};

			target.bind();
			gl::ClearColor(0.f, 0.f, 0.f, 0.f);
			gl::Clear(gl::COLOR_BUFFER_BIT);

			impl().hblur_container.at(*hblur_params).invoke(impl().bloom_texture[0], target);
		}

		// Apply vertical blur to bloom

		if (opts.bloom)
		{
			render_target target;
			target.framebuffer = &impl().bloom_framebuffer[2];
			target.draw_buffer = gl::COLOR_ATTACHMENT0;
			target.viewport = {{{0, impl().bloom_texture[2].width()}, {0, impl().bloom_texture[2].height()}}};

			target.bind();
			gl::ClearColor(0.f, 0.f, 0.f, 0.f);
			gl::Clear(gl::COLOR_BUFFER_BIT);

			impl().vblur_container.at(*vblur_params).invoke(impl().bloom_texture[1], target);
		}

		// Bind g-buffer textures

		gl::ActiveTexture(gl::TEXTURE0);
		impl().g_buffer_texture[0].bind();
		gl::ActiveTexture(gl::TEXTURE1);
		impl().g_buffer_texture[1].bind();
		gl::ActiveTexture(gl::TEXTURE2);
		impl().g_buffer_texture[2].bind();
		gl::ActiveTexture(gl::TEXTURE3);
		impl().g_buffer_texture[3].bind();

		gl::Disable(gl::DEPTH_TEST);
		gl::Disable(gl::BLEND);

		// Generate SSAO

		if (opts.ssao)
		{
			impl().ssao_framebuffer[0].bind();
			gl::Viewport(0, 0, impl().ssao_texture[0].width(), impl().ssao_texture[0].height());
			gl::ClearColor(0.f, 0.f, 0.f, 0.f);
			gl::Clear(gl::COLOR_BUFFER_BIT);

			impl().ssao_pass_program.bind();
			gl::ActiveTexture(gl::TEXTURE4);
			impl().ssao_rotation_texture.bind();
			impl().ssao_pass_program["u_ssao_rotation_step"] = math::cast<float>(impl().ssao_texture[0].size()) / (1.f * impl().ssao_rotation_texture.width());
			impl().ssao_pass_program["u_ssao_radius"] = opts.ssao->radius;
			impl().ssao_pass_program["u_transform"] = camera_transform;
			impl().ssao_pass_program["u_view"] = opts.camera->view();

			gl::DrawArrays(gl::TRIANGLES, 0, 6);
		}

		// Blur ssao horizontally

		if (opts.ssao)
		{
			render_target target;
			target.framebuffer = &impl().ssao_framebuffer[1];
			target.draw_buffer = gl::COLOR_ATTACHMENT0;
			target.viewport = {{{0, impl().ssao_texture[1].width()}, {0, impl().ssao_texture[1].height()}}};

			target.bind();
			gl::ClearColor(0.f, 0.f, 0.f, 0.f);
			gl::Clear(gl::COLOR_BUFFER_BIT);

			impl().ssao_hblur.invoke(impl().ssao_texture[0], target);
		}

		// Blur ssao vertically

		if (opts.ssao)
		{
			render_target target;
			target.framebuffer = &impl().ssao_framebuffer[2];
			target.draw_buffer = gl::COLOR_ATTACHMENT0;
			target.viewport = {{{0, impl().ssao_texture[2].width()}, {0, impl().ssao_texture[2].height()}}};

			target.bind();
			gl::ClearColor(0.f, 0.f, 0.f, 0.f);
			gl::Clear(gl::COLOR_BUFFER_BIT);

			impl().ssao_vblur.invoke(impl().ssao_texture[1], target);
		}

		// Setup destination framebuffer

		gl::ActiveTexture(gl::TEXTURE0);
		impl().g_buffer_texture[0].bind();

		target.bind();

		gl::Clear(gl::COLOR_BUFFER_BIT);

		impl().screen_mesh.bind();

		// Draw unlit & ambient layers

		impl().ambient_pass_program.bind();
		impl().ambient_pass_program["u_ambient"] = opts.ambient;
		impl().ambient_pass_program["u_max_intensity"] = opts.max_intensity;
		if (opts.ssao)
		{
			gl::ActiveTexture(gl::TEXTURE4);
			impl().ssao_texture[2].bind();
			impl().ambient_pass_program["u_use_ssao"] = 1;
		}
		else
		{
			impl().ambient_pass_program["u_use_ssao"] = 0;
		}
		gl::DrawArrays(gl::TRIANGLES, 0, 6);

		// Directional lights

		math::point<float, 2> bbox_hull_screen[8];
		auto lit_bbox_hull_size = bbox_to_screen_fan(camera_transform, lit_bbox, bbox_hull_screen);
		impl().screen_mesh.load(bbox_hull_screen, lit_bbox_hull_size, gl::TRIANGLE_FAN);

		gl::ActiveTexture(gl::TEXTURE4);
		impl().directional_shadow_texture.bind();

		for (auto const & l : opts.directional_lights)
		{
			std::vector<math::matrix<float, 4, 4>> light_transform;

			if (l.shadowed)
			{
				if (l.cascades == 0)
					throw util::exception("The number of shadow map cascades should be positive");
				if (l.cascades > 8)
					throw util::exception("More than 8 shadow map cascades are not supported");

				light_transform.resize(l.cascades);

				// Compute cascade split points

				auto dir = math::as_vector(camera_clip_planes[4]);
				std::vector<math::interval<float>> cascade_splits(l.cascades);
				{
					float offset;
					auto dir_length = math::length(dir);
					float near = camera_clip_planes[4][3] / dir_length;
					dir /= dir_length;
					offset = math::dot(camera_position - math::point<float, 3>::zero(), dir);
					near += offset;
					float far = -camera_clip_planes[5][3] / math::length(math::as_vector(camera_clip_planes[5])) - offset;

					if (l.cascade_ranges.empty())
					{
						float lambda = 1.f / 24.f;
						for (int i = 0; i < l.cascades; ++i)
						{
							cascade_splits[i].max = (1.f - lambda) * near * std::pow(far / near, i * 1.f / l.cascades) + lambda * (near + (far - near) * (i * 1.f / l.cascades));
							cascade_splits[i].min = (1.f - lambda) * near * std::pow(far / near, (i + 1) * 1.f / l.cascades) + lambda * (near + (far - near) * ((i + 1) * 1.f / l.cascades));
							cascade_splits[i] -= offset;
						}
					}
					else
					{
						for (int i = 0; i < l.cascades; ++i)
						{
							cascade_splits[i] = - l.cascade_ranges[i] - offset;
						}
					}
				}

				std::size_t dir_max_index = 0;
				if (std::abs(dir[1]) > std::abs(dir[dir_max_index])) dir_max_index = 1;
				if (std::abs(dir[2]) > std::abs(dir[dir_max_index])) dir_max_index = 2;

				if (impl().directional_shadow_texture.width() != l.shadow_map_size || impl().directional_shadow_texture.depth() != l.cascades)
				{
					impl().directional_shadow_texture.load<depth24_pixel>({l.shadow_map_size, l.shadow_map_size, l.cascades});
				}

				math::vector<float, 3> light_axes[3];

				light_axes[2] = -math::normalized(l.direction);
				light_axes[1] = {0.f, 0.f, 1.f};

				if (light_axes[2][2] > 0.5f)
					light_axes[1] = {0.f, 1.f, 0.f};

				math::gram_schmidt(light_axes[2], light_axes[1]);

				light_axes[0] = math::cross(light_axes[2], light_axes[1]);

				for (int cascade = 0; cascade < l.cascades; ++cascade)
				{
					math::box<float, 3> shadowed_bbox;

					math::matrix<float, 4, 4> cascade_transform = camera_transform;

					{
						float near = cascade_splits[cascade].max;
						float far = cascade_splits[cascade].min;

						math::vector<float, 2> b;
						b[0] = -2.f * cascade_transform[3][dir_max_index];
						b[1] = -2.f * cascade_transform[3][3];

						math::matrix<float, 2, 2> m;
						m[0][0] = - dir[dir_max_index];
						m[0][1] =   dir[dir_max_index];
						m[1][0] = -near;
						m[1][1] = far;

						math::gauss(m, b);

						for (std::size_t i = 0; i < 3; ++i)
							cascade_transform[2][i] = dir[i] * (b[0] + b[1]) / 2.f;
						cascade_transform[2][3] = (b[0] * near + b[1] * far) / 2.f;
					}

					cg::frustum cascade_frustum{cascade_transform};

					for (auto v : cg::vertices(cascade_frustum))
						shadowed_bbox |= v;

					shadowed_bbox &= casts_shadow_bbox;

					static auto const origin = math::point<float, 3>::zero();

					math::box<float, 3> light_bbox;

					for (auto const & v : math::vertices(shadowed_bbox))
					{
						for (std::size_t i = 0; i < 3; ++i)
							light_bbox[i] |= math::dot(light_axes[i], v - origin);
					}

					for (auto const & v : math::vertices(casts_shadow_bbox))
						light_bbox[2] |= math::dot(light_axes[2], v - origin);

					light_transform[cascade] = math::orthographic_camera{light_bbox}.projection() * math::homogeneous(math::by_rows(light_axes[0], light_axes[1], light_axes[2]));

					cg::frustum<float, 3> light_frustum{light_transform[cascade]};

					impl().directional_shadow_framebuffer.bind();
					gl::DrawBuffer(gl::NONE);

					impl().directional_shadow_framebuffer.depth(impl().directional_shadow_texture, cascade);
					impl().directional_shadow_framebuffer.assert_complete();

					gl::Viewport(0, 0, impl().directional_shadow_texture.width(), impl().directional_shadow_texture.height());

					gl::Clear(gl::DEPTH_BUFFER_BIT);

					gl::Enable(gl::DEPTH_TEST);
					gl::DepthFunc(gl::LEQUAL);

					gl::Enable(gl::CULL_FACE);
					gl::CullFace(gl::FRONT);

					gl::Disable(gl::BLEND);

					impl().shadow_builder_program.bind();
					impl().shadow_builder_program["u_light_transform"] = light_transform[cascade];

					auto & program = impl().shadow_builder_program;

					for (int bi = 0; bi < bins_total; ++bi)
					{
						auto const & b = bins.data()[bi];

						if (cg::separation(light_frustum, cg::box{b.bbox}).distance > 0.f) continue;

						for (auto const & p : b.buckets)
						{
							if (p.second.objects.empty()) continue;

							std::uint32_t mask = std::get<0>(p.first);

							if (!(mask & O_CASTS_SHADOW)) continue;

							program["u_flag_mask"] = mask;

							for (std::size_t i = 0; i < p.second.objects.size(); ++i)
							{
								std::size_t id = p.second.objects[i];

								auto const & o = objects[id];

								if (mask & O_PRE_TRANSFORM)
									program["u_pre_transform"] = *o.pre_transform;

								if (mask & O_POST_TRANSFORM)
									program["u_post_transform"] = *o.post_transform;

								o.mesh->draw();
							}
						}
					}
				}
			}

			target.bind();
			gl::Enable(gl::BLEND);
			gl::BlendFunc(gl::ONE, gl::ONE);

			gl::Disable(gl::DEPTH_TEST);
			gl::CullFace(gl::BACK);

			impl().directional_light_pass_program.bind();
			impl().directional_light_pass_program["u_camera_position"] = camera_position;
			impl().directional_light_pass_program["u_max_intensity"] = opts.max_intensity;

			impl().directional_light_pass_program["u_light_direction"] = math::normalized(l.direction);
			impl().directional_light_pass_program["u_light_color"] = l.color;
			impl().directional_light_pass_program["u_shadowed"] = l.shadowed;
			if (l.shadowed)
			{
				impl().directional_light_pass_program["u_cascades"] = static_cast<int>(l.cascades);
				for (int i = 0; i < l.cascades; ++i)
				{
					impl().directional_light_pass_program[util::to_string("u_light_transform[", i, "]").data()] = light_transform[i];
				}
			}
			impl().screen_mesh.draw();
		}

		// Point lights

		gl::Enable(gl::TEXTURE_CUBE_MAP_SEAMLESS);
		gl::ActiveTexture(gl::TEXTURE4);
		impl().point_shadow_texture.bind();

		float min_intensity = opts.min_intensity.value_or(opts.max_intensity / 256.f);

		for (auto const & l : opts.point_lights)
		{
			float I = std::max({l.color[0], l.color[1], l.color[2]});
			auto r = math::solve_quadratic(l.attenuation.c2, l.attenuation.c1, l.attenuation.c0 - I / min_intensity);

			float near = l.min_shadow_distance;
			math::vector<float, 3> far_positive;
			math::vector<float, 3> far_negative;

			for (std::size_t i = 0; i < 3; ++i)
			{
				far_positive[i] = casts_shadow_bbox[i].max - l.position[i];
				far_negative[i] = l.position[i] - casts_shadow_bbox[i].min;

				if (r)
				{
					far_positive[i] = std::min(far_positive[i], r->second);
					far_negative[i] = std::min(far_negative[i], r->second);
				}

				far_positive[i] = std::max(far_positive[i], near);
				far_negative[i] = std::max(far_negative[i], near);
			}

			if (l.shadowed)
			{
				math::matrix<float, 4, 4> transform[6];

				// +X
				{
					float far = far_positive[0];
					transform[0] = math::matrix<float, 4, 4>::zero();

					transform[0][0][2] = -1.f;
					transform[0][1][1] = -1.f;

					transform[0][2][0] = (far + near) / (far - near);
					transform[0][2][3] = - 2.f * far * near / (far - near);
					transform[0][3][0] = 1.f;
				}

				// -X
				{
					float far = far_negative[0];
					transform[1] = math::matrix<float, 4, 4>::zero();

					transform[1][0][2] = 1.f;
					transform[1][1][1] = -1.f;

					transform[1][2][0] = - (far + near) / (far - near);
					transform[1][2][3] = - 2.f * far * near / (far - near);
					transform[1][3][0] = - 1.f;
				}

				// +Y
				{
					float far = far_positive[1];
					transform[2] = math::matrix<float, 4, 4>::zero();

					transform[2][0][0] = 1.f;
					transform[2][1][2] = 1.f;

					transform[2][2][1] = (far + near) / (far - near);
					transform[2][2][3] = - 2.f * far * near / (far - near);
					transform[2][3][1] = 1.f;
				}

				// -Y
				{
					float far = far_negative[1];
					transform[3] = math::matrix<float, 4, 4>::zero();

					transform[3][0][0] = 1.f;
					transform[3][1][2] = -1.f;

					transform[3][2][1] = - (far + near) / (far - near);
					transform[3][2][3] = - 2.f * far * near / (far - near);
					transform[3][3][1] = - 1.f;
				}

				// +Z
				{
					float far = far_positive[2];
					transform[4] = math::matrix<float, 4, 4>::zero();

					transform[4][0][0] = 1.f;
					transform[4][1][1] = -1.f;

					transform[4][2][2] = (far + near) / (far - near);
					transform[4][2][3] = - 2.f * far * near / (far - near);
					transform[4][3][2] = 1.f;
				}

				// -Z
				{
					float far = far_negative[2];
					transform[5] = math::matrix<float, 4, 4>::zero();

					transform[5][0][0] = -1.f;
					transform[5][1][1] = -1.f;

					transform[5][2][2] = - (far + near) / (far - near);
					transform[5][2][3] = - 2.f * far * near / (far - near);
					transform[5][3][2] = - 1.f;
				}

				math::matrix<float, 4, 4> const translate_by_light = math::translation<float, 3>(math::point<float, 3>::zero() - l.position).homogeneous_matrix();

				impl().point_shadow_framebuffer.bind();

				if (impl().point_shadow_texture.width() != l.shadow_map_size)
				{
					for (int f = 0; f < 6; ++f)
						impl().point_shadow_texture.load<depth24_pixel>(f, {l.shadow_map_size, l.shadow_map_size});
					impl().point_shadow_framebuffer.depth(impl().point_shadow_texture);
					impl().point_shadow_framebuffer.assert_complete();
				}
				gl::DrawBuffer(gl::NONE);
				gl::Viewport(0, 0, impl().point_shadow_texture.width(), impl().point_shadow_texture.height());

				gl::Clear(gl::DEPTH_BUFFER_BIT);

				gl::Enable(gl::DEPTH_TEST);
				gl::DepthFunc(gl::LEQUAL);

				gl::Enable(gl::CULL_FACE);
				gl::CullFace(gl::FRONT);

				gl::Disable(gl::BLEND);

				impl().cubemap_shadow_builder_program.bind();
				impl().cubemap_shadow_builder_program["u_light_transform"] = math::matrix<float, 4, 4>::identity();

				impl().cubemap_shadow_builder_program["u_layer_transform[0]"] = transform[0] * translate_by_light;
				impl().cubemap_shadow_builder_program["u_layer_transform[1]"] = transform[1] * translate_by_light;
				impl().cubemap_shadow_builder_program["u_layer_transform[2]"] = transform[2] * translate_by_light;
				impl().cubemap_shadow_builder_program["u_layer_transform[3]"] = transform[3] * translate_by_light;
				impl().cubemap_shadow_builder_program["u_layer_transform[4]"] = transform[4] * translate_by_light;
				impl().cubemap_shadow_builder_program["u_layer_transform[5]"] = transform[5] * translate_by_light;

				render_all(impl().cubemap_shadow_builder_program, [](auto mask){ return (mask & O_CASTS_SHADOW); });
			}

			target.bind();
			gl::Enable(gl::BLEND);
			gl::BlendFunc(gl::ONE, gl::ONE);

			gl::Disable(gl::DEPTH_TEST);
			gl::CullFace(gl::BACK);

			impl().point_light_pass_program.bind();
			impl().point_light_pass_program["u_camera_position"] = camera_position;
			impl().point_light_pass_program["u_max_intensity"] = opts.max_intensity;
			impl().point_light_pass_program["u_shadowed"] = l.shadowed;

			if (l.shadowed)
			{
				impl().point_light_pass_program["u_shadow_near"] = near;
				impl().point_light_pass_program["u_shadow_far_positive"] = far_positive;
				impl().point_light_pass_program["u_shadow_far_negative"] = far_negative;
			}

			math::box<float, 3> light_bbox;
			if (r)
			{
				float light_influence_radius = r->second;

				for (std::size_t i = 0; i < 3; ++i)
					light_bbox[i] = {l.position[i] - light_influence_radius, l.position[i] + light_influence_radius};

				light_bbox &= lit_bbox;
			}
			else
			{
				light_bbox = lit_bbox;
			}

			auto light_bbox_hull_size = bbox_to_screen_fan(camera_transform, light_bbox, bbox_hull_screen);
			impl().screen_mesh.load(bbox_hull_screen, light_bbox_hull_size, gl::TRIANGLE_FAN);

			impl().point_light_pass_program["u_light_position"] = l.position;
			impl().point_light_pass_program["u_light_color"] = l.color;
			impl().point_light_pass_program["u_light_attenuation"] = math::vector{l.attenuation.c0, l.attenuation.c1, l.attenuation.c2};
			impl().screen_mesh.draw();
		}

		// Overlay bloom

		if (opts.bloom)
		{
			gl::Enable(gl::BLEND);
			gl::BlendFunc(gl::SRC_ALPHA, gl::ONE_MINUS_SRC_ALPHA);
			impl().bloom_overlay.invoke(impl().bloom_texture[2], target);
		}

		impl().prev_transform = camera_transform;

		check_error();
	}

	texture_2d const & deferred_renderer::depth() const
	{
		return impl().g_buffer_depth;
	}

}

#endif
