Why isn't MeshBasicMaterial the base class for MeshLambertMaterial?

I was wondering why in three.js, MeshLambertMaterial isn’t a subclass of MeshBasicMaterial, if lambert is just the basic material plus simple lighting.

Same thing for phong – MeshPhongMaterial is just a lambert material with phong reflections.

So technically, MeshBasicMaterial should be the base class for MeshLambertMaterial, and MeshLambertMaterial should be the base class for MeshPhongMaterial, am I right? Right now, it is not so in the three.js code – all three of those material classes are “independent” from one another.

Right now those three material classes look like this in three.js:

class MeshBasicMaterial extends Material { }
class MeshLambertMaterial extends Material { }
class MeshPhongMaterial extends Material { }

But, shouldn’t it look like this?

class MeshBasicMaterial extends Material { }
class MeshLambertMaterial extends MeshBasicMaterial { }
class MeshPhongMaterial extends MeshLambertMaterial { }

This is a problem when you are trying do “proper” type checking when working with materials. For example, if you are writing a function that needs to set the color property of a material, you have to know ahead of time of all of the materials that could possibly support a color property, like this:

function setColor(material) {
    if((material instanceof MeshBasicMaterial) || (material instanceof MeshLambertMaterial) || (material instanceof MeshPhongMaterial)) {
        material.color = new Color(1, 0, 0);
        return;
    }
    throw new Error(`material type ${material} not supported`);
}

On the other hand, if those three material types were in a prototype chain, where MeshBasicMaterial were the highest-level material type that supports a color property, you could simplify your code to this:

function setColor(material) {
    if(material instanceof MeshBasicMaterial) {
        // we still make it here if the material is MeshLambertMaterial or MeshPhongMaterial
        material.color = new Color(1, 0, 0);
        return;
    }
    throw new Error(`material type ${material} not supported`);
}
function setColor(material) {
    if(material.color) {
        material.color = new Color(1, 0, 0);
        return;
    }
    throw new Error(`material type ${material} not supported`);
}
3 Likes

I knew someone here would post that exact workaround, but simply checking for the existence of a property is not type checking, and would not be acceptable in a professional library.

You framed “proper” in quotes. I interpreted this as pseudo proper.

BTW even if you do what you propose, what about MeshStandardMaterial? You still have to explicitly check for it.

1 Like

In a high-performance 3D engine, ‘professional’ design patterns often look different than they do in standard enterprise JS. Three.js has evolved over a decade with a clear priority: Performance > Minimal Bloat > Maintainability. > While both materials share shader chunks and inherit from the base Material class, their shading models are fundamentally different.

Forcing an inheritance relationship between them would create ‘code smells’ where the child class is forced to override or ignore almost everything the parent does.

If you prefer a more traditional logical abstraction, libraries like R3F provide that layer, but the core engine stays flat and low-level for the sake of the GPU.

2 Likes

Like everything in graphics… it’s complicated.

The real question is why not a ShaderMaterial

You wouldn’t really get much by extending Phong from Lambert in JavaScript. They are all just some flags (like is it transparent) and some uniforms (like a color or texture).

The flags are mostly not shader related, they tell the renderer what to do with WebGL before running the shader. Thus makes sense that they live on the super class. Some materials won’t have color, so i think you’d quickly run into the diamond problem.

So this is mostly a convention, that the particular material instance will have some conveniently named properties, some other won’t. Also legacy, there are many internal checks in the renderer, that do specific stuff based on the type of material.

At the end of the day all of the materials get broken down into the vertex/fragment shader pair and uniforms. Which is no more and no less than what the ShaderMaterial is.

Except, the actual shaders, the strings themselves do not exist on something like MeshBasicMaterial, it’s very indirect (shader chunks) and the renderer ends up making a ShaderMaterial essentially but which you can’t see.

So there is a “template” which is written in augmented GLSL, each material has one. Inheritance doesn’t work here, composition does, so many of these templates overlap. Yes, lambert and phong will have everything that basic does. Phong may not have everything that lambert does because if I’m not mistaken, historically lambert has been done in the vertex shader in three. The common ancestor would be some “material with normals”.

1 Like