간단한 3D 프로젝트를 만들기 위한 사전 지식들을 배웠다.
우선 ProBuilder와 PolyBrush를 설치해준다.
ProBuilder : 간단한 모델링 도구
PolyBrush : 지형을만들거나, Vertax Painting , + 텍스쳐와 관련된 기능
이 두 가지로 지형의 프로토타입을 만들기는 충분하다.
01. ProBuilder
3D 모델을 빠르고 효율적으로 제작하고 편집할수 있는 모델링 도구이다.
복잡한 외부 3D 소프트웨어 없이도, 유니티 에디터 안에서 직접 모델링 작업을 수행할 수 있다는 강점이 있다.
Plane, Cube, Stair, Arch 등 기본적인 형태를 선택하고 만들 수 있다.
그 후 위의 4가지 옵션으로 점,선,면 등을 선택해 특정 부분만 늘리거나 줄일 수 있다.
또한, 우측 하단에 Height Cuts, Width Cuts등을 늘려 폴리곤 수를 늘릴 수 있다.
계단 모양도 가능하며, 계단이 휘어진 형태도 표현 가능하다.
폴리곤이 기본 형태는 바깥을 향해 있지만 Flip Nomal을 사용해서 실내 공간을 표현할 수 있다.
문을 만들어서 애니메이션을 적용하는 것도 가능하다.
선택한 두 줄을 잇는 선을 만들어 폴리곤을 나누는 기능인 Connect Edge를 사용해
추출해낼 문의 모양을 만든다.
선택한 면을 돌출시키는 기능인 Exturde를 통해 돌출시키고, Detach를 할 수도 있고
Exturde를 두 번해서 다음과 같이 입구모양을 표현 할 수도 있다.
(플레이어가 막힌 입구를 보지 못하도록 올라가는 느낌만 표현해도 된다)
UV모드를 선택하면 평면에서 변화시킨 폴리곤들이 정렬이 되어 있지 않은 모습이 있다.
자동 정렬로 맞춰주면 조금 더 깔끔한 모양의 지형이 나온다.
선택한 Faces들을 같은 그룹으로 해서 각진 부분을 부드럽게 표현할 수도 있다.
실내 공간을 설정할 때는, 카메라가 실내 공간을 벗어나면 안되므로 특정한 처리가 필요하다.
카메라가 벽을 뚫지 못하게 하거나, 플레이어 자체가 벽에 다가갈 수 없게 처리하거나 등등..
만일 이렇게 ProBuilder로 만들어진 두 개의 형태를 쉽게 잇고 싶다면 아직 시험중이지만 다음 기능을
이용할 수도 있다.
Experimantal Feature Enable 체크
Tool -> Probuilder > Experimantal > Boolean > 으로
세 가지 옵션 합집합, 교집합 차집합의 옵션으로 선택 가능하다.
혹은 두 오브젝트를 하나의 오브젝트로 통합한 뒤 (Merge 기능)
Bridge Edges를 통 통로를 만들 수도 있다.
02. PolyBrush
PolyBrush는 유니티 에디터 내에서 3D 모델의 Mesh를 직접 편집하고 텍스쳐와 색상을 페인팅할 수 있는 도구다.
복잡한 모델링 없이 유니티 에디터 안에서 직관적으로 할 수 있어 편리하다.
지형을 만들 수 있다.
Ctrl + 휠 > Outer Radius 값 변화
Ctrl + 클릭 > 아래로 지형 변화
Smooth하게 만들어줌
지형의 끝부분은 조절이 안되서(Smooth Brush를 통한 조절) 그냥 Probuilder로 끝의 점을 잡고 내려주면 된다.
텍스쳐를 선택하고 페인팅 할 수도 있다.
03. Light
Unlit은 조명과 상관 없이 (쉐이더) 특정 색을 유지시킬 수 있ㅇ다.
파티클 : 이펙트를 표현하는 방식
보통 이펙트의 밝기는 일정하지 않다.
한 번에 많은 양의 로드를 하면 힘들기 때문에
첫 방 / 다음 방만 로드를 해 두고 나머지는 나중에 로드하는 것도 좋은 방법이다.
04. Terrain
PolyBrush와 ProBuilder 이외에도 많이 쓰이는 방법은 Terrain을 이용하는 방법이다.
특히 광활한 지형 (산, 계곡 ,언덕 등)을 표현하는데 자주 쓰인다.
Terrain은 Brush를 이용해서 지면의 높낮이를 표현하게 된다. 다양한 Bursh의 형태와 사이즈를 지원한다.
SetHeight로 특정 Height로 지형을 만들 수도 있고,
폴리브러쉬처럼 그리는 방식으로 지형의 높낮이를 조절할 수도 있다.
다만 주의할 점은, 하나의 네모칸에 하나의 텍스쳐가 들어가니까 Smooth작업을 해 주어야 자연스러운 지형이 된다.
Detail Mesh로 프리팹을 생성할 수도 있다.
이 Detail Mesh에 텍스쳐를 넣을 수도 있다, 다만 이렇게 한다면 하나하나 생성되는 것이 아니라
텍스쳐가 입혀지는 것이기 때문에 성능상에서는 좋으나, 실제로는 텍스쳐가 플레이어를 따라다닌다.
05. 캐릭터 움직임 & 카메라
CharacterController로 이동을 표현할 것이다.
Character의 하위 항목들에는 다음과 같은 것들이 있다.
- slope Limit : 캐릭터가 오를 수 있는 경사의 높이
- Step offset : 오를 수 있는 계단의 높이
- Skin Width : 콜라이더와 메시 사이의 간격
- Min Move Distance : 충돌 감지를 위한 최소 이동거리
질문 : Skin Width와 Layer Overrides는 무엇이며 왜 필요한가 ?
다른 것들은 어떻게 쓰일지 바로 감이 오는데 이 두 항목은 좀 더 알아봐야 할 것 같아 찾아봤다.
SkinWidth
- 두 콜라이더가 서로 스킨 너비만큼 관통할 수 있다.
- Skin Width가 클 수록 지터링 현상이 감소한다. (보통 반지름의 10% 정도로 설정)
도저히 설명이 이해가 가지 않아 거의 한 시간 찾아봤다.
처음에는 Two Colliders can penerate each otehr as deep as their Skin Width 라는 표현 때문에,
Character Controller가 갖고 있는 자체적인 캡슐 모양의 콜라이더 '안쪽으로' 충돌을 허용하는 것인줄 알았으나
실험 결과 '충돌 감지 기준'이 바뀌는 것이다. 물리적 엔진의 동작 자체는 변화하지 않으나, 충돌의 감지 기준을
Skin Width만큼 살짝 밖으로 충돌 감지 기준이 확장되어서 Skin Width 범위 내에서 충돌을 조기에 종료한다.
이것이 지터링을 왜 바꿀 수 있느냐 !
우선 지터링(Jittering)은 충돌 감지 후 너무 강하게 튕겨나는 문제다.
이산적인 충돌 감지는 다음과 같은 과정을 통해 지터링을 만든다.
- 프레임마다 캐릭터가 중력으로 인해 바닥으로 떨어진다.
- 충돌 감지 시, 이미 조금 깊이 박혀 있는 상태에서 튕겨 나온다.
- 충돌 처리가 강하게 작용되어, 다시 위로 밀려나게 된다.
- 다음 프레임에서 다시 중력으로 내려오고, 또 충돌하고 튕겨 나간다.
- 이 과정을 반복하며 미세하게 떨리는 현상(지터링)이 발생한다.
다만, Skin Width를 적용하게 되면, 조금 겹쳐도 깊이 박히기 전에 충돌 처리가 종료된다(미리 감지하므로).
따라서 덜 깊이 박히기 때문에 튀어오르는 강한 반작용이 줄어들고 지터링이 줄어들게 된다.
콜라이더의 크기를 키우는 것과는 무엇이다르냐면, 콜라이더의 크기를 키우는 것은 물체의 크기가 커지는 것이므로
물리 엔진의 동작 자체가 변하는 것이고, Skin Width는 단순히 충돌 감지의 기준을 조절하는 개념이므로
게임 내 크기 변화 없이도 효과를 볼 수 있다. 즉, 콜라이더 크기를 키우면 상호작용 자체가 변화하지만
Skin Width는 충돌 판정 방식만 바꾸므로 지터링 해결에 최적이다.
Layer Overrides
- 캐릭터 컨트롤러가 특정 레이어의 오브젝트와 상호작용하는 방식을 재정의하는 기능
- 특정 레이어의 오브젝트에 대해서 충돌 검사를무시하거나, 다른 충돌 방식을 적용 가능
- 예를 들어 특정 레이어의 투명한 벽을 통과하거나, 특정 레이어만 충돌하게 할 수 있다.
경사면이면 땅에 안닿아 있다고 판단할 수도 있다.
예전에 배운 것처럼 다양한 방식으로 이 문제를 해결할 수 있다. ( 바닥에 다른 콜라이더를 추가하거나, 레이케스트 활용 )
Apply Root Motion, 애니메이션에 있는 transform 이동을 그대로 사용하겠다.
만일 믹사모 사이트에서 isPlaced와 같은 것들을 했다면 상관 없겠지만
애니메이션 자체에 이동이 있다면 고려해 봐야 한다.
Updated Mode
- Noraml : Update() 함수의 주기에 맞춰 업데이트, Time.timeScale의 영향 O
- Animate Physics : FixedUpdate 주기에 맞춰 애니메이션 업데이트 ( 물리적 상호작용과 적합)
- Unscaled Time : Update 주기에 맞춰 업데이트, Time.timeScale의 영향 X
Culling Mode : 애니메이션 컬링 모드 설정, 카메라 밖이나 가려진 오브젝트의 애니메이션 계산에 관함
- Always Animate : 항상 애니메이션 계산
- Cull Update Transforms : 렌더러가 보이지 않을 때 리타겟, IK, 트랜스폼 업데이트 생략
- Cull Completely : 렌더러가 보이지 않을 때 애니메이션 계산 완전 생략
* Time.TimeScale : 게임 전체의 시간 흐름의 속도, 배속이라고 이해하면 편함
카메라 관련
플레이어의 자식 오브젝트로 카메라를 할당하면 , 이동은 구현이 가능하지만 회전이 문제다.
보통 플레이어의 pivot이 바닥에 닿는 면이기 때문에 조정을 조금 해 주어야 한다.
( 카메라가 LookAt() 함수를 적용할 위치를 따로 만들어 주는 것이 좋다 )
다음과 같이 빙글빙글 돌면서 올바르게 회전해야 하려면 추가적인 계산이 필요하다.
유니티에서 주어진 기능으로 구현하면 조금 더 편할 수 있지만, 커스터마이징 등 원리를 이해해야 하는
상황이 생길 수 있기 때문에 설명이 필요하다. 구현은 구면 좌표계를 활용해서 한다.
구면 좌표계
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(Camera))]
public class CameraController : MonoBehaviour
{
[SerializeField] private Transform target;
[SerializeField] private float rotationSpeed = 100f;
// 타겟과 카메라 사이의 거리
[SerializeField] private float distance = 5f;
// 직각 삼각형 ( 서 있는 )각도
private float _azimuthAngle = 0f;
//누워 있는 직각 삼각형의 각도
private float _polarAngle = 45f;
private void Start()
{
var cartesianPosition = GetCameraPosition(distance, _azimuthAngle, _polarAngle);
var cameraPosition = target.position - cartesianPosition;
transform.position = cameraPosition;
transform.LookAt(target);
}
private void LateUpdate()
{
float mouseX = Input.GetAxis("Mouse X");
float mouseY = Input.GetAxis("Mouse Y");
//Debug.Log($"Mouse X : {mouseX} Mouse Y : {mouseY}");
_polarAngle -= mouseY * rotationSpeed * Time.deltaTime;
_azimuthAngle += mouseX * rotationSpeed * Time.deltaTime;
_polarAngle = Mathf.Clamp(_polarAngle, 10f, 45f);
var cartesianPosition = GetCameraPosition(distance, _azimuthAngle, _polarAngle);
var cameraPosition = target.position - cartesianPosition;
transform.position = cameraPosition;
transform.LookAt(target);
}
// 두 개의 각을 넣고, 거리를 넣으면 좌표값을 얻어 올 수 있는 함수
Vector3 GetCameraPosition(float r, float azimuthAngle, float polarAngle)
{
// 코싸인 함수는 Rad로 각을 변환해 주어야 함.
float b = r * Mathf.Cos( polarAngle * Mathf.Deg2Rad);
// b는 밑변의 길이를 구했다.
float z = b * Mathf.Cos(azimuthAngle * Mathf.Deg2Rad);
float y = r * Mathf.Sin(polarAngle * Mathf.Deg2Rad) * -1;
float x = b * Mathf.Sin(azimuthAngle * Mathf.Deg2Rad);
return new Vector3(x, y, z);
}
}
전체 코드는 다음과 같다. 2D 입력 ( 마우스 입력 )을 3D 좌표계인 구면 좌표계로 전환할 것이다.
우선 마우스 입력을 통해 PolarAnlge ( 수직 각 ) , AzimuthAngle (수평 각)을 구해준다.
이 때 PolarAngle에 -를 해 주는 이유는 마우스 입력 방향을 반전시키기 위해서이다.
일반적으로 Input.GetAxis("Mouse Y")값은 위로 올리면 +값, 아래로 내리면 -값이 나온다.
하지만 일반적으로 3D 게임에서는 마우스를 위로 올리면 카메라가 아래를 내려다봐야 한다.
따라서 -처리를 해 줌으로써 입력 방향을 반전시켜준다.
이 수직각과 수평각, 그리고 카메라로부터의 거리인 Distance (r)을 사용해서 구면 좌표계로 전환할 것이다.
r이 의미하는 바를 먼저 설명하자면, r은 타겟(중심)에서 카메라부터의 거리다. 따라서 구면 좌표계에서 보면 빗변이 된다.
이제, 빗변과 Polar Angle을 사용해서 밑변을 구해준다.
Cos(PolarAngle) = 밑변 / 빗변 , 따라서 밑변 = Cos(PolarAngle) * 빗변
즉, b = r * Cos(PolarAngle)
2) Y 좌표 구하기
우리가 구하고자 하는 Y좌표는 '높이'이다. 따라서 Sin(PolarAngle)을 활용해서 구할 수 있다.
Sin(PolarAngle) = 높이 / 빗변 , 따라서 높이 = Sin(PolarAngle) * 빗변
즉, Y = r * Sin(PolarAngle)
위에서 -로 마우스의 입력 방향을 반전시켜 주었지만 또 다시 -를 해 주는 이유는 현재 카메라 위치를 구하는 방법때문이다.
위 영상에서 분명 똑같이 마우스를 올리고 있음에도, 특정 구간에서는 카메라가 올라가는 듯한 효과를
특정 구간에서는 카메라가 내려가는 듯한 효과가 나타나고 있다. 이는 Sin(PolarAngle)의 특성 때문이다.
-를 곱하지 않은 일반적인 공식을 생각해보자.
Y = r * Sing(PolarAngle)
이 공식에서 중요한 점은:
- 𝜃 = 0° → sin(0°)=0\sin(0°) = 0, Y = 0 (시작점)
- 𝜃 = 90° → sin(90°)=1\sin(90°) = 1, Y = r (최대 높이)
- 𝜃 = 180° → sin(180°)=0\sin(180°) = 0, Y = 0 (다시 원점)
즉, 각도가 증가할수록 Y가 증가하다가 다시 감소한다는 것이다.
그러므로 카메라 이동의 방향을 고정하기 위해서는, PolarAngle값을 특정 각도에 맞춰 한정시키고,
그 각도 내에서 증가와 감소할 때의 Y값을 부호를 통해 올바르게 세팅해 주는 것이 필요하다.
그 코드가 바로 다음 코드이다.
_polarAngle = Mathf.Clamp(_polarAngle, 10f, 45f);
10f ~ 45f로 polarAnlge의 값이 고정되어 있으므로, 이에 맞는 부호를 통해 원하는 동작을 구현하면 된다.
현재 마우스를 아래로 내리면 polarAngle값이 증가하게 되고, 우리가 의도하는 바는 캐릭터가 땅을 보는 것,
즉 카메라가 위로 이동하는 것을 뜻한다.
그렇게 된다면 PolarAngle값이 증가할 때, Y값도 같이 증가해야 하는것이 맞다.
다만, 고려해야 할 것은 우리가 현재 카메라 위치를 다음과 같이 구하고 있다는 것이다.
cameraPosition=targetPosition−cartesianPosition
카메라는 타겟 기준으로 "뒤쪽"으로 이동한 위치에 있고, cartesianPosition을 빼 준 Vector3값에 위치한다.
따라서 PolarAngle값이 증가할 때 Y값이 같이 증가하기 위해서는 -1을 곱해주어 타겟을 기준으로 '반대 방향'으로
이동해야 한다.
3) X, Z좌표 구하기
위의 삼각형과는 다른 , XZ축으로 존재하는 삼각형을 생각하면 된다.
이 삼각형에서는 X축의 거리가 높이, Z축의 거리가 밑변, b가 빗변이 된다.
다음 그림을 참고하면 알 수 있다.
즉 X = b * Sin(PolarAngle)
Z = b * Cos(PolarAngle)이 된다.
마찬가지로 왼쪽을 보면 카메라가 오른쪽으로, 오른쪽을 보면 카메라가 왼쪽으로 이동하는 것은
카메라 위치를 타겟 위치에서 구한 포지션을 빼 주어서 정하고 있기 때문이다.
자투리 지식
1. 에셋을 하나 받을 때마다 혹시 모르니 깃 푸쉬로 저장해두자.
2. Tag는 문자열 형태로 비교하고, layer는 4비트로 비교한다.
3. Animation Blend를 통해 애니메이션을 파라미터로 섞어서 구현할 수 있다.
'TIL' 카테고리의 다른 글
[멋쟁이사자처럼 부트캠프 TIL회고] 애니메이션, 상태 EditorGUILayout,렉돌(Ragdoll), 충돌 처리, 카메라 처리 (0) | 2025.04.14 |
---|---|
[멋쟁이 사자처럼 부트캠프 TIL회고] 04.02 : Light (0) | 2025.04.02 |
[멋사 부트캠프 TIL회고] 오목 멀티플레이 오류 수정(2) (0) | 2025.03.25 |
[멋사 부트캠프 TIL 회고] 03.20 ~ 03.22 오류 수정 (0) | 2025.03.22 |
[멋사 부트캠프] 오목 게임 만들기 : 간단한 멀티플레이 구현 (0) | 2025.03.20 |