๐ŸŽ“ 5์ฃผ์ฐจ: ๋…ธ์ด์ฆˆ (Perlin / Simplex / Worley Noise)

BamgasiJMยท2025๋…„ 11์›” 5์ผ

p5.js Art

๋ชฉ๋ก ๋ณด๊ธฐ
16/67
post-thumbnail

noise๋Š” ์žก์Œ, ์›์น˜ ์•Š๋Š” ์‹ ํ˜ธ๋‚˜ ๋ถˆ๊ทœ์น™ํ•œ ํŒจํ„ด์„ ๋œปํ•ฉ๋‹ˆ๋‹ค.

์ผ๋ฐ˜์ ์ธ ๋…ธ์ด์ฆˆ๋Š” ์—ฐ์†์ ์ด์ง€ ์•Š๊ณ  ๊ธ‰๊ฒฉํ•œ ๋ณ€ํ™”๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ Ken Perlin์ด ๊ฐœ๋ฐœํ•œ ํŽ„๋ฆฐ ๋…ธ์ด์ฆˆ(Perlin Noise)๋Š” ๋ถ€๋“œ๋Ÿฝ๊ณ  ์ž์—ฐ์Šค๋Ÿฌ์šด ํŒจํ„ด์„ ์ƒ์„ฑํ•˜๋Š” ์•Œ๊ณ ๋ฆฌ์ฆ˜์œผ๋กœ, ์ž์—ฐ ํ˜„์ƒ์—์„œ ๋ณด์ด๋Š” ์œ ๊ธฐ์ ์ธ ํŒจํ„ด์„ ๋งŒ๋“ค ๋•Œ ์ž์ฃผ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
Perlin noise ์œ„ํ‚คํ”ผ๋””์•„ ๋งํฌ

๋Œ€์ฒด๋กœ Generative Art์—์„œ ์‚ฌ์šฉํ•˜๋Š” Noise๋Š” ์—ฐ์†์„ฑ ์žˆ๋Š” ๋ฌด์ž‘์œ„(continuous randomness)์ž…๋‹ˆ๋‹ค. ์ฆ‰, ์™„์ „ํžˆ ๋žœ๋ค์ด์ง€๋งŒ ์ธ์ ‘ํ•œ ๊ฐ’๋ผ๋ฆฌ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ์—ฐ๊ฒฐ๋˜์–ด ์žˆ๋Š” ๊ฒƒ์ด์ง€์š”.
์ด๊ฑธ ์ด์šฉํ•ด์„œ ์ž์—ฐ ํ˜„์ƒ์ด๋‚˜ ํ…์Šค์ฒ˜, ์ง€ํ˜•, ํ๋ฆ„, ์œ ๊ธฐ์  ํ˜•ํƒœ๋ฅผ ํ‘œํ˜„ํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

Perlin Noise์™€ ํ•จ๊ป˜ ์ž˜ ์“ฐ์ด๋Š” ๋…ธ์ด์ฆˆ๋Š” ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ Perlin์ด 2001๋…„์— ๊ฐœ๋ฐœํ•œ Simplex Noise์ž…๋‹ˆ๋‹ค. ์ด ๋…ธ์ด์ฆˆ ์•Œ๊ณ ๋ฆฌ์ฆ˜์€ "๊ณ ์ „์ "์ธ ํŽ„๋ฆฐ ๋…ธ์ด์ฆˆ์™€ ์œ ์‚ฌํ•˜์ง€๋งŒ ํšจ์œจ์„ฑ์ด ๊ฐœ์„ ๋˜์—ˆ๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

์•„๋ž˜๋Š” ๋ช‡๊ฐ€์ง€ ๋…ธ์ด์ฆˆ ์•Œ๊ณ ๋ฆฌ์ฆ˜์ž…๋‹ˆ๋‹ค.

Noise ์ข…๋ฅ˜ํŠน์ง•p5.js ์ง€์› ์—ฌ๋ถ€
Perlin Noise๋ถ€๋“œ๋Ÿฝ๊ณ  ๊ทธ๋ผ๋””์–ธํŠธ ๊ธฐ๋ฐ˜. ์ž์—ฐ์Šค๋Ÿฌ์šด ์—ฐ์†์„ฑ.โœ… ๋‚ด์žฅ (noise())
Simplex NoisePerlin์˜ ๊ฐœ์„  ๋ฒ„์ „. ๊ณ ์ฐจ์›์—์„œ ๊ณ„์‚ฐ์ด ๋น ๋ฅด๊ณ  artifacts ์ ์Œ.โŒ ์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ•„์š”
OpenSimplex NoiseSimplex ํŠนํ—ˆ ๋ฌธ์ œ๋ฅผ ํ”ผํ•œ ์˜คํ”ˆ ๋ฒ„์ „.โŒ ์™ธ๋ถ€ ์ฝ”๋“œ ํ•„์š”
Worley Noise (Cellular Noise)์…€ ๊ตฌ์กฐ ๊ธฐ๋ฐ˜ ํŒจํ„ด (์˜ˆ: ์•”์„, ํ”ผ๋ถ€ ํ…์Šค์ฒ˜ ๋“ฑ)โŒ ์™ธ๋ถ€ ์ฝ”๋“œ ํ•„์š”
Value Noise๊ฒฉ์ž์ ์˜ ๋žœ๋ค๊ฐ’์„ ๋ณด๊ฐ„ํ•œ ์ดˆ๊ธฐ ํ˜•ํƒœ์˜ noiseโŒ ์ง์ ‘ ๊ตฌํ˜„ ๊ฐ€๋Šฅ

Perlin Noise BASIC

ํŽ„๋ฆฐ ๋…ธ์ด์ฆˆ์˜ ๊ธฐ๋ณธ์ ์ธ 1์ฐจ์› ๋ผ์ธ์œผ๋กœ ํ‘œํ˜„ํ•œ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค.
์™ผ์ชฝ์—์„œ ์˜ค๋ฅธ์ชฝ์œผ๋กœ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ๋…ธ์ด์ฆˆ ๋ผ์ธ์ด ์ƒ๊ฒจ๋‚ฉ๋‹ˆ๋‹ค.

let xoff = 0;

function setup() {
  createCanvas(800, 400);
  background(255);
  frameRate(60);
}

function draw() {
  let y = noise(xoff) * height;
  noStroke();
  fill(0, 100);
  // x ์œ„์น˜: frameCount % width (์Šคํฌ๋กค์ฒ˜๋Ÿผ ํ‘œ์‹œ)
  ellipse((frameCount - 1) % width, y, 8, 8);
  xoff += 0.02;

  if (frameCount > width) {
    noLoop();
  }
}

๋‹ค๋ฅธ ๋А๋‚Œ์˜ ์• ๋‹ˆ๋ฉ”์ด์…˜์œผ๋กœ ์ฝ”๋“œ๋ฅผ ์งค ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.
์‹ฌ์ „๋„๊ธฐ์˜ ๊ทธ๋ž˜ํ”„์ฒ˜๋Ÿผ ํ๋ฅด๋Š” ๋“ฏํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ํ‘œํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

let start = 0;

function setup() {
  createCanvas(800, 400);
  frameRate(60);
}

function draw() {
  background(25);
  noFill();
  stroke(10, 190, 180);
  strokeWeight(5);
  beginShape();
  let xoff = start;
  for (let x = 0; x < width; x++) {
    let y = noise(xoff) * height;
    vertex(x, y);
    xoff += 0.01;
  }
  endShape();

  start += 0.01;
}

Perlin Noise Cloud

let scale = 0.02;

function setup() {
  createCanvas(800, 400);
  pixelDensity(1); // โœ… ํ”ฝ์…€ ๋ฐ€๋„ 1๋กœ ์„ค์ •
  noLoop();
}

function draw() {
  background(25);
  loadPixels();

  for (let x = 0; x < width; x++) {
    for (let y = 0; y < height; y++) {
      let n = noise(x * scale, y * scale);
      let bright = map(n, 0, 1, 0, 255);
      // let bright = floor(n * 255);
      let idx = (x + y * width) * 4;
      
      pixels[idx] = bright;
      pixels[idx + 1] = bright;
      pixels[idx + 2] = bright;
      pixels[idx + 3] = 255;
    }
  }

  updatePixels();
}

Noise Flow Field

์ž…์ž์˜ ํ๋ฆ„์„ ๋…ธ์ด์ฆˆ ํ•„๋“œ๋กœ ํ‘œํ˜„ํ•˜๋ฉด ์ด๋ ‡๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

let inc = 0.1;
let scl = 10;
let cols, rows;
let zoff = 0;
let particles = [];
let flowfield = [];

function setup() {
  createCanvas(800, 400);
  cols = floor(width / scl);
  rows = floor(height / scl);
  flowfield = new Array(cols * rows);
  for (let i = 0; i < 2000; i++) {
    particles.push(new Particle());
  }
  background(25);
  frameRate(60);
}

function draw() {
  // build flow field
  let yoff = 0;
  for (let y = 0; y < rows; y++) {
    let xoff = 0;
    for (let x = 0; x < cols; x++) {
      let angle = noise(xoff, yoff, zoff) * TWO_PI * 4;
      let v = p5.Vector.fromAngle(angle);
      v.setMag(0.1);
      let index = x + y * cols;
      flowfield[index] = v;
      xoff += inc;
    }
    yoff += inc;
  }
  zoff += 0.003;

  // update particles
  for (let p of particles) {
    p.follow(flowfield);
    p.update();
    p.edges();
    p.show();
  }
}

// Particle class with safe follow (clamp indices)
class Particle {
  constructor() {
    this.pos = createVector(random(width), random(height));
    this.vel = createVector(0, 0);
    this.acc = createVector(0, 0);
    this.maxSpeed = 2;
    this.prev = this.pos.copy();
  }

  follow(vectors) {
    let x = floor(this.pos.x / scl);
    let y = floor(this.pos.y / scl);
    // ์•ˆ์ „ํ•˜๊ฒŒ ๋ฒ”์œ„ ํด๋žจํ”„
    x = constrain(x, 0, cols - 1);
    y = constrain(y, 0, rows - 1);
    let index = x + y * cols;
    let force = vectors[index];
    if (force) {
      this.applyForce(force);
    }
  }

  applyForce(force) {
    // ๋ณต์‚ฌํ•ด์„œ ์ ์šฉ (์›๋ณธ ๋ฒกํ„ฐ๋ฅผ mutate ํ•˜์ง€ ์•Š์Œ)
    let f = force.copy();
    this.acc.add(f);
  }

  update() {
    this.vel.add(this.acc);
    this.vel.limit(this.maxSpeed);
    this.pos.add(this.vel);
    this.acc.mult(0);
  }

  show() {
    stroke(30, 210, 200, 10);
    line(this.pos.x, this.pos.y, this.prev.x, this.prev.y);
    this.updatePrev();
  }

  updatePrev() {
    this.prev.x = this.pos.x;
    this.prev.y = this.pos.y;
  }

  edges() {
    if (this.pos.x > width) {
      this.pos.x = 0;
      this.updatePrev();
    }
    if (this.pos.x < 0) {
      this.pos.x = width;
      this.updatePrev();
    }
    if (this.pos.y > height) {
      this.pos.y = 0;
      this.updatePrev();
    }
    if (this.pos.y < 0) {
      this.pos.y = height;
      this.updatePrev();
    }
  }
}

๐Ÿง  ์ฐธ๊ณ 

Retina ํ™”๋ฉด์—์„œ๋Š” pixelDensity() ๊ธฐ๋ณธ๊ฐ’์ด 2์ด๋ฏ€๋กœ
loadPixels() ์‹œ ์‹ค์ œ pixels[] ๋ฐฐ์—ด ํฌ๊ธฐ๊ฐ€ width pixelDensity() height pixelDensity() 4๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.
์ง์ ‘ ํ”ฝ์…€ ๋ฐฐ์—ด์„ ์กฐ์ž‘ํ•  ๋•Œ๋Š” ํ•ญ์ƒ pixelDensity(1)์„ ๋„ฃ๋Š” ๊ฒƒ์ด ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.


Simplex Noise BASIC (์• ๋‹ˆ๋ฉ”์ด์…˜)

<!-- index.html -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>Sketch</title>

    <link rel="stylesheet" type="text/css" href="/style.css">

    <script src="/libraries/p5.min.js"></script>
    <script src="/libraries/p5.sound.min.js"></script>
  </head>

  <body>
    <!-- ES6 ๋ชจ๋“ˆ ๋ฐฉ์‹ ์‚ฌ์šฉ -->
    <script type="module" src="simplex_noise_basic.js"></script>
  </body>
</html>
// simplex_noise_basic.js
// ES6 ๋ชจ๋“ˆ ๋ฐฉ์‹ ์‚ฌ์šฉ

import { createNoise3D } from "https://unpkg.com/simplex-noise@4.0.1/dist/esm/simplex-noise.js";

let noise3D;
let time = 0;

function setup() {
  createCanvas(800, 400);
  noise3D = createNoise3D();
  pixelDensity(1);
  noStroke();
}

function draw() {
  background(0);

  // ํ”ฝ์…€ ๊ฐ„๊ฒฉ์„ ๋„์›Œ์„œ ๊ณ„์‚ฐ๋Ÿ‰ ์ค„์ด๊ธฐ
  let step = 5;

  for (let x = 0; x < width; x += step) {
    for (let y = 0; y < height; y += step) {
      let noiseVal = noise3D(x * 0.005, y * 0.005, time);
      let brightness = map(noiseVal, -1, 1, 0, 255);

      fill(brightness);
      rect(x, y, step, step);
    }
  }

  time += 0.01;

  // fps ํ‘œ์‹œ
  fill(255);
  text(floor(frameRate()), 10, 20);
}

window.setup = setup;
window.draw = draw;

Worley Noise BASIC

let points = [];

function setup() {
  createCanvas(800, 400);
  // ๋žœ๋ค ํŠน์ง•์  ์ƒ์„ฑ
  for (let i = 0; i < 50; i++) {
    points.push(createVector(random(width), random(height)));
  }
}

function worleyNoise(x, y) {
  let minDist = Infinity;
  for (let point of points) {
    let distance = dist(x, y, point.x, point.y);
    minDist = min(minDist, distance);
  }
  return minDist;
}

function draw() {
  for (let x = 0; x < width; x += 2) {
    for (let y = 0; y < height; y += 2) {
      let value = worleyNoise(x, y);
      let color = map(value, 0, 100, 255, 0);
      fill(color);
      noStroke();
      rect(x, y, 2, 2);
    }
  }
  noLoop();
}

profile
Coding Art with Blender / oF / Processing / p5.js / nannou

0๊ฐœ์˜ ๋Œ“๊ธ€