๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
Graphic/ThreeJS

30 Animated galaxy

by curious week 2025. 8. 5.

๐ŸŒŒ 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