Compare commits

..
20 changed files with 145 additions and 351 deletions

View file

@ -1,6 +1,5 @@
cmake_minimum_required(VERSION 3.25) cmake_minimum_required(VERSION 3.25)
include(cmake/Project.cmake) # project.json: the version, links, creators project(Fry3DX VERSION 0.8.0 LANGUAGES C CXX)
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) 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) if(UNIX AND NOT APPLE)
add_link_options(-rdynamic) # function names in crash reports add_link_options(-rdynamic) # function names in crash reports

View file

@ -11,7 +11,6 @@ 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 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. updates an app signed with the key it was installed with.
""" """
import json
import os import os
import secrets import secrets
import shutil import shutil
@ -31,8 +30,11 @@ def run(*cmd, **kw):
def app_version(): def app_version():
with open(os.path.join(ROOT, "project.json")) as f: # (the version and the rest live there) with open(os.path.join(ROOT, "CMakeLists.txt")) as f:
return json.load(f)["version"] for line in f:
if line.startswith("project(Fry3DX VERSION "):
return line.split()[2]
return "0"
def main(): def main():

View file

@ -1,81 +0,0 @@
# 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,7 +129,6 @@ fry.frame()`,
}, },
versions: [ 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.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.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"] }, { 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,28 +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)}); 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::dvec3& under, const glm::vec3& u, const glm::vec3& v, float metre, bool dark, float reach) { void Renderer::drawStreak(const glm::vec3& from, const glm::vec3& to, float width, const glm::vec4& color, TextureId texture, bool additive) {
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; auto& list = additive ? packet_.glowSprites : packet_.sprites;
if (list.size() >= 8192) return; if (list.size() >= 8192) return;
uint32_t bits = texture; uint32_t bits = texture;
float tex; float tex;
std::memcpy(&tex, &bits, 4); std::memcpy(&tex, &bits, 4);
// up.xyz: half its length along it (non-zero marks a streak); right.w: half its width at `to`. // 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, 0.0f, 0.0f, (widthTo < 0.0f ? width : widthTo) * 0.5f), glm::vec4((to - from) * 0.5f, tex)}); list.push_back({glm::vec4((from + to) * 0.5f, width * 0.5f), color, glm::vec4(0.0f), 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, void Renderer::drawModel(ModelId model, const glm::mat4& transform, const glm::vec4& tint, const std::vector<glm::mat4>* skin,
@ -461,13 +447,11 @@ void Renderer::buildPacket(uint32_t width, uint32_t height) {
for (const glm::vec3& q : corners) radius = std::max(radius, glm::length(q - center)); for (const glm::vec3& q : corners) radius = std::max(radius, glm::length(q - center));
radius = std::ceil(radius / 16.0f) * 16.0f; radius = std::ceil(radius / 16.0f) * 16.0f;
const float texel = 2.0f * radius / cascadeSize; const float texel = 2.0f * radius / cascadeSize;
// (in doubles: far from the origin a float's steps are near a texel's, and it'd shimmer) glm::mat4 lightView = glm::lookAt(glm::vec3(0), -sunDir_, up);
const glm::dmat4 lightView0 = glm::lookAt(glm::dvec3(0), -glm::dvec3(sunDir_), glm::dvec3(up)); glm::vec4 lc = lightView * glm::vec4(center, 1);
glm::dvec4 lc = lightView0 * glm::dvec4(glm::dvec3(center), 1);
lc.x = std::floor(lc.x / texel) * texel; lc.x = std::floor(lc.x / texel) * texel;
lc.y = std::floor(lc.y / texel) * texel; lc.y = std::floor(lc.y / texel) * texel;
center = glm::vec3(glm::inverse(lightView0) * lc); center = glm::vec3(glm::inverse(lightView) * lc);
glm::mat4 lightView;
lightView = glm::lookAt(center + sunDir_ * 8000.0f, center, up); lightView = glm::lookAt(center + sunDir_ * 8000.0f, center, up);
frame.cascades[c] = glm::ortho(-radius, radius, -radius, radius, 1.0f, 16000.0f) * lightView; frame.cascades[c] = glm::ortho(-radius, radius, -radius, radius, 1.0f, 16000.0f) * lightView;
frame.splits[c] = f; frame.splits[c] = f;
@ -773,16 +757,16 @@ void Renderer::buildPacket(uint32_t width, uint32_t height) {
up = glm::cross(right, f); up = glm::cross(right, f);
for (auto* list : {&p.sprites, &p.glowSprites}) for (auto* list : {&p.sprites, &p.glowSprites})
for (gpu::Sprite& s : *list) { 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: its side faces the eye
if (const glm::vec3 half(s.up); glm::dot(half, half) > 0.0f) { // a streak: each end's side faces the eye (sprite.vert) glm::vec3 side = glm::cross(half, camera_.origin - glm::vec3(s.posSize));
s.right = glm::vec4(camera_.origin, s.right.w); side = glm::length(side) > 1e-6f ? glm::normalize(side) : right;
s.posSize.w = -std::max(s.posSize.w, 1e-9f); // (below 0: a streak; its half width at `from`) s.right = glm::vec4(side * s.posSize.w, 0.0f);
s.posSize.w = 1.0f;
continue; continue;
} }
s.right = glm::vec4(right, s.right.w), s.up = glm::vec4(up, s.up.w); 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) { 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); return glm::dot(glm::vec3(a.posSize) - camera_.origin, f) > glm::dot(glm::vec3(b.posSize) - camera_.origin, f);
}); });
} }

View file

@ -98,15 +98,9 @@ public:
// A particle: a camera-facing square `size` across, unlit (color can be HDR-bright). // 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). // 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); 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 (`widthTo` at `to`: < 0, the same), // A sprite stretched from `from` to `to`, `width` across, turned to face the eye as much as
// each end turned to face the eye as much as it can (rain; lines drawn in pieces meet exactly). // it can (rain).
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); void drawStreak(const glm::vec3& from, const glm::vec3& to, float width, const glm::vec4& color, TextureId texture, bool additive = false);
// 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. // skin: one matrix per joint (anim::skinMatrices) for skinned models; ignored otherwise.
// lighting: probe light for moving things (nullptr = lightmap or simple sun shading). // 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), void drawModel(ModelId model, const glm::mat4& transform, const glm::vec4& tint = glm::vec4(1.0f),

View file

@ -366,72 +366,24 @@ void main() { o_color = vec4(u_color.rgb * (0.5 + 0.5 * max(dot(v_normal, normal
const char* kSpriteVert = R"( const char* kSpriteVert = R"(
uniform mat4 u_viewProj; uniform mat4 u_viewProj;
uniform vec4 u_posSize, u_right, u_up; // right.w: turn (radians) uniform vec4 u_posSize, u_right, u_up; // right.w: turn (radians)
uniform vec4 u_color;
out vec2 v_uv; 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)); 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() { void main() {
vec2 q = kQuad[gl_VertexID]; vec2 q = kQuad[gl_VertexID];
float c = cos(u_right.w), s = sin(u_right.w); 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); 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; 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_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 = u_viewProj * vec4(world, 1.0);
gl_Position.z = gl_Position.z * 2.0 - gl_Position.w; gl_Position.z = gl_Position.z * 2.0 - gl_Position.w;
} }
)"; )";
const char* kSpriteFrag = R"( const char* kSpriteFrag = R"(
in vec2 v_uv; in vec2 v_uv;
in vec2 v_rel;
uniform sampler2D u_albedo; uniform sampler2D u_albedo;
uniform vec4 u_color, u_posSize, u_right, u_up; uniform vec4 u_color;
out vec4 o_color; 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() { 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); 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); 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,17 +1413,6 @@ void VulkanBackend::render(FramePacket& p) {
vkCmdSetScissor(cmd, 0, 1, &scissor); vkCmdSetScissor(cmd, 0, 1, &scissor);
drawMeshes(cmd, p.opaque, p.viewProj, false, depthMeshPipeline_, depthSkinnedPipeline_, cullFor_, true); drawMeshes(cmd, p.opaque, p.viewProj, false, depthMeshPipeline_, depthSkinnedPipeline_, cullFor_, true);
vkCmdEndRendering(cmd); 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"); mark(cmd, "depth pre-pass");
depth.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; depth.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
} }

View file

@ -5,9 +5,6 @@ layout(location = 0) out vec3 v_normal;
layout(location = 1) out vec2 v_uv; layout(location = 1) out vec2 v_uv;
layout(location = 2) out vec2 v_lightmapUv; layout(location = 2) out vec2 v_lightmapUv;
layout(location = 3) out vec3 v_world; 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() { void main() {
Vertex vx = VERTEX(VERTEX_ID); Vertex vx = VERTEX(VERTEX_ID);

View file

@ -5,9 +5,6 @@ layout(location = 0) out vec3 v_normal;
layout(location = 1) out vec2 v_uv; layout(location = 1) out vec2 v_uv;
layout(location = 2) out vec2 v_lightmapUv; layout(location = 2) out vec2 v_lightmapUv;
layout(location = 3) out vec3 v_world; 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() { void main() {
Vertex vx = VERTEX(VERTEX_ID); Vertex vx = VERTEX(VERTEX_ID);

View file

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

View file

@ -9,20 +9,13 @@ PUSH_BLOCK {
vec4 up; // camera up, w = texture index (bits) vec4 up; // camera up, w = texture index (bits)
} pc; } pc;
layout(location = 0) out vec2 v_uv; 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)); 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() { void main() {
vec2 q = kQuad[VERTEX_ID]; vec2 q = kQuad[VERTEX_ID];
float c = cos(pc.right.w), s = sin(pc.right.w); 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); 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; 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 = q * 0.5 + 0.5;
v_uv.y = 1.0 - v_uv.y; 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); gl_Position = pc.viewProj * vec4(world, 1.0);
} }

View file

@ -1,20 +0,0 @@
{
"_": "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,13 +47,12 @@ else()
add_executable(fry3dx ${FRY_APP_SOURCES}) add_executable(fry3dx ${FRY_APP_SOURCES})
endif() endif()
if(WIN32) # a window program (no console window), with its icon if(WIN32) # a window program (no console window), with its icon
target_sources(fry3dx PRIVATE ${CMAKE_BINARY_DIR}/generated/fry3dx.rc) target_sources(fry3dx PRIVATE fry3dx.rc)
set_target_properties(fry3dx PROPERTIES WIN32_EXECUTABLE ON) set_target_properties(fry3dx PROPERTIES WIN32_EXECUTABLE ON)
target_link_options(fry3dx PRIVATE /ENTRY:mainCRTStartup) target_link_options(fry3dx PRIVATE /ENTRY:mainCRTStartup)
endif() endif()
target_link_libraries(fry3dx PRIVATE fry_core fry_script keel stb) target_link_libraries(fry3dx PRIVATE fry_core fry_script keel stb)
fry_project_header(${CMAKE_BINARY_DIR}/generated/fry_project.h ${CMAKE_BINARY_DIR}/generated/fry3dx.rc) # (project.json) target_compile_definitions(fry3dx PRIVATE FRY_VERSION="${PROJECT_VERSION}")
target_include_directories(fry3dx PRIVATE ${CMAKE_BINARY_DIR}/generated)
add_dependencies(fry3dx content) add_dependencies(fry3dx content)
foreach(t fry_core fry_script fry3dx) foreach(t fry_core fry_script fry3dx)
if(MSVC) if(MSVC)

View file

@ -354,12 +354,7 @@ private:
glm::vec3 target_{0, 0, 0.4f}; glm::vec3 target_{0, 0, 0.4f};
float yaw_ = -135.0f, pitch_ = 25.0f, dist_ = 4.0f, fov_ = 50.0f; float yaw_ = -135.0f, pitch_ = 25.0f, dist_ = 4.0f, fov_ = 50.0f;
glm::vec2 nearFar_{2.0f, 16384.0f}; glm::vec2 nearFar_{2.0f, 16384.0f};
struct Reach { std::map<uint32_t, std::pair<uint64_t, float>> radii_; // (each object's mesh: how far it reaches from its place)
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}; ImVec2 viewMin_{0, 0}, viewMax_{1, 1};
glm::mat4 vp_{1.0f}; glm::mat4 vp_{1.0f};
glm::vec3 eye_{0.0f}; glm::vec3 eye_{0.0f};
@ -1476,7 +1471,6 @@ private:
void lookBy(ImVec2 d); void lookBy(ImVec2 d);
void refocus(); void refocus();
float nearestThing(); 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) 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 benchStep();
void benchEnd(std::chrono::steady_clock::time_point began); void benchEnd(std::chrono::steady_clock::time_point began);

View file

@ -1,6 +1,5 @@
// Fry3DX: the panels: layers, properties, menus, the commands and their keys. // Fry3DX: the panels: layers, properties, menus, the commands and their keys.
#include "app.h" #include "app.h"
#include "fry_project.h"
using namespace fry; using namespace fry;
namespace stdfs = std::filesystem; namespace stdfs = std::filesystem;
@ -789,12 +788,7 @@ auto App::menus() -> void {
if (ImGui::BeginMenu("Help")) { if (ImGui::BeginMenu("Help")) {
if (ImGui::MenuItem("Keys and mouse", "F1")) showHelp_ = true; if (ImGui::MenuItem("Keys and mouse", "F1")) showHelp_ = true;
commandItem("help.search"); commandItem("help.search");
ImGui::Separator(); if (ImGui::MenuItem("About Fry3DX")) showAbout_ = true;
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(); ImGui::EndMenu();
} }
} }

View file

@ -1,6 +1,5 @@
// Fry3DX: scripts, your own materials, plug-ins, the windows that go with them. // Fry3DX: scripts, your own materials, plug-ins, the windows that go with them.
#include "app.h" #include "app.h"
#include "fry_project.h"
using namespace fry; using namespace fry;
namespace stdfs = std::filesystem; namespace stdfs = std::filesystem;
@ -748,13 +747,14 @@ auto App::aboutWindow() -> void {
logoImage(logo); logoImage(logo);
ImGui::Dummy(ImVec2(0, 6)); ImGui::Dummy(ImVec2(0, 6));
ImGui::PushFont(keel::ui::font(keel::ui::Font::Title), 30.0f); ImGui::PushFont(keel::ui::font(keel::ui::Font::Title), 30.0f);
centred(project::name, roleColor(Role::Text)); centred("Fry3DX", roleColor(Role::Text));
ImGui::PopFont(); ImGui::PopFont();
centred(project::long_, accentText()); centred("Frying 3D Extreme", accentText());
centred(std::format("Version {}", project::version).c_str(), roleColor(Role::TextSecondary)); centred("Version " FRY_VERSION, roleColor(Role::TextSecondary));
ImGui::Dummy(ImVec2(0, 8)); ImGui::Dummy(ImVec2(0, 8));
ImGui::PushStyleColor(ImGuiCol_Text, roleColor(Role::TextSecondary)); ImGui::PushStyleColor(ImGuiCol_Text, roleColor(Role::TextSecondary));
ImGui::TextWrapped("%s", project::description); 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::PopStyleColor(); ImGui::PopStyleColor();
ImGui::Dummy(ImVec2(0, 4)); ImGui::Dummy(ImVec2(0, 4));
@ -762,7 +762,7 @@ auto App::aboutWindow() -> void {
const keel::GpuInfo gpu = r_->gpuInfo(); const keel::GpuInfo gpu = r_->gpuInfo();
const auto& machine = keel::sys::machine(); const auto& machine = keel::sys::machine();
const std::vector<std::pair<const char*, std::string>> rows = { const std::vector<std::pair<const char*, std::string>> rows = {
{"Version", project::version}, {"Drawn with", r_->apiName()}, {"Graphics", gpu.name.empty() ? "?" : gpu.name}, {"System", machine.os}, {"Processor", machine.cpu}, {"Version", FRY_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(); }))}}; {"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); ImGui::BeginChild("details", ImVec2(0, 0), ImGuiChildFlags_FrameStyle | ImGuiChildFlags_AutoResizeY);
if (ImGui::BeginTable("details", 2, ImGuiTableFlags_SizingFixedFit)) { if (ImGui::BeginTable("details", 2, ImGuiTableFlags_SizingFixedFit)) {
@ -781,39 +781,21 @@ auto App::aboutWindow() -> void {
ImGui::Dummy(ImVec2(0, 4)); ImGui::Dummy(ImVec2(0, 4));
const float bw = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x * 2) / 3; const float bw = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x * 2) / 3;
pushAccent(); pushAccent();
if (ImGui::Button("Website", ImVec2(bw, 0))) SDL_OpenURL(project::website); if (ImGui::Button("Website", ImVec2(bw, 0))) SDL_OpenURL("https://nexoniarz.github.io/Fry3DX/");
popAccent(); popAccent();
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::Button("Source on GitHub", ImVec2(bw, 0))) SDL_OpenURL(project::repo); if (ImGui::Button("Source on GitHub", ImVec2(bw, 0))) SDL_OpenURL("https://github.com/Nexoniarz/Fry3DX");
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::Button("Copy details", ImVec2(bw, 0))) { if (ImGui::Button("Copy details", ImVec2(bw, 0))) {
std::string text = project::name; std::string text = "Fry3DX";
for (const auto& [k, v] : rows) text += std::format("\n{}: {}", k, v); for (const auto& [k, v] : rows) text += std::format("\n{}: {}", k, v);
SDL_SetClipboardText(text.c_str()); SDL_SetClipboardText(text.c_str());
say("The details are copied: paste them into a bug report"); 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::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::PushStyleColor(ImGuiCol_Text, roleColor(Role::TextTertiary));
ImGui::TextWrapped("Open source (%s). Built on %s; the look is %s.", project::license, project::builtWith, project::look); 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::PopStyleColor(); ImGui::PopStyleColor();
ImGui::End(); ImGui::End();
} }

View file

@ -191,38 +191,18 @@ auto App::refocus() -> void {
if (t > 0.0f) dist_ = std::clamp(t, 0.01f, 5000.0f), target_ = eye + f * dist_; 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 // 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. To each // shade in corners go by it, so looking past something close doesn't break them.
// object's box (not a ball round it: inside a long flat one's ball, it would come to nothing).
auto App::nearestThing() -> float { auto App::nearestThing() -> float {
float best = std::abs(eye_.z); // (the ground) float best = std::abs(eye_.z); // (the ground)
for (const Object& o : scene_.objects) { for (const Object& o : scene_.objects) {
if (!o.visible) continue; if (!o.visible) continue;
const Reach& r = reachOf(o); auto& [version, radius] = radii_[o.id];
const glm::mat4 m = objectMatrix(o); if (version != shapeKey(o) || !version) {
const glm::vec3 local = glm::inverse(m) * glm::vec4(eye_, 1.0f); version = shapeKey(o), radius = 0;
best = std::min(best, glm::distance(eye_, glm::vec3(m * glm::vec4(glm::clamp(local, r.lo, r.hi), 1.0f)))); for (const glm::vec3& v : o.mesh.verts) radius = std::max(radius, glm::length(v));
} }
for (const Reference& ref : scene_.references) { 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));
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); return std::max(best, 0.001f);
} }
@ -284,10 +264,8 @@ auto App::updateCamera(ImGuiIO& io) -> void {
else { else {
float size = 0.0f; // (the biggest object's size: the least step is a little of it) float size = 0.0f; // (the biggest object's size: the least step is a little of it)
for (const Object& o : scene_.objects) 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)})); 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)}));
// (what's ahead: no further than ten times how far the nearest thing is, so with only const float step = std::clamp(std::max(dist_ * 0.2f, size * 0.05f), 0.001f, 500.0f) * fry_movespeed.f();
// 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; target_ += viewDir() * step * io.MouseWheel;
refocus(); refocus();
} }
@ -330,16 +308,17 @@ auto App::settleView() -> void {
auto App::nearFar() -> glm::vec2 { auto App::nearFar() -> glm::vec2 {
float reach = 1.0f; // (how far from the middle the furthest object reaches, m) float reach = 1.0f; // (how far from the middle the furthest object reaches, m)
for (const Object& o : scene_.objects) { for (const Object& o : scene_.objects) {
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)})); 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)}));
} }
// (the near plane as far out as it can be: the depth's precision is spread between them, so // (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 flicker through each other. Nothing is nearer than the // the nearer it is, the more surfaces far off flicker through each other)
// nearest thing, and a quarter of the way to it keeps even the screen's corners clear of it) const float nearM = std::clamp(viewScale() * 0.005f, 1e-5f, 50.0f);
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; return glm::vec2(nearM, farM) * kUnits;
} }
@ -1404,16 +1383,71 @@ 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 // 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. // it belongs to (once), in pieces, each a pixel wide where it is.
if (tab_ == Tab::Render ? renderGrid_ : showGrid_) { 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; glm::vec3 p, n;
workPlane(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 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 glm::vec3 v = glm::cross(n, u);
const float reach = nearFar_.y / kUnits * 0.9f; const float first = gridStep() * 0.1f;
const glm::dvec3 eye(eye_), dn(n), under = (eye - dn * glm::dot(eye, dn)) * double(kUnits); // (the grid's plane goes through the origin) const int count = 20, pieces = 12, levels = 4;
r.drawGrid(under, u, v, kUnits, rendered, reach * kUnits); // (dark lines on the sunny ground, light ones in the grey room) 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);
}
} }
if (tab_ == Tab::Print) drawPrintBed(r, light); if (tab_ == Tab::Print) drawPrintBed(r, light);
} }

25
src/fry3dx.rc Normal file
View file

@ -0,0 +1,25 @@
// 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,7 +12,6 @@
Each unpacks to one Fry3DX/ folder: the program, its content (engine/, Fry3DX/; on a Mac inside 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.) the app), LICENSE and a README.txt. (The Android APK: android/build_apk.py.)
""" """
import json
import os import os
import shutil import shutil
import subprocess import subprocess
@ -37,8 +36,11 @@ Open source under the Apache License 2.0 (LICENSE). Sky pictures: Poly Haven, CC
def version(): def version():
with open(os.path.join(ROOT, "project.json")) as f: # (the version and the rest live there) with open(os.path.join(ROOT, "CMakeLists.txt")) as f:
return json.load(f)["version"] for line in f:
if line.startswith("project(Fry3DX VERSION "):
return line.split()[2]
return "0"
PC_NEEDS = "a graphics card with Vulkan (or OpenGL 4.6)" PC_NEEDS = "a graphics card with Vulkan (or OpenGL 4.6)"