This document summarizes the technology behind the rendering of various effects in the game Shadow Warrior, including:
1. Skinned decals were implemented using a geometry-based approach to allow decals to stably cover animating character meshes. The decals are generated asynchronously using adjacency information and skinning matrices.
2. A foliage system was created to allow large open levels with instanced vegetation that uses LoD and is easy to author. Vegetation is planted procedurally based on spawn meshes and stored in multi-resolution grids.
3. Dynamic water rendering was implemented with multiple LoD levels, distortion based on wave parameters, and filtering to prevent aliasing based on vertex frequency limits. Waves are
2. Facts about Shadow Warrior
published by Devolver Digital
18 months production time
team ~35 people (2 tech programmers, 6 total)
modified Hard Reset’s engine (Roadhog)
3. Presentation overview
Act I, Skinned decals generation and rendering
Act II, Foliage authoring and rendering
Act III, Seawater rendering
5. Skinned decals – entry point
In Hard Reset decals only on non-skinned geometry
(static or movable)
Characters destruction by showing/hiding parts of a
model or by changing texture
Lot of blood and gore in Shadow Warrior, must have
skinned decals
6. Skinned decals – two techniques
1. Deffered decals [1][2]
+ mesh generation not needed
+ small amount of data to store and pass to graphics card
- decal floats when mesh is animated
- can be projected on other surfaces, need to mask out
(additional gbuffer usage or additional passes)
Source: http://broniac.blogspot.com/2011/06/deferred-decals.html
7. Skinned decals – two techniques
2. Geometry based [3]
+ stable result when animating mesh
+ cover only desired surface
- mesh generation: time and memory
- additional input data required to generate a decal
8. Skinned decals – two techniques
2. Geometry based [3]
+ stable result when animating mesh
+ cover only desired surface
- mesh generation: time and memory
- additional input data required to generate a decal
9. Skinned decals – input data
Load vertex buffer into CPU memory
Generating adjacency per each triangle in mesh
3 adjacent triangles
Mesh can consist of many isolated elements =>
adjacency groups (store first triangle index of each
adjacency group)
struct STriangleAdjacency
{
UInt32 m_adj0;
UInt32 m_adj1;
UInt32 m_adj2;
UInt32 m_group;
};
10. Skinned decals – generation
Asynchronous (job based)
Copy decal parameters and skinning matrices to job
Basic algorithm
for all adjacency groups
{
find triangle closest to hit point
expand decal by adding adjacent triangles until size reached
and calculate UVs and TBN for each new triangle
}
11. Skinned decals – generation
1. Find triangle closest to hit point
Don't want to process entire mesh
Skeleton and weights == skinned
mesh is naturally divided
In preprocess step create triangle
list for every bone
Iterate through selected and
adjacent bones' lists
Use skinning matrices to get
worldspace positions
12. Skinned decals – generation
2. Expand decal
add hit triangle to “open” list
while open list not empty
{
pop front and calculate its vertices’ positions
if any inside decal (bounding box test, sizeZ = max( sizeX, sizeY ))
{
add triangle to the output list with new UVs and TBN
add adjacent triangles to “open” list if not already processed
}
}
13. Skinned decals – generation
2. Expand decal
add hit triangle to “open” list
while open list not empty
{
pop front and calculate its vertices’ positions
if any inside decal (bounding box test, sizeZ = max( sizeX, sizeY ))
{
add triangle to the output list with new UVs and TBN
add adjacent triangles to “open” list if not already processed
}
}
Special case for first triangle:
If triangle field > decal field always pass bounding box
test
14. Skinned decals – dismemberment
Character dismemberment
implemented
Decals must be split, how?
15. Skinned decals – dismemberment
First version
Store decal spawn info,
recompute on
destruction
- additional CPU time
(5 enemies destroyed at
once can produce 50
jobs)
- cannot show anything until
recomputed decal arrive
== blink
16. Skinned decals – dismemberment
Second version
Character dismemberment is hand-made
by creating separate
meshes
Modify spawn algorithm – create
separate decal chunks for every
visible mesh
Input: adjacency per chunk
On cutting create new decals that
references initial decal geometry
Render proper decal chunks
17. Skinned decals – dismemberment
Second version
+ no recomputation
+ split decals available instantly
- more draws for initial decal (merge on render)
Modified algorithm
for all visible chunks
{
for all adjacency groups
{
find triangle closest to hit point
expand decal chunk by adding adjacent triangles until size
reached
and calculate UVs and TBN for each new triangle
}
}
18. Skinned decals – rendering
Rendered through dynamic vertex buffers
One pass (compose) or 2 passes (normal+compose)
Possible animation through alpha test level shifting
(lower alpha test reference value == bigger decal)
19. Skinned decals – details
Decal size hard limit: 10k vertices
Decal count limit: 100 decals (FIFO)
Vertex memory: 30 MB total, in pool
Typical bullet decal (500 triangles) spawn time
around 0.5 ms on Intel core i7 (async, still can do
better)
Big decals == skinned geometry rendered multiple
times, avoid them, use other techniques, e.g.
texture layering
Use „clamp to border color” with alpha 0.0
21. Foliage system – entry point
• In Hard Reset vegetation only in one area of
one DLC level
• In Shadow Warrior many open levels: forests,
villages, towns, etc.
• Vegetation made as level geometry == no
LoD, no instancing, hard to create and control
(overdraw)
22. Foliage system – entry point
Requirements:
● Instancing
● Specific LoD system
● Easy to plant (levels created in 3dsmax,
gameplay in game editor)
23. Foliage system – entry point
Requirements:
● Instancing
● Specific LoD system
● Easy to plant (levels created in 3dsmax,
gameplay in game editor)
Spawn meshes – meshes with relative
foliage density stored as vertex color
25. Foliage system – planting
• Render spawn meshes in top-down view to an
image (density, position.z and normal)
• In 50x50cm blocks generate random plant
positions (ρ = ρmesh * ρblock, pos.z interpolated)
• Set random yaw
• Optionally align with normal vector
• Store packed matrix
All random values are static!
26. Foliage system – planting
Many levels of foliage possible by splitting
spawn meshes to separate 3dsmax objects
28. Foliage system – storage
Initially one quad tree per map, batch index
and LoD level stored with transformation
Changed to multi resolution grids (2 levels: 4x4
and 64x64 meters, one grid per batch)
29. Foliage system – storage
Grid node contains min/max Z coord and object
ranges for low and high density arrays
Transformation packed into 32 bytes
struct SObject
{
Half4 m_plane0; // 8
Half4 m_plane1; // 16
Half4 m_plane2; // 24
Vec3Packed64 m_position;// 32
};
31. Foliage system – storage
inline Vec3 Vec3Packed64Unpack( const Vec3Packed64 vecPacked, Float unpackScale )
{
Vec3 result;
{
Int32 value = Int32( vecPacked & VEC3PACKED64_MASK );
value <<= VEC3PACKED64_SIGN_RECOVER_SHIFT;
value >>= VEC3PACKED64_SIGN_RECOVER_SHIFT;
result.Z = Float( value ) * unpackScale;
}
{
Int32 value = Int32( ( vecPacked >> 21 ) & VEC3PACKED64_MASK );
value <<= VEC3PACKED64_SIGN_RECOVER_SHIFT;
value >>= VEC3PACKED64_SIGN_RECOVER_SHIFT;
result.Y = Float( value ) * unpackScale;
}
{
Int32 value = Int32( ( vecPacked >> 42 ) & VEC3PACKED64_MASK );
value <<= VEC3PACKED64_SIGN_RECOVER_SHIFT;
value >>= VEC3PACKED64_SIGN_RECOVER_SHIFT;
result.X = Float( value ) * unpackScale;
}
return result;
}
Shift
arithmetic
right!
32. Foliage system – rendering
• Dynamic vertex buffer, 8192 instances max
• Several batches (one batch == all visible
objects with the same mesh and materials)
• LoD levels:
Low – 20% density, range multiplier x1
High – 100% density, range multiplier x1
Ultra – 100% density, range multiplier x2
• Gather with Z range ±15 meters
• Dissolve out on last 5 meters
33. Foliage system – details
• Gather time for 8192 instances in 9 batches: 0.41 ms on Intel
core i7 3.4 GHz
• GPU time: 1.22 ms (0.89 normal + 0.33 compose, 730k + 470k
PSPixelsOut) on Radeon R9 270, 1920x1080
• Memory usage: from 0.7 to 10 MB per level
38. Seawater – entry point
• Docks location with stormy weather planned
in Shadow Warrior
• DX9 renderer (no hw tesselation)
• Dedicated translucent water shader used in
Hard Reset (simple waves, refraction, water
fog, foam)
39. Seawater – mesh
• 3 LoD levels (quad size: 0.5 x 0.5, 2x2, 8x8
meters, LoD 0 dims: 48x48 meters)
• Edge vertices stretched beyond camera far Z
• 33k tris total
• Mesh moved with camera, snapped to integer
world coordinates (constant sampling
positions)
• Stencil test
40. Seawater – vertex shader
Position processing
Distortion Filter Asymmetry Choppiness
Add vertex
texture
Flatten
edges
Distortion
derivative
Filter
Flatten
edges
Modulate
Normals
Distortion
derivative
w. phase
offset
Filter
Bias and
modulate
Foam multiplier
Affect and
orthogonalize
TBN
42. Seawater – waves
• Sea waves are the signal, mesh is a sampling
mechanism
• Nyquist theroem: sampling frequency must be
at least 2 times higher than peak signal
frequency to avoid aliasing
• Different LoD == different sampling
frequencies
43. Seawater – waves
Solution:
• Calculate cuttoff frequency for each vertex
• Pass it to a shader as a vertex attribute
• Filter waves generated in vertex shader using
this frequency limit
struct WaterVertex
{
Vec3 m_pos;
Half2 m_uv0;
Half2 m_uv1;
Float m_geomSoftness;
Float m_waveFreqLimit;
};
44. Seawater – filter
Diagonal direction has lowest
sampling frequency
Lerp cutoff frequencies on
LoD boundaries
Only fc0 and fc1 used in
practice
49. Seawater – vertex texture
• Displace vertices with perlin
noise to avoid wave tiling
• Only LoD 1 and LoD 2
• Calculate proper mip level to
avoid aliasing
• 256x256 R32F vertex texture
• 1024x1024 normals (read in PS)
50. Seawater – pixel shader
• Translucent at the begining, changed to opaque
later on
• 2 x diffuse (water + foam)
• 2 sliding normals + perlin noise normal
• Environment map
• Deffered lighting
51. Seawater – results
• GPU time 0.80 ms (0.02 ms mask, 0.25 ms normal,
0.53 ms compose) @ Radeon R9 270, 1920x1080
• 0 pixels draw (depth & stencil fail): 0.16 ms (0.00 ms mask, 0.07 ms normal,
0.09 ms compose)
• 0 pixels draw (stencil fail): 0.39 ms (0.00 ms mask, 0.30 ms normal,
0.09 ms compose)
52. Special thanks
Łukasz Zdunowski – Lead Artist
Zbigniew Siatecki – Environment Artist
Dominik Misiurski – FX Artist
Artur Maksara – Producer
… and the rest of our team.
53. References
1. http://broniac.blogspot.com/2011/06/deferred-decals.html
2. http://humus.name/index.php?page=3D&ID=83
3. “Character Animation with Direct3D”, Carl Granberg, Charles River Media, 2009
Questions?
Contact:
Email: jarek.pleskot AT flyingwildhog.com
Facebook: Jarosław Pleskot
Twitter: @JaroslawPleskot