Repository navigation
Releases: GEMI/bevy-grass
Release list
bevy_grass 0.20.6: strategy cameras, terrain from code, no blinking blades
Strategy cameras, terrain built from code, and no more blinking blades.
GrassCamera::radius_from_ground draws grass across the ground a high
camera sees; GrassField::with_ground_from_fn and with_density_from_fn
grow a field on terrain a game builds itself; GrassLightFrame and
GrassWindFrame::advance let a game drive grass from a sun or wind of its
own. Blades no longer flash the clear colour behind a depth prepass or
white in a low sun's glint, gusts no longer show a grid from above, and an
MSAA sample count the GPU cannot do no longer turns the window black. The
web demo has every setting of the
crate in its panel and copies what you tune as Rust.
Breaking, and what to write instead
GrassSettings has a new field, mid_range_start. A struct literal that
names every field without ..GrassSettings::HIGH (or another tier) needs
mid_range_start: 0.1 added.
GrassWindFrame no longer implements Bevy's ShaderType, and GrassCamera
and NoGrass no longer implement ExtractComponent. The public types are
plain data now, so a change to the shaders' wind layout or to Bevy's
extraction API no longer breaks them. Nothing changes for a game that writes
them; the renderer copies them into the render world itself. A game that
uploaded GrassWindFrame to a shader of its own copies its fields into a
ShaderType struct of its own.
GrassPlugin has a new field, lights (true by default). A literal that
names every field without ..default() needs lights: true added.
GrassCamera has a new field, radius_from_ground (false by default). A
literal that names every field without ..default() needs
radius_from_ground: false added.
GrassFieldOverrides has a new field, wind_scale (1 by default). A literal
that names every field without ..default() needs wind_scale: 1.0 added.
GrassWindFrame::gust_offset and previous_gust_offset now wrap at 256
repeats of the gust texture instead of 1, for the second layer of gusts (see
Fixed). A custom wind that calls GrassWindFrame::advance, or leaves the
offsets at zero, needs no change. One that wraps the offsets itself wraps
them at 256 (rem_euclid(Vec2::splat(256.0))); wrapped at 1, the gusts
jump each time they cross a repeat.
Added
- The web demo's panel has every public setting of the crate, built from
the types'Reflectdata and their doc comments:GrassSettings,
GrassStylewith its species,GrassWindand its presets,FarGrass,
GrassFieldOverrides,GrassRegrowth,GrassInteractor,GrassCamera,
aGrassBrushon the left mouse button,GrassDebugandGrassStats.
Each section copies as Rust (only what differs from its tier or preset),
the style also as a.grass.ron, and a.grass.roncan be pasted in. The
address keeps the changes, so a link shares them. It replaces the
wind-strength slider. FarGrass::with_sheen(sheen), likewith_fadeandwith_area: a struct
literal with..FarGrass::new(&style)does not compile outside the crate,
so setting the sheen took a variable.FarGrass::uniform(&wind, &settings)andFarGrass::GUSTS: what a
terrain material of the game's own needs to callgrass_far_colorfrom
its shader, the far grassFarGrassMaterialdraws. Before, its uniform
could not be built outside the crate. The newcustom_terrainexample is
a material with its own shader doing it.GrassFieldOverrides::wind_scale: one field moves less in the shared
wind, or more: a courtyard, a greenhouse, a windswept ridge. The gusts
stay in step with the fields around it. The tip flutter scales with it.GrassCamera::radius_from_ground, for strategy and city-builder cameras:
the radius is measured from the ground below the camera, so grass reaches
the same distance across the ground at any zoom, thinned for the camera's
height so the number of blades, and the cost, stay about the same. The
newrts_cameraexample and Strategy cameras
show the setup and its costs. Off by default; nothing changes without it.GrassField::with_ground_from_fn(&mut images, cell_size, height)and
GrassField::with_density_from_fn(&mut images, cell_size, density): a
field on terrain built from code, from the function that built it. Each is
sampled once into a heightmap or a mask, so terrain in chunk meshes, or
meshes kept only on the GPU, needs no heightmap file and no field per
chunk. A height that is not finite leaves no ground, for coastlines. The
newcode_terrainexample is an island in 16 chunks with one field.GpuStats::total_blades(), the blades of every level of detail together:
stats.gpu.total_blades() == 0after the first reading means nothing was
drawn. The README's new "Check it works" paragraph points to it and to the
debug gizmos, which show a field buried under hills.- A warning, once per camera, when a perspective camera has had grass fields
in view for two seconds and all of them were beyond its grass radius, so
it drew nothing. The radius is a distance from the camera itself, so a
strategy camera high above the ground drew no grass at all, and nothing in
the log said why. The check is one box test per camera and field a frame. GrassWindFrame::advance(delta, gust_speed)scrolls the gusts along the
frame's direction, as the built-in wind does, so a custom wind source no
longer has to work out the gust texture's scrolling itself. The
custom_windexample uses it.GrassLightFrame, the light grass follows this frame (the shadow-casting
light's direction and the sun's), written by the new built-in
GrassLightPluginfrom Bevy's directional lights in
GrassLightSystems::Sample. A game whose sun is not aDirectionalLight
buildsGrassPlugin { lights: false, .. }and writes the frame itself, as
it can the wind. The renderer reads the frame instead of querying Bevy's
lights, so Bevy's light types are read in one place.
Fixed
- A camera asking for an MSAA sample count the GPU does not support no
longer turns the window black. Bevy stops rendering at the first
unsupported count, and WebGPU supports only 1 and 4 samples, so
Msaa::Sample2andMsaa::Sample8stopped every browser. Grass now logs
one warning naming the camera and uses the most samples below it that the
device has (8x becomes 4x, 2x becomes off). The web demo offers only Off
and 4x. - With a depth prepass (which SSAO, depth of field, outlines and contact
shadows need), a few blades a frame flashed the clear colour, often a
pale sky blue, all over the screen. The main pass draws a blade only where
its depth equals the prepass's, and the two shaders were free to round a
blade's position differently; where the main pass came out a hair
farther, neither the blade nor the ground behind it was drawn. Both now
mark the position@invariant, so they place blades identically. In a
game scene, 21 such pixels a frame became none. The goldens differ by a
mean of at most 0.7 of 255 as blade edges move by a rounding step. - Blades with sheen (
GrassBlade::specular, 0.3 inMEADOW) flashed white
for a frame, a few at a time all over the screen, when the sun was low and
in front of the camera: a blade a pixel or two wide that swayed into the
sun's mirror angle caught a glint hundreds of times its usual brightness,
which bloom spread. The sheen now widens where a blade's normal turns
within a pixel (specular antialiasing), and a blade's reflectance stops at
0.3, below where Bevy's grazing reflectance reaches full. Looking into a
low sun,MEADOWflashes about 0.2 pixels a frame instead of 36. Elsewhere
the look is unchanged to the eye; the goldens differ by a mean of at most
0.5 of 255, and the forward and deferred paths change alike. Styles
without sheen pay nothing for it. - Seen from a strategy camera, the gusts rolled across the field in squares.
The gust texture repeats everygust_scalemetres (25 m inBREEZE), and
from 100 m up dozens of repeats show at once, in a grid along the world's
axes. The gusts are now two layers of it, the second turned 36.8° and 1.17
times as large, moving together; the sum does not repeat. Gusts keep their
size, speed and depth, but their shapes change, and so do the goldens. FarGrassMaterial's gust sheen, the paler undersides of bent grass, now
shows where the ground is seen at a glancing angle and fades as the camera
looks down on it, as on real grass. From a strategy camera the far ground
showed gust patterns with no grass in sight, and through the gaps between
blades. Ground-level views are unchanged (themeadowexample differs by
at most 2 of 255). Where the sheen would change the colour by less than
0.4%, the gust texture is not read.- A perspective camera high above the ground, a strategy game's, kept its
trample window (64 m across) centred under itself, while the ground it
showed began 65 m or more ahead, so units in view flattened no grass. The
window now moves ahead onto the nearest ground in view, never past where
grass is drawn. It moves only once that ground is about 11 m away (a sixth
of the window), so first- and third-person cameras keep the window around
themselves as before, and the trail behind a walker with it. GrassSamplerreported grass where the ground has a hole, past the edge
of aGrassGround::Meshor where a height function gave no height, while
the GPU draws none there. Itsdensityis now 0 there, so footsteps and
stealth checks agree with what is drawn.
Changed
- The mid-range thinning (
mid_range_density, half the blades, the rest
wider) now begins at its own distance,mid_range_start: 10...
bevy_grass 0.20.5: grass in the browser
Grass in the browser: it draws through WebGPU, in 10 storage buffers per
shader stage, and a device that cannot draw it gets one warning instead of a
stopped renderer. A web demo and
docs/web.md come with it. Tested in Chrome, Edge and Brave on
Windows and in Chrome on macOS, and natively on macOS (Metal). Safari 27
cannot run Bevy 0.20's standard material yet, with or without grass
(docs/web.md). 0.20.4 was not published; its changes
are in this release.
Breaking, and what to write instead
GrassStyleFileError::Parse no longer holds ron's SpannedError, so a
ron upgrade is no longer a breaking change of this crate. It is now
Parse { line, column, message }: where the parser stopped, counted from 1,
and why. Match Parse { message, .. } instead of Parse(error), and read
message or the error's Display (unchanged apart from showing only the
start of the span) instead of the ron error.
Added
- The examples build for the browser (
wasm32-unknown-unknown, WebGPU):
python tools/web_example.pybuilds and serves them, and CI checks the
wasm build. docs/web.md lists the browsers tested. - A web demo for GitHub Pages (
web/, a package of its own that is not
published): a meadow with a quality switch, starting atMEDIUM, and a
preset picker.python tools/web_demo.pybuilds it for size, serves it,
and smoke-tests it in a browser. examples/webgpu_minimal.rs: the quick start, ready for the browser
(a canvas that fills the page,LOWon the web, and a message on screen
whenGrassSupportsays the device cannot draw grass). The README has a
Platforms section and links it.- docs/web.md, running in the browser: how to build for
WebGPU, the browsers tested, what a device needs, the shadow and timing
caveats, and frame times per tier on the devices measured. GrassSupport, a resource: whether this GPU can draw grass, and if not,
why (no compute shaders, as in WebGL2; no indirect draws; no storage
buffers in the vertex shader; too few storage buffers per shader stage; too
small a storage binding). Decided once when the renderer starts.- The examples'
--no-grassflag: the same scene without grass. - The web demo's and examples' pages log each shader a browser rejects, with
the compiler's reason and line. - The examples'
--max-storage-buffers Nflag: ask the GPU for at most N
storage buffers per shader stage, as a weaker device or browser offers.
Fixed
- On a device that cannot draw grass, the grass pipelines were still created,
and the wgpu validation error stopped Bevy's renderer. Now grass logs one
warning saying why and draws nothing, and the rest of the scene renders.
Grass binds 10 storage buffers in its busiest shader stage, so a device
that allows fewer gets the warning.
Changed
- Grass fits in 10 storage buffers per shader stage, down from 14, so it
draws on devices that offer 10 to 13: most browsers on macOS, Samsung
Internet and some of Firefox. No picture changes. Each family of compute
passes has its own layout, the masks and heightmaps share one buffer, and
the draws bind storage buffers in the vertex stage only (see
docs/rendering.md). Fields whose masks and
heightmaps together are larger than one storage binding (128 MiB in
WebGPU) log a warning and draw no grass, where before each had a binding
of its own.
bevy_grass = "0.20.5"