mrdoob / three.js

JavaScript 3D Library.
https://threejs.org/
MIT License
101.69k stars 35.3k forks source link

WebGPU - Duplicate shadow when logarithmicDepthBuffer is true - Shadow appears below and above object (WebGPU build 167) #29200

Open PoseidonEnergy opened 3 weeks ago

PoseidonEnergy commented 3 weeks ago

Description

Note: I'm using the WebGPU build 167 of three.js for this report.

See screenshot, JSFiddle, and provided code. I have a simple sphere between two "floors", with a DirectionalLight pointing straight down from above. Notice how the sphere casts a shadow on the top "floor". This top shadow appears when logarithmicDepthBuffer is true. How can I remove the shadow from the top floor? I don't want to disable logarithmicDepthBuffer, because I need it elsewhere in my scene. I also don't want to disable the top floor's ability to receive shadows from other objects, so I can't set receiveShadow = false on the top floor mesh.

JSFiddle: https://jsfiddle.net/0fwkyvu6

discourse thread: https://discourse.threejs.org/t/duplicate-shadow-shadow-appears-below-and-above-object-webgpu-build-167/69481

StackOverflow question: https://stackoverflow.com/questions/78893677/three-js-duplicate-shadow-issue-bug-shadow-appears-below-and-above-object

Update 1: setting logarithmicDepthBuffer: false appears to fix the issue somewhat, however the logarithmic depth buffer is a requirement for my scene, so I'd like to get the shadow working with it.

Update 2: the top shadow is not visible in the standard WebGLRenderer build of three.js, so there must be some issue specifically with the WebGPU build.

image

Reproduction steps

Please use the provided code to reproduce the issue.

Code

<!doctype html>
<html>
<head>
<title>WebGPU 167 Shadow Bug<title>
<style>
* {
  margin: 0;
  border: 0;
  padding: 0;
}

html {
  height: 100%;
  overflow: hidden;
}

body {
  height: 100%;
  overflow: hidden;
  font-family: sans-serif;
  background-color:#808080;
}
</style>
</head>
<body>
<script>
(async function() {
  const THREE = await (async function() {
    const url = "https://unpkg.com/three@0.167.1/build/three.webgpu.min.js";
    const code = await (await fetch(url)).text();
    return import("data:text/javascript;base64," + btoa(code));
  })();
  const renderer = new THREE.WebGPURenderer({
    antialias: false,
    logarithmicDepthBuffer: true,
    powerPreference: "high-performance"
  });
  if (renderer.backend.isWebGPUBackend !== true) {
    const div = document.createElement("div");
    div.style.cssText = "position:absolute;inset:0;z-index:10;background-color:#ffffff;padding:20px;text-align:center;";
    div.innerHTML = "Alert: This demo must be run with WebGPU. Please use a browser that supports WebGPU.";
    document.body.appendChild(div);
    return;
  }
  renderer.shadowMap.enabled = true;
  renderer.shadowMap.type = THREE.BasicShadowMap;
  renderer.outputColorSpace = THREE.SRGBColorSpace;
  document.body.appendChild(renderer.domElement);
  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
  camera.position.set(0, 8, 40);
  camera.rotation.set(-0.37, 0, 0);
  const light = new THREE.DirectionalLight(0xffffff, Math.PI);
  light.position.set(0, 15, 0);
  light.target.position.set(0, 0, 0);
  const shadowCam = light.shadow.camera;
  light.castShadow = true;
  light.shadow.mapSize.width = 8192;
  light.shadow.mapSize.height = 8192;
  shadowCam.near = 5;
  shadowCam.far = 40;
  const d = 16;
  shadowCam.left = -d;
  shadowCam.right = d;
  shadowCam.top = d;
  shadowCam.bottom = -d;
  shadowCam.updateProjectionMatrix();
  const box1Geometry = new THREE.BoxGeometry(40, 1, 20, 1, 1, 1);
  const box2Geometry = new THREE.BoxGeometry(40, 1, 20, 1, 1, 1);
  const sphereGeometry = new THREE.SphereGeometry(6, 32, 32);
  const box1Mesh = new THREE.Mesh(box1Geometry, new THREE.MeshLambertMaterial({
    color: new THREE.Color(0xffffff)
  }));
  const box2Mesh = new THREE.Mesh(box1Geometry, new THREE.MeshLambertMaterial({
    color: new THREE.Color(0x3ff57b)
  }));
  const sphereMesh = new THREE.Mesh(sphereGeometry, new THREE.MeshLambertMaterial({
    color: new THREE.Color(0x3e9df4)
  }));
  sphereMesh.position.set(0, -10, 0);
  sphereMesh.castShadow = true;
  box2Mesh.position.set(0, -20, 0);
  box1Mesh.receiveShadow = true;
  box2Mesh.receiveShadow = true;
  scene.add(light);
  scene.add(light.target);
  scene.add(box1Mesh);
  scene.add(box2Mesh);
  scene.add(sphereMesh);
  scene.add(new THREE.CameraHelper(shadowCam));
  await renderer.renderAsync(scene, camera);
  setInterval(function() {
    camera.aspect = document.body.offsetWidth / document.body.offsetHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(document.body.offsetWidth, document.body.offsetHeight);
    renderer.render(scene, camera);
  }, 100);
})();
</script>
</body>
</html>

Version

167

Device

Desktop

Browser

Chrome, Edge

OS

Windows

PoseidonEnergy commented 3 weeks ago

I've been playing around with the AnalyticLightNode.setupShadow() function in three.webgpu.js (version 168dev) in the hope that I'll stumble upon a fix, but I've had no luck. The code in the AnalyticLightNode class looks interesting, and may be where the bug is, but I'm not sure...the fix may lie elsewhere.

Link to version 168: https://unpkg.com/browse/three@0.168.0/build/three.webgpu.js

if ( renderer.coordinateSystem === WebGPUCoordinateSystem ) {
    coordZ = coordZ.mul( 2 ).sub( 1 ); // WebGPU: Convertion [ 0, 1 ] to [ - 1, 1 ]
}