Jump to content
  • entries
    943
  • comments
    5,899
  • views
    924,381

Vegetation


Josh

5,340 views

 Share

There's basically two kinds of vegetation, trees and grass.

 

Grass is plentiful, ubiquitous, dense, but can only be seen at a near distance. Its placement is haphazard and unimportant. It typically does not have any collision. ("Grass" includes any small plants, bushes, rocks, and other debris.)

 

Trees are fewer in number, larger, and can be seen from a great distance. Placement of groups of trees and individual instances is a little bit more important. Because these tend to be larger objects, they have physics collision.

 

Now figuring out how to take advantage of modern GPU architecture with both of those types of objects is an interesting problem. I started with geometry shaders, thinking I could use them to create duplicate instances on the fly. This geometry shader will do just that:

#version 400

 

#define MAX_INSTANCES 81

#define MAX_VERTICES 243

#define MAX_ROWS 1

 

uniform mat4 projectioncameramatrix;

uniform mat4 camerainversematrix;

 

layout(triangles) in;

layout(triangle_strip,max_vertices=MAX_VERTICES) out;

 

in mat4 ex_entitymatrix[3];

in vec4 ex_gcolor[3];

in vec2 ex_texcoords0[3];

in float ex_selectionstate[3];

in vec3 ex_VertexCameraPosition[3];

in vec3 ex_gnormal[3];

in vec3 ex_tangent[3];

in vec3 ex_binormal[3];

in float clipdistance0[3];

 

out vec3 ex_normal;

//out vec4 ex_color;

out vec3 ex_jtangent;

out vec3 ex_jbinormal;

out vec3 ex_jjtangent;

 

void main()

{

//mat3 nmat = mat3(camerainversematrix);//[0].xyz,camerainversematrix[1].xyz,camerainversematrix[2].xyz);//39

//nmat = nmat * mat3(ex_entitymatrix[0][0].xyz,ex_entitymatrix[0][1].xyz,ex_entitymatrix[0][2].xyz);//40

 

for(int x=0; x<MAX_ROWS; x++)

{

for(int y=0; y<MAX_ROWS; y++)

{

for(int i=0; i<3; i++)

{

mat4 m = ex_entitymatrix;

m[3][0] += x * 4.0f;

m[3][2] += y * 4.0f;

vec4 modelvertexposition = m * gl_in.gl_Position;

gl_Position = projectioncameramatrix * modelvertexposition;

ex_normal = ex_gnormal;

//ex_color = ex_gcolor;

ex_jjtangent = ex_gnormal;

ex_jtangent = ex_gnormal;

ex_jbinormal = ex_gnormal;

EmitVertex();

}

EndPrimitive();

}

}

}

 

I soon discovered some severe hardware limitations that make geometry shaders unusable for this type of application. There's a 255 limit on the number of vertices that can be emitted, but there's an even harsher limit on the number of varying you can output from the shader. Once you add values for texcoords, normals, binormals, and tangents, geometry shaders actually only allow 16 instances per render...making them inappropriate for this purpose.

 

What's really needed is an "instance shader" that could control how many times an object is rendered. This would simply discard an entire instance if it isn't in the camera frustum:

uniform vec4 cameraplane0;

uniform vec4 cameraplane1;

uniform vec4 cameraplane2;

uniform vec4 cameraplane3;

uniform vec4 cameraplane4;

uniform vec4 cameraplane5;

 

uniform objectradius;

 

uniform instancematrix[MAX_INSTANCES]

 

bool PlaneDistanceToSphere(in vec4 plane in vec3 point, in float radius) {}

 

main ()

{

mat4 mat = instancematrix[gl_InstanceID];

vec3 pos = mat[3].xyz;

float radius = objectradius * max(max(mat[0].length(),max[1].length),mat[2].length);

if (PlaneDistanceToSphere(cameraplane0,position,radius) > 0.0) discard;

}

 

I realized I could render a number of instances without actually sending their 4x4 matrices to the GPU, and just generate the positions along a grid. This would start with an n X n grid and then add some noise to randomly rotate and scale each instance. The randomized positions would use the object's XZ position on the grid as the input, so I could dynamically generate the same orientation each time, without ever storing the object's actual position in memory. (This is why some Leadwerks 2 maps could be hundreds of megs of data.)

 

Here is the code in the vertex shader that randomizes object scale and rotation:

    #define ROWSIZE 15

#define SEED 1

#define randomness 3.0

#define density 3.0

#define scalevariation 1

 

int id = gl_InstanceID;

int x = id/ROWSIZE;

int z = id - x * ROWSIZE;

 

float angle = rand(vec2(SEED-z,SEED+x))*6.2831853;

 

//Random rotation

mat4 rotmat = mat4(1.0);

rotmat[0][0] = sin(angle);

rotmat[0][2] = cos(angle);

rotmat[2][0] = -sin(angle+1.570796325);

rotmat[2][2] = -cos(angle+1.570796325);

entitymatrix_=rotmat*entitymatrix_;

 

float scale = rand(vec2(SEED+z,SEED-x));

float sgn = sign(scale-0.5);

scale = (abs(scale-0.5));

scale *= scale;

scale = (scale *sgn + 0.5);

 

scale = scale * scalevariation + 1.0-scalevariation/2.0;

entitymatrix_[0] *= scale;

entitymatrix_[1] *= scale;

entitymatrix_[2] *= scale;

entitymatrix_[3][0] = x*density + rand(vec2(SEED+z,SEED-x))*randomness;

entitymatrix_[3][2] = z*density + rand(vec2(SEED+x,SEED-z))*randomness;

 

Here is the result with trees randomly oriented entirely on the GPU:

 

blogentry-1-0-01957000-1435347160_thumb.jpg

 

Of course you can have issues like no way to prevent two instances from being too close together, but a random seed, density, and randomness values are adjustable. It would also be possible to calculate a neighbor's position and use that to weight the position of the current instance.

 

There are still many questions to be answered, but I think this approach is going in the right direction to design a more powerful and lower overhead vegetation rendering system for Leadwerks 3.

  • Upvote 8
 Share

14 Comments


Recommended Comments

I feel like the more dense the better in a way. The more dense a forest the more you can have a shorter drawing distance and not notice the vegetation popping into view.

 

What's the main bottleneck with this stuff? Is it memory, drawing speed, or disk space?

Link to comment

Probably will turn out to be the vertex shader, in our system, which is good because that is typically never the bottleneck (meaning ours is very efficient).

Link to comment

I've found that gs is mostly usable for billboards or fans of grass.

You emit typically 3-12 new verts per vertex on geometry you are shading.

 

some grass on a mesh:

http://steamcommunity.com/sharedfiles/filedetails/?id=269191666

 

trees on terrain, closest ones are just instanced models, but the far ones are billboards done on the gs.

http://steamcommunity.com/sharedfiles/filedetails/?id=275631982

http://steamcommunity.com/sharedfiles/filedetails/?id=284435423

  • Upvote 2
Link to comment

You do not need to have an output as much as 255 vertices. A tree model could be generated by a smaller primative with a geometry shader then use a tesselation shader to use as an LOD from the distance to camera.

Link to comment

So would this system enable the use of mechanics on objects, for example “chopping down trees / regrowth after chopped down” or would behavior like that need to be manually placed.

Link to comment

So would this system enable the use of mechanics on objects, for example “chopping down trees / regrowth after chopped down” or would behavior like that need to be manually placed.

 

 

I plan for it to allow dynamically adding and removing individual instances.

 

Yes this would be easy to do since each invocation has it's own ID value in opengl.

Link to comment

The invocation option doesn't increase the number of output vertices. I'm not going to use geometry shaders for this.

Link to comment

It would be great to have grass density exposed to the editor and as a dynamic variable that could be changed at runtime like Unity can do.

Be able to let users change grass density in game options is standard in games today.

  • Upvote 1
Link to comment

The invocation option doesn't increase the number of output vertices. I'm not going to use geometry shaders for this.

 

Yes. And that is what a tesselation shader is for.

Link to comment

Looking at this comments and that you want to individually add/remove instances. How about using a texture mask on the terrain to define the area of coverage? You could use it to paint tree and vegetation...

 

The texture mask could surely be used directly by the shader. I'm not able to do this myself but I think that could be possible.

Link to comment

Coverage masks like you describe are used some editors and engines. You can import and export vegetation layers as coverage masks in GROME. If you use World Machine you can export generated coverage masks for other engines to render clutter/trees over those areas. The largest data-set I played with was a monstrous 60Gb, many mask layers per terrain tile. It was very detailed but not practical due to the storage requirements. But you can choose to use as much or as little as you like, specify the resolution of the mask, use different channels for different layers.

Link to comment

Looking at this comments and that you want to individually add/remove instances. How about using a texture mask on the terrain to define the area of coverage? You could use it to paint tree and vegetation...

 

The texture mask could surely be used directly by the shader. I'm not able to do this myself but I think that could be possible.

 

Yes, that is exactly how it works.

 

I am also considering linking a vegetation layer to a paint layer so whereever you paint a grass texture, for example, you will have grass models popping up.

  • Upvote 3
Link to comment
Guest
Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...