One of the main advantages of procedural content is being able to generate a near-infinite number of variations on your content without having to waste valuable artist time. This is especially true when you want to make variants on what is generally “boring” background content, like rocks, trees, walls, and floor tiles. Instead of having an artist make 10 variations of rocks for your level, you can simple have them make one (or none!) and create 100 variations by applying perturbation functions to the content in code.

In this week’s post, I apply this concept by using a fractal generation function to add varying patterns of darkness to my floor tiles. By doing this, I can create an arbitrary number of texture variations for my floor tiles each representing different patterns of random wear-and-tear. With a decent number of variations, it becomes very unlikely for two neighboring floor tiles to look exactly the same which greatly reduces the unrelenting visual regularity you get from seeing the same tile rendered over and over again.

In order to create my floor texture variations, I take my PlanetCute floor texture:

And multiply it in a shader with a plasma fractal like this one:

By programmatically generating random plasma fractals, combining them with the floor tile, and rendering them to a texture, I can create sprite textures that the rest of my engine can use just like any other pre-authored texture.

In XNA, creating a texture at runtime is a simple matter of creating a texture render target and temporarily directing your render commands there instead of to the screen:

for (int i = 0; i < variationCount; ++i) { RenderTarget2D renderTarget = new RenderTarget2D(_graphics, baseTexture.Width, baseTexture.Height); //_graphics is a reference to Game.GraphicDevice _graphics.Textures[1] = _getMyFractal(); _graphics.SetRenderTarget(renderTarget); _graphics.Clear(Color.Transparent); //_renderEffect is an Effect instance that I loaded with a custom shader (.fx) file spriteBatch.Begin(0, null, null, null, null, _renderEffect); spriteBatch.Draw(baseTexture, Vector2.Zero, Color.White); spriteBatch.End(); textureArray[i] = renderTarget; } //Setting RenderTarget to null retargets rendering back to the screen _graphics.SetRenderTarget(null);

The shader I use to combine the two textures is about as simple as they come, a simple multiplication of the RGB from the second texture onto the first:

sampler TextureSampler : register(s0); sampler DetailTextureSampler : register(s1); float4 main(float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 { // Look up the texture color. float4 tex = tex2D(TextureSampler, texCoord); float4 detTex = tex2D(DetailTextureSampler, texCoord); tex.rgb *= detTex.rgb; return tex; } technique Desaturate { pass Pass1 { PixelShader = compile ps_2_0 main(); } }

With my texture rendering code in place, it’s just a matter of finding an algorithm for creating my plasma fractal textures. As usual, Wikipedia provides a convenient solution for this with the

Diamond-Square Algorithm. This algorithm generates a height field of values dividing a texture into smaller and smaller squares, generating values for points by averaging the corners of their containing squares (and diamonds). This page at GameProgrammer.com does a good job of describing the algorithm steps:

The diamond step: Taking a square of four points, generate a random value at the square midpoint, where the two diagonals meet. The midpoint value is calculated by averaging the four corner values, plus a random amount. This gives you diamonds when you have multiple squares arranged in a grid.

The square step: Taking each diamond of four points, generate a random value at the center of the diamond. Calculate the midpoint value by averaging the corner values, plus a random amount generated in the same range as used for the diamond step. This gives you squares again.

And the usage of said steps:

While the length of the side of the squares is greater than zero

{

Pass through the array and perform the diamond step for each square present.

Pass through the array and perform the square step for each diamond present.

Reduce the random number range.

}

If you’re like me and sometimes find code easier to understand than pseudo-code, this answer at Stack Overflow provides Java code implementing the algorithm.

Once I have an implementation of the diamond-square algorithm working in C#, I’m able to create a buffer of Color data that I can apply to a Texture2D using SetData(). Then I can use the resulting texture in the rendering code I presented above to create my floor texture variations.

With my floor texture variations ready, I randomly assign them to floor tiles and turn this:

To many it’s a subtle difference perhaps, but the increased visual irregularity in the floor goes a long way to reduce the sense of repetition in the dungeon. It’s a nice touch if we expect our players to spend a significant amount of time in our dungeons (which we do!)

For reference, I’ve included my texture and fractal rendering code below:

//************* TextureCreator.cs ********************* using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace ProceduralWorldLib { public class TextureCreator { protected GraphicsDevice _graphics; List<Texture2D> _textureSamples; protected Effect _renderEffect; public TextureCreator(GraphicsDevice graphicDevice, Effect renderEffect) { _graphics = graphicDevice; _textureSamples = new List<Texture2D>(); _renderEffect = renderEffect; Debug.Assert(_renderEffect != null); } public void AddSampleTexture(Texture2D texture) { Debug.Assert(texture != null); _textureSamples.Add(texture); } public virtual Texture2D[] GenerateTextureArray() { Texture2D baseTexture = _textureSamples[0]; int variationCount = _textureSamples.Count - 1; SpriteBatch spriteBatch = new SpriteBatch(_graphics); Texture2D[] textureArray = new Texture2D[variationCount]; for (int i = 0; i < variationCount; ++i) { RenderTarget2D renderTarget = new RenderTarget2D(_graphics, baseTexture.Width, baseTexture.Height); _graphics.Textures[1] = _textureSamples[i + 1]; _graphics.SetRenderTarget(renderTarget); _graphics.Clear(Color.Transparent); spriteBatch.Begin(0, null, null, null, null, _renderEffect); spriteBatch.Draw(baseTexture, Vector2.Zero, Color.White); spriteBatch.End(); textureArray[i] = renderTarget; } _graphics.SetRenderTarget(null); return textureArray; } } } //************* PlasmaVariationCreator.cs ********************* using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; namespace ProceduralWorldLib { public class PlasmaVariationCreator : TextureCreator { Texture2D _baseTexture; int _variationCount; int _rngSeed; float[] _plasmaArray; const int PLASMA_DIMENSION_SIZE = 64; public PlasmaVariationCreator(Game game, int variationCount, int rngSeed) : base(game.GraphicsDevice, game.Content.Load<Effect>("Shader/TextureGen")) { _variationCount = variationCount; _rngSeed = rngSeed; } public void SetBaseTexture(Texture2D baseTexture) { _baseTexture = baseTexture; } public Texture2D[] GenerateTextureArrayWithBase(Texture2D baseTexture) { SetBaseTexture(baseTexture); return GenerateTextureArray(); } public override Texture2D[] GenerateTextureArray() { Debug.Assert(_baseTexture != null); Texture2D[] textureArray = new Texture2D[_variationCount]; Random rng = new Random(_rngSeed); SpriteBatch spriteBatch = new SpriteBatch(_graphics); for (int i = 0; i < textureArray.Length; ++i) { _GeneratePlasmaArray(); Texture2D sampleTexture = new Texture2D(_graphics, PLASMA_DIMENSION_SIZE, PLASMA_DIMENSION_SIZE); Color[] colorData = new Color[PLASMA_DIMENSION_SIZE * PLASMA_DIMENSION_SIZE]; for (int x = 0; x < PLASMA_DIMENSION_SIZE; ++x) { for (int y = 0; y < PLASMA_DIMENSION_SIZE; ++y) { float genVal = Math.Min(Math.Max(_GetPlasmaValue(rng, x, y), 0.0f), 1.0f); colorData[y * PLASMA_DIMENSION_SIZE + x] = new Color(genVal, genVal, genVal, 1.0f); } } sampleTexture.SetData<Color>(colorData); RenderTarget2D renderTarget = new RenderTarget2D(_graphics, _baseTexture.Width, _baseTexture.Height); _graphics.Textures[1] = sampleTexture; _graphics.SetRenderTarget(renderTarget); _graphics.Clear(Color.Transparent); spriteBatch.Begin(0, null, null, null, null, _renderEffect); spriteBatch.Draw(_baseTexture, Vector2.Zero, Color.White); spriteBatch.End(); textureArray[i] = renderTarget; } _graphics.SetRenderTarget(null); return textureArray; } float _GetPlasmaValue(Random rng, int x, int y) { return 1.0f - _plasmaArray[plasmaIndex(x, y)] * 0.5f; } void _GeneratePlasmaArray() { const float INITIAL_SEED = 0.5f; _plasmaArray = new float[(PLASMA_DIMENSION_SIZE + 1) * (PLASMA_DIMENSION_SIZE + 1)]; _plasmaArray[plasmaIndex(0,0)] = INITIAL_SEED; //0,0 _plasmaArray[plasmaIndex(PLASMA_DIMENSION_SIZE, 0)] = INITIAL_SEED; //MAX,0 _plasmaArray[plasmaIndex(0, PLASMA_DIMENSION_SIZE)] = INITIAL_SEED; //0,MAX _plasmaArray[plasmaIndex(PLASMA_DIMENSION_SIZE, PLASMA_DIMENSION_SIZE)] = INITIAL_SEED; //MAX,MAX Random rng = new Random(); float h = 0.25f; int currentSideLength = PLASMA_DIMENSION_SIZE; while (currentSideLength >= 2) { int halfSideLength = currentSideLength / 2; for (int x = 0; x < PLASMA_DIMENSION_SIZE; x+= currentSideLength) { for (int y = 0; y < PLASMA_DIMENSION_SIZE; y += currentSideLength) { float average = _plasmaArray[plasmaIndex(x, y)]; //top left average += _plasmaArray[plasmaIndex(x + currentSideLength, y)]; //top right average += _plasmaArray[plasmaIndex(x, y + currentSideLength)]; //bottom left average += _plasmaArray[plasmaIndex(x + currentSideLength, y + currentSideLength)]; //bottom right average *= 0.25f; _plasmaArray[plasmaIndex(x + halfSideLength, y + halfSideLength)] = average + (float)(rng.NextDouble() * 2.0) * h - h; } } for (int x = 0; x < PLASMA_DIMENSION_SIZE; x += halfSideLength) { for (int y = (x + halfSideLength) % currentSideLength; y < PLASMA_DIMENSION_SIZE; y += currentSideLength) { float average = _plasmaArray[plasmaIndex((x - halfSideLength + (PLASMA_DIMENSION_SIZE )) % (PLASMA_DIMENSION_SIZE), y)]; //left of center average += _plasmaArray[plasmaIndex((x + halfSideLength) % (PLASMA_DIMENSION_SIZE), y)]; //right of center average += _plasmaArray[plasmaIndex(x, (y + halfSideLength) % (PLASMA_DIMENSION_SIZE))]; //below center average += _plasmaArray[plasmaIndex(x, (y - halfSideLength + (PLASMA_DIMENSION_SIZE)) % (PLASMA_DIMENSION_SIZE))]; //above center average *= 0.25f; _plasmaArray[plasmaIndex(x, y)] = average + (float)(rng.NextDouble() * 2.0) * h - h; if(x == 0) _plasmaArray[plasmaIndex(PLASMA_DIMENSION_SIZE, y)] = average; if(y == 0) _plasmaArray[plasmaIndex(x, PLASMA_DIMENSION_SIZE)] = average; } } currentSideLength *= 0.5F; h *= 0.5f; } } int plasmaIndex(int x, int y) { return (PLASMA_DIMENSION_SIZE + 1) * y + x; } } }

Pingback: Know Thy Enemy, Know Thyself: The Mary-Sue Trap of Game Design | Game Dev Without a Cause

Pingback: An Island in the Clouds | Game Dev Without a Cause