logo

DowanKim

2. 자연스러운 우주 느낌의 배경 파티클을 웹에서 만들어 보자.

2025년 8월 15일

To Infinity

배경 파티클 시스템 구현

Todo

  • 자연스럽게 흐르는 움직임
  • 시간에 따라 변화하는 패턴
  • 사용자가 등장하기 전 무한한 공간을 연출

랜덤값으로 하면?

처음에는 Math.random()으로 각 파티클의 위치를 업데이트했습니다. 결과:

  • 파티클이 각자 무작위로 움직여 어수선함
  • 패턴이 없어 유기적 흐름이 부족
  • 프레임마다 급격히 변해 좀 많이 부자연스러움

랜덤의 한계:

// 랜덤 사용 시 문제점 positions[i3 + 0] += (Math.random() - 0.5) * 2; // 너무 급격한 변화 positions[i3 + 1] += (Math.random() - 0.5) * 2; // 패턴 없음 positions[i3 + 2] += (Math.random() - 0.5) * 2; // 부자연스러움

노이즈(Noise)의 선택

Perlin Noise(Simplex Noise)를 선택한 이유:

  1. 연속성: 인접한 입력값이 비슷한 출력을 만들어 부드러운 변화
  2. 자연스러움: 유기적 흐름과 소용돌이 패턴
  3. 시간 변화: 4D 노이즈로 시간 축 추가 가능
  4. 성능: 경량 라이브러리로 실시간 처리 가능

Three.js 파티클 시스템 구조

1. BufferGeometry: 대량 파티클 처리

20,000개 파티클을 효율적으로 처리하기 위해 BufferGeometry를 사용했습니다.

this.particles = new THREE.BufferGeometry(); this.positions = new Float32Array(this.particleCount * 3);

왜 BufferGeometry인가?

  • TypedArray로 메모리 효율적
  • GPU에 직접 전달 가능
  • 일반 Geometry보다 빠름

메모리 구조:

  • 각 파티클은 (x, y, z) 3개 값
  • 20,000개 × 3 = 60,000개 Float32 값
  • 약 240KB (Float32 = 4 bytes)

2. 초기 위치 설정

파티클을 3D 공간에 균등하게 분산:

for (let i = 0; i < this.particleCount; i++) { const i3 = i * 3; this.positions[i3 + 0] = (Math.random() - 0.5) * 600; // X: -300 ~ 300 this.positions[i3 + 1] = (Math.random() - 0.5) * 600; // Y: -300 ~ 300 this.positions[i3 + 2] = (Math.random() - 0.5) * 600; // Z: -300 ~ 300 }

공간 크기 결정:

  • 카메라 초기 위치: Z = 50
  • 시야각: 75도
  • 600 단위 공간으로 카메라 주변을 채움

3. Three.js에 데이터 연결

this.particles.setAttribute('position', new THREE.BufferAttribute(this.positions, 3));
  • BufferAttribute: TypedArray를 Three.js 형식으로 변환
  • 3: 각 정점당 3개 값 (x, y, z)

3D 공간 설정

카메라 설정

this.camera = new THREE.PerspectiveCamera( 75, // 시야각 (FOV) window.innerWidth / window.innerHeight, // 종횡비 0.1, // near plane 1000 // far plane ); this.camera.position.z = 50;

공간 범위:

  • Near: 0.1
  • Far: 1000
  • 파티클 범위: -300 ~ 300 (카메라 주변)

씬(Scene) 설정

this.scene = new THREE.Scene();
  • 별도 조명 없이 파티클 자체 발광 사용

텍스처 적용

이미지 텍스처 로드

this.textureLoader = new THREE.TextureLoader(); this.particleTexture = this.textureLoader.load('./asset.png');

텍스처 사용 이유: 파티클이 카메라 가까이 왔을 때, 단순히 점이면 몰입도가 떨어진다고 생각해습니다. 파티클 이미지를 자체 제작하여, 이미지로 사용했습니다.

PointsMaterial 설정

this.particleMaterial = new THREE.PointsMaterial({ size: 2, // 파티클 크기 map: this.particleTexture, // 텍스처 이미지 transparent: true, // 투명도 사용 alphaTest: 0.1, // 알파 테스트 (투명 부분 제거) blending: THREE.AdditiveBlending, // 가산 블렌딩 });

AdditiveBlending:

  • 겹치는 파티클이 밝게 합성
  • 별처럼 보이는 효과
  • 우주 느낌 강화

노이즈 구현: 4D 노이즈로 시간 변화

SimplexNoise 초기화

this.noise = new SimplexNoise(); this.clock = new THREE.Clock();
  • SimplexNoise: Perlin Noise 개선 버전
  • THREE.Clock: 경과 시간 추적

4D 노이즈 샘플링

각 축에 대해 서로 다른 위치에서 노이즈를 샘플링:

const noiseScale = 0.005; // 공간적 스케일 (Frequency) const timeScale = 0.1; // 시간적 변화 속도 const forceStrength = 0.8; // 힘의 세기 const noiseX = this.noise.noise4D(x * noiseScale, y * noiseScale, z * noiseScale, time * timeScale) * forceStrength; const noiseY = this.noise.noise4D(y * noiseScale, z * noiseScale, x * noiseScale, time * timeScale) * forceStrength; const noiseZ = this.noise.noise4D(z * noiseScale, x * noiseScale, y * noiseScale, time * timeScale) * forceStrength; positions[i3 + 0] += noiseX; positions[i3 + 1] += noiseY; positions[i3 + 2] += noiseZ;

왜 각 축마다 다른 입력을 사용하는가?

  • X축: (x, y, z, time)
  • Y축: (y, z, x, time) - 순환 이동
  • Z축: (z, x, y, time) - 순환 이동

noise에 들어가는 x, y, z값을 이전 좌표에 근거하는 동시에 한칸씩 땡겨주면 직선운동이 아닌 소용돌이 같은 자연스러운 움직임이 생길거라 예상했고, 의도한 대로 잘 움직였습니다.

노이즈 파라미터 튜닝

공간적 스케일 (noiseScale = 0.005):

  • 작을수록 큰 패턴 (부드러움)
  • 클수록 작은 패턴 (세밀함)
  • 실행시켜보고 적당한 값 사용

시간적 스케일 (timeScale = 0.1):

  • 변화 속도 조절
  • 0.1은 천천히 변화
  • 실행시켜보고 적당한 값 사용

힘의 세기 (forceStrength = 0.8):

  • 파티클 이동 거리
  • 너무 크면 빠르게 사라짐
  • 너무 작으면 움직임이 미미함
  • 실행시켜보고 적당한 값 사용

애니메이션 루프

update() 메서드

update() { const time = this.clock.getElapsedTime(); const positions = this.particleSystem.geometry.attributes.position.array; // 각 파티클 위치 업데이트 for (let i = 0; i < this.particleCount; i++) { // ... 노이즈 계산 및 위치 업데이트 } // Three.js에 변경사항 알림 this.particleSystem.geometry.attributes.position.needsUpdate = true; }

핵심 포인트:

  1. getElapsedTime(): 시작부터 경과 시간
  2. 직접 배열 수정: positions 배열을 직접 변경
  3. needsUpdate = true: Three.js에 변경 알림 (필수)

메인 애니메이션 루프

animate() { requestAnimationFrame(() => this.animate()); this.backgroundParticles.update(); this.renderer.render(this.scene, this.camera); }
  • requestAnimationFrame: 브라우저 최적화된 프레임 주기
  • 보통 60fps (약 16.67ms/프레임)

문제 해결 과정

문제 1: 파티클이 움직이지 않음

원인: needsUpdate 플래그 누락

// 이전 코드 positions[i3 + 0] += noiseX; // needsUpdate 없음! // 수정 코드 positions[i3 + 0] += noiseX; this.particleSystem.geometry.attributes.position.needsUpdate = true;

해결: 매 프레임마다 needsUpdate = true 설정

문제 2: 파티클이 너무 빠르게 사라짐

원인: forceStrength가 너무 큼

// 초기 시도 const forceStrength = 2.0; // 너무 큼 // 조정 후 const forceStrength = 0.8; // 적절함

해결: 파라미터를 반복 조정

문제 3: 노이즈 패턴이 너무 작음

원인: noiseScale이 너무 큼

// 초기 시도 const noiseScale = 0.1; // 패턴이 너무 세밀함 // 조정 후 const noiseScale = 0.005; // 적절한 크기의 패턴

해결: 시각적 피드백으로 스케일 조정

문제 4: 성능 저하

원인: 아마도 불필요한 연산

최적화:

  • 노이즈 계산은 필수이므로 유지
  • 파티클 개수 조정 가능 (20,000 → 15,000)
  • 불필요한 렌더링 제거

최종 구현 결과

성공적으로 구현된 기능:

  • 20,000개 파티클 실시간 렌더링
  • 4D 노이즈 기반 자연스러운 움직임
  • 텍스처 적용으로 시각적 품질 향상
  • 60fps 유지

성능:

  • 초기 로딩: 약 1초
  • 프레임레이트: 60fps 유지
  • 메모리 사용: 약 240KB

이를 통해, 자연스럽게 파티클이 이동하는 우주 느낌의 배경 파티클 시스템을 구현할 수 있었습니다.