Grass and undergrowth

Grass models are defined in .stsdk files, exactly as tree models are. This means that they are defined and edited in the SpeedTree Modeler as if they were any other tree model (often as individual tufts of grass), subject to the same geometry, lighting, and wind definitions. The only restriction is that they must have a single level of detail (LOD). Grass models also do not use billboards for the lowest LOD since there is no LOD in the grass system.

The grass system can be used to populate the scene with any ground cover like rocks, twigs, or leaves. If the Modeler can load it, it'll go through the pipeline and can be used as a grass model.

Note: Be sure to use the grass vertex packer when exporting grass from the Modeler.

Overview

The SDK lets the client application stream in those grass instances that are visible and automatically flushes those instances that are no longer visible.

The SDK organizes the world as a series of cells. As the camera moves, cells go in and out of visibility. As cells become visible, the SDK provides a list of the cells that need to have their populations streamed in. Hence, the SDK will perform most efficiently when the client has the grass instances already organized by cells so that the data might be passed into the SDK without further on-the-fly processing.

In the reference application, grass cells are populated using a procedural approach, but any population procedure will work.

Getting started

There are several classes and structures you'll need to get started:

  • Class CView: CView encapsulates many of the common values associated with a particular view like projection and modelview matrices and near and far clipping planes, but it also includes code for deriving values like camera azimuth and pitch, and frustum data.

    Note: Unlike the tree stream/cull system, the main camera's CView class cannot be used directly. The far clipping plane is used to keep the grass's population restricted to a short distance. In the reference application, CMyPopulate::StreamGrass() shows an example of deriving a grass CView object from the main CView by using CView::AdjustPerspectiveNearAndFar().
  • Class CTree: These are known as base trees, but since grass models are .stsdk files too, this class doubles as a grass base model. There is one of these objects for every .stsdk file loaded in a scene. It contains everything necessary to define a single grass model.
  • Structure SInstance: Each base grass model may have one or more instances, each with unique position, orientation, and scale.
  • Class CCell: The forest is divided into a series of evenly spaced cells. Cells contain SInstances.
  • Class CVisibleInstances: The central class for instance culling and streaming. It contains the functions for determining which cells are visible, instance culling, and 3D instance LOD computations.

    Note: For tree instances, one CVisibleInstances object will handle a whole forest of different 3D tree base models. For grass instances, one CVisibleInstances object is needed per base grass type. This permits individual cell size tuning based on population density as well as streamlining the cell population procedure.

These data structures are detailed in Culling and Population Structures.

Streaming and culling

As shown in CMyPopulate::StreamGrass(), the general procedure followed for each frame where the camera has moved is outlined below:

  • Set view: Make sure the CView object has been updated with the current view. In CMyPopulate::StreamGrass(), the main camera's CView is copied and the far clip value is modified to accommodate the grass layer's much shorter draw distance (typically the camera might have a 2,000 or 3,000 ft draw distance, but the grass shouldn't render any farther than a few hundred feet. CView::AdjustPerspectiveNearAndFar() is used to make the adjustment.

    Note: Normally one CView object is used per view (for example, one for the main camera, another for shadows) and they commonly persist with the world.
  • Rough cull: Call CVisibleInstances::RoughCullCells() with the current CView. Because the SDK does not know the grass instance locations and dimensions ahead of time, it cannot know complete extents of the cells. Specifically, it knows the width and height of the cells because they're in a grid layout, but it cannot know the height on a rolling terrain. CVisibleInstances::RoughCullCells() will return a long list of cells that would be visible if the grid were composed of infinitely tall cells.

    Note: CVisibleInstances::RoughCullCells() returns a bool value indicating if the rough cell list has changed from the last call (true indicates it has changed). If it hasn't changed, the grass stream/cull function can exit.
  • Set rough cell extents: Provide rough cell extents. Depending on the height of the grass models in use, sometimes it's enough just to pass the extents of the terrain in the cell area or take the terrain extents and add the highest grass model height to the max.z component. Providing these extents quickly is key.

    Use CCell::GetExtents() on each of the rough culled cells to get its x/y extents.
  • Fine cull: Call CVisibleInstances::FineCullGrassCells() with the current CView. This gives the SDK a chance to use the updated cell extents to give an exact list of those cells that are within the view frustum.
  • Get newly visible cells: Once CVisibleInstances::FineCullGrassCells() has been called, a list of newly visible cells can be retrieved from the class by calling CVisibleInstances::NewlyVisibleCells(). Newly visible cells are those cells that were not in the frustum the last time FineCullCells() was called.
  • Populate cells with instances: Populate newly visible cells by invoking CCell::SetGrassInstances() on each one. This function simply takes an array of SInstance objects. Note that unlike the tree population function, CCell::SetTreeInstances(), CCell::SetGrassInstances() does not take instance pointers, but instance objects. This is because the grass is created at stream/cull time and not previously created and stored.
  • * Optional * – Update Instance Lists: The SDK Render Interface library derives the CVisibleInstancesRI class from CVisibleInstances, allowing it to add rendering components to it, specifically instancing-based rendering code for 3D trees, grass, and billboards. As such the client app can control when this derived class updates its instance list vertex buffers by calling CVisibleInstancesRI::UpdateGrassGpuInstBuffer().

Additional considerations

Cell size

You can pick different cell sizes per grass type and it can impact both CPU and GPU performance a great deal. Each grass model is culled in a separate call, so separate sizes are easily accommodated. In contrast to tree instances, grass instances are not individually culled. If the cell is in the frustum, then every instance in it will be rendered. This cuts down on CPU usage greatly, but can be hard on the GPU for higher grass densities. To strike a balance, smaller cell sizes can be used. We recommend smaller cell sizes for high-density grass, and larger sizes for sparse models like rocks or boulders.

By way of example, the reference application's example forest (modeled in feet) uses cell sizes of 20 for the high-density grass, and up to 100 for the sparsely populated models.

Heap fragmentation control

The organization of multiple types of grass into an arbitrary collection of cells can easily lead to a great number of heap allocations. We leave it to the user to implement their own population approach that doesn't cause undue heap fragmentation if that's a requirement for your title or project.

To control the SDK's heap behavior, be sure to read about the Reserves System.