diff --git a/application/driveruipanels.cpp b/application/driveruipanels.cpp index 9548dd1e..579b9619 100644 --- a/application/driveruipanels.cpp +++ b/application/driveruipanels.cpp @@ -773,6 +773,8 @@ debug_panel::render_section_scenario() { (to_string(std::exp(terrainrange), 0, 5) + " m###terrainrange").c_str(), &terrainrange, std::log(500.0f), std::log(100000.0f), "Terrain range")) { Global.TerrainRange = std::clamp(std::exp(terrainrange), 500.0f, 100000.0f); } + // pixels a terrain sample may cover before a finer level takes over + ImGui::SliderFloat("###terraindetail", &Global.TerrainDetail, 1.0f, 8.0f, "Terrain detail: %.1f px per sample"); } } @@ -1524,8 +1526,8 @@ debug_panel::update_section_renderer( std::vector &Output ) { auto const last { level + 1 == field.levels }; levels += " L" + std::to_string( level ) - + ( last ? " beyond " + to_string( terrain_level_distance( level > 0 ? level - 1 : 0, field.tilesize ), 0 ) - : " <" + to_string( terrain_level_distance( level, field.tilesize ), 0 ) ) + + ( last ? " beyond " + to_string( terrain_level_distance( level > 0 ? level - 1 : 0, field.finest ), 0 ) + : " <" + to_string( terrain_level_distance( level, field.finest ), 0 ) ) + " m (" + to_string( field.gridstep * static_cast( 1u << level ), 0 ) + " m grid): " + std::to_string( stats.perlevel[ level ] ); if( false == last ) { levels += " |"; } diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index 6550faa4..4348a156 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -2912,6 +2912,11 @@ void opengl33_renderer::Render_terrain() { // the terrain has a range of its own, not the pass draw range field->range(Global.TerrainRange); + // how far each level reaches follows from what the view can resolve + auto const fovy{glm::radians(Global.FieldOfView / Global.ZoomFactor)}; + field->detail( + static_cast(m_current_viewport->height) / (2.0 * std::tan(fovy * 0.5)), + Global.TerrainDetail); field->update(viewpoint); } field->render(viewpoint); @@ -2932,6 +2937,7 @@ void opengl33_renderer::Render_terrain() status.levels = field.levels(); } status.range = field.range(); + status.finest = field.finest(); status.backlog = field.backlog(); status.stats = field.stats(); } diff --git a/rendering/terrainclipmap.cpp b/rendering/terrainclipmap.cpp index b2fec31d..f193e03b 100644 --- a/rendering/terrainclipmap.cpp +++ b/rendering/terrainclipmap.cpp @@ -208,9 +208,8 @@ terrain_clipmap::build_palette() { std::uint32_t terrain_clipmap::level_for( double const Distance ) const { - auto const finest { terrain_level_distance( 0, m_reader.tilesize() ) }; - if( Distance <= finest ) { return 0; } - auto const steps { static_cast( std::log2( Distance / finest ) ) + 1 }; + if( Distance <= m_finest ) { return 0; } + auto const steps { static_cast( std::log2( Distance / m_finest ) ) + 1 }; return std::min( steps, m_reader.levels() - 1 ); } @@ -265,11 +264,18 @@ terrain_clipmap::retire( resident_tile &Tile ) { m_stats.texturebytes -= std::min( m_stats.texturebytes, bytes ); } +void +terrain_clipmap::detail( double const Pixelsperunit, double const Pixels ) { + + m_finestwanted = terrain_finest_range( m_reader.gridstep(), m_reader.tilesize(), Pixelsperunit, Pixels ); +} + void terrain_clipmap::scan( glm::dvec3 const &Viewpoint ) { m_scanpoint = Viewpoint; m_scanrange = m_range; + m_finest = ( m_finestwanted > 0.0 ? m_finestwanted : terrain_finest_tiles * m_reader.tilesize() ); m_inrange.clear(); m_wantedlevel.clear(); @@ -324,7 +330,12 @@ terrain_clipmap::update( glm::dvec3 const &Viewpoint ) { auto const moved { glm::length( glm::dvec2 { Viewpoint.x - m_scanpoint.x, Viewpoint.z - m_scanpoint.z } ) }; + // a zoom or a resized window changes the levels; a change under a percent is not worth + // redoing the scan for + auto const detailchanged { + ( m_finestwanted > 0.0 ) && ( std::abs( m_finestwanted - m_finest ) > 0.01 * m_finest ) }; if( ( m_range != m_scanrange ) + || ( true == detailchanged ) || ( moved > m_reader.tilesize() * terrain_rescan_tiles ) ) { scan( Viewpoint ); } @@ -446,7 +457,8 @@ terrain_clipmap::report_residency() { WriteLog( "Terrain: " + std::to_string( m_stats.resident ) + " tiles resident, " + std::to_string( m_stats.texturebytes / 1048576 ) + " MB of samples, " - + std::to_string( m_stats.uploaded ) + " uploads so far" ); + + std::to_string( m_stats.uploaded ) + " uploads so far, finest level out to " + + std::to_string( static_cast( m_finest ) ) + " m" ); } void @@ -487,8 +499,8 @@ terrain_clipmap::render( glm::dvec3 const &Viewpoint ) { if( tile.level + 1 < m_reader.levels() ) { ::glUniform2f( m_uniforms.morph, - static_cast( terrain_morph_begin( tile.level, tilesize ) ), - static_cast( terrain_morph_end( tile.level, tilesize ) ) ); + static_cast( terrain_morph_begin( tile.level, m_finest, tilesize ) ), + static_cast( terrain_morph_end( tile.level, m_finest, tilesize ) ) ); } else { // far enough to never be reached, with a band that does not divide by zero diff --git a/rendering/terrainclipmap.cppm b/rendering/terrainclipmap.cppm index ea6c69ae..9a6f4f92 100644 --- a/rendering/terrainclipmap.cppm +++ b/rendering/terrainclipmap.cppm @@ -71,6 +71,11 @@ public: // how far from the viewpoint terrain is drawn, in metres void range( float const Range ) { m_range = Range; } float range() const { return m_range; } + // what the screen can resolve: the viewport's pixels per unit of size over distance, and + // how many pixels a sample spacing may cover. decides how far each level reaches + void detail( double const Pixelsperunit, double const Pixels ); + // the finest level's range the tiles in range were last worked out with + float finest() const { return static_cast( m_finest ); } // metres between samples at the finest level; a scenery's fields are drawn from the // finest to the coarsest so that the depth test keeps the better surface @@ -135,6 +140,10 @@ private: std::unordered_map m_wantedlevel; glm::dvec3 m_scanpoint { 0.0 }; float m_scanrange { -1.f }; + // the finest level's range the screen asks for, and the one the last scan used. levels and + // morphing both follow the scanned one, so they always agree + double m_finestwanted { 0.0 }; + double m_finest { 0.0 }; std::unordered_map m_tiles; // one index buffer per mip level, over a grid of that level's sample count. vertices // are computed in the shader from gl_VertexID, so there is no vertex buffer at all. diff --git a/rendering/terrainstatus.cppm b/rendering/terrainstatus.cppm index 33cd2794..9e886c33 100644 --- a/rendering/terrainstatus.cppm +++ b/rendering/terrainstatus.cppm @@ -8,6 +8,7 @@ http://mozilla.org/MPL/2.0/. */ module; +#include #include #include #include @@ -19,20 +20,29 @@ export module eu07.rendering.terrainstatus; export { -// distance out to which a terrain tile is drawn at its finest level, in tile sides; each -// further level doubles it. four sides is 512 m for the usual 128 m tile. the ranges are -// measured in tiles rather than metres because the morphing below only closes the seams -// when a level's range is several tiles long, whatever the tile size +// Which level a tile is drawn at follows from what its samples look like on screen: a level is +// used out to the distance where one of its sample spacings covers the detail setting in +// pixels. Each level doubles the spacing, so each doubles that distance too. Pixelsperunit is +// the viewport height over twice the tangent of half the vertical field of view - what a +// metre at a metre's distance spans - so zooming in or a taller window pushes detail further. +// +// The finest range never drops below four tile sides, whatever the screen: the morphing below +// only closes the seams when a level's range is several tiles long. inline constexpr double terrain_finest_tiles { 4.0 }; +inline double +terrain_finest_range( double const Gridstep, double const Tilesize, double const Pixelsperunit, double const Detail ) { + return std::max( terrain_finest_tiles * Tilesize, Gridstep * Pixelsperunit / std::max( 0.1, Detail ) ); +} + // how far the camera may move before the tiles in range and their levels are worked out // again, in tile sides inline constexpr double terrain_rescan_tiles { 0.25 }; -// the distance up to which a level is used, for tiles of the given side +// the distance up to which a level is used, given the finest level's range inline double -terrain_level_distance( std::uint32_t const Level, double const Tilesize ) { - return terrain_finest_tiles * Tilesize * std::pow( 2.0, Level ); +terrain_level_distance( std::uint32_t const Level, double const Finest ) { + return Finest * std::pow( 2.0, Level ); } // A tile morphs into the next level over a band at the far end of its level's range. Two @@ -42,19 +52,20 @@ terrain_level_distance( std::uint32_t const Level, double const Tilesize ) { // was at the last rescan while the morph follows the camera every frame; and it is a fifth // of the range wide, which keeps the coarser tile's band, a whole range further out, clear of // any edge a finer tile can reach. Both were settled by a geometric test over random camera -// positions and three tile sizes, which finds no gap with these values and finds them at -// every level boundary with a band ending at the range itself. +// positions, three tile sizes and several finest ranges (tools/terraincook/seamcheck.py), +// which finds no gap with these values and finds them at every level boundary with a band +// ending at the range itself. inline constexpr double terrain_morph_band { 0.2 }; // the distances, from the camera, over which a level L tile morphs into level L+1 inline double -terrain_morph_end( std::uint32_t const Level, double const Tilesize ) { - return terrain_level_distance( Level, Tilesize ) - terrain_rescan_tiles * Tilesize; +terrain_morph_end( std::uint32_t const Level, double const Finest, double const Tilesize ) { + return terrain_level_distance( Level, Finest ) - terrain_rescan_tiles * Tilesize; } inline double -terrain_morph_begin( std::uint32_t const Level, double const Tilesize ) { - return terrain_morph_end( Level, Tilesize ) - terrain_morph_band * terrain_level_distance( Level, Tilesize ); +terrain_morph_begin( std::uint32_t const Level, double const Finest, double const Tilesize ) { + return terrain_morph_end( Level, Finest, Tilesize ) - terrain_morph_band * terrain_level_distance( Level, Finest ); } // what one terrain field's streamer is doing, kept by the field and read by the debug panel @@ -75,6 +86,7 @@ struct terrain_field_status { float gridstep { 0.f }; float tilesize { 0.f }; float range { 0.f }; + float finest { 0.f }; // range of the finest level, from the screen std::uint32_t levels { 0 }; std::size_t backlog { 0 }; // tiles the loader has queued or finished and not handed over terrain_statistics stats; diff --git a/tools/terraincook/seamcheck.py b/tools/terraincook/seamcheck.py index 3024f815..ccf1aea6 100755 --- a/tools/terraincook/seamcheck.py +++ b/tools/terraincook/seamcheck.py @@ -16,14 +16,16 @@ import math import random import sys -FINEST_TILES = 4.0 # terrain_finest_tiles +# finest ranges tried, in tile sides: terrain_finest_tiles is the minimum, and the screen +# can push it further (terrain_finest_range) +FINEST_TILES = (4.0, 5.7, 11.3) RESCAN_TILES = 0.25 # terrain_rescan_tiles MORPH_BAND = 0.2 # terrain_morph_band LEVELS = 6 -def check(tilesize, step, trials, reach): - finest = FINEST_TILES * tilesize +def check(tilesize, step, trials, reach, finest_tiles): + finest = finest_tiles * tilesize def level_range(level): return finest * 2 ** level @@ -84,10 +86,12 @@ def check(tilesize, step, trials, reach): def main(): random.seed(1) failed = False - for tilesize, step, trials, reach in ((128, 1.0, 300, 40), (256, 2.0, 300, 40), (12800, 100.0, 60, 6)): - gaps, example = check(tilesize, step, trials, reach) - print(f"tile {tilesize} m, grid {step} m: {gaps} gaps" + (f" (e.g. {example})" if gaps else "")) - failed = failed or gaps > 0 + for finest_tiles in FINEST_TILES: + for tilesize, step, trials, reach in ((128, 1.0, 150, 60), (256, 2.0, 150, 60), (12800, 100.0, 30, 8)): + gaps, example = check(tilesize, step, trials, reach, finest_tiles) + print(f"finest {finest_tiles} tiles, tile {tilesize} m, grid {step} m: {gaps} gaps" + + (f" (e.g. {example})" if gaps else "")) + failed = failed or gaps > 0 return 1 if failed else 0 diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index ab5fb91c..9945de85 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -271,6 +271,12 @@ bool global_settings::ConfigParseGraphics(cParser& Parser, const std::string& to return true; } + if (token == "terraindetail") + { + ParseOne(Parser, TerrainDetail, 1); + return true; + } + if (token == "vsync") { ParseOne(Parser, VSync); @@ -1540,6 +1546,7 @@ global_settings::export_as_text( std::ostream &Output ) const { export_as_text( Output, "targetfps", targetfps ); export_as_text( Output, "basedrawrange", BaseDrawRange ); export_as_text( Output, "terrainrange", TerrainRange ); + export_as_text( Output, "terraindetail", TerrainDetail ); export_as_text( Output, "fullscreen", bFullScreen ); export_as_text( Output, "fullscreenmonitor", fullscreen_monitor ); export_as_text( Output, "fullscreenwindowed", fullscreen_windowed ); diff --git a/utilities/Globals.cppm b/utilities/Globals.cppm index b840c283..43c979b4 100644 --- a/utilities/Globals.cppm +++ b/utilities/Globals.cppm @@ -196,6 +196,9 @@ struct global_settings { // how far terrain tiles are streamed and drawn, independent of BaseDrawRange: a baked // heightfield is cheap far out, and seeing it to the horizon is the point of having it float TerrainRange{ 20000.f }; + // pixels one terrain sample spacing may cover on screen before a finer level takes over. + // smaller is sharper and costs more tiles at their finest + float TerrainDetail{ 3.f }; int DynamicLightCount{ 7 }; bool ScaleSpecularValues{ true }; std::string GfxRenderer{ "default" };