씬 그래프

0

이 글은 태양계 시뮬레이터 시리즈의 일부다.

  1. Three.js 좌표계 기초
  2. Three.js PBR (물리 기반 렌더링) 재질 기초
  3. 사실적인 지구 만들기
  4. 사실적인 태양 만들기
  5. 고리가 있는 행성 만들기
  6. 불규칙한 형태의 위성 만들기
  7. 태양을 빛나게 하기
  8. 은하수 스카이박스 만들기
  9. 타원 궤도 계산하기

태양-지구 시스템

이 글에서는 간단한 태양-지구 시스템을 만든다. 먼저 태양과 지구를 각각 나타내는 주황색 구와 파란색 구를 생성한다.

// 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_orbitearth_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();