Prechádzať zdrojové kódy

Editor: Add gizmo rotation and snapping

Panagiotis Christopoulos Charitos 2 týždňov pred
rodič
commit
b701e6f3d2

+ 7 - 0
AnKi/Collision/Plane.h

@@ -85,6 +85,13 @@ public:
 	/// Set from plane equation is ax+by+cz+d
 	void setFromPlaneEquation(F32 a, F32 b, F32 c, F32 d);
 
+	// Set the plane from a point in plane and a direction
+	void setFromRay(Vec3 rayOrigin, Vec3 rayDir)
+	{
+		m_normal = rayDir.xyz0();
+		m_offset = rayDir.dot(rayOrigin);
+	}
+
 	/// Return the transformed
 	Plane getTransformed(const Transform& trf) const;
 

+ 271 - 115
AnKi/Editor/EditorUi.cpp

@@ -12,6 +12,7 @@
 #include <AnKi/Util/Filesystem.h>
 #include <AnKi/Renderer/Renderer.h>
 #include <AnKi/Renderer/Dbg.h>
+#include <AnKi/Collision.h>
 #include <filesystem>
 #include <ThirdParty/ImGui/Extra/IconsMaterialDesignIcons.h> // See all icons in https://pictogrammers.com/library/mdi/
 
@@ -91,6 +92,28 @@ static F32 projectNdcToRay(Vec2 ndc, Vec3 rayOrigin, Vec3 rayDir)
 	return distFromOrign;
 }
 
+static Bool projectNdcToPlane(Vec2 ndc, Plane plane, Vec3& point)
+{
+	const Frustum& frustum = SceneGraph::getSingleton().getActiveCameraNode().getFirstComponentOfType<CameraComponent>().getFrustum();
+	const Mat4 invMvp = frustum.getViewProjectionMatrix().invert();
+
+	Vec4 v4 = invMvp * Vec4(ndc, 1.0f, 1.0f);
+	v4 /= v4.w();
+
+	const Vec3 rayOrigin = frustum.getWorldTransform().getOrigin().xyz();
+	const Vec3 rayDir = (v4.xyz() - rayOrigin).normalize();
+
+	Vec4 collisionPoint;
+	const Bool collides = testCollision(plane, Ray(rayOrigin, rayDir), collisionPoint);
+
+	if(collides)
+	{
+		point = collisionPoint.xyz();
+	}
+
+	return collides;
+}
+
 EditorUi::EditorUi()
 {
 	Logger::getSingleton().addMessageHandler(this, loggerMessageHandler);
@@ -253,144 +276,182 @@ void EditorUi::draw(UiCanvas& canvas)
 	ImGui::PopStyleVar();
 	ImGui::PopFont();
 
-	m_mouseHoveredOverAnyWindow = ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow);
+	// Mouse is over any window if mouse is hovering or if clicked on a window
+	m_mouseOverAnyWindow = ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow) || ImGui::GetIO().WantCaptureMouse;
 
 	m_canvas = nullptr;
 }
 
 void EditorUi::mainMenu()
 {
-	if(ImGui::BeginMainMenuBar())
-	{
-		if(ImGui::BeginMenu(ICON_MDI_FOLDER_OUTLINE " File"))
-		{
-			if(ImGui::MenuItem(ICON_MDI_CLOSE_CIRCLE " Quit", "CTRL+Q"))
-			{
-				m_quit = true;
-			}
-			ImGui::EndMenu();
-		}
+	// The menu and toolbox is based on the comments in https://github.com/ocornut/imgui/issues/3518
 
-		if(ImGui::BeginMenu(ICON_MDI_APPLICATION_OUTLINE " Windows"))
-		{
-			if(ImGui::MenuItem(ICON_MDI_APPLICATION_OUTLINE " Console"))
-			{
-				m_showConsoleWindow = true;
-			}
-
-			if(ImGui::MenuItem(ICON_MDI_APPLICATION_OUTLINE " SceneNode Props"))
-			{
-				m_showSceneNodePropsWindow = true;
-			}
+	ImGuiViewport* viewport = ImGui::GetMainViewport();
+	const ImGuiWindowFlags windowFlags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar;
 
-			if(ImGui::MenuItem(ICON_MDI_APPLICATION_OUTLINE " Scene Hierarchy"))
+	if(ImGui::BeginViewportSideBar("##MainMenu", viewport, ImGuiDir_Up, ImGui::GetFrameHeight(), windowFlags))
+	{
+		if(ImGui::BeginMenuBar())
+		{
+			if(ImGui::BeginMenu(ICON_MDI_FOLDER_OUTLINE " File"))
 			{
-				m_showSceneHierarcyWindow = true;
+				if(ImGui::MenuItem(ICON_MDI_CLOSE_CIRCLE " Quit", "CTRL+Q"))
+				{
+					m_quit = true;
+				}
+				ImGui::EndMenu();
 			}
 
-			if(ImGui::MenuItem(ICON_MDI_APPLICATION_OUTLINE " Assets"))
+			if(ImGui::BeginMenu(ICON_MDI_APPLICATION_OUTLINE " Windows"))
 			{
-				m_showAssetsWindow = true;
-			}
+				if(ImGui::MenuItem(ICON_MDI_APPLICATION_OUTLINE " Console"))
+				{
+					m_showConsoleWindow = true;
+				}
 
-			if(ImGui::MenuItem(ICON_MDI_APPLICATION_OUTLINE " CVars Editor"))
-			{
-				m_showCVarEditorWindow = true;
-			}
+				if(ImGui::MenuItem(ICON_MDI_APPLICATION_OUTLINE " SceneNode Props"))
+				{
+					m_showSceneNodePropsWindow = true;
+				}
 
-			if(ImGui::MenuItem(ICON_MDI_APPLICATION_OUTLINE " Debug Render Targets"))
-			{
-				m_showDebugRtsWindow = true;
-			}
+				if(ImGui::MenuItem(ICON_MDI_APPLICATION_OUTLINE " Scene Hierarchy"))
+				{
+					m_showSceneHierarcyWindow = true;
+				}
 
-			ImGui::EndMenu();
-		}
+				if(ImGui::MenuItem(ICON_MDI_APPLICATION_OUTLINE " Assets"))
+				{
+					m_showAssetsWindow = true;
+				}
 
-		if(ImGui::BeginMenu(ICON_MDI_CUBE_SCAN " Debug"))
-		{
-			Bool bBoundingBoxes = !!(Renderer::getSingleton().getDbg().getOptions() & DbgOption::kBoundingBoxes);
-			if(ImGui::Checkbox("Visible Renderables", &bBoundingBoxes))
-			{
-				DbgOption options = Renderer::getSingleton().getDbg().getOptions();
-				if(bBoundingBoxes)
+				if(ImGui::MenuItem(ICON_MDI_APPLICATION_OUTLINE " CVars Editor"))
 				{
-					options |= DbgOption::kBoundingBoxes;
+					m_showCVarEditorWindow = true;
 				}
-				else
+
+				if(ImGui::MenuItem(ICON_MDI_APPLICATION_OUTLINE " Debug Render Targets"))
 				{
-					options &= ~(DbgOption::kBoundingBoxes);
+					m_showDebugRtsWindow = true;
 				}
-				Renderer::getSingleton().getDbg().setOptions(options);
+
+				ImGui::EndMenu();
 			}
 
-			Bool bPhysics = !!(Renderer::getSingleton().getDbg().getOptions() & DbgOption::kPhysics);
-			if(ImGui::Checkbox("Physics Bodies", &bPhysics))
+			if(ImGui::BeginMenu(ICON_MDI_CUBE_SCAN " Debug"))
 			{
-				DbgOption options = Renderer::getSingleton().getDbg().getOptions();
-				if(bPhysics)
+				Bool bBoundingBoxes = !!(Renderer::getSingleton().getDbg().getOptions() & DbgOption::kBoundingBoxes);
+				if(ImGui::Checkbox("Visible Renderables", &bBoundingBoxes))
 				{
-					options |= DbgOption::kPhysics;
+					DbgOption options = Renderer::getSingleton().getDbg().getOptions();
+					if(bBoundingBoxes)
+					{
+						options |= DbgOption::kBoundingBoxes;
+					}
+					else
+					{
+						options &= ~(DbgOption::kBoundingBoxes);
+					}
+					Renderer::getSingleton().getDbg().setOptions(options);
 				}
-				else
+
+				Bool bPhysics = !!(Renderer::getSingleton().getDbg().getOptions() & DbgOption::kPhysics);
+				if(ImGui::Checkbox("Physics Bodies", &bPhysics))
 				{
-					options &= ~DbgOption::kPhysics;
+					DbgOption options = Renderer::getSingleton().getDbg().getOptions();
+					if(bPhysics)
+					{
+						options |= DbgOption::kPhysics;
+					}
+					else
+					{
+						options &= ~DbgOption::kPhysics;
+					}
+					Renderer::getSingleton().getDbg().setOptions(options);
 				}
-				Renderer::getSingleton().getDbg().setOptions(options);
-			}
 
-			Bool bDepthTest = !!(Renderer::getSingleton().getDbg().getOptions() & DbgOption::kDepthTest);
-			if(ImGui::Checkbox("Depth Test", &bDepthTest))
-			{
-				DbgOption options = Renderer::getSingleton().getDbg().getOptions();
-				if(bDepthTest)
+				Bool bDepthTest = !!(Renderer::getSingleton().getDbg().getOptions() & DbgOption::kDepthTest);
+				if(ImGui::Checkbox("Depth Test", &bDepthTest))
 				{
-					options |= DbgOption::kDepthTest;
+					DbgOption options = Renderer::getSingleton().getDbg().getOptions();
+					if(bDepthTest)
+					{
+						options |= DbgOption::kDepthTest;
+					}
+					else
+					{
+						options &= ~DbgOption::kDepthTest;
+					}
+					Renderer::getSingleton().getDbg().setOptions(options);
 				}
-				else
+
+				ImGui::EndMenu();
+			}
+
+			// Title
+			{
+				CString text = "AnKi 3D Engine Editor";
+				const F32 menuBarWidth = ImGui::GetWindowWidth();
+				const F32 textWidth = ImGui::CalcTextSize(text.cstr()).x;
+				ImGui::SameLine(menuBarWidth - menuBarWidth / 2.0f - textWidth / 2.0f);
+
+				ImGui::TextUnformatted(text.cstr());
+			}
+
+			// Quit bnt
+			{
+				const Char* text = ICON_MDI_CLOSE_CIRCLE;
+				const Vec2 textSize = ImGui::CalcTextSize(text);
+
+				const F32 menuBarWidth = ImGui::GetWindowWidth();
+				ImGui::SameLine(menuBarWidth - textSize.x() - ImGui::GetStyle().FramePadding.x * 2.0f - kMargin);
+
+				ImGui::PushStyleColor(ImGuiCol_ButtonHovered, Vec4(1.0f, 0.0f, 0.0f, 1.0f));
+
+				if(ImGui::Button(text))
 				{
-					options &= ~DbgOption::kDepthTest;
+					m_quit = true;
 				}
-				Renderer::getSingleton().getDbg().setOptions(options);
+
+				ImGui::PopStyleColor();
 			}
 
-			ImGui::EndMenu();
+			ImGui::EndMenuBar();
 		}
 
-		// Title
+		if(Input::getSingleton().getKey(KeyCode::kLeftCtrl) > 0 && Input::getSingleton().getKey(KeyCode::kQ) > 0)
 		{
-			CString text = "AnKi 3D Engine Editor";
-			const F32 menuBarWidth = ImGui::GetWindowWidth();
-			const F32 textWidth = ImGui::CalcTextSize(text.cstr()).x;
-			ImGui::SameLine(menuBarWidth - menuBarWidth / 2.0f - textWidth / 2.0f);
-
-			ImGui::TextUnformatted(text.cstr());
+			m_quit = true;
 		}
+	}
+	ImGui::End();
 
-		// Quit bnt
+	// Toolbox
+	if(ImGui::BeginViewportSideBar("##Toolbox", viewport, ImGuiDir_Up, ImGui::GetFrameHeight(), windowFlags))
+	{
+		if(ImGui::BeginMenuBar())
 		{
-			const Char* text = ICON_MDI_CLOSE_CIRCLE;
-			const Vec2 textSize = ImGui::CalcTextSize(text);
+			ImGui::SetNextItemWidth(ImGui::CalcTextSize("00.000").x);
 
-			const F32 menuBarWidth = ImGui::GetWindowWidth();
-			ImGui::SameLine(menuBarWidth - textSize.x() - ImGui::GetStyle().FramePadding.x * 2.0f - kMargin);
+			if(ImGui::SliderFloat(ICON_MDI_AXIS_ARROW "&" ICON_MDI_ARROW_EXPAND_ALL " Snapping", &m_toolbox.m_scaleTranslationSnapping, 0.0, 10.0f))
+			{
+				const F32 roundTo = 0.5f;
+				m_toolbox.m_scaleTranslationSnapping = round(m_toolbox.m_scaleTranslationSnapping / roundTo) * roundTo;
+			}
+
+			ImGui::SameLine();
 
-			ImGui::PushStyleColor(ImGuiCol_ButtonHovered, Vec4(1.0f, 0.0f, 0.0f, 1.0f));
+			ImGui::SetNextItemWidth(ImGui::CalcTextSize("00.000").x);
 
-			if(ImGui::Button(text))
+			if(ImGui::SliderFloat(ICON_MDI_ROTATE_ORBIT " Snapping", &m_toolbox.m_rotationSnappingDeg, 0.0, 90.0f))
 			{
-				m_quit = true;
+				const F32 roundTo = 1.0f;
+				m_toolbox.m_rotationSnappingDeg = round(m_toolbox.m_rotationSnappingDeg / roundTo) * roundTo;
 			}
 
-			ImGui::PopStyleColor();
+			ImGui::EndMenuBar();
 		}
-
-		ImGui::EndMainMenuBar();
-	}
-
-	if(Input::getSingleton().getKey(KeyCode::kLeftCtrl) > 0 && Input::getSingleton().getKey(KeyCode::kQ) > 0)
-	{
-		m_quit = true;
 	}
+	ImGui::End();
 }
 
 void EditorUi::sceneNode(SceneNode& node)
@@ -1580,7 +1641,7 @@ void EditorUi::loadImageToCache(CString fname, ImageResourcePtr& img)
 
 void EditorUi::objectPicking()
 {
-	if(!m_mouseHoveredOverAnyWindow && Input::getSingleton().getMouseButton(MouseButton::kLeft) == 1)
+	if(!m_mouseOverAnyWindow && Input::getSingleton().getMouseButton(MouseButton::kLeft) == 1)
 	{
 		const DbgObjectPickingResult& res = Renderer::getSingleton().getDbg().getObjectPickingResultAtMousePosition();
 
@@ -1598,6 +1659,7 @@ void EditorUi::objectPicking()
 
 			m_objectPicking.m_translationAxisSelected = kMaxU32;
 			m_objectPicking.m_scaleAxisSelected = kMaxU32;
+			m_objectPicking.m_rotationAxisSelected = kMaxU32;
 		}
 		else if(res.isValid())
 		{
@@ -1614,11 +1676,39 @@ void EditorUi::objectPicking()
 			{
 				m_objectPicking.m_translationAxisSelected = res.m_translationAxis;
 				m_objectPicking.m_scaleAxisSelected = kMaxU32;
+				m_objectPicking.m_rotationAxisSelected = kMaxU32;
 			}
 			else if(res.m_scaleAxis < 3)
 			{
 				m_objectPicking.m_translationAxisSelected = kMaxU32;
 				m_objectPicking.m_scaleAxisSelected = res.m_scaleAxis;
+				m_objectPicking.m_rotationAxisSelected = kMaxU32;
+			}
+			else if(res.m_rotationAxis < 3)
+			{
+				m_objectPicking.m_translationAxisSelected = kMaxU32;
+				m_objectPicking.m_scaleAxisSelected = kMaxU32;
+				m_objectPicking.m_rotationAxisSelected = res.m_rotationAxis;
+
+				// Calc the pivot point
+				const Transform camTrf =
+					SceneGraph::getSingleton().getActiveCameraNode().getFirstComponentOfType<CameraComponent>().getFrustum().getWorldTransform();
+
+				Plane axisPlane;
+				axisPlane.setFromRay(nodeOrigin, rotationAxis[res.m_rotationAxis]);
+
+				Bool collides = projectNdcToPlane(Input::getSingleton().getMousePositionNdc(), axisPlane, m_objectPicking.m_pivotPoint);
+				if(!collides)
+				{
+					// Clicked the gizmo from the back side, use the negative plane
+					axisPlane.setFromRay(nodeOrigin, -rotationAxis[res.m_rotationAxis]);
+					collides = projectNdcToPlane(Input::getSingleton().getMousePositionNdc(), axisPlane, m_objectPicking.m_pivotPoint);
+					if(!collides)
+					{
+						ANKI_LOGW("Can't determin the pivot point");
+						m_objectPicking.m_pivotPoint = nodeOrigin;
+					}
+				}
 			}
 
 			if(res.m_translationAxis < 3 || res.m_scaleAxis < 3)
@@ -1634,12 +1724,13 @@ void EditorUi::objectPicking()
 			m_sceneHierarchyWindow.m_selectedNode = nullptr;
 			m_objectPicking.m_translationAxisSelected = kMaxU32;
 			m_objectPicking.m_scaleAxisSelected = kMaxU32;
+			m_objectPicking.m_rotationAxisSelected = kMaxU32;
 		}
 	}
 
-	if(!m_mouseHoveredOverAnyWindow && Input::getSingleton().getMouseButton(MouseButton::kLeft) > 1)
+	if(!m_mouseOverAnyWindow && Input::getSingleton().getMouseButton(MouseButton::kLeft) > 1 && m_sceneHierarchyWindow.m_selectedNode)
 	{
-		// Dragging?
+		// Possibly dragging
 
 		const Transform& nodeTrf = m_sceneHierarchyWindow.m_selectedNode->getLocalTransform();
 		Array<Vec3, 3> rotationAxis;
@@ -1651,53 +1742,118 @@ void EditorUi::objectPicking()
 		{
 			const U32 axis = m_objectPicking.m_translationAxisSelected;
 			const F32 moveDistance = projectNdcToRay(Input::getSingleton().getMousePositionNdc(), m_objectPicking.m_pivotPoint, rotationAxis[axis]);
-			m_objectPicking.m_pivotPoint = m_objectPicking.m_pivotPoint + rotationAxis[axis] * moveDistance;
 
-			if(m_objectPicking.m_translationAxisSelected == 0)
-			{
-				m_sceneHierarchyWindow.m_selectedNode->moveLocalX(moveDistance);
-			}
-			else if(m_objectPicking.m_translationAxisSelected == 1)
-			{
-				m_sceneHierarchyWindow.m_selectedNode->moveLocalY(moveDistance);
-			}
-			else
+			const Vec3 oldPosition = nodeTrf.getOrigin().xyz();
+			Vec3 newPosition = oldPosition + rotationAxis[axis] * moveDistance;
+
+			// Snap position
+			for(U32 i = 0; i < 3 && m_toolbox.m_scaleTranslationSnapping > kEpsilonf; ++i)
 			{
-				m_sceneHierarchyWindow.m_selectedNode->moveLocalZ(moveDistance);
+				newPosition[i] = round(newPosition[i] / m_toolbox.m_scaleTranslationSnapping) * m_toolbox.m_scaleTranslationSnapping;
 			}
+			m_sceneHierarchyWindow.m_selectedNode->setLocalOrigin(newPosition);
+
+			// Update the pivot
+			const Vec3 moveDiff = newPosition - oldPosition; // Move the pivot as you moved the node origin
+			m_objectPicking.m_pivotPoint = m_objectPicking.m_pivotPoint + moveDiff;
 		}
 		else if(m_objectPicking.m_scaleAxisSelected < 3)
 		{
 			const U32 axis = m_objectPicking.m_scaleAxisSelected;
 			const F32 moveDistance = projectNdcToRay(Input::getSingleton().getMousePositionNdc(), m_objectPicking.m_pivotPoint, rotationAxis[axis]);
-			m_objectPicking.m_pivotPoint = m_objectPicking.m_pivotPoint + rotationAxis[axis] * moveDistance;
 
-			Vec3 scale = nodeTrf.getScale().xyz();
+			const F32 oldAxisScale = nodeTrf.getScale()[axis];
+			F32 newAxisScale = oldAxisScale + moveDistance;
 
-			if(m_objectPicking.m_scaleAxisSelected == 0)
+			// Snap scale
+			if(m_toolbox.m_scaleTranslationSnapping > kEpsilonf)
 			{
-				scale.x() += moveDistance;
+				newAxisScale = round(newAxisScale / m_toolbox.m_scaleTranslationSnapping) * m_toolbox.m_scaleTranslationSnapping;
 			}
-			else if(m_objectPicking.m_scaleAxisSelected == 1)
+
+			Vec3 scale = nodeTrf.getScale().xyz();
+			scale[axis] = max(newAxisScale, m_toolbox.m_scaleTranslationSnapping);
+			m_sceneHierarchyWindow.m_selectedNode->setLocalScale(scale);
+
+			// Update the pivot
+			const F32 adjustedMoveDistance = newAxisScale - oldAxisScale;
+			m_objectPicking.m_pivotPoint = m_objectPicking.m_pivotPoint + rotationAxis[axis] * adjustedMoveDistance;
+		}
+		else if(m_objectPicking.m_rotationAxisSelected < 3)
+		{
+			const U32 axis = m_objectPicking.m_rotationAxisSelected;
+			const Vec3 nodeOrigin = nodeTrf.getOrigin().xyz();
+
+			// Compute the new pivot point
+			Plane axisPlane;
+			axisPlane.setFromRay(nodeOrigin, rotationAxis[axis]);
+
+			Vec3 newPivotPoint;
+			Bool collides = projectNdcToPlane(Input::getSingleton().getMousePositionNdc(), axisPlane, newPivotPoint);
+			if(!collides)
 			{
-				scale.y() += moveDistance;
+				// Clicked the gizmo from the back side, use the negative plane
+				axisPlane.setFromRay(nodeOrigin, -rotationAxis[axis]);
+				collides = projectNdcToPlane(Input::getSingleton().getMousePositionNdc(), axisPlane, newPivotPoint);
+				if(!collides)
+				{
+					ANKI_LOGW("Can't determin the pivot point");
+					newPivotPoint = nodeOrigin;
+				}
 			}
-			else
+
+			// Compute the angle between the points
+			const Vec3 dir0 = (newPivotPoint - nodeOrigin).normalize();
+			const Vec3 dir1 = (m_objectPicking.m_pivotPoint - nodeOrigin).normalize();
+			F32 angle = acos(saturate(dir1.dot(dir0)));
+			if(m_toolbox.m_rotationSnappingDeg > kEpsilonf)
 			{
-				scale.z() += moveDistance;
+				const F32 rad = toRad(m_toolbox.m_rotationSnappingDeg);
+				angle = round(angle / rad) * rad;
 			}
 
-			m_sceneHierarchyWindow.m_selectedNode->setLocalScale(scale);
+			Vec3 cross = dir1.cross(dir0);
+
+			if(cross != Vec3(0.0f))
+			{
+				cross = cross.normalize();
+				const F32 angleSign = (cross.dot(rotationAxis[axis]) < 1.0f) ? -1.0f : 1.0f;
+				angle *= angleSign;
+
+				// Apply the angle
+				if(m_objectPicking.m_rotationAxisSelected == 0)
+				{
+					m_sceneHierarchyWindow.m_selectedNode->rotateLocalX(angle);
+				}
+				else if(m_objectPicking.m_rotationAxisSelected == 1)
+				{
+					m_sceneHierarchyWindow.m_selectedNode->rotateLocalY(angle);
+				}
+				else
+				{
+					m_sceneHierarchyWindow.m_selectedNode->rotateLocalZ(angle);
+				}
+
+				// Use the snapped angle to adjust the pivot point
+				Mat3 rot;
+				rot.setZAxis(rotationAxis[axis]);
+				rot.setXAxis(dir1);
+				rot.setYAxis(rotationAxis[axis].cross(dir1).normalize());
+				rot.rotateZAxis(angle);
+				newPivotPoint = nodeOrigin + rot.getXAxis() * (newPivotPoint - nodeOrigin).length();
+
+				m_objectPicking.m_pivotPoint = newPivotPoint;
+			}
 		}
 	}
 
 	if(m_sceneHierarchyWindow.m_selectedNode)
 	{
-		Renderer::getSingleton().getDbg().setGizmosTransform(m_sceneHierarchyWindow.m_selectedNode->getWorldTransform(), true);
+		Renderer::getSingleton().getDbg().enableGizmos(m_sceneHierarchyWindow.m_selectedNode->getWorldTransform(), true);
 	}
 	else
 	{
-		Renderer::getSingleton().getDbg().setGizmosTransform(Transform::getIdentity(), false);
+		Renderer::getSingleton().getDbg().enableGizmos(Transform::getIdentity(), false);
 	}
 }
 

+ 9 - 1
AnKi/Editor/EditorUi.h

@@ -24,7 +24,7 @@ class EditorUi
 public:
 	Bool m_quit = false;
 
-	Bool m_mouseHoveredOverAnyWindow = false; // Mouse is over one of the editor windows.
+	Bool m_mouseOverAnyWindow = false; // Mouse is over one of the editor windows.
 
 	EditorUi();
 
@@ -95,6 +95,7 @@ private:
 	public:
 		U32 m_translationAxisSelected = kMaxU32;
 		U32 m_scaleAxisSelected = kMaxU32;
+		U32 m_rotationAxisSelected = kMaxU32;
 		Vec3 m_pivotPoint;
 	} m_objectPicking;
 
@@ -152,6 +153,13 @@ private:
 		Bool m_disableTonemapping = false;
 	} m_debugRtsWindow;
 
+	class
+	{
+	public:
+		F32 m_scaleTranslationSnapping = 0.1f;
+		F32 m_rotationSnappingDeg = 1.0f;
+	} m_toolbox;
+
 	void mainMenu();
 
 	// Windows

+ 75 - 8
AnKi/Renderer/Dbg.cpp

@@ -373,6 +373,25 @@ static const U16 g_gizmoRingIndices[512][3] = {
 	{173, 172, 245}, {174, 173, 247}, {175, 174, 226}, {176, 175, 225}, {129, 176, 227}, {128, 129, 229}, {132, 128, 228}, {131, 132, 235},
 	{130, 131, 231}, {138, 130, 230}, {134, 138, 234}, {133, 134, 232}, {137, 133, 233}, {135, 137, 255}, {136, 135, 250}, {181, 136, 251}};
 
+static constexpr F32 kCubePositions[] = {
+	// Front face
+	-0.5f, -0.5f, 0.5f, 0.5f, -0.5f, 0.5f, 0.5f, 0.5f, 0.5f, -0.5f, -0.5f, 0.5f, 0.5f, 0.5f, 0.5f, -0.5f, 0.5f, 0.5f,
+
+	// Back face
+	0.5f, -0.5f, -0.5f, -0.5f, -0.5f, -0.5f, -0.5f, 0.5f, -0.5f, 0.5f, -0.5f, -0.5f, -0.5f, 0.5f, -0.5f, 0.5f, 0.5f, -0.5f,
+
+	// Left face
+	-0.5f, -0.5f, -0.5f, -0.5f, -0.5f, 0.5f, -0.5f, 0.5f, 0.5f, -0.5f, -0.5f, -0.5f, -0.5f, 0.5f, 0.5f, -0.5f, 0.5f, -0.5f,
+
+	// Right face
+	0.5f, -0.5f, 0.5f, 0.5f, -0.5f, -0.5f, 0.5f, 0.5f, -0.5f, 0.5f, -0.5f, 0.5f, 0.5f, 0.5f, -0.5f, 0.5f, 0.5f, 0.5f,
+
+	// Top face
+	-0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, -0.5f, -0.5f, 0.5f, 0.5f, 0.5f, 0.5f, -0.5f, -0.5f, 0.5f, -0.5f,
+
+	// Bottom face
+	-0.5f, -0.5f, -0.5f, 0.5f, -0.5f, -0.5f, 0.5f, -0.5f, 0.5f, -0.5f, -0.5f, -0.5f, 0.5f, -0.5f, 0.5f, -0.5f, -0.5f, 0.5f};
+
 Dbg::Dbg()
 {
 	registerDebugRenderTarget("ObjectPicking");
@@ -411,9 +430,9 @@ Error Dbg::init()
 		buffInit.m_size = sizeof(Vec3) * 8;
 		buffInit.m_usage = BufferUsageBit::kVertexOrIndex;
 		buffInit.m_mapAccess = BufferMapAccessBit::kWrite;
-		m_cubeVertsBuffer = GrManager::getSingleton().newBuffer(buffInit);
+		m_boxLines.m_positionsBuff = GrManager::getSingleton().newBuffer(buffInit);
 
-		Vec3* verts = static_cast<Vec3*>(m_cubeVertsBuffer->map(0, kMaxPtrSize, BufferMapAccessBit::kWrite));
+		Vec3* verts = static_cast<Vec3*>(m_boxLines.m_positionsBuff->map(0, kMaxPtrSize, BufferMapAccessBit::kWrite));
 
 		constexpr F32 kSize = 1.0f;
 		verts[0] = Vec3(kSize, kSize, kSize); // front top right
@@ -425,14 +444,14 @@ Error Dbg::init()
 		verts[6] = Vec3(-kSize, -kSize, -kSize); // back bottom left
 		verts[7] = Vec3(kSize, -kSize, -kSize); // back bottom right
 
-		m_cubeVertsBuffer->unmap();
+		m_boxLines.m_positionsBuff->unmap();
 
 		constexpr U kIndexCount = 12 * 2;
 		buffInit.setName("Dbg cube indices");
 		buffInit.m_usage = BufferUsageBit::kVertexOrIndex;
 		buffInit.m_size = kIndexCount * sizeof(U16);
-		m_cubeIndicesBuffer = GrManager::getSingleton().newBuffer(buffInit);
-		U16* indices = static_cast<U16*>(m_cubeIndicesBuffer->map(0, kMaxPtrSize, BufferMapAccessBit::kWrite));
+		m_boxLines.m_indexBuff = GrManager::getSingleton().newBuffer(buffInit);
+		U16* indices = static_cast<U16*>(m_boxLines.m_indexBuff->map(0, kMaxPtrSize, BufferMapAccessBit::kWrite));
 
 		U c = 0;
 		indices[c++] = 0;
@@ -462,13 +481,25 @@ Error Dbg::init()
 		indices[c++] = 3;
 		indices[c++] = 7;
 
-		m_cubeIndicesBuffer->unmap();
+		m_boxLines.m_indexBuff->unmap();
 
 		ANKI_ASSERT(c == kIndexCount);
 	}
 
 	initGizmos();
 
+	{
+		BufferInitInfo buffInit("Debug cube");
+		buffInit.m_mapAccess = BufferMapAccessBit::kWrite;
+		buffInit.m_size = sizeof(kCubePositions);
+		buffInit.m_usage = BufferUsageBit::kVertexOrIndex;
+		m_debugPoint.m_positionsBuff = GrManager::getSingleton().newBuffer(buffInit);
+
+		void* mapped = m_debugPoint.m_positionsBuff->map(0, kMaxPtrSize, BufferMapAccessBit::kWrite);
+		memcpy(mapped, kCubePositions, sizeof(kCubePositions));
+		m_debugPoint.m_positionsBuff->unmap();
+	}
+
 	return Error::kNone;
 }
 
@@ -634,9 +665,9 @@ void Dbg::populateRenderGraphMain(RenderingContext& ctx)
 			consts.m_depthFailureVisualization = !(m_options & DbgOption::kDepthTest);
 
 			cmdb.setFastConstants(&consts, sizeof(consts));
-			cmdb.bindVertexBuffer(0, BufferView(m_cubeVertsBuffer.get()), sizeof(Vec3));
+			cmdb.bindVertexBuffer(0, BufferView(m_boxLines.m_positionsBuff.get()), sizeof(Vec3));
 			cmdb.setVertexAttribute(VertexAttributeSemantic::kPosition, 0, Format::kR32G32B32_Sfloat, 0);
-			cmdb.bindIndexBuffer(BufferView(m_cubeIndicesBuffer.get()), IndexType::kU16);
+			cmdb.bindIndexBuffer(BufferView(m_boxLines.m_indexBuff.get()), IndexType::kU16);
 		}
 
 		// GBuffer AABBs
@@ -735,6 +766,42 @@ void Dbg::populateRenderGraphMain(RenderingContext& ctx)
 			}
 		}
 
+		// Debug point
+		if(m_debugPoint.m_position != kMaxF32)
+		{
+			struct Consts
+			{
+				Mat4 m_mvp;
+				Vec4 m_color;
+			} consts;
+			const Mat4 trf = Mat4(m_debugPoint.m_position, Mat3::getIdentity(), Vec3(m_debugPoint.m_size));
+			consts.m_mvp = ctx.m_matrices.m_viewProjection * trf;
+			consts.m_color = m_debugPoint.m_color;
+			cmdb.setFastConstants(&consts, sizeof(consts));
+
+			ShaderProgramResourceVariantInitInfo variantInitInfo(m_dbgProg);
+			variantInitInfo.addMutation("OBJECT_TYPE", 0);
+			variantInitInfo.requestTechniqueAndTypes(ShaderTypeBit::kVertex | ShaderTypeBit::kPixel, "Gizmos");
+			const ShaderProgramResourceVariant* variant;
+			m_dbgProg->getOrCreateVariant(variantInitInfo, variant);
+			cmdb.bindShaderProgram(&variant->getProgram());
+
+			cmdb.setVertexAttribute(VertexAttributeSemantic::kPosition, 0, Format::kR32G32B32_Sfloat, 0);
+			cmdb.bindVertexBuffer(0, BufferView(m_debugPoint.m_positionsBuff.get()), sizeof(Vec3));
+
+			if(!m_debugPoint.m_enableDepthTest)
+			{
+				cmdb.setDepthCompareOperation(CompareOperation::kAlways);
+			}
+
+			cmdb.draw(PrimitiveTopology::kTriangles, sizeof(kCubePositions) / sizeof(F32));
+
+			if(!m_debugPoint.m_enableDepthTest)
+			{
+				cmdb.setDepthCompareOperation(CompareOperation::kLess);
+			}
+		}
+
 		if(m_gizmos.m_enabled)
 		{
 			cmdb.setDepthCompareOperation(CompareOperation::kAlways);

+ 28 - 4
AnKi/Renderer/Dbg.h

@@ -92,12 +92,22 @@ public:
 		return m_runCtx.m_objPickingRes;
 	}
 
-	void setGizmosTransform(const Transform& trf, Bool enableGizmos)
+	void enableGizmos(const Transform& trf, Bool enableGizmos)
 	{
 		m_gizmos.m_trf = Mat3x4(trf.getOrigin().xyz(), trf.getRotation().getRotationPart());
 		m_gizmos.m_enabled = enableGizmos;
 	}
 
+	// Draw a debug solid cube in world space
+	void enableDebugCube(Vec3 pos, F32 size, Vec4 color, Bool enable, Bool enableDepthTesting = true)
+	{
+		ANKI_ASSERT(size > kEpsilonf);
+		m_debugPoint.m_position = (enable) ? pos : Vec3(kMaxF32);
+		m_debugPoint.m_size = size;
+		m_debugPoint.m_color = color;
+		m_debugPoint.m_enableDepthTest = enableDepthTesting;
+	}
+
 private:
 	RenderTargetDesc m_rtDescr;
 	RenderTargetDesc m_objectPickingRtDescr;
@@ -109,13 +119,17 @@ private:
 	ImageResourcePtr m_decalImage;
 	ImageResourcePtr m_reflectionImage;
 
-	BufferPtr m_cubeVertsBuffer;
-	BufferPtr m_cubeIndicesBuffer;
-
 	ShaderProgramResourcePtr m_dbgProg;
 
 	MultiframeReadbackToken m_readback;
 
+	class
+	{
+	public:
+		BufferPtr m_positionsBuff;
+		BufferPtr m_indexBuff;
+	} m_boxLines;
+
 	class
 	{
 	public:
@@ -130,6 +144,16 @@ private:
 		Bool m_enabled = false;
 	} m_gizmos;
 
+	class
+	{
+	public:
+		Vec3 m_position = Vec3(kMaxF32);
+		BufferPtr m_positionsBuff;
+		F32 m_size = 1.0f;
+		Vec4 m_color = Vec4(1.0f);
+		Bool m_enableDepthTest = false;
+	} m_debugPoint;
+
 	DbgOption m_options = DbgOption::kDepthTest;
 
 	class

+ 1 - 0
AnKi/Ui/Common.h

@@ -8,6 +8,7 @@
 // Include ImGUI
 #include <AnKi/Ui/ImGuiConfig.h>
 #include <ImGui/imgui.h>
+#include <ImGui/imgui_internal.h>
 
 #include <AnKi/Util/Ptr.h>
 #include <AnKi/Gr/Texture.h>

+ 1 - 1
Tools/Editor/EditorMain.cpp

@@ -111,7 +111,7 @@ public:
 			mousePosOn1stClick = in.getMousePositionNdc();
 		}
 
-		if(in.getMouseButton(MouseButton::kRight) > 0 && !m_editorUiNode->m_editorUi.m_mouseHoveredOverAnyWindow)
+		if(in.getMouseButton(MouseButton::kRight) > 0 && !m_editorUiNode->m_editorUi.m_mouseOverAnyWindow)
 		{
 			in.hideCursor(true);