GIS Developer ๋์ Three.js ๊ฐ์ข : https://www.youtube.com/watch?v=Oe4n0vyrSiU
์นด๋ฉ๋ผ (Camera)
Camera๋ฅผ ์์๋ฐ๋ ํด๋์ค (Camera๋ object 3D๋ฅผ ์์๋ฐ๋๋ค
- PerspectiveCamera : ๊ฐ๊น์ด ์๋ ๋ฌผ์ฒด๊ฐ ๋ฉ๋ฆฌ ์๋ ๋ฌผ์ฒด๋ณด๋ค ์๋์ ์ผ๋ก ํฌ๊ฒ ๋ณด์ด๋๋ก ์๊ทผ๊ฐ์ ํํํด์ ๋ ๋๋งํ๋ค. fovy, aspect, zNear, zFar๋ฅผ ์ธ์๋ก ๋ฐ๋๋ค. ์ด ์ธ์๋ค์ ํตํด ์ ๋์ฒด๊ฐ ๊ตฌ์ฑ๋๋ค. (์ ๋์ฒด ์์ ์กด์ฌํ๋ ๋ฌผ์ฒด๊ฐ ์นด๋ฉ๋ผ๋ฅผ ํตํด ๋ณด์ฌ์ง๊ฒ ๋๋ค) zNear์ zFar ๊ฑฐ๋ฆฌ ์ฌ์ด๋ฅผ ๋ฒ์ด๋๋ฉด ๋ ๋๋ง๋์ง ์๋๋ค.
- OrthographicCamera : ๋ฌผ์ฒด๊ฐ์ ์๊ทผ๊ฐ์์ด ๋ฌผ์ฒด์ ํฌ๊ธฐ ํฌ๊ธฐ๋๋ก ๋ ๋๋งํ๋ค. zLeft, xRight, yTop, yBottom, zNear, zFar๋ฅผ ์ธ์๋ก ๋ฐ๋๋ค.
// 07-camera.js
import * as THREE from '../build/three.module.js';
import {OrbitControls} from '../examples/jsm/controls/OrbitControls.js';
import {RectAreaLightUniformsLib} from '../examples/jsm/lights/RectAreaLightUniformsLib.js';
import {RectAreaLightHelper} from '../examples/jsm/helpers/RectAreaLightHelper.js';
class App {
constructor() {
const divContainer = document.querySelector('#webgl-container');
this._divContainer = divContainer;
const renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setPixelRatio(window.devicePixelRatio);
divContainer.appendChild(renderer.domElement);
this._renderer = renderer;
const scene = new THREE.Scene();
this._scene = scene;
this._setupCamera();
this._setupLight();
this._setupModel();
this._setupControls();
window.onresize = this.resize.bind(this);
this.resize();
requestAnimationFrame(this.render.bind(this));
}
_setupControls() {
new OrbitControls(this._camera, this._divContainer);
}
_setupCamera() {
const width = this._divContainer.clientWidth;
const height = this._divContainer.clientHeight;
// PerspectiveCamera(์ ๋์ฒด์ ๋์ด ๋ฐฉํฅ์ ๋ํ ๊ฐ๋, ์ ๋์ฒด์ ๊ฐ๋ก ๊ธธ์ด๋ฅผ ์ธ๋ก ๊ธธ์ด๋ก ๋๋ ๋น์จ, ์นด๋ฉ๋ผ๋ก๋ถํฐ์ ๊ฑฐ๋ฆฌ ์์ชฝ, ์นด๋ฉ๋ผ๋ก๋ถํฐ์ ๊ฑฐ๋ฆฌ ๋ค์ชฝ)
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 100);
// OrthographicCamera(์ํ์ถ ์ผ์ชฝ์ ๋ํ ์ขํ๊ฐ, ์ํ์ถ ์ค๋ฅธ์ชฝ์ ๋ํ ์ขํ๊ฐ, ์์ง์ถ ์์ชฝ์ ๋ํ ์ขํ๊ฐ, ์์ง์ถ ์๋์ชฝ์ ๋ํ ์ขํ๊ฐ, ์นด๋ฉ๋ผ๋ก๋ถํฐ์ ๊ฑฐ๋ฆฌ ์์ชฝ, ์นด๋ฉ๋ผ๋ก๋ถํฐ์ ๊ฑฐ๋ฆฌ ๋ค์ชฝ)
// aspect : ๋ ๋๋ง ๊ฒฐ๊ณผ๊ฐ ํ์๋๋ DOM ์์ ํฌ๊ธฐ์ ๋ํ ์ข
ํก๋น๋ฅผ ์ ์ฉ
/* const aspect = window.innerWidth / window.innerHeight;
const camera = new THREE.OrthographicCamera(aspect * -1, aspect * 1, 1, -1, 0.1, 100);
camera.zoom = 0.15; */
//camera.position.z = 2;
// ์นด๋ฉ๋ผ์ ์์น
camera.position.set(7, 7, 0);
// ์นด๋ฉ๋ผ๊ฐ ๋ฐ๋ผ๋ณด๋ ์์น
camera.lookAt(0, 0, 0);
this._camera = camera;
}
_setupLight() {
RectAreaLightUniformsLib.init();
const light = new THREE.RectAreaLight(0xffffff, 10, 6, 1);
light.position.set(0, 5, 0);
light.rotation.x = THREE.Math.degToRad(-90);
const helper = new RectAreaLightHelper(light);
this._scene.add(helper);
this._lightHelper = helper;
this._scene.add(light);
this._light = light;
}
_setupModel() {
const groundGeometry = new THREE.PlaneGeometry(10, 10);
const groundMaterial = new THREE.MeshStandardMaterial({
color: '#2c3e50',
roughness: 0.5,
metalness: 0.5,
side: THREE.DoubleSide,
});
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = THREE.Math.degToRad(-90);
this._scene.add(ground);
const bigSphereGeometry = new THREE.SphereGeometry(1.5, 64, 64, 0, Math.PI);
const bigSphereMaterial = new THREE.MeshStandardMaterial({
color: '#ffffff',
roughness: 0.1,
metalness: 0.2,
});
const bigSphere = new THREE.Mesh(bigSphereGeometry, bigSphereMaterial);
bigSphere.rotation.x = THREE.Math.degToRad(-90);
this._scene.add(bigSphere);
const torusGeometry = new THREE.TorusGeometry(0.4, 0.1, 32, 32);
const torusMaterial = new THREE.MeshStandardMaterial({
color: '#9b59b6',
roughness: 0.5,
metalness: 0.9,
});
for (let i = 0; i < 8; i++) {
const torusPivot = new THREE.Object3D();
const torus = new THREE.Mesh(torusGeometry, torusMaterial);
torusPivot.rotation.y = THREE.Math.degToRad(45 * i);
torus.position.set(3, 0.5, 0);
torusPivot.add(torus);
this._scene.add(torusPivot);
}
const smallSphereGeometry = new THREE.SphereGeometry(0.3, 32, 32);
const smallSphereMaterial = new THREE.MeshStandardMaterial({
color: '#e74c3c',
roughness: 0.2,
metalness: 0.5,
});
const smallSpherePivot = new THREE.Object3D();
const smallSphere = new THREE.Mesh(smallSphereGeometry, smallSphereMaterial);
smallSpherePivot.add(smallSphere);
smallSpherePivot.name = 'smallSpherePivot';
smallSphere.position.set(3, 0.5, 0);
this._scene.add(smallSpherePivot);
// target : ์นด๋ฉ๋ผ์ ์์น๋ฅผ ์กฐ์ ํ๊ธฐ ์ํด ์์ฑ
// Object3D๋ก ์์ฑํ๊ธฐ๋๋ฌธ์ ํ๋ฉด์์ ๋ ๋๋ง๋์ง๋ ์์ง๋ง scene์ ๊ตฌ์ฑ์์๋ก ์๋ฆฌ์ก๋๋ค.
const targetPivot = new THREE.Object3D();
const target = new THREE.Object3D();
targetPivot.add(target);
targetPivot.name = 'targetPivot';
target.position.set(3, 0.5, 0);
this._scene.add(targetPivot);
}
resize() {
const width = this._divContainer.clientWidth;
const height = this._divContainer.clientHeight;
const aspect = width / height;
if (this._camera instanceof THREE.PerspectiveCamera) {
// PerspectiveCamera
this._camera.aspect = aspect;
} else {
// OrthographicCamera
this._camera.left = aspect * -1;
this._camera.right = aspect * 1;
}
this._camera.updateProjectionMatrix();
this._renderer.setSize(width, height);
}
render(time) {
this._renderer.render(this._scene, this._camera);
this.update(time);
requestAnimationFrame(this.render.bind(this));
}
update(time) {
time *= 0.001; // second unit
const smallSpherePivot = this._scene.getObjectByName('smallSpherePivot');
if (smallSpherePivot) {
smallSpherePivot.rotation.y = THREE.Math.degToRad(time * 50);
// ์นด๋ฉ๋ผ์ ์์น๊ฐ ์์ ๊ตฌ๋ฅผ ๋ฐ๋ผ๊ฐ๋๋ก
const smallSphere = smallSpherePivot.children[0];
smallSphere.getWorldPosition(this._camera.position);
// target์ด ์์ ๊ตฌ๋ณด๋ค ์์๋๋ก
const targetPivot = this._scene.getObjectByName('targetPivot');
if (targetPivot) {
targetPivot.rotation.y = THREE.Math.degToRad(time * 50 + 10);
const target = targetPivot.children[0];
const pt = new THREE.Vector3();
target.getWorldPosition(pt);
// ์นด๋ฉ๋ผ์ ์์ ์ด ์์ ๊ตฌ์ ๋ค์ ์์น(target์ ์์น)๋ฅผ ํฅํ๋๋ก
this._camera.lookAt(pt);
}
if (this._light.target) {
const smallSphere = smallSpherePivot.children[0];
smallSphere.getWorldPosition(this._light.target.position);
if (this._lightHelper) {
this._lightHelper.update();
}
}
}
}
}
window.onload = function() {
new App();
}
๊ทธ๋ฆผ์(Shadow)
: Three.js๋ ๋์ ์ผ๋ก ๊ทธ๋ฆผ์๋ฅผ ๋ ๋๋ง ํ ์ ์๋ค. ๋ด๋ถ์ ์ผ๋ก ํ ์ค์ณ ๋งตํ์ ํตํด ๊ทธ๋ฆผ์๋ฅผ ์ํ ์ด๋ฏธ์ง๋ฅผ ์์ฑํ๊ณ , ์ด ํ ์ค์ณ ๋งตํ ์ด๋ฏธ์ง๋ฅผ ์ด์ฉํด ๊ทธ๋ฆผ์๋ฅผ ์๊ฐํํ๋ค.
: Three.js ์์ ๊ทธ๋ฆผ์๋ฅผ ๋ ๋๋งํ๊ธฐ ์ํด์๋ Renderer, light, model ๊ฐ์ฒด์ ์ค์ ์ด ํ์ํ๋ค.
// renderer
const renderer = new THREE.WebGLRenderer({antialias: true});
renderer.shadowMap.enabled = true;
// light
const light = new THREE.DirectionalLight(0xffffff, 0.5);
light.castShadow = true;
// model
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.receiveShadow = true;
ground.castShadow = true;
: Three.js ์์ ๊ทธ๋ฆผ์๋ฅผ ์ ๊ณตํ๋ ๊ด์์ DirectionalLight, PointLight, SpotLight๋ค.
๊ทธ๋ฆผ์๋ฅผ ์ง์ํ๋ ๊ด์์ ๋ชจ๋ shadow๋ผ๋ ์์ฑ์ ๊ฐ๊ณ ์ด shadow ์์ฑ์๋ camera ์์ฑ์ด ์กด์ฌํ๋ค. ์ด camera๊ฐ ๊ทธ๋ฆผ์์ ๋ํ ํ ์ค์ณ ์ด๋ฏธ์ง๋ฅผ ์์ฑํ๊ธฐ ์ํด์ ์ฌ์ฉ๋๋ค.
์ด ์นด๋ฉ๋ผ์ ์ ๋์ฒด๋ฅผ ๋ฒ์ด๋๋ ๊ฐ์ฒด(๊ทธ๋ฆผ์)๋ ๋ชจ๋ ์๋ ค๋๊ฐ๊ฒ ๋๋ค. ์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๊ทธ๋ฆผ์๋ฅผ ์ํ ์นด๋ฉ๋ผ์ ์ ๋์ฒด๋ฅผ ์ข ๋ ํฌ๊ฒ ํด์ฃผ๋ฉด ๋๋ค.
: ๊ทธ๋ฆผ์๋ ํ ์ค์ณ ๋งตํ ์ด๋ฏธ์ง๋ฅผ ์ด์ฉํด ํํ๋๋๋ฐ, ์ด๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๊ฐ๋ก์ ์ธ๋ก ํฌ๊ธฐ๊ฐ 512๋ค. ์ด ํฌ๊ธฐ๋ฅผ ๋ ํฌ๊ฒํ๋ฉด ๊ทธ๋ฆผ์์ ํ์ง์ด ํฅ์๋๋ค.
: ์ํฉ์ ๋ฐ๋ผ ๊ทธ๋ฆผ์์ ๊ฒฝ๊ณ๊ฐ ์ ๋ช ํ์ง ์๊ณ ๋ธ๋ฌ๋ง ์ฒ๋ฆฌ๊ฐ ๋ ํ์๊ฐ ์๋๋ฐ, ์ด๋ shadow์ radius์ ๊ฐ์ ํตํด ์ค์ ํ ์ ์๋ค.
// 08-shadow.js
import * as THREE from '../build/three.module.js';
import {OrbitControls} from '../examples/jsm/controls/OrbitControls.js';
import {RectAreaLightUniformsLib} from '../examples/jsm/lights/RectAreaLightUniformsLib.js';
import {RectAreaLightHelper} from '../examples/jsm/helpers/RectAreaLightHelper.js';
class App {
constructor() {
const divContainer = document.querySelector('#webgl-container');
this._divContainer = divContainer;
const renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setPixelRatio(window.devicePixelRatio);
// ๊ทธ๋ฆผ์ ๋งต ํ์ฑํ
renderer.shadowMap.enabled = true;
divContainer.appendChild(renderer.domElement);
this._renderer = renderer;
const scene = new THREE.Scene();
this._scene = scene;
this._setupCamera();
this._setupLight();
this._setupModel();
this._setupControls();
window.onresize = this.resize.bind(this);
this.resize();
requestAnimationFrame(this.render.bind(this));
}
_setupControls() {
new OrbitControls(this._camera, this._divContainer);
}
_setupCamera() {
const width = this._divContainer.clientWidth;
const height = this._divContainer.clientHeight;
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 100);
//camera.position.z = 2;
camera.position.set(7, 7, 0);
camera.lookAt(0, 0, 0);
this._camera = camera;
}
_setupLight() {
// ํ๋ฉด์ ๋ฐํ๊ธฐ ์ํด ๊ด์ ์ถ๊ฐ
const auxLight = new THREE.DirectionalLight(0xffffff, 0.5);
auxLight.position.set(0, 5, 0);
auxLight.target.position.set(0, 0, 0);
this._scene.add(auxLight.target);
this._scene.add(auxLight);
// DirectionalLight
/* const light = new THREE.DirectionalLight(0xffffff, 0.5);
light.position.set(0, 5, 0);
light.target.position.set(0, 0, 0);
this._scene.add(light.target); */
// PointLight
/* const light = new THREE.PointLight(0xffffff, 0.7);
light.position.set(0, 5, 0); */
// SpotLight
const light = new THREE.SpotLight(0xffffff, 0.5);
light.position.set(0, 5, 0);
light.target.position.set(0, 0, 0);
light.angle = THREE.Math.degToRad(30);
light.penumbra = 0.2;
this._scene.add(light.target);
// ๊ทธ๋ฆผ์๊ฐ ์๋ฆฌ๋ ํ์ ํด๊ฒฐ : ๊ทธ๋ฆผ์๋ฅผ ์ํ ์นด๋ฉ๋ผ์ ์ ๋์ฒด๋ฅผ ์ข ๋ ํฌ๊ฒ ํด์ค
light.shadow.camera.top = light.shadow.camera.right = 6;
light.shadow.camera.bottom = light.shadow.camera.left = -6;
// ๊ทธ๋ฆผ์์ ํ์ง ํฅ์
light.shadow.mapSize.width = light.shadow.mapSize.height = 1024;
// ๊ทธ๋ฆผ์์ ์ธ๊ณฝ์ ๋ธ๋ฌ๋ง ์ฒ๋ฆฌ
light.shadow.radius = 1;
const cameraHelper = new THREE.CameraHelper(light.shadow.camera);
this._scene.add(cameraHelper);
this._scene.add(light);
this._light = light;
// ๊ทธ๋ฆผ์ ํ์ฑํ ์ฌ๋ถ
light.castShadow = true;
}
_setupModel() {
const groundGeometry = new THREE.PlaneGeometry(10, 10);
const groundMaterial = new THREE.MeshStandardMaterial({
color: '#2c3e50',
roughness: 0.5,
metalness: 0.5,
side: THREE.DoubleSide,
});
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = THREE.Math.degToRad(-90);
// ๊ทธ๋ฆผ์๋ฅผ ๋ฐ์์ ํํํ ์ง์ ๋ํ ์ฌ๋ถ
ground.receiveShadow = true;
this._scene.add(ground);
//const bigSphereGeometry = new THREE.SphereGeometry(1.5, 64, 64, 0, Math.PI);
const bigSphereGeometry = new THREE.TorusKnotGeometry(1, 0.3, 128, 64, 2, 3);
const bigSphereMaterial = new THREE.MeshStandardMaterial({
color: '#ffffff',
roughness: 0.1,
metalness: 0.2,
});
const bigSphere = new THREE.Mesh(bigSphereGeometry, bigSphereMaterial);
//bigSphere.rotation.x = THREE.Math.degToRad(-90);
bigSphere.position.y = 1.6;
// ๊ทธ๋ฆผ์๋ฅผ ๋ฐ์์ ํํํ ์ง์ ๋ํ ์ฌ๋ถ
bigSphere.receiveShadow = true;
// ๊ทธ๋ฆผ์๋ฅผ ์ ๋ฌํ ์ง์ ๋ํ ์ฌ๋ถ
bigSphere.castShadow = true;
this._scene.add(bigSphere);
const torusGeometry = new THREE.TorusGeometry(0.4, 0.1, 32, 32);
const torusMaterial = new THREE.MeshStandardMaterial({
color: '#9b59b6',
roughness: 0.5,
metalness: 0.9,
});
for (let i = 0; i < 8; i++) {
const torusPivot = new THREE.Object3D();
const torus = new THREE.Mesh(torusGeometry, torusMaterial);
torusPivot.rotation.y = THREE.Math.degToRad(45 * i);
torus.position.set(3, 0.5, 0);
torus.receiveShadow = true;
torus.castShadow = true;
torusPivot.add(torus);
this._scene.add(torusPivot);
}
const smallSphereGeometry = new THREE.SphereGeometry(0.3, 32, 32);
const smallSphereMaterial = new THREE.MeshStandardMaterial({
color: '#e74c3c',
roughness: 0.2,
metalness: 0.5,
});
const smallSpherePivot = new THREE.Object3D();
const smallSphere = new THREE.Mesh(smallSphereGeometry, smallSphereMaterial);
smallSpherePivot.add(smallSphere)
smallSpherePivot.name = 'smallSpherePivot';
smallSphere.position.set(3, 0.5, 0);
this._scene.add(smallSpherePivot);
}
resize() {
const width = this._divContainer.clientWidth;
const height = this._divContainer.clientHeight;
this._camera.aspect = width / height;
this._camera.updateProjectionMatrix();
this._renderer.setSize(width, height);
}
render(time) {
this._renderer.render(this._scene, this._camera);
this.update(time);
requestAnimationFrame(this.render.bind(this));
}
update(time) {
time *= 0.001; // second unit
const smallSpherePivot = this._scene.getObjectByName('smallSpherePivot');
if (smallSpherePivot) {
smallSpherePivot.rotation.y = THREE.Math.degToRad(time * 50);
if (this._light.target) {
const smallSphere = smallSpherePivot.children[0];
smallSphere.getWorldPosition(this._light.target.position);
if (this._lightHelper) {
this._lightHelper.update();
}
}
// PointLight
if (this._light instanceof THREE.PointLight) {
const smallSphere = smallSpherePivot.children[0];
smallSphere.getWorldPosition(this._light.position);
}
}
}
}
window.onload = function() {
new App();
}