The World class (src/world.h/cpp) is the core data structure managing all voxel data, lighting, rendering, and persistence. It coordinates chunks, block types, lighting propagation, and the rendering pipeline.
World Class Structure
class World {
public:
Shader * shader;
Player * player;
TextureManager * texture_manager;
std ::vector < BlockType *> block_types;
std ::unordered_map < glm ::ivec3, Chunk * , Util ::IVec3Hash > chunks;
std ::vector < Chunk *> visible_chunks;
// Lighting queues
std ::deque < std ::pair < glm ::ivec3, int >> light_increase_queue;
std ::deque < std ::pair < glm ::ivec3, int >> light_decrease_queue;
std ::deque < std ::pair < glm ::ivec3, int >> skylight_increase_queue;
std ::deque < std ::pair < glm ::ivec3, int >> skylight_decrease_queue;
// Day/night cycle
float daylight = 1800 ;
long time = 0 ;
// Shadow mapping (CSM)
Shader * shadow_shader;
GLuint shadow_fbo, shadow_map;
std ::vector < glm ::mat4 > shadow_matrices;
std ::vector < float > shadow_splits;
Save * save_system;
};
Coordinate Systems
MC-CPP uses three coordinate systems:
1. World Coordinates
Global floating-point coordinates where the player and entities exist.
glm ::vec3 player_position = glm :: vec3 ( 100.5 f , 64.0 f , - 200.3 f );
2. Chunk Coordinates
Integer coordinates identifying which chunk a position belongs to:
glm :: ivec3 World :: get_chunk_pos ( glm :: vec3 pos ) {
return glm :: ivec3 ( floor ( pos . x / 16 ),
floor ( pos . y / 128 ),
floor ( pos . z / 16 ));
}
Chunk dimensions:
X: 16 blocks
Y: 128 blocks (vertical)
Z: 16 blocks
Example:
World position (100, 64, -200) → Chunk (6, 0, -13)
Chunk (0, 0, 0) spans world X: [0, 16), Y: [0, 128), Z: [0, 16)
3. Local (Block) Coordinates
Position within a chunk (0-15 for X/Z, 0-127 for Y):
glm :: ivec3 World :: get_local_pos ( glm :: vec3 pos ) {
int x = ( int ) floor ( pos . x ) % 16 ; if (x < 0 ) x += 16 ;
int y = ( int ) floor ( pos . y ) % 128 ; if (y < 0 ) y += 128 ;
int z = ( int ) floor ( pos . z ) % 16 ; if (z < 0 ) z += 16 ;
return glm :: ivec3 (x, y, z);
}
Example:
World position (100, 64, -200) → Local (4, 64, 8)
The modulo operation handles negative coordinates correctly by adding the chunk dimension when the result is negative.
Chunk Management
Chunk Storage
Chunks are stored in an unordered map keyed by chunk coordinates:
std ::unordered_map < glm ::ivec3, Chunk * , Util ::IVec3Hash > chunks;
The hash function for glm::ivec3 is defined in src/util.h:
struct IVec3Hash {
std :: size_t operator () ( const glm :: ivec3 & v ) const {
return std :: hash < int >()( v . x ) ^ ( std :: hash < int >()( v . y ) << 1 ) ^ ( std :: hash < int >()( v . z ) << 2 );
}
};
Chunk Structure
class Chunk {
public:
World * world;
glm ::ivec3 chunk_position; // Chunk coordinates
glm ::vec3 position; // World position of origin (0,0,0) block
bool modified; // Dirty flag for save system
uint8_t blocks [CHUNK_WIDTH][CHUNK_HEIGHT][CHUNK_LENGTH]; // Block IDs
uint8_t lightmap [CHUNK_WIDTH][CHUNK_HEIGHT][CHUNK_LENGTH]; // Packed light data
std ::map < std ::tuple < int , int , int > , Subchunk *> subchunks;
std ::vector < uint32_t > mesh; // Opaque mesh data
std ::vector < uint32_t > translucent_mesh; // Translucent mesh data
GLuint vao, vbo;
};
Block Storage:
blocks[x][y][z]: 8-bit block ID (0 = air, 1-255 = various blocks)
Total: 16 × 128 × 16 = 32,768 blocks per chunk
Lightmap Storage:
lightmap[x][y][z]: Packed nibbles
High 4 bits: Skylight (0-15)
Low 4 bits: Block light (0-15)
int Chunk :: get_block_light ( glm :: ivec3 pos ) const {
return lightmap [ pos . x ][ pos . y ][ pos . z ] & 0x 0F ; // Mask low 4 bits
}
int Chunk :: get_sky_light ( glm :: ivec3 pos ) const {
return ( lightmap [ pos . x ][ pos . y ][ pos . z ] >> 4 ) & 0x 0F ; // Shift and mask
}
Subchunks
Each chunk is subdivided into 8 vertical subchunks (16×16×16 each) for efficient mesh updates:
Chunk (16×128×16)
├─ Subchunk 0: Y [0, 16)
├─ Subchunk 1: Y [16, 32)
├─ Subchunk 2: Y [32, 48)
├─ Subchunk 3: Y [48, 64)
├─ Subchunk 4: Y [64, 80)
├─ Subchunk 5: Y [80, 96)
├─ Subchunk 6: Y [96, 112)
└─ Subchunk 7: Y [112, 128)
When a block changes, only the affected subchunk(s) are remeshed.
Block Operations
Getting a Block
int World :: get_block_number ( glm :: ivec3 pos ) {
glm ::ivec3 cp = get_chunk_pos ( glm :: vec3 (pos));
if ( chunks . find (cp) == chunks . end ()) return 0 ; // Air if chunk doesn't exist
glm ::ivec3 lp = get_local_pos ( glm :: vec3 (pos));
return chunks [cp]-> blocks [ lp . x ][ lp . y ][ lp . z ];
}
Setting a Block
void World :: set_block ( glm :: ivec3 pos , int number ) {
if ( pos . y < 0 || pos . y >= CHUNK_HEIGHT) return ;
glm ::ivec3 cp = get_chunk_pos ( glm :: vec3 (pos));
if ( chunks . find (cp) == chunks . end ()) {
if (number == 0 ) return ; // Don't create chunk for air
chunks [cp] = new Chunk ( this , cp);
init_skylight ( chunks [cp]);
stitch_sky_light ( chunks [cp]);
stitch_block_light ( chunks [cp]);
}
glm ::ivec3 lp = get_local_pos ( glm :: vec3 (pos));
Chunk * c = chunks [cp];
if ( c -> blocks [ lp . x ][ lp . y ][ lp . z ] == number) return ; // No change
c -> blocks [ lp . x ][ lp . y ][ lp . z ] = number;
c -> modified = true ;
c -> update_at_position (lp);
// Update lighting (block light and skylight)
// ... (see Lighting section below)
// Notify neighboring chunks if on border
if ( lp . x == 0 && chunks . count (cp + Util ::WEST))
chunks [cp + Util ::WEST]-> update_at_position (lp + glm :: ivec3 ( 15 , 0 , 0 ));
// ... (similar for all 6 faces)
}
Key steps:
Create chunk if needed (lazy allocation)
Initialize skylight for new chunks
Update block ID
Mark chunk as modified for save system
Queue mesh update for affected subchunk
Update lighting queues
Notify neighboring chunks if on border
Collision-Checked Placement
bool World :: try_set_block ( glm :: ivec3 pos , int number , const Collider & player_collider ) {
if ( pos . y < 0 || pos . y >= CHUNK_HEIGHT) return false ;
if (number == 0 ) { set_block (pos, 0 ); return true ; } // Always allow breaking
if (number < block_types . size () && block_types [number]) {
for ( const auto & block_col : block_types [number]-> colliders ) {
Collider world_col = block_col + glm :: vec3 (pos);
if (world_col & player_collider) return false ; // AABB collision
}
}
set_block (pos, number);
return true ;
}
Prevents placing blocks inside the player’s AABB.
Lighting System
MC-CPP uses two independent light systems that combine for final shading:
1. Block Light
Sources: Torches, lava, glowstone, etc.
Properties:
15 light levels (0 = dark, 15 = brightest)
Decays by 1 per block in all directions
Blocked by opaque blocks
Light blocks defined in World:
std ::unordered_set < int > light_blocks = { 10 , 11 , 50 , 51 , 62 , 75 };
2. Skylight
Source: The sun (sky)
Properties:
15 light levels at the top of the world
No decay when traveling downward (unless blocked)
Decays by 1 per horizontal block
Affected by day/night cycle
Lighting Propagation
Lighting uses a flood-fill algorithm with separate increase/decrease queues:
std ::deque < std ::pair < glm ::ivec3, int >> light_increase_queue;
std ::deque < std ::pair < glm ::ivec3, int >> light_decrease_queue;
std ::deque < std ::pair < glm ::ivec3, int >> skylight_increase_queue;
std ::deque < std ::pair < glm ::ivec3, int >> skylight_decrease_queue;
Increase Propagation (Block Light)
void World :: propagate_increase ( bool update , int max_steps ) {
int steps_left = (max_steps < 0 ) ? Options ::LIGHT_STEPS_PER_TICK : max_steps;
while ( ! light_increase_queue . empty () && steps_left -- > 0 ) {
auto [pos, level] = light_increase_queue . front ();
light_increase_queue . pop_front ();
for ( auto & d : Util ::DIRECTIONS) { // 6 directions
glm ::ivec3 n = pos + d;
glm ::ivec3 cp = get_chunk_pos ( glm :: vec3 (n));
if ( chunks . find (cp) == chunks . end ()) continue ;
int l = get_light (n);
if ( ! is_opaque_block (n) && l + 2 <= level) {
chunks [cp]-> set_block_light ( get_local_pos ( glm :: vec3 (n)), level - 1 );
light_increase_queue . push_back ({n, level - 1 });
if (update) chunks [cp]-> update_at_position ( get_local_pos ( glm :: vec3 (n)));
}
}
}
}
Algorithm:
Pop position and light level from queue
For each of 6 neighbors:
Skip if chunk doesn’t exist or block is opaque
If neighbor’s light is at least 2 lower, propagate (level - 1)
Add neighbor to queue
Optionally trigger mesh update
Decrease Propagation
When a light source is removed:
void World :: decrease_light ( glm :: ivec3 pos ) {
int old = get_light (pos);
chunks [ get_chunk_pos ( glm :: vec3 (pos))]-> set_block_light ( get_local_pos ( glm :: vec3 (pos)), 0 );
light_decrease_queue . push_back ({pos, old});
propagate_decrease ( true );
propagate_increase ( true ); // Re-fill from remaining sources
}
void World :: propagate_decrease ( bool update , int max_steps ) {
while ( ! light_decrease_queue . empty () && steps_left -- > 0 ) {
auto [pos, level] = light_decrease_queue . front ();
light_decrease_queue . pop_front ();
for ( auto & d : Util ::DIRECTIONS) {
glm ::ivec3 n = pos + d;
int nl = get_light (n);
if (nl == 0 ) continue ;
if (nl < level) {
// Neighbor was lit by this source, remove it
chunks [...]-> set_block_light (..., 0 );
light_decrease_queue . push_back ({n, nl});
} else if (nl >= level) {
// Neighbor is lit by another source, re-propagate
light_increase_queue . push_back ({n, nl});
}
}
}
}
Two-phase removal:
Decrease pass : Clear all light that came from the removed source
Increase pass : Re-propagate from remaining sources to fill gaps
Skylight Initialization
When a chunk is created, skylight is initialized:
void World :: init_skylight ( Chunk * c ) {
glm ::ivec3 global_base = c -> chunk_position * glm :: ivec3 (CHUNK_WIDTH, CHUNK_HEIGHT, CHUNK_LENGTH);
// Vertical pass: Find top solid block per column
for ( int x = 0 ; x < CHUNK_WIDTH; x ++ ) {
for ( int z = 0 ; z < CHUNK_LENGTH; z ++ ) {
int height = - 1 ;
for ( int y = CHUNK_HEIGHT - 1 ; y >= 0 ; y -- ) {
if ( c -> blocks [x][y][z] != 0 && is_opaque_block ({...})) {
height = y;
break ;
}
}
// Fill sunlight above height
for ( int y = CHUNK_HEIGHT - 1 ; y > height; y -- ) {
c -> set_sky_light ({x, y, z}, 15 );
skylight_increase_queue . push_back ({global_base + glm :: ivec3 (x, y, z), 15 });
}
// Fill darkness below height
for ( int y = height; y >= 0 ; y -- ) {
c -> set_sky_light ({x, y, z}, 0 );
}
}
}
// Border pass: Check neighbors for light bleed-in
// ... (check all 4 horizontal borders)
}
Skylight Special Rules
void World :: propagate_skylight_increase ( bool update , int max_steps ) {
// ...
for ( auto & d : Util ::DIRECTIONS) {
glm ::ivec3 n = pos + d;
int decay = 1 ;
if ( d . y == - 1 ) { // Downward
decay = 0 ; // No decay when going down
int block_id = get_block_number (n);
if (block_id != 0 && ! block_types [block_id]-> glass )
decay = 1 ; // Except through non-glass blocks
}
int new_l = level - decay;
// ...
}
}
Downward propagation:
Light level 15 at sky → 15 all the way down (if no obstructions)
Allows sunlight to reach deep caves through open shafts
Day/Night Cycle
void World :: tick ( float dt ) {
time ++ ; // Increments every tick
// 36000 ticks = 1 full day/night cycle
double phase = std :: fmod ( static_cast < double > (time) + 9000.0 , 36000.0 ) / 36000.0 ;
float sun_height = static_cast < float > ( 0.5 * ( std :: sin (phase * glm :: two_pi < double >()) + 1.0 ));
daylight = glm :: mix ( 480.0 f , 1800.0 f , sun_height);
}
float World :: get_daylight_factor () const {
return std :: clamp (daylight / 1800.0 f , 0.0 f , 1.0 f );
}
Daylight values:
480 : Dawn/dusk (dim)
1800 : Noon (bright)
Sinusoidal interpolation creates smooth transitions
Sun direction for shadows:
glm :: vec3 World :: get_light_direction () const {
double phase = std :: fmod ( static_cast < double > (time) + 9000.0 , 36000.0 ) / 36000.0 ;
double azimuth = phase * glm :: two_pi < double >();
float elevation = glm :: mix ( 0.2 f , 0.85 f , static_cast < float > ( 0.5 * ( std :: sin (azimuth) + 1.0 )));
return glm :: normalize ( glm :: vec3 ( static_cast < float > ( std :: cos (azimuth)), - elevation, static_cast < float > ( std :: sin (azimuth))));
}
Rendering Pipeline
1. Prepare Rendering (Visibility Culling)
void World :: prepare_rendering () {
visible_chunks . clear ();
std ::vector < std ::pair < float , Chunk *>> candidates;
for ( auto & kv : chunks) {
glm ::vec3 center = kv . second -> position +
glm :: vec3 (CHUNK_WIDTH * 0.5 f , CHUNK_HEIGHT * 0.5 f , CHUNK_LENGTH * 0.5 f );
float dist2 = glm :: length2 ( player -> position - center);
candidates . push_back ({dist2, kv . second });
}
// Sort farthest to nearest
std :: sort ( candidates . begin (), candidates . end (),
[]( const auto& a , const auto& b ) { return a . first > b . first ; });
for ( auto & c : candidates) visible_chunks . push_back ( c . second );
}
Chunks are sorted farthest to nearest to ensure proper back-to-front rendering of translucent blocks (water, glass, etc.).
2. Shadow Pass
See the Shadow Mapping documentation for detailed CSM implementation.
3. Main Draw
void World :: draw () {
shader -> use ();
shader -> setFloat (shader_daylight_loc, get_daylight_factor ());
shader -> setInt ( shader -> find_uniform ( "u_TextureArraySampler" ), 0 );
// Upload shadow uniforms if enabled
if (shadows_enabled && shadow_map) {
glActiveTexture (GL_TEXTURE1);
glBindTexture (GL_TEXTURE_2D_ARRAY, shadow_map);
shader -> setInt (shader_shadow_map_loc, 1 );
shader -> setMat4Array (shader_light_space_mats_loc, shadow_matrices);
// ...
}
// Draw opaque
for ( auto * c : visible_chunks) c -> draw (GL_TRIANGLES);
// Draw translucent (depth writes disabled)
draw_translucent ();
}
void World :: draw_translucent () {
glDepthMask (GL_FALSE);
for ( auto * c : visible_chunks) c -> draw_translucent (GL_TRIANGLES);
glDepthMask (GL_TRUE);
}
Persistence (Save System)
The Save class (src/save.cpp) handles:
Loading : Read NBT files from save/ directory
Saving : Write modified chunks to disk
Streaming : Load chunks near player, unload distant ones
Terrain Generation : Fallback flat world if no save exists
world . save_system = new Save ( & world);
world . save_system -> load ();
// In game loop
world . save_system -> update_streaming ( player . position );
world . save_system -> stream_next ( 1 ); // Load 1 chunk per frame
// On exit or key press
world . save_system -> save ();
Incremental Lighting
void World :: propagate_increase ( bool update , int max_steps ) {
int steps_left = (max_steps < 0 ) ? Options ::LIGHT_STEPS_PER_TICK : max_steps;
// ...
}
Limits light propagation to a fixed number of steps per tick to avoid frame spikes. Configured in Options::LIGHT_STEPS_PER_TICK.
Lazy Chunk Creation
Chunks are only allocated when non-air blocks are placed:
if ( chunks . find (cp) == chunks . end ()) {
if (number == 0 ) return ; // Don't create chunk for air
chunks [cp] = new Chunk ( this , cp);
}
Subchunk Meshing
Only affected 16×16×16 subchunks are remeshed when blocks change, not the entire 16×128×16 chunk.
Shared Index Buffer
All chunks use a shared index buffer (IBO) for quad-to-triangle conversion:
std ::vector < unsigned int > indices;
for ( int i = 0 ; i < CHUNK_WIDTH * CHUNK_HEIGHT * CHUNK_LENGTH * 8 ; i ++ ) {
indices . insert ( indices . end (), { 4 u * i, 4 u * i + 1 , 4 u * i + 2 , 4 u * i + 2 , 4 u * i + 3 , 4 u * i});
}
glGenBuffers ( 1 , & ibo);
glBufferData (GL_ELEMENT_ARRAY_BUFFER, indices . size () * sizeof ( unsigned int ), indices . data (), GL_STATIC_DRAW);
Game Loop Detailed frame execution and update cycle
Architecture Overview High-level architecture overview