My Boundary As Much As I Experienced

펄린 노이즈(Perlin Noise)를 이용하여 예쁜(?) 무작위성을 주기 본문

Interactive(HTML Canvas ・three.js ・game)/CSS・Canvas ・ p5.js ・ Pixi.js

펄린 노이즈(Perlin Noise)를 이용하여 예쁜(?) 무작위성을 주기

Bumang 2024. 5. 17. 02:18

펄린 노이즈(Perlin Noise)란?

펄린 노이즈(Perlin Noise)는 컴퓨터 그래픽스와 애니메이션에서 자연스러운 텍스처와 패턴을 생성하기 위해 사용되는 알고리즘이다.

켄 펄린(Ken Perlin)이 개발한 이 기법은 랜덤한 값을 기반으로 하지만, 부드럽고 연속적인 변화를 가지도록 설계되어 구름, 산맥, 물결 등의 자연 현상을 시뮬레이션하는 데 유용하다. 펄린 노이즈는 각기 다른 해상도의 노이즈를 합성하여 복잡하고 디테일한 결과를 얻는 프랙탈 노이즈 생성에도 자주 사용된다.

 

p5.js에서의 펄린 노이즈

p5.js에선 noise함수를 이용하여 펄린 노이즈를 만들 수 있다. 안에 좌표가 될 숫자값을 하나 넣어줘야한다.

 

그리고 p5.js의 기본적인 사용성 중 하나인데, draw함수는 1초에 60프레임으로 scene을 계속 재생성한다.

draw함수는 실제로 실행시킬 필요가 없고, p5.js 스크립트가 로드된 html에선 전역적으로 자동 실행된다.

즉, draw 자체가 하나의 예약어가 된 것이다.

 

즉, 다른 용도로 함수를 만들 때 실수로 draw나 setup으로 하면 충돌이 날 것이니 주의하라.

function draw() {
  const noise = noise(0);
}

 

이 외에도 실제 완전한 무작위 랜덤을 만들기 위한 random함수도 존재한다.

Math.random()과의 차이점은 아직 나도 모르겠다. 하여튼 이것도 랜덤 생성기이다.

function draw() {
  const random = random(0);
}

 

 

그렇다면 noise함수와 random함수의 차이가 뭘까?

맨 위에서 설명했듯이 noise는 랜덤값이지만, 가까운 좌표들 끼리는 어느정도 유사한 값들을 가지게 된다.

그러나 random은 그런거 없다. 그냥 혼돈의 카오스이다.

펄린노이즈 예시, x = 0; x < 255; x+1 반복문을 돌리면서 y값을 노이즈로 넣고 버텍스로 만든 이미지
랜덤 예시, x = 0; x < 255; x+1 반복문을 돌리면서 y값을 랜덤으로 넣고 버텍스로 만든 이미지

 

 

이미지를 보니 대강 감이 잡힐 것이다. noise는 왠지모르게 지형도, 등고선 같은 느낌을 준다.

2차원 평면으로는 저렇게 생겼지만 3차원 평면으로 구현해놓으면 영락없이 산처럼 생겼다.

 

하여튼 각설하고 이번 실습을 하면서 만들어본 이미지들을 모두 기록해보겠다.

 

 

 

 

 

 

노이즈값을 조금씩 변화시켜 원의 x로 설정할 때 (y는 고정)

let xoff = 0;

function setup() {
  createCanvas(400, 400);

  background(200);
}

// 1초에 60프레임 그려주는 함수이다.
// random은 항상 다른 값이 나오는데
// noise는 계수가 같다면 항상 같은 값이 나온다.
function draw() {
  background(51);
  
  // map의 첫번째 인자가 타겟 값이고,
  // 두번째~세번째가 원래 스펙트럼, 네번째~5번째가 변형할 스펙트럼이다.
  // 사실 이것만 읽으면 이해가 안 될테니 직접 찾아보길 권장한다.
  let x = map(noise(xoff), 0, 1, 0, width);
  
  // noise에 공급하는 xoff를 조금씩 변화시킨다.
  xoff += 0.01;

  ellipse(x, 200, 24, 24);
}

 

 

 

 

x가 무작위의 random함수일 때 (어우 정신없어;;)

let xoff = 0;

function setup() {
  createCanvas(400, 400);

  background(200);
}

// 정말 100%의 무작위성으로 혼란스럽게 수평으로 왔다리갔다리 하는걸 볼 수 있다.
function draw() {
  background(51);

  let x = random(width);

  ellipse(x, 200, 24, 24);
}

 

 

 

3초마다 x축의 속도가 달라지는 비행체(?)

/*
  3초마다 가속도가 달라지는 무작위 비행체(?) - setTimeout으로 구현
*/

let xoff = 0;
let diff = 0.01;

let timed = null; // 시간 제한을 할당할 변수

function setup() {
  createCanvas(400, 400);
  background(200);
}

function draw() {

  let x = map(noise(xoff), 0, 1, 0, width);

  // 1. time가 null이면 참이 되어 setTimeout을 만들고,
  // 2. 그 이후엔 timed가 더 이상 falshy가 아니니 그냥 지나치게 된다.
  // 3. 그리고 setTimeout 안에 timed를 다시 null로 재할당 해버리고
  // 결국 1번부터 다시 반복된다.
  if (!timed) { // 시간 할당이 없다면
    timed = setTimeout(() => { // 셋 타임아웃 설정
      console.log("timed launched!");
      diff = random() / 10; // 진폭 생성
      timed = null; // timed 초기화
    }, 3000);
  }

  // xoff 1초에 60번씩 조금씩(diff만큼) 변화시켜 x를 약간씩 다른 좌표로 이동시킨다는 느낌.
  xoff += diff;

  ellipse(x, 200, 24, 24);
}

 

아래는 setInterval로 구현한 것이다. mouse를 올리면 멈추고 내리면 다시 시작하는 이벤트 리스너도 넣어보았다.

/*
  3초마다 가속도가 달라지는 무작위 비행체(?), 그런데 마우스가 올라가면 멈추는 - setInterval로 구현
*/

let xoff = 0;
let diff = 0.01;
let play = true;

// 처음 코드에선 setTimeout을 이용해서 구현했는데
// 생각해보니깐 그냥 setInterval 쓰면 되잖아...
// 2초마다 diff를 다르게 만드는 interval
setInterval(() => {
  diff = random();
}, 2000);

let timed;

function setup() {
  const canvas = createCanvas(400, 400);
  background(200);
  // canvas의 메소드에 콜백으로 넣어주면 이벤트 리스너처럼 작동한다.
  canvas.mouseOver(handleMouseOver); // play를 false로 하는 핸들러
  canvas.mouseOut(handleMouseOut); // play를 true로 하는 핸들러
}

function draw() {
  background(50);
  let x = map(noise(xoff), 0, 1, 0, width);

  if (play) {
    xoff += diff;
  }

  ellipse(x, 200, 24, 24);
}

function handleMouseOver() {
  play = false;
}

function handleMouseOut() {
  play = true;
}

 

 

x와 y에 모두 noise를 줬다. ( 완전히 똑같지 않게 하기 위해 계수는 다르게 설정했다.)

마우스를 올려놓으면 정지하고, 내리면 정지하는 로직도 구현했다.

// off와 off2의 seed를 매우 다르게 설정
let off = 0;
let off2 = 10000;

let diff = 0.01;
let play = true;

setInterval(() => {
  diff = random() / 20;
}, 2000);

let timed;

function setup() {
  const canvas = createCanvas(400, 400);
  background(200);
  canvas.mouseOver(handleMouseOver);
  canvas.mouseOut(handleMouseOut);
}

function draw() {
  background(50);
  let x = map(noise(off), 0, 1, 0, width);
  let y = map(noise(off2), 0, 1, 0, width);

  if (play) {
    off += diff; // 같은 값을 더하지만
    off2 += diff; // 둘이 출발 seed가 달라서 매우 다른 움직임을 보인다.
  }

  ellipse(x, y, 24, 24);
}

function handleMouseOver() {
  play = false;
}

function handleMouseOut() {
  play = true;
}

 

0부터 400까지 (캔버스 사이즈) 반복문을 돌면서 x 점 찍기 (y는 고정)

function setup() {
  const canvas = createCanvas(400, 400);
  background(200);
}

function draw() {
  background(50);

  for (let x = 0; x < width; x++) {
    // stroke는 stroke의 색상설정을 말함
    // 패러미터를 하나 넣으면 r,g,b 모두 같은 값 적용.
    // 다르게 넣을 수도 있음 예시: stroke(200, 255, 180)
    stroke(255);
    // strokeWeight는 말그대로 굵기를 말함
    strokeWeight(4);
    
    // y는 200고정, x도 0부터 width까지 그냥 쭉 일직선으로 긋는다.
    point(x, 200);
  }
}

 

아우 눈 아퍼.. 이전 예시에서 y값을 완전 랜덤으로 설정했을 때

function setup() {
  const canvas = createCanvas(400, 400);
  background(200);
}

function draw() {
  background(50);

  for (let x = 0; x < width; x++) {
    stroke(255);
    strokeWeight(4);
    
    // y를 랜덤으로 하니 x는 좌표마다 한개인데 y값이 매우 무작위여서 신기한 파티클?이 생김
    point(x, random(height));
  }
}

 

 

noLoop 해제. 지글지글거린다.

function setup() {
  const canvas = createCanvas(400, 400);
  background(200);
}

function draw() {
  background(50);
  stroke(255);
  noFill();
  
  // 이를 vertex로 이어주면 찌릿찌릿 그래프가 나온다.
  beginShape(); // vertex를 쓰려면 beginShape를 해주고
  for (let x = 0; x < width; x++) {
    stroke(255);
    strokeWeight(1);
    // vertex 적용
    vertex(x, random(height));
  }
  // endShape를 해줘야한다.
  endShape();
  
  // 루프를 취소하여 애니메이션이 아니게 했다.
  noLoop();
}

 

 

펄린노이즈로 y값을 설정하였다.

요상한 등고선들이 나온다.

펄린 노이즈는 실행될 때 무작위 시드값을 기준으로 생성된다.

그래서 한 번 실행되면 그 좌표는 항상 같은 값을 유지하지만 새로고침 할때마다 다시 새로운 값으로 설정된다.

만약 새로고침해도 계속 같은 그래프를 얻고 싶으면, noiseSeed함수로 seed값을 고정시켜라.

function setup() {
  const canvas = createCanvas(400, 400);
  background(200);
}

function draw() {
  background(50);
  stroke(255);
  noFill();
  beginShape();
  let xoff = 0;
  for (let x = 0; x < width; x++) {
    // 패러미터를 하나 넣으면 x, x, x가 적용되나보다..
    // stroke는 stroke의 색상설정을 말함
    stroke(255);
    // strokeWeight는 말그대로 굵기를 말함
    strokeWeight(1);
    const y = noise(xoff) * height;
    vertex(x, y);

    // xoff가 클수록 가파르고 변화무쌍한 값이 되고, xoff가 작을수록 완만한 그래프가 된다.
    xoff += 0.02;
  }
  endShape();
}

 

 

 

 

 

 

이렇게 하면 애니메이션이 된다. 사인 함수 파동처럼 생김.

beginShape-endShape는 한 장의 그림을 그림.

그 안에서 noise(xoff)은 0.1~ 0.9 사이의 값들을 반환하고,

이를 캔버스 width만큼 뻗게 하여 캔버스 전체를 범위로해서 noise값을 뿌리게 한다.

noise(…)만 쓰면 캔버스에 아무것도 안 보이는 것처럼 보이는 이유는 단순히 noise가 0~1이니까다.

캔버스가 400px일 때, noise(…) * 200만 곱하면 캔버스 절반만 이용하게 된다.

function setup() {
  const canvas = createCanvas(400, 400);
  background(200);
}

let inc = 0.01;
let start = 0;

function draw() {
  background(50);
  stroke(255);
  noFill();
  // beginShape-endShape는 한 장의 그림을 그림.
  beginShape();
  let xoff = start;
  for (let x = 0; x < width; x++) {
    // 패러미터를 하나 넣으면 x, x, x가 적용되나보다..
    // stroke는 stroke의 색상설정을 말함
    stroke(255);
    // strokeWeight는 말그대로 굵기를 말함
    strokeWeight(1);
    const y = noise(xoff) * height;
    vertex(x, y);

    xoff += inc;
  }
  endShape();

  //
  start += inc;
}

 

 

이렇게 하면 sin wave다!

사인함수.. 수1 풀때 태곳적 기억속에 있는데 다시 공부해야겠다. 대강 이렇게 생겼다는 것만 기억난다.

function setup() {
  const canvas = createCanvas(400, 400);
  background(200);
}

let inc = 0.01;
let start = 0;

function draw() {
  background(50);
  stroke(255);
  noFill();
  // beginShape-endShape는 한 장의 그림을 그림.
  beginShape();
  let xoff = start;
  for (let x = 0; x < width; x++) {
    // 패러미터를 하나 넣으면 x, x, x가 적용되나보다..
    // stroke는 stroke의 색상설정을 말함
    stroke(255);
    // strokeWeight는 말그대로 굵기를 말함
    strokeWeight(1);
    const y = noise(xoff) * height;
    vertex(x, y);

    xoff += inc;
  }
  endShape();

  //
  start += inc;
}

 

 

위 사인함수에 조금의 랜덤니스를 부여하고 싶으면?

사인 함수 값에다가 매우 약간의 noise를 넣어 위같은 결과를 만들 수 있다.

function setup() {
  const canvas = createCanvas(400, 400);
  background(200);
}

let inc = 0.01;
let start = 0;

function draw() {
  background(50);
  stroke(255);
  noFill();
  // beginShape-endShape는 한 장의 그림을 그림.
  beginShape();
  let xoff = start;
  for (let x = 0; x < width; x++) {
    stroke(255);
    strokeWeight(1);

    let n = map(noise(xoff), 0, 1, -50, 50);
    let s = map(sin(xoff), -1, 1, 0, height);
    let y = s + n;
    vertex(x, y);

    xoff += inc;
  }
  endShape();

  //
  start += inc;
}