mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-13 12:07:41 +00:00
Improve texture displacement smoothing and UV editor framing
This commit is contained in:
@@ -123,10 +123,10 @@ DecodedTextureCache g_decoded_texture_cache;
|
||||
} // namespace
|
||||
|
||||
namespace {
|
||||
// A few passes of a separable box blur approximate a Gaussian, cheaply. `radius` is in pixels; 0 is
|
||||
// a no-op. Wraps at the edges so a tiling height map stays seamless after smoothing. Operates on the
|
||||
// grayscale byte buffer in place.
|
||||
void smooth_height_pixels(std::vector<uint8_t> &pixels, int width, int height, int radius)
|
||||
// A few passes of a separable box blur approximate a Gaussian, cheaply. `radius` is in whole texels;
|
||||
// 0 is a no-op. Wraps at the edges so a tiling height map stays seamless after smoothing. Operates on
|
||||
// the grayscale byte buffer in place.
|
||||
void smooth_height_pixels_box(std::vector<uint8_t> &pixels, int width, int height, int radius)
|
||||
{
|
||||
if (radius <= 0 || width <= 0 || height <= 0 || pixels.size() != size_t(width) * size_t(height))
|
||||
return;
|
||||
@@ -162,6 +162,28 @@ void smooth_height_pixels(std::vector<uint8_t> &pixels, int width, int height, i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The same blur with a *continuous* radius, which is what the Smoothing slider drives.
|
||||
//
|
||||
// A box blur can only work in whole texels, so mapping the slider straight onto a rounded radius
|
||||
// made it move in visible jumps - and its very first step off zero was a full one-texel blur rather
|
||||
// than a hint of one, which is what made the control feel like it switched on rather than ramped up.
|
||||
// Blur at the next whole texel up and cross-fade the raw image back in by the fraction left over:
|
||||
// below one texel that fade *is* the sub-texel kernel, and above it it turns each integer step into
|
||||
// a continuous ramp.
|
||||
void smooth_height_pixels(std::vector<uint8_t> &pixels, int width, int height, float radius)
|
||||
{
|
||||
if (radius <= 0.f || width <= 0 || height <= 0 || pixels.size() != size_t(width) * size_t(height))
|
||||
return;
|
||||
|
||||
const int whole = std::max(1, int(std::ceil(radius)));
|
||||
const float mix = std::clamp(radius / float(whole), 0.f, 1.f);
|
||||
const std::vector<uint8_t> raw = (mix < 0.999f) ? pixels : std::vector<uint8_t>{};
|
||||
smooth_height_pixels_box(pixels, width, height, whole);
|
||||
if (!raw.empty())
|
||||
for (size_t i = 0; i < pixels.size(); ++i)
|
||||
pixels[i] = uint8_t(std::lround(float(raw[i]) + (float(pixels[i]) - float(raw[i])) * mix));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer)
|
||||
@@ -231,11 +253,14 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
|
||||
entries[key] = { std::weak_ptr<const std::vector<unsigned char>>(layer.image_data), result };
|
||||
}
|
||||
|
||||
// Smoothing radius scales with the texture so the effect is resolution-independent; capped so the
|
||||
// blur stays affordable on large maps.
|
||||
// Smoothing radius scales with the texture so the same slider value blurs the same *fraction* of
|
||||
// the image whatever resolution it came in at, and stays continuous in the slider - see
|
||||
// smooth_height_pixels(). The cap is a cost limit, not part of the mapping: the blur is
|
||||
// O(width * height * radius) per pass, so a large map with the slider at the top would otherwise
|
||||
// stall every preview rebuild.
|
||||
if (layer.smoothing > 0.f) {
|
||||
const int max_radius = std::clamp(int(std::lround(0.02f * std::min(result.width, result.height))), 1, 32);
|
||||
const int radius = std::max(1, int(std::lround(layer.smoothing * float(max_radius))));
|
||||
const float span = 0.05f * float(std::min(result.width, result.height));
|
||||
const float radius = std::clamp(layer.smoothing, 0.f, 1.f) * std::min(span, 48.f);
|
||||
smooth_height_pixels(result.pixels, result.width, result.height, radius);
|
||||
// Colour gets the same blur, per channel. It is the same knob for the same reason: detail in
|
||||
// the image finer than the mesh can carry is noise either way, and low-passing it here is the
|
||||
@@ -1372,7 +1397,8 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
|
||||
const std::vector<TextureDisplacementLayer> &layers,
|
||||
const TextureDisplacementFacetsData &facets_data,
|
||||
const TextureDisplacementOptions &options,
|
||||
const DisplacementProgressFn &progress)
|
||||
const DisplacementProgressFn &progress,
|
||||
bool flip_normals)
|
||||
{
|
||||
HeightFieldSampler combined = make_combined_displacement_sampler(mesh, layers, facets_data);
|
||||
if (!combined)
|
||||
@@ -1424,11 +1450,21 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
|
||||
return combined(pos, smooth_normal);
|
||||
};
|
||||
|
||||
// The pipeline takes its displacement direction from the soup's winding, so a mirrored placement
|
||||
// would drive the whole relief inwards. The paint masks were read off `mesh` above, against its
|
||||
// own vertex order, so the winding can only be turned round after that - here, on the copy that
|
||||
// becomes the soup - and has to be turned back on the way out, since the caller undoes the same
|
||||
// mirror when it maps the result back into the volume's coordinates.
|
||||
indexed_triangle_set oriented = mesh;
|
||||
if (flip_normals)
|
||||
for (stl_triangle_vertex_indices &t : oriented.indices)
|
||||
std::swap(t[1], t[2]);
|
||||
|
||||
// 0 means no simplification, i.e. Bake mode.
|
||||
const TextureBake::PipelineMode mode = settings.max_triangles > 0 ? TextureBake::PipelineMode::Export
|
||||
: TextureBake::PipelineMode::Bake;
|
||||
TextureBake::PipelineResult result = TextureBake::run_pipeline(
|
||||
TextureBake::to_soup(mesh, excluded), sample, settings, bounds, mode, excluded,
|
||||
TextureBake::to_soup(oriented, excluded), sample, settings, bounds, mode, excluded,
|
||||
[&progress](const char *, double f) {
|
||||
return !progress || progress(std::clamp(int(f * 100.0), 0, 99));
|
||||
});
|
||||
@@ -1436,17 +1472,29 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
|
||||
return {};
|
||||
|
||||
indexed_triangle_set out = TextureBake::to_indexed_triangle_set(result.geometry);
|
||||
return out.indices.empty() ? mesh : out;
|
||||
if (out.indices.empty())
|
||||
return mesh;
|
||||
if (flip_normals)
|
||||
for (stl_triangle_vertex_indices &t : out.indices)
|
||||
std::swap(t[1], t[2]);
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
indexed_triangle_set build_texture_displacement(const indexed_triangle_set &base_mesh,
|
||||
const std::vector<TextureDisplacementLayer> &layers,
|
||||
const TextureDisplacementFacetsData &facets_data,
|
||||
const TextureDisplacementOptions &options,
|
||||
const DisplacementProgressFn &progress,
|
||||
const TextureColorRequest *color)
|
||||
// The bake proper. Runs entirely in whatever space `base_mesh` is given in; the public entry point
|
||||
// below is what puts it in world space and brings the result back.
|
||||
// `flip_normals` says the mesh is wound the opposite way round from its outward direction, which is
|
||||
// what a mirroring world transform leaves behind: the positions are right, but every normal derived
|
||||
// from the winding points into the model. See build_texture_displacement().
|
||||
static indexed_triangle_set build_texture_displacement_in_place(
|
||||
const indexed_triangle_set &base_mesh,
|
||||
const std::vector<TextureDisplacementLayer> &layers,
|
||||
const TextureDisplacementFacetsData &facets_data,
|
||||
const TextureDisplacementOptions &options,
|
||||
const DisplacementProgressFn &progress,
|
||||
const TextureColorRequest *color,
|
||||
bool flip_normals)
|
||||
{
|
||||
// Returns true to keep going. An aborted run returns {} (see the header): an empty mesh is the
|
||||
// one result no caller can mistake for a finished bake and commit onto the volume.
|
||||
@@ -1465,7 +1513,7 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
return mesh;
|
||||
|
||||
if (options.pipeline_v2)
|
||||
return build_texture_displacement_v2(mesh, layers, facets_data, options, progress);
|
||||
return build_texture_displacement_v2(mesh, layers, facets_data, options, progress, flip_normals);
|
||||
|
||||
// Layers are combined in slot order, like stacked layers in an image editor: each one folds its
|
||||
// own displacement into the running total via its blend mode (see TextureBlendMode).
|
||||
@@ -1518,6 +1566,14 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
}
|
||||
}
|
||||
|
||||
// Both normal passes above read their direction out of the triangle winding, so a mirrored
|
||||
// placement leaves every one of them pointing into the model - the relief would be carved rather
|
||||
// than raised. Correct them once, here, where every later stage (displacement direction, the
|
||||
// planar/triplanar projection axes, the cylinder axis) picks them up already right.
|
||||
if (flip_normals)
|
||||
for (Vec3f &n : vertex_normals)
|
||||
n = -n;
|
||||
|
||||
if (!report(5))
|
||||
return {};
|
||||
|
||||
@@ -1782,6 +1838,11 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
if (!any_displacement)
|
||||
return mesh;
|
||||
|
||||
// The model's own resting plane, taken before anything moves - see the clamp after smoothing.
|
||||
float resting_z = std::numeric_limits<float>::max();
|
||||
for (const Vec3f &v : mesh.vertices)
|
||||
resting_z = std::min(resting_z, v.z());
|
||||
|
||||
for (size_t vi = 0; vi < mesh.vertices.size(); ++vi)
|
||||
if (displaced[vi])
|
||||
mesh.vertices[vi] += vertex_normals[vi] * displacement[vi];
|
||||
@@ -1807,6 +1868,18 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
}) : DisplacementProgressFn{});
|
||||
}
|
||||
|
||||
// Nothing driven below the model's own resting plane can be printed: it is either through the
|
||||
// build plate or, once the slicer drops the part back down onto it, holding the whole model up in
|
||||
// the air. Push it back up to the plane. Only vertices the displacement actually moved are
|
||||
// eligible - untouched geometry is already exactly where it started - and only those that ended
|
||||
// up below it, so downward relief that stays clear of the plate is left alone. Runs in world
|
||||
// space (see build_texture_displacement()), so this really is the plate and not some scaled
|
||||
// stand-in for it.
|
||||
if (resting_z < std::numeric_limits<float>::max())
|
||||
for (size_t vi = 0; vi < mesh.vertices.size(); ++vi)
|
||||
if (displaced[vi] && mesh.vertices[vi].z() < resting_z)
|
||||
mesh.vertices[vi].z() = resting_z;
|
||||
|
||||
if (!report(99))
|
||||
return {};
|
||||
if (want_color) {
|
||||
@@ -1835,6 +1908,51 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
return mesh;
|
||||
}
|
||||
|
||||
indexed_triangle_set build_texture_displacement(const indexed_triangle_set &base_mesh,
|
||||
const std::vector<TextureDisplacementLayer> &layers,
|
||||
const TextureDisplacementFacetsData &facets_data,
|
||||
const TextureDisplacementOptions &options,
|
||||
const DisplacementProgressFn &progress,
|
||||
const TextureColorRequest *color,
|
||||
const Transform3d &volume_to_world)
|
||||
{
|
||||
// An untransformed volume on an untransformed instance is by far the common case, and the round
|
||||
// trip costs two matrix multiplies per vertex on a mesh that can carry millions of them - so take
|
||||
// the identity out of the way rather than paying for it.
|
||||
if (volume_to_world.matrix().isApprox(Transform3d::Identity().matrix()))
|
||||
return build_texture_displacement_in_place(base_mesh, layers, facets_data, options, progress, color, false);
|
||||
|
||||
const Transform3d to_local = volume_to_world.inverse();
|
||||
// A mirroring placement leaves the positions correct but every winding-derived normal pointing
|
||||
// the wrong way. The winding itself is deliberately *not* touched here: the paint masks encode
|
||||
// each split triangle against its own vertex order, so reordering a triangle's vertices would
|
||||
// mirror the paint inside it. The bake is told instead, and negates the normals it computes.
|
||||
const bool mirrored = volume_to_world.linear().determinant() < 0.0;
|
||||
|
||||
indexed_triangle_set world = base_mesh;
|
||||
for (Vec3f &v : world.vertices)
|
||||
v = (volume_to_world * v.cast<double>()).cast<float>();
|
||||
|
||||
indexed_triangle_set out =
|
||||
build_texture_displacement_in_place(world, layers, facets_data, options, progress, color, mirrored);
|
||||
// A cancelled run returns {} and must stay {} - an empty mesh is the signal the caller checks
|
||||
// before committing anything onto the volume.
|
||||
if (out.vertices.empty())
|
||||
return out;
|
||||
|
||||
for (Vec3f &v : out.vertices)
|
||||
v = (to_local * v.cast<double>()).cast<float>();
|
||||
return out;
|
||||
}
|
||||
|
||||
Transform3d texture_displacement_volume_to_world(const ModelVolume &volume)
|
||||
{
|
||||
const ModelObject *object = volume.get_object();
|
||||
if (object == nullptr || object->instances.empty() || object->instances.front() == nullptr)
|
||||
return volume.get_matrix();
|
||||
return object->instances.front()->get_matrix() * volume.get_matrix();
|
||||
}
|
||||
|
||||
indexed_triangle_set build_texture_displacement(const ModelVolume &volume)
|
||||
{
|
||||
TextureDisplacementFacetsData facets_data;
|
||||
@@ -1842,7 +1960,8 @@ indexed_triangle_set build_texture_displacement(const ModelVolume &volume)
|
||||
facets_data[size_t(i)] = volume.texture_displacement_facet(i).get_data();
|
||||
|
||||
return build_texture_displacement(volume.mesh().its, volume.texture_displacement_layers, facets_data,
|
||||
volume.texture_displacement_options);
|
||||
volume.texture_displacement_options, {}, nullptr,
|
||||
texture_displacement_volume_to_world(volume));
|
||||
}
|
||||
|
||||
void smooth_mesh_vertices(indexed_triangle_set &mesh, const std::vector<uint8_t> &movable, float strength,
|
||||
|
||||
@@ -709,12 +709,28 @@ struct TextureColorRequest
|
||||
// straight to a TriangleSelector without a second mapping table.
|
||||
std::vector<uint8_t> *out_triangle = nullptr;
|
||||
};
|
||||
// Where the volume sits on the plate: its instance transform times its own volume transform, i.e.
|
||||
// mesh coordinates -> world millimetres.
|
||||
//
|
||||
// Every number the user sets is in real millimetres on the printed part - "Depth (mm)", "Tile size
|
||||
// (mm)" - and the build plate is a world plane, so the bake runs in world space and transforms the
|
||||
// result back. Doing it in the volume's own coordinates instead made a scaled instance stretch both
|
||||
// the relief depth and the tiling by the scale factor, and under a non-uniform scale it also
|
||||
// displaced along the wrong direction: a mesh normal maps to the world normal through the inverse
|
||||
// transpose, not through the transform itself, so the relief leaned. Identity - the default - is
|
||||
// exactly the old behaviour and is what an untransformed volume gives.
|
||||
indexed_triangle_set build_texture_displacement(const indexed_triangle_set &base_mesh,
|
||||
const std::vector<TextureDisplacementLayer> &layers,
|
||||
const TextureDisplacementFacetsData &facets_data,
|
||||
const TextureDisplacementOptions &options = {},
|
||||
const DisplacementProgressFn &progress = {},
|
||||
const TextureColorRequest *color = nullptr);
|
||||
const TextureColorRequest *color = nullptr,
|
||||
const Transform3d &volume_to_world = Transform3d::Identity());
|
||||
|
||||
// `volume`'s mesh coordinates -> world millimetres: its first instance's transform times its own.
|
||||
// The mesh is shared by every instance, so a multi-instance object can only be baked for one of
|
||||
// them; the first is what the gizmo edits against. Identity when the volume has no object yet.
|
||||
Transform3d texture_displacement_volume_to_world(const ModelVolume &volume);
|
||||
|
||||
// Convenience overload for main-thread callers: extracts the mesh/layers/paint data/options from
|
||||
// `volume` and forwards to the overload above.
|
||||
|
||||
@@ -959,10 +959,30 @@ float GLGizmoTextureDisplacement::layer_texture_aspect(const TextureDisplacement
|
||||
return (tex.width > 0 && tex.height > 0) ? float(tex.width) / float(tex.height) : 1.f;
|
||||
}
|
||||
|
||||
std::vector<Vec2f> GLGizmoTextureDisplacement::compute_layer_vertex_uvs(const indexed_triangle_set &patch,
|
||||
indexed_triangle_set GLGizmoTextureDisplacement::patch_in_world(const indexed_triangle_set &patch) const
|
||||
{
|
||||
const ModelVolume *mv = texture_volume();
|
||||
if (mv == nullptr)
|
||||
return patch;
|
||||
const Transform3d to_world = texture_displacement_volume_to_world(*mv);
|
||||
if (to_world.matrix().isApprox(Transform3d::Identity().matrix()))
|
||||
return patch;
|
||||
|
||||
indexed_triangle_set world = patch;
|
||||
for (Vec3f &v : world.vertices)
|
||||
v = (to_world * v.cast<double>()).cast<float>();
|
||||
return world;
|
||||
}
|
||||
|
||||
std::vector<Vec2f> GLGizmoTextureDisplacement::compute_layer_vertex_uvs(const indexed_triangle_set &local_patch,
|
||||
const TextureDisplacementLayer &layer) const
|
||||
{
|
||||
const float aspect = layer_texture_aspect(layer);
|
||||
// The bake maps the texture in world millimetres, so "Tile size (mm)" means the same thing on a
|
||||
// scaled instance as it does on an untouched one (see build_texture_displacement()). Everything
|
||||
// that has to agree with the bake - the fast preview's uvs, the checker/distortion overlays -
|
||||
// therefore has to project from the same world positions, not from the volume's own.
|
||||
const indexed_triangle_set patch = patch_in_world(local_patch);
|
||||
const float aspect = layer_texture_aspect(layer);
|
||||
if (layer.projection_method == TextureProjectionMethod::LSCM) {
|
||||
// compute_lscm_uvs() returns the unwrap's own (raw, mm) coordinates with the island placement
|
||||
// folded in - it does *not* apply the layer's tiling/rotation/offset. The bake applies those
|
||||
@@ -1654,6 +1674,7 @@ void GLGizmoTextureDisplacement::queue_preview_job()
|
||||
input.base_mesh = mv->mesh().its;
|
||||
input.layers = mv->texture_displacement_layers;
|
||||
input.options = mv->texture_displacement_options;
|
||||
input.volume_to_world = texture_displacement_volume_to_world(*mv);
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||
input.facets_data[size_t(i)] = mv->texture_displacement_facet(i).get_data();
|
||||
// Captured here rather than read in the handler: get_extruders_colors() is main-thread state and
|
||||
@@ -1774,7 +1795,9 @@ void GLGizmoTextureDisplacement::update_uv_editor()
|
||||
bool unwrap_changed = false;
|
||||
if (m_uv_unwrap_pending) {
|
||||
m_uv_unwrap_pending = false;
|
||||
const indexed_triangle_set patch = extract_painted_patch(mv->mesh().its, state.facets);
|
||||
// World millimetres, the space the bake unwraps in - otherwise the pane would lay the islands
|
||||
// out at the volume's own scale and show the texture at a different size than it bakes at.
|
||||
const indexed_triangle_set patch = patch_in_world(extract_painted_patch(mv->mesh().its, state.facets));
|
||||
if (patch.indices.empty()) {
|
||||
m_uv_editor_state = UVEditorState{};
|
||||
m_uv_editor_unwrap = PatchUnwrap{};
|
||||
@@ -4418,8 +4441,6 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
if (icon_toggle(704, "texture_displacement_distortion.svg", cur_mode == 3, _L("Distortion"),
|
||||
_L("Distortion - blue-to-red stretch heatmap over the unwrap (needs the Unwrap/LSCM projection)"))) new_mode = 3;
|
||||
ImGui::SameLine();
|
||||
ImGui::Dummy(ImVec2(m_imgui->scaled(0.6f), 0.f));
|
||||
ImGui::SameLine();
|
||||
if (icon_toggle(705, "texture_displacement_wireframe.svg", m_wireframe_overlay, _L("Wireframe"),
|
||||
_L("Wireframe - overlay the mesh edges; independent of the view above"))) wf_toggle = true;
|
||||
|
||||
@@ -4497,8 +4518,61 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
// worth of controls the panel otherwise runs off the bottom of the screen, and there was
|
||||
// nothing to tell "settings that belong to this layer" apart from "settings that belong to
|
||||
// the tool".
|
||||
// ImGui's stock scrollbar is a wide, square-cornered slab in a tinted track - against this
|
||||
// flat panel it reads as a raw widget bolted onto the edge. Slim it to a rounded thumb over
|
||||
// an invisible track. The narrower bar also gives back content width: a scrollbar eats it
|
||||
// from the child's content region, which is what was clipping "Tile size (mm)" mid-word.
|
||||
const float scrollbar_w = m_imgui->scaled(0.5f);
|
||||
const ImVec4 grab = wxGetApp().dark_mode() ? ImVec4(1.f, 1.f, 1.f, 0.26f) : ImVec4(0.f, 0.f, 0.f, 0.26f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, scrollbar_w);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, 0.5f * scrollbar_w);
|
||||
// The panel-wide 20 px window padding is meant for the panel; inside a region that is itself
|
||||
// already inset and tinted it is just a second margin, and it was spending on empty gutters
|
||||
// the width the layer rows needed. Vertical padding is left alone - it separates the first
|
||||
// layer's header from the region's top edge.
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(m_imgui->scaled(0.5f), ImGui::GetStyle().WindowPadding.y));
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, ImVec4(0.f, 0.f, 0.f, 0.f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, grab);
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, ImVec4(grab.x, grab.y, grab.z, 0.45f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, ImVec4(grab.x, grab.y, grab.z, 0.65f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.f, 1.f, 1.f, 0.04f));
|
||||
ImGui::BeginChild("##texture_layers", ImVec2(0.f, m_imgui->scaled(20.f)), true);
|
||||
// NoScrollWithMouse: the wheel is handled below so the scroll can be eased instead of
|
||||
// teleporting five text lines per notch, which on blocks this tall lost the reader's place.
|
||||
ImGui::BeginChild("##texture_layers", ImVec2(0.f, m_imgui->scaled(20.f)), true,
|
||||
ImGuiWindowFlags_NoScrollWithMouse);
|
||||
{
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
const float scroll_now = ImGui::GetScrollY();
|
||||
const float scroll_max = ImGui::GetScrollMaxY();
|
||||
|
||||
// Anything that moved the scroll without us - dragging the grab, a keyboard/gamepad nav
|
||||
// step, the content shrinking under a clamped offset - has to re-seed the target, or the
|
||||
// easing below would immediately drag the view back to where it last animated to.
|
||||
if (m_layer_scroll_applied < 0.f || std::abs(scroll_now - m_layer_scroll_applied) > 0.5f)
|
||||
m_layer_scroll_target = scroll_now;
|
||||
|
||||
if (io.MouseWheel != 0.f && ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows))
|
||||
m_layer_scroll_target -= io.MouseWheel * ImGui::GetFontSize() * 4.f;
|
||||
// Whole pixels: ImGui floors whatever SetScrollY() is given, so a fractional target could
|
||||
// never be reached and the "still gliding" test below would stay true forever, repainting
|
||||
// the canvas for good.
|
||||
m_layer_scroll_target = std::floor(std::clamp(m_layer_scroll_target, 0.f, scroll_max));
|
||||
|
||||
const float delta = m_layer_scroll_target - scroll_now;
|
||||
if (std::abs(delta) >= 1.f) {
|
||||
// Exponential ease, formulated against the frame time so the glide takes the same wall
|
||||
// time whether the canvas is running at 30 or 144 fps. The last sub-pixel step would be
|
||||
// floored away, so land on the target outright once the remainder is that small.
|
||||
const float t = 1.f - std::exp(-20.f * std::clamp(io.DeltaTime, 1.f / 240.f, 1.f / 15.f));
|
||||
const float step = (std::abs(delta * t) < 1.f) ? delta : delta * t;
|
||||
const float next = std::floor(scroll_now + step);
|
||||
ImGui::SetScrollY(next);
|
||||
m_layer_scroll_applied = next;
|
||||
m_parent.set_as_dirty(); // nothing else would redraw mid-glide once the mouse stops
|
||||
} else {
|
||||
m_layer_scroll_applied = scroll_now;
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t li = 0; li < ordered.size(); ++li) {
|
||||
TextureDisplacementLayer *layer = ordered[li];
|
||||
@@ -5017,7 +5091,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopStyleColor(5);
|
||||
ImGui::PopStyleVar(3);
|
||||
|
||||
if (slot_to_remove >= 0)
|
||||
remove_texture_layer(slot_to_remove); // deferred: see slot_to_remove's declaration
|
||||
|
||||
@@ -621,6 +621,10 @@ private:
|
||||
// the shader projects on its own. Shared by the bump preview and the UV-check overlay.
|
||||
std::vector<Vec2f> compute_layer_vertex_uvs(const indexed_triangle_set &patch,
|
||||
const TextureDisplacementLayer &layer) const;
|
||||
// `patch` with its vertices moved into world millimetres - the space the bake maps the texture in
|
||||
// (see build_texture_displacement()). Returned by value because the caller usually still needs the
|
||||
// original: the patch doubles as render geometry, which is drawn through the volume's own matrix.
|
||||
indexed_triangle_set patch_in_world(const indexed_triangle_set &patch) const;
|
||||
|
||||
// UV-check overlay drawn over the painted patch to sanity-check the unwrap (#13/#14). Built by
|
||||
// rebuild_uvcheck_mesh(), drawn by render_uvcheck_mesh() with the "texture_displacement_uvcheck"
|
||||
@@ -751,6 +755,13 @@ private:
|
||||
// open/close within a session, so the choice sticks while working.
|
||||
bool m_undocked = false;
|
||||
|
||||
// Smooth scrolling for the layer-stack child region. ImGui jumps a fixed number of lines per
|
||||
// wheel notch, which on a list of tall per-layer blocks reads as a hard jolt rather than a
|
||||
// scroll. The wheel is intercepted (ImGuiWindowFlags_NoScrollWithMouse) and moves a *target*
|
||||
// offset instead; the real scroll is eased toward it over the following frames.
|
||||
float m_layer_scroll_target = 0.f;
|
||||
float m_layer_scroll_applied = -1.f; // what the easing wrote last frame; <0 until the first one
|
||||
|
||||
// See the "Adjust Texture" block of private methods above.
|
||||
bool m_adjust_texture_mode = false;
|
||||
bool m_adjust_anchor_valid = false;
|
||||
|
||||
@@ -61,7 +61,7 @@ void TextureDisplacementBakeJob::process(Ctl &ctl)
|
||||
}
|
||||
return true;
|
||||
},
|
||||
color));
|
||||
color, m_input.volume_to_world));
|
||||
|
||||
// Always finish at 100: this is what closes the notification. Reported even on cancel, where
|
||||
// build_texture_displacement() returns an empty mesh and finalize() commits nothing.
|
||||
@@ -152,6 +152,7 @@ void queue_texture_displacement_bake(const ModelVolume &volume, const TextureCol
|
||||
input.base_mesh = volume.mesh().its;
|
||||
input.layers = volume.texture_displacement_layers;
|
||||
input.options = volume.texture_displacement_options;
|
||||
input.volume_to_world = texture_displacement_volume_to_world(volume);
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||
input.facets_data[size_t(i)] = volume.texture_displacement_facet(i).get_data();
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@ struct TextureDisplacementBakeInput
|
||||
std::vector<TextureDisplacementLayer> layers;
|
||||
TextureDisplacementFacetsData facets_data;
|
||||
TextureDisplacementOptions options;
|
||||
// Mesh coordinates -> world millimetres. Captured here with everything else so the worker never
|
||||
// reaches back into the live Model for it. See build_texture_displacement().
|
||||
Transform3d volume_to_world = Transform3d::Identity();
|
||||
// Captured on the main thread. Empty unless some layer is colouring, in which case the bake also
|
||||
// writes the volume's mmu_segmentation_facets - the same per-triangle filament assignment the MMU
|
||||
// paint gizmo writes - alongside the displaced geometry.
|
||||
|
||||
@@ -43,7 +43,7 @@ void TextureDisplacementPreviewJob::process(Ctl &ctl)
|
||||
(!m_current_generation ||
|
||||
m_current_generation->load() == m_generation);
|
||||
},
|
||||
color);
|
||||
color, m_input.volume_to_world);
|
||||
}
|
||||
|
||||
void TextureDisplacementPreviewJob::finalize(bool canceled, std::exception_ptr &eptr)
|
||||
|
||||
@@ -23,6 +23,9 @@ struct TextureDisplacementPreviewInput
|
||||
std::vector<TextureDisplacementLayer> layers;
|
||||
TextureDisplacementFacetsData facets_data;
|
||||
TextureDisplacementOptions options;
|
||||
// Mesh coordinates -> world millimetres, so the preview is displaced in the same space the bake
|
||||
// is and the two cannot disagree. See build_texture_displacement().
|
||||
Transform3d volume_to_world = Transform3d::Identity();
|
||||
// Empty unless a layer is colouring, in which case the preview reports the filament per triangle
|
||||
// alongside the mesh, so the Normal view shows what the bake will produce - interleaving included.
|
||||
TextureColorSettings color;
|
||||
|
||||
@@ -39,6 +39,25 @@ const ColorRGBA UV_COLOR_DIAL = { 1.f, 0.85f, 0.2f, 0.9f }; // rota
|
||||
|
||||
constexpr float SNAP_PIXELS = 28.f; // how close a boundary vertex has to come before it sticks (#2)
|
||||
|
||||
// wxWidgets reports this canvas' size in *logical* points, while the GL drawable behind it is sized
|
||||
// in device pixels. On the backends where those differ under HiDPI (the same pair GLCanvas3D guards
|
||||
// its RetinaHelper with) a viewport built straight from GetSize() covers only the bottom-left
|
||||
// 1/scale of the drawable, which is exactly where the whole editor ended up drawn, shrunken.
|
||||
// Mouse coordinates arrive in logical points, so only the viewport needs converting - every other
|
||||
// GetSize() use here is compared against event coordinates and must stay logical.
|
||||
wxSize gl_drawable_size(const wxWindow *win, const wxSize &logical_size)
|
||||
{
|
||||
#if defined(__APPLE__) || defined(__WXGTK3__)
|
||||
const double scale = (win != nullptr) ? win->GetContentScaleFactor() : 1.0;
|
||||
if (scale > 0.0)
|
||||
return wxSize(std::max(1, int(std::lround(logical_size.GetWidth() * scale))),
|
||||
std::max(1, int(std::lround(logical_size.GetHeight() * scale))));
|
||||
#else
|
||||
(void) win;
|
||||
#endif
|
||||
return wxSize(std::max(1, logical_size.GetWidth()), std::max(1, logical_size.GetHeight()));
|
||||
}
|
||||
|
||||
// The pixel format this canvas is created with has to match the one the app's single shared
|
||||
// wxGLContext was created against (that of View3D's canvas, from OpenGLManager::create_wxglcanvas()),
|
||||
// so this mirrors that attribute list *including its multisampling*: WGL requires the HDC passed to
|
||||
@@ -333,13 +352,37 @@ void UVEditorCanvas::content_bounds(Vec2f &min_uv, Vec2f &max_uv) const
|
||||
}
|
||||
}
|
||||
|
||||
void UVEditorCanvas::framed_bounds(Vec2f &min_uv, Vec2f &max_uv) const
|
||||
{
|
||||
content_bounds(min_uv, max_uv);
|
||||
// With tiling on, the backdrop is snapped out to whole tiles, so it is bigger than the raw
|
||||
// bounds - and by a different amount on each side. Framing the raw bounds therefore left that
|
||||
// backdrop visibly off-centre: hanging past one edge of the pane with dead space against the
|
||||
// other. Frame what is drawn instead. (Tiling off draws only the first tile, which the bounds
|
||||
// already contain, so there is nothing to snap.)
|
||||
if (m_tile_enabled) {
|
||||
min_uv = Vec2f(std::floor(min_uv.x()), std::floor(min_uv.y()));
|
||||
max_uv = Vec2f(std::ceil(max_uv.x()), std::ceil(max_uv.y()));
|
||||
}
|
||||
}
|
||||
|
||||
void UVEditorCanvas::fit_view_to_content()
|
||||
{
|
||||
Vec2f min_uv, max_uv;
|
||||
content_bounds(min_uv, max_uv);
|
||||
framed_bounds(min_uv, max_uv);
|
||||
|
||||
m_pan = 0.5f * (min_uv + max_uv);
|
||||
m_zoom = std::max(0.5f * (max_uv - min_uv).maxCoeff() * 1.1f, 0.05f);
|
||||
const wxSize size = GetSize();
|
||||
const float aspect = float(std::max(1, size.GetWidth())) / float(std::max(1, size.GetHeight()));
|
||||
const Vec2f half = 0.5f * (max_uv - min_uv);
|
||||
|
||||
m_pan = 0.5f * (min_uv + max_uv);
|
||||
// m_zoom is the half-extent shown across the *shorter* pane edge (see view_half_extents()), so
|
||||
// each axis' required half-extent has to be converted back into that unit before the larger of
|
||||
// the two is taken. Sizing off the bigger axis alone, as this did, ignores the pane's shape and
|
||||
// zooms out further than either axis needs on anything but a square pane. The 1.1 leaves a
|
||||
// margin so the outermost island edge is not flush against the frame.
|
||||
m_zoom = std::max(1.1f * std::max(half.x() / std::max(aspect, 1.f), half.y() * std::min(aspect, 1.f)),
|
||||
0.05f);
|
||||
m_needs_fit = false;
|
||||
}
|
||||
|
||||
@@ -1073,12 +1116,11 @@ void UVEditorCanvas::rebuild_background_quad()
|
||||
return;
|
||||
|
||||
Vec2f lo, hi;
|
||||
content_bounds(lo, hi);
|
||||
if (m_tile_enabled) {
|
||||
// Snap out to whole tiles. Only cosmetic, but it keeps the backdrop's edge on a tile
|
||||
// boundary instead of slicing a brick in half at an arbitrary place.
|
||||
lo = Vec2f(std::floor(lo.x()), std::floor(lo.y()));
|
||||
hi = Vec2f(std::ceil(hi.x()), std::ceil(hi.y()));
|
||||
// Whole tiles, so the backdrop's edge lands on a tile boundary instead of slicing a brick in
|
||||
// half. framed_bounds() applies exactly this, and the view is framed on its result - the two
|
||||
// must not drift apart or the backdrop stops being centred in the pane.
|
||||
framed_bounds(lo, hi);
|
||||
} else {
|
||||
// Tiling off: the sampler reads 0 outside the first tile and nothing else exists, so the
|
||||
// backdrop is exactly that one tile (GL_CLAMP_TO_BORDER in render() gives it the same
|
||||
@@ -1165,8 +1207,13 @@ void UVEditorCanvas::render()
|
||||
rebuild_grid();
|
||||
rebuild_rotation_dial();
|
||||
|
||||
const wxSize size = GetSize();
|
||||
glsafe(::glViewport(0, 0, size.GetWidth(), size.GetHeight()));
|
||||
const wxSize size = GetSize(); // logical points; the on-screen handle sizes below use it
|
||||
const wxSize viewport = gl_drawable_size(this, size);
|
||||
glsafe(::glViewport(0, 0, viewport.GetWidth(), viewport.GetHeight()));
|
||||
// Line widths below are authored in logical points (they are chosen against the same scale the
|
||||
// hit-test thresholds use), so they take the same logical -> device conversion as the viewport.
|
||||
const float px_scale = float(viewport.GetWidth()) / float(std::max(1, size.GetWidth()));
|
||||
const auto line_width = [px_scale](float w) { set_line_width(w * px_scale); };
|
||||
glsafe(::glClearColor(UV_COLOR_BG.r(), UV_COLOR_BG.g(), UV_COLOR_BG.b(), 1.f));
|
||||
glsafe(::glClear(GL_COLOR_BUFFER_BIT));
|
||||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||||
@@ -1224,7 +1271,7 @@ void UVEditorCanvas::render()
|
||||
}
|
||||
}
|
||||
|
||||
set_line_width(1.f);
|
||||
line_width(1.f);
|
||||
draw(m_grid_glmodel, UV_COLOR_GRID, identity);
|
||||
|
||||
// The texture's first tile. Always drawn, even with nothing painted, so the pane always has a
|
||||
@@ -1244,7 +1291,7 @@ void UVEditorCanvas::render()
|
||||
tile.add_line(i, (i + 1) % 4);
|
||||
m_tile_outline_glmodel.init_from(std::move(tile));
|
||||
}
|
||||
set_line_width(2.f);
|
||||
line_width(2.f);
|
||||
draw(m_tile_outline_glmodel, UV_COLOR_TILE_OUTLINE, identity);
|
||||
|
||||
const auto island_matrix = [this, &identity](int c) {
|
||||
@@ -1262,7 +1309,7 @@ void UVEditorCanvas::render()
|
||||
draw(m_island_fill[size_t(c)], is_selected(c) ? UV_COLOR_SEL_FILL : fill, island_matrix(c));
|
||||
}
|
||||
|
||||
set_line_width(1.f);
|
||||
line_width(1.f);
|
||||
for (int c = 0; c < int(m_island_wireframe.size()); ++c)
|
||||
draw(m_island_wireframe[size_t(c)], is_selected(c) ? UV_COLOR_SEL_WIRE : UV_COLOR_WIRE, island_matrix(c));
|
||||
|
||||
@@ -1271,16 +1318,16 @@ void UVEditorCanvas::render()
|
||||
// pane exists to answer. The selected one gets a bold edge (#7).
|
||||
for (int c = 0; c < int(m_island_boundary.size()); ++c) {
|
||||
const bool selected = is_selected(c);
|
||||
set_line_width(selected ? 3.f : 1.5f);
|
||||
line_width(selected ? 3.f : 1.5f);
|
||||
draw(m_island_boundary[size_t(c)], selected ? UV_COLOR_SEL_BOUNDARY : UV_COLOR_BOUNDARY, island_matrix(c));
|
||||
}
|
||||
set_line_width(1.f);
|
||||
line_width(1.f);
|
||||
|
||||
// The rotation protractor, on top of everything while a rotation gesture is live (#11).
|
||||
if (m_dial_glmodel.is_initialized()) {
|
||||
set_line_width(2.f);
|
||||
line_width(2.f);
|
||||
draw(m_dial_glmodel, UV_COLOR_DIAL, identity);
|
||||
set_line_width(1.f);
|
||||
line_width(1.f);
|
||||
}
|
||||
|
||||
// Vertex/Edge mode handles: a small square drawn over the picked vertex (or each endpoint of the
|
||||
@@ -1357,10 +1404,10 @@ void UVEditorCanvas::render()
|
||||
m_cursor_sign_glmodel.reset();
|
||||
if (!sign.is_empty())
|
||||
m_cursor_sign_glmodel.init_from(std::move(sign));
|
||||
set_line_width(3.f);
|
||||
line_width(3.f);
|
||||
draw(m_cursor_sign_glmodel, removing ? ColorRGBA(1.f, 0.30f, 0.25f, 0.95f) : ColorRGBA(0.35f, 0.90f, 0.45f, 0.95f),
|
||||
identity);
|
||||
set_line_width(1.f);
|
||||
line_width(1.f);
|
||||
}
|
||||
|
||||
// The GL context is shared with the 3D view; leave the bits we touched as we found them.
|
||||
|
||||
@@ -155,7 +155,11 @@ private:
|
||||
// The UV region worth looking at: every island, plus always at least the texture's first tile, so
|
||||
// there is something sensibly framed even before anything is painted.
|
||||
void content_bounds(Vec2f &min_uv, Vec2f &max_uv) const;
|
||||
// Frames content_bounds(). Bound to Home, and run once each time an unwrap first appears.
|
||||
// What is actually *drawn*, which is content_bounds() snapped out to whole tiles whenever the
|
||||
// backdrop tiles (see rebuild_background_quad()). Both the framing and the backdrop go through
|
||||
// this so they cannot disagree.
|
||||
void framed_bounds(Vec2f &min_uv, Vec2f &max_uv) const;
|
||||
// Frames framed_bounds(). Bound to Home, and run once each time an unwrap first appears.
|
||||
void fit_view_to_content();
|
||||
|
||||
// Half-extents of the visible UV region. Split out because both rendering and every mouse
|
||||
|
||||
@@ -61,6 +61,19 @@ static std::shared_ptr<std::vector<unsigned char>> make_checkerboard_png(size_t
|
||||
return std::make_shared<std::vector<unsigned char>>(std::move(bytes));
|
||||
}
|
||||
|
||||
// The bake never drives relief below the model's own resting plane (see build_texture_displacement()),
|
||||
// so on a fully painted closed solid the vertices already sitting on that plane - a cube's four bottom
|
||||
// corners, whose normals point downwards - are clamped in Z and do not move by the full depth. The
|
||||
// tests below are about the displacement maths, so they check the vertices the clamp cannot touch;
|
||||
// the clamp itself has its own test.
|
||||
static bool above_resting_plane(const indexed_triangle_set &mesh, size_t vi)
|
||||
{
|
||||
float bottom = std::numeric_limits<float>::max();
|
||||
for (const Vec3f &v : mesh.vertices)
|
||||
bottom = std::min(bottom, v.z());
|
||||
return mesh.vertices[vi].z() > bottom + 1e-4f;
|
||||
}
|
||||
|
||||
TEST_CASE("TextureDisplacement: decode_height_texture round-trips an 8-bit grayscale PNG", "[TextureDisplacement]")
|
||||
{
|
||||
TextureDisplacementLayer layer;
|
||||
@@ -110,6 +123,8 @@ TEST_CASE("TextureDisplacement: fully painting a mesh displaces every vertex alo
|
||||
|
||||
REQUIRE(result.vertices.size() == cube.vertices.size());
|
||||
for (size_t i = 0; i < cube.vertices.size(); ++i) {
|
||||
if (!above_resting_plane(cube, i))
|
||||
continue;
|
||||
const float moved = (result.vertices[i] - cube.vertices[i]).norm();
|
||||
CHECK_THAT(moved, WithinAbs(layer.depth_mm, 1e-3f));
|
||||
}
|
||||
@@ -154,7 +169,8 @@ TEST_CASE("TextureDisplacement: a second layer over the same area is applied too
|
||||
REQUIRE(result.vertices.size() == cube.vertices.size());
|
||||
REQUIRE(result.indices.size() == cube.indices.size());
|
||||
for (size_t i = 0; i < cube.vertices.size(); ++i)
|
||||
CHECK_THAT((result.vertices[i] - cube.vertices[i]).norm(), WithinAbs(1.5f, 1e-3f)); // 1.0 + 0.5, not just 1.0
|
||||
if (above_resting_plane(cube, i))
|
||||
CHECK_THAT((result.vertices[i] - cube.vertices[i]).norm(), WithinAbs(1.5f, 1e-3f)); // 1.0 + 0.5, not just 1.0
|
||||
}
|
||||
|
||||
TEST_CASE("TextureDisplacement: blend modes combine a layer with the ones below it", "[TextureDisplacement]")
|
||||
@@ -189,7 +205,8 @@ TEST_CASE("TextureDisplacement: blend modes combine a layer with the ones below
|
||||
|
||||
REQUIRE(result.vertices.size() == cube.vertices.size());
|
||||
for (size_t i = 0; i < cube.vertices.size(); ++i)
|
||||
CHECK_THAT((result.vertices[i] - cube.vertices[i]).norm(), WithinAbs(std::get<1>(expected), 1e-3f));
|
||||
if (above_resting_plane(cube, i))
|
||||
CHECK_THAT((result.vertices[i] - cube.vertices[i]).norm(), WithinAbs(std::get<1>(expected), 1e-3f));
|
||||
}
|
||||
|
||||
TEST_CASE("TextureDisplacement: the lowest layer ignores its blend mode", "[TextureDisplacement]")
|
||||
@@ -211,7 +228,104 @@ TEST_CASE("TextureDisplacement: the lowest layer ignores its blend mode", "[Text
|
||||
const indexed_triangle_set result = build_texture_displacement(cube, {layer}, facets);
|
||||
|
||||
for (size_t i = 0; i < cube.vertices.size(); ++i)
|
||||
CHECK_THAT((result.vertices[i] - cube.vertices[i]).norm(), WithinAbs(2.0f, 1e-3f));
|
||||
if (above_resting_plane(cube, i))
|
||||
CHECK_THAT((result.vertices[i] - cube.vertices[i]).norm(), WithinAbs(2.0f, 1e-3f));
|
||||
}
|
||||
|
||||
TEST_CASE("TextureDisplacement: relief is never driven below the model's resting plane", "[TextureDisplacement]")
|
||||
{
|
||||
// A fully painted cube displaces outward everywhere, which on the bottom face means straight
|
||||
// down - through the build plate. That geometry cannot be printed, so it is clamped back up.
|
||||
const indexed_triangle_set cube = its_make_cube(10., 10., 10.);
|
||||
|
||||
TextureDisplacementFacetsData facets{};
|
||||
facets[0] = paint_whole_mesh(cube);
|
||||
|
||||
TextureDisplacementLayer layer;
|
||||
layer.slot = 0;
|
||||
layer.depth_mm = 2.0f;
|
||||
layer.tiling_scale = 5.0f;
|
||||
layer.image_data = make_flat_gray_png(255); // full depth everywhere
|
||||
|
||||
float bottom = std::numeric_limits<float>::max();
|
||||
for (const Vec3f &v : cube.vertices)
|
||||
bottom = std::min(bottom, v.z());
|
||||
|
||||
const indexed_triangle_set result = build_texture_displacement(cube, {layer}, facets);
|
||||
|
||||
REQUIRE(result.vertices.size() == cube.vertices.size());
|
||||
for (const Vec3f &v : result.vertices)
|
||||
CHECK(v.z() >= bottom - 1e-4f);
|
||||
|
||||
// ...and the clamp is confined to Z: a bottom corner still moves outwards in X and Y by the same
|
||||
// amount it would have, rather than being pinned wholesale.
|
||||
bool any_bottom_moved_sideways = false;
|
||||
for (size_t i = 0; i < cube.vertices.size(); ++i)
|
||||
if (!above_resting_plane(cube, i) &&
|
||||
(result.vertices[i].head<2>() - cube.vertices[i].head<2>()).norm() > 1e-3f)
|
||||
any_bottom_moved_sideways = true;
|
||||
CHECK(any_bottom_moved_sideways);
|
||||
}
|
||||
|
||||
TEST_CASE("TextureDisplacement: depth is measured in world millimetres, not the volume's own", "[TextureDisplacement]")
|
||||
{
|
||||
// The same painted patch, baked once untransformed and once through a 3x scale. "Depth (mm)" is
|
||||
// a millimetre on the printed part, so the *world* relief must come out the same height either
|
||||
// way - which means the vertices of the scaled volume move by a third as much in its own
|
||||
// coordinates. Baking both in volume space instead gave a 3x deeper relief on the scaled one.
|
||||
indexed_triangle_set fan;
|
||||
fan.vertices = { {0.f, 0.f, 1.f}, {1.f, 0.f, 1.f}, {0.f, 1.f, 1.f}, {-1.f, 0.f, 1.f}, {0.f, -1.f, 1.f} };
|
||||
fan.indices = { {0, 1, 2}, {0, 2, 3}, {0, 3, 4}, {0, 4, 1} };
|
||||
|
||||
TextureDisplacementFacetsData facets{};
|
||||
facets[0] = paint_whole_mesh(fan);
|
||||
|
||||
TextureDisplacementLayer layer;
|
||||
layer.slot = 0;
|
||||
layer.depth_mm = 1.0f;
|
||||
layer.tiling_scale = 5.0f;
|
||||
layer.image_data = make_flat_gray_png(255);
|
||||
|
||||
const indexed_triangle_set plain = build_texture_displacement(fan, {layer}, facets);
|
||||
Transform3d scale3 = Transform3d::Identity();
|
||||
scale3.scale(Vec3d(3.0, 3.0, 3.0));
|
||||
const indexed_triangle_set scaled = build_texture_displacement(fan, {layer}, facets, {}, {}, nullptr, scale3);
|
||||
|
||||
REQUIRE(plain.vertices.size() == fan.vertices.size());
|
||||
REQUIRE(scaled.vertices.size() == fan.vertices.size());
|
||||
for (size_t i = 0; i < fan.vertices.size(); ++i) {
|
||||
CHECK_THAT(plain.vertices[i].z() - fan.vertices[i].z(), WithinAbs(1.0f, 1e-3f));
|
||||
// A third of the movement locally is the same movement once the 3x scale is applied.
|
||||
CHECK_THAT(scaled.vertices[i].z() - fan.vertices[i].z(), WithinAbs(1.0f / 3.0f, 1e-3f));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("TextureDisplacement: a mirrored placement still raises the relief outwards", "[TextureDisplacement]")
|
||||
{
|
||||
// Mirroring reverses the winding, and every normal in the bake is derived from the winding - so
|
||||
// without correcting for it the whole relief is carved into the surface instead of raised off it.
|
||||
indexed_triangle_set fan;
|
||||
fan.vertices = { {0.f, 0.f, 1.f}, {1.f, 0.f, 1.f}, {0.f, 1.f, 1.f}, {-1.f, 0.f, 1.f}, {0.f, -1.f, 1.f} };
|
||||
fan.indices = { {0, 1, 2}, {0, 2, 3}, {0, 3, 4}, {0, 4, 1} };
|
||||
|
||||
TextureDisplacementFacetsData facets{};
|
||||
facets[0] = paint_whole_mesh(fan);
|
||||
|
||||
TextureDisplacementLayer layer;
|
||||
layer.slot = 0;
|
||||
layer.depth_mm = 1.0f;
|
||||
layer.tiling_scale = 5.0f;
|
||||
layer.image_data = make_flat_gray_png(255);
|
||||
|
||||
// Mirrored in X: the patch's outward direction in world space is still +Z, so in the volume's own
|
||||
// coordinates the vertices must still move +Z.
|
||||
Transform3d mirror_x = Transform3d::Identity();
|
||||
mirror_x.scale(Vec3d(-1.0, 1.0, 1.0));
|
||||
const indexed_triangle_set result = build_texture_displacement(fan, {layer}, facets, {}, {}, nullptr, mirror_x);
|
||||
|
||||
REQUIRE(result.vertices.size() == fan.vertices.size());
|
||||
for (size_t i = 0; i < fan.vertices.size(); ++i)
|
||||
CHECK_THAT(result.vertices[i].z() - fan.vertices[i].z(), WithinAbs(1.0f, 1e-3f));
|
||||
}
|
||||
|
||||
TEST_CASE("TextureDisplacement: the patch border is displaced by default and pinned on request", "[TextureDisplacement]")
|
||||
|
||||
Reference in New Issue
Block a user