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¹
Assigning UVs to specific textures:
TLDR - Codepen (lines 5 - 91)
- Create plenty of UVs (feel free to ignore naming them, since they are renamed automatically either way):
-
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. useUVMap.002
above.) -
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:
-
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.)
-
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'
});
});
- For FBX - UV maps are renamed to
uv
instead oftexcoord_
, so just be sure to pass a proper prefix and offset (to skipuv2
):
gltf.scene.traverse(child => {
if (!child.material) {
return;
}
remapMaterialUVs(child.material, {
map: 'uv3',
emissiveMap: 'uv4'
}, {
uvAttributePrefix: 'uv',
uvAttributeOffset: 3
});
});
- You’re done - with little-to-none shader code written on that day
Codepen Preview
Troubleshooting
Expand
- 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
});
- 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.