Using multiple UV maps per model (r150 and earlier)

:world_map: How to assign custom UV map to any texture on the model material

Having way more time than it is appropriate on my hands and inspired by the unbearable lack of closure to “Multiple UVs Material for one single mesh. Is it somehow possible?” - here we are.


Introductory notes:

  • Starting with r151, three.js supports multiple UV maps. This fix should be necessary only when using earlier versions of three.
  • This should work on any built-in Material type in three - for as long as it supports at least basic UV mapping (MeshStandardMaterial, MeshPhongMaterial etc.)
  • You will not need a custom ShaderMaterial. While the vision of writing custom shaders brings warmth to any developer’s heart - they can be hard to maintain and to keep in-sync with core three (ie. lights / shadows / other chunks.)
  • Tested on glTF and FBX models. If you’re using OBJ - consider not using OBJ¹ :slightly_smiling_face:

Assigning UVs to specific textures:

TLDR - Codepen (lines 5 - 91)

  1. Create plenty of UVs (feel free to ignore naming them, since they are renamed automatically either way):

  1. Note - don’t use second UV map from the list for your own purposes. uv2 (ie. UVMap.001 above) is hardcoded in a few places in built-in three shaders. Using it for your own purposes may work but may also cause weird stuff to happen - use maps from index 3 and further for a worry-less experience (ex. use UVMap.002 above.)

  2. Export the model to GLB / glTF / FBX. While the names stay the same in Blender, UVs will be likely renamed by both the exporter and three’s GLTFLoader in one way or another:

uv-names

  1. Load the model in your code and copy-paste the remapMaterialUVs function (lines 5 - 91, rest is just boilerplate.) This function does not create any new ShaderMaterial type, it just copies and unwraps an existing material - while also adding necessary attributes / uniforms and texture sampling to both the fragment and vertex shaders². That way all the built-in quirks and features of the original material type are still there - including skinning, shadow maps, fog, instancing etc (which would not be the case for ShaderMaterial unless you’d code and add it.)

  2. If you’re using GLB / glTF format, you can just reassign the maps right away:

gltf.scene.traverse(child => {
  if (!child.material) {
    return;
  }

  remapMaterialUVs(child.material, {
    map: 'texcoord_2',
    emissiveMap: 'texcoord_3'
  });
});
  1. For FBX - UV maps are renamed to uv instead of texcoord_, so just be sure to pass a proper prefix and offset (to skip uv2):
gltf.scene.traverse(child => {
  if (!child.material) {
    return;
  }

  remapMaterialUVs(child.material, {
    map: 'uv3',
    emissiveMap: 'uv4'
  }, {
    uvAttributePrefix: 'uv',
    uvAttributeOffset: 3
  });
});
  1. You’re done - with little-to-none shader code written on that day :relieved:

Codepen Preview


Troubleshooting

Expand
  1. If you’re using another model format or another glTF exporter - your file may have UV maps renamed differently. Export and load your model to three, then just console log the model and find .children[i].geometry.attributes:

In this case maps uv3 and uv4 use uv<Number> prefix, so just pass it to the remapping function:

remapMaterialUVs(child.material, {
  map: 'uv2',
  emissiveMap: 'uv3'
}, {
  uvAttributePrefix: 'uv',
  uvAttributeOffset: 3 // NOTE Since the third UV map is called `uv3` - pass 3 as the default offset
});
  1. If you’re getting THREE is not defined errors - it’s likely that you’re using module imports instead of global scope. In that case it should be enough to just add:
// NOTE Assuming you're importing three like so:
import * as Three from 'three';

// NOTE Just add the following line before remapMaterialUVs definition:
const THREE = Three;

¹ More seriously - there’s a small chance it will also work with other model types like OBJ - it just wasn’t tested and UV maps support / renaming seems to be defined on a per-model-format basis.
² Keep in mind - overriding the mapping must create a new material in three’s materials cache, since built-in materials support only uv and uv2 maps. Consider reusing or limiting the amount of remapped materials because of this.

10 Likes

I have been looking for this for a while unfortunately I think three.js updated some of the shaders lib code so this solution no longer work.

I really wanted to have a separate set of uv for my roughness and metalness map so I had to scratch my head a little bit to make it work in recent three.js version.

The code is not as clean and fexible as you @mjurczyk but I just wanted to share it in case someone had a similar use case :slight_smile:

const remapMaterialUVs = (material = {}) => {    
    material.customProgramCacheKey = () => Math.random()
    material.onBeforeCompile = (shader, context) => {  
      const resolveIncludes = (shader) => {
        const includePattern = /^[ \t]*#include +<([\w\d./]+)>/gm
  
        return shader.replace(includePattern, (match, include) => {
          const string = ShaderChunk[include]
          if (!string) return
          return resolveIncludes(string)
        })
      }
      
      shader.vertexShader = resolveIncludes(shader.vertexShader)
      shader.fragmentShader = resolveIncludes(shader.fragmentShader)

      shader.vertexShader = shader.vertexShader.replace(`attribute vec2 uv2;`, `
        attribute vec2 uv2;
        attribute vec2 texcoord_2;
      `)

      shader.vertexShader = shader.vertexShader.replace(`vRoughnessMapUv = ( roughnessMapTransform * vec3( ROUGHNESSMAP_UV, 1 ) ).xy;`, `
        vRoughnessMapUv = ( roughnessMapTransform * vec3( texcoord_2, 1 ) ).xy;
      `)
      
      shader.vertexShader = shader.vertexShader.replace(`vMetalnessMapUv = ( metalnessMapTransform * vec3( METALNESSMAP_UV, 1 ) ).xy;`, `
        vMetalnessMapUv = ( metalnessMapTransform * vec3( texcoord_2, 1 ) ).xy;
      `)
    }
  }

  useMemo(() => {
    remapMaterialUVs(nodes.Forest.material)
  }, [materials])

Later maybe I’ll write a more flexible code with a code sandbox!

1 Like

Unless I’m mistaken, this fix is needed only for older versions of three.js. Starting with r151 - three should support multiple UV maps (github) ?

Wow I didn’t see that… Dammit I should have upgraded three.js before spending time on this one :sweat_smile:

After a bit of research last night I am not sure how well integrated the multiple UV feature is yet. I am only testing with react three fiber at the moment but seems to me that the new channel properties only works for uv1 and 2.

I was able with blender to export a glb with the roughness channel mapped to uv2 which is already a win but if I try uv3 or uv4 in threejs it defaults back to uv1. Not sure if it’s blender gltf exporter, blender itself, react drei gltf loader or just three.js but personally I still couldnt achieve using 4 set of uvs unless I change the shader code myself.

Which three.js version are you using?

Three.js has supported 2 UV sets for a while, but had major restrictions on how they could be used. You couldn’t pick and choose which texture used which UVs. Beginning in r151 you are still limited to 2 UVs, but you can assign any texture to either of them. Beginning in r152 three.js supports up to 4 UVs, and you can still assign any texture to any of them.

I’m not sure which releases of Drei or three-stdlib correspond to those three.js releases, though. Blender and Blender’s glTF exporter should have no trouble with any of this.

Oh I see!! My bad then… I didn’t see only r152 brought the 4 UVs feature!
Sorry I got confused :sweat_smile:

1 Like