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_distMAX_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 를 구현한다면 구현이 난해해질 수가 있어 이쪽이 더 바람직할 것이다.

댓글남기기