⚠️ 1분 코딩님의 강의를 바탕으로 작성한 글입니다.
Particle
'미립자', '입자'를 의미하는 것처럼 하나의 덩어리로 존재하던 material을 아주 작은 입자처럼 표현하는 방법이다.
particle을 사용하기 위해서는 기존 방식이었던 geometry + material을 mesh에 전달해 주는 방식이 아닌 Points라는 메서드를 사용해야 한다. 역시 말보다는 코드를 보며 자세히 풀어보겠다!
const geometry = new THREE.SphereGeometry(1, 32, 32);
const material = new THREE.PointsMaterial({
size: 0.02,
sizeAttenuation: false, // 점(vertax)들을 원근에 상관없이 균일한 크기로 지정
});
const points = new THREE.Points(geometry, material); // particle은 mesh가 아닌 points로 제작
scene.add(points);
geometry는 모양, 뼈대와 같은 존재이기 때문에 기존 방식과 같다. 그러나 material에서는 PointsMaterial이라는 메서드를 호출하여 점(point)으로 이루어진 재질로 geometry를 감싸는 것이다. 그리고 이를 Mesh가 아닌 Points라는 클래스를 사용하여 점(point)으로 구성된 3D 객체를 생성하는 것이다.
무작위 값에 파티클 위치시키기
파티클의 특징을 이용해서 마치 우주의 별과 같은 형태를 표현할 수도 있다.
이때 핵심은 특정 형태를 가지고 있지 않는 BufferGeometry를 사용한다는 것이다! 정확히 말하면 원하는 모양에 대해 정의한 데이터를 저장하고 있다가 렌더링 때 해당 데이터를 GPU로 전송한다. 이후 전달받은 데이터를 이용하여 객체를 화면에 그려내는 것이다.
👀 BufferGeometry에 대해 더 알아보기
추상화된 형태를 가진 geometry이다. 때문에 다양한 형태의 기하학 모형들을 만들어 낼 수 있다. 단, vertex position, face index, normals, color 등을 직접 설정해야 한다. 기존의 geometry 클래스보다 메모리 사용과 성능 면에서 더 최적화되어 메모리 효율성을 높이고 GPU와의 데이터 전송을 최적화한다.
이제껏 사용하던 BoxGeometry와 같은 형태를 지닌 geometry들도 내부적으로는 BufferGeometry를 사용하기 때문에 렌더링 성능 면에서는 동일한 성능을 제공한다. 그러나 특정한 형상이나 효과를 구현하고 싶을 때, BufferGeometry를 사용하면 더 많은 제어가 가능해지기 때문에 이럴 때는 BufferGeometry를 활용하면 좋다. 이러한 표현이 필요 없다면 기존의 정의된 형태를 사용하여 빠르게 적용하면 된다.
+) 공식문서에서도 확인해 보면 BufferGeometry를 Geometries가 아닌 Core로 분류해 두었다.
BufferGeometry를 사용하기 위한 방법에 대해 자세히 얘기해 보도록 하겠다. 우선 전체 코드를 보자.
const geometry = new THREE.BufferGeometry();
const count = 1000; // 파티클 갯수
const positions = new Float32Array(count * 3);
for (let i = 0; i < positions.length; i++) {
positions[i] = (Math.random() - 0.5) * 10;
}
geometry.setAttribute(
'position',
new THREE.BufferAttribute(positions, 3)
);
1️⃣ 정점(vertex)의 위치 정의하기
첫 번째로 정점의 위치를 나타낼 position 배열을 만들어야 한다. 이때 Three.js는 아시다시피 3D 공간이기 때문에 하나의 정점(vertex)마다 x, y, z 축이 필요하다. 때문에 자바스크립트 내장 객체인 Float32Array(32비트 부동소수점 숫자)를 사용하여 배열을 만들게 된다.
Float32Array
JavaScript의 내장 객체로, 32비트 부동소수점 숫자를 효율적으로 저장하고 처리하기 위한 배열입니다. 이 배열은 주로 WebGL과 같은 그래픽 API에서 정점 데이터, 텍스처 좌표, 색상 등과 같은 수치 데이터를 효율적으로 처리하는 데 사용됩니다. 고성능 3D 렌더링을 위해 필수적인 데이터 구조 중 하나입니다.
- ChatGPT
// Float32Array 생성
const positions = new Float32Array([
0.0, 0.0, 0.0, // 첫 번째 정점
1.0, 0.0, 0.0, // 두 번째 정점
0.0, 1.0, 0.0 // 세 번째 정점
]);
보통은 위의 예시처럼 정점의 위치를 지정할 수도 있지만, 현재 만들고자 하는 모양(우주의 별처럼...)은 특정한 형태를 지니고 있다기보다는 흩뿌려지는, 즉 명확한 위치 값이 아닌 아무 위치에나 존재하면 되기 때문에 원하는 객체의 수만큼 x, y, z 값만 담고 있으면 된다. 때문에 x, y, z를 의미하는 3개의 값을 곱해준 것이다.
const count = 1000; // 파티클 갯수
const positions = new Float32Array(count * 3);
그리고 규칙적인 위치가 아닌 무작위 하게 위치해야 하기 때문에 Math.random을 사용하였다.
Math.random은 0~1의 범위를 가지는데, 범위가 좁다고 생각하여, 1에서 0.5를 빼면 0.5, 0에서 0.5를 빼면 -0.5이고 여기에 10을 곱해서 -5에서 5의 범위를 주었다.
for (let i = 0; i < positions.length; i++) {
positions[i] = (Math.random() - 0.5) * 10;
}
2️⃣ BufferGeomery에 속성 전달하기
이렇게 정점의 위치는 완성하였다. 이제 BufferGeometry에게 정의한 속성을 전달해주어야 한다.
geometry.setAttribute(
'position',
new THREE.BufferAttribute(positions, 3)
);
geometry를 console.log로 찍어보면 attributes 프로퍼티 내부에 position이라는 속성이 있다. 해당 속성에 우리가 정의한 새로운 위치 값으로 갱신하는 것이다. 각 정점은 x, y, z로 이루어져 있기 때문에 3개의 요소로 구성되어 있음을 의미하는 3을 전달해주어야 한다.
material과 파티클로 출력하는 것은 처음에 작성한 코드에서 따로 변경하지 않아도 된다.
이미지를 사용하여 파티클 구현하기
지금껏 작성한 코드에서 이미지를 로드한 다음에 material에 이미지를 씌어주는 코드를 추가해 보도록 하겠다.
텍스쳐 이미지 넣는 것에 대해서는 아래 글에서 더 자세히 확인할 수 있다.
👉 [Three.js] To infinity and beyond! 3D 웹 도전기 #5
const textureLoader = new THREE.TextureLoader();
const particleTexture = textureLoader.load('/images/star.png');
이후 기존에 정의되어 있는 material에 속성을 추가 정의해 준다.
const material = new THREE.PointsMaterial({
size: 0.3,
map: particleTexture, // 별 텍스쳐 적용
// 텍스쳐 이미지를 투명하게 정의하는 속성
transparent: true,
alphaMap: particleTexture,
depthWrite: false
});
추가로 색상을 지정해 줄 수 있다. 이때는 geometry의 속성 중에 color에 접근하여 정의해주어야 한다. 작성할 예시의 경우, positions 배열을 전달하여 랜덤한 위치를 주었던 것처럼 RGB 색상 모델을 활용하여 랜덤한 색상을 주는 방법에 대해 설명한다.
정점(vertex) 색상을 적용하기 위해서는 BufferGeometry와 연결이 필요하다. 그래야 각 particle 당 정의한 postion과 color가 적절히 배치될 것이기 때문이다. 이때 geometry attributes에 데이터를 넣어두는 이유는 BufferGeometry의 특징을 다시 되짚어보면 좋다. 고정된 형태를 지니고 있지 않기에 정의한 데이터를 렌더링 시 GPU로 전달하여 그린다고 하였는데, 이 말은 즉슨 mesh에 대한 모든 데이터를 담고 있다는 의미로 해석하였다. BufferGeometry를 단순히 geometry에 초점을 둔다면 헷갈릴 수 있겠지만, Core가 되는 클래스임을 상기하고 메모리 효율성에 초점을 둔다면 충분히 color 데이터를 저장해 둘 수 있다고 생각한다.
geometry.setAttribute(
'color',
new THREE.BufferAttribute(colors, 3)
);
랜덤한 색상을 담은 colors라는 배열을 만들어 보겠다. 기존 for문을 활용하였다.
const colors = new Float32Array(count * 3);
for (let i = 0; i < positions.length; i++) {
...
colors[i] = Math.random();
}
color의 경우에도 3개의 값(R, G, B)을 지니기 때문에 Float32Array를 통해 표현할 수 있다.
👀 RGB에 대해 더 알아보기
RGB는 0과 1 사이의 값으로 표현된 색상 모델이다. R은 Red를 의미하며 빨간색의 강도를 나타내며, G는 Green을 의미하며 초록색의 강도를 나타낸다. B는 Blue를 의미하며 파란색의 강도를 나타낸다. 해당 색상의 강도를 잘 조합하여 다양한 색상을 표현할 수 있게 되는 것이다.
예시
- 검은색(0, 0, 0): 모든 색 성분이 0.
- 흰색(1, 1, 1): 모든 색 성분이 최댓값.
- 빨간색(1, 0, 0): 빨간색만 최대.
- 초록색(0, 1, 0): 초록색만 최대.
- 파란색(0, 0, 10): 파란색만 최대.
- 회색(0.5, 0.5, 0.5): 각 색 성분이 중간값.
- 노란색(1, 1, 0): 빨간색과 초록색을 혼합.
- 자홍색(1, 0, 1): 빨간색과 파란색을 혼합.
- 청색(0, 1, 1): 초록색과 파란색을 혼합.
끝으로 material의 속성 중에 정점(vertex)의 색상을 정의할 때 사용하는 vertexColors 속성을 true로 설정해 주면 된다.
const material = new THREE.PointsMaterial({
...
vertexColors: true
});
vertexColors 속성을 true로 설정하면 material이 정점 색상 정보를 사용하게 된다. false일 경우, 정의한 색상 데이터를 고려하지 않고 렌더링 하게 된다.
Geometry의 point 좌표를 이용하여 새로운 Mesh 배치하기
해당 기능을 구현하기 위해서는 3가지의 재료가 필요하다. 첫 번째는 전체 형태를 결정할 geometry이며 두 번째는 만든 geometry의 position 배열이다. 세 번째는 해당 좌표 위치에 배치하고 싶은 Mesh이다.
const sphereGeometry = new THREE.SphereGeometry(1, 8, 8);
const positionArray = sphereGeometry.attributes.position.array;
앞서 posions라는 배열을 만들어보면서 알았듯이, positionArray 변수는 sphere를 구성하는 정점(vertex) 좌표들의 위치값을 가지고 있다.
좌표애 배치할 새로운 Mesh도 만들어보자.
const planeMesh = new THREE.Mesh(
new THREE.PlaneGeometry(0.3, 0.3),
new THREE.MeshBasicMaterial({
color: 'blue',
side: THREE.DoubleSide
})
);
이때 material을 정의해 줄 때 재질이 렌더링 될 면을 결정할 수도 있다. side 속성을 추가하면 된다. 값은 세 가지가 있다.
- THREE.FrontSide: 기본값. 객체의 앞면에만 렌더링 된다.
- THREE.BackSide: 객체의 뒷면만 렌더링 된다.
- THREE.DoubleSide: 객체의 양면에 렌더링 된다.
이제 3가지의 재료들을 이용해서 평면(plane)의 객체들로 이루어진 구형(sphere)의 형태를 만들어보겠다.
let plane;
for (let i = 0; i < positionArray.length; i += 3) {
plane = planeMesh.clone();
plane.position.x = positionArray[i];
plane.position.y = positionArray[i + 1];
plane.position.z = positionArray[i + 2];
plane.lookAt(0, 0, 0);
scene.add(plane);
}
plane mesh가 sphereGeometry를 감싸기 위해서는 여러 개의 plane mesh가 필요하다. 정점(vertex)의 좌표를 담고 있는 배열의 길이만큼 plane mesh를 생성해 줄 것이다. 그리고 한 번 순회할 때마다 3씩 커지며, 3개의 원소들을 x, y, z에 할당하여 각 plane mesh를 배치시킨다. 정면을 바라보는 plane들이 중심을 보도록 lookAt 메서드까지 실행시켜주어야 한다.