Simple shader material with lighting support (Lambertian based)

I’m aware of onBeforeCompile but I don’t find it convenient to work with due to the fact that it requires knowledge of internal library includes and working with customProgramCacheKey is also not very intuitive for me.

So, I wrote this shader as a default template, maybe it will be useful for somebody else who writes custom shaders and in need of simple illumination.

It is based on per-pixel Lambertian diffusion and provides the same results for Ambient and Directional lights and similar results for Point lights (except at a close distance).

Below is a split-canvas test for comparison, you can move split using buttons or arrow keys, or pan the camera.

Link:
https://jsfiddle.net/tfoller/r4tghjnz/9/

Screenshot:
test_lambert

Here is the shader:

<script type="x-shader/x-vertex" id="vs_lamb">

  // interpolated world positions and normals 
  varying mat2x3 vpn;

  void main() {
        
    vec4 pos = vec4(position, 1.0);
    vec4 nrm = vec4(normal, 0.0);    

    gl_Position = projectionMatrix * modelViewMatrix * pos;
    
    vpn = mat2x3(modelMatrix * mat2x4(pos, nrm));
    vpn[1] = normalize(vpn[1]);
  }
</script>
<script type="x-shader/x-vertex" id="fs_lamb">
   
  struct ambientLight {
    vec3 color;
    float intensity;
  };

  struct directLight {
    vec3 position;
    vec3 color;
    float intensity;
  };

  struct pointLight {
    vec3 position;
    vec3 color;
    float intensity;
    float power;
    float range;
    float decay;
  };

  uniform vec3 dif_color;
  uniform ambientLight amb_light;
  uniform directLight dir_light;
  uniform pointLight pnt_light;
  
  varying mat2x3 vpn;

  void main() {
   
    // ambient light

    vec3 light = amb_light.color * amb_light.intensity;
    
    // direct light

    vec3 dl_pos = dir_light.position;
    vec3 dl_dir = normalize(dl_pos);
    float dl_dot = dot(dl_dir, vpn[1]);      
    light += clamp(dl_dot, 0.0, 1.0) * dir_light.color * dir_light.intensity;

    // point light

    vec3 pl_pos = pnt_light.position;
    vec3 pl_dist = pl_pos - vpn[0];
    float range = length(pl_dist);
    if(pnt_light.range == 0.0 || pnt_light.range >= range) {

      vec3 pl_dir = normalize(pl_dist); 
      float pl_dot = dot(pl_dir, vpn[1]);
      float decay = 1.0;
      if(pnt_light.decay > 0.0) 
        decay = pnt_light.power * pow(range, -pnt_light.decay);    
      light += clamp(pl_dot, 0.0, 1.0) * pnt_light.color * pnt_light.intensity * decay;
    }

    gl_FragColor = vec4(light * dif_color, 1.0);
  }
</script>

And the most relevant code - setup for the shader mesh:

const intensity = {amb: 0.2, dir: 0.8, pnt: 1.5};

const amb_light = new THREE.AmbientLight(0xfa0000, intensity.amb);
const dir_light = new THREE.DirectionalLight(0x008f00, intensity.dir);

const pl = { power: 1, decay: 0, };
const pnt_light = new THREE.PointLight(0xffffff, intensity.pnt, 1000, pl.decay);

const amb_uni = {
  color: Object.values(amb_light.color),
  get intensity() { return amb_light.intensity },
};

const dir_uni = {
  position: Object.values(dir_light.position), 
  color: Object.values(dir_light.color),
  get intensity() { return dir_light.intensity },
};

const pnt_uni = {
  position: Object.values(pnt_light.position), 
  color: Object.values(pnt_light.color),
  range: pnt_light.distance,
  get intensity() { return pnt_light.intensity },
  get power() { return pl.power },
  get decay() { return pl.decay },
};

const cube2 = new THREE.Mesh(
  new THREE.BoxGeometry(1, 1, 1),
  new THREE.ShaderMaterial( {
  uniforms: {
    dif_color: { value: Object.values(cube.material.color) },
    amb_light: { value: amb_uni },
    dir_light: { value: dir_uni },
    pnt_light: { get value() { return pnt_uni } },
  },
  vertexShader: getShader('vs_lamb'),
  fragmentShader: getShader('fs_lamb')
}),
);
5 Likes

Cool, very nice of you to share, thanks :slight_smile:

While I’m on it, added a couple lines of code for specular reflections.

https://jsfiddle.net/tfoller/89f1zw0r/5/

1 Like