[three.js][typescript] - Camera(2)
[three.js][typescript] - Camera(1)Camera는 Scene(장면)을 어떤 시점에서 볼 지 결정하는 중요한 요소이며, 그림자를 생성하는 매우 핵심적인 요소이다. Three.js에서 제공하는 기본적인 카메라는 다음과
yoohwanihn.tistory.com
이전 글에선 Three.js가 제공하는 Perspective Camera, Orthographic Camera에 대해 알아보았다.
이번에는 Camera를 응용해 보자.
기존의 Orthographic Camera에서 일반적으로 사용하는 Perspective Camera로 변경하여 응용해 보자.
카메라가 항상 움직이는 빨간색 구체(smallSphere)를 따라가도록 해보자.
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)
//private camera?: THREE.OrthographicCamera //?를 붙이면 OrthographicCamera 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
)
// const aspect = width / height
// this.camera = new THREE.OrthographicCamera(
// -1*aspect, 1*aspect, // xLeft, xRight
// 1, -1, // yTop, yBottom
// 0.1, 100 // zNear, zFar
// )
//this.camera.zoom = 0.2
this.camera.position.set(2, 2, 3.5) // (2, 2, 3.5)
this.camera.lookAt(new THREE.Vector3(0, 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
// const aspect = width / height
// camera.left = -1*aspect
// camera.right = 1*aspect
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;
const euler = new THREE.Euler(0, time, 0)
const quaternion = new THREE.Quaternion().setFromEuler(euler)
smallSpherePivot.setRotationFromQuaternion(quaternion)
//smallSpherePivot.quaternion.setFromEuler(euler)
const redSphere = smallSpherePivot.children[0] // 빨간색 구체 mesh
const target = new THREE.Vector3() // 빨간색 구의 좌표
redSphere.getWorldPosition(target)
this.camera?.lookAt(target)
}
}
private render(time: number){
// time : setAnimationLoop의 값에 의해서 결정되는데 단위는 ms
this.update(time)
this.renderer.render(this.scene, this.camera!)
}
}
new App()
위와 같이 코드를 수정해 주었다.
먼저 카메라를 Perspective Camera로 변경한 뒤,

카메라가 항상 smallSphere를 따라가게 하기 위해 update를 수정해 주었다.
빨간색 구체의 mesh 정보를 통해 타겟을 정해준 뒤, 0.001ms 단위로 camera가 추적하도록 하였다.

화면(Scene)에 렌더링 되는 붉은색 구체를 지속적으로 update를 하여 회전시키고,
이를 카메라 역시 지속적으로 update 하여 추적하는 결과를 볼 수 있다.
이번에는 반대로 카메라의 시점을 변경시키지 않고, 카메라의 위치를 붉은색 구체로 지정해 보자

기존의 target을 바라보는 코드를 주석처리 하고
target의 포지션을 copy 하는 코드를 추가해 준다.

카메라의 시점은 변하지 않지만 카메라 위치가 붉은색 구체와 동일하게 변경되는 것을 확인할 수 있다.
이번에는 카메라의 타겟도 붉은색 구체가 움직이는 방향으로 수정해 보자.
여러 가지 방법이 있겠지만 타겟이 될 붉은색 구체를 한 개 더 추가하는 방법으로 진행해보려 한다.

먼저, 기존의 looktAt(카메라 시점)과 copy(카메라 위치) 코드를 주석처리한다.
그리고, 붉은색 구체와 동일한 Scene Graph를 사용해 회전하는 구체를 만들어 준다.
(꼭 구체로 할 필요는 없다. 기존의 코드 재활용하는 방법이 쉬워서 그럼)
[three.js][typescript] - Transform(3)
SceneGraph [three.js][typescript] - Transform(2)행렬(Matrix4)을 적용한 Transform [three.js][typescript] - Transform(1)Object3D 클래스 Mesh, Scene, Camera, Audio는 Object3D를 상속받는 클래스다. Object3D는 Position,
yoohwanihn.tistory.com
Scene Graph와 관련된 내용은 Transform에서 한번 다루었다.

setupModels에 Transform에서 만들었던 smallSphere와 동일한 Scene Graph이면서,
동일한 포지션과 로테이션을 지정해 주었다.

구체가 생성되었다. 이제 회전시켜야 한다.

기존의 update에 smallSpherePivot이 회전한다면, smallSpherePivot2도 회전하도록 하였다.
이때 Target의 회전이 10 라디안 앞서도록 하였다.

이제, 앞서가는 붉은색 구체의 위치를 얻어서 카메라의 Target으로 적용시키면 된다.
그런데 앞서가는 붉은색 구체가 꼭 보일 필요는 없다.
우리는 정상적으로 회전되는 물체가 렌더링 되는지 확인이 필요했기 때문에 그 수단으로 구체를 사용했다.
그렇기에 붉은색 구체를 보이지 않도록 수정하자.

setupModels에서 smallSphere2(target)의 Mesh를 Object3D로 변환시켜준다.
이제, 눈에 보이지 않는 Object3D 객체의 위치를 얻어보자.
update 메서드에서 다음과 같이 코드를 수정해 준다.

target Object3D 객체의 위치를 바라보도록 카메라의 시점을 설정해 주었고
기존에 회전하는 붉은색 구체의 위치를 카메라 위치로 지정해 주었다.
결과는 다음과 같다.

이처럼 Camera를 활용하여 절두체 내부의 렌더링 되는 물체들을
다양한 시점, 다양한 방향에서 보여줄 수 있고 사용자로 하여금 다양한 경험을 제공할 수 있다.
다음은 카메라와 밀접한 관계가 있는 그림자(shadow)에 대해 알아보자.
전체 코드(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)
//private camera?: THREE.OrthographicCamera //?를 붙이면 OrthographicCamera 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
)
// const aspect = width / height
// this.camera = new THREE.OrthographicCamera(
// -1*aspect, 1*aspect, // xLeft, xRight
// 1, -1, // yTop, yBottom
// 0.1, 100 // zNear, zFar
// )
//this.camera.zoom = 0.2
this.camera.position.set(2, 2, 3.5) // (2, 2, 3.5)
this.camera.lookAt(new THREE.Vector3(0, 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)
smallSpherePivot.position.y = 0.5 // bigSphere의 y position이 -.5기 때문에
smallSpherePivot.name = "smallSpherePivot" // update에서 접근하기 쉽게 명명함
// target용 redSphere
// const smallSphere2 = new THREE.Mesh(geomSmallSphere, matSmallSphere)
const smallSphere2 = new THREE.Object3D()
const smallSpherePivot2 = new THREE.Object3D()
smallSpherePivot2.add(smallSphere2)
bigSphere.add(smallSpherePivot2)
smallSphere2.position.x = 2
smallSpherePivot2.rotation.y = THREE.MathUtils.degToRad(-45)
smallSpherePivot2.position.y = 0.5
smallSpherePivot2.name = "targetPivot"
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
// const aspect = width / height
// camera.left = -1*aspect
// camera.right = 1*aspect
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;
const euler = new THREE.Euler(0, time, 0)
const quaterion = new THREE.Quaternion().setFromEuler(euler)
smallSpherePivot.setRotationFromQuaternion(quaterion)
//smallSpherePivot.quaternion.setFromEuler(euler)
const redSphere = smallSpherePivot.children[0] // 빨간색 구체 mesh
const target = new THREE.Vector3() // 빨간색 구의 좌표
redSphere.getWorldPosition(target)
//this.camera?.lookAt(target)
//this.camera?.position.copy(target)
const smallSpherePivot2 = this.scene.getObjectByName("targetPivot")
if(smallSpherePivot2){
const euler2= new THREE.Euler(0, time+THREE.MathUtils.degToRad(10),0)
const quaterion2 = new THREE.Quaternion().setFromEuler(euler2)
smallSpherePivot2.setRotationFromQuaternion(quaterion2)
const targetPos = smallSpherePivot2.children[0] // target Object3D 객체
const cameraTarget = new THREE.Vector3()
targetPos.getWorldPosition(cameraTarget) // target Object3D(smallSphere2)의 위치
this.camera?.lookAt(cameraTarget)
this.camera?.position.copy(target)
}
}
}
private render(time: number) {
// time : setAnimationLoop의 값에 의해서 결정되는데 단위는 ms
this.update(time)
this.renderer.render(this.scene, this.camera!)
}
}
new App()'three.js' 카테고리의 다른 글
| [three.js][typescript] - Shader (0) | 2025.04.01 |
|---|---|
| [three.js][typescript] - Shadow(1) (0) | 2024.07.19 |
| [three.js][typescript] - Camera(2) (0) | 2024.07.17 |
| [three.js][typescript] - Camera(1) (1) | 2024.07.16 |
| [three.js][typescript] - Light(3) : HDRI (5) | 2024.07.16 |