Blog of roxlu, co-founder of Apollo Media. Contact info[shift+2]apollomedia.nl.

Real-time oil painting with openGL

I'm testing different approaches towards a real-time 1080p oil painting simulation. To create an oil painting, I capture the scene into a texture (using FBO RTT) and use a shader to calculate the gradients (from grayscale values). I use these gradients to align quads in the same direction so they align nicely with the color grades in the source image. I write all the gradients into a GL_R32F float texture, for each pixel. (this is all work in progress)

Gradient vectors on GPU

Then in a next step, I render N-quads where I omit vertex attributes and a VBO completely by using the gl_VertexID keyword to emit the correct vertex for the quad/triangle strip (see below for source). This is a lot faster then generating vertices in a geometry shader!

Aligned quads. Input from slit-scan

During this second step I rotate the quad based on the angle/gradient I calculated during the first pass (from the u_angle_tex uniform). I also get the correct from the scene texture.

With my radeon 4850 I'm not yet getting a satisfying framerate: 10-15fps with 100.000 quads. Though with my GTX 670 I'm getting 60fps with 3.000.000 quads (with texturing!)

Some more screenshots

#ifndef ROXLU_OILS_H
#define ROXLU_OILS_H
 
#include <glr/GL.h>
#include <roxlu/math/Random.h>
#include <roxlu/math/Vec2.h>
#include <roxlu/opengl/GL.h>
#include <roxlu/opengl/Error.h>
 
using namespace roxlu;
using namespace gl;
 
static const char* OSA_VS = ""
  "#version 150\n"
  "in vec4 a_pos;"
 
  "void main() {"
  "  gl_Position = a_pos; "
  "}";
 
static const char* OSA_FS = ""
  "#version 150\n"
  "uniform sampler2D u_scene;"
  "out float angle;"
 
  "float bw(vec3 col) {"
  "  return (col.r * 0.2126 + col.g * 0.7152 + col.b * 0.0722); "
  "}"
 
  "void main() {"
  "  ivec2 tc = ivec2(gl_FragCoord);"
  "  vec3 col_left = texelFetchOffset(u_scene, tc, 0, ivec2(-1, 0)).rgb;"
  "  vec3 col_right = texelFetchOffset(u_scene, tc, 0, ivec2(1, 0)).rgb;"
  "  vec3 col_top = texelFetchOffset(u_scene, tc, 0, ivec2(0, -1)).rgb;"
  "  vec3 col_bottom = texelFetchOffset(u_scene, tc, 0, ivec2(0, 1)).rgb;"
  "  float left = bw(col_left); "
  "  float right = bw(col_right); "
  "  float top = bw(col_top); "
  "  float bottom = bw(col_bottom); "
  "  float dx = (right - left) * 0.5;"
  "  float dy = (bottom - top) * 0.5;"
  "  angle = atan(dy, dx); "
  "}";
 
 
static const char* OS_VS = ""
  "#version 150\n"
  "uniform mat4 u_pm;"
  "uniform samplerBuffer u_pos_buffer;"
  "uniform sampler2D u_angle_tex;"
  "uniform sampler2D u_scene_tex;"
 
  "const float w = 3.1;"
  "const float h = 12.0;"
  "out vec3 v_col;"
 
  "const vec2 data[4] = vec2[] ("
  "     vec2(-w, h), "
  "     vec2(-w, -h), "
  "     vec2( w,  h), "
  "     vec2( w, -h) "
  ");"
 
  "void main() {"
 
  "  vec2 pos = texelFetch(u_pos_buffer, gl_InstanceID).rg; "
  "  float angle = texelFetch(u_angle_tex, ivec2(720 - pos.x, 1280 - pos.y), 0).r; "
  "  float ca = cos(angle);"
  "  float sa = sin(angle);"
 
  "  mat4 rot_z; "
  "  rot_z[0].x = ca; "
  "  rot_z[0].y = -sa; "
  "  rot_z[0].z = 0.0; "
  "  rot_z[0].w = 0.0; "
 
  "  rot_z[1].x = sa; "
  "  rot_z[1].y = ca; "
  "  rot_z[1].z = 0.0; "
  "  rot_z[1].w = 0.0; "
 
  "  rot_z[2].x = 0.0; "
  "  rot_z[2].y = 0.0; "
  "  rot_z[2].z = 1.0; "
  "  rot_z[2].w = 0.0; "
 
  "  rot_z[3].x = 0.0; "
  "  rot_z[3].y = 0.0; "
  "  rot_z[3].z = 0.0; "
  "  rot_z[3].w = 1.0; "
 
  "  vec4 rot_vert = vec4(data[gl_VertexID], 0.0, 0.0) * rot_z;"
  "  vec4 vpos = rot_vert + vec4(pos, 0.0, 1.0);"
  "  gl_Position = u_pm * ( vpos ); "
  "  v_col = texelFetch(u_scene_tex, ivec2(720 - pos.x, 1280 - pos.y), 0).rgb; "
  "}"
  "";
 
static const char* OS_FS = ""
  "#version 150\n"
  "out vec4 fragcolor;"
  "in vec3 v_col;"
 
  "void main() {"
  "  fragcolor = vec4(v_col, 1.0); "
  "}"
  "";
 
class Oils {
 public:
  Oils();
  ~Oils();
  bool setup(int winW, int winH);
  void draw();
  void generateBrushVertices();
  void blitScene();
  void findAngles();
 
 public:
  int win_w;
  int win_h;
 
  /* angle */
  GLuint fs_vao;
  GLuint fs_vbo;
  GLuint fs_frag;
  GLuint fs_vert;
  GLuint fs_prog;
 
  GLuint fbo;
  GLuint depth;
  GLuint scene_tex;
  GLuint angle_tex;
 
  /* brush */
  GLuint br_prog;
  GLuint br_vert;
  GLuint br_frag;
  GLuint br_vao;
 
  int num_particles;
  GLuint tbo_position;
  GLuint tbo_tex;
  std::vector<GLfloat> positions;
};
 
#endif
#include <assert.h>
#include <roxlu/core/Log.h>
#include "Oils.h"
 
#define USE_ANGLES
 
Oils::Oils() 
  :win_w(0)
  ,win_h(0)
  ,br_prog(0)
  ,br_vert(0)
  ,br_frag(0)
  ,br_vao(0)
  ,num_particles(100000)
  ,tbo_position(0)
  ,tbo_tex(0)
  ,fs_vao(0)
  ,fs_vbo(0)
  ,fs_frag(0)
  ,fs_vert(0)
  ,fs_prog(0)
  ,fbo(0)
  ,depth(0)
  ,scene_tex(0)
  ,angle_tex(0)
{
}
 
Oils::~Oils() {
  RX_ERROR("Cleanup vao, prog, vert etc..");
}
 
bool Oils::setup(int winW, int winH) {
  glr_init();
 
  assert(winW);
  assert(winH);
  assert(br_prog == 0);
 
  win_w = winW;
  win_h = winH;
 
  br_vert = glCreateShader(GL_VERTEX_SHADER); glShaderSource(br_vert, 1, &OS_VS,NULL); glCompileShader(br_vert); eglGetShaderInfoLog(br_vert);
  br_frag = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(br_frag, 1, &OS_FS,NULL); glCompileShader(br_frag); eglGetShaderInfoLog(br_frag);
  br_prog = glCreateProgram(); glAttachShader(br_prog,br_vert); glAttachShader(br_prog,br_frag); glLinkProgram(br_prog); eglGetShaderLinkLog(br_prog);
  glUseProgram(br_prog);
 
  float pm[16] = { 0 };
  rx_ortho_top_left(win_w, win_h, -0.1f, 1.0f, pm);
 
  GLint u_pm = glGetUniformLocation(br_prog, "u_pm");
  if(u_pm < 0) {
    RX_ERROR("Cannot find u_pm");
    return false;
  }
  glUniformMatrix4fv(u_pm, 1, GL_FALSE, pm);
 
  // Pos sampler
  GLint u_pos_buffer = glGetUniformLocation(br_prog, "u_pos_buffer");
  if(u_pos_buffer < 0) {
    RX_ERROR("Cannot find u_pos_buffer");
    return false;
  }
  glUniform1i(u_pos_buffer, 0);
 
  // Angle tex
  GLint u_angle_tex = glGetUniformLocation(br_prog, "u_angle_tex");
  if(u_angle_tex < 0) {
    RX_ERROR("Canot find u_angle_tex");
    return false;
  }
  glUniform1i(u_angle_tex, 1);
 
  // Scene tex
  GLint u_scene_tex = glGetUniformLocation(br_prog, "u_scene_tex");
  if(u_scene_tex < 0) {
    RX_ERROR("Cannot find u_scene_tex");
    return false;
  }
  glUniform1i(u_scene_tex, 2);
 
  glGenVertexArrays(1, &br_vao);
 
 
  // TBO
  glGenTextures(1, &tbo_tex);
  glBindTexture(GL_TEXTURE_BUFFER, tbo_tex);
 
  glGenBuffers(1, &tbo_position);
  glBindBuffer(GL_TEXTURE_BUFFER, tbo_position);
  glBufferData(GL_TEXTURE_BUFFER, num_particles * sizeof(GLfloat) * 2, NULL, GL_DYNAMIC_DRAW);
 
  glTexBuffer(GL_TEXTURE_BUFFER, GL_RG32F, tbo_position);
  generateBrushVertices();
 
 
  // ANGLES
  // --------------------------------------------------------------
 
  // FBO
  glGenFramebuffers(1, &fbo);
  glBindFramebuffer(GL_FRAMEBUFFER, fbo);
 
  glGenRenderbuffers(1, &depth);
  glBindRenderbuffer(GL_RENDERBUFFER, depth);
  glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT32, win_w, win_h);
  glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depth);
 
  glGenTextures(1, &scene_tex);
  glBindTexture(GL_TEXTURE_2D, scene_tex);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, win_w, win_h, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, scene_tex, 0);
 
  glGenTextures(1, &angle_tex);
  glBindTexture(GL_TEXTURE_2D, angle_tex);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_R32F, win_w, win_h, 0, GL_RED, GL_FLOAT, NULL);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, angle_tex, 0);
 
  RX_VERBOSE("OILS SCENE TEX: %d", scene_tex);
  RX_VERBOSE("OILS ANGLE TEX: %d", angle_tex);
 
  GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
  if(status != GL_FRAMEBUFFER_COMPLETE) {
    RX_ERROR("Framebuffer for Oil is not complete.");
    return false;
  }
 
  // FULLSCREEN BUFFER
  GLfloat buf[] = {
    -1.0, -1.0, 
    1.0, -1.0, 
    1.0, 1.0, 
    -1.0, -1.0,
    1.0, 1.0,
    -1.0, 1.0 
  };
 
  glGenVertexArrays(1, &fs_vao);
  glBindVertexArray(fs_vao);
 
  glGenBuffers(1, &fs_vbo);
  glBindBuffer(GL_ARRAY_BUFFER, fs_vbo);
  glBufferData(GL_ARRAY_BUFFER, sizeof(buf), buf, GL_STATIC_DRAW);
 
  glEnableVertexAttribArray(0); // pos
  glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 2, (GLvoid*)0);
 
  glBindVertexArray(0);
 
  // FIND ANGLES PROG
  fs_vert = glCreateShader(GL_VERTEX_SHADER);
  glShaderSource(fs_vert, 1, &OSA_VS, NULL);
  glCompileShader(fs_vert); eglGetShaderInfoLog(fs_vert);
 
  fs_frag = glCreateShader(GL_FRAGMENT_SHADER);
  glShaderSource(fs_frag, 1, &OSA_FS, NULL);
  glCompileShader(fs_frag); eglGetShaderInfoLog(fs_frag);
 
  fs_prog = glCreateProgram();
  glAttachShader(fs_prog, fs_vert);
  glAttachShader(fs_prog, fs_frag);
  glBindAttribLocation(fs_prog, 0, "a_pos");
  glLinkProgram(fs_prog); eglGetShaderLinkLog(fs_prog);
  glUseProgram(fs_prog);
 
  GLint u_scene = glGetUniformLocation(fs_prog, "u_scene");
  if(u_scene < 0) {
    RX_ERROR("Cannot find u_scene");
    return false;
  }
  glUniform1i(u_scene, 0);
 
  return true;
}
 
void Oils::generateBrushVertices() {
  assert(win_w);
  assert(win_h);
 
 
  float max_w = win_w;
  float max_h = win_h;
  for(int i = 0; i < num_particles; ++i) {
    float x = rx_random(0.0f, max_w);
    float y = rx_random(0.0f, max_h);
    positions.push_back(x);
    positions.push_back(y);
   }
 
  glBindBuffer(GL_TEXTURE_BUFFER, tbo_position);
  glBufferSubData(GL_TEXTURE_BUFFER, 0, positions.size() * sizeof(GLfloat), &positions[0]);
 
}
void Oils::blitScene() {
  GLenum draw_bufs[] = { GL_COLOR_ATTACHMENT0 };
  glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
  glDrawBuffers(1, draw_bufs);
  glViewport(0,0,win_w,win_h);
  glBlitFramebuffer(0, 0, win_w, win_h, 0, 0, win_w, win_h, GL_COLOR_BUFFER_BIT, GL_LINEAR);
 
  glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
}
 
void Oils::findAngles() {
  glViewport(0,0,win_w, win_h);
  GLenum draw_bufs[] = { GL_COLOR_ATTACHMENT1 };
  glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
  glDrawBuffers(1, draw_bufs);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 
  glActiveTexture(GL_TEXTURE0);
  glBindTexture(GL_TEXTURE_2D, scene_tex);
 
  glBindVertexArray(fs_vao);
  glUseProgram(fs_prog);
  glDrawArrays(GL_TRIANGLES, 0, 6);
 
  glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
  glDrawBuffer(GL_BACK_LEFT);
}
 
void Oils::draw() {
 
  glActiveTexture(GL_TEXTURE0);
  glBindTexture(GL_TEXTURE_BUFFER, tbo_tex);
  glTexBuffer(GL_TEXTURE_BUFFER, GL_RG32F, tbo_position);
 
  glActiveTexture(GL_TEXTURE1);
  glBindTexture(GL_TEXTURE_2D, angle_tex);
 
  glActiveTexture(GL_TEXTURE2);
  glBindTexture(GL_TEXTURE_2D, scene_tex);
 
  glUseProgram(br_prog);
  glBindVertexArray(br_vao);
 
  glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, num_particles);
 
}