17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-31 00:50:29 +01:00

Cook the ground into a quadtree of mesh tiles, and keep the mesh the scenery drew

The ground in a scenery is already an irregular mesh, so the cook keeps it: it cuts the
triangles along the tile grid, welds what coincides, and builds each coarser level by
collapsing edges. Nothing is resampled, nothing is interpolated, and a hole the scenery left
stays a hole. What was here before threw the triangles away, kept their vertices and built a
fresh Delaunay triangulation over them, which filled every hole, merged the two heights of
every retaining wall into one, and - because it never erased the super-triangle - emitted
indices three too large together with triangles reaching out to the artificial corners.

Tiles form a quadtree, as Cesium Quantized Mesh intends. A distant tile is a large one, so the
horizon costs tens of tiles rather than the thousands a fixed grid needed, and the level to
draw follows from the tile's own geometric error against what the screen can resolve - which is
what the format carries that field for. Every vertex on a tile's border survives into all of
its coarser levels, and so does every edge between two of them, so two neighbours drawn at
different levels share their border exactly: there is nothing to stitch and nothing to morph.

The file layout is the format's, with two deliberate departures, both written down in
quantizedmeshformat.h: the geodetic header fields carry scenery metres, there being no
geographic reference to convert from, and the material a vertex belongs to rides in an
extension of ours. Texture coordinates are not stored at all - ground tiles in world space, so
the coordinate follows from the position and from how many metres one repeat of the material
covers, measured off the scenery once and kept in the terrain's table.

Tiles live in one archive beside the scenery, an entry each, compressed on its own. Baltyk
cooks to 546 kB against 2.2 MB of heightfields, quark to 361 kB against 9.6 MB, tomaszewo to
375 kB against 9.9 MB.

Drawing is one call a frame: every resident tile lives in the same vertex and index buffer,
with its vertices measured from the middle of the terrain so that the whole map fits in floats,
and a frame is one glMultiDrawElementsBaseVertex over the tiles the walk chose and the frustum
kept. On baltyk that settles at 291 tiles resident, 16 MB of mesh, 85 tiles and 6k triangles
drawn.

What counts as ground is what the scenery says, not what its triangles look like. A node under
origin, rotate or scale is an object standing on the ground; a material with an alpha channel is
a tree, a bush, a fence or a catenary mast. Everything else is laid whichever way it faces, so
the side of an embankment or a cutting goes in with the rest instead of leaving a hole along
every bank. Whatever is left out is recorded per node, and the scenery goes on drawing it.

A cook is a cache: it is used again only when the archive, the note of which nodes went into it
and the manifest of what the scenery was made from all agree. A manifest listing a file the
scenery names but does not have used to fail that test for ever, since an absent file never
matches - quark has such an include, and nothing beside it could ever be reused.

tools/terraincook cooks the same way outside the engine, and meshtest beside it checks what the
cook promises: a tile reads back as written, neighbouring tiles agree on their border to the
millimetre, a hole is not filled in, a wall is laid exactly once, and a material's tiling comes
out of the source. seamcheck.py goes: it tested the morphing constants of a heightfield nothing
reads any more.
This commit is contained in:
maj00r
2026-09-20 08:04:46 +02:00
parent 975e62df95
commit b877bc0772
36 changed files with 3356 additions and 3614 deletions

View File

@@ -1,23 +0,0 @@
// TIN Terrain Fragment Shader
// Minimal material lookup, no blending for now
#version 330 core
// Input from vertex shader
in vec2 vUV;
flat in uint vMaterial;
// Uniforms
uniform sampler2DArray ground; // Material texture array
// Output
out vec4 fragColor;
void main() {
// Sample material texture
// TODO: Add blending between materials for smooth transitions
vec3 texCoord = vec3(vUV, float(vMaterial));
vec4 color = texture(ground, texCoord);
fragColor = color;
}

View File

@@ -1,30 +0,0 @@
// TIN Terrain Vertex Shader
// Replaces texture-based heightfield with vertex attributes
#version 330 core
// Vertex attributes from VBO
layout(location = 0) in vec3 aPosition; // Relative to tile origin
layout(location = 1) in vec2 aUV;
layout(location = 2) in uint aMaterial;
// Uniforms
uniform vec3 tile_offset; // Tile origin relative to camera
uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
// Output to fragment shader
out vec2 vUV;
flat out uint vMaterial;
void main() {
// Position is tile-relative, add offset to get camera-relative
vec3 worldPos = aPosition + tile_offset;
// Transform to clip space
gl_Position = projectionMatrix * modelViewMatrix * vec4(worldPos, 1.0);
// Pass through to fragment shader
vUV = aUV;
vMaterial = aMaterial;
}

56
shaders/terrainmesh.frag Normal file
View File

@@ -0,0 +1,56 @@
// Terrain mesh, fragment stage.
//
// One triangle carries one material: the cook only welds vertices that share it, so nothing has to
// be blended here and a material boundary follows the edges the scenery drew it with. The texture
// coordinate is the world position over the material's tiling, so the ground keeps the scale it was
// authored at whatever level the tile is drawn at.
//
// The mesh carries no normals - that would be four more bytes on every vertex - so the surface
// normal comes from how the view position changes across the triangle, which is its face normal.
in vec4 f_pos;
in vec2 f_ground;
flat in uint f_material;
#include <common>
#include <apply_fog.glsl>
#include <tonemapping.glsl>
uniform sampler2DArray ground;
// per material: rgb the colour to show while it has no texture, a the metres one texture repeat
// covers - negative while there is no texture layer for it yet
uniform sampler2D materialtable;
layout(location = 0) out vec4 out_color;
#if MOTIONBLUR_ENABLED
layout(location = 1) out vec4 out_motion;
#endif
void main()
{
vec4 entry = texelFetch( materialtable, ivec2( int( f_material ), 0 ), 0 );
vec3 albedo =
( entry.a < 0.0 )
? entry.rgb
: texture( ground, vec3( f_ground / max( entry.a, 0.01 ), float( f_material ) ) ).rgb;
vec3 normal = normalize( cross( dFdx( f_pos.xyz ), dFdy( f_pos.xyz ) ) );
// towards the camera, whichever way the derivatives came out
if( dot( normal, -f_pos.xyz ) < 0.0 ) { normal = -normal; }
// a plain lambert term against the first directional light, so that relief reads
float lambert = 0.35;
if( lights_count >= 1U && lights[ 0 ].type == LIGHT_DIR ) {
lambert = 0.35 + 0.65 * max( dot( normal, normalize( -lights[ 0 ].dir ) ), 0.0 );
}
vec3 col = albedo * lambert;
#if POSTFX_ENABLED
out_color = vec4( apply_fog( col ), 1.0 );
#else
out_color = tonemap( vec4( apply_fog( col ), 1.0 ) );
#endif
#if MOTIONBLUR_ENABLED
out_motion = vec4( 0.0 );
#endif
}

32
shaders/terrainmesh.vert Normal file
View File

@@ -0,0 +1,32 @@
// Terrain mesh, vertex stage.
//
// A vertex is a position and the ground material it belongs to, nothing else. The position is
// measured from the middle of the terrain rather than from the scenery's origin, so that the whole
// of a map fits in floats and every resident tile can live in one vertex buffer - which is what
// lets a frame of terrain be one draw call. terrainorigin is where that middle is relative to the
// camera, terrainworld where it is in the world.
//
// There are no texture coordinates: ground textures tile in world space, so the coordinate follows
// from where the vertex is and from how many metres one repeat of its material covers, which the
// cook measured from the scenery and left in the material table.
#include <common>
layout(location = 0) in vec3 in_position;
layout(location = 1) in uint in_material;
uniform vec3 terrainorigin; // the terrain's middle, relative to the camera
uniform vec2 terrainworld; // the terrain's middle, in the world
out vec4 f_pos;
out vec2 f_ground; // world position on the ground, for texturing
flat out uint f_material;
void main()
{
vec3 local = in_position + terrainorigin;
f_ground = terrainworld + in_position.xz;
f_material = in_material;
f_pos = modelview * vec4( local, 1.0 );
gl_Position = projection * f_pos;
}