#include "Engine.h"
#include "../Framework3/CPU.h"
#include "../Framework3/Math/Scissor.h"
#include "../Framework3/Math/Noise.h"

#define EXPLOSION_TIME 0.7f

// Vertex attrib names
const char *attribs[] = { NULL, "textureCoord", "tangent", "binormal", "normal" };
const char *gunAttribs[] = { NULL, "textureCoord", "normal" };

struct Rocket {
	Light *light;
	vec3 dir;
	float explosionTime;
	TextureID texture;

	SoundSourceID soundSource;
};

struct Material {
	TextureID base;
	TextureID bump;
	TextureID horizon;
	TextureID emissive;
	float parallax;
	float gloss;
	uint flags;
};

bool isInCube(const vec3 &pos, const vec3 &minLim, const vec3 &maxLim){
	return (pos.x > minLim.x && pos.x < maxLim.x &&
			pos.y > minLim.y && pos.y < maxLim.y &&
			pos.z > minLim.z && pos.z < maxLim.z);
}

struct Cube {
	Cube(){
		visibleCubes = NULL;
		cubeVisibility = NULL;
	}

	void drawLineBox(){
		glBegin(GL_LINE_LOOP);
			glVertex3f(minLim.x, maxLim.y, minLim.z);
			glVertex3f(minLim.x, maxLim.y, maxLim.z);
			glVertex3f(maxLim.x, maxLim.y, maxLim.z);
			glVertex3f(maxLim.x, maxLim.y, minLim.z);
			glVertex3f(maxLim.x, minLim.y, minLim.z);
			glVertex3f(maxLim.x, minLim.y, maxLim.z);
			glVertex3f(minLim.x, minLim.y, maxLim.z);
			glVertex3f(minLim.x, minLim.y, minLim.z);
		glEnd();
		glBegin(GL_LINES);
			glVertex3f(minLim.x, minLim.y, maxLim.z);
			glVertex3f(minLim.x, maxLim.y, maxLim.z);
			glVertex3f(maxLim.x, minLim.y, maxLim.z);
			glVertex3f(maxLim.x, maxLim.y, maxLim.z);
			glVertex3f(minLim.x, minLim.y, minLim.z);
			glVertex3f(maxLim.x, minLim.y, minLim.z);
			glVertex3f(minLim.x, maxLim.y, minLim.z);
			glVertex3f(maxLim.x, maxLim.y, minLim.z);
		glEnd();
	}

	void drawBox(){
		glBegin(GL_TRIANGLE_STRIP);
			glVertex3f(minLim.x, maxLim.y, maxLim.z);
			glVertex3f(maxLim.x, maxLim.y, maxLim.z);
			glVertex3f(minLim.x, maxLim.y, minLim.z);
			glVertex3f(maxLim.x, maxLim.y, minLim.z);
			glVertex3f(minLim.x, minLim.y, minLim.z);
			glVertex3f(maxLim.x, minLim.y, minLim.z);
			glVertex3f(minLim.x, minLim.y, maxLim.z);
			glVertex3f(maxLim.x, minLim.y, maxLim.z);
		glEnd();
		glBegin(GL_TRIANGLE_STRIP);
			glVertex3f(maxLim.x, minLim.y, minLim.z);
			glVertex3f(maxLim.x, maxLim.y, minLim.z);
			glVertex3f(maxLim.x, minLim.y, maxLim.z);
			glVertex3f(maxLim.x, maxLim.y, maxLim.z);
			glVertex3f(minLim.x, minLim.y, maxLim.z);
			glVertex3f(minLim.x, maxLim.y, maxLim.z);
			glVertex3f(minLim.x, minLim.y, minLim.z);
			glVertex3f(minLim.x, maxLim.y, minLim.z);
		glEnd();
	}


	bool intersectSphere(const vec3 &pos, const float radius) const {
		float d = 0;
		for (int i = 0; i < 3; i++){
			if (pos[i] < minLim[i]){
				float s = pos[i] - minLim[i];
				d += s * s;
			} else if (pos[i] > maxLim[i]){
				float s = pos[i] - maxLim[i];
				d += s * s;
			}
		}
		return (d < radius * radius);
	}

	vec3 minLim, maxLim;
	float maxDepth;

	uint *visibleCubes;
	uint nVisibleCubes;
	bool *cubeVisibility;

	bool toDraw[2];
	bool hasBeenVisited[4];

	vec3 *positions;
	uint posCount;
};

void Light::setupClipPlanes(){
	if (fov > 0){
		mat4 proj = perspectiveMatrixX(fov, 1, 1, 1.0f, lightRadius);
		mat4 mv = rotateXY(-wx, -wy) * translate(-position);

		mat4 mvp = proj * mv;


		Frustum frustum;
		frustum.loadFrustum(mvp);

		for (uint i = 0; i < 6; i++){
			Plane p = frustum.getPlane(i);
			double plane[] = { p.normal.x, p.normal.y, p.normal.z, p.offset };

			glClipPlane(GL_CLIP_PLANE0 + i, plane);
		}


	} else {
		// Set up a bounding box around the light with six clip planes
		double plane0[] = {  1,  0,  0, lightRadius - position.x };
		double plane1[] = { -1,  0,  0, lightRadius + position.x };
		double plane2[] = {  0,  1,  0, lightRadius - position.y };
		double plane3[] = {  0, -1,  0, lightRadius + position.y };
		double plane4[] = {  0,  0,  1, lightRadius - position.z };
		double plane5[] = {  0,  0, -1, lightRadius + position.z };

		glClipPlane(GL_CLIP_PLANE0, plane0);
		glClipPlane(GL_CLIP_PLANE1, plane1);
		glClipPlane(GL_CLIP_PLANE2, plane2);
		glClipPlane(GL_CLIP_PLANE3, plane3);
		glClipPlane(GL_CLIP_PLANE4, plane4);
		glClipPlane(GL_CLIP_PLANE5, plane5);
	}
}


Engine::Engine(){
	initNoise();

	materials = NULL;
	cubes = NULL;
	sizeX = sizeY = sizeZ = 0;
	global = NULL;
	rocketLauncher = NULL;

	fov = 1.78f;
	useClipPlanes = true;
	useScissor    = true;
	useStencilBranching = true;
	useSpecular   = true;
	useParallax   = true;
	useShadow     = true;
	useSelfShadow = true;
	useVolumetric = true;

	needsSave = false;

	indexOffsets = NULL;
	indexCounts = NULL;

	audio = new Audio();
	fireSound = audio->addSound("../Sounds/fire.wav");
	boomSound = audio->addSound("../Sounds/explosion.wav");

	boingSound = audio->addSound("../Sounds/boing.wav");
	boingSource = audio->addSoundSource(boingSound);
	audio->setSourceAttenuation(boingSource, 0.06f, 3.0f);

	shootSound = audio->addSound("../Sounds/Shotgun1.wav");
	shootSource = audio->addSoundSource(shootSound, RELATIVEPOS);
	audio->setSourceAttenuation(shootSource, 0, 0);

	music = audio->addSound("../Sounds/Music/Humus.ogg");
	musicSource = audio->addSoundSource(music, RELATIVEPOS | LOOPING);
	audio->setSourceAttenuation(musicSource, 0, 0);
}

Engine::~Engine(){
	int nCubes = sizeX * sizeY * sizeZ;

	delete indexOffsets;
	delete indexCounts;

	delete global;

	for (int i = 0; i < nCubes; i++){
		delete cubes[i].visibleCubes;
		delete cubes[i].cubeVisibility;
	}

	if (particleSystems.goToFirst()){
		do {
			delete particleSystems.getCurrent();
		} while (particleSystems.goToNext());
	}

	if (rockets.goToFirst()){
		do {
			delete rockets.getCurrent();
		} while (rockets.goToNext());
	}

	if (lights.goToFirst()){
		do {
			delete lights.getCurrent();
		} while (lights.goToNext());
	}

	delete cubes;
	delete materials;
	delete rocketLauncher;

	audio->stop(musicSource);
	for (uint i = 0; i < ambientSounds.getCount(); i++){
		audio->stop(ambientSounds[i].source);
	}

	audio->stop(boingSource);
	audio->stop(shootSource);
	delete audio;
}

bool Engine::collision(const vec3 &p0, const vec3 &p1, vec3 *point, vec3 *normal, float *offset) const {
	const BTri *tri;
	if (bsp.intersects(p0, p1, point, &tri)){
//		if (normal) *normal = tri->normal;
//		if (offset) *offset = tri->offset;
		if (normal) *normal = tri->plane.xyz();
		if (offset) *offset = tri->plane.w;
		return true;
	}
	return false;
}

bool Engine::push(vec3 &pos, const float radius) const {
	return bsp.pushSphere(pos, radius);
}

bool Engine::isInOpenSpace(const vec3 &pos) const {
	return bsp.isInOpenSpace(pos);
}

void Engine::addRocket(const vec3 &camPos, const vec3 &dir){
	Light *light = new Light(camPos, 400, vec3(1, 0.5f, 0), 0, 0, 0);
	lights.addLast(light);
	Rocket *rocket = new Rocket;
	rocket->light = light;
	rocket->dir = dir;
	rocket->explosionTime = -10;
	rocket->texture = explosion[0];
//	rocket->texture = explosion[rand() % EXPLOSION_TEXTURES];

	rocket->soundSource = SOUND_NONE;

	rockets.addLast(rocket);

	audio->play(shootSource);
}

void Engine::updateRockets(const float frameTime, vec3 &camPos, vec3 &camDir, const float soundGain){
	if (rockets.goToFirst()){
		do {
			Rocket *rocket = rockets.getCurrent();
			rocket->explosionTime += frameTime;

			if (rocket->explosionTime < 0){
				vec3 newPos = rocket->light->position + 1300 * frameTime * rocket->dir;
				vec3 cPoint;
				if (collision(rocket->light->position, newPos, &cPoint)){

					vec3 d = camPos - cPoint;
					vec3 dr = normalize(d) * (2 * saturate(1.0f - 0.00003f * dot(d, d)));
					camDir += dr;
					camPos += 20 * dr;

					rocket->light->position = cPoint + 100 * normalize(d);
					rocket->explosionTime = 0;
					rocket->light->color = vec4(1, 0.4f, 0, 1);

					rocket->soundSource = audio->addSoundSource(boomSound);
					audio->setSourcePosition(rocket->soundSource, rocket->light->position);
					audio->setSourceAttenuation(rocket->soundSource, 0.01f, 5.0f);
					audio->setSourceGain(rocket->soundSource, soundGain);
					audio->play(rocket->soundSource);
				} else {
					rocket->light->position = newPos;
				}
			} else {

				if (rocket->explosionTime < EXPLOSION_TIME){
					float t = rocket->explosionTime / EXPLOSION_TIME;
					float r = 4 * t * (1 - t);
					rocket->light->lightRadius = 800 * r * r + 400 * (1 - t);

				} else {
					lights.goToObject(rocket->light);
					delete lights.getCurrent();
					lights.removeCurrent();

					if (rocket->soundSource != SOUND_NONE){
						audio->stop(rocket->soundSource);
						audio->deleteSoundSource(rocket->soundSource);
					}

					delete rocket;
					rockets.removeCurrent();
				}
			}
		} while (rockets.goToNext());
	}
}


bool Engine::loadGeometry(const char *srcModel, const float cSize, bool useCache){
	Model *model = new Model();
	if (!model->loadT3d(srcModel, true, true, true, 512)) return false;
	model->reverseWinding();
	model->flipNormals();

	StreamID vertexStream = model->findStream(TYPE_VERTEX);



	// TODO: Fix!!!
	model->scale(vertexStream, vec3(0.5f));



	model->getBoundingBox(vertexStream, minLim, maxLim);

	if (!useCache || !bsp.loadFile("World.bsp")){
		vec3 *vertices = (vec3 *) model->getStream(vertexStream).vertices;
		uint *indices = model->getStream(vertexStream).indices;
		for (uint i = 0; i < model->getIndexCount(); i += 3){
			bsp.addTriangle(vertices[indices[i]], vertices[indices[i + 1]], vertices[indices[i + 2]]);
		}


		//int64 start = getCycleNumber();

		bsp.build(1, 1);

		//int64 end = getCycleNumber();
		//char str[256];
		//sprintf(str, "%.3f", ((double) (end - start) / (double) (int64) getHz()));
		//InfoMsg(str);

		needsSave = true;
	}

	nMaterials = model->getBatchCount();
	materials = new Material[nMaterials];
	for (uint i = 0; i < nMaterials; i++){
		materials[i].base = TEXTURE_NONE;
		materials[i].bump = TEXTURE_NONE;
		materials[i].horizon = TEXTURE_NONE;
		materials[i].emissive = TEXTURE_NONE;
		materials[i].parallax = 0;
		materials[i].gloss = 0.5f;
	}

	global = new Model();

	if (useCache){
		if (!global->load("Global.hmdl")){
			useCache = false;
			needsSave = true;
		}
	}

	if (!useCache){

		//model->cleanUp();

		// Fix tangent space ...
/*
		model->removeStream(model->findStream(TYPE_NORMAL));
		model->removeStream(model->findStream(TYPE_GENERIC, 1));
		model->removeStream(model->findStream(TYPE_GENERIC, 0));
*/
		model->removeStream(model->findStream(TYPE_TANGENT));
		model->removeStream(model->findStream(TYPE_BINORMAL));
		model->removeStream(model->findStream(TYPE_NORMAL));

		model->computeTangentSpace(true);
	}

	cubeSize = cSize;
	float cx = ceilf((maxLim.x - minLim.x) / cubeSize);
	float cy = ceilf((maxLim.y - minLim.y) / cubeSize);
	float cz = ceilf((maxLim.z - minLim.z) / cubeSize);

	vec3 mid = 0.5f * (minLim + maxLim);
	minLim = mid - 0.5f * cubeSize * vec3(cx, cy, cz);
	maxLim = mid + 0.5f * cubeSize * vec3(cx, cy, cz);

	sizeX = (int) cx;
	sizeY = (int) cy;
	sizeZ = (int) cz;
	uint nCubes = sizeX * sizeY * sizeZ;
	cubes = new Cube[nCubes];

	Cube *dest = cubes;
	if (useCache){

		for (int z = 0; z < sizeZ; z++){
			float fz0 = minLim.z + cubeSize * z;
			float fz1 = minLim.z + cubeSize * (z + 1);
			for (int y = 0; y < sizeY; y++){
				float fy0 = minLim.y + cubeSize * y;
				float fy1 = minLim.y + cubeSize * (y + 1);
				for (int x = 0; x < sizeX; x++){
					float fx0 = minLim.x + cubeSize * x;
					float fx1 = minLim.x + cubeSize * (x + 1);
					dest->minLim = vec3(fx0, fy0, fz0);
					dest->maxLim = vec3(fx1, fy1, fz1);
					dest++;
				}
			}
		}

	} else {
		for (int z = 0; z < sizeZ; z++){
			float fz0 = minLim.z + cubeSize * z;
			float fz1 = minLim.z + cubeSize * (z + 1);

			Model *frontZ = new Model();
			Model *backZ = new Model();
			model->split(vec3(0, 0, -1), fz1, frontZ, backZ);
			delete model;

			for (int y = 0; y < sizeY; y++){
				float fy0 = minLim.y + cubeSize * y;
				float fy1 = minLim.y + cubeSize * (y + 1);

				Model *frontY = new Model();
				Model *backY = new Model();
				frontZ->split(vec3(0, -1, 0), fy1, frontY, backY);
				delete frontZ;

				for (int x = 0; x < sizeX; x++){
					float fx0 = minLim.x + cubeSize * x;
					float fx1 = minLim.x + cubeSize * (x + 1);

					Model *frontX = new Model();
					Model *backX = new Model();
					frontY->split(vec3(-1, 0, 0), fx1, frontX, backX);
					delete frontY;

					dest->minLim = vec3(fx0, fy0, fz0);
					dest->maxLim = vec3(fx1, fy1, fz1);

					global->merge(frontX);
					dest++;

					delete frontX;
					frontY = backX;
				}

				delete frontY;
				frontZ = backY;
			}

			delete frontZ;
			model = backZ;
		}
	}
	delete model;

	global->changeAllGeneric(true);


	for (uint d = 0; d < nCubes; d++){
		cubes[d].cubeVisibility = new bool[nCubes];
		memset(cubes[d].cubeVisibility, 0, sizeof(bool) * nCubes);
		cubes[d].cubeVisibility[d] = true;
	}



	rocketLauncher = new Model();
	if (!rocketLauncher->loadObj("../Models/RocketLauncher/Gun.obj")) return false;

	vec3 min, max;
	rocketLauncher->getBoundingBox(0, min, max);
	rocketLauncher->translate(0, -0.5f * (min + max));
	rocketLauncher->flipComponents(0, 1, 2);
	rocketLauncher->flipComponents(2, 1, 2);
	rocketLauncher->reverseWinding();

	rocketLauncher->changeAllGeneric(true);


	indexOffsets = new intptr[nCubes * nMaterials];
	indexCounts = new GLint[nCubes * nMaterials];

	return true;
}

void Engine::save() const {
	if (needsSave){
		FILE *file = fopen("Visibility.vis", "wb");
		if (file){
			uint32 nCubes = sizeX * sizeY * sizeZ;
			fwrite(&nCubes, 4, 1, file);
			for (uint i = 0; i < nCubes; i++){
				fwrite(cubes[i].cubeVisibility, sizeof(bool), nCubes, file);
			}
			fclose(file);
		}

		global->save("Global.hmdl");
		bsp.saveFile("World.bsp");
	}
}

struct CubeDist {
	uint index;
	int lenSqr;
};

int compCD(const CubeDist &elem0, const CubeDist &elem1){
	return elem0.lenSqr - elem1.lenSqr;
}

void Engine::precomputeCube(const int tx, const int ty, const int tz){
	uint dstCube = (tz * sizeY + ty) * sizeX + tx;

	Array <CubeDist> cubeDists;
	float maxDepth = 50;
	uint srcCube = 0;
	for (int z = 0; z < sizeZ; z++){
		for (int y = 0; y < sizeY; y++){
			for (int x = 0; x < sizeX; x++){
				if (cubes[dstCube].cubeVisibility[srcCube]){
					float s = 0;
					float dx = (tx < x)?
						cubes[dstCube].minLim.x - cubes[srcCube].maxLim.x :
						cubes[dstCube].maxLim.x - cubes[srcCube].minLim.x;
					s += dx * dx;

					float dy = (ty < y)?
						cubes[dstCube].minLim.y - cubes[srcCube].maxLim.y :
						cubes[dstCube].maxLim.y - cubes[srcCube].minLim.y;
					s += dy * dy;

					float dz = (tz < z)?
						cubes[dstCube].minLim.z - cubes[srcCube].maxLim.z :
						cubes[dstCube].maxLim.z - cubes[srcCube].minLim.z;
					s += dz * dz;

					if (s > maxDepth * maxDepth) maxDepth = sqrtf(s);

					CubeDist cd;
					cd.index = srcCube;
					int idx = (tx - x);
					int idy = (ty - y);
					int idz = (tz - z);
					cd.lenSqr = idx * idx + idy * idy + idz * idz;
					cubeDists.add(cd);
				}
				srcCube++;
			}
		}
	}

	cubeDists.sort(compCD);
	cubes[dstCube].maxDepth = maxDepth;
	cubes[dstCube].nVisibleCubes = cubeDists.getCount();
	cubes[dstCube].visibleCubes = (uint *) realloc(cubes[dstCube].visibleCubes, cubeDists.getCount() * sizeof(uint));
	for (uint i = 0; i < cubeDists.getCount(); i++){
		cubes[dstCube].visibleCubes[i] = cubeDists[i].index;
	}
}

int Engine::getCube(const vec3 &pos) const {
	int x, y, z;
	if (getCubeCoords(pos, x, y, z)){
		return ((z * sizeY + y) * sizeX) + x;
	} else {
		return -1;
	}
}

bool Engine::getCubeCoords(const vec3 &pos, int &x, int &y, int &z) const {
	if (isInCube(pos, minLim, maxLim)){
		x = (int) ((pos.x - minLim.x) / cubeSize);
		y = (int) ((pos.y - minLim.y) / cubeSize);
		z = (int) ((pos.z - minLim.z) / cubeSize);
		return true;
	}
	return false;
}

TextureID Engine::prerenderShadowMap(Renderer *renderer, const Light *light, const int size) const {
	renderer->changeRenderTarget(spotShadowMapColorRT, shadowMapDepthRT);

		renderer->reset();
		renderer->setShader(depth);
		renderer->setShaderConstant3f("lightPos", light->position);
		renderer->setShaderConstant1f("invRadius", 1.0f / light->lightRadius);
		renderer->apply();

		Image img;
		if (light->fov > 0){
			mat4 proj = perspectiveMatrixX(light->fov, 1, 1, 1.0f, light->lightRadius);

			ubyte *dest = img.create(FORMAT_RGBA8, size, size, 1, 1);

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


			mat4 mv = rotateXY(-light->wx, -light->wy) * translate(-light->position);
			mat4 mvp = proj * mv;
			renderer->setShaderConstant4x4f("mvp", mvp);
			renderer->applyConstants();

			global->draw(renderer);

			glReadPixels(0, 0, size, size, GL_RGBA, GL_UNSIGNED_BYTE, dest);
			//glReadPixels(0, 0, size, size, GL_RGBA, GL_UNSIGNED_SHORT, dest);


		} else {
			//mat4 proj = cubeProjectionMatrix(1, light->lightRadius) * scale(1, -1, 1);
			mat4 proj = cubeProjectionMatrixGL(1, light->lightRadius);

			ubyte *dest = img.create(FORMAT_RGBA8, size, size, 0, 1);
			int bytesPerFace = img.getMipMappedSize(0, 1) / 6;

			for (int face = 0; face < 6; face++){
				glClearColor(1, 1, 1, 1);
				glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
				mat4 mv = cubeViewMatrix(face);
				mv.translate(-light->position);

				mat4 mvp = proj * mv;
				renderer->setShaderConstant4x4f("mvp", mvp);
				renderer->applyConstants();

				global->draw(renderer);

				glReadPixels(0, 0, size, size, GL_RGBA, GL_UNSIGNED_BYTE, dest);
				//glReadPixels(0, 0, size, size, GL_RGBA, GL_UNSIGNED_SHORT, dest);

				dest += bytesPerFace;
			}
		}

		img.toGrayScale();

	renderer->changeToMainFramebuffer();

	return renderer->addTexture(img, linearClamp);
}

void Engine::updateShadowMap(Renderer *renderer, Light *light, const RasterizerStateID cullBack){
	Cube *lightCube = cubes + getCube(light->position);

//	mat4 proj = cubeProjectionMatrix(1, light->lightRadius) * scale(1, -1, 1);
	mat4 proj = cubeProjectionMatrixGL(1, light->lightRadius);

	for (int face = 0; face < 6; face++){
		mat4 mv = cubeViewMatrix(face);
		mv.translate(-light->position);

		mat4 mvp = proj * mv;

		Frustum frustum;
		frustum.loadFrustum(mvp);

		bool needDraw = false;
		for (uint i = 0; i < lightCube->nVisibleCubes; i++){
			uint cIndex = lightCube->visibleCubes[i];
			Cube *c = cubes + cIndex;
			c->toDraw[0] = false;

			if (c->intersectSphere(light->position, light->lightRadius)){
				// If the cube is outside the frustum, it need not be drawn
				if (frustum.cubeInFrustum(c->minLim.x, c->maxLim.x, c->minLim.y, c->maxLim.y, c->minLim.z, c->maxLim.z)){
					c->toDraw[0] = true;
					needDraw  = true;
				}
			}
		}
		if (!needDraw) continue;

		renderer->changeRenderTarget(omniShadowMapColorRT, shadowMapDepthRT, face);

		glClearColor(1, 1, 1, 1);
		glClear(/*GL_COLOR_BUFFER_BIT | */GL_DEPTH_BUFFER_BIT);


		renderer->reset();
		global->setBuffers(renderer);
		renderer->setShader(depth);
		renderer->setRasterizerState(cullBack);
		renderer->setShaderConstant3f("lightPos", light->position);
		renderer->setShaderConstant1f("invRadius", 1.0f / light->lightRadius);
		renderer->setShaderConstant4x4f("mvp", mvp);
		renderer->apply();

		drawSelected(renderer, lightCube);
	}

	renderer->changeToMainFramebuffer();
}

TextureID createAngleMap(Renderer *renderer, const SamplerStateID linearClamp, const int size){
	unsigned char *destPos, *destNeg, *pixels = new unsigned char[6 * size * size * 2];
	int x, y, z;

	float mid = 0.5f * (size - 1);

	destPos = pixels;
	destNeg = pixels + size * size * 2;
	for (y = 0; y < size; y++){
		for (z = 0; z < size; z++){
			float vy = mid - y;
			float vz = mid - z;

			float ang0 = atan2f(vy, mid) / PI;
			float ang1 = dot(normalize(vec3(mid, vz, vy)), normalize(vec3(mid, 0, vy)));
			ang1 = sqrtf(1 - ang1 * ang1);

			*destPos++ = (unsigned char) (255.0f * fabsf(ang0));
			*destPos++ = (unsigned char) (255.0f * ang1 * (vz > 0));
			*destNeg++ = (unsigned char) (255.0f * fabsf(ang0 - sign(vy)));
			*destNeg++ = (unsigned char) (255.0f * ang1 * (vz < 0));
		}
	}

	destPos = pixels + 2 * size * size * 2;
	destNeg = pixels + 3 * size * size * 2;
	for (z = 0; z < size; z++){
		for (x = 0; x < size; x++){
			float vx = x - mid;
			float vz = z - mid;

			float ang0 = atan2f(mid, vx) / PI;
			float ang1 = dot(normalize(vec3(vx, vz, mid)), normalize(vec3(vx, 0, mid)));
			ang1 = sqrtf(1 - ang1 * ang1);

			*destPos++ = (unsigned char) (255.0f * ang0);
			*destPos++ = (unsigned char) (255.0f * ang1 * (vz > 0));
			*destNeg++ = (unsigned char) (255.0f * ang0);
			*destNeg++ = (unsigned char) (255.0f * ang1 * (vz < 0));
		}
	}

	destPos = pixels + 4 * size * size * 2;
	destNeg = pixels + 5 * size * size * 2;
	for (y = 0; y < size; y++){
		for (x = 0; x < size; x++){
			float vx = x - mid;
			float vy = mid - y;

			float ang0 = atan2f(vy, vx) / PI;
			float ang1 = dot(normalize(vec3(vx, mid, vy)), normalize(vec3(vx, 0, vy)));
			ang1 = sqrtf(1 - ang1 * ang1);

			*destPos++ = (unsigned char) (255.0f * fabsf(ang0));
			*destPos++ = (unsigned char) (255.0f * ang1);
			*destNeg++ = (unsigned char) (255.0f * fabsf(sign(vy) - ang0));
			*destNeg++ = 0;
		}
	}

	Image img;
	img.loadFromMemory(pixels, FORMAT_IA8, size, size, 0, 1, true);

	return renderer->addTexture(img, false, linearClamp);
}

bool Engine::makeDrawable(Renderer *renderer, MaterialDesc *matDesc, const uint nMats, const RasterizerStateID cullBackState, const DepthStateID noDepthTestState, const DepthStateID noDepthWriteState, const SamplerStateID linearClampState){
	cullBack = cullBackState;
	noDepthTest = noDepthTestState;
	noDepthWrite = noDepthWriteState;
	linearClamp = linearClampState;

	
	const char *vendor = (const char *) glGetString(GL_VENDOR);

	int fboDepthBits = (strstr(vendor, "NVIDIA") == NULL)? 16 : 24;

	int dynamicShadowMapSize = 256;
	int staticShadowMapSize  = 512;

	if ((omniShadowMapColorRT = renderer->addRenderTarget(dynamicShadowMapSize, dynamicShadowMapSize, FORMAT_RGBA8, linearClamp, CUBEMAP)) == TEXTURE_NONE) return false;
	if ((spotShadowMapColorRT = renderer->addRenderTarget(staticShadowMapSize,  staticShadowMapSize,  FORMAT_RGBA8, linearClamp)) == TEXTURE_NONE) return false;
	if ((shadowMapDepthRT     = renderer->addRenderDepth (staticShadowMapSize,  staticShadowMapSize,  fboDepthBits)) == TEXTURE_NONE) return false;

	if (!global->makeDrawable(renderer)) return false;
	if (!rocketLauncher->makeDrawable(renderer)) return false;

#ifdef USE_OCC_QUERY_WORKAROUND
	useOccWorkaround = (strstr(vendor, "ATI Technologies") != NULL);
#endif


	memset(ambientEmissive, TEXTURE_NONE, sizeof(ambientEmissive));
	memset(litShader,       TEXTURE_NONE, sizeof(litShader));
	memset(lightingShader,  TEXTURE_NONE, sizeof(lightingShader));
	memset(weaponShader,    TEXTURE_NONE, sizeof(weaponShader));

	if ((depth = renderer->addShader("depth.shd")) == SHADER_NONE) return false;
	if ((volumetric = renderer->addShader("volumetric.shd")) == SHADER_NONE) return false;
	if ((weaponVolumetric = renderer->addShader("weaponVolumetric.shd")) == SHADER_NONE) return false;
	if ((skyBoxShader = renderer->addShader("skybox.shd")) == SHADER_NONE) return false;
	if ((particleShader = renderer->addShader("particleSystem.shd")) == SHADER_NONE) return false;


	if (lights.goToFirst()){
		do {
			Light *light = lights.getCurrent();
			if ((light->flags & NOSHADOWS) == 0){
				light->shadowMap = prerenderShadowMap(renderer, light, staticShadowMapSize);
			}
		} while (lights.goToNext());
	}

	((OpenGLRenderer *) renderer)->setRenderTargetSize(spotShadowMapColorRT, dynamicShadowMapSize, dynamicShadowMapSize);
	((OpenGLRenderer *) renderer)->setRenderTargetSize(shadowMapDepthRT, dynamicShadowMapSize, dynamicShadowMapSize);



	uint maxMat = min(nMaterials, nMats);

	TextureID defaultBase = TEXTURE_NONE, defaultBump = TEXTURE_NONE, defaultEmmisive = TEXTURE_NONE;
	/*if (nMaterials > maxMat){
		if ((defaultBase = renderer->addTexture("../Textures/pipetech.dds",     TRILINEAR_ANISO)) == TEXTURE_NONE) return false;
		if ((defaultBump = renderer->addTexture("../Textures/pipetechBump.png", TRILINEAR_ANISO, NORMALHEIGHTMAP)) == TEXTURE_NONE) return false;
		if ((defaultEmmisive = renderer->addTexture("../Textures/pipetechEmissive.dds", TRILINEAR_ANISO)) == TEXTURE_NONE) return false;
	}*/

	if ((bilinearClamp  = renderer->addSamplerState(BILINEAR,  CLAMP, CLAMP, CLAMP)) == SS_NONE) return false;
	if ((trilinearClamp = renderer->addSamplerState(TRILINEAR, CLAMP, CLAMP, CLAMP)) == SS_NONE) return false;
	if ((trilinearAniso = renderer->addSamplerState(TRILINEAR_ANISO, WRAP, WRAP, WRAP)) == SS_NONE) return false;
	if ((linearLod = renderer->addSamplerState(LINEAR, WRAP, WRAP, WRAP, 0.8f)) == SS_NONE) return false;


	char str[128];
	strcpy(str, "../Textures/");
	for (uint i = 0; i < nMaterials; i++){
		if (i < maxMat){
			if (matDesc[i].flags & SKYBOX){
				sprintf(str + 12, "CubeMaps/%s/", matDesc[i].name);

				char posx[128], negx[128], posy[128], negy[128], posz[128], negz[128];

				sprintf(posx, "%s%s", str, "posx.htex");
				sprintf(negx, "%s%s", str, "negx.htex");
				sprintf(posy, "%s%s", str, "posy.htex");
				sprintf(negy, "%s%s", str, "negy.htex");
				sprintf(posz, "%s%s", str, "posz.htex");
				sprintf(negz, "%s%s", str, "negz.htex");

				const char *skyBoxFiles[] = { posx, negx, posy, negy, posz, negz };

				if ((materials[i].base = renderer->addCubemap(skyBoxFiles, true, trilinearClamp)) == TEXTURE_NONE) return false;

				materials[i].bump     = TEXTURE_NONE;
				materials[i].horizon  = TEXTURE_NONE;
				materials[i].emissive = TEXTURE_NONE;

			} else {
				sprintf(str + 12, "%s.htex", matDesc[i].name);
				if ((materials[i].base = renderer->addTexture(str, true, trilinearAniso)) == TEXTURE_NONE) return false;

				sprintf(str + 12, "%sBump.dds", matDesc[i].name);
				if ((materials[i].bump = renderer->addNormalMap(str, FORMAT_RGBA8, true, trilinearAniso)) == TEXTURE_NONE) return false;

				if (matDesc[i].flags & SELFSHADOW){
					sprintf(str + 12, "%sHorizon.dds", matDesc[i].name);
					if ((materials[i].horizon = renderer->addTexture(str, false, linearLod)) == TEXTURE_NONE) return false;
				}

				if (matDesc[i].flags & EMISSIVE){
					sprintf(str + 12, "%sEmissive.htex", matDesc[i].name);
					if ((materials[i].emissive = renderer->addTexture(str, true, trilinearAniso)) == TEXTURE_NONE) return false;
				}
			}

			materials[i].parallax = matDesc[i].parallax;
			materials[i].gloss = matDesc[i].gloss;
			materials[i].flags = matDesc[i].flags;
		} else {
			materials[i].base = defaultBase;
			materials[i].bump = defaultBump;
			materials[i].emissive = defaultEmmisive;
			materials[i].flags = 0;
		}
	}
	if ((lightTex = renderer->addTexture("../Textures/Corona.dds", true, bilinearClamp)) == TEXTURE_NONE) return false;
	if ((explosion[0] = renderer->addTexture("../Textures/Volumes/explosion3.dds", false, linearClamp)) == TEXTURE_NONE) return false;
//	if ((explosion[1] = renderer->addTexture("../Textures/Volumes/explosion4.dds", false, linearClamp)) == TEXTURE_NONE) return false;

	if ((spot = renderer->addTexture("../Textures/spot2.dds", false, linearClamp)) == TEXTURE_NONE) return false;

	if ((angleMap = createAngleMap(renderer, linearClamp, 32)) == TEXTURE_NONE) return false;

	if ((blendAdd = renderer->addBlendState(ONE, ONE)) == BS_NONE) return false;
	if ((blendOneInvSrcAlpha = renderer->addBlendState(ONE, ONE_MINUS_SRC_ALPHA)) == BS_NONE) return false;
	if ((blendOneInvSrcColor = renderer->addBlendState(ONE, ONE_MINUS_SRC_COLOR)) == BS_NONE) return false;
	if ((blendInvDstColorOne = renderer->addBlendState(ONE_MINUS_DST_COLOR, ONE)) == BS_NONE) return false;

	if ((noColorWrite = renderer->addBlendState(ONE, ZERO, BM_ADD, 0)) == BS_NONE) return false;


	// Create a vertex array for the particle system. vec3((s,t), time)
	vec4 *pArray = new vec4[4 * PARTICLE_COUNT];
	for (int i = 0; i < PARTICLE_COUNT; i++){
		float t = float(i) / PARTICLE_COUNT;
		float r = 2 * PI * (2 * float(rand()) / RAND_MAX - 1);
		pArray[4 * i + 0] = vec4(0, 0, t, r);
		pArray[4 * i + 1] = vec4(1, 0, t, r);
		pArray[4 * i + 2] = vec4(1, 1, t, r);
		pArray[4 * i + 3] = vec4(0, 1, t, r);
	}
	if ((particleBuffer = renderer->addVertexBuffer(4 * PARTICLE_COUNT * sizeof(vec4), STATIC, pArray)) == VB_NONE) return false;
	delete pArray;

	FormatDesc pFormat[] = {
		{ 0, TYPE_VERTEX, FORMAT_FLOAT, 4 }
	};
	if ((particleFormat = renderer->addVertexFormat(pFormat, elementsOf(pFormat))) == VF_NONE) return false;





	// FIXME
	indexSize = 2;
	indexType = GL_UNSIGNED_SHORT;

	return true;
}

void Engine::unmakeDrawable(Renderer *renderer){
}

void Engine::computeVisibleCubes(const int thread, const uint dstCubeIndex, const int x, const int y, const int z){
	int srcCubeIndex = (z * sizeY + y) * sizeX + x;

	Cube *srcCube = cubes + srcCubeIndex;
	Cube *dstCube = cubes + dstCubeIndex;

	srcCube->hasBeenVisited[thread] = true;

	bool isVisible;

	if (dstCube->cubeVisibility[srcCubeIndex]){
		isVisible = true;
	} else {
		isVisible = false;

		for (uint j = 0; j < dstCube->posCount; j++){
			vec3 point = dstCube->positions[j];

			for (uint k = 0; k < srcCube->posCount; k++){
				vec3 cPoint = srcCube->positions[k];

				if (!bsp.intersectsCached(point, cPoint)){
					dstCube->cubeVisibility[srcCubeIndex] = true;
					srcCube->cubeVisibility[dstCubeIndex] = true;
					isVisible = true;

					goto visibleDone;
					//break;
				}
			}
		}
	}

visibleDone:
	if (isVisible){
		int minX = max(x - 1, 0);
		int maxX = min(x + 1, sizeX - 1);
		int minY = max(y - 1, 0);
		int maxY = min(y + 1, sizeY - 1);
		int minZ = max(z - 1, 0);
		int maxZ = min(z + 1, sizeZ - 1);

		for (int dz = minZ; dz <= maxZ; dz++){
			for (int dy = minY; dy <= maxY; dy++){
				for (int dx = minX; dx <= maxX; dx++){
					int cubeIndex = (dz * sizeY + dy) * sizeX + dx;
					Cube *cube = cubes + cubeIndex;

					if (!cube->hasBeenVisited[thread]){
						computeVisibleCubes(thread, dstCubeIndex, dx, dy, dz);
					}
				}
			}
		}
	}

}

void Engine::processMessage(const int thread, const int message, void *data, const int size){
	int *arg = (int *) data;
	uint nCubes = sizeX * sizeY * sizeZ;

	switch (message){
	case THREAD_COMPUTE_VISIBLE_CUBES:
		for (uint i = 0; i < nCubes; i++){
			cubes[i].hasBeenVisited[thread] = false;
		}
		computeVisibleCubes(thread, arg[0], arg[1], arg[2], arg[3]);

		break;
	};
}

void Engine::computeVisibility(Renderer *renderer, const int n, bool useCache){
	uint32 nCubes = sizeX * sizeY * sizeZ;

	if (useCache){
		FILE *file = fopen("Visibility.vis", "rb");
		if (file){
			uint32 nCacheCubes = 0;
			fread(&nCacheCubes, 4, 1, file);
			if (nCacheCubes != nCubes){
				useCache = false;
			} else {
				for (uint i = 0; i < nCubes; i++){
					fread(cubes[i].cubeVisibility, sizeof(bool), nCacheCubes, file);
				}
			}
			fclose(file);
		}
	}

	if (!useCache){
		float invN = 1.0f / (n - 1);

		uint nDiffs = n * n * n;
		if (n > 2) nDiffs -= (n - 2) * (n - 2) * (n - 2);

		vec3 *diffs = new vec3[nDiffs];
		vec3 *dest = diffs;
		for (int z = 0; z < n; z++){
			for (int y = 0; y < n; y++){
				for (int x = 0; x < n; x++){
					if (x == 0 || x == n - 1 || y == 0 || y == n - 1 || z == 0 || z == n - 1){
						*dest++ = 0.1f + vec3(x * invN, y * invN, z * invN) * (cubeSize - 0.2f);
					}
				}
			}
		}

		for (uint i = 0; i < nCubes; i++){
			cubes[i].positions = new vec3[nDiffs];

			uint count = 0;
			for (uint j = 0; j < nDiffs; j++){
				vec3 pos = cubes[i].minLim + diffs[j];
				bsp.pushSphere(pos, 1);
				if (isInCube(pos, cubes[i].minLim, cubes[i].maxLim)){
					if (bsp.isInOpenSpace(pos)){
						cubes[i].positions[count] = pos;
						count++;
					}
				}
			}
			cubes[i].posCount = count;
		}

		startThreads(cpuCount);

		for (int z = 0; z < sizeZ; z++){
			for (int y = 0; y < sizeY; y++){
				for (int x = 0; x < sizeX; x++){
					int destCubeIndex = (z * sizeY + y) * sizeX + x;

					int arg[4] = { destCubeIndex, x, y, z };
					int thread = (cpuCount * z) / sizeZ;

					postMessage(thread, THREAD_COMPUTE_VISIBLE_CUBES, arg, sizeof(arg));
				}
			}
		}

		postMessage(ALL_THREADS, THREAD_QUIT);
		waitForExit();

		for (uint i = 0; i < nCubes; i++){
			delete cubes[i].positions;
		}

		delete diffs;

		needsSave = true;
	}

	for (int tz = 0; tz < sizeZ; tz++){
		for (int ty = 0; ty < sizeY; ty++){
			for (int tx = 0; tx < sizeX; tx++){
				precomputeCube(tx, ty, tz);
			}
		}
	}
}

void Engine::updateVisibility(Renderer *renderer, const vec3 &camPos, const int width, const int height, const int resW, const int resH){
	int x, y, z;
	if (!getCubeCoords(camPos, x, y, z)) return;
	int cube = getCube(camPos);

	glMatrixMode(GL_PROJECTION);
	mat4 projection = cubeProjectionMatrixGL(1, 6000);
	glLoadTransposeMatrixfARB(projection);

	int w = min(width,  resW);
	int h = min(height, resH);
	glViewport(0, 0, w, h);

	int nCubes = sizeX * sizeY * sizeZ;

	GLuint *queries = new GLuint[nCubes * 6];
	glGenQueriesARB(nCubes * 6, queries);

	for (int side = 0; side < 6; side++){
		glMatrixMode(GL_MODELVIEW);
		mat4 modelview = cubeViewMatrix(side);
		modelview.translate(-camPos);
		glLoadTransposeMatrixfARB(modelview);

		renderer->reset();
		renderer->setRasterizerState(cullBack);
		renderer->setDepthState(noDepthWrite);
		renderer->apply();

		// Depth-only pass
		glClear(GL_DEPTH_BUFFER_BIT);
		glEnable(GL_POLYGON_OFFSET_FILL);
			glPolygonOffset(0.6f, 0.6f);

			global->draw(renderer);
		glDisable(GL_POLYGON_OFFSET_FILL);


		// Test visibility of all cubes
		renderer->reset();
		renderer->setRasterizerState(cullBack);
		renderer->setDepthState(noDepthWrite);
		renderer->setBlendState(noColorWrite);
		renderer->apply();

		for (int i = 0; i < nCubes; i++){
			glBeginQueryARB(GL_SAMPLES_PASSED_ARB, queries[nCubes * side + i]);
			if (!cubes[cube].cubeVisibility[i]) cubes[i].drawBox();
			glEndQueryARB(GL_SAMPLES_PASSED_ARB);
		}
	}
	//glFinish();
	for (int side = 0; side < 6; side++){
		for (int i = 0; i < nCubes; i++){
			GLuint passed = 0;
			glGetQueryObjectuivARB(queries[nCubes * side + i], GL_QUERY_RESULT_ARB, &passed);

			if (passed > 24){
				cubes[cube].cubeVisibility[i] = true;
				//cubes[i].cubeVisibility[cube] = true;
			}
		}
	}

	glViewport(0, 0, width, height);

	precomputeCube(x, y, z);


	glDeleteQueriesARB(nCubes * 6, queries);
	delete queries;
}

void Engine::updateAudio(const vec3 &camPos, const vec3 &zDir, const float soundGain, const float musicGain){
	audio->setSourceGain(musicSource, musicGain);
	if (!audio->isPlaying(musicSource)){
		audio->play(musicSource);
	}

	audio->setSourceGain(shootSource, 0.3f * soundGain);

	audio->setListenerOrientation(camPos, zDir);

	for (uint i = 0; i < ambientSounds.getCount(); i++){
		vec3 d = ambientSounds[i].position - camPos;
		if (dot(d, d) < 1500 * 1500){
			if (!audio->isPlaying(ambientSounds[i].source)){
				audio->play(ambientSounds[i].source);
			}
			audio->setSourceGain(ambientSounds[i].source, soundGain);
		} else {
			if (audio->isPlaying(ambientSounds[i].source)){
				audio->pause(ambientSounds[i].source);
			}
		}
	}
}

TextureID Engine::getAmbientEmissiveShader(Renderer *renderer, const bool hasDiffuse, const bool hasEmissive, const bool hasParallax){
	if (ambientEmissive[hasDiffuse][hasEmissive][hasParallax] == SHADER_NONE){
		char defines[64];
		defines[0] = '\0';
		if (hasDiffuse)  strcat(defines, "#define USE_DIFFUSE\n");
		if (hasEmissive) strcat(defines, "#define USE_EMISSIVE\n");
		if (hasParallax) strcat(defines, "#define USE_PARALLAX\n");

		ambientEmissive[hasDiffuse][hasEmissive][hasParallax] = renderer->addShader("ambientEmissive.shd", attribs, elementsOf(attribs), defines);
	}
	return ambientEmissive[hasDiffuse][hasEmissive][hasParallax];
}

TextureID Engine::getLitShader(Renderer *renderer, const bool isSpotLight){
	if (litShader[isSpotLight] == SHADER_NONE){
		char defines[512];
		defines[0] = '\0';
		if (isSpotLight) strcat(defines, "#define SPOTLIGHT\n");

		litShader[isSpotLight] = renderer->addShader("lit.shd", attribs, elementsOf(attribs), defines);
	}

	return litShader[isSpotLight];
}

TextureID Engine::getLightingShader(Renderer *renderer, const bool isSpotLight, const bool hasSpecular, const bool hasParallax, const bool hasShadow, const bool hasSelfShadow){
	if (lightingShader[isSpotLight][hasSpecular][hasParallax][hasShadow][hasSelfShadow] == SHADER_NONE){
		char defines[512];
		defines[0] = '\0';
		if (isSpotLight) strcat(defines, "#define SPOTLIGHT\n");
		if (hasShadow)   strcat(defines, "#define USE_SHADOW\n");
		if (hasSpecular) strcat(defines, "#define USE_SPECULAR\n");
		if (hasParallax) strcat(defines, "#define USE_PARALLAX\n");
		if (hasSelfShadow) strcat(defines, "#define USE_SELFSHADOW\n");

		lightingShader[isSpotLight][hasSpecular][hasParallax][hasShadow][hasSelfShadow] = renderer->addShader("lighting.shd", attribs, elementsOf(attribs), defines);
	}

	return lightingShader[isSpotLight][hasSpecular][hasParallax][hasShadow][hasSelfShadow];
}

TextureID Engine::getWeaponShader(Renderer *renderer, const bool isSpotLight, const bool hasShadow){
	if (weaponShader[isSpotLight][hasShadow] == SHADER_NONE){
		char defines[128];
		defines[0] = '\0';
		if (isSpotLight) strcat(defines, "#define SPOTLIGHT\n");
		if (hasShadow)   strcat(defines, "#define USE_SHADOW\n");

		weaponShader[isSpotLight][hasShadow] = renderer->addShader("weapon.shd", gunAttribs, elementsOf(gunAttribs), defines);
	}

	return weaponShader[isSpotLight][hasShadow];
}

void Engine::drawVisibleCubes(Renderer *renderer, Cube *cube, const bool invert){
	renderer->reset();
	renderer->setBlendState(blendAdd);
	renderer->setDepthState(noDepthWrite);
	renderer->apply();

	glLineWidth(4);

	glColor3f(0.05f, 0.05f, 0.05f);

	uint nCubes = sizeX * sizeY * sizeZ;
	for (uint i = 0; i < nCubes; i++){
		if (cube->cubeVisibility[i] ^ invert){
			cubes[i].drawLineBox();
		}
	}

	glLineWidth(1);
}

void Engine::drawSelected(Renderer *renderer, Cube *cube, const int mat, const int slot){
	int nToDraw = 0;

	if (mat < 0){
		// Draw all materials
		for (uint i = 0; i < cube->nVisibleCubes; i++){
			uint cIndex = cube->visibleCubes[i];
			Cube *c = cubes + cIndex;
			if (c->toDraw[slot]){
				uint batch = cIndex * nMaterials;

				uint start = global->getBatch(batch).startIndex;
				uint end = global->getBatch(batch + nMaterials - 1).startIndex + global->getBatch(batch + nMaterials - 1).nIndices;
				uint nIndices = end - start;
				if (nIndices){
					indexCounts[nToDraw] = nIndices;
					indexOffsets[nToDraw] = start * indexSize;
					nToDraw++;
				}
				nDrawnBatches += nMaterials;
			}
		}

	} else {
		// Draw selected material
		for (uint i = 0; i < cube->nVisibleCubes; i++){
			uint cIndex = cube->visibleCubes[i];
			Cube *c = cubes + cIndex;

			if (c->toDraw[slot]){
				uint batch = cIndex * nMaterials + mat;
				if (global->getBatch(batch).nIndices){
					indexCounts[nToDraw] = global->getBatch(batch).nIndices;
					indexOffsets[nToDraw] = global->getBatch(batch).startIndex * indexSize;
					nToDraw++;
				}
				nDrawnBatches++;
			}
		}
	}

	if (nToDraw){
		glMultiDrawElementsEXT(GL_TRIANGLES, indexCounts, indexType, (const void **) indexOffsets, nToDraw);
		renderer->addDrawCalls(1);
	}
}

void Engine::drawSkyBox(Renderer *renderer, const mat4 &mvp, const vec3 &camPos, Cube *cube){
	/*
		Visibility should already be set up as appropriate from the ambient pass
	*/

	for (uint mat = 0; mat < nMaterials; mat++){
		if (materials[mat].flags & SKYBOX){
			renderer->reset();
			global->setBuffers(renderer);
			renderer->setRasterizerState(cullBack);
			renderer->setShader(skyBoxShader);
			renderer->setTexture("SkyBox", materials[mat].base);
			renderer->setShaderConstant4x4f("mvp", mvp);
			renderer->setShaderConstant3f("camPos", camPos);
			renderer->apply();

			drawSelected(renderer, cube, mat);
		}
	}
}

void Engine::drawGun(Renderer *renderer, const mat4 &mvp, const mat4 &modelview, const mat4 &gunMV, const vec3 &camPos){
	renderer->reset();
	renderer->setRasterizerState(cullBack);
	renderer->setShader(getAmbientEmissiveShader(renderer, false, false, false));
	renderer->setShaderConstant4x4f("mvp", mvp);
	renderer->apply();

	renderer->setShaderConstant4f("color", 0.1f * vec4(0.80f, 0.45f, 0.21f, 0));
	renderer->applyConstants();
	rocketLauncher->drawBatch(renderer, 0);

	renderer->setShaderConstant4f("color", 0.1f * vec4(0.58f, 0.74f, 0.71f, 0));
	renderer->applyConstants();
	rocketLauncher->drawBatch(renderer, 1);

	renderer->setShaderConstant4f("color", 0.1f * vec4(0.23f, 0.42f, 0.80f, 0));
	renderer->applyConstants();
	rocketLauncher->drawBatch(renderer, 2);

	if (lights.goToFirst()){
		do {
			Light *light = lights.getCurrent();

			vec3 dir = camPos - light->position;
			if (dot(dir, dir) > light->lightRadius * light->lightRadius) continue;

			bool isSpotLight = (light->fov > 0);
			bool hasShadow = useShadow && (light->shadowMap != TEXTURE_NONE);

			mat4 spotMvp;
			if (isSpotLight){
				spotMvp = perspectiveMatrixX(light->fov, 1, 1, 1.0f, light->lightRadius) * rotateXY(-light->wx, -light->wy) * translate(-light->position);
				Frustum spotFrustum;
				spotFrustum.loadFrustum(spotMvp);
				if (!spotFrustum.pointInFrustum(camPos)) continue;
			}


			ShaderID weaponShader = getWeaponShader(renderer, isSpotLight, hasShadow);

			renderer->reset();
			renderer->setRasterizerState(cullBack);
			renderer->setShader(weaponShader);
			renderer->setBlendState(blendAdd);
			renderer->setDepthState(noDepthWrite);
			if (isSpotLight){
				renderer->setTexture("Spot", spot);
				renderer->setShaderConstant4x4f("spotMvp", spotMvp);
			}
			renderer->setShaderConstant4x4f("mvp", mvp);
			renderer->setShaderConstant4x4f("gunMV", gunMV);
			renderer->setShaderConstant4x4f("mv", modelview);
			renderer->setShaderConstant3f("lightPos", light->position);
			renderer->setShaderConstant3f("camPos", camPos);
			renderer->setShaderConstant1f("invRadius", 1.0f / light->lightRadius);
			if (hasShadow){
				renderer->setTexture("Shadow", light->shadowMap);
			}
			renderer->setShaderConstant4f("lightColor", light->color);
			renderer->apply();


			renderer->setShaderConstant4f("color", vec4(0.80f, 0.45f, 0.21f, 0));
			renderer->applyConstants();
			rocketLauncher->drawBatch(renderer, 0);

			renderer->setShaderConstant4f("color", vec4(0.58f, 0.74f, 0.71f, 0));
			renderer->applyConstants();
			rocketLauncher->drawBatch(renderer, 1);

			renderer->setShaderConstant4f("color", vec4(0.23f, 0.42f, 0.80f, 0));
			renderer->applyConstants();
			rocketLauncher->drawBatch(renderer, 2);

		} while (lights.goToNext());
	}
}

void Engine::drawAmbientEmissive(Renderer *renderer, const mat4 &mvp, const vec3 &camPos, const Frustum &frustum, Cube *cube){
	if (useClipPlanes){
		// Clip planes creates new geometry, so we need to add a depth offset to avoid z-fighting.
		glEnable(GL_POLYGON_OFFSET_FILL);
		glPolygonOffset(0.5f, 0.5f);
	}

	for (uint i = 0; i < cube->nVisibleCubes; i++){
		Cube *c = cubes + cube->visibleCubes[i];
		c->toDraw[0] = frustum.cubeInFrustum(c->minLim.x, c->maxLim.x, c->minLim.y, c->maxLim.y, c->minLim.z, c->maxLim.z);
	}

	for (uint mat = 0; mat < nMaterials; mat++){
		if (materials[mat].flags & SKYBOX) continue;

		bool hasDiffuse = true;
		bool hasEmissive = (materials[mat].emissive != TEXTURE_NONE);
		bool hasParallax = useParallax && (materials[mat].parallax > 0);

		renderer->reset();
		global->setBuffers(renderer);
		renderer->setRasterizerState(cullBack);
		renderer->setShader(getAmbientEmissiveShader(renderer, hasDiffuse, hasEmissive, hasParallax));
		renderer->setTexture("Base", materials[mat].base);
		if (hasParallax){
			renderer->setTexture("Bump", materials[mat].bump);
			renderer->setShaderConstant3f("camPos", camPos);
			renderer->setShaderConstant2f("plxCoeffs", vec2(2, -1) * materials[mat].parallax);
		}
		if (hasEmissive){
			renderer->setTexture("Emissive", materials[mat].emissive);
		}
		renderer->setShaderConstant4x4f("mvp", mvp);
		renderer->setShaderConstant4f("color", vec4(0.13f));
		renderer->apply();

		drawSelected(renderer, cube, mat);
	}

	if (useClipPlanes) glDisable(GL_POLYGON_OFFSET_FILL);
}

void Engine::drawLighting(Renderer *renderer, const mat4 &mvp, const mat4 &modelview, const vec3 &camPos, const Frustum &frustum, const int width, const int height, const float time, Cube *cube){
	if (!lights.goToFirst()) return;


	/***************************************************************************
		TODO: Improvement ... Check overlap of visibility from light and camera
	 ***************************************************************************/





	//uint nCubes = sizeX * sizeY * sizeZ;

	do {
		Light *light = lights.getCurrent();
		light->isVisible = false;

		// If the sphere is outside the frustum, it's not visible
		if (!frustum.sphereInFrustum(light->position, light->lightRadius)) continue;

		int lightCubeIndex = getCube(light->position);
		if (lightCubeIndex < 0) continue;

		Cube *lightCube = cubes + lightCubeIndex;

		// Set up scissor rectangle
		int x, y, w, h;
		if (useScissor){
			if (getScissorRectangle(modelview, light->position, light->lightRadius, fov, width, height, &x, &y, &w, &h)){
				glScissor(x, y, w, h);
			} else {
				continue;
			}
		}

		bool hasShadow = useShadow && ((light->flags & NOSHADOWS) == 0);
		bool isSpotLight = (light->fov > 0);


		mat4 spotMvp;
		Frustum spotFrustum;
		if (isSpotLight){
			spotMvp = perspectiveMatrixX(light->fov, 1, 1, 1.0f, light->lightRadius) * rotateXY(-light->wx, -light->wy) * translate(-light->position);
			spotFrustum.loadFrustum(spotMvp);
		}

		// Tag which cubes to draw
		bool needDraw = false;
		for (uint i = 0; i < cube->nVisibleCubes; i++){
			uint cIndex = cube->visibleCubes[i];
			Cube *c = cubes + cIndex;
			c->toDraw[1] = false;
			if (lightCube->cubeVisibility[cIndex]){
				// If the cube is outside the frustum, it need not be drawn
				if (frustum.cubeInFrustum(c->minLim.x, c->maxLim.x, c->minLim.y, c->maxLim.y, c->minLim.z, c->maxLim.z)){
					if (isSpotLight){
						// If the cube isn't within the spotlight frustum, it's not going to affected by the light
						if (spotFrustum.cubeInFrustum(c->minLim.x, c->maxLim.x, c->minLim.y, c->maxLim.y, c->minLim.z, c->maxLim.z)){
							c->toDraw[1] = true;
							needDraw = true;
						}
					} else {
						// If the cube isn't within the light radius, it's not going to affected by the light
						if (c->intersectSphere(light->position, light->lightRadius)){
							c->toDraw[1] = true;
							needDraw = true;
						}
					}
				}
			}
		}
		if (!needDraw) continue;


		light->isVisible = true;

		// Update shadow map for dynamic lights
		if (hasShadow && (light->shadowMap < 0)){
			if (useClipPlanes){
				for (uint i = 0; i < 6; i++) glDisable(GL_CLIP_PLANE0 + i);
			}
			if (useScissor) glDisable(GL_SCISSOR_TEST);

			updateShadowMap(renderer, light, cullBack);
		}

		if (useScissor) glEnable(GL_SCISSOR_TEST);

		if (useClipPlanes){
			light->setupClipPlanes();
			for (uint i = 0; i < 6; i++) glEnable(GL_CLIP_PLANE0 + i);
		}



		TextureID shadowMap = (light->shadowMap >= 0)? light->shadowMap : isSpotLight? spotShadowMapColorRT : omniShadowMapColorRT;

		if (hasShadow && useStencilBranching){
			/*
				Dynamic branching using stencil.
				This is the condition pass.
			*/
			//glClear(GL_STENCIL_BUFFER_BIT);

			glEnable(GL_STENCIL_TEST);

			glEnable(GL_ALPHA_TEST);
			glAlphaFunc(GL_GREATER, 0);

			glStencilFunc(GL_ALWAYS, 1, ~0);
			glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);

			renderer->reset();
			global->setBuffers(renderer);
			renderer->setShader(getLitShader(renderer, light->fov > 0));
			renderer->setTexture("Shadow", shadowMap);
			renderer->setDepthState(noDepthWrite);
			renderer->setBlendState(noColorWrite);
			if (isSpotLight) renderer->setShaderConstant4x4f("spotMvp", spotMvp);
			renderer->setShaderConstant4x4f("mvp", mvp);
			renderer->setShaderConstant3f("lightPos", light->position);
			renderer->setShaderConstant1f("invRadius", 1.0f / light->lightRadius);
			renderer->apply();

			drawSelected(renderer, cube, -1, 1);

			glDisable(GL_ALPHA_TEST);

			glStencilFunc(GL_EQUAL, 1, ~0);
			glStencilOp(GL_KEEP, GL_KEEP, GL_ZERO);
			//glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
		}

		for (uint mat = 0; mat < nMaterials; mat++){
			if (materials[mat].flags & SKYBOX) continue;

			bool hasSpecular   = useSpecular   && (materials[mat].gloss > 0);
			bool hasParallax   = useParallax   && (materials[mat].parallax > 0);
			bool hasSelfShadow = useSelfShadow && (materials[mat].horizon != TEXTURE_NONE);

			ShaderID lighting = getLightingShader(renderer, isSpotLight, hasSpecular, hasParallax, hasShadow && !useStencilBranching, hasSelfShadow);

			renderer->reset();
			global->setBuffers(renderer);
			renderer->setRasterizerState(cullBack);
			renderer->setShader(lighting);
			renderer->setTexture("Base", materials[mat].base);
			renderer->setTexture("Bump", materials[mat].bump);
			if (hasShadow && !useStencilBranching){
				renderer->setTexture("Shadow", shadowMap);
			}
			if (isSpotLight) renderer->setTexture("Spot", spot);
			if (hasSelfShadow){
				renderer->setTexture("Angle", angleMap);
				renderer->setTexture("Horizon", materials[mat].horizon);
			}
			renderer->setDepthState(noDepthWrite);
			renderer->setBlendState(blendAdd);
			// Setup lighting parameters
			if (isSpotLight) renderer->setShaderConstant4x4f("spotMvp", spotMvp);
			renderer->setShaderConstant4x4f("mvp", mvp);
			renderer->setShaderConstant3f("camPos", camPos);

			renderer->setShaderConstant3f("lightPos", light->position);
			renderer->setShaderConstant1f("invRadius", 1.0f / light->lightRadius);

			vec4 color = light->color;
			if (light->flickerFactor){
				color *= (1.0f - light->flickerFactor * (noise1(20.0f * time) * 0.5f + 0.5f));
			}

			renderer->setShaderConstant4f("lightColor", color);

			// Setup material properties
			if (hasParallax){
				renderer->setShaderConstant2f("plxCoeffs", vec2(2, -1) * materials[mat].parallax);
			}
			if (hasSpecular){
				renderer->setShaderConstant1f("gloss", materials[mat].gloss);
			}
			renderer->apply();

			drawSelected(renderer, cube, mat, 1);
		}

		if (hasShadow && useStencilBranching){
			glDisable(GL_STENCIL_TEST);
		}

		nDrawnLights++;

	} while (lights.goToNext());

	if (useScissor) glDisable(GL_SCISSOR_TEST);
	if (useClipPlanes){
		for (uint i = 0; i < 6; i++) glDisable(GL_CLIP_PLANE0 + i);
	}
}

int compLights(Light *const &light0, Light *const &light1){
	return (int) (light1->sortDist - light0->sortDist);
};

void Engine::drawVolumetricLighting(Renderer *renderer, const mat4 &mvp, const mat4 &modelview, const mat4 &gunMV, const vec3 &camPos, const Frustum &frustum, const int width, const int height, const float frameTime, Cube *cube){
	if (!lights.goToFirst()) return;

	Array <Light *> volumeLights;

	do {
		Light *light = lights.getCurrent();

		// If the light sphere is outside the frustum, it won't contribute to the scene
		if (frustum.sphereInFrustum(light->position, light->lightRadius)){
			// TODO: Check visibility
			vec3 d = light->position - camPos;
			light->sortDist = dot(d, d);
			volumeLights.add(light);
		}

	} while (lights.goToNext());


	if (volumeLights.getCount() > 0){
		// Sort lights back to front
		volumeLights.sort(compLights);


		Plane plane = frustum.getPlane(FRUSTUM_NEAR);

		// Adjusted inverse square fall-off function that falls to zero at the light radius
		float a0 = 0.2f;
		float a1 = a0 + 1.0f;
		float c = sqrtf(a1 / a0 - 1.0f);

		for (uint i = 0; i < volumeLights.getCount(); i++){
			Light *light = volumeLights[i];
			if (light->volumetricy > 0){
				float invRadius = c / light->lightRadius;

				// Set up scissor rectangle
				int x, y, w, h;
				if (useScissor){
					if (getScissorRectangle(modelview, light->position, light->lightRadius, fov, width, height, &x, &y, &w, &h)){
						glScissor(x, y, w, h);
					} else {
						continue;
					}
				}

				Cube *lightCube = cubes + getCube(light->position);

				// Tag which cubes to draw
				bool needDraw = false;
				for (uint i = 0; i < cube->nVisibleCubes; i++){
					uint cIndex = cube->visibleCubes[i];
					Cube *c = cubes + cIndex;

					c->toDraw[0] = false;
					if (lightCube->cubeVisibility[cIndex]){
						// If the cube is outside the frustum, it need not be drawn
						if (frustum.cubeInFrustum(c->minLim.x, c->maxLim.x, c->minLim.y, c->maxLim.y, c->minLim.z, c->maxLim.z)){
							c->toDraw[0] = true;
							needDraw = true;
						}
					}
				}

				if (!needDraw) continue;

				if (useScissor) glEnable(GL_SCISSOR_TEST);

				renderer->reset();
				global->setBuffers(renderer);
				renderer->setRasterizerState(cullBack);
				renderer->setShader(volumetric);
				renderer->setDepthState(noDepthWrite);
				renderer->setBlendState(blendOneInvSrcAlpha);
				renderer->setShaderConstant4x4f("mvp", mvp);
				renderer->setShaderConstant3f("camPos", camPos);
				renderer->setShaderConstant3f("lightPos", light->position);
				renderer->setShaderConstant1f("invRadius", invRadius);
				renderer->setShaderConstant4f("lightColor0", a1 * light->volumetricy * light->color);
				renderer->setShaderConstant4f("lightColor1", a0 * light->volumetricy * light->color);
				renderer->apply();

				drawSelected(renderer, cube);
				//global->draw(renderer);

				nDrawnVolumeLights++;

				if (useScissor) glDisable(GL_SCISSOR_TEST);
			}


			if (light->coronaIntensity > 0){
				float ip = powf(0.0025f, frameTime);

				renderer->reset();
				((OpenGLRenderer *) renderer)->setTexture(lightTex);
				renderer->setDepthState(noDepthTest);
				renderer->setBlendState(blendOneInvSrcColor);
				renderer->apply();

				bool invisible;
				if (useOccWorkaround){
					invisible = bsp.intersects(camPos, light->position);
				} else {
					GLuint samples = 0;
					glGetQueryObjectuivARB(light->query, GL_QUERY_RESULT_ARB, &samples);
					invisible = (samples < 4);
				}

				if (invisible){
					light->currentCoronaIntensity = lerp(0, light->currentCoronaIntensity, ip);
				} else {
					light->currentCoronaIntensity = lerp(light->coronaIntensity, light->currentCoronaIntensity, ip);
				}

				vec3 dir = light->position - camPos;
				float d = dot(dir, dir);
				float intensity = light->currentCoronaIntensity * saturate(0.000004f * d);

				if (intensity > 0.01f){
					glColor4fv(intensity * light->color);

					float size = light->lightRadius * 0.3f;

					vec3 dx = modelview.rows[0].xyz();
					vec3 dy = modelview.rows[1].xyz();
					glBegin(GL_QUADS);
						glTexCoord2f(0, 0);
						glVertex3fv(light->position - size * dx + size * dy);
						glTexCoord2f(1, 0);
						glVertex3fv(light->position + size * dx + size * dy);
						glTexCoord2f(1, 1);
						glVertex3fv(light->position + size * dx - size * dy);
						glTexCoord2f(0, 1);
						glVertex3fv(light->position - size * dx - size * dy);
					glEnd();
				}
				renderer->addDrawCalls(1);
			}
		}


		renderer->reset();
		renderer->setRasterizerState(cullBack);
		renderer->setShader(weaponVolumetric);
		renderer->setDepthState(noDepthWrite);
		renderer->setBlendState(blendOneInvSrcAlpha);
		renderer->setShaderConstant4x4f("mvp", mvp * gunMV);
		renderer->setShaderConstant4x4f("mv", gunMV);
		renderer->apply();

		for (uint i = 0; i < volumeLights.getCount(); i++){
			Light *light = volumeLights[i];

			vec3 d = light->position - camPos;
			if (dot(d, d) < light->lightRadius * light->lightRadius){
				renderer->setShaderConstant3f("lightPos", light->position);
				renderer->setShaderConstant1f("invRadius", 1.0f / light->lightRadius);
				renderer->setShaderConstant4f("lightColor", light->volumetricy * light->color);
				renderer->applyConstants();

				rocketLauncher->draw(renderer);
			}
		}
	}
}

void Engine::drawLights(Renderer *renderer, const mat4 &projection, const mat4 &modelview, const bool debug){
	// TODO: Optimize for visibility ...

	if (lights.goToFirst()){
		vec3 dx = modelview.rows[0].xyz();
		vec3 dy = modelview.rows[1].xyz();

		glMatrixMode(GL_PROJECTION);
		glLoadTransposeMatrixfARB(projection);

		glMatrixMode(GL_MODELVIEW);
		glLoadTransposeMatrixfARB(modelview);

		do {
			Light *light = lights.getCurrent();

			renderer->reset();
			((OpenGLRenderer *) renderer)->setTexture(lightTex);
			renderer->setBlendState(blendAdd);
			if (debug && light->isVisible) renderer->setDepthState(noDepthTest);
			renderer->apply();

			if (!useOccWorkaround && light->coronaIntensity > 0){
				if (light->query == 0){
					glGenQueriesARB(1, &light->query);
				}
				glBeginQueryARB(GL_SAMPLES_PASSED_ARB, light->query);
			}

//			renderer->changeMask(light->flags & INVISIBLE? 0 : COLOR);
			renderer->changeDepthState(noDepthWrite);
			if (light->flags & INVISIBLE){
				renderer->changeBlendState(noColorWrite);
			}


			glColor4fv(light->color);
			glBegin(GL_QUADS);
				glTexCoord2f(0, 0);
				glVertex3fv(light->position - 15 * dx + 15 * dy);
				glTexCoord2f(1, 0);
				glVertex3fv(light->position + 15 * dx + 15 * dy);
				glTexCoord2f(1, 1);
				glVertex3fv(light->position + 15 * dx - 15 * dy);
				glTexCoord2f(0, 1);
				glVertex3fv(light->position - 15 * dx - 15 * dy);
			glEnd();
			renderer->addDrawCalls(1);

			if (!useOccWorkaround && light->coronaIntensity > 0){
				glEndQueryARB(GL_SAMPLES_PASSED_ARB);
			}
		} while (lights.goToNext());

		glFlush();
	}
}

void Engine::drawExplosions(Renderer *renderer, const mat4 &projection, const mat4 &modelview){
	if (rockets.goToFirst()){
		glMatrixMode(GL_PROJECTION);
		glLoadTransposeMatrixfARB(projection);

		glMatrixMode(GL_MODELVIEW);
		glLoadTransposeMatrixfARB(modelview);

		renderer->reset();
		renderer->setDepthState(noDepthWrite);
//		renderer->setBlending(ONE_MINUS_DST_COLOR, ONE);
		renderer->setBlendState(blendInvDstColorOne);
		renderer->apply();

		vec3 dx = modelview.rows[0].xyz();
		vec3 dy = modelview.rows[1].xyz();

		glColor3f(1, 1, 1);
		do {
			Rocket *rocket = rockets.getCurrent();

			if (rocket->explosionTime > 0){
				vec3 pos = rocket->light->position;

				float t = rocket->explosionTime / EXPLOSION_TIME;

				//renderer->changeTexture(0U, rocket->texture);
				((OpenGLRenderer *) renderer)->setTexture(rocket->texture);
				renderer->applyTextures();

				glBegin(GL_QUADS);
					glTexCoord3f(0, 0, t);
					glVertex3fv(pos - 150 * dx + 150 * dy);

					glTexCoord3f(1, 0, t);
					glVertex3fv(pos + 150 * dx + 150 * dy);

					glTexCoord3f(1, 1, t);
					glVertex3fv(pos + 150 * dx - 150 * dy);

					glTexCoord3f(0, 1, t);
					glVertex3fv(pos - 150 * dx - 150 * dy);
				glEnd();
				renderer->addDrawCalls(1);
			}
		} while (rockets.goToNext());
	}
}

void Engine::drawParticleSystems(Renderer *renderer, const mat4 &mvp, const mat4 &modelview, const float time, const Frustum &frustum){
	// TODO: Cube visibility

	if (particleSystems.goToFirst()){
		renderer->reset();
		renderer->setShader(particleShader);
		renderer->setTexture("Texture", explosion[0]);
		renderer->setShaderConstant1f("time", time);
		renderer->setVertexFormat(particleFormat);
		renderer->setVertexBuffer(0, particleBuffer);
		renderer->setDepthState(noDepthWrite);
		renderer->setBlendState(blendAdd);
		renderer->apply();

		do {
			ParticleSystem *pSys = particleSystems.getCurrent();

			vec3 midPoint = pSys->position;
			midPoint.y += 0.5f * pSys->height;
			float r = 0.5f * max(pSys->height, pSys->size);

			if (frustum.sphereInFrustum(midPoint, r)){
				renderer->setShaderConstant4x4f("mvp", mvp * translate(pSys->position));
				renderer->setShaderConstant3f("dx", pSys->size * modelview.rows[0].xyz());
				renderer->setShaderConstant3f("dy", pSys->size * modelview.rows[1].xyz());
				renderer->setShaderConstant1f("height", pSys->height);
				renderer->setShaderConstant3f("rTrans", pSys->red);
				renderer->setShaderConstant3f("gTrans", pSys->green);
				renderer->setShaderConstant3f("bTrans", pSys->blue);
				renderer->applyConstants();

				renderer->drawArrays(PRIM_QUADS, 0, 4 * PARTICLE_COUNT);
			}
		} while (particleSystems.goToNext());
	}
}

float Engine::draw(Renderer *renderer, const vec3 &camPos, const float wx, const float wy, const float time, const float frameTime, const float gunZ, const int width, const int height, const bool debug, const bool invert){
	nDrawnBatches = 0;
	nDrawnLights  = 0;
	nDrawnVolumeLights = 0;

//	renderer->changeMask(ALL);
	renderer->reset();
	renderer->apply();

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

	int cubeIndex = getCube(camPos);
	if (cubeIndex < 0) return 0;
	Cube *cube = cubes + cubeIndex;

	mat4 projection = perspectiveMatrixX(fov, width, height, 5, cube->maxDepth);
	mat4 modelview = rotateXY(-wx, -wy);
	modelview.translate(-camPos);
	mat4 mvp = projection * modelview;

	Frustum frustum;
	frustum.loadFrustum(mvp);

	glMatrixMode(GL_PROJECTION);
	glLoadTransposeMatrixfARB(projection);

	glMatrixMode(GL_MODELVIEW);
	glLoadTransposeMatrixfARB(modelview);


	uint nCubes = sizeX * sizeY * sizeZ;


	/*
		Draw the gun.
		It will always be front-most, so we draw it first to take advantage of HiZ.
	*/
	mat4 invModelview = translate(camPos) * rotateY(wy) * rotateX(wx);

	mat4 gunMV = invModelview * translate(vec3(10, -5, 5 + gunZ)) * rotateXY(0.07f * cosf(1.423f * time), 0.05f * sinf(1.334f * time));
	drawGun(renderer, mvp * gunMV, modelview, gunMV, camPos);

	/*
		Draw ambient lighting
	*/
	drawAmbientEmissive(renderer, mvp, camPos, frustum, cube);

	/*
		Skybox
	*/
	drawSkyBox(renderer, mvp, camPos, cube);

	/*
		Main lighting loop
	*/
	drawLighting(renderer, mvp, modelview, camPos, frustum, width, height, time, cube);


	// Draw lights
	drawLights(renderer, projection, modelview, debug && !invert);

	// Draw explosions
	drawExplosions(renderer, projection, modelview);

	// Draw particle systems
	drawParticleSystems(renderer, mvp, modelview, time, frustum);


	/*
		Volumetric lighting pass
	*/
	if (useVolumetric) drawVolumetricLighting(renderer, mvp, modelview, gunMV, camPos, frustum, width, height, frameTime, cube);


	if (debug) drawVisibleCubes(renderer, cube, invert);

	return float(cube->nVisibleCubes) / nCubes;
//	return float(nDrawnBatches) / ((1 + lights.getCount()) * nMaterials * nCubes);
}
