A Templated Implementation of Noise Texture

Introduction

The icon for this page was from my attempt to recreate a realtime shader for a varnished woodgrain effect. This approach requires the combination of multiple layers of Perlin noise, which is probably infeasible to generate on the fly within the fragment shader (although I’d love to be proven wrong). The generation of the wood grain shader will be saved for another post - in this one I’d like to discuss how I implemented a generic C++ template for the initialisation of the “general” dimensional noise texture on OpenGL.

The High Level Interface

The central idea behind this class was to provide the following generalisations:

  1. General Dimensional Textures - noise textures are typically either 2D or 3D (so you can “slice through” your material and expect a consistent pattern).
  2. Generic Generator Functions - the function that actually generates the noise must be completely generic.

For (1) above, this is realised by using a simple template parameter DIM:

template <size_t DIM>
class NoiseTexture

So my interface to creating a 3D texture is something like:

	MyNoiseTexture<3> m_noise;

For (2), there were several ways to approach this. I could, for example, have passed a generator function / functor class as a template parameter. This would have required me to create different classes for each type of generation. However there are a couple of reasons why I used inheritance instead of this approach (which could be called the strategy pattern):

  • A function cannot store state, which is need for many noise generators, for example boost or libnoise,
  • A functor can store state, but will have the same level of class management and complexity as using inheritance, but more importantly
  • I may want to override other parts of the class to support parallel noise generation.

The interface noisetexture.h provides is to inherit from the base class and override the protected function

    /// A generator function to make the process of building noise easy
    virtual inline GLfloat generator_func(const CoordinateArrayf &) = 0;	

This function simply accepts a coordinate in DIM space and returns a floating point value. Note that the NoiseTexture makes use of std::array as it should - this makes sure that this is managed memory rather than pointers. Two different arrays are defined:

    /// A static (i.e. not dynamic allocated) vector
    typedef std::array<size_t,DIM> CoordinateArray;
    typedef std::array<float, DIM> CoordinateArrayf;

One is for indices and the other for positions. This will be used in the recursive generation of the data block.

Generating the data

Ultimately the initialisation of the OpenGL texture consists of copying a lump of data from the CPU memory to the GPU memory using a call to glTexImage. The following section of code creates the memory, fills it with data and copies it to the GPU:

   // Allocate a slab of data for the stuffing
    GLfloat *data = (GLfloat*) malloc(sizeof(GLfloat) * pow(m_res,DIM) * 3);

    // Use the recursive function to generate the data recursively
    CoordinateArray coord;
    generate_recurse(DIM, coord, data);

    // Copy our data over to the GPU
    copyTextureDataToGPU(data);

    // Delete our data - it's been copied onto the GPU right?
    free(data);

Unpicking the memory allocation from left to right:

  • sizeof(GLfloat) - the size of an individual value (single precision)
  • pow(m_res,DIM) - the size of the block of data is squared/cubed as you would expect
  • 3 - we need RGB values to copy to the GPU

Generate_recurse is defined as follows:

    /// Recursively generate the data
    virtual void generate_recurse(const size_t &/*dim*/, const CoordinateArray &/*coord*/, GLfloat */*data*/);

It will recursively evaluate the generator function on each of the different positions in the block of memory and write it to the appropriate index:

template <size_t DIM>
void NoiseTexture<DIM>::generate_recurse(const size_t &d,
                                         const CoordinateArray& coord,
                                         GLfloat *data) {
    size_t dim_length = 1, data_pos = 0, i;
    CoordinateArrayf coordf, tmp;
    CoordinateArray ncoord;
    switch(d) {
    case 0:
        // At the end of the recursion chain we can evaluate the noise function
        for (i=0; i<DIM; ++i) {
            data_pos += coord[i] * dim_length;
            dim_length *= m_res;
            coordf[i] = m_inv_resf * float(coord[i]);
        }
        data_pos *= 3;

        // Fill up the data with data defined by the generator_func()
        for (i=0; i<3; ++i) {
            tmp = coordf; coordf[i] += 1.0f;
            data[data_pos + i] = generator_func(coordf);
        }
        break;
    default:
        // For the rest of the dimensions we recursively call all the remaining coordinates
        ncoord = coord;
        for (i=0; i<m_res; ++i) {
            ncoord[d-1] = i;
            generate_recurse(d-1, ncoord, data);
        }
    }
}

Note that I have arbitrarily added 1.0f for each of the R, G, B channels to give a bit of a richer data set to play with.

OpenGL integration

There are a number of standard functions which make this class usable in a GL context. For example, the function to find the texture:

template <size_t DIM>
void NoiseTexture<DIM>::bind() const {
    if (m_isInit) {
        glBindTexture(m_target, m_texID);
        CheckError("NoiseTexture<DIM>::bind() - glBindTexture()");
    }
}

Note the variable m_target, which is actually a constant, defined using this from within the class declaration:

    constexpr GLuint target() const {
        switch(DIM) {
        case 1:
            return GL_TEXTURE_1D;
        case 2:
            return GL_TEXTURE_2D;
        case 3:
            return GL_TEXTURE_3D;
        }
        return 0;
    }
    const GLuint m_target = target();

Take a moment to digest this: the function target is actually being executed at compile time, and this is in fact an example of template metaprogramming (albeit, a rather simple one). The keyword constexpr allows this function to be executed in setting a constant at compile time. The use of the switch statement (and using multiple return statements in a function) is only allowable in C++14 or better (a simple if-else structure would remove this dependency).

The function which copies the data to the GPU is pretty straightforward:

template <size_t DIM>
void NoiseTexture<DIM>::copyTextureDataToGPU(GLfloat *data) {
    // Transfer this data to our texture
    glGenTextures(1, &m_texID);
    glBindTexture(m_target, m_texID);

    // Repeat the texture for coordinates <0 or >1
    ...

    // Use blending when texels are bigger or smaller than pixels
    ...

    // Upload the texture data to the GPU
    switch(DIM) {
    case 1:
        glTexImage1D(m_target,      // Target
                     0,             // Level
                     GL_RGB,        // Internal Format
                     m_res,         // width
                     0,             // border
                     GL_RGB,        // format
                     GL_FLOAT,      // type
                     data);
        break;
    case 2:
	...
    case 3:
	...
    }
}

Note that template specialisms could have been used here for the different TexImage calls, but due to the duplication of the texture parameters I decided against it.

Deriving your own Noise Texture class

The simplest example of a derived noise texture class is one which just sets the values in the data block to 1. This is demonstrated in testnoisetexture.h:

template<size_t DIM>
class TestNoiseTexture : public NoiseTexture<DIM> {
public:
    /// Constructor
    explicit TestNoiseTexture(float /*lower*/ = 0.0f,
                              float /*upper*/ = 1.0f,
                              size_t /*resolution*/ = 64);

    /// Dtor
    ~TestNoiseTexture() {}

protected:
    /// Specialisation of this class to generate pure white noise
    inline GLfloat generator_func(const typename NoiseTexture<DIM>::CoordinateArrayf &);
};

template<size_t DIM>
TestNoiseTexture<DIM>::TestNoiseTexture(float _lower,
                                          float _upper,
                                          size_t _resolution)
    : NoiseTexture<DIM>(_lower,_upper,_resolution) {}


template<size_t DIM>
inline GLfloat TestNoiseTexture<DIM>::generator_func(const typename NoiseTexture<DIM>::CoordinateArrayf &coord) {
    return 1.0f;
}

As you see above, all you need to do here is override the virtual generator_func with your own favourite noise generator. See the whitenoisetexture.h example for something a little more interesting. I’ll be looking to write a post about how this method was used to create the wood shader soon.

Downloads

Download the source files for these here:

Richard Southern
Richard Southern
Lead Developer, Katana

Leader, Graphics Researcher, Software Engineer

Related