💡 시작하기
오늘은 본격적으로 three.js를 사용해서 포트폴리오 사이트를 만들기 전, three.js에 관한 문서를 읽고 개념과 사용 방법을 정리하고, 포트폴리오 사이트의 배경을 먼저 설정해보기로 하였다.
⚙️ Three.js의 개념과 기본 구조?
Three.js란?
웹페이지에 3D 객체를 쉽게 렌더링하도록 도와주는 자바스크립트 3D 라이브러리
Three.js의 구조
three.js를 사용하려면 반드시 알아야 할 개념 3가지가 존재한다.
⇒ Scene, Camera, Renderer
- Scene : 3D object가 존재하는 공간이다. Scene 그래프의 최상위 노드로서 배경색, 안개(fog) 등의 요소를 포함한다.
- Scene에 포함된 객체들 또한 부모/자식의 트리 구조로 이루어지며, 이는 각 객체의 유래와 방향성을 나타낸다.
ex) 자동차(부모) → 바퀴(자식) : 자동차 객체의 방향을 움직일 때, 바퀴 객체의 방향 또한 같이 움직인다.
- Scene에 포함된 객체들 또한 부모/자식의 트리 구조로 이루어지며, 이는 각 객체의 유래와 방향성을 나타낸다.
- Camera : 3D object가 존재하는 Scene을 바라보는 역할을 한다. 굳이 Scene 그래프에 포함될 필요가 없다.
- Renderer : Three.js의 핵심 객체이다. Scene과 Camera 객체를 넘겨 받아 3D 씬의 일부를 평면 이미지로 렌더링한다.
- Mesh : 어떤 Material로 하나의 Geometry를 그리는 객체이다.
- Geometry : 기하학 객체의 정점 데이터
ex) sphere, cube, plane, 개, 고양이, 나무, 사람, 건물 등 - Material : 기하학 객체를 그리는 데 사용하는 표면 속성
ex) 색, 밝기 등
- 하나의 Material는 여러 개의 Texture 사용 가능 - Texture : 이미지나 파일에서 로드한 이미지, canvas로 생성한 이미지 또는 다른 Scene 객체에서 렌더링한 결과물
- Light : 여러 종류의 광원
- Geometry : 기하학 객체의 정점 데이터
🖥️ 직접 사용해보기
Three.js를 react 문법에 맞게 사용할 수 있도록 해주는 라이브러리가 있다. 바로 @react-three/fiber 다.
그래서 요것도 저번에 함께 깔아줬던 것! (타이틀도 Three.js에서 R3F로 바꿨다 ㅎ)
function App() {
return (
<div className='App'>
<Canvas>
</Canvas>
</div>
)
}
처음엔 Canvas를 불러와야 한다.
그리고 Canvas 컴포넌트에 원하는 Three 관련 컴포넌트를 넣어주면 된다.
(개발일지 1에서 boxGeometry를 사용하여 cube 객체를 하나 만들어줬었다.)
처음에 무엇을 먼저 구현해볼까 하다가 포트폴리오에 Three.js로 만든 배경을 먼저 만들기로 결정하였다.
R3F 공식문서에 있는 example을 둘러보다가 구름 예제가 있어 가져와봤다!
↓ 전체 코드
import { Canvas, useFrame } from '@react-three/fiber';
import { Clouds, Cloud, CameraControls, Sky as SkyImpl, StatsGl, useFaceControls } from "@react-three/drei"
import * as THREE from 'three';
import { useRef } from 'react';
function Sky() {
const ref = useRef()
const cloud0 = useRef()
const { color, x, y, z, range, ...config } = useFaceControls({
seed: { value: 1, min: 1, max: 100, step: 1 },
segments: { value: 20, min: 1, max: 80, step: 1 },
volume: { value: 6, min: 0, max: 100, step: 0.1 },
opacity: { value: 0.8, min: 0, max: 1, step: 0.01 },
fade: { value: 10, min: 0, max: 400, step: 1 },
growth: { value: 4, min: 0, max: 20, step: 1 },
speed: { value: 0.1, min: 0, max: 1, step: 0.01 },
x: { value: 6, min: 0, max: 100, step: 1 },
y: { value: 1, min: 0, max: 100, step: 1 },
z: { value: 1, min: 0, max: 100, step: 1 },
color: "white",
})
useFrame((state, delta) => {
ref.current.rotation.y = Math.cos(state.clock.elapsedTime / 5) / 4
ref.current.rotation.x = Math.sin(state.clock.elapsedTime / 5) / 4
cloud0.current.rotation.y -= delta
})
return (
<>
<SkyImpl />
<group ref={ref}>
<Clouds material={THREE.MeshLambertMaterial} limit={400} range={range}>
<Cloud ref={cloud0} {...config} bounds={[x, y, z]} color={color} />
<Cloud {...config} bounds={[x, y, z]} color="#eed0d0" seed={2} position={[15, 0, 0]} />
<Cloud {...config} bounds={[x, y, z]} color="#d0e0d0" seed={3} position={[-15, 0, 0]} />
<Cloud {...config} bounds={[x, y, z]} color="#a0b0d0" seed={4} position={[0, 0, -12]} />
<Cloud {...config} bounds={[x, y, z]} color="#c0c0dd" seed={5} position={[0, 0, 12]} />
<Cloud concentrate="outside" growth={100} color="#ffccdd" opacity={1.25} seed={0.3} bounds={200} volume={200} />
</Clouds>
</group>
</>
)
}
function App() {
return (
<Canvas
camera={{ position: [0, -10, 10], fov: 75 }}
style={{width: '100vw', height: '100vh'}}
>
<StatsGl />
<Sky />
<ambientLight intensity={Math.PI / 1.5} />
<spotLight position={[0, 40, 0]} decay={0} distance={45} penumbra={1} intensity={100} />
<spotLight position={[-20, 0, 10]} color="red" angle={0.15} decay={0} penumbra={-1} intensity={30} />
<spotLight position={[20, -10, 10]} color="red" angle={0.2} decay={0} penumbra={-1} intensity={20} />
{/* <CameraControls /> */}
</Canvas>
);
}
export default App;
↓ Sky 컴포넌트: 하늘을 배경으로 하는 구름을 렌더링하는 컴포넌트
function Sky() {
const ref = useRef()
const cloud0 = useRef()
const { color, x, y, z, range, ...config } = useFaceControls({
seed: { value: 1, min: 1, max: 100, step: 1 },
segments: { value: 20, min: 1, max: 80, step: 1 },
volume: { value: 6, min: 0, max: 100, step: 0.1 },
opacity: { value: 0.8, min: 0, max: 1, step: 0.01 },
fade: { value: 10, min: 0, max: 400, step: 1 },
growth: { value: 4, min: 0, max: 20, step: 1 },
speed: { value: 0.1, min: 0, max: 1, step: 0.01 },
x: { value: 6, min: 0, max: 100, step: 1 },
y: { value: 1, min: 0, max: 100, step: 1 },
z: { value: 1, min: 0, max: 100, step: 1 },
color: "white",
})
useFrame((state, delta) => {
ref.current.rotation.y = Math.cos(state.clock.elapsedTime / 5) / 4
ref.current.rotation.x = Math.sin(state.clock.elapsedTime / 5) / 4
cloud0.current.rotation.y -= delta
})
return (
<>
<SkyImpl />
<group ref={ref}>
<Clouds material={THREE.MeshLambertMaterial} limit={400} range={range}>
<Cloud ref={cloud0} {...config} bounds={[x, y, z]} color={color} />
<Cloud {...config} bounds={[x, y, z]} color="#eed0d0" seed={2} position={[15, 0, 0]} />
<Cloud {...config} bounds={[x, y, z]} color="#d0e0d0" seed={3} position={[-15, 0, 0]} />
<Cloud {...config} bounds={[x, y, z]} color="#a0b0d0" seed={4} position={[0, 0, -12]} />
<Cloud {...config} bounds={[x, y, z]} color="#c0c0dd" seed={5} position={[0, 0, 12]} />
<Cloud concentrate="outside" growth={100} color="#ffccdd" opacity={1.25} seed={0.3} bounds={200} volume={200} />
</Clouds>
</group>
</>
)
}
- useFaceControls : 다양한 구름의 속성값을 동적으로 제어할 수 있다.
- seed : 구름의 초기 무작위 값을 설정
- 구름의 모양과 배치를 제어하는 랜덤성 제공
- segment : 구름을 이루는 구성 요소(segment)의 개수 결정
- segment가 많을수록 더 복잡한 형태의 구름을 렌더링함
- volume : 구름의 크기를 결정
- opacity : 구름의 불투명도 결정
- fade : 구름의 가장자리가 어떻게 페이드될지 설정
- 페이드값이 클수록 구름의 가장자리가 더 부드럽게 흐려짐
- growth : 구름의 팽창률 조정
- 값이 클수록 구름이 더 팽창함
- speed : 구름이 움직이는 속도
- x, y, z : 구름의 위치 설정
- color : 구름의 색상 지정
- useFrame : 매 프레임마다 구름의 회전 애니메이션을 구현한다.
- SkyImpl : 하늘의 배경을 생성한다.
구름의 개수를 조절하기 위하여 Clouds에 있는 limit를 줄여주고, 위치 등과 같은 속성들을 조절하였다.
<Clouds material={THREE.MeshLambertMaterial} limit={30} range={range}>
<Cloud ref={cloud0} {...config} bounds={[x, y, z]} color={color} />
<Cloud concentrate="outside" growth={200} color="#ffccdd" opacity={1.25} seed={0.3} bounds={300} volume={300} />
</Clouds>
↓ App 컴포넌트
function App() {
return (
<Canvas
camera={{ position: [0, -10, 10], fov: 75 }}
style={{width: '100vw', height: '100vh'}}
>
<StatsGl />
<Sky />
<ambientLight intensity={Math.PI / 1.5} />
<spotLight position={[0, 40, 0]} decay={0} distance={45} penumbra={1} intensity={100} />
<spotLight position={[-20, 0, 10]} color="red" angle={0.15} decay={0} penumbra={-1} intensity={30} />
<spotLight position={[20, -10, 10]} color="red" angle={0.2} decay={0} penumbra={-1} intensity={20} />
<CameraControls />
</Canvas>
);
}
export default App;
- Canvas
- camera : 카메라의 시야각 설정
- position : 카메라의 위치 선정 - CameraControls : 카메라 제어, 사용자가 3D 환경을 드래그하거나 확대/축소 할 수 있게 함
- StatsGl : 성능을 모니터링함, 프레임 속도 등 확인 가능
나는 이를 배경으로 사용할 것이기 때문에, 카메라의 드래그, 확대/축소를 하지 못하도록 만들었다.
<CameraControls enabled={false} minDistance={200} maxDistance={200}/>
OrbitControls에서는 enablePan, enableZoom을 사용한다면,
cameraControls에서는 enabled로 한 번에 정의할 수 있었다.
또한 minDistance와 maxDistance를 설정하여 원하는 화면이 나올 수 있도록 조정하였다.
(enablePan에 대해서는 생각해봐야 할 것 같다. 직접 움직이는 게 좋은 것 같기도 하고…)
그리하여 원본 cloud에서
요렇게 바꼈다! 🤩🤩
또한 나는 three.js를 사용하여 나의 프로젝트들을 띄울 예정이기에,
boxGeometry에 텍스처를 입히는 Texture Mapping에 대해 더 알아보려고 한다.
🍀 Texture Mapping
Texture ? 객체의 색상과 질감을 표현하는 Material에서 사용되는 이미지나 비디오
Texture Mapping ? 객체의 표면에 색을 칠하거나 이미지를 입히는 것
텍스처를 로드하려면, 텍스처의 위치를 전달하고 지도를 다시 가져올 수 있는 useLoader와 함께 TextureLoader를 사용해야 한다.
↓ 예시 코드
import { Suspense } from 'react'
import { Canvas, useLoader } from '@react-three/fiber'
import { TextureLoader } from 'three/src/loaders/TextureLoader'
function Scene() {
const colorMap = useLoader(TextureLoader, 'PavingStones092_1K_Color.jpg')
return (
<>
<ambientLight intensity={0.2} />
<directionalLight />
<mesh>
<sphereGeometry args={[1, 32, 32]} />
<meshStandardMaterial />
</mesh>
</>
)
}
export default function App() {
return (
<Canvas>
<Suspense fallback={null}>
<Scene />
</Suspense>
</Canvas>
)
}
- useLoader(원하는 Loader, 입히고 싶은 텍스처 파일)
저렇게 하면은 텍스처가 아래처럼 잘 보이는 걸 알 수 있다!
지금 진행하고 있는 프로젝트의 사진을 assets 폴더에 넣고 텍스처 매핑을 시도했다
import { Suspense } from 'react'
import { Canvas, useLoader } from '@react-three/fiber'
import { TextureLoader } from 'three/src/loaders/TextureLoader'
import simter from './assets/simter.jpg'
import { OrbitControls } from '@react-three/drei'
function Scene() {
const simterMap = useLoader(TextureLoader, simter);
return (
<mesh>
<ambientLight intensity={0.8} />
<boxGeometry args={[10, 5, 1]} />
<meshStandardMaterial
map={simterMap}
/>
</mesh>
);
}
export default function App() {
return (
<Canvas>
<Suspense fallback={null}>
<OrbitControls autoRotate={true} />
<Scene />
</Suspense>
</Canvas>
);
}
.. 엄청 어둡고.. 화질도 안 좋고.. 사진도 안 보여서 너무너무 당황쓰..
그래서 일단은 조명들을 사용해서 밝기를 밝혔다.
✨ Light
- ambientLight : 방향성이 없으며, scene 전체에 균일하게 적용됨
- 그림자가 생기지 않음
- intensity : 빛의 세기 조절
- color : 빛의 색상 결정
- directionalLight : 무한히 떨어지는 평행한 빛을 표현
- intensity : 빛의 세기 조절
- color : 빛의 색상 결정
- position : 빛의 위치 결정
- pointLight : 하나의 지점에서 모든 방향으로 빛을 쐬줌
- intensity : 빛의 세기 조절
- color : 빛의 색상 결정
- position : 빛의 위치 결정
- distance : 빛의 영향 범위 설정
- spotLight : 스포트라이트
- position : 빛의 위치 결정
- angle : 빛의 각도 조절
- penumbra : 부분 그림자의 강도 설정
나는 여기에서 ambientLight와 함께 directionalLight, pointLight를 사용했는데..
밝기를 너무 높이면 아예 그림이 날라가고.. 조금이라도 낮추면 바로 어두워져서 문제..
흠 이거에 대해서는 조금 더 공부를 해봐야 할 것 같다. 아예 안되면 그냥 img 태그로 띄우는 수밖에..
📝 정리
R3F로 만든 사이트를 보면 항상 예쁜 배경이 3D로 움직이는 게 너무 예뻤어서 꼭 구현해보고 싶었는데,
이렇게라도 구현할 수 있게 되어 기뻤다!
또한 다음에 이미지를 어떻게 하면 이미지 그대로 객체에 매핑하여 띄울 수 있을 지에 대해 더 공부하고 적용해봐야겠다.
🔗 참고
https://r3f.docs.pmnd.rs/getting-started/examples
https://codesandbox.io/p/sandbox/mbfzf?file=/src/App.js:1,1-58,1
'개발일지' 카테고리의 다른 글
[개발일지] R3F를 활용한 3D 포트폴리오 사이트 만들기 4 (1) | 2024.12.04 |
---|---|
[개발일지] R3F를 활용한 3D 포트폴리오 사이트 만들기 3 (1) | 2024.12.04 |
[개발일지] Three.js를 활용한 3D 포트폴리오 사이트 만들기 1 (12) | 2024.09.14 |