GrassField

class GrassField

Bases: TypedReferenceCount

The CPU side of instanced grass: where the blades stand, in square cells round the camera, and which cells draw at which level of detail.

Blades sit on a world-anchored jittered grid of `density` blades per square metre; each grid point keeps its blade with the probability get_density_at() gives there (the masks: exclusion, water, slope, dry ground, an optional density image, and the light under an Ecosystem’s crowns), so a blade’s existence and shape depend only on its world spot and the seed – cells are seamless and come out byte-identical whenever they are rebuilt. Blades gather in clumps (after Ghost of Tsushima’s grass, Wohllaib, GDC 2021): each belongs to the nearest of a jittered lattice of clump centres, which pulls it in a little and gives it a shared height, facing and tone.

A cell’s table is one RGBA32F texel per blade, ROW blades a row: x, y, z of its root (world), and a 24-bit integer as a float: bits 0-7 its height (0..1 of the tallest), 8-15 its facing (0..2 pi), 16-23 its clump’s tone. The table is sorted by a random rank, so drawing its first n blades thins the cell evenly (the far cells draw fewer, the shader shrinks the blades past their rank as they recede).

`update(camera)` picks the cells within the last distance, gives each a level (0, 1, 2: the vertex shader’s 7, 3 or 1 segments per blade) by its nearest distance with hysteresis and a draw count, builds the tables of cells that come into range, frees those far out of it, and reports the cells whose drawing changed.

Threads: update() releases the GIL; it reads a snapshot of the terrain taken when it starts, and the field’s other methods wait for it (a lock). Limits: cells of 1 to 1024 m, at most MAX_CELL_BLADES blades a cell, and at most MAX_RANGE_CELLS cells a side in range (the last LOD distance is cut to that); a non-finite setting or camera is rejected with an error.

Inheritance diagram

digraph inheritance4b28043716 { bgcolor=transparent; rankdir=BT; size="8.0, 12.0"; "DTOOL_SUPER_BASE" [fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=11,height=0.25,shape=box,style=""]; "GrassField" [URL="acron.vegetation.GrassField.html#acron.vegetation.GrassField",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=11,height=0.25,shape=box,style="",target="_top",tooltip="/**"]; "TypedReferenceCount" -> "GrassField" [arrowsize=0.75,style=""]; "ReferenceCount" [URL="acron.core.ReferenceCount.html#acron.core.ReferenceCount",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=11,height=0.25,shape=box,style="",target="_top",tooltip="/**"]; "DTOOL_SUPER_BASE" -> "ReferenceCount" [arrowsize=0.75,style=""]; "TypedObject" [URL="acron.core.TypedObject.html#acron.core.TypedObject",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=11,height=0.25,shape=box,style="",target="_top",tooltip="/**"]; "DTOOL_SUPER_BASE" -> "TypedObject" [arrowsize=0.75,style=""]; "TypedReferenceCount" [URL="acron.core.TypedReferenceCount.html#acron.core.TypedReferenceCount",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=11,height=0.25,shape=box,style="",target="_top",tooltip="/**"]; "TypedObject" -> "TypedReferenceCount" [arrowsize=0.75,style=""]; "ReferenceCount" -> "TypedReferenceCount" [arrowsize=0.75,style=""]; }
GrassField.__init__()

Grass over `terrain` in cells of `cell_size` metres (1 to 1024).

GrassField.build_cell()

Builds cell (cx, cy)’s blades now (in range or not); returns its slot.

GrassField.clear()

Forgets every cell (after a change to the density, the masks or the ecosystem); the next update() rebuilds what is in range and reports it. Cell slots stay valid (their generation moves on).

GrassField.clear_density_image()
GrassField.clear_ecosystem()
GrassField.find_cell()

The slot of cell (cx, cy), or -1 if it was never in range.

GrassField.get_build_seconds()
GrassField.get_canopy_response()
GrassField.get_cell_draw_count()
GrassField.get_cell_generation()

Counts the builds of a cell’s table: a renderer re-uploads when it moves.

GrassField.get_cell_lod()
GrassField.get_cell_max()
GrassField.get_cell_min()
GrassField.get_cell_num_blades()
GrassField.get_cell_size()
GrassField.get_cell_table()

The cell’s table, four floats a blade (see the class description).

GrassField.get_cell_x()
GrassField.get_cell_y()
GrassField.get_changed()
static GrassField.get_class_type()
GrassField.get_clump_pull()
GrassField.get_clump_size()
GrassField.get_density()
GrassField.get_density_at()

The probability 0..1 that a blade stands at (x, y): the exclusion and water masks clear it, steep slopes thin it, dry ground thins it a little, the density image scales it, and so does the light under an ecosystem’s crowns.

GrassField.get_draw_fraction()

The share of a cell’s blades drawn at `distance`: 1 up to the thinning start, falling linearly to the far fraction at the last distance.

GrassField.get_drawn_blades()
GrassField.get_far_fraction()
GrassField.get_hysteresis()
GrassField.get_lod_cells()
GrassField.get_lod_distances()
GrassField.get_num_built()
GrassField.get_num_cells()
GrassField.get_num_changed()
GrassField.get_num_visible()
GrassField.is_cell_built()
GrassField.is_cell_visible()
GrassField.set_canopy_response()
GrassField.set_clumping()

Clumps `clump_size` metres apart (0: none); `pull` 0..1 draws each blade that far towards its clump’s centre.

GrassField.set_density()

Blades per square metre where nothing thins them (default 48).

GrassField.set_density_image()

A density multiplier 0..1 (grey level) over a rectangle of the world (rows north to south, as EcoTerrain’s masks); outside it, 1.

GrassField.set_ecosystem()

Thins the grass under `ecosystem`’s crowns (its kept tiles’ ground light, to the power of the canopy response). Rebuild (clear()) after simulating new tiles under cells already built.

GrassField.set_far_fraction()
GrassField.set_hysteresis()
GrassField.set_lod_distances()

x: level 0 (7 segments) up to here, y: level 1 (3 segments) up to here, level 2 (one) beyond; z: thinning starts, w: the last blade (metres).

GrassField.set_slope_limits()

Full grass up to `full_degrees` of slope, none past `none_degrees`.

GrassField.update()

Chooses the cells in range of `camera` and their levels, builds the tables of the new ones and frees those far behind. Returns how many cells changed (get_changed()).

GrassField.write_cell_texture()

Writes a cell’s table into `texture` (RGBA32F, ROW texels wide, nearest filtering).