Glusoft

Random terrain and perlin noise with SDL

In this tutorial the goal is to render to a texture and create a random terrain with perlin noise with SDL.
You will need to have a good understanding of texture for this tutorial, if it’s your first time you can see the other tutorial : Display an image with SDL_image

Result of perlin noise with SDL

Generating perlin noise with SDL

Noise function

To construct the perlin noise we need to have a 2D noise function, this function will generate a pseudo random number from two number.
To help us with this task we define an arbitrary SEED and a arbitrary array of number HASH. Then the noise function will use the x and y position as index of the HASH array. Here is the source of the HASH.

static const int  SEED = 1985;

static const unsigned char  HASH[] = {
	208,34,231,213,32,248,233,56,161,78,24,140,71,48,140,254,245,255,247,247,40,
	185,248,251,245,28,124,204,204,76,36,1,107,28,234,163,202,224,245,128,167,204,
	9,92,217,54,239,174,173,102,193,189,190,121,100,108,167,44,43,77,180,204,8,81,
	70,223,11,38,24,254,210,210,177,32,81,195,243,125,8,169,112,32,97,53,195,13,
	203,9,47,104,125,117,114,124,165,203,181,235,193,206,70,180,174,0,167,181,41,
	164,30,116,127,198,245,146,87,224,149,206,57,4,192,210,65,210,129,240,178,105,
	228,108,245,148,140,40,35,195,38,58,65,207,215,253,65,85,208,76,62,3,237,55,89,
	232,50,217,64,244,157,199,121,252,90,17,212,203,149,152,140,187,234,177,73,174,
	193,100,192,143,97,53,145,135,19,103,13,90,135,151,199,91,239,247,33,39,145,
	101,120,99,3,186,86,99,41,237,203,111,79,220,135,158,42,30,154,120,67,87,167,
	135,176,183,191,253,115,184,21,233,58,129,233,142,39,128,211,118,137,139,255,
	114,20,218,113,154,27,127,246,250,1,8,198,250,209,92,222,173,21,88,102,219
};
	
static int noise2(int x, int y)
{
	int  yindex = (y + SEED) % 256;
	if (yindex < 0)
		yindex += 256;
	int  xindex = (HASH[yindex] + x) % 256;
	if (xindex < 0)
		xindex += 256;
	const int  result = HASH[xindex];
	return result;
}

Linear interpolation

Another useful function we will need to compute the perlin noise is linear interpolation :

static double lin_inter(double x, double y, double s)
{
	return x + s * (y-x);
}

But unfortunately for the algorithm to work we will use a smooth version of the linear interpolation :

static double smooth_inter(double x, double y, double s)
{
    return lin_inter( x, y, s * s * (3-2*s) );
}

Final noise function

This is time for creating a final noise function consisting of a first iteration of the perlin noise. We will construct this noise function by using the previous defined functions.

static double noise2d(double x, double y)
{
    const int  x_int = floor( x );
    const int  y_int = floor( y );
    const double  x_frac = x - x_int;
    const double  y_frac = y - y_int;
    const int  s = noise2( x_int, y_int );
    const int  t = noise2( x_int+1, y_int );
    const int  u = noise2( x_int, y_int+1 );
    const int  v = noise2( x_int+1, y_int+1 );
    const double  low = smooth_inter( s, t, x_frac );
    const double  high = smooth_inter( u, v, x_frac );
    const double  result = smooth_inter( low, high, y_frac );
    return result;
}

Perlin noise function

This time we can define the perlin noise function, we simply iterate over noise2d, and we can control more precisely the amount of noise by ajusting the frequency and the depth.

double perlin2d(double x, double y, double freq, int depth)
{
    double  xa = x*freq;
    double  ya = y*freq;
    double  amp = 1.0;
    double  fin = 0;
    double  div = 0.0;
    for (int i=0; i<depth; i++)
    {
        div += 256 * amp;
        fin += noise2d( xa, ya ) * amp;
        amp /= 2;
        xa *= 2;
        ya *= 2;
    }
    return fin/div;
}

Render to a texture

Basic perlin texture

The next step is to render to a texture, for that we need to create a renderer with the flag SDL_RENDERER_TARGETTEXTURE:

SDL_Renderer *renderer = SDL_CreateRenderer(win, -1,SDL_RENDERER_ACCELERATED | SDL_RENDERER_TARGETTEXTURE);

Then we can create the texture with the right RGBA format:

SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_STATIC,WIN_WIDTH, WIN_HEIGHT);

Here the size of the texture is simply the size of the screen:

#define WIN_WIDTH 800
#define WIN_HEIGHT 600

To get the pixel color we will use an object called pixel format, we need to define it with the same format RGBA8888:

SDL_PixelFormat *formatPix = SDL_AllocFormat(SDL_PIXELFORMAT_RGBA8888);

Then we can start generating some perlin noise in a pixel array:

int xOrg = 100000;
int yOrg = 100000;
float freq = 0.7f;
int depth = 5;
int scale = 10;

Uint32 pixels[WIN_WIDTH*WIN_HEIGHT];

for (int y = 0; y < WIN_HEIGHT; y++) {
    for (int x = 0; x < WIN_WIDTH; x++) {
        float xCoord = xOrg + x / ((float)WIN_WIDTH) * scale;
        float yCoord = yOrg + y / ((float)WIN_HEIGHT) * scale;
        float value1 = perlin2d(yCoord, xCoord, freq, depth);
        Uint8 color = 255*value1;
        pixels[y * WIN_WIDTH + x] = SDL_MapRGBA(formatPix, color, color, color, 255);
    }
}

After the pixels array is filled with shade of grey computed by the perlin noise function.
We can update the texture accordingly:

SDL_UpdateTexture(texture, NULL, pixels, sizeof(Uint32) * WIN_WIDTH);

The texture should look like this :

Perlin texture

To visualize the texture you will need to have an event loop, something simple like this :

while (true) {
    SDL_Event e;
    if (SDL_WaitEvent(&e)) {
        if (e.type == SDL_QUIT) {
            break;
        }
    }
    SDL_RenderClear(renderer);
    SDL_Rect rect{0, 0, WIN_WIDTH, WIN_HEIGHT};
    SDL_RenderCopyEx(renderer, texture, NULL, &rect, 0, NULL, SDL_FLIP_NONE);
    SDL_RenderPresent(renderer);
}

Random terrain texture

To generate the terrain we need to pick some colors for that we choose and array of 7 colors of some shade of blue and grey.

SDL_Color pickColor(int val)
{
    SDL_Color arrColors[] = {
        SDL_Color{40,40,40,255}, // black
        SDL_Color{41,54,111,255}, // dark blue
        SDL_Color{59,93,201,255},
        SDL_Color{64,166,245,255},
        SDL_Color{114,239,247,255}, // light blue
        SDL_Color{148,175,194}, // light grey
        SDL_Color{86,108,134} // dark grey
    };

    return arrColors[val];
}

After that wee need to use that function when we fill the array of pixels:

for (int y = 0; y < WIN_HEIGHT; y++) {
    for (int x = 0; x < WIN_WIDTH; x++) {
        float xCoord = xOrg + x / ((float)WIN_WIDTH) * scale;
        float yCoord = yOrg + y / ((float)WIN_HEIGHT) * scale;
        float value1 = perlin2d(yCoord, xCoord, freq, depth);
  
        SDL_Color col = pickColor((int)(value1*16)%7);
        pixels[y * WIN_WIDTH + x] = SDL_MapRGBA(formatPix, col.r, col.g, col.b, 255);
    }
}

Full source code for random terrain and perlin noise with SDL

#include <iostream>
#include <math.h>
#include <SDL2/SDL.h>
#include <SDL2_image/SDL_image.h>
    
#define WIN_WIDTH 800
#define WIN_HEIGHT 600
    
static const int  SEED = 1985;
    
static const unsigned char  HASH[] = {
    208,34,231,213,32,248,233,56,161,78,24,140,71,48,140,254,245,255,247,247,40,
    185,248,251,245,28,124,204,204,76,36,1,107,28,234,163,202,224,245,128,167,204,
    9,92,217,54,239,174,173,102,193,189,190,121,100,108,167,44,43,77,180,204,8,81,
    70,223,11,38,24,254,210,210,177,32,81,195,243,125,8,169,112,32,97,53,195,13,
    203,9,47,104,125,117,114,124,165,203,181,235,193,206,70,180,174,0,167,181,41,
    164,30,116,127,198,245,146,87,224,149,206,57,4,192,210,65,210,129,240,178,105,
    228,108,245,148,140,40,35,195,38,58,65,207,215,253,65,85,208,76,62,3,237,55,89,
    232,50,217,64,244,157,199,121,252,90,17,212,203,149,152,140,187,234,177,73,174,
    193,100,192,143,97,53,145,135,19,103,13,90,135,151,199,91,239,247,33,39,145,
    101,120,99,3,186,86,99,41,237,203,111,79,220,135,158,42,30,154,120,67,87,167,
    135,176,183,191,253,115,184,21,233,58,129,233,142,39,128,211,118,137,139,255,
    114,20,218,113,154,27,127,246,250,1,8,198,250,209,92,222,173,21,88,102,219
};
    
static int noise2(int x, int y)
{
    int  yindex = (y + SEED) % 256;
    if (yindex < 0)
        yindex += 256;
    int  xindex = (HASH[yindex] + x) % 256;
    if (xindex < 0)
        xindex += 256;
    const int  result = HASH[xindex];
    return result;
}
    
static double lin_inter(double x, double y, double s)
{
    return x + s * (y-x);
}
    
static double smooth_inter(double x, double y, double s)
{
    return lin_inter( x, y, s * s * (3-2*s) );
}
    
static double noise2d(double x, double y)
{
    const int  x_int = floor( x );
    const int  y_int = floor( y );
    const double  x_frac = x - x_int;
    const double  y_frac = y - y_int;
    const int  s = noise2( x_int, y_int );
    const int  t = noise2( x_int+1, y_int );
    const int  u = noise2( x_int, y_int+1 );
    const int  v = noise2( x_int+1, y_int+1 );
    const double  low = smooth_inter( s, t, x_frac );
    const double  high = smooth_inter( u, v, x_frac );
    const double  result = smooth_inter( low, high, y_frac );
    return result;
}
    
double perlin2d(double x, double y, double freq, int depth)
{
    double  xa = x*freq;
    double  ya = y*freq;
    double  amp = 1.0;
    double  fin = 0;
    double  div = 0.0;
    for (int i=0; i < depth; i++)
    {
        div += 256 * amp;
        fin += noise2d( xa, ya ) * amp;
        amp /= 2;
        xa *= 2;
        ya *= 2;
    }
    return fin/div;
}
    
SDL_Color pickColor(int val)
{
    SDL_Color arrColors[] = {
        SDL_Color{40,40,40,255}, // black
        SDL_Color{41,54,111,255}, // dark blue
        SDL_Color{59,93,201,255},
        SDL_Color{64,166,245,255},
        SDL_Color{114,239,247,255}, // light blue
        SDL_Color{148,175,194}, // light grey
        SDL_Color{86,108,134} // dark grey
    };
    
    return arrColors[val];
}
    
int main(int argc, const char * argv[]) {
    SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS);
    SDL_Window *win = SDL_CreateWindow("Random Terrain", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, WIN_WIDTH, WIN_HEIGHT, 0);
    SDL_Renderer *renderer = SDL_CreateRenderer(win, -1,SDL_RENDERER_ACCELERATED | SDL_RENDERER_TARGETTEXTURE);
    
    SDL_PixelFormat *formatPix = SDL_AllocFormat(SDL_PIXELFORMAT_RGBA8888);
    
    SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_STATIC,WIN_WIDTH, WIN_HEIGHT); /* Il ne faudra pas oublier les vérifications */
        
    int xOrg = 100000;
    int yOrg = 100000;
    float freq = 0.7f;
    int depth = 5;
    int scale = 10;
    
    Uint32 pixels[WIN_WIDTH*WIN_HEIGHT];

    for (int y = 0; y < WIN_HEIGHT; y++) {
        for (int x = 0; x < WIN_WIDTH; x++) {
            float xCoord = xOrg + x / ((float)WIN_WIDTH) * scale;
            float yCoord = yOrg + y / ((float)WIN_HEIGHT) * scale;
            float value1 = perlin2d(yCoord, xCoord, freq, depth);
            SDL_Color col = pickColor((int)(value1*16)%7);
            pixels[y * WIN_WIDTH + x] = SDL_MapRGBA(formatPix, col.r, col.g, col.b, 255);
        }
    }

    SDL_UpdateTexture(texture, NULL, pixels, sizeof(Uint32) * WIN_WIDTH);
    SDL_FreeFormat(formatPix);
        
    while (true) {
        SDL_Event e;
        if (SDL_WaitEvent(&e)) {
            if (e.type == SDL_QUIT) {
                break;
            }
        }
        SDL_RenderClear(renderer);
        SDL_Rect rect{0, 0, WIN_WIDTH, WIN_HEIGHT};
        SDL_RenderCopyEx(renderer, texture, NULL, &rect, 0, NULL, SDL_FLIP_NONE);
        SDL_RenderPresent(renderer);
    }
    
    SDL_DestroyTexture(texture);
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(win);
    SDL_Quit();
        
    return 0;
}