티스토리 뷰

반응형

three.js OrbitControls 조명 그림자 교량 모델 교량 BIM 바이브코딩 3편 대표 이미지

2편에서는 박스 하나를 띄우면서 three.js의 기본 구조(장면·카메라·렌더러)를 익혔다. 그런데 화면을 마우스로 돌려볼 수 없으니, 교량처럼 큰 구조물은 한쪽 면밖에 볼 수 없었다.

3편에서는 OrbitControls로 마우스 회전·확대·이동을 붙이고, 조명 5가지와 그림자를 다뤄서 1편의 교량 모델을 구조해석 프로그램처럼 자유롭게 돌려볼 수 있게 만든다.

 

이번 편에서 만들 것
 

▲ 실제로 동작하는 데모. 마우스 왼쪽 드래그 회전, 휠 확대·축소, 오른쪽 드래그 이동. 아래 버튼으로 조명과 그림자를 켜고 끌 수 있다.

 

○ 버튼을 눌러 조명을 하나씩 켜고 꺼보면, 조명마다 교량이 어떻게 달라 보이는지 바로 확인할 수 있다.

○ 그림자를 끄면 교량이 땅에서 떠 보이고, 켜면 높이 관계가 한눈에 들어온다.

○ 자동 회전은 OrbitControls에 들어 있는 기능으로, 발표 화면이나 모델 소개용으로 쓰기 좋다.

 

OrbitControls란

○ 카메라가 중심점(target)을 기준으로 공전하도록 마우스·터치 입력을 처리해 주는 three.js 부가 기능이다.

○ 구조해석 프로그램에서 모델을 돌려보는 뷰 회전, 줌, 팬 기능을 코드 몇 줄로 붙일 수 있다.

three.js OrbitControls 마우스 조작 회전 확대 이동

조작 마우스 터치(스마트폰) 하는 일
회전 (Rotate) 왼쪽 버튼 드래그 한 손가락 드래그 카메라가 중심점 주위를 공전
확대·축소 (Zoom) 휠 스크롤 두 손가락 벌리기·오므리기 중심점 쪽으로 다가가거나 멀어짐
이동 (Pan) 오른쪽 버튼 드래그 두 손가락 드래그 카메라와 중심점을 함께 평행 이동

 

○ 주의할 점 : OrbitControls는 물체를 돌리는 것이 아니라 카메라를 움직이는 것이다. 그래서 2편처럼 box.rotation.y를 바꾸는 코드와는 전혀 별개로 동작한다.

 

OrbitControls 붙이는 3단계

 

① 불러오기

○ OrbitControls는 three.js 본체가 아니라 addons 폴더에 들어 있어서 따로 불러와야 한다. importmap에 three/addons/ 경로를 추가한다.

<script type="importmap">
  {
    "imports": {
      "three": "https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.module.js",
      "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.186.1/examples/jsm/"
    }
  }
</script>

import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

 

② 만들기

○ 카메라와 렌더러의 그림판(canvas)을 넘겨주면 끝이다. 중심점은 교량 중앙 높이로 잡는다.

const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, pierH, 0);   // 교량 중앙을 중심으로 회전
controls.update();

 

③ 매 프레임 갱신

○ 관성(enableDamping)을 켰다면 애니메이션 루프 안에서 controls.update()를 반드시 호출해야 한다. 빠뜨리면 관성이 동작하지 않는다.

renderer.setAnimationLoop(() => {
  controls.update();
  renderer.render(scene, camera);
});

 

교량 모델에 쓰기 좋은 옵션
옵션 기본값 교량 예제 설정 효과
target (0, 0, 0) (0, 7, 0) 회전 중심. 교량 중앙(교각 상단 높이)으로
enableDamping false true 손을 떼도 부드럽게 멈춤 (관성)
minDistance 0 10 너무 가까이 들어가 모델 안으로 파고드는 것 방지
maxDistance 무한대 150 너무 멀어져 모델을 잃어버리는 것 방지
maxPolarAngle π (180°) π/2 − 0.05 카메라가 지면 아래로 내려가지 않게
enablePan true true false로 하면 이동 금지 (회전만)
autoRotate false 버튼으로 전환 중심점 주위를 자동 회전 (update 필요)

 

○ maxPolarAngle은 위에서부터 잰 각도다. 0이면 바로 위(평면도), π/2면 수평, π면 바로 아래다. 지면이 있는 모델에서는 π/2보다 조금 작게 두면 땅 밑으로 카메라가 들어가지 않는다.

 

조명 5가지 비교

○ three.js의 주요 조명은 5가지다. 같은 교량을 조명 하나씩만 켜서 비교하면 다음과 같다.

three.js 조명 5가지 Ambient Hemisphere Directional Point Spot 교량 비교

조명 비유 그림자 교량 모델에서 쓰임
AmbientLight 흐린 날 전체 밝기 X 어두운 부분이 새까맣게 되지 않도록 바닥값만 깔아줌
HemisphereLight 하늘빛 + 땅 반사광 X 야외 구조물에 자연스러운 바탕 조명 (Ambient보다 추천)
DirectionalLight 햇빛 (평행 광선) O 주 조명. 면마다 명암을 만들고 그림자를 드리움
PointLight 전구 O 야간 조명, 특정 부위 강조
SpotLight 손전등·무대 조명 O 점검 위치, 손상 부위 하이라이트

 

○ 구조물 모델에는 HemisphereLight(바탕) + DirectionalLight(주 조명, 그림자) 조합이 가장 무난하다. 위 데모의 기본 설정도 이 조합이다.

 

빛의 각도와 거리

 

면의 각도에 따라 밝기가 달라진다

○ DirectionalLight를 받는 면의 밝기는 빛이 면에 닿는 각도의 코사인(cos θ)에 비례한다. 빛을 정면으로 받는 면은 가장 밝고, 비스듬할수록 어두워진다.

○ 교량의 윗면, 옆면, 거더 하부가 서로 다른 밝기로 보이는 이유이고, 이 차이가 입체감을 만든다.

 

 

○ 그래서 DirectionalLight의 위치를 정면(카메라 쪽)에 두면 모든 면이 비슷하게 밝아져 오히려 납작해 보인다. 카메라와 옆으로 비껴서 위쪽에 두는 것이 좋다.

 

PointLight는 거리가 멀수록 급격히 어두워진다

○ PointLight와 SpotLight는 실제 빛처럼 거리의 제곱에 반비례해서 약해진다(decay = 2, 기본값). 거리가 2배가 되면 밝기는 1/4이 된다.

 

 

○ three.js 최신 버전은 물리적으로 정확한 단위를 쓰기 때문에, 교량처럼 수십 m 크기 모델에서는 PointLight 세기를 수백~수천 수준으로 크게 줘야 보인다. 예제에서는 new THREE.PointLight(0xffe2b0, 1200)을 썼다.

○ distance 값을 주면 그 거리에서 빛이 0이 되도록 잘라낸다. 조명이 많을 때 계산량을 줄이는 용도다.

증상 원인 해결
PointLight를 넣었는데 안 보임 세기가 너무 작음 (예: 1) 세기를 수백 이상으로, 또는 조명을 모델 가까이
조명 근처만 하얗게 타버림 세기가 너무 크거나 너무 가까움 세기를 줄이거나 위치를 멀리
예전 예제 코드가 너무 어두움 옛 버전 three.js 기준으로 쓴 세기 값 최신 버전 기준으로 세기 재조정

 

그림자 만들기 4단계

○ three.js의 그림자는 계산 비용이 커서 기본값이 꺼져 있다. 아래 4곳을 모두 켜야 그림자가 생긴다. 하나라도 빠지면 그림자가 안 나온다.

단계 코드 의미
① 렌더러 renderer.shadowMap.enabled = true 그림자 기능 전체 켜기
② 조명 sun.castShadow = true 이 빛이 그림자를 만든다
③ 물체 mesh.castShadow = true 이 물체가 그림자를 드리운다 (교량 부재)
④ 바닥 ground.receiveShadow = true 이 물체 위에 그림자가 맺힌다 (지면)

three.js 그림자 끄기 켜기 교량 비교

 

그림자 범위를 모델 크기에 맞추기

○ DirectionalLight의 그림자는 정해진 사각형 범위 안에서만 계산된다. 기본 범위는 ±5 정도라, 40 m 교량에서는 그림자가 잘려 나간다.

three.js DirectionalLight 그림자 범위 shadow camera 비교

const s = sun.shadow.camera;     // 그림자 계산 범위
s.left = -35;  s.right = 35;     // 교량 길이(40 m)보다 넉넉하게
s.top  =  35;  s.bottom = -35;
s.near = 1;    s.far = 120;

○ 범위를 너무 크게 잡으면 같은 해상도로 넓은 면적을 덮어야 해서 그림자 가장자리가 계단처럼 거칠어진다. 모델보다 약간 크게가 적당하다.

 

그림자 해상도 (mapSize)
mapSize 품질 속도 추천 용도
512 × 512 (기본) 거칠다 가장 빠름 작은 물체, 모바일
1024 × 1024 보통 빠름 일반 웹 데모
2048 × 2048 깔끔 보통 교량 한 경간 정도 (예제 설정)
4096 × 4096 매우 깔끔 느림 고해상도 캡처용

 

전체 코드

○ 메모장에 붙여넣고 bridge3.html로 저장한 뒤 더블클릭하면 실행된다. ★ 표시가 이번 편에서 추가된 부분이다.

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <title>three.js 교량 - 마우스 조작과 조명</title>
  <style>
    body { margin: 0; overflow: hidden; }
  </style>
  <script type="importmap">
    {
      "imports": {
        "three": "https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.module.js",
        "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.186.1/examples/jsm/"
      }
    }
  </script>
</head>
<body>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// 1. 장면, 카메라, 렌더러
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xdfe8f0);

const camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 0.1, 500);
camera.position.set(32, 18, 34);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
renderer.shadowMap.enabled = true;                  // ★ 그림자 켜기 (1단계)
document.body.appendChild(renderer.domElement);

// 2. 치수 (단위: m)
const span = 40, width = 10, girderH = 1.8, pierH = 7;

// 3. 재료
const concrete = new THREE.MeshStandardMaterial({ color: 0xb8bcc2, roughness: 0.9 });
const steel = new THREE.MeshStandardMaterial({ color: 0x3f6e9e, metalness: 0.3, roughness: 0.5 });
const groundMat = new THREE.MeshStandardMaterial({ color: 0x9fb39a, roughness: 1 });

function box(w, h, d, mat, x, y, z) {
  const mesh = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), mat);
  mesh.position.set(x, y, z);
  mesh.castShadow = true;                           // ★ 그림자를 드리운다 (3단계)
  mesh.receiveShadow = true;
  scene.add(mesh);
  return mesh;
}

// 4. 교량 부재
box(span, 0.25, width, concrete, 0, pierH + girderH + 0.125, 0);          // 바닥판
for (const z of [-3.5, 0, 3.5]) box(span, girderH, 0.4, steel, 0, pierH + girderH / 2, z); // 거더
for (const x of [-span / 2 + 2, span / 2 - 2]) {
  box(1.5, pierH, 1.5, concrete, x, pierH / 2, 0);                        // 교각
  box(2.0, 0.8, width - 1, concrete, x, pierH - 0.4, 0);                  // 코핑
}

// 5. 지면 (그림자를 받는 바닥)
const ground = new THREE.Mesh(new THREE.PlaneGeometry(120, 120), groundMat);
ground.rotation.x = -Math.PI / 2;                   // 눕히기 (Y-up 좌표계)
ground.receiveShadow = true;                        // ★ 그림자를 받는다 (4단계)
scene.add(ground);

// 6. 조명
const hemi = new THREE.HemisphereLight(0xffffff, 0x6b7f63, 0.8);   // 하늘색 + 땅색
scene.add(hemi);

const sun = new THREE.DirectionalLight(0xffffff, 2.2);             // 햇빛
sun.position.set(25, 35, 15);
sun.castShadow = true;                              // ★ 이 빛이 그림자를 만든다 (2단계)
sun.shadow.mapSize.set(2048, 2048);                 // 그림자 해상도
const s = sun.shadow.camera;                        // 그림자 계산 범위를 교량 크기에 맞춤
s.left = -35; s.right = 35; s.top = 35; s.bottom = -35; s.near = 1; s.far = 120;
scene.add(sun);

// 7. 마우스 조작 (OrbitControls)
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, pierH, 0);                   // 교량 중앙을 중심으로 회전
controls.enableDamping = true;                      // 관성(부드럽게 멈춤)
controls.minDistance = 10;                          // 너무 가까이 못 가게
controls.maxDistance = 150;                         // 너무 멀리 못 가게
controls.maxPolarAngle = Math.PI / 2 - 0.05;        // 땅 밑으로 못 내려가게
controls.update();

// 8. 화면 그리기
renderer.setAnimationLoop(() => {
  controls.update();                                // damping 사용 시 매 프레임 필요
  renderer.render(scene, camera);
});

addEventListener('resize', () => {
  camera.aspect = innerWidth / innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
});
</script>
</body>
</html>

 

Claude에게 이렇게 요청했다

○ 1편 코드를 붙여넣고 아래처럼 요청하면 위 코드와 비슷한 결과가 나온다.

아래 three.js 교량 코드에 다음 기능을 추가해줘.

- OrbitControls로 마우스 회전·확대·이동. 회전 중심은 교량 중앙, 관성 켜기

- 카메라가 지면 아래로 내려가지 않게 제한

- 지면(초록색)을 추가하고 DirectionalLight로 그림자 표시

- 그림자 범위는 교량 전체(40 m)를 덮도록

- 바탕 조명은 HemisphereLight

- 추가하거나 바꾼 줄에는 ★ 표시와 한글 주석

(1편 코드 붙여넣기)

 

○ "바꾼 줄에 표시해줘"라고 요청하면 어디가 달라졌는지 바로 찾을 수 있다. 기존 코드를 고치는 요청에서 특히 유용하다.

 

자주 겪는 문제
증상 원인 해결
마우스로 돌려도 안 움직임 OrbitControls 불러오기 실패 (콘솔에 import 오류) importmap에 three/addons/ 경로 확인
관성이 안 먹음, 회전이 뚝뚝 끊김 controls.update() 누락 애니메이션 루프 안에 update 추가
그림자가 아예 안 나옴 4단계 중 하나 누락 renderer·조명·물체·바닥 네 곳 확인
그림자가 중간에 잘림 그림자 범위(shadow.camera)가 좁음 left·right·top·bottom을 모델보다 크게
그림자에 줄무늬가 생김 (shadow acne) 면이 자기 자신에게 그림자를 드리움 sun.shadow.bias = -0.0005 정도로 조정
콘솔에 "PCFSoftShadowMap has been removed" 경고 인터넷의 옛 예제 코드를 그대로 씀 최신 버전에서 제거된 옵션. 그 줄을 지우면 기본 그림자(PCFShadowMap)가 쓰임
카메라가 땅 밑으로 들어감 maxPolarAngle 미설정 controls.maxPolarAngle = Math.PI / 2 - 0.05

 

정리

○ OrbitControls는 카메라를 움직이는 기능이다. target을 모델 중앙에 두고, update()를 매 프레임 호출한다.

○ 조명은 HemisphereLight + DirectionalLight 조합이 구조물 모델에 가장 무난하다.

○ 면의 밝기는 빛의 각도(cos θ)에, PointLight는 거리의 제곱에 반비례해서 달라진다.

○ 그림자는 렌더러·조명·물체·바닥 4곳을 켜고, 그림자 범위를 모델 크기에 맞춰야 제대로 나온다.

 

다음 편 예고

4편에서는 이번처럼 Claude에게 3D 모델을 요청할 때 원하는 결과를 한 번에 받는 프롬프트 작성법을 정리한다. 치수·좌표·단위를 설명하는 법, 기존 코드 수정을 요청하는 법, 결과가 틀렸을 때 다시 요청하는 법을 실제 예시로 다룬다.

 

관련 글

     → [교량 BIM 바이브코딩 1편] Claude 바이브코딩으로 3D 교량 만들기

     → [교량 BIM 바이브코딩 2편] three.js 기초: 장면·카메라·렌더러

     → [교량 BIM 바이브코딩 4편] AI 코딩 프롬프트 작성법: Claude에게 3D 모델 정확히 요청하기

     → [교량 BIM 바이브코딩 5편] three.js ExtrudeGeometry: I형 거더 단면 3D 모델링

     → [교량 BIM 바이브코딩 6편] three.js 파라메트릭 모델링: 슬라이더로 바뀌는 교량

     → [교량 BIM 바이브코딩 7편] three.js 교량 BIM 모델: 클릭하면 속성이 나오는 부재 조립

     → [교량 BIM 바이브코딩 8편] three.js 곡선교 모델링: 곡선 반경 R과 편경사로 휘는 교량

     → 토목 엔지니어가 자바스크립트와 Three.js를 배워야 하는 이유

 

반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/10   »
일 월 화 수 목 금 토
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