@@ -26,6 +26,8 @@ const encodeColor = rhi_types.encodeColor;
2626const encodeNormal = rhi_types .encodeNormal ;
2727const encodeMeta = rhi_types .encodeMeta ;
2828const encodeBlocklight = rhi_types .encodeBlocklight ;
29+ const QuadricSimplifier = @import ("meshing/quadric_simplifier.zig" ).QuadricSimplifier ;
30+ const log = @import ("../engine/core/log.zig" );
2931
3032/// Size of each LOD mesh grid cell in blocks
3133pub fn getCellSize (lod : LODLevel ) u32 {
@@ -143,6 +145,94 @@ pub const LODMesh = struct {
143145 }
144146 }
145147
148+ /// Build mesh from simplified LOD data using QEM decimation.
149+ /// Generates a full-detail heightmap mesh first, then simplifies via quadric error metrics.
150+ /// Falls back to naive `buildFromSimplifiedData` if QEM input is too small or fails.
151+ pub fn buildFromSimplifiedDataWithQEM (
152+ self : * LODMesh ,
153+ data : * const LODSimplifiedData ,
154+ world_x : i32 ,
155+ world_z : i32 ,
156+ target_triangles : u32 ,
157+ min_input_triangles : u32 ,
158+ ) ! void {
159+ const full_mesh = buildFullDetailHeightmapMesh (self .allocator , data ) catch | err | {
160+ log .log .warn ("LOD{} full-detail mesh build failed, falling back: {}" , .{ @intFromEnum (self .lod_level ), err });
161+ return self .buildFromSimplifiedData (data , world_x , world_z );
162+ };
163+ defer {
164+ self .allocator .free (full_mesh .vertices );
165+ self .allocator .free (full_mesh .indices );
166+ }
167+
168+ if (full_mesh .indices .len % 3 != 0 ) {
169+ log .log .warn ("LOD{} mesh has invalid index count {}, falling back" , .{ @intFromEnum (self .lod_level ), full_mesh .indices .len });
170+ return self .buildFromSimplifiedData (data , world_x , world_z );
171+ }
172+ const input_triangles : u32 = @intCast (full_mesh .indices .len / 3 );
173+ if (input_triangles < min_input_triangles ) {
174+ return self .buildFromSimplifiedData (data , world_x , world_z );
175+ }
176+
177+ // No simplification needed — target already meets or exceeds input
178+ if (target_triangles >= input_triangles ) {
179+ try self .setPendingFromIndexed (full_mesh .vertices , full_mesh .indices );
180+ return ;
181+ }
182+ const effective_target = target_triangles ;
183+
184+ const simplified = QuadricSimplifier .simplify (
185+ self .allocator ,
186+ full_mesh .vertices ,
187+ full_mesh .indices ,
188+ effective_target ,
189+ ) catch | err | {
190+ log .log .warn ("LOD{} QEM simplification failed, falling back to naive: {}" , .{ @intFromEnum (self .lod_level ), err });
191+ return self .buildFromSimplifiedData (data , world_x , world_z );
192+ };
193+ defer {
194+ self .allocator .free (simplified .vertices );
195+ self .allocator .free (simplified .indices );
196+ }
197+
198+ if (simplified .indices .len == 0 ) {
199+ return self .buildFromSimplifiedData (data , world_x , world_z );
200+ }
201+
202+ log .log .debug ("LOD{} QEM: {} -> {} triangles (error={d:.2})" , .{
203+ @intFromEnum (self .lod_level ),
204+ simplified .original_triangle_count ,
205+ simplified .simplified_triangle_count ,
206+ simplified .error_estimate ,
207+ });
208+
209+ self .setPendingFromIndexed (simplified .vertices , simplified .indices ) catch | err | {
210+ log .log .warn ("LOD{} failed to expand simplified mesh, falling back: {}" , .{ @intFromEnum (self .lod_level ), err });
211+ return self .buildFromSimplifiedData (data , world_x , world_z );
212+ };
213+ }
214+
215+ /// Convert indexed triangle mesh to non-indexed vertex list and store as pending.
216+ fn setPendingFromIndexed (self : * LODMesh , vertices : []const Vertex , indices : []const u32 ) ! void {
217+ self .mutex .lock ();
218+ defer self .mutex .unlock ();
219+
220+ if (self .pending_vertices ) | p | {
221+ self .allocator .free (p );
222+ self .pending_vertices = null ;
223+ }
224+
225+ if (indices .len == 0 ) return ;
226+
227+ const expanded = try self .allocator .alloc (Vertex , indices .len );
228+ for (expanded , 0.. ) | * dst , i | {
229+ const idx = indices [i ];
230+ if (idx >= vertices .len ) return error .InvalidIndex ;
231+ dst .* = vertices [idx ];
232+ }
233+ self .pending_vertices = expanded ;
234+ }
235+
146236 /// Build mesh from full chunk heightmap data
147237 pub fn buildFromHeightmap (
148238 self : * LODMesh ,
@@ -266,6 +356,171 @@ pub const LODMesh = struct {
266356 }
267357};
268358
359+ const FullDetailMesh = struct {
360+ vertices : []Vertex ,
361+ indices : []u32 ,
362+ };
363+
364+ /// Build a full-detail indexed triangle mesh from LOD heightmap data.
365+ /// Produces fine-grained quads with per-vertex heights suitable for QEM simplification.
366+ /// The mesh uses 1-block resolution: each cell in the heightmap grid becomes a quad
367+ /// subdivided into 2 triangles with separate indices for QEM edge collapse.
368+ fn appendIndexedQuad (
369+ vertices : * std .ArrayListUnmanaged (Vertex ),
370+ indices : * std .ArrayListUnmanaged (u32 ),
371+ allocator : std.mem.Allocator ,
372+ quad : * const [4 ]Vertex ,
373+ ) ! void {
374+ const base : u32 = @intCast (vertices .items .len );
375+ try vertices .appendSlice (allocator , quad );
376+ try indices .appendSlice (allocator , &.{
377+ base , base + 1 , base + 2 ,
378+ base , base + 2 , base + 3 ,
379+ });
380+ }
381+
382+ const SkirtParams = struct {
383+ x : f32 ,
384+ z : f32 ,
385+ size : f32 ,
386+ avg_h : f32 ,
387+ avg_c : u32 ,
388+ brightness : f32 ,
389+ dir : SkirtDir ,
390+ };
391+
392+ const SkirtDir = enum { north , south , east , west };
393+
394+ fn makeSkirtQuad (params : SkirtParams ) [4 ]Vertex {
395+ const p = params ;
396+ const cr = unpackR (p .avg_c ) * p .brightness ;
397+ const cg = unpackG (p .avg_c ) * p .brightness ;
398+ const cb = unpackB (p .avg_c ) * p .brightness ;
399+ const skirt_bottom = p .avg_h - p .size * 4.0 ;
400+ const normal : [3 ]f32 = switch (p .dir ) {
401+ .north = > .{ 0 , 0 , -1 },
402+ .south = > .{ 0 , 0 , 1 },
403+ .west = > .{ -1 , 0 , 0 },
404+ .east = > .{ 1 , 0 , 0 },
405+ };
406+ const col = [3 ]f32 { cr , cg , cb };
407+ return switch (p .dir ) {
408+ .north = > .{
409+ makeLODVertex (.{ p .x + p .size , skirt_bottom , p .z }, col , normal , .{ 0 , 0 }),
410+ makeLODVertex (.{ p .x , skirt_bottom , p .z }, col , normal , .{ 1 , 0 }),
411+ makeLODVertex (.{ p .x , p .avg_h , p .z }, col , normal , .{ 1 , 1 }),
412+ makeLODVertex (.{ p .x + p .size , p .avg_h , p .z }, col , normal , .{ 0 , 1 }),
413+ },
414+ .south = > .{
415+ makeLODVertex (.{ p .x , skirt_bottom , p .z + p .size }, col , normal , .{ 0 , 0 }),
416+ makeLODVertex (.{ p .x + p .size , skirt_bottom , p .z + p .size }, col , normal , .{ 1 , 0 }),
417+ makeLODVertex (.{ p .x + p .size , p .avg_h , p .z + p .size }, col , normal , .{ 1 , 1 }),
418+ makeLODVertex (.{ p .x , p .avg_h , p .z + p .size }, col , normal , .{ 0 , 1 }),
419+ },
420+ .west = > .{
421+ makeLODVertex (.{ p .x , skirt_bottom , p .z }, col , normal , .{ 0 , 0 }),
422+ makeLODVertex (.{ p .x , skirt_bottom , p .z + p .size }, col , normal , .{ 1 , 0 }),
423+ makeLODVertex (.{ p .x , p .avg_h , p .z + p .size }, col , normal , .{ 1 , 1 }),
424+ makeLODVertex (.{ p .x , p .avg_h , p .z }, col , normal , .{ 0 , 1 }),
425+ },
426+ .east = > .{
427+ makeLODVertex (.{ p .x + p .size , skirt_bottom , p .z + p .size }, col , normal , .{ 0 , 0 }),
428+ makeLODVertex (.{ p .x + p .size , skirt_bottom , p .z }, col , normal , .{ 1 , 0 }),
429+ makeLODVertex (.{ p .x + p .size , p .avg_h , p .z }, col , normal , .{ 1 , 1 }),
430+ makeLODVertex (.{ p .x + p .size , p .avg_h , p .z + p .size }, col , normal , .{ 0 , 1 }),
431+ },
432+ };
433+ }
434+
435+ fn buildFullDetailHeightmapMesh (
436+ allocator : std.mem.Allocator ,
437+ data : * const LODSimplifiedData ,
438+ ) ! FullDetailMesh {
439+ const w = data .width ;
440+ const grid_total = w * w ;
441+ if (grid_total == 0 ) return error .EmptyData ;
442+ std .debug .assert (w <= data .heightmap .len and w <= data .biomes .len );
443+
444+ const region_size_32 : u32 = 32 ;
445+ const cell_size : u32 = if (w > 0 and w <= region_size_32 ) region_size_32 / w else 2 ;
446+ std .debug .assert (cell_size >= 1 );
447+
448+ var vertices = std .ArrayListUnmanaged (Vertex ){};
449+ errdefer vertices .deinit (allocator );
450+ var indices = std .ArrayListUnmanaged (u32 ){};
451+ errdefer indices .deinit (allocator );
452+
453+ var gz : u32 = 0 ;
454+ while (gz < w ) : (gz += 1 ) {
455+ var gx : u32 = 0 ;
456+ while (gx < w ) : (gx += 1 ) {
457+ const h00 = data .heightmap [gx + gz * w ];
458+ const h10 = if (gx + 1 < w ) data .heightmap [(gx + 1 ) + gz * w ] else h00 ;
459+ const h01 = if (gz + 1 < w ) data .heightmap [gx + (gz + 1 ) * w ] else h00 ;
460+ const h11 = if (gx + 1 < w and gz + 1 < w ) data .heightmap [(gx + 1 ) + (gz + 1 ) * w ] else h00 ;
461+
462+ const c00 = biome_mod .getBiomeColor (data .biomes [gx + gz * w ]);
463+ const c10 = if (gx + 1 < w ) biome_mod .getBiomeColor (data .biomes [(gx + 1 ) + gz * w ]) else c00 ;
464+ const c01 = if (gz + 1 < w ) biome_mod .getBiomeColor (data .biomes [gx + (gz + 1 ) * w ]) else c00 ;
465+ const c11 = if (gx + 1 < w and gz + 1 < w ) biome_mod .getBiomeColor (data .biomes [(gx + 1 ) + (gz + 1 ) * w ]) else c00 ;
466+
467+ const wx : f32 = @floatFromInt (gx * cell_size );
468+ const wz : f32 = @floatFromInt (gz * cell_size );
469+ const size : f32 = @floatFromInt (cell_size );
470+
471+ const top_quad = [4 ]Vertex {
472+ makeLODVertex (.{ wx , h00 , wz }, .{ unpackR (c00 ), unpackG (c00 ), unpackB (c00 ) }, .{ 0 , 1 , 0 }, .{ 0 , 0 }),
473+ makeLODVertex (.{ wx + size , h10 , wz }, .{ unpackR (c10 ), unpackG (c10 ), unpackB (c10 ) }, .{ 0 , 1 , 0 }, .{ 1 , 0 }),
474+ makeLODVertex (.{ wx + size , h11 , wz + size }, .{ unpackR (c11 ), unpackG (c11 ), unpackB (c11 ) }, .{ 0 , 1 , 0 }, .{ 1 , 1 }),
475+ makeLODVertex (.{ wx , h01 , wz + size }, .{ unpackR (c01 ), unpackG (c01 ), unpackB (c01 ) }, .{ 0 , 1 , 0 }, .{ 0 , 1 }),
476+ };
477+ try appendIndexedQuad (& vertices , & indices , allocator , & top_quad );
478+
479+ if (gz == 0 ) try appendIndexedQuad (& vertices , & indices , allocator , & makeSkirtQuad (.{
480+ .x = wx ,
481+ .z = wz ,
482+ .size = size ,
483+ .avg_h = (h00 + h10 ) * 0.5 ,
484+ .avg_c = averageColor (c00 , c10 , c00 , c10 ),
485+ .brightness = 0.7 ,
486+ .dir = .north ,
487+ }));
488+ if (gz == w - 1 ) try appendIndexedQuad (& vertices , & indices , allocator , & makeSkirtQuad (.{
489+ .x = wx ,
490+ .z = wz ,
491+ .size = size ,
492+ .avg_h = (h01 + h11 ) * 0.5 ,
493+ .avg_c = averageColor (c01 , c11 , c01 , c11 ),
494+ .brightness = 0.7 ,
495+ .dir = .south ,
496+ }));
497+ if (gx == 0 ) try appendIndexedQuad (& vertices , & indices , allocator , & makeSkirtQuad (.{
498+ .x = wx ,
499+ .z = wz ,
500+ .size = size ,
501+ .avg_h = (h00 + h01 ) * 0.5 ,
502+ .avg_c = averageColor (c00 , c01 , c00 , c01 ),
503+ .brightness = 0.6 ,
504+ .dir = .west ,
505+ }));
506+ if (gx == w - 1 ) try appendIndexedQuad (& vertices , & indices , allocator , & makeSkirtQuad (.{
507+ .x = wx ,
508+ .z = wz ,
509+ .size = size ,
510+ .avg_h = (h10 + h11 ) * 0.5 ,
511+ .avg_c = averageColor (c10 , c11 , c10 , c11 ),
512+ .brightness = 0.6 ,
513+ .dir = .east ,
514+ }));
515+ }
516+ }
517+
518+ return .{
519+ .vertices = try vertices .toOwnedSlice (allocator ),
520+ .indices = try indices .toOwnedSlice (allocator ),
521+ };
522+ }
523+
269524const FaceDir = enum { north , south , east , west };
270525
271526// Helper functions for unpacking colors
0 commit comments