Three.js - Shadows

Aus Wikizone
Wechseln zu: Navigation, Suche
ThreeJS - Snippets

Schatten einschalten[Bearbeiten]

// 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[Bearbeiten]

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.[Bearbeiten]

Lichter benutzen ein eigenes Camera Objekt (OrthoCamera) um die Schatten zu berechnen.

CameraHelper[Bearbeiten]

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[Bearbeiten]

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[Bearbeiten]

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[Bearbeiten]

//...
/**
 * 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[Bearbeiten]

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[Bearbeiten]

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[Bearbeiten]

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[Bearbeiten]

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[Bearbeiten]

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.

Fehler beim Erstellen des Vorschaubildes: Datei fehlt

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()