Article for those interested in graphics, terrain mapping, shadows etc

A great article deconstructing the graphics behind the game Supreme Commander. Lots of interesting techniques.

http://www.adriancourreges.com/blog/2015/06/23/supreme-commander-graphics-study/

http://www.adriancourreges.com/img/blog/2015/supcom/shot/b_95_final_frame.jpg

thanks for sharing!

That’s a great article, wonderfully presented too, with all the before-and-after animations. I hadn’t heard of albedo textures before, had to look it up.

One thing I do when texturing terrain which makes an enormous difference, but is quite a bit simpler to implement than these next-gen techniques, is texture blending between two textures (I guess it’s kind of similar to the “splat map” technique discussed in the article).

You need two textures that are around the same scale (not hard, as lots of tileable textures tend to be 512x512 or 1024x1024 anyway), as this method uses the same texCoords for both textures, using the tileable texCoord technique.

How you generate the “splat map” is up to you.

Here, I use a noise map, to create organic patches:

https://youtu.be/JPqP_nz-GdQ

It really helps to disguise the fact that the same texture is repeating over-and-over.

In the below video, the splat map is related to height, as the textures are meant to reflect above/ below the tree-line:

https://youtu.be/G_j6mYks8Sc

Here’s the shader. You can put the texture blend variable in its own vertex attribute float, or, to save memory and the hassle of setting up a custom vertex attribute, you can hijack an unused value, such as the alpha of the colour attribute (as a vertex attribute float takes up as much memory as a vec4).

I think if you added a third texture to the mix, it would look really cool.

Remember that when setting up the shader, the second texture has to be assigned a little differently, as it is not in the “built-in” slot:

m.texture = "Dropbox:Barren Reds"
m.shader.texture2 = "Dropbox:Crystal Mountain"

The texture blending is done with GLES’s mix command:

lowp vec4 pixel = mix(texture2D( texture, vec2(fract(vTexCoord.x), fract(vTexCoord.y))), texture2D( texture2, vec2(fract(vTexCoord.x), fract(vTexCoord.y))), vColor.a); //use alpha to mix two textures

the shader:

GroundShader = {  --texture blending!
    vert = [[
    uniform mat4 modelViewProjection;
    uniform mat4 modelMatrix;
    uniform vec4 lightColor; //--directional light colour
    uniform vec4 light; //--directional light direction xyz0
            
    attribute vec4 position;
   // attribute lowp float texBlend; //0=100% texture1, 1=100% texture2...
    attribute vec4 color; // ...or put the texBlend value into the alpha
    attribute vec2 texCoord; //=mesh width / texture width
    attribute vec3 normal;

    varying lowp vec4 vColor;
    varying highp vec2 vTexCoord;
    varying lowp vec4 vPosition;
    varying vec4 vDirectDiffuse;
    
    void main()
    {
        vTexCoord = texCoord;
   
        vPosition = position * modelMatrix;
        vec4 norm = normalize(modelMatrix * vec4(normal,0.0)); 
        vDirectDiffuse = lightColor *  max( 0.0, dot( norm, light )) * 1.5; //last is direct strength   
        vColor = color; 
        gl_Position = modelViewProjection * position;
    }
    
    ]],
    
    frag=[[
    precision highp float;

    uniform lowp sampler2D texture;
    uniform lowp sampler2D texture2; //second texture
    uniform lowp vec4 aerial; //colour of fog/aerial perspective
    uniform float ambient; //power of ambient light
    uniform vec4 eye;  //xyz1
    uniform float fogRadius;
    
    varying lowp vec4 vColor;
    varying highp vec2 vTexCoord;
    varying lowp vec4 vPosition;
    varying vec4 vDirectDiffuse;   
    
    void main()
    {

     lowp vec4 pixel = mix(texture2D( texture, vec2(fract(vTexCoord.x), fract(vTexCoord.y))), texture2D( texture2, vec2(fract(vTexCoord.x), fract(vTexCoord.y))), vColor.a); //use alpha to mix two textures
        lowp vec4 ambLight = pixel * aerial * ambient;       //vColor was here
     //   ambLight.a = 1.;
        lowp vec4 directional = pixel * vDirectDiffuse;
        float dist = clamp(1.0-distance(vPosition.xyz, eye.xyz)/fogRadius+0.1, 0.0, 1.1); // calculate distance from eye
        vec4 totalColor = mix(aerial, clamp(ambLight + directional,0.,1.), dist * dist); //fog varies by distance squared
         totalColor.a = 1.; 

        gl_FragColor = totalColor;
    }
    
    ]] 
}

Great, thank you, I look forward to trying that out! :-bd

Damn that YouTube video quality is so awful you can barely see what’s going on. Here’s an overhead shot which should make the patches of mineral deposit against the red rock surface easier to see (I guess I should do a before-and-after comparison…)

blending

I was aiming for a Mad Max vibe here…

I get the blending idea, but how do you apply this with infinite terrain and texture repetitions?

@yojimbo2000 your 3d blaster is a jaw-dropper… =D>

Here’s the code for the splat map terrain in the top racer. The height and the mineral deposits are both noise maps. I apply a smooth step algorithm to the splat map, because it’s meant to be blotches of fairly solid “1” and “0”, with gradations in between, rather than a smoothly undulating height map.

    grid={} --holds the actual height map
    --values for heightMap and "splat" map
    local seed=math.random(5000)
    local seed2=seed + math.random(5000)
    local bumpy=level.bumpy
    --values for blending texture2 "splats" (mineral deposits)
    local depositSize = 1.5 + (math.random() * 3)
    local depositFallOff = 0.1 + (math.random() * 0.3)
    print ("deposit size "..depositSize.."\
deposit falloff "..depositFallOff)
    local bumpy2=bumpy / depositSize
    --arrays for verts, texCoords, colors
    local pp={}
    local tc={}
    local cc={}
    --scale of texture
    local texw,texh=1024, 1024
    for x=1,arena.cell.x do
        grid[x]={}
        for y=1,arena.cell.y do
            local p=vec2(x-1,y-1)*arena.step --convert cell to world coords
            local n=noise(p.x/bumpy, p.y/bumpy, seed) --first noise value is height
            local n2=noise(p.x/bumpy2, p.y/bumpy2, seed2) --second noise value is "splat" map
            local texBlend = smoothstep(n2, -depositFallOff, depositFallOff) * 255 --smooth step it into patches, convert to colour value (as will be placed in alpha)
            p = p - arena.cent --place geometry origin in centre of arena
            grid[x][y]={p=vec3(p.x,p.y,n*level.height), tc=vec2(p.x/texw, p.y/texh), col=color(255, texBlend)}
            if x>1 and y>1 then --start making squares
                local a,b,c,d = grid[x][y], grid[x][y-1], grid[x-1][y-1], grid[x-1][y] --4 unique points
                local ord={a,b,c, a,c,d} --wind triangles clockwise
                for k=1, 6 do
                    pp[#pp+1]= ord[k].p --write the vertex            
                    tc[#tc+1] = ord[k].tc --write tex coord
                    cc[#cc+1] = ord[k].col --the color (actually just the texture blend value in the alpha)

                end
            end
        end
    end
    terrain.shader=shader(GroundShader.vert, GroundShader.frag)
    terrain.m=Mesh{texture="Dropbox:Barren Reds", vertices=pp, texCoords=tc, colors=cc, normals=CalculateAverageNormals(pp), shader=terrain.shader}
    terrain.m.m.shader.texture2="Dropbox:Crystal Mountain"

I use @Ignatz 's incredibly convenient calculate average normals function here to do the normals.

Here’s the smooth step (got this from @LoopSpace I think?)

function smoothstep(t,a,b)
    local a,b = a or 0,b or 1
    local t = math.min(1,math.max(0,(t-a)/(b-a)))
    return t * t * (3 - 2 * t)
end

Regarding making it infinite (the second video), the infinite code terrain is a bit of a mess right now. Firstly tho, it only infinitely generates in one direction (“forwards”). Left and right it wraps (I don’t think you notice this too much), and you can’t go backwards. So it’s only really appropriate for a fairly arcadey setup. I use two meshes for the forward movement, each one is drawn twice for the left/right wrap.

The texture coords are easy because they never change, you can just set them once. Whatever the point in the terrain mesh, the texCoords are always worldPoint/scaledTextureSize. (ie steep terrain stretches the texture. It’s not too noticeable unless it’s super steep)

The really hard thing with the endless terrain is calculating average normals on the fly. You have to keep a record of every vertex that takes each unique point in the height map.

The blaster is kind of on hold at the moment. I’m not satisfied with the 2.5d gameplay - eg when the enemies come to mountains they kind of drift over them, you can just about see it happen once in the above video, it doesn’t really work as a gameplay mechanic. So I’m thinking that if you have a full 3D terrain that you can interact with, then the flight also needs to be full 3D, otherwise you start having to add lots of strange workarounds (like having enemies glide over mountains). Would totally change the mechanic though (enemies would be much harder to hit for a start)