Post

p5.js로 헤엄치는 고래 만들기(2)

프로젝트 소개

지난 포스트 에 이어서 오늘은 제일 복잡했던 고래의 움직임에 대해 글을 쓰려고 한다.

👉🏻 소스 코드

고래의 움직임

(1) whale.ts

1. 고래위치

1
  this.wPos.add(this.speedX, this.speedY);

위치 = 위치 + 속도

  • wPos는 고래의 현재 위치를 나타내며, speedX와 speedY는 고래의 속도를 나타낸다.
  • add 메서드는 고래의 현재 위치에 속도를 더하여 새로운 위치를 계산.

2. 벽과의 충돌

  • 고래가 화면의 경계(boundary)를 넘어가면 speedX speedY 의 부호를 바꿔서 방향을 반전한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 벽과의 충돌 체크
    if (
      this.wPos.x < this.boundary ||
      this.wPos.x > window.innerWidth - this.boundary
    ) {
      this.speedX *= -1; // x축 방향 반전
    }

    if (
      this.wPos.y < this.boundary ||
      this.wPos.y > window.innerHeight - this.boundary
    ) {
      this.speedY *= -1; // y축 방향 반전
    }

3. 몸통

1
2
3
4
5
6
7
8
  this.fins.forEach((f, i) => {
      if (i > 0) {
        const prevFin = this.fins[i - 1];
        f.update(prevFin.pos.x, prevFin.pos.y, i === this.numFin - 1, i);
      } else {
        this.fins[0].update(this.wPos.x, this.wPos.y);
      }
    });
  • fins 배열을 순회하여 각 지느러미를 업데이트한다.
  • 머리(첫번째 몸통)는 고래의 현재 위치에 따라 업데이트하고, 나머지 몸통은 이전 몸통의 위치를 참조하여 업데이트한다.
  • 꼬리(마지막 몸통)는 사다리꼴이 아닌 삼각형으로 나타내야 하므로 따로 조건을 주었다. (i === this.numFin - 1).

(2) fin.ts

작명이 잘못된건 인정.. 지느러미라고 쓰고 몸통이라고 읽겠다.

  • 고래의 형태를 그리고, 이를 움직이는 물체의 위치와 방향에 맞게 조정하는 과정을 설명하고자한다. 이를 통해 각 몸통 부분을 화면에 그리면서 실제로 움직이는 물체의 위치와 방향에 적절하게 반영하였다. 코드를 이해하기 위해 각 단계별로 작성한 이유와 고민했던 부분을 자세히 살펴보겠다. 👉🏻 Koi flock: p5.js 이 코드를 많이 참고 하였다.

고민:

몸통의 길이와 방향을 계산하여, 움직이는 위치와 방향으로 그려질 수 있도록 해야했다. 몸통이 향해야 할 목표 위치(target)와 현재 위치(this.pos) 간의 벡터를 계산하여 사다리꼴의 모양과 방향을 계산하는것이 어려웠다.

구현

1. 각도 계산 및 위치 업데이트

  • angle은 몸통의 끝부분이 현재 위치에서 얼마나 떨어져 있는지를 결정하는 데 필요하다. 지느러미의 높이와 방향에 따라 끝부분의 위치를 정확하게 계산하려면 이 각도가 필요하다.
1
2
3
4
5
6
7
8
9
10
     // 현재 움직이는 위치와 fin위치간 거리의 차이를 구한다.
    const target = this.p5.createVector(x, y);
    const dir = p5.Vector.sub(target, this.pos);
    dir.setMag(isTail ? this.w - 30 : this.w);
    dir.mult(-1);

    // 현재 움직이는 위치가 어디로 향하는지, 각도를 구한다.
    this.angle = dir.heading();
    this.pos = p5.Vector.add(target, dir);

2. 몸통 그리기

calc_angle

(1) 삼각함수를 사용한 위치 계산

  • 지느러미의 높이(h)를 설정하고, 각도에 따른 이동 벡터를 계산하여 arg2에 위치를 설정
    • h: 지느러미의 높이, 즉 지느러미가 실제로 차지하는 공간의 크기
    • arg2: 현재 위치(this.pos)와 지느러미의 방향(angle) 및 길이(h)를 기반으로 지느러미의 끝부분 위치 설정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
 // 사다리꼴의 기본 형태 정의
    let orig = this.p5.createVector(0, 0);
    let other = this.p5.createVector(
      0,
      -this.p5.dist(this.pos.x, this.pos.y, arg2.x, arg2.y) // 아래 방향 기준 사다리꼴의 길이
    );

    const h = isTail ? 20 : 50;
    const arg2 = this.p5.createVector(0, 0);
    const dx = h * this.p5.cos(this.angle);
    const dy = h * this.p5.sin(this.angle);

    arg2.set(this.pos.x + dx, this.pos.y + dy);

cos와 sin 함수를 사용하여 각도(this.angle)에 따른 이동 벡터를 계산한다. 각도에 따라 이동 벡터를 조정하면 지느러미가 실제로 회전하고 움직이는 방향에 맞게 위치를 조정할 수 있다.

whale_shark 이미지출처: nature of code

👉🏻 참고

벡터를 x축과 y축 방향으로 분해할 때, 삼각함수는 다음과 같은 역할을 한다:

  • 코사인 함수 (cos): 주어진 각도에서 x축 방향으로의 거리를 계산 ```ts const dx = h * this.p5.cos(this.angle);

// 현재 위치에서 몸통의 끝부분이 x축 방향으로 이동해야 하는 거리 // : 각도에 따라 코사인 값은 x축 방향으로의 이동량을 제공

1
2
3
4
5
- 사인 함수 (sin): 주어진 각도에서 y축 방향으로의 거리를 계산
```ts
 const dy = h * this.p5.sin(this.angle);
// 현재 위치에서 지느러미의 끝부분이 y축 방향으로 이동해야 하는 거리
// 각도에 따라 사인 값은 y축 방향으로의 이동량을 제공
  • arg2.set에서 현재 위치(this.pos)와 지느러미의 방향(angle) 및 길이(h)를 기반으로 지느러미의 끝부분 위치 설정한다.

(2) 사다리꼴의 네점 계산

1
2
3
4
5
6
7
8
9
10
11
12
13
    let p1, p2, p3, p4;
    p1 = orig.copy(); // 왼쪽 위
    p2 = orig.copy(); // 오른쪽 위
    p3 = other.copy(); // 오른쪽 아래
    p4 = other.copy(); // 왼쪽 아래

    p1.add(-widthFront / 2, margin);
    p2.add(widthFront / 2, margin);
    p3.add(widthBack / 2, -margin);
    p4.add(-widthBack / 2, -margin);

    //  사다리꼴의 네 점을 배열로 저장
    const trapPoints = [p1, p2, p3, p4];

(3) 회전 및 위치 조정

  • 사다리꼴의 각 점을 회전시켜 올바른 방향으로 조정. 이후 위치를 this.pos로 이동시킨다. => (0,0) 임의 값을 기준으로 점이 계산되었으므로 실제 위치값을 넣어준다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  for (let i = 0; i < trapPoints.length; i++) {
      let h = this.pos.copy();
      // h 벡터는 현재 위치 this.pos와 이동된 위치 arg2 사이의 벡터.두 위치 간의 방향.
      h.sub(arg2);
      //  사다리꼴의 각 점을 h 벡터의 방향으로 회전
      trapPoints[i].rotate(h.heading());
      // 사다리꼴을 90도 회전. 사다리꼴이 h 벡터와 수직으로 배치되도록 하기 위함
      trapPoints[i].rotate(this.p5.radians(90));

      // 회전된 사다리꼴의 각 점을 원래 위치 this.pos로 이동
      trapPoints[i].add(this.pos);
    }
    this.p5.fill(255, 200);
    this.p5.noStroke();

(4) 사다리꼴 그리기

  • fill, noStroke, curveTightness 등을 설정한 후, 사다리꼴의 형태를 그렸다. curveVertex를 사용하여 곡선 형태로 사다리꼴을 그릴 수 있다. 꼬리의 경우 trapPoints를 3개만 주면된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 	this.p5.fill(255, 200);
    this.p5.noStroke();

    // curveTightness 설정
    this.p5.curveTightness(0.5);

    // 둥근 사다리꼴 그리기
    this.p5.beginShape();

    for (let i = 0; i < trapPoints.length; i++) {
      this.p5.curveVertex(trapPoints[i].x, trapPoints[i].y);
    }

    // 마지막 점은 시작점을 위해 반복
    this.p5.curveVertex(trapPoints[0].x, trapPoints[0].y);
    this.p5.curveVertex(trapPoints[1].x, trapPoints[1].y);

    this.p5.endShape(this.p5.CLOSE);

👉🏻 최종 코드는 여기를 확인해주세용 :)

추가적으로 개발이 필요한 것

  • 머리를 기준으로 몸통과 꼬리가 sine 운동을 한다.
  • 몸통의 무늬는 머리에서 꼬리로 갈 수록 점(dot)의 밀도가 낮아진다.
  • 양 옆의 지느러미는 움직이는 방향을 기준으로 상하 운동을 한다.

done

  • 전시하였을때 모두가 고래를 터치하는 행동을 보였다. 고래를 터치하였을 때 꿈틀거리거나 말풍선을 띄워 대화하는것처럼 보이는 효과를 추가해야겠다.

참고자료

This post is licensed under CC BY 4.0 by the author.