Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,43 @@ As of 2026-08-31 · `cargo test --workspace`: **1136 tests green** · clippy and
spent in query order, so a field five hundred metres away filled in while the one under
the window stayed bare. And a hero card drew a painted card inside the model standing on
it.
- **Meadow grass on the GPU (2026-09-04, `world_render::grass`):** the default terrain's
grass was first grown like the crops — every 32 m cell within reach of the camera a mesh
of a million vertices, built on the main thread the frame the camera came near, uploaded
whole, and drawn through the full PBR path with all three LOD levels in one mesh. A train
at line speed entered a new row of cells every second, and each one was a visible hitch.
Nothing of the meadow is built on the CPU any more. **(1) A ground cache:** the terrain
tiles around the camera and the fields, roads and waters draped on them are drawn once,
top down, into a 1024² texture of (height, grass weight) by a pass of its own over the
meshes already on the GPU — highest surface wins, an excluded surface lifted a little so
it wins the coplanar fight against the ground under it — and drawn again only when the
camera has left a 64 m margin, a tile has streamed in or out, or the origin was rebased.
**(2) A scatter compute pass** each frame over a grid of 4 m patches: a patch out of the
frustum costs one box test; a patch in view lays its blades out on Roberts' R2 sequence
(any prefix of it is evenly spread, which is what lets a prefix be the thinned stand),
keeps a blade where its rank is below the density its own distance asks for — so coming
closer only ever adds blades and never moves one — reads its foot off the ground cache,
culls it against the frustum, and appends the survivors to one of three instance lists,
whose counts land straight in the indirect draw arguments. **(3) Three indirect draws**
in the opaque pass, one per level of detail (eleven, seven and three vertices), through
Bevy's own mesh pipeline for the view's key with the shaders and the mesh bind group
swapped — so MSAA, HDR, the shadow filter, the fog and the atmosphere are whatever the
camera has, with no second copy of any of it. The vertex shader grows each 32-byte
record into a quadratic Bézier bent by its own lean and the weather's wind (a gust front
travelling downwind, a ripple on it, each blade's own flutter, less of all three on a
stiff blade), tapers it, keeps it at least a pixel and a half wide so far grass does not
shimmer, and turns the normal across the blade so it lights as a rounded stalk; the
fragment shader lights it with diffuse transmission for the back-lit glow, a root-to-tip
occlusion gradient, and `weather_pbr` for the rain and the snow. Density is 450 blades/m²
at the camera falling as 1/(1+(d/10 m)²) out to the range — 220 m on High, 160 on
Medium, 110 on Low, the density scaled with it — the survivors widening by 1/√keep so
the sward keeps its cover, and a blade at the threshold growing in rather than popping.
Fields, roads and water carry `GroundSurface::Excluded`, so no blade stands on them; the
splat's gravel and rock take the grass weight away on the formation and on steep ground,
and the edge is dithered so a verge thins into the ballast instead of stopping at a
line. Deep winter takes the meadow away, and the grassland phenology sets its height, so
it is short after the cut. The crop cards and hero models of the fields are untouched.
Nothing is state: every blade is a function of the ground and a hash of its position.
- **No two fields on one piece of ground (2026-08-31, `content::farmland`,
`fields::geometry`):** a cadastral register's parcels are meant to tile the land and do
not quite: neighbours are digitised from either side of a boundary and agree to a few
Expand Down
38 changes: 10 additions & 28 deletions crates/app/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ pub struct Graphics {
pub texture_quality: Quality,
/// Rendered blade geometry over the painted ground material.
pub grass: bool,
/// Reach and immediate-detail radius of rendered grass. High is the full
/// authored sward; the renderer's lossless batching applies to every level.
/// Reach and density of the rendered grass. High is the full authored
/// sward; the lower levels shorten the reach and thin the stand.
pub grass_quality: Quality,
/// Which anti-aliasing runs on the cab camera.
pub anti_aliasing: AntiAliasing,
Expand Down Expand Up @@ -709,24 +709,16 @@ pub fn ground_quality(graphics: &Graphics) -> world_render::GroundQuality {
world_render::GroundQuality { size, anisotropy }
}

/// Radial grass bands for the selected quality. High is the authored default;
/// lower levels trade only grass reach and the radius of the densest local layer.
/// Reach and density of the meadow for the selected quality. High is the
/// authored default; lower levels trade reach and blades per square metre,
/// which is what the scatter pass and the three draws cost.
fn grass_quality(graphics: &Graphics) -> world_render::GrassRenderSettings {
let (bands, fades) = match graphics.grass_quality {
Quality::Low => (
Vec4::new(24.0, 52.0, 110.0, 14.0),
Vec4::new(10.0, 10.0, 12.0, 5.0),
),
Quality::Medium => (
Vec4::new(28.0, 62.0, 145.0, 18.0),
Vec4::new(11.0, 11.0, 12.0, 6.0),
),
Quality::High => (
Vec4::new(30.0, 70.0, 170.0, 22.0),
Vec4::new(12.0, 12.0, 12.0, 8.0),
),
let (range, density) = match graphics.grass_quality {
Quality::Low => (110.0, 0.4),
Quality::Medium => (160.0, 0.7),
Quality::High => (220.0, 1.0),
};
world_render::GrassRenderSettings::new(graphics.grass, bands, fades)
world_render::GrassRenderSettings::new(graphics.grass, range, density)
}

/// Generates the ground textures again into the handles the terrain material already
Expand Down Expand Up @@ -810,7 +802,6 @@ fn apply_scene(
quality: Option<ResMut<world_render::mist::Quality>>,
clouds: Option<ResMut<world_render::clouds::Quality>>,
grass: Option<ResMut<world_render::GrassRenderSettings>>,
grass_materials: Option<ResMut<Assets<world_render::GrassMaterial>>>,
cameras: Query<(Entity, Has<Bloom>), With<CabCamera>>,
mut projections: Query<&mut bevy::camera::Projection, With<CabCamera>>,
) {
Expand All @@ -837,15 +828,6 @@ fn apply_scene(
{
*grass = wanted_grass;
}
// Existing material instances update immediately; newly grown cells read
// the same resource in `update_field_plants` when their material is made.
if let Some(mut materials) = grass_materials {
for (_, material) in materials.iter_mut() {
if material.extension.grass != wanted_grass.params {
material.extension.grass = wanted_grass.params;
}
}
}
if let Some(mut view) = view {
view.0 = graphics.view_distance;
}
Expand Down
2 changes: 1 addition & 1 deletion crates/i18n/locales/de/main.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -1582,7 +1582,7 @@ set-texture-quality-hint = Größe und Filterung der erzeugten Bodentexturen. Gi
set-grass = Gerendertes Gras
set-grass-hint = Zeichnet echte, windbewegte Grashalme über dem Bodenmaterial. Änderungen gelten sofort.
set-grass-quality = Grasqualität
set-grass-quality-hint = Reichweite des gerenderten Grases und Größe des besonders dichten, detaillierten Bereichs direkt um die Kamera.
set-grass-quality-hint = Reichweite des gerenderten Grases und wie viele Halme auf einem Quadratmeter stehen. Das Gras wird jedes Bild auf der Grafikkarte verteilt; eine niedrigere Stufe kostet dort weniger.
set-shadow-quality = Schattenqualität
set-shadow-quality-hint = Kantenlänge der Schattenkarte der Sonne: 1024, 2048 oder 4096 Texel. Eine Stufe kostet die vierfache Zahl an Texeln — die Einstellung, die man zuerst senkt.
set-mist-quality = Nebelqualität
Expand Down
2 changes: 1 addition & 1 deletion crates/i18n/locales/en/main.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -1579,7 +1579,7 @@ set-texture-quality-hint = Size and filtering of the generated ground textures.
set-grass = Rendered grass
set-grass-hint = Draws real, wind-animated grass blades over the ground material. Changes apply immediately.
set-grass-quality = Grass quality
set-grass-quality-hint = Reach of rendered grass and size of the especially dense, detailed area immediately around the camera.
set-grass-quality-hint = Reach of the rendered grass and how many blades stand on a square metre. The grass is laid out on the graphics card every frame; a lower level costs less there.
set-shadow-quality = Shadow quality
set-shadow-quality-hint = Edge length of the sun's shadow map: 1024, 2048 or 4096 texels. Four times the texels a step, so it is the setting to lower first.
set-mist-quality = Mist quality
Expand Down
3 changes: 3 additions & 0 deletions crates/world-render/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ glam = { workspace = true }
# train moves. A mip chain is the only cure, and DDS is the one format that
# carries one and that ImageMagick can write (`tools/cars/import_cars.mjs`).
bevy = { workspace = true, features = ["jpeg", "dds"] }
# Only for `wgpu::Color`, the clear value of the grass ground cache; unifies
# with the version Bevy pins.
wgpu = { version = "29", default-features = false }
3 changes: 3 additions & 0 deletions crates/world-render/src/farmland.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,9 @@ pub fn spawn_fields(
// level meshes with their own distances — the patch grows it
// when a camera comes near, and takes it back when none does.
crate::plants::FieldPlants::default(),
// No meadow grass grows through it: the patch cuts a hole into
// the grass ground cache (`crate::grass`).
crate::grass::GroundSurface::Excluded,
));
}
});
Expand Down
213 changes: 0 additions & 213 deletions crates/world-render/src/grass.wgsl

This file was deleted.

Loading