Tuesday 28 October 2014

Ye Olde Leadwerks

This is an irregular place to write up interesting code snippets for what is still my preferred edition of Leadwerks. No longer supported (or even available to purchase) it's still a nice capable deferred rendering engine which was ahead of it's time when originally released.

There's a small number of people, myself included  who still enjoy tinkering with this engine. Version 3 now available on Steam but I feel the earlier version has a charm that's foolishly worth keeping alive.

Leadwerks 2 had wrappers for several languages, C++, two C# libraries, LUA and the cross-platform BlitzMax language used to create Leadwerks 2. This is all the background I'm going to write since anyone interested will already know it. So that's the context, little used OpenGL 3D engine, obscure languages, rapid prototyping and engine hax galore.

Let the fun and games commence with an overview of areas we're going to be playing around with.

Shader.pak


Many of the cool effects and rendering are done using GPU shaders. And Leadwerks 2 is a great way to play and learn how to edit and create GLSL shaders. When you successfully compile and execute any LE2 program, the engine expects to find all the shaders it needs in a zip file called "Shader.pak"

Before we can start tweaking our shaders we need to extract this into a folder. Extracting all the shaders into "shaders" will have a nice directory structure that is self-explanatory.


The "Mesh" folder is the most interesting to us as this contains the core fragment and vertex shader programs "mesh.frag" and "mesh.vert"

We can insert new modes and effects in this code. It can be super-ugly but there are some cache benefits to coding things into one super-class. General cache rules often apply; keep shader changes to a minimum, state changes, batching.

You'll find it hard to pick out mesh.vert and mesh.frag as there's a large number of files using a hierarchical naming structure separated by underscores.



If we open "mesh_diffuse_bumpmap_specular.frag" it will contain the necessary #define's and an include directive.

#define LW_DIFFUSE texture0
#define LW_BUMPMAP texture1
#define LW_SPECULAR

Include "mesh.frag"


The above defines DIFFUSE, BUMPMAP and SPECULAR so when MESH.FRAG is executed, any conditional statements for these are executed for either the vertex or fragment (pixel) shader.

It also makes it easy to extend and add new effect combinations (if fiddly, see again the above directory screen-shot).

Let it Snow


What we're going to do is add snow effects to LE2 shaders as they don't come as standard and rather pretty. It's worth noting that a lot of this is made possible by work done by Leadwerks community members and shader-gods who spent many hours converting stuff to work in Leadwerks. Sometimes standard GLSL shaders you can find on the internet will work without much hacking but you do need to know a little about how Leadwerks organises it's uniforms and input/output channels.


We're going to add a new header file "mesh_diffuse_bumpmap_snow.frag" containing the following definiations:

#define LW_DIFFUSE texture0
#define LW_BUMPMAP texture1
#define SNOW 1

Include "abstract::mesh.frag"


All we're doing here is adding the SNOW = 1 definition. To to the magic we're going to need to splice into MESH.FRAG.

#ifdef SNOW
uniform float SnowMultipler=0.0;
#endif
...

The above defines a multiplier uniform we can pass an external value into to adjust the amount of "snow". This is used in the following segment.

...
...
normal.z /= bumpscale;
normal = T * normal.x + B * normal.y + N * normal.z;
normal = normalize(normal);

#ifdef SNOW
vec3 WN = normal * gl_NormalMatrix;
float slope=WN.y*SnowMultipler;
diffuse = diffuse*clamp((1-slope),0,1) + clamp(slope,0,1)*vec4(1,1,1,1);
#endif


What this does is compute the normal on the model as a value of "slope" in the "y" direction (from above, in Leadwerks the y axis is vertical). This is multiplied by the amount of snow we want (0.0 = no snow).

The "diffuse" value is passed through to the end of the shader and this is the diffuse colour value that is written out.

The effect is that any model using this snow material will have all triangles facing upwards blended with white. See screen-shot below.



It's a credible effect especially when combined with snowy terrain textures and particles. And controllable in code simply by adjusting the "SnowMultiplier" uniform.

In Leadwerks, we need the material reference and shader. Assuming we have the surface material reference, we can set the snow value in code like this:

SetShaderFloat(surf.material.reference.shader, "SnowMultipler", _fSnowValue) ;


The full mesh.frag code is presented below. It includes the standard Leadwerks 2 declarations as well as some additional custom ones which we will look at later.


#extension GL_ARB_draw_buffers : enable

uniform vec3 cameraposition;
uniform vec2 buffersize;
uniform vec2 camerarange;

include "abstract::greyscale.txt"
include "abstract::DepthToZPosition.frag"

#ifdef LW_DETAIL
 uniform sampler2D LW_DETAIL;
#endif

#ifdef LW_DIFFUSE
 uniform sampler2D LW_DIFFUSE;
#endif

#ifdef LW_DIFFUSE2
 uniform sampler2D LW_DIFFUSE2;
#endif

#ifdef LW_DECAL
 uniform sampler2D LW_DECAL;
 varying float decaloffset;
#endif

#ifdef LW_SPECULARMAP
 uniform sampler2D LW_SPECULARMAP;
#endif

#ifdef LW_BUMPMAP
 uniform sampler2D LW_BUMPMAP;
#endif

#ifdef LW_BUMPMAP2
 uniform sampler2D LW_BUMPMAP2;
#endif

#ifdef LW_BLOOM
 uniform sampler2D LW_BLOOM;
#endif

#ifdef LW_CUBEMAP
 uniform samplerCube LW_CUBEMAP;
 varying vec3 cubemapdir;
 varying vec3 cubemapnormal;
#endif

#ifdef LW_CUBEMAP_MIX
 uniform samplerCube LW_CUBEMAP_MIX;
 varying vec3 cubemapdir;
 varying vec3 cubemapnormal;
#endif

#ifdef LW_GIMAP
 uniform sampler2D LW_GIMAP;
#endif

#ifdef LW_PARALLAXMAP
 uniform sampler2D LW_PARALLAXMAP;
 varying vec3 eyevec;
#endif

#ifdef LE_REFRACTION
 uniform sampler2D LE_REFRACTION;
 uniform sampler2D LE_DEPTHBUFFER;
 uniform float refractionstrength = 0.01;
#endif

#ifdef LW_POMMAP
 vec3 vLightTS=vec3(0.577,0.577,0.577);
 varying vec3 eyevec;
 float depthP = 0.01;
 float nMinSamples = 20;
 float nMaxSamples = 50;
#endif

#ifdef LW_MESHLAYER
 varying float vegetationfade;
#endif

#ifdef LW_ALPHABLEND
 uniform sampler2D LW_ALPHABLEND_INCOMINGCOLOR;
 uniform sampler2D LW_ALPHABLEND_INCOMINGNORMAL;
#endif

#ifdef SNOWMAP
 uniform sampler2D texture8;
#endif
#ifdef SNOWMAPBUMP
 uniform sampler2D texture9;
#endif
#ifdef SNOW
 uniform float SnowMultipler=0.0;
#endif

varying vec4 projectionspace;
varying vec3 vertexposition;
varying vec3 T,B,N;
varying vec2 texcoord0;
varying vec2 texcoord1;
varying vec4 modelvertex;
varying vec4 fragcolor;

float fOcclusionShadow = 1.0;

uniform sampler2D texture14;
uniform float terrainsize;
uniform vec3 terrainscale;
uniform float bumpscale;
uniform float specular;
uniform float gloss;

//Terrain color map

uniform sampler2D texture12;



void main(void) {

 vec4 diffuse = fragcolor;
 vec3 normal;
 float shininess = 0.0;
 vec2 texcoord=texcoord0;// only use this because of parallax mapping
 float selfillumination = 0.0;

 #ifdef LW_DECAL
  vec2 texcoorddecal = texcoord;
  texcoorddecal.x += decaloffset;
  vec4 decal = texture2D(LW_DECAL,texcoorddecal);
  diffuse = vec4(1);
 #endif 
 
 vec2 terraincoord;
 float terrainresolution;

 #ifdef LW_VERTEXGI
  diffuse = vec4(1);
 #endif

 #ifdef LW_PARALLAXMAP
  texcoord += (diffuse.w * 0.04 - 0.036) * normalize(eyevec).xy;
 #endif

 #ifdef LW_POMMAP
  // for POM, the heightmap is in the alpha of the diffuse so save ur diffuse with DXT5 I chose this because the alpha of DXT5 is higher precision
  Include "POM.frag"
 #endif


 #ifdef LW_DIFFUSE2
  diffuse = vec4(1);
  vec4 diffuse2 = texture2D(LW_DIFFUSE2,texcoord);
 #endif

 #ifdef LW_DIFFUSE
  diffuse *= texture2D(LW_DIFFUSE,texcoord);//*fOcclusionShadow;
 #endif

 #ifdef LW_ALPHATEST
  if (diffuse.w<0.5) {
   discard;
  }
 #endif


 normal = N;

 #ifdef LW_BUMPMAP

  #ifdef LW_TERRAINNORMALS
   //Use terrain normals
   terraincoord=vec2(vertexposition.x,-vertexposition.z) / terrainsize + 0.5;
   terrainresolution = terrainsize / terrainscale.x;
   terraincoord += 0.5 / terrainresolution;
   vec3 worldNormal = ((texture2D(texture14,terraincoord).xyz - 0.5) * 2.0).xyz;
   normal = normalize(gl_NormalMatrix*worldNormal);
  #else
   #ifdef SNOWMAPBUMP
   vec3 WNB = normal * gl_NormalMatrix;
   float bslope=WNB.y*SnowMultipler;
   vec4 bumpcolor = clamp((1-bslope),0,1)*texture2D(LW_BUMPMAP,texcoord)+clamp((bslope),0,1)*texture2D(texture9,texcoord);
   #else
   vec4 bumpcolor = texture2D(LW_BUMPMAP,texcoord);
   #endif
   normal = bumpcolor.xyz * 2.0 - 1.0;
  #endif

  #ifdef LW_DETAIL
   normal += texture2D(LW_DETAIL,texcoord * 4.0).xyz * 2.0 - 1.0;
  #endif

  normal.z /= bumpscale;
  normal = T * normal.x + B * normal.y + N * normal.z;
  normal = normalize(normal);

  #ifdef SNOW
  vec3 WN = normal * gl_NormalMatrix;
  float slope=WN.y*SnowMultipler;
   #ifdef SNOWMAP
   diffuse = diffuse*clamp((1-slope),0,1) + clamp(slope,0,1)*texture2D(texture8,texcoord);
   #else
   diffuse = diffuse*clamp((1-slope),0,1) + clamp(slope,0,1)*vec4(1,1,1,1);
   #endif
  #endif

  #ifdef LW_SPECULAR
   shininess = bumpcolor.a*specular;//*fOcclusionShadow
  #endif
  #ifdef LW_SPECULARMAP
   shininess = texture2D(LW_SPECULARMAP,texcoord).x*specular;//*fOcclusionShadow
  #endif

 #else
  #ifdef SNOWMAPBUMP
   normal=texture2D(texture9,texcoord).xyz * 2 - 1.0;
   normal.z /= bumpscale;
   normal = T * normal.x + B * normal.y + N * normal.z;
   normal = normalize(normal);
  #else
   normal=normalize(normal);
  #endif
  #ifdef SNOW
  vec3 WN = normal * gl_NormalMatrix;
  float slope=WN.y*SnowMultipler;
   #ifdef SNOWMAP
   diffuse = diffuse*clamp((1-slope),0,1) + clamp(slope,0,1)*texture2D(texture8,texcoord);
   #else
   diffuse = diffuse*clamp((1-slope),0,1) + clamp(slope,0,1)*vec4(1,1,1,1);
   #endif
  #endif
 #endif

 #ifdef LW_BUMPMAP2
  vec3 normal2;
  float shininess2;

  vec4 bumpcolor2 = texture2D(LW_BUMPMAP2,texcoord);
  normal2 = bumpcolor2.xyz * 2.0 - 1.0;

  normal2.z /= bumpscale;
  normal2 = T * normal2.x + B * normal2.y + N * normal2.z;
  normal2 = normalize(normal2);
  #ifdef LW_SPECULAR
   shininess2 = bumpcolor2.a*specular;
  #endif

 #endif

 #ifdef LW_TERRAINNORMALS
  #ifndef LW_BUMPMAP
   //Use terrain normals
   terraincoord=vec2(vertexposition.x,-vertexposition.z) / terrainsize + 0.5;
   terrainresolution = terrainsize / terrainscale.x;
   terraincoord.x -= 0.5 / terrainresolution;
   vec3 worldNormal = ((texture2D(texture14,terraincoord).xyz - 0.5) * 2.0).xyz;
   normal = normalize(gl_NormalMatrix*worldNormal);
  #endif
 #endif

 #ifdef LW_TERRAINCOLOR
  //Use terrain color
  terraincoord=vec2(vertexposition.x,-vertexposition.z) / terrainsize + 0.5;
  terrainresolution = terrainsize / terrainscale.x;
  terraincoord.x -= 0.5 / terrainresolution;
  //terraincoord.y += 0.5 / terrainresolution;
  vec4 terraincolor = texture2D(texture12,terraincoord);
  diffuse = vec4( greyscale(diffuse.xyz) * 2.0 * terraincolor.xyz,diffuse.w);

  #ifdef LW_MESHLAYER
   float temp_w=diffuse.w;
   diffuse = diffuse * (1.0-vegetationfade) + terraincolor * vegetationfade;
   diffuse.w=temp_w;
  #endif

  shininess = terraincolor.w;
 #endif

 #ifdef LE_REFRACTION
  diffuse.a=0.25;
  vec4 refractionvector = vec4( gl_FragCoord.x/buffersize.x, gl_FragCoord.y/buffersize.y, gl_FragCoord.z, 1.0 );
  vec4 refractionvector2 = refractionvector + refractionstrength * vec4(normal,0.0);
  if (gl_FragCoord.z<DepthToZPosition(texture2DProj(LE_DEPTHBUFFER,refractionvector2).x)) {
   refractionvector=refractionvector2;
  }
  vec4 transparency = texture2DProj(LE_REFRACTION,refractionvector);
  diffuse = transparency * diffuse;
 #endif

 #ifdef LW_BUMPMAP2
  shininess = mix( shininess , shininess , fragcolor.r);
  normal = mix( normal , normal2 , fragcolor.r);
 #endif

 vec3 adjustednormal = normal*0.5+0.5;
 float adjustedgloss = gloss;

 shininess=clamp(shininess,0.0,1.0)*0.5;


 #ifdef LW_DIFFUSE2
  diffuse = mix(diffuse, diffuse2,fragcolor.r);
 #endif


 #ifdef LW_DECAL
  if (texcoord.x > 0.05 && texcoord.x < 0.18) {
   diffuse = mix( decal,diffuse, 1.0 - decal.w);
  }
 #endif

    #ifdef LW_CUBEMAP
  vec3 cubecoord = reflect( normalize( modelvertex.xyz - cameraposition ), normal * gl_NormalMatrix );
  diffuse = vec4( textureCube(LW_CUBEMAP,cubecoord).xyz, diffuse.w);
    #endif

    #ifdef LW_CUBEMAP_MIX
  vec3 cubecoord = reflect( normalize( modelvertex.xyz - cameraposition ), normal * gl_NormalMatrix );
  diffuse = vec4(mix(textureCube(LW_CUBEMAP_MIX,cubecoord).xyz, diffuse.xyz , 1.0 - diffuse.w).xyz , diffuse.w );
    #endif

 //Diffuse
 gl_FragData[0] = diffuse;

 //Normal
 #ifdef LW_FULLBRIGHT
  gl_FragData[1] = vec4(1.0,1.0,1.0,diffuse.w);
 #else
  gl_FragData[1] = vec4(adjustednormal,diffuse.w);
 #endif


 //Bloom
 #ifdef LW_BLOOM
  vec4 bloomcolor = texture2D(LW_BLOOM,texcoord) * fragcolor;
  gl_FragData[3] = bloomcolor * 4.0;
  gl_FragData[3].w = 0;
 #else
  #ifdef LW_GIMAP
   vec4 gicolor = texture2D(LW_GIMAP,vec2(texcoord1.x,1.0-texcoord1.y));
   gicolor *= diffuse;
   gicolor.w = 1.0;
   gl_FragData[3] = gicolor;
  #else
   #ifdef LW_VERTEXGI
    gl_FragData[3] = fragcolor * diffuse;
    gl_FragData[3].w = 1.0;
   #else
    gl_FragData[3] = vec4(0);
   #endif
  #endif
 #endif

 // I need the depth decaloffset proportional to the camera distance from the vertex
 #ifdef LW_OVERLAY2
  float dist = length(modelvertex.xyz - cameraposition ) / 512.0;
  float z = DepthToZPosition(gl_FragCoord.z);
  z = z - dist;
  gl_FragDepth = ZPositionToDepth( z );
 #endif

 #ifdef LW_OVERLAY3
  float dist = length(modelvertex.xyz - cameraposition ) / 32.0;
  float z = DepthToZPosition(gl_FragCoord.z);
  z = z - dist;
  gl_FragDepth = ZPositionToDepth( z );
 #endif

 //Modify depth output for shadows
 #ifdef LW_POMMAP
  float z = DepthToZPosition(gl_FragCoord.z);
  z = z -diffuse.a * fparallaxlength * depthP;
  gl_FragDepth = ZPositionToDepth( z );
 #endif

 gl_FragData[2]=vec4(shininess,gloss,0.0,diffuse.w);

}