LandscapeLab:
general-purpose real-time landscape visualization based on geospatial data
realistic
realistic
stylized
EZTree: open source
SpeedTree: industry standard
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
rembg i -m birefnet-general Fagus_sylvatica_in_Aubrac.jpg output.png
https://github.com/danielgatis/rembg
EZTree
Blender
Godot
rembg & GIMP
model rough shape
scale individual quads
bend normals
texture and shade
more at https://simonschreibt.de/gat/airborn-trees/
Geometry versus Billboards
Geometry versus Billboards
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
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
don't use nodes!
use GPU instancing!
draw x1
draw x1
draw x1
draw x1000
vs
TRANSFORM[3][0] = pos.x; TRANSFORM[3][1] = pos.y; TRANSFORM[3][2] = pos.z;
multimesh.set_instance_transform( instance_id, Transform3D().translated(pos) )
good for space-covering plants with frequent LOD updates (grass)
good for large chunks of meshes with complex placement (trees)
ground-covering foliage (particles)
trees
(MultiMesh chunks)
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, ...
// Check land cover at this position: int land_cover_id = int(round( texture(land_cover, position).r ));
// Get specific plant data for shading:
CUSTOM = texture( distributions[land_cover_id], position ).rgb:
// Get texture based on CUSTOM.r
int texture_id = int( INSTANCE_CUSTOM.r * 255.0 ) vec4 color = texture( texture_map[texture_id], UV );
features = geo_layer.get_features_in_square( top_left_x, top_left_y, size, 10000000 ) # ... or # anything # that gives # you points
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, ...
only terrain normal:
NORMAL = terrain_basis * vec3(0.0, 1.0, 0.0);
only half-dome normal:
NORMAL = terrain_basis * normalize(VERTEX);
blended by distance:
NORMAL = terrain_basis * (normalize(VERTEX) + vec3(0.0, 1.0, 0.0) * inv_distance_factor)
no variation
random size variation
(between min and max)
random color variation
variation in satellite image
world-space stripe noise
also apply to AO
VERTEX.xz += texture(wind_noise, pos)
* inverse(MODEL_MATRIX)
* wind_direction
* (1.0 - UV.y)true alpha
10x frame time!
alpha scissor
ALPHA_SCISSOR_THRESHOLD = 0.8
ALPHA_SCISSOR_THRESHOLD = 0.2
ALPHA_SCISSOR_THRESHOLD = mix(0.8, 0.2, smoothstep( 1.0, 30.0, world_distance));
no AO
AO = length(VERTEX) * strength;
no AO
AO = length(VERTEX) * strength;
~ 7000 vertices
8 vertices
high-LOD:
billboard clusters
low-LOD:
impostor plane
particle system
next pass material of Terrain
geometry
impostor
links:
https://hexaquo.at
https://github.com/boku-ilen/landscapelab
get in touch:
mail: karl@hexaquo.at
bluesky: @hexaquo.at