Three.js - Shadows
Schatten einschalten
// Decide for every object if it casts and/or receives shadows
sphere.castShadow = true
plane.receiveShadow = true
// Decide for every light if it casts shadows
directionalLight.castShadow = true
// Enable shadowmap in renderer
renderer.shadowMap.enabled = true
Shadowmap optimieren
Jedes Licht nutzt eine Shadowmap mit einer Default Tilegröße von 512x512px. Über die shadowmap-Property kann man die Größe beeinflussen. Wegen dem Mipmapping solltest du eine Zweierpotenz nutzen (siehe Mipmapping). Vorsicht - Schatten kosten viel Performance.
directionalLight.shadow.mapSize.x = 1024
directionalLight.shadow.mapSize.y = 1024
Schattenkamera einstellen.
Lichter benutzen ein eigenes Camera Objekt (OrthoCamera) um die Schatten zu berechnen.
CameraHelper
Für genaue Tweaks erstellen wie einen CameraHelper und übergeben diesem die Schattenkamera.
// Shadow camera helper
const directionalLightCameraHelper = new THREE.CameraHelper(directionalLight.shadow.camera)
scene.add(directionalLightCameraHelper)
Renderbereich einstellen
Mit den near, far, left, right, top, bottom Werten lassen sich die Grenzen der Kamera und damit auch für die Schattenerzeugung einstellen. Der Camerahelper hilft dabei. near und far helfen nicht die Qualität der Schatten zu verbessern. Falsche Werte können aber für verbuggte Schatten verantwortlich sein.
Schmale Kameraausschnitte verbessern das Ergebnis, da die Mipmaps effektiver verwendet werden. Sind sie zu schmal können aber Schatten abgeschnitten werden.
directionalLight.shadow.camera.near = 1
directionalLight.shadow.camera.far = 6
directionalLight.shadow.camera.left = -2
directionalLight.shadow.camera.right = 2
directionalLight.shadow.camera.top = 2
directionalLight.shadow.camera.bottom = -2
Renderer - Shadow map algorithm
Es gibt verschiedene Algorythmen für die Berechnung der ShadowMap:
THREE.BasicShadowMap Very performant but lousy quality THREE.PCFShadowMap Less performant but smoother edges - DEFAULT THREE.PCFSoftShadowMap Less performant but even softer edges THREE.VSMShadowMap Less performant, more constraints, can have unexpected results
renderer.shadowMap.type = THREE.PCFSoftShadowMap
Komplettes Beispiel
//...
/**
* Lights
*/
// Ambient light
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
gui.add(ambientLight, 'intensity').min(0).max(1).step(0.001)
scene.add(ambientLight)
// Directional light
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5)
directionalLight.position.set(2, 2, - 1)
gui.add(directionalLight, 'intensity').min(0).max(1).step(0.001)
gui.add(directionalLight.position, 'x').min(- 5).max(5).step(0.001)
gui.add(directionalLight.position, 'y').min(- 5).max(5).step(0.001)
gui.add(directionalLight.position, 'z').min(- 5).max(5).step(0.001)
// Add Shadow and mapsize
directionalLight.castShadow = true
directionalLight.shadow.mapSize.x = 1024
directionalLight.shadow.mapSize.y = 1024
// Shadow camera settings...
directionalLight.shadow.camera.near = 1
directionalLight.shadow.camera.far = 6
// ...important for quality
directionalLight.shadow.camera.left = -2
directionalLight.shadow.camera.right = 2
directionalLight.shadow.camera.top = 2
directionalLight.shadow.camera.bottom = -2
// adding a bit of a cheap blur
// directionalLight.shadow.radius = 4
scene.add(directionalLight)
// Shadow camera helper
const directionalLightCameraHelper = new THREE.CameraHelper(directionalLight.shadow.camera)
scene.add(directionalLightCameraHelper)
// ...
/**
* Objects
*/
sphere.castShadow = true
// ...
plane.receiveShadow = true
// ...
scene.add(sphere, plane)
/**
* Renderer
*/
const renderer = new THREE.WebGLRenderer({
canvas: canvas
})
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
// Renderer Shadowmap settings
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // default THREE.PCFShadowMap
Spotlight Schatten
Die Spotlightkamera nutzt - passend zum Kegelförmigen Licht - eine PerspectiveCamera zum Berechnen der Shadowmap. Daher gibt es hier keine left, right, top, bottom Werte sondern eine Field of view Einstellung:
spotLight.shadow.camera.fov = 30
spotLight.shadow.camera.near = 1
spotLight.shadow.camera.far = 5
Pointlight Schatten
Auch hier wird eine PerspectiveCamera genutzt. Aber da das PointLight in jede Richtung geht muss Three.je eine Cube ShadowMap berechnen - also 6 Renderings für jede Seite des Cubs. Daher ist aus Performance Sicht ein PointLight die schlechteste Variante.
pointLight.shadow.mapSize.width = 1024
pointLight.shadow.mapSize.height = 1024
pointLight.shadow.camera.near = 0.1
pointLight.shadow.camera.far = 5
Die einzigen Eigenschaften hier sind mapSize, near and far:
Techniken für bessere Schatten
Vor allem wenn mehrere Schatten übereinanderfallen erzeugt der Renderer keine überzeugenden Ergebnisse. Aber es gibt noch ein paar Tricks um Schatten zu erzeugen und trotzdem die Performance zu erhalten
Shadow Baking
Wie Texturen kann man auch Schatten baken. Nachteil. Bei Bewegung des Objekts bewegt sich der
/**
* Textures
*/
const textureLoader = new THREE.TextureLoader()
const bakedShadow = textureLoader.load('/textures/bakedShadow.jpg')
//...
const plane = new THREE.Mesh(
new THREE.PlaneGeometry(5, 5),
new THREE.MeshBasicMaterial({
map: bakedShadow
})
)
// we don't need the rendered shadows in this case
renderer.shadowMap.enabled = false
Dynamic Shadow Baking
Alternativ können wir einen einfachen Schatten unter dem Objekt, knapp über dem Boden platzieren und passend zum Objekt animieren. Dies ist nicht ganz so realistisch aber performant und auch bei Animationen möglich
Schattenobjekt
Die Textur ist ein einfaches Halo. Sie wird im Alphakanal genutzt, so dass der weiße Teil sichtbar und der schwarze unsichtbar ist.
Die Textur setzen wir auf eine schwarze Plane (Ebene), diese wirkt dann wie ein Schatten.
Animation
Wir bewegen den Schatten passend zum Objekt über dem Boden. Entfernt sich das Objekt verstärken wir einfach die Transparenz.
Beispiel Kugel mit animiertem Fake-Schatten
const textureLoader = new THREE.TextureLoader()
const simpleShadow = textureLoader.load('/textures/simpleShadow.jpg')
// Sphere Shadow
const sphereShadow = new THREE.Mesh(
new THREE.PlaneGeometry(1.5, 1.5),
new THREE.MeshBasicMaterial({
color: 0x000000,
transparent: true,
alphaMap: simpleShadow
})
)
sphereShadow.rotation.x = - Math.PI * 0.5
sphereShadow.position.y = plane.position.y + 0.01
scene.add(sphere, sphereShadow, plane)
//...
/**
* Animate
*/
const clock = new THREE.Clock()
const tick = () =>
{
const elapsedTime = clock.getElapsedTime()
// Update the sphere
sphere.position.x = Math.cos(elapsedTime) * 1.5
sphere.position.z = Math.sin(elapsedTime) * 1.5
sphere.position.y = Math.abs(Math.sin(elapsedTime * 3))
// Update the shadow accordingly
sphereShadow.position.x = sphere.position.x
sphereShadow.position.z = sphere.position.z
sphereShadow.material.opacity = (1 - sphere.position.y) * 0.3
// Update controls
controls.update()
// Render
renderer.render(scene, camera)
// Call tick again on the next frame
window.requestAnimationFrame(tick)
}
tick()