Gizmo
Description
nelari Blog 를 참고해서 만든 Gizmo 기능이다.
위 Gizmo 구현에서는 RayTrace 를 통해 Gizmo Picking 및 Gizmo Manipulating 을 구현한다. 이에 관한 수식은 위 블로그에 잘 정리되어 있어 따로 기록하진 않는다.(단 closest_distance_between_lines()
의 6번째 줄은 오타로 보인다. v1v2 * v1v2 - v12 * v22
부분은 빼기 순서가 잘못되어 부호가 반대로 나온다.) 여기선 이를 이용한 구현에 대해서 설명해보려고 한다.
설명
Cache
struct GizmoUpdateCache
{
void SetOriginMatrix(const Matrix& m)
{
axises[0] = m.Axis(0).Normalize();
axises[1] = m.Axis(1).Normalize();
axises[2] = m.Axis(2).Normalize();
m.DecomposeSRT(scl, rot, pos);
}
Vector3 axises[3];
Vector3 pos, scl; Quaternion rot;
int axis = -1; // X = 0, Y = 1, Z = 2
float lineProj = 0; // rans, Scale Mode 의 RayTrace 결과
Vector3 circleProj = 0; // Rotate Mode 의 RayTrace 결과
Color4 color;
};
Gizmo 조작 시 저장할 데이터들이다.
Gizmo 조작 시작 시 기준이 되는 SRT 와 Axis 정보를 저장하고, 시작 지점의 RayTrace 결과를 저장한다. World 공간에서 조작할 건지 Local 공간에서 조작할 건지는 이때 저장하는 정보로 결정된다.
const float SCREENED_SIZE = 0.1f; // Screen 상에서 Gizmo 가 보일 크기
float screen_scl = 1.f; // Gizmo Model 의 Scale
const float MAX_DIST = 0.3f; // RayTrace 에서 충돌허용을 위한 오차범위
추가로 다음의 변수가 중요하게 사용된다.
Gizmo Model 의 Scale 값인 screen_scl
은 약간의 설명이 필요하다.
- Gizmo 의 크기가 항상 동일하게 보이기 위해선 Gizmo 위치를 Camera 의 View 행렬로 곱해서 얻은 깊이 값을 Gizmo Model 의 Scale 에 곱하면 된다. ViewProj 과정에서 이 깊이 값으로
(X, Y, Z)
을 나누기 때문에 미리 곱해놓으면 좌표가 깊이와 상관없이 일정한 값이 되기 때문이다. 이러한 깊이 값과 화면 상의 크기 비율인SCREENED_SIZE
를 곱한 값이screen_scl
에 저장된다.
Update
{
// ...
// ray1 = camera ray 로 주어짐
// Cache 저장
cache.SetOriginMatrix(GetTransform()->GetWorldMatrix());
cache.scl = target->GetTransform()->GetScale();
// 가장 가까운 Axis 선택
float min_dist = FLT_MAX, proj_len = 0;
int axis = -1;
Ray ray2; ray2.origin = cache.pos;
for (int i = 0; i < 3; i++)
{
ray2.dir = cache.axises[i];
float t1, t2;
float tmp = IntersectHelpers::ClosestDistanceBetweenRays(ray1, ray2, t1, t2);
if (0 <= t2 && t2 <= screen_scl && tmp < min_dist) { min_dist = tmp; proj_len = t2; axis = i; }
}
// 범위에 맞으면 초기값 저장
if (axis >= 0 && min_dist < MAX_DIST * screen_scl)
{
cache.axis = axis;
cache.lineProj = proj_len;
ChangeGizmoColor(false);
}
}
위 코드는 마우스를 클릭해 Gizmo 조작을 시작할 때 수행된다.
재미있는 부분은 ClosestDistanceBetweenRays()
의 결과로 나오는 t2
의 성질이다.
- 이 함수는
(ray1.pos + ray1.dir * t1) - (ray2.pos + ray2.dir * t2)
의 절댓값을 최솟값으로 만드는 미지수t1, t2
를 구한다.ray2.dir
은 Gizmo 의 Axis 중 하나이므로t2
는 Gizmo 의 축 상의 위치가 된다. - 그래서
0 <= t2 && t2 <= screen_scl
은 화면 상에서SCREENED_SIZE = 0.1f
길이에 해당되는 범위가 된다. Gizmo Model 의 크기가1
이면 이는 곧 Gizmo 의 길이가 된다.
구해진 최소거리인 min_dist
는 MAX_DIST * screen_scl
보자 작아야한다. 이유는 위와 비슷하다. Axis 에 닿았다고 판정할 거리가 화면 상에서 균등토록 하기 위함이다.
Rotate 의 경우는 위와 비슷하게 구현했다. 원은 직선과 달리 크기에 한계가 있어서 min_dist < MAX_DIST * screen_scl
만 체크해주면 되므로 더 간단하다.
{
// ...
Ray ray2(cache.pos, cache.axises[cache.axis]);
float t1, t2;
IntersectHelpers::ClosestDistanceBetweenRays(ray, ray2, t1, t2);
if(mode==EGizmoMode::Translate)
target->GetTransform()->SetPosition(cache.pos + cache.axises[cache.axis] * (t2 - cache.lineProj));
else if(mode==EGizmoMode::Scale)
{
if(t2 - cache.lineProj >= 0)
target->GetTransform()->SetScale(cache.scl + cache.axises[cache.axis] * (t2 - cache.lineProj));
else
target->GetTransform()->SetScale(cache.scl / (cache.scl + cache.axises[cache.axis] * (cache.lineProj - t2)));
}
}
위 코드는 초기값을 저장한 후 Gizmo 조작 중에 매 틱마다 호출된다. RayTrace 를 수행하여 그 결과값을 초기값과 비교해 Target 의 Transform 을 업데이트하는 역할을 한다.
위와 달리 매 틱마다 Delta 를 구하는 방식을 쓸 수도 있다. 하지만 틱이 너무 빠르면 Delta 가 항상 0 이 되어 이를 또 처리해줘야하고, 만약 훗날에 Command 패턴으로 Redo/Undo 를 구현한다면 구현이 난해해질 수가 있어 이쪽이 더 바람직할 것이다.
댓글남기기