Oolite 2: materials

General discussion for players of Oolite.

Moderators: winston, another_commander

Post Reply
User avatar
JensAyton
Grand Admiral Emeritus
Grand Admiral Emeritus
Posts: 6657
Joined: Sat Apr 02, 2005 2:43 pm
Location: Sweden
Contact:

Oolite 2: materials

Post by JensAyton »

So, as you may have noticed, the two-week schedule on 1.75 betas has slipped a bit. This is entirely my fault. My current plan is to release 1.75.2 next weekend, regardless of how many more bugs are fixed before then.

But fear not! I haven’t wasted my time on any of that boring having-a-life stuff. Instead, I’ve been hacking on Oolite 2. A lot of it is dull back-end stuff, but for the last few days I’ve been working on the material system for the new graphics engine, so I now have some pretty pictures to show. Well, slightly pretty.

Image

This is an Oolite 2 Cobra Mark 3 in Dry Dock 2, the test bed for the new engine (cleverly named OoliteGraphics). If it seems less impressive than Oolite 1, you’re right: it currently only handles diffuse lighting and emission maps. However, it does it in a much smarter way.

In Oolite 1, materials without custom shaders are rendered using the default shader, which is 341 lines of very messy GLSL. To control the various features, Oolite writes a bunch of macro definitions in front of the shader. This is not only ugly, it’s also inflexible. For example, there are exactly two ways textures can be combined: normal_and_parallax_map and emission_and_illumination_map. If you want to combine a normal map and an emission map into one texture, well, you have to write your own shader.

In the new regime, the game will instead write a shader which implements only the features specified in a material. When it does, any particular texture-mapped value can be specified as any combination of channels from a texture, and if a texture is used for more than one map it will only be read once. For example, the material specification for the Cobra is:

Code: Select all

materials:
{
    main:
    {
        diffuseMap: "griff_cobra_mk3_mainhull_diffuse_spec.png",
        normalMap: "griff_cobra_mk3_mainhull_normal.png",
        specularExponent: 5,
        specularColor: [0.2, 0.2, 0.2],
        emissionMap: { name: "griff_cobra_mk3_mainhull_normal.png", extract: "a" },
        emissionColor: [0.99, 0.97, 0.73]
    }
}
Note that the emissionMap uses the same texture as the normalMap. This particular case, extracting only one channel, is possible in Oolite 1, but it will actually result in a second, greyscale texture being created. In Oolite 2, extract can take one to four channels, as in rgb, br or rrra – exactly like swizzle operations in GLSL.

Here is the output of the shader generator. Most of the actual shader code is temporary, but it demonstrates the idea:

Code: Select all

// Vertex shader:
// Attributes
attribute vec3      aPosition;
attribute vec2      aTexCoords;
attribute vec3      aNormal;


// Varyings
varying vec2        vTexCoords;
varying vec3        vNormal;
varying vec3        vLightVector;
varying vec4        vPosition;


void main(void)
{
    vec4 position = gl_ModelViewProjectionMatrix * vec4(aPosition, 1.0);
    gl_Position = position;
    
    vTexCoords = aTexCoords;
    vPosition = position;
    vNormal = normalize(gl_NormalMatrix * aNormal);
    vLightVector = gl_LightSource[0].position.xyz;
}

// Fragment shader:
// Uniforms
uniform sampler2D   uTexture0;
uniform sampler2D   uTexture1;


// Varyings
varying vec2        vTexCoords;
varying vec3        vNormal;
varying vec3        vLightVector;
varying vec4        vPosition;


void main(void)
{
    vec4 totalColor = vec4(0.0);
    
    // Texture reads
    vec2 texCoords = vTexCoords;
    vec4 tex0Sample = texture2D(uTexture0, texCoords);  // griff_cobra_mk3_mainhull_diffuse_spec.png
    vec4 tex1Sample = texture2D(uTexture1, texCoords);  // griff_cobra_mk3_mainhull_normal.png
    
    // Placeholder lighting
    vec3 eyeVector = normalize(-vPosition.xyz);
    vec3 lightVector = normalize(vLightVector);
    vec3 normal = normalize(vNormal);
    float intensity = 0.8 * dot(normal, lightVector) + 0.2;
    vec4 diffuseLight = vec4(vec3(intensity), 1.0);
    
    // Diffuse colour
    vec4 diffuseColor = tex0Sample;
    totalColor += diffuseColor * diffuseLight;
    
    // Emission (glow)
    vec3 emissionColor = tex1Sample.aaa;
    emissionColor *= vec3(0.99, 0.97, 0.73);
    totalColor += vec4(emissionColor, 0.0);
    
    gl_FragColor = totalColor;
}

// Uniforms:
{
    uTexture0: { type: "texture", value: 0 },
    uTexture1: { type: "texture", value: 1 }
}
Note the line vec3 emissionColor = tex1Sample.aaa;, which extracts the alpha channel as instructed and “splats” it into a vec3. When normal mapping is implemented, the normal will be read as vec3 normal = normalize(tex1Sample.rgb); (“rgb” being the default extract mode for normal maps), reusing the same texture sample.

As a more concrete example, here’s what happens if you change the diffuseMap to { name: "griff_cobra_mk3_mainhull_diffuse_spec.png", extract: "brg" }:

Image

In the shader, the line vec4 diffuseColor = tex0Sample; changes to vec4 diffuseColor = vec4(tex0Sample.brg, 1.0);.

The extra flexibility also means it would be quite easy to support multiple emission maps, each with their own colours. Intensity could be bound to anything that can be used in a uniform binding. For example, engine glows could be implemented this way, without treating them as a special case.

Another thing to note is the “attributes” section at the top of the vertex shader. Attributes are per-vertex values. In Oolite 1.x, custom shaders can only use the predefined attributes gl_Vertex, gl_Normal, gl_MultiTexCoord0.st and tangent. These are now deprecated in OpenGL (except tangent, which is a new-style attribute, but hard-coded in Oolite). In Oolite 2, mesh files will be able to specify arbitrary attributes of type float, vec2, vec3 or vec4. Possible uses include multiple sets of texture coordinates, vertex colours, and animation weights. The only required attribute is position, which is needed for simulation purposes. Even if you don’t use any of those, aTexCoords is a lot more convenient than gl_MultiTexCoord0.st.
User avatar
DaddyHoggy
Intergalactic Spam Assassin
Intergalactic Spam Assassin
Posts: 8515
Joined: Tue Dec 05, 2006 9:43 pm
Location: Newbury, UK
Contact:

Re: Oolite 2: materials

Post by DaddyHoggy »

Oooh, interesting...

Glad to see your non-life is yielding such positive results for what will be Oolite 2.

Looking forward to seeing my first semi-transparent cockpit canopy, with reflections... :wink:
Selezen wrote:
Apparently I was having a DaddyHoggy moment.
Oolite Life is now revealed here
User avatar
Griff
Oolite 2 Art Director
Oolite 2 Art Director
Posts: 2481
Joined: Fri Jul 14, 2006 12:29 pm
Location: Probably hugging his Air Fryer

Re: Oolite 2: materials

Post by Griff »

Wow Ahruman, this is amazing! How massively complex must something like this be to program? *doffs hat*
It's like some sort of magic shader wish machine, you list the textures and the effects you want and it writes out a shader for you
User avatar
JensAyton
Grand Admiral Emeritus
Grand Admiral Emeritus
Posts: 6657
Joined: Sat Apr 02, 2005 2:43 pm
Location: Sweden
Contact:

Re: Oolite 2: materials

Post by JensAyton »

Image

Normal mapping, specular lighting and mapping, and three separate light maps. (I’ve darkened the diffuse color to make the effects clearer.)

Here’s the material specification:

Code: Select all

{
    diffuseColor: [0.2, 0.2, 0.2],
    diffuseMap: "griff_cobra_mk3_mainhull_diffuse_spec.png",
    normalMap: "griff_cobra_mk3_mainhull_normal.png",
    specularExponent: 5,
    specularColor: [0.4, 0.4, 0.4],
    specularColorMap: { name: "griff_cobra_mk3_mainhull_diffuse_spec.png", extract: "a" },
    lightMaps:
    [
        {
            map: { name: "griff_cobra_mk3_mainhull_normal.png", extract: "a" },
            color: [0.99, 0.97, 0.73]
        },
        {
            map: { name: "griff_cobra_mk3_mainhull_effects.png", extract: "r" },
            color: [0.2, 0.7, 0.9, 2]
        },
        {
            map: { name: "griff_cobra_mk3_mainhull_effects.png", extract: "b" },
            color: [1, 0.367, 0.133, 0.5]
        }
    ]
}
Light maps can work like Oolite 1 emission maps, like these do, or like illumination maps, by specifying premultiplied = false. The alpha channel of light map modulate colours can be used to modify overall brightness (it’s multiplied into the other terms).

Griff wrote:
Wow Ahruman, this is amazing! How massively complex must something like this be to program?
Not very complex at all. There are clever ways to handle code generation, but this is the simple way; it’s just bashing a bunch of strings together.
User avatar
JensAyton
Grand Admiral Emeritus
Grand Admiral Emeritus
Posts: 6657
Joined: Sat Apr 02, 2005 2:43 pm
Location: Sweden
Contact:

Re: Oolite 2: materials

Post by JensAyton »

I originally planned to write the shader generator in JavaScript, partly so it could be replaced by expansion packs and partly because JavaScript is somewhat better at writing this type of code. I decided against it, at least for the prototype stage, because integrating a JavaScript implementation into tools would be more difficult.

Still, I wanted to see how much nicer it would be to do it in JS, so I tried it, and here it is. This is a pretty direct port of the prototype in its current states, including major warts (like the fact that dependencies are tracked entirely in my head). It turned out to be a definite win, but not a huge one.

The actual banging-strings-together code is here.
Post Reply