Camera는 Scene(장면)을 어떤 시점에서 볼 지 결정하는 중요한 요소이며, 그림자를 생성하는 매우 핵심적인 요소이다.
Three.js에서 제공하는 기본적인 카메라는

다음과 같이 PerspectiveCamera, OrthographicCamera이며 Object3D를 상속받으며, Camera를 상속받는다.
Object3D를 상속받는 클래스의 객체는 Scene(장면)을 구성하는 객체로 사용될 수 있다.
장면을 구성할 수 있다는 것은 이동, 회전, 크기등을 변환(Transform)할 수 있다는 것이다.
카메라는 Scene에서 특정한 위치에 놓이고 특정한 것을 관찰하기 위해 사용된다.

PerspectiveCamera는 지금까지 쭉 사용해 왔기 매우 익숙할 것이다.
가까이 있는 물체는 크게 보이고 멀리 있는 물체는 더 작게 보이는 원근감을 표현한 카메라이다.
OrthographicCamera는 지금까지 사용해 온 카메라와는 다르게 원근감을 표현하지 않는다.
오브젝트가 카메라의 거리와는 상관없이 모두 정해진 크기로 표현된다.
그렇기에 이 OrthographicCamera는 CAD와 같은 설계 프로그램의 뷰를 구성하는 데 사용된다.
두 개의 카메라를 코드를 통해 살펴보자.
학습을 진행하기 위해서 기존의 코드를 활용하자.
기존의 Transform을 학습할 때 사용한 코드를 재활용할 것이다.
three.js/03_Transform at main · yoohwanihn/three.js
[three.js][typescript]. Contribute to yoohwanihn/three.js development by creating an account on GitHub.
github.com
위 깃의 Transform을 clone 하거나 기존의 Transform 코드를 복사한다.
import { OrbitControls } from 'three/examples/jsm/Addons.js'
import './style.css'
import * as THREE from 'three'
class App {
private renderer: THREE.WebGLRenderer //Renderer Field 추가
private domApp: Element
private scene: THREE.Scene
private camera?: THREE.PerspectiveCamera //?를 붙이면 PerspectiveCamera Type이나 Undefined Type을 가질 수 있음.(Optional Properties)
constructor(){
console.log("YooHwanIhn");
this.renderer = new THREE.WebGLRenderer({antialias:true}) // 안티-알리아스 : 높은 렌더링 결과를 얻기 위해 픽셀 사이에 계단 현상을 방지하는 효과 추가
this.renderer.setPixelRatio(Math.min(2, window.devicePixelRatio)) // 고해상도에서 깨짐 방지를 위해 픽셀 비율 지정
this.domApp = document.querySelector('#app')!
this.domApp.appendChild(this.renderer.domElement) // renderer.domeElement : canvas Type의 DOM 객체
this.scene = new THREE.Scene()
this.setupCamera()
this.setupLight()
this.setupModels()
this.setupEvents()
}
private setupCamera(){
const width = this.domApp.clientWidth
const height = this.domApp.clientHeight
this.camera = new THREE.PerspectiveCamera(75, width/height, 0.1, 100)
this.camera.position.z = 5 // (0, 0, 5)
new OrbitControls(this.camera, this.domApp as HTMLElement)
}
private setupLight(){
//빛의 색상 값과 빛의 강도
const color = 0xffffff // white
const intensity = 1
const light = new THREE.DirectionalLight(color, intensity) //광원
light.position.set(-1, 2, 4)
this.scene.add(light)
}
private setupModels(){
const axisHelper = new THREE.AxesHelper(10) // 좌표축
this.scene.add(axisHelper)
const geomGround = new THREE.PlaneGeometry(5, 5) // ground (5x5)
const matGround = new THREE.MeshStandardMaterial({
color: "#2c3e50",
roughness: 0.5,
metalness: 0.5,
side: THREE.DoubleSide
})
const ground = new THREE.Mesh(geomGround, matGround)
ground.rotation.x = -THREE.MathUtils.degToRad(90)
ground.position.y = -.5
this.scene.add(ground)
const geomBigSphere = new THREE.SphereGeometry(1, 32, 16, 9, THREE.MathUtils.degToRad(360), 0, THREE.MathUtils.degToRad(90))
const matBigSphere = new THREE.MeshStandardMaterial({
color: "#ffffff",
roughness: 0.1,
metalness: 0.2
});
const bigSphere = new THREE.Mesh(geomBigSphere,matBigSphere)
bigSphere.position.y = -.5
this.scene.add(bigSphere)
const geomSmallSphere = new THREE.SphereGeometry(0.2)
const matSmallSphere = new THREE.MeshStandardMaterial({
color: "#e74c3c",
roughness: 0.2,
metalness: 0.5
})
const smallSphere = new THREE.Mesh(geomSmallSphere, matSmallSphere)
const smallSpherePivot = new THREE.Object3D();
smallSpherePivot.add(smallSphere)
bigSphere.add(smallSpherePivot);
smallSphere.position.x = 2
smallSpherePivot.rotation.y = THREE.MathUtils.degToRad(-45)
smallSphere.position.y = 0.5 // bigSphere의 y position이 -.5기 때문에
smallSpherePivot.name = "smallSpherePivot" // update에서 접근하기 쉽게 명명함
const cntItems = 8
const geomTorus = new THREE.TorusGeometry(0.3, 0.1)
const matTorus = new THREE.MeshStandardMaterial({
color: "#9b59b6",
roughness: 0.5,
metalness: 0.9
})
for(let i=0; i<cntItems; i++){
const torus = new THREE.Mesh(geomTorus, matTorus)
const torusPivot = new THREE.Object3D() // Torus 역시 반 구를 기준으로 회전하면서 8개를 생성하기 때문에 피봇이 필요함
bigSphere.add(torusPivot)
torus.position.x = 2 // smallSphere의 x 포지션과 일치하게함
torusPivot.position.y = 0.5 // bigSphere의 y 포지션이 -.5임
torusPivot.rotation.y = THREE.MathUtils.degToRad(360) / cntItems * i
torusPivot.add(torus)
}
}
//실제 이벤트와 렌더링 처리를 다룰 메서드
private setupEvents(){
window.onresize = this.resize.bind(this); // html의 size가 변경될 때마다 호출되는 함수(addEventListener 느낌??)
this.resize();
this.renderer.setAnimationLoop(this.render.bind(this)) // 연속적으로 render 메서드 호출(모니터 Frame에 맞게)
}
// html창 resize시 호출할 함수
private resize(){
const width = this.domApp.clientWidth
const height = this.domApp.clientHeight
//앞선 setUpCamera에서 설정한 camera 정보를 다시 수정해줌.
const camera = this.camera
if(camera){
camera.aspect = width / height
camera.updateProjectionMatrix() // 카메라의 값이 변경되었다면 수정하도록 함.
}
//renderer도 마찬가지로 사이즈 수정함
this.renderer.setSize(width, height)
}
private update(time: number){
time *= 0.001 // ms -> s 단위로 변경
const smallSpherePivot = this.scene.getObjectByName("smallSpherePivot")
if(smallSpherePivot){
smallSpherePivot.rotation.y = time;
}
}
private render(time: number){
// time : setAnimationLoop의 값에 의해서 결정되는데 단위는 ms
this.update(time)
this.renderer.render(this.scene, this.camera!)
}
}
new App()
코드를 위와 같이 변경시킨다. 변경된 점으로는
matGround, matSphere, matTorus 재질에 color, roughness, metalness 옵션을 추가해 주었다.

위와 같이 Transform에서 만들었던 결과에서 색, 금속성, 거칠기가 수정된 결과가 나올 것이다.

카메라에 해당하는 부분의 코드를 살펴보자
Three.js가 제공하는 PerpectiveCamera 클래스의 생성자를 사용하였고 인자값으로 4개의 값을 건네주었다.

이 4개의 인자는 Fov, Aspect, zNear, zFar인데, 이 4개의 인자를 통해 연두색의 육면체가 생성된다.
이 연두색의 육면체를 절두체라 하는데 이 절두체에 포함되거나 교차하는 물체가
카메라에 보여지게 되고 화면상에 렌더링 된다. (zNear와 zFar 사이에 존재하는 물체만 렌더링)
이 절두체에 포함되거나 교차되지 않으면 화면에 렌더링 되지 않는다.
PerspectiveCamera 파라미터
코드를 통해 PerspectiveCamera의 인자를 살펴보면

첫 번째 인자 Fov(카메라의 화각)는 75를 사용하였다. 이때 단위는 radian이 아닌 degree임을 명시하자.
두 번째 인자는 Aspect로

3차원에 출력되는 dom요소의 가로를 세로로 나눈 값을 카메라의 Aspect 값으로 지정해 주었다.
이는, dom요소의 크기가 변경될 때 Aspect값 역시 변경되는 의미이다.

창 크기가 변경될 때 위와 같이 resize함수를 통해 변경된 dom 크기에 맞춰 Aspect 역시 변경되게끔
이전에 코드를 작성한 이유가 카메라의 2번째 인자로 Aspect 값을 넘겨줘야 하기 때문이었다.
이때 변화된 값을 적용시키기 위해선 Camera 객체의 updateProjectionMatrix 메서드를 호출해야 한다.

그리고 3, 4번째 인자로 zNear와 zFar를 지정해 주었는데, zNear를 0.1에서 5로 수정해 보았다.
결과를 통해 확인해 보자.

zNear(5)와 zFar(100) 사이에 존재하는 물체만 렌더링 되기 때문에 카메라의 각도에 따라 렌더링 되는 물체가 다르다.
zFar는 따로 수정하지 않겠다. 결과가 예상되지 않는가??
Perspective Camera 포지션, 방향
카메라 포지션을 수정하는 코드는 많이 다뤄 보았다.

기존의 카메라 포지션은 z 축을 5로 설정하여 (0, 0, 5)에 위치한다.

이를 위와 같이 set 메서드 호출로 한 번에 설정할 수 있다는 것은 지난 글에서 다루었기 때문에 알고 있을 것이다.
그리고 당연하게도 (0, 0, 5)로 동일한 포지션에 위치할 것이다.
이번엔 카메라가 바라보는 방향을 수정해 보자.

setupCamera에 다음 코드를 추가해 준다.
그러면 결과에 아무런 변화가 없을 것이다.. 그 이유는 OrbitControls를 인스턴스화하기 때문인데,
OrbitControls가 카메라가 바라보는 지점을 내부적으로 제어하고 있기 때문이다.
해당 코드를 주석처리하면

(0, 0, 5) 포지션의 카메라가 (2, 0, 0) 방향을 바라보는 결과를 확인할 수 있다.
다음 글에서는 Perspective Camera에 이어 Orthographic Camera에 대해 알아보자.
전체 코드(main.ts)
import { OrbitControls } from 'three/examples/jsm/Addons.js'
import './style.css'
import * as THREE from 'three'
class App {
private renderer: THREE.WebGLRenderer //Renderer Field 추가
private domApp: Element
private scene: THREE.Scene
private camera?: THREE.PerspectiveCamera //?를 붙이면 PerspectiveCamera Type이나 Undefined Type을 가질 수 있음.(Optional Properties)
constructor(){
console.log("YooHwanIhn");
this.renderer = new THREE.WebGLRenderer({antialias:true}) // 안티-알리아스 : 높은 렌더링 결과를 얻기 위해 픽셀 사이에 계단 현상을 방지하는 효과 추가
this.renderer.setPixelRatio(Math.min(2, window.devicePixelRatio)) // 고해상도에서 깨짐 방지를 위해 픽셀 비율 지정
this.domApp = document.querySelector('#app')!
this.domApp.appendChild(this.renderer.domElement) // renderer.domeElement : canvas Type의 DOM 객체
this.scene = new THREE.Scene()
this.setupCamera()
this.setupLight()
this.setupModels()
this.setupEvents()
}
private setupCamera(){
const width = this.domApp.clientWidth
const height = this.domApp.clientHeight
this.camera = new THREE.PerspectiveCamera(75, width/height, 0.1, 100)
this.camera.position.set(0, 0, 5) // (0, 0, 5)
this.camera.lookAt(new THREE.Vector3(2, 0, 0))
//new OrbitControls(this.camera, this.domApp as HTMLElement)
}
private setupLight(){
//빛의 색상 값과 빛의 강도
const color = 0xffffff // white
const intensity = 1
const light = new THREE.DirectionalLight(color, intensity) //광원
light.position.set(-1, 2, 4)
this.scene.add(light)
}
private setupModels(){
const axisHelper = new THREE.AxesHelper(10) // 좌표축
this.scene.add(axisHelper)
const geomGround = new THREE.PlaneGeometry(5, 5) // ground (5x5)
const matGround = new THREE.MeshStandardMaterial({
color: "#2c3e50",
roughness: 0.5,
metalness: 0.5,
side: THREE.DoubleSide
})
const ground = new THREE.Mesh(geomGround, matGround)
ground.rotation.x = -THREE.MathUtils.degToRad(90)
ground.position.y = -.5
this.scene.add(ground)
const geomBigSphere = new THREE.SphereGeometry(1, 32, 16, 9, THREE.MathUtils.degToRad(360), 0, THREE.MathUtils.degToRad(90))
const matBigSphere = new THREE.MeshStandardMaterial({
color: "#ffffff",
roughness: 0.1,
metalness: 0.2
});
const bigSphere = new THREE.Mesh(geomBigSphere,matBigSphere)
bigSphere.position.y = -.5
this.scene.add(bigSphere)
const geomSmallSphere = new THREE.SphereGeometry(0.2)
const matSmallSphere = new THREE.MeshStandardMaterial({
color: "#e74c3c",
roughness: 0.2,
metalness: 0.5
})
const smallSphere = new THREE.Mesh(geomSmallSphere, matSmallSphere)
const smallSpherePivot = new THREE.Object3D();
smallSpherePivot.add(smallSphere)
bigSphere.add(smallSpherePivot);
smallSphere.position.x = 2
smallSpherePivot.rotation.y = THREE.MathUtils.degToRad(-45)
smallSphere.position.y = 0.5 // bigSphere의 y position이 -.5기 때문에
smallSpherePivot.name = "smallSpherePivot" // update에서 접근하기 쉽게 명명함
const cntItems = 8
const geomTorus = new THREE.TorusGeometry(0.3, 0.1)
const matTorus = new THREE.MeshStandardMaterial({
color: "#9b59b6",
roughness: 0.5,
metalness: 0.9
})
for(let i=0; i<cntItems; i++){
const torus = new THREE.Mesh(geomTorus, matTorus)
const torusPivot = new THREE.Object3D() // Torus 역시 반 구를 기준으로 회전하면서 8개를 생성하기 때문에 피봇이 필요함
bigSphere.add(torusPivot)
torus.position.x = 2 // smallSphere의 x 포지션과 일치하게함
torusPivot.position.y = 0.5 // bigSphere의 y 포지션이 -.5임
torusPivot.rotation.y = THREE.MathUtils.degToRad(360) / cntItems * i
torusPivot.add(torus)
}
}
//실제 이벤트와 렌더링 처리를 다룰 메서드
private setupEvents(){
window.onresize = this.resize.bind(this); // html의 size가 변경될 때마다 호출되는 함수(addEventListener 느낌??)
this.resize();
this.renderer.setAnimationLoop(this.render.bind(this)) // 연속적으로 render 메서드 호출(모니터 Frame에 맞게)
}
// html창 resize시 호출할 함수
private resize(){
const width = this.domApp.clientWidth
const height = this.domApp.clientHeight
//앞선 setUpCamera에서 설정한 camera 정보를 다시 수정해줌.
const camera = this.camera
if(camera){
camera.aspect = width / height
camera.updateProjectionMatrix() // 카메라의 값이 변경되었다면 수정하도록 함.
}
//renderer도 마찬가지로 사이즈 수정함
this.renderer.setSize(width, height)
}
private update(time: number){
time *= 0.001 // ms -> s 단위로 변경
const smallSpherePivot = this.scene.getObjectByName("smallSpherePivot")
if(smallSpherePivot){
smallSpherePivot.rotation.y = time;
}
}
private render(time: number){
// time : setAnimationLoop의 값에 의해서 결정되는데 단위는 ms
this.update(time)
this.renderer.render(this.scene, this.camera!)
}
}
new App()
'three.js' 카테고리의 다른 글
| [three.js][typescript] - Camera(3) (0) | 2024.07.18 |
|---|---|
| [three.js][typescript] - Camera(2) (0) | 2024.07.17 |
| [three.js][typescript] - Light(3) : HDRI (5) | 2024.07.16 |
| [three.js][typescript] - Light(2) (1) | 2024.07.16 |
| [three.js][typescript] - Light(1) (0) | 2024.07.15 |