8

I have ".obj" and ".mtl" files of a model and I'm loading it via OBJMTLLoader. ".mtl" specifies texture to apply to a model, and three.js loads model and renders it with applied texture just fine.

But here's the thing.

Once an object is loaded, I would like to apply another texture onto it. This is because first texture represents surface material of an object. And second texture is a drawing, that I'd like to position at a specific location on a model.

My question is: how to apply a second texture onto already loaded (and texturized) object?

I see that three.js creates an instance of THREE.Object3D, and that instance has "children" array with one instance of THREE.Mesh.

When I try to apply a texture to that mesh (mesh.material.map = texture), I lose initial texture.

I looked into this question about applying multiple textures and JSONLoader but didn't find an answer.

I also tried using new THREE.MeshFaceMaterial( materials ) (as suggested in this answer) but unsuccessfully.

UPDATE:

I tried @WestLangley's suggestion to use multi-material object, but am still unable to render one material on top of another one.

I made this simple demo, adapted from three.js OBJLoader — http://dl.dropboxusercontent.com/u/822184/webgl_multiple_texture/index.html

I'm using THREE.SceneUtils.createMultiMaterialObject as suggested, passing it cloned geometry of main mesh loaded from .obj. I'm also giving it 2 textures — one for entire surface, another one — for front surface of the model.

But this doesn't work. I added 2 checkboxes that toggle "visible" property of corresponding materials. You can see that materials are present, but I can't see the first one from beneath second one.

The crux of the loading/rendering is as follows:

var texture = THREE.ImageUtils.loadTexture('fabric.jpg');
var texture2 = THREE.ImageUtils.loadTexture('table.jpg');

texture2.offset.set(-0.65, -2.5);
texture2.repeat.set(4, 4);

var loader = new THREE.OBJLoader();
loader.addEventListener( 'load', function ( event ) {

  var mainMesh = event.content.children[0].children[0];

  multiMaterialObject = THREE.SceneUtils.createMultiMaterialObject( 
    mainMesh.geometry.clone(), [
      new THREE.MeshLambertMaterial({ map: texture2 }),
      new THREE.MeshLambertMaterial({ map: texture })
    ]);

  multiMaterialObject.position.y = -80;
  scene.add(multiMaterialObject);
});

loader.load( 'male02.obj' );

UPDATE #2

At this point, I think the best bet is to use THREE.ShaderMaterial to apply one texture onto another. I see some examples of using one texture but still unsure how to display both in overlaid state. I'm also not sure how to position texture at a specific location on a mesh.

gman
  • 100,619
  • 31
  • 269
  • 393
kangax
  • 38,898
  • 13
  • 99
  • 135
  • Similar question (unfortunately without any solid resolution at the moment) — http://stackoverflow.com/questions/12494781/how-to-use-and-blend-multiple-textures-with-custom-values-in-three-js – kangax May 28 '13 at 00:11
  • did you find a solution for positioning the second texture at a specific location on the mesh? I'm having the same issue, I'm able to render a texture onto another with a ShaderMaterial, but I'm struggling understanding how positioning works. – Alberto Dallaporta Apr 12 '20 at 10:55

2 Answers2

19

You have a couple of choices:

  1. You can mix the images on the javascript side using canvas tools, and create a single material with a single texture map.

  2. You can achieve a multi-texture effect with a custom ShaderMaterial. Have two texture inputs, and implement color mixing in the shader.

Here an example of just about the simplest three.js ShaderMaterial possible that implements mixing of two textures: https://jsfiddle.net/fvb85z92/.

three.js r.150

WestLangley
  • 102,557
  • 10
  • 276
  • 276
  • Thanks! If I was to go 2nd route — multi-material object — how would I work with already created one (since loader does this for me)? I assume `THREE.SceneUtils.createMultiMaterialObject` creates an object. Is there a way to modify it? – kangax May 27 '13 at 23:40
  • Actually, I see what's going on in [`createMultiMaterialObject`](https://github.com/mrdoob/three.js/blob/master/src/extras/SceneUtils.js#L7-L17). Like you said, it creates mesh with each of the materials. I suppose if I add another child into that loader-created instance of `THREE.Object3D`, and give it another texture, that should work? Well, I'll try. Thanks again! – kangax May 27 '13 at 23:43
  • I tried your 2nd approach but unsuccessfully. Please see update in question. Am I missing something? – kangax May 28 '13 at 17:55
  • The second approach will only work if one of the materials is transparent. `multiMaterialObject.children[0].material.transparent = true`, `multiMaterialObject.children[0].material.opacity = 0.5`. Like I said, it is usually used when one of the materials is wireframe. – WestLangley May 28 '13 at 19:22
  • Interesting. So looks like multi-material object won't really cut it here, since I need to overlay one opaque texture on top of another. I'll look into your #1 and #3 suggestions. – kangax May 28 '13 at 22:05
  • Thanks for the example. I'm experimenting with it right now and have 2 issues. The material seems overly exposed from the light that's set in a shader (AFAIU). My 0x101030 AmbientLight and 0xffeedd DirectionalLight — at (0, 0, 1) position — that are added to a scene don't seem to have effect? I also don't know how to position 2nd texture at a specific location, now that it's output via shader. – kangax Jun 03 '13 at 20:53
  • With all due respect, I did answer your original question, and then you modified the question with additional issues/requirements. In the spirit of fairness, I would have been better if you made a new post where you could ask questions about how to write a shader. But yes, by setting `lights: true` in the shader params, you can have access to the lights in your scene from within the shader. That is a separate issue. See the three.js examples. I made the example here simple for you so I could focus only on your original issue. To position the texture, you need to understand how to set UVs. – WestLangley Jun 03 '13 at 21:35
  • 1
    No problem. I'll move further issues to separate questions. The shader example is the main thing I needed clarification with. Thanks again. – kangax Jun 03 '13 at 23:52
4

Your loaded object has geometry (along with its vertices, faces and UVs) and material. Create ShaderMaterial that combines the textures in some way that suits you and create mesh with geometry from loaded object.

Use ShaderMaterial and set both textures as uniforms, and then blend them within shader.

So, you make ShaderMaterial:

var vertShader = document.getElementById('vertex_shh').innerHTML;
var fragShader = document.getElementById('fragment_shh').innerHTML;

var attributes = {}; // custom attributes

var uniforms = {    // custom uniforms (your textures)

  tOne: { type: "t", value: THREE.ImageUtils.loadTexture( "cover.png" ) },
  tSec: { type: "t", value: THREE.ImageUtils.loadTexture( "grass.jpg" ) }

};

var material_shh = new THREE.ShaderMaterial({

  uniforms: uniforms,
  attributes: attributes,
  vertexShader: vertShader,
  fragmentShader: fragShader

});

And create mesh with that material:

var me = new THREE.Mesh( my_loaded_model, material_shh ); // you previously loaded geometry of the object

You can put simplest vertex shader:

varying vec2 vUv;

void main()
{
    vUv = uv;
    vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
    gl_Position = projectionMatrix * mvPosition;
}

And fragment shader that will actually do the blending:

#ifdef GL_ES
precision highp float;
#endif

uniform sampler2D tOne;
uniform sampler2D tSec;

varying vec2 vUv;

void main(void)
{
    vec3 c;
    vec4 Ca = texture2D(tOne, vUv);
    vec4 Cb = texture2D(tSec, vUv);
    c = Ca.rgb * Ca.a + Cb.rgb * Cb.a * (1.0 - Ca.a);  // blending equation or wahtever suits you
    gl_FragColor= vec4(c, 1.0);
}
Dragan Okanovic
  • 7,509
  • 3
  • 32
  • 48