diff --git a/assets/shaders/outline.fs b/assets/shaders/outline.fs index 744406a..2190e5d 100644 --- a/assets/shaders/outline.fs +++ b/assets/shaders/outline.fs @@ -5,6 +5,7 @@ uniform vec3 scale; in vec3 vPos; out vec4 fColor; +uniform vec3 color; void main() { // float thickness = 0.2; @@ -20,5 +21,6 @@ void main() { // else // fColor = vec4(0); - fColor = vec4(0.204, 0.584, 0.922, 1); + // fColor = vec4(0.204, 0.584, 0.922, 1); + fColor = vec4(color, 1); } \ No newline at end of file diff --git a/core/src/partassembly.cpp b/core/src/partassembly.cpp new file mode 100644 index 0000000..484879f --- /dev/null +++ b/core/src/partassembly.cpp @@ -0,0 +1,86 @@ +#include "partassembly.h" +#include "common.h" +#include "datatypes/cframe.h" +#include "datatypes/vector.h" +#include "math_helper.h" +#include "objects/base/instance.h" +#include "objects/part.h" +#include +#include +#include + +PartAssembly::PartAssembly(std::vector> parts, bool worldMode) : parts(parts) { + if (parts.size() == 0) return; + if (parts.size() == 1 && !worldMode) { + _assemblyOrigin = parts[0]->cframe; + _bounds = parts[0]->size; + return; + } + + glm::vec3 min = parts[0]->position(), max = parts[0]->position(); + + for (auto part : parts) { + Vector3 aabbSize = part->GetAABB(); + expandAABB(min, max, part->position() - aabbSize / 2.f); + expandAABB(min, max, part->position() + aabbSize / 2.f); + } + + glm::vec3 pos, size; + getAABBCoords(pos, size, min, max); + + _assemblyOrigin = CFrame() + pos; + _bounds = size; +} + +PartAssembly PartAssembly::FromSelection(std::vector> newSelection) { + std::vector> selection; + + for (std::weak_ptr obj : newSelection) { + if (obj.expired() || !obj.lock()->IsA()) continue; + + selection.push_back(obj.lock()->CastTo().expect()); + } + + return PartAssembly(selection, editorToolHandles.worldMode); +} + +void PartAssembly::SetOrigin(CFrame newOrigin) { + for (auto part : parts) { + part->cframe = newOrigin * (_assemblyOrigin.Inverse() * part->cframe); + part->UpdateProperty("CFrame"); + } + + _assemblyOrigin = newOrigin; +} + +void PartAssembly::TransformBy(CFrame transform) { + for (auto part : parts) { + part->cframe = transform * part->cframe; + part->UpdateProperty("CFrame"); + } + + _assemblyOrigin = transform * _assemblyOrigin; +} + +void PartAssembly::Scale(Vector3 newSize, bool scaleUp) { + if (parts.size() == 1) { + parts[0]->size = newSize; + parts[0]->UpdateProperty("Size"); + _bounds = newSize; + return; + } + + float sx = newSize.X() / _bounds.X(), sy = newSize.Y() / _bounds.Y(), sz = newSize.Z() / _bounds.Z(); + float factor = scaleUp ? glm::max(sx, sy, sz) : glm::min(sx, sy, sz); + + for (auto part : parts) { + Vector3 localOff = _assemblyOrigin.Inverse() * part->cframe.Position(); + localOff = localOff * factor; + part->cframe = part->cframe.Rotation() + _assemblyOrigin * localOff; + part->UpdateProperty("CFrame"); + part->size *= factor; + part->UpdateProperty("Size"); + } + + _bounds = _bounds * factor; +} \ No newline at end of file diff --git a/core/src/partassembly.h b/core/src/partassembly.h new file mode 100644 index 0000000..a857312 --- /dev/null +++ b/core/src/partassembly.h @@ -0,0 +1,35 @@ +#pragma once + +#include "datatypes/cframe.h" +#include +#include + +class Part; +class Instance; + +const std::vector> getSelection(); + +class PartAssembly { + CFrame _assemblyOrigin; + Vector3 _bounds; + + std::vector> parts; +public: + PartAssembly(std::vector>, bool worldMode = false); + + static PartAssembly FromSelection(std::vector> selection = getSelection()); + + inline CFrame assemblyOrigin() { return _assemblyOrigin; }; + inline Vector3 bounds() { return _bounds; }; + + // Transforms the assembly such that newOrigin is now this assembly's new assemblyOrigin + void SetOrigin(CFrame newOrigin); + + // Rotates and translates the assembly by the transformation + void TransformBy(CFrame transform); + + // Scales the assembly to the desired size + // If multiple parts are selected, finds the greatest scale factor of each component pair, and + // scales it up by that amount + void Scale(Vector3 newSize, bool scaleUp = true); +}; \ No newline at end of file diff --git a/core/src/rendering/renderer.cpp b/core/src/rendering/renderer.cpp index 2db2d48..49a6a69 100644 --- a/core/src/rendering/renderer.cpp +++ b/core/src/rendering/renderer.cpp @@ -16,8 +16,10 @@ #include "datatypes/cframe.h" #include "datatypes/color3.h" +#include "datatypes/vector.h" #include "handles.h" #include "math_helper.h" +#include "partassembly.h" #include "rendering/torus.h" #include "shader.h" #include "mesh.h" @@ -442,10 +444,11 @@ void renderOutlines() { outlineShader->set("viewPos", camera.cameraPos); outlineShader->set("thickness", 0.4f); - // outlineShader->set("color", glm::vec3(1.f, 0.f, 0.f)); + outlineShader->set("color", glm::vec3(0.204, 0.584, 0.922)); glm::vec3 min, max; bool first = true; + int count = 0; for (auto it = gWorkspace()->GetDescendantsStart(); it != gWorkspace()->GetDescendantsEnd(); it++) { InstanceRef inst = *it; @@ -453,6 +456,7 @@ void renderOutlines() { std::shared_ptr part = std::dynamic_pointer_cast(inst); if (!part->selected) continue; + count++; if (first) min = part->position(), max = part->position(); first = false; @@ -471,7 +475,7 @@ void renderOutlines() { } // Render AABB of selected parts - if (first) return; + if (count <= 1) return; glm::vec3 outlineSize, outlinePos; outlineSize = (max - min); @@ -487,6 +491,41 @@ void renderOutlines() { glDrawArrays(GL_TRIANGLES, 0, OUTLINE_MESH->vertexCount); } +void renderSelectionAssembly() { + glDepthMask(GL_TRUE); + glEnable(GL_CULL_FACE); + glCullFace(GL_BACK); + glFrontFace(GL_CCW); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + PartAssembly selectionAssembly = PartAssembly::FromSelection(); + + // Use shader + outlineShader->use(); + + // view/projection transformations + glm::mat4 projection = glm::perspective(glm::radians(45.f), (float)viewportWidth / (float)viewportHeight, 0.1f, 1000.0f); + glm::mat4 view = camera.getLookAt(); + outlineShader->set("projection", projection); + outlineShader->set("view", view); + + // Pass in the camera position + outlineShader->set("viewPos", camera.cameraPos); + outlineShader->set("thickness", 0.4f); + + outlineShader->set("color", glm::vec3(1.f, 0.f, 0.f)); + + glm::mat4 model = selectionAssembly.assemblyOrigin(); + model = glm::scale(model, (glm::vec3)selectionAssembly.bounds() + glm::vec3(0.1)); + outlineShader->set("model", model); + outlineShader->set("scale", (glm::vec3)selectionAssembly.bounds() + glm::vec3(0.05)); + outlineShader->set("thickness", 0.2f); + + OUTLINE_MESH->bind(); + glDrawArrays(GL_TRIANGLES, 0, OUTLINE_MESH->vertexCount); +} + void renderRotationArcs() { if (!editorToolHandles.active || editorToolHandles.handlesType != HandlesType::RotateHandles) return; @@ -599,6 +638,7 @@ void render(GLFWwindow* window) { renderParts(); renderSurfaceExtras(); renderOutlines(); + // renderSelectionAssembly(); renderRotationArcs(); if (wireframeRendering) renderWireframe(); diff --git a/editor/mainglwidget.cpp b/editor/mainglwidget.cpp index 9e99781..cf662e6 100755 --- a/editor/mainglwidget.cpp +++ b/editor/mainglwidget.cpp @@ -5,12 +5,14 @@ #include #include #include "mainglwidget.h" +#include "datatypes/vector.h" #include "handles.h" #include "logger.h" #include "mainwindow.h" #include "common.h" #include "math_helper.h" #include "objects/base/instance.h" +#include "partassembly.h" #include "physics/util.h" #include "rendering/renderer.h" #include "rendering/shader.h" @@ -171,6 +173,7 @@ inline glm::vec3 vec3fy(glm::vec4 vec) { } // Taken from Godot's implementation of moving handles (godot/editor/plugins/gizmos/gizmo_3d_helper.cpp) +Vector3 dragStartHandleOffset; void MainGLWidget::handleLinearTransform(QMouseEvent* evt) { if (!isMouseDragging || !draggingHandle|| !editorToolHandles.active) return; @@ -183,7 +186,9 @@ void MainGLWidget::handleLinearTransform(QMouseEvent* evt) { glm::vec3 pointDir = camera.getScreenDirection(glm::vec2(position.x(), position.y()), glm::vec2(width(), height())); pointDir = glm::normalize(pointDir); - CFrame handleCFrame = getHandleCFrame(draggingHandle.value()); + // We use lastDragStartPos instead to consider the mouse's actual position, rather than the center + // of the handle. That way, transformations are "smoother" and do not jump the first movement + CFrame handleCFrame = getHandleCFrame(draggingHandle.value()) + dragStartHandleOffset; // Current frame. Identity frame if worldMode == true, selected object's frame if worldMode == false CFrame frame = editorToolHandles.worldMode ? CFrame::IDENTITY + part->position() : part->cframe.Rotation(); @@ -200,60 +205,64 @@ void MainGLWidget::handleLinearTransform(QMouseEvent* evt) { glm::vec3 handlePoint, rb; get_closest_points_between_segments(axisSegment0, axisSegment1, mouseSegment0, mouseSegment1, handlePoint, rb); - // Find new part position - glm::vec3 centerPoint = partCFrameFromHandlePos(draggingHandle.value(), handlePoint).Position(); + // We transform the handlePoint to the handle's cframe, and get it's length (Z) + float diff = (handleCFrame.Inverse() * handlePoint).Z(); + // Vector3 absDiff = ((Vector3)handlePoint - handleCFrame.Position()); // Commented out because it is functionally identical to the below + Vector3 absDiff = handleCFrame.Rotation() * Vector3(0, 0, diff); - // Apply snapping in the current frame - glm::vec3 diff = centerPoint - (glm::vec3)part->position(); - if (snappingFactor()) diff = frame.Rotation() * (glm::round(glm::vec3(frame.Inverse().Rotation() * diff) / snappingFactor()) * snappingFactor()); - - Vector3 oldSize = part->size; - - switch (mainWindow()->selectedTool) { - case TOOL_MOVE: { - // Add difference - part->cframe = part->cframe + diff; - } break; - - case TOOL_SCALE: { - // Find local difference - glm::vec3 localDiff = frame.Inverse() * diff; - // Find outwarwd difference - localDiff = localDiff * glm::sign(draggingHandle->normal); - - // Special case: minimum size to size mod snapping factor - if (snappingFactor() > 0 && glm::all(glm::lessThan((part->size + localDiff) * glm::abs(draggingHandle->normal), glm::vec3(0.01f)))) { - // I tried something fancy here, but honestly I'm not smart enough. Return; - // glm::vec3 finalSize = part->size + localDiff; - // finalSize = glm::mod(finalSize * glm::abs(draggingHandle->normal), snappingFactor()) + finalSize * (glm::vec3(1) - glm::abs(draggingHandle->normal)); - // localDiff = finalSize - part->size; - return; - } - - // Minimum size of 0.01f - localDiff = glm::max(part->size + localDiff, 0.01f) - part->size; - diff = frame * (localDiff * glm::sign(draggingHandle->normal)); - - // Add local difference to size - part->size += localDiff; - - // If ctrl is not pressed, offset the part by half the size difference to keep the other bound where it was originally - if (!(evt->modifiers() & Qt::ControlModifier)) - part->cframe = part->cframe + diff * 0.5f; - } break; - - default: - Logger::error("Invalid tool was set to be handled by handleLinearTransform\n"); + // Apply snapping + if (snappingFactor() > 0) { + diff = round(diff / snappingFactor()) * snappingFactor(); + absDiff = handleCFrame.Rotation() * Vector3(0, 0, diff); } - if (snappingFactor() != 0 && mainWindow()->editSoundEffects && (oldSize != part->size) && QFile::exists("./assets/excluded/switch.wav")) - playSound("./assets/excluded/switch.wav"); + PartAssembly selectionAssembly = PartAssembly::FromSelection(); - gWorkspace()->SyncPartPhysics(part); - part->UpdateProperty("Position"); - part->UpdateProperty("Size"); - sendPropertyUpdatedSignal(part, "Position", part->position()); - sendPropertyUpdatedSignal(part, "Size", Vector3(part->size)); + if (editorToolHandles.handlesType == MoveHandles) { + selectionAssembly.TransformBy(CFrame() + absDiff); + } else if (editorToolHandles.handlesType == ScaleHandles) { + if (evt->modifiers() & Qt::AltModifier) { + // If size gets too small, don't + if (glm::any(glm::lessThan(glm::vec3(selectionAssembly.bounds() + abs(draggingHandle->normal) * diff * 2.f), glm::vec3(0.001f)))) + return; + + selectionAssembly.Scale(selectionAssembly.bounds() + abs(draggingHandle->normal) * diff * 2.f, diff > 0); + } else { + // If size gets too small, don't + if (glm::any(glm::lessThan(glm::vec3(selectionAssembly.bounds() + abs(draggingHandle->normal) * diff), glm::vec3(0.001f)))) + return; + + selectionAssembly.TransformBy(CFrame() + absDiff * 0.5f); + selectionAssembly.Scale(selectionAssembly.bounds() + abs(draggingHandle->normal) * diff, diff > 0); + } + } +} + +void MainGLWidget::startLinearTransform(QMouseEvent* evt) { + if (!editorToolHandles.active) return; + + QPoint position = evt->pos(); + + // This was actually quite a difficult problem to solve, managing to get the handle to go underneath the cursor + + glm::vec3 pointDir = camera.getScreenDirection(glm::vec2(position.x(), position.y()), glm::vec2(width(), height())); + pointDir = glm::normalize(pointDir); + + CFrame handleCFrame = getHandleCFrame(draggingHandle.value()); + + // Segment from axis stretching -4096 to +4096 rel to handle's position + glm::vec3 axisSegment0 = handleCFrame.Position() + (-handleCFrame.LookVector() * 4096.0f); + glm::vec3 axisSegment1 = handleCFrame.Position() + (-handleCFrame.LookVector() * -4096.0f); + + // Segment from camera stretching 4096 forward + glm::vec3 mouseSegment0 = camera.cameraPos; + glm::vec3 mouseSegment1 = camera.cameraPos + pointDir * 4096.0f; + + // Closest point on the axis segment between the two segments + glm::vec3 handlePoint, rb; + get_closest_points_between_segments(axisSegment0, axisSegment1, mouseSegment0, mouseSegment1, handlePoint, rb); + + dragStartHandleOffset = (Vector3)handlePoint - handleCFrame.Position(); } // Also implemented based on Godot: [c7ea8614](godot/editor/plugins/canvas_item_editor_plugin.cpp#L1490) @@ -369,6 +378,7 @@ void MainGLWidget::mousePressEvent(QMouseEvent* evt) { initialFrame = getHandleAdornee()->cframe; isMouseDragging = true; draggingHandle = handle; + startLinearTransform(evt); return; } @@ -501,4 +511,4 @@ float MainGLWidget::snappingFactor() { case GridSnappingMode::SNAP_OFF: return 0; } return 0; -} +} \ No newline at end of file diff --git a/editor/mainglwidget.h b/editor/mainglwidget.h index 9fa2241..9c7ac0e 100644 --- a/editor/mainglwidget.h +++ b/editor/mainglwidget.h @@ -26,6 +26,7 @@ protected: void handleLinearTransform(QMouseEvent* evt); void handleRotationalTransform(QMouseEvent* evt); void handleCursorChange(QMouseEvent* evt); + void startLinearTransform(QMouseEvent* evt); std::optional raycastHandle(glm::vec3 pointDir); void wheelEvent(QWheelEvent* evt) override;