diff --git a/.gitignore b/.gitignore index 8017f59..81f5c0f 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,7 @@ CMakeLists.txt.user* # Clangd /compile_commands.json -/.cache \ No newline at end of file +/.cache + +# Gdb +.gdb_history \ No newline at end of file diff --git a/core/src/datatypes/cframe.cpp b/core/src/datatypes/cframe.cpp index 9ea6199..791f308 100644 --- a/core/src/datatypes/cframe.cpp +++ b/core/src/datatypes/cframe.cpp @@ -33,7 +33,7 @@ Data::CFrame::CFrame(const rp::Transform& transform) : Data::CFrame::CFrame(rpTo } glm::mat3 lookAt(Data::Vector3 position, Data::Vector3 lookAt, Data::Vector3 up) { - // https://github.com/sgorsten/glm/issues/29#issuecomment-743989030 + // https://github.com/sgorsten/linalg/issues/29#issuecomment-743989030 Data::Vector3 f = (lookAt - position).Unit(); // Forward/Look Data::Vector3 u = up.Unit(); // Up Data::Vector3 s = f.Cross(u).Unit(); // Right diff --git a/core/src/math_helper.cpp b/core/src/math_helper.cpp new file mode 100644 index 0000000..3d8a4b3 --- /dev/null +++ b/core/src/math_helper.cpp @@ -0,0 +1,103 @@ +#include "math_helper.h" + +#define CMP_EPSILON 0.00001 + +// After a long time researching, I was able to use and adapt Godot's implementation of movable handles (godot/editor/plugins/gizmos/gizmo_3d_helper.cpp) +// All thanks goes to them and David Eberly for his algorithm. + +void get_closest_points_between_segments(const glm::vec3 &p_p0, const glm::vec3 &p_p1, const glm::vec3 &p_q0, const glm::vec3 &p_q1, glm::vec3 &r_ps, glm::vec3 &r_qt) { + // Based on David Eberly's Computation of Distance Between Line Segments algorithm. + + glm::vec3 p = p_p1 - p_p0; + glm::vec3 q = p_q1 - p_q0; + glm::vec3 r = p_p0 - p_q0; + + float a = glm::dot(p, p); + float b = glm::dot(p, q); + float c = glm::dot(q, q); + float d = glm::dot(p, r); + float e = glm::dot(q, r); + + float s = 0.0f; + float t = 0.0f; + + float det = a * c - b * b; + if (det > CMP_EPSILON) { + // Non-parallel segments + float bte = b * e; + float ctd = c * d; + + if (bte <= ctd) { + // s <= 0.0f + if (e <= 0.0f) { + // t <= 0.0f + s = (-d >= a ? 1 : (-d > 0.0f ? -d / a : 0.0f)); + t = 0.0f; + } else if (e < c) { + // 0.0f < t < 1 + s = 0.0f; + t = e / c; + } else { + // t >= 1 + s = (b - d >= a ? 1 : (b - d > 0.0f ? (b - d) / a : 0.0f)); + t = 1; + } + } else { + // s > 0.0f + s = bte - ctd; + if (s >= det) { + // s >= 1 + if (b + e <= 0.0f) { + // t <= 0.0f + s = (-d <= 0.0f ? 0.0f : (-d < a ? -d / a : 1)); + t = 0.0f; + } else if (b + e < c) { + // 0.0f < t < 1 + s = 1; + t = (b + e) / c; + } else { + // t >= 1 + s = (b - d <= 0.0f ? 0.0f : (b - d < a ? (b - d) / a : 1)); + t = 1; + } + } else { + // 0.0f < s < 1 + float ate = a * e; + float btd = b * d; + + if (ate <= btd) { + // t <= 0.0f + s = (-d <= 0.0f ? 0.0f : (-d >= a ? 1 : -d / a)); + t = 0.0f; + } else { + // t > 0.0f + t = ate - btd; + if (t >= det) { + // t >= 1 + s = (b - d <= 0.0f ? 0.0f : (b - d >= a ? 1 : (b - d) / a)); + t = 1; + } else { + // 0.0f < t < 1 + s /= det; + t /= det; + } + } + } + } + } else { + // Parallel segments + if (e <= 0.0f) { + s = (-d <= 0.0f ? 0.0f : (-d >= a ? 1 : -d / a)); + t = 0.0f; + } else if (e >= c) { + s = (b - d <= 0.0f ? 0.0f : (b - d >= a ? 1 : (b - d) / a)); + t = 1; + } else { + s = 0.0f; + t = e / c; + } + } + + r_ps = (1 - s) * p_p0 + s * p_p1; + r_qt = (1 - t) * p_q0 + t * p_q1; +} \ No newline at end of file diff --git a/core/src/math_helper.h b/core/src/math_helper.h new file mode 100644 index 0000000..65c316c --- /dev/null +++ b/core/src/math_helper.h @@ -0,0 +1,5 @@ +#pragma once +#include <glm/glm.hpp> + +// From godot/editor/plugins/gizmos/gizmo_3d_helper.h +void get_closest_points_between_segments(const glm::vec3 &p_p0, const glm::vec3 &p_p1, const glm::vec3 &p_q0, const glm::vec3 &p_q1, glm::vec3 &r_ps, glm::vec3 &r_qt); \ No newline at end of file diff --git a/core/src/objects/handles.cpp b/core/src/objects/handles.cpp index c1b58d6..69b571c 100644 --- a/core/src/objects/handles.cpp +++ b/core/src/objects/handles.cpp @@ -42,6 +42,15 @@ Data::CFrame Handles::GetCFrameOfHandle(HandleFace face) { return adornee->lock()->cframe + glm::vec3(glm::mat4(adornee->lock()->cframe.Rotation()) * glm::vec4((adornee->lock()->size * 0.5f * face.normal) + face.normal * 5.f, 0)); } +Data::CFrame Handles::PartCFrameFromHandlePos(HandleFace face, Data::Vector3 newPos) { + if (!adornee.has_value() || adornee->expired()) return Data::CFrame(glm::vec3(0,0,0), (Data::Vector3)glm::vec3(0,0,0)); + + // return adornee->lock()->cframe + face.normal * 5.f; + if (worldMode) + return adornee->lock()->cframe.Rotation() + newPos - (adornee->lock()->size * 0.5f * face.normal) - face.normal * 5.f; + return adornee->lock()->cframe.Rotation() + newPos - glm::vec3(glm::mat4(adornee->lock()->cframe.Rotation()) * glm::vec4((adornee->lock()->size * 0.5f * face.normal) + face.normal * 5.f, 0)); +} + std::optional<HandleFace> Handles::RaycastHandle(rp3d::Ray ray) { for (HandleFace face : HandleFace::Faces) { Data::CFrame cframe = GetCFrameOfHandle(face); diff --git a/core/src/objects/handles.h b/core/src/objects/handles.h index ac9860d..e4c7885 100644 --- a/core/src/objects/handles.h +++ b/core/src/objects/handles.h @@ -37,6 +37,7 @@ public: // World-space handles vs local-space handles bool worldMode = false; Data::CFrame GetCFrameOfHandle(HandleFace face); + Data::CFrame PartCFrameFromHandlePos(HandleFace face, Data::Vector3 newPos); std::optional<HandleFace> RaycastHandle(rp3d::Ray ray); static inline std::shared_ptr<Handles> New() { return std::make_shared<Handles>(); }; diff --git a/core/src/rendering/renderer.cpp b/core/src/rendering/renderer.cpp index cfcd800..f89cdd0 100644 --- a/core/src/rendering/renderer.cpp +++ b/core/src/rendering/renderer.cpp @@ -114,6 +114,7 @@ void renderParts() { if (inst->GetClass()->className != "Part") continue; std::shared_ptr<Part> part = std::dynamic_pointer_cast<Part>(inst); glm::mat4 model = part->cframe; + if (inst->name == "camera") model = camera.getLookAt(); model = glm::scale(model, part->size); shader->set("model", model); shader->set("material", Material { @@ -196,8 +197,8 @@ void render(GLFWwindow* window) { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); renderSkyBox(); - renderParts(); renderHandles(); + renderParts(); } void setViewport(int width, int height) { diff --git a/editor/mainglwidget.cpp b/editor/mainglwidget.cpp index 85ccd4e..0168bd5 100644 --- a/editor/mainglwidget.cpp +++ b/editor/mainglwidget.cpp @@ -3,6 +3,7 @@ #include <QMouseEvent> #include <glm/ext/matrix_projection.hpp> +#include <glm/ext/matrix_transform.hpp> #include <glm/ext/vector_float3.hpp> #include <glm/geometric.hpp> #include <glm/matrix.hpp> @@ -11,7 +12,9 @@ #include <reactphysics3d/collision/RaycastInfo.h> #include <vector> +#include "datatypes/cframe.h" #include "editorcommon.h" +#include "objects/handles.h" #include "physics/util.h" #include "qcursor.h" #include "qevent.h" @@ -25,6 +28,8 @@ #include "rendering/shader.h" #include "mainglwidget.h" +#include "../core/src/rendering/defaultmeshes.h" +#include "math_helper.h" MainGLWidget::MainGLWidget(QWidget* parent): QOpenGLWidget(parent) { setFocusPolicy(Qt::FocusPolicy::ClickFocus); @@ -55,8 +60,47 @@ glm::vec2 firstPoint; glm::vec2 secondPoint; extern std::optional<std::weak_ptr<Part>> draggingObject; +extern std::optional<HandleFace> draggingHandle; +extern Shader* shader; void MainGLWidget::paintGL() { ::render(NULL); + + if (!editorToolHandles->adornee) return; + + for (HandleFace face : HandleFace::Faces) { + // Ignore negatives (for now) + if (face.normal != glm::abs(face.normal)) continue; + + glm::vec3 axisNormal = face.normal; + // // glm::vec3 planeNormal = camera.cameraFront; + glm::vec3 planeRight = glm::cross(axisNormal, glm::normalize(camera.cameraFront)); + glm::vec3 planeNormal = glm::cross(glm::normalize(planeRight), axisNormal); + + auto a = axisNormal; + auto b = planeRight; + auto c = planeNormal; + + glm::mat3 matrix = { + axisNormal, + planeRight, + -planeNormal, + }; + + glm::mat4 model = glm::translate(glm::mat4(1.0f), (glm::vec3)editorToolHandles->adornee->lock()->position()) * glm::mat4(matrix); + model = glm::scale(model, glm::vec3(5, 5, 0.2)); + + shader->set("model", model); + shader->set("material", Material { + .diffuse = glm::vec3(0.5, 1.f, 0.5), + .specular = glm::vec3(0.5f, 0.5f, 0.5f), + .shininess = 16.0f, + }); + glm::mat3 normalMatrix = glm::mat3(glm::transpose(glm::inverse(model))); + shader->set("normalMatrix", normalMatrix); + + CUBE_MESH->bind(); + // glDrawArrays(GL_TRIANGLES, 0, 36); + } } bool isMouseRightDragging = false; @@ -90,6 +134,10 @@ void MainGLWidget::handleObjectDrag(QMouseEvent* evt) { syncPartPhysics(draggingObject->lock()); } +inline glm::vec3 vec3fy(glm::vec4 vec) { + return vec / vec.w; +} + QPoint lastPoint; void MainGLWidget::handleHandleDrag(QMouseEvent* evt) { QPoint cLastPoint = lastPoint; @@ -97,24 +145,39 @@ void MainGLWidget::handleHandleDrag(QMouseEvent* evt) { if (!isMouseDragging || !draggingHandle || !editorToolHandles->adornee) return; - // https://stackoverflow.com/a/57380616/16255372 QPoint position = evt->pos(); - glm::mat4 projection = glm::perspective(glm::radians(45.f), (float)width() / (float)height(), 0.1f, 100.0f); - glm::mat4 view = camera.getLookAt(); - glm::mat4 worldToScreen = projection * view; - glm::vec4 a = worldToScreen * glm::vec4((glm::vec3)editorToolHandles->adornee->lock()->position(), 1); - glm::vec4 b = worldToScreen * glm::vec4((glm::vec3)editorToolHandles->adornee->lock()->position() + draggingHandle->normal, 1); - glm::vec2 screenDir = b / b.w - a / a.w; - QPoint mouseDelta_ = evt->pos() - cLastPoint; - glm::vec2 mouseDelta(mouseDelta_.x(), mouseDelta_.y() * -1.f); - float changeBy = glm::dot(mouseDelta, screenDir); + // 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); + + Data::CFrame handleCFrame = editorToolHandles->GetCFrameOfHandle(draggingHandle.value()); + + // Segment from axis stretching -4096 to +4096 rel to handle's position + glm::vec3 axisSegment0 = handleCFrame.Position() + glm::abs(draggingHandle->normal) * 4096.0f; + glm::vec3 axisSegment1 = handleCFrame.Position() + glm::abs(draggingHandle->normal) * -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); if (selectedTool == SelectedTool::MOVE) - editorToolHandles->adornee->lock()->cframe = editorToolHandles->adornee->lock()->cframe + draggingHandle->normal * changeBy; + editorToolHandles->adornee->lock()->cframe = editorToolHandles->PartCFrameFromHandlePos(draggingHandle.value(), handlePoint); else if (selectedTool == SelectedTool::SCALE) { - if (!(evt->modifiers() & Qt::ControlModifier)) editorToolHandles->adornee->lock()->cframe = editorToolHandles->adornee->lock()->cframe + draggingHandle->normal * changeBy * 0.5f; - editorToolHandles->adornee->lock()->size += glm::abs(draggingHandle->normal) * changeBy; + glm::vec3 handlePos = editorToolHandles->PartCFrameFromHandlePos(draggingHandle.value(), handlePoint).Position(); + + // Find change in handles, and negate difference in sign between axes + glm::vec3 diff = handlePos - glm::vec3(editorToolHandles->adornee->lock()->position()); + editorToolHandles->adornee->lock()->size += diff * glm::sign(draggingHandle->normal); + + // If alt is not pressed, also reposition the part such that only the dragged side gets lengthened + if (!(evt->modifiers() & Qt::ControlModifier)) + editorToolHandles->adornee->lock()->cframe = editorToolHandles->adornee->lock()->cframe + (diff / 2.0f); } syncPartPhysics(std::dynamic_pointer_cast<Part>(editorToolHandles->adornee->lock()));