8

I have an OBJ that uses four textures. The UVs defined in the file range from (0, 0) to (2, 2), such that (0.5, 0.5) refers to a coordinate in the first texture, (0.5, 1.5) is a UV coordinate in the second texture, (1.5, 0.5) is a coordinate in the third texture, and (1.5, 1.5) is a coordinate in the last texture.

I already have the correct three.js geometry or object. However, I now need to be able to apply the correct texture maps to these objects.

In code:

I have a THREE.Mesh with the correct geometry (with UVs coords such that U = [0, 2], V = [0, 2]) and a dummy placeholder material. I currently load a single texture like so:

var texture = new THREE.TextureLoader().load('tex_u1_v1.png', function() {
    object.material.map = texture;
    object.material.map.needsUpdate = true;
});

As expected, one fourth of the mesh is textured correctly. I have three more texture files, tex_u1_v2.png, tex_u2_v1.png, and tex_u2_v2.png. I want to be able to apply these textures as well to object (the THREE.js mesh), such that there is a texture for every valid UV in the mesh.

However, I do not know how to add multiple materials to object after it has been created. Moreover, I do not know how to specify to the mesh that tex_u1_v2.png, for example, should be used for UVs in range (U = [0, 2], V = [1, 2]).

The Obscure Question
  • 1,134
  • 11
  • 26
  • When you post here, the idea is that you have already done lots of research yourself. What have you tried so far? Where exactly are you stuck? What is the question? It shouldn't start with "how to". (also, the straightforward answer is to divide your UV coords by 2 and combine the four images into a single texture. did you try that?) –  May 15 '18 at 01:15
  • Currently, I have tried using a model with UVs from 0 to 1, with a single textured image. However, I am working with extremely high resolution textures (16384^2), which results in extremely low FPS. Having done various testing, it appears that this low FPS is a function of a single large texture, rather than the total number of pixels in all textures. – The Obscure Question May 15 '18 at 02:21
  • There is probably a different approach to rendering your massive textures. There is no screen that can render that anyway, so maybe some kind of LOD system might be worth investigating. Otherwise this does not work out of the box. You can try modifying your `object.material` with `onBeforeCompile`, but it's clunky. – pailhead May 15 '18 at 18:43
  • What I found is that four identical meshes with four different 8192^2 textures runs at 30-60 fps, while a single mesh with a 16384^2 texture runs at <1 fps. I assume that somehow for extremely large textures, rendering has to be offloaded to the CPU. – The Obscure Question May 15 '18 at 19:11
  • Therefore, I expect that if I were to use four (or more) smaller textures on a single mesh, instead of a single larger texture, then I could achieve much higher FPS. Now, I'm trying to figure out how to assign these four textures to the single mesh, such that they use the extended range of UV coordinates (i.e. texture 1 is used for UVs in range (U = [0,1], V = [0, 1]), texture 2 is used for UVs in range (U = [1,2], V = [0, 1]), etc.). – The Obscure Question May 15 '18 at 19:13
  • Why do you need it to be a single mesh then? Why not 4 meshes? And again, you wont find a way out of the box. Out of the box three has a mechanism to extend built in materials `material.onBeforeCompile` but it is extremely clunky. Other than this, three.js allows you to easily write GLSL shaders, even taking away some of the legwork. this is the only other way. – pailhead May 15 '18 at 20:25
  • Technically I don't need it to be a single mesh, but the nature of the textures would result in a lot of redundant vertices, which can blow up the filesize. – The Obscure Question May 15 '18 at 20:50
  • I actually just tried using four meshes, and there is crazy flickering due to precision issues re: the gaps between the meshes. – The Obscure Question May 17 '18 at 07:16
  • You should probably try to set a jsfiddle. – pailhead May 19 '18 at 04:47
  • How is the nature of the textures affecting the vertices in your files? Do you have smoothing over uv splits? – pailhead May 19 '18 at 09:57

5 Answers5

7

The standard materials in Three will only accept a single texture object for the various map-parameters (and the texture objects will only hold a single image), so in order to use multiple textures on your object you will have to use multiple materials or create your own multi-texture-material. If you have experience with shader programming you will probably get the best performance with the latter approach (assuming you have enough video memory for your large textures) as you can draw the entire mesh in a single draw call and without having to load new shaders or textures.

To create your own shader you can use the ShaderMaterial or RawShaderMaterial, give it one texture uniform for every texture you will need (four in your case) and then in the shader code pick the correct one to sample depending on the coordinates.

To make an object use more than one material you can set the material property to an array of materials (either during creation with the constructor parameter, or just replace it manually at a later stage).

const myMaterials = [tex1Material, tex2Material, tex3Material, tex4Material];
const myMesh = new THREE.Mesh(myGeometry, myMaterials);
//Or:
myMesh.materials = myMaterials;

Then, to make the different parts of your mesh use the appropriate materials you will have to create groups if it is a BufferGeometry; or set the materialIndex of the faces if you are using a Geometry. The material index (both in the group and the face) is the index of the material in the mesh.material array shown above.

Now that you have different parts of the mesh with different materials, you can just give each material their own textures.

  • The probably easiest way to get the correct uv coordinates for the textures would be to just keep each part in the [0,1] interval. Since each part of the mesh uses a unique material you don't have to worry about overlapping coordinates.

If you don't want to modify your already existing coordinates there are two alternative approaches:

  • Set the texture wrapping to THREE.RepeatWrapping:

    myTexture.wrapS = THREE.RepeatWrapping;
    myTexture.wrapT = THREE.RepeatWrapping;
    

    This will make the texture repeat beyond the standard [0-1] uv interval.

  • The other way is to use the offset property of the texture to push it back into the [0-1] interval. For a texture to be placed in the u[0,1], v[1,2] interval you would set the offset the v-coordinate by -1:

    myTexture.offset = new THREE.Vector2(0, -1);
    

Here is a link to a jsfiddle that demonstrate these methods: https://jsfiddle.net/xfehvmb4/

Jave
  • 31,598
  • 14
  • 77
  • 90
  • Why not just make four meshes? – pailhead May 19 '18 at 04:47
  • @pailhead Technically there is probably no difference, but depending on how the meshes are created, stored and used in the code it might be more convenient to have a single mesh object. But, the question is about using a single mesh with multiple materials/textures. – Jave May 19 '18 at 07:04
  • It makes sense but im thinking if this is an assumption worth taking for granted. Conceptually this is more or less the same as having four meshes, but might be slightly more efficient if its only transformed once before being rendered, rather than four times. I don't think the question is as clear as it could be, when i think of multiple textures, i usually think of some kind of an overlay, this is a very very specific request, and maybe it would be helpful to edit the question a bit to make it more clear? – pailhead May 19 '18 at 09:31
  • `I have a THREE.Mesh with the CORRECT geometry` <- correct for what? This part i think could use some more explanation, and the usage of the word "correct" could use some clarification. – pailhead May 19 '18 at 09:34
  • I'm also not entirely sure if the offset example would work, but i know i read it earlier and it made sense. If you have a uv coordinate in the domain [1,2] is it not the same as if it were in [0,1]? Offsetting it by 1,-1 in either or both directions should just bring it to where it started? A mesh with it's uvs a rectangle [1,1,2,2] would be just as same as [0,0,1,1]. But my understanding of the described geometry makes this confusing. This all sounds like the opposite of a texture atlas. – pailhead May 19 '18 at 09:38
  • @pailhead coordinates in the [1,2] range would only behave the same as coordinates in [0,1] if you have set the `wrap`-paramters to repeat. Otherwise they would be clamped to the values at 1. – Jave May 19 '18 at 11:54
  • Damn, this is too simple for this much philosophy heh. Apologies i think i get it now, this is specifically if you are clamping. – pailhead May 19 '18 at 18:04
  • Hah, no problem. The default behaviour in Three is to clamp, not wrap, which is probably the cause for some of the confusion. – Jave May 19 '18 at 19:13
  • Would still love to see the obj file. – pailhead May 19 '18 at 22:51
  • Love the discussion, I'll respond to @pailhead (and hopefully clarify) in the comments to his question. – The Obscure Question May 20 '18 at 00:21
0

I have a THREE.Mesh with the correct geometry (with UVs coords such that U = [0, 2], V = [0, 2])

Your understanding of "correct" in this case, may be incorrect. Lets consider a tetrahedron, and map it, yielding 4 triangles. Let's place each triangle into it's own quadrant in UV space. The four quadrants would be:

02--12--22
 | B | C |
01--11--21
 | A | D |
00--10--20

I'm not entirely sure how to convey this best, but the lookup for the texture is always done in quad A (0011) if that makes sense. As soon as a value goes below 0 or above 1 it just wraps around and looks up the same space. So, all 4 triangles here (ABCD), are actually overlapping. There exists no texture past this range. You either clamp to the edge pixel, or you wrap around (or mirror possibly).

There might be a good reason to have UVs outside of this range, but it doesn't make much sense in your case. I imagine you do not have any triangles going across these boundaries, so you might as well just have 4 different meshes, with their own UVs in 0,1 domain, using their own textures.

It can also be achieved with the other answer, by using an array of materials and setting groups. This is all you need. When it renders the mesh whos uvs are all in 1,1,2,2, it would be exactly the same as if they were in 0,0,1,1.

pailhead
  • 5,162
  • 2
  • 25
  • 46
  • Your understanding of my question is correct. I may as well just have 4 different meshes. The four UV quadrants (centered at (1, 1)) are functionally distinct, so there should theoretically be no issue with splitting it up into four meshes. However, the issues comes during rendering. It should make intuitive sense that some vertices are shared between the four meshes to make them seem cohesive. However, they are not explicitly connected. – The Obscure Question May 20 '18 at 00:24
  • The reason why is that I perform lossy compression on the mesh. Using a custom algorithm, I'm able to drop my geometry filesize by >1000x. However, this also results in a loss of precision, which means that the "same" vertices don't have the same value. Therefore, you have flickering. http://jsfiddle.net/v311zxj2/1/ – The Obscure Question May 20 '18 at 00:49
  • Using a single mesh, i.e. preserving which vertices are the "same," so to speak, allows me to use my compression algorithm while bypassing this problem. – The Obscure Question May 20 '18 at 00:50
  • I need this compression b/c the geometry filesize is >100 MB, which is impossible to deliver readily over a web browser. The only way to completely avoid the precision issue is to use lossless compression, which already happens through GZIP, and saves ~40 MB at best. W/ lossy compression, I'm able to drop filesize to sub-100 KB. – The Obscure Question May 20 '18 at 01:04
0

@Jave's answer looks pretty good to me, but if you want to instead go the ShaderMaterial route, here's how you'd do it:

// Make the material
var material = new new THREE.ShaderMaterial({
      uniforms: {
        u_tex1: {value: null},
        u_tex2: {value: null},
        u_tex3: {value: null},
        u_tex4: {value: null},
      },
      vertexShader: `
        varying vec2 v_uv;
        varying float v_textureIndex;
        void main() {
          // This maps the uvs you mentioned to [0, 1, 2, 3]
          v_textureIndex = step(0.5, uv.x) + step(0.5, uv.y) * 2.0;
          v_uv = uv;
          gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
      `,
      fragmentShader: `
        varying vec2 v_uv;
        varying float v_textureIndex;
        uniform sampler2D u_tex1;
        uniform sampler2D u_tex2;
        uniform sampler2D u_tex3;
        uniform sampler2D u_tex4;
        void main() {
          vec4 color = texture2D(u_tex1, v_uv);
          // These lines make sure you get the right texture
          color = mix(color, texture2D(u_tex2, v_uv), step(0.5, v_textureIndex));
          color = mix(color, texture2D(u_tex3, v_uv), step(1.5, v_textureIndex));
          color = mix(color, texture2D(u_tex4, v_uv), step(2.5, v_textureIndex));
          gl_FragColor = color;
        }
      `,
    });

var texture1 = new THREE.TextureLoader().load('tex_u1_v1.png', function() { material.uniforms.u_tex1.value = texture1 });
var texture2 = new THREE.TextureLoader().load('tex_u2_v1.png', function() { material.uniforms.u_tex2.value = texture2 });
var texture3 = new THREE.TextureLoader().load('tex_u1_v2.png', function() { material.uniforms.u_tex3.value = texture3 });
var texture4 = new THREE.TextureLoader().load('tex_u2_v2.png', function() { material.uniforms.u_tex4.value = texture4 });

This is slightly inefficient, as you are doing 4 texture samples, but is very flexible.

Aaron Krajeski
  • 757
  • 5
  • 18
0

Also it's possible combine different types of textures for one mash, for example

  • bump map
  • diffuse
  • normal map ...etc

I downloaded @types/three and it helped me a lot to understand properties of MeshPhongMaterial, which instantiated my mesh material. There are props bumpMap, map, normalMap and other. Also there is a color property which can change texture's color.

Nikolay Podolnyy
  • 931
  • 10
  • 19
0

After a week try debuging code, I found this solution:

  1. If 3D object loaded, then set it uv in any 3D software (example blender) to Modal, this will add more mesh to object in three code and export. Then load in Threejs.

  2. You can get Mesh detail by console logging the 3D object:

    loader.load(
      './../../assets/img/3d/1/tshirtv4.glb',
      gltf => {
        let tshirtObj = gltf.scene;
        tshirtObj.position.set(0, 0, 0);
        // text canvas as Texture function
        console.log('Modal', tshirtObj);
        let MeshCommon = tshirtObj.getObjectByName('t-shirt002_1');
        let MeshBack = tshirtObj.getObjectByName('t-shirt002_2');
        let MeshSleeve = tshirtObj.getObjectByName('t-shirt002_3');
        
        const texturePatchUV2 = tshirtObj.getObjectByName('t-shirt002_3').geometry.attributes.uv2.array;
        tshirtObj.getObjectByName('t-shirt002_3').geometry.attributes.uv.array = texturePatchUV2;
        let texturePatchUV1 = tshirtObj.getObjectByName('t-shirt002_3').geometry.attributes.uv.array
        **console.log('Patchuv', texturePatchUV1)**
        function sceneUpdate() {
          //console.log('Mesh Patch material', MeshSleeve.material);
          const material = materialV2();
          gltf.scene.traverse(child => {
            if (child.isMesh) {
              //child.material = material[0];
              MeshCommon.material = material[0];
              MeshBack.material = material[1];
              MeshSleeve.material = material[2];

              // child.material = materialV2();
            }
          });
          scene.add(tshirtObj);
        }
        sceneUpdate();

        jsNameInput.addEventListener('input', sceneUpdate);
        jsNumberInput.addEventListener('input', sceneUpdate);
      },
      // called while loading is progressing
      function (xhr) {
        // console.log((xhr.loaded / xhr.total) * 100 + '% loaded');
      },
      // called when loading has errors
      function (error) {
        console.error(error);
      }
    );
  1. Then replace UV map with what desire uvmap, I assign uv2 to uv as in screenshot:
const texturePatchUV2= tshirtObj.getObjectByName('t-shirt002_3').geometry.attributes.uv2.array;
tshirtObj.getObjectByName('t-shirt002_3').geometry.attributes.uv.array = texturePatchUV2;
let texturePatchUV1 = tshirtObj.getObjectByName('t-shirt002_3').geometry.attributes.uv.array
console.log('Patchuv', texturePatchUV1)
Zach Jensz
  • 3,650
  • 5
  • 15
  • 30