Compare commits

...
Author SHA1 Message Date
Nexoniarz
1ee38c966c 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 <noreply@anthropic.com>
2026-10-21 17:41:25 +02:00
Nexoniarz
ee56ed4005 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 <noreply@anthropic.com>
2026-10-21 17:30:43 +02:00
Nexoniarz
b2fc0437bc 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 <noreply@anthropic.com>
2026-10-21 17:18:13 +02:00
Nexoniarz
9d9557117a 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 <noreply@anthropic.com>
2026-10-21 17:08:13 +02:00
20 changed files with 351 additions and 145 deletions

View file

@ -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

View file

@ -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():

81
cmake/Project.cmake Normal file
View file

@ -0,0 +1,81 @@
# project.json, read before project(): FRY_VERSION and the rest as FRY_<KEY>; 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 <span>\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<const Creator> 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()

View file

@ -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"] },

View file

@ -211,14 +211,28 @@ 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::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(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) {
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<glm::mat4>* skin,
@ -447,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;
@ -757,16 +773,16 @@ 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 (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`)
continue;
}
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);
});
}

View file

@ -98,9 +98,15 @@ 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);
// 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; 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),

View file

@ -366,24 +366,72 @@ 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 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];
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_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_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 vec2 v_rel;
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: -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(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(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(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);
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_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);
o_color = vec4(t.rgb * u_color.rgb, clamp((t.a - 0.03) / 0.97, 0.0, 1.0) * u_color.a);
}

View file

@ -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;
}

View file

@ -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);

View file

@ -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);

View file

@ -9,8 +9,49 @@ PUSH_BLOCK {
vec4 up;
} pc;
layout(location = 0) in vec2 v_uv;
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(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(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(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);
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 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);
// Block compression leaves a little alpha where there should be none; bright sprites
// would show it as a square: cut it.

View file

@ -9,13 +9,20 @@ PUSH_BLOCK {
vec4 up; // camera up, w = texture index (bits)
} pc;
layout(location = 0) out vec2 v_uv;
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];
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.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_rel = pc.color.a < 0.0 ? q * pc.posSize.w : vec2(0.0);
gl_Position = pc.viewProj * vec4(world, 1.0);
}

20
project.json Normal file
View file

@ -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"]
}

View file

@ -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)

View file

@ -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<uint32_t, std::pair<uint64_t, float>> 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<uint32_t, Reach> 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);

View file

@ -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();
}
}

View file

@ -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<std::pair<const char*, std::string>> 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();
}

View file

@ -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,8 +284,10 @@ 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)}));
const float step = std::clamp(std::max(dist_ * 0.2f, size * 0.05f), 0.001f, 500.0f) * fry_movespeed.f();
size = std::max(size, reachOf(o).radius * std::max({std::abs(o.scale.x), std::abs(o.scale.y), std::abs(o.scale.z)}));
// (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();
}
@ -308,17 +330,16 @@ 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);
const float farM = std::max({dist_ * 60.0f, (glm::length(eye_) + reach) * 2.0f, 100.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);
// (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;
}
@ -1383,71 +1404,16 @@ 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): 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;
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)
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);
}
const float reach = nearFar_.y / kUnits * 0.9f;
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);
}

View file

@ -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

View file

@ -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)"