GMS 2 Generate noise texture with shader and retrieve its data in GML

NeutronCat

Member
GM Version: GMS 2.3.1 (and all other GMS versions that support shaders, but some changes might be required)
Target Platform: ALL that support shaders
Download: Project File
Links: GLSL Noise Algorithms

Summary:
In this tutorial you will create a Fractional Brownian motion (FBM) noise texture, and retrieve its data from the texture in GML for further usage. The main purpose of this tutorial is to demonstrate how to generate noise texture with shader, and how to use shader to do simple computations.

Tutorial:
Noise textures can be very useful for procedural generation like terrains. We can use a shader that make use of the parallel computations power of GPU, for faster noise texture generation. The noise algorithm is from here (link above), Fractional Brownian motion, which can provide fractual detail features and look more realistic.

Create a new object named "oNoiseGen", which is for the whole process for this tutorial, and then place it to the initial room.
Write the scripts in Create event:
GML:
noise_width = 512;    // The width of the noise texture
noise_height = 512;    // The height of the noise texture
noise_scale = 10;    // The UV scale of the noise texture

surf = surface_create(noise_width, noise_height);    // Surface for noise texture rendering
data[noise_height - 1][noise_width - 1] = 0;    // For storing data from the noise texture

generated = false;    // Whether the generation process is finished

randomize();
And then in Draw event:
GML:
if(!surface_exists(surf))
{
    surf = surface_create(noise_width, noise_height);
}
else
{
    if(generated)
    {
        draw_surface(surf, x, y);
    }
    else
    {
        // Using shader to render the FBM noise texture to the surface
        surface_set_target(surf);
        shader_set(shFBM);
        var scale_param = shader_get_uniform(shFBM, "u_Scale");
        var offset_param = shader_get_uniform(shFBM, "u_vTexcoordOffset");
        shader_set_uniform_f(scale_param, noise_scale);
        // The range of the random number should not be too large, due to floating-point precision problem
        shader_set_uniform_f(offset_param, random_range(-10000, 10000), random_range(-10000, 10000));
        draw_surface(surf, 0, 0);
        shader_reset();
        surface_reset_target();
    
        // Create a buffer for the surface and extract the data from it
        var buff = buffer_create(noise_width * noise_height * 4, buffer_fixed, 1);
        buffer_get_surface(buff, surf, 0);
        buffer_seek(buff, buffer_seek_start, 0);
        for(var j = 0; j < noise_height; j++)
        {
            for(var i = 0; i < noise_width; i++)
            {
                // 4 bytes (32 bits) per pixel, only the last byte is needed
                data[j][i] = buffer_read(buff, buffer_u32) & 255;
            }
        }
        buffer_delete(buff);
    
        // Set the flag to stop the whole process after successfully getting the data
        generated = true;
    }
}
The basic idea is that, use the FBM noise shader to render noise patterns to the surface, and create a buffer of the surface for faster reading data in GML, but since buffer is not convenient to use, it then stores the data to a 2D array.

2.
Create a new shader named "shFBM", for rendering FBM noise patterns.

In its vertex shader:
GML:
attribute vec3 in_Position;                  // (x,y,z)
attribute vec2 in_TextureCoord;              // (u,v)

varying vec2 v_vTexcoord;

void main()
{
    vec4 object_space_pos = vec4( in_Position.x, in_Position.y, in_Position.z, 1.0);
    gl_Position = gm_Matrices[MATRIX_WORLD_VIEW_PROJECTION] * object_space_pos;

    v_vTexcoord = in_TextureCoord;
}
The vertex color is not needed here, so it is removed.

In its fragment shader:
GML:
varying vec2 v_vTexcoord;

float rand(vec2 n)
{
    return fract( sin( dot( n, vec2(12.9898, 4.1414) ) ) * 43758.5453 );
}

float noise(vec2 n)
{
    const vec2 d = vec2( 0.0, 1.0 );
    vec2 b = floor( n ), f = smoothstep( vec2(0.0), vec2(1.0), fract(n) );
    return mix( mix( rand(b), rand(b + d.yx), f.x ), mix( rand(b + d.xy), rand(b + d.yy), f.x ), f.y );
}

// Rotate to reduce axial bias
const mat2 rot = mat2( 0.8, -0.6, 0.6, 0.8 );

float fbm( vec2 p )
{
    float f = 0.0;
    f += 0.5000 * noise( p ); p = rot * p * 2.02;
    f += 0.2500 * noise( p ); p = rot * p * 2.03;
    f += 0.1250 * noise( p ); p = rot * p * 2.01;
    f += 0.0625 * noise( p );

    return f / 0.9375;
}

uniform float u_Scale;
uniform vec2 u_vTexcoordOffset;

void main()
{
    float f = fbm( v_vTexcoord * u_Scale + u_vTexcoordOffset );
    gl_FragColor = vec4( f, f, f, 1. );
}
The basic idea for FBM here is that, compute the first noise layer based on the noise function with the vector2 uv sample point parameter, and then rotate and scale the sample point to continue computing the second noise layer with half of the previous amplitude. A 2x2 matrix is used for doing rotate operation, which can reduce axial alignment artifact. Finally it accumulate several noise layers with different detail.

The result:


This example can be further optimized or improved, for example, making use of all 4 RGBA bytes to store data, or increasing the contrast of the texture to make the generated values that are closed to 0 or 255 more possible. However, they are not hard to implement and won't be included in this tutorial.
 
Last edited:
Top