-
어쩌다보니 three.js를 잠깐 공부할 계기가 생겨서 잠깐 공부한걸 정리해보려한다.
three.js란?
WebGL은 점, 선, 삼각형만을 그리는 단순한 시스템이고 이런 복잡한 3D요소들의 처리를 도와 직관적으로 코드를 짤 수 있도록 해주는게 Three.js이다.
scene
씬 그래프의 최상위노드로서 배경색, 안개등의 요소를 포함하고 포함한다.
포함된 객체들 또한 부모/ 자식 트리 구조로 이루어진다.
각 객체의 유래와 방향성을 나타낸다.
자식 객체의 위치와 방향이 부모 기준이다.Three.js 구조
mesh
- Meterial로 하나의 Geometry를 그리는 객체이다.
- Meterial,Geometry는 재사용이 가능하여 여러개의 Mesh가 하나의 Meterial 또는 Geometry를 동시에 참조할 수 있다.
- 파란색 정육면체 2개를 그린다고 가정했을때 정육면체의 위치가 달라야하니 Mesh가 2개가 필요하다.
- 정점(꼭지점)데이터를 가진 한개의 Geometry와 채색을 위한 하나의 Meterial이 필요하다
- 이때 Mesh는 객체를 복사할 필요 없이 같은 Geometry,Meterial을 참조할 수 있다.
Geometry
- 기하학 객체의 정점 데이터
- 구, 정육면체, 면, 개 , 고양이 , 사람 , 나무, 건물등 아주 다양한 것이 될 수 있다.
Meterial
- 객체를 그리는 데 사용하는 표면 속성
- 색이나 밝기 등을 지정할 수 있다.
- 하나의 Meterial는 여러개의 Texture를 사용할 수 있다.
Texture
- 이미지나 파일에서 로드한 이미지, canvas로 생성된 이미지 또는 scene객체에서 렌더링한 결과물에 해당한다.
Light
- 여러 종류의 광원에 해당한다.
Hello Cude 만들기
Three.js 로드
<script type="module"> import * as THREE from 'three'; </script>
- 여기서 중요한건 script type를 module로 지정을 해주는거다.
- 모듈로 로드하면 다른 의존성 스크립트를 로드하기 때문이다.
<body> <canvas id="c"></canvas> </body>
<script type="module"> import * as THREE from 'three'; function main() { const canvas = document.querySelector('#c'); const renderer = new THREE.WebGLRenderer({antialias: true, canvas}); ... </script>
- canvas 요소를 참조한 이후 WebGLRenderer를 생성한다.
- 이 렌더러는 입력한 데이터를 실제로 canbas에 그려준다.
PerspectiveCanera(원근 카메라) 객체를 생성
const fov = 75; const aspect = 2; // the canvas default const near = 0.1; const far = 5; const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
fov (field of view) 시야각
- 수직면 75도로 설정
- Three.js의 대부분 각도 단위로 호도(reaians)를 사용하는데, 원근 카메라만 특이하게 도(degrees)를 인자로 받는다.
aspect
- canvas의 가로 세로 비율
- 기본 설정으로 canvas의 크기는 300 / 150 이니 비율도 2로 설정
near, far
- 카메라 앞에 렌더링 되는 공간 범위를 지정하는 요소
- 이 공간 바깥에 있는 요소를 화면에서 잘려나가며, 렌더링 되지 않는다.
위에서 살펴본건 절두체를 만든다.
절두체란?
끝부분이 잘려나간 피라미드 처럼 생긴 3차원 모양near와 far 평면의 높이는 시야각(fov), 너비는 시야각과 aspect에 의해 결정된다.
기본 설정으로 카메라는 -Z 축 +Y 축, 즉 아래를 바라본다.
정육면체를 원점에 놓기 위해서 카메라를 조금 뒤로 움직여 화면 안에 들어오게 한다.
camera.position.z = 2;
카메라는 Z = 2 위치에서 -Z방향을 바라본다.
- 절두체는 카메라 앞 0.1칸에서 5칸까지 차지한다.
- 아래를 바라보는 형태이기 때문에, 시야각은 canvas크기의 영향을 받는다.
- 앞서 만든 canvas는 두 배 더 크기 때문에 실제로는 시야각이 위에서 정의한 75도 보다는 훨씬 넓을 것이다.
Scene 만들기
const scene = new THREE.Scene();
간단한 정육면체 만들기
렌더링되는 대부분의 3D 요소는 정점 데이터가 정의된 기하학 객체를 필요로 한다.
1. 정육면체를 만들기 위해 BoxGeometry생성자를 호출한다.
const boxWidth = 1; const boxHeight = 1; const boxDepth = 1; const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
2. Meterial을 만들고 색을 지정한다. 색은 hex코드를 이용해서 지정한다.
- 여기서 주의할 점은 ‘#’ 대신 ‘0x’ 를 사용한다.
const material = new THREE.MeshBasicMaterial({color: 0x44aa88});
3. 그리고 앞서 만든 Geometry(물체의 형태)와 [Material](<https://threejs.org/docs/#api/ko/materials/Material>)(물체의 색, 밝기, 질감 등)을 이용해 [Mesh](<https://threejs.org/docs/#api/ko/objects/Mesh>)를 만든다.
- [Mesh](<https://threejs.org/docs/#api/ko/objects/Mesh>)는 Geometry, [Material](<https://threejs.org/docs/#api/ko/materials/Material>) 외에도 물체의 위치, 방향, 크기 등을 담은 객체이다.
const cube = new THREE.Mesh(geometry, material);
4. 마지막으로 완성된 성육면체 Mesh를 Scene에 넣어준다.
scene.add(cube);
5. renderer의 render 메서드에 Scene과 Camera를 넘겨주면 화면을 렌더링할 수 있다.
renderer.render(scene, camera);
코드 상으로 3D정육면체를 그렸지만, 카메라가 -Z방향을 바라보고 정육면체도 Z축에 맞추어 정렬되어 있기 때문에 한 면만 보인다.
애니메이션 구현을 위해 requestAnimationFrame 루프 안에서 렌더링 함수를 호출한다.
function render(time) { time *= 0.001; // convert time to seconds cube.rotation.x = time; cube.rotation.y = time; renderer.render(scene, camera); requestAnimationFrame(render); } requestAnimationFrame(render);
requestAnimationFrame
- 브라우저에 애니메이션 프레임을 요청하는 함수
- 페이지가 로드된 이후의 시간값을 밀리초 단위로 넘겨준다. (위 예제는 초단위로 변경함)
- 정육면체의 x,y축 회전값을 현재 시간값으로 설정한다. (이 회전값은 라디안(redians) 단위를 사용)
- 360˚도는 2π 라디안이니 큐브는 각 축마다 약 6.28초에 한 바퀴를 돌게 된다.
- 씬을 렌더링 한 후, 브라우저에 재귀적으로 애니메이션 프레임을 요청해 이 애니메이션이 반복되도록 한다.
- 마지막으로 루프 바깥에서 requestAnimationFrame을 한 번 호출해 루프를 시작한다
DirectionalLight 광원
{ const color = 0xFFFFFF; const intensity = 3; const light = new THREE.DirectionalLight(color, intensity); light.position.set(-1, 2, 4); scene.add(light); }
- 위치(position)와 목표(target) 속성이 있다.
- 기본값은 0, 0, 0 이다.
- position을 -1, 2, 4로 설정해 카메라보다 약간 동서쪽, Z축으로는 약간 위로 보낸다.
- target은 기본값 0,0,0 그대로 두어 공간의 중앙을 비추도록 한다.
- MeshBasicMaterial은 광원에 반응하지 않으니 관원에 반응하는 MeshPhongMaterial로 바꿔준다.
- (phong은 광원 반사 모델을 처음 개발한 사람 이름)
위에서 만든 정육면체를 이용해서 2개 더 만들어보기
1. 미리 만든 Geometry를 사용하여 새로운 큐브를 만드는 함수를 만든다.
function makeInstance(geometry, color, x) { const material = new THREE.MeshPhongMaterial({color}); const cube = new THREE.Mesh(geometry, material); scene.add(cube); cube.position.x = x; return cube; }
- 위 함수는 넘겨받은 색상값으로 새로운 Material을 만들고, 넘겨받은 Geometrydhk whgkqgo tofhdns Mesh를 만든다.
- 씬에 추가한 후 넘겨받은 X축 값을 통해 물체를 이동한다.
2. 3가지 다른 색상과 X축 값으로 이 함수를 호출해 결과물을 배열로 저장한다.
const cubes = [ makeInstance(geometry, 0x44aa88, 0), makeInstance(geometry, 0x8844aa, -2), makeInstance(geometry, 0xaa8844, 2), ];
3. render함수에서 3개의 정육면체를 회전한다. 동적인 효과를 주기 위해 각 큐브마다 약간 다른값을 주도록 한다.
function render(time) { time *= 0.001; // convert time to seconds cubes.forEach((cube, ndx) => { const speed = 1 + ndx * .1; const rot = time * speed; cube.rotation.x = rot; cube.rotation.y = rot; }); ...
X축으로 -2, +2만큼 이동한 정육면체들의 일부가 절두체에서 약간 벗어났고 가운데 정육면체에 비해 굉장히 굴절되어 보인다.
원인은 설정한 시야각이 너무 좁은 탓이다.
프로그램 구조
- Mesh객체는 같은 BoxGeometry를 참조한다.
- 각 Mesh는 다른 MesgPhongMaterial을 참조하므로 다른 색은 띈다.
Responsive Design(반응형 디자인)
canvas요소의 기본 크기 : 300 x 150 px
canvas가 페이지 전체를 차지하도록 CSS 작성
<style> html, body { margin: 0; height: 100%; } #c { width: 100%; height: 100%; display: block; } </style>
- 바디에 기본적으로 있는 마진 5px을 0으로 지정해 여백을 없앤다.
- 높이를 100%로 맞춰 창전체를 채운다.
- id=c 인 요소의 크기를 지정해 컨테이너를 body요소의 크기와 동일하게 맞춘다.
canvas요소의 기본 display 속성은 inline이다. inline 속성은 글자처럼 취급되어 흰공백을 남길 수 있어 block으로 지정한다.
창 크기에 따라 늘어나 정육면체가 너무 길거나 넓은 육면체처럼 보인다.
또한 창을 크게하면 저화질로 깨지고 흐릿하게 보인다.
창 크기에 따라 늘어나는 문제 해결하기
- 카메라의 aspect(비율) 속성을 canvas의 화면 크기에 맞춰야 한다.
- clientWidth, clientHeight속성을 이용
function render(time) { time *= 0.001; const canvas = renderer.domElement; camera.aspect = canvas.clientWidth / canvas.clientHeight; camera.updateProjectionMatrix(); ...
이렇게 하면 창 크기와 상관없이 정육면체 크기가 그대로 유지된다.
계단현상 문제 해결하기
- CSS로 설정한 canvas요소의 크기
- canvas원본 픽셀 수에 대한 값
ex) 128 x 64 픽셀인 이미지가 있을때 CSS를 이용해 이 이미지 요소를 400 x200 픽셀로 보이도록 할 수 있는 것 처럼 canvas도 마찬가지다.
<img src="some128x64image.jpg" style="width:400px; height:200px">
드로잉버퍼(drawingbuffer)
- canvas의 원본 크기, 해상도
- renderer.setSize 메서드를 호출해 canvas의 드로잉 버퍼 크기를 지정할 수 있다.
- canvas의 디스플레이 크기로 지정 clientWidth, clientHeight속성을 이용
canvas의 원본 크기와 디스플레이 크기를 비교해 원본 크기를 변경할지 결정하는 함수를 하나 만들어준다.
function resizeRendererToDisplaySize(renderer) { const canvas = renderer.domElement; const width = canvas.clientWidth; const height = canvas.clientHeight; const needResize = canvas.width !== width || canvas.height !== height; if (needResize) { renderer.setSize(width, height, false); } return needResize; }
canvas를 리사이징할 필요가 있는지 검사했다는 점에 주의!. canvas 스펙상 리사이징은 화면을 다시 렌더링해야만 하므로, 같은 사이즈일 때는 리사이징을 하지 않으므로써 불필요한 자원 낭비를 막는 것이 좋다.
canvas의 크기가 다르다면, renderer.setSize메서드를 호출해 새로운 크기를 넘겨준다.
- 마지막 인자로 false를 넘겨줘야한다.
위 함수는 canvas를 리사이징 했으면 true를 반환한다. 이 값을 이용해 다른 요소들이 업데이트 될 수 있다.
function render(time) { time *= 0.001; if (resizeRendererToDisplaySize(renderer)) { const canvas = renderer.domElement; camera.aspect = canvas.clientWidth / canvas.clientHeight; camera.updateProjectionMatrix(); } ...
canvas의 비율이 변하려면 canvas의 사이즈가 변해야 하므로, resizeRendererToDisplaySize 함수가 true를 반환했을 때만 카메라의 비율을 변경합니다.
이렇게 되면 디스플레이 크기에 맞는 해상도로 렌더링 되게 된다.
HD-DPI(high-density dot per inch 고해상도) 디스플레이 다루기
- 많은 windows 기기나 맥, 스마트폰
- 스마트폰의 실제 화면 크기가 데스크탑에 비해 훨씬 작지만 해상도는 비슷하다.
- 한 픽셀을 선명하게 표현하기 위해 다수의 작은 픽셀을 넣는것
- 브라우저에서는 이에 대응하기 위해 픽셀의 집적도에 상관 없이 CSS 픽셀을 이용해 요소의 크기를 지정한다.
- 아무것도 하지 않는다.
- 3D렌더링은 많은 GPU 자원을 소모한다.
- HD-DPI와 비교했을떄 9배나 더 많은 렌더링 작업을 처리해야 한다.
- 코드가 1픽셀을 계산 할 때 마다 브라우저는 해당 픽셀보다 3배 큰 픽셀을 렌더링 해야한다.
- 이는 낮은 FPS, 즉 화면이 버벅거리에 만들것이므로 특히 무거운 Three.js앱을 만들 때는 지양해야한다.
- rederer.setPixelRatio메서드를 이용해 해상도 배율을 알려준다.
- 브라우저로부터 CSS픽셀과 실제 기기 픽셀의 배율을 받아 Three.js에게 넘겨주는 것이다.
알아서 사이즈에 배율을 곱해 리사이징한다. (이방법도 추천 x)renderer.setPixelRatio(window.devicePixelRatio);
- canvas를 리사이징할 떄 직접 계산한다.
function resizeRendererToDisplaySize(renderer) { const canvas = renderer.domElement; const pixelRatio = window.devicePixelRatio; const width = canvas.clientWidth * pixelRatio | 0; const height = canvas.clientHeight * pixelRatio | 0; const needResize = canvas.width !== width || canvas.height !== height; if (needResize) { renderer.setSize(width, height, false); } return needResize; }
이 방법으로는 개발자가 원하는 결과가 나온다. 여러 상황에 따라 Three.js가 쓰는 크기가 무엇인지 일일이 계산해서 사용해야 확실히 알 수 있고, 예외도 줄어든다.
Prerequisites
defer
브라우저가 스크립트를 문서 분석 이후에, 그러나 DOMContentLoaded발생 이전에 실행해야 함을 나타내는 불리언 속성입니다. defer 속성을 가진 스크립트는 자신의 평가가 끝나기 전까지 DOMContentLoaded 이벤트의 발생을 막습니다.처음 공부를 하다보니 어렵게만 느껴지고 이제 튜토리얼인데 과연 뒤에 이해가 갈까 싶었고, 다음번엔 뭔가 만들어보는걸 블로그를 적을 예정이다.