Leved Design Curve Tool
Key Achievements
Speeding up level design and set dressing process greatly
Tool integration in custom C++ scene editor
Deeper knowledge about Bezier curves
Deeper knowledge about Dear ImGui and ImGuizmos
Introduction & Motivation
After my team finished a Diablo 3 inspired action rpg, I sat down and looked at how many set dressing assets that had been placed in each scene. For each scene about 99 percent of all models placed were set dressing. We’re talking five to ten thousand objects, each one placed, offseted and rotated by hand. No wonder my level design and art colleagues felt stressed.
For our upcoming project I decided to make a tool that could speed things up for them, while letting me explore our custom C++ engines editor and a 3-dimensional Bezier curve. This tool took about 1-2 weeks to create.
Placing Objects Along the Curve
Since the game project that inspired me to make this tool had a world full of trees, rocks and other foliage, I knew that my tool should not only be able to place objects evenly along the curve. It should also be able to randomly offset and rotate the objects, to easily attain a natural look. It was also important that you could place many different objects at once, such as a tree followed by a flower, then a bush and so forth. Preferably in random order if you’d like.
The new game we were making had it’s setting in a dirty cityscape. I therefore thought that it would benefit the tool to also be able to rotate objects along the curve so that they looked symmetric and industrially placed.
Code below handles the placement of multiple asset types at once
Show Code
▼void Forge::CurveTool::PlaceObjectsAlongCurve(std::vector<Tga::StringId>& objectDefinitionNames) { Tga::SceneDocument* sceneDocument = Tga::Editor::GetEditor()->GetActiveSceneDocument(); Tga::Scene* scene = nullptr; if (sceneDocument) { scene = sceneDocument->AccessScene(); } if (scene) { const int typeAmount = static_cast<int>(objectDefinitionNames.size()); int nameIndex = -1; for (int indexAlongCurve = 0; indexAlongCurve < myNumberOfObjectsToPlace; ++indexAlongCurve) { const float percentage = static_cast<float>(indexAlongCurve) / static_cast<float>(myNumberOfObjectsToPlace - 1); const Tga::Vector3f point = myCurve.GetPointOnCurve(percentage); auto object = std::make_shared<Tga::SceneObject>(); if (myPlaceDifferentTypesInRandomOrder) { nameIndex = rand() % typeAmount; } else if (!myPlaceDifferentTypesInRandomOrder) { ++nameIndex; if (nameIndex >= typeAmount) { nameIndex = 0; } } NameObject(scene, object, objectDefinitionNames[nameIndex]); TransformObject(object, point); AddObjectToScene(scene, object, sceneDocument); } } }
Code below shows the randomization transform function
Show Code
▼void Forge::CurveTool::TransformObject(std::shared_ptr<Tga::SceneObject> object, const Tga::Vector3f& point) { object->GetTRS().translation = point; RandomizeTRS(object->GetTRS()); } void Forge::CurveTool::RandomizeTRS(Tga::TRS& aTrs) { if (myHasRandomHorizontalOffset) { int degrees = rand() % 360; float radians = (FMath::Pi / 180) * degrees; float x = cosf(radians); float z = sinf(radians); Tga::Vector3f direction = { x, 0.f, z }; aTrs.translation += direction * myHorizontalOffsetLength; } if (myHasRandomVerticalOffset) { int degrees = rand() % 360; float radians = (FMath::Pi / 180) * degrees; float dir = cosf(radians); Tga::Vector3f direction = { 0.f, dir, 0.f }; aTrs.translation += direction * myVerticalOffsetLength; } if (myHasRandomRotation) { const Tga::Vector3<int> randRot = { rand() % (myRandomXRotation + 1), rand() % (myRandomYRotation + 1), rand() % (myRandomZRotation + 1) }; aTrs.rotation = { static_cast<float>(randRot.x), static_cast<float>(randRot.y), static_cast<float>(randRot.z) }; } }
Code below handles placement of axially aligned objects
Show Code
▼void Forge::CurveTool::AlignObjectsAlongCurve(Tga::StringId& objectDefinitionName) { Tga::SceneDocument* sceneDocument = Tga::Editor::GetEditor()->GetActiveSceneDocument(); Tga::Scene* scene = nullptr; if (sceneDocument) { scene = sceneDocument->AccessScene(); } if (scene) { for (int indexAlongCurve = 0; indexAlongCurve < myNumberOfObjectsToPlace; ++indexAlongCurve) { const float currentPercentage = static_cast<float>(indexAlongCurve) / static_cast<float>(myNumberOfObjectsToPlace - 1); const Tga::Vector3f point = myCurve.GetPointOnCurve(currentPercentage); auto object = std::make_shared<Tga::SceneObject>(); NameObject(scene, object, objectDefinitionName); { if (indexAlongCurve < myNumberOfObjectsToPlace - 1) { const float nextPercentage = static_cast<float>(indexAlongCurve + 1) / static_cast<float>(myNumberOfObjectsToPlace - 1); object->SetTransform(GetAlignedTransform(nextPercentage, point)); } else { const float priorPercentage = static_cast<float>(indexAlongCurve - 1) / static_cast<float>( myNumberOfObjectsToPlace - 1); Tga::Vector3f directionOfLast = (myCurve.GetPointOnCurve(currentPercentage) - myCurve.GetPointOnCurve(priorPercentage)). GetNormalized(); object->SetTransform(GetAlignedTransformFromLast(directionOfLast, point)); } } AddObjectToScene(scene, object, sceneDocument); } } }
Code below show one of the alignment functions
Show Code
▼Tga::Matrix4x4f Forge::CurveTool::GetAlignedTransform(float nextPercentage, const Tga::Vector3f& posOfCurrentObject) { Tga::Matrix4x4f alignedTransform; alignedTransform.SetPosition(posOfCurrentObject); //this is the un-normalized vector between the current object being placed, and the next one up. const Tga::Vector3f forward = (myCurve.GetPointOnCurve(nextPercentage) - posOfCurrentObject).GetNormalized(); Tga::Vector3f worldUp = { 0.f, 1.f, 0.f }; if (fabsf(forward.Dot(worldUp)) > 0.999f) { worldUp = { 1.f, 0.f, 0.f }; // fallback if near parallel } Tga::Vector3f right = worldUp.Cross(forward).GetNormalized(); Tga::Vector3f up = forward.Cross(right); // already normalized switch (myAlignment) { case Alignment::Right: { alignedTransform.SetRight(forward * -1.f); alignedTransform.SetUp(up); alignedTransform.SetForward(right); break; } case Alignment::Up: { alignedTransform.SetRight(right); alignedTransform.SetUp(forward * -1.f); alignedTransform.SetForward(up); break; } case Alignment::Forward: { alignedTransform.SetRight(right); alignedTransform.SetUp(up); alignedTransform.SetForward(forward); break; } default: { break; } } return alignedTransform; }
Code below shows the UI for the placing-of-assets window
Show Code
▼void Forge::CurveTool::PlacementUI() { static Tga::StringId objectDefinitionName0 = Tga::StringRegistry::RegisterOrGetString("<Object type ...>"); static const Tga::StringId resetName = Tga::StringRegistry::RegisterOrGetString("<Object type ...>"); static bool placeNow = false; placeNow = false; if (ImGui::Button("Reset object data")) { objectDefinitionName0 = resetName; myNumberOfObjectsToPlace = 0; myHasRandomRotation = false; placeNow = false; } // drag and drop object(s) to places ImGui::Text(objectDefinitionName0.GetString()); if (ImGui::BeginDragDropTarget()) { if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(DRAG_PAYLOAD_TYPE_STR[static_cast<int>(DragPayloadType::TGO)])) { const std::string_view payloadView = static_cast<const char*>(payload->Data); objectDefinitionName0 = Tga::StringRegistry::RegisterOrGetString(fs::path(payloadView).stem().string()); } ImGui::EndDragDropTarget(); } // placed multiple assets types at once ImGui::Checkbox("Place more than one type", &myPlaceObjectsOfDifferentTypes); static std::vector<Tga::StringId> extraObjectDefinitionsNames; static int numberOfTypes = 0; ImGui::SameLine(); if (ImGui::Button("Reset multi-objects")) { myPlaceObjectsOfDifferentTypes = false; extraObjectDefinitionsNames.clear(); } if (myPlaceObjectsOfDifferentTypes) { ImGui::SameLine(); ImGui::Checkbox("Place in random order", &myPlaceDifferentTypesInRandomOrder); ImGui::SameLine(); ImGui::PushItemWidth(50.f); if (ImGui::DragInt("Number of types", &numberOfTypes, 1, 2, 10)) { extraObjectDefinitionsNames.clear(); extraObjectDefinitionsNames.resize(numberOfTypes, resetName); } ImGui::PopItemWidth(); if (!extraObjectDefinitionsNames.empty()) { ImGui::Text(extraObjectDefinitionsNames[0].GetString()); if (ImGui::BeginDragDropTarget()) { if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(DRAG_PAYLOAD_TYPE_STR[static_cast<int>(DragPayloadType::TGO)])) { const std::string_view payloadView = static_cast<const char*>(payload->Data); objectDefinitionName0 = Tga::StringRegistry::RegisterOrGetString(fs::path(payloadView).stem().string()); extraObjectDefinitionsNames[0] = objectDefinitionName0; } ImGui::EndDragDropTarget(); } } for (int defIndex = 1; defIndex < extraObjectDefinitionsNames.size(); ++defIndex) { ImGui::Text(extraObjectDefinitionsNames[defIndex].GetString()); if (ImGui::BeginDragDropTarget()) { if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(DRAG_PAYLOAD_TYPE_STR[static_cast<int>(DragPayloadType::TGO)])) { const std::string_view payloadView = static_cast<const char*>(payload->Data); extraObjectDefinitionsNames[defIndex] = Tga::StringRegistry::RegisterOrGetString(fs::path(payloadView).stem().string()); } ImGui::EndDragDropTarget(); } } } ImGui::PushItemWidth(50.f); ImGui::DragInt("Number of objects along curve", &myNumberOfObjectsToPlace, 1, 0, 100); ImGui::PopItemWidth(); auto setRandomPreviewOffsets = [this]() { myRandomOffsetPreviews.clear(); myRandomOffsetPreviews.resize(myNumberOfObjectsToPlace); for (Tga::TRS& trs : myRandomOffsetPreviews) { RandomizeTRS(trs); } }; // randomize the rotation and translation of placed assets, for a more organic feel. if (ImGui::Checkbox("Place with random rotation", &myHasRandomRotation)) { setRandomPreviewOffsets(); } if (myHasRandomRotation) { ImGui::PushItemWidth(50.f); ImGui::SameLine(); if (ImGui::DragInt("X", &myRandomXRotation, 1, 0, 359)) { setRandomPreviewOffsets(); } ImGui::SameLine(); if (ImGui::DragInt("Y", &myRandomYRotation, 1, 0, 359)) { setRandomPreviewOffsets(); } ImGui::SameLine(); if (ImGui::DragInt("Z", &myRandomZRotation, 1, 0, 359)) { setRandomPreviewOffsets(); } ImGui::PopItemWidth(); } if (ImGui::Checkbox("Random Horizontal Offset", &myHasRandomHorizontalOffset)) { setRandomPreviewOffsets(); } if (myHasRandomHorizontalOffset) { ImGui::PushItemWidth(50.f); ImGui::SameLine(); if (ImGui::DragFloat("Horizontal Offset length", &myHorizontalOffsetLength, 1.f, 0, 100000.f, "%1.f")) { setRandomPreviewOffsets(); } ImGui::PopItemWidth(); } if (ImGui::Checkbox("Random Vertical Offset", &myHasRandomVerticalOffset)) { setRandomPreviewOffsets(); } if (myHasRandomVerticalOffset) { ImGui::PushItemWidth(50.f); ImGui::SameLine(); if (ImGui::DragFloat("Vertical Offset length", &myVerticalOffsetLength, 1.f, 0, 100000.f, "%1.f")) { setRandomPreviewOffsets(); } ImGui::PopItemWidth(); } // signal to user if it is possible to place objects or not if (objectDefinitionName0 == resetName || myNumberOfObjectsToPlace < 1) { ImGui::PushStyleColor(ImGuiCol_Button, static_cast<ImVec4>(ImColor::HSV(0.f / 7.0f, 0.6f, 0.6f))); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, static_cast<ImVec4>(ImColor::HSV(0.f / 7.0f, 0.7f, 0.7f))); ImGui::PushStyleColor(ImGuiCol_ButtonActive, static_cast<ImVec4>(ImColor::HSV(0.f / 7.0f, 0.8f, 0.8f))); ImGui::Button("Not ready to place"); ImGui::PopStyleColor(3); } else { ImGui::PushStyleColor(ImGuiCol_Button, static_cast<ImVec4>(ImColor::HSV(3.f / 7.0f, 0.6f, 0.6f))); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, static_cast<ImVec4>(ImColor::HSV(3.f / 7.0f, 0.7f, 0.7f))); ImGui::PushStyleColor(ImGuiCol_ButtonActive, static_cast<ImVec4>(ImColor::HSV(3.f / 7.0f, 0.8f, 0.8f))); if (ImGui::Button(" Ready to place ")) { placeNow = true; } ImGui::PopStyleColor(3); } ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Button, static_cast<ImVec4>(ImColor::HSV(1.f / 7.0f, 0.6f, 0.6f))); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, static_cast<ImVec4>(ImColor::HSV(1.f / 7.0f, 0.7f, 0.7f))); ImGui::PushStyleColor(ImGuiCol_ButtonActive, static_cast<ImVec4>(ImColor::HSV(1.f / 7.0f, 0.8f, 0.8f))); // enable user to preview the placements. Placement preview is approximate if random setting is on. if (ImGui::Button("Preview placement")) { myWatchPlacementPreview = !myWatchPlacementPreview; if (myWatchPlacementPreview) { myWatchPlacementPreview = myNumberOfObjectsToPlace > 0; } } ImGui::PopStyleColor(3); // place objects along line, without axially aligning them if (placeNow) { placeNow = false; myIdOfRecentlyAddedObjects.clear(); if (objectDefinitionName0 != resetName && !myPlaceObjectsOfDifferentTypes) { PlaceObjectsAlongCurve(objectDefinitionName0); } else if (!extraObjectDefinitionsNames.empty() && myPlaceObjectsOfDifferentTypes) { bool allNamesOk = true; for (auto& nameOk : extraObjectDefinitionsNames) { if (nameOk == resetName) { allNamesOk = false; break; } } if (allNamesOk) { PlaceObjectsAlongCurve(extraObjectDefinitionsNames); } } } const bool hasRecentPlacement = !myIdOfRecentlyAddedObjects.empty(); if (hasRecentPlacement) { ImGui::PushStyleColor(ImGuiCol_Button, static_cast<ImVec4>(ImColor::HSV(0.f / 7.0f, 0.6f, 0.6f))); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, static_cast<ImVec4>(ImColor::HSV(0.f / 7.0f, 0.7f, 0.7f))); ImGui::PushStyleColor(ImGuiCol_ButtonActive, static_cast<ImVec4>(ImColor::HSV(0.f / 7.0f, 0.8f, 0.8f))); } else { ImGui::PushStyleColor(ImGuiCol_Button, static_cast<ImVec4>(ImColor::HSV(6.f / 7.0f, 0.3f, 0.3f))); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, static_cast<ImVec4>(ImColor::HSV(6.f / 7.0f, 0.6f, 0.6f))); ImGui::PushStyleColor(ImGuiCol_ButtonActive, static_cast<ImVec4>(ImColor::HSV(6.f / 7.0f, 0.7f, 0.7f))); } ImGui::SameLine(); // undo option auto undo = [this, hasRecentPlacement]() { if (!hasRecentPlacement) { return; } Tga::SceneDocument* sceneDocument = Tga::Editor::GetEditor()->GetActiveSceneDocument(); Tga::Scene* scene = nullptr; if (sceneDocument) { scene = sceneDocument->AccessScene(); } if (scene) { Tga::SetActiveScene(scene); // Tga::SceneSelection::GetActiveSceneSelection()->ClearSelection(); for (uint32_t id : myIdOfRecentlyAddedObjects) { sceneDocument->AccessSceneSelection().RemoveFromSelection(id); scene->DeleteSceneObject(id); } myIdOfRecentlyAddedObjects.clear(); sceneDocument->SetSceneDirty(); Tga::SetActiveScene(nullptr); } }; if (ImGui::Button("Undo recent placement")) { undo(); } ImGui::PopStyleColor(3); static bool align = false; ImGui::PushFont(Tga::ImGuiInterface::GetIconFontLarge()); ImGui::Checkbox(ICON_LC_TRAIN_TRACK, &align); ImGui::PopFont(); ImGui::SetItemTooltip("Place objects along curve, axially aligned"); if (align) { // align objects along curve, along a selected axis of the mode. bool okToAlign = (myNumberOfObjectsToPlace > 0) && objectDefinitionName0 != resetName && myAlignment != Alignment::Invalid; if (!okToAlign) { ImGui::PushStyleColor(ImGuiCol_Button, static_cast<ImVec4>(ImColor::HSV(0.f / 7.0f, 0.6f, 0.6f))); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, static_cast<ImVec4>(ImColor::HSV(0.f / 7.0f, 0.7f, 0.7f))); ImGui::PushStyleColor(ImGuiCol_ButtonActive, static_cast<ImVec4>(ImColor::HSV(0.f / 7.0f, 0.8f, 0.8f))); } else { ImGui::PushStyleColor(ImGuiCol_Button, static_cast<ImVec4>(ImColor::HSV(3.f / 7.0f, 0.6f, 0.6f))); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, static_cast<ImVec4>(ImColor::HSV(3.f / 7.0f, 0.7f, 0.7f))); ImGui::PushStyleColor(ImGuiCol_ButtonActive, static_cast<ImVec4>(ImColor::HSV(3.f / 7.0f, 0.8f, 0.8f))); } if (ImGui::Button("Place objects with axis following curve") && okToAlign) { AlignObjectsAlongCurve(objectDefinitionName0); } ImGui::PopStyleColor(3); ImGui::SameLine(); Alignment value = myAlignment; int item_current_idx = static_cast<int>(value); ImGui::PushItemWidth(100.f); if (ImGui::BeginCombo("Select axis to align", magic_enum::enum_name(myAlignment).data())) { for (int alignmentIndex = 0; alignmentIndex < static_cast<int>(Alignment::count); ++alignmentIndex) { const bool is_selected = (item_current_idx == alignmentIndex); Alignment label = static_cast<Alignment>(alignmentIndex); auto labelName = magic_enum::enum_name(label); if (ImGui::Selectable(std::string(labelName).c_str(), is_selected)) { item_current_idx = alignmentIndex; Alignment newValue = static_cast<Alignment>(alignmentIndex); myAlignment = newValue; } } ImGui::EndCombo(); } bool okToAutoAlign = objectDefinitionName0 != resetName && myAlignment != Alignment::Invalid; if (!okToAutoAlign) { ImGui::PushStyleColor(ImGuiCol_Button, static_cast<ImVec4>(ImColor::HSV(0.f / 7.0f, 0.6f, 0.6f))); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, static_cast<ImVec4>(ImColor::HSV(0.f / 7.0f, 0.7f, 0.7f))); ImGui::PushStyleColor(ImGuiCol_ButtonActive, static_cast<ImVec4>(ImColor::HSV(0.f / 7.0f, 0.8f, 0.8f))); } else { ImGui::PushStyleColor(ImGuiCol_Button, static_cast<ImVec4>(ImColor::HSV(3.f / 7.0f, 0.6f, 0.6f))); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, static_cast<ImVec4>(ImColor::HSV(3.f / 7.0f, 0.7f, 0.7f))); ImGui::PushStyleColor(ImGuiCol_ButtonActive, static_cast<ImVec4>(ImColor::HSV(3.f / 7.0f, 0.8f, 0.8f))); } ImGui::PushFont(Tga::ImGuiInterface::GetIconFontLarge()); const char* botIcon = okToAutoAlign ? ICON_LC_BOT : ICON_LC_BOT_OFF; // align as many model that could fit along the curve. if (ImGui::Button(botIcon) && okToAutoAlign) { AutoAlignObjects(objectDefinitionName0); } ImGui::PopFont(); ImGui::SetItemTooltip("Let the tool calculate the appropriate amount of objects to place"); ImGui::PopStyleColor(3); ImGui::SameLine(); ImGui::PushItemWidth(100.f); ImGui::DragFloat("Spacing when autoaligning", &mySpacing, 1.f, -2000.f, 2000.f, "%.1f"); ImGui::PopItemWidth(); } }
The curve tool is easily accessed via the editor, with a tool tip welcoming the user.
Editing the Curve
First off I created the Bezier curve class and made sure it had all the necessary debug functions to be drawn and represented in 3D space. After that I set up a class for the actual tool and linked access to the tool via our own editor called Forge.
Then it was time to create an interface for editing the curve. For this I used the Dear Imgui library together with gizmos from ImGuizmo. I made sure to think about readability and user friendliness. Getting feedback from colleagues and industry professionals was a big part in making sure that I was not the only one who could use the tool. I added lots of visibility features, such as color coding and tooltips.
It is also possible to move the curve point by locking or snapping them to the camera using an aim.
When the tool is opened an interface for manipulating the curve appears.
The curve is edited with the mouse using gizmos, that either snap or moves smoothly. The points on the curve can be finetuned using a Vector3 XYZ-slider. The interface is color coded so that each point is easily distinguished.
If the level designer is after perfectly straight lines, that’s also possible.
Show Code
▼void Forge::CurveTool::EditPoints() { Tga::SceneDocument* scene = Tga::Editor::GetEditor()->GetActiveSceneDocument(); if (scene == nullptr) { return; } // Selection of gizmo to move static std::string pointName; pointName = ""; ImGui::Text("Point Positions"); ImGui::Separator(); ImGui::PushFont(Tga::ImGuiInterface::GetIconFontLarge()); ImGui::Text(ICON_LC_MOVE_3D); ImGui::PopFont(); ImGui::SetItemTooltip("Move base points with gizmo"); for (uint8_t indexToMove = 0; indexToMove < myCurve.AccessCurvePoints().size(); ++indexToMove) { pointName = "Gizmo to Point " + std::to_string(indexToMove + 1); const Tga::Vector4f blendedColor = FMath::Lerp(myCurve.GetColorA(), myCurve.GetColorB(), static_cast<float>(indexToMove) / static_cast<float>(myCurve.AccessCurvePoints(). size())); ImVec4 baseColor; baseColor.x = blendedColor.x; baseColor.y = blendedColor.y; baseColor.z = blendedColor.z; baseColor.w = blendedColor.w; const ImVec4 hoveredColor = baseColor + ImVec4(.1f, .1f, .1f, 0.f); ImVec4 activeColor = baseColor + ImVec4(.2f, .2f, .2f, 0.f); if (indexToMove == myPointToEditIndex) { baseColor = baseColor + ImVec4(.3f, .3f, .3f, .3f); } ImGui::PushStyleColor(ImGuiCol_Button, baseColor); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, hoveredColor); ImGui::PushStyleColor(ImGuiCol_ButtonActive, activeColor); if (ImGui::Button(pointName.c_str())) { myPointToEditIndex = indexToMove; } ImGui::PopStyleColor(3); if (indexToMove < myCurve.AccessCurvePoints().size() - 1) { ImGui::SameLine(); } } // select gizmo with numkeys if (ImGui::IsKeyPressed(ImGuiKey_1)) { myPointToEditIndex = 0; } if (ImGui::IsKeyPressed(ImGuiKey_2)) { myPointToEditIndex = 1; } if (ImGui::IsKeyPressed(ImGuiKey_3)) { myPointToEditIndex = 2; } if (ImGui::IsKeyPressed(ImGuiKey_4)) { myPointToEditIndex = 3; } ImGui::Text(" Gizmo Translation Snap: "); ImGui::SameLine(); ImGui::Checkbox("Snap Enabled", &mySnap.snapPos); ImGui::SameLine(); ImGui::PushItemWidth(100.f); ImGui::DragFloat("Snap Amount", &mySnap.pos); ImGui::PopItemWidth(); // Fine adjustment by XYZ-sliders ImGui::Separator(); int pointNumber = 1; static std::vector<ImVec4> debugColors; debugColors.clear(); for (auto& point : myCurve.AccessCurvePoints()) { pointName = "Point " + std::to_string(pointNumber); const float percentage = static_cast<float>(pointNumber) / static_cast<float>(myCurve.AccessCurvePoints().size()); const Tga::Vector4f color = FMath::Lerp(myCurve.GetColorA(), myCurve.GetColorB(), percentage); debugColors.emplace_back(color.x, color.y, color.z, color.w); ImGui::GetStyle().Colors[ImGuiCol_Text] = ImVec4(color.x, color.y, color.z, color.w); ImGui::DragFloat3(pointName.c_str(), &point.x, 1.f, -5000000.f, FLT_MAX, "%.1f"); ++pointNumber; } ImGui::GetStyle().Colors[ImGuiCol_Text] = ImVec4(1.f, 1.f, 1.f, 1.f); ImGui::Separator(); // by snapping or locking to camera if (ImGui::BeginTable("Points in relation to camera", 2)) { ImGui::TableSetupColumn("Snap to camera"); ImGui::TableSetupColumn("Lock to camera"); static bool boolLocks[4]; for (int row = 0; row < myCurve.AccessCurvePoints().size(); row++) { ImGui::TableNextRow(); for (int column = 0; column < 2; column++) { ImGui::TableSetColumnIndex(column); if (column == 1) { ImGui::GetStyle().Colors[ImGuiCol_Text] = debugColors[row]; pointName = "Snap P" + std::to_string(row + 1) + " to camera"; if (ImGui::Button(pointName.c_str())) { myCurve.AccessCurvePoints()[row] = GetPositionInFrontOfCamera(); } } else { ImGui::GetStyle().Colors[ImGuiCol_Text] = debugColors[row]; pointName = "Lock P" + std::to_string(row + 1) + " to camera"; ImGui::Checkbox(pointName.c_str(), &boolLocks[row]); if (boolLocks[row]) { myCurve.AccessCurvePoints()[row] = GetPositionInFrontOfCamera(); } } } } ImGui::EndTable(); } ImGui::GetStyle().Colors[ImGuiCol_Text] = ImVec4(1.f, 1.f, 1.f, 1.f); ImGui::Separator(); // Snap whole structure in front of camera { ImGui::PushFont(Tga::ImGuiInterface::GetIconFontLarge()); if (ImGui::Button(ICON_LC_SWITCH_CAMERA)) { MoveCurveInFrontOfCamera(); } ImGui::PopFont(); ImGui::SetItemTooltip("Move whole intact curve in front of camera"); ImGui::SameLine(); ImGui::Checkbox("Render replacement-aim", &myRenderCameraAim); ImGui::SameLine(); ImGui::SameLine(); ImGui::PushItemWidth(100.f); ImGui::DragFloat("this much ahead", &myAheadOfCamera, 5.f, 0.f, 100000.f, "%.1f"); ImGui::PopItemWidth(); } // Flatten and/or make straight line. ImGui::PushFont(Tga::ImGuiInterface::GetIconFontLarge()); if (ImGui::Button(ICON_LC_PLANE_LANDING)) { for (auto& point : myCurve.AccessCurvePoints()) { point.y = 0.f; } } ImGui::PopFont(); ImGui::SetItemTooltip("Flatten vertically, and set all Y-coordinates to zero"); ImGui::SameLine(); ImGui::PushFont(Tga::ImGuiInterface::GetIconFontLarge()); if (ImGui::Button(ICON_LC_PLANE_TAKEOFF)) { const float newY = GetPositionInFrontOfCamera().y; for (auto& point : myCurve.AccessCurvePoints()) { point.y = newY; } } ImGui::PopFont(); ImGui::SetItemTooltip("Flatten vertically, and set all Y-coordinates to camera height"); ImGui::SameLine(); ImGui::PushFont(Tga::ImGuiInterface::GetIconFontLarge()); if (ImGui::Button(ICON_LC_SLASH)) { const Tga::Vector3f firstPoint = myCurve[0]; const Tga::Vector3f endPoint = myCurve[3]; for (int i = 1; i < myCurve.AccessCurvePoints().size() - 1; ++i) { const float percentage = static_cast<float>(i) / static_cast<float>(myCurve.AccessCurvePoints().size() - 1); myCurve[static_cast<uint8_t>(i)] = FMath::Lerp(firstPoint, endPoint, percentage); } } ImGui::PopFont(); ImGui::SetItemTooltip("Make a straight line between first and last point"); if (ImGui::Button("Reset Curve To World Origin")) { myCurve[0] = { 0.f, 0.f, 0.f }; myCurve[1] = { 0.f, 500.f, 0.f }; myCurve[2] = { 500.f, 500.f, 0.f }; myCurve[3] = { 500.f, 0.f, 0.f }; } ImGui::SetItemTooltip("Resets curve point close to world origin"); }
The code below draws the curve’s edit interface and takes input.
Show Code
▼void Forge::CurveTool::DrawAndEditGizmos(const Tga::Camera& camera, Tga::Vector2i aViewportPos, Tga::Vector2i aViewportSize) { ImGuizmo::SetID(ImGui::GetID(0)); ImGui::SetItemDefaultFocus(); auto io = ImGui::GetIO(); if (!ImGuizmo::IsUsing()) { Tga::Vector3f curvePos = myCurve[myPointToEditIndex]; if (mySnap.snapPos) { curvePos = curvePos / mySnap.pos; curvePos.x = round(curvePos.x); curvePos.y = round(curvePos.y); curvePos.z = round(curvePos.z); curvePos = mySnap.pos * curvePos; } myManipulationStartPos = curvePos; } Tga::Matrix4x4f cameraToWorld = camera.GetTransform(); cameraToWorld.SetPosition(cameraToWorld.GetPosition() - myManipulationStartPos); Tga::Matrix4x4f view = Tga::Matrix4x4f::GetFastInverse(cameraToWorld); Tga::Matrix4x4f proj = camera.GetProjection(); const float left = static_cast<float>(aViewportPos.x); const float top = static_cast<float>(aViewportPos.y); const float width = static_cast<float>(aViewportSize.x); const float height = static_cast<float>(aViewportSize.y); ImGuizmo::SetRect(left, top, width, height); ImGuizmo::SetOrthographic(false); ImGuizmo::SetDrawlist(ImGui::GetWindowDrawList()); Tga::Matrix4x4f transformBefore = myManipulationCurrentTransform; Tga::Matrix4x4f transformAfter; float snap[3]; snap[0] = snap[1] = snap[2] = (!mySnap.snapPos != !io.KeyCtrl) ? mySnap.pos : 0.f; transformBefore.SetPosition(myCurve[myPointToEditIndex] - myManipulationStartPos); transformAfter = transformBefore; ImGuizmo::SetDrawlist(ImGui::GetWindowDrawList()); ImGuizmo::Manipulate(view.GetDataPtr(), proj.GetDataPtr(), ImGuizmo::TRANSLATE, ImGuizmo::WORLD, transformAfter.GetDataPtr(), nullptr, snap); myManipulationCurrentTransform = transformAfter; if (!io.KeyAlt) { if (ImGuizmo::IsOver() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { myManipulationInitialTransform = transformBefore; myIsManipulating = true; } if (ImGui::IsMouseDown(ImGuiMouseButton_Left) && myIsManipulating) { if (!ImGui::IsItemHovered()) { int a = 2; a = 3; } Tga::Vector3f moveVec = transformAfter.GetPosition() - transformBefore.GetPosition(); myCurve[myPointToEditIndex] += moveVec; } if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { Tga::Vector3f pos, scale; Tga::Quaternionf rot; transformAfter.DecomposeMatrix(pos, rot, scale); Tga::Vector3f euler = rot.GetYawPitchRoll(); euler; myIsManipulating = false; } } }
The code below shows how the gizmos are edited and drawn
Auto Align - work in progress
Finally I would like to showcase a feature that I’m still working on. The goal is to save level designers a lot of time when they want to create walls, railing and other stuff that should align perfectly along a given curve.
In the GIF below I show placing one fence piece at the time is very slow, compared to letting my tool calculate how many of that model type could fit along the line/curve. As demonstrated it works fine with straight lines, but has a harder time with curves. This, I think, has to do with the function not taking into account where the pivot point of the model is set. This will be something for me to investigate and improve upon in the future, as it is common that the pivot point differs from asset to asset.
Code showing the Auto Align function that need improving
Show Code
▼void Forge::CurveTool::AutoAlignObjects(Tga::StringId& objectDefinitionName) { auto sceneObject = std::make_shared<Tga::SceneObject>(); sceneObject->SetSceneObjectDefinitionName(objectDefinitionName); Tga::SceneObjectDefinitionManager sceneObjectDefinitionManager{}; sceneObjectDefinitionManager.Init(Tga::Settings::GameAssetRoot().string()); std::vector<Tga::ScenePropertyDefinition> sceneObjectProperties; sceneObject->CalculateCombinedPropertySet(sceneObjectDefinitionManager, sceneObjectProperties); SceneLoading::ScenePropertyExtractor props(sceneObjectProperties); const Tga::SceneModel* sceneModel = props.GetCopyOnWriteWrapperByType<Tga::SceneModel>(); const Tga::SceneVertexPaintableModel* paintableSceneModel = props.GetCopyOnWriteWrapperByType<Tga::SceneVertexPaintableModel>(); Tga::AnimatedModelInstance animatedModelInstance; Tga::ModelInstance modelInstance; // { nullptr }; Tga::BoxSphereBounds bounds; if (sceneModel != nullptr) { if (!sceneModel->isAnimated) { modelInstance = ReadModelInstance(*sceneModel); std::shared_ptr<Tga::Model> model = modelInstance.GetModel(); bounds = model->GetMeshData(0).Bounds; } else { animatedModelInstance = ReadAnimatedModelInstance(*sceneModel); std::shared_ptr<Tga::Model> model = animatedModelInstance.GetModel(); bounds = model->GetMeshData(0).Bounds; } } else if (paintableSceneModel != nullptr) { //I don't think vertex painted objects will be used on anything other than set dressing (at least, not yet) Tga::PaintableModelInstance paintable = ReadPaintableModelInstance(*paintableSceneModel); std::shared_ptr<Tga::Model> model = paintable.GetModel(); bounds = model->GetMeshData(0).Bounds; } const float curveLength = myCurve.CalculateCurveLengthFastSample(); float objectSize = 0.f; switch (myAlignment) { case Alignment::Right: { objectSize = bounds.BoxExtents.x; break; } case Alignment::Up: { objectSize = bounds.BoxExtents.y; break; } case Alignment::Forward: { objectSize = bounds.BoxExtents.z; break; } case Alignment::Invalid: { return; } case Alignment::count: { return; } default: { return; } } const int objectAmount = static_cast<int>(curveLength / (objectSize + myPreferredSpacing)); Tga::SceneDocument* sceneDocument = Tga::Editor::GetEditor()->GetActiveSceneDocument(); Tga::Scene* scene = nullptr; if (sceneDocument) { scene = sceneDocument->AccessScene(); } if (scene) { for (int indexAlongCurve = 0; indexAlongCurve < objectAmount; ++indexAlongCurve) { const float currentPercentage = static_cast<float>(indexAlongCurve) / static_cast<float>(objectAmount - 1); const Tga::Vector3f point = myCurve.GetPointOnCurve(currentPercentage); auto object = std::make_shared<Tga::SceneObject>(); NameObject(scene, object, objectDefinitionName); if (indexAlongCurve < myNumberOfObjectsToPlace - 1) { const float nextPercentage = static_cast<float>(indexAlongCurve + 1) / static_cast<float>(myNumberOfObjectsToPlace - 1); object->SetTransform(GetAlignedTransform(nextPercentage, point)); } else { const float priorPercentage = static_cast<float>(indexAlongCurve - 1) / static_cast<float>( myNumberOfObjectsToPlace - 1); Tga::Vector3f directionOfLast = (myCurve.GetPointOnCurve(currentPercentage) - myCurve.GetPointOnCurve(priorPercentage)). GetNormalized(); object->SetTransform(GetAlignedTransformFromLast(directionOfLast, point)); } AddObjectToScene(scene, object, sceneDocument); } } }