Please help, self-shadows not working on shaderMaterial

Hi,
I’ve been playing with a shader material and added some perlin noise to it.

The scene is simple:

  • a sphere
  • a light source
  • a plane with a custom vertex/fragment shader code

After a lot of researching I’ve managed to get the shadow casted by the sphere working on the ShaderMaterial by adding the required includes in the vertex/shader code.

Everything went well apart from “self-shadows”. I just cannot get the “waves” on the plane to have their own shadows.

Following different topics and forums I went ahead and added a custom depth material to the plane mesh but even that doesn’t seem to do the trick.

I have a codepen here with the entire thing.

What am I doing wrong?

Any help appreciated.
Thank you!

Full code here:

import * as THREE from "https://cdn.skypack.dev/three@0.156.1";
import OrbitControls from "https://cdn.skypack.dev/threejs-orbit-controls";

const canvas = document.getElementById("c");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

// webgl Renderer ------------------
const webglRenderer = new THREE.WebGLRenderer({
  canvas,
  antialias: true,
  alpha: true
});

webglRenderer.outputColorSpace = THREE.SRGBColorSpace;
webglRenderer.toneMapping = THREE.ACESFilmicToneMapping;
webglRenderer.toneMappingExposure = 2.3;
webglRenderer.shadowMap.enabled = true;
webglRenderer.shadowMap.type = THREE.PCFSoftShadowMap;

// Scene ----------------------------
const scene = new THREE.Scene();

// Camera ---------------------------
const perspectiveCamera = new THREE.PerspectiveCamera(
  35,
  canvas.width / canvas.height,
  0.1,
  100
);
perspectiveCamera.position.x = -10;
perspectiveCamera.position.y = -10;
perspectiveCamera.position.z = 20;
perspectiveCamera.lookAt(scene.position);
scene.add(perspectiveCamera);

// OrbitControls ---------------------
const orbitControls = new OrbitControls(perspectiveCamera, canvas);
orbitControls.enableDamping = true;
orbitControls.enableZoom = true;
orbitControls.enablePan = true;
orbitControls.maxPolarAngle = Math.PI;
orbitControls.target.set(0, 0, 0);

// Lights ----------------------------
const directionalLight = new THREE.DirectionalLight(0xffffff, 3, 100, 1);
directionalLight.position.set(3, 1, 2);
directionalLight.castShadow = true;
directionalLight.shadow.camera.near = 0.01;
directionalLight.shadow.camera.far = 30;
directionalLight.shadow.camera.top = 15;
directionalLight.shadow.camera.bottom = -15;
directionalLight.shadow.camera.left = -15;
directionalLight.shadow.camera.right = 15;
directionalLight.shadow.mapSize.set(2048, 2048);
directionalLight.shadow.normalBias = 0.05;
scene.add(directionalLight);

const helper = new THREE.DirectionalLightHelper(directionalLight, 5, 0xff0000);
scene.add(helper);

// Sphere -----------------------------
const sphereGeometry = new THREE.SphereGeometry(1, 32, 32);
const sphereMaterial = new THREE.MeshLambertMaterial({
  color: 0xca6234
});

const sphereMesh = new THREE.Mesh(sphereGeometry, sphereMaterial, 2);
sphereMesh.position.set(0, 0, 2);
sphereMesh.castShadow = true;
sphereMesh.receiveShadow = true;
scene.add(sphereMesh);

// Plane ------------------------------
const planeGeometry = new THREE.PlaneGeometry(15, 15, 800, 800);
const vertex_shader = `
#include <common>
#include <shadowmap_pars_vertex>
varying vec3 currentPosition;
uniform float time;

vec4 mod289(vec4 x)
{
  return x - floor(x * (1.0 / 289.0)) * 289.0;
}

vec4 permute(vec4 x)
{
  return mod289(((x*34.0)+10.0)*x);
}

vec4 taylorInvSqrt(vec4 r)
{
  return 1.79284291400159 - 0.85373472095314 * r;
}

vec2 fade(vec2 t) {
  return t*t*t*(t*(t*6.0-15.0)+10.0);
}

// Classic Perlin noise
float cnoise(vec2 P)
{
  vec4 Pi = floor(P.xyxy) + vec4(0.0, 0.0, 1.0, 1.0);
  vec4 Pf = fract(P.xyxy) - vec4(0.0, 0.0, 1.0, 1.0);
  Pi = mod289(Pi); // To avoid truncation effects in permutation
  vec4 ix = Pi.xzxz;
  vec4 iy = Pi.yyww;
  vec4 fx = Pf.xzxz;
  vec4 fy = Pf.yyww;

  vec4 i = permute(permute(ix) + iy);

  vec4 gx = fract(i * (1.0 / 41.0)) * 2.0 - 1.0 ;
  vec4 gy = abs(gx) - 0.5 ;
  vec4 tx = floor(gx + 0.5);
  gx = gx - tx;

  vec2 g00 = vec2(gx.x,gy.x);
  vec2 g10 = vec2(gx.y,gy.y);
  vec2 g01 = vec2(gx.z,gy.z);
  vec2 g11 = vec2(gx.w,gy.w);

  vec4 norm = taylorInvSqrt(vec4(dot(g00, g00), dot(g01, g01), dot(g10, g10), dot(g11, g11)));
  g00 *= norm.x;  
  g01 *= norm.y;  
  g10 *= norm.z;  
  g11 *= norm.w;  

  float n00 = dot(g00, vec2(fx.x, fy.x));
  float n10 = dot(g10, vec2(fx.y, fy.y));
  float n01 = dot(g01, vec2(fx.z, fy.z));
  float n11 = dot(g11, vec2(fx.w, fy.w));

  vec2 fade_xy = fade(Pf.xy);
  vec2 n_x = mix(vec2(n00, n01), vec2(n10, n11), fade_xy.x);
  float n_xy = mix(n_x.x, n_x.y, fade_xy.y);

  return 2.3 * n_xy;
}

void main() {
  #include <beginnormal_vertex>
  #include <defaultnormal_vertex>

  #include <begin_vertex>

  #include <worldpos_vertex>
  #include <shadowmap_vertex>

  vec3 pos = position;
  pos.z += cnoise(pos.xy * 0.4 + time * 0.1) * 1.5;
  pos.z += cnoise(pos.xy * 0.4 - time * 0.1) * 0.5;

  currentPosition = pos;

  gl_Position += projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`;

const fragment_shader = `
#include <common>
#include <packing>
#include <lights_pars_begin>
#include <shadowmap_pars_fragment>
#include <shadowmask_pars_fragment>
varying vec3 currentPosition;

void main() {
  vec3 shadowColor = vec3(0, 0, 0);
  float shadowPower = 0.5;
  vec3 color = vec3(0.0, 0.51, 0.91);
  color.r = clamp(currentPosition.z * 1.5, 0.0, 1.0);
  color.g = smoothstep(0.0, 1.0, currentPosition.z * 1.5);
  
  gl_FragColor = vec4( mix( color, shadowColor, (1.0 - getShadowMask() ) * shadowPower), 1.0);
}
`;
const planeMaterial = new THREE.ShaderMaterial({
  vertexShader: vertex_shader,
  fragmentShader: fragment_shader,
  lights: true,
  uniforms: THREE.UniformsUtils.merge([
    THREE.UniformsLib.lights,
    {
      time: { value: 0 }
    }
  ])
});

// Custom depth material for the plane mesh -----
const vertex_shader_depth_beforeVoidMain = `
varying vec2 vUv;
varying vec3 currentPosition;
uniform float time;

vec4 mod289(vec4 x)
{
  return x - floor(x * (1.0 / 289.0)) * 289.0;
}

vec4 permute(vec4 x)
{
  return mod289(((x*34.0)+10.0)*x);
}

vec4 taylorInvSqrt(vec4 r)
{
  return 1.79284291400159 - 0.85373472095314 * r;
}

vec2 fade(vec2 t) {
  return t*t*t*(t*(t*6.0-15.0)+10.0);
}

// Classic Perlin noise
float cnoise(vec2 P)
{
  vec4 Pi = floor(P.xyxy) + vec4(0.0, 0.0, 1.0, 1.0);
  vec4 Pf = fract(P.xyxy) - vec4(0.0, 0.0, 1.0, 1.0);
  Pi = mod289(Pi); // To avoid truncation effects in permutation
  vec4 ix = Pi.xzxz;
  vec4 iy = Pi.yyww;
  vec4 fx = Pf.xzxz;
  vec4 fy = Pf.yyww;

  vec4 i = permute(permute(ix) + iy);

  vec4 gx = fract(i * (1.0 / 41.0)) * 2.0 - 1.0 ;
  vec4 gy = abs(gx) - 0.5 ;
  vec4 tx = floor(gx + 0.5);
  gx = gx - tx;

  vec2 g00 = vec2(gx.x,gy.x);
  vec2 g10 = vec2(gx.y,gy.y);
  vec2 g01 = vec2(gx.z,gy.z);
  vec2 g11 = vec2(gx.w,gy.w);

  vec4 norm = taylorInvSqrt(vec4(dot(g00, g00), dot(g01, g01), dot(g10, g10), dot(g11, g11)));
  g00 *= norm.x;  
  g01 *= norm.y;  
  g10 *= norm.z;  
  g11 *= norm.w;  

  float n00 = dot(g00, vec2(fx.x, fy.x));
  float n10 = dot(g10, vec2(fx.y, fy.y));
  float n01 = dot(g01, vec2(fx.z, fy.z));
  float n11 = dot(g11, vec2(fx.w, fy.w));

  vec2 fade_xy = fade(Pf.xy);
  vec2 n_x = mix(vec2(n00, n01), vec2(n10, n11), fade_xy.x);
  float n_xy = mix(n_x.x, n_x.y, fade_xy.y);

  return 2.3 * n_xy;
}
`;

const vertex_shader_depth_innerVoidMain = `
  vec3 pos = position;
  pos.z += cnoise(pos.xy * 0.4 + time * 0.1) * 1.5;
  pos.z += cnoise(pos.xy * 0.4 - time * 0.1) * 0.5;

  currentPosition = pos;

  gl_Position += projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
`;
const depthMaterial = new THREE.MeshDepthMaterial({
  side: THREE.DoubleSide,
  depthTest: true,
  depthWrite: true
});

depthMaterial.onBeforeCompile = onBeforeCompile;

// Extend the MeshDepthMaterial and insert the same vertex transformations
// to match the plane vertex shader
function onBeforeCompile(shader) {
  shader.vertexShader = shader.vertexShader.replace(
    `void main()`,
    `${vertex_shader_depth_beforeVoidMain}\n\nvoid main()`
  );
  shader.vertexShader = shader.vertexShader.replace(
    `#include <uv_vertex>`,
    `${vertex_shader_depth_innerVoidMain}\n#include <uv_vertex>`
  );
  shader.uniforms.time = { value: 0.0 };
  depthMaterial.userData.shader = shader;
}

const planeMesh = new THREE.Mesh(planeGeometry, planeMaterial);
planeMesh.position.set(0, 1, 0);
planeMesh.castShadow = true;
planeMesh.receiveShadow = true;
planeMesh.customDepthMaterial = depthMaterial;
scene.add(planeMesh);

function updateUniforms(t) {
  planeMesh.material.uniforms.time.value = t;
  if (depthMaterial.userData.shader) {
    depthMaterial.userData.shader.uniforms.time.value = t;
  }
}

function render() {
  webglRenderer.render(scene, perspectiveCamera);
}

function update(t) {
  updateUniforms(t * 0.001);
  render();
  orbitControls.update();
  requestAnimationFrame(update);
}
update();