Today was cool… at least the first part of it was, before I spend 5 hours tracking down a bug in some of the game code.
I stumbled onto a pretty cool little article geared towards a very awesome technique called “parallax occlusion mapping”. This is essentially bump mapping with some extra samples taken at each pixel to provide a realistic depth (or parallax) effect to the surface. This provides the illusion of realistic surface detail all while shading a single polygon. The article was somewhat dated and targeted for Direct3D instead of OpenGL, but I was able to port it over to GLSL 150 for OpenGL 3.2.
Here’s the original article:
http://www.gamedev.net/page/resources/_/technical/graphics-programming-and-theory/a-closer-look-at-parallax-occlusion-mapping-r3262
And here’s my port of the shader code for anyone who is interested.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
in highp vec4 position;
in mediump vec3 normal;
in mediump vec2 texcoord0;
in mediump vec3 tangent;
out mediump vec2 va_texcoord;
out mediump vec3 va_eye;
out mediump vec3 va_normal2;
out mediump vec3 va_light;
uniform mat4 modelViewProjectionMatrix;
uniform mediump mat4 modelViewMatrix;
uniform mat4 modelMatrix;
uniform mat4 modelMatrixInverse;
uniform mediump vec2 textureScale;
uniform mediump vec2 bumpScale;
uniform mediump mat3 normalMatrix;
uniform float shadowBiasFactor;
uniform vec3 cameraPosition;
uniform bool doNormalize;
#define LIGHT 4
struct Light
{
mediump vec4 worldPosition;
};
#ifdef GL_ES
highp mat3 transpose(in highp mat3 inMatrix)
{
highp vec3 i0 = inMatrix[0];
highp vec3 i1 = inMatrix[1];
highp vec3 i2 = inMatrix[2];
//highp vec4 i3 = inMatrix[3];
highp mat3 outMatrix = mat3(
vec3(i0.x, i1.x, i2.x),
vec3(i0.y, i1.y, i2.y),
vec3(i0.z, i1.z, i2.z)
);
return outMatrix;
}
#endif
uniform Light lights[8];
void main()
{
vec3 P = (modelMatrix * position).xyz;
vec3 N = normal;
vec3 E = P - cameraPosition;
vec3 L = -lights[LIGHT].worldPosition.xyz - P;
//Compute transformed normal
vec3 eyeNormal = normalize(normalMatrix * normal);
//Pass transformed texcoord.
va_texcoord = texcoord0*textureScale;
vec4 nNormal = vec4(normalize(normal), 0.0);
vec4 nTangent = vec4(normalize(tangent), 0.0);
vec4 nBinormal = vec4(cross(nNormal.xyz, nTangent.xyz), 0.0);
mat3 tangentToWorldSpace;
tangentToWorldSpace[0] = (modelMatrix * nTangent).xyz;
tangentToWorldSpace[1] = (modelMatrix * nBinormal).xyz;
tangentToWorldSpace[2] = (modelMatrix * nNormal).xyz;
mat3 worldToTangentSpace = transpose(tangentToWorldSpace);
va_eye = E * worldToTangentSpace;
va_normal2 = N * worldToTangentSpace;
va_light = L * worldToTangentSpace;
//Pass GL-transformed to vertex down pipeline
gl_Position = modelViewProjectionMatrix * position;
} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
#ifdef GL_ES
#extension GL_OES_standard_derivatives : require
#extension GL_EXT_shader_texture_lod : require
#endif
precision mediump float;
in mediump vec2 va_texcoord;
in mediump vec4 ec_pos;
in mediump vec3 va_eye;
in mediump vec3 va_normal2;
in mediump vec3 va_light;
out vec4 fragColor;
#define LIGHT 4
//Prototypes
vec4 computeLight(in mediump vec3 normal, in mediump vec4 ecPosition, in lowp float alphaFade, out lowp vec4 otherSideColor, out lowp vec4 secondaryHighlight);
//Lights
struct Light
{
mediump vec4 worldPosition;
};
uniform vec3 cameraPosition;
uniform Light lights[8];
uniform vec4 lightModelProductSceneColor;
uniform lowp sampler2D texture0;
uniform sampler2D bumpTexture;
uniform sampler2D heightTexture;
//uniform bool lightingEnabled;
const float fHeightMapScale = 0.02;
const int nMaxSamples = 32;
const int nMinSamples = 8;
void main()
{
// Calculate the geometric surface normal vector, the vector from
// the viewer to the fragment, and the vector from the fragment
// to the light.
vec3 N = normalize(va_normal2);
vec3 E = normalize(va_eye);
vec3 L = normalize(va_light);
float fParallaxLimit = -length( va_eye.xy ) / va_eye.z;
fParallaxLimit *= -fHeightMapScale;
vec2 vOffsetDir = normalize(va_eye.xy);
vec2 vMaxOffset = vOffsetDir * fParallaxLimit;
int nNumSamples = int(mix(float(nMaxSamples), float(nMinSamples), dot(E, N)));
float fStepSize = 1.0 / float(nNumSamples);
vec2 dx = dFdx(va_texcoord);
vec2 dy = dFdy(va_texcoord);
float fCurrRayHeight = 1.0;
vec2 vCurrOffset = vec2(0.0);
vec2 vLastOffset = vec2(0.0);
float fLastSampledHeight = 1.0;
float fCurrSampledHeight = 1.0;
int nCurrSample = 0;
while(nCurrSample < nNumSamples)
{
fCurrSampledHeight = textureGrad(heightTexture, va_texcoord+vCurrOffset, dx, dy).r;
if(fCurrSampledHeight > fCurrRayHeight)
{
float delta1 = fCurrSampledHeight - fCurrRayHeight;
float delta2 = ( fCurrRayHeight + fStepSize ) - fLastSampledHeight;
float ratio = delta1/(delta1+delta2);
vCurrOffset = (ratio) * vLastOffset + (1.0-ratio) * vCurrOffset;
nCurrSample = nNumSamples + 1;
}
else
{
nCurrSample++;
fCurrRayHeight -= fStepSize;
vLastOffset = vCurrOffset;
vCurrOffset += fStepSize * vMaxOffset;
fLastSampledHeight = fCurrSampledHeight;
}
}
vec2 vFinalCoords = va_texcoord + vCurrOffset;
vec4 vFinalNormal = texture(bumpTexture, va_texcoord + vCurrOffset);
lowp vec4 vFinalColor = texture(texture0, vFinalCoords); //vec4(1.0);
vFinalNormal = vFinalNormal * 2.0 - 1.0;
vec3 vAmbient = vFinalColor.rgb * 0.1;
vec3 vDiffuse = vFinalColor.rgb * max( 0.0, dot( L, vFinalNormal.xyz ) ) * 0.5;
vFinalColor.rgb = vAmbient + vDiffuse;
fragColor = vFinalColor;
} |
Some of this code is “massaged” by Verto Studio’s code converter when ran on mobile. It’s definitely a pretty cool effect! It requires 3 texture maps to work currently, a standard diffuse (color texture) map, a normal map, and a displacement or height map. Lucky for me, there’s a sick little program called “crazy bump” for mac that can generate both normal maps and displacement maps from any standard diffuse texture map!
WebGL demo
For those who want to see the shader effect in action, I got a WebGL demo which runs on chrome and safari (firefox and IE don’t work).
It’s an expensive effect however, so I’m not sure yet if I can work it in for Driveby Gangster. Either way, it’ll definitely be a nice little addition to the shader arsenal. If I do actually put it into use, I hope to eventually add self-shadowing effects as well.