nick vanheer - grass geometry shader

14
Nick Vanheer 1 Digital Arts & Entertainment, Graphics Programming

Upload: nick-van-heer

Post on 28-Dec-2015

255 views

Category:

Documents


2 download

DESCRIPTION

Geometry Shader I've written and documented for my graphics programming course as part of my exam assignment. The document walks you trough the creation of a grass geometry shader using C++ and HLSL.Nick VanheerDigital Arts & Entertainment, Second Year.2014

TRANSCRIPT

Page 1: Nick Vanheer - Grass Geometry Shader

Nick Vanheer 1 Digital Arts & Entertainment, Graphics Programming

Page 2: Nick Vanheer - Grass Geometry Shader

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

WHAT’S 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

Page 3: Nick Vanheer - Grass Geometry Shader

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. Let’s jump into it.

Procedural grass in UNIGINE game engine Billboarded grass in Final Fantasy XIV

Sample of our grass shader covered in this document

Page 4: Nick Vanheer - Grass Geometry Shader

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 we’ll add height variation and direction to each blade individually.

To start we’ll take a theoretical look at how we will form the grass blades:

Instead of just rendering tall planes, we’ll 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 aren’t 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 Let’s 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;

Page 5: Nick Vanheer - Grass Geometry Shader

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, let’s 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);

Page 6: Nick Vanheer - Grass Geometry Shader

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 Let’s 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 we’ll never generate grass at the corner of an object

and always on the object’s surface. We’re 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 object’s 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 blade’s 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

Page 7: Nick Vanheer - Grass Geometry Shader

Nick Vanheer 7 Digital Arts & Entertainment, Graphics Programming

global variables, but I’ve 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 won’t be using this value as a raw direction, but we’ll 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, here’s 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 we’ll 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 I’ve 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 can’t 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 we’re offsetting to simulate the top vertex bending more than

the bottom ones. We’re 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++.).

Page 8: Nick Vanheer - Grass Geometry Shader

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 Shader’s stream object, the

vertex position, the normal (which we don’t 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;

Page 9: Nick Vanheer - Grass Geometry Shader

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, let’s 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

don’t want to do this, we’ll 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 that’s 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, we’ll divide the received triangle in 3 smaller triangles and render 3

blades of grass instead of 1.

Page 10: Nick Vanheer - Grass Geometry Shader

Nick Vanheer 10 Digital Arts & Entertainment, Graphics Programming

Custom structs, Vertex and Pixel shader The vertex shader and pixel shaders are really straightforward, but I’ve 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; }

Page 11: Nick Vanheer - Grass Geometry Shader

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 I’ll quickly show you

how to wire up the global variables to our C++ application.

We’ll cover 2 parameter types, a texture and a float variable.

First let’s 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, we’ll start by initializing the static variables

ID3DX11EffectShaderResourceVariable* GrassMaterial::m_DiffuseSRVvariable = nullptr; D3DX11EffectScalarVariable* GrassMaterial::m_MaxHeightVariable = nullptr;

Next up, we’ll 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; }

Page 12: Nick Vanheer - Grass Geometry Shader

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, we’ll 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.

WHAT’S MORE TO DO

Improve the wind offsets Right now we’re 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 he’s moving or telling you when parts of the

level are unlocked or altered.

Page 13: Nick Vanheer - Grass Geometry Shader

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 we’re still rendering 7 vertices (21 when IsDense is true) for every triangle, even

when it’s 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)

Page 14: Nick Vanheer - Grass Geometry Shader

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 game’s camera. Nevertheless this game and it’s 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 Xenoblade’s grass: http://youtu.be/C4y8991XDWU