구현하고자 하는 결과는 아래의 그림처럼 노란색 경로가 있고 빨간색 직육면체가 이 경로를 따라 자연스럽게 이동하는 것입니다.
비디오를 지원하지 않는 웹브라우져입니다.
먼저 제가 사용하는 three.js의 구성 중 거의 변경되지 않는 HTML과 CSS를 살펴보겠습니다. HTML은 다음과 같습니다.
< meta name = "viewport" content = "width=device-width, initial-scale=1" >
< link rel = "stylesheet" href = "style.css" >
< script type = "module" src = "app.js" defer >
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css">
<script type="module" src="app.js" defer>
</head>
<body>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css">
<script type="module" src="app.js" defer>
</head>
<body>
</body>
</html>
CSS는 다음과 같구요.
* {
outline: none;
padding: 0;
margin: 0;
}
* {
outline: none;
padding: 0;
margin: 0;
}
그리고 이제 app.js에 대한 코드를 살펴보겠습니다. 먼저 기초 코드입니다.
import * as THREE from 'https://cdnjs.cloudflare.com/ajax/libs/three.js/r126/three.module.min.js'
this . domWebGL = document . createElement ( 'div' ) ;
document . body . appendChild ( this . domWebGL ) ;
let scene = new THREE. Scene ( ) ;
let renderer = new THREE. WebGLRenderer ( ) ;
renderer. setClearColor ( 0x000000 , 1.0 ) ;
this . domWebGL . appendChild ( renderer. domElement ) ;
window . onresize = this . resize . bind ( this ) ;
this . renderer = renderer;
const light = new THREE. DirectionalLight ( 0xffffff , 1 ) ;
light. position . set ( 30 , 50 , 20 ) ;
let camera = new THREE. PerspectiveCamera ( fov, aspect, zNear, zFar ) ;
camera. position . set ( 40 , 40 , 40 ) . multiplyScalar ( 0.3 ) ;
requestAnimationFrame ( this . render . bind ( this ) ) ;
this . renderer . render ( this . scene , this . camera ) ;
let camera = this . camera ;
let renderer = this . renderer ;
renderer. setPixelRatio ( window . devicePixelRatio ) ;
renderer. setSize ( window . innerWidth , window . innerHeight ) ;
camera. aspect = window . innerWidth / window . innerHeight ;
camera. updateProjectionMatrix ( ) ;
window . onload = function ( ) {
import * as THREE from 'https://cdnjs.cloudflare.com/ajax/libs/three.js/r126/three.module.min.js'
class App {
constructor() {
this._initialize();
}
_initialize() {
this.domWebGL = document.createElement('div');
document.body.appendChild(this.domWebGL);
let scene = new THREE.Scene();
let renderer = new THREE.WebGLRenderer();
renderer.setClearColor(0x000000, 1.0);
this.domWebGL.appendChild(renderer.domElement);
window.onresize = this.resize.bind(this);
this.renderer = renderer;
this.scene = scene;
this._setupModel();
this._setupLights()
this._setupCamera();
this.resize();
}
_setupModel() {
// 경로 및 직사각형 모델 구성
}
update(time) {
// 직사각형 모델을 경로에 따라 이동시킴
}
_setupLights() {
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(30, 50, 20);
this.scene.add(light);
}
_setupCamera() {
const fov = 60;
const aspect = 1;
const zNear = 0.1;
const zFar = 1000;
let camera = new THREE.PerspectiveCamera(fov, aspect, zNear, zFar);
camera.position.set(40, 40, 40).multiplyScalar(0.3);
camera.lookAt(0,-2,0);
this.scene.add(camera);
this.camera = camera;
}
render(time) {
requestAnimationFrame(this.render.bind(this));
this.update(time);
this.renderer.render(this.scene, this.camera);
}
resize() {
let camera = this.camera;
let renderer = this.renderer;
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
}
}
window.onload = function() {
(new App()).render(0);
}
import * as THREE from 'https://cdnjs.cloudflare.com/ajax/libs/three.js/r126/three.module.min.js'
class App {
constructor() {
this._initialize();
}
_initialize() {
this.domWebGL = document.createElement('div');
document.body.appendChild(this.domWebGL);
let scene = new THREE.Scene();
let renderer = new THREE.WebGLRenderer();
renderer.setClearColor(0x000000, 1.0);
this.domWebGL.appendChild(renderer.domElement);
window.onresize = this.resize.bind(this);
this.renderer = renderer;
this.scene = scene;
this._setupModel();
this._setupLights()
this._setupCamera();
this.resize();
}
_setupModel() {
// 경로 및 직사각형 모델 구성
}
update(time) {
// 직사각형 모델을 경로에 따라 이동시킴
}
_setupLights() {
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(30, 50, 20);
this.scene.add(light);
}
_setupCamera() {
const fov = 60;
const aspect = 1;
const zNear = 0.1;
const zFar = 1000;
let camera = new THREE.PerspectiveCamera(fov, aspect, zNear, zFar);
camera.position.set(40, 40, 40).multiplyScalar(0.3);
camera.lookAt(0,-2,0);
this.scene.add(camera);
this.camera = camera;
}
render(time) {
requestAnimationFrame(this.render.bind(this));
this.update(time);
this.renderer.render(this.scene, this.camera);
}
resize() {
let camera = this.camera;
let renderer = this.renderer;
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
}
}
window.onload = function() {
(new App()).render(0);
}
위의 코드에서 경로와 정육면체 매쉬를 구성하는 _setupModel과 매쉬 모델을 움직이도록 속성값을 업데이트하는 update 함수는 아직 비어 있습니다.
모델을 구성하는 _setupModel 함수의 코드는 다음과 같습니다.
const path = new THREE. SplineCurve ( [
new THREE. Vector2 ( 10 , 5 ) ,
new THREE. Vector2 ( 5 , 5 ) ,
new THREE. Vector2 ( 5 , 10 ) ,
new THREE. Vector2 ( -5 , 10 ) ,
new THREE. Vector2 ( -5 , 5 ) ,
new THREE. Vector2 ( -10 , 5 ) ,
new THREE. Vector2 ( -10 , -5 ) ,
new THREE. Vector2 ( -5 , -5 ) ,
new THREE. Vector2 ( -5 , -10 ) ,
new THREE. Vector2 ( 5 , -10 ) ,
new THREE. Vector2 ( 5 , -5 ) ,
new THREE. Vector2 ( 10 , -5 ) ,
new THREE. Vector2 ( 10 , 5 ) ,
const points = path. getPoints ( 100 ) ;
const geometry = new THREE. BufferGeometry ( ) . setFromPoints ( points ) ;
const material = new THREE. LineBasicMaterial ( { color : 0xffff00 } ) ;
const pathLine = new THREE. Line ( geometry, material ) ;
pathLine. rotation . x = Math. PI * .5 ;
this . scene . add ( pathLine ) ;
const boxGeometry = new THREE. BoxGeometry ( 1 , 1 , 3 ) ;
const boxMaterial = new THREE. MeshPhongMaterial ( { color: 0xff0000 } ) ;
const boxMesh = new THREE. Mesh ( boxGeometry, boxMaterial ) ;
_setupModel() {
const path = new THREE.SplineCurve( [
new THREE.Vector2( 10, 5 ),
new THREE.Vector2( 5, 5 ),
new THREE.Vector2( 5, 10 ),
new THREE.Vector2( -5, 10 ),
new THREE.Vector2( -5, 5 ),
new THREE.Vector2( -10, 5 ),
new THREE.Vector2( -10, -5 ),
new THREE.Vector2( -5, -5 ),
new THREE.Vector2( -5, -10 ),
new THREE.Vector2( 5, -10 ),
new THREE.Vector2( 5, -5 ),
new THREE.Vector2( 10, -5 ),
new THREE.Vector2( 10, 5 ),
] );
this.path = path;
const points = path.getPoints( 100 );
const geometry = new THREE.BufferGeometry().setFromPoints( points );
const material = new THREE.LineBasicMaterial( { color : 0xffff00 } );
const pathLine = new THREE.Line( geometry, material );
pathLine.rotation.x = Math.PI * .5;
this.scene.add(pathLine);
const boxGeometry = new THREE.BoxGeometry(1, 1, 3);
const boxMaterial = new THREE.MeshPhongMaterial({color: 0xff0000});
const boxMesh = new THREE.Mesh(boxGeometry, boxMaterial);
this.scene.add(boxMesh);
this.boxMesh = boxMesh;
}
_setupModel() {
const path = new THREE.SplineCurve( [
new THREE.Vector2( 10, 5 ),
new THREE.Vector2( 5, 5 ),
new THREE.Vector2( 5, 10 ),
new THREE.Vector2( -5, 10 ),
new THREE.Vector2( -5, 5 ),
new THREE.Vector2( -10, 5 ),
new THREE.Vector2( -10, -5 ),
new THREE.Vector2( -5, -5 ),
new THREE.Vector2( -5, -10 ),
new THREE.Vector2( 5, -10 ),
new THREE.Vector2( 5, -5 ),
new THREE.Vector2( 10, -5 ),
new THREE.Vector2( 10, 5 ),
] );
this.path = path;
const points = path.getPoints( 100 );
const geometry = new THREE.BufferGeometry().setFromPoints( points );
const material = new THREE.LineBasicMaterial( { color : 0xffff00 } );
const pathLine = new THREE.Line( geometry, material );
pathLine.rotation.x = Math.PI * .5;
this.scene.add(pathLine);
const boxGeometry = new THREE.BoxGeometry(1, 1, 3);
const boxMaterial = new THREE.MeshPhongMaterial({color: 0xff0000});
const boxMesh = new THREE.Mesh(boxGeometry, boxMaterial);
this.scene.add(boxMesh);
this.boxMesh = boxMesh;
}
그리고 update 함수는 다음과 같습니다.
const boxTime = time * .0001 ;
const boxPosition = new THREE. Vector3 ( ) ;
const boxNextPosition = new THREE. Vector2 ( ) ;
this . path . getPointAt ( boxTime % 1 , boxPosition ) ;
this . path . getPointAt ( ( boxTime + 0.01 ) % 1 , boxNextPosition ) ;
this . boxMesh . position . set ( boxPosition. x , 0 , boxPosition. y ) ;
this . boxMesh . lookAt ( boxNextPosition. x , 0 , boxNextPosition. y ) ;
update(time) {
const boxTime = time * .0001;
const boxPosition = new THREE.Vector3();
const boxNextPosition = new THREE.Vector2();
this.path.getPointAt(boxTime % 1, boxPosition);
this.path.getPointAt((boxTime + 0.01) % 1, boxNextPosition);
this.boxMesh.position.set(boxPosition.x, 0, boxPosition.y);
this.boxMesh.lookAt(boxNextPosition.x, 0, boxNextPosition.y);
}
update(time) {
const boxTime = time * .0001;
const boxPosition = new THREE.Vector3();
const boxNextPosition = new THREE.Vector2();
this.path.getPointAt(boxTime % 1, boxPosition);
this.path.getPointAt((boxTime + 0.01) % 1, boxNextPosition);
this.boxMesh.position.set(boxPosition.x, 0, boxPosition.y);
this.boxMesh.lookAt(boxNextPosition.x, 0, boxNextPosition.y);
}
위의 코드 중 7과 8번 라인의 getPointAt은 경로를 구성하는 좌표를 얻을 수 있는데, 이 함수의 첫번째 인자는 0에서 1사이의 값을 가질 수 있고 0일때 경로의 시작점 1일때 경로의 끝점을 얻을 수 있습니다.