Using 3D texture in OpenGL/openFrameworks with programmable pipeline

Here we explain how to work with 3D textures using modern OpenGL, verson >= 3.2, using C++ code with openFrameworks.

For preparing this text we used several sources:
We will performs the folliwing steps to create and 3D texture for openFrameworks 0.10.1:
1. Enable programmable pipeline
2. Define the function for printing GL errors
3. Create some volume data
4. Create and upload 3D texture
5. Setting 3D texture to shader
6. Create shader files

1. Enable programmable pipeline

Be sure you are using programmable pipeline, so in main.cpp, main() use the following code:
int main() {
    ofGLWindowSettings settings;
    settings.setGLVersion(3, 2);    //GL Programmable Renderer
    settings.setSize(1024, 768);
    ofCreateWindow(settings);      //setup the GL context

    ofRunApp(new ofApp());
}

2. Define the function for printing GL errors

To write GL-related errors during creating 3D texture, please define in ofApp.cpp the following function:
  void errorCheck(string pass) {
      GLenum res = glGetError();
      if (res != GL_NO_ERROR) {
          cout << "GL error at " << pass << ": " << res << endl;
      }
  }

3. Create some volume data

We created RGBA volume of size 256x256x256. 
Note: size of 3D texture may be artitrary  (not cube, not power of 2), but several GPUs want size not less 256.
I checked it's working with the size 800x800x800 on GPU with memory 6 Gb.

Codefor creating 3D volume data, put it into for setup():
    const unsigned int D = 256;
    const unsigned int DX = D, DY = D, DZ = D;   
    const unsigned int Size = DX * DY * DZ * 4;    //RGBA
    cout << "Cube size: " << D << endl;
    cout << "Memory size, bytes: " << Size << endl;
   unsigned char *volumeData = new unsigned char[Size];

    for (unsigned int z = 0; z < DZ; z++) {
        for (unsigned int y = 0; y < DY; y++) {
            for (unsigned int x = 0; x < DX; x++) {
                unsigned int i = 4 * (x + DX * (y + DY * z));
                float r = ofPoint(float(x) - DX / 2, float(y) - DY / 2, float(z) - DZ / 2).length();
                float value = ofMap(r, 0, DX/2 * 1, 1, 0, true);    //smooth step

                volumeData[i] = float(x) / DX * 255; //value * 255; //float(z) / DX * 1;// +0.5;
                volumeData[i + 1] = value * 255; //float(z) / DX * 1;// +0.5;
                volumeData[i + 2] = 0.3 * 255;// value;// float(y) / DX * 1;;
                volumeData[i + 3] = (sin(x*0.05)*0.5+0.5)*255;
            }
        }
    }

4. Create and upload 3D texture

Define textureID as a ofApp class member or just global variable:
    GLuint textureID;

Add the following code of creating 3D texture to setup():
    glGenTextures(1, &textureID);
    errorCheck("glGenTextures(1, &textureID);");

    glBindTexture(GL_TEXTURE_3D, textureID);
    errorCheck("glBindTexture(GL_TEXTURE_3D, textureID);");
   
    if (!(glIsTexture(textureID) == GL_TRUE)) cout << "Texture Binding Failed." << endl;
   
    //Wrap modes
    //https://www.khronos.org/registry/OpenGL-Refpages/gl2.1/xhtml/glTexParameter.xml
    //GL_CLAMP,    GL_REPEAT, GL_MIRRORED_REPEAT (GL_CLAMP_TO_BORDER, GL_CLAMP_TO_EDGE)
    glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_REPEAT);

    //Interpolation
    //https://www.khronos.org/registry/OpenGL-Refpages/es2.0/xhtml/glTexParameter.xml
    //GL_LINEAR,GL_NEAREST
    glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

   //Upload texture data to GPU
   glTexImage3D(GL_TEXTURE_3D, 0, GL_RGBA, DX, DY, DZ, 0, GL_RGBA, GL_UNSIGNED_BYTE, volumeData);
    errorCheck("glTexImage3D");

    glBindTexture(GL_TEXTURE_3D, 0);
    errorCheck(" glBindTexture(GL_TEXTURE_3D, 0) ");

Detele volume data if it's not required anymore:
    delete[] volumeData;

5. Setting 3D texture to shader

Here we illustrate how to use 3D texture in shader by rendering it's slices.

Define shader object as ofApp class member of as a global variable:
    ofShader shader;
Load shader in setup()  (we will give shader's code later)
    shader.load("3dtexture"); 
 
Now, we will draw on the screen 3D texture's slices using the shader, so add the following code to draw():

    //Bind the shader
    shader.begin();

    //Set 3D texture to shader
    int textureLocation = 1; // Location an be 1,2,3,... - but unique for each used texture
    glActiveTexture(GL_TEXTURE0 + textureLocation);
    glBindTexture(GL_TEXTURE_3D, textureID);
    shader.setUniform1i("volume", textureLocation);    glActiveTexture(GL_TEXTURE0);
   
    //Also we pass the time in seconds to the shader to scan through 3D texture slices
    shader.setUniform1f("time", ofGetElapsedTimef());

    //Draw rectangle (passed through the shader
    ofDrawRectangle(0, 0, ofGetWidth(), ofGetHeight());

    //Unbind the shader
    shader.end();

6. Create shader files

   Vertex shader computes rectangle's output position gl_Position in normalized device coordinates [-1,1]x[-1,1]x[-1,1], and computes pos - 2D vector with uniform coordinates [0,1]x[0,1], which we will use for rendering slice of the 3D texture in fragment shader.
Put the following code to data/3dtexture.vert file:

    #version 330
    precision mediump float;
    uniform mat4 modelViewProjectionMatrix;
    in vec4 position;
    out vec2 pos;  //[0..1,0..1]

    void main(){
        gl_Position = modelViewProjectionMatrix * position;
        pos = gl_Position.xy / gl_Position.w;
        pos =  pos*0.5 + vec2(0.5,0.5);   
    }

Fragment shader computes FragColorby sampling it from 3D texture volume.
Put the following code to data/3dtexture.vert file:

    #version 330
    precision mediump float;   

    in vec2 pos;  //[0..1,0..1]

    uniform sampler3D volume;    //3D texture

    uniform float time;    //time in seconds

    out vec4 FragColor; //resulted pixel size

    void main( void ) {   
        //Important note: coordinates for 3D texture are uniform [0,1]x[0,1]x[0,1]
 
        //Get Z as a slice, changing over time
        float Z = sin(time)*0.5+0.5;

        //We multiply X and Y by 3 to repeat the texture
        //(when creating texture we set warp mode in CPU code to GL_REPEAT)
        FragColor = texture(volume,vec3(pos.x*3,pos.y*3,Z));
}

In result, you will see on the screen some animated picture, which shows scanning through Z-slices of the created 3D texture.

Comments

Popular posts from this blog

Computing ray origin and direction from Model View Projection matrices for raymarching

Forward and backward alpha blending for raymarching

Forward, Deferred and Raytracing rendering in openFrameworks and web