TIL 121 | Three.js 기본 - Shadows 2

meow·2025년 4월 5일
0

Interactive

목록 보기
11/11
post-thumbnail

SpotLight

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

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)

🧁 Baking Shadows란?

그림자를 실시간으로 계산하지 않고, 미리 텍스처로 구워서 사용하는 기법이다.
즉, 조명과 그림자 결과를 렌더링된 이미지(또는 텍스처)로 저장해두고, 실행 중에는 이미지처럼 보여주는 방식이다.


🎯 왜 Baking을 사용할까?

실시간 그림자 (Shadow Map)그림자 베이킹 (Baked Shadow)
매 프레임마다 그림자 계산미리 계산된 그림자 사용
GPU 연산 부하 큼성능이 매우 좋음
동적 씬에 적합정적인 씬에 적합

🧠 작동 방식

  1. 조명과 오브젝트가 고정된 씬을 구성한다.
  2. 해당 씬을 조명 포함하여 미리 렌더링한다.
  3. 결과를 텍스처(Lightmap 또는 Shadow Texture)에 저장한다.
  4. 런타임에는 그림자 계산 없이 해당 텍스처만 표시한다.

🔨 Three.js에서의 베이킹 적용 방법

✅ 방법 1: 외부에서 베이크한 텍스처 사용

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)

📸 언제 Baking을 써야 할까?

베이킹이 유리한 경우베이킹이 적합하지 않은 경우
고정된 오브젝트와 조명오브젝트나 빛이 자주 움직일 때
모바일 등 저사양 환경실시간 날씨나 시간 변화가 필요한 씬
성능 최적화가 필요할 때동적 그림자가 필수일 때

📌 LightMap 활용 팁

MeshStandardMaterial이나 MeshPhongMaterial은 lightMap을 지원하며, 강도를 조절할 수 있다.

material.lightMap = bakedLightMapTexture
material.lightMapIntensity = 1.0

☁️ Fake Shadow 기법

이 예제는 실시간 그림자 계산 없이, 그림자처럼 보이는 텍스처를 바닥에 깔아 사용하는 가짜 그림자 기법이다.
렌더링 성능을 높이면서도 자연스러운 시각 효과를 낼 수 있어 많이 활용된다.


🧾 텍스처 불러오기

const simpleShadow = textureLoader.load('/textures/simpleShadow.jpg')
simpleShadow.colorSpace = THREE.SRGBColorSpace

• simpleShadow.jpg는 중앙이 검고 가장자리가 투명한 그림자 텍스처다.
• 대부분의 이미지가 sRGB 색상 공간에 있으므로, colorSpace를 명시해야 색 왜곡을 방지할 수 있다.

📐 그림자 Plane 생성

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()
profile
🌙`、、`ヽ`ヽ`、、ヽヽ、`、ヽ`ヽ`ヽヽ` ヽ`、`ヽ`、ヽ``、ヽ`ヽ`、ヽヽ`ヽ、ヽ `ヽ、ヽヽ`ヽ`、``ヽ`ヽ、ヽ、ヽ`ヽ`ヽ 、ヽ`ヽ`ヽ、ヽ、ヽ`ヽ`ヽ 、ヽ、ヽ、ヽ``、ヽ`、ヽヽ 🚶‍♀ ヽ``ヽ``、ヽ`、

0개의 댓글