Sunteți pe pagina 1din 14

Nick Vanheer 1

Digital Arts & Entertainment, Graphics Programming












Nick Vanheer 2
Digital Arts & Entertainment, Graphics Programming


Contents
INTRODUCTION ................................................................................................................................... 3
SETTING UP AND TERMINOLOGY ........................................................................................................ 4
GENERATING GRASS BLADES ............................................................................................................... 4
HLSL code ........................................................................................................................................ 4
Explanation ...................................................................................................................................... 6
Geometry Shader ................................................................................................................................ 8
HLSL code ........................................................................................................................................ 8
Explanation ...................................................................................................................................... 9
Custom structs, Vertex and Pixel shader ........................................................................................... 10
EXTRA: wiring the shader up in a C++ DirectX application. ............................................................... 11
WHATS MORE TO DO ....................................................................................................................... 12
Improve the wind offsets .............................................................................................................. 12
Add more noise maps .................................................................................................................... 13
Take the camera distance into account. ....................................................................................... 13
Further improve the grass shader for games ................................................................................ 13
Closing remarks and inspiration to write this paper ......................................................................... 14
References ......................................................................................................................................... 14













Nick Vanheer 3
Digital Arts & Entertainment, Graphics Programming

INTRODUCTION
Generally grass in games is done through billboarding, generating planes and rendering grass
textures on them. This is a cheap technique that works but is limited in functionality and results in
grass that looks the same across the whole level. Nowadays developers use multiple billboarding
textures and tint them to add some variation, but nevertheless attentive gamers will notice the
repetition of textures.
This paper handles displaying realistic grass in real time for games, where each individual blade is
generated through a HLSL geometry shader. Traditionally this would be a GPU-intensive task, but
with improved hardware and some hacks along the way this becomes usable solution. One of they
goals of writing this shader was to create a system that was flexible and easy to use, while also being
optimized for real-time rendering. Lets jump into it.
Procedural grass in UNIGINE game engine Billboarded grass in Final Fantasy XIV

Sample of our grass shader covered in this document



Nick Vanheer 4
Digital Arts & Entertainment, Graphics Programming

SETTING UP AND TERMINOLOGY
To create realistic looking grass we will be generating each grass blade and by using various black and
white noise maps well add height variation and direction to each blade individually.
To start well take a theoretical look at how we will form the grass blades:

Instead of just rendering tall planes, well
give each grass blade 3 parts. A bottom
stationary part, a middle part and a top
part. This gives us more the visual look of
grass.

These parts have their own bend factor
and their own width and height
(specifically edge [2, 3], [4,5] and [6]).

All of this is done in relation to the surface
normal (so we can have grass growing on
meshes and not only flat planes).

To save time and computation we arent
calculating normals and lighting info for
each grass blade, we will fake this by
using a green gradient diffuse texture
(with the bottom being darker reminiscing
occlusion shadows)

GENERATING GRASS BLADES
Lets get these things in there by making global variables for them after which they can be easily
adjusted from within your shader editor or C++ code.
HLSL code
bool IsWind = true;
bool IsDense = false;
bool AddOriginalGeometry = true;
float m_WindVelocity = 4;

Texture2D heightmap;
Texture2D grassTexture;
Texture2D directionTexture;

int maxHeight = 80;
int maxWidth = 2.5;
float unitHeightSegmentBottom = 0.3;
float unitHeightSegmentMid = 0.4;
float unitHeightSegmentTop = 0.5;

float unitWidthSegmentBottom = 0.5;
float unitWidthSegmentMid = 0.4;
float unitWidthSegmentTop = 0.2;

float bendSegmentBottom = 1;
float bendSegmentMid = 1;
Nick Vanheer 5
Digital Arts & Entertainment, Graphics Programming

float bendSegmentTop = 2;

SamplerState m_TextureSampler
{
Filter = ANISOTROPIC;
AddressU = WRAP;
AddressV = WRAP;
AddressW = WRAP;
};


float m_Time : TIME;
bool m_DiffuseTexture = true;


Next up, lets add the function that will create a single grass shard using these global variables

void CreateGrass(inout TriangleStream<GS_DATA> triStream, float height, float
direction, float3 pos0, float3 pos1, float3 pos2, float3 normal, float3 normal2,
float3 normal3)
{
//1: Calculate basepoints to start at
float3 basePoint = ( pos0 + pos1 + pos2 ) / 3;
float3 normalbasepoint = ( normal + normal2 + normal3 ) / 3;

//2: Calculate segment height, width and total height, width
float grassHeight = height * maxHeight;
float segmentBottomHeight = grassHeight * unitHeightSegmentBottom;
float segmentMidHeight = grassHeight * unitHeightSegmentMid;
float segmentTopHeight = grassHeight * unitHeightSegmentTop;

float grassWidth = maxWidth;
float segmentBottomWidth = grassWidth * unitWidthSegmentBottom;
float segmentMidWidth = grassWidth * unitWidthSegmentMid;
float segmentTopWidth = grassWidth * unitWidthSegmentTop;

//3: initial direction in which to generate the grass blades
direction -= -0.5; //make direction range from [0,1] to [-0.5, 0.5]
float3 grassDirection = normalize((pos2 - pos0) * direction);

//4: calculate the positions for each vertex
float3 v[7]; //trianglestrip
v[0] = basePoint - grassDirection * segmentBottomWidth;
v[1] = basePoint + grassDirection * segmentBottomWidth;
v[2] = basePoint - (grassDirection * segmentMidWidth) + (segmentBottomHeight
* normalbasepoint);
v[3] = basePoint + (grassDirection * segmentMidWidth) + (segmentBottomHeight
* normalbasepoint);


v[4] = v[3] - ((grassDirection) * segmentTopWidth) + (segmentMidHeight *
normalbasepoint);
v[5] = v[3] + ((grassDirection) * segmentTopWidth) + (segmentMidHeight *
normalbasepoint);
v[6] = v[5] + ((grassDirection) * segmentTopWidth) + (segmentTopHeight *
normalbasepoint);

//5: apply wind in the same direction for each grass blade
grassDirection = float3(1,0,0);
Nick Vanheer 6
Digital Arts & Entertainment, Graphics Programming

v[2] += grassDirection * ((m_WindVelocity * bendSegmentBottom) *
sin(m_Time));
v[3] += grassDirection * ((m_WindVelocity * bendSegmentBottom) *
sin(m_Time));
v[4] += grassDirection * ((m_WindVelocity * bendSegmentMid) * sin(m_Time));
v[5] += grassDirection * ((m_WindVelocity * bendSegmentMid) * sin(m_Time));
v[6] += grassDirection * ((m_WindVelocity * bendSegmentTop) * sin(m_Time));

//6: create the vertices with a helper method
CreateVertex(triStream, v[0], float3(0,0,0), float2(0,0));
CreateVertex(triStream, v[1], float3(0,0,0), float2(0.5,0));
CreateVertex(triStream, v[2], float3(0,0,0), float2(0.3,0.3));
CreateVertex(triStream, v[3], float3(0,0,0), float2(0.6,0.3));

CreateVertex(triStream, v[4], float3(0,0,0), float2(0.6,0.3));
CreateVertex(triStream, v[5], float3(0,0,0), float2(0.9,0.6));
CreateVertex(triStream, v[6], float3(0,0,0), float2(1,1));

triStream.RestartStrip();
}

Explanation
Lets go over what happens.
This function gets called from a geometry shader and takes in 3 positions and 3 normals.
Conveniently this is what a geometry shader gets as input when its primitive type is set to a triangle.
(More on the geometry shader part later on.)
(1) Having 3 positions and 3 normals has a couple of advantages for us: we can generate grass blades
in the center of these 3 positions, ensuring us well never generate grass at the corner of an object
and always on the objects surface. Were also able to calculate the direction the grass should be
facing with these positions, so grass can grow on objects in the direction of their surface normal
rather than just going up.

Grass blades growing in the direction of the surface normal
enables us to still read the objects shape.


(2) Next up we calculate the total width and height of each blade, and the individual width and height
of each segment. Remember, the height parameter is a unit value from 0 to 1 so it gets multiplied by
the maximum height.
To calculate the segments height and width, the unit values we had as global parameters come in
handy as they get multiplied by our total segment width or height. Say we want the top segment to
only be 10% of the total glass blades height? We simply set the value of unitHeightSegmentTop to
0.1. To provide even more randomness we could link these unit values to noise maps instead of fixed
Nick Vanheer 7
Digital Arts & Entertainment, Graphics Programming

global variables, but Ive found this too be too much work/performance overhead with only little
visible end result.
(3) Next we calculate the direction the grass should be growing. The passed direction parameter
gives us a value from 0 to 1 from our noise map again, which we bring in range of -0.5 to 0.5 by
subtracting 0.5 from it. We wont be using this value as a raw direction, but well add it as an
increment to the surface normal direction. This will make the grass blade grow in the direction of the
normal, with a slight offset to provide some randomness.
For clarification, heres this step in code
//3: initial direction
direction -= -0.5; //make direction range from [0,1] to [-0.5, 0.5]
float3 grassDirection = normalize((pos2 - pos0) * direction);


(4) Next up, we calculate the final position for the vertices using all of the data calculated above, the
calculations might look difficult but are in essence rather simple. Refer to the following grass blade
infographic (larger version can be found above)

float3 v[7]; //trianglestrip
v[0] = basePoint - grassDirection * segmentBottomWidth;
v[1] = basePoint + grassDirection * segmentBottomWidth;
v[2] = basePoint - (grassDirection * segmentMidWidth) + (segmentBottomHeight
* normalbasepoint);
v[3] = basePoint + (grassDirection * segmentMidWidth) + (segmentBottomHeight
* normalbasepoint);


v[4] = v[3] - ((grassDirection) * segmentTopWidth) + (segmentMidHeight *
normalbasepoint);
v[5] = v[3] + ((grassDirection) * segmentTopWidth) + (segmentMidHeight *
normalbasepoint);
v[6] = v[5] + ((grassDirection) * segmentTopWidth) + (segmentTopHeight *
normalbasepoint);


(5) As a last but quite big step well offset the vertices based on the passed time to simulate wind.
Making the grass move (by using a technique called shearing) adds so much depth to perceiving this
blob of vertices as real grass. This is also a rather complex topic and for the sake of time and
simplicity Ive added a basic offset using sine waves. This is cheap, and unfortunately there are games
out there that still use it, but it works and still amounts to a large amount of realism.
We simulate wind by taking the sine of the total elapsed time since the start of our application, this
value gets pushed in trough C++ each frame since HLSL cant contain state info and wipes its stack
every iteration the shader runs. More on that later on.
The value we take the sine of uses the bendSegmentBottom, bendSegmentMid, bendSegmentTop
values depending on which vertices were offsetting to simulate the top vertex bending more than
the bottom ones. Were also reusing and setting the grassDirection variable to a fixed value so that
all of the vertices bend in the same direction. This saves us some memory creating a new (global)
variable. If we just used the grassDirection value without altering it some grass would bend in
different directions, which would look a bit strange.(As a further expansion we could create a global
variable for the wind direction and set it from C++.).

Nick Vanheer 8
Digital Arts & Entertainment, Graphics Programming

(6) And finally we create all vertices:
//create the vertices with a helper method
CreateVertex(triStream, v[0], float3(0,0,0), float2(0,0));
CreateVertex(triStream, v[1], float3(0,0,0), float2(0.5,0));
CreateVertex(triStream, v[2], float3(0,0,0), float2(0.3,0.3));
CreateVertex(triStream, v[3], float3(0,0,0), float2(0.6,0.3));

CreateVertex(triStream, v[4], float3(0,0,0), float2(0.6,0.3));
CreateVertex(triStream, v[5], float3(0,0,0), float2(0.9,0.6));
CreateVertex(triStream, v[6], float3(0,0,0), float2(1,1));

triStream.RestartStrip();


These vertices are added as a triangle strip which means the order is important as it
determines the triangles.

The CreateVertex method is a helper method taking in the Geometry Shaders stream object, the
vertex position, the normal (which we dont need), and texture coordinates.
The texture coordinates are generated to map to a gradient texture from light to
darker green. The top parts of the grass blades will use the lighter part, while the
bottom parts closer to the ground will use the darker parts, simulating occlusion
shadows. This gradient texture can be very small, even a gradient (1px x 64px) strip,
saving a lot of memory.
All of this generates our final single blade of grass.
Geometry Shader
The above function gets called in our geometry shader, which we'll handle next.
HLSL code

[maxvertexcount(60)]
void MainGS(triangle VS_OUTPUT input[3], inout TriangleStream<GS_DATA> triStream)
{
//add original geometry
if(AddOriginalGeometry)
{
CreateVertex(triStream, input[0].Position, input[0].Normal,
input[0].TexCoord);
CreateVertex(triStream, input[1].Position, input[1].Normal,
input[1].TexCoord);
CreateVertex(triStream, input[2].Position, input[2].Normal,
input[2].TexCoord);

triStream.RestartStrip();
}

//sample height and direction noise maps
float samplePoint = heightmap.SampleLevel(samHeightmap, input[0].TexCoord,
1.0f).r;
float samplePoint2 = heightmap.SampleLevel(samHeightmap, input[1].TexCoord,
1.0f).r;
Nick Vanheer 9
Digital Arts & Entertainment, Graphics Programming

float samplePoint3 = heightmap.SampleLevel(samHeightmap, input[2].TexCoord,
1.0f).r;

float directionSamplePoint = directionTexture.SampleLevel(samHeightmap,
input[0].TexCoord, 1.0f).r;
float directionSamplePoint2 = directionTexture.SampleLevel(samHeightmap,
input[1].TexCoord, 1.0f).r;
float directionSamplePoint3 = directionTexture.SampleLevel(samHeightmap,
input[2].TexCoord, 1.0f).r;


//split the received triangle in 3 sub-triangles
if(IsDense)
{
float3 m0 = (input[0].Position + input[1].Position) * 0.5;
float3 m1 = (input[1].Position + input[2].Position) * 0.5;
float3 m2 = (input[2].Position + input[0].Position) * 0.5;


CreateGrass(triStream, samplePoint, directionSamplePoint, m1,
input[1].Position, m0, input[0].Normal, input[1].Normal, input[2].Normal);
CreateGrass(triStream, samplePoint2, directionSamplePoint2,
input[0].Position, m0, m2, input[0].Normal, input[1].Normal, input[2].Normal);
CreateGrass(triStream, samplePoint3, directionSamplePoint3, m2, m1,
input[2].Position, input[0].Normal, input[1].Normal, input[2].Normal);
}
else
{
CreateGrass(triStream, samplePoint, directionSamplePoint,
input[0].Position, input[1].Position, input[2].Position, input[0].Normal,
input[1].Normal, input[2].Normal);
}
}


Explanation
Again, lets go over this function step by step.
We start of by adding our original geometry if AddOriginalGeometry is set to true. In most cases we
dont want to do this, well just render the original object with their own shader, and then apply the
grass on top of it with this shader. If this bool is set to true, then the geometry will get added but will
also end up with a green color like the grass, which might not be much of a problem when the grass
is really dense. We could further enhance this shader and add diffuse, specular, Fresnel and other
lighting info to our original geometry, and render the grass on top of that, but thats beyond the
scope of this paper.
Next we sample the height and direction noise maps using the SampleLevel method of the Texture2D
variable.
Then we simply call the CreateGrass method discussed earlier, and pass in the vertices the geometry
shader received.
If the IsDense bool is true, well divide the received triangle in 3 smaller triangles and render 3
blades of grass instead of 1.

Nick Vanheer 10
Digital Arts & Entertainment, Graphics Programming

Custom structs, Vertex and Pixel shader
The vertex shader and pixel shaders are really straightforward, but Ive included them and their
return type structs for completeness.
struct VS_INPUT
{
float3 Position : POSITION;
float3 Normal : NORMAL;
float2 TexCoord : TEXCOORD0;
};

struct VS_OUTPUT
{
float4 Position : SV_POSITION;
float3 Normal : NORMAL;
float2 TexCoord : TEXCOORD0;
float4 worldPosition: COLOR0;
};

struct GS_DATA
{

float4 Position : SV_POSITION;
float3 Normal : NORMAL;
float2 TexCoord : TEXCOORD0;
};

//Vertex shader
VS_OUTPUT MainVS(VS_INPUT input)
{
VS_OUTPUT output = (VS_OUTPUT)0;

output.Position = float4(input.Position,1);

// Store the texture coordinates for the pixel shader.
output.TexCoord = input.TexCoord * float2(uvScaleX, uvScaleY);

// Calculate the normal vector against the world matrix.
output.Normal = mul(input.Normal, (float3x3)m_MatrixWorld);

// Normalize the normal vector.
output.Normal = normalize(output.Normal);

// Calculate the position of the vertex in world space.
output.worldPosition = mul(input.Position, m_MatrixWorld);

return output;
}

//Pixel shader
float4 MainPS(GS_DATA input) : SV_TARGET
{
float3 diffuse;
float3 color_rgb = float3(76,115,49); //simple green color

if(m_DiffuseTexture)
{
float4 d = grassTexture.Sample(m_TextureSampler, -input.TexCoord);
return d;
}
Nick Vanheer 11
Digital Arts & Entertainment, Graphics Programming


return float4(diffuse, 1);
}

With our shader implemented, we can quickly create various different kinds of grass, tall, low,
straight, moving furiously in a storm, simply by adjusting the global variables. Here are some quick
examples.

(edges look jagged because no anti-aliasing was applied)
EXTRA: wiring the shader up in a C++ DirectX application.
This shader can be tested in shader tools such as FxComposer, but as an extra Ill quickly show you
how to wire up the global variables to our C++ application.
Well cover 2 parameter types, a texture and a float variable.
First lets declare them in our header file:
TextureData* m_DiffuseMapTexture;
static ID3DX11EffectShaderResourceVariable* m_DiffuseSRVvariable;

float m_MaxHeight;
static ID3DX11EffectScalarVariable* m_MaxHeightVariable;

Each parameter in HLSL has 2 variables in C++ code. One variable to store the actual data
(m_DiffuseMapTexture and m_MaxHeight), and another one that handles the connection to the
shader and retrieves and stores the value (m_DiffuseSRVvariable and m_MaxHeightVariable).
In our C++ code file, well start by initializing the static variables
ID3DX11EffectShaderResourceVariable* GrassMaterial::m_DiffuseSRVvariable = nullptr;
D3DX11EffectScalarVariable* GrassMaterial::m_MaxHeightVariable = nullptr;

Next up, well load these connection variables at the start of our application
if (!m_DiffuseSRVvariable)
{
m_DiffuseSRVvariable = m_pEffect->GetVariableByName("grassTexture")-
>AsShaderResource();
if (!m_DiffuseSRVvariable->IsValid())
{
m_DiffuseSRVvariable = nullptr;
}
Nick Vanheer 12
Digital Arts & Entertainment, Graphics Programming

}

if (!m_MaxHeightVariable)
{
m_MaxHeightVariable = m_pEffect->GetVariableByName("maxHeight")-
>AsScalar();
if (!m_MaxHeightVariable->IsValid())
{
Logger::LogWarning(L"DiffuseMaterial::LoadEffectVariables() >
\'max height time\' variable not found!");
m_MaxHeightVariable = nullptr;
}
}



In our update loop, well update these variables each frame
//set diffuse texture
if (m_DiffuseMapTexture && m_DiffuseSRVvariable)
{
m_DiffuseSRVvariable->SetResource(m_DiffuseMapTexture-
>GetShaderResourceView());
}

if(m_ MaxHeightVariable)
{
m_ MaxHeightVariable ->SetFloat(m_MaxHeight);
}

This is pretty straightforward, and the same for all other noise maps and global variables in our
shader file.
WHATS MORE TO DO
Improve the wind offsets
Right now were using simple sine waves to stimulate wind, but the shader can be improved to
stimulate real-life scenarios better. We can pass in a wind direction variable so that we can modify
the wind direction from our C++ game. Similarly we can also change wire the wind velocity variable
to our C++ applications so we can dynamically change the speed that wind is applied.
We can also give each blade of grass a separate weight (through another noise map) so that some
grass will move a lot under influence of the wind while others will only move a little bit.
We could also take the distance of the
camera/player into account and apply more wind
to grass blades that are closer to the viewer, for
example when moving fast trough the game
level.
Flower (PS3/PS4) uses wind influences
extensively as a means of telling the player how
fast hes moving or telling you when parts of the
level are unlocked or altered.
Nick Vanheer 13
Digital Arts & Entertainment, Graphics Programming

Add more noise maps
We could add even more noise maps to add more randomization to the grass. One map you could
add is a color variation map. This can be a colored noise map, or black and white map with one or
more colors defined in the shader. The pixel shader would sample this map and add or multiply this
value to the existing color, resulting in some grass blades having a darker or lighter tint.
We could also add a map to control the bend segments so that some grass blades would bend more
than others.
Take the camera distance into account.
Now the same amount of grass is rendered everywhere, even in the distance. This is a performance
overhead because were still rendering 7 vertices (21 when IsDense is true) for every triangle, even
when its barely visible. We could opt to pass in the camera position into the shader and simplify the
rendered grass in the distance (some games billboard distant grass and only render grass blades that
are close to the camera)
Further improve the grass shader for games
Now grass grows in the direction of the surface normal, but for games having grass that only grows
up could be enough. The CreateGrass method can be altered and optimized a lot when you just have
to render grass that grows up. For some games having 3 segments might not even be necessary
either, and the more data we can cut (pun not intended) for each blade, the larger the amount of
blades we can render.


Procedural grass in Outerra, can you spot the LODs? (level of detail)

Nick Vanheer 14
Digital Arts & Entertainment, Graphics Programming

Closing remarks and inspiration to write this paper

A big inspiration to write this paper was the video game Xenoblade Chronicles. This Wii game
pushed the (not so graphically advanced) console to its limits with a large fantasy open world
design. The game made tremendous use of billboarding to generate foliage, it even rotated the
billboards to always face the games camera.

Nevertheless this game and its gorgeous design captured my heart, making me stop playing at
times and just left me gazing at my TV while enjoying the scenery and music. This game was
instrumental in writing this shader and my goal of trying to give level designers a good amount of
customization, while also making the shader fast and easy to maintain.

References
Procedural Grass in Outerra: http://youtu.be/pdMaFWGLxKE
Accompanying blog post: http://outerra.blogspot.be/2012/05/procedural-grass-rendering.html
UNIGINE procedural grass: http://unigine.com/devlog/2008/09/25/47
Info on perlin and fractal noise:
http://www.neilblevins.com/cg_education/fractal_noise/fractal_noise.html
Video reference of Xenoblades grass: http://youtu.be/C4y8991XDWU

S-ar putea să vă placă și