100%(1)100% au considerat acest document util (1 vot)
2K vizualizări14 pagini
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 Vanheer
Digital Arts & Entertainment, Second Year.
2014
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 Vanheer
Digital Arts & Entertainment, Second Year.
2014
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 Vanheer
Digital Arts & Entertainment, Second Year.
2014
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;
//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);
//5: apply wind in the same direction for each grass blade grassDirection = float3(1,0,0); Nick Vanheer 6 Digital Arts & Entertainment, Graphics Programming
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)
(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));
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
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; };
// 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;
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()); }
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