씬 그래프
이 글은 태양계 시뮬레이터 시리즈의 일부다.
- Three.js 좌표계 기초
- Three.js PBR (물리 기반 렌더링) 재질 기초
- 사실적인 지구 만들기
- 사실적인 태양 만들기
- 고리가 있는 행성 만들기
- 불규칙한 형태의 위성 만들기
- 태양을 빛나게 하기
- 은하수 스카이박스 만들기
- 타원 궤도 계산하기
태양-지구 시스템
이 글에서는 간단한 태양-지구 시스템을 만든다. 먼저 태양과 지구를 각각 나타내는 주황색 구와 파란색 구를 생성한다.
// create sphere geometry for Sun and Earth
const geometry_sphere = new THREE.SphereGeometry(1, 30, 30);
// object Sun uses basicMaterial since it emits orange light
const material_sun = new THREE.MeshBasicMaterial({color: 0xffaa00});
const sun = new THREE.Mesh(geometry_sphere, material_sun);
sun.position.set(0, 0, 0);
const light = new THREE.PointLight(0xffffff, 50);
light.position.set(0, 0, 0);
// object Earth uses lambertMaterial since it reflects sunlight
const material_earth = new THREE.MeshLambertMaterial({color: 0x4444ff});
const earth = new THREE.Mesh(geometry_sphere, material_earth);
earth.position.set(3, 0, 0); // distant from Sun
earth.scale.set(0.2, 0.2, 0.2); // smaller size than Sun
const scene = new THREE.Scene();
scene.add(sun);
scene.add(earth);
scene.add(light);
태양과 지구는 모양이 같으므로 하나의 geometry 속성 geometry_sphere를 공유한다. 태양은 시스템에서 유일한 광원이므로 태양의 재질은 조명의 영향을 받지 않는 MeshBasicMaterial을 사용하고, PointLight를 태양의 중심에 위치시킨다. 지구는 태양으로부터 떨어져 있으며 태양보다 크기가 작다. 결과는 다음과 같다:

지구의 공전
지구가 자전하면서 태양 주위를 공전하도록 만들어 보자. 다음 함수를 정의하고 애니메이션 루프 안에 추가한다.
const w_orbit = 0.5;
const w_rotate = 0.1;
function updateSystem(sec) {
earth.position.set(3*Math.cos(w_orbit*sec), 0, -3*Math.sin(w_orbit*sec));
earth.rotateY(w_rotate);
}
한편, requestAnimationFrame()에 바인딩되는 애니메이션 함수는 타임스탬프를 나타내는 단일 인자 msec를 전달받을 수 있다.
이 콜백 함수는 단일 인자를 전달받는다: 이전 프레임의 렌더링이 끝난 시각을 나타내는
DOMHighResTimeStamp(시간 원점 이후의 밀리초 수를 기준으로 한다). 1
function animate (msec) {
requestAnimationFrame(animate);
updateSystem(msec * 0.001);
controls.update();
renderer.render(scene, camera);
}
animate();
여기서는 영상에서 지구의 자전을 관찰하기 쉽도록, SphereGeometry의 분할 수를 줄이고 지구 재질의 flatShading = true로 설정한다. flatShading은 모델의 각 fragment가 단일 색상을 가지도록 한다.
const geometry_sphere = new THREE.SphereGeometry(1, 10, 10);
const material_earth = new THREE.MeshLambertMaterial({color: 0x4444ff, flatShading: true});
최종 결과는 다음과 같다:

지역 좌표계
이제 태양-지구 시스템에 달을 추가하려고 하면, 달의 위치를 명시적으로 계산하여야 한다.
\[x_{\rm moon} = dist_{\rm sun-earth} \times \cos(\omega_{\rm rev, earth}\times t) + dist_{\rm earth-moon}\times \cos(\omega_{\rm rev, moon}\times t)\] \[y_{\rm moon} = -dist_{\rm sun-earth} \times \sin(\omega_{\rm rev, earth}\times t) - dist_{\rm earth-moon}\times \sin(\omega_{\rm rev, moon}\times t)\]게다가 사실적인 태양계를 표현하려고 하면, 실제 지구의 궤도와 자전축은 기울어져 있기 때문에 위 식은 훨씬 더 복잡해진다. 이를 단순화하기 위해, 모든 물체를 하나의 scene에 추가하는 것이 아니라, 각 물체의 상위 클래스 물체에 추가할 수 있다. 이를 지역 좌표계라고 표현할 수 있으며, 지구는 태양 좌표계에, 달은 지구 좌표계에 추가하면 위치 계산이 단순해진다. 지역 좌표계 (local coordinate) 로서 지구의 궤도면과 적도면, 그리고 달의 궤도면을 생성하여 위 코드를 수정해 보자.
const earth_orbit = new THREE.Object3D();
const earth_equator = new THREE.Object3D();
const moon_orbit = new THREE.Object3D();
earth_equator.rotateZ(23.5*Math.PI/180); // tilted rotation axis
const scene = new THREE.Scene();
scene.add(sun);
scene.add(earth_orbit);
sun.add(light);
earth_orbit.add(earth_equator);
earth_equator.add(earth);
earth_equator.add(moon_orbit);
moon_orbit.add(moon);
const w_moon = 5;
const w_orbit = 0.5;
const w_rotate = 0.1;
function updateSystem(sec) {
moon.position.set(0.4*Math.cos(w_moon*sec), 0, -0.4*Math.sin(w_moon*sec));
earth_equator.position.set(3*Math.cos(w_orbit*sec), 0, -3*Math.sin(w_orbit*sec));
earth.rotateY(w_rotate);
}
위 코드에서 moon_orbit은 earth_equator에 속하므로, moon의 위치는 earth_equator 좌표계에서 결정된다. 따라서 달의 위치 계산은 앞서의 공식보다 단순해진다.

씬 그래프
결과적으로 위 시스템의 씬 그래프(scene graph)는 아래와 같다. 위성에 따라 그 객체는 모(母) 천체의 적도면이나 궤도면에 추가될 수 있다. 정확히, 씬 그래프는 Object3D, Mesh, Geometry, Material, Texture 간의 관계도 포함한다. 하지만 여기서는 Object3D만 표시하였다.

전체 코드
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
const canvas = document.createElement("canvas");
document.body.appendChild(canvas);
const renderer = new THREE.WebGLRenderer({canvas: canvas, alpha: true, antialias: true});
renderer.setSize( window.innerWidth, window.innerHeight );
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 0, 5);
const geometry_sphere = new THREE.SphereGeometry(1, 10, 10);
const material_sun = new THREE.MeshBasicMaterial({color: 0xffaa00});
const sun = new THREE.Mesh(geometry_sphere, material_sun);
const material_earth = new THREE.MeshLambertMaterial({color: 0x4444ff, flatShading: true});
const earth = new THREE.Mesh(geometry_sphere, material_earth);
earth.scale.set(0.2, 0.2, 0.2);
const material_moon = new THREE.MeshLambertMaterial({color: 0xaaaaaa, flatShading: true});
const moon = new THREE.Mesh(geometry_sphere, material_moon);
moon.scale.set(0.1, 0.1, 0.1);
const light = new THREE.PointLight(0xffffff, 50);
light.position.set(0, 0, 0);
const earth_orbit = new THREE.Object3D();
const earth_equator = new THREE.Object3D();
const moon_orbit = new THREE.Object3D();
earth_equator.rotateZ(23.5*Math.PI/180);
const scene = new THREE.Scene();
scene.add(sun);
scene.add(earth_orbit);
sun.add(light);
earth_orbit.add(earth_equator);
earth_equator.add(earth);
earth_equator.add(moon_orbit);
moon_orbit.add(moon);
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;
const w_moon = 5;
const w_orbit = 0.5;
const w_rotate = 0.1;
function updateSystem(sec) {
moon.position.set(0.4*Math.cos(w_moon*sec), 0, -0.4*Math.sin(w_moon*sec));
earth_equator.position.set(3*Math.cos(w_orbit*sec), 0, -3*Math.sin(w_orbit*sec));
earth.rotateY(w_rotate);
}
function animate (msec) {
requestAnimationFrame(animate);
updateSystem(msec * 0.001);
controls.update();
renderer.render(scene, camera);
}
animate();