Three.js에서 SpotLight (스포트라이트)를 설정하고, 그림자(shadow)를 생성하며, Helper로 시각화까지 하는 예제
// Spotlight
const spotLight = new THREE.SpotLight(0xff0000, 3.6, 10, Math.PI * 0.2)
spotLight.castShadow = true
spotLight.position.set(0, 2, 2)
spotLight.shadow.mapSize.width = 1024
spotLight.shadow.mapSize.height = 1024
spotLight.shadow.camera.near = 1
spotLight.shadow.camera.far = 6
scene.add(spotLight)
scene.add(spotLight.target)
const spotLightHelper = new THREE.SpotLightHelper(spotLight)
scene.add(spotLightHelper)
PointLight의 Shadow Map은 일반적인 평면 그림자와는 조금 달라서, 원리를 이해하는 게 중요해.
⸻
💡 PointLight란?
• PointLight는 말 그대로 한 점에서 모든 방향으로 빛을 내뿜는 광원이야.
• 전구처럼 360도 방향으로 빛이 퍼짐.
const pointLight = new THREE.PointLight(0xffffff, 1, 100)
pointLight.castShadow = true
⸻
🌑 PointLight의 Shadow Map은 특별함!
👉 큐브 맵(Cube Map) 기반 그림자
일반적인 DirectionalLight나 SpotLight는 그림자 맵을 1장만 쓰지만,
PointLight는 모든 방향을 커버해야 하므로, 6개의 그림자 맵을 만들어야 해.
• +X, -X, +Y, -Y, +Z, -Z 방향으로 총 6장의 Shadow Map을 씀.
• 이걸 Cube Camera처럼 처리해서 그림자를 구함.
⸻
🧠 요약: 내부 처리 방식
1. PointLight의 위치에서 6개의 방향으로 Shadow Map을 만든다.
2. 각 방향에 대해 Scene을 depth-only로 렌더링한다.
3. 이후 픽셀 위치가 빛에 보이는지 6면에서 비교해서 그림자 적용한다.
➡️ 그래서 성능 부담이 크다!
// Point light
const pointLight = new THREE.PointLight(0x00ff00, 3, 10)
pointLight.castShadow = true
pointLight.position.set(-1, 1, 0)
pointLight.shadow.mapSize.width = 1024
pointLight.shadow.mapSize.height = 1024
pointLight.shadow.camera.near = 1
pointLight.shadow.camera.far = 6
scene.add(pointLight)
scene.add(pointLight.target)
const pointLightHelper = new THREE.PointLightHelper(pointLight)
// pointLightHelper.visible = false
scene.add(pointLightHelper)
그림자를 실시간으로 계산하지 않고, 미리 텍스처로 구워서 사용하는 기법이다.
즉, 조명과 그림자 결과를 렌더링된 이미지(또는 텍스처)로 저장해두고, 실행 중에는 이미지처럼 보여주는 방식이다.
실시간 그림자 (Shadow Map) | 그림자 베이킹 (Baked Shadow) |
---|---|
매 프레임마다 그림자 계산 | 미리 계산된 그림자 사용 |
GPU 연산 부하 큼 | 성능이 매우 좋음 |
동적 씬에 적합 | 정적인 씬에 적합 |
Blender, Substance Painter 등에서 light map을 만든 뒤 Three.js에 적용한다.
const texture = new THREE.TextureLoader().load('bakedShadow.png')
const material = new THREE.MeshBasicMaterial({ map: texture })
✅ 방법 2: 바닥에 투명한 Shadow Texture 사용
const shadowTexture = new THREE.TextureLoader().load('simpleShadow.png')
const shadowMaterial = new THREE.MeshBasicMaterial({
map: shadowTexture,
transparent: true
})
const shadowPlane = new THREE.Mesh(
new THREE.PlaneGeometry(2, 2),
shadowMaterial
)
shadowPlane.rotation.x = -Math.PI / 2
shadowPlane.position.y = 0.01 // 바닥과 겹침 방지
scene.add(shadowPlane)
⸻
베이킹이 유리한 경우 | 베이킹이 적합하지 않은 경우 |
---|---|
고정된 오브젝트와 조명 | 오브젝트나 빛이 자주 움직일 때 |
모바일 등 저사양 환경 | 실시간 날씨나 시간 변화가 필요한 씬 |
성능 최적화가 필요할 때 | 동적 그림자가 필수일 때 |
⸻
MeshStandardMaterial이나 MeshPhongMaterial은 lightMap을 지원하며, 강도를 조절할 수 있다.
material.lightMap = bakedLightMapTexture
material.lightMapIntensity = 1.0
이 예제는 실시간 그림자 계산 없이, 그림자처럼 보이는 텍스처를 바닥에 깔아 사용하는 가짜 그림자 기법이다.
렌더링 성능을 높이면서도 자연스러운 시각 효과를 낼 수 있어 많이 활용된다.
const simpleShadow = textureLoader.load('/textures/simpleShadow.jpg')
simpleShadow.colorSpace = THREE.SRGBColorSpace
• simpleShadow.jpg는 중앙이 검고 가장자리가 투명한 그림자 텍스처다.
• 대부분의 이미지가 sRGB 색상 공간에 있으므로, colorSpace를 명시해야 색 왜곡을 방지할 수 있다.
⸻
const sphereShadow = new THREE.Mesh(
new THREE.PlaneGeometry(1.5, 1.5),
new THREE.MeshBasicMaterial({
color: 0x000000,
transparent: true,
alphaMap: simpleShadow,
})
)
• Plane은 그림자의 범위를 담당하며 바닥에 위치한다.
• MeshBasicMaterial은 조명 영향을 받지 않기 때문에 그림자 형태를 정확히 표현할 수 있다.
• alphaMap은 투명도를 정의하는 텍스처로, 그림자 모양을 나타낸다.
⸻
sphereShadow.rotation.x = -Math.PI * 0.5
sphereShadow.position.y = plane.position.y + 0.01
scene.add(sphereShadow)
• X축으로 -90도 회전시켜 바닥에 평평하게 놓는다.
• 바닥과 살짝 띄워서 z-fighting 현상을 방지한다.
⸻
const clock = new THREE.Clock()
const tick = () => {
const elapsedTime = clock.getElapsedTime()
// 구체 움직임
sphere.position.x = Math.cos(elapsedTime) * 1.5
sphere.position.z = Math.sin(elapsedTime) * 1.5
sphere.position.y = Math.abs(Math.sin(elapsedTime)) * 1.5
// 그림자 위치 따라가기
sphereShadow.position.x = sphere.position.x
sphereShadow.position.z = sphere.position.z
// 그림자 투명도 조절 (높이에 따라)
sphereShadow.material.opacity = (1 - sphere.position.y) * 0.3
// 컨트롤 및 렌더링
controls.update()
renderer.render(scene, camera)
window.requestAnimationFrame(tick)
}
tick()
• 구체는 원형 경로를 따라 움직이며 위아래로 튄다.
• 그림자 Plane은 구체의 x, z 위치를 따라가며 바닥에서 고정된다.
• 구체가 높이 올라갈수록 그림자가 연해지며, 내려올수록 진해진다.
⸻
✅ 요약
alphaMap: 그림자 형태를 정의하는 투명도 텍스처
opacity: 구체 높이에 따라 실시간으로 조절
MeshBasicMaterial: 조명 영향을 받지 않으며 성능이 좋다
활용 목적: 성능 최적화, 간단한 가짜 그림자 표현
⸻
📌 언제 사용하는가?
• 모바일이나 저사양 환경에서 실시간 그림자 대신 사용할 수 있다.
• 구체나 캐릭터가 바닥 가까이에서 움직일 때 자연스러운 효과를 줄 수 있다.
• 리얼타임 섀도우의 성능 부담이 클 경우 적절한 대안이 된다.
const simpleShadow = textureLoader.load('/textures/simpleShadow.jpg')
simpleShadow.colorSpace = THREE.SRGBColorSpace
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(sphereShadow)
/**
* 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)) * 1.5
// Update the sphere shadow
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()