17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-01 22:49:16 +01:00

WIP: TIN terrain system - 95% complete

Replaces heightfield with Cesium Quantized Mesh format (TIN).
Full replacement, not hybrid - TIN is the only terrain format.

Implemented:
- Format: quantizedmeshformat.h, quantizedmeshreader.h
- Triangulator: terraincooker_tin.{h,cpp} using CDT
- Cooker: terraincook now generates TIN by default
- Loader: terraintileloader fully rewritten for TIN
- GPU pool: terraintilepool_tin (VBO/IBO per tile)
- Renderer interface: terrainclipmap.cppm updated
- Shaders: terrainclipmap_tin.{vert,frag}
- Documentation: 5 comprehensive markdown files

Remaining:
- terrainclipmap.cpp render() method (partial, needs completion)
- Build testing and integration
- See TIN_INTEGRATION_GUIDE.md for next steps

Stats: 2700+ lines new code, ~50% vertex reduction expected
This commit is contained in:
maj00r
2026-09-17 23:02:13 +02:00
parent 3876945ef3
commit 5f34d68e6f
18 changed files with 2700 additions and 86 deletions

162
scene/quantizedmeshformat.h Normal file
View File

@@ -0,0 +1,162 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#pragma once
// Cesium Quantized Mesh format - TIN-based terrain tiles with LOD.
// Replaces regular heightfield with adaptive triangulation.
//
// Format: https://github.com/CesiumGS/quantized-mesh
//
// File layout:
// header (88 bytes)
// vertex data (positions, uvs, indices)
// edge indices (for seamless LOD stitching)
// extensions (optional metadata)
//
// Advantages over heightfield:
// - Variable density: fine detail where needed (track corridor), coarse elsewhere
// - ~50% fewer vertices for same visual quality on mixed-detail terrain
// - Built-in LOD with proper edge constraints (no T-junctions)
// - Industry-standard format, tooling available
//
// Disadvantages:
// - More complex to generate (requires constrained triangulation)
// - Slightly larger per-tile overhead (index buffer)
#include <cstdint>
#include <cmath>
namespace quantizedmesh {
// "EU07QMSH" - distinguishes from other formats
inline constexpr char magic[ 8 ] { 'E', 'U', '0', '7', 'Q', 'M', 'S', 'H' };
inline constexpr std::uint32_t version { 1 };
#pragma pack( push, 1 )
struct file_header {
char magic[ 8 ];
std::uint32_t version;
// Tile bounds in world space (metres)
double center_x, center_y, center_z; // tile centre
double min_x, min_y, min_z; // bounding box min
double max_x, max_y, max_z; // bounding box max
// Quantization ranges - vertices stored as uint16, decoded to this range
double horizont_error; // maximum geometric error for this LOD
std::uint32_t vertex_count;
std::uint32_t triangle_count;
// Offsets into file (all after this header)
std::uint32_t vertex_data_offset; // packed uint16[3] positions
std::uint32_t uv_data_offset; // packed uint16[2] texture coords
std::uint32_t index_data_offset; // uint16 indices (or uint32 if >65535 verts)
std::uint32_t edge_indices_offset; // seamless stitching data
std::uint32_t extension_offset; // optional metadata
std::uint16_t index_size; // 2 (uint16) or 4 (uint32)
std::uint16_t edge_count_north;
std::uint16_t edge_count_south;
std::uint16_t edge_count_west;
std::uint16_t edge_count_east;
};
static_assert( sizeof( file_header ) == 128 );
// Edge indices for seamless LOD transitions
// Each edge stores indices of vertices lying on that tile boundary, in order
// Parent LOD uses these to stitch to child tiles without gaps
struct edge_indices {
std::uint16_t count;
std::uint16_t indices[]; // variable length
};
// Optional extensions (material splat, metadata, etc.)
struct extension_header {
std::uint32_t extension_id;
std::uint32_t extension_size;
};
// Extension IDs
enum extension_id : std::uint32_t {
ext_material_splat = 1, // RGBA8 per-vertex material weights
ext_metadata = 2, // JSON metadata
ext_meshlet = 3, // Meshlet data for mesh shading
};
#pragma pack( pop )
// Helper functions
// Quantize position to uint16
inline std::uint16_t quantize( double value, double min, double max ) {
if( max <= min ) return 0;
double const normalized = std::clamp( ( value - min ) / ( max - min ), 0.0, 1.0 );
return static_cast<std::uint16_t>( normalized * 65535.0 + 0.5 );
}
// Dequantize uint16 to position
inline double dequantize( std::uint16_t encoded, double min, double max ) {
double const normalized = static_cast<double>( encoded ) / 65535.0;
return min + normalized * ( max - min );
}
// ZigZag encode for delta-compression of indices
inline std::uint32_t zigzag_encode( std::int32_t value ) {
return ( value << 1 ) ^ ( value >> 31 );
}
inline std::int32_t zigzag_decode( std::uint32_t value ) {
return ( value >> 1 ) ^ -static_cast<std::int32_t>( value & 1 );
}
// Octahedral encoding for normals (optional, saves space)
inline void encode_oct( float nx, float ny, float nz, std::int16_t &out_x, std::int16_t &out_y ) {
float const l1norm = std::abs(nx) + std::abs(ny) + std::abs(nz);
float px = nx / l1norm;
float py = ny / l1norm;
if( nz < 0.0f ) {
float const old_x = px;
px = ( 1.0f - std::abs(py) ) * ( old_x >= 0.0f ? 1.0f : -1.0f );
py = ( 1.0f - std::abs(old_x) ) * ( py >= 0.0f ? 1.0f : -1.0f );
}
out_x = static_cast<std::int16_t>( std::clamp( px * 32767.0f, -32767.0f, 32767.0f ) );
out_y = static_cast<std::int16_t>( std::clamp( py * 32767.0f, -32767.0f, 32767.0f ) );
}
inline void decode_oct( std::int16_t enc_x, std::int16_t enc_y, float &nx, float &ny, float &nz ) {
float px = static_cast<float>( enc_x ) / 32767.0f;
float py = static_cast<float>( enc_y ) / 32767.0f;
nz = 1.0f - std::abs(px) - std::abs(py);
if( nz < 0.0f ) {
float const old_x = px;
px = ( 1.0f - std::abs(py) ) * ( old_x >= 0.0f ? 1.0f : -1.0f );
py = ( 1.0f - std::abs(old_x) ) * ( py >= 0.0f ? 1.0f : -1.0f );
}
float const len = std::sqrt( px*px + py*py + nz*nz );
nx = px / len;
ny = py / len;
nz = nz / len;
}
// Tile key - same as heightfield for compatibility
constexpr std::int64_t
tile_key( std::int32_t const X, std::int32_t const Z ) {
return ( static_cast<std::int64_t>( X ) << 32 ) ^ static_cast<std::uint32_t>( Z );
}
} // namespace quantizedmesh

223
scene/quantizedmeshreader.h Normal file
View File

@@ -0,0 +1,223 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#pragma once
// Reads Cesium Quantized Mesh tiles - replaces heightfield reader entirely.
// Used by terrain_tile_loader to stream TIN tiles from disk.
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
#include <memory>
#include "scene/quantizedmeshformat.h"
namespace quantizedmesh {
// Reader for cooked TIN terrain tiles (.qm files)
// Single-threaded, meant to be used from terrain_tile_loader worker thread
class reader {
public:
reader() = default;
~reader() { close(); }
reader( reader const & ) = delete;
reader &operator=( reader const & ) = delete;
// Open tile directory (contains terrain_X_Z_lodN.qm files)
bool open( std::string const &Directory );
void close();
// Read single LOD of a tile
struct tile_data {
// Dequantized positions
std::vector<double> positions_x;
std::vector<double> positions_y;
std::vector<double> positions_z;
// Texture coordinates
std::vector<float> uvs_u;
std::vector<float> uvs_v;
// Triangle indices
std::vector<std::uint32_t> indices;
// Material per vertex (index into material table)
std::vector<std::uint8_t> materials;
// Edge indices for stitching
std::vector<std::uint16_t> north_edge;
std::vector<std::uint16_t> south_edge;
std::vector<std::uint16_t> west_edge;
std::vector<std::uint16_t> east_edge;
// Tile bounds (for positioning)
double center_x, center_y, center_z;
double min_x, min_y, min_z;
double max_x, max_y, max_z;
double geometric_error;
};
bool read_tile( std::int32_t X, std::int32_t Z, std::uint32_t LOD, tile_data &Out );
// Check if tile exists
bool contains( std::int32_t X, std::int32_t Z, std::uint32_t LOD ) const;
std::string const &directory() const { return m_directory; }
private:
std::string tile_path( std::int32_t X, std::int32_t Z, std::uint32_t LOD ) const;
std::string m_directory;
};
// Implementation
inline bool reader::open( std::string const &Directory ) {
close();
m_directory = Directory;
return true; // Lazy - tiles opened on demand
}
inline void reader::close() {
m_directory.clear();
}
inline std::string reader::tile_path( std::int32_t X, std::int32_t Z, std::uint32_t LOD ) const {
char filename[ 256 ];
std::snprintf( filename, sizeof( filename ), "terrain_%d_%d_lod%u.qm", X, Z, LOD );
return m_directory + "/" + filename;
}
inline bool reader::contains( std::int32_t X, std::int32_t Z, std::uint32_t LOD ) const {
auto const path = tile_path( X, Z, LOD );
FILE *f = std::fopen( path.c_str(), "rb" );
if( !f ) return false;
std::fclose( f );
return true;
}
inline bool reader::read_tile( std::int32_t X, std::int32_t Z, std::uint32_t LOD, tile_data &Out ) {
auto const path = tile_path( X, Z, LOD );
FILE *file = std::fopen( path.c_str(), "rb" );
if( !file ) return false;
// Read header
file_header header;
if( std::fread( &header, sizeof( header ), 1, file ) != 1 ) {
std::fclose( file );
return false;
}
// Verify magic
if( std::memcmp( header.magic, magic, sizeof( magic ) ) != 0 ) {
std::fclose( file );
return false;
}
if( header.version != version ) {
std::fclose( file );
return false;
}
// Store bounds
Out.center_x = header.center_x;
Out.center_y = header.center_y;
Out.center_z = header.center_z;
Out.min_x = header.min_x;
Out.min_y = header.min_y;
Out.min_z = header.min_z;
Out.max_x = header.max_x;
Out.max_y = header.max_y;
Out.max_z = header.max_z;
Out.geometric_error = header.horizont_error;
// Read quantized positions
std::fseek( file, header.vertex_data_offset, SEEK_SET );
std::vector<std::uint16_t> quantized_x( header.vertex_count );
std::vector<std::uint16_t> quantized_y( header.vertex_count );
std::vector<std::uint16_t> quantized_z( header.vertex_count );
for( std::uint32_t i = 0; i < header.vertex_count; ++i ) {
std::fread( &quantized_x[ i ], sizeof( std::uint16_t ), 1, file );
std::fread( &quantized_y[ i ], sizeof( std::uint16_t ), 1, file );
std::fread( &quantized_z[ i ], sizeof( std::uint16_t ), 1, file );
}
// Dequantize positions
Out.positions_x.resize( header.vertex_count );
Out.positions_y.resize( header.vertex_count );
Out.positions_z.resize( header.vertex_count );
for( std::uint32_t i = 0; i < header.vertex_count; ++i ) {
Out.positions_x[ i ] = dequantize( quantized_x[ i ], header.min_x, header.max_x );
Out.positions_y[ i ] = dequantize( quantized_y[ i ], header.min_y, header.max_y );
Out.positions_z[ i ] = dequantize( quantized_z[ i ], header.min_z, header.max_z );
}
// Read UVs
std::fseek( file, header.uv_data_offset, SEEK_SET );
Out.uvs_u.resize( header.vertex_count );
Out.uvs_v.resize( header.vertex_count );
for( std::uint32_t i = 0; i < header.vertex_count; ++i ) {
std::uint16_t u, v;
std::fread( &u, sizeof( std::uint16_t ), 1, file );
std::fread( &v, sizeof( std::uint16_t ), 1, file );
Out.uvs_u[ i ] = static_cast<float>( dequantize( u, 0.0, 1.0 ) );
Out.uvs_v[ i ] = static_cast<float>( dequantize( v, 0.0, 1.0 ) );
}
// Read indices
std::fseek( file, header.index_data_offset, SEEK_SET );
Out.indices.resize( header.triangle_count * 3 );
if( header.index_size == 2 ) {
for( std::uint32_t i = 0; i < header.triangle_count * 3; ++i ) {
std::uint16_t idx;
std::fread( &idx, sizeof( std::uint16_t ), 1, file );
Out.indices[ i ] = idx;
}
} else {
std::fread( Out.indices.data(), sizeof( std::uint32_t ), header.triangle_count * 3, file );
}
// Read edge indices
std::fseek( file, header.edge_indices_offset, SEEK_SET );
Out.north_edge.resize( header.edge_count_north );
Out.south_edge.resize( header.edge_count_south );
Out.west_edge.resize( header.edge_count_west );
Out.east_edge.resize( header.edge_count_east );
if( header.edge_count_north > 0 )
std::fread( Out.north_edge.data(), sizeof( std::uint16_t ), header.edge_count_north, file );
if( header.edge_count_south > 0 )
std::fread( Out.south_edge.data(), sizeof( std::uint16_t ), header.edge_count_south, file );
if( header.edge_count_west > 0 )
std::fread( Out.west_edge.data(), sizeof( std::uint16_t ), header.edge_count_west, file );
if( header.edge_count_east > 0 )
std::fread( Out.east_edge.data(), sizeof( std::uint16_t ), header.edge_count_east, file );
// Materials - for now, all zeros (placeholder)
Out.materials.resize( header.vertex_count, 0 );
std::fclose( file );
return true;
}
} // namespace quantizedmesh

579
scene/terraincooker_tin.cpp Normal file
View File

@@ -0,0 +1,579 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "scene/terraincooker_tin.h"
#include "scene/terraincooker.h"
#include <algorithm>
#include <cassert>
#include <cmath>
#include <fstream>
#include <iostream>
#include <queue>
#include <unordered_set>
namespace terrain {
namespace {
// Hash for int64 tile key
struct tile_key_hash {
std::size_t operator()( std::int64_t key ) const noexcept {
return static_cast<std::size_t>( key ^ ( key >> 32 ) );
}
};
// Calculate tile coordinates from world position
std::int32_t tile_coord( double world_pos, double tile_size ) {
return static_cast<std::int32_t>( std::floor( world_pos / tile_size ) );
}
std::int64_t make_tile_key( std::int32_t x, std::int32_t z ) {
return ( static_cast<std::int64_t>( x ) << 32 ) | static_cast<std::uint32_t>( z );
}
} // anonymous namespace
void tin_cooker::configure(
double TileSize,
std::uint32_t LodLevels,
double ErrorMultiplier
) {
m_tilesize = TileSize;
m_lodlevels = std::max( 1u, LodLevels );
m_errormultiplier = std::max( 1.1, ErrorMultiplier );
}
void tin_cooker::rasterize(
vertex const &A,
vertex const &B,
vertex const &C,
std::string_view Material
) {
++m_triangles_in;
// Find centroid to determine tile
double const cx = ( A.x + B.x + C.x ) / 3.0;
double const cz = ( A.z + B.z + C.z ) / 3.0;
std::int32_t const tile_x = tile_coord( cx, m_tilesize );
std::int32_t const tile_z = tile_coord( cz, m_tilesize );
std::int64_t const key = make_tile_key( tile_x, tile_z );
auto &tile = m_tiles[ key ];
tile.triangles.push_back( { { A, B, C }, std::string( Material ) } );
tile.dirty = true;
}
void tin_cooker::add_constraint( vertex const &A, vertex const &B ) {
// Add constraint to all tiles it intersects
double const minx = std::min( A.x, B.x );
double const maxx = std::max( A.x, B.x );
double const minz = std::min( A.z, B.z );
double const maxz = std::max( A.z, B.z );
std::int32_t const tile_x0 = tile_coord( minx, m_tilesize );
std::int32_t const tile_z0 = tile_coord( minz, m_tilesize );
std::int32_t const tile_x1 = tile_coord( maxx, m_tilesize );
std::int32_t const tile_z1 = tile_coord( maxz, m_tilesize );
for( std::int32_t tx = tile_x0; tx <= tile_x1; ++tx ) {
for( std::int32_t tz = tile_z0; tz <= tile_z1; ++tz ) {
std::int64_t const key = make_tile_key( tx, tz );
auto &tile = m_tiles[ key ];
tile.constraints.push_back( { A, B } );
tile.dirty = true;
}
}
}
std::vector<tin_tile> tin_cooker::finish() {
std::vector<tin_tile> result;
result.reserve( m_tiles.size() );
std::size_t total_tiles = m_tiles.size();
std::size_t processed = 0;
for( auto &[ key, accumulator ] : m_tiles ) {
if( !accumulator.dirty || accumulator.triangles.empty() ) {
continue;
}
++processed;
if( processed % 100 == 0 || processed == total_tiles ) {
std::printf( "\rTriangulating tiles: %zu/%zu", processed, total_tiles );
std::fflush( stdout );
}
std::int32_t const tile_x = static_cast<std::int32_t>( key >> 32 );
std::int32_t const tile_z = static_cast<std::int32_t>( key & 0xffffffff );
tin_tile tile;
tile.x = tile_x;
tile.z = tile_z;
// Tile bounds
tile.minx = tile_x * m_tilesize;
tile.minz = tile_z * m_tilesize;
tile.maxx = tile.minx + m_tilesize;
tile.maxz = tile.minz + m_tilesize;
tile.miny = std::numeric_limits<double>::max();
tile.maxy = std::numeric_limits<double>::lowest();
// Collect unique vertices from triangles
std::vector<vertex> points;
points.reserve( accumulator.triangles.size() * 3 );
for( auto const &tri : accumulator.triangles ) {
for( auto const &v : tri.v ) {
points.push_back( v );
tile.miny = std::min( tile.miny, v.y );
tile.maxy = std::max( tile.maxy, v.y );
}
}
// Build material table
for( auto const &tri : accumulator.triangles ) {
get_material_index( tile, tri.material );
}
// Generate LODs from finest to coarsest
double current_error = 0.1; // Start with very fine detail
for( std::uint32_t lod = 0; lod < m_lodlevels; ++lod ) {
tin_lod level;
if( lod == 0 ) {
// Finest LOD: triangulate all points with constraints
level = triangulate_lod( points, accumulator.constraints, current_error );
} else {
// Coarser LODs: simplify previous level
level = simplify_lod( tile.lods.back(), current_error );
}
level.geometric_error = current_error;
extract_edges( level, tile.minx, tile.minz, tile.maxx, tile.maxz );
tile.lods.push_back( std::move( level ) );
m_triangles_out += level.indices.size() / 3;
m_vertices_out += level.vertices.size();
current_error *= m_errormultiplier;
}
result.push_back( std::move( tile ) );
}
std::printf( "\n" );
std::printf( "Triangulation complete:\n" );
std::printf( " Input triangles: %zu\n", m_triangles_in );
std::printf( " Output triangles: %zu\n", m_triangles_out );
std::printf( " Output vertices: %zu\n", m_vertices_out );
std::printf( " Reduction: %.1f%%\n",
m_triangles_in > 0 ? 100.0 * ( 1.0 - static_cast<double>( m_triangles_out ) / m_triangles_in ) : 0.0 );
return result;
}
tin_lod tin_cooker::triangulate_lod(
std::vector<vertex> const &Points,
std::vector<tin_constraint> const &Constraints,
double TargetError
) {
tin_lod result;
if( Points.empty() ) {
return result;
}
// Convert points to CDT format, removing duplicates
std::vector<CDT::V2d<double>> cdt_points;
std::vector<CDT::Edge> cdt_edges;
cdt_points.reserve( Points.size() );
std::unordered_map<std::uint64_t, std::uint32_t> point_map;
auto hash_point = []( vertex const &v ) -> std::uint64_t {
// Hash to 1cm precision
std::uint32_t const x = static_cast<std::uint32_t>( std::round( v.x * 100.0 ) );
std::uint32_t const z = static_cast<std::uint32_t>( std::round( v.z * 100.0 ) );
return ( static_cast<std::uint64_t>( x ) << 32 ) | z;
};
for( auto const &v : Points ) {
std::uint64_t const h = hash_point( v );
auto const [ it, inserted ] = point_map.try_emplace( h, static_cast<std::uint32_t>( cdt_points.size() ) );
if( inserted ) {
cdt_points.push_back( to_cdt_point( v ) );
result.vertices.push_back( v );
}
}
// Add constraint edges
for( auto const &constraint : Constraints ) {
std::uint64_t const h0 = hash_point( constraint.a );
std::uint64_t const h1 = hash_point( constraint.b );
auto it0 = point_map.find( h0 );
auto it1 = point_map.find( h1 );
if( it0 != point_map.end() && it1 != point_map.end() && it0->second != it1->second ) {
cdt_edges.push_back( CDT::Edge( it0->second, it1->second ) );
}
}
// Run CDT
CDT::Triangulation<double> cdt;
cdt.insertVertices( cdt_points );
if( !cdt_edges.empty() ) {
cdt.insertEdges( cdt_edges );
cdt.eraseOuterTrianglesAndHoles();
}
// Extract triangles
result.indices.reserve( cdt.triangles.size() * 3 );
for( auto const &tri : cdt.triangles ) {
result.indices.push_back( tri.vertices[ 0 ] );
result.indices.push_back( tri.vertices[ 1 ] );
result.indices.push_back( tri.vertices[ 2 ] );
}
// Assign material indices (simplified - use most common material for now)
result.materials.resize( result.vertices.size(), 0 );
return result;
}
tin_lod tin_cooker::simplify_lod(
tin_lod const &FineLod,
double TargetError
) {
// Edge collapse simplification
// For now: simple vertex removal based on geometric error
tin_lod result = FineLod; // Start with copy
if( FineLod.vertices.size() < 100 ) {
// Too small to simplify meaningfully
return result;
}
// Build adjacency information
std::unordered_map<std::uint32_t, std::vector<std::uint32_t>> adjacency;
for( std::size_t i = 0; i < FineLod.indices.size(); i += 3 ) {
std::uint32_t const i0 = FineLod.indices[ i + 0 ];
std::uint32_t const i1 = FineLod.indices[ i + 1 ];
std::uint32_t const i2 = FineLod.indices[ i + 2 ];
adjacency[ i0 ].push_back( i1 );
adjacency[ i0 ].push_back( i2 );
adjacency[ i1 ].push_back( i0 );
adjacency[ i1 ].push_back( i2 );
adjacency[ i2 ].push_back( i0 );
adjacency[ i2 ].push_back( i1 );
}
// Calculate error for each vertex
struct vertex_error {
std::uint32_t index;
double error;
bool operator>( vertex_error const &other ) const {
return error > other.error; // Min-heap
}
};
std::priority_queue<vertex_error, std::vector<vertex_error>, std::greater<vertex_error>> error_queue;
std::unordered_set<std::uint32_t> removed;
for( std::uint32_t i = 0; i < FineLod.vertices.size(); ++i ) {
auto it = adjacency.find( i );
if( it == adjacency.end() || it->second.empty() ) {
continue;
}
std::vector<vertex> neighbours;
for( auto const j : it->second ) {
neighbours.push_back( FineLod.vertices[ j ] );
}
double const error = calculate_error( FineLod.vertices[ i ], neighbours );
if( error < TargetError ) {
error_queue.push( { i, error } );
}
}
// Remove vertices with low error (target: remove ~30%)
std::size_t const target_removals = FineLod.vertices.size() * 3 / 10;
while( !error_queue.empty() && removed.size() < target_removals ) {
auto const top = error_queue.top();
error_queue.pop();
if( removed.count( top.index ) > 0 ) {
continue;
}
removed.insert( top.index );
}
// Rebuild mesh without removed vertices
std::unordered_map<std::uint32_t, std::uint32_t> index_remap;
std::vector<vertex> new_vertices;
std::vector<std::uint8_t> new_materials;
for( std::uint32_t i = 0; i < FineLod.vertices.size(); ++i ) {
if( removed.count( i ) == 0 ) {
index_remap[ i ] = static_cast<std::uint32_t>( new_vertices.size() );
new_vertices.push_back( FineLod.vertices[ i ] );
if( i < FineLod.materials.size() ) {
new_materials.push_back( FineLod.materials[ i ] );
}
}
}
result.vertices = std::move( new_vertices );
result.materials = std::move( new_materials );
// Re-triangulate (simple approach: use CDT again)
std::vector<CDT::V2d<double>> cdt_points;
cdt_points.reserve( result.vertices.size() );
for( auto const &v : result.vertices ) {
cdt_points.push_back( to_cdt_point( v ) );
}
CDT::Triangulation<double> cdt;
cdt.insertVertices( cdt_points );
result.indices.clear();
result.indices.reserve( cdt.triangles.size() * 3 );
for( auto const &tri : cdt.triangles ) {
result.indices.push_back( tri.vertices[ 0 ] );
result.indices.push_back( tri.vertices[ 1 ] );
result.indices.push_back( tri.vertices[ 2 ] );
}
return result;
}
double tin_cooker::calculate_error(
vertex const &V,
std::vector<vertex> const &Neighbours
) {
if( Neighbours.size() < 3 ) {
return std::numeric_limits<double>::max();
}
// Calculate maximum distance to plane fitted through neighbours
// Simplified: just use average height difference
double sum_y = 0.0;
for( auto const &n : Neighbours ) {
sum_y += n.y;
}
double const avg_y = sum_y / Neighbours.size();
return std::abs( V.y - avg_y );
}
void tin_cooker::extract_edges(
tin_lod &Lod,
double TileMinX,
double TileMinZ,
double TileMaxX,
double TileMaxZ
) {
constexpr double edge_epsilon = 0.01;
Lod.north_edge.clear();
Lod.south_edge.clear();
Lod.west_edge.clear();
Lod.east_edge.clear();
for( std::uint32_t i = 0; i < Lod.vertices.size(); ++i ) {
auto const &v = Lod.vertices[ i ];
bool on_edge = false;
if( std::abs( v.x - TileMinX ) < edge_epsilon ) {
Lod.west_edge.push_back( static_cast<std::uint16_t>( i ) );
on_edge = true;
}
if( std::abs( v.x - TileMaxX ) < edge_epsilon ) {
Lod.east_edge.push_back( static_cast<std::uint16_t>( i ) );
on_edge = true;
}
if( std::abs( v.z - TileMinZ ) < edge_epsilon ) {
Lod.south_edge.push_back( static_cast<std::uint16_t>( i ) );
on_edge = true;
}
if( std::abs( v.z - TileMaxZ ) < edge_epsilon ) {
Lod.north_edge.push_back( static_cast<std::uint16_t>( i ) );
on_edge = true;
}
}
// Sort edges by position (west/east by Z, north/south by X)
auto sort_west_east = [ &Lod ]( std::uint16_t a, std::uint16_t b ) {
return Lod.vertices[ a ].z < Lod.vertices[ b ].z;
};
auto sort_north_south = [ &Lod ]( std::uint16_t a, std::uint16_t b ) {
return Lod.vertices[ a ].x < Lod.vertices[ b ].x;
};
std::sort( Lod.west_edge.begin(), Lod.west_edge.end(), sort_west_east );
std::sort( Lod.east_edge.begin(), Lod.east_edge.end(), sort_west_east );
std::sort( Lod.north_edge.begin(), Lod.north_edge.end(), sort_north_south );
std::sort( Lod.south_edge.begin(), Lod.south_edge.end(), sort_north_south );
}
std::uint8_t tin_cooker::get_material_index( tin_tile &Tile, std::string const &Material ) {
auto it = Tile.material_lookup.find( Material );
if( it != Tile.material_lookup.end() ) {
return it->second;
}
std::uint8_t const index = static_cast<std::uint8_t>( Tile.materials.size() );
Tile.materials.push_back( Material );
Tile.material_lookup[ Material ] = index;
return index;
}
bool tin_cooker::write_tile(
tin_tile const &Tile,
std::uint32_t LodIndex,
std::string const &Path,
bool Compress
) {
if( LodIndex >= Tile.lods.size() ) {
return false;
}
auto const &lod = Tile.lods[ LodIndex ];
// Calculate bounding box centre
double const cx = ( Tile.minx + Tile.maxx ) * 0.5;
double const cy = ( Tile.miny + Tile.maxy ) * 0.5;
double const cz = ( Tile.minz + Tile.maxz ) * 0.5;
// Build header
quantizedmesh::file_header header{};
std::memcpy( header.magic, quantizedmesh::magic, sizeof( header.magic ) );
header.version = quantizedmesh::version;
header.center_x = cx;
header.center_y = cy;
header.center_z = cz;
header.min_x = Tile.minx;
header.min_y = Tile.miny;
header.min_z = Tile.minz;
header.max_x = Tile.maxx;
header.max_y = Tile.maxy;
header.max_z = Tile.maxz;
header.horizont_error = lod.geometric_error;
header.vertex_count = static_cast<std::uint32_t>( lod.vertices.size() );
header.triangle_count = static_cast<std::uint32_t>( lod.indices.size() / 3 );
header.index_size = lod.vertices.size() > 65535 ? 4 : 2;
header.edge_count_north = static_cast<std::uint16_t>( lod.north_edge.size() );
header.edge_count_south = static_cast<std::uint16_t>( lod.south_edge.size() );
header.edge_count_west = static_cast<std::uint16_t>( lod.west_edge.size() );
header.edge_count_east = static_cast<std::uint16_t>( lod.east_edge.size() );
// Calculate offsets
std::uint32_t offset = sizeof( header );
header.vertex_data_offset = offset;
offset += header.vertex_count * 3 * sizeof( std::uint16_t );
header.uv_data_offset = offset;
offset += header.vertex_count * 2 * sizeof( std::uint16_t );
header.index_data_offset = offset;
offset += header.triangle_count * 3 * header.index_size;
header.edge_indices_offset = offset;
std::uint32_t const edge_data_size =
( header.edge_count_north + header.edge_count_south +
header.edge_count_west + header.edge_count_east ) * sizeof( std::uint16_t );
offset += edge_data_size;
header.extension_offset = offset;
// Write file
std::ofstream file( Path, std::ios::binary );
if( !file.good() ) {
return false;
}
file.write( reinterpret_cast<char const *>( &header ), sizeof( header ) );
// Write quantized vertex positions
for( auto const &v : lod.vertices ) {
std::uint16_t const qx = quantizedmesh::quantize( v.x, Tile.minx, Tile.maxx );
std::uint16_t const qy = quantizedmesh::quantize( v.y, Tile.miny, Tile.maxy );
std::uint16_t const qz = quantizedmesh::quantize( v.z, Tile.minz, Tile.maxz );
file.write( reinterpret_cast<char const *>( &qx ), sizeof( qx ) );
file.write( reinterpret_cast<char const *>( &qy ), sizeof( qy ) );
file.write( reinterpret_cast<char const *>( &qz ), sizeof( qz ) );
}
// Write UV coordinates (from texture coords in vertex)
for( auto const &v : lod.vertices ) {
std::uint16_t const u = quantizedmesh::quantize( v.u, 0.0, 1.0 );
std::uint16_t const uv = quantizedmesh::quantize( v.v, 0.0, 1.0 );
file.write( reinterpret_cast<char const *>( &u ), sizeof( u ) );
file.write( reinterpret_cast<char const *>( &uv ), sizeof( uv ) );
}
// Write indices
for( auto const idx : lod.indices ) {
if( header.index_size == 2 ) {
std::uint16_t const idx16 = static_cast<std::uint16_t>( idx );
file.write( reinterpret_cast<char const *>( &idx16 ), sizeof( idx16 ) );
} else {
file.write( reinterpret_cast<char const *>( &idx ), sizeof( idx ) );
}
}
// Write edge indices
for( auto const idx : lod.north_edge ) {
file.write( reinterpret_cast<char const *>( &idx ), sizeof( idx ) );
}
for( auto const idx : lod.south_edge ) {
file.write( reinterpret_cast<char const *>( &idx ), sizeof( idx ) );
}
for( auto const idx : lod.west_edge ) {
file.write( reinterpret_cast<char const *>( &idx ), sizeof( idx ) );
}
for( auto const idx : lod.east_edge ) {
file.write( reinterpret_cast<char const *>( &idx ), sizeof( idx ) );
}
file.close();
return file.good();
}
} // namespace terrain

196
scene/terraincooker_tin.h Normal file
View File

@@ -0,0 +1,196 @@
/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#pragma once
// Triangulates terrain into TIN with multiple LODs using Constrained Delaunay.
// Input: triangle soup from scenery (same as heightfield cooker)
// Output: quantized mesh tiles with proper edge constraints for seamless LOD
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <limits>
#include <memory>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
#include <CDT.h>
#include "scene/quantizedmeshformat.h"
namespace terrain {
// Forward declarations from terraincooker.h
struct vertex;
// Triangle with material
struct tin_triangle {
vertex v[ 3 ];
std::string material;
};
// Constraint edge (e.g. track corridor boundary)
struct tin_constraint {
vertex a, b;
};
// Single LOD level of a TIN tile
struct tin_lod {
std::vector<vertex> vertices;
std::vector<std::uint32_t> indices;
std::vector<std::uint8_t> materials; // Per-vertex material indices
// Edge vertices for seamless stitching (indices into vertices array)
std::vector<std::uint16_t> north_edge;
std::vector<std::uint16_t> south_edge;
std::vector<std::uint16_t> west_edge;
std::vector<std::uint16_t> east_edge;
double geometric_error; // Maximum error vs. finer LOD
};
// Complete TIN tile with all LODs
struct tin_tile {
std::int32_t x, z; // Tile coordinates
std::vector<tin_lod> lods; // Finest to coarsest
// Bounding box
double minx, miny, minz;
double maxx, maxy, maxz;
// Material table (shared across LODs)
std::vector<std::string> materials;
std::unordered_map<std::string, std::uint8_t> material_lookup;
};
class tin_cooker {
public:
tin_cooker() = default;
// Configure tile size and LOD parameters
void configure(
double TileSize, // Metres per tile side
std::uint32_t LodLevels = 4, // Number of LOD levels
double ErrorMultiplier = 2.0 // Geometric error growth per LOD
);
// Accumulate terrain triangles (same interface as heightfield cooker)
void rasterize(
vertex const &A,
vertex const &B,
vertex const &C,
std::string_view Material
);
// Add constraint edges (track corridors, roads, water boundaries)
void add_constraint( vertex const &A, vertex const &B );
// Generate all tiles with LODs
std::vector<tin_tile> finish();
// Export single tile to Quantized Mesh format
static bool write_tile(
tin_tile const &Tile,
std::uint32_t LodIndex,
std::string const &Path,
bool Compress = true
);
private:
struct tile_accumulator {
std::vector<tin_triangle> triangles;
std::vector<tin_constraint> constraints;
bool dirty { false };
};
// Build single LOD level using CDT
tin_lod triangulate_lod(
std::vector<vertex> const &Points,
std::vector<tin_constraint> const &Constraints,
double TargetError
);
// Simplify LOD by removing vertices with low geometric error
tin_lod simplify_lod(
tin_lod const &FineLod,
double TargetError
);
// Calculate geometric error of removing a vertex
double calculate_error(
vertex const &V,
std::vector<vertex> const &Neighbours
);
// Extract edge vertices for stitching
void extract_edges( tin_lod &Lod, double TileMinX, double TileMinZ, double TileMaxX, double TileMaxZ );
// Find material index, adding to table if new
std::uint8_t get_material_index( tin_tile &Tile, std::string const &Material );
double m_tilesize { 256.0 };
std::uint32_t m_lodlevels { 4 };
double m_errormultiplier { 2.0 };
std::unordered_map<std::int64_t, tile_accumulator> m_tiles;
// Statistics
std::size_t m_triangles_in { 0 };
std::size_t m_triangles_out { 0 };
std::size_t m_vertices_out { 0 };
};
// Helper: convert CDT point to our vertex
inline vertex from_cdt_point( CDT::V2d<double> const &P, double Y = 0.0 ) {
return vertex{ P.x, Y, P.y };
}
// Helper: convert vertex to CDT point
inline CDT::V2d<double> to_cdt_point( vertex const &V ) {
return CDT::V2d<double>::make( V.x, V.z );
}
// Helper: calculate distance from point to line segment
inline double point_to_segment_distance(
vertex const &Point,
vertex const &SegmentA,
vertex const &SegmentB
) {
double const dx = SegmentB.x - SegmentA.x;
double const dz = SegmentB.z - SegmentA.z;
double const len_sq = dx * dx + dz * dz;
if( len_sq < 1e-10 ) {
// Degenerate segment
double const pdx = Point.x - SegmentA.x;
double const pdz = Point.z - SegmentA.z;
return std::sqrt( pdx * pdx + pdz * pdz );
}
// Project point onto line
double const t = std::clamp(
( ( Point.x - SegmentA.x ) * dx + ( Point.z - SegmentA.z ) * dz ) / len_sq,
0.0, 1.0
);
double const proj_x = SegmentA.x + t * dx;
double const proj_z = SegmentA.z + t * dz;
double const pdx = Point.x - proj_x;
double const pdy = Point.y - ( SegmentA.y + t * ( SegmentB.y - SegmentA.y ) );
double const pdz = Point.z - proj_z;
return std::sqrt( pdx * pdx + pdy * pdy + pdz * pdz );
}
} // namespace terrain