如何在不编辑底层网格的情况下操纵 Three.js 中的阴影?

How do I manipulate shadows in Three.js without editing the underlying mesh?

我正在开发一个应用程序,它应该允许用户在场景中操作 3D 对象并观察它们的变化如何影响地面阴影:

在此场景中,黄色圆柱体在白色平面上投射阴影,圆柱体的中间包含在绿色立方体中。我想要的是让立方体移除阴影的中间部分,如下所示:

显然,我的第一个想法是从黄色圆柱体体积中减去绿色立方体体积,经过一番谷歌搜索后我发现 CSG.js。不幸的是,CSG.js 对于我要使用的实际模型来说太慢了,它至少有 15k 个顶点。

我开始深入研究 Three.js 源代码并阅读阴影贴​​图以了解阴影是如何产生的,但我的着色器功能还不够强大,无法完全掌握如何调整阴影渲染。

如何实现这种"shadow subtraction"效果?

var camera, scene, renderer;

init();
animate();

function init() {
  scene = new THREE.Scene();

  camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 1, 1000 );
  camera.position.z = 500;
  camera.position.y = 100;
  camera.lookAt(scene.position);

  var ambient = new THREE.AmbientLight(0x909090);
  scene.add(ambient);

  var directionalLight = new THREE.DirectionalLight( 0xffffff, 1.0 );
  directionalLight.position.set( -300, 300, 0 );
  directionalLight.castShadow = true;
  directionalLight.shadow.camera.near    =   10;
  directionalLight.shadow.camera.far     =   2000;
  directionalLight.shadow.camera.right   =   350;
  directionalLight.shadow.camera.left    =  -350;
  directionalLight.shadow.camera.top     =   350;
  directionalLight.shadow.camera.bottom  =  -350;
  directionalLight.shadow.mapSize.width  = 1024;
  directionalLight.shadow.mapSize.height = 1024;
  scene.add( directionalLight );

  //var lightHelper = new THREE.CameraHelper(directionalLight.shadow.camera);
  //scene.add(lightHelper);

  var geometry = new THREE.CylinderGeometry( 50, 50, 400, 32 );
  var material = new THREE.MeshPhongMaterial( {color: 0xffff00} );
  var cylinder = new THREE.Mesh( geometry, material );
  cylinder.castShadow = true;
  scene.add( cylinder );

  var geometry = new THREE.BoxGeometry( 110, 110, 110 );
  var material = new THREE.MeshPhongMaterial( {color: 0x00ff00} );
  var cube = new THREE.Mesh( geometry, material );
  cube.castShadow = true;
  scene.add( cube );

  var geometry = new THREE.PlaneGeometry( 3000, 3000, 32 );
  var material = new THREE.MeshPhongMaterial( {color: 0xffffff, side: THREE.DoubleSide} );
  var plane = new THREE.Mesh( geometry, material );
  plane.lookAt(new THREE.Vector3(0, 1, 0));
  plane.position.y = -200;
  plane.receiveShadow = true;
  scene.add( plane );

  renderer = new THREE.WebGLRenderer();
  renderer.setPixelRatio( window.devicePixelRatio );
  renderer.setSize( window.innerWidth, window.innerHeight );
  renderer.shadowMap.enabled = true;
  renderer.shadowMap.type = THREE.BasicShadowMap;
  document.body.appendChild( renderer.domElement );

  window.addEventListener( 'resize', onWindowResize, false );
}

function onWindowResize() {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();

  renderer.setSize( window.innerWidth, window.innerHeight );
}

function animate() {
  requestAnimationFrame( animate );

  renderer.render( scene, camera );
}

jsFiddle

更新:

更复杂的场景呢?红色圆柱体的阴影是否可能不受影响(你可以看到它被切成两半cube.customDepthMaterial = new THREE.MeshBasicMaterial({ depthTest: false}))?

Updated jsFiddle

您可以通过设置对象的 .customDepthMaterial 属性 来从场景的其余部分减去对象的阴影:

var cube = new THREE.Mesh( geometry, material );
cube.castShadow = true;
cube.receiveShadow = false;

// The secret sauce
cube.customDepthMaterial =
  new THREE.MeshBasicMaterial({ depthTest: false});

scene.add( cube );

jsFiddle

不需要 shader-fu。

为什么这样行得通
渲染阴影贴图时,每个对象的深度 material(.customDepthMaterial 或默认值)为 used to render the scene from the light's perspective。深度 material 的结果渲染表示对象从相机打包为 RGBA 的深度。由于 THREE.MeshBasicMaterial 默认为 { color: 0xffffff, opacity: 1 },它将 return 使物体比阴影相机的 far.

更远的最大深度

我禁用了 depthTest 因为在你想要的结果截图中你剪掉了立方体给定圆柱体不存在的区域。禁用 depthTest 意味着被圆柱体阻挡的立方体部分仍会切出阴影,从而获得您想要的结果。

文档
不幸的是,还没有关于 .customDepthMaterial 的文档,但我确实找到了 an official example 的使用位置。

更新答案:
允许对象的阴影始终显示:

  1. 您可以使用与上述相同的技巧,只需将 material 的 coloropacity 设置为 0
  2. 确保将其添加到场景中 'subtractive shadow' 对象之后。这样即使它们都禁用了 depthTest,附加阴影也会胜出。

updated jsFiddle

如果您有更复杂的事情,将由您想出一种方法来管理阴影渲染的顺序。

在 r77 中测试