Galaxy Generator
Introduction
이제 particles를 사용할 수 있으니, 멋진 은하(galaxy)를 만들어볼 수 있습니다.
하지만 하나의 은하만 만드는 대신, 매번 다른 은하를 생성할 수 있는 은하 생성기(galaxy generator) 를 만들어보겠습니다.
사용자가 다양한 파라미터를 조절할 수 있도록 하기 위해 lil-gui를 사용할 것입니다.
Setup
시작 프로젝트에는 단순히 장면(scene) 중앙에 큐브 하나만 존재합니다.
기본 작동 여부를 확인하기 위한 구조입니다.
Base Particles
- 먼저 큐브를 제거하고 generateGalaxy 함수를 생성합니다.
이 함수는 기존 은하가 존재하면 제거하고 새로운 은하를 생성합니다.
/**
* Galaxy
*/
const generateGalaxy = () => {};
generateGalaxy();
- 은하의 파라미터를 저장할 객체를 만들고, 이 객체를 generateGalaxy 함수 위에 정의합니다:
const parameters = {};
- 기본 geometry를 만들고, particle의 개수를 지정합니다:
const parameters = {};
parameters.count = 1000;
const generateGalaxy = () => {
/**
* Geometry 생성
*/
const geometry = new THREE.BufferGeometry();
// 각 입자의 좌표(x, y, z)를 저장할 배열
const positions = new Float32Array(parameters.count * 3);
for (let i = 0; i < parameters.count; i++) {
const i3 = i * 3;
positions[i3] = (Math.random() - 0.5) * 3;
positions[i3 + 1] = (Math.random() - 0.5) * 3;
positions[i3 + 2] = (Math.random() - 0.5) * 3;
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
};
generateGalaxy();
- PointsMaterial을 사용해서 material을 생성합니다:
parameters.size = 0.02;
const generateGalaxy = () => {
// ...
/**
* Material 생성
*/
const material = new THREE.PointsMaterial({
size: parameters.size,
sizeAttenuation: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
});
};
- Points 객체를 생성하고 scene에 추가합니다:
const generateGalaxy = () => {
// ...
/**
* Points 생성 및 scene 추가
*/
const points = new THREE.Points(geometry, material);
scene.add(points);
};
Tweaks
현재 count, size라는 두 가지 파라미터가 있습니다. 이를 lil-gui에서 조절할 수 있게 추가합니다:
gui.add(parameters, 'count').min(100).max(1000000).step(100);
gui.add(parameters, 'size').min(0.001).max(0.1).step(0.001);
하지만 이렇게만 하면 값을 바꿔도 은하가 갱신되지 않습니다. onFinishChange(generateGalaxy) 이벤트를 등록해야 합니다:
gui
.add(parameters, 'count')
.min(100)
.max(1000000)
.step(100)
.onFinishChange(generateGalaxy);
gui
.add(parameters, 'size')
.min(0.001)
.max(0.1)
.step(0.001)
.onFinishChange(generateGalaxy);
이벤트는 generateGalaxy 함수 정의 이후에 등록해야 정상 작동합니다.
이전 은하 제거하기
기존 은하를 제거하지 않으면 메모리에 계속 쌓여서 컴퓨터가 느려집니다. 아래처럼 처리합니다:
let geometry = null;
let material = null;
let points = null;
const generateGalaxy = () => {
// 이전 은하 제거
if (points !== null) {
geometry.dispose();
material.dispose();
scene.remove(points);
}
geometry = new THREE.BufferGeometry();
material = new THREE.PointsMaterial({
size: parameters.size,
sizeAttenuation: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
});
points = new THREE.Points(geometry, material);
scene.add(points);
};
Shape
Radius
반지름 설정:
parameters.radius = 5;
gui
.add(parameters, 'radius')
.min(0.01)
.max(20)
.step(0.01)
.onFinishChange(generateGalaxy);
입자 하나하나가 이 반지름 값에 따라 거리 0부터 반지름 값 사이에 배치됩니다.
for (let i = 0; i < parameters.count; i++) {
const i3 = i * 3;
const radius = Math.random() * parameters.radius;
positions[i3] = radius;
positions[i3 + 1] = 0;
positions[i3 + 2] = 0;
}
Branches
은하의 팔 개수를 조절하는 파라미터:
parameters.branches = 3;
gui
.add(parameters, 'branches')
.min(2)
.max(20)
.step(1)
.onFinishChange(generateGalaxy);
브랜치 각도 계산 후 배치:
const branchAngle =
((i % parameters.branches) / parameters.branches) * Math.PI * 2;
positions[i3] = Math.cos(branchAngle) * radius;
positions[i3 + 2] = Math.sin(branchAngle) * radius;
Spin
회전을 적용하여 소용돌이 형태 생성:
parameters.spin = 1;
gui
.add(parameters, 'spin')
.min(-5)
.max(5)
.step(0.001)
.onFinishChange(generateGalaxy);
회전 각도 적용:
const spinAngle = radius * parameters.spin;
positions[i3] = Math.cos(branchAngle + spinAngle) * radius;
positions[i3 + 2] = Math.sin(branchAngle + spinAngle) * radius;
Randomness
너무 정렬된 입자들을 무작위성으로 퍼뜨리기:
parameters.randomness = 0.2;
gui
.add(parameters, 'randomness')
.min(0)
.max(2)
.step(0.001)
.onFinishChange(generateGalaxy);
x, y, z 축 모두에 무작위 값 적용:
const randomX = (Math.random() - 0.5) * parameters.randomness * radius;
const randomY = (Math.random() - 0.5) * parameters.randomness * radius;
const randomZ = (Math.random() - 0.5) * parameters.randomness * radius;
Randomness Power
더 자연스럽게 분산시키기 위해 Math.pow() 사용:
parameters.randomnessPower = 3;
gui
.add(parameters, 'randomnessPower')
.min(1)
.max(10)
.step(0.001)
.onFinishChange(generateGalaxy);
음수 방향으로도 분포시키기 위해 -1을 곱함:
const randomX =
Math.pow(Math.random(), parameters.randomnessPower) *
(Math.random() < 0.5 ? 1 : -1) *
parameters.randomness *
radius;
Colors
중앙과 외곽의 색상을 다르게 설정:
parameters.insideColor = '#ff6030';
parameters.outsideColor = '#1b3984';
gui.addColor(parameters, 'insideColor').onFinishChange(generateGalaxy);
gui.addColor(parameters, 'outsideColor').onFinishChange(generateGalaxy);
재질 설정 시 vertexColors 활성화:
material = new THREE.PointsMaterial({
size: parameters.size,
sizeAttenuation: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
vertexColors: true,
});
색상 속성 추가:
const colors = new Float32Array(parameters.count * 3);
const colorInside = new THREE.Color(parameters.insideColor);
const colorOutside = new THREE.Color(parameters.outsideColor);
const mixedColor = colorInside.clone();
mixedColor.lerp(colorOutside, radius / parameters.radius);
colors[i3] = mixedColor.r;
colors[i3 + 1] = mixedColor.g;
colors[i3 + 2] = mixedColor.b;
마무리
이제 아름다운 은하 생성기가 완성되었습니다.
lil-gui로 다양한 파라미터를 조정하며 원하는 스타일의 은하를 만들어 보세요.
다양한 분포, 색상, 분기 수 등을 조절하며 실험해 보시길 바랍니다.
주의: 파티클 개수를 지나치게 높이면 컴퓨터가 뜨거워질 수 있습니다. 적절한 값으로 조절하세요.
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import GUI from 'lil-gui';
/**
* Base
*/
// Debug
const gui = new GUI({ width: 360 });
// Canvas
const canvas = document.querySelector('canvas.webgl');
// Scene
const scene = new THREE.Scene();
/**
* Galaxy
*/
const parameters = {};
parameters.count = 10000;
parameters.size = 0.01;
parameters.radius = 5;
parameters.branches = 3;
parameters.spin = 1;
parameters.randomness = 0.2;
parameters.randomnessPower = 3;
parameters.insideColor = '#ff6030';
parameters.outsideColor = '#1b3984';
let geometry = null;
let material = null;
let points = null;
const generateGalaxy = () => {
/**
* Destroy old galaxy
*/
if (points !== null) {
geometry.dispose();
material.dispose();
scene.remove(points);
}
geometry = new THREE.BufferGeometry();
const position = new Float32Array(parameters.count * 3);
const colors = new Float32Array(parameters.count * 3);
const colorInside = new THREE.Color(parameters.insideColor);
const colorOutside = new THREE.Color(parameters.outsideColor);
for (let i = 0; i < parameters.count; i++) {
const i3 = i * 3;
// Position
const radius = Math.random() * parameters.radius;
const spinAngle = radius * parameters.spin;
const branchesAngle =
((i % parameters.branches) / parameters.branches) * Math.PI * 2;
const randomX =
Math.pow(Math.random(), parameters.randomnessPower) *
(Math.random() < 0.5 ? 1 : -1);
const randomY =
Math.pow(Math.random(), parameters.randomnessPower) *
(Math.random() < 0.5 ? 1 : -1);
const randomZ =
Math.pow(Math.random(), parameters.randomnessPower) *
(Math.random() < 0.5 ? 1 : -1);
position[i3 + 0] = Math.cos(branchesAngle + spinAngle) * radius + randomX; // x
position[i3 + 1] = randomY; // y
position[i3 + 2] = Math.sin(branchesAngle + spinAngle) * radius + randomZ; // z
// Color
const mixedColor = colorInside.clone(); // 원본 변경 방지를 위해 복사
mixedColor.lerp(colorOutside, radius / parameters.radius); // 섞기 (섞을 색, 0 적게 ~ 1 많이)
colors[i3 + 0] = mixedColor.r; // r
colors[i3 + 1] = mixedColor.g; // g
colors[i3 + 2] = mixedColor.b; // b
}
geometry.setAttribute('position', new THREE.BufferAttribute(position, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
// Material
material = new THREE.PointsMaterial({
size: parameters.size,
sizeAttenuation: true,
depthWrite: true,
blending: THREE.AdditiveBlending,
vertexColors: true,
});
//Points
points = new THREE.Points(geometry, material);
scene.add(points);
};
generateGalaxy();
gui
.add(parameters, 'count')
.min(100)
.max(100000)
.step(100)
.onFinishChange(generateGalaxy);
gui
.add(parameters, 'size')
.min(0.01)
.max(0.1)
.step(0.01)
.onFinishChange(generateGalaxy);
gui
.add(parameters, 'radius')
.min(0.01)
.max(20)
.step(0.01)
.onFinishChange(generateGalaxy);
gui
.add(parameters, 'branches')
.min(2)
.max(20)
.step(1)
.onFinishChange(generateGalaxy);
gui
.add(parameters, 'spin')
.min(-5)
.max(5)
.step(1)
.onFinishChange(generateGalaxy);
gui
.add(parameters, 'randomness')
.min(0)
.max(2)
.step(0.001)
.onFinishChange(generateGalaxy);
gui
.add(parameters, 'randomness')
.min(1)
.max(10)
.step(0.01)
.onFinishChange(generateGalaxy);
gui.addColor(parameters, 'insideColor').onFinishChange(generateGalaxy);
gui.addColor(parameters, 'outsideColor').onFinishChange(generateGalaxy);
/**
* Sizes
*/
const sizes = {
width: window.innerWidth,
height: window.innerHeight,
};
window.addEventListener('resize', () => {
// Update sizes
sizes.width = window.innerWidth;
sizes.height = window.innerHeight;
// Update camera
camera.aspect = sizes.width / sizes.height;
camera.updateProjectionMatrix();
// Update renderer
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});
/**
* Camera
*/
// Base camera
const camera = new THREE.PerspectiveCamera(
75,
sizes.width / sizes.height,
0.1,
100,
);
camera.position.x = 3;
camera.position.y = 3;
camera.position.z = 3;
scene.add(camera);
// Controls
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;
/**
* Renderer
*/
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
});
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
/**
* Animate
*/
const clock = new THREE.Clock();
const tick = () => {
const elapsedTime = clock.getElapsedTime();
// Update controls
controls.update();
// Render
renderer.render(scene, camera);
// Call tick again on the next frame
window.requestAnimationFrame(tick);
};
tick();
'2D&3D > ThreeJS' 카테고리의 다른 글
20 물리엔진(Physics) (2) | 2025.07.29 |
---|---|
19 스크롤 기반 애니메이션 (2) | 2025.07.28 |
17 Particle(Points) (3) | 2025.07.28 |
16 Haunted House 프로젝트 (1) | 2025.07.26 |
15 그림자(Shadow) (3) | 2025.07.26 |