Plants, Polygons and Pixels: Large-Scale Vegetation Rendering in Godot
Agenda
- Our background
- Properties of vegetation
- Creating plant assets
- Scattering plants
- Shader Tricks
- Level of Detail
1. Our background
LandscapeLab:
general-purpose real-time landscape visualization based on geospatial data
LandscapeLab
- open, reusable, modern, data-driven
- turn geographic data into realistic 3D environment
- Geodot: GDExtension for loading data

realistic
realistic
stylized
2. Why is rendering vegetation difficult?
complex geometry

complex lighting

huge diversity

complex interaction with the world
it's everywhere
3. Creating plant assets
Trees

Geometry
versus Billboards


Geometry versus Billboards



Creating 3D Tree Assets

manual modelling




dedicated software

EZTree: open source
SpeedTree: industry standard

Problem:
billboard geometry does not match implied geometry


bend your normals!



convex hull
data transfer: normals from hull to billboard clusters
NOTE: required in fragment shader when using bent normals:
if (!FRONT_FACING) NORMAL = -NORMAL;
see https://github.com/godotengine/godot/issues/42411
create albedo texture from photographs
rembg i -m birefnet-general Fagus_sylvatica_in_Aubrac.jpg output.png
https://github.com/danielgatis/rembg


normal map generated from albedo (GIMP)







EZTree
Blender
Godot
rembg & GIMP
quick stylized shrubs




model rough shape
scale individual quads
bend normals
texture and shade
more at https://simonschreibt.de/gat/airborn-trees/
Grass



Geometry versus Billboards


Geometry versus Billboards



Grass geometry from Blender
Ground-Covering Billboard Clouds


1. cluster of quads

2. scale to texture aspect ratio

3. bend normals

3. bend normals

4. apply texture

5. apply alpha

6. fine-tune shading
Creating Good Billboard Textures

1. take photos,
cut alpha,
collage







2. avoid shadows and hard borders


3. add alpha padding


gmic -input in.png -solidify 75 -output out.png

with alpha padding
without alpha padding






4. Scattering plants in a game world
don't use nodes!

use GPU instancing!

why GPU instancing?
draw x1
draw x1
draw x1
draw x1000
Particles
vs
MultiMeshes
TRANSFORM[3][0] = pos.x; TRANSFORM[3][1] = pos.y; TRANSFORM[3][2] = pos.z;
multimesh.set_instance_transform( instance_id, Transform3D().translated(pos) )
Particles
- GPU placement, no frame stutter
- Allows flexible placement every frame
good for space-covering plants with frequent LOD updates (grass)
MultiMeshes
- One-time CPU overhead instead of GPU overhead
- Allows complex placement logic and variable instances
good for large chunks of meshes with complex placement (trees)
ground-covering foliage (particles)
trees
(MultiMesh chunks)
Particles: scattering
vec3 pos = vec3(0.0, 0.0, 0.0); pos.z = float(INDEX); pos.x = mod(pos.z, rows); pos.z = (pos.z - pos.x) / rows; // apply spacing, random offset, ...
Particles: choosing plants
// Check land cover at this position: int land_cover_id = int(round( texture(land_cover, position).r ));

Particles: choosing plants
// Get specific plant data for shading:
CUSTOM = texture( distributions[land_cover_id], position ).rgb:

Particles: applying plant
// Get texture based on CUSTOM.r
int texture_id = int( INSTANCE_CUSTOM.r * 255.0 ) vec4 color = texture( texture_map[texture_id], UV );

MultiMesh: getting points
features = geo_layer.get_features_in_square( top_left_x, top_left_y, size, 10000000 ) # ... or # anything # that gives # you points

MultiMesh: applying data
for feature in features: transforms.append(Transform3D() .scaled(instance_scale) .rotated( Vector3.UP, PI * rng.randf_range(-1.0, 1.0)) .translated(instance_position) )
1 MultiMesh per mesh and chunk
trade-off: culling, loading, diversity, draw calls, ...
5. Shader Tricks
adapt normal bend at runtime

only terrain normal:
NORMAL = terrain_basis * vec3(0.0, 1.0, 0.0);
adapt normal bend at runtime
only half-dome normal:
NORMAL = terrain_basis * normalize(VERTEX);

adapt normal bend at runtime
blended by distance:
NORMAL = terrain_basis * (normalize(VERTEX) + vec3(0.0, 1.0, 0.0) * inv_distance_factor)

create variety

no variation
create variety
random size variation
(between min and max)

create variety
random color variation

create variety
variation in satellite image



world-space stripe noise
wind animation
wind animation
also apply to AO
VERTEX.xz += texture(wind_noise, pos)
* inverse(MODEL_MATRIX)
* wind_direction
* (1.0 - UV.y)use alpha scissor!


true alpha
10x frame time!
alpha scissor
dealing with alpha scissor
ALPHA_SCISSOR_THRESHOLD = 0.8
dealing with alpha scissor
ALPHA_SCISSOR_THRESHOLD = 0.2
dealing with alpha scissor
ALPHA_SCISSOR_THRESHOLD = mix(0.8, 0.2, smoothstep( 1.0, 30.0, world_distance));
ambient occlusion

no AO
ambient occlusion
AO = length(VERTEX) * strength;

ambient occlusion
no AO

ambient occlusion
AO = length(VERTEX) * strength;

backlight, roughness, specular
- scale backlight by greenness
- scale roughness by greenness
- scale specular by AO (around 0.15)


6. Level of Detail
Impostors
3 reasons:
- reduce vertex count
- reduce overdraw
- reduce visual noise
Tree Impostors


~ 7000 vertices
8 vertices




LOD for Ground-Covering Foliage?

high-LOD:
billboard clusters
low-LOD:
impostor plane
particle system
next pass material of Terrain
- re-use as much shader code and textures as possible
- fake geometry with additional textures
- check channels separately
Foliage Impostors


same approach for grass geometry

geometry
impostor
Thanks!
links:
https://hexaquo.at
https://github.com/boku-ilen/landscapelab
get in touch:
mail: karl@hexaquo.at
bluesky: @hexaquo.at
Plants, Polygons and Pixels: Large-Scale Vegetation Rendering in Godot
By hexaquo
Plants, Polygons and Pixels: Large-Scale Vegetation Rendering in Godot
- 11