Unity 구(Sphere)를 직접 만들어보자 🏐

9581 단어 UnityUnity

Unity에서 기본적으로 제공하는 3D 오브젝트중 Sphere를 만들었을때 WireFrame을 보면 이런 삼각형들로 이루어져있다.

사실 컴퓨터로 표현되는 3D모델들은 모두 폴리곤이라는 삼각형들이 모여 하나의 물체를 이루기 때문에 구 또한 삼각형들의 집함으로 이루어져있다는걸 알 수 있다. 이 폴리곤들이 촘촘하면 촘촘 할 수록 더 매끈한 구가 되겠지만 그만큼 연산량은 많아질것이다.

넓은의미로 모두 구 이다.

구를 만들기 위해 차근차근 [ 평면 -> 정육면체 -> 구체 ] 순으로 만들어 볼 생각이다.


평면 만들기

평면을 만들기 위해 생성자로 3개의 인자를 전달받을 껀데, 정점배열들을 이용해서 면을 채워줄 mesh, 정점들의 갯수를 결정하는 resolution, 면이 중심으로부터 생성될 방향을 결정할 localUp을 정의해준다.

public class MeshFace
{
	private Mesh mesh;
	private int resolution;
	private Vector3 localUp;
	private Vector3 axisA;
	private Vector3 axisB;

    public MeshFace(Mesh mesh, int resolution, Vector3 localUp)
    {
        this.mesh = mesh;
        this.resolution = resolution;
        this.localUp = localUp;

        axisA = new Vector3(localUp.y, localUp.z, localUp.x);
        axisB = Vector3.Cross(localUp, axisA);
    }
}

axisA와 axisB는 localUp벡터를 y축방향 벡터라고 생각한다면 각각 x, z 벡터라 할 수 있겠다.

화면에 직접 보이도록하기 위해서는 Mesh라는 컴포넌트를 이용해야하는데, Mesh에게 정점의 위치정보 mesh.vertices와 정점을 그리는 순서를 담은 mesh.triangles를 정해주면 그에따라 면을 그려주게 된다. https://docs.unity3d.com/kr/530/ScriptReference/Mesh.html

mesh.vertices 
mesh.triangles

그렇다면 이제 만들어야할것은 vertices와 triangles에 들어가야할 배열들을 계산해줘야할듯 싶다.

먼저 vertices, 정점의 갯수는 resolution의 값에 따라 일괄적으로 줄여주거나 늘려줄것이다. resolution = 5 일경우 가로 5개 x 세로 5개 총 25개의 정점을 만들도록 할 것이다.

Vector3[] vertices = new Vector3[resolution * resolution];

다음 triangles,

resolution = 5 일경우 (5 - 1)개의 가로사각형 (5-1)개의 세로사각형 = 16개의 사각형을 만들수 있을것이고, 사각형1개는 2개의 삼각형으로 이루어져있고, 이 삼각형은 3개의 정점으로 이루어져 있다.

int[] triangles = new int[(resolution - 1) * (resolution - 1) * 2 * 3];

이제 vertices와 triangles의 값들을 채워보자

public void CreateMesh()
{
    Vector3[] vertices = new Vector3[resolution * resolution];
    int[] triangles = new int[(resolution - 1) * (resolution - 1) * 2 * 3];

    int triIndex = 0;
    for (int y = 0; y < resolution; y++)
    {
        for (int x = 0; x < resolution; x++)
        {
            int vertexIndex = x + y * resolution;
            Vector2 percent = new Vector2(x, y) / (resolution - 1); // 0~1로 노멀라이즈
            Vector3 pointOnUnitCube = localUp + (percent.x - .5f) * 2 * axisA + (percent.y - .5f) * 2 * axisB;
            Vector3 pointOnUnitSphere = pointOnUnitCube.normalized;
            vertices[vertexIndex] = pointOnUnitCube;

            if (x != resolution - 1 && y != resolution - 1)
            {
                triangles[triIndex] = vertexIndex;
                triangles[triIndex + 1] = vertexIndex + resolution + 1;
                triangles[triIndex + 2] = vertexIndex + resolution;

                triangles[triIndex + 3] = vertexIndex;
                triangles[triIndex + 4] = vertexIndex + 1;
                triangles[triIndex + 5] = vertexIndex + resolution + 1;
                triIndex += 6;
            }
        }
    }
    mesh.Clear();
    mesh.vertices = vertices;
    mesh.triangles = triangles;
    mesh.RecalculateNormals();
}

코드를 하나하나 봐보자면

int vertexIndex = x + y * resolution;
Vector2 percent = new Vector2(x, y) / (resolution - 1); // 0~1로 노멀라이즈
Vector3 pointOnUnitCube = (percent.x - 0.5f) * 2 * axisA + (percent.y - 0.5f) * 2 * axisB + localUp; 

resolution = 5라고 가정하면,

percent : x, y가 (0,0) 일때부터 (4,4) 까지의 값을 가질텐데 그 값을 4로 나눠주기 때문에 (0,0) ~ (1,1) 의 값으로 노멀라이즈 해주게된다.

pointOnUnitCube : x랑 y의 값을 - 0.5 만큼 뺀 값을 2만큼 곱해주어, (0,0) ~ (1,1)의 범위의 값들이 (-1,-1) ~ (1,1)의 값으로 바뀔것이고 이에 localUp 벡터만큼 더해주면 다음과같은 평면의 벡터들을 구할 수 있을것이다.

triangles[ ] : 아래 그림의 각 숫자가 vertexIndex인데 삼각형의 정점을 시계방향순으로 써보면 vertexIndex, vertexIndex + 1, vertexIndex + resolution 이라는 하단 삼각형과 vertexIndex, vertexIndex + 1, vertexIndex + resolution + 1 이라는 상단 삼각형의 순서를 구할 수 있다.

이렇게 vertices와 triangles를 구했으면 mesh에 넣어주면된다.

mesh.Clear();
mesh.vertices = vertices;
mesh.triangles = triangles
mesh.RecalculateNormals();

테스트를 해보자

using UnityEngine;

public class Sphere : MonoBehaviour
{
    [Range(2, 256)]
    public int resolution = 5;

    private void OnValidate()
    {
        TEST();
    }

    void TEST()
    {
        GameObject meshObj = new GameObject("mesh");
        meshObj.transform.parent = transform;

        meshObj.AddComponent<MeshRenderer>().sharedMaterial = new Material(Shader.Find("Standard"));
        MeshFilter meshFilter = meshObj.AddComponent<MeshFilter>();
        meshFilter.sharedMesh = new Mesh();

        MeshFace face = new MeshFace(meshFilter.sharedMesh, resolution, Vector3.up);
        face.CreateMesh();
    }
}

GOOD!


정육면체 만들기

평면을 만들었다면 정육면체를 만드는건 너무나 쉽다. 평면을 6개만 만들면 되니까.

Vector3[] directions = { Vector3.up, Vector3.down, Vector3.left, Vector3.right, Vector3.forward, Vector3.back };
for (int i = 0; i < 6; i++)
{
    if (meshFilters[i] == null)
    {
        GameObject meshObj = new GameObject("mesh");
        meshObj.transform.parent = transform;

        meshObj.AddComponent<MeshRenderer>().sharedMaterial = new Material(Shader.Find("Standard"));
        meshFilters[i] = meshObj.AddComponent<MeshFilter>();
        meshFilters[i].sharedMesh = new Mesh();
    }

    terrainFaces[i] = new MeshFace(meshFilters[i].sharedMesh, resolution, directions[i]);
}

EASY~


마지막으로 구체 만들기

고등학교 수학시간에 구를 배울때 구의 정의를 생각해보면 3차원에서 한 중점으로부터 거리가 같은 점들의 집합으로 배웠었다. 그렇다면 다시 돌아가서 아까 평면을 만들었을때를 봐보자

저 평면을 이루는 벡터는 중점으로부터 거리가 정점마다 모두 다를 것이다. 이 벡터들을 방향은 같고 크기만 동일하게 노멀라이즈 해준다면

마치 이런모양으로 바뀔것이다.

Vector3 pointOnUnitCube = localUp + (percent.x - .5f) * 2 * axisA + (percent.y - .5f) * 2 * axisB;
Vector3 pointOnUnitSphere = pointOnUnitCube.normalized; // 노멀라이즈
vertices[vertexIndex] = pointOnUnitSphere; // 노멀라이즈한 값을 넣어줌

GREAT!


Sphere.cs의 코드를 좀더 깔끔하게 다듬어 보면

using UnityEngine;

public class Sphere : MonoBehaviour
{
    [Range(2, 256)]
    public int resolution = 10;

    [SerializeField, HideInInspector]
    MeshFilter[] meshFilters;
    MeshFace[] terrainFaces;

    private void OnValidate()
    {
        Initialize();
        GenerateMesh();
    }

    void Initialize()
    {
        if (meshFilters == null || meshFilters.Length == 0)
        {
            meshFilters = new MeshFilter[6];
        }
        terrainFaces = new MeshFace[6];

        Vector3[] directions = { Vector3.up, Vector3.down, Vector3.left, Vector3.right, Vector3.forward, Vector3.back };
        for (int i = 0; i < 6; i++)
        {
            if (meshFilters[i] == null)
            {
                GameObject meshObj = new GameObject("mesh");
                meshObj.transform.parent = transform;

                meshObj.AddComponent<MeshRenderer>().sharedMaterial = new Material(Shader.Find("Standard"));
                meshFilters[i] = meshObj.AddComponent<MeshFilter>();
                meshFilters[i].sharedMesh = new Mesh();
            }

            terrainFaces[i] = new MeshFace(meshFilters[i].sharedMesh, resolution, directions[i]);
        }
    }

    void GenerateMesh()
    {
        foreach (MeshFace face in terrainFaces)
        {
            face.CreateMesh();
        }
    }
}

좋은 웹페이지 즐겨찾기