OpenGL ES 3.1 and OpenGL 4.0 Tessellation Demo




OpenGL ES 3.1 tessellation demo on the Tinker Board

For the release of GeeXLab 0.20.1, I coded a small tessellation demo for the Tinker Board (OpenGL ES 3.1). I also ported this demo to OpenGL 4.0 (Windows, Linux and macOS).

This demo is cool because the whole environment (the sky, the ground as well as the shadow on the ground) is in reality a simple quad rendered with a pixel shader. All parts of the environment are generated by the pixel shader. I found this pixel shader in the Mali OpenGL ES SDK. The rest of the demo is a simple mesh sphere rendered with a tessellation GPU program.

For desktops (Windows, Linux and macOS), you can find the in the gl-40/tessellation_demo/ folder of the full code sample pack. For Tinker Board users, the demo is shipped with GeeXLab for Tinker Board GLES and is available in the demos/tessellation/ folder (demo03.xml).

How to run the demo:

  • On Windows and macOS, start GeeXLab and drag/drop the demo into GeeXLab.
  • On Linux, edit the demo.sh file with the path to the demo. Then run the demo.sh in command line: sh ./demo.sh
  • On Tinker Board, same thing than on Linux: edit the demo.sh and run demo.sh with sh ./demo.sh

And for fun, here are some framerate:
– Tinker Board with Mali T760 GPU: 30 FPS
– Windows 10 + GeForce GTX 1070: 3300 FPS


OpenGL 4.0 tessellation demo on Windows with a GTX 1080

 
Here is complete GPU program for Tinker Board / OpenGL ES 3.1:

Vertex shader

#version 310 es
precision highp float;
in vec4 gxl3d_Position;
in vec4 gxl3d_Normal;
in vec4 gxl3d_TexCoord0;
out vec4 v_normal;
out vec4 v_uv;
void main()
{
  gl_Position = gxl3d_Position;
  v_normal = gxl3d_Normal;
  v_uv = gxl3d_TexCoord0;
}

 
Tessellation control shader

#version 310 es
#extension GL_EXT_tessellation_shader : require
precision highp float;
 
layout(vertices = 3) out;
 
uniform vec4 tess_params;
 
struct pn_patch
{
  float b210;
  float b120;
  float b021;
  float b012;
  float b102;
  float b201;
  float b111;
  float n110;
  float n011;
  float n101;
};
 
in vec4 v_normal[];
in vec4 v_uv[];
 
out vec4 tcs_normal[];
out vec4 tcs_uv[];
out pn_patch tcs_patch[];
 
float wij(int i, int j)
{
  return dot(gl_in[j].gl_Position.xyz - gl_in[i].gl_Position.xyz, v_normal[i].xyz);
}
 
float vij(int i, int j)
{
  vec3 Pj_minus_Pi = gl_in[j].gl_Position.xyz - gl_in[i].gl_Position.xyz;
  vec3 Ni_plus_Nj  = v_normal[i].xyz + v_normal[j].xyz;
  return 2.0*dot(Pj_minus_Pi, Ni_plus_Nj) / dot(Pj_minus_Pi, Pj_minus_Pi);
}
 
void main()
{
  // get data
  gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
 
  tcs_normal[gl_InvocationID]  = v_normal[gl_InvocationID];
  tcs_uv[gl_InvocationID]  = v_uv[gl_InvocationID];
 
  // set base 
  float P0 = gl_in[0].gl_Position[gl_InvocationID];
  float P1 = gl_in[1].gl_Position[gl_InvocationID];
  float P2 = gl_in[2].gl_Position[gl_InvocationID];
  float N0 = v_normal[0][gl_InvocationID];
  float N1 = v_normal[1][gl_InvocationID];
  float N2 = v_normal[2][gl_InvocationID];
 
  // compute control points
  tcs_patch[gl_InvocationID].b210 = (2.0*P0 + P1 - wij(0,1)*N0)/3.0;
  tcs_patch[gl_InvocationID].b120 = (2.0*P1 + P0 - wij(1,0)*N1)/3.0;
  tcs_patch[gl_InvocationID].b021 = (2.0*P1 + P2 - wij(1,2)*N1)/3.0;
  tcs_patch[gl_InvocationID].b012 = (2.0*P2 + P1 - wij(2,1)*N2)/3.0;
  tcs_patch[gl_InvocationID].b102 = (2.0*P2 + P0 - wij(2,0)*N2)/3.0;
  tcs_patch[gl_InvocationID].b201 = (2.0*P0 + P2 - wij(0,2)*N0)/3.0;
 
  float E = ( tcs_patch[gl_InvocationID].b210
			+ tcs_patch[gl_InvocationID].b120
			+ tcs_patch[gl_InvocationID].b021
			+ tcs_patch[gl_InvocationID].b012
			+ tcs_patch[gl_InvocationID].b102
			+ tcs_patch[gl_InvocationID].b201 ) / 6.0;
 
  float V = (P0 + P1 + P2) / 3.0;
 
  tcs_patch[gl_InvocationID].b111 = E + (E - V)*0.5;
  tcs_patch[gl_InvocationID].n110 = N0+N1-vij(0,1)*(P1-P0);
  tcs_patch[gl_InvocationID].n011 = N1+N2-vij(1,2)*(P2-P1);
  tcs_patch[gl_InvocationID].n101 = N2+N0-vij(2,0)*(P0-P2);
 
  float tessLevelOuter = tess_params.x;
  float tessLevelInner  = tess_params.y;
 
  gl_TessLevelOuter[gl_InvocationID] = tessLevelOuter;
  gl_TessLevelInner[0] = tessLevelInner;
}

 
Tessellation evaluation shader

#version 310 es
#extension GL_EXT_tessellation_shader : require
precision highp float;
 
layout(triangles, fractional_even_spacing, ccw) in;
 
struct pn_patch
{
  float b210;
  float b120;
  float b021;
  float b012;
  float b102;
  float b201;
  float b111;
  float n110;
  float n011;
  float n101;
};
 
in vec4 tcs_normal[gl_MaxPatchVertices];
in vec4 tcs_uv[gl_MaxPatchVertices];
in pn_patch tcs_patch[gl_MaxPatchVertices];
 
out vec4 tes_normal;
out vec4 tes_uv;
out vec4 v_eye_dir;
out vec4 v_light_dir;
 
uniform mat4 gxl3d_ProjectionMatrix;
uniform mat4 gxl3d_ViewMatrix;
uniform mat4 gxl3d_ModelMatrix;
uniform vec4 uv_tiling;
uniform vec4 light_position;
uniform vec4 tess_params;
 
uniform sampler2D tex_bump;
 
#define uvw gl_TessCoord
 
void main()
{
  vec3 uvwSquared = uvw * uvw;
  vec3 uvwCubed   = uvwSquared * uvw;
 
  // extract control points
  vec3 b210 = vec3(tcs_patch[0].b210, tcs_patch[1].b210, tcs_patch[2].b210);
  vec3 b120 = vec3(tcs_patch[0].b120, tcs_patch[1].b120, tcs_patch[2].b120);
  vec3 b021 = vec3(tcs_patch[0].b021, tcs_patch[1].b021, tcs_patch[2].b021);
  vec3 b012 = vec3(tcs_patch[0].b012, tcs_patch[1].b012, tcs_patch[2].b012);
  vec3 b102 = vec3(tcs_patch[0].b102, tcs_patch[1].b102, tcs_patch[2].b102);
  vec3 b201 = vec3(tcs_patch[0].b201, tcs_patch[1].b201, tcs_patch[2].b201);
  vec3 b111 = vec3(tcs_patch[0].b111, tcs_patch[1].b111, tcs_patch[2].b111);
 
  // extract control normals
  vec3 n110 = normalize(vec3(tcs_patch[0].n110, tcs_patch[1].n110, tcs_patch[2].n110));
  vec3 n011 = normalize(vec3(tcs_patch[0].n011, tcs_patch[1].n011, tcs_patch[2].n011));
  vec3 n101 = normalize(vec3(tcs_patch[0].n101, tcs_patch[1].n101, tcs_patch[2].n101));
 
  // compute texcoords
  vec4 uv  = (gl_TessCoord[2]*tcs_uv[0] + gl_TessCoord[0]*tcs_uv[1] + gl_TessCoord[1]*tcs_uv[2])  * uv_tiling;
  tes_uv = uv;
 
  float bump = texture(tex_bump, uv.xy).x;
 
 
  // normal
  // Barycentric normal
  vec3 barNormal = gl_TessCoord[2]*tcs_normal[0].xyz + gl_TessCoord[0]*tcs_normal[1].xyz + gl_TessCoord[1]*tcs_normal[2].xyz;
  vec3 pnNormal  = tcs_normal[0].xyz*uvwSquared[2] + tcs_normal[1].xyz*uvwSquared[0] + tcs_normal[2].xyz*uvwSquared[1]
                            + n110*uvw[2]*uvw[0] + n011*uvw[0]*uvw[1]+ n101*uvw[2]*uvw[1];
 
  float tess_alpha = tess_params.z;
 
  vec3 Normal = normalize(tess_alpha*pnNormal + (1.0-tess_alpha) * barNormal);
 
 
  // compute interpolated pos
  vec3 barPos = gl_TessCoord[2]*gl_in[0].gl_Position.xyz + gl_TessCoord[0]*gl_in[1].gl_Position.xyz + gl_TessCoord[1]*gl_in[2].gl_Position.xyz;
 
  // save some computations
  uvwSquared *= 3.0;
 
  // compute PN position
  vec3 pnPos  = gl_in[0].gl_Position.xyz*uvwCubed[2]
                    + gl_in[1].gl_Position.xyz*uvwCubed[0]
                    + gl_in[2].gl_Position.xyz*uvwCubed[1]
                    + b210*uvwSquared[2]*uvw[0]
                    + b120*uvwSquared[0]*uvw[2]
                    + b201*uvwSquared[2]*uvw[1]
                    + b021*uvwSquared[0]*uvw[1]
                    + b102*uvwSquared[1]*uvw[2]
                    + b012*uvwSquared[1]*uvw[0]
                    + b111*6.0*uvw[0]*uvw[1]*uvw[2];
 
 
  // final position and normal
  vec3 finalPos = tess_alpha*pnPos + (1.0-tess_alpha)*barPos;
 
  float bump_scale = tess_params.w;
  finalPos += bump * Normal * bump_scale;
 
  mat4 modelview = gxl3d_ViewMatrix * gxl3d_ModelMatrix;
 
  tes_normal = modelview * vec4(Normal, 0.0);
 
  vec4 view_pos = modelview * vec4(finalPos,1.0);
  v_eye_dir = -view_pos;
  vec4 lp = gxl3d_ViewMatrix * light_position;
  v_light_dir = lp - view_pos;  
 
  gl_Position = gxl3d_ProjectionMatrix * view_pos;
}

 
Pixel shader

precision highp float;
 
in vec4 tes_normal;
in vec4 tes_uv;
in vec4 v_eye_dir;
in vec4 v_light_dir;
 
uniform sampler2D tex_diffuse;
uniform sampler2D tex_normal;
 
out vec4 FragColor;
 
mat3 cotangent_frame(vec3 N, vec3 p, vec2 uv)
{
    // get edge vectors of the pixel triangle
    vec3 dp1 = dFdx( p );
    vec3 dp2 = dFdy( p );
    vec2 duv1 = dFdx( uv );
    vec2 duv2 = dFdy( uv );
 
    // solve the linear system
    vec3 dp2perp = cross( dp2, N );
    vec3 dp1perp = cross( N, dp1 );
    vec3 T = dp2perp * duv1.x + dp1perp * duv2.x;
    vec3 B = dp2perp * duv1.y + dp1perp * duv2.y;
 
    // construct a scale-invariant frame 
    float invmax = inversesqrt( max( dot(T,T), dot(B,B) ) );
    return mat3( T * invmax, B * invmax, N );
}
 
vec3 perturb_normal(vec3 N, vec3 V, vec2 texcoord)
{
    // assume N, the interpolated vertex normal and 
    // V, the view vector (vertex to eye)
   vec3 map = texture(tex_normal, texcoord ).xyz;
   map = map * 255./127. - 128./127.;
   mat3 TBN = cotangent_frame(N, -V, texcoord);
   return normalize(TBN * map);
}
 
void main()
{
  vec2 uv = tes_uv.xy;
 
  vec3 c0 = texture(tex_diffuse, uv).rgb;
 
  vec3 N = normalize(tes_normal.xyz);
  vec3 E = normalize(v_eye_dir.xyz);
  vec3 L = normalize(v_light_dir.xyz);  
  //L.y *= -1.0; 
  vec3 PN = perturb_normal(N, E, uv);
 
 
  vec3 fc = vec3(0.0, 0.0, 0.0);
 
  vec3 la0 = vec3(0.2, 0.2, 0.2);
  vec3 ld0 = vec3(0.9, 0.85, 0.8);
  vec3 ls0 = vec3(0.6, 0.6, 0.6);
  vec3 mA = vec3(0.9, 0.9, 0.9);
  vec3 mD = vec3(0.9, 0.9, 0.9);
  vec3 mS = vec3(0.01, 0.01, 0.01);
  float mSpecExp = 16.0;  
 
  fc += (la0 * mA) * c0.rgb;
  float lambert = dot(PN, L);
  if (lambert > 0.0)
  {
    fc += ld0 * mD * lambert * c0;
 
    vec3 R = reflect(-L, PN);
    float specular = pow(abs(dot(R, E)), mSpecExp);
    fc += ls0 * mS * specular;	
  } 
 
  FragColor = vec4(fc, 1.0);
}