castShadows = true
코드가 존재한다면, 광원은 Camera 의 frustums(절두체) 를 관리한다.
showdow 는 기본적으로 Light 에 내장된 shadow Camera에 의해 계산된다.
이 shadow Camera는 광원이 바라보는 영역을 정의한다.
참고로 이 shadow Camera 가 너무 넓은 영역을 비추고 있다면 모든 역영을 계산하기 때문에 메모리를 굉장히 많이 잡아먹을 수 있다. 심지어 아무 Mesh 가 없는 곳이라도.
Shadow Camera 는 장면을 렌더링하여 깊이 맵(Depth Map)을 생성한다.
이 Depth map 은 광원에서 각 픽셀까지의 거리를 저장한 텍스처이다.
이때 이 픽셀의 깊이 값이 Shadow Map에 저장된 값보다 크다면, 해당 픽셀은 그림자 내부로 간주된다.
Shadow Map은 GPU 메모리에 저장되며, 해상도(shadow.mapSize)가 메모리 사용량에 큰 영향을 미친다.
directionalLightFolder.add(data, 'shadowMapSizeWidth', [256, 512, 1024, 2048, 4096]).onChange(() => updateDirectionalLightShadowMapSize())
directionalLightFolder.add(data, 'shadowMapSizeHeight', [256, 512, 1024, 2048, 4096]).onChange(() => updateDirectionalLightShadowMapSize())
function updateDirectionalLightShadowMapSize() {
directionalLight.shadow.mapSize.width = data.shadowMapSizeWidth
directionalLight.shadow.mapSize.height = data.shadowMapSizeHeight
directionalLight.shadow.map = null
}
그리고 Shadow 를 사용하려면 단드시 rederer 에 shadowMap 속성을 넣어줘야한다.
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFShadowMap // (default)
그리고 Shadow 는 최종랜더링 때 GPU 에서 병렬로 처리된다.
그림자는 총 4가지 Shadow Map 이 존재한다.
BasicShadowMap
필터링되지 않은 섀도맵을 제공
가장 빠르다
그림자 색이 1개이다
품질이 가장 낮다.
PCFShadowMap(기본값)
Pecentage-Closer Filtering(PCF) 알고리즘을 사용하여 섀도 맵을 필터링
PCFSoftShadowMap
Percentage-Closer Soft Shadows(PCSS) 알고리즘을 사용하여 섀도 맵을 필터
VSMShadowMap
Variance Shadow Map(VSM) 알고리즘을 사용하여 섀도 맵을 필터링
VSMShadowMap 을 사용하면 모든 그림자 receivers 도 그림자를 cast 한다.
또한 그림자 드리우기, 그림자 만들기 모두 true 로 만들어줘야한다.
물론 이는 ground(e.g., plane), object(e.g., box) 와 같이 각각 그림자를 만드는지 혹은 그림자가 드리우는지 확인하여 작성하면 된다.
box.castShadow = true
ground.receiveShadow = true
Mesh 의 castShadow, receiveShadow 가 모두 true 로 설정되어있으면 이는 위 사진 처럼 다른 Mesh 표면에 artefacts 나 검은색 선이 생길 수 있다.
이는 shadow bias 속성값을 통해 조절이 가능한데 이는 기본값은 0 이고 0.1, -0.0001, 0.002 과 같은 매우 작은 값을 설정하여 조절하면 된다.
참고로 위 사진에서 볼 수 있듯, MeshBasic 과 MeshNormal 은 receiveShadow=true
를 해도 다른 Mesh 로 부터 그림자 영향을 받지 않는다.
( 광원이 아래서 위를 비추는데 바닥을 통과하여 그림자가 생성되는 모습 )
만약 gournd 를 planeGeometry 를 사용하여 만들고 여기에 castShadow 를 꺼버리면 더 이상 그림자를 만들지 않기 때문에 그냥 통화를 시켜버린다.
Shadow Camera 는 어떠한 변경이 있을 때 반드시 위 함수를 실행해줘야 계산되어 반영된다.
PointLight 와 SpotLight 는 PerspectiveCamera frustum 를 사용하여 각각의 그림자를 계산한다.
속성 | 기본값 |
---|---|
fov | 90 |
aspect | 1 |
near | 0.5 |
far | 500 |
DirectionalLight 는 OrthographicCamera frustum 를 사용하여 그림자를 그린다. 얘는 직교방식을 사용하는 이유는 DirectionalLight 의 광원은 평항으로 비추기 때문이다.
속성 | 기본값 |
---|---|
left | -5 |
right | 5 |
top | 5 |
bottom | -5 |
near | 0.5 |
far | 500 |
그리고 각 광원은 cashShadow 속성을 true 로 만들어줘야한다.
directionalLight.castShadow = true
pointLight.castShadow = true
spotLight.castShadow = true
const directionalLight = new THREE.DirectionalLight(data.lightColor, Math.PI)
directionalLight.position.set(1, 1, 1)
directionalLight.castShadow = true
directionalLight.shadow.camera.near = 0
directionalLight.shadow.camera.far = 10
directionalLight.shadow.mapSize.width = data.shadowMapSizeWidth
directionalLight.shadow.mapSize.height = data.shadowMapSizeHeight
scene.add(directionalLight)
이 광원의 .cashShadow
는 위 light 에 따른 광원의 기본값
에서 적은 거 처럼 internal camera frustum 을 만들어 그림자가 어떻게 생성될 지 계산한다.
showdow 를 얼마나 부드럽게 할 것인가
옵션 1.PCF(PCFShadowMap)
옵션 2.VSM(VSMShadowMap)
VSMShadowMap only 거의 사용되지 않음
앞서 언급했듯 perspective 카메라 원리로 계산한다. 그래서 광원의 위치에 따라 그림자가 다르게 생성된다.
위 사진들을 보면 헬퍼의 모양이 조금 다르다는 것을 알 수 있다. 이는 light helper 가 아닌 camera helper 를 사용해서 그런데, 파란색 화살표가 top 을 뜻하고 near, far plane 을 표현하고있다. 그리고 정사각형인 이유는 light 에 따른 광원의 기본값
에서 설명했듯 directional 은 OrthographicCamera 를 기반으로 frusum 을 만들기 때문이다.
위 사진은 directional Light 에서 Orthographic 이기 때문에 모든 그림자의 길이가 동일한 것을 알 수 있다.
import './style.css'
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import Stats from 'three/addons/libs/stats.module.js'
import { GUI } from 'three/addons/libs/lil-gui.module.min.js'
const scene = new THREE.Scene()
scene.add(new THREE.GridHelper())
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100)
camera.position.set(-1, 4, 2.5)
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.shadowMap.type = THREE.PCFShadowMap // (default)
// renderer.shadowMap.type = THREE.PCFSoftShadowMap
//renderer.shadowMap.type = THREE.BasicShadowMap
// renderer.shadowMap.type = THREE.VSMShadowMap
renderer.shadowMap.enabled = true
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
})
const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
const plane = new THREE.Mesh(new THREE.PlaneGeometry(100, 100), new THREE.MeshStandardMaterial({ color: 0xffffff }))
plane.rotation.x = -Math.PI / 2
plane.receiveShadow = true
plane.castShadow = true
scene.add(plane)
const data = {
color: 0x00ff00,
lightColor: 0xffffff,
shadowMapSizeWidth: 512,
shadowMapSizeHeight: 512,
}
const geometry = new THREE.IcosahedronGeometry(1, 1)
const meshes = [
new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({ color: data.color })),
new THREE.Mesh(geometry, new THREE.MeshNormalMaterial({ flatShading: true })),
new THREE.Mesh(geometry, new THREE.MeshPhongMaterial({ color: data.color, flatShading: true })),
new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({ color: data.color, flatShading: true })),
]
meshes[0].position.set(-3, 1, 0)
meshes[1].position.set(-1, 1, 0)
meshes[2].position.set(1, 1, 0)
meshes[3].position.set(3, 1, 0)
meshes.map((m) => {
m.castShadow = true
m.receiveShadow = true
})
scene.add(...meshes)
const gui = new GUI()
// #region DirectionalLight
const directionalLight = new THREE.DirectionalLight(data.lightColor, Math.PI)
directionalLight.position.set(1, 1, 1)
directionalLight.castShadow = true
directionalLight.shadow.camera.near = 0
directionalLight.shadow.camera.far = 10
directionalLight.shadow.mapSize.width = data.shadowMapSizeWidth
directionalLight.shadow.mapSize.height = data.shadowMapSizeHeight
scene.add(directionalLight)
// const directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight)
const directionalLightHelper = new THREE.CameraHelper(directionalLight.shadow.camera)
directionalLightHelper.visible = false
scene.add(directionalLightHelper)
const directionalLightFolder = gui.addFolder('DirectionalLight')
directionalLightFolder.add(directionalLight, 'visible')
directionalLightFolder.addColor(data, 'lightColor').onChange(() => {
directionalLight.color.set(data.lightColor)
})
directionalLightFolder.add(directionalLight, 'intensity', 0, Math.PI * 10)
directionalLightFolder.add(directionalLight.position, 'x', -5, 5, 0.001).onChange(() => {
directionalLightHelper.update()
})
directionalLightFolder.add(directionalLight.position, 'y', -5, 5, 0.001).onChange(() => {
directionalLightHelper.update()
})
directionalLightFolder.add(directionalLight.position, 'z', -5, 5, 0.001).onChange(() => {
directionalLightHelper.update()
})
directionalLightFolder.add(directionalLightHelper, 'visible').name('Helper Visible')
directionalLightFolder.add(directionalLight.shadow.camera, 'left', -10, -1, 0.1).onChange(() => {
directionalLight.shadow.camera.updateProjectionMatrix()
directionalLightHelper.update()
})
directionalLightFolder.add(directionalLight.shadow.camera, 'right', 1, 10, 0.1).onChange(() => {
directionalLight.shadow.camera.updateProjectionMatrix()
directionalLightHelper.update()
})
directionalLightFolder.add(directionalLight.shadow.camera, 'top', 1, 10, 0.1).onChange(() => {
directionalLight.shadow.camera.updateProjectionMatrix()
directionalLightHelper.update()
})
directionalLightFolder.add(directionalLight.shadow.camera, 'bottom', -10, -1, 0.1).onChange(() => {
directionalLight.shadow.camera.updateProjectionMatrix()
directionalLightHelper.update()
})
directionalLightFolder.add(directionalLight.shadow.camera, 'near', 0, 100).onChange(() => {
directionalLight.shadow.camera.updateProjectionMatrix()
directionalLightHelper.update()
})
directionalLightFolder.add(directionalLight.shadow.camera, 'far', 0.1, 100).onChange(() => {
directionalLight.shadow.camera.updateProjectionMatrix()
directionalLightHelper.update()
})
directionalLightFolder.add(data, 'shadowMapSizeWidth', [256, 512, 1024, 2048, 4096]).onChange(() => updateDirectionalLightShadowMapSize())
directionalLightFolder.add(data, 'shadowMapSizeHeight', [256, 512, 1024, 2048, 4096]).onChange(() => updateDirectionalLightShadowMapSize())
directionalLightFolder.add(directionalLight.shadow, 'radius', 1, 10, 1).name('radius (PCF | VSM)') // PCFShadowMap or VSMShadowMap
directionalLightFolder.add(directionalLight.shadow, 'blurSamples', 1, 20, 1).name('blurSamples (VSM)') // VSMShadowMap only
directionalLightFolder.open()
function updateDirectionalLightShadowMapSize() {
directionalLight.shadow.mapSize.width = data.shadowMapSizeWidth
directionalLight.shadow.mapSize.height = data.shadowMapSizeHeight
directionalLight.shadow.map = null
}
// Pointlight
const pointLight = new THREE.PointLight(data.lightColor, Math.PI)
pointLight.position.set(2, 1, 0)
pointLight.visible = false
pointLight.castShadow = true
scene.add(pointLight)
const pointLightHelper = new THREE.PointLightHelper(pointLight)
pointLightHelper.visible = false
scene.add(pointLightHelper)
const pointLightFolder = gui.addFolder('Pointlight')
pointLightFolder.add(pointLight, 'visible')
pointLightFolder.addColor(data, 'lightColor').onChange(() => {
pointLight.color.set(data.lightColor)
})
pointLightFolder.add(pointLight, 'intensity', 0, Math.PI * 10)
pointLightFolder.add(pointLight.position, 'x', -10, 10)
pointLightFolder.add(pointLight.position, 'y', -10, 10)
pointLightFolder.add(pointLight.position, 'z', -10, 10)
pointLightFolder.add(pointLight, 'distance', 0.01, 20)
pointLightFolder.add(pointLight, 'decay', 0, 10)
pointLightFolder.add(pointLightHelper, 'visible').name('Helper Visible')
pointLightFolder.add(pointLight.shadow.camera, 'near', 0.01, 100).onChange(() => {
pointLight.shadow.camera.updateProjectionMatrix()
pointLightHelper.update()
})
pointLightFolder.add(pointLight.shadow.camera, 'far', 0.1, 100).onChange(() => {
pointLight.shadow.camera.updateProjectionMatrix()
pointLightHelper.update()
})
pointLightFolder.add(data, 'shadowMapSizeWidth', [256, 512, 1024, 2048, 4096]).onChange(() => updatePointLightShadowMapSize())
pointLightFolder.add(data, 'shadowMapSizeHeight', [256, 512, 1024, 2048, 4096]).onChange(() => updatePointLightShadowMapSize())
pointLightFolder.add(pointLight.shadow, 'radius', 1, 10, 1).name('radius (PCF | VSM)') // PCFShadowMap or VSMShadowMap
pointLightFolder.add(pointLight.shadow, 'blurSamples', 1, 20, 1).name('blurSamples (VSM)') // VSMShadowMap only
pointLightFolder.close()
function updatePointLightShadowMapSize() {
pointLight.shadow.mapSize.width = data.shadowMapSizeWidth
pointLight.shadow.mapSize.height = data.shadowMapSizeHeight
pointLight.shadow.map = null
}
// Spotlight
const spotLight = new THREE.SpotLight(data.lightColor, Math.PI)
spotLight.position.set(3, 2.5, 1)
spotLight.visible = false
//spotLight.target.position.set(5, 0, -5)
spotLight.castShadow = true
scene.add(spotLight)
//const spotLightHelper = new THREE.SpotLightHelper(spotLight)
const spotLightHelper = new THREE.CameraHelper(spotLight.shadow.camera)
spotLightHelper.visible = false
scene.add(spotLightHelper)
const spotLightFolder = gui.addFolder('Spotlight')
spotLightFolder.add(spotLight, 'visible')
spotLightFolder.addColor(data, 'lightColor').onChange(() => {
spotLight.color.set(data.lightColor)
})
spotLightFolder.add(spotLight, 'intensity', 0, Math.PI * 10)
spotLightFolder.add(spotLight.position, 'x', -10, 10).onChange(() => {
spotLight.shadow.camera.updateProjectionMatrix()
spotLightHelper.update()
})
spotLightFolder.add(spotLight.position, 'y', -10, 10).onChange(() => {
spotLight.shadow.camera.updateProjectionMatrix()
spotLightHelper.update()
})
spotLightFolder.add(spotLight.position, 'z', -10, 10).onChange(() => {
spotLight.shadow.camera.updateProjectionMatrix()
spotLightHelper.update()
})
spotLightFolder.add(spotLight, 'distance', 0.01, 100).onChange(() => {
spotLight.shadow.camera.updateProjectionMatrix()
spotLightHelper.update()
})
spotLightFolder.add(spotLight, 'decay', 0, 10).onChange(() => {
spotLight.shadow.camera.updateProjectionMatrix()
spotLightHelper.update()
})
spotLightFolder.add(spotLight, 'angle', 0, 1).onChange(() => {
spotLight.shadow.camera.updateProjectionMatrix()
spotLightHelper.update()
})
spotLightFolder.add(spotLight, 'penumbra', 0, 1, 0.001).onChange(() => {
spotLight.shadow.camera.updateProjectionMatrix()
spotLightHelper.update()
})
spotLightFolder.add(spotLightHelper, 'visible').name('Helper Visible')
spotLightFolder.add(spotLight.shadow.camera, 'near', 0.01, 100).onChange(() => {
spotLight.shadow.camera.updateProjectionMatrix()
spotLightHelper.update()
})
spotLightFolder.add(data, 'shadowMapSizeWidth', [256, 512, 1024, 2048, 4096]).onChange(() => updateSpotLightShadowMapSize())
spotLightFolder.add(data, 'shadowMapSizeHeight', [256, 512, 1024, 2048, 4096]).onChange(() => updateSpotLightShadowMapSize())
spotLightFolder.add(spotLight.shadow, 'radius', 1, 10, 1).name('radius (PCF | VSM)') // PCFShadowMap or VSMShadowMap
spotLightFolder.add(spotLight.shadow, 'blurSamples', 1, 20, 1).name('blurSamples (VSM)') // VSMShadowMap only
spotLightFolder.close()
function updateSpotLightShadowMapSize() {
spotLight.shadow.mapSize.width = data.shadowMapSizeWidth
spotLight.shadow.mapSize.height = data.shadowMapSizeHeight
spotLight.shadow.map = null
}
const stats = new Stats()
document.body.appendChild(stats.dom)
const labels = document.querySelectorAll<HTMLDivElement>('.label')
let x, y
const v = new THREE.Vector3()
function animate() {
requestAnimationFrame(animate)
controls.update()
for (let i = 0; i < 4; i++) {
v.copy(meshes[i].position)
v.project(camera)
x = ((1 + v.x) / 2) * innerWidth - 50
y = ((1 - v.y) / 2) * innerHeight
labels[i].style.left = x + 'px'
labels[i].style.top = y + 'px'
}
renderer.render(scene, camera)
stats.update()
}
animate()