Starting in 1.73, there will be a new default vertex shader,
oolite-tangent-space-vertex.vertex.
oolite-standard-vertex.vertex will be kept around for compatibility, although I don’t think anyone except me used it in OXPs. The fragment shader
default fragment shader has of course also been updated.
From a user perspective, the main new feature of these shaders is the normal and parallax mapping, but those only add up to four lines of code. From a shader-hacking perspective, the main change is that everything is done in tangent space instead of mixed world space and model space.
Warning: if you happen to know the formal mathematical definitions of “tangent space” and “binormal”, note that these terms are abused a bit in graphics. “Normal” is used sensibly and the “tangent vector” is a vector in the actual tangent space of a surface, though.
Tangent space is a coordinate
basis using vectors based on the surface of an object and its texture map. In particular, the basis vectors are called
tangent,
binormal and
normal.
Normal is the, er, normal normal, that is, a vector pointing straight out from the object (and defining what “straight out” means).
Tangent is defined in terms of the texture map. This is slightly tricky to explain without a diagram, but it’s the vector from texture coordinate (0, 0) to texture coordinate (1, 0), projected onto the plane perpendicular to the normal (i.e. the tangent plane) and normalized. The binormal is the cross product of the normal and tangent, i.e. a vector that’s perpendicular to both. The normal is specified in the DAT file, and the tangent is calculated by Oolite (at least in the trunk), and the binormal is calculated in the vertex shader:
Code: Select all
attribute vec3 tangent; // Provided by Oolite
//...
void main(void)
{
// Build tangent basis
vec3 n = normalize(gl_NormalMatrix * gl_Normal);
vec3 t = normalize(gl_NormalMatrix * tangent);
vec3 b = cross(n, t);
mat3 TBN = mat3(t, b, n);
The first line declares an attribute variable, which we haven’t seen before in Oolite-related shaders. This is a variable which has a value set for each vertex by the host (i.e. Oolite), unlike a uniform where the host sets a value that applies to the whole model.
The last line builds a transformation matrix from model space to tangent space. This is then used to convert the various vectors we’re interested in:
Code: Select all
vec3 eyeVector = -vec3(gl_ModelViewMatrix * gl_Vertex);
vEyeVector = eyeVector * TBN;
vec3 light0Vector = gl_LightSource[0].position.xyz + eyeVector;
vLight0Vector = light0Vector * TBN;
vec3 light1Vector = gl_LightSource[1].position.xyz + eyeVector;
vLight1Vector = light1Vector * TBN;
The new vertex shader also sticks the texture coordinate into a varying vector instead of extracting it in the fragment shader (I’m not sure whether this makes a difference either way for performance, but it simplifies the fragment shader slightly, which is good) and projects the vertex onto the screen:
Code: Select all
vTexCoord = gl_MultiTexCoord0.st;
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
Over in the fragment shader, most things actually work exactly as before, since we’ve applied the same transformation to both the eye vector and the light vectors. However, if we’re not doing normal mapping we can make some simplifications because we now know that the normal will allways be (0, 0, 1). For instance, the expression
dot(normal, lightVector) in the diffuse lighting calculation simplifies to
lightVector.z.
Proof wrote:By definition, dot(u, v) is equivalent to u.x * v.x + u.y * v.y + u.z * v.z.
Given u = (0, 0, 1), we get v.x * 0 + v.y * 0 + v.z * 1 = v.z.
Additionally, the expression
-reflect(lightVector, normal) simplifies to
vec3(-lightVector.x, -lightVector.y, lightVector.z).
Proof wrote:By definition, reflect(v, n) is eqivalent to v - 2.0 * dot(n, v) * n.
Given n = (0, 0, 1), we get:
v - 2.0 * v.z * (0, 0, 1) [see previous proof]
= v - (0, 0, 2.0 * v.z)
= (v.x, v.y, -v.z)
The negation is of course (-v.x, -v.y, v.z).
Since the surface normal is a constant, normal mapping is simply implemented by replacing it with a value from the normal map texture.
Code: Select all
#if OOSTD_NORMAL_MAP // Defined to 1 by Oolite if a normal map is to be used
vec3 normal = texture2D(uNormalMap, texCoord).rgb;
#else
const vec3 normal = vec3(0.0, 0.0, 1.0);
#endif
The very simple, but very fast, parallax mapping method used in the default shader works like this:
The parallax map is sampled where the eye vector hits the actual surface, and is used to project the eye vector onto the virtual surface defined by the parallax map. (The offset can be positive or negative.) The diagram accurately illustrates how imprecise this method is: the parallax value at the projected point is different from the one at the intersection pount, which is what’s actually being used. The shallower the viewing angle and the higher the parallax scale, the more wrong it gets, but this technique should be sufficient for stuff like hull plating. I’ll probably implement one of various more accurate (but slower) techniques that exist for high-quality mode. Google for “offset limiting” and “relief mapping” if you’re curious.
Code: Select all
#if OOSTD_NORMAL_AND_PARALLAX_MAP
float parallax = texture2D(uNormalMap, vTexCoord).a;
parallax = parallax * uParallaxScale + uParallaxBias;
vec2 texCoord = vTexCoord - parallax * eyeVector.xy * vec2(-1.0, 1.0);
#else
#define texCoord vTexCoord
#endif
The updated shader also does away with the macros used for lighting before and uses functions instead:
Code: Select all
vec4 CalcDiffuseLight(in vec3 lightVector, in vec3 normal, in vec4 lightColor)
{
#if OOSTD_NORMAL_MAP
float intensity = dot(normal, lightVector);
#else
float intensity = lightVector.z;
#endif
intensity = max(intensity, 0.0);
return lightColor * intensity;
}
vec4 CalcSpecularLight(in vec3 lightVector, in vec3 eyeVector, in float exponent, in vec3 normal, in vec4 lightColor)
{
#if OOSTD_NORMAL_MAP
vec3 reflection = -reflect(lightVector, normal);
#else
vec3 reflection = vec3(-lightVector.x, -lightVector.y, lightVector.z);
#endif
float intensity = dot(reflection, eyeVector);
intensity = pow(max(intensity, 0.0), exponent);
return lightColor * intensity;
}
...
diffuseLight += CalcDiffuseLight(light1Vector, normal, gl_LightSource[1].diffuse);
specularLight += CalcSpecularLight(light1Vector, eyeVector, exponent, normal, gl_LightSource[1].specular);
There’s not much to say here, except that
varying variables are now used for light vectors instead of
gl_LightSource[idx].position.xyz because the light positions need to be transformed to tangent space in the vertex shader. The new shader also avoids normalizing the light vectors twice, which was a bit silly.