Trying to add lighting to ShaderMaterial (Creating lightweight Lambert Material)

Im attempting to add lights to my basic shader material. The expected behaviour is that i have a simple cube that i apply the ShaderMaterial to and it results in the same lights and colors as a Lambert Cube right next to it. However. Ive tried many different approaches. Ive tried passing all the lights into the material as uniforms and calculating the light and color data from those like so:

vec3 calcGlobalLight(vec3 baseColor) {
        float normalDotLight = dot(abs(normalize(_DirToLight)), vec3(0.0, 1.0, 0.0));
        float lightIntensity = 1.0 - (1.0 - normalDotLight);
        vec3 lightCol = ((_Moon + _Sun) * lightIntensity);
        vec3 diffuse = lightCol * (RECIPROCAL_PI * baseColor);
        diffuse += _Ambient * baseColor;
        return diffuse;

Here i have 3 lights that are added: Moon, Sun and Ambient. The moon and sun are simple directional lights with intensity and color. The ambient is just an ambient light. The normal is just up because this material will be used on flat surfaces only like an ocea.
While this somewhat works. It looks vastly different from the lambert material next to it.

So i dug through the three js docs and forums and found that you can include lights by setting lights: true on your material. I did this and came up with this code instead (fragment shader portion):

uniform vec3 diffuse;
uniform float opacity;
uniform vec3 ambientLightColor;
uniform sampler2D map;
struct DirectionalLight {
    vec3 direction;
    vec3 color;
uniform DirectionalLight directionalLights[NUM_DIR_LIGHTS];
varying vec3 vViewPosition;
varying vec3 vNormal;
vec3 getDirectionalIndirectLightIrradiance(const in DirectionalLight directionalLight, const in vec3 normal) {
    vec3 irradiance = vec3(0.0);
    vec3 lightDirection = directionalLight.direction;
    float dotNL = max(dot(normal, lightDirection), 0.0);
    irradiance += dotNL * directionalLight.color;
    return irradiance;
void getDirectionalDirectLightIrradiance(const in DirectionalLight directionalLight, const in vec3 normal, out vec3 directLight) {
    directLight = max(dot(normal, directionalLight.direction), 0.0) * directionalLight.color;
void main() {
    vec4 diffuseColor = vec4(1.0, 0.0, 0.0, 1.0);

    // Include map fragment functionality directly
    #ifdef USE_MAP
        vec4 texelColor = texture2D(map, gl_FragCoord.xy);
        texelColor = mapTexelToLinear(texelColor);
        diffuseColor *= texelColor;
    struct ReflectedLight {
        vec3 directDiffuse;
        vec3 indirectDiffuse;
    ReflectedLight reflectedLight;
    reflectedLight.directDiffuse = vec3(0.0);
    reflectedLight.indirectDiffuse = ambientLightColor;
    vec3 normal = normalize(vNormal);
    for (int i = 0; i < NUM_DIR_LIGHTS; i++) {
        reflectedLight.indirectDiffuse += diffuse * getDirectionalIndirectLightIrradiance(directionalLights[i], normal);
    for (int i = 0; i < NUM_DIR_LIGHTS; i++) {
        vec3 directLight;
        getDirectionalDirectLightIrradiance(directionalLights[i], normal, directLight);
        reflectedLight.directDiffuse += directLight * diffuse;
    #if defined RE_IndirectDiffuse
        vec3 irradiance = ambientLightColor;
        reflectedLight.indirectDiffuse += diffuse * irradiance;
    vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse;
    gl_FragColor = vec4(outgoingLight, diffuseColor.a);
    #include <tonemapping_fragment>
    #include <colorspace_fragment>

The result still differs (right = lambert, left = shader material)

I tried to go through the entire flow of the Lambert material to see what im missing. All i want to do is make a super basic version of lambert that doesnt include all the bloat and things like normal maps, specular and all this and only does the lighting calculations the same way as a lambert for color and map data.

What about MeshPhong did you try that, it is considered to be the fastest/ lightest in three.js renderer since you don’t want extra stuff

[Edit] Forgot to mention lightest among materials that offer lightning effects obv basic is lighter

The idea is to use as lightweight a material as possible. Phong isnt exactly lightweight. And the issue is that i would like to use the lighting in a ShaderMaterial to do additional calcluations inside the shader.

MeshBasic doesnt work with directional lights as far as i know

yeah they dont work with that

Phong is worse for performance and Basic doesnt use lights. So those are no help to my original question im afraid. Not sure if you understood the question. Ill reword it

MeshLambertMaterial with onBeforeCompile. No?

1 Like

Yes but then we have all the bloat from a lambert material. And going into it and removing all of the lambert materials includes is a ton of work. So i figured it should be easy to just use the actual lighting on a Shader Material. The point is to avoid using lambert since all we need is the lighting calculations for map and color and not the rest of lambert.

This site is super useful for understanding/modding the builtin material shaders:

Just unfold all and copy/paste to your hearts content!

1 Like

Thats where i got the previous code snippet i tested from. And i looked through the entire main function and took out all the pieces to do with directional lights. But the end result is still different.

If i try to add things like:

#include <lights_lambert_fragment>	LambertMaterial material;

To a ShaderMaterial obviously there will be errors. The question is what am i missing from a lambert that the light results are so different. I figured adding the lights: true flag should simply apply lighting to the ShaderMaterial by default.

Might just be tonemapping at the end? Could be so many things…

Tonemapping is disabled so that include doesnt do anything. And the colorspace one is directly from the lambert material. I do wonder is there some kind of example of a shader material that works with lights at all?

I think the closest thing to an example is the actual shader :slight_smile:

Because AFAIK the ShaderMaterial does the same setup and plugging in of uniforms from the etc. that is used by the built in materials.

It’s only RawShaderMaterial that gives you Nothing and has it all from scratch.

Ive setup this fiddle to show whats going on. The lights dont behave at all now. Ive taken all of the relevant parts of the Lambert material to no evail.

Why does it bother you that much?
Most of the shader code is wrapped with ifdef, and if there is no specific define, the wrapped part won’t be used. :thinking:

1 Like

This material gets added to around 50k objects in the scene so id like to keep it as lightweight as possible. I imagine thats the reason they added the lights: true option to ShaderMaterial to begin with

50k of the same objects?

Yes im planning to use this material on an Mesh that uses an InstancedBufferGeometry with a lot of elements.

I just need the material to be as lightweight as possible and going in and overriding the lambert material with beforeOnCompile still leaves a ton of bloat and checks that arent needed.