2달 전에 공부한 것을 이제야 블로그 포스팅 합니다.
0. 유니티에서의 일반적인 물
Unity에서 물은 일반적으로 쉐이더를 사용하여 구현한다.
이때 쉐이더는 메테리얼의 형태로 평면 메쉬에 적용된다.
평면메쉬에 적용 되기 때문에, 출렁이는 시각적인 효과를 나타내는 데는 수월하다. (쉐이더를 활용해 파티클을 묘사해도 부작용이 거의 없다.)
그러나 평면 메쉬이기 때문에
실제 물과같은 운동을 기대하기는 어렵다.
1. 유니티에서 파동운동을 구현하는 법
내가 묘사하고 싶은 실제 물의 운동은 아래 영상과 같다.
물 표면에 닿는 오브젝트의 운동량에 따라 물의 표면이 파동이 치는 효과를 연출하고 싶었다.
이런 효과는 최근에 개발되는 3d 게임에서는 꽤 흔한 편이지만,
이런 효과가 구현 되어있는 라이브러리는 거의 존재하지 않았다.대부분의 3d 게임에서는 물의 표면을 쉐이더로 처리하는 형태로 물을 구현했었다.
이는 물에 들어간다는 느낌이 들지않았다.
때문에 나는 파동운동을 구현하는 방법에 대해 연구하기 시작했다.
1 - 1. 삼각함수를 이용하는 형태
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SinMovement : MonoBehaviour
{
private float speed = 1f;
private float length = 1f;
private float runningTime = 0f;
private float yPos = 0f;
// Use this for initialization
void Start()
{
runningTime += Time.deltaTime * speed;
yPos = Mathf.Sin(runningTime) * length;
Debug.Log(yPos);
this.transform.position = new Vector2(0, yPos);
}
private void Update()
{
runningTime += Time.deltaTime * speed;
yPos = Mathf.Sin(runningTime) * length;
Debug.Log(yPos);
this.transform.position = new Vector3(transform.position.x, yPos , transform.position.z);
}
}
코드 구성은 간단한 편이다.
게임 경과시간을 X값로 Sin그래프의 Y값을 받아오는 파동 형태의 운동을 구현한 것이다.
처음 생각한 것은 Sin 운동을 하는 오브젝트를 겹겹이 쌓으면 파동을 구현할 수 있다고 생각했다.
그러나 다른 방법을 찾기 시작했다.
왜냐하면 이 코드를 통해 움직이는 구(Sphere)는 파동 운동의 모습이지, 파동 그 자체라곤 보기 어렵다고 생각했기 때문이다.
2. 파동에 대한 생각 정리
파동은 연속적인 운동이고, 매질을 필요로 한다.
그러니까 운동하는 매질이 1개가 아닌 연속적일 때 파동이 성립할 수 있다.
즉 일반적인 메쉬 평면으로 파동을 구현 할 수 없는 이유는, 하나의 메쉬 평면은 연속적이지 않기 때문이다.
그렇다면 메쉬를 여러 개 만들면 되지 않을까?
메쉬를 일렬로 세우고
한 메쉬가 운동을 받으면 그 운동을 양 옆에 메쉬에 전달하는 방법을 사용한다면
충분히 파동 운동을 구현 가능하다고 생각했다.
이러한 생각을 3D 공간에서 구현하고 싶었으나 3D 공간에서 운동량을 계산하는 것은 아주 복잡한 문제였다.
때문에 2D 그래프인 Sin 삼각함수에 그려넣었던 메쉬를 구현하기 위해 2D Unity 환경으로 넘어갔다.
3. 2D Unity에서 메쉬 크래스 선언
먼저 하나의 메쉬를 만드는 것이 아니기 때문에
유니티에서 제공하는 메쉬 컴포넌트를 직접 Scene에 배치하는 것이 아닌,
코드의 형태로 메쉬 컴포넌트를 Scene에 선언해야했다.
아래는 메쉬 하나의 정보를 담을 클래스이다.
public class WaterColumn {
public float xPos, height, targetHeight, k, velocity, drag;
public Vector3 rayVector;
public Ray ray;
public WaterColumn(float xPos , float targetHeight , float k , float drag)
{
this.xPos = xPos;
this.height = targetHeight;
this.targetHeight = targetHeight;
this.k = k;
this.drag = drag;
this.rayVector = new Vector3(xPos, height, 0.0f);
this.ray = new Ray(rayVector, new Vector3(0.0f, 100.0f, 0.0f));
}
public void UpdateColumn()
{
float acc = -k * (height - targetHeight);
velocity += acc;
velocity -= drag * velocity;
height += velocity;
}
}
이 클래스는 기본적으로 메쉬가 가지고 있어야할 좌표를 가지고있다. (xPos , height , targetHeight)
추가적으로 파동이 될 메쉬의 운동형태를 조절할 변수도 선언 되어있다. (k , m , velocity , drag , acc)
메쉬는 출렁이는 움직임을 가져야하는데, 이는 용수철이 움직이는 운동에서 따온다.
k는 매질의 탄성을 정해주는 변수로 후크의 법칙에서 용수철 상수 (F = -kx)에서 따왔다.
https://ko.wikipedia.org/wiki/%ED%9B%85%EC%9D%98_%EB%B2%95%EC%B9%99
훅의 법칙 - 위키백과, 우리 모두의 백과사전
위키백과, 우리 모두의 백과사전. 훅의 법칙(영어: Hooke’s law)은 용수철과 같이 탄성이 있는 물체가 외력에 의해 늘어나거나 줄어드는 등 변형되었을 때 자신의 원래 모습으로 돌아오려고 저항
ko.wikipedia.org
후크의 법칙은 뉴턴의 가속도 운동 법칙(F = ma)을 이용하여 아래의 식으로 변형이 가능하다.
F : a = -k * (x / m)
그리고 x는 후크의 법칙에서 외력이 없을 때 용수철 끝의 길이를 의미한다.
즉 x = (height - targetHeight) 이다.
코드 "float acc = -k * (height - targetHeight);" (m = 1) 는 이를 표현한 식이다.
하지만 위 식은 감속없는 영원한 스프링 운동을 하게되므로 서서히 운동량을 줄여갈 필요가있다.
drag 변수는 감속을 담당하는 변수이다.
해당 변수를 통해 매 운동마다 운동량을 감소시킨다.
이로서 스프링 운동에 필요한 변수들을 가진 메쉬 클래스가 완성되었다.
4. 양 옆의 메쉬에 힘을 전달
먼저 메쉬 클래스를 담을 리스트를 선언한다.
private List<WaterColumn> columns = new List<WaterColumn>();
private void Setup()
{
columns.Clear();
float space = width / columnCount;
for (int i = 0; i < columnCount + 1; i++)
{
columns.Add(new WaterColumn(i * space - width * 0.5f, height, k, m, drag));
}
}
Setup 함수는 Scene에 나타날 메쉬 배열의 개수와 길이를 받아서 Watercolumn 인스턴스를 생성한다.
생성된 Watercolumn들을 columns 배열에 저장한다.
.SetUp 함수는 Begin 함수에서 실행된다.
다음은 Watercolumn 인스턴스의 스프링 운동을 인접한 배열에 전달하기 위한 코드를 작성한다.
private void FixedUpdate()
{
for (int i = 0; i < columns.Count; i++)
{
columns[i].UpdateColumn();
}
float[] leftDeltas = new float[columns.Count];
float[] rightDeltas = new float[columns.Count];
for (int i = 0; i < columns.Count; i++)
{
if (i > 0)
{
leftDeltas[i] = (columns[i].height - columns[i - 1].height) * spread;
columns[i - 1].velocity += leftDeltas[i];
}
if (i < columns.Count - 1)
{
rightDeltas[i] = (columns[i].height - columns[i + 1].height) * spread;
columns[i + 1].velocity += rightDeltas[i];
}
}
for (int i = 0; i < columns.Count; i++)
{
if (i > 0)
{
columns[i - 1].height += leftDeltas[i];
}
if (i < columns.Count - 1)
{
columns[i + 1].height += rightDeltas[i];
}
}
MakeMesh();
}
rightDeltas는 파동이 시작된 Watercolumns에서 오른쪽에 있는 배열들의 높이 차이를 저장하는 배열이다.
leftDeltas는 왼쪽에 있는 배열들의 높이 차이를 저장하는 배열이다.
양 측의 메쉬 배열들의 높이 차이를 저장하고, 이것을 통해 양측 메쉬 배열들의 높이를 수정한다.
배열들의 높이 수정은 파동처럼 서로의 높이를 수정해 나간다.
또한 높이 차이는 스프링 운동의 영향을 받으므로, 시간이 지날수록 점점 줄어든다.
즉 이를 통해 파동의 움직임을 표현할 수 있으며
이는 스프링운동의 가속에 영향을 받는 내가 원하는 현실적인 물을 만들 수 있다.
private void MakeMesh()
{
Mesh mesh = new Mesh();
Vector3[] vertices = new Vector3[columns.Count * 2];
int v = 0;
for (int i = 0; i < columns.Count; i++)
{
vertices[v] = new Vector2(columns[i].xPos, columns[i].height);
vertices[v + 1] = new Vector2(columns[i].xPos, 0f);
v += 2;
}
int[] triAngles = new int[(columns.Count - 1) * 6];
int t = 0;
v = 0;
for (int i = 0; i < columns.Count - 1; i++)
{
triAngles[t] = v;
triAngles[t + 1] = v + 2;
triAngles[t + 2] = v + 1;
triAngles[t + 3] = v + 1;
triAngles[t + 4] = v + 2;
triAngles[t + 5] = v + 3;
v += 2;
t += 6;
}
mesh.vertices = vertices;
mesh.triangles = triAngles;
mesh.RecalculateNormals();
mesh.RecalculateBounds();
mesh.Optimize();
meshFilter.mesh = mesh;
}
MakeMesh 함수는 Mesh를 구성하는 정보를 통해 Mesh를 만드는 함수이다.
Mesh는 2차원 벡터의 삼각형으로 구성이 되어있으므로, 사각형 메쉬를 구성하는 정보를 삼각형 메쉬로 변환하여 만드는 함수이다. 사실 이 함수는 그래픽스를 이용하는 함수라, Mesh의 구성원리를 이해하는 단계까지만 나아가고,
사각형 메쉬를 만들 수 있는 코드를 인용하는데 그쳤다.
그래픽스와 관련된 공부는 더 자세히 해야겠다.
더 해보고 싶은것
1. 해당 운동을 3D 환경에서 재현하기
2. 유니티에서 선언의 형식으로 그래픽스 도전하기.
'게임 수학과 물리 > Math In Unity' 카테고리의 다른 글
선형 보간과 구면 선형 보간 (0) | 2023.06.30 |
---|