Plants, Polygons and Pixels: Large-Scale Vegetation Rendering in Godot

Agenda

  1. Our background
  2. Properties of vegetation
  3. Creating plant assets
  4. Scattering plants
  5. Shader Tricks
  6. 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