Vegetation
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:
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.
- 8
14 Comments
Recommended Comments