mirror of
https://github.com/Nexoniarz/Fry3DX.git
synced 2026-11-03 04:24:26 +00:00
Sculpting: the Paint tab's other side (Tab) — Raise, Puff out, Smooth, Flatten, Pinch, Grab, Ctrl the other way, Shift to smooth, the same on both sides (X); shapes are made finer first (the same shape); o:sculpt() in scripts, fry_sculpt for pictures; tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e93ae44684
commit
5c28e3da4e
9 changed files with 472 additions and 9 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
# Fry3DX: the app (app.cpp) on the engine, and its modelling core (mesh, scene) as a library the
|
# Fry3DX: the app (app.cpp) on the engine, and its modelling core (mesh, scene) as a library the
|
||||||
# tests use too.
|
# tests use too.
|
||||||
add_library(fry_core STATIC mesh.cpp tools.cpp cut.cpp uv.cpp image.cpp sticker.cpp shapes.cpp scene.cpp package.cpp export.cpp print.cpp boolean.cpp modify.cpp)
|
add_library(fry_core STATIC mesh.cpp tools.cpp cut.cpp uv.cpp image.cpp sticker.cpp shapes.cpp scene.cpp package.cpp export.cpp print.cpp boolean.cpp modify.cpp sculpt.cpp)
|
||||||
target_include_directories(fry_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(fry_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
target_link_libraries(fry_core PUBLIC glm::glm libzstd_static)
|
target_link_libraries(fry_core PUBLIC glm::glm libzstd_static)
|
||||||
target_compile_definitions(fry_core PUBLIC GLM_ENABLE_EXPERIMENTAL)
|
target_compile_definitions(fry_core PUBLIC GLM_ENABLE_EXPERIMENTAL)
|
||||||
|
|
|
||||||
225
src/app.cpp
225
src/app.cpp
|
|
@ -7,6 +7,7 @@
|
||||||
#include "mesh.h"
|
#include "mesh.h"
|
||||||
#include "scene.h"
|
#include "scene.h"
|
||||||
#include "script.h"
|
#include "script.h"
|
||||||
|
#include "sculpt.h"
|
||||||
#include "shapes.h"
|
#include "shapes.h"
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
@ -140,6 +141,22 @@ fry.frame()
|
||||||
)";
|
)";
|
||||||
const MaterialInfo kMaterials[] = {{"PLA", 1.24f}, {"PETG", 1.27f}, {"ABS", 1.04f}, {"ASA", 1.07f}, {"TPU", 1.21f}, {"Nylon", 1.14f}, {"Resin", 1.12f}};
|
const MaterialInfo kMaterials[] = {{"PLA", 1.24f}, {"PETG", 1.27f}, {"ABS", 1.04f}, {"ASA", 1.07f}, {"TPU", 1.21f}, {"Nylon", 1.14f}, {"Resin", 1.12f}};
|
||||||
|
|
||||||
|
struct SculptToolInfo {
|
||||||
|
SculptBrush brush;
|
||||||
|
Icon icon;
|
||||||
|
const char* name;
|
||||||
|
const char* key;
|
||||||
|
const char* hint;
|
||||||
|
};
|
||||||
|
const SculptToolInfo kSculptTools[] = {
|
||||||
|
{SculptBrush::Draw, Icon::Sculpt, "Raise", "1", "Builds the surface up where you brush. Ctrl digs in instead."},
|
||||||
|
{SculptBrush::Inflate, Icon::Inflate, "Puff out", "2", "Swells it out all round, like blowing it up. Ctrl shrinks it."},
|
||||||
|
{SculptBrush::Smooth, Icon::Smooth, "Smooth", "3", "Evens out bumps. Hold Shift with any brush to smooth."},
|
||||||
|
{SculptBrush::Flatten, Icon::Flatten, "Flatten", "4", "Presses it flat, like a palette knife."},
|
||||||
|
{SculptBrush::Pinch, Icon::Pinch, "Pinch", "5", "Pulls it in towards the middle of the brush: sharp creases and ridges. Ctrl pushes apart."},
|
||||||
|
{SculptBrush::Grab, Icon::Grab, "Grab", "6", "Take hold of a patch and drag it where it should be."},
|
||||||
|
};
|
||||||
|
|
||||||
const PaintToolInfo kPaintTools[] = {
|
const PaintToolInfo kPaintTools[] = {
|
||||||
{PaintTool::Brush, Icon::PaintBrush, "Brush", "B", "Paint on the model or on the flat picture. Alt+click picks a colour; [ and ] change the size."},
|
{PaintTool::Brush, Icon::PaintBrush, "Brush", "B", "Paint on the model or on the flat picture. Alt+click picks a colour; [ and ] change the size."},
|
||||||
{PaintTool::Eraser, Icon::Eraser, "Eraser", "E", "Paints its plain colour back."},
|
{PaintTool::Eraser, Icon::Eraser, "Eraser", "E", "Paints its plain colour back."},
|
||||||
|
|
@ -290,6 +307,20 @@ public:
|
||||||
if (a.size() > 2) self->view_ = View::Perspective, self->fov_ = 50, self->yaw_ = std::stof(a[1]), self->pitch_ = std::stof(a[2]);
|
if (a.size() > 2) self->view_ = View::Perspective, self->fov_ = 50, self->yaw_ = std::stof(a[1]), self->pitch_ = std::stof(a[2]);
|
||||||
if (a.size() > 3) self->dist_ = std::stof(a[3]);
|
if (a.size() > 3) self->dist_ = std::stof(a[3]);
|
||||||
}, "Fry3DX: fry_orbit yaw pitch [distance]");
|
}, "Fry3DX: fry_orbit yaw pitch [distance]");
|
||||||
|
// A sculpt stroke across the view (0..1 of it), for pictures of it: brush 1-6, then from and to.
|
||||||
|
static keel::Cmd sculptStroke("fry_sculpt", [](const keel::Args& a) {
|
||||||
|
if (a.size() < 6) return;
|
||||||
|
const int brush = std::clamp(std::stoi(a[1]) - 1, 0, int(std::size(kSculptTools)) - 1);
|
||||||
|
self->sculptBrush_ = kSculptTools[brush].brush;
|
||||||
|
const bool other = a.size() > 6 && a[6] == "invert";
|
||||||
|
const auto at = [&](float t) {
|
||||||
|
const ImVec2 lo = self->viewMin_, hi = self->viewMax_;
|
||||||
|
return ImVec2(lo.x + (hi.x - lo.x) * glm::mix(std::stof(a[2]), std::stof(a[4]), t), lo.y + (hi.y - lo.y) * glm::mix(std::stof(a[3]), std::stof(a[5]), t));
|
||||||
|
};
|
||||||
|
self->sculptAt(at(0), true, other, false);
|
||||||
|
for (int i = 1; i <= 60; ++i) self->sculptAt(at(float(i) / 60.0f), false, other, false);
|
||||||
|
self->endSculpt();
|
||||||
|
}, "Fry3DX: fry_sculpt brush x0 y0 x1 y1 [invert] (0..1 of the view)");
|
||||||
static keel::Cmd sel("fry_select", [](const keel::Args& a) {
|
static keel::Cmd sel("fry_select", [](const keel::Args& a) {
|
||||||
self->selected_.clear();
|
self->selected_.clear();
|
||||||
for (const Object& o : self->scene_.objects)
|
for (const Object& o : self->scene_.objects)
|
||||||
|
|
@ -340,8 +371,9 @@ public:
|
||||||
static keel::Cmd frame("fry_frame", [](const keel::Args&) { self->frameAll(); }, "Fry3DX: frame everything");
|
static keel::Cmd frame("fry_frame", [](const keel::Args&) { self->frameAll(); }, "Fry3DX: frame everything");
|
||||||
static keel::Cmd tab("fry_tab", [](const keel::Args& a) {
|
static keel::Cmd tab("fry_tab", [](const keel::Args& a) {
|
||||||
if (a.size() > 1)
|
if (a.size() > 1)
|
||||||
self->setTab(a[1] == "paint" ? Tab::Paint : a[1] == "layout" ? Tab::Layout : a[1] == "render" ? Tab::Render : a[1] == "print" ? Tab::Print : Tab::Model);
|
self->setTab(a[1] == "paint" || a[1] == "sculpt" ? Tab::Paint : a[1] == "layout" ? Tab::Layout : a[1] == "render" ? Tab::Render : a[1] == "print" ? Tab::Print : Tab::Model);
|
||||||
}, "Fry3DX: fry_tab model|paint|render");
|
if (a.size() > 1) self->sculpting_ = a[1] == "sculpt";
|
||||||
|
}, "Fry3DX: fry_tab model|paint|sculpt|layout|render|print");
|
||||||
// (points as parts of the view: 0..1 across, 0..1 down)
|
// (points as parts of the view: 0..1 across, 0..1 down)
|
||||||
auto viewPoint = [](const std::string& x, const std::string& y) {
|
auto viewPoint = [](const std::string& x, const std::string& y) {
|
||||||
return ImVec2(self->viewMin_.x + std::stof(x) * (self->viewMax_.x - self->viewMin_.x), self->viewMin_.y + std::stof(y) * (self->viewMax_.y - self->viewMin_.y));
|
return ImVec2(self->viewMin_.x + std::stof(x) * (self->viewMax_.x - self->viewMin_.x), self->viewMin_.y + std::stof(y) * (self->viewMax_.y - self->viewMin_.y));
|
||||||
|
|
@ -688,6 +720,21 @@ private:
|
||||||
glm::vec3 last{0.0f};
|
glm::vec3 last{0.0f};
|
||||||
glm::vec2 last2D{0.0f};
|
glm::vec2 last2D{0.0f};
|
||||||
} paint_;
|
} paint_;
|
||||||
|
// Sculpting (the Paint tab's Shape side): brushes that push the surface about like clay.
|
||||||
|
bool sculpting_ = false;
|
||||||
|
SculptBrush sculptBrush_ = SculptBrush::Draw;
|
||||||
|
float sculptStrength_ = 0.5f, sculptPx_ = 60.0f; // (its own brush size: sculpting wants a bigger one than painting)
|
||||||
|
bool sculptMirror_ = false; // the same on the other side (left and right: the object's Y)
|
||||||
|
struct SculptStroke {
|
||||||
|
bool active = false, haveLast = false, waiting = false;
|
||||||
|
uint32_t object = 0;
|
||||||
|
std::unique_ptr<SculptMesh> mesh;
|
||||||
|
glm::vec3 last{0.0f};
|
||||||
|
// Grab: where it was taken hold of (the world), and what (the object's own space).
|
||||||
|
glm::vec3 start{0.0f};
|
||||||
|
std::vector<std::pair<int, float>> held[2];
|
||||||
|
std::vector<glm::vec3> from[2];
|
||||||
|
} sculpt_;
|
||||||
// A picture layer unlocked to be moved: which, and what's being dragged (a corner 0-3, 4 the
|
// A picture layer unlocked to be moved: which, and what's being dragged (a corner 0-3, 4 the
|
||||||
// whole of it, 5 the turning handle) from where.
|
// whole of it, 5 the turning handle) from where.
|
||||||
struct LayerEdit {
|
struct LayerEdit {
|
||||||
|
|
@ -1626,7 +1673,7 @@ private:
|
||||||
if (t == tab_) return;
|
if (t == tab_) return;
|
||||||
if (drag_.active) cancelDrag();
|
if (drag_.active) cancelDrag();
|
||||||
lockLayer();
|
lockLayer();
|
||||||
endStroke();
|
endStroke(), endSculpt();
|
||||||
knifePts_.clear(), drawPts_.clear();
|
knifePts_.clear(), drawPts_.clear();
|
||||||
if (t != Tab::Model) leaveEdit();
|
if (t != Tab::Model) leaveEdit();
|
||||||
if ((t == Tab::Render || t == Tab::Print) && tool_ != Tool::Select && tool_ != Tool::Move && tool_ != Tool::Rotate && tool_ != Tool::Scale) tool_ = Tool::Move;
|
if ((t == Tab::Render || t == Tab::Print) && tool_ != Tool::Select && tool_ != Tool::Move && tool_ != Tool::Rotate && tool_ != Tool::Scale) tool_ = Tool::Move;
|
||||||
|
|
@ -1951,6 +1998,107 @@ private:
|
||||||
paint_.image.reset();
|
paint_.image.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Sculpting ──
|
||||||
|
|
||||||
|
// The object ready to sculpt: its finishing touches made part of it, and made finer (the same
|
||||||
|
// shape) if it has too few points to push about.
|
||||||
|
Object* readyToSculpt(uint32_t id) {
|
||||||
|
Object* o = find(id);
|
||||||
|
if (!o) return nullptr;
|
||||||
|
if (o->locked) return say("That object is locked"), nullptr;
|
||||||
|
pushUndo();
|
||||||
|
if (o->hasFinish()) {
|
||||||
|
o->mesh = finishedMesh(*o);
|
||||||
|
o->mirror = 0, o->round = 0, o->smoothLevels = 0;
|
||||||
|
}
|
||||||
|
handMade(*o);
|
||||||
|
if (finerForSculpting(o->mesh)) {
|
||||||
|
o->smooth = true;
|
||||||
|
say(std::format("{} was made finer to sculpt: {} points now, the same shape", o->name, o->mesh.verts.size()));
|
||||||
|
}
|
||||||
|
touched(o->id);
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
void endSculpt() {
|
||||||
|
if (!sculpt_.active) return;
|
||||||
|
sculpt_ = {};
|
||||||
|
}
|
||||||
|
// The brush at a point of the view: starts a stroke on what's there, or carries one on.
|
||||||
|
void sculptAt(ImVec2 mouse, bool start, bool ctrl, bool shift) {
|
||||||
|
const Ray ray = mouseRay(mouse);
|
||||||
|
const auto toLocal = [](const Object& o, float radius) { return radius * 3.0f / glm::max(glm::abs(o.scale.x) + glm::abs(o.scale.y) + glm::abs(o.scale.z), 1e-6f); };
|
||||||
|
const glm::vec3 other(1, -1, 1); // (the other side: the object's Y turned round)
|
||||||
|
if (start) {
|
||||||
|
const Hit h = pickObject(ray, 0, true);
|
||||||
|
if (!h.object) return;
|
||||||
|
selected_ = {h.object}, active_ = h.object;
|
||||||
|
Object* o = readyToSculpt(h.object);
|
||||||
|
if (!o) return;
|
||||||
|
sculpt_.active = true, sculpt_.object = o->id;
|
||||||
|
sculpt_.mesh = std::make_unique<SculptMesh>(o->mesh);
|
||||||
|
if (sculptBrush_ == SculptBrush::Grab && !shift) { // (made finer: the same shape, so the same spot)
|
||||||
|
sculpt_.start = ray.origin + ray.dir * h.t;
|
||||||
|
const glm::vec3 at = glm::vec3(glm::inverse(objectMatrix(*o)) * glm::vec4(sculpt_.start, 1.0f));
|
||||||
|
const float radius = toLocal(*o, sculptPx_ * pixelSize(sculpt_.start));
|
||||||
|
for (int side = 0; side < (sculptMirror_ ? 2 : 1); ++side) {
|
||||||
|
sculpt_.held[side] = grabbed(o->mesh, side ? at * other : at, radius);
|
||||||
|
for (const auto& [i, w] : sculpt_.held[side]) sculpt_.from[side].push_back(o->mesh.verts[size_t(i)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!sculpt_.active) return;
|
||||||
|
Object* o = find(sculpt_.object);
|
||||||
|
if (!o) return endSculpt();
|
||||||
|
const glm::mat4 inv = glm::inverse(objectMatrix(*o));
|
||||||
|
if (!sculpt_.from[0].empty()) { // grabbing: it follows the mouse across the view
|
||||||
|
const auto p = hitPlane(ray, sculpt_.start, forwardOf(yaw_, pitch_));
|
||||||
|
if (!p) return;
|
||||||
|
const glm::vec3 d = glm::vec3(inv * glm::vec4(*p - sculpt_.start, 0.0f));
|
||||||
|
moveGrabbed(o->mesh, sculpt_.held[0], sculpt_.from[0], d);
|
||||||
|
if (sculptMirror_) moveGrabbed(o->mesh, sculpt_.held[1], sculpt_.from[1], d * other);
|
||||||
|
return touched(o->id);
|
||||||
|
}
|
||||||
|
const Hit h = pickObject(ray, o->id, true);
|
||||||
|
if (!h.object) {
|
||||||
|
sculpt_.haveLast = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const glm::vec3 at = ray.origin + ray.dir * h.t;
|
||||||
|
const float radius = sculptPx_ * pixelSize(at);
|
||||||
|
const SculptBrush brush = shift ? SculptBrush::Smooth : sculptBrush_;
|
||||||
|
const auto dab = [&](const glm::vec3& world) {
|
||||||
|
Dab d{glm::vec3(inv * glm::vec4(world, 1.0f)), toLocal(*o, radius), sculptStrength_, ctrl};
|
||||||
|
sculptDab(o->mesh, *sculpt_.mesh, brush, d);
|
||||||
|
if (sculptMirror_) d.at *= other, sculptDab(o->mesh, *sculpt_.mesh, brush, d);
|
||||||
|
};
|
||||||
|
// A dab every quarter of the brush along the way, not one a frame (fast strokes stay even).
|
||||||
|
const float gap = glm::distance(at, sculpt_.last);
|
||||||
|
if (!sculpt_.haveLast) dab(at);
|
||||||
|
else if (gap >= radius * 0.25f && gap < radius * 8) {
|
||||||
|
const int steps = int(gap / (radius * 0.25f));
|
||||||
|
for (int i = 1; i <= steps; ++i) dab(glm::mix(sculpt_.last, at, float(i) / float(steps)));
|
||||||
|
} else if (gap < radius * 0.25f)
|
||||||
|
return; // (not far enough for the next dab yet)
|
||||||
|
else
|
||||||
|
dab(at);
|
||||||
|
sculpt_.last = at, sculpt_.haveLast = true;
|
||||||
|
touched(o->id);
|
||||||
|
}
|
||||||
|
void sculptViewport(const ImGuiIO& io) {
|
||||||
|
if (sculpt_.waiting) {
|
||||||
|
if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) sculpt_.waiting = false;
|
||||||
|
else if (pickObject(mouseRay(io.MousePos), 0, true).object) sculpt_.waiting = false, sculptAt(io.MousePos, true, io.KeyCtrl, io.KeyShift);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sculpt_.active) {
|
||||||
|
if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) return endSculpt();
|
||||||
|
return sculptAt(io.MousePos, false, io.KeyCtrl, io.KeyShift);
|
||||||
|
}
|
||||||
|
if (!mouseInView(io) || !ImGui::IsMouseClicked(ImGuiMouseButton_Left)) return;
|
||||||
|
if (pickObject(mouseRay(io.MousePos), 0, true).object) sculptAt(io.MousePos, true, io.KeyCtrl, io.KeyShift);
|
||||||
|
else sculpt_.waiting = sculptBrush_ != SculptBrush::Grab;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Picture layers ──
|
// ── Picture layers ──
|
||||||
|
|
||||||
PictureLayer* editLayer() {
|
PictureLayer* editLayer() {
|
||||||
|
|
@ -4274,6 +4422,7 @@ private:
|
||||||
}
|
}
|
||||||
// Painting: on the model under the mouse.
|
// Painting: on the model under the mouse.
|
||||||
void paintViewport(ImGuiIO& io) {
|
void paintViewport(ImGuiIO& io) {
|
||||||
|
if (sculpting_) return sculptViewport(io);
|
||||||
if (paint_.waiting) {
|
if (paint_.waiting) {
|
||||||
if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) paint_.waiting = false;
|
if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) paint_.waiting = false;
|
||||||
else if (pickObject(mouseRay(io.MousePos), 0, true).object) paint_.waiting = false, paintAt(io.MousePos, true);
|
else if (pickObject(mouseRay(io.MousePos), 0, true).object) paint_.waiting = false, paintAt(io.MousePos, true);
|
||||||
|
|
@ -4340,13 +4489,19 @@ private:
|
||||||
if (ctrl && key(ImGuiKey_1)) setTab(Tab::Model);
|
if (ctrl && key(ImGuiKey_1)) setTab(Tab::Model);
|
||||||
if (ctrl && key(ImGuiKey_2)) setTab(Tab::Paint);
|
if (ctrl && key(ImGuiKey_2)) setTab(Tab::Paint);
|
||||||
if (ctrl && key(ImGuiKey_3)) setTab(Tab::Layout);
|
if (ctrl && key(ImGuiKey_3)) setTab(Tab::Layout);
|
||||||
|
if (tab_ == Tab::Paint && !ctrl && !alt && key(ImGuiKey_Tab)) return (void)(sculpting_ = !sculpting_, endStroke(), endSculpt());
|
||||||
|
if (tab_ == Tab::Paint && sculpting_ && !ctrl && !alt)
|
||||||
|
for (int i = 0; i < int(std::size(kSculptTools)); ++i)
|
||||||
|
if (key(ImGuiKey(int(ImGuiKey_1) + i))) sculptBrush_ = kSculptTools[i].brush;
|
||||||
|
if (tab_ == Tab::Paint && sculpting_ && !ctrl && !alt && key(ImGuiKey_X)) sculptMirror_ = !sculptMirror_;
|
||||||
if (tab_ == Tab::Paint && !ctrl && !alt) {
|
if (tab_ == Tab::Paint && !ctrl && !alt) {
|
||||||
if (key(ImGuiKey_B)) paintTool_ = PaintTool::Brush;
|
if (key(ImGuiKey_B)) paintTool_ = PaintTool::Brush;
|
||||||
if (key(ImGuiKey_E)) paintTool_ = PaintTool::Eraser;
|
if (key(ImGuiKey_E)) paintTool_ = PaintTool::Eraser;
|
||||||
if (key(ImGuiKey_G)) paintTool_ = PaintTool::Fill;
|
if (key(ImGuiKey_G)) paintTool_ = PaintTool::Fill;
|
||||||
if (key(ImGuiKey_I)) paintTool_ = PaintTool::Picker;
|
if (key(ImGuiKey_I)) paintTool_ = PaintTool::Picker;
|
||||||
if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket)) brushPx_ = std::max(1.0f, brushPx_ / 1.2f);
|
float& size = sculpting_ ? sculptPx_ : brushPx_;
|
||||||
if (ImGui::IsKeyPressed(ImGuiKey_RightBracket)) brushPx_ = std::min(500.0f, brushPx_ * 1.2f);
|
if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket)) size = std::max(1.0f, size / 1.2f);
|
||||||
|
if (ImGui::IsKeyPressed(ImGuiKey_RightBracket)) size = std::min(500.0f, size * 1.2f);
|
||||||
if (key(ImGuiKey_Enter) || key(ImGuiKey_KeypadEnter)) lockLayer();
|
if (key(ImGuiKey_Enter) || key(ImGuiKey_KeypadEnter)) lockLayer();
|
||||||
if (key(ImGuiKey_Delete) && editLayer()) deleteLayer(*find(ledit_.object), ledit_.index);
|
if (key(ImGuiKey_Delete) && editLayer()) deleteLayer(*find(ledit_.object), ledit_.index);
|
||||||
if (key(ImGuiKey_F1)) showHelp_ = !showHelp_;
|
if (key(ImGuiKey_F1)) showHelp_ = !showHelp_;
|
||||||
|
|
@ -4716,7 +4871,9 @@ private:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (mouseInView(io) && !editLayer() && paintTool_ != PaintTool::Picture)
|
if (sculpting_) {
|
||||||
|
if (mouseInView(io)) dl->AddCircle(io.MousePos, sculptPx_, io.KeyCtrl ? IM_COL32(120, 180, 255, 220) : IM_COL32(255, 255, 255, 200), 40, 1.5f);
|
||||||
|
} else if (mouseInView(io) && !editLayer() && paintTool_ != PaintTool::Picture)
|
||||||
dl->AddCircle(io.MousePos, brushPx_, paintTool_ == PaintTool::Fill || paintTool_ == PaintTool::Picker ? IM_COL32(255, 255, 255, 90) : IM_COL32(255, 255, 255, 200), 40, 1.5f);
|
dl->AddCircle(io.MousePos, brushPx_, paintTool_ == PaintTool::Fill || paintTool_ == PaintTool::Picker ? IM_COL32(255, 255, 255, 90) : IM_COL32(255, 255, 255, 200), 40, 1.5f);
|
||||||
dl->PopClipRect();
|
dl->PopClipRect();
|
||||||
return;
|
return;
|
||||||
|
|
@ -5264,6 +5421,21 @@ private:
|
||||||
}
|
}
|
||||||
|
|
||||||
void paintToolbox(float w) {
|
void paintToolbox(float w) {
|
||||||
|
const float half = (w - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||||
|
if (wideButton("##colour-side", Icon::PaintBrush, "Colour", !sculpting_, half, "Paint colours on it (Tab)")) sculpting_ = false, endSculpt();
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (wideButton("##shape-side", Icon::Sculpt, "Sculpt", sculpting_, half, "Push its surface about like clay (Tab)")) sculpting_ = true, endStroke();
|
||||||
|
ImGui::Spacing();
|
||||||
|
if (sculpting_) {
|
||||||
|
grid(int(std::size(kSculptTools)), 38.0f, [&](int i) {
|
||||||
|
const SculptToolInfo& t = kSculptTools[i];
|
||||||
|
const std::string id = std::format("##{}", t.name), tip = std::format("{} ({})\n{}", t.name, t.key, t.hint);
|
||||||
|
if (iconButton(id.c_str(), t.icon, sculptBrush_ == t.brush, tip.c_str())) sculptBrush_ = t.brush;
|
||||||
|
});
|
||||||
|
for (const SculptToolInfo& t : kSculptTools)
|
||||||
|
if (t.brush == sculptBrush_) ImGui::TextColored(accentText(), "%s", t.name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
grid(int(std::size(kPaintTools)), 38.0f, [&](int i) {
|
grid(int(std::size(kPaintTools)), 38.0f, [&](int i) {
|
||||||
const PaintToolInfo& t = kPaintTools[i];
|
const PaintToolInfo& t = kPaintTools[i];
|
||||||
const std::string id = std::format("##{}", t.name), tip = std::format("{} ({})\n{}", t.name, t.key, t.hint);
|
const std::string id = std::format("##{}", t.name), tip = std::format("{} ({})\n{}", t.name, t.key, t.hint);
|
||||||
|
|
@ -5283,7 +5455,48 @@ private:
|
||||||
if (t.tool == paintTool_) ImGui::TextColored(accentText(), "%s", t.name);
|
if (t.tool == paintTool_) ImGui::TextColored(accentText(), "%s", t.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void sculptOptions(float w) {
|
||||||
|
for (const SculptToolInfo& t : kSculptTools)
|
||||||
|
if (t.brush == sculptBrush_) ImGui::SeparatorText(t.name), ImGui::TextWrapped("%s", t.hint);
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::SetNextItemWidth(w);
|
||||||
|
ImGui::SliderFloat("##size", &sculptPx_, 4.0f, 400.0f, "size %.0f px", ImGuiSliderFlags_Logarithmic);
|
||||||
|
if (sculptBrush_ != SculptBrush::Grab) {
|
||||||
|
ImGui::SetNextItemWidth(w);
|
||||||
|
ImGui::SliderFloat("##strength", &sculptStrength_, 0.02f, 1.0f, "strength %.2f");
|
||||||
|
}
|
||||||
|
ImGui::Checkbox("Same on both sides (X)", &sculptMirror_);
|
||||||
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("What you do on its left is done on its right too");
|
||||||
|
if (Object* o = selected_.size() == 1 ? find(active_) : nullptr) {
|
||||||
|
ImGui::SeparatorText(o->name.c_str());
|
||||||
|
ImGui::TextDisabled("%zu points", o->mesh.verts.size());
|
||||||
|
size_t after = 0;
|
||||||
|
for (const auto& f : o->mesh.faces) after += f.size();
|
||||||
|
ImGui::BeginDisabled(after > 400000 || o->locked);
|
||||||
|
if (ImGui::Button("Finer, for small details", ImVec2(w, 0))) {
|
||||||
|
pushUndo();
|
||||||
|
if (o->hasFinish()) o->mesh = finishedMesh(*o), o->mirror = 0, o->round = 0, o->smoothLevels = 0;
|
||||||
|
handMade(*o);
|
||||||
|
subdivide(o->mesh, false);
|
||||||
|
touched(o->id);
|
||||||
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("Each face split in four: the same shape, more points to push about");
|
||||||
|
if (ImGui::Button("Smooth all of it", ImVec2(w, 0)) && !o->locked) {
|
||||||
|
pushUndo();
|
||||||
|
handMade(*o);
|
||||||
|
const SculptMesh sm(o->mesh);
|
||||||
|
const glm::vec3 mid = (boundsMin(o->mesh) + boundsMax(o->mesh)) * 0.5f;
|
||||||
|
sculptDab(o->mesh, sm, SculptBrush::Smooth, {mid, glm::distance(boundsMin(o->mesh), boundsMax(o->mesh)) * 2.0f, 0.5f});
|
||||||
|
touched(o->id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::TextDisabled("Ctrl: the other way Shift: smooth\n[ and ]: brush size 1-6: brushes");
|
||||||
|
}
|
||||||
|
|
||||||
void paintOptions(float w) {
|
void paintOptions(float w) {
|
||||||
|
if (sculpting_) return sculptOptions(w);
|
||||||
for (const PaintToolInfo& t : kPaintTools)
|
for (const PaintToolInfo& t : kPaintTools)
|
||||||
if (t.tool == paintTool_) ImGui::SeparatorText(t.name);
|
if (t.tool == paintTool_) ImGui::SeparatorText(t.name);
|
||||||
if (paintTool_ == PaintTool::Brush || paintTool_ == PaintTool::Eraser) {
|
if (paintTool_ == PaintTool::Brush || paintTool_ == PaintTool::Eraser) {
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,45 @@ void drawIcon(ImDrawList* dl, Icon icon, ImVec2 o, float s, ImU32 c) {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
// Sculpting: a line of surface, and what the brush does to it.
|
||||||
|
case Icon::Sculpt: { // a bump raised, an arrow up out of it
|
||||||
|
const ImVec2 pts[] = {P(0.08f, 0.80f), P(0.30f, 0.78f), P(0.42f, 0.58f), P(0.58f, 0.58f), P(0.70f, 0.78f), P(0.92f, 0.80f)};
|
||||||
|
dl->AddPolyline(pts, 6, c, 0, t * 1.2f);
|
||||||
|
line(0.5f, 0.48f, 0.5f, 0.24f), tri(0.5f, 0.08f, 0.38f, 0.26f, 0.62f, 0.26f);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Icon::Inflate:
|
||||||
|
dl->AddCircle(P(0.5f, 0.5f), 0.2f * s, c, 24, t);
|
||||||
|
for (int i = 0; i < 4; ++i) {
|
||||||
|
const float a = 0.785f + 1.5708f * float(i), x = std::cos(a), y = std::sin(a);
|
||||||
|
line(0.5f + x * 0.28f, 0.5f + y * 0.28f, 0.5f + x * 0.42f, 0.5f + y * 0.42f);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Icon::Smooth: { // bumps above, evened out below
|
||||||
|
ImVec2 pts[9];
|
||||||
|
for (int i = 0; i < 9; ++i) pts[i] = P(0.1f + 0.1f * float(i), 0.3f + (i % 2 ? -0.1f : 0.08f));
|
||||||
|
dl->AddPolyline(pts, 9, dim, 0, t);
|
||||||
|
dl->PathArcTo(P(0.5f, 1.25f), 0.62f * s, 3.14159f * 1.2f, 3.14159f * 1.8f, 20);
|
||||||
|
dl->PathStroke(c, 0, t * 1.2f);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Icon::Flatten: // a bump pressed flat by a bar
|
||||||
|
dl->AddRectFilled(P(0.2f, 0.18f), P(0.8f, 0.32f), c, 0.04f * s);
|
||||||
|
line(0.5f, 0.36f, 0.5f, 0.5f), tri(0.5f, 0.62f, 0.4f, 0.48f, 0.6f, 0.48f);
|
||||||
|
line(0.08f, 0.78f, 0.92f, 0.78f, t * 1.2f);
|
||||||
|
break;
|
||||||
|
case Icon::Pinch: // two sides pulled in to a crease
|
||||||
|
line(0.08f, 0.5f, 0.34f, 0.5f), tri(0.46f, 0.5f, 0.32f, 0.4f, 0.32f, 0.6f);
|
||||||
|
line(0.92f, 0.5f, 0.66f, 0.5f), tri(0.54f, 0.5f, 0.68f, 0.4f, 0.68f, 0.6f);
|
||||||
|
line(0.5f, 0.2f, 0.5f, 0.8f, t * 0.7f);
|
||||||
|
break;
|
||||||
|
case Icon::Grab: { // a patch of surface pulled up and over
|
||||||
|
const ImVec2 pts[] = {P(0.08f, 0.82f), P(0.32f, 0.80f), P(0.46f, 0.50f), P(0.62f, 0.34f), P(0.72f, 0.80f), P(0.92f, 0.82f)};
|
||||||
|
dl->AddPolyline(pts, 6, c, 0, t * 1.2f);
|
||||||
|
dot(0.62f, 0.34f, 0.08f);
|
||||||
|
line(0.62f, 0.34f, 0.84f, 0.16f, t * 0.8f), tri(0.92f, 0.08f, 0.78f, 0.12f, 0.88f, 0.22f);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case Icon::LayFlat:
|
case Icon::LayFlat:
|
||||||
quad(0.22f, 0.40f, 0.62f, 0.22f, 0.86f, 0.50f, 0.46f, 0.68f, false, c);
|
quad(0.22f, 0.40f, 0.62f, 0.22f, 0.86f, 0.50f, 0.46f, 0.68f, false, c);
|
||||||
line(0.10f, 0.88f, 0.90f, 0.88f);
|
line(0.10f, 0.88f, 0.90f, 0.88f);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
namespace fry {
|
namespace fry {
|
||||||
|
|
||||||
enum class Icon { Select, Move, Rotate, Scale, Draw, Extrude, Inset, Bevel, LoopCut, Objects, Shape, Points, Edges, Faces, Box, Brush, Lasso, Add, Knife, Hole, PaintBrush, Eraser, Fill, Picker, Picture, Render, Layout, Print, Ruler, LayFlat };
|
enum class Icon { Select, Move, Rotate, Scale, Draw, Extrude, Inset, Bevel, LoopCut, Objects, Shape, Points, Edges, Faces, Box, Brush, Lasso, Add, Knife, Hole, PaintBrush, Eraser, Fill, Picker, Picture, Render, Layout, Print, Ruler, LayFlat, Sculpt, Inflate, Smooth, Flatten, Pinch, Grab };
|
||||||
|
|
||||||
void drawIcon(ImDrawList* dl, Icon icon, ImVec2 topLeft, float size, ImU32 color);
|
void drawIcon(ImDrawList* dl, Icon icon, ImVec2 topLeft, float size, ImU32 color);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#include "script.h"
|
#include "script.h"
|
||||||
|
#include "sculpt.h"
|
||||||
#include "shapes.h"
|
#include "shapes.h"
|
||||||
|
|
||||||
// (Lua is built as C++ here — cmake/Dependencies.cmake — so its errors are exceptions and unwind
|
// (Lua is built as C++ here — cmake/Dependencies.cmake — so its errors are exceptions and unwind
|
||||||
|
|
@ -332,6 +333,30 @@ int objSubdivide(lua_State* L) {
|
||||||
lua_settop(L, 1);
|
lua_settop(L, 1);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
// o:sculpt("raise", at, radius, strength, times): a brush pressed at a point (the scene's space),
|
||||||
|
// like the Sculpt brushes. Made finer first if it has too few points, as in the app.
|
||||||
|
int objSculpt(lua_State* L) {
|
||||||
|
Object& o = object(L, 1);
|
||||||
|
static const std::pair<const char*, std::pair<SculptBrush, bool>> names[] = {
|
||||||
|
{"raise", {SculptBrush::Draw, false}}, {"dig", {SculptBrush::Draw, true}}, {"puff", {SculptBrush::Inflate, false}},
|
||||||
|
{"shrink", {SculptBrush::Inflate, true}}, {"smooth", {SculptBrush::Smooth, false}}, {"flatten", {SculptBrush::Flatten, false}},
|
||||||
|
{"pinch", {SculptBrush::Pinch, false}}, {"spread", {SculptBrush::Pinch, true}},
|
||||||
|
};
|
||||||
|
const std::string name = lower(luaL_checkstring(L, 2));
|
||||||
|
const auto* it = std::find_if(std::begin(names), std::end(names), [&](const auto& n) { return name == n.first; });
|
||||||
|
if (it == std::end(names)) return luaL_error(L, "there's no brush called %s (raise, dig, puff, shrink, smooth, flatten, pinch, spread)", name.c_str());
|
||||||
|
const glm::mat4 inv = glm::inverse(objectMatrix(o));
|
||||||
|
const float scale = (std::abs(o.scale.x) + std::abs(o.scale.y) + std::abs(o.scale.z)) / 3.0f;
|
||||||
|
Dab dab{glm::vec3(inv * glm::vec4(toVec(L, 3), 1.0f)), float(luaL_optnumber(L, 4, 0.2)) / std::max(scale, 1e-6f), float(std::clamp(luaL_optnumber(L, 5, 0.5), 0.0, 1.0)), it->second.second};
|
||||||
|
const int times = std::clamp(int(luaL_optinteger(L, 6, 1)), 1, 1000);
|
||||||
|
if (o.hasFinish()) o.mesh = finishedMesh(o), o.mirror = 0, o.round = 0, o.smoothLevels = 0;
|
||||||
|
if (finerForSculpting(o.mesh)) o.smooth = true;
|
||||||
|
const SculptMesh sm(o.mesh);
|
||||||
|
for (int i = 0; i < times; ++i) sculptDab(o.mesh, sm, it->second.first, dab);
|
||||||
|
handMade(o);
|
||||||
|
lua_settop(L, 1);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
int objBevel(lua_State* L) {
|
int objBevel(lua_State* L) {
|
||||||
Object& o = object(L, 1);
|
Object& o = object(L, 1);
|
||||||
const float width = float(luaL_checknumber(L, 2));
|
const float width = float(luaL_checknumber(L, 2));
|
||||||
|
|
@ -480,7 +505,7 @@ int objDrop(lua_State* L) { // stands it on the ground (z = 0)
|
||||||
const luaL_Reg kMethods[] = {
|
const luaL_Reg kMethods[] = {
|
||||||
{"move", objMove}, {"rotate", objRotate}, {"scale", objScale}, {"delete", objDelete}, {"copy", objCopy},
|
{"move", objMove}, {"rotate", objRotate}, {"scale", objScale}, {"delete", objDelete}, {"copy", objCopy},
|
||||||
{"add", objAdd}, {"subtract", objSubtract}, {"overlap", objOverlap}, {"join", objJoin}, {"thicken", objThicken},
|
{"add", objAdd}, {"subtract", objSubtract}, {"overlap", objOverlap}, {"join", objJoin}, {"thicken", objThicken},
|
||||||
{"subdivide", objSubdivide}, {"bevel", objBevel}, {"bake", objBake}, {"points", objPoints}, {"faces", objFaces},
|
{"subdivide", objSubdivide}, {"sculpt", objSculpt}, {"bevel", objBevel}, {"bake", objBake}, {"points", objPoints}, {"faces", objFaces},
|
||||||
{"setpoints", objSetPoints}, {"setshape", objSetShape}, {"set", objSet}, {"settings", objSettings}, {"bounds", objBounds},
|
{"setpoints", objSetPoints}, {"setshape", objSetShape}, {"set", objSet}, {"settings", objSettings}, {"bounds", objBounds},
|
||||||
{"select", objSelect}, {"drop", objDrop}, {nullptr, nullptr},
|
{"select", objSelect}, {"drop", objDrop}, {nullptr, nullptr},
|
||||||
};
|
};
|
||||||
|
|
@ -811,6 +836,8 @@ An object (o):
|
||||||
o:copy() o:delete() o:select() o:bounds() -> lowest, highest corner
|
o:copy() o:delete() o:select() o:bounds() -> lowest, highest corner
|
||||||
o:add(others...) o:subtract(others...) o:overlap(others...) o:join(others...) (the others go)
|
o:add(others...) o:subtract(others...) o:overlap(others...) o:join(others...) (the others go)
|
||||||
o:thicken(t) o:subdivide(smooth, times) o:bevel(width, steps, angle) o:bake()
|
o:thicken(t) o:subdivide(smooth, times) o:bevel(width, steps, angle) o:bake()
|
||||||
|
o:sculpt(brush, at, radius, strength, times) brush: raise, dig, puff, shrink, smooth,
|
||||||
|
flatten, pinch, spread; at: a point on it (the scene's space). Made finer first if needed
|
||||||
o:points() o:faces() o:setpoints(points) o:setshape(points, faces) o:set(name, value) o:settings()
|
o:points() o:faces() o:setpoints(points) o:setshape(points, faces) o:set(name, value) o:settings()
|
||||||
)";
|
)";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
100
src/sculpt.cpp
Normal file
100
src/sculpt.cpp
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
#include "sculpt.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace fry {
|
||||||
|
|
||||||
|
SculptMesh::SculptMesh(const Mesh& m) : next(m.verts.size()), faces(m.verts.size()) {
|
||||||
|
for (const auto& [a, b] : edges(m)) next[size_t(a)].push_back(b), next[size_t(b)].push_back(a);
|
||||||
|
for (size_t f = 0; f < m.faces.size(); ++f)
|
||||||
|
for (int v : m.faces[f]) faces[size_t(v)].push_back(int(f));
|
||||||
|
}
|
||||||
|
|
||||||
|
float sculptFalloff(float d, float radius) {
|
||||||
|
if (d >= radius) return 0.0f;
|
||||||
|
const float s = 1.0f - d / radius;
|
||||||
|
return s * s * (3.0f - 2.0f * s);
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::vec3 pointNormal(const Mesh& m, const SculptMesh& s, int point) {
|
||||||
|
glm::vec3 n(0.0f);
|
||||||
|
for (int f : s.faces[size_t(point)]) n += faceNormal(m, f) * std::max(faceArea(m, f), 1e-12f);
|
||||||
|
const float len = glm::length(n);
|
||||||
|
return len > 1e-20f ? n / len : glm::vec3(0, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sculptDab(Mesh& m, const SculptMesh& s, SculptBrush brush, const Dab& dab) {
|
||||||
|
if (brush == SculptBrush::Grab || dab.radius <= 0.0f) return;
|
||||||
|
// The points it reaches, how much, and the surface under it: its normal and middle.
|
||||||
|
std::vector<std::pair<int, float>> under;
|
||||||
|
glm::vec3 normal(0.0f), middle(0.0f);
|
||||||
|
float total = 0.0f;
|
||||||
|
for (size_t i = 0; i < m.verts.size(); ++i) {
|
||||||
|
const float w = sculptFalloff(glm::distance(m.verts[i], dab.at), dab.radius);
|
||||||
|
if (w <= 0.0f || s.faces[i].empty()) continue;
|
||||||
|
under.push_back({int(i), w});
|
||||||
|
normal += pointNormal(m, s, int(i)) * w;
|
||||||
|
middle += m.verts[i] * w, total += w;
|
||||||
|
}
|
||||||
|
if (under.empty()) return;
|
||||||
|
middle /= total;
|
||||||
|
normal = glm::length(normal) > 1e-12f ? glm::normalize(normal) : glm::vec3(0, 0, 1);
|
||||||
|
const float sign = dab.invert ? -1.0f : 1.0f;
|
||||||
|
// A sixth of the brush per dab at full strength (dabs come every quarter brush, so a stroke
|
||||||
|
// over a point raises it about half the brush's size).
|
||||||
|
const float step = dab.radius * 0.16f * std::clamp(dab.strength, 0.0f, 1.0f);
|
||||||
|
|
||||||
|
// (Worked out from where the points were before this dab, so it doesn't depend on order.)
|
||||||
|
std::vector<glm::vec3> moved(under.size());
|
||||||
|
for (size_t k = 0; k < under.size(); ++k) {
|
||||||
|
const auto [i, w] = under[k];
|
||||||
|
const glm::vec3 p = m.verts[size_t(i)];
|
||||||
|
switch (brush) {
|
||||||
|
case SculptBrush::Draw: moved[k] = p + normal * (sign * step * w); break;
|
||||||
|
case SculptBrush::Inflate: moved[k] = p + pointNormal(m, s, i) * (sign * step * w); break;
|
||||||
|
case SculptBrush::Smooth: {
|
||||||
|
glm::vec3 avg(0.0f);
|
||||||
|
for (int n : s.next[size_t(i)]) avg += m.verts[size_t(n)];
|
||||||
|
if (s.next[size_t(i)].empty()) avg = p;
|
||||||
|
else avg /= float(s.next[size_t(i)].size());
|
||||||
|
moved[k] = glm::mix(p, avg, std::min(1.0f, w * dab.strength));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SculptBrush::Flatten: moved[k] = p - normal * (glm::dot(p - middle, normal) * std::min(1.0f, w * dab.strength * 0.5f)); break;
|
||||||
|
case SculptBrush::Pinch: {
|
||||||
|
glm::vec3 in = dab.at - p;
|
||||||
|
in -= normal * glm::dot(in, normal); // along the surface, not into it
|
||||||
|
moved[k] = p + in * (sign * std::min(0.5f, w * dab.strength * 0.15f));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SculptBrush::Grab: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (size_t k = 0; k < under.size(); ++k) m.verts[size_t(under[k].first)] = moved[k];
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::pair<int, float>> grabbed(const Mesh& m, const glm::vec3& at, float radius) {
|
||||||
|
std::vector<std::pair<int, float>> out;
|
||||||
|
for (size_t i = 0; i < m.verts.size(); ++i)
|
||||||
|
if (const float w = sculptFalloff(glm::distance(m.verts[i], at), radius); w > 0.0f) out.push_back({int(i), w});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
void moveGrabbed(Mesh& m, const std::vector<std::pair<int, float>>& points, const std::vector<glm::vec3>& from, const glm::vec3& offset) {
|
||||||
|
for (size_t k = 0; k < points.size() && k < from.size(); ++k) m.verts[size_t(points[k].first)] = from[k] + offset * points[k].second;
|
||||||
|
}
|
||||||
|
|
||||||
|
int finerForSculpting(Mesh& m, size_t points, size_t limit) {
|
||||||
|
int times = 0;
|
||||||
|
while (m.verts.size() < points) {
|
||||||
|
size_t after = 0; // (each face becomes as many as it has corners)
|
||||||
|
for (const auto& f : m.faces) after += f.size();
|
||||||
|
if (after > limit || m.faces.empty()) break;
|
||||||
|
subdivide(m, false);
|
||||||
|
++times;
|
||||||
|
}
|
||||||
|
return times;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fry
|
||||||
51
src/sculpt.h
Normal file
51
src/sculpt.h
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
#pragma once
|
||||||
|
// Sculpting: brushes that push a mesh's points about like clay — the points under the brush
|
||||||
|
// move most in its middle, less towards its edge. It only moves points (no new ones): a shape
|
||||||
|
// with few points is made finer first (finerForSculpting).
|
||||||
|
#include "mesh.h"
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fry {
|
||||||
|
|
||||||
|
enum class SculptBrush {
|
||||||
|
Draw, // raise (or, inverted, dig into) the surface under it
|
||||||
|
Inflate, // puff out along each point's own normal (inverted: shrink)
|
||||||
|
Smooth, // even out bumps
|
||||||
|
Flatten, // press onto the plane the surface under it is closest to
|
||||||
|
Pinch, // pull points in to the middle (inverted: push apart) — sharpens creases
|
||||||
|
Grab, // take hold of a patch and drag it (grabbed / moveGrabbed)
|
||||||
|
};
|
||||||
|
|
||||||
|
// What a stroke needs to know about the mesh, made once when it starts (the faces don't change
|
||||||
|
// while sculpting, only where the points are).
|
||||||
|
struct SculptMesh {
|
||||||
|
explicit SculptMesh(const Mesh& m);
|
||||||
|
std::vector<std::vector<int>> next; // each point's neighbours along edges
|
||||||
|
std::vector<std::vector<int>> faces; // the faces round each point
|
||||||
|
};
|
||||||
|
|
||||||
|
// One dab, in the mesh's own space. strength: 0..1; invert: the other way.
|
||||||
|
struct Dab {
|
||||||
|
glm::vec3 at{0.0f};
|
||||||
|
float radius = 0.1f, strength = 0.5f;
|
||||||
|
bool invert = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// How much of a dab reaches a point `d` away: 1 in the middle, 0 at the edge, smoothly.
|
||||||
|
float sculptFalloff(float d, float radius);
|
||||||
|
// A point's normal: its faces', averaged.
|
||||||
|
glm::vec3 pointNormal(const Mesh& m, const SculptMesh& s, int point);
|
||||||
|
void sculptDab(Mesh& m, const SculptMesh& s, SculptBrush brush, const Dab& dab);
|
||||||
|
|
||||||
|
// Grab: the points under the brush and how much each follows it; then, as it's dragged, each
|
||||||
|
// put `offset` × its share away from where it was when grabbed (`from`).
|
||||||
|
std::vector<std::pair<int, float>> grabbed(const Mesh& m, const glm::vec3& at, float radius);
|
||||||
|
void moveGrabbed(Mesh& m, const std::vector<std::pair<int, float>>& points, const std::vector<glm::vec3>& from, const glm::vec3& offset);
|
||||||
|
|
||||||
|
// Made finer without changing its shape (each face split into four, flat) until it has at least
|
||||||
|
// `points` points, or would have more than `limit` faces. Returns how many times it was split.
|
||||||
|
int finerForSculpting(Mesh& m, size_t points = 6000, size_t limit = 400000);
|
||||||
|
|
||||||
|
} // namespace fry
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
#include "image.h"
|
#include "image.h"
|
||||||
#include "scene.h"
|
#include "scene.h"
|
||||||
#include "shapes.h"
|
#include "shapes.h"
|
||||||
|
#include "sculpt.h"
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
|
@ -425,6 +426,36 @@ int main() {
|
||||||
const std::string text((std::istreambuf_iterator<char>(j)), {});
|
const std::string text((std::istreambuf_iterator<char>(j)), {});
|
||||||
CHECK(text.find("\"translation\":[2,3,1]") != std::string::npos, "in glTF's axes (Y up): the position is [2,3,1]");
|
CHECK(text.find("\"translation\":[2,3,1]") != std::string::npos, "in glTF's axes (Y up): the position is [2,3,1]");
|
||||||
|
|
||||||
|
{ // Sculpting: a cube made finer keeps its shape; brushes move only what's under them.
|
||||||
|
Mesh m = makeCube(1.0f);
|
||||||
|
const int times = finerForSculpting(m, 2000);
|
||||||
|
CHECK(times == 5 && m.verts.size() >= 2000 && closed(m) && boundsMax(m) == glm::vec3(0.5f), "a cube made finer for sculpting: %d times, %zu points, same box", times, m.verts.size());
|
||||||
|
const SculptMesh sm(m);
|
||||||
|
const Mesh before = m;
|
||||||
|
const glm::vec3 top(0, 0, 0.5f);
|
||||||
|
for (int i = 0; i < 20; ++i) sculptDab(m, sm, SculptBrush::Draw, {top, 0.25f, 1.0f});
|
||||||
|
CHECK(boundsMax(m).z > 0.55f && boundsMin(m) == boundsMin(before), "draw raises the top (%.3f) and leaves the rest", boundsMax(m).z);
|
||||||
|
CHECK(closed(m), "still closed after sculpting");
|
||||||
|
Mesh dug = before;
|
||||||
|
for (int i = 0; i < 20; ++i) sculptDab(dug, sm, SculptBrush::Draw, {top, 0.25f, 1.0f, true});
|
||||||
|
CHECK(boundsMax(dug).z == 0.5f, "inverted, it digs in: nothing above the top");
|
||||||
|
float rough = 0.0f, smooth = 0.0f; // how far the top's points are from flat
|
||||||
|
Mesh bumpy = m;
|
||||||
|
for (int i = 0; i < 10; ++i) sculptDab(bumpy, sm, SculptBrush::Smooth, {top, 0.3f, 1.0f});
|
||||||
|
for (size_t i = 0; i < m.verts.size(); ++i)
|
||||||
|
if (glm::length(glm::vec2(m.verts[i])) < 0.15f && m.verts[i].z > 0.4f) rough += m.verts[i].z - 0.5f, smooth += bumpy.verts[i].z - 0.5f;
|
||||||
|
CHECK(smooth < rough, "smoothing lowers the bump (%.3f to %.3f)", rough, smooth);
|
||||||
|
Mesh flat = m;
|
||||||
|
for (int i = 0; i < 30; ++i) sculptDab(flat, sm, SculptBrush::Flatten, {top, 0.3f, 1.0f});
|
||||||
|
CHECK(boundsMax(flat).z < boundsMax(m).z, "flatten presses it down");
|
||||||
|
Mesh g = before;
|
||||||
|
const auto held = grabbed(g, top, 0.2f);
|
||||||
|
std::vector<glm::vec3> from;
|
||||||
|
for (const auto& [i, w] : held) from.push_back(g.verts[size_t(i)]);
|
||||||
|
moveGrabbed(g, held, from, glm::vec3(0, 0, 0.3f));
|
||||||
|
CHECK(!held.empty() && std::abs(boundsMax(g).z - 0.8f) < 0.01f && closed(g), "grab pulls the middle up with it (%.3f)", boundsMax(g).z);
|
||||||
|
}
|
||||||
|
|
||||||
std::printf("%s (%d failure%s)\n", g_failures ? "FAILED" : "passed", g_failures, g_failures == 1 ? "" : "s");
|
std::printf("%s (%d failure%s)\n", g_failures ? "FAILED" : "passed", g_failures, g_failures == 1 ? "" : "s");
|
||||||
return g_failures ? 1 : 0;
|
return g_failures ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,8 @@ int main() {
|
||||||
r = runScript(host, "local o = fry.add('cube')\no.material = 'Gold'\nlocal m = fry.add('cube', {color = '#102030', material = 'glossy'})\nprint(#fry.materials())");
|
r = runScript(host, "local o = fry.add('cube')\no.material = 'Gold'\nlocal m = fry.add('cube', {color = '#102030', material = 'glossy'})\nprint(#fry.materials())");
|
||||||
CHECK(r.ok && scene.objects.back().metal == 0.0f && scene.objects.back().rough < 0.2f && scene.objects.back().color.r < 0.1f, "a material keeping its colour (%s)", r.error.c_str());
|
CHECK(r.ok && scene.objects.back().metal == 0.0f && scene.objects.back().rough < 0.2f && scene.objects.back().color.r < 0.1f, "a material keeping its colour (%s)", r.error.c_str());
|
||||||
CHECK(scene.objects[scene.objects.size() - 2].metal == 1.0f && scene.objects[scene.objects.size() - 2].color.g > 0.7f, "gold");
|
CHECK(scene.objects[scene.objects.size() - 2].metal == 1.0f && scene.objects[scene.objects.size() - 2].color.g > 0.7f, "gold");
|
||||||
|
r = runScript(host, "local b = fry.add('sphere', {radius = 1})\nb:sculpt('raise', {0, 0, 1}, 0.4, 1, 20)\nlocal lo, hi = b:bounds()\nprint(hi.z > 1.05)");
|
||||||
|
CHECK(r.ok && out.find("true\n") != std::string::npos, "o:sculpt raises the top (%s)", r.error.c_str());
|
||||||
r = runScript(host, "fry.add('cube').material = 'cheese'");
|
r = runScript(host, "fry.add('cube').material = 'cheese'");
|
||||||
CHECK(!r.ok && r.error.find("no material called cheese") != std::string::npos, "an unknown material is an error");
|
CHECK(!r.ok && r.error.find("no material called cheese") != std::string::npos, "an unknown material is an error");
|
||||||
r = runScript(host, "fry.clear()");
|
r = runScript(host, "fry.clear()");
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue