diff --git a/README.md b/README.md index b50ee1d..ee01903 100644 --- a/README.md +++ b/README.md @@ -102,8 +102,10 @@ Five tabs along the top: **Model**, **Paint**, **Layout**, **Render**, **Print** - **Layout tab**: move, turn and size the flat frames of the surface yourself; the paint follows. **Seeing it** -- **Getting round**: right drag orbits, middle drag pans, the wheel goes towards what's under the - mouse; the **arrow keys** move you along the ground (Page Up / Down: up and down, Shift: faster). +- **Getting round**, as in games, Unreal or Unity: right drag turns the camera where it stands, + and while it's held W A S D move where you look (Q / E: down, up). The **arrow keys** move any + time (Page Up / Down: down and up), the **wheel** goes forward and back, **Shift + wheel** sets + how fast it all goes. Alt + right drag orbits round what's picked; middle drag pans. - **Solid** view for modelling, **Rendered** with a real sky, sun and soft shadows (Z). Wireframe (Shift+Z), X-ray (Alt+Z). The Render tab: the sun, sky, ground, brightness, shadows, glow; F12 saves a picture of just the view. diff --git a/engine/render/renderer.cpp b/engine/render/renderer.cpp index bcc8127..18b1204 100644 --- a/engine/render/renderer.cpp +++ b/engine/render/renderer.cpp @@ -745,7 +745,7 @@ void Renderer::buildPacket(uint32_t width, uint32_t height) { p.exposure *= glm::mix(1.0f, 0.3f, std::clamp(p.frame.weather.z, 0.0f, 1.0f)); const float tanY = std::tan(glm::radians(camera_.fovY) * 0.5f); p.post.aoStrength = std::clamp(r_ssao.f(), 0.0f, 1.0f); // (the TAA fields were set with the camera above) - p.post.aoRadius = std::max(r_ssao_radius.f(), 1.0f); + p.post.aoRadius = std::max(r_ssao_radius.f(), 0.001f); // (small for close-up work: a modeller sets it to its view) p.post.bloomStrength = std::clamp(r_bloom.f(), 0.0f, 1.0f); p.post.saturation = r_saturation.f(); p.post.vignette = r_vignette.f(); diff --git a/engine/shaders/ssao.frag b/engine/shaders/ssao.frag index b177797..d34479c 100644 --- a/engine/shaders/ssao.frag +++ b/engine/shaders/ssao.frag @@ -56,7 +56,8 @@ void main() { vec3 v = viewPos(q) - p; float vv = dot(v, v); float falloff = max(0.0, 1.0 - vv / (pc.radius * pc.radius)); - occlusion += max(0.0, dot(v, n) - 0.01 * p.z) / (vv + 1.0) * falloff; + // (+ a little against dividing by nothing: its share of the reach, so any scale looks the same) + occlusion += max(0.0, dot(v, n) - 0.01 * p.z) / (vv + pc.radius * pc.radius * 0.0017) * falloff; } float ao = max(0.0, 1.0 - 2.0 * occlusion / float(kSamples) * pc.radius * 0.6); o_ao = vec4(mix(1.0, ao * ao, pc.strength)); diff --git a/src/app.cpp b/src/app.cpp index 5a2d433..cbbd613 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -21,6 +21,7 @@ keel::Cvar fry_touch("fry_touch", "0", keel::CVAR_ARCHIVE, "Fry3DX: the phone la #endif keel::Cvar fry_uiscale("fry_uiscale", "0", 0, "Fry3DX: draw the UI this many times bigger (0: by the screen)"); keel::Cvar fry_uisize("fry_uisize", "1", keel::CVAR_ARCHIVE, "Fry3DX: the phone layout's size, times"); +keel::Cvar fry_movespeed("fry_movespeed", "1", keel::CVAR_ARCHIVE, "Fry3DX: how fast the camera moves (keys, wheel, pan): Shift + wheel changes it"); keel::Cvar fry_theme("fry_theme", "0", keel::CVAR_ARCHIVE, "Fry3DX: how it looks: 0 like the desktop, 1 dark, 2 light"); keel::Cvar fry_pluginsoff("fry_pluginsoff", "", keel::CVAR_ARCHIVE, "Fry3DX: plug-ins turned off (their file names, with | between)"); keel::Cvar fry_autosave("fry_autosave", "2", keel::CVAR_ARCHIVE, "Fry3DX: autosave every this many minutes while there are changes (0: off)"); @@ -54,7 +55,8 @@ auto App::init() -> void { static keel::Cmd orbit("fry_orbit", [](const keel::Args& a) { if (a.size() > 2) self->view_ = View::Perspective, self->fov_ = 50, self->yaw_ = std::stof(a[1]), self->pitch_ = std::stof(a[2]); if (a.size() > 3) self->dist_ = std::stof(a[3]); - }, "Fry3DX: fry_orbit yaw pitch [distance]"); + if (a.size() > 6) self->target_ = {std::stof(a[4]), std::stof(a[5]), std::stof(a[6])}; + }, "Fry3DX: fry_orbit yaw pitch [distance [target x y z]]"); // A sculpt stroke across the view (0..1 of it), for pictures of it: brush 1-6, then from and to. static keel::Cmd sculptStroke("fry_sculpt", [](const keel::Args& a) { if (!self->viewReady()) return self->say("fry_sculpt: the view isn't up yet (+wait 1 first)"); @@ -264,6 +266,16 @@ auto App::init() -> void { }, "Fry3DX: fry_bench orbit|smooth|angle|round frames: time that many frames while the camera turns or the picked object's setting is dragged"); static keel::Cmd hold("fry_hold", [](const keel::Args& a) { // a key held down for some frames, as if pressed (for trying keys out) if (a.size() < 3) return self->say("fry_hold key frames (UpArrow, PageUp, LeftShift...)"); + if (a[1] == "MouseRight") { // (pressed now, let go after that many frames) + ImGui::GetIO().AddMouseButtonEvent(ImGuiMouseButton_Right, true); + return self->holding_.push_back({ImGuiKey_MouseRight, std::stoi(a[2])}); + } + if (a[1] == "Escape") { // (the engine takes this one from the window's own events, before the panels) + SDL_Event e{}; + e.type = SDL_EVENT_KEY_DOWN, e.key.scancode = SDL_SCANCODE_ESCAPE, e.key.key = SDLK_ESCAPE, e.key.down = true; + SDL_PushEvent(&e); + return; + } for (int k = ImGuiKey_NamedKey_BEGIN; k < ImGuiKey_NamedKey_END; ++k) if (a[1] == ImGui::GetKeyName(ImGuiKey(k))) { ImGui::GetIO().AddKeyEvent(ImGuiKey(k), true); @@ -271,6 +283,10 @@ auto App::init() -> void { } self->say("fry_hold: no key called " + a[1]); }, "Fry3DX: fry_hold key frames: hold a key down (UpArrow, PageUp...)"); + static keel::Cmd mouse("fry_mouse", [](const keel::Args& a) { // the mouse to x y (0..1 of the view) + if (a.size() < 3) return; + ImGui::GetIO().AddMousePosEvent(self->viewMin_.x + (self->viewMax_.x - self->viewMin_.x) * std::stof(a[1]), self->viewMin_.y + (self->viewMax_.y - self->viewMin_.y) * std::stof(a[2])); + }, "Fry3DX: fry_mouse x y: the mouse to that point of the view"); static keel::Cmd wheel("fry_wheel", [](const keel::Args& a) { // the wheel turned at x y (0..1 of the view) if (a.size() < 4) return; ImGuiIO& io = ImGui::GetIO(); @@ -459,7 +475,10 @@ auto App::allowQuit() -> bool { } auto App::onEscape() -> void { - if (askSave_) askSave_ = 0; + // (the engine takes Escape before the panels see it: whatever's open on top goes first) + if (search_.open) search_.open = false; + else if (pie_) pie_ = 0; + else if (askSave_) askSave_ = 0; else if (showRecover_) showRecover_ = false; else if (editLayer()) cancelFloating(); else if (!knifePts_.empty()) knifePts_.clear(); @@ -476,7 +495,7 @@ auto App::onEscape() -> void { // (fry_bench) Each frame: the change being timed. auto App::benchStep() -> void { for (auto& [k, left] : holding_) - if (--left <= 0) ImGui::GetIO().AddKeyEvent(k, false); + if (--left <= 0) k == ImGuiKey_MouseRight ? ImGui::GetIO().AddMouseButtonEvent(ImGuiMouseButton_Right, false) : ImGui::GetIO().AddKeyEvent(k, false); std::erase_if(holding_, [](const auto& h) { return h.second <= 0; }); if (bench_.left <= 0) return; const int i = int(bench_.ms.size()); @@ -1628,7 +1647,7 @@ auto App::setTab(Tab t) -> void { // ───────────────────────────── keys ───────────────────────────── auto App::shortcuts(ImGuiIO& io) -> void { - if (io.WantTextInput || slide_.active || drag_.active) return; // (a slide, a drag: their own keys) + if (io.WantTextInput || slide_.active || drag_.active || looking_) return; // (a slide, a drag: their own keys; turning the camera: WASD, Q and E move it) if (tool_ == Tool::Draw && !drawPts_.empty() && sketchKeys(io)) return; const bool ctrl = io.KeyCtrl, shift = io.KeyShift, alt = io.KeyAlt; auto key = [](ImGuiKey k) { return ImGui::IsKeyPressed(k, false); }; diff --git a/src/app.h b/src/app.h index de7eada..777a783 100644 --- a/src/app.h +++ b/src/app.h @@ -275,6 +275,7 @@ extern keel::Cvar fry_touch; extern keel::Cvar fry_uiscale; extern keel::Cvar fry_uisize; extern keel::Cvar fry_theme; +extern keel::Cvar fry_movespeed; extern keel::Cvar fry_pluginsoff; extern keel::Cvar fry_autosave; @@ -356,7 +357,8 @@ private: ImVec2 viewMin_{0, 0}, viewMax_{1, 1}; glm::mat4 vp_{1.0f}; glm::vec3 eye_{0.0f}; - bool orbiting_ = false, panning_ = false; + bool orbiting_ = false, panning_ = false, looking_ = false; // (looking: right drag turns the camera where it stands) + float nearest_ = 1.0f; // (how far the nearest thing is from the eye, m: worked out with the view) bool xray_ = false, showGrid_ = true, renderGrid_ = false; std::string appliedLook_; keel::ModelId ground_{}; @@ -1464,6 +1466,11 @@ private: std::vector ms; } bench_; std::vector> holding_; // (fry_hold: keys held down for that many more frames) + float aoReach() const; + void lookBy(ImVec2 d); + void refocus(); + float nearestThing(); + float viewScale() const { return std::max(std::min(dist_, nearest_), 0.001f); } // (how big the view is where it matters: what's ahead, or nearer) void benchStep(); void benchEnd(std::chrono::steady_clock::time_point began); diff --git a/src/app_panels.cpp b/src/app_panels.cpp index 6056aaf..0f6e8df 100644 --- a/src/app_panels.cpp +++ b/src/app_panels.cpp @@ -1985,19 +1985,19 @@ auto App::statusBar() -> void { else if (tab_ == Tab::Layout) { for (const LayToolInfo& t : kLayTools) if (t.tool == layTool_) - ImGui::TextDisabled("%s: %s · Tab: frames or corners Right drag: orbit (pan in the Picture panel) F1: all keys", t.name, t.hint); + ImGui::TextDisabled("%s: %s · Tab: frames or corners Right drag: look (pan in the Picture panel) F1: all keys", t.name, t.hint); } else if ((tab_ == Tab::Print && printTool_ != PrintTool::None) || printTool_ == PrintTool::Measure) { ImGui::TextDisabled(printTool_ == PrintTool::Measure ? "Measure: click two points on the model (corners catch the mouse); a third click starts again · M (Print) or Esc: done" : "Lay flat: click the face it should stand on: it turns so that face is down, on the bed · L: done"); } else if (tab_ == Tab::Paint && sculpting_) { for (const SculptToolInfo& t : kSculptTools) - if (t.brush == sculptBrush_) ImGui::TextDisabled("%s: %s · Shift: smooth X: both sides Tab: colour Right drag: orbit F1: all keys", t.name, t.hint); + if (t.brush == sculptBrush_) ImGui::TextDisabled("%s: %s · Shift: smooth X: both sides Tab: colour Right drag: look F1: all keys", t.name, t.hint); } else if (tab_ == Tab::Paint) { for (const PaintToolInfo& t : kPaintTools) - if (t.tool == paintTool_) ImGui::TextDisabled("%s: %s · Right drag: orbit Wheel: zoom (in the Picture panel too) F1: all keys", t.name, t.hint); + if (t.tool == paintTool_) ImGui::TextDisabled("%s: %s · Right drag: look Wheel: forward / back (zoom in the Picture panel) F1: all keys", t.name, t.hint); } else for (const ToolInfo& t : kTools) - if (t.tool == tool_) ImGui::TextDisabled("%s: %s · Right drag: orbit Middle drag: pan Wheel: zoom F1: all keys", t.name, t.hint); + if (t.tool == tool_) ImGui::TextDisabled("%s: %s · Right drag + WASD: look and move Alt+right: orbit Wheel: forward / back F1: all keys", t.name, t.hint); ImGui::End(); } diff --git a/src/app_scripts.cpp b/src/app_scripts.cpp index c3d1a70..1085d3b 100644 --- a/src/app_scripts.cpp +++ b/src/app_scripts.cpp @@ -446,8 +446,10 @@ auto App::helpWindow() -> void { rows("Finding things", {{"F3 or Ctrl+Space", "Search for any command by name and run it"}, {"Space (held)", "The tools round the mouse: point at one, let go"}, {"Shift+Space (held)", "The views round the mouse"}}); myKeysTable(); - rows("Looking around", {{"Right drag", "Orbit round what you look at"}, {"Middle drag / Shift+Right drag", "Pan"}, {"Wheel", "Zoom (towards what's under the mouse)"}, - {"Arrow keys", "Move round (Page Up / Down: up and down; Shift: faster)"}, + rows("Looking around", {{"Right drag", "Turn the camera where it stands"}, {"Right held + W A S D", "Move where you look (Q / E: down and up)"}, + {"Arrow keys", "Move where you look, any time (Page Up / Down: down and up)"}, {"Wheel", "Forward and back"}, + {"Shift + wheel", "How fast every way of moving goes"}, {"Shift (while moving)", "Faster"}, + {"Alt + right drag", "Orbit round what's picked (or what you look at)"}, {"Middle drag / Shift+Right drag", "Pan"}, {"Orbit / Pan / Zoom buttons", "Drag them (touchpads)"}, {"F", "Frame the selection"}, {"Home", "Frame everything"}, {"Numpad 1 / 3 / 7", "Front / side / top (Shift: the opposite)"}, {"Numpad 5", "Flat view / 3D"}, {"Z / Shift+Z", "Solid or Rendered / Wireframe"}, {"Alt+Z", "X-ray: see and pick through"}}); diff --git a/src/app_view.cpp b/src/app_view.cpp index d1257f4..1961460 100644 --- a/src/app_view.cpp +++ b/src/app_view.cpp @@ -171,57 +171,119 @@ auto App::orbitBy(ImVec2 d) -> void { pitch_ = std::clamp(pitch_ + d.y * 0.3f, -89.9f, 89.9f); } +// Turning the camera where it stands (right drag): what it looks at moves, the eye doesn't. +auto App::lookBy(ImVec2 d) -> void { + if (std::abs(d.x) + std::abs(d.y) > 0.5f) leaveFlatView(); + const glm::vec3 eye = target_ - viewDir() * dist_; + yaw_ -= d.x * 0.2f; + pitch_ = std::clamp(pitch_ + d.y * 0.2f, -89.9f, 89.9f); + target_ = eye + viewDir() * dist_; +} + +// What the camera looks at now: the object or the ground straight ahead (not further than the +// scene reaches). The point it turns round, how fast it moves and how near it draws follow it. +auto App::refocus() -> void { + const glm::vec3 f = viewDir(), eye = target_ - f * dist_; + const Ray ray{eye, f}; + float t = -1.0f; + if (const Hit h = pickObject(ray); h.object) t = h.t; + else if (auto g = hitPlane(ray, glm::vec3(0.0f), glm::vec3(0, 0, 1))) t = std::min(glm::distance(eye, *g), std::max(glm::length(eye) * 2.0f, 50.0f)); + if (t > 0.0f) dist_ = std::clamp(t, 0.01f, 5000.0f), target_ = eye + f * dist_; +} + +// How far the nearest thing (an object, the ground) is from the eye: the near plane and the +// shade in corners go by it, so looking past something close doesn't break them. +auto App::nearestThing() -> float { + float best = std::abs(eye_.z); // (the ground) + for (const Object& o : scene_.objects) { + if (!o.visible) continue; + auto& [version, radius] = radii_[o.id]; + if (version != shapeKey(o) || !version) { + version = shapeKey(o), radius = 0; + for (const glm::vec3& v : o.mesh.verts) radius = std::max(radius, glm::length(v)); + } + best = std::min(best, std::max(glm::distance(eye_, o.position) - radius * std::max({std::abs(o.scale.x), std::abs(o.scale.y), std::abs(o.scale.z)}), 0.0f)); + } + return std::max(best, 0.001f); +} + auto App::panBy(ImVec2 d) -> void { const glm::vec3 f = viewDir(), right = glm::normalize(glm::cross(f, std::abs(f.z) > 0.99f ? glm::vec3(std::cos(glm::radians(yaw_)), std::sin(glm::radians(yaw_)), 0) : glm::vec3(0, 0, 1))); const glm::vec3 up = glm::cross(right, f); const float perPixel = viewHeight() / std::max(ImGui::GetIO().DisplaySize.y, 1.0f); - target_ += (-right * d.x + up * d.y) * perPixel; + target_ += (-right * d.x + up * d.y) * perPixel * fry_movespeed.f(); } auto App::updateCamera(ImGuiIO& io) -> void { const bool inView = mouseInView(io); // Orbit: right drag. Pan: middle drag (or Shift + right). Zoom: the wheel. const bool rightDown = ImGui::IsMouseDown(ImGuiMouseButton_Right), midDown = ImGui::IsMouseDown(ImGuiMouseButton_Middle); - if (inView && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) orbiting_ = !io.KeyShift; - if (inView && (ImGui::IsMouseClicked(ImGuiMouseButton_Middle) || (io.KeyShift && ImGui::IsMouseClicked(ImGuiMouseButton_Right)))) panning_ = true, orbiting_ = false; - if (!rightDown) orbiting_ = false; + // Right drag turns the camera where it stands (as in games, Unreal, Unity); Alt + right drag + // turns it round what's picked (or what it looks at). A finger on a phone: round, still. + if (inView && ImGui::IsMouseClicked(ImGuiMouseButton_Right) && !io.KeyShift) { + if (io.KeyAlt || touch()) { + orbiting_ = true; + if (!selected_.empty() && !touch()) { // (round the middle of what's picked) + glm::vec3 lo(1e30f), hi(-1e30f); + for (const Object& o : scene_.objects) + if (selected_.count(o.id)) lo = glm::min(lo, o.position), hi = glm::max(hi, o.position); + const glm::vec3 eye = target_ - viewDir() * dist_, pivot = (lo + hi) * 0.5f; + if (glm::distance(eye, pivot) > 1e-4f) { + const glm::vec3 f = glm::normalize(pivot - eye); + yaw_ = glm::degrees(std::atan2(f.y, f.x)), pitch_ = glm::degrees(std::asin(std::clamp(-f.z, -1.0f, 1.0f))); + target_ = pivot, dist_ = glm::distance(eye, pivot); + } + } + } else looking_ = true; + } + if (inView && (ImGui::IsMouseClicked(ImGuiMouseButton_Middle) || (io.KeyShift && ImGui::IsMouseClicked(ImGuiMouseButton_Right)))) panning_ = true, orbiting_ = looking_ = false; + if (!rightDown) { + if (looking_) refocus(); + orbiting_ = looking_ = false; + } if (!midDown && !rightDown) panning_ = false; const ImVec2 d = io.MouseDelta; if (orbiting_ && (d.x != 0 || d.y != 0)) orbitBy(d); + if (looking_ && (d.x != 0 || d.y != 0)) lookBy(d); if (panning_ && (d.x != 0 || d.y != 0)) panBy(d); if (inView && io.MouseWheel != 0 && drag_.active && softSel_ && mode_ == Mode::Edit) { // the soft selection's reach, not zoom softReach_ = std::clamp(softReach_ * std::pow(1.15f, io.MouseWheel), 0.001f, 1000.0f); findSoft(); message_ = std::format("Soft selection reaches {:.3f} m (wheel)", softReach_), messageAt_ = now(); + } else if (inView && io.MouseWheel != 0 && io.KeyShift) { // Shift + wheel: how fast every way of moving goes + const float speed = std::clamp(fry_movespeed.f() * std::pow(1.25f, io.MouseWheel), 0.05f, 20.0f); + keel::execute(std::format("fry_movespeed {:.3g}", speed)); + message_ = std::format("Moving speed: x{:.2g} (Shift + wheel)", speed), messageAt_ = now(); } else if (inView && io.MouseWheel != 0) { - // Towards what's under the mouse: an object, else the ground, else the plane the target's - // on. The whole camera closes in on that point (it stays under the mouse), so far from - // everything you still get somewhere instead of creeping at a spot in the air. - const float k = std::pow(0.85f, io.MouseWheel); - const Ray ray = mouseRay(io.MousePos); - std::optional at; - if (const Hit h = pickObject(ray); h.object) at = ray.origin + ray.dir * h.t; - else if (auto g = hitPlane(ray, glm::vec3(0.0f), glm::vec3(0, 0, 1)); g && glm::distance(*g, eye_) < dist_ * 40.0f) at = g; - if (!at && k < 1.0f) target_ += ray.dir * dist_ * (1.0f - k); // (nothing there: the sky; on that way) + // The wheel moves the camera straight forward or back (where it looks), a fifth of the + // way to what's ahead each notch, but never less than a little of the scene's size: so it + // doesn't creep to a stop in front of a surface (it goes through). In a flat view: closer. + if (view_ != View::Perspective) dist_ = std::clamp(dist_ * std::pow(0.85f, io.MouseWheel), 0.01f, 5000.0f); else { - if (!at) at = hitPlane(ray, target_, -viewDir()); - const float to = std::clamp(dist_ * k, 0.01f, 5000.0f); - if (at) target_ = *at + (target_ - *at) * (to / dist_); - dist_ = to; + float size = 0.0f; // (the biggest object's size: the least step is a little of it) + for (const Object& o : scene_.objects) + if (auto r = radii_.find(o.id); r != radii_.end()) size = std::max(size, r->second.second * std::max({std::abs(o.scale.x), std::abs(o.scale.y), std::abs(o.scale.z)})); + const float step = std::clamp(std::max(dist_ * 0.2f, size * 0.05f), 0.001f, 500.0f) * fry_movespeed.f(); + target_ += viewDir() * step * io.MouseWheel; + refocus(); } } - // Arrow keys: forward, back and sideways along the ground (in a flat view: the way they - // point on the screen), Page Up / Down: up and down; Shift: faster. As fast as the view is big. + // Moving: the arrow keys any time, and WASD (Q / E down and up) while the right button turns + // the camera; forward is where it looks (in a flat view: up the screen). Page Up / Down: up + // and down. Shift: faster. As fast as what's near is far. if (!io.WantTextInput && !search_.open && !ImGui::IsAnyItemActive() && !io.KeyCtrl && !io.KeyAlt) { - const float x = float(ImGui::IsKeyDown(ImGuiKey_RightArrow)) - float(ImGui::IsKeyDown(ImGuiKey_LeftArrow)); - const float y = float(ImGui::IsKeyDown(ImGuiKey_UpArrow)) - float(ImGui::IsKeyDown(ImGuiKey_DownArrow)); - const float z = float(ImGui::IsKeyDown(ImGuiKey_PageUp)) - float(ImGui::IsKeyDown(ImGuiKey_PageDown)); + auto down = [](ImGuiKey k) { return float(ImGui::IsKeyDown(k)); }; + const float wasd = looking_ ? 1.0f : 0.0f; + const float x = down(ImGuiKey_RightArrow) - down(ImGuiKey_LeftArrow) + wasd * (down(ImGuiKey_D) - down(ImGuiKey_A)); + const float y = down(ImGuiKey_UpArrow) - down(ImGuiKey_DownArrow) + wasd * (down(ImGuiKey_W) - down(ImGuiKey_S)); + const float z = down(ImGuiKey_PageUp) - down(ImGuiKey_PageDown) + wasd * (down(ImGuiKey_E) - down(ImGuiKey_Q)); if (x != 0 || y != 0 || z != 0) { const glm::vec3 f = viewDir(), right = glm::normalize(glm::cross(f, std::abs(f.z) > 0.99f ? glm::vec3(std::cos(glm::radians(yaw_)), std::sin(glm::radians(yaw_)), 0) : glm::vec3(0, 0, 1))); - const glm::vec3 ahead = view_ == View::Perspective ? glm::normalize(glm::cross(glm::vec3(0, 0, 1), right)) : glm::cross(right, f); - const float dt = std::min(io.DeltaTime, 0.1f), speed = dist_ * (io.KeyShift ? 2.5f : 0.8f) * dt; + const glm::vec3 ahead = view_ == View::Perspective ? f : glm::cross(right, f); + const float dt = std::min(io.DeltaTime, 0.1f), speed = std::max(viewScale() * 1.5f, 0.3f) * fry_movespeed.f() * (io.KeyShift ? 3.0f : 1.0f) * dt; target_ += (right * x + ahead * y + (view_ == View::Perspective ? glm::vec3(0, 0, z) : glm::vec3(0.0f))) * speed; if (view_ != View::Perspective && z != 0) dist_ = std::clamp(dist_ * std::pow(io.KeyShift ? 0.1f : 0.4f, z * dt), 0.01f, 5000.0f); // (flat: closer or further) + if (view_ == View::Perspective && !looking_) refocus(); // (while turning: when the button's let go) } } settleView(); @@ -234,6 +296,7 @@ auto App::settleView() -> void { const float aspect = io.DisplaySize.x / std::max(io.DisplaySize.y, 1.0f); shift_ = glm::vec2((viewMin_.x + viewMax_.x) / std::max(io.DisplaySize.x, 1.0f) - 1.0f, (viewMin_.y + viewMax_.y) / std::max(io.DisplaySize.y, 1.0f) - 1.0f); eye_ = target_ - viewDir() * dist_; + nearest_ = nearestThing(); nearFar_ = nearFar(); vp_ = viewProjection(eye_, yaw_, pitch_, fov_, aspect, shift_, nearFar_); } @@ -252,7 +315,7 @@ auto App::nearFar() -> glm::vec2 { } // (the near plane as far out as it can be: the depth's precision is spread between them, so // the nearer it is, the more surfaces far off flicker through each other) - const float nearM = std::clamp(dist_ * 0.005f, 1e-5f, 50.0f); + const float nearM = std::clamp(viewScale() * 0.005f, 1e-5f, 50.0f); const float farM = std::max({dist_ * 60.0f, (glm::length(eye_) + reach) * 2.0f, 100.0f}); return glm::vec2(nearM, farM) * kUnits; } @@ -1137,11 +1200,17 @@ auto App::dragNow(const ImGuiIO& io) -> void { // Solid: a plain grey room and soft studio light, no shadows or effects — for seeing shapes. // Rendered: sky, sun, shadows and effects. +// How far the shade in corners reaches (units): a share of how far away you look, so close up it +// stays in the corners instead of smearing over everything in a pattern. (24 at the first view.) +auto App::aoReach() const -> float { + return std::clamp(viewScale() * kUnits * 0.15f, 0.005f, 400.0f); +} + auto App::applyShading() -> void { const Look& k = scene_.look; const std::string want = shading() == Shading::Rendered - ? std::format("r_ssao {}; r_bloom {}; r_vignette {}; r_exposure {}; r_saturation {}; r_sharpen {}; r_shadowsoft {}; r_gi {}; r_fog 0; r_skycolor \"{} {} {}\"", k.ao, + ? std::format("r_ssao {}; r_ssao_radius {:.2g}; r_bloom {}; r_vignette {}; r_exposure {}; r_saturation {}; r_sharpen {}; r_shadowsoft {}; r_gi {}; r_fog 0; r_skycolor \"{} {} {}\"", k.ao, aoReach(), k.bloom, k.vignette, k.exposure, k.saturation, k.sharpen, k.shadowSoft, kRayLight, k.background.r, k.background.g, k.background.b) : std::string("r_ssao 0; r_bloom 0; r_vignette 0; r_exposure 1; r_saturation 1; r_sharpen 0; r_gi 0; r_fog 0; r_skycolor \"0.2 0.2 0.215\""); if (want == appliedLook_) return; @@ -1305,53 +1374,77 @@ auto App::draw3D(keel::Renderer& r) -> void { r.drawModel(id, toUnits, glm::vec4(1.0f, 1.0f, 1.0f, ref.opacity), nullptr, &flat, false); } // The grid on the work plane (the ground, or what a flat view looks at), and the axes. + // Four sets of lines, each ten times further apart than the one before, each drawn round + // where you look and fading out before its edge. A line's look goes only by how far apart + // it and its like are on the screen: too close together, it fades away; far apart, it's + // brighter and a little wider. So nothing jumps as the view moves or the steps change: new + // lines come in too close together to see and fade in. Each line is drawn by the widest set + // it belongs to (once), in pieces, each a pixel wide where it is. if (tab_ == Tab::Render ? renderGrid_ : showGrid_) { glm::vec3 p, n; workPlane(p, n); const glm::vec3 u = n.z > 0.5f ? glm::vec3(1, 0, 0) : n.x > 0.5f ? glm::vec3(0, 1, 0) : glm::vec3(1, 0, 0); const glm::vec3 v = glm::cross(n, u); - const float step = gridStep(); - const int count = 20; + const float first = gridStep() * 0.1f; + const int count = 20, pieces = 12, levels = 4; const glm::vec3 centre = target_ - n * glm::dot(target_, n); + const float cu = glm::dot(centre, u), cv = glm::dot(centre, v); const float shade = rendered ? 0.0f : 1.0f; // (dark lines on the sunny ground, light ones in the grey room) - // Two rings: the fine lines round where you look, then lines ten times apart out to - // the horizon (they leave out the middle, so no line is drawn twice). - for (int level = 0; level < 2; ++level) { - const float s = step * (level ? 10.0f : 1.0f), inner = count * step; // (the fine ring's reach) - const float cu = snapTo(glm::dot(centre, u), step * 10), cv = snapTo(glm::dot(centre, v), step * 10); - const float fade = level ? 0.6f : 1.0f; - for (int i = -count; i <= count; ++i) - for (int dir = 0; dir < 2; ++dir) { - const glm::vec3 along = dir ? u : v, across = dir ? v : u; - const float at = (dir ? cv : cu) + float(i) * s, mid = dir ? cu : cv; - const bool major = std::abs(snapTo(at, s * 10) - at) < s * 0.01f; - const bool axis = std::abs(at) < s * 0.01f; - glm::vec4 col = glm::vec4(glm::vec3(shade), (major ? (rendered ? 0.35f : 0.13f) : (rendered ? 0.16f : 0.06f)) * fade); - if (axis) col = dir ? (n.x > 0.5f ? glm::vec4(0.3f, 0.75f, 0.25f, 0.9f) : glm::vec4(0.9f, 0.25f, 0.25f, 0.9f)) - : (n.z > 0.5f ? glm::vec4(0.3f, 0.75f, 0.25f, 0.9f) : glm::vec4(0.25f, 0.45f, 0.95f, 0.9f)); - const float from = mid - count * s, to = mid + count * s; - // (a far line through the fine ring, which has it already: only its two ends) - const bool split = level == 1 && std::abs(at - (dir ? cv : cu)) <= inner * 1.0001f; - const int pieces = level ? 24 : 8; - for (int k = 0; k < pieces; ++k) { - const float t0 = from + (to - from) * float(k) / float(pieces), t1 = from + (to - from) * float(k + 1) / float(pieces); - if (split && t1 > mid - inner && t0 < mid + inner) continue; - const glm::vec3 a = across * at + along * t0, b = across * at + along * t1, c = (a + b) * 0.5f; - // Each piece about a pixel wide where it is, and fading as the lines get - // too close together on the screen to tell apart (far off, or seen edge - // on): no crawling, flickering band towards the horizon. - const glm::vec3 sight = glm::normalize(c - eye_); - const float px = pixelSize(c), seen = glm::length(across - sight * glm::dot(across, sight)); - const float apart = s * (major ? 10.0f : 1.0f) * seen / std::max(px, 1e-9f); // (pixels to the next line like it: a major line's are ten apart) - glm::vec4 pc = col; - pc.a *= axis ? std::max(glm::smoothstep(1.5f, 6.0f, apart), 0.35f) : glm::smoothstep(3.0f, 12.0f, apart); - if (pc.a < 0.004f) continue; - // (each end a hair towards the eye along its own line of sight: the same - // place on the screen, but in front of the ground it lies on, so they don't fight) - auto lift = [&](const glm::vec3& q) { return q + (eye_ - q) * 0.002f; }; - r.drawStreak(lift(a) * kUnits, lift(b) * kUnits, px * (major || axis ? 1.6f : 1.0f) * kUnits, pc, keel::kWhiteTexture); - } + const float dim = rendered ? 0.14f : 0.05f, bright = rendered ? 0.4f : 0.16f; + auto lift = [&](const glm::vec3& q) { return q + (eye_ - q) * 0.002f; }; // (towards the eye along its line of sight: the same place on the screen, but in front of the ground) + // One line, at `at` across, from `from` to `to` along; s: how far apart it and its like + // are; ring: how far its set reaches from the middle (0: no end, the axes). + auto line = [&](int dir, float at, float from, float to, float s, float ring, glm::vec4 col, bool axis) { + const glm::vec3 along = dir ? u : v, across = dir ? v : u; + const float mid = dir ? cu : cv, side = std::abs(at - (dir ? cv : cu)); + // (short pieces near the eye, longer further off: each one's width and fading is + // worked out at its middle, so it has to be about the same all along it) + const float near = std::clamp(glm::dot(eye_, along), from, to); + for (int k = 0; k < pieces * 2; ++k) { + const int sideOf = k < pieces ? -1 : 1, j = k % pieces; + const float len = sideOf < 0 ? near - from : to - near; + if (len <= 0.0f) continue; + const float f0 = float(j) / float(pieces), f1 = float(j + 1) / float(pieces); + const float t0 = near + float(sideOf) * len * f0 * f0 * f0, t1 = near + float(sideOf) * len * f1 * f1 * f1; + const glm::vec3 a = across * at + along * t0, b = across * at + along * t1, c = (a + b) * 0.5f; + // (pixels to the next line like it, across this one as it lies on the screen) + const glm::vec4 c0 = vp_ * glm::vec4(c, 1.0f), c1 = vp_ * glm::vec4(c + across * s, 1.0f), c2 = vp_ * glm::vec4(c + along * s, 1.0f); + if (c0.w <= 1e-6f || c1.w <= 1e-6f || c2.w <= 1e-6f) continue; // (behind the eye) + const glm::vec2 half = glm::vec2(ImGui::GetIO().DisplaySize.x, ImGui::GetIO().DisplaySize.y) * 0.5f; + const glm::vec2 s0 = glm::vec2(c0) / c0.w * half, s1 = glm::vec2(c1) / c1.w * half, s2 = glm::vec2(c2) / c2.w * half; + const glm::vec2 way = s2 - s0; + const float wayLen = glm::length(way), d = s1.x - s0.x, e = s1.y - s0.y; + const float apart = wayLen > 1e-6f ? std::abs(d * way.y - e * way.x) / wayLen : glm::length(s1 - s0); + const float px = pixelSize(c); + float alpha = axis ? std::max(glm::smoothstep(1.5f, 6.0f, apart), 0.35f) : glm::smoothstep(5.0f, 20.0f, apart); + if (ring > 0.0f) alpha *= glm::smoothstep(ring, ring * 0.6f, std::max(side, std::abs((t0 + t1) * 0.5f - mid))); + const float strong = axis ? 1.0f : glm::smoothstep(20.0f, 160.0f, apart); + glm::vec4 pc = axis ? col : glm::vec4(glm::vec3(shade), glm::mix(dim, bright, strong)); + pc.a *= alpha; + if (pc.a < 0.004f) continue; + r.drawStreak(lift(a) * kUnits, lift(b) * kUnits, px * (1.0f + 0.6f * strong) * kUnits, pc, keel::kWhiteTexture); + } + }; + for (int level = 0; level < levels; ++level) { + const float s = first * std::pow(10.0f, float(level)), ring = count * s; + const bool top = level == levels - 1; + for (int dir = 0; dir < 2; ++dir) { + const float c0 = snapTo(dir ? cv : cu, s), mid = dir ? cu : cv; + for (int i = -count; i <= count; ++i) { + const float at = c0 + float(i) * s; + if (std::abs(at) < s * 0.01f) continue; // (the axis: drawn on its own) + if (!top && std::abs(snapTo(at, s * 10.0f) - at) < s * 0.01f) continue; // (the next set's) + line(dir, at, mid - ring, mid + ring, s, ring, {}, false); } + } + } + // The axes: right through, as far as the widest set reaches. + const float reach = count * first * std::pow(10.0f, float(levels - 1)); + for (int dir = 0; dir < 2; ++dir) { + const glm::vec4 col = dir ? (n.x > 0.5f ? glm::vec4(0.3f, 0.75f, 0.25f, 0.9f) : glm::vec4(0.9f, 0.25f, 0.25f, 0.9f)) + : (n.z > 0.5f ? glm::vec4(0.3f, 0.75f, 0.25f, 0.9f) : glm::vec4(0.25f, 0.45f, 0.95f, 0.9f)); + const float mid = dir ? cu : cv; + line(dir, 0.0f, mid - reach, mid + reach, first, 0.0f, col, true); } } if (tab_ == Tab::Print) drawPrintBed(r, light); diff --git a/src/commands.cpp b/src/commands.cpp index 64ac084..4d91501 100644 --- a/src/commands.cpp +++ b/src/commands.cpp @@ -88,6 +88,8 @@ int CommandSearch::draw(const std::vector& all, const std::vector