summary refs log tree commit diff
path: root/src/MapRenderer.c
diff options
context:
space:
mode:
authorWlodekM <[email protected]>2024-06-16 10:35:45 +0300
committerWlodekM <[email protected]>2024-06-16 10:35:45 +0300
commitabef6da56913f1c55528103e60a50451a39628b1 (patch)
treeb3c8092471ecbb73e568cd0d336efa0e7871ee8d /src/MapRenderer.c
initial commit
Diffstat (limited to 'src/MapRenderer.c')
-rw-r--r--src/MapRenderer.c809
1 files changed, 809 insertions, 0 deletions
diff --git a/src/MapRenderer.c b/src/MapRenderer.c
new file mode 100644
index 0000000..d09e7d2
--- /dev/null
+++ b/src/MapRenderer.c
@@ -0,0 +1,809 @@
+#include "MapRenderer.h"
+#include "Block.h"
+#include "Builder.h"
+#include "Camera.h"
+#include "Entity.h"
+#include "EnvRenderer.h"
+#include "Event.h"
+#include "ExtMath.h"
+#include "Funcs.h"
+#include "Game.h"
+#include "Graphics.h"
+#include "Platform.h"
+#include "TexturePack.h"
+#include "Utils.h"
+#include "World.h"
+#include "Options.h"
+
+int MapRenderer_1DUsedCount;
+struct ChunkPartInfo* MapRenderer_PartsNormal;
+struct ChunkPartInfo* MapRenderer_PartsTranslucent;
+
+static cc_bool inTranslucent;
+static IVec3 chunkPos;
+
+/* The number of non-empty Normal/Translucent ChunkPartInfos (across entire world) for each 1D atlas batch. */
+/* 1D atlas batches that do not have any ChunkPartInfos can be entirely skipped. */
+static int normPartsCount[ATLAS1D_MAX_ATLASES], tranPartsCount[ATLAS1D_MAX_ATLASES];
+/* Whether there are any visible Normal/Translucent ChunkPartInfos for each 1D atlas batch. */
+/* 1D atlas batches that do not have any visible ChunkPartInfos can be skipped. */
+static cc_bool hasNormParts[ATLAS1D_MAX_ATLASES], hasTranParts[ATLAS1D_MAX_ATLASES];
+/* Whether renderer should check if there are any visible Normal/Translucent ChunkPartInfos for each 1D atlas batch. */
+static cc_bool checkNormParts[ATLAS1D_MAX_ATLASES], checkTranParts[ATLAS1D_MAX_ATLASES];
+
+/* Render info for all chunks in the world. Unsorted. */
+static struct ChunkInfo* mapChunks;
+/* Pointers to render info for all chunks in the world, sorted by distance from the camera. */
+static struct ChunkInfo** sortedChunks;
+/* Pointers to render info for all chunks in the world, sorted by distance from the camera. */
+/* Only chunks that can be rendered (i.e. not empty and are visible) are included in this.  */
+static struct ChunkInfo** renderChunks;
+/* Number of actually used pointers in the renderChunks array. Entries past this are ignored and skipped. */
+static int renderChunksCount;
+/* Distance of each chunk from the camera. */
+static cc_uint32* distances;
+/* Maximum number of chunk updates that can be performed in one frame. */
+static int maxChunkUpdates;
+/* Cached number of chunks in the world */
+static int chunksCount;
+
+static void ChunkInfo_Reset(struct ChunkInfo* chunk, int x, int y, int z) {
+	chunk->centreX = x + HALF_CHUNK_SIZE; chunk->centreY = y + HALF_CHUNK_SIZE; 
+	chunk->centreZ = z + HALF_CHUNK_SIZE;
+#ifndef CC_BUILD_GL11
+	chunk->vb = 0;
+#endif
+
+	chunk->visible = true;  
+	chunk->empty   = false;
+	chunk->dirty   = false; 
+	chunk->allAir  = false;
+	chunk->noData  = true;
+
+	chunk->drawXMin = false; chunk->drawXMax = false; chunk->drawZMin = false;
+	chunk->drawZMax = false; chunk->drawYMin = false; chunk->drawYMax = false;
+
+	chunk->normalParts      = NULL;
+	chunk->translucentParts = NULL;
+}
+
+/* Index of maximum used 1D atlas + 1 */
+CC_NOINLINE static int MapRenderer_UsedAtlases(void) {
+	TextureLoc maxLoc = 0;
+	int i;
+
+	for (i = 0; i < Array_Elems(Blocks.Textures); i++) {
+		maxLoc = max(maxLoc, Blocks.Textures[i]);
+	}
+	return Atlas1D_Index(maxLoc) + 1;
+}
+
+
+/*########################################################################################################################*
+*-------------------------------------------------------Map rendering-----------------------------------------------------*
+*#########################################################################################################################*/
+static void CheckWeather(float delta) {
+	IVec3 pos;
+	BlockID block;
+	cc_bool outside;
+	IVec3_Floor(&pos, &Camera.CurrentPos);
+
+	block   = World_SafeGetBlock(pos.x, pos.y, pos.z);
+	outside = pos.y < 0 || !World_ContainsXZ(pos.x, pos.z);
+	inTranslucent = Blocks.Draw[block] == DRAW_TRANSLUCENT || (pos.y < Env.EdgeHeight && outside);
+
+	/* If we are under water, render weather before to blend properly */
+	if (!inTranslucent || Env.Weather == WEATHER_SUNNY) return;
+	Gfx_SetAlphaBlending(true);
+	EnvRenderer_RenderWeather(delta);
+	Gfx_SetAlphaBlending(false);
+}
+
+#ifdef CC_BUILD_GL11
+#define DrawFace(face, ign)    Gfx_BindVb(part.vbs[face]); Gfx_DrawIndexedTris_T2fC4b(0, 0);
+#define DrawFaces(f1, f2, ign) DrawFace(f1, ign); DrawFace(f2, ign);
+#else
+#define DrawFace(face, offset)    Gfx_DrawIndexedTris_T2fC4b(part.counts[face], offset);
+#define DrawFaces(f1, f2, offset) Gfx_DrawIndexedTris_T2fC4b(part.counts[f1] + part.counts[f2], offset);
+#endif
+
+#define DrawNormalFaces(minFace, maxFace) \
+if (drawMin && drawMax) { \
+	Gfx_SetFaceCulling(true); \
+	DrawFaces(minFace, maxFace, offset); \
+	Gfx_SetFaceCulling(false); \
+	Game_Vertices += (part.counts[minFace] + part.counts[maxFace]); \
+} else if (drawMin) { \
+	DrawFace(minFace, offset); \
+	Game_Vertices += part.counts[minFace]; \
+} else if (drawMax) { \
+	DrawFace(maxFace, offset + part.counts[minFace]); \
+	Game_Vertices += part.counts[maxFace]; \
+}
+
+static void RenderNormalBatch(int batch) {
+	int batchOffset = chunksCount * batch;
+	struct ChunkInfo* info;
+	struct ChunkPartInfo part;
+	cc_bool drawMin, drawMax;
+	int i, offset, count;
+
+	for (i = 0; i < renderChunksCount; i++) {
+		info = renderChunks[i];
+		if (!info->normalParts) continue;
+
+		part = info->normalParts[batchOffset];
+		if (part.offset < 0) continue;
+		hasNormParts[batch] = true;
+
+#ifndef CC_BUILD_GL11
+		Gfx_BindVb_Textured(info->vb);
+#endif
+
+		offset  = part.offset + part.spriteCount;
+		drawMin = info->drawXMin && part.counts[FACE_XMIN];
+		drawMax = info->drawXMax && part.counts[FACE_XMAX];
+		DrawNormalFaces(FACE_XMIN, FACE_XMAX);
+
+		offset  += part.counts[FACE_XMIN] + part.counts[FACE_XMAX];
+		drawMin = info->drawZMin && part.counts[FACE_ZMIN];
+		drawMax = info->drawZMax && part.counts[FACE_ZMAX];
+		DrawNormalFaces(FACE_ZMIN, FACE_ZMAX);
+
+		offset  += part.counts[FACE_ZMIN] + part.counts[FACE_ZMAX];
+		drawMin = info->drawYMin && part.counts[FACE_YMIN];
+		drawMax = info->drawYMax && part.counts[FACE_YMAX];
+		DrawNormalFaces(FACE_YMIN, FACE_YMAX);
+
+		if (!part.spriteCount) continue;
+		offset = part.offset;
+		count  = part.spriteCount >> 2; /* 4 per sprite */
+
+		Gfx_SetFaceCulling(true);
+		/* TODO: fix to not render them all */
+#ifdef CC_BUILD_GL11
+		Gfx_BindVb(part.vbs[FACE_COUNT]);
+		Gfx_DrawIndexedTris_T2fC4b(0, 0);
+		Game_Vertices += count * 4;
+		Gfx_SetFaceCulling(false);
+		continue;
+#endif
+		if (info->drawXMax || info->drawZMin) {
+			Gfx_DrawIndexedTris_T2fC4b(count, offset); Game_Vertices += count;
+		} offset += count;
+
+		if (info->drawXMin || info->drawZMax) {
+			Gfx_DrawIndexedTris_T2fC4b(count, offset); Game_Vertices += count;
+		} offset += count;
+
+		if (info->drawXMin || info->drawZMin) {
+			Gfx_DrawIndexedTris_T2fC4b(count, offset); Game_Vertices += count;
+		} offset += count;
+
+		if (info->drawXMax || info->drawZMax) {
+			Gfx_DrawIndexedTris_T2fC4b(count, offset); Game_Vertices += count;
+		}
+		Gfx_SetFaceCulling(false);
+	}
+}
+
+void MapRenderer_RenderNormal(float delta) {
+	int batch;
+	if (!mapChunks) return;
+
+	Gfx_SetVertexFormat(VERTEX_FORMAT_TEXTURED);
+	Gfx_SetAlphaTest(true);
+	
+	Gfx_EnableMipmaps();
+	for (batch = 0; batch < MapRenderer_1DUsedCount; batch++) {
+		if (normPartsCount[batch] <= 0) continue;
+		if (hasNormParts[batch] || checkNormParts[batch]) {
+			Gfx_BindTexture(Atlas1D.TexIds[batch]);
+			RenderNormalBatch(batch);
+			checkNormParts[batch] = false;
+		}
+	}
+	Gfx_DisableMipmaps();
+
+	CheckWeather(delta);
+	Gfx_SetAlphaTest(false);
+#if DEBUG_OCCLUSION
+	DebugPickedPos();
+#endif
+}
+
+#define DrawTranslucentFaces(minFace, maxFace) \
+if (drawMin && drawMax) { \
+	DrawFaces(minFace, maxFace, offset); \
+	Game_Vertices += (part.counts[minFace] + part.counts[maxFace]); \
+} else if (drawMin) { \
+	DrawFace(minFace, offset); \
+	Game_Vertices += part.counts[minFace]; \
+} else if (drawMax) { \
+	DrawFace(maxFace, offset + part.counts[minFace]); \
+	Game_Vertices += part.counts[maxFace]; \
+}
+
+static void RenderTranslucentBatch(int batch) {
+	int batchOffset = chunksCount * batch;
+	struct ChunkInfo* info;
+	struct ChunkPartInfo part;
+	cc_bool drawMin, drawMax;
+	int i, offset;
+
+	for (i = 0; i < renderChunksCount; i++) {
+		info = renderChunks[i];
+		if (!info->translucentParts) continue;
+
+		part = info->translucentParts[batchOffset];
+		if (part.offset < 0) continue;
+		hasTranParts[batch] = true;
+
+#ifndef CC_BUILD_GL11
+		Gfx_BindVb_Textured(info->vb);
+#endif
+
+		offset  = part.offset;
+		drawMin = (inTranslucent || info->drawXMin) && part.counts[FACE_XMIN];
+		drawMax = (inTranslucent || info->drawXMax) && part.counts[FACE_XMAX];
+		DrawTranslucentFaces(FACE_XMIN, FACE_XMAX);
+
+		offset  += part.counts[FACE_XMIN] + part.counts[FACE_XMAX];
+		drawMin = (inTranslucent || info->drawZMin) && part.counts[FACE_ZMIN];
+		drawMax = (inTranslucent || info->drawZMax) && part.counts[FACE_ZMAX];
+		DrawTranslucentFaces(FACE_ZMIN, FACE_ZMAX);
+
+		offset  += part.counts[FACE_ZMIN] + part.counts[FACE_ZMAX];
+		drawMin = (inTranslucent || info->drawYMin) && part.counts[FACE_YMIN];
+		drawMax = (inTranslucent || info->drawYMax) && part.counts[FACE_YMAX];
+		DrawTranslucentFaces(FACE_YMIN, FACE_YMAX);
+	}
+}
+
+void MapRenderer_RenderTranslucent(float delta) {
+	int vertices, batch;
+	if (!mapChunks) return;
+
+	/* First fill depth buffer */
+	vertices = Game_Vertices;
+	Gfx_SetVertexFormat(VERTEX_FORMAT_TEXTURED);
+	Gfx_SetAlphaBlending(false);
+	Gfx_DepthOnlyRendering(true);
+
+	for (batch = 0; batch < MapRenderer_1DUsedCount; batch++) {
+		if (tranPartsCount[batch] <= 0) continue;
+		if (hasTranParts[batch] || checkTranParts[batch]) {
+			RenderTranslucentBatch(batch);
+			checkTranParts[batch] = false;
+		}
+	}
+	Game_Vertices = vertices;
+
+	/* Then actually draw the transluscent blocks */
+	Gfx_SetAlphaBlending(true);
+	Gfx_DepthOnlyRendering(false);
+	Gfx_SetDepthWrite(false); /* already calculated depth values in depth pass */
+
+	Gfx_EnableMipmaps();
+	for (batch = 0; batch < MapRenderer_1DUsedCount; batch++) {
+		if (tranPartsCount[batch] <= 0) continue;
+		if (!hasTranParts[batch]) continue;
+		Gfx_BindTexture(Atlas1D.TexIds[batch]);
+		RenderTranslucentBatch(batch);
+	}
+	Gfx_DisableMipmaps();
+
+	Gfx_SetDepthWrite(true);
+	/* If we weren't under water, render weather after to blend properly */
+	if (!inTranslucent && Env.Weather != WEATHER_SUNNY) {
+		Gfx_SetAlphaTest(true);
+		EnvRenderer_RenderWeather(delta);
+		Gfx_SetAlphaTest(false);
+	}
+	Gfx_SetAlphaBlending(false);
+}
+
+
+/*########################################################################################################################*
+*---------------------------------------------------Chunk functionality---------------------------------------------------*
+*#########################################################################################################################*/
+/* Deletes vertex buffer associated with the given chunk and updates internal state */
+static void DeleteChunk(struct ChunkInfo* info) {
+	struct ChunkPartInfo* ptr;
+	int i;
+#ifdef CC_BUILD_GL11
+	int j;
+#else
+	Gfx_DeleteVb(&info->vb);
+#endif
+
+	info->empty  = false; 
+	info->allAir = false;
+	info->noData = true;
+#ifdef OCCLUSION
+	info.OcclusionFlags = 0;
+	info.OccludedFlags = 0;
+#endif
+
+	if (info->normalParts) {
+		ptr = info->normalParts;
+		for (i = 0; i < MapRenderer_1DUsedCount; i++, ptr += chunksCount) {
+			if (ptr->offset < 0) continue; 
+			normPartsCount[i]--;
+#ifdef CC_BUILD_GL11
+			for (j = 0; j < CHUNKPART_MAX_VBS; j++) Gfx_DeleteVb(&ptr->vbs[j]);
+#endif
+		}
+		info->normalParts = NULL;
+	}
+
+	if (info->translucentParts) {
+		ptr = info->translucentParts;
+		for (i = 0; i < MapRenderer_1DUsedCount; i++, ptr += chunksCount) {
+			if (ptr->offset < 0) continue;
+			tranPartsCount[i]--;
+#ifdef CC_BUILD_GL11
+			for (j = 0; j < CHUNKPART_MAX_VBS; j++) Gfx_DeleteVb(&ptr->vbs[j]);
+#endif
+		}
+		info->translucentParts = NULL;
+	}
+}
+
+/* Builds the mesh (hence vertex buffer) for the given chunk, and updates internal state */
+static void BuildChunk(struct ChunkInfo* info, int* chunkUpdates) {
+	struct ChunkPartInfo* ptr;
+	int i;
+
+	Game.ChunkUpdates++;
+	(*chunkUpdates)++;
+	Builder_MakeChunk(info);
+
+	info->dirty  = false;
+	info->noData = !info->normalParts && !info->translucentParts;
+	info->empty  = info->noData;
+	if (info->empty) return;
+	
+	if (info->normalParts) {
+		ptr = info->normalParts;
+		for (i = 0; i < MapRenderer_1DUsedCount; i++, ptr += chunksCount) {
+			if (ptr->offset >= 0) normPartsCount[i]++;
+		}
+	}
+
+	if (info->translucentParts) {
+		ptr = info->translucentParts;
+		for (i = 0; i < MapRenderer_1DUsedCount; i++, ptr += chunksCount) {
+			if (ptr->offset >= 0) tranPartsCount[i]++;
+		}
+	}
+}
+
+
+/*########################################################################################################################*
+*----------------------------------------------------Chunks mangagement---------------------------------------------------*
+*#########################################################################################################################*/
+static void FreeParts(void) {
+	Mem_Free(MapRenderer_PartsNormal);
+	MapRenderer_PartsNormal      = NULL;
+	MapRenderer_PartsTranslucent = NULL;
+}
+
+static void FreeChunks(void) {
+	Mem_Free(mapChunks);
+	Mem_Free(sortedChunks);
+	Mem_Free(renderChunks);
+	Mem_Free(distances);
+
+	mapChunks    = NULL;
+	sortedChunks = NULL;
+	renderChunks = NULL;
+	distances    = NULL;
+}
+
+static void AllocateParts(void) {
+	struct ChunkPartInfo* ptr;
+	cc_uint32 count = chunksCount * MapRenderer_1DUsedCount;
+
+	ptr = (struct ChunkPartInfo*)Mem_AllocCleared(count * 2, sizeof(struct ChunkPartInfo), "chunk parts");
+	MapRenderer_PartsNormal      = ptr;
+	MapRenderer_PartsTranslucent = ptr + count;
+}
+
+static void AllocateChunks(void) {
+	mapChunks    = (struct ChunkInfo*) Mem_Alloc(chunksCount, sizeof(struct ChunkInfo),  "chunk info");
+	sortedChunks = (struct ChunkInfo**)Mem_Alloc(chunksCount, sizeof(struct ChunkInfo*), "sorted chunk info");
+	renderChunks = (struct ChunkInfo**)Mem_Alloc(chunksCount, sizeof(struct ChunkInfo*), "render chunk info");
+	distances    = (cc_uint32*)Mem_Alloc(chunksCount, 4, "chunk distances");
+}
+
+static void ResetPartFlags(void) {
+	int i;
+	for (i = 0; i < ATLAS1D_MAX_ATLASES; i++) {
+		checkNormParts[i] = true;
+		hasNormParts[i]   = false;
+		checkTranParts[i] = true;
+		hasTranParts[i]   = false;
+	}
+}
+
+static void ResetPartCounts(void) {
+	int i;
+	for (i = 0; i < ATLAS1D_MAX_ATLASES; i++) {
+		normPartsCount[i] = 0;
+		tranPartsCount[i] = 0;
+	}
+}
+
+static void InitChunks(void) {
+	int x, y, z, index = 0;
+	for (z = 0; z < World.Length; z += CHUNK_SIZE) {
+		for (y = 0; y < World.Height; y += CHUNK_SIZE) {
+			for (x = 0; x < World.Width; x += CHUNK_SIZE) {
+				ChunkInfo_Reset(&mapChunks[index], x, y, z);
+				sortedChunks[index] = &mapChunks[index];
+				renderChunks[index] = &mapChunks[index];
+				distances[index]    = 0;
+				index++;
+			}
+		}
+	}
+}
+
+static void ResetChunks(void) {
+	int x, y, z, index = 0;
+	for (z = 0; z < World.Length; z += CHUNK_SIZE) {
+		for (y = 0; y < World.Height; y += CHUNK_SIZE) {
+			for (x = 0; x < World.Width; x += CHUNK_SIZE) {
+				ChunkInfo_Reset(&mapChunks[index], x, y, z);
+				index++;
+			}
+		}
+	}
+}
+
+static void DeleteChunks(void) {
+	int i;
+	if (!mapChunks) return;
+
+	for (i = 0; i < chunksCount; i++) {
+		DeleteChunk(&mapChunks[i]);
+	}
+	ResetPartCounts();
+}
+
+void MapRenderer_Refresh(void) {
+	int oldCount;
+	chunkPos = IVec3_MaxValue();
+
+	if (mapChunks && World.Blocks) {
+		DeleteChunks();
+		ResetChunks();
+
+		oldCount = MapRenderer_1DUsedCount;
+		MapRenderer_1DUsedCount = MapRenderer_UsedAtlases();
+		/* Need to reallocate parts array in this case */
+		if (MapRenderer_1DUsedCount != oldCount) {
+			FreeParts();
+			AllocateParts();
+		}
+	}
+	ResetPartCounts();
+}
+
+/* Refreshes chunks on the border of the map whose y is less than 'maxHeight'. */
+static void RefreshBorderChunks(int maxHeight) {
+	int cx, cy, cz;
+	cc_bool onBorder;
+
+	chunkPos = IVec3_MaxValue();
+	if (!mapChunks || !World.Blocks) return;
+
+	for (cz = 0; cz < World.ChunksZ; cz++) {
+		for (cy = 0; cy < World.ChunksY; cy++) {
+			for (cx = 0; cx < World.ChunksX; cx++) {
+				onBorder = cx == 0 || cz == 0 || cx == (World.ChunksX - 1) || cz == (World.ChunksZ - 1);
+
+				if (onBorder && (cy * CHUNK_SIZE) < maxHeight) {
+					MapRenderer_RefreshChunk(cx, cy, cz);
+				}
+			}
+		}
+	}
+}
+
+
+/*########################################################################################################################*
+*--------------------------------------------------Chunks updating/sorting------------------------------------------------*
+*#########################################################################################################################*/
+#define CHUNK_TARGET_TIME ((1.0f/30) + 0.01f)
+static int chunksTarget = 12;
+static Vec3 lastCamPos;
+static float lastYaw, lastPitch;
+/* Max distance from camera that chunks are rendered within */
+/* This may differ from the view distance configured by the user */
+static int renderDistSquared;
+/* Max distance from camera that chunks are built within */
+/* Chunks past this distance are automatically unloaded */
+static int buildDistSquared;
+
+static int AdjustDist(int dist) {
+	if (dist < CHUNK_SIZE) dist = CHUNK_SIZE;
+	dist = Utils_AdjViewDist(dist);
+	return (dist + 24) * (dist + 24);
+}
+
+static void CalcViewDists(void) {
+	buildDistSquared  = AdjustDist(Game_UserViewDistance);
+	renderDistSquared = AdjustDist(Game_ViewDistance);
+}
+
+static int UpdateChunksAndVisibility(int* chunkUpdates) {
+	int renderDistSqr = renderDistSquared;
+	int buildDistSqr  = buildDistSquared;
+
+	struct ChunkInfo* info;
+	int i, j = 0, distSqr;
+	cc_bool noData;
+
+	for (i = 0; i < chunksCount; i++) {
+		info = sortedChunks[i];
+		if (info->empty) continue;
+
+		distSqr = distances[i];
+		noData  = info->noData;
+		
+		/* Auto unload chunks far away chunks */
+		if (!noData && distSqr >= buildDistSqr + 32 * 16) {
+			DeleteChunk(info); continue;
+		}
+		noData |= info->dirty;
+
+		if (noData && distSqr <= buildDistSqr && *chunkUpdates < chunksTarget) {
+			DeleteChunk(info);
+			BuildChunk(info, chunkUpdates);
+		}
+
+		info->visible = distSqr <= renderDistSqr &&
+			FrustumCulling_SphereInFrustum(info->centreX, info->centreY, info->centreZ, 14); /* 14 ~ sqrt(3 * 8^2) */
+		if (info->visible && !info->empty) { renderChunks[j] = info; j++; }
+	}
+	return j;
+}
+
+static int UpdateChunksStill(int* chunkUpdates) {
+	int renderDistSqr = renderDistSquared;
+	int buildDistSqr  = buildDistSquared;
+
+	struct ChunkInfo* info;
+	int i, j = 0, distSqr;
+	cc_bool noData;
+
+	for (i = 0; i < chunksCount; i++) {
+		info = sortedChunks[i];
+		if (info->empty) continue;
+
+		distSqr = distances[i];
+		noData  = info->noData;
+
+		/* Auto unload chunks far away chunks */
+		if (!noData && distSqr >= buildDistSqr + 32 * 16) {
+			DeleteChunk(info); continue;
+		}
+		noData |= info->dirty;
+
+		if (noData && distSqr <= buildDistSqr && *chunkUpdates < chunksTarget) {
+			DeleteChunk(info);
+			BuildChunk(info, chunkUpdates);
+
+			/* only need to update the visibility of chunks in range. */
+			info->visible = distSqr <= renderDistSqr &&
+				FrustumCulling_SphereInFrustum(info->centreX, info->centreY, info->centreZ, 14); /* 14 ~ sqrt(3 * 8^2) */
+			if (info->visible && !info->empty) { renderChunks[j] = info; j++; }
+		} else if (info->visible) {
+			renderChunks[j] = info; j++;
+		}
+	}
+	return j;
+}
+
+static void UpdateChunks(float delta) {
+	struct LocalPlayer* p;
+	cc_bool samePos;
+	int chunkUpdates = 0;
+
+	/* Build more chunks if 30 FPS or over, otherwise slowdown */
+	chunksTarget += delta < CHUNK_TARGET_TIME ? 1 : -1; 
+	Math_Clamp(chunksTarget, 4, maxChunkUpdates);
+
+	p = Entities.CurPlayer;
+	samePos = Vec3_Equals(&Camera.CurrentPos, &lastCamPos)
+		&& p->Base.Pitch == lastPitch && p->Base.Yaw == lastYaw;
+
+	renderChunksCount = samePos ?
+		UpdateChunksStill(&chunkUpdates) :
+		UpdateChunksAndVisibility(&chunkUpdates);
+
+	lastCamPos = Camera.CurrentPos;
+	lastPitch  = p->Base.Pitch;
+	lastYaw    = p->Base.Yaw;
+
+	if (!samePos || chunkUpdates) ResetPartFlags();
+}
+
+static void SortMapChunks(int left, int right) {
+	struct ChunkInfo** values = sortedChunks; struct ChunkInfo* value;
+	cc_uint32* keys = distances; cc_uint32 key;
+
+	while (left < right) {
+		int i = left, j = right;
+		cc_uint32 pivot = keys[(i + j) >> 1];
+
+		/* partition the list */
+		while (i <= j) {
+			while (pivot > keys[i]) i++;
+			while (pivot < keys[j]) j--;
+			QuickSort_Swap_KV_Maybe();
+		}
+		/* recurse into the smaller subset */
+		QuickSort_Recurse(SortMapChunks)
+	}
+}
+
+static void UpdateSortOrder(void) {
+	struct ChunkInfo* info;
+	IVec3 pos;
+	int i, dx, dy, dz;
+
+	/* pos is centre coordinate of chunk camera is in */
+	IVec3_Floor(&pos, &Camera.CurrentPos);
+	pos.x = (pos.x & ~CHUNK_MASK) + HALF_CHUNK_SIZE;
+	pos.y = (pos.y & ~CHUNK_MASK) + HALF_CHUNK_SIZE;
+	pos.z = (pos.z & ~CHUNK_MASK) + HALF_CHUNK_SIZE;
+
+	/* If in same chunk, don't need to recalculate sort order */
+	if (pos.x == chunkPos.x && pos.y == chunkPos.y && pos.z == chunkPos.z) return;
+	chunkPos = pos;
+	if (!chunksCount) return;
+
+	for (i = 0; i < chunksCount; i++) {
+		info = sortedChunks[i];
+		/* Calculate distance to chunk centre */
+		dx = info->centreX - pos.x; dy = info->centreY - pos.y; dz = info->centreZ - pos.z;
+		distances[i] = dx * dx + dy * dy + dz * dz;
+
+		/* Consider these 3 chunks: */
+		/* |       X-1      |        X        |       X+1      | */
+		/* |################|########@########|################| */
+		/* Assume the player is standing at @, then DrawXMin/XMax is calculated as this */
+		/*    X-1: DrawXMin = false, DrawXMax = true  */
+		/*    X  : DrawXMin = true,  DrawXMax = true  */
+		/*    X+1: DrawXMin = true,  DrawXMax = false */
+
+		info->drawXMin = dx >= 0; info->drawXMax = dx <= 0;
+		info->drawZMin = dz >= 0; info->drawZMax = dz <= 0;
+		info->drawYMin = dy >= 0; info->drawYMax = dy <= 0;
+	}
+
+	SortMapChunks(0, chunksCount - 1);
+	ResetPartFlags();
+	/*SimpleOcclusionCulling();*/
+}
+
+void MapRenderer_Update(float delta) {
+	if (!mapChunks) return;
+	UpdateSortOrder();
+	UpdateChunks(delta);
+}
+
+
+/*########################################################################################################################*
+*---------------------------------------------------------General---------------------------------------------------------*
+*#########################################################################################################################*/
+void MapRenderer_RefreshChunk(int cx, int cy, int cz) {
+	struct ChunkInfo* info;
+	if (cx < 0 || cy < 0 || cz < 0 || cx >= World.ChunksX || cy >= World.ChunksY || cz >= World.ChunksZ) return;
+
+	info = &mapChunks[World_ChunkPack(cx, cy, cz)];
+	if (info->allAir) return; /* do not recreate chunks completely air */
+	info->empty = false;
+	info->dirty = true;
+}
+
+void MapRenderer_OnBlockChanged(int x, int y, int z, BlockID block) {
+	int cx = x >> CHUNK_SHIFT, cy = y >> CHUNK_SHIFT, cz = z >> CHUNK_SHIFT;
+	struct ChunkInfo* chunk;
+
+	chunk = &mapChunks[World_ChunkPack(cx, cy, cz)];
+	chunk->allAir &= Blocks.Draw[block] == DRAW_GAS;
+	/* TODO: Don't lookup twice, refresh directly using chunk pointer */
+	MapRenderer_RefreshChunk(cx, cy, cz);
+}
+
+static void OnEnvVariableChanged(void* obj, int envVar) {
+	if (envVar == ENV_VAR_SUN_COLOR || envVar == ENV_VAR_SHADOW_COLOR) {
+		MapRenderer_Refresh();
+	} else if (envVar == ENV_VAR_EDGE_HEIGHT || envVar == ENV_VAR_SIDES_OFFSET) {
+		int oldClip        = Builder_EdgeLevel;
+		Builder_SidesLevel = max(0, Env_SidesHeight);
+		Builder_EdgeLevel  = max(0, Env.EdgeHeight);
+
+		/* Only need to refresh chunks on map borders up to highest edge level.*/
+		RefreshBorderChunks(max(oldClip, Builder_EdgeLevel));
+	}
+}
+
+static void OnTerrainAtlasChanged(void* obj) {
+	static int tilesPerAtlas;
+	/* e.g. If old atlas was 256x256 and new is 256x256, don't need to refresh */
+	if (MapRenderer_1DUsedCount && tilesPerAtlas != Atlas1D.TilesPerAtlas) {
+		MapRenderer_Refresh();
+	}
+
+	MapRenderer_1DUsedCount = MapRenderer_UsedAtlases();
+	tilesPerAtlas = Atlas1D.TilesPerAtlas;
+	ResetPartFlags();
+}
+
+static void OnBlockDefinitionChanged(void* obj) {
+	MapRenderer_Refresh();
+	MapRenderer_1DUsedCount = MapRenderer_UsedAtlases();
+	ResetPartFlags();
+}
+
+static void OnVisibilityChanged(void* obj) {
+	lastCamPos = Vec3_BigPos();
+	CalcViewDists();
+}
+static void DeleteChunks_(void* obj) { DeleteChunks(); }
+static void Refresh_(void* obj)      { MapRenderer_Refresh(); }
+
+static void OnNewMap(void) {
+	Game.ChunkUpdates = 0;
+	DeleteChunks();
+	ResetPartCounts();
+
+	chunkPos = IVec3_MaxValue();
+	FreeChunks();
+	FreeParts();
+}
+
+static void OnNewMapLoaded(void) {
+	chunksCount = World.ChunksCount;
+	/* TODO: Only perform reallocation when map volume has changed */
+	/*if (chunksCount != World.ChunksCount) { */
+		/* chunksCount = World.ChunksCount; */
+		FreeChunks();
+		FreeParts();
+		AllocateChunks();
+		AllocateParts();
+	/*}*/
+
+	InitChunks();
+	lastCamPos = Vec3_BigPos();
+}
+
+static void OnInit(void) {
+	Event_Register_(&TextureEvents.AtlasChanged,  NULL, OnTerrainAtlasChanged);
+	Event_Register_(&WorldEvents.EnvVarChanged,   NULL, OnEnvVariableChanged);
+	Event_Register_(&BlockEvents.BlockDefChanged, NULL, OnBlockDefinitionChanged);
+
+	Event_Register_(&GfxEvents.ViewDistanceChanged, NULL, OnVisibilityChanged);
+	Event_Register_(&GfxEvents.ProjectionChanged,   NULL, OnVisibilityChanged);
+	Event_Register_(&GfxEvents.ContextLost,         NULL, DeleteChunks_);
+	Event_Register_(&GfxEvents.ContextRecreated,    NULL, Refresh_);
+
+	/* This = 87 fixes map being invisible when no textures */
+	MapRenderer_1DUsedCount = 87; /* Atlas1D_UsedAtlasesCount(); */
+	chunkPos   = IVec3_MaxValue();
+	maxChunkUpdates = Options_GetInt(OPT_MAX_CHUNK_UPDATES, 4, 1024, 30);
+	CalcViewDists();
+}
+
+struct IGameComponent MapRenderer_Component = {
+	OnInit, /* Init */
+	OnNewMap, /* Free */
+	OnNewMap, /* Reset */
+	OnNewMap, /* OnNewMap */
+	OnNewMapLoaded /* OnNewMapLoaded */
+};