/* 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, as the terrain tiles are written. // https://github.com/CesiumGS/quantized-mesh // // The ground in a MaSzyna scenery is already an irregular mesh: fine along the track where it // came from a survey, coarse in the open where it was drawn by hand. The cook keeps that mesh // rather than resampling it - it cuts it into tiles, welds the vertices and builds coarser // levels of it - so there is nothing to interpolate, nothing to fill in where the ground has // holes, and no grid step to pick. // // Tiles form a quadtree, as the format intends: one tile at the root covers the whole terrain, // and each level splits every tile into four. A distant tile is therefore a large one, and the // horizon costs tens of tiles rather than thousands. Every vertex on a tile's border survives // into all of its coarser levels, so two neighbours drawn at different levels share their // border exactly, and nothing has to be stitched or morphed at draw time. // // Where this departs from the specification, and why: // // - the header's fields are geodetic (ECEF centre, bounding sphere, horizon occlusion point). // A scenery has a local metric frame and no geographic reference to convert from, so those // fields carry scenery metres. The geometry reads back correctly anywhere; a Cesium client // would place the tile wrongly on the globe, which is a trade for something we do not want. // - the format has no texture coordinates and no materials: its u and v are the vertex's // horizontal position within the tile. A vertex of ours carries which ground material it // belongs to, in extension ext_ground below, which is what the extension mechanism is for. // Texture coordinates are not stored at all: ground textures tile in world space, so the // coordinate follows from where the vertex is and how many metres one repeat of its material // covers, which is measured from the scenery once and kept in the terrain's table. // - tiles are stored inside the scenery's archive, one zstd-compressed entry each, in place of // the gzip the format expects from an HTTP layer. The bytes of a tile are the format's. #include #include #include #include #include namespace quantizedmesh { // u, v and height are quantized to this, not to 65535: the format says so inline constexpr std::uint16_t quantum_max { 32767 }; // what the cook counted as ground, and how it cut and simplified it. tiles cooked under other // rules are baked again rather than read inline constexpr std::uint32_t cook_rules { 2 }; #pragma pack( push, 1 ) // the format's header, unchanged. 88 bytes struct file_header { double centre_x, centre_y, centre_z; float lowest, highest; // the tile's height range, in metres double sphere_x, sphere_y, sphere_z; // bounding sphere, for culling double sphere_radius; double horizon_x, horizon_y, horizon_z; // horizon occlusion point; unused here }; static_assert( sizeof( file_header ) == 88 ); #pragma pack( pop ) // extension ids. 1, 2 and 4 are taken by the format for normals, the water mask and metadata enum extension_id : std::uint8_t { ext_normals = 1, ext_watermask = 2, ext_metadata = 4, // ours: one uint16 per vertex, its material's place in the terrain's table ext_ground = 64, }; // zigzag, as the format uses it on the deltas between consecutive vertices inline std::uint16_t zigzag_encode( std::int32_t const Value ) { return static_cast( ( Value << 1 ) ^ ( Value >> 31 ) ); } inline std::int32_t zigzag_decode( std::uint16_t const Stored ) { return ( Stored >> 1 ) ^ -static_cast( Stored & 1 ); } // a quantized coordinate back to metres, against the range it was quantized in inline double dequantize( std::uint16_t const Stored, double const Low, double const Span ) { return Low + ( static_cast( Stored ) / quantum_max ) * Span; } inline std::uint16_t quantize( double const Value, double const Low, double const Span ) { if( Span <= 0.0 ) { return 0; } auto const share { std::clamp( ( Value - Low ) / Span, 0.0, 1.0 ) }; return static_cast( share * quantum_max + 0.5 ); } // Indices are stored high-water-mark encoded: a code of zero means the next index never used // before, and anything else counts back from the highest index used so far. The run of codes is // small numbers whatever the mesh looks like, which is what makes the tile compress. template void highwater_encode( std::vector const &Indices, std::vector &Out ) { Out.clear(); Out.reserve( Indices.size() ); std::int64_t highest { 0 }; for( auto const index : Indices ) { Out.push_back( static_cast( highest - static_cast( index ) ) ); if( static_cast( index ) == highest ) { ++highest; } } } // returns false when a code points past what has been used, which a corrupted tile can do template bool highwater_decode( std::vector const &Codes, std::vector &Out ) { Out.clear(); Out.reserve( Codes.size() ); std::int64_t highest { 0 }; for( auto const code : Codes ) { auto const index { highest - static_cast( code ) }; if( ( index < 0 ) || ( index > highest ) ) { return false; } Out.push_back( static_cast( index ) ); if( index == highest ) { ++highest; } } return true; } // A tile's place in the quadtree. Level 0 is the root, covering the whole terrain; each level // splits every tile into four. X grows east, Z grows with the scenery's z, so a tile's square // follows from the terrain's extent and the level alone. struct tile_address { std::uint32_t level { 0 }; std::int32_t x { 0 }, z { 0 }; bool operator==( tile_address const & ) const = default; }; constexpr std::int64_t tile_key( tile_address const &Tile ) { // a level fits in six bits for any terrain worth cooking, and the coordinates cannot exceed // the level, so this is unique return ( static_cast( Tile.level ) << 58 ) ^ ( static_cast( Tile.x ) << 29 ) ^ static_cast( Tile.z ); } // where a tile's file sits in the archive, following the format's own layout of level, column // and row inline std::string tile_name( tile_address const &Tile ) { return std::to_string( Tile.level ) + "/" + std::to_string( Tile.x ) + "/" + std::to_string( Tile.z ) + ".terrain"; } // the terrain's extent, height range, depth and material table are the same for every tile, so // they are written once beside them, under this name. the format keeps the same thing in // layer.json inline constexpr char const *tablename { "terrain.table" }; // what that file says struct terrain_table { std::uint32_t rules { 0 }; // the square the root tile covers. always square, so that a tile at any level is too double west { 0.0 }, north { 0.0 }, side { 0.0 }; double lowest { 0.0 }, highest { 0.0 }; // levels cooked, root included: addresses run from 0 to levels - 1 std::uint32_t levels { 1 }; // metres the ground of a tile at each level departs from the finest one. what decides which // level to draw: a level is good enough while its error covers few enough pixels std::vector errors; // one ground texture: its name, and how many metres of ground one repeat of it covers, as // measured from the texture coordinates the scenery drew it with struct material { std::string name; float repeat { 4.f }; }; // in the order the vertices' material indices count std::vector materials; // metres along the side of a tile at this level double tilesize( std::uint32_t const Level ) const { return side / static_cast( 1u << Level ); } float error( std::uint32_t const Level ) const { return Level < errors.size() ? errors[ Level ] : 0.f; } }; } // namespace quantizedmesh