Cascaded Shadow Maps (CSM)
Cascaded Shadow Maps are a concept where higher resolution shadows are used near the camera and lower resolution further away.
The default CSM object creates 4 frustums with 4 directional lights with shadows and different shadow map sizes.
It is a good solution for when you want to cast shadows across a very large area with many objects near and far.
Code
./src/server/server.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | import express from "express" import path from "path" import http from "http" const port: number = 3000 class App { private server: http.Server private port: number constructor(port: number) { this.port = port const app = express() app.use(express.static(path.join(__dirname, '../client'))) app.use('/build/three.module.js', express.static(path.join(__dirname, '../../node_modules/three/build/three.module.js'))) app.use('/jsm/libs/dat.gui.module', express.static(path.join(__dirname, '../../node_modules/three/examples/jsm/libs/dat.gui.module.js'))) app.use('/jsm/controls/OrbitControls', express.static(path.join(__dirname, '../../node_modules/three/examples/jsm/controls/OrbitControls.js'))) app.use('/jsm/loaders/MTLLoader', express.static(path.join(__dirname, '../../node_modules/three/examples/jsm/loaders/MTLLoader.js'))) app.use('/jsm/loaders/OBJLoader', express.static(path.join(__dirname, '../../node_modules/three/examples/jsm/loaders/OBJLoader.js'))) app.use('/jsm/libs/stats.module', express.static(path.join(__dirname, '../../node_modules/three/examples/jsm/libs/stats.module.js'))) app.use('/jsm/csm/CSM', express.static(path.join(__dirname, '../../node_modules/three/examples/jsm/csm/CSM.js'))) app.use('/jsm/csm/CSMHelper', express.static(path.join(__dirname, '../../node_modules/three/examples/jsm/csm/CSMHelper.js'))) app.use('/jsm/', express.static(path.join(__dirname, '../../node_modules/three/examples/jsm/'))) this.server = new http.Server(app); } public Start() { this.server.listen(this.port, () => { console.log(`Server listening on port ${this.port}.`) }) } } new App(port).Start() |
./src/client/client.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | import * as THREE from '/build/three.module.js' import { OrbitControls } from '/jsm/controls/OrbitControls' import { OBJLoader } from '/jsm/loaders/OBJLoader' import { MTLLoader } from '/jsm/loaders/MTLLoader' import Stats from '/jsm/libs/stats.module' import { GUI } from '/jsm/libs/dat.gui.module' import { CSM } from '/jsm/csm/CSM' import { CSMHelper } from '/jsm/csm/CSMHelper' const scene: THREE.Scene = new THREE.Scene() scene.background = new THREE.Color(0x87CEEB) const camera: THREE.PerspectiveCamera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000) camera.position.x = 4 camera.position.y = 1 camera.position.z = 7 const csm = new CSM({ maxFar: 1000, fade: true, far: camera.far, cascades: 4, shadowMapSize: 8192, lightDirection: new THREE.Vector3(-1, -1, 0), camera: camera, parent: scene, lightIntensity: 0.5 }); console.log(csm) const csmHelper = new CSMHelper(csm) csmHelper.displayFrustum = true csmHelper.displayPlanes = true csmHelper.displayShadowBounds = true scene.add(csmHelper as any) const renderer: THREE.WebGLRenderer = new THREE.WebGLRenderer({ antialias: true }) renderer.setSize(window.innerWidth, window.innerHeight) renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; document.body.appendChild(renderer.domElement) const controls = new OrbitControls(camera, renderer.domElement) controls.dampingFactor = 0.05 controls.enableDamping = true const trees = ["birchTreeWithLeaves", "saplingTree", "tree1WithLeaves"] const material = new THREE.MeshPhongMaterial({ color: 0x567d46 }) const mtlLoader = new MTLLoader(); trees.forEach((tree) => { mtlLoader.load('models/' + tree + '.mtl', (materials) => { materials.preload(); const objLoader: OBJLoader = new OBJLoader(); objLoader.setMaterials(materials); objLoader.load( 'models/' + tree + '.obj', (object) => { object.traverse(function (child) { if ((<THREE.Mesh>child).isMesh) { child.castShadow = true } }) for (let i = 0; i < 20; i++) { const copy = object.clone() copy.scale.multiplyScalar(Math.max(Math.random() / 50, .01)) copy.rotateY(Math.random() * Math.PI * 2) copy.position.set((Math.random() * 20) - 10, 0, (Math.random() * 20) - 10) scene.add(copy); } }, (xhr) => { console.log((xhr.loaded / xhr.total * 100) + '% loaded'); }, (error) => { console.log('An error happened'); } ); }, (xhr) => { console.log((xhr.loaded / xhr.total * 100) + '% loaded'); }, (error) => { console.log('An error happened'); } ) }) const planeGeometry: THREE.PlaneGeometry = new THREE.PlaneGeometry(25, 25) const planeMesh: THREE.Mesh = new THREE.Mesh(planeGeometry, material) planeMesh.rotateX(-Math.PI / 2) planeMesh.receiveShadow = true; scene.add(planeMesh) window.addEventListener('resize', onWindowResize, false) function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight camera.updateProjectionMatrix() renderer.setSize(window.innerWidth, window.innerHeight) render() } const gui = new GUI() const csmFolder = gui.addFolder("CSM") csmFolder.add(csm.lightDirection, "x", -1, 1, 0.01) csmFolder.add(csm.lightDirection, "y", -1, 1, 0.01) csmFolder.add(csm.lightDirection, "z", -1, 1, 0.01) csmFolder.add(csm, "lightNear", 1, 1000, 1).onChange(function (value) { for (var i = 0; i < csm.lights.length; i++) { csm.lights[i].shadow.camera.near = value; csm.lights[i].shadow.camera.updateProjectionMatrix(); } }) csmFolder.add(csm, "lightFar", 1, 1000, 1).onChange(function (value) { for (var i = 0; i < csm.lights.length; i++) { csm.lights[i].shadow.camera.far = value; csm.lights[i].shadow.camera.updateProjectionMatrix(); } }) csmFolder.add(csm, "lightIntensity", .1, 2, .1).onChange(function (value) { for (var i = 0; i < csm.lights.length; i++) { csm.lights[i].intensity = value } }) csmFolder.open() const stats = Stats() document.body.appendChild(stats.dom) var animate = function () { requestAnimationFrame(animate) controls.update() csm.update(); csmHelper.update(); render() stats.update() }; function render() { renderer.render(scene, camera) } animate(); |