Search code examples
three.jsblendergltf

Blended materials / textures support using GLTF


Before I get into the problem, I want to emphasize I'm far from an expert when it comes to 3D modelling, texturing and rendering techniques. I know some of the basics, but please let me know if my approach to this is wrong in the first place.

Also, forgive the lack of embedded images. StackOverflow requires me to earn 10 reputation first...

The problem

I'm currently working on a project that involves a pipeline in which models are created in Blender (2.80 Beta), exported as GLTF using these export settings and then imported in three.js 0.102.1. This works fine for most models. However, when I want to export my terrain model, the materials aren't exported (or perhaps imported) as expected.

Example image of the problem

Since this is terrain, multiple textures (or better, materials) are preferred, blending them where they intersect. Of course, this could be done using one giant texture for the whole terrain, but the beauty of using multiple textures is that you simply repeat them across the terrain, and use a mask to determine the blending. You keep a high level of detail without having to create a massive singular texture.

Why I think GLTF is the bottleneck

It's not Blender:

In Blender the map looks fine and materials blend as intended:
Blending of grass and dirt

Using the following node setup:
Material node graph

It's not three.js:

I've created a similar material setup in three.js using NodeMaterials:

// MATERIAL
let mtl = new THREE.StandardNodeMaterial();
mtl.roughness = new THREE.FloatNode( .9 );
mtl.metalness = new THREE.FloatNode( 0 );

function createUv(scale, offset) {
    let uvOffset = new THREE.FloatNode( offset || 0 );
    let uvScale = new THREE.FloatNode( scale || 1 );

    let uvNode = new THREE.UVNode();
    let offsetNode = new THREE.OperatorNode(
        uvOffset,
        uvNode,
        THREE.OperatorNode.ADD
    );
    let scaleNode = new THREE.OperatorNode(
        offsetNode,
        uvScale,
        THREE.OperatorNode.MUL
    );

    return scaleNode;
}

let grass = new THREE.TextureNode( getTexture("grass"), createUv(35) );
let dirt = new THREE.TextureNode( getTexture("dirt"), createUv(35) );
let mask = new THREE.TextureNode( getTexture("mask"), createUv() );
let maskAlphaChannel = new THREE.SwitchNode(mask, "w");
let blend = new THREE.Math3Node(
    grass,
    dirt,
    maskAlphaChannel,
    THREE.Math3Node.MIX
);
mtl.color = blend;
mtl.normal = new THREE.NormalMapNode(
    new THREE.TextureNode( getTexture("dirtNormal"), createUv(35) )
);

let normalMask = new THREE.OperatorNode(
    new THREE.TextureNode( getTexture("mask"), createUv() ),
    new THREE.FloatNode(1),
    THREE.OperatorNode.MUL
);

mtl.normalScale = normalMask;

// build shader
mtl.build();

// set material
return mtl;

Which results in the following, pretty decent looking terrain (this is in my three.js project):
Properly textured terrain
Proper blending

So I guess it's GLTF?

Of course, it might be the exporter or importer failing here.

  • Importing the GLTF back into Blender gives the same result as three.js (black terrain with white highlights where dirt was supposed to go), so I doubt that's the issue.
  • I'm really in the dark when it comes to exporting, so the problem might lie here.

Additionally, when I tried to read and understand the GLTF 2.0 specification I didn't see anything in the material or texture specifications that allowed blending or mixing of any kind.
I also came across the KHR_technique_webgl extension, which could potentially resolve this issue as well, although I'm unsure on the specifics.

Conclusion

Since I would really like to avoid the hard-coded solution or using a single giant texture, GLTF would still be the way to go for me. So, am I missing anything in my pipeline? Is it possible to use GLTF in this case at this moment? Is there anything in the works that would support materials like this?

Insights of any kind would be greatly appreciated. Thank you.

Additional links:


Solution

  • Unfortunately there’s no format you can export to that will preserve a custom node graph from Blender and get it into a realtime engine, as of this writing. One texture per Principled BSDF socket is all you’ll be able to do. There is an option in the glTF exporter to include texture transforms (rotation, scale, repeat) if that helps.

    For something custom like this, I’d suggest adding names and custom properties (text or numeric) to your materials. With “Export custom properties / extras” enabled in export settings, those will be passed into threejs as material.userData, and you can reconstruct the node graph from that.