🙌🏻 해당 글은 Three.js Journey의 강의 노트입니다.
Particles에서 다룰 것은 말 그대로이다. 별들, 연기, 비, 먼지, 불 등 많은 혀과들을 만드는데 사용된다. 합리적인 프레임 속도로 수십만 개의 파티클을 화면에 나타낼 수 있다. 여기에 단점이 있는데, 파티클은 사실 카메라를 향하는 단면(우리가 지금껏 다루었던 PlaneGeometry)로 구성되어있다는 것!
우선 기본 Three.js geometry를 사용한다. BufferGeometries를 사용하는 것이 좋으며, Geometry의 각 vertex가 입자가 되는 원리이다.
particles를 만들기 위해서는 pointMaterial이라는 material이 필요하다. PointMaterial은 두 가지 속성을 갖는데, 입자의 크기를 결정하는 size와 먼 입자가 가까운 입자보다 작아야하는가를 나타내는 sizeAttenuation이 있다.
이제 Mesh를 만드는 것과 같은 방식으로 Geometry와 Material을 결합해준다!
const particlesGeometry = new THREE.SphereBufferGeometry(1, 32, 32);
// Material
const particlesMaterial = new THREE.PointsMaterial();
particlesMaterial.size = 0.02;
particlesMaterial.sizeAttenuation = true;
// Points
const particles = new THREE.Points(particlesGeometry, particlesMaterial);
scene.add(particles);
가능은 하지만 조금 문제가 있다! 평면, 직선으로 구성된 부분에는 particles가 채워지지 않는다. 이 부분은 뒤에서 강의를 다 듣고 다시 시도해보도록 하자!
fontLoader.load("/fonts/helvetiker_regular.typeface.json", (font) => {
const textGeometry = new THREE.TextGeometry("o0o0o0o0o", {
font: font,
size: 0.5,
height: 0.2,
curveSegments: 12,
bevelEnabled: true,
bevelThickness: 0.03,
bevelSize: 0.02,
bevelOffset: 0,
bevelSegments: 5,
});
textGeometry.center();
const particlesMaterial = new THREE.PointsMaterial({
size: 0.002,
sizeAttenuation: true,
});
const particles = new THREE.Points(textGeometry, particlesMaterial);
scene.add(particles);
});
custom geometry를 사용하기 위해서는 BufferGeometry에서 시작해서 position 속성을 추가해준다.
// Geometry
const particlesGeometry = new THREE.BufferGeometry()
const count = 500
const positions = new Float32Array(count * 3) // Multiply by 3 because each position is composed of 3 values (x, y, z)
for(let i = 0; i < count * 3; i++) // Multiply by 3 for same reason
{
positions[i] = (Math.random() - 0.5) * 10 // Math.random() - 0.5 to have a random value between -0.5 and +0.5
}
particlesGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)) // Create the Three.js BufferAttribute and specify that each information is composed of 3 values
입자의 개수를 각각 5000, 50000, 500000으로 늘려주어도 여전히 적절한 프레임 속도를 유지한다.
당연하게도 particle의 색상과 texture를 바꿔줄 수 있다!
particlesMaterial.color = new THREE.Color('#ff88cc')
/**
* Textures
*/
const textureLoader = new THREE.TextureLoader()
const particleTexture = textureLoader.load('/textures/particles/2.png')
// ...
particlesMaterial.map = particleTexture
아래와 같이 색상과 입자의 모양을 바꿔줄 수도 있다.
kenney에서 다양한 입자를 찾을 수 있다!
그런데 문제는 자세히보면 앞의 입자가 뒤의 입자를 네모 모양으로 가리고 있다는 것을 알 수 있다.
이럴 때는 transparency를 활성화해주어야 한다.
이 때는 texture를 그냥 map을 사용하는 것이 아니라, alphaMap을 사용해준다.
// particlesMaterial.map = particleTexture;
particlesMaterial.transparent = true;
particlesMaterial.alphaMap = particleTexture;
투명도가 반영되어 조금은 개선되었지만, 여전히 앞의 입자가 뒤의 입자를 가리는 경우가 있다.
WebGL은 무엇이 앞에 있는 것이고 뒤에 있는 것인지 인지를 못하기 때문이다.
이 문제를 해결해보자.
alphaTest는 0부터 1까지 값으로 WebGL이 해당 픽셀을 렌더링하지 않을 시기를 알 수 있도록 한다.
particlesMaterial.alphaTest = 0.001
그림을 그려낼 때, WebGL은 그려지는 것이 우리가 이미 그린 것보다 가까이 있는가를 테스트한다. 이걸 depthTest라고 하는데, 이걸 비활성화할 수 있다!
얼추 문제가 해결된 듯 보이지만, Scene에 다른 Object가 있거나, 다른 색상의 particle이 존재할 때, depthTest를 비활성화하면, 버그가 발생한다. 아래에 큐브를 추가한 예시!
이미 살펴보았듯이 WebGL은 지금 그릴 것이 이미 그린것보다 가까운지 테스트한다. 지금 그리는 것의 depth는 depth buffer에 저장되어 있다. 우리는 particle이 depth buffer에 있는것보다 가까운지 테스트 하지 않고, WebGL에 해당 depth buffer에 particle을 그리지 않도록 할 수 있다.
const particlesMaterial = new THREE.PointsMaterial();
particlesMaterial.size = 0.1;
particlesMaterial.sizeAttenuation = true;
particlesMaterial.color = new THREE.Color("#aa3344");
// particlesMaterial.map = particleTexture;
particlesMaterial.transparent = true;
particlesMaterial.alphaMap = particleTexture;
// particlesMaterial.alphaTest = 0.001;
// particlesMaterial.depthTest = false;
particlesMaterial.depthWrite = false;
사실 이런 문제에서 완벽한 해결책은 없고, 다만 최상의 결과를 내는 조합을 찾아서 사용해야 한다.
현재 WebGL은 픽셀 위에 픽셀을 그려낸다. Blending 속성을 변경하면, 이미 그린 픽셀의 색상에 이제 그릴 픽셀의 색상을 추가할 수 있다. 채도효과를 내는 것 (이 부분 잘 이해 안 감... 이건 채도가 아니라 그냥 안 그려지는 것 아닌가...?)
particlesMaterial.blending = THREE.AdditiveBlending
particlesGeometry의 color 속성을 변경해 아래와 같이 입자들의 색깔을 모두 다르게 설정할 수 있다.
const positions = new Float32Array(count * 3)
const colors = new Float32Array(count * 3)
for(let i = 0; i < count * 3; i++)
{
positions[i] = (Math.random() - 0.5) * 10
colors[i] = Math.random()
}
particlesGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3))
particlesGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3))
중요한 것은 particlesMaterial.vertexColors = true
이 설정을 해줘야 한다는 점! 해주지 않으면 색이 반영되지 않는다.
애니메이션을 적용하는 다양한 방법이 있다!
// Update particles
particles.rotation.y = elapsedTime * 0.2;
그런데 여기서 의문이 드는 것이 왜 particles를 rotation 적용했는데, 전체가 돌아가는 듯 출력되는지.
회전을 시키기지 않고, 각 vertex의 위치를 각각 업데이트 해줄 수도 있다. 이렇게 하면, 각각의 vertex가 각기 다른 궤적을 가질 수 있다는 점이 다르다.
각 정점은 1차원 배열에 저장된다. 위아래로 물결만 치게 만들고 싶다면, y좌표만 수정해주면 된다. 물결치는 모습을 구현하기 위한 가장 쉬운 방법은 sinus함수를 사용하는 것이다. 물론 위치를 변경할 때는 needsUpdate 속성을 활성화해주어야 한다. 그러나 사실 이 방법은 너무 많은 계산을 요구하기 때문에 지양해야 한다. (실제로 구동해놓고 블로깅하고 있으니, 렉 먹는다.)
const tick = () =>
{
// ...
for(let i = 0; i < count; i++)
{
let i3 = i * 3
const x = particlesGeometry.attributes.position.array[i3]
particlesGeometry.attributes.position.array[i3 + 1] = Math.sin(elapsedTime + x)
}
particlesGeometry.attributes.position.needsUpdate = true
// ...
}
바람직한 프레임 속도로 수많은 particle을 업데이트하려면 자체 셰이더를 이용해야 한다. 이 부분은 나중에 다루도록 한다.
안녕하세요 글 잘 보았습니다.
저도 textgeomtry와 pointmaterial로 구현해봤는데
몇몇 부분에서 point가 빠져있더라구요
'가능은 하지만 조금 문제가 있다! 평면, 직선으로 구성된 부분에는 particles가 채워지지 않는다. 이 부분은 뒤에서 강의를 다 듣고 다시 시도해보도록 하자! '
이부분은 혹시 어떻게 해결해야하나요 :? 도움주시면 감사하겠습니다.ㅠ