Thursday, June 28, 2012

Triplanar Texturing

Something I'd been meaning to get around to was improving the terrain texturing.  Steep slopes produced artifacts:

You can clearly see the smearing on the very vertical textures.  This is caused by the planar, or grid based,  nature of basic texture projecting.  The terrain texture coordinates are being added in the X and Z, and are ignoring height (Y).  So if you think about it, a flat terrain square has a certain surface area.  One that is nearly vertical will have a much, much larger surface area, though it is small when viewed from above (as flat planar XZ texture projection would).

The solution is what is referred to as Triplanar Projection, or Triplanar Mapping.  Basically in addition to projecting down on the X and Z, you also project in two directions sideways.  (Why two?  Think about a cube.  From above you can flatly project against the top and bottom.  From the left, the left and right.  That leaves the front and back, thus the third direction).

Shamus illustrates this nicely here.  Another good, simple and straightforward post is here; this in particular was the one I looked at and said 'yeah, this is pretty simple'.

In my case it was a bit more complicated as my terrain is using multitexturing.  My original texture sampling looked like*:

    output.Color  = tex2D(TextureSampler0, PSIn.TextureCoords)  * PSIn.TextureWeights.x;
    output.Color += tex2D(TextureSampler1, PSIn.TextureCoords) * PSIn.TextureWeights.y;
    output.Color += tex2D(TextureSampler2, PSIn.TextureCoords) * PSIn.TextureWeights.z;
    output.Color += tex2D(TextureSampler3, PSIn.TextureCoords) * PSIn.TextureWeights.w;  

*this may look familiar.  A post on that later.

This takes the four textures and blends them based on the weights on the vertex.  The new code (pretty much exactly the example above; you really can't simplify it more):

    float mXY = abs(PSIn.Normal.z);
    float mXZ = abs(PSIn.Normal.y);
    float mYZ = abs(PSIn.Normal.x);   

    float total = (mXY + mXZ + mYZ);
    mXY /= total;
    mXZ /= total;
    mYZ /= total;

    float4 cXY = tex2D(TextureSampler0, PSIn.TextureCoords.xy);
    float4 cXZ = tex2D(TextureSampler0, PSIn.TextureCoords.xz);
    float4 cYZ = tex2D(TextureSampler0, PSIn.TextureCoords.yz);
    output.Color = (cXY*mXY + cXZ*mXZ + cYZ*mYZ) * PSIn.TextureWeights.x;

    cXY = tex2D(TextureSampler1, PSIn.TextureCoords.xy);
    cXZ = tex2D(TextureSampler1, PSIn.TextureCoords.xz);
    cYZ = tex2D(TextureSampler1, PSIn.TextureCoords.yz);
    output.Color += (cXY*mXY + cXZ*mXZ + cYZ*mYZ) * PSIn.TextureWeights.y;

    cXY = tex2D(TextureSampler2, PSIn.TextureCoords.xy);
    cXZ = tex2D(TextureSampler2, PSIn.TextureCoords.xz);
    cYZ = tex2D(TextureSampler2, PSIn.TextureCoords.yz);
    output.Color += (cXY*mXY + cXZ*mXZ + cYZ*mYZ) * PSIn.TextureWeights.z;

    cXY = tex2D(TextureSampler3, PSIn.TextureCoords.xy);
    cXZ = tex2D(TextureSampler3, PSIn.TextureCoords.xz);
    cYZ = tex2D(TextureSampler3, PSIn.TextureCoords.yz);
    output.Color += (cXY*mXY + cXZ*mXZ + cYZ*mYZ) * PSIn.TextureWeights.w;

 Pretty much the same except for the weighting by the normal factors.  Simple, but this makes quite a bit of difference in the final look:

No comments:

Post a Comment