๐ Galaxy Particles Shader
์ปค์คํ ์ ฐ์ด๋๋ก ์ํ ํํฐํด์ ํ์ ์ํค๋ ๊ณผ์ ์ ์ค๋ช ํฉ๋๋ค. ํผํฌ๋จผ์ค ์ต์ ํ๋ฅผ ์ํด GPU๊ฐ ์ง์ ๊ฐ ํํฐํด์ ์ ๋๋ฉ์ด์ ์ ์ฒ๋ฆฌํ๋๋ก vertex shader๋ฅผ ์ฌ์ฉํ๋ฉฐ, ๋ค์ํ ํฌ๊ธฐ, ์์, ํํ๋ฅผ ํํํ๊ธฐ ์ํด shader attribute์ varying, uniforms๋ฅผ ์ ๊ทน ํ์ฉํฉ๋๋ค.
๐ฆ 1. ๊ธฐ์ด ์ ์
โ ๊ธฐ์กด PointsMaterial ์ ๊ฑฐ → ShaderMaterial ์ฌ์ฉ
material = new THREE.ShaderMaterial({
depthWrite: false,
blending: THREE.AdditiveBlending,
vertexColors: true,
});
- size, sizeAttenuation์ ShaderMaterial์์ ์ง์ ๊ตฌํ ํ์
๐งฑ 2. ์ ฐ์ด๋ ๊ธฐ๋ณธ ๊ตฌ์กฐ
vertex.glsl
void main() {
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
vec4 viewPosition = viewMatrix * modelPosition;
vec4 projectedPosition = projectionMatrix * viewPosition;
gl_Position = projectedPosition;
gl_PointSize = 2.0; // ๊ธฐ๋ณธ ํฌ๊ธฐ
}
fragment.glsl
void main() {
gl_FragColor = vec4(1.0);
#include <colorspace_fragment>
}
๐งฎ 3. ํํฐํด ์ฌ์ด์ฆ ์ ์ด
Base size๋ฅผ JS → shader๋ก ์ ๋ฌ
uniforms: {
uSize: {
value: 8;
}
}
uniform float uSize;
gl_PointSize = uSize;
๋๋คํ ํํฐํด ํฌ๊ธฐ aScale
const scales = new Float32Array(count);
scales[i] = Math.random();
geometries.setAttribute('aScale', new THREE.BufferAttribute(scales, 1));
attribute float aScale;
gl_PointSize = uSize * aScale;
๊ณ ํด์๋ ๋์คํ๋ ์ด ๋์
uSize: {
value: 8 * renderer.getPixelRatio();
}
๐ฏ 4. ๊ฑฐ๋ฆฌ ๊ธฐ๋ฐ Size Attenuation ๊ตฌํ
vec4 viewPosition = viewMatrix * modelPosition;
gl_PointSize *= 1.0 / -viewPosition.z;
- ๊ฐ๊น์ด ํํฐํด์ ์ปค์ง๊ณ , ๋ฉ์๋ก ์์์ง (์๊ทผ ํจ๊ณผ)
๐ 5. ํํฐํด ํํ ํํ (gl_PointCoord ํ์ฉ)

โ light point ํํ
float strength = distance(gl_PointCoord, vec2(0.5));
strength = 1.0 - strength;
strength = pow(strength, 10.0);
vec3 color = mix(vec3(0.0), vColor, strength);
gl_FragColor = vec4(color, 1.0);
- gl_PointCoord: ๊ฐ ํํฐํด ๋ด ํฝ์ ์ UV ์ขํ (0~1), GL_POINTS๋ก ๋ ๋๋ง๋ ํ๋๊ทธ๋จผํธ ์์ด๋์์๋ง ์ฌ์ฉ ๊ฐ๋ฅ
- ์ค์ฌ์ด ๋ฐ๊ณ ์ฃผ๋ณ์ด ์ด๋์ด ๋น๋๋ ํจ๊ณผ
pow(base, exponent) ํจ์๋?
float pow(float base, float exponent);
- base: ๋ฐ(base), ์ฆ ๊ณฑํด์ง ์
- exponent: ์ง์(exponent), ๊ณฑํด์ง๋ ํ์
pow(a, b) = a๋ฅผ b๋ฒ ๊ณฑํ ๊ฐ (a^b)
- uv.y๊ฐ 0์ ๊ฐ๊น์ฐ๋ฉด pow(0.1, 2.0) = 0.01 → ๊ฑฐ์ 0
- uv.y๊ฐ 1.0์ด๋ฉด pow(1.0, 2.0) = 1.0 → ๊ทธ๋๋ก ์ ์ง
- ์๋์ชฝ์์๋ ์ํฅ์ด ๊ฑฐ์ ์๊ณ , ์์ชฝ์์๋ ๊ธ๊ฒฉํ ๊ฐํด์ง
์ด๋ฐ ์์ผ๋ก ๊ณก์ ์ ํจ๊ณผ(curved falloff), ์ค๋ฌด์ค ํ์ด๋, ๊ฐ์๋ ๋ฑ์ ๊ตฌํ
๐จ 6. ์์ ๋ณต์ (vertex → fragment ์ ๋ฌ)
// vertex shader
varying vec3 vColor;
vColor = color;
// fragment shader
varying vec3 vColor;
vec3 color = mix(vec3(0.0), vColor, strength);
โฑ๏ธ 7. ์๊ฐ์ ๋ฐ๋ฅธ ํ์ ์ ๋๋ฉ์ด์
JS์์ uTime uniform ์ค์
uniforms: {
uTime: {
value: 0;
}
}
material.uniforms.uTime.value = clock.getElapsedTime();
์ค์ฌ๊ณผ ๊ฑฐ๋ฆฌ ๊ธฐ๋ฐ์ผ๋ก ๊ฐ ํํฐํด ํ์
๊ฑฐ๋ฆฌ๋ณด์ node_modules/three/src/renderers/shaders/shaderLib/points.glsl.js๋ฅผ ๋ณด๋ฉด gl_PointSize *- (scale / -mvPosition.z ); ์ฝ๋๋ก ๊ฑฐ๋ฆฌ๋ณด์ ์ ํ๋ค๋ ๊ฑธ ํ์ธํ ์ ์๋ค.
vec4 modelPosition = modelMatrix * vec4(position, 1.0);
float angle = atan(modelPosition.x, modelPosition.z);
float distanceToCenter = length(modelPosition.xz);
float angleOffset = (1.0 / distanceToCenter) * uTime * 0.2;
angle += angleOffset;
modelPosition.x = cos(angle) * distanceToCenter;
modelPosition.z = sin(angle) * distanceToCenter;
- ์ค์ฌ์ ๊ฐ๊น์ธ์๋ก ๋ ๋น ๋ฅด๊ฒ ํ์
๐ 8. ์์น ๋๋ค์ฑ ๋ณด์กด: aRandomness

const randomness = new Float32Array(count * 3);
randomness[i3 + 0] = randomX;
randomness[i3 + 1] = randomY;
randomness[i3 + 2] = randomZ;
geometry.setAttribute('aRandomness', new THREE.BufferAttribute(randomness, 3));
attribute vec3 aRandomness;
modelPosition.xyz += aRandomness;
- ํ์ ์ ์ฉ ํ ๋ง์ง๋ง์ ์์น ๋ ธ์ด์ฆ ์ถ๊ฐ
ํ์ฅ ์์ด๋์ด
| ๋๋ฒ๊ทธ GUI ์ถ๊ฐ | uSize, speed, branch count ๋ฑ ์กฐ์ |
| ๋ธ๋ํ ๊ตฌํ | ์ค์ฌ์ ๊ตฌ์ฒด ์ถ๊ฐ, gravitational pull ํํ |
| ํํฐํด ์์ ์ ๋๋ฉ์ด์ | ์๋๋ ์์น์ ๋ฐ๋ผ ์์ ๋ณด๊ฐ |
์ ๋ฆฌ ์์ฝ
| ์์น | Shader์์ ์ง์ ํ์ ์ ๋๋ฉ์ด์ ์ ์ฉ |
| ํฌ๊ธฐ | uSize × aScale × 1.0 / -viewPosition.z |
| ์์ | vertex → fragment via vColor |
| ์ ฐ์ดํ | gl_PointCoord + pow(strength, n) ์ฌ์ฉ |
| ๋๋ค์ฑ ์ ์ง | aRandomness๋ฅผ position์ ๋์ค์ ๋ํจ |
์ด์ ์์ฒ ๊ฐ์ ๋ณ์ด ํ์ ํ๊ณ ๋น๋๋ ์ํ๋ฅผ ์ ฐ์ด๋๋ง์ผ๋ก ๊ตฌํํ ์ ์์ต๋๋ค. ๐
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import GUI from 'lil-gui';
import galaxyVertexShader from './shaders/galaxy/vertex.glsl';
import galaxyFragmentShader from './shaders/galaxy/fragment.glsl';
/**
* Base
*/
// ๋๋ฒ๊น
์ฉ GUI ์์ฑ
const gui = new GUI();
// ์บ๋ฒ์ค ์๋ฆฌ๋จผํธ ์ ํ
const canvas = document.querySelector('canvas.webgl');
// ์ฌ ์์ฑ
const scene = new THREE.Scene();
/**
* Galaxy ํ๋ผ๋ฏธํฐ
*/
const parameters = {};
parameters.count = 200000; // ์ด ์
์ ์
parameters.size = 0.005; // ์
์ ๊ธฐ๋ณธ ํฌ๊ธฐ
parameters.radius = 5; // ์ํ ์ ์ฒด ๋ฐ์ง๋ฆ
parameters.branches = 3; // ์ํ ํ ๊ฐ์
parameters.spin = 1; // ํ ํ์ ๋
parameters.randomness = 0.5; // ๋ฌด์์ ์์น ๋ณํ ๋ฒ์
parameters.randomnessPower = 3; // ๋ฌด์์ ๋ณํ ๊ณก์ ๊ฐ๋
parameters.insideColor = '#ff6030'; // ์ค์ฌ๋ถ ์์
parameters.outsideColor = '#1b3984'; // ์ธ๊ณฝ๋ถ ์์
let geometry = null;
let material = null;
let points = null;
// ์ํ ์์ฑ ํจ์
const generateGalaxy = () => {
// ์ด์ ์ํ ์ ๊ฑฐ
if (points !== null) {
geometry.dispose();
material.dispose();
scene.remove(points);
}
/**
* Geometry
*/
geometry = new THREE.BufferGeometry();
const positions = new Float32Array(parameters.count * 3); // ์์น ์์ฑ
const colors = new Float32Array(parameters.count * 3); // ์์ ์์ฑ
const scales = new Float32Array(parameters.count * 1); // ํฌ๊ธฐ ์ค์ผ์ผ ์์ฑ
const randomness = new Float32Array(parameters.count * 3); // ๋ฌด์์ ์คํ์
const insideColor = new THREE.Color(parameters.insideColor);
const outsideColor = new THREE.Color(parameters.outsideColor);
for (let i = 0; i < parameters.count; i++) {
const i3 = i * 3;
// ๋ฐ์ง๋ฆ ๋ฐ ํ์ ๊ฐ๋ ๊ณ์ฐ
const radius = Math.random() * parameters.radius;
const branchAngle =
((i % parameters.branches) / parameters.branches) * Math.PI * 2;
// ๊ธฐ๋ณธ ์์น (ํ์ ์ ์ฉ ์ )
positions[i3] = Math.cos(branchAngle) * radius;
positions[i3 + 1] = 0;
positions[i3 + 2] = Math.sin(branchAngle) * radius;
// ๋ฌด์์ ์์น ๋ณํ
const randomX =
Math.pow(Math.random(), parameters.randomnessPower) *
(Math.random() < 0.5 ? 1 : -1) *
parameters.randomness *
radius;
const randomY =
Math.pow(Math.random(), parameters.randomnessPower) *
(Math.random() < 0.5 ? 1 : -1) *
parameters.randomness *
radius;
const randomZ =
Math.pow(Math.random(), parameters.randomnessPower) *
(Math.random() < 0.5 ? 1 : -1) *
parameters.randomness *
radius;
randomness[i3 + 0] = randomX;
randomness[i3 + 1] = randomY;
randomness[i3 + 2] = randomZ;
// ์ค์ฌ์์ ์ธ๊ณฝ์ผ๋ก ์์ ๋ณด๊ฐ
const mixedColor = insideColor.clone();
mixedColor.lerp(outsideColor, radius / parameters.radius);
colors[i3] = mixedColor.r;
colors[i3 + 1] = mixedColor.g;
colors[i3 + 2] = mixedColor.b;
// ์
์ ํฌ๊ธฐ ์ค์ผ์ผ ๋๋คํ
scales[i] = Math.random();
}
// ์์ฑ ๋ฒํผ์ ์ค์
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
geometry.setAttribute('aScale', new THREE.BufferAttribute(scales, 1));
geometry.setAttribute(
'aRandomness',
new THREE.BufferAttribute(randomness, 3),
);
/**
* Material
*/
material = new THREE.ShaderMaterial({
depthWrite: false, // ๊น์ด ๋ฒํผ์ ๊ธฐ๋กํ์ง ์์
blending: THREE.AdditiveBlending, // ์์ ํผํฉ ๋ฐฉ์ (๋ ๋ฐ๊ฒ)
vertexColors: true, // vertex ์์ฑ์ color ์ฌ์ฉ
vertexShader: galaxyVertexShader, // ์ ์ ์์ด๋
fragmentShader: galaxyFragmentShader, // ํ๋๊ทธ๋จผํธ ์์ด๋
uniforms: {
uTime: { value: 0 }, // ์๊ฐ๊ฐ (์ ๋๋ฉ์ด์
์ฉ)
uSize: { value: 30 * renderer.getPixelRatio() }, // ํฌ์ธํธ ์ฌ์ด์ฆ ๋ณด์
},
});
/**
* Points (์ ์
์ ์์คํ
)
*/
points = new THREE.Points(geometry, material);
scene.add(points);
};
// GUI ์ธํฐํ์ด์ค ์ค์
// ๊ฐ ํ๋ผ๋ฏธํฐ ๋ณ๊ฒฝ ์ ์ํ ๋ค์ ์์ฑ
['count', 'radius', 'branches', 'randomness', 'randomnessPower'].forEach(
(key) => {
gui.add(parameters, key).onFinishChange(generateGalaxy);
},
);
gui.addColor(parameters, 'insideColor').onFinishChange(generateGalaxy);
gui.addColor(parameters, 'outsideColor').onFinishChange(generateGalaxy);
/**
* ํ๋ฉด ํฌ๊ธฐ ๋์
*/
const sizes = {
width: window.innerWidth,
height: window.innerHeight,
};
window.addEventListener('resize', () => {
sizes.width = window.innerWidth;
sizes.height = window.innerHeight;
camera.aspect = sizes.width / sizes.height;
camera.updateProjectionMatrix();
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});
/**
* ์นด๋ฉ๋ผ
*/
const camera = new THREE.PerspectiveCamera(
75,
sizes.width / sizes.height,
0.1,
100,
);
camera.position.set(3, 3, 3);
scene.add(camera);
// ๊ถค๋ ์ปจํธ๋กค
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;
/**
* ๋ ๋๋ฌ
*/
const renderer = new THREE.WebGLRenderer({ canvas });
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
/**
* ์ํ ์์ฑ ๋ฐ ์ ๋๋ฉ์ด์
๋ฃจํ
*/
generateGalaxy();
const clock = new THREE.Clock();
const tick = () => {
const elapsedTime = clock.getElapsedTime();
material.uniforms.uTime.value = elapsedTime; // ์๊ฐ ์
๋ฐ์ดํธ
controls.update();
renderer.render(scene, camera);
window.requestAnimationFrame(tick); // ๋ค์ ํ๋ ์ ํธ์ถ
};
tick();
// vertex.glsl
// Uniforms: ์ธ๋ถ์์ ์ ๋ฌ๋๋ ์ ์ญ ๋ณ์
uniform float uTime; // ์๊ฐ์ ๋ฐ๋ผ ํ์ ์ ๋๋ฉ์ด์
์ ๋ง๋ค๊ธฐ ์ํ ์๊ฐ ๊ฐ (์ด ๋จ์)
uniform float uSize; // ๊ฐ ์
์์ ๊ธฐ๋ณธ ํฌ๊ธฐ (์นด๋ฉ๋ผ ๊ฑฐ๋ฆฌ ๋ณด์ ์ )
// Attributes: ๊ฐ ์ ์ ๋ง๋ค ๊ณ ์ ํ๊ฒ ์กด์ฌํ๋ ์์ฑ
attribute float aScale; // ์
์์ ๋๋ค ํฌ๊ธฐ ๊ณ์ (0.0~1.0 ์ฌ์ด)
attribute vec3 aRandomness; // ์
์ ์์น์ ์ ์ฉ๋ ๋ฌด์์ ์คํ์
(x, y, z)
// Varying: ์ ์ → ํ๋๊ทธ๋จผํธ ์
ฐ์ด๋๋ก ์ ๋ฌ๋๋ ๊ฐ
varying vec3 vColor; // ์ต์ข
ํ๋๊ทธ๋จผํธ์์ ์ฌ์ฉํ ์์ ๊ฐ
void main() {
/**
* Position ๊ณ์ฐ (๋ชจ๋ธ ์ขํ → ๋ทฐ ์ขํ → ํด๋ฆฝ ์ขํ)
*/
vec4 modelPosition = modelMatrix * vec4(position, 1.0); // ๋ก์ปฌ ์์น → ์๋ ์์น
// ๊ฐ ์
์์ ์๊ฐ ๊ธฐ๋ฐ ํ์ ์ ๋๋ฉ์ด์
์ ์ฉ
float angle = atan(modelPosition.x, modelPosition.z); // ์ค์ฌ ๊ธฐ์ค ๊ฐ๋ ๊ณ์ฐ
float distanceToCenter = length(modelPosition.xz); // ์ค์ฌ์์์ ๊ฑฐ๋ฆฌ (ํ์ ๋ฐ์ง๋ฆ)
float angleOffset = (1.0 / distanceToCenter) * uTime * 0.2; // ๊ฑฐ๋ฆฌ์ ๋ฐ๋ผ ๊ฐ๋ ๋ณด์ (๋ ๋ฉ๋ฆฌ ์๋ ์
์๊ฐ ๋ ์ฒ์ฒํ ํ์ )
angle += angleOffset; // ๋ณด์ ๊ฐ๋๋ฅผ ์ ์ฉํ ์ต์ข
ํ์ ๊ฐ
modelPosition.x = cos(angle) * distanceToCenter; // x์ขํ ํ์ ๋ฐ์
modelPosition.z = sin(angle) * distanceToCenter; // z์ขํ ํ์ ๋ฐ์
// ๋ฌด์์ ์คํ์
์ ํตํด ์
์ ํผ์ง ํจ๊ณผ ๊ฐํ
modelPosition.xyz += aRandomness;
// ๋ทฐ ๋ณํ ๋ฐ ํฌ์ ๋ณํ ์ํ
vec4 viewPosition = viewMatrix * modelPosition;
vec4 projectedPosition = projectionMatrix * viewPosition;
gl_Position = projectedPosition;
/**
* Size ๊ณ์ฐ (์นด๋ฉ๋ผ ๊ฑฐ๋ฆฌ ๊ธฐ๋ฐ ๋ณด์ ํฌํจ)
*/
gl_PointSize = uSize * aScale; // ๊ธฐ๋ณธ ์ฌ์ด์ฆ์ ์ค์ผ์ผ์ ๊ณฑํจ
gl_PointSize *= (aScale / -viewPosition.z); // z์ถ ๊ฑฐ๋ฆฌ ๋ฐ๋น๋ก๋ก ํฌ๊ธฐ ์กฐ์ (์นด๋ฉ๋ผ ๊ฐ๊น์ด์ผ์๋ก ํผ)
/**
* ์์ ์ ๋ฌ
*/
vColor = color; // color ์์ฑ(attribute vec3 color;)์ varying์ผ๋ก ์ ๋ฌ
}
// fragment.glsl
// varying: ์ ์ ์
ฐ์ด๋์์ ๋์ด์จ ๋ณด๊ฐ๋ ๊ฐ
varying vec3 vColor; // ์
์์ ์์ ์ ๋ณด
void main() {
/**
* ์ํ ์
์ ๋ง์คํฌ ์์ฑ (gl_PointCoord ๊ธฐ์ค)
* ์ค์ฌ์ผ๋ก๋ถํฐ ๋ฉ์ด์ง์๋ก ์ด๋์์ง๊ฒ ์ฒ๋ฆฌ
*/
float strength = distance(gl_PointCoord, vec2(0.5)); // ์ค์ฌ์์์ ๊ฑฐ๋ฆฌ (0.0 ~ 0.707)
strength = 1.0 - strength; // ๋ฐ์ (์ค์์ด 1.0, ์ธ๊ณฝ์ด 0.0)
strength = pow(strength, 10.0); // ๊ฐ์ฅ์๋ฆฌ ๋ถ๋๋ฝ๊ฒ ๊ฐ์์ํค๊ธฐ (๊ฑฐ๋ญ์ ๊ณฑ ์ปค๋ธ)
/**
* ์ต์ข
์์ ์กฐํฉ (์
์ ์์๊ณผ ๊ฐ๋ ๊ธฐ๋ฐ์ผ๋ก ํผํฉ)
*/
vec3 color = mix(vec3(0.0), vColor, strength); // strength๊ฐ 0์ ๊ฐ๊น์ธ์๋ก ๊ฒ์ ์์ ๊ฐ๊น์์ง
gl_FragColor = vec4(color, 1.0); // ์ํ๊ฐ์ 1.0 (๋ถํฌ๋ช
)
#include <colorspace_fragment> // sRGB ์ ๊ณต๊ฐ์ผ๋ก ์์ ๋ณด์
}'Graphic > ThreeJS' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| 45 ํ์ฒ๋ฆฌ(Post-Processing) (5) | 2025.08.06 |
|---|---|
| 31 MeshStandardMaterial ์ ฐ์ด๋ ํ์ฅ(Modified materials) (2) | 2025.08.05 |
| 29 Animated Water Shader (2) | 2025.08.04 |
| 28 Shader patterns (4) | 2025.08.04 |
| 27 Shaders (5) | 2025.08.02 |