Feedback on / alternative to this technique

'lo again.

I’ve been toying around with this technique and just wondered if anyone has any issues with it, can suggest any better alternatives, etc.

The end result is that you can have 1 model (or 1 flattened node path, etc), 1 texture and ultimately only 1 batch, but mix repeating and non-repeating textures. I’ve read/heard about this being done, but have never found any specific implementations.

The current workflow I have is:

  1. I model something in Blender. Let’s say it’s a house. There’s maybe 1 repeating texture/material for the roof tiles, 1 repeating texture for the bricks and then another texture for all the individual pieces/details - so that’s 3 overall (and normally that would mean 3 batches, if I’m not much wrong?)

  2. When I’m ready to export, a Blender plugin compiles the 3 textures into 1 texture (i.e. a texture atlas) and corrects the UV coordinates accordingly only for faces using non-repeating textures; repeating texture UVs are left as-is.

  3. Here is where things get weird. The plugin creates 2 additional UV coordinate sets (or layouts or layers or whatever you want to call them). The purpose of the 1st additional set is to indicate the bottom left corner of the “subtexture” within the aforementioned atlas; the 2nd additional set indicates the top right corner.

e.g. For every vertex using the details texture, there will be a UV for the bottom left (and another UV for the top right) of the area within the atlas containing the details. For the vertices using the repeating roof tile texture, these UVs will point to the bottom left and top right of the area within the atlas containing the roof tile texture, etc.

For some quick test code, in your Panda app you might have something like:

ts1 = TextureStage('ts1')
test.setTexture(ts1, texture)
ts2 = TextureStage('ts2')
test.setTexture(ts2, texture)
ts3 = TextureStage('ts3')
test.setTexture(ts3, texture)

(SubtextureMapU is the UV set with the bottom left UVs; SubtextureMapV is for the top right. It seems you need to explicitly set the texture stages and their respective coordinate sets like this to get the correct values to be passed to the shader in the right order, etc.)

Below is the shader in its current state:

/* subtextures.sha */

// ...

void vshader(
    uniform float4x4 mat_modelproj,
    in float4 vtx_position : POSITION,
    in float2 vtx_texcoord0 : TEXCOORD0,
    in float2 vtx_texcoord1 : TEXCOORD1,
    in float2 vtx_texcoord2 : TEXCOORD2,
    out float4 l_position : POSITION,
    out float2 l_texcoord0 : TEXCOORD0,
    out float2 l_texcoord1 : TEXCOORD1,
    out float2 l_texcoord2 : TEXCOORD2
) {
    l_position = mul(mat_modelproj, vtx_position);
    l_texcoord0 = vtx_texcoord0;
    l_texcoord1 = vtx_texcoord1;
    l_texcoord2 = vtx_texcoord2;

// ...

void fshader(
    uniform sampler2D tex_0 : TEXUNIT0,
    in float2 l_texcoord0 : TEXCOORD0,
    in float2 l_texcoord1 : TEXCOORD1,
    in float2 l_texcoord2 : TEXCOORD2,
    out float4 o_color : COLOR
) {
    float width = abs(l_texcoord2.x - l_texcoord1.x);
    float height = abs(l_texcoord2.y - l_texcoord1.y);
    float2 virtual = float2(
        fmod(width * l_texcoord0.x, width),
        fmod(height * l_texcoord0.y, height)
    // fmod #2 to fix negative numbers
    // (and add offset while we're here)
    virtual.x = l_texcoord1.x + fmod(virtual.x + width, width);
    virtual.y = l_texcoord1.y + fmod(virtual.y + height, height);
    // Sample from texture, etc.
    o_color = tex2D(tex_0, virtual);

The basic idea here is that, rather than l_texcoord0 being treated as lookup coordinates within the whole texture, it’s treated as coordinates within the virtual boundary set by l_texcoord1 and l_texcoord2.

… I think that pretty much covers it. I’d welcome any thoughts/criticisms/etc.

(I’m already aware that vertex colours could be used instead of UVs, but obviously that means vertex colours can’t then be used for other things.)

Interesting technique. I assume it still suffers from bleeding issues when mipmapping is enabled, however.

An effective way to do this is by using texture arrays, which wouldn’t have any bleeding issues as different array indices can’t interact with each other. Each texture could have a different index into the array. However, the downside is that texture arrays require all (repeating) textures to have the exact same size.