MC-CPP uses a subchunk-based meshing system that subdivides each chunk into smaller units for efficient parallel mesh generation with advanced lighting effects.
Subchunk Architecture
Subchunks are defined in src/chunk/subchunk.h:12-14:
const int SUBCHUNK_WIDTH = 16 ;
const int SUBCHUNK_HEIGHT = 16 ;
const int SUBCHUNK_LENGTH = 16 ;
Each 16×128×16 chunk contains 8 subchunks arranged vertically (1×8×1), where each subchunk manages 4,096 blocks (16³).
Subchunk Class
The Subchunk class (src/chunk/subchunk.h:16-41) handles mesh generation for its region:
class Subchunk {
public:
Chunk * parent;
World * world;
glm ::ivec3 subchunk_position; // Position in subchunk grid (0-0, 0-7, 0-0)
glm ::ivec3 local_position; // Position in chunk-local coords (0-15)
glm ::vec3 position; // Absolute world position
std ::vector < uint32_t > mesh;
std ::vector < uint32_t > translucent_mesh;
void update_mesh ();
private:
// Lighting helpers
float smooth ( float a , float b , float c , float d );
float ao_val ( bool s1 , bool s2 , bool c );
std :: array < float , 4 > get_face_ao (...);
std :: array < float , 4 > get_smooth_face_light (...);
std :: array < glm :: ivec3 , 8 > get_neighbour_voxels ( glm :: ivec3 npos , int face );
// Per-face calculations
std :: array < float , 4 > get_light ( int block , int face , glm :: ivec3 pos , glm :: ivec3 npos );
std :: array < float , 4 > get_skylight ( int block , int face , glm :: ivec3 pos , glm :: ivec3 npos );
std :: array < float , 4 > get_shading ( int block , BlockType & bt , int face , glm :: ivec3 npos );
void add_face ( int face , glm :: ivec3 pos , glm :: ivec3 lpos , int block , BlockType & bt , glm :: ivec3 npos );
bool can_render_face ( BlockType & bt , int block_number , glm :: ivec3 position );
};
Mesh Generation Process
Main Update Loop
The update_mesh() method (src/chunk/subchunk.cpp:199-225) iterates through all blocks:
void Subchunk :: update_mesh () {
mesh . clear ();
translucent_mesh . clear ();
for ( int x = 0 ; x < SUBCHUNK_WIDTH; x ++ )
for ( int y = 0 ; y < SUBCHUNK_HEIGHT; y ++ )
for ( int z = 0 ; z < SUBCHUNK_LENGTH; z ++ ) {
int lx = local_position . x + x;
int ly = local_position . y + y;
int lz = local_position . z + z;
int bn = parent -> blocks [lx][ly][lz];
if ( ! bn) continue ; // Skip air
BlockType & bt = * world -> block_types [bn];
glm ::ivec3 pos = glm :: ivec3 (position) + glm :: ivec3 (x, y, z);
glm ::ivec3 lpos (lx, ly, lz);
if ( bt . is_cube ) {
// Standard 6-face cube meshing
for ( int f = 0 ; f < 6 ; f ++ ) {
glm ::ivec3 npos = pos + Util :: DIRECTIONS [f];
if ( can_render_face (bt, bn, npos))
add_face (f, pos, lpos, bn, bt, npos);
}
} else {
// Non-cube models (plants, torches, etc.) - render all faces
for ( int f = 0 ; f < bt . vertex_positions . size (); f ++ ) {
add_face (f, pos, lpos, bn, bt, pos);
}
}
}
}
Face Culling
The can_render_face() method (src/chunk/subchunk.cpp:186-197) implements intelligent culling:
bool Subchunk :: can_render_face ( BlockType & bt , int block_number , glm :: ivec3 position ) {
int neighbor_id = parent -> get_block_number_cached (position);
if (neighbor_id == 0 ) return true ; // Air - always render
// Glass optimization - don't render faces between same glass blocks
if ( bt . glass && neighbor_id == block_number) return false ;
// Render if neighbor is transparent
BlockType * neighbor_type = world -> block_types [neighbor_id];
if (neighbor_type && neighbor_type -> transparent ) return true ;
return false ; // Opaque neighbor - cull face
}
Vertex Data Packing
Each vertex is packed into 3 × uint32_t (12 bytes total) for GPU efficiency.
Packing Functions
From src/chunk/subchunk.cpp:9-40:
// Position: 16-bit fixed-point (1/16 precision)
inline int16_t pack_pos_component ( float v ) {
int val = static_cast < int > ( std :: round (v * 16.0 f ));
return static_cast < int16_t > ( std :: clamp (val, - 32768 , 32767 ));
}
// UV: 8-bit normalized (0.0-1.0)
inline uint8_t pack_uv ( float v ) {
int val = static_cast < int > ( std :: round (v * 255.0 f ));
return static_cast < uint8_t > ( std :: clamp (val, 0 , 255 ));
}
// Pack into 32-bit words
inline uint32_t pack_pos_xy ( int16_t x , int16_t y ) {
return static_cast < uint16_t > (x) |
( static_cast < uint32_t > ( static_cast < uint16_t > (y)) << 16 );
}
inline uint32_t pack_pos_z_uv ( int16_t z , uint8_t u , uint8_t v ) {
return static_cast < uint16_t > (z) |
( static_cast < uint32_t > (u) << 16 ) |
( static_cast < uint32_t > (v) << 24 );
}
inline uint32_t pack_attr ( uint8_t layer , uint8_t shading ,
uint8_t blocklight , uint8_t skylight ) {
return static_cast < uint32_t > (layer) |
( static_cast < uint32_t > (shading) << 8 ) |
( static_cast < uint32_t > (blocklight & 0x F ) << 16 ) |
( static_cast < uint32_t > (skylight & 0x F ) << 20 );
}
Vertex Layout
Word Bits 0-15 Bits 16-23 Bits 24-31 0 X position (int16) Y position (int16) - 1 Z position (int16) U coord (uint8) V coord (uint8) 2 Texture layer (uint8) Shading (uint8) Block light (4-bit) + Sky light (4-bit)
Lighting System
Smooth Lighting
When Options::SMOOTH_LIGHTING is enabled, vertex lighting is interpolated from neighboring blocks.
The get_smooth_face_light() method (src/chunk/subchunk.cpp:75-77) averages 4 adjacent samples:
std :: array < float , 4 > Subchunk :: get_smooth_face_light (
float light , float l1 , float l2 , float l3 ,
float l4 , float l5 , float l6 , float l7 , float l8 ) {
return {
smooth (light, l2, l4, l1), // Vertex 0
smooth (light, l4, l7, l6), // Vertex 1
smooth (light, l5, l7, l8), // Vertex 2
smooth (light, l2, l5, l3) // Vertex 3
};
}
The smooth() helper (src/chunk/subchunk.cpp:47-64) handles edge cases where lighting transitions to darkness:
float Subchunk :: smooth ( float a , float b , float c , float d ) {
if (a == 0 || b == 0 || c == 0 || d == 0 ) {
float min_val = a;
if (a > 0 ) {
if (b > 0 ) min_val = std :: min (min_val, b);
if (c > 0 ) min_val = std :: min (min_val, c);
if (d > 0 ) min_val = std :: min (min_val, d);
} else {
min_val = 0.0 f ;
}
a = std :: max (a, min_val);
b = std :: max (b, min_val);
c = std :: max (c, min_val);
d = std :: max (d, min_val);
}
return (a + b + c + d) / 4.0 f ;
}
Ambient Occlusion
AO is calculated from the 8 blocks surrounding each vertex (src/chunk/subchunk.cpp:66-73):
float Subchunk :: ao_val ( bool s1 , bool s2 , bool c ) {
if (s1 && s2) return 0.25 f ; // Both sides blocked - darkest
return 1.0 f - (s1 + s2 + c) / 4.0 f ; // Partial occlusion
}
std :: array < float , 4 > Subchunk :: get_face_ao (
bool s1 , bool s2 , bool s3 , bool s4 ,
bool s5 , bool s6 , bool s7 , bool s8 ) {
return {
ao_val (s2, s4, s1),
ao_val (s4, s7, s6),
ao_val (s5, s7, s8),
ao_val (s2, s5, s3)
};
}
Neighbor Sampling Pattern
For each face, 8 neighbors are sampled in a specific pattern (src/chunk/subchunk.cpp:79-89):
std :: array < glm :: ivec3 , 8 > Subchunk :: get_neighbour_voxels ( glm :: ivec3 npos , int face ) {
using namespace Util ;
std ::array < glm ::ivec3, 8 > n{};
if (face == 0 ) // East (+X)
n = {npos + UP + SOUTH, npos + UP, npos + UP + NORTH,
npos + SOUTH, npos + NORTH,
npos + DOWN + SOUTH, npos + DOWN, npos + DOWN + NORTH};
else if (face == 1 ) // West (-X)
n = {npos + UP + NORTH, npos + UP, npos + UP + SOUTH,
npos + NORTH, npos + SOUTH,
npos + DOWN + NORTH, npos + DOWN, npos + DOWN + SOUTH};
// ... similar for faces 2-5
return n;
}
This pattern ensures consistent vertex ordering for correct AO/lighting interpolation.
Face Addition
The add_face() method (src/chunk/subchunk.cpp:152-184) assembles all vertex data:
void Subchunk :: add_face ( int face , glm :: ivec3 pos , glm :: ivec3 lpos ,
int block , BlockType & bt , glm :: ivec3 npos ) {
auto & target = bt . translucent ? translucent_mesh : mesh;
auto shading = get_shading (block, bt, face, npos);
auto lights = get_light (block, face, pos, npos);
auto skylights = get_skylight (block, face, pos, npos);
bool has_uv = (face < bt . tex_coords . size ());
for ( int i = 0 ; i < 4 ; i ++ ) { // 4 vertices per quad
float vx = bt . vertex_positions [face][i * 3 + 0 ] + lpos . x ;
float vy = bt . vertex_positions [face][i * 3 + 1 ] + lpos . y ;
float vz = bt . vertex_positions [face][i * 3 + 2 ] + lpos . z ;
uint8_t u = 0 , v = 0 ;
if (has_uv) {
u = pack_uv ( bt . tex_coords [face][i * 2 + 0 ]);
v = pack_uv ( bt . tex_coords [face][i * 2 + 1 ]);
}
uint8_t layer = static_cast < uint8_t > ( bt . tex_indices [face]);
uint8_t shade = pack_shading ( shading [i]);
uint8_t bl = static_cast < uint8_t > ( std :: clamp < int >( lights [i], 0 , 15 ));
uint8_t sl = static_cast < uint8_t > ( std :: clamp < int >( skylights [i], 0 , 15 ));
int16_t px = pack_pos_component (vx);
int16_t py = pack_pos_component (vy);
int16_t pz = pack_pos_component (vz);
target . push_back ( pack_pos_xy (px, py));
target . push_back ( pack_pos_z_uv (pz, u, v));
target . push_back ( pack_attr (layer, shade, bl, sl));
}
}
Parallel meshing - Multiple subchunks can mesh simultaneously
Incremental updates - Only affected subchunks remesh when blocks change
Memory locality - Better cache performance during iteration
Early culling - Empty subchunks skip meshing entirely
Reduced bandwidth - 12 bytes/vertex vs 48+ bytes for floats
Better cache utilization - More vertices fit in GPU cache
Smaller VBOs - Typical chunk uses ~100KB vs ~400KB unpacked
Fixed-point precision - Sufficient for block-based geometry
Lighting Calculation Cost
Smooth lighting queries 9 blocks per vertex (1 center + 8 neighbors):
Without smooth lighting : ~1 query per vertex
With smooth lighting : ~9 queries per vertex
Chunk neighbor caching makes these queries nearly free
Debug Tips
If you see missing faces at chunk boundaries , ensure update_at_position() queues neighbor subchunks when blocks change on borders.
To visualize subchunk boundaries, modify add_face() to color-code vertices based on subchunk_position.
Chunk Architecture Learn about chunk storage and neighbor caching
Block Types Understand vertex positions and texture coordinates