-
베지어 곡선과 카이사 Q 스킬 이케시안 폭우기타/참고 2020. 6. 23. 13:34
베지어 곡선을 이용해 Unity 에디터에서 카이사 Q 스킬 연출을 따라해보자.
우선 베지어 곡선(Bezier Curve)이란 무엇인가? 에 대해서는 아래 링크를 참고하자.
https://blog.coderifleman.com/2016/12/30/bezier-curves/
중학생도 알 수 있는 베지에 곡선(Bezier Curves)
이 문서는 일본어 문서 「中学生でもわかるベジェ曲線」을 번역한 것으로 곡선을 그리거나 애니메이션 처리에 근간이 되는 베지에 곡선(Bezier Curves)의 원리에 관해서 쉽고 간단하게 소개합니다
blog.coderifleman.com
쉽게 얘기하자면 어도비 포토샵에서 지원하는 펜 툴에 적용되는 원리이며, 곡선을 그리는 공식이다.
이케시안 폭우 미사일의 궤적이 곡선을 그리므로 우리는 베지어 곡선을 이용해 따라하기로 한다.
출처: https://www.quora.com/What-is-a-Bezier-curve-in-Adobe-Photoshop-Why-would-we-use-it 위 그림은 포토샵에서 펜툴을 사용해 그린 베지어 곡선이다.
곡선은 하얀 네모(Anchor Point)에서 시작해 하얀 네모에서 끝난다. 여기서 까만 네모(Handle)에 의해 곡선의 휘어짐이 결정된다.
그림에서 A1에서 A2까지 곡선1, A2에서 A3까지 곡선2, 총 두 개의 곡선이 그려져있다.
곡선1은 h1, h2에 의해서 곡선이 결정되고 곡선2는 h3, h4에 의해서 곡선이 결정된다.
h5는 다음 그려질 곡선을 위해 있는 것으로 곡선1과 곡선2 어느 곡선에도 관여하지 않는다.
베지어 곡선은 위 그림에서의 까만 네모(Handle)의 갯수에 따라 1차(Linear), 2차(Quadratic), 3차(Cubic)으로 분류된다.
곡선의 시작과 끝을 담당하는 Anchor Point는 2개로 고정되고,
곡선의 휘어짐을 담당하는 Handle이 없으면 1차, 1개면 2차, 2개면 3차 베지어 곡선인 것이다.
여기서 사용할 것은 2개의 Anchor Point와 2개의 Handle을 사용하는 3차 베지어 곡선이다.
www.desmos.com/calculator/d1ofwre0fr 에서 수식을 활용해 3차 베지어 곡선을 그려볼 수 있다. 우리가 사용할 공식에 대해서는 대충 알겠다. 그래서 이걸로 어떻게 이케시안 폭우를 만들 것인가?
우선 이케시아 폭우 미사일의 궤적에 대해서 생각해보자.
시전자의 몸에서 사출되어 크게 휘는 곡선을 그리며 대상에게 날아간다.
- 시전자의 몸에서 사출한다.
- 사출하자마자 크게 휜다.
- 대상 근처에서 작게 휜다.
- 대상에게 명중한다.
위 과정에서 시전자의 몸에서 사출하고 대상에게 명중하는 1번과 4번은 3차 베지어 곡선에서 Anchor Point 이고,
2번 크게 휘는 것과 3번 작게 휘는 것은 3차 베지어 곡선에서의 Handle 이다.
이를 수식으로 그려보면 위 그림과 같다.
주황색 점은 Anchor Point 와 Handle 을 의미하고, 빨간색 선이 베지어 곡선이다.
위 그림은 이케시안 폭우 미사일 12개 중 단 하나의 궤적만을 나타내며, 이케시안 폭우 미사일은 다양한 방향으로 사출되어 다양한 곡선을 그리며 날아간다.
Anchor Point를 기준으로 일정한 거리의 범위를 그려 Handle 의 이동 경로를 표현한다.
그럼 위 그림에서 오른쪽 움짤의 빨간선처럼 다양한 곡선이 나타난다. 이것이 우리가 구하고자 하는 이케시안 폭우 미사일의 궤도이다.
여태까지 베지어 곡선을 이용해 이케시안 폭우 미사일의 궤적을 그려봤다.
이제 그 궤적을 따라 날아가는 미사일을 Unity 에서 구현해보는 것만 남았다.
$$P = {(1-t)}^{3}{P}_{0}+3{(1-t)}^{2}t{P}_{1}+3(1-t){t}^{2}{P}_{2}+{t}^{3}{P}_{3}$$
3차 베지어 곡선의 식은 위와 같다. (참고 링크: https://en.wikipedia.org/wiki/B%C3%A9zier_curve)
이를 토대로 메소드를 작성하자면 아래와 같을 것이다.
1234567891011121314private void DrawTrajectory() {transform.position = new Vector2(FourPointBezier(point[0].x, point[1].x, point[2].x, point[3].x),FourPointBezier(point[0].y, point[1].y, point[2].y, point[3].y));}private float FourPointBezier(float a, float b, float c, float d) {return Mathf.Pow((1 - t), 3) * a+ Mathf.Pow((1 - t), 2) * 3 * t * b+ Mathf.Pow(t, 2) * 3 * (1 - t) * c+ Mathf.Pow(t, 3) * d;}cs 베지어 곡선 공식은 FourPointBezier 메소드에서 담당한다. 매개변수 a~d 에는 각각 P0~P3 의 x좌표와 y좌표가 들어가게 된다. DrawTrajectory 메소드에서 보이는 point[0~3]이 P0~P3의 좌표값(Vector2)이다.
DrawTrajectory 메소드에서 미사일의 위치를 그리게 된다.
변수 t는 시간의 경과이며 0 ≤ t ≤ 1 이다. 1이 되었을 때 대상의 위치에 도달하게 된다
1234567891011121314151617181920[SerializeField] [Range(0, 1)] private float t = 0;[SerializeField] public float posA = 0.55f;[SerializeField] public float posB = 0.45f;
Vector2[] point = new Vector2[4];void Start() {point[0] = master.transform.position; // P0point[1] = PointSetting(master.transform.position); // P1point[2] = PointSetting(enemy.transform.position); // P2point[3] = enemy.transform.position; // P3}Vector2 PointSetting(Vector2 origin) {float x, y;x = posA * Mathf.Cos(Random.Range(0, 360) * Mathf.Deg2Rad)+ origin.x;y = posB * Mathf.Sin(Random.Range(0, 360) * Mathf.Deg2Rad)+ origin.y;return new Vector2(x, y);}cs posA 와 posB는 각각 P0과 P1의 거리, P2와 P3의 거리를 결정한다. Handle이 Anchor Point 에서 멀어지므로 휘어지는 정도가 심해진다.
코드 전문은 아래와 같다.
BezierMissile.cs
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970using System.Collections;using System.Collections.Generic;using UnityEngine;public class BezierMissile : MonoBehaviour {Vector2[] point = new Vector2[4];Animator anim;bool hit = false;[SerializeField] [Range(0, 1)] private float t = 0;[SerializeField] public float spd = 5;[SerializeField] public float posA = 0.55f;[SerializeField] public float posB = 0.45f;public GameObject master;public GameObject enemy;void Start() {anim = GetComponent<Animator>();point[0] = master.transform.position; // P0point[1] = PointSetting(master.transform.position); // P1point[2] = PointSetting(enemy.transform.position); // P2point[3] = enemy.transform.position; // P3}void FixedUpdate() {if (t > 1) return;if (hit) return;t += Time.deltaTime * spd;DrawTrajectory();}Vector2 PointSetting(Vector2 origin) {float x, y;x = posA * Mathf.Cos(Random.Range(0, 360) * Mathf.Deg2Rad)+ origin.x;y = posB * Mathf.Sin(Random.Range(0, 360) * Mathf.Deg2Rad)+ origin.y;return new Vector2(x, y);}void DrawTrajectory() {transform.position = new Vector2(FourPointBezier(point[0].x, point[1].x, point[2].x, point[3].x),FourPointBezier(point[0].y, point[1].y, point[2].y, point[3].y));}void OnTriggerEnter2D(Collider2D collision) {if (collision.gameObject == enemy) {hit = true;anim.SetTrigger("hit");Destroy(gameObject, 0.35f);}}private float FourPointBezier(float a, float b, float c, float d) {return Mathf.Pow((1 - t), 3) * a+ Mathf.Pow((1 - t), 2) * 3 * t * b+ Mathf.Pow(t, 2) * 3 * (1 - t) * c+ Mathf.Pow(t, 3) * d;}}cs Shooter.cs
123456789101112131415161718192021222324252627282930using System.Collections;using System.Collections.Generic;using UnityEngine;public class Shooter : MonoBehaviour {[SerializeField] public GameObject missile;[SerializeField] public GameObject target;[SerializeField] public float spd;[SerializeField] public int shot = 12;public void Shot() {StartCoroutine(CreateMissile());}IEnumerator CreateMissile() {int _shot = shot;while (_shot > 0) {_shot--;GameObject bullet = Instantiate(missile, transform);bullet.GetComponent<BezierMissile>().master = gameObject;bullet.GetComponent<BezierMissile>().enemy = target;yield return new WaitForSeconds(0.1f);}yield return null;}}cs Shooter.cs 는 시전자 오브젝트에 넣고 BezierMissile.cs 는 Prefab으로 저장된 미사일 오브젝트에 적용하면 된다.
Prefab으로 저장된 미사일 오브젝트는 Shooter.cs 의 missile 에 배정해주고
목표물 오브젝트를 만든 후 Shooter.cs 의 target 에 배정해주면 미사일 오브젝트가 곡선을 그리며 목표물 오브젝트를 향해 날아간다.
미사일에 명중 시 애니메이션 및 Trail Renderer 를 적용해서 완성된 모습이다.