From 9d9557117a9c188caee0e02899a91c19881818fb Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Wed, 21 Oct 2026 17:08:13 +0200 Subject: [PATCH 1/4] Close up no longer falls apart: how near the nearest thing is goes by each object's box (not a ball round it: inside a big or long flat one's ball it came to nothing, the near plane with it, and surfaces fought each other in blocks, jittering, the sky and grid gone), the near plane a quarter of the way to it; reference pictures count too. Grid lines meet exactly where their pieces join (no crawling dots): a streak's ends each face the eye, each its own width; pieces a quarter further from the eye each. The depth pre-pass waited on before the scene uses it; the same depth in both (invariant) Co-Authored-By: Claude Opus 5.5 --- engine/render/renderer.cpp | 14 ++--- engine/render/renderer.h | 6 +- engine/rhi/gl/gl_legacy.cpp | 4 ++ engine/rhi/vulkan/vk_backend.cpp | 11 ++++ engine/shaders/mesh.vert | 3 + engine/shaders/skinned.vert | 3 + engine/shaders/sprite.vert | 4 ++ src/app.h | 8 ++- src/app_view.cpp | 100 +++++++++++++++++++------------ 9 files changed, 102 insertions(+), 51 deletions(-) diff --git a/engine/render/renderer.cpp b/engine/render/renderer.cpp index 18b1204..bfc166a 100644 --- a/engine/render/renderer.cpp +++ b/engine/render/renderer.cpp @@ -211,14 +211,14 @@ void Renderer::drawSprite(const glm::vec3& at, float size, const glm::vec4& colo list.push_back({glm::vec4(at, size * 0.5f), color, glm::vec4(0, 0, 0, rotation), glm::vec4(0, 0, 0, tex)}); } -void Renderer::drawStreak(const glm::vec3& from, const glm::vec3& to, float width, const glm::vec4& color, TextureId texture, bool additive) { +void Renderer::drawStreak(const glm::vec3& from, const glm::vec3& to, float width, const glm::vec4& color, TextureId texture, bool additive, float widthTo) { auto& list = additive ? packet_.glowSprites : packet_.sprites; if (list.size() >= 8192) return; uint32_t bits = texture; float tex; std::memcpy(&tex, &bits, 4); - // up.xyz: half its length along it (non-zero marks a streak; its side is found per frame). - list.push_back({glm::vec4((from + to) * 0.5f, width * 0.5f), color, glm::vec4(0.0f), glm::vec4((to - from) * 0.5f, tex)}); + // up.xyz: half its length along it (non-zero marks a streak); right.w: half its width at `to`. + list.push_back({glm::vec4((from + to) * 0.5f, width * 0.5f), color, glm::vec4(0.0f, 0.0f, 0.0f, (widthTo < 0.0f ? width : widthTo) * 0.5f), glm::vec4((to - from) * 0.5f, tex)}); } void Renderer::drawModel(ModelId model, const glm::mat4& transform, const glm::vec4& tint, const std::vector* skin, @@ -757,11 +757,9 @@ void Renderer::buildPacket(uint32_t width, uint32_t height) { up = glm::cross(right, f); for (auto* list : {&p.sprites, &p.glowSprites}) for (gpu::Sprite& s : *list) { - if (const glm::vec3 half(s.up); glm::dot(half, half) > 0.0f) { // a streak: its side faces the eye - glm::vec3 side = glm::cross(half, camera_.origin - glm::vec3(s.posSize)); - side = glm::length(side) > 1e-6f ? glm::normalize(side) : right; - s.right = glm::vec4(side * s.posSize.w, 0.0f); - s.posSize.w = 1.0f; + if (const glm::vec3 half(s.up); glm::dot(half, half) > 0.0f) { // a streak: each end's side faces the eye (sprite.vert) + s.right = glm::vec4(camera_.origin, s.right.w); + s.posSize.w = -std::max(s.posSize.w, 1e-9f); // (below 0: a streak; its half width at `from`) continue; } s.right = glm::vec4(right, s.right.w), s.up = glm::vec4(up, s.up.w); diff --git a/engine/render/renderer.h b/engine/render/renderer.h index c51fb88..873304c 100644 --- a/engine/render/renderer.h +++ b/engine/render/renderer.h @@ -98,9 +98,9 @@ public: // A particle: a camera-facing square `size` across, unlit (color can be HDR-bright). // additive: light added on (fire, sparks, flashes); else blended (smoke, dust). void drawSprite(const glm::vec3& at, float size, const glm::vec4& color, TextureId texture, float rotation = 0.0f, bool additive = true); - // A sprite stretched from `from` to `to`, `width` across, turned to face the eye as much as - // it can (rain). - void drawStreak(const glm::vec3& from, const glm::vec3& to, float width, const glm::vec4& color, TextureId texture, bool additive = false); + // A sprite stretched from `from` to `to`, `width` across (`widthTo` at `to`: < 0, the same), + // each end turned to face the eye as much as it can (rain; lines drawn in pieces meet exactly). + void drawStreak(const glm::vec3& from, const glm::vec3& to, float width, const glm::vec4& color, TextureId texture, bool additive = false, float widthTo = -1.0f); // skin: one matrix per joint (anim::skinMatrices) for skinned models; ignored otherwise. // lighting: probe light for moving things (nullptr = lightmap or simple sun shading). void drawModel(ModelId model, const glm::mat4& transform, const glm::vec4& tint = glm::vec4(1.0f), diff --git a/engine/rhi/gl/gl_legacy.cpp b/engine/rhi/gl/gl_legacy.cpp index ed5b466..b1f45d5 100644 --- a/engine/rhi/gl/gl_legacy.cpp +++ b/engine/rhi/gl/gl_legacy.cpp @@ -373,6 +373,10 @@ void main() { float c = cos(u_right.w), s = sin(u_right.w); vec2 r = vec2(c * q.x - s * q.y, s * q.x + c * q.y); vec3 world = u_posSize.xyz + (u_right.xyz * r.x + u_up.xyz * r.y) * u_posSize.w; + if (u_posSize.w < 0.0) { // a streak: right.xyz the eye, w its half width at the far end; each end's side faces the eye + vec3 end = u_posSize.xyz + u_up.xyz * q.y, side = cross(u_up.xyz, u_right.xyz - end); + world = end + (length(side) > 1e-9 ? normalize(side) : vec3(0.0, 0.0, 1.0)) * q.x * (q.y < 0.0 ? -u_posSize.w : u_right.w); + } v_uv = vec2(q.x * 0.5 + 0.5, 0.5 - q.y * 0.5); gl_Position = u_viewProj * vec4(world, 1.0); gl_Position.z = gl_Position.z * 2.0 - gl_Position.w; diff --git a/engine/rhi/vulkan/vk_backend.cpp b/engine/rhi/vulkan/vk_backend.cpp index 7042540..b42d213 100644 --- a/engine/rhi/vulkan/vk_backend.cpp +++ b/engine/rhi/vulkan/vk_backend.cpp @@ -1413,6 +1413,17 @@ void VulkanBackend::render(FramePacket& p) { vkCmdSetScissor(cmd, 0, 1, &scissor); drawMeshes(cmd, p.opaque, p.viewProj, false, depthMeshPipeline_, depthSkinnedPipeline_, cullFor_, true); vkCmdEndRendering(cmd); + // The scene pass tests against that depth: wait for it to be written (two render passes + // aren't ordered on their own; without this, tiles of it could still be the old depth). + VkMemoryBarrier2 mb{VK_STRUCTURE_TYPE_MEMORY_BARRIER_2}; + mb.srcStageMask = VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT; + mb.srcAccessMask = VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + mb.dstStageMask = VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT; + mb.dstAccessMask = VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + VkDependencyInfo dep{VK_STRUCTURE_TYPE_DEPENDENCY_INFO}; + dep.memoryBarrierCount = 1; + dep.pMemoryBarriers = &mb; + vkCmdPipelineBarrier2(cmd, &dep); mark(cmd, "depth pre-pass"); depth.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; } diff --git a/engine/shaders/mesh.vert b/engine/shaders/mesh.vert index 22e1165..3bb52fe 100644 --- a/engine/shaders/mesh.vert +++ b/engine/shaders/mesh.vert @@ -5,6 +5,9 @@ layout(location = 0) out vec3 v_normal; layout(location = 1) out vec2 v_uv; layout(location = 2) out vec2 v_lightmapUv; layout(location = 3) out vec3 v_world; +// The same depth in the depth pre-pass and the scene pass (else the compiler, dropping what the pre-pass +// doesn't use, can round it differently and the scene pass fails its depth test in patches). +invariant gl_Position; void main() { Vertex vx = VERTEX(VERTEX_ID); diff --git a/engine/shaders/skinned.vert b/engine/shaders/skinned.vert index b5406cc..a186f36 100644 --- a/engine/shaders/skinned.vert +++ b/engine/shaders/skinned.vert @@ -5,6 +5,9 @@ layout(location = 0) out vec3 v_normal; layout(location = 1) out vec2 v_uv; layout(location = 2) out vec2 v_lightmapUv; layout(location = 3) out vec3 v_world; +// The same depth in the depth pre-pass and the scene pass (else the compiler, dropping what the pre-pass +// doesn't use, can round it differently and the scene pass fails its depth test in patches). +invariant gl_Position; void main() { Vertex vx = VERTEX(VERTEX_ID); diff --git a/engine/shaders/sprite.vert b/engine/shaders/sprite.vert index cf238cf..3ee2c40 100644 --- a/engine/shaders/sprite.vert +++ b/engine/shaders/sprite.vert @@ -15,6 +15,10 @@ void main() { float c = cos(pc.right.w), s = sin(pc.right.w); vec2 r = vec2(c * q.x - s * q.y, s * q.x + c * q.y); vec3 world = pc.posSize.xyz + (pc.right.xyz * r.x + pc.up.xyz * r.y) * pc.posSize.w; + if (pc.posSize.w < 0.0) { // a streak: right.xyz the eye, w its half width at the far end; each end's side faces the eye + vec3 end = pc.posSize.xyz + pc.up.xyz * q.y, side = cross(pc.up.xyz, pc.right.xyz - end); + world = end + (length(side) > 1e-9 ? normalize(side) : vec3(0.0, 0.0, 1.0)) * q.x * (q.y < 0.0 ? -pc.posSize.w : pc.right.w); + } v_uv = q * 0.5 + 0.5; v_uv.y = 1.0 - v_uv.y; gl_Position = pc.viewProj * vec4(world, 1.0); diff --git a/src/app.h b/src/app.h index cd6ddcd..70b8718 100644 --- a/src/app.h +++ b/src/app.h @@ -354,7 +354,12 @@ private: glm::vec3 target_{0, 0, 0.4f}; float yaw_ = -135.0f, pitch_ = 25.0f, dist_ = 4.0f, fov_ = 50.0f; glm::vec2 nearFar_{2.0f, 16384.0f}; - std::map> radii_; // (each object's mesh: how far it reaches from its place) + struct Reach { + uint64_t version = 0; + float radius = 0.0f; // (how far its mesh reaches from its place) + glm::vec3 lo{0.0f}, hi{0.0f}; // (the box round its mesh, its mirrored copies too) + }; + std::map radii_; // (each object's) ImVec2 viewMin_{0, 0}, viewMax_{1, 1}; glm::mat4 vp_{1.0f}; glm::vec3 eye_{0.0f}; @@ -1471,6 +1476,7 @@ private: void lookBy(ImVec2 d); void refocus(); float nearestThing(); + auto reachOf(const Object& o) -> const Reach&; 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_view.cpp b/src/app_view.cpp index b434c54..27fecda 100644 --- a/src/app_view.cpp +++ b/src/app_view.cpp @@ -191,18 +191,38 @@ auto App::refocus() -> void { if (t > 0.0f) dist_ = std::clamp(t, 0.01f, 5000.0f), target_ = eye + f * dist_; } +// How far an object's mesh reaches (worked out again when its shape changes). +auto App::reachOf(const Object& o) -> const Reach& { + Reach& r = radii_[o.id]; + if (r.version == shapeKey(o) && r.version) return r; + r = {shapeKey(o)}; + if (!o.mesh.verts.empty()) r.lo = boundsMin(o.mesh), r.hi = boundsMax(o.mesh); + for (int a = 0; a < 3; ++a) + if (o.mirror & (1 << a)) r.lo[a] = std::min(r.lo[a], -r.hi[a]), r.hi[a] = std::max(r.hi[a], -r.lo[a]); + for (const glm::vec3& v : o.mesh.verts) r.radius = std::max(r.radius, glm::length(v)); + return r; +} + // 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. +// shade in corners go by it, so looking past something close doesn't break them. To each +// object's box (not a ball round it: inside a long flat one's ball, it would come to nothing). 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)); + const Reach& r = reachOf(o); + const glm::mat4 m = objectMatrix(o); + const glm::vec3 local = glm::inverse(m) * glm::vec4(eye_, 1.0f); + best = std::min(best, glm::distance(eye_, glm::vec3(m * glm::vec4(glm::clamp(local, r.lo, r.hi), 1.0f)))); + } + for (const Reference& ref : scene_.references) { + if (!ref.visible || (ref.ownViewOnly && !lookingAt(ref.axis))) continue; + const Picture& pic = picture(ref); + if (!pic.ok) continue; + const auto c = referenceCorners(ref, pic.aspect); + const glm::vec3 u = c[1] - c[0], v = c[3] - c[0], d = eye_ - c[0]; + const float s = std::clamp(glm::dot(d, u) / std::max(glm::dot(u, u), 1e-12f), 0.0f, 1.0f), t = std::clamp(glm::dot(d, v) / std::max(glm::dot(v, v), 1e-12f), 0.0f, 1.0f); + best = std::min(best, glm::distance(eye_, c[0] + u * s + v * t)); } return std::max(best, 0.001f); } @@ -264,7 +284,7 @@ auto App::updateCamera(ImGuiIO& io) -> void { else { 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)})); + size = std::max(size, reachOf(o).radius * 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(); @@ -308,16 +328,12 @@ auto App::settleView() -> void { auto App::nearFar() -> glm::vec2 { float reach = 1.0f; // (how far from the middle the furthest object reaches, m) for (const Object& o : scene_.objects) { - 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)); - } - reach = std::max(reach, glm::length(o.position) + radius * std::max({std::abs(o.scale.x), std::abs(o.scale.y), std::abs(o.scale.z)})); + reach = std::max(reach, glm::length(o.position) + reachOf(o).radius * std::max({std::abs(o.scale.x), std::abs(o.scale.y), std::abs(o.scale.z)})); } // (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(viewScale() * 0.005f, 1e-5f, 50.0f); + // the nearer it is, the more surfaces flicker through each other. Nothing is nearer than the + // nearest thing, and a quarter of the way to it keeps even the screen's corners clear of it) + const float nearM = std::clamp(std::max(viewScale() * 0.005f, nearest_ * 0.25f), 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; } @@ -1388,7 +1404,7 @@ auto App::draw3D(keel::Renderer& r) -> void { 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 first = gridStep() * 0.1f; - const int count = 20, pieces = 12, levels = 4; + const int count = 20, 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) @@ -1400,31 +1416,37 @@ auto App::draw3D(keel::Renderer& r) -> void { 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) + // worked out at its middle, so it has to be about the same all along it. Each piece + // reaches a quarter further from the eye than the last: from the eye's nearest + // point on the line, steps growing with how far the eye is from the line) 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 glm::vec3 off = eye_ - across * at; + const float gap = std::max(glm::length(off - along * glm::dot(off, along)), 1e-4f); // (from the eye to the line) + for (int sideOf : {-1, 1}) { 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); + const int pieces = std::clamp(int(std::ceil(std::log1p(len / gap) / std::log(1.25f))), 2, 64); + for (int j = 0; j < pieces; ++j) { + const float t0 = near + float(sideOf) * gap * std::expm1(std::log1p(len / gap) * float(j) / float(pieces)); + const float t1 = near + float(sideOf) * gap * std::expm1(std::log1p(len / gap) * float(j + 1) / float(pieces)); + 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); + 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; + const float thick = (1.0f + 0.6f * strong) * kUnits; // (a pixel or so at each end: the pieces meet exactly) + r.drawStreak(lift(a) * kUnits, lift(b) * kUnits, pixelSize(a) * thick, pc, keel::kWhiteTexture, false, pixelSize(b) * thick); + } } }; for (int level = 0; level < levels; ++level) { From b2fc0437bc1ec73f3dbb4838d1a6bf6c6b97577f Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Wed, 21 Oct 2026 17:18:13 +0200 Subject: [PATCH 2/4] The grid worked out for each pixel (as in Blender): one square on its plane, every set of lines showing everywhere their lines are far enough apart on the screen (no patch round the middle, no pieces smearing or fading as you move), a pixel or so wide and smooth, the axes in the colour of the world's axis they run along; in every renderer (drawGrid: a sprite marked by its colour's alpha) Co-Authored-By: Claude Opus 5.5 --- engine/render/renderer.cpp | 8 ++++ engine/render/renderer.h | 5 +++ engine/rhi/gl/gl_legacy.cpp | 43 ++++++++++++++++++++- engine/shaders/sprite.frag | 36 ++++++++++++++++++ engine/shaders/sprite.vert | 5 ++- src/app_view.cpp | 74 ++++--------------------------------- 6 files changed, 101 insertions(+), 70 deletions(-) diff --git a/engine/render/renderer.cpp b/engine/render/renderer.cpp index bfc166a..3a41607 100644 --- a/engine/render/renderer.cpp +++ b/engine/render/renderer.cpp @@ -211,6 +211,12 @@ void Renderer::drawSprite(const glm::vec3& at, float size, const glm::vec4& colo list.push_back({glm::vec4(at, size * 0.5f), color, glm::vec4(0, 0, 0, rotation), glm::vec4(0, 0, 0, tex)}); } +void Renderer::drawGrid(const glm::vec3& centre, const glm::vec3& u, const glm::vec3& v, float first, const glm::vec3& look, float reach) { + if (packet_.sprites.size() >= 8192) return; + // (colour.a < 0 marks it; sprite.vert / sprite.frag) + packet_.sprites.push_back({glm::vec4(centre, reach), glm::vec4(look, -1.0f), glm::vec4(u, first), glm::vec4(v, 0.0f)}); +} + void Renderer::drawStreak(const glm::vec3& from, const glm::vec3& to, float width, const glm::vec4& color, TextureId texture, bool additive, float widthTo) { auto& list = additive ? packet_.glowSprites : packet_.sprites; if (list.size() >= 8192) return; @@ -757,6 +763,7 @@ void Renderer::buildPacket(uint32_t width, uint32_t height) { up = glm::cross(right, f); for (auto* list : {&p.sprites, &p.glowSprites}) for (gpu::Sprite& s : *list) { + if (s.color.a < 0.0f) continue; // (a grid: on its own plane) if (const glm::vec3 half(s.up); glm::dot(half, half) > 0.0f) { // a streak: each end's side faces the eye (sprite.vert) s.right = glm::vec4(camera_.origin, s.right.w); s.posSize.w = -std::max(s.posSize.w, 1e-9f); // (below 0: a streak; its half width at `from`) @@ -765,6 +772,7 @@ void Renderer::buildPacket(uint32_t width, uint32_t height) { s.right = glm::vec4(right, s.right.w), s.up = glm::vec4(up, s.up.w); } std::sort(p.sprites.begin(), p.sprites.end(), [&](const gpu::Sprite& a, const gpu::Sprite& b) { + if ((a.color.a < 0.0f) != (b.color.a < 0.0f)) return a.color.a < 0.0f; // (grids first: they're under everything) return glm::dot(glm::vec3(a.posSize) - camera_.origin, f) > glm::dot(glm::vec3(b.posSize) - camera_.origin, f); }); } diff --git a/engine/render/renderer.h b/engine/render/renderer.h index 873304c..f827d7a 100644 --- a/engine/render/renderer.h +++ b/engine/render/renderer.h @@ -101,6 +101,11 @@ public: // A sprite stretched from `from` to `to`, `width` across (`widthTo` at `to`: < 0, the same), // each end turned to face the eye as much as it can (rain; lines drawn in pieces meet exactly). void drawStreak(const glm::vec3& from, const glm::vec3& to, float width, const glm::vec4& color, TextureId texture, bool additive = false, float widthTo = -1.0f); + // A grid of lines on the plane through `centre` along u and v (unit), drawn for each pixel: + // four sets `first` × 10 apart, each showing by how far apart its lines are on the screen, + // the axes coloured; look: the lines' shade (0 dark, 1 light), how faint, how strong; fading + // out `reach` from the centre. + void drawGrid(const glm::vec3& centre, const glm::vec3& u, const glm::vec3& v, float first, const glm::vec3& look, float reach); // skin: one matrix per joint (anim::skinMatrices) for skinned models; ignored otherwise. // lighting: probe light for moving things (nullptr = lightmap or simple sun shading). void drawModel(ModelId model, const glm::mat4& transform, const glm::vec4& tint = glm::vec4(1.0f), diff --git a/engine/rhi/gl/gl_legacy.cpp b/engine/rhi/gl/gl_legacy.cpp index b1f45d5..3ddde8b 100644 --- a/engine/rhi/gl/gl_legacy.cpp +++ b/engine/rhi/gl/gl_legacy.cpp @@ -366,28 +366,67 @@ void main() { o_color = vec4(u_color.rgb * (0.5 + 0.5 * max(dot(v_normal, normal const char* kSpriteVert = R"( uniform mat4 u_viewProj; uniform vec4 u_posSize, u_right, u_up; // right.w: turn (radians) +uniform vec4 u_color; out vec2 v_uv; +out vec3 v_world; const vec2 kQuad[6] = vec2[](vec2(-1,-1), vec2(1,-1), vec2(1,1), vec2(-1,-1), vec2(1,1), vec2(-1,1)); void main() { vec2 q = kQuad[gl_VertexID]; float c = cos(u_right.w), s = sin(u_right.w); vec2 r = vec2(c * q.x - s * q.y, s * q.x + c * q.y); vec3 world = u_posSize.xyz + (u_right.xyz * r.x + u_up.xyz * r.y) * u_posSize.w; - if (u_posSize.w < 0.0) { // a streak: right.xyz the eye, w its half width at the far end; each end's side faces the eye + if (u_color.a < 0.0) world = u_posSize.xyz + (u_right.xyz * q.x + u_up.xyz * q.y) * u_posSize.w; // a grid: a square on its plane + else if (u_posSize.w < 0.0) { // a streak: right.xyz the eye, w its half width at the far end; each end's side faces the eye vec3 end = u_posSize.xyz + u_up.xyz * q.y, side = cross(u_up.xyz, u_right.xyz - end); world = end + (length(side) > 1e-9 ? normalize(side) : vec3(0.0, 0.0, 1.0)) * q.x * (q.y < 0.0 ? -u_posSize.w : u_right.w); } v_uv = vec2(q.x * 0.5 + 0.5, 0.5 - q.y * 0.5); + v_world = world; gl_Position = u_viewProj * vec4(world, 1.0); gl_Position.z = gl_Position.z * 2.0 - gl_Position.w; } )"; const char* kSpriteFrag = R"( in vec2 v_uv; +in vec3 v_world; uniform sampler2D u_albedo; -uniform vec4 u_color; +uniform vec4 u_color, u_posSize, u_right, u_up; out vec4 o_color; +// A grid (a sprite with colour.a < 0), worked out for each pixel where it lies on its plane: +// four sets of lines each `first` × 10 apart, each set showing by how far apart its lines are on +// the screen (none where they crowd together, stronger where they're far apart), lines about a +// pixel wide, smooth; the axes through the middle in the colour of the world's axis they run along. +// look: x the lines' shade (0 dark, 1 light), y how faint, z how strong; reach: it fades out there. +vec3 gridAxisColor(vec3 d) { + d = abs(d); + return d.x > 0.5 ? vec3(0.9, 0.25, 0.25) : d.y > 0.5 ? vec3(0.3, 0.75, 0.25) : vec3(0.25, 0.45, 0.95); +} +vec4 gridColor(vec3 w, vec3 centre, vec3 u, vec3 v, float first, vec3 look, float reach) { + vec2 c = vec2(dot(w, u), dot(w, v)); + vec2 fw = max(fwidth(c), vec2(1e-9)); + float lines = 0.0; + for (int k = 0; k < 4; ++k) { + float s = first * pow(10.0, float(k)); + vec2 apart = s / fw; // (pixels between this set's lines, each way) + vec2 off = abs(fract(c / s + 0.5) - 0.5) * apart; // (pixels to the nearest of them) + vec2 strong = smoothstep(20.0, 160.0, apart); + vec2 cover = clamp(0.5 * (1.0 + 0.6 * strong) + 0.5 - off, 0.0, 1.0); + vec2 a = cover * smoothstep(5.0, 20.0, apart) * mix(vec2(look.y), vec2(look.z), strong); + lines = max(lines, max(a.x, a.y)); + } + vec2 axis = clamp(1.25 - abs(c) / fw, 0.0, 1.0) * 0.9; // (x: the line along v, y: along u) + float fade = smoothstep(reach, reach * 0.6, length(w - centre)); + vec3 col = vec3(look.x) * lines; + float a = lines; + col = mix(col, gridAxisColor(v), axis.x), a = mix(a, 1.0, axis.x); + col = mix(col, gridAxisColor(u), axis.y), a = mix(a, 1.0, axis.y); + return vec4(col / max(a, 1e-6), a * fade); +} void main() { + if (u_color.a < 0.0) { + o_color = gridColor(v_world, u_posSize.xyz, normalize(u_right.xyz), normalize(u_up.xyz), u_right.w, u_color.rgb, u_posSize.w); + return; + } vec4 t = texture(u_albedo, v_uv); o_color = vec4(t.rgb * u_color.rgb, clamp((t.a - 0.03) / 0.97, 0.0, 1.0) * u_color.a); } diff --git a/engine/shaders/sprite.frag b/engine/shaders/sprite.frag index a55454b..6973b06 100644 --- a/engine/shaders/sprite.frag +++ b/engine/shaders/sprite.frag @@ -9,8 +9,44 @@ PUSH_BLOCK { vec4 up; } pc; layout(location = 0) in vec2 v_uv; +layout(location = 1) in vec3 v_world; +// A grid (a sprite with colour.a < 0), worked out for each pixel where it lies on its plane: +// four sets of lines each `first` × 10 apart, each set showing by how far apart its lines are on +// the screen (none where they crowd together, stronger where they're far apart), lines about a +// pixel wide, smooth; the axes through the middle in the colour of the world's axis they run along. +// look: x the lines' shade (0 dark, 1 light), y how faint, z how strong; reach: it fades out there. +vec3 gridAxisColor(vec3 d) { + d = abs(d); + return d.x > 0.5 ? vec3(0.9, 0.25, 0.25) : d.y > 0.5 ? vec3(0.3, 0.75, 0.25) : vec3(0.25, 0.45, 0.95); +} +vec4 gridColor(vec3 w, vec3 centre, vec3 u, vec3 v, float first, vec3 look, float reach) { + vec2 c = vec2(dot(w, u), dot(w, v)); + vec2 fw = max(fwidth(c), vec2(1e-9)); + float lines = 0.0; + for (int k = 0; k < 4; ++k) { + float s = first * pow(10.0, float(k)); + vec2 apart = s / fw; // (pixels between this set's lines, each way) + vec2 off = abs(fract(c / s + 0.5) - 0.5) * apart; // (pixels to the nearest of them) + vec2 strong = smoothstep(20.0, 160.0, apart); + vec2 cover = clamp(0.5 * (1.0 + 0.6 * strong) + 0.5 - off, 0.0, 1.0); + vec2 a = cover * smoothstep(5.0, 20.0, apart) * mix(vec2(look.y), vec2(look.z), strong); + lines = max(lines, max(a.x, a.y)); + } + vec2 axis = clamp(1.25 - abs(c) / fw, 0.0, 1.0) * 0.9; // (x: the line along v, y: along u) + float fade = smoothstep(reach, reach * 0.6, length(w - centre)); + vec3 col = vec3(look.x) * lines; + float a = lines; + col = mix(col, gridAxisColor(v), axis.x), a = mix(a, 1.0, axis.x); + col = mix(col, gridAxisColor(u), axis.y), a = mix(a, 1.0, axis.y); + return vec4(col / max(a, 1e-6), a * fade); +} + layout(location = 0) out vec4 o_color; void main() { + if (pc.color.a < 0.0) { // a grid: right.w its finest step, color.rgb its look, posSize.w how far it reaches + o_color = gridColor(v_world, pc.posSize.xyz, normalize(pc.right.xyz), normalize(pc.up.xyz), pc.right.w, pc.color.rgb, pc.posSize.w); + return; + } vec4 t = sampleTex(floatBitsToUint(pc.up.w), v_uv); // Block compression leaves a little alpha where there should be none; bright sprites // would show it as a square: cut it. diff --git a/engine/shaders/sprite.vert b/engine/shaders/sprite.vert index 3ee2c40..bc96d4d 100644 --- a/engine/shaders/sprite.vert +++ b/engine/shaders/sprite.vert @@ -9,17 +9,20 @@ PUSH_BLOCK { vec4 up; // camera up, w = texture index (bits) } pc; layout(location = 0) out vec2 v_uv; +layout(location = 1) out vec3 v_world; const vec2 kQuad[6] = vec2[](vec2(-1,-1), vec2(1,-1), vec2(1,1), vec2(-1,-1), vec2(1,1), vec2(-1,1)); void main() { vec2 q = kQuad[VERTEX_ID]; float c = cos(pc.right.w), s = sin(pc.right.w); vec2 r = vec2(c * q.x - s * q.y, s * q.x + c * q.y); vec3 world = pc.posSize.xyz + (pc.right.xyz * r.x + pc.up.xyz * r.y) * pc.posSize.w; - if (pc.posSize.w < 0.0) { // a streak: right.xyz the eye, w its half width at the far end; each end's side faces the eye + if (pc.color.a < 0.0) world = pc.posSize.xyz + (pc.right.xyz * q.x + pc.up.xyz * q.y) * pc.posSize.w; // a grid: a square on its plane + else if (pc.posSize.w < 0.0) { // a streak: right.xyz the eye, w its half width at the far end; each end's side faces the eye vec3 end = pc.posSize.xyz + pc.up.xyz * q.y, side = cross(pc.up.xyz, pc.right.xyz - end); world = end + (length(side) > 1e-9 ? normalize(side) : vec3(0.0, 0.0, 1.0)) * q.x * (q.y < 0.0 ? -pc.posSize.w : pc.right.w); } v_uv = q * 0.5 + 0.5; v_uv.y = 1.0 - v_uv.y; + v_world = world; gl_Position = pc.viewProj * vec4(world, 1.0); } diff --git a/src/app_view.cpp b/src/app_view.cpp index 27fecda..5eb0c5d 100644 --- a/src/app_view.cpp +++ b/src/app_view.cpp @@ -1399,77 +1399,17 @@ auto App::draw3D(keel::Renderer& r) -> void { // 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_) { + // Worked out for each pixel (as in Blender): four sets of lines from a tenth of the + // grid step, each showing by how far apart its lines are on the screen, the axes + // coloured; round the point under the eye, out to where the widest set ends. 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 first = gridStep() * 0.1f; - const int count = 20, 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) - 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. Each piece - // reaches a quarter further from the eye than the last: from the eye's nearest - // point on the line, steps growing with how far the eye is from the line) - const float near = std::clamp(glm::dot(eye_, along), from, to); - const glm::vec3 off = eye_ - across * at; - const float gap = std::max(glm::length(off - along * glm::dot(off, along)), 1e-4f); // (from the eye to the line) - for (int sideOf : {-1, 1}) { - const float len = sideOf < 0 ? near - from : to - near; - if (len <= 0.0f) continue; - const int pieces = std::clamp(int(std::ceil(std::log1p(len / gap) / std::log(1.25f))), 2, 64); - for (int j = 0; j < pieces; ++j) { - const float t0 = near + float(sideOf) * gap * std::expm1(std::log1p(len / gap) * float(j) / float(pieces)); - const float t1 = near + float(sideOf) * gap * std::expm1(std::log1p(len / gap) * float(j + 1) / float(pieces)); - 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); - 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; - const float thick = (1.0f + 0.6f * strong) * kUnits; // (a pixel or so at each end: the pieces meet exactly) - r.drawStreak(lift(a) * kUnits, lift(b) * kUnits, pixelSize(a) * thick, pc, keel::kWhiteTexture, false, pixelSize(b) * thick); - } - } - }; - 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); - } + const float first = gridStep() * 0.1f, reach = std::max(first * 20000.0f, nearFar_.y / kUnits * 0.5f); + const glm::vec3 under = eye_ - n * glm::dot(eye_, n); // (the grid's plane goes through the origin) + const glm::vec3 look = rendered ? glm::vec3(0.0f, 0.14f, 0.4f) : glm::vec3(1.0f, 0.05f, 0.16f); // (dark lines on the sunny ground, light ones in the grey room) + r.drawGrid(under * kUnits, u, v, first * kUnits, look, reach * kUnits); } if (tab_ == Tab::Print) drawPrintBed(r, light); } From ee56ed4005a710b99d2c38c25d0fdbe0a96023e1 Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Wed, 21 Oct 2026 17:30:43 +0200 Subject: [PATCH 3/4] 0.8.1: project.json holds the version, the links, who made it and what it's built on (CMake reads it into fry_project.h and Windows' file details, which had stayed at 0.5.0); the About box lists the creators, the Help menu the website, reporting a problem and the newest version. Steadier far out: the grid's line sizes go by each pixel (not the focus, which jumps between the ground and the sky as you fly, popping every set at once), the far plane doesn't follow that focus when flying, and a wheel notch is at most twice as far as the nearest thing; the site's 0.8.1 Co-Authored-By: Claude Opus 5.5 --- CMakeLists.txt | 3 +- cmake/Project.cmake | 81 +++++++++++++++++++++++++++++++++++++ docs/content.js | 1 + engine/render/renderer.cpp | 4 +- engine/render/renderer.h | 8 ++-- engine/rhi/gl/gl_legacy.cpp | 12 +++--- engine/shaders/sprite.frag | 14 ++++--- project.json | 20 +++++++++ src/CMakeLists.txt | 5 ++- src/app_panels.cpp | 8 +++- src/app_scripts.cpp | 40 +++++++++++++----- src/app_view.cpp | 19 +++++---- src/fry3dx.rc | 25 ------------ 13 files changed, 176 insertions(+), 64 deletions(-) create mode 100644 cmake/Project.cmake create mode 100644 project.json delete mode 100644 src/fry3dx.rc diff --git a/CMakeLists.txt b/CMakeLists.txt index 62b4052..e643c5b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,6 @@ cmake_minimum_required(VERSION 3.25) -project(Fry3DX VERSION 0.8.0 LANGUAGES C CXX) +include(cmake/Project.cmake) # project.json: the version, links, creators +project(Fry3DX VERSION ${FRY_VERSION} LANGUAGES C CXX) set(CMAKE_CXX_SCAN_FOR_MODULES OFF) # (no C++ modules; the scan can pick up a different clang than the NDK's) if(UNIX AND NOT APPLE) add_link_options(-rdynamic) # function names in crash reports diff --git a/cmake/Project.cmake b/cmake/Project.cmake new file mode 100644 index 0000000..33d93a9 --- /dev/null +++ b/cmake/Project.cmake @@ -0,0 +1,81 @@ +# project.json, read before project(): FRY_VERSION and the rest as FRY_; fry_project_header() +# writes them as C++ constants (fry_project.h) and Windows' file details (fry3dx.rc). +set(FRY_PROJECT_JSON ${CMAKE_CURRENT_LIST_DIR}/../project.json) +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${FRY_PROJECT_JSON}) +file(READ ${FRY_PROJECT_JSON} FRY_JSON) +foreach(key name long version tagline description website repo issues releases license copyright company look) + string(TOUPPER ${key} upper) + string(JSON FRY_${upper} GET "${FRY_JSON}" ${key}) +endforeach() + +# out: the header's path; rc: Windows' file details (the version as 0,8,1,0 too). +function(fry_project_header out rc) + set(h "// Made by CMake from project.json: change that, not this.\n#pragma once\n#include \n\nnamespace fry::project {\n") + foreach(key name long version tagline description website repo issues releases license copyright company look) + string(TOUPPER ${key} upper) + set(ident ${key}) + if(key STREQUAL "long") + set(ident long_) # (a C++ word) + endif() + string(APPEND h "inline constexpr const char* ${ident} = R\"fry(${FRY_${upper}})fry\";\n") + endforeach() + string(APPEND h "\nstruct Creator {\n const char* name;\n const char* role;\n const char* url;\n};\ninline constexpr Creator kCreators[] = {\n") + string(JSON n LENGTH "${FRY_JSON}" creators) + math(EXPR last "${n} - 1") + foreach(i RANGE ${last}) + string(JSON who GET "${FRY_JSON}" creators ${i} name) + string(JSON role ERROR_VARIABLE none GET "${FRY_JSON}" creators ${i} role) + string(JSON url ERROR_VARIABLE none GET "${FRY_JSON}" creators ${i} url) + if(role MATCHES "NOTFOUND$") + set(role "") + endif() + if(url MATCHES "NOTFOUND$") + set(url "") + endif() + string(APPEND h " {R\"fry(${who})fry\", R\"fry(${role})fry\", R\"fry(${url})fry\"},\n") + endforeach() + string(APPEND h "};\ninline constexpr std::span creators{kCreators};\n\n// What it's built on (for the About box).\ninline constexpr const char* builtWith = R\"fry(") + string(JSON n LENGTH "${FRY_JSON}" builtWith) + math(EXPR last "${n} - 1") + foreach(i RANGE ${last}) + string(JSON part GET "${FRY_JSON}" builtWith ${i}) + if(i GREATER 0) + if(i EQUAL last) + string(APPEND h " and ") + else() + string(APPEND h ", ") + endif() + endif() + string(APPEND h "${part}") + endforeach() + string(APPEND h ")fry\";\n} // namespace fry::project\n") + file(CONFIGURE OUTPUT ${out} CONTENT "${h}" @ONLY) + + string(REPLACE "." "," commas "${FRY_VERSION}") + file(CONFIGURE OUTPUT ${rc} CONTENT "// Made by CMake from project.json (Windows: the program's icon and details, Explorer's Properties). +1 ICON \"${PROJECT_SOURCE_DIR}/branding/fry3dx.ico\" +1 VERSIONINFO +FILEVERSION ${commas},0 +PRODUCTVERSION ${commas},0 +BEGIN + BLOCK \"StringFileInfo\" + BEGIN + BLOCK \"040904b0\" + BEGIN + VALUE \"CompanyName\", \"${FRY_COMPANY}\" + VALUE \"FileDescription\", \"${FRY_NAME} - ${FRY_LONG}\" + VALUE \"FileVersion\", \"${FRY_VERSION}\" + VALUE \"InternalName\", \"fry3dx\" + VALUE \"LegalCopyright\", \"${FRY_COPYRIGHT}, ${FRY_LICENSE}\" + VALUE \"OriginalFilename\", \"fry3dx.exe\" + VALUE \"ProductName\", \"${FRY_NAME}\" + VALUE \"ProductVersion\", \"${FRY_VERSION}\" + END + END + BLOCK \"VarFileInfo\" + BEGIN + VALUE \"Translation\", 0x409, 1200 + END +END +" @ONLY) +endfunction() diff --git a/docs/content.js b/docs/content.js index 764a341..f2c2920 100644 --- a/docs/content.js +++ b/docs/content.js @@ -129,6 +129,7 @@ fry.frame()`, }, versions: [ + { v: "0.8.1", date: "2026-10-21", items: ["Fixes", "Close up to big or long flat shapes the view no longer breaks into blocks, jitters or loses the sky and grid", "The grid drawn as in Blender: every size of line everywhere it's far enough apart, smooth, nothing smeared or popping as you move", "Far from the middle of the scene: steadier, and the wheel doesn't leap", "Help menu: the website, reporting a problem, the newest version; the About box lists who made it"] }, { v: "0.8", date: "2026-10-21", items: ["Quicker to use, exact to size", "Find any command by name (F3), the tools round the mouse (Space), your own keys, the undo history, workspaces", "Typed sizes while dragging, snapping, measuring, a line's length while drawing", "Cuts you can still move; combining that's exact", "3D text, spin and screw, copies along a path, crease, edge slide, mirror editing", "Import STL, OBJ, 3MF and glTF", "Print: a scaler, thin walls, a slice preview, repair all at once", "Painting both sides and inside a stencil; plug-ins can add mouse tools", "A fly camera as in games (right drag, WASD, the wheel forward and back, Shift + wheel for speed), or the orbiting one (Shift+C)", "Smoother: dragging settings in Properties, a grid that doesn't crawl or jump, clean close-ups in Rendered", "Far away things no longer vanish; steadier and smoother on phones, which stay sideways"] }, { v: "0.7", date: "2026-10-21", items: ["Steadier and tidier", "Cutting a shape inside an earlier cut no longer crashes, and cuts come out closed", "Edges hidden behind the surface stay hidden on big or cut-up shapes", "Every panel has a least size, so it stays readable; the first layout fits small windows", "Eye and padlock buttons in Layers; the tabs fit narrow windows", "A failed tool keeps your redo"] }, { v: "0.6", date: "2026-10-20", items: ["Sculpting, scripts and a Mac app", "Sculpting: raise, puff out, smooth, flatten, pinch and grab", "Materials shown in the view, sixteen to start from", "Scripts: Lua and a Script panel, with examples, kept in the .fry", "Plug-ins: menu commands and materials, three to start with", "Your own materials", "Soft selection, bridge, push / pull, smooth points, show only the selected", "macOS: one app for Apple silicon and Intel", "A new look (Niveus): light and dark, following the desktop", "A new logo", "Drop a file on the window to open it"] }, diff --git a/engine/render/renderer.cpp b/engine/render/renderer.cpp index 3a41607..9b5c89b 100644 --- a/engine/render/renderer.cpp +++ b/engine/render/renderer.cpp @@ -211,10 +211,10 @@ void Renderer::drawSprite(const glm::vec3& at, float size, const glm::vec4& colo list.push_back({glm::vec4(at, size * 0.5f), color, glm::vec4(0, 0, 0, rotation), glm::vec4(0, 0, 0, tex)}); } -void Renderer::drawGrid(const glm::vec3& centre, const glm::vec3& u, const glm::vec3& v, float first, const glm::vec3& look, float reach) { +void Renderer::drawGrid(const glm::vec3& centre, const glm::vec3& u, const glm::vec3& v, float metre, const glm::vec3& look, float reach) { if (packet_.sprites.size() >= 8192) return; // (colour.a < 0 marks it; sprite.vert / sprite.frag) - packet_.sprites.push_back({glm::vec4(centre, reach), glm::vec4(look, -1.0f), glm::vec4(u, first), glm::vec4(v, 0.0f)}); + packet_.sprites.push_back({glm::vec4(centre, reach), glm::vec4(look, -1.0f), glm::vec4(u, metre), glm::vec4(v, 0.0f)}); } void Renderer::drawStreak(const glm::vec3& from, const glm::vec3& to, float width, const glm::vec4& color, TextureId texture, bool additive, float widthTo) { diff --git a/engine/render/renderer.h b/engine/render/renderer.h index f827d7a..5f0e9a4 100644 --- a/engine/render/renderer.h +++ b/engine/render/renderer.h @@ -102,10 +102,10 @@ public: // each end turned to face the eye as much as it can (rain; lines drawn in pieces meet exactly). void drawStreak(const glm::vec3& from, const glm::vec3& to, float width, const glm::vec4& color, TextureId texture, bool additive = false, float widthTo = -1.0f); // A grid of lines on the plane through `centre` along u and v (unit), drawn for each pixel: - // four sets `first` × 10 apart, each showing by how far apart its lines are on the screen, - // the axes coloured; look: the lines' shade (0 dark, 1 light), how faint, how strong; fading - // out `reach` from the centre. - void drawGrid(const glm::vec3& centre, const glm::vec3& u, const glm::vec3& v, float first, const glm::vec3& look, float reach); + // every 10^n metres (`metre`: units in one), each set showing by how far apart its lines are + // on the screen, the axes coloured; look: the lines' shade (0 dark, 1 light), how faint, how + // strong; fading out `reach` from the centre. + void drawGrid(const glm::vec3& centre, const glm::vec3& u, const glm::vec3& v, float metre, const glm::vec3& look, float reach); // skin: one matrix per joint (anim::skinMatrices) for skinned models; ignored otherwise. // lighting: probe light for moving things (nullptr = lightmap or simple sun shading). void drawModel(ModelId model, const glm::mat4& transform, const glm::vec4& tint = glm::vec4(1.0f), diff --git a/engine/rhi/gl/gl_legacy.cpp b/engine/rhi/gl/gl_legacy.cpp index 3ddde8b..0b1536a 100644 --- a/engine/rhi/gl/gl_legacy.cpp +++ b/engine/rhi/gl/gl_legacy.cpp @@ -393,20 +393,22 @@ uniform sampler2D u_albedo; uniform vec4 u_color, u_posSize, u_right, u_up; out vec4 o_color; // A grid (a sprite with colour.a < 0), worked out for each pixel where it lies on its plane: -// four sets of lines each `first` × 10 apart, each set showing by how far apart its lines are on -// the screen (none where they crowd together, stronger where they're far apart), lines about a -// pixel wide, smooth; the axes through the middle in the colour of the world's axis they run along. +// lines every 10^n metres (`metre`: units in one), the four sets from the finest that isn't +// crowded together here, each showing by how far apart its lines are on the screen (none where +// they crowd, stronger where they're far apart: nothing jumps as the view moves), about a pixel +// wide, smooth; the axes through the middle in the colour of the world's axis they run along. // look: x the lines' shade (0 dark, 1 light), y how faint, z how strong; reach: it fades out there. vec3 gridAxisColor(vec3 d) { d = abs(d); return d.x > 0.5 ? vec3(0.9, 0.25, 0.25) : d.y > 0.5 ? vec3(0.3, 0.75, 0.25) : vec3(0.25, 0.45, 0.95); } -vec4 gridColor(vec3 w, vec3 centre, vec3 u, vec3 v, float first, vec3 look, float reach) { +vec4 gridColor(vec3 w, vec3 centre, vec3 u, vec3 v, float metre, vec3 look, float reach) { vec2 c = vec2(dot(w, u), dot(w, v)); vec2 fw = max(fwidth(c), vec2(1e-9)); + float finest = floor(log(min(fw.x, fw.y) * 5.0 / metre) / log(10.0)); // (10^this m: 5 pixels apart or more) float lines = 0.0; for (int k = 0; k < 4; ++k) { - float s = first * pow(10.0, float(k)); + float s = metre * pow(10.0, finest + float(k)); vec2 apart = s / fw; // (pixels between this set's lines, each way) vec2 off = abs(fract(c / s + 0.5) - 0.5) * apart; // (pixels to the nearest of them) vec2 strong = smoothstep(20.0, 160.0, apart); diff --git a/engine/shaders/sprite.frag b/engine/shaders/sprite.frag index 6973b06..32d352f 100644 --- a/engine/shaders/sprite.frag +++ b/engine/shaders/sprite.frag @@ -11,20 +11,22 @@ PUSH_BLOCK { layout(location = 0) in vec2 v_uv; layout(location = 1) in vec3 v_world; // A grid (a sprite with colour.a < 0), worked out for each pixel where it lies on its plane: -// four sets of lines each `first` × 10 apart, each set showing by how far apart its lines are on -// the screen (none where they crowd together, stronger where they're far apart), lines about a -// pixel wide, smooth; the axes through the middle in the colour of the world's axis they run along. +// lines every 10^n metres (`metre`: units in one), the four sets from the finest that isn't +// crowded together here, each showing by how far apart its lines are on the screen (none where +// they crowd, stronger where they're far apart: nothing jumps as the view moves), about a pixel +// wide, smooth; the axes through the middle in the colour of the world's axis they run along. // look: x the lines' shade (0 dark, 1 light), y how faint, z how strong; reach: it fades out there. vec3 gridAxisColor(vec3 d) { d = abs(d); return d.x > 0.5 ? vec3(0.9, 0.25, 0.25) : d.y > 0.5 ? vec3(0.3, 0.75, 0.25) : vec3(0.25, 0.45, 0.95); } -vec4 gridColor(vec3 w, vec3 centre, vec3 u, vec3 v, float first, vec3 look, float reach) { +vec4 gridColor(vec3 w, vec3 centre, vec3 u, vec3 v, float metre, vec3 look, float reach) { vec2 c = vec2(dot(w, u), dot(w, v)); vec2 fw = max(fwidth(c), vec2(1e-9)); + float finest = floor(log(min(fw.x, fw.y) * 5.0 / metre) / log(10.0)); // (10^this m: 5 pixels apart or more) float lines = 0.0; for (int k = 0; k < 4; ++k) { - float s = first * pow(10.0, float(k)); + float s = metre * pow(10.0, finest + float(k)); vec2 apart = s / fw; // (pixels between this set's lines, each way) vec2 off = abs(fract(c / s + 0.5) - 0.5) * apart; // (pixels to the nearest of them) vec2 strong = smoothstep(20.0, 160.0, apart); @@ -43,7 +45,7 @@ vec4 gridColor(vec3 w, vec3 centre, vec3 u, vec3 v, float first, vec3 look, floa layout(location = 0) out vec4 o_color; void main() { - if (pc.color.a < 0.0) { // a grid: right.w its finest step, color.rgb its look, posSize.w how far it reaches + if (pc.color.a < 0.0) { // a grid: right.w the units in a metre, color.rgb its look, posSize.w how far it reaches o_color = gridColor(v_world, pc.posSize.xyz, normalize(pc.right.xyz), normalize(pc.up.xyz), pc.right.w, pc.color.rgb, pc.posSize.w); return; } diff --git a/project.json b/project.json new file mode 100644 index 0000000..0455066 --- /dev/null +++ b/project.json @@ -0,0 +1,20 @@ +{ + "_": "Fry3DX's details, in one place: the build reads them (the version, the About box, the Help menu, Windows' file details). Change them here; CMake picks them up on the next build.", + "name": "Fry3DX", + "long": "Frying 3D Extreme", + "version": "0.8.1", + "tagline": "A 3D modeller that does what you'd expect.", + "description": "A 3D modeller that does what you'd expect: pick a tool, click and drag. Model, sculpt, paint, render and get it ready to print, and write a script when clicking gets repetitive.", + "website": "https://nexoniarz.github.io/Fry3DX/", + "repo": "https://github.com/Nexoniarz/Fry3DX", + "issues": "https://github.com/Nexoniarz/Fry3DX/issues", + "releases": "https://github.com/Nexoniarz/Fry3DX/releases/latest", + "license": "Apache License 2.0", + "copyright": "Copyright 2026 Nexoniarz", + "company": "Nexoniarz", + "look": "Niveus", + "creators": [ + { "name": "Nexoniarz", "role": "Made it: the idea, the design, the code, the look", "url": "https://github.com/Nexoniarz" } + ], + "builtWith": ["SDL 3", "Dear ImGui", "Lua 5.4", "GLM", "Manifold", "zstd", "stb", "volk", "VMA"] +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4dd7e33..cac0173 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -47,12 +47,13 @@ else() add_executable(fry3dx ${FRY_APP_SOURCES}) endif() if(WIN32) # a window program (no console window), with its icon - target_sources(fry3dx PRIVATE fry3dx.rc) + target_sources(fry3dx PRIVATE ${CMAKE_BINARY_DIR}/generated/fry3dx.rc) set_target_properties(fry3dx PROPERTIES WIN32_EXECUTABLE ON) target_link_options(fry3dx PRIVATE /ENTRY:mainCRTStartup) endif() target_link_libraries(fry3dx PRIVATE fry_core fry_script keel stb) -target_compile_definitions(fry3dx PRIVATE FRY_VERSION="${PROJECT_VERSION}") +fry_project_header(${CMAKE_BINARY_DIR}/generated/fry_project.h ${CMAKE_BINARY_DIR}/generated/fry3dx.rc) # (project.json) +target_include_directories(fry3dx PRIVATE ${CMAKE_BINARY_DIR}/generated) add_dependencies(fry3dx content) foreach(t fry_core fry_script fry3dx) if(MSVC) diff --git a/src/app_panels.cpp b/src/app_panels.cpp index da9d8bb..374ca92 100644 --- a/src/app_panels.cpp +++ b/src/app_panels.cpp @@ -1,5 +1,6 @@ // Fry3DX: the panels: layers, properties, menus, the commands and their keys. #include "app.h" +#include "fry_project.h" using namespace fry; namespace stdfs = std::filesystem; @@ -788,7 +789,12 @@ auto App::menus() -> void { if (ImGui::BeginMenu("Help")) { if (ImGui::MenuItem("Keys and mouse", "F1")) showHelp_ = true; commandItem("help.search"); - if (ImGui::MenuItem("About Fry3DX")) showAbout_ = true; + ImGui::Separator(); + if (ImGui::MenuItem("Website")) SDL_OpenURL(project::website); + if (ImGui::MenuItem("Report a problem")) SDL_OpenURL(project::issues); + if (ImGui::MenuItem("Newest version")) SDL_OpenURL(project::releases); + ImGui::Separator(); + if (ImGui::MenuItem(std::format("About {}", project::name).c_str())) showAbout_ = true; ImGui::EndMenu(); } } diff --git a/src/app_scripts.cpp b/src/app_scripts.cpp index f0460a6..52ec120 100644 --- a/src/app_scripts.cpp +++ b/src/app_scripts.cpp @@ -1,5 +1,6 @@ // Fry3DX: scripts, your own materials, plug-ins, the windows that go with them. #include "app.h" +#include "fry_project.h" using namespace fry; namespace stdfs = std::filesystem; @@ -747,14 +748,13 @@ auto App::aboutWindow() -> void { logoImage(logo); ImGui::Dummy(ImVec2(0, 6)); ImGui::PushFont(keel::ui::font(keel::ui::Font::Title), 30.0f); - centred("Fry3DX", roleColor(Role::Text)); + centred(project::name, roleColor(Role::Text)); ImGui::PopFont(); - centred("Frying 3D Extreme", accentText()); - centred("Version " FRY_VERSION, roleColor(Role::TextSecondary)); + centred(project::long_, accentText()); + centred(std::format("Version {}", project::version).c_str(), roleColor(Role::TextSecondary)); ImGui::Dummy(ImVec2(0, 8)); ImGui::PushStyleColor(ImGuiCol_Text, roleColor(Role::TextSecondary)); - ImGui::TextWrapped("A 3D modeller that does what you'd expect: pick a tool, click and drag. Model, sculpt, paint, render and get it ready " - "to print, and write a script when clicking gets repetitive."); + ImGui::TextWrapped("%s", project::description); ImGui::PopStyleColor(); ImGui::Dummy(ImVec2(0, 4)); @@ -762,7 +762,7 @@ auto App::aboutWindow() -> void { const keel::GpuInfo gpu = r_->gpuInfo(); const auto& machine = keel::sys::machine(); const std::vector> rows = { - {"Version", FRY_VERSION}, {"Drawn with", r_->apiName()}, {"Graphics", gpu.name.empty() ? "?" : gpu.name}, {"System", machine.os}, {"Processor", machine.cpu}, + {"Version", project::version}, {"Drawn with", r_->apiName()}, {"Graphics", gpu.name.empty() ? "?" : gpu.name}, {"System", machine.os}, {"Processor", machine.cpu}, {"Plug-ins", std::format("{} loaded", std::count_if(plugins_.begin(), plugins_.end(), [](const PluginEntry& e) { return e.p && e.error.empty(); }))}}; ImGui::BeginChild("details", ImVec2(0, 0), ImGuiChildFlags_FrameStyle | ImGuiChildFlags_AutoResizeY); if (ImGui::BeginTable("details", 2, ImGuiTableFlags_SizingFixedFit)) { @@ -781,21 +781,39 @@ auto App::aboutWindow() -> void { ImGui::Dummy(ImVec2(0, 4)); const float bw = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x * 2) / 3; pushAccent(); - if (ImGui::Button("Website", ImVec2(bw, 0))) SDL_OpenURL("https://nexoniarz.github.io/Fry3DX/"); + if (ImGui::Button("Website", ImVec2(bw, 0))) SDL_OpenURL(project::website); popAccent(); ImGui::SameLine(); - if (ImGui::Button("Source on GitHub", ImVec2(bw, 0))) SDL_OpenURL("https://github.com/Nexoniarz/Fry3DX"); + if (ImGui::Button("Source on GitHub", ImVec2(bw, 0))) SDL_OpenURL(project::repo); ImGui::SameLine(); if (ImGui::Button("Copy details", ImVec2(bw, 0))) { - std::string text = "Fry3DX"; + std::string text = project::name; for (const auto& [k, v] : rows) text += std::format("\n{}: {}", k, v); SDL_SetClipboardText(text.c_str()); say("The details are copied: paste them into a bug report"); } + // Who made it (project.json's creators: a name with a link opens it). ImGui::Dummy(ImVec2(0, 6)); + ImGui::TextColored(roleColor(Role::TextTertiary), "Made by"); + for (const project::Creator& c : project::creators) { + ImGui::PushID(c.name); + if (*c.url) { + ImGui::PushStyleColor(ImGuiCol_Text, accentText()); + if (ImGui::Selectable(c.name, false, ImGuiSelectableFlags_None, ImVec2(ImGui::CalcTextSize(c.name).x, 0))) SDL_OpenURL(c.url); + ImGui::PopStyleColor(); + if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", c.url); + } else { + ImGui::TextUnformatted(c.name); + } + if (*c.role) { + ImGui::SameLine(); + ImGui::TextColored(roleColor(Role::TextSecondary), " %s", c.role); + } + ImGui::PopID(); + } + ImGui::Dummy(ImVec2(0, 4)); ImGui::PushStyleColor(ImGuiCol_Text, roleColor(Role::TextTertiary)); - ImGui::TextWrapped("Made by Nexoniarz, open source (Apache License 2.0). Built on SDL 3, Dear ImGui, Lua 5.4, GLM, zstd, stb, volk and VMA; " - "the look is Niveus."); + ImGui::TextWrapped("Open source (%s). Built on %s; the look is %s.", project::license, project::builtWith, project::look); ImGui::PopStyleColor(); ImGui::End(); } diff --git a/src/app_view.cpp b/src/app_view.cpp index 5eb0c5d..81b727c 100644 --- a/src/app_view.cpp +++ b/src/app_view.cpp @@ -285,7 +285,9 @@ auto App::updateCamera(ImGuiIO& io) -> void { float size = 0.0f; // (the biggest object's size: the least step is a little of it) for (const Object& o : scene_.objects) size = std::max(size, reachOf(o).radius * 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(); + // (what's ahead: no further than ten times how far the nearest thing is, so with only + // the sky or the far ground ahead a notch doesn't leap) + const float step = std::clamp(std::max(std::min(dist_, nearest_ * 10.0f) * 0.2f, size * 0.05f), 0.001f, 500.0f) * fry_movespeed.f(); target_ += viewDir() * step * io.MouseWheel; refocus(); } @@ -334,7 +336,10 @@ auto App::nearFar() -> glm::vec2 { // the nearer it is, the more surfaces flicker through each other. Nothing is nearer than the // nearest thing, and a quarter of the way to it keeps even the screen's corners clear of it) const float nearM = std::clamp(std::max(viewScale() * 0.005f, nearest_ * 0.25f), 1e-5f, 50.0f); - const float farM = std::max({dist_ * 60.0f, (glm::length(eye_) + reach) * 2.0f, 100.0f}); + // (far enough for the whole scene and the ground out to near the horizon; what it looks at + // too when it turns round that (flying, that jumps between the ground and the sky as it moves)) + const bool flying = view_ == View::Perspective && fry_camera.i() != 1; + const float farM = std::max({flying ? 0.0f : dist_ * 60.0f, (glm::length(eye_) + reach) * 2.0f, std::abs(eye_.z) * 2000.0f, 100.0f}); return glm::vec2(nearM, farM) * kUnits; } @@ -1399,17 +1404,17 @@ auto App::draw3D(keel::Renderer& r) -> void { // 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_) { - // Worked out for each pixel (as in Blender): four sets of lines from a tenth of the - // grid step, each showing by how far apart its lines are on the screen, the axes - // coloured; round the point under the eye, out to where the widest set ends. + // Worked out for each pixel (as in Blender): lines every 10^n metres, each set showing by + // how far apart its lines are on the screen, the axes coloured; round the point under the + // eye, out to most of the way to the far plane. 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 first = gridStep() * 0.1f, reach = std::max(first * 20000.0f, nearFar_.y / kUnits * 0.5f); + const float reach = nearFar_.y / kUnits * 0.9f; const glm::vec3 under = eye_ - n * glm::dot(eye_, n); // (the grid's plane goes through the origin) const glm::vec3 look = rendered ? glm::vec3(0.0f, 0.14f, 0.4f) : glm::vec3(1.0f, 0.05f, 0.16f); // (dark lines on the sunny ground, light ones in the grey room) - r.drawGrid(under * kUnits, u, v, first * kUnits, look, reach * kUnits); + r.drawGrid(under * kUnits, u, v, kUnits, look, reach * kUnits); } if (tab_ == Tab::Print) drawPrintBed(r, light); } diff --git a/src/fry3dx.rc b/src/fry3dx.rc deleted file mode 100644 index 5ae697b..0000000 --- a/src/fry3dx.rc +++ /dev/null @@ -1,25 +0,0 @@ -// Windows: the program's icon and details (Explorer's Properties). -1 ICON "../branding/fry3dx.ico" -1 VERSIONINFO -FILEVERSION 0,5,0,0 -PRODUCTVERSION 0,5,0,0 -BEGIN - BLOCK "StringFileInfo" - BEGIN - BLOCK "040904b0" - BEGIN - VALUE "CompanyName", "Nexoniarz" - VALUE "FileDescription", "Fry3DX - Frying 3D Extreme" - VALUE "FileVersion", "0.5.0" - VALUE "InternalName", "fry3dx" - VALUE "LegalCopyright", "Copyright 2026 Nexoniarz, Apache License 2.0" - VALUE "OriginalFilename", "fry3dx.exe" - VALUE "ProductName", "Fry3DX" - VALUE "ProductVersion", "0.5.0" - END - END - BLOCK "VarFileInfo" - BEGIN - VALUE "Translation", 0x409, 1200 - END -END From 1ee38c966cd84da78adc524cd4dc9f1440db9cca Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Wed, 21 Oct 2026 17:41:25 +0200 Subject: [PATCH 4/4] Far out stays steady: the grid's lines worked out from small numbers (its square's middle a whole number of kilometres from the world's, found in doubles; where on it from there), so far from the origin they no longer turn grainy and shake; the sun's shadow maps snap to their texels in doubles; packaging reads the version from project.json Co-Authored-By: Claude Opus 5.5 --- android/build_apk.py | 8 +++----- engine/render/renderer.cpp | 20 +++++++++++++++----- engine/render/renderer.h | 9 +++++---- engine/rhi/gl/gl_legacy.cpp | 35 +++++++++++++++++++---------------- engine/shaders/sprite.frag | 33 ++++++++++++++++++--------------- engine/shaders/sprite.vert | 4 ++-- src/app_view.cpp | 5 ++--- tools/package.py | 8 +++----- 8 files changed, 67 insertions(+), 55 deletions(-) diff --git a/android/build_apk.py b/android/build_apk.py index ca10d6e..288d694 100644 --- a/android/build_apk.py +++ b/android/build_apk.py @@ -11,6 +11,7 @@ Signed with ~/.local/share/Nexoniarz/Fry3DX-signing/release.keystore (made the f or the key in $FRY_KEYSTORE / $FRY_KEYSTORE_PASS. Keep it safe and keep using it: Android only updates an app signed with the key it was installed with. """ +import json import os import secrets import shutil @@ -30,11 +31,8 @@ def run(*cmd, **kw): def app_version(): - with open(os.path.join(ROOT, "CMakeLists.txt")) as f: - for line in f: - if line.startswith("project(Fry3DX VERSION "): - return line.split()[2] - return "0" + with open(os.path.join(ROOT, "project.json")) as f: # (the version and the rest live there) + return json.load(f)["version"] def main(): diff --git a/engine/render/renderer.cpp b/engine/render/renderer.cpp index 9b5c89b..71c91dd 100644 --- a/engine/render/renderer.cpp +++ b/engine/render/renderer.cpp @@ -211,10 +211,18 @@ void Renderer::drawSprite(const glm::vec3& at, float size, const glm::vec4& colo list.push_back({glm::vec4(at, size * 0.5f), color, glm::vec4(0, 0, 0, rotation), glm::vec4(0, 0, 0, tex)}); } -void Renderer::drawGrid(const glm::vec3& centre, const glm::vec3& u, const glm::vec3& v, float metre, const glm::vec3& look, float reach) { +void Renderer::drawGrid(const glm::dvec3& under, const glm::vec3& u, const glm::vec3& v, float metre, bool dark, float reach) { if (packet_.sprites.size() >= 8192) return; + // Its middle a whole number of kilometres along u and v (so the lines are worked out from + // small numbers, sprite.frag): a square round it out to `reach` past the eye. + const double km = 1000.0 * double(metre); + const glm::dvec3 du(u), dv(v); + const double cu = std::round(glm::dot(under, du) / km) * km, cv = std::round(glm::dot(under, dv) / km) * km; + const glm::dvec3 centre = du * cu + dv * cv; + const glm::vec2 eye(float(glm::dot(under, du) - cu), float(glm::dot(under, dv) - cv)); + const float half = reach + glm::length(eye); // (colour.a < 0 marks it; sprite.vert / sprite.frag) - packet_.sprites.push_back({glm::vec4(centre, reach), glm::vec4(look, -1.0f), glm::vec4(u, metre), glm::vec4(v, 0.0f)}); + packet_.sprites.push_back({glm::vec4(glm::vec3(centre), half), glm::vec4(eye, 0.0f, dark ? -1.0f : -2.0f), glm::vec4(u, metre), glm::vec4(v, reach)}); } void Renderer::drawStreak(const glm::vec3& from, const glm::vec3& to, float width, const glm::vec4& color, TextureId texture, bool additive, float widthTo) { @@ -453,11 +461,13 @@ void Renderer::buildPacket(uint32_t width, uint32_t height) { for (const glm::vec3& q : corners) radius = std::max(radius, glm::length(q - center)); radius = std::ceil(radius / 16.0f) * 16.0f; const float texel = 2.0f * radius / cascadeSize; - glm::mat4 lightView = glm::lookAt(glm::vec3(0), -sunDir_, up); - glm::vec4 lc = lightView * glm::vec4(center, 1); + // (in doubles: far from the origin a float's steps are near a texel's, and it'd shimmer) + const glm::dmat4 lightView0 = glm::lookAt(glm::dvec3(0), -glm::dvec3(sunDir_), glm::dvec3(up)); + glm::dvec4 lc = lightView0 * glm::dvec4(glm::dvec3(center), 1); lc.x = std::floor(lc.x / texel) * texel; lc.y = std::floor(lc.y / texel) * texel; - center = glm::vec3(glm::inverse(lightView) * lc); + center = glm::vec3(glm::inverse(lightView0) * lc); + glm::mat4 lightView; lightView = glm::lookAt(center + sunDir_ * 8000.0f, center, up); frame.cascades[c] = glm::ortho(-radius, radius, -radius, radius, 1.0f, 16000.0f) * lightView; frame.splits[c] = f; diff --git a/engine/render/renderer.h b/engine/render/renderer.h index 5f0e9a4..be42d7a 100644 --- a/engine/render/renderer.h +++ b/engine/render/renderer.h @@ -101,11 +101,12 @@ public: // A sprite stretched from `from` to `to`, `width` across (`widthTo` at `to`: < 0, the same), // each end turned to face the eye as much as it can (rain; lines drawn in pieces meet exactly). void drawStreak(const glm::vec3& from, const glm::vec3& to, float width, const glm::vec4& color, TextureId texture, bool additive = false, float widthTo = -1.0f); - // A grid of lines on the plane through `centre` along u and v (unit), drawn for each pixel: + // A grid of lines on the plane through the origin along u and v (unit), drawn for each pixel: // every 10^n metres (`metre`: units in one), each set showing by how far apart its lines are - // on the screen, the axes coloured; look: the lines' shade (0 dark, 1 light), how faint, how - // strong; fading out `reach` from the centre. - void drawGrid(const glm::vec3& centre, const glm::vec3& u, const glm::vec3& v, float metre, const glm::vec3& look, float reach); + // on the screen, the axes coloured; dark lines (on the sunny ground) or light ones (in the + // grey room); fading out `reach` from `under` (the point under the eye, as a double: far out, + // a float's too coarse for lines a pixel wide). + void drawGrid(const glm::dvec3& under, const glm::vec3& u, const glm::vec3& v, float metre, bool dark, float reach); // skin: one matrix per joint (anim::skinMatrices) for skinned models; ignored otherwise. // lighting: probe light for moving things (nullptr = lightmap or simple sun shading). void drawModel(ModelId model, const glm::mat4& transform, const glm::vec4& tint = glm::vec4(1.0f), diff --git a/engine/rhi/gl/gl_legacy.cpp b/engine/rhi/gl/gl_legacy.cpp index 0b1536a..68ee323 100644 --- a/engine/rhi/gl/gl_legacy.cpp +++ b/engine/rhi/gl/gl_legacy.cpp @@ -368,7 +368,7 @@ uniform mat4 u_viewProj; uniform vec4 u_posSize, u_right, u_up; // right.w: turn (radians) uniform vec4 u_color; out vec2 v_uv; -out vec3 v_world; +out vec2 v_rel; // (a grid: where on it, from its middle) const vec2 kQuad[6] = vec2[](vec2(-1,-1), vec2(1,-1), vec2(1,1), vec2(-1,-1), vec2(1,1), vec2(-1,1)); void main() { vec2 q = kQuad[gl_VertexID]; @@ -381,43 +381,46 @@ void main() { world = end + (length(side) > 1e-9 ? normalize(side) : vec3(0.0, 0.0, 1.0)) * q.x * (q.y < 0.0 ? -u_posSize.w : u_right.w); } v_uv = vec2(q.x * 0.5 + 0.5, 0.5 - q.y * 0.5); - v_world = world; + v_rel = u_color.a < 0.0 ? q * u_posSize.w : vec2(0.0); gl_Position = u_viewProj * vec4(world, 1.0); gl_Position.z = gl_Position.z * 2.0 - gl_Position.w; } )"; const char* kSpriteFrag = R"( in vec2 v_uv; -in vec3 v_world; +in vec2 v_rel; uniform sampler2D u_albedo; uniform vec4 u_color, u_posSize, u_right, u_up; out vec4 o_color; -// A grid (a sprite with colour.a < 0), worked out for each pixel where it lies on its plane: -// lines every 10^n metres (`metre`: units in one), the four sets from the finest that isn't -// crowded together here, each showing by how far apart its lines are on the screen (none where -// they crowd, stronger where they're far apart: nothing jumps as the view moves), about a pixel -// wide, smooth; the axes through the middle in the colour of the world's axis they run along. -// look: x the lines' shade (0 dark, 1 light), y how faint, z how strong; reach: it fades out there. +// A grid (a sprite with colour.a < 0: -1 dark lines, -2 light ones), worked out for each pixel +// where it lies on its plane: lines every 10^n metres (`metre`: units in one), the four sets from +// the finest that isn't crowded together here, each showing by how far apart its lines are on the +// screen (none where they crowd, stronger where they're far apart: nothing jumps as the view +// moves), about a pixel wide, smooth; the axes through the middle in the colour of the world's +// axis they run along. All from `rel`, where it is from the square's middle (a whole number of +// kilometres from the world's: far out, the world's own numbers are too coarse for lines a +// pixel wide); `eye`: the point under the eye, from the middle; it fades out `reach` from there. vec3 gridAxisColor(vec3 d) { d = abs(d); return d.x > 0.5 ? vec3(0.9, 0.25, 0.25) : d.y > 0.5 ? vec3(0.3, 0.75, 0.25) : vec3(0.25, 0.45, 0.95); } -vec4 gridColor(vec3 w, vec3 centre, vec3 u, vec3 v, float metre, vec3 look, float reach) { - vec2 c = vec2(dot(w, u), dot(w, v)); - vec2 fw = max(fwidth(c), vec2(1e-9)); +vec4 gridColor(vec2 rel, vec3 centre, vec3 u, vec3 v, float metre, float kind, vec2 eye, float reach) { + vec2 base = vec2(dot(centre, u), dot(centre, v)); // (the middle: whole kilometres) + vec2 fw = max(fwidth(rel), vec2(1e-9)); float finest = floor(log(min(fw.x, fw.y) * 5.0 / metre) / log(10.0)); // (10^this m: 5 pixels apart or more) + vec3 look = kind < -1.5 ? vec3(1.0, 0.05, 0.16) : vec3(0.0, 0.14, 0.4); // (shade, how faint, how strong) float lines = 0.0; for (int k = 0; k < 4; ++k) { float s = metre * pow(10.0, finest + float(k)); vec2 apart = s / fw; // (pixels between this set's lines, each way) - vec2 off = abs(fract(c / s + 0.5) - 0.5) * apart; // (pixels to the nearest of them) + vec2 off = abs(fract(fract(base / s) + rel / s + 0.5) - 0.5) * apart; // (pixels to the nearest of them) vec2 strong = smoothstep(20.0, 160.0, apart); vec2 cover = clamp(0.5 * (1.0 + 0.6 * strong) + 0.5 - off, 0.0, 1.0); vec2 a = cover * smoothstep(5.0, 20.0, apart) * mix(vec2(look.y), vec2(look.z), strong); lines = max(lines, max(a.x, a.y)); } - vec2 axis = clamp(1.25 - abs(c) / fw, 0.0, 1.0) * 0.9; // (x: the line along v, y: along u) - float fade = smoothstep(reach, reach * 0.6, length(w - centre)); + vec2 axis = clamp(1.25 - abs(base + rel) / fw, 0.0, 1.0) * 0.9; // (x: the line along v, y: along u) + float fade = smoothstep(reach, reach * 0.6, length(rel - eye)); vec3 col = vec3(look.x) * lines; float a = lines; col = mix(col, gridAxisColor(v), axis.x), a = mix(a, 1.0, axis.x); @@ -426,7 +429,7 @@ vec4 gridColor(vec3 w, vec3 centre, vec3 u, vec3 v, float metre, vec3 look, floa } void main() { if (u_color.a < 0.0) { - o_color = gridColor(v_world, u_posSize.xyz, normalize(u_right.xyz), normalize(u_up.xyz), u_right.w, u_color.rgb, u_posSize.w); + o_color = gridColor(v_rel, u_posSize.xyz, normalize(u_right.xyz), normalize(u_up.xyz), u_right.w, u_color.a, u_color.xy, u_up.w); return; } vec4 t = texture(u_albedo, v_uv); diff --git a/engine/shaders/sprite.frag b/engine/shaders/sprite.frag index 32d352f..2ba66e4 100644 --- a/engine/shaders/sprite.frag +++ b/engine/shaders/sprite.frag @@ -9,33 +9,36 @@ PUSH_BLOCK { vec4 up; } pc; layout(location = 0) in vec2 v_uv; -layout(location = 1) in vec3 v_world; -// A grid (a sprite with colour.a < 0), worked out for each pixel where it lies on its plane: -// lines every 10^n metres (`metre`: units in one), the four sets from the finest that isn't -// crowded together here, each showing by how far apart its lines are on the screen (none where -// they crowd, stronger where they're far apart: nothing jumps as the view moves), about a pixel -// wide, smooth; the axes through the middle in the colour of the world's axis they run along. -// look: x the lines' shade (0 dark, 1 light), y how faint, z how strong; reach: it fades out there. +layout(location = 1) in vec2 v_rel; +// A grid (a sprite with colour.a < 0: -1 dark lines, -2 light ones), worked out for each pixel +// where it lies on its plane: lines every 10^n metres (`metre`: units in one), the four sets from +// the finest that isn't crowded together here, each showing by how far apart its lines are on the +// screen (none where they crowd, stronger where they're far apart: nothing jumps as the view +// moves), about a pixel wide, smooth; the axes through the middle in the colour of the world's +// axis they run along. All from `rel`, where it is from the square's middle (a whole number of +// kilometres from the world's: far out, the world's own numbers are too coarse for lines a +// pixel wide); `eye`: the point under the eye, from the middle; it fades out `reach` from there. vec3 gridAxisColor(vec3 d) { d = abs(d); return d.x > 0.5 ? vec3(0.9, 0.25, 0.25) : d.y > 0.5 ? vec3(0.3, 0.75, 0.25) : vec3(0.25, 0.45, 0.95); } -vec4 gridColor(vec3 w, vec3 centre, vec3 u, vec3 v, float metre, vec3 look, float reach) { - vec2 c = vec2(dot(w, u), dot(w, v)); - vec2 fw = max(fwidth(c), vec2(1e-9)); +vec4 gridColor(vec2 rel, vec3 centre, vec3 u, vec3 v, float metre, float kind, vec2 eye, float reach) { + vec2 base = vec2(dot(centre, u), dot(centre, v)); // (the middle: whole kilometres) + vec2 fw = max(fwidth(rel), vec2(1e-9)); float finest = floor(log(min(fw.x, fw.y) * 5.0 / metre) / log(10.0)); // (10^this m: 5 pixels apart or more) + vec3 look = kind < -1.5 ? vec3(1.0, 0.05, 0.16) : vec3(0.0, 0.14, 0.4); // (shade, how faint, how strong) float lines = 0.0; for (int k = 0; k < 4; ++k) { float s = metre * pow(10.0, finest + float(k)); vec2 apart = s / fw; // (pixels between this set's lines, each way) - vec2 off = abs(fract(c / s + 0.5) - 0.5) * apart; // (pixels to the nearest of them) + vec2 off = abs(fract(fract(base / s) + rel / s + 0.5) - 0.5) * apart; // (pixels to the nearest of them) vec2 strong = smoothstep(20.0, 160.0, apart); vec2 cover = clamp(0.5 * (1.0 + 0.6 * strong) + 0.5 - off, 0.0, 1.0); vec2 a = cover * smoothstep(5.0, 20.0, apart) * mix(vec2(look.y), vec2(look.z), strong); lines = max(lines, max(a.x, a.y)); } - vec2 axis = clamp(1.25 - abs(c) / fw, 0.0, 1.0) * 0.9; // (x: the line along v, y: along u) - float fade = smoothstep(reach, reach * 0.6, length(w - centre)); + vec2 axis = clamp(1.25 - abs(base + rel) / fw, 0.0, 1.0) * 0.9; // (x: the line along v, y: along u) + float fade = smoothstep(reach, reach * 0.6, length(rel - eye)); vec3 col = vec3(look.x) * lines; float a = lines; col = mix(col, gridAxisColor(v), axis.x), a = mix(a, 1.0, axis.x); @@ -45,8 +48,8 @@ vec4 gridColor(vec3 w, vec3 centre, vec3 u, vec3 v, float metre, vec3 look, floa layout(location = 0) out vec4 o_color; void main() { - if (pc.color.a < 0.0) { // a grid: right.w the units in a metre, color.rgb its look, posSize.w how far it reaches - o_color = gridColor(v_world, pc.posSize.xyz, normalize(pc.right.xyz), normalize(pc.up.xyz), pc.right.w, pc.color.rgb, pc.posSize.w); + if (pc.color.a < 0.0) { // a grid: right.w the units in a metre, up.w how far it reaches, color.xy where the eye is over it + o_color = gridColor(v_rel, pc.posSize.xyz, normalize(pc.right.xyz), normalize(pc.up.xyz), pc.right.w, pc.color.a, pc.color.xy, pc.up.w); return; } vec4 t = sampleTex(floatBitsToUint(pc.up.w), v_uv); diff --git a/engine/shaders/sprite.vert b/engine/shaders/sprite.vert index bc96d4d..8b9cfc1 100644 --- a/engine/shaders/sprite.vert +++ b/engine/shaders/sprite.vert @@ -9,7 +9,7 @@ PUSH_BLOCK { vec4 up; // camera up, w = texture index (bits) } pc; layout(location = 0) out vec2 v_uv; -layout(location = 1) out vec3 v_world; +layout(location = 1) out vec2 v_rel; // (a grid: where on it, from its middle) const vec2 kQuad[6] = vec2[](vec2(-1,-1), vec2(1,-1), vec2(1,1), vec2(-1,-1), vec2(1,1), vec2(-1,1)); void main() { vec2 q = kQuad[VERTEX_ID]; @@ -23,6 +23,6 @@ void main() { } v_uv = q * 0.5 + 0.5; v_uv.y = 1.0 - v_uv.y; - v_world = world; + v_rel = pc.color.a < 0.0 ? q * pc.posSize.w : vec2(0.0); gl_Position = pc.viewProj * vec4(world, 1.0); } diff --git a/src/app_view.cpp b/src/app_view.cpp index 81b727c..150f768 100644 --- a/src/app_view.cpp +++ b/src/app_view.cpp @@ -1412,9 +1412,8 @@ auto App::draw3D(keel::Renderer& r) -> void { 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 reach = nearFar_.y / kUnits * 0.9f; - const glm::vec3 under = eye_ - n * glm::dot(eye_, n); // (the grid's plane goes through the origin) - const glm::vec3 look = rendered ? glm::vec3(0.0f, 0.14f, 0.4f) : glm::vec3(1.0f, 0.05f, 0.16f); // (dark lines on the sunny ground, light ones in the grey room) - r.drawGrid(under * kUnits, u, v, kUnits, look, reach * kUnits); + const glm::dvec3 eye(eye_), dn(n), under = (eye - dn * glm::dot(eye, dn)) * double(kUnits); // (the grid's plane goes through the origin) + r.drawGrid(under, u, v, kUnits, rendered, reach * kUnits); // (dark lines on the sunny ground, light ones in the grey room) } if (tab_ == Tab::Print) drawPrintBed(r, light); } diff --git a/tools/package.py b/tools/package.py index 2ba8401..4fcb9e6 100644 --- a/tools/package.py +++ b/tools/package.py @@ -12,6 +12,7 @@ Each unpacks to one Fry3DX/ folder: the program, its content (engine/, Fry3DX/; on a Mac inside the app), LICENSE and a README.txt. (The Android APK: android/build_apk.py.) """ +import json import os import shutil import subprocess @@ -36,11 +37,8 @@ Open source under the Apache License 2.0 (LICENSE). Sky pictures: Poly Haven, CC def version(): - with open(os.path.join(ROOT, "CMakeLists.txt")) as f: - for line in f: - if line.startswith("project(Fry3DX VERSION "): - return line.split()[2] - return "0" + with open(os.path.join(ROOT, "project.json")) as f: # (the version and the rest live there) + return json.load(f)["version"] PC_NEEDS = "a graphics card with Vulkan (or OpenGL 4.6)"