Skip to content

07 - Building a Forest Scene

This chapter describes how to use LESS to build a realistic forest scene, including random tree placement, multi-tree species configurations, and LAI measurements.

Scene design

We will build a 50m × 50m deciduous forest scene using the built-in European Ash (FREX) model. Scenarios include:

  • 60 trees, randomly distributed
  • 5 autumn leaf colors (simulating the phenological differences of different individuals)
  • Random size and orientation

Place tool (less.place)

LESS provides a series of placement tools to facilitate the generation of naturally distributed plant locations:

Randomly put

import less

scene = less.Scene()
scene.size = 50.0

# Poisson disk sampling: guaranteed minimum spacing
positions = less.place.random(scene, n=60, min_dist=3.0, seed=42)
print(f"Generate {len(positions)} positions")

The min_dist parameter ensures that the distance between any two points is not less than this value to avoid overlapping trees.

Grid placement

# Regular grid with optional dithering
positions = less.place.grid(scene, spacing=5.0, jitter=0.5)

Random scaling and rotation

n = len(positions)

# Uniform scaling: 0.6x ~ 1.5x
scales = less.place.uniform_scale(n, lo=0.6, hi=1.5, seed=42)

# Random rotation: 0° ~ 360°
rotations = less.place.random_rotation(n, seed=42)

Complete Example: Autumn Forest

import less
import numpy as np

# ── Create a scene ───────────────────────────────────────────────
scene = less.Scene()
scene.size = 50.0

# ground
scene.terrain = less.Terrain(
    property=less.Lambertian(reflectance=0.08)
)

# illumination
scene.illumination = less.Illumination(source=less.Sun(zenith=40, azimuth=150), atmosphere=less.NoAtmosphere())

# ── Definition of 5 autumn leaf colors ─────────────────────────────────────
autumn_colors = {
    "golden_yellow": less.Prospect(cab=10, car=20, canth=0,  cbrown=0.15, cw=0.012, cm=0.009, N=1.8),
    "orange":        less.Prospect(cab=3,  car=12, canth=8,  cbrown=0.25, cw=0.010, cm=0.008, N=1.7),
    "red":           less.Prospect(cab=2,  car=5,  canth=30, cbrown=0.15, cw=0.011, cm=0.009, N=1.8),
    "dark_red":      less.Prospect(cab=1,  car=3,  canth=40, cbrown=0.35, cw=0.009, cm=0.010, N=2.0),
    "yellow_green":  less.Prospect(cab=20, car=15, canth=0,  cbrown=0.05, cw=0.013, cm=0.009, N=1.7),
}

# ── Create a set of trees for each color ───────────────────────────────────
mesh_path = less.examples.asset_path("FREX.obj")
rng = np.random.RandomState(42)
trees_per_color = 12

for color_name, leaf_prop in autumn_colors.items():
    # Create objects and set properties
    tree = less.Object(f"ash_{color_name}", mesh=mesh_path)
    tree.set_property("leaves", leaf_prop)
    tree.set_property("stem_branch", less.Lambertian(reflectance=0.06))

    # Random location, size, orientation
    n = trees_per_color + rng.randint(-2, 3)
    px = rng.uniform(2, 48, n)
    py = rng.uniform(2, 48, n)
    positions = np.column_stack([px, py, np.zeros(n)])
    scales    = rng.uniform(0.6, 1.5, n)
    rotations = rng.uniform(0, 360, n)

    scene.add(tree, positions=positions, scales=scales, rotations=rotations)

# ──Construction─────────────────────────────────────────────────
lai = scene.measure(less.LAIMeasurement())
print(f"Scenario LAI = {lai:.2f}")

# Group by component
lai_detail = scene.measure(less.LAIMeasurement(group_by='component'))
print(f"Blade LAI = {lai_detail.get('leaves', 0):.2f}")
print(f"Branch LAI = {lai_detail.get('stem_branch', 0):.2f}")

# ── Orthophoto RGB image ──────────────────────────────────────────
sensor = less.OpticalImager(
    less.Orthographic(image_size=512),
    bands=[630, 532, 465],   # Autumn RGB
    quality=128,
)
image = scene.simulate(sensor)
image.save("07_autumn_forest_nadir.png")

# ──Perspective camera (oblique angle)──────────────────────────────────
sensor_persp = less.OpticalImager(
    less.Perspective(
        resolution=1024, fov=35,
        position=(25, 60, 45),     # above south side
        target=(25, 25, 6),        # Look slightly higher at the center of the scene
    ),
    bands=[630, 532, 465],
    quality=128,
)
image_persp = scene.simulate(sensor_persp)
image_persp.save("07_autumn_forest_perspective.png")

print("Tutorial 07 completed!")

Supporting script: scripts/07_forest_scene.py

Code interpretation

Construction strategy of multi-tree species scene

Key tip: Create a separate Object for each leaf color and then add them in bulk. LESS internally manages instantiation efficiently - objects with the same geometry but different properties share mesh data.

for color_name, leaf_prop in autumn_colors.items():
    tree = less.Object(f"ash_{color_name}", mesh=mesh_path)
    tree.set_property("leaves", leaf_prop)
    # ... add to scene

Perspective perspective camera

Perspective projection simulates the imaging method of the human eye or camera, with near and far effects:

less.Perspective(
    resolution=1024,           # Number of pixels in the output image
    fov=35,                    # Field of view (degrees)
    position=(25, 60, 45),     # camera position (x, y, z)
    target=(25, 25, 6),        # Observe target point
)
  • position: 3D position of the camera in the scene
  • target: The target point where the camera is looking
  • fov: Field of view, the larger the field of view, the wider the field of view

LAI Measurement

LAIMeasurement calculates the leaf area index by analyzing the areas of all triangular patches in the scene:

# Total LAI
lai = scene.measure(less.LAIMeasurement())

# Group by components (distinguish between leaves and branches)
lai = scene.measure(less.LAIMeasurement(group_by='component'))

# Group by objects (distinguish between different tree species)
lai = scene.measure(less.LAIMeasurement(group_by='object'))

Scene repetition (Repetitive)

For plots that are flat, homogeneous, and need to represent infinite tiles, periodic boundaries can be enabled:

# Isolated limited scene (default): AABB silhouette emission automatically handles oblique edges
scene.repetitive = False

# scene.repetitive = True # Infinite tiling (wrap upper limit 100, only flat homogeneous terrain)

For the physical differences between the two modes, see 04 - Core Concepts. The current public workflow defaults to False; True represents infinite tiling (wrap limit 100), while an integer >=5 sets an explicit wrap limit. Values 1–4 are rejected. DEM/grid terrain cannot be repeated or combined with terrain_following. Edge darkening under oblique illumination is handled automatically for finite scenes, so periodic boundaries are not needed as a workaround.

Next step

  • less.Sceneless.Objectless.Terrain
  • less.place.poisson_disk()less.place.grid()
  • less.Prospectless.Lambertian
  • less.LAIMeasurementless.Orthographicless.Perspectiveless.OpticalImager