Compare commits

...

2 Commits

Author SHA1 Message Date
ExPikaPaka
3514249197 Removed files that were accidently added 2026-07-09 08:40:37 +02:00
ExPikaPaka
7f2598d0d6 POC 2026-07-08 08:50:47 +02:00
21 changed files with 1413 additions and 7 deletions

View File

@@ -945,7 +945,7 @@ if (WIN32)
endif ()
endif ()
set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Orca Slicer is an open source slicer for FDM printers")
set (CPACK_PACKAGE_HOMEPAGE_URL "https://github.com/OrcaSlicer/OrcaSlicer")
set (CPACK_PACKAGE_HOMEPAGE_URL "https://github.com/OrcaSlicer_3/OrcaSlicer")
set (CPACK_PACKAGE_INSTALL_DIRECTORY ${CPACK_PACKAGE_NAME})
set (CPACK_PACKAGE_ICON "${CMAKE_SOURCE_DIR}/resources/images\\\\OrcaSlicer.ico")
set (CPACK_NSIS_MUI_ICON "${CPACK_PACKAGE_ICON}")

View File

@@ -0,0 +1,81 @@
#version 110
// See resources/shaders/140/texture_displacement_bump.fs for full documentation; this is the
// GLSL 1.10 compatibility variant (same logic, older syntax).
#define INTENSITY_CORRECTION 0.6
const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);
#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION)
#define LIGHT_TOP_SPECULAR (0.125 * INTENSITY_CORRECTION)
#define LIGHT_TOP_SHININESS 20.0
const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074);
#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION)
#define INTENSITY_AMBIENT 0.3
const vec3 ZERO = vec3(0.0, 0.0, 0.0);
uniform vec4 uniform_color;
uniform bool volume_mirrored;
uniform mat4 view_model_matrix;
uniform mat3 view_normal_matrix;
uniform sampler2D height_tex;
uniform vec2 height_tex_texel;
uniform float depth_mm;
uniform float tiling_scale;
uniform float rotation_rad;
uniform vec2 uv_offset;
uniform bool invert;
varying vec3 clipping_planes_dots;
varying vec4 model_pos;
varying vec4 world_pos;
varying float weight;
vec2 project_uv(vec3 p, vec3 n)
{
vec3 an = abs(n);
vec2 planar = (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy);
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float cs = cos(rotation_rad);
float sn = sin(rotation_rad);
return vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs) + uv_offset;
}
void main()
{
if (any(lessThan(clipping_planes_dots, ZERO)))
discard;
vec3 triangle_normal = normalize(cross(dFdx(model_pos.xyz), dFdy(model_pos.xyz)));
if (volume_mirrored)
triangle_normal = -triangle_normal;
if (weight > 0.0) {
vec2 uv = project_uv(model_pos.xyz, triangle_normal);
float hL = texture2D(height_tex, uv - vec2(height_tex_texel.x, 0.0)).r;
float hR = texture2D(height_tex, uv + vec2(height_tex_texel.x, 0.0)).r;
float hD = texture2D(height_tex, uv - vec2(0.0, height_tex_texel.y)).r;
float hU = texture2D(height_tex, uv + vec2(0.0, height_tex_texel.y)).r;
float sign_mul = invert ? -1.0 : 1.0;
vec3 bumped_normal = normalize(triangle_normal + sign_mul * depth_mm * vec3(hL - hR, hD - hU, 0.0));
triangle_normal = normalize(mix(triangle_normal, bumped_normal, clamp(weight, 0.0, 1.0)));
}
vec3 eye_normal = normalize(view_normal_matrix * triangle_normal);
float NdotL = max(dot(eye_normal, LIGHT_TOP_DIR), 0.0);
vec2 intensity = vec2(0.0);
intensity.x = INTENSITY_AMBIENT + NdotL * LIGHT_TOP_DIFFUSE;
vec3 position = (view_model_matrix * model_pos).xyz;
intensity.y = LIGHT_TOP_SPECULAR * pow(max(dot(-normalize(position), reflect(-LIGHT_TOP_DIR, eye_normal)), 0.0), LIGHT_TOP_SHININESS);
NdotL = max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0);
intensity.x += NdotL * LIGHT_FRONT_DIFFUSE;
gl_FragColor = vec4(vec3(intensity.y) + uniform_color.rgb * intensity.x, uniform_color.a);
}

View File

@@ -0,0 +1,30 @@
#version 110
uniform mat4 view_model_matrix;
uniform mat4 projection_matrix;
uniform mat4 volume_world_matrix;
// Clipping plane, x = min z, y = max z. Used by the FFF and SLA previews to clip with a top / bottom plane.
uniform vec2 z_range;
// Clipping plane - general orientation. Used by the SLA gizmo.
uniform vec4 clipping_plane;
attribute vec3 v_position;
// Per-vertex paint weight for the currently active texture-displacement layer, 0..1.
attribute float v_weight;
varying vec3 clipping_planes_dots;
varying vec4 model_pos;
varying vec4 world_pos;
varying float weight;
void main()
{
model_pos = vec4(v_position, 1.0);
world_pos = volume_world_matrix * model_pos;
gl_Position = projection_matrix * view_model_matrix * model_pos;
clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z);
weight = v_weight;
}

View File

@@ -0,0 +1,92 @@
#version 140
// Fast, geometry-free preview of texture displacement: perturbs the *shading* normal from the
// height texture's local gradient (a bump map), faded out by the per-vertex paint weight. Used
// while the brush is actively dragging (see project plan, "Preview shaders" section); the true,
// exact result is what "Bake" produces via libslic3r/TextureDisplacement.cpp on the CPU.
//
// NOTE: the gradient-to-normal conversion below assumes the two planar-projection axes chosen by
// project_uv() are reasonably aligned with the surface here; it is a cheap approximation (no
// tangent/bitangent basis is reconstructed on the GPU), acceptable for a live preview but not
// intended to be pixel-exact versus the CPU-side bake.
#define INTENSITY_CORRECTION 0.6
// normalized values for (-0.6/1.31, 0.6/1.31, 1./1.31)
const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);
#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION)
#define LIGHT_TOP_SPECULAR (0.125 * INTENSITY_CORRECTION)
#define LIGHT_TOP_SHININESS 20.0
// normalized values for (1./1.43, 0.2/1.43, 1./1.43)
const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074);
#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION)
#define INTENSITY_AMBIENT 0.3
const vec3 ZERO = vec3(0.0, 0.0, 0.0);
uniform vec4 uniform_color;
uniform bool volume_mirrored;
uniform mat4 view_model_matrix;
uniform mat3 view_normal_matrix;
uniform sampler2D height_tex;
uniform vec2 height_tex_texel; // (1/width, 1/height) of height_tex
uniform float depth_mm;
uniform float tiling_scale;
uniform float rotation_rad;
uniform vec2 uv_offset;
uniform bool invert;
in vec3 clipping_planes_dots;
in vec4 model_pos;
in vec4 world_pos;
in float weight;
out vec4 out_color;
vec2 project_uv(vec3 p, vec3 n)
{
vec3 an = abs(n);
vec2 planar = (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy);
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float cs = cos(rotation_rad);
float sn = sin(rotation_rad);
return vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs) + uv_offset;
}
void main()
{
if (any(lessThan(clipping_planes_dots, ZERO)))
discard;
vec3 triangle_normal = normalize(cross(dFdx(model_pos.xyz), dFdy(model_pos.xyz)));
if (volume_mirrored)
triangle_normal = -triangle_normal;
if (weight > 0.0) {
vec2 uv = project_uv(model_pos.xyz, triangle_normal);
float hL = texture(height_tex, uv - vec2(height_tex_texel.x, 0.0)).r;
float hR = texture(height_tex, uv + vec2(height_tex_texel.x, 0.0)).r;
float hD = texture(height_tex, uv - vec2(0.0, height_tex_texel.y)).r;
float hU = texture(height_tex, uv + vec2(0.0, height_tex_texel.y)).r;
float sign_mul = invert ? -1.0 : 1.0;
vec3 bumped_normal = normalize(triangle_normal + sign_mul * depth_mm * vec3(hL - hR, hD - hU, 0.0));
triangle_normal = normalize(mix(triangle_normal, bumped_normal, clamp(weight, 0.0, 1.0)));
}
vec3 eye_normal = normalize(view_normal_matrix * triangle_normal);
float NdotL = max(dot(eye_normal, LIGHT_TOP_DIR), 0.0);
vec2 intensity = vec2(0.0);
intensity.x = INTENSITY_AMBIENT + NdotL * LIGHT_TOP_DIFFUSE;
vec3 position = (view_model_matrix * model_pos).xyz;
intensity.y = LIGHT_TOP_SPECULAR * pow(max(dot(-normalize(position), reflect(-LIGHT_TOP_DIR, eye_normal)), 0.0), LIGHT_TOP_SHININESS);
NdotL = max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0);
intensity.x += NdotL * LIGHT_FRONT_DIFFUSE;
out_color = vec4(vec3(intensity.y) + uniform_color.rgb * intensity.x, uniform_color.a);
}

View File

@@ -0,0 +1,32 @@
#version 140
uniform mat4 view_model_matrix;
uniform mat4 projection_matrix;
uniform mat4 volume_world_matrix;
// Clipping plane, x = min z, y = max z. Used by the FFF and SLA previews to clip with a top / bottom plane.
uniform vec2 z_range;
// Clipping plane - general orientation. Used by the SLA gizmo.
uniform vec4 clipping_plane;
in vec3 v_position;
// Per-vertex paint weight for the currently active texture-displacement layer, 0..1 (see
// TriangleSelectorWeighted... note: Phase 1 stores this as a plain enforced/not-enforced mask,
// so this is 0.0 or 1.0 today; a continuous value is a natural later-phase extension).
in float v_weight;
out vec3 clipping_planes_dots;
out vec4 model_pos;
out vec4 world_pos;
out float weight;
void main()
{
model_pos = vec4(v_position, 1.0);
world_pos = volume_world_matrix * model_pos;
gl_Position = projection_matrix * view_model_matrix * model_pos;
clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z);
weight = v_weight;
}

View File

@@ -442,6 +442,8 @@ set(lisbslic3r_sources
Tesselate.cpp
Tesselate.hpp
TextConfiguration.hpp
TextureDisplacement.cpp
TextureDisplacement.hpp
Thread.cpp
Thread.hpp
Time.cpp

View File

@@ -1977,6 +1977,11 @@ void ModelVolume::reset_extra_facets()
this->seam_facets.reset();
this->mmu_segmentation_facets.reset();
this->fuzzy_skin_facets.reset();
// Texture-displacement paint data has no remap-across-topology-change support yet (see
// build_texture_displacement()'s documented limitation), so it must be dropped here rather
// than left referring to a mesh that no longer matches it.
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
this->texture_displacement_facet(i).reset();
}
std::optional<TriangleSelector::SavedPainting> ModelVolume::save_painting() const

View File

@@ -19,6 +19,7 @@
#include "TextConfiguration.hpp"
#include "EmbossShape.hpp"
#include "TriangleSelector.hpp"
#include "TextureDisplacement.hpp"
//BBS: add bbs 3mf
#include "Format/bbs_3mf.hpp"
@@ -28,6 +29,7 @@
#include "Format/STL.hpp"
#include "Format/OBJ.hpp"
#include <array>
#include <map>
#include <memory>
#include <string>
@@ -878,6 +880,64 @@ public:
// List of mesh facets painted for fuzzy skin.
FacetsAnnotation fuzzy_skin_facets;
// One independent paint mask per texture-displacement layer slot (see texture_displacement_layers
// below). Unlike the other facets fields above, a triangle may be painted (ENFORCER) in more
// than one of these simultaneously -- that overlap is what makes the layers "blend".
//
// These are 8 plain named fields rather than a std::array<FacetsAnnotation, N>: FacetsAnnotation's
// default/copy constructors are private and friended only to ModelVolume, but std::array's own
// implicitly-defined default/copy constructors are generated with std::array's access rights,
// not ModelVolume's -- so an array of FacetsAnnotation ends up with its default/copy
// constructors implicitly deleted regardless of the friend declaration. Use
// texture_displacement_facet(slot) below for array-like indexed access.
FacetsAnnotation texture_displacement_facets_0;
FacetsAnnotation texture_displacement_facets_1;
FacetsAnnotation texture_displacement_facets_2;
FacetsAnnotation texture_displacement_facets_3;
FacetsAnnotation texture_displacement_facets_4;
FacetsAnnotation texture_displacement_facets_5;
FacetsAnnotation texture_displacement_facets_6;
FacetsAnnotation texture_displacement_facets_7;
FacetsAnnotation& texture_displacement_facet(int slot) {
switch (slot) {
case 0: return texture_displacement_facets_0;
case 1: return texture_displacement_facets_1;
case 2: return texture_displacement_facets_2;
case 3: return texture_displacement_facets_3;
case 4: return texture_displacement_facets_4;
case 5: return texture_displacement_facets_5;
case 6: return texture_displacement_facets_6;
default: assert(slot == 7); return texture_displacement_facets_7;
}
}
const FacetsAnnotation& texture_displacement_facet(int slot) const { return const_cast<ModelVolume*>(this)->texture_displacement_facet(slot); }
// Small helpers for the constructor asserts below (kept out of line-noise at each call site).
bool texture_displacement_facets_ids_valid() const {
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
if (!texture_displacement_facet(i).id().valid() || texture_displacement_facet(i).id() == this->id())
return false;
return true;
}
bool texture_displacement_facets_ids_invalid() const {
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
if (texture_displacement_facet(i).id().valid())
return false;
return true;
}
bool texture_displacement_facets_all_empty() const {
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
if (!texture_displacement_facet(i).empty())
return false;
return true;
}
// Texture assets (height maps) and their projection/displacement parameters. Element order
// is not meaningful for baking (layers are applied in TextureDisplacementLayer::slot order,
// see build_texture_displacement()); it only reflects UI insertion order.
std::vector<TextureDisplacementLayer> texture_displacement_layers;
// Save painting data before reset_extra_facets() discards it.
// Used for replacing mesh without losing painting data.
// Only for model parts (not modifiers/connectors).
@@ -1007,13 +1067,18 @@ public:
this->seam_facets.set_new_unique_id();
this->mmu_segmentation_facets.set_new_unique_id();
this->fuzzy_skin_facets.set_new_unique_id();
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
this->texture_displacement_facet(i).set_new_unique_id();
}
bool is_fdm_support_painted() const { return !this->supported_facets.empty(); }
bool is_seam_painted() const { return !this->seam_facets.empty(); }
bool is_mm_painted() const { return !this->mmu_segmentation_facets.empty(); }
bool is_fuzzy_skin_painted() const { return !this->fuzzy_skin_facets.empty(); }
bool is_any_painted() const { return is_fdm_support_painted() || is_seam_painted() || is_mm_painted() || is_fuzzy_skin_painted(); }
bool is_texture_displacement_painted() const { return !this->texture_displacement_facets_all_empty(); }
bool is_any_painted() const {
return is_fdm_support_painted() || is_seam_painted() || is_mm_painted() || is_fuzzy_skin_painted() || is_texture_displacement_painted();
}
// Orca: Implement prusa's filament shrink compensation approach
// Returns 0-based indices of extruders painted by multi-material painting gizmo.
@@ -1066,6 +1131,7 @@ private:
assert(this->seam_facets.id().valid());
assert(this->mmu_segmentation_facets.id().valid());
assert(this->fuzzy_skin_facets.id().valid());
assert(this->texture_displacement_facets_ids_valid());
assert(this->id() != this->config.id());
assert(this->id() != this->supported_facets.id());
assert(this->id() != this->seam_facets.id());
@@ -1082,6 +1148,7 @@ private:
assert(this->seam_facets.id().valid());
assert(this->mmu_segmentation_facets.id().valid());
assert(this->fuzzy_skin_facets.id().valid());
assert(this->texture_displacement_facets_ids_valid());
assert(this->id() != this->config.id());
assert(this->id() != this->supported_facets.id());
assert(this->id() != this->seam_facets.id());
@@ -1096,6 +1163,7 @@ private:
assert(this->seam_facets.id().valid());
assert(this->mmu_segmentation_facets.id().valid());
assert(this->fuzzy_skin_facets.id().valid());
assert(this->texture_displacement_facets_ids_valid());
assert(this->id() != this->config.id());
assert(this->id() != this->supported_facets.id());
assert(this->id() != this->seam_facets.id());
@@ -1109,10 +1177,16 @@ private:
name(other.name), source(other.source), m_mesh(other.m_mesh), m_convex_hull(other.m_convex_hull),
config(other.config), m_type(other.m_type), object(object), m_transformation(other.m_transformation),
supported_facets(other.supported_facets), seam_facets(other.seam_facets), mmu_segmentation_facets(other.mmu_segmentation_facets),
fuzzy_skin_facets(other.fuzzy_skin_facets), cut_info(other.cut_info), text_configuration(other.text_configuration), emboss_shape(other.emboss_shape)
fuzzy_skin_facets(other.fuzzy_skin_facets),
texture_displacement_facets_0(other.texture_displacement_facets_0), texture_displacement_facets_1(other.texture_displacement_facets_1),
texture_displacement_facets_2(other.texture_displacement_facets_2), texture_displacement_facets_3(other.texture_displacement_facets_3),
texture_displacement_facets_4(other.texture_displacement_facets_4), texture_displacement_facets_5(other.texture_displacement_facets_5),
texture_displacement_facets_6(other.texture_displacement_facets_6), texture_displacement_facets_7(other.texture_displacement_facets_7),
texture_displacement_layers(other.texture_displacement_layers),
cut_info(other.cut_info), text_configuration(other.text_configuration), emboss_shape(other.emboss_shape)
{
assert(this->id().valid());
assert(this->config.id().valid());
assert(this->id().valid());
assert(this->config.id().valid());
assert(this->supported_facets.id().valid());
assert(this->seam_facets.id().valid());
assert(this->mmu_segmentation_facets.id().valid());
@@ -1162,6 +1236,8 @@ private:
assert(this->seam_facets.empty());
assert(this->mmu_segmentation_facets.empty());
assert(this->fuzzy_skin_facets.empty());
assert(this->texture_displacement_facets_all_empty());
assert(this->texture_displacement_layers.empty());
}
ModelVolume& operator=(ModelVolume &rhs) = delete;
@@ -1169,13 +1245,17 @@ private:
friend class cereal::access;
friend class UndoRedo::StackImpl;
// Used for deserialization, therefore no IDs are allocated.
ModelVolume() : ObjectBase(-1), config(-1), supported_facets(-1), seam_facets(-1), mmu_segmentation_facets(-1), fuzzy_skin_facets(-1), object(nullptr) {
ModelVolume() : ObjectBase(-1), config(-1), supported_facets(-1), seam_facets(-1), mmu_segmentation_facets(-1), fuzzy_skin_facets(-1),
texture_displacement_facets_0(-1), texture_displacement_facets_1(-1), texture_displacement_facets_2(-1), texture_displacement_facets_3(-1),
texture_displacement_facets_4(-1), texture_displacement_facets_5(-1), texture_displacement_facets_6(-1), texture_displacement_facets_7(-1),
object(nullptr) {
assert(this->id().invalid());
assert(this->config.id().invalid());
assert(this->supported_facets.id().invalid());
assert(this->seam_facets.id().invalid());
assert(this->mmu_segmentation_facets.id().invalid());
assert(this->fuzzy_skin_facets.id().invalid());
assert(this->texture_displacement_facets_ids_invalid());
}
template<class Archive> void load(Archive &ar) {
bool has_convex_hull;
@@ -1195,6 +1275,13 @@ private:
mesh_changed |= t != mmu_segmentation_facets.timestamp();
cereal::load_by_value(ar, fuzzy_skin_facets);
mesh_changed |= t != fuzzy_skin_facets.timestamp();
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) {
FacetsAnnotation &f = texture_displacement_facet(i);
Timestamp tf = f.timestamp();
cereal::load_by_value(ar, f);
mesh_changed |= tf != f.timestamp();
}
ar(texture_displacement_layers);
cereal::load_by_value(ar, config);
cereal::load(ar, text_configuration);
cereal::load(ar, emboss_shape);
@@ -1216,6 +1303,9 @@ private:
cereal::save_by_value(ar, seam_facets);
cereal::save_by_value(ar, mmu_segmentation_facets);
cereal::save_by_value(ar, fuzzy_skin_facets);
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
cereal::save_by_value(ar, texture_displacement_facet(i));
ar(texture_displacement_layers);
cereal::save_by_value(ar, config);
cereal::save(ar, text_configuration);
cereal::save(ar, emboss_shape);

View File

@@ -0,0 +1,226 @@
#include "TextureDisplacement.hpp"
#include <algorithm>
#include <cassert>
#include <cmath>
#include <optional>
#include "Model.hpp"
#include "PNGReadWrite.hpp"
#include "TriangleSelector.hpp"
namespace Slic3r {
float DecodedHeightTexture::sample(const Vec2f &uv) const
{
if (empty())
return 0.f;
auto wrap01 = [](float x) {
x = std::fmod(x, 1.f);
return x < 0.f ? x + 1.f : x;
};
const float fx = wrap01(uv.x()) * float(width);
const float fy = wrap01(uv.y()) * float(height);
int x0 = int(std::floor(fx)) % width;
int y0 = int(std::floor(fy)) % height;
if (x0 < 0) x0 += width;
if (y0 < 0) y0 += height;
const int x1 = (x0 + 1) % width;
const int y1 = (y0 + 1) % height;
const float tx = fx - std::floor(fx);
const float ty = fy - std::floor(fy);
auto at = [this](int x, int y) { return float(pixels[size_t(y) * size_t(width) + size_t(x)]) / 255.f; };
const float top = at(x0, y0) * (1.f - tx) + at(x1, y0) * tx;
const float bottom = at(x0, y1) * (1.f - tx) + at(x1, y1) * tx;
return top * (1.f - ty) + bottom * ty;
}
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer)
{
DecodedHeightTexture result;
if (layer.empty())
return result;
const png::ReadBuf rbuf{ layer.image_data->data(), layer.image_data->size() };
if (!png::is_png(rbuf))
// Only 8-bit grayscale PNG height maps are supported. The GUI is responsible for
// converting any imported image (jpg, color png, ...) to that format on import, so this
// code never needs a dependency on wxWidgets/libjpeg to decode arbitrary user images.
return result;
png::ImageGreyscale img;
if (!png::decode_png(rbuf, img) || img.cols == 0 || img.rows == 0)
return result;
result.width = int(img.cols);
result.height = int(img.rows);
result.pixels = std::move(img.buf);
return result;
}
Vec2f project_texture_displacement_uv(const Vec3f &position, const Vec3f &normal, const TextureDisplacementLayer &layer)
{
// Planar-project onto the two axes orthogonal to the dominant component of `normal`. Using a
// single projection axis for an entire patch (rather than per-vertex normals) avoids visible
// seams where the local normal direction changes. Proper seam-aware UV parametrization is a
// later-phase improvement.
const Vec3f n = normal.cwiseAbs();
Vec2f planar;
if (n.x() >= n.y() && n.x() >= n.z())
planar = Vec2f(position.y(), position.z());
else if (n.y() >= n.x() && n.y() >= n.z())
planar = Vec2f(position.x(), position.z());
else
planar = Vec2f(position.x(), position.y());
const float scale = (layer.tiling_scale > 1e-6f) ? (1.f / layer.tiling_scale) : 1.f;
planar *= scale;
const float rad = layer.rotation_deg * float(M_PI) / 180.f;
const float cs = std::cos(rad);
const float sn = std::sin(rad);
const Vec2f rotated(planar.x() * cs - planar.y() * sn, planar.x() * sn + planar.y() * cs);
return rotated + layer.offset;
}
// Area-weighted vertex normals computed from the patch's own (pre-displacement) connectivity.
// Since the patch's vertices have not moved yet at the point this is called, these are identical
// to the surface normals of the mesh the patch was extracted from.
static std::vector<Vec3f> texture_displacement_vertex_normals(const indexed_triangle_set &its)
{
std::vector<Vec3f> normals(its.vertices.size(), Vec3f::Zero());
for (const stl_triangle_vertex_indices &tri : its.indices) {
const Vec3f &a = its.vertices[tri[0]];
const Vec3f &b = its.vertices[tri[1]];
const Vec3f &c = its.vertices[tri[2]];
// Cross product magnitude is twice the face area, so this naturally area-weights the
// contribution of each incident face to its vertices.
const Vec3f area_weighted_normal = (b - a).cross(c - a);
normals[tri[0]] += area_weighted_normal;
normals[tri[1]] += area_weighted_normal;
normals[tri[2]] += area_weighted_normal;
}
for (Vec3f &n : normals) {
const float len = n.norm();
n = (len > 1e-8f) ? Vec3f(n / len) : Vec3f::UnitZ();
}
return normals;
}
indexed_triangle_set build_texture_displacement(const indexed_triangle_set &base_mesh,
const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementFacetsData &facets_data)
{
const indexed_triangle_set original_mesh = base_mesh;
indexed_triangle_set working_mesh = original_mesh;
bool mesh_modified = false;
// Layers behave like stacked image-editor layers: applied in slot order, each one sculpting
// the surface left by the layers before it. This is what makes overlapping layers "blend".
std::vector<const TextureDisplacementLayer *> ordered_layers;
for (const TextureDisplacementLayer &layer : layers)
if (!layer.empty() && layer.slot >= 0 && size_t(layer.slot) < TEXTURE_DISPLACEMENT_MAX_LAYERS)
ordered_layers.push_back(&layer);
std::sort(ordered_layers.begin(), ordered_layers.end(),
[](const TextureDisplacementLayer *a, const TextureDisplacementLayer *b) { return a->slot < b->slot; });
for (const TextureDisplacementLayer *layer : ordered_layers) {
const TriangleSelector::TriangleSplittingData &stored_data = facets_data[size_t(layer->slot)];
if (stored_data.triangles_to_split.empty())
continue;
const DecodedHeightTexture height = decode_height_texture(*layer);
if (height.empty())
continue;
TriangleSelector::TriangleSplittingData data = stored_data;
if (mesh_modified) {
// The stored paint mask is relative to the volume's original (unbaked) mesh; remap it
// onto the working mesh, which earlier layers may have already displaced/subdivided.
data = TriangleSelector::remap_painting(original_mesh, data, working_mesh, Transform3d::Identity(),
std::optional<std::reference_wrapper<const TriangleSelector::TriangleSplittingData>>{});
if (data.bitstream.empty())
continue;
}
const TriangleMesh selector_mesh(working_mesh);
TriangleSelector selector(selector_mesh);
selector.deserialize(data, false);
const indexed_triangle_set patch = selector.get_facets_strict(EnforcerBlockerType::ENFORCER);
if (patch.indices.empty())
continue;
// get_facets_strict() returns the *entire* mesh's vertex array regardless of which state
// was asked for (only the returned triangle indices are filtered by state) -- so `patch`
// and `rest` share the exact same vertex indexing. That is what lets the weld below be a
// plain index check instead of a position-based lookup.
indexed_triangle_set rest = selector.get_facets_strict(EnforcerBlockerType::NONE);
assert(rest.vertices.size() == patch.vertices.size());
// A vertex that is also used by at least one *unpainted* triangle is a boundary vertex:
// its final position is ambiguous (it belongs to both the painted and untouched surface),
// so it must never be displaced -- otherwise the baked patch would tear away from the
// rest of the mesh. Only vertices used exclusively by painted triangles ("interior" to the
// patch) are eligible for displacement. This is what keeps the bake seamless without any
// remeshing/hole-filling at the boundary.
std::vector<bool> is_boundary_vertex(rest.vertices.size(), false);
for (const stl_triangle_vertex_indices &tri : rest.indices)
for (int i = 0; i < 3; ++i)
is_boundary_vertex[tri[i]] = true;
const std::vector<Vec3f> vertex_normals = texture_displacement_vertex_normals(patch);
Vec3f average_normal = Vec3f::Zero();
for (const stl_triangle_vertex_indices &tri : patch.indices)
for (int i = 0; i < 3; ++i)
average_normal += vertex_normals[tri[i]];
average_normal = (average_normal.norm() > 1e-8f) ? Vec3f(average_normal.normalized()) : Vec3f::UnitZ();
const float sign = layer->invert ? -1.f : 1.f;
std::vector<int> interior_vertex_remap(rest.vertices.size(), -1);
indexed_triangle_set working_mesh_next = std::move(rest);
for (const stl_triangle_vertex_indices &tri : patch.indices) {
stl_triangle_vertex_indices final_tri;
for (int i = 0; i < 3; ++i) {
const int vi = tri[i];
if (is_boundary_vertex[vi]) {
// Shared with the untouched surface: reuse the existing, undisplaced vertex.
final_tri[i] = vi;
continue;
}
if (interior_vertex_remap[vi] < 0) {
const Vec2f uv = project_texture_displacement_uv(patch.vertices[vi], average_normal, *layer);
const float h = height.sample(uv);
const Vec3f displaced = patch.vertices[vi] + vertex_normals[vi] * (h * layer->depth_mm * sign);
interior_vertex_remap[vi] = int(working_mesh_next.vertices.size());
working_mesh_next.vertices.push_back(displaced);
}
final_tri[i] = interior_vertex_remap[vi];
}
working_mesh_next.indices.push_back(final_tri);
}
working_mesh = std::move(working_mesh_next);
mesh_modified = true;
}
if (mesh_modified)
its_compactify_vertices(working_mesh);
return working_mesh;
}
indexed_triangle_set build_texture_displacement(const ModelVolume &volume)
{
TextureDisplacementFacetsData facets_data;
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
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);
}
} // namespace Slic3r

View File

@@ -0,0 +1,120 @@
#ifndef slic3r_TextureDisplacement_hpp_
#define slic3r_TextureDisplacement_hpp_
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
#include <cereal/cereal.hpp>
#include <cereal/types/string.hpp>
#include <cereal/types/vector.hpp>
#include <array>
#include "Point.hpp"
#include "TriangleMesh.hpp"
#include "TriangleSelector.hpp"
namespace Slic3r {
class ModelVolume;
// Maximum number of simultaneous texture-displacement layers a single ModelVolume can hold.
// Each layer owns its own paint mask (ModelVolume::texture_displacement_facet(slot)), so this
// is also the number of independent EnforcerBlockerType selectors kept per volume.
static constexpr size_t TEXTURE_DISPLACEMENT_MAX_LAYERS = 8;
// One texture asset plus its projection/displacement parameters. Several layers may be painted
// onto overlapping areas of the same volume: they are applied in slot order (like layers in an
// image editor), each one displacing the surface that resulted from the layers before it. This
// is what "layered/blended" texture displacement means in this feature.
struct TextureDisplacementLayer
{
// Index into ModelVolume::texture_displacement_facets, assigned once when the layer is
// created. Not reused for the lifetime of the ModelVolume, so a deleted layer's slot simply
// becomes unused rather than being handed to a different layer.
int slot = -1;
std::string name;
// Path on the local filesystem the image was loaded from (informational; may be stale or
// empty, e.g. after loading a .3mf on a different machine).
std::string path;
// Path inside the .3mf archive once saved (empty until the project is saved once).
std::string path_in_3mf;
// Raw encoded image bytes. Only 8-bit grayscale PNG is understood by decode_height_texture()
// (libslic3r has no GUI image toolkit available); the GUI converts any imported image to that
// format before storing it here, so the baking code never needs to depend on wxWidgets.
std::shared_ptr<std::vector<unsigned char>> image_data;
float depth_mm = 0.4f; // maximum displacement along the surface normal, in mm
float tiling_scale = 10.f; // size of one texture tile, in mm
float rotation_deg = 0.f;
Vec2f offset = Vec2f::Zero();
bool invert = false;
bool empty() const { return !image_data || image_data->empty(); }
template<class Archive> void save(Archive &ar) const
{
std::string blob = image_data ? std::string(image_data->begin(), image_data->end()) : std::string();
ar(slot, name, path, path_in_3mf, blob, depth_mm, tiling_scale, rotation_deg, offset, invert);
}
template<class Archive> void load(Archive &ar)
{
std::string blob;
ar(slot, name, path, path_in_3mf, blob, depth_mm, tiling_scale, rotation_deg, offset, invert);
image_data = blob.empty() ? nullptr : std::make_shared<std::vector<unsigned char>>(blob.begin(), blob.end());
}
};
// Decoded 8-bit grayscale height sample, independent of any GUI/OpenGL texture object so it can
// be evaluated from a background bake Job as well as from GUI-side preview code.
struct DecodedHeightTexture
{
std::vector<uint8_t> pixels; // row-major, top-to-bottom, one byte per pixel
int width = 0;
int height = 0;
bool empty() const { return width <= 0 || height <= 0 || pixels.empty(); }
// Bilinearly sampled height in [0, 1] at a tiling (wrapping) normalized uv coordinate.
float sample(const Vec2f &uv) const;
};
// Decode a layer's raw image bytes into sampleable grayscale height data. Returns an empty
// DecodedHeightTexture if image_data is empty or is not an 8-bit grayscale PNG.
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer);
// Project a mesh-local position onto a layer's height texture, honouring its tiling scale,
// rotation and offset. Uses a simple dominant-axis planar projection of the given normal: proper
// seam-aware UV parametrization is a later-phase improvement (see project plan).
Vec2f project_texture_displacement_uv(const Vec3f &position, const Vec3f &normal, const TextureDisplacementLayer &layer);
// One paint mask (as stored by ModelVolume::texture_displacement_facets) per possible layer slot.
using TextureDisplacementFacetsData = std::array<TriangleSelector::TriangleSplittingData, TEXTURE_DISPLACEMENT_MAX_LAYERS>;
// Bake all painted texture-displacement layers into `base_mesh`'s geometry, restricted to the
// painted area(s) only (the rest of the mesh is left untouched). Layers are applied in slot
// order, each measuring its displacement from the surface left by the previous one. Returns the
// mesh unchanged if nothing is painted or no layer has a usable texture.
//
// Takes plain copied data rather than a ModelVolume reference so it is safe to call from a
// background thread (e.g. a bake Job's process() method) on a snapshot captured on the main
// thread, without touching the live Model concurrently with the UI.
//
// Known Phase 1-3 limitation: this does not attempt to remap texture-displacement paint data
// across topology-changing operations performed outside this gizmo (e.g. ModelObject::split(),
// mesh-boolean ops) the way TriangleSelector::remap_painting() does for the other paint channels.
// Such operations will silently drop any unbaked texture-displacement paint on the affected
// volume. This is an explicit extension point for a later phase, not an oversight.
indexed_triangle_set build_texture_displacement(const indexed_triangle_set &base_mesh,
const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementFacetsData &facets_data);
// Convenience overload for main-thread callers: extracts the mesh/layers/paint data from `volume`
// and forwards to the overload above.
indexed_triangle_set build_texture_displacement(const ModelVolume &volume);
} // namespace Slic3r
#endif // slic3r_TextureDisplacement_hpp_

View File

@@ -172,6 +172,8 @@ set(SLIC3R_GUI_SOURCES
GUI/Gizmos/GLGizmosManager.hpp
GUI/Gizmos/GLGizmoSVG.cpp
GUI/Gizmos/GLGizmoSVG.hpp
GUI/Gizmos/GLGizmoTextureDisplacement.cpp
GUI/Gizmos/GLGizmoTextureDisplacement.hpp
GUI/Gizmos/GLGizmoUtils.cpp
GUI/Gizmos/GLGizmoUtils.hpp
#GUI/Gizmos/GLGizmoText.cpp
@@ -297,6 +299,8 @@ set(SLIC3R_GUI_SOURCES
GUI/Jobs/SLAImportDialog.hpp
GUI/Jobs/SLAImportJob.cpp
GUI/Jobs/SLAImportJob.hpp
GUI/Jobs/TextureDisplacementBakeJob.cpp
GUI/Jobs/TextureDisplacementBakeJob.hpp
GUI/Jobs/ThreadSafeQueue.hpp
GUI/Jobs/UpgradeNetworkJob.cpp
GUI/Jobs/UpgradeNetworkJob.hpp

View File

@@ -100,6 +100,8 @@ std::pair<bool, std::string> GLShadersManager::init()
valid &= append_shader("mm_gouraud", { prefix + "mm_gouraud.vs", prefix + "mm_gouraud.fs" }, { "FLIP_TRIANGLE_NORMALS"sv });
else
valid &= append_shader("mm_gouraud", { prefix + "mm_gouraud.vs", prefix + "mm_gouraud.fs" });
// Fast bump-map preview for the texture displacement gizmo (see libslic3r/TextureDisplacement.hpp).
valid &= append_shader("texture_displacement_bump", { prefix + "texture_displacement_bump.vs", prefix + "texture_displacement_bump.fs" });
return { valid, error };
}

View File

@@ -27,7 +27,8 @@ enum class PainterGizmoType {
FDM_SUPPORTS,
SEAM,
MM_SEGMENTATION,
FUZZY_SKIN
FUZZY_SKIN,
TEXTURE_DISPLACEMENT
};
class TriangleSelectorGUI : public TriangleSelector {

View File

@@ -0,0 +1,362 @@
#include "GLGizmoTextureDisplacement.hpp"
#include <fstream>
#include <boost/filesystem.hpp>
#include <boost/system/error_code.hpp>
#include "libslic3r/Model.hpp"
#include "libslic3r/PNGReadWrite.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/MsgDialog.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/Jobs/TextureDisplacementBakeJob.hpp"
#include "slic3r/Utils/UndoRedo.hpp"
#include "GLGizmoUtils.hpp"
#include <glad/gl.h>
#include <algorithm>
namespace Slic3r::GUI {
GLGizmoTextureDisplacement::GLGizmoTextureDisplacement(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id)
: GLGizmoPainterBase(parent, icon_filename, sprite_id)
{
}
bool GLGizmoTextureDisplacement::on_init()
{
m_desc["cursor_size"] = _L("Brush size");
m_desc["circle"] = _L("Circle");
m_desc["sphere"] = _L("Sphere");
m_desc["add_texture"] = _L("Add texture...");
m_desc["remove_layer"] = _L("Remove");
m_desc["bake"] = _L("Bake");
m_desc["remove_all"] = _L("Erase all");
return true;
}
std::string GLGizmoTextureDisplacement::on_get_name() const
{
return _u8L("Texture displacement");
}
void GLGizmoTextureDisplacement::on_shutdown()
{
m_parent.toggle_model_objects_visibility(true);
}
PainterGizmoType GLGizmoTextureDisplacement::get_painter_type() const
{
return PainterGizmoType::TEXTURE_DISPLACEMENT;
}
wxString GLGizmoTextureDisplacement::handle_snapshot_action_name(bool shift_down, GLGizmoPainterBase::Button button_down) const
{
return shift_down ? _L("Erase texture displacement paint") : _L("Paint texture displacement");
}
void GLGizmoTextureDisplacement::render_painter_gizmo()
{
const Selection &selection = m_parent.get_selection();
glsafe(::glEnable(GL_BLEND));
glsafe(::glEnable(GL_DEPTH_TEST));
// Phase 1 reuses the standard paint-mask overlay (as every other paint gizmo does) to show
// which area of the active layer is painted. A dedicated bump-map preview shader
// (resources/shaders/*/texture_displacement_bump.*) is registered and ready to use, but
// wiring it into this render path is deferred to a later phase -- see the project plan.
// "Bake" already shows the true, real-geometry result, which is the most important preview.
render_triangles(selection);
m_c->object_clipper()->render_cut();
m_c->instances_hider()->render_cut();
render_cursor();
glsafe(::glDisable(GL_BLEND));
}
ModelVolume* GLGizmoTextureDisplacement::texture_volume()
{
ModelObject *mo = m_c->selection_info()->model_object();
if (!mo)
return nullptr;
for (ModelVolume *mv : mo->volumes)
if (mv->is_model_part())
return mv;
return nullptr;
}
const ModelVolume* GLGizmoTextureDisplacement::texture_volume() const
{
return const_cast<GLGizmoTextureDisplacement *>(this)->texture_volume();
}
void GLGizmoTextureDisplacement::update_model_object()
{
bool updated = false;
ModelObject *mo = m_c->selection_info()->model_object();
int idx = -1;
for (ModelVolume *mv : mo->volumes) {
if (!mv->is_model_part())
continue;
++idx;
updated |= mv->texture_displacement_facet(m_active_layer_slot).set(*m_triangle_selectors[idx]);
}
if (updated) {
const ModelObjectPtrs &mos = wxGetApp().model().objects;
wxGetApp().obj_list()->update_info_items(std::find(mos.begin(), mos.end(), mo) - mos.begin());
m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS));
}
}
void GLGizmoTextureDisplacement::update_from_model_object(bool first_update)
{
wxBusyCursor wait;
const ModelObject *mo = m_c->selection_info()->model_object();
m_triangle_selectors.clear();
std::vector<ColorRGBA> ebt_colors;
ebt_colors.push_back(GLVolume::NEUTRAL_COLOR);
ebt_colors.push_back(TriangleSelectorGUI::enforcers_color);
ebt_colors.push_back(TriangleSelectorGUI::blockers_color);
for (const ModelVolume *mv : mo->volumes) {
if (!mv->is_model_part())
continue;
const TriangleMesh *mesh = &mv->mesh();
m_triangle_selectors.emplace_back(std::make_unique<TriangleSelectorPatch>(*mesh, ebt_colors));
m_triangle_selectors.back()->deserialize(mv->texture_displacement_facet(m_active_layer_slot).get_data(), false);
m_triangle_selectors.back()->request_update_render_data();
}
}
void GLGizmoTextureDisplacement::set_active_layer(int slot)
{
if (slot == m_active_layer_slot)
return;
// Flush edits made while the previous layer was active before switching what the selectors
// reflect -- otherwise they would be silently lost.
update_model_object();
m_active_layer_slot = slot;
update_from_model_object(false);
}
void GLGizmoTextureDisplacement::add_texture_layer()
{
ModelVolume *mv = texture_volume();
if (!mv)
return;
std::array<bool, TEXTURE_DISPLACEMENT_MAX_LAYERS> used{};
for (const TextureDisplacementLayer &l : mv->texture_displacement_layers)
if (l.slot >= 0 && size_t(l.slot) < used.size())
used[size_t(l.slot)] = true;
int free_slot = -1;
for (size_t i = 0; i < used.size(); ++i)
if (!used[i]) { free_slot = int(i); break; }
if (free_slot < 0) {
show_error(nullptr, _u8L("Maximum number of texture displacement layers reached."));
return;
}
const wxString wildcard = "Images (*.png;*.jpg;*.jpeg;*.bmp)|*.png;*.jpg;*.jpeg;*.bmp";
wxFileDialog dialog(nullptr, _L("Choose a texture image (height map)"), wxEmptyString, wxEmptyString, wildcard,
wxFD_OPEN | wxFD_FILE_MUST_EXIST);
if (dialog.ShowModal() != wxID_OK)
return;
const std::string path = into_u8(dialog.GetPath());
wxImage image;
if (!image.LoadFile(dialog.GetPath()) || !image.IsOk()) {
show_error(nullptr, _u8L("Could not load the selected image."));
return;
}
// libslic3r's PNG decoder (used by the baking code, which has no wxWidgets dependency) only
// understands true 8-bit grayscale PNG. Convert and re-encode through Slic3r's own PNG writer
// here rather than relying on wxImage's PNG encoder to pick a compatible color type.
const wxImage gray = image.ConvertToGreyscale();
const int w = gray.GetWidth();
const int h = gray.GetHeight();
if (w <= 0 || h <= 0) {
show_error(nullptr, _u8L("The selected image is empty."));
return;
}
std::vector<uint8_t> gray_pixels(size_t(w) * size_t(h));
const unsigned char *rgb = gray.GetData();
for (size_t i = 0; i < gray_pixels.size(); ++i)
gray_pixels[i] = rgb[i * 3];
const boost::filesystem::path tmp_path = boost::filesystem::temp_directory_path()
/ boost::filesystem::unique_path("orca_texdisp_%%%%%%%%.png");
if (!Slic3r::png::write_gray_to_file(tmp_path.string(), size_t(w), size_t(h), gray_pixels)) {
show_error(nullptr, _u8L("Failed to prepare the texture for use."));
return;
}
std::vector<unsigned char> bytes;
{
std::ifstream ifs(tmp_path.string(), std::ios::binary);
bytes.assign(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
}
boost::system::error_code ec;
boost::filesystem::remove(tmp_path, ec);
if (bytes.empty()) {
show_error(nullptr, _u8L("Failed to prepare the texture for use."));
return;
}
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Add texture displacement layer"), UndoRedo::SnapshotType::GizmoAction);
TextureDisplacementLayer layer;
layer.slot = free_slot;
layer.name = boost::filesystem::path(path).filename().string();
layer.path = path;
layer.image_data = std::make_shared<std::vector<unsigned char>>(std::move(bytes));
mv->texture_displacement_layers.push_back(std::move(layer));
set_active_layer(free_slot);
m_parent.set_as_dirty();
}
void GLGizmoTextureDisplacement::remove_texture_layer(int slot)
{
ModelVolume *mv = texture_volume();
if (!mv)
return;
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Remove texture displacement layer"), UndoRedo::SnapshotType::GizmoAction);
auto &layers = mv->texture_displacement_layers;
layers.erase(std::remove_if(layers.begin(), layers.end(),
[slot](const TextureDisplacementLayer &l) { return l.slot == slot; }),
layers.end());
if (slot >= 0 && slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS))
mv->texture_displacement_facet(slot).reset();
if (m_active_layer_slot == slot)
update_from_model_object(false);
m_parent.set_as_dirty();
}
void GLGizmoTextureDisplacement::bake()
{
ModelVolume *mv = texture_volume();
if (!mv || m_bake_in_progress)
return;
// Make sure the currently active layer's in-progress edits are flushed into the model before
// baking, otherwise the most recent, not-yet-committed strokes would be silently skipped.
update_model_object();
if (!mv->is_texture_displacement_painted()) {
show_error(nullptr, _u8L("Nothing is painted, there is nothing to bake."));
return;
}
m_bake_in_progress = true;
queue_texture_displacement_bake(*mv, [this]() { m_bake_in_progress = false; });
}
void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float bottom_limit)
{
ModelObject *mo = m_c->selection_info()->model_object();
if (!mo)
return;
ModelVolume *mv = texture_volume();
const float approx_height = m_imgui->scaled(24.f);
y = std::min(y, bottom_limit - approx_height);
GizmoImguiSetNextWIndowPos(x, y, ImGuiCond_Always, 1.0f, 0.0f);
ImGuiWrapper::push_toolbar_style(m_parent.get_scale());
GizmoImguiBegin(get_name(), ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar);
m_imgui->text(m_desc.at("cursor_size"));
ImGui::SameLine();
ImGui::PushItemWidth(m_imgui->scaled(7.f));
m_imgui->slider_float("##cursor_radius", &m_cursor_radius, CursorRadiusMin, CursorRadiusMax, "%.2f");
bool is_circle = m_cursor_type == TriangleSelector::CursorType::CIRCLE;
if (ImGui::RadioButton(m_desc.at("circle").ToUTF8().data(), is_circle))
m_cursor_type = TriangleSelector::CursorType::CIRCLE;
ImGui::SameLine();
if (ImGui::RadioButton(m_desc.at("sphere").ToUTF8().data(), !is_circle))
m_cursor_type = TriangleSelector::CursorType::SPHERE;
ImGui::Separator();
m_imgui->text(_L("Texture layers"));
if (mv != nullptr) {
std::vector<TextureDisplacementLayer *> ordered;
for (TextureDisplacementLayer &l : mv->texture_displacement_layers)
ordered.push_back(&l);
std::sort(ordered.begin(), ordered.end(), [](const auto *a, const auto *b) { return a->slot < b->slot; });
for (TextureDisplacementLayer *layer : ordered) {
ImGui::PushID(layer->slot);
const bool is_active = layer->slot == m_active_layer_slot;
if (ImGui::RadioButton("##active", is_active))
set_active_layer(layer->slot);
ImGui::SameLine();
ImGui::Text("%s", layer->name.empty() ? "texture" : layer->name.c_str());
ImGui::SameLine();
if (m_imgui->button(m_desc.at("remove_layer")))
remove_texture_layer(layer->slot);
ImGui::PushItemWidth(m_imgui->scaled(7.f));
m_imgui->slider_float(_u8L("Depth (mm)"), &layer->depth_mm, 0.02f, 5.f, "%.2f");
m_imgui->slider_float(_u8L("Tile size (mm)"), &layer->tiling_scale, 0.5f, 100.f, "%.1f");
m_imgui->slider_float(_u8L("Rotation"), &layer->rotation_deg, 0.f, 360.f, "%.0f");
ImGui::PopItemWidth();
ImGui::Checkbox(_u8L("Invert").c_str(), &layer->invert);
ImGui::PopID();
ImGui::Separator();
}
}
if (m_imgui->button(m_desc.at("add_texture")))
add_texture_layer();
ImGui::Separator();
m_imgui->disabled_begin(mv == nullptr || !mv->is_texture_displacement_painted());
if (m_imgui->button(m_desc.at("remove_all"))) {
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Reset texture displacement selection"), UndoRedo::SnapshotType::GizmoAction);
int idx = -1;
for (ModelVolume *v : mo->volumes)
if (v->is_model_part()) {
++idx;
m_triangle_selectors[idx]->reset();
m_triangle_selectors[idx]->request_update_render_data();
}
update_model_object();
m_parent.set_as_dirty();
}
m_imgui->disabled_end();
ImGui::SameLine();
m_imgui->disabled_begin(m_bake_in_progress || mv == nullptr || !mv->is_texture_displacement_painted());
if (m_imgui->button(m_bake_in_progress ? _L("Baking...") : m_desc.at("bake")))
bake();
m_imgui->disabled_end();
ImGui::Separator();
if (m_imgui->button(_L("Done")))
m_parent.reset_all_gizmos();
GizmoImguiEnd();
ImGuiWrapper::pop_toolbar_style();
}
} // namespace Slic3r::GUI

View File

@@ -0,0 +1,70 @@
#ifndef slic3r_GLGizmoTextureDisplacement_hpp_
#define slic3r_GLGizmoTextureDisplacement_hpp_
#include "GLGizmoPainterBase.hpp"
#include "libslic3r/TextureDisplacement.hpp"
#include "slic3r/GUI/I18N.hpp"
namespace Slic3r::GUI {
// Paint-style gizmo that assigns one or more texture-displacement "layers" (see
// libslic3r/TextureDisplacement.hpp) to painted areas of a model, and can bake the result into
// real mesh geometry. See the project plan for the overall architecture; in short:
// - each layer owns its own independent paint mask (ModelVolume::texture_displacement_facets),
// reusing the same TriangleSelector/FacetsAnnotation machinery as every other paint gizmo --
// only one layer is "active" (paintable) at a time, selected in the panel below;
// - "Bake" runs build_texture_displacement() in a background job and commits the result exactly
// like the Emboss/SVG "project on surface" gizmo does.
class GLGizmoTextureDisplacement : public GLGizmoPainterBase
{
public:
GLGizmoTextureDisplacement(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id);
void render_painter_gizmo() override;
protected:
void on_render_input_window(float x, float y, float bottom_limit) override;
std::string on_get_name() const override;
wxString handle_snapshot_action_name(bool shift_down, Button button_down) const override;
std::string get_gizmo_entering_text() const override { return _u8L("Entering Texture displacement painting"); }
std::string get_gizmo_leaving_text() const override { return _u8L("Leaving Texture displacement painting"); }
std::string get_action_snapshot_name() const override { return _u8L("Texture displacement editing"); }
EnforcerBlockerType get_left_button_state_type() const override { return EnforcerBlockerType::ENFORCER; }
EnforcerBlockerType get_right_button_state_type() const override { return EnforcerBlockerType::NONE; }
private:
bool on_init() override;
void update_model_object() override;
void update_from_model_object(bool first_update) override;
void on_opening() override {}
void on_shutdown() override;
PainterGizmoType get_painter_type() const override;
// Phase 1 restricts the texture layer list to the first model-part volume of the current
// object (the common single-volume case); multi-part objects only get texture layers on
// their first part until a later phase. Returns nullptr if there is no model part.
ModelVolume* texture_volume();
const ModelVolume* texture_volume() const;
void add_texture_layer();
void remove_texture_layer(int slot);
void set_active_layer(int slot); // flushes the previous layer's edits, then reloads selectors
void bake();
// Which of the up to TEXTURE_DISPLACEMENT_MAX_LAYERS paint masks the brush currently writes
// into. Always a valid slot index (0 by default) so the base class's per-volume selector
// machinery always has something to work with, even before any texture has been added --
// painting into a slot with no texture assigned is harmless, it just has no visible/bake
// effect until a texture is added to that slot.
int m_active_layer_slot = 0;
bool m_bake_in_progress = false;
std::map<std::string, wxString> m_desc;
};
} // namespace Slic3r::GUI
#endif // slic3r_GLGizmoTextureDisplacement_hpp_

View File

@@ -22,6 +22,7 @@
//#include "slic3r/GUI/Gizmos/GLGizmoHollow.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoSeam.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoSimplify.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoEmboss.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoSVG.hpp"
@@ -164,6 +165,9 @@ void GLGizmosManager::switch_gizmos_icon_filename()
case(EType::FuzzySkin):
gizmo->set_icon_filename(m_is_dark ? "toolbar_fuzzy_skin_paint_dark.svg" : "toolbar_fuzzy_skin_paint.svg");
break;
case(EType::TextureDisplacement):
gizmo->set_icon_filename(m_is_dark ? "toolbar_fuzzy_skin_paint_dark.svg" : "toolbar_fuzzy_skin_paint.svg");
break;
case(EType::MeshBoolean):
gizmo->set_icon_filename(m_is_dark ? "toolbar_meshboolean_dark.svg" : "toolbar_meshboolean.svg");
break;
@@ -213,6 +217,9 @@ bool GLGizmosManager::init()
m_gizmos.emplace_back(new GLGizmoSeam(m_parent, m_is_dark ? "toolbar_seam_dark.svg" : "toolbar_seam.svg", EType::Seam));
m_gizmos.emplace_back(new GLGizmoFuzzySkin(m_parent, m_is_dark ? "toolbar_fuzzy_skin_paint_dark.svg" : "toolbar_fuzzy_skin_paint.svg", EType::FuzzySkin));
m_gizmos.emplace_back(new GLGizmoMmuSegmentation(m_parent, m_is_dark ? "mmu_segmentation_dark.svg" : "mmu_segmentation.svg", EType::MmSegmentation));
// TODO: placeholder icon reused from the fuzzy-skin gizmo; swap in a dedicated
// toolbar_texture_displacement(_dark).svg once one exists.
m_gizmos.emplace_back(new GLGizmoTextureDisplacement(m_parent, m_is_dark ? "toolbar_fuzzy_skin_paint_dark.svg" : "toolbar_fuzzy_skin_paint.svg", EType::TextureDisplacement));
m_gizmos.emplace_back(new GLGizmoEmboss(m_parent, m_is_dark ? "toolbar_text_dark.svg" : "toolbar_text.svg", EType::Emboss));
m_gizmos.emplace_back(new GLGizmoSVG(m_parent));
m_gizmos.emplace_back(new GLGizmoMeasure(m_parent, m_is_dark ? "toolbar_measure_dark.svg" : "toolbar_measure.svg", EType::Measure));
@@ -524,6 +531,8 @@ bool GLGizmosManager::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_p
return dynamic_cast<GLGizmoCut3D*>(m_gizmos[Cut].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
else if (m_current == FuzzySkin)
return dynamic_cast<GLGizmoFuzzySkin*>(m_gizmos[FuzzySkin].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
else if (m_current == TextureDisplacement)
return dynamic_cast<GLGizmoTextureDisplacement*>(m_gizmos[TextureDisplacement].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
else if (m_current == MeshBoolean)
return dynamic_cast<GLGizmoMeshBoolean*>(m_gizmos[MeshBoolean].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
else if (m_current == BrimEars)
@@ -537,6 +546,7 @@ bool GLGizmosManager::is_paint_gizmo()
return m_current == EType::FdmSupports ||
m_current == EType::MmSegmentation ||
m_current == EType::FuzzySkin ||
m_current == EType::TextureDisplacement ||
m_current == EType::Seam;
}
@@ -1486,6 +1496,8 @@ std::string get_name_from_gizmo_etype(GLGizmosManager::EType type)
return "Color Painting";
case GLGizmosManager::EType::FuzzySkin:
return "Fuzzy Skin Painting";
case GLGizmosManager::EType::TextureDisplacement:
return "Texture Displacement";
default:
return "";
}

View File

@@ -84,6 +84,7 @@ public:
Seam,
FuzzySkin,
MmSegmentation,
TextureDisplacement,
Emboss,
Svg,
Measure,

View File

@@ -0,0 +1,88 @@
#include "TextureDisplacementBakeJob.hpp"
#include <algorithm>
#include "libslic3r/Model.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/Utils/UndoRedo.hpp"
namespace Slic3r::GUI {
TextureDisplacementBakeJob::TextureDisplacementBakeJob(TextureDisplacementBakeInput &&input, std::function<void()> on_finished)
: m_input(std::move(input)), m_on_finished(std::move(on_finished))
{
}
void TextureDisplacementBakeJob::process(Ctl &ctl)
{
ctl.update_status(0, _u8L("Baking texture displacement"));
// Only ever touches m_input (captured by value before this job was queued) and local state --
// never the live Model -- so this is safe to run concurrently with the UI thread.
m_result = TriangleMesh(build_texture_displacement(m_input.base_mesh, m_input.layers, m_input.facets_data));
}
void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &eptr)
{
struct OnExit
{
std::function<void()> fn;
~OnExit() { if (fn) fn(); }
} on_exit{m_on_finished};
if (canceled || eptr || m_result.empty())
return;
Plater *plater = wxGetApp().plater();
Plater::TakeSnapshot snapshot(plater, _u8L("Bake texture displacement"), UndoRedo::SnapshotType::GizmoAction);
ModelVolume *volume = get_model_volume(m_input.volume_id, plater->model().objects);
if (volume == nullptr)
return;
volume->set_mesh(std::move(m_result));
volume->set_new_unique_id();
volume->calculate_convex_hull();
// Clear the paint mask of every layer that was actually baked so a repeat bake (or the paint
// overlay) doesn't act on triangles that no longer represent the same unbaked surface. The
// texture layer definitions themselves (and paint outside the baked area, if any) are left
// untouched so the user can keep sculpting with the same textures.
for (const TextureDisplacementLayer &layer : m_input.layers)
if (!layer.empty() && layer.slot >= 0 && layer.slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS))
volume->texture_displacement_facet(layer.slot).reset();
ModelObject *object = volume->get_object();
if (object == nullptr)
return;
if (ObjectList *obj_list = wxGetApp().obj_list()) {
const ModelObjectPtrs &objs = plater->model().objects;
auto it = std::find(objs.begin(), objs.end(), object);
if (it != objs.end())
obj_list->update_info_items(size_t(it - objs.begin()));
}
plater->changed_object(*object);
}
void queue_texture_displacement_bake(const ModelVolume &volume, std::function<void()> on_finished)
{
TextureDisplacementBakeInput input;
input.volume_id = volume.id();
input.base_mesh = volume.mesh().its;
input.layers = volume.texture_displacement_layers;
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
input.facets_data[size_t(i)] = volume.texture_displacement_facet(i).get_data();
auto &worker = wxGetApp().plater()->get_ui_job_worker();
queue_job(worker, std::make_unique<TextureDisplacementBakeJob>(std::move(input), std::move(on_finished)));
}
} // namespace Slic3r::GUI

View File

@@ -0,0 +1,51 @@
#ifndef slic3r_TextureDisplacementBakeJob_hpp_
#define slic3r_TextureDisplacementBakeJob_hpp_
#include <functional>
#include <vector>
#include "libslic3r/ObjectID.hpp"
#include "libslic3r/TextureDisplacement.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "Job.hpp"
namespace Slic3r::GUI {
// Everything process() needs, captured by value on the main thread when the job is queued so the
// worker thread never touches the live Model concurrently with the UI (mirrors how EmbossJob's
// DataBase is captured before process() runs).
struct TextureDisplacementBakeInput
{
ObjectID volume_id;
indexed_triangle_set base_mesh;
std::vector<TextureDisplacementLayer> layers;
TextureDisplacementFacetsData facets_data;
};
// Bakes a volume's painted texture-displacement layers into real mesh geometry in the background,
// then commits the result on the main thread -- mirrors EmbossJob's UpdateJob/update_volume()
// bake-and-commit pattern (see EmbossJob.cpp).
class TextureDisplacementBakeJob : public Job
{
public:
TextureDisplacementBakeJob(TextureDisplacementBakeInput &&input, std::function<void()> on_finished);
void process(Ctl &ctl) override;
void finalize(bool canceled, std::exception_ptr &eptr) override;
private:
TextureDisplacementBakeInput m_input;
TriangleMesh m_result;
std::function<void()> m_on_finished;
};
// Captures `volume`'s current mesh/layers/paint data and queues a TextureDisplacementBakeJob on
// the app's UI job worker. `on_finished` is always called once the job settles (success, failure,
// or cancellation), so the caller can clear its own "bake in progress" UI state. Must be called
// from the main thread.
void queue_texture_displacement_bake(const ModelVolume &volume, std::function<void()> on_finished);
} // namespace Slic3r::GUI
#endif // slic3r_TextureDisplacementBakeJob_hpp_

View File

@@ -29,6 +29,7 @@ add_executable(${_TEST_NAME}_tests
test_optimizers.cpp
# test_png_io.cpp
test_indexed_triangle_set.cpp
test_texture_displacement.cpp
../libnest2d/printer_parts.cpp
)

View File

@@ -0,0 +1,136 @@
#define NOMINMAX
#include <catch2/catch_all.hpp>
#include <fstream>
#include <boost/filesystem.hpp>
#include "libslic3r/TextureDisplacement.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/TriangleSelector.hpp"
#include "libslic3r/PNGReadWrite.hpp"
using namespace Slic3r;
using Catch::Matchers::WithinAbs;
// Encodes a flat (uniform-value) grayscale image through Slic3r's own PNG writer/reader round
// trip, so decode_height_texture() (which only accepts true 8-bit grayscale PNG) is guaranteed a
// compatible file, exactly like the GUI's "Add texture" import path does.
static std::shared_ptr<std::vector<unsigned char>> make_flat_gray_png(uint8_t value, size_t w = 4, size_t h = 4)
{
std::vector<uint8_t> pixels(w * h, value);
const boost::filesystem::path tmp_path = boost::filesystem::temp_directory_path()
/ boost::filesystem::unique_path("texdisp_test_%%%%%%%%.png");
REQUIRE(Slic3r::png::write_gray_to_file(tmp_path.string(), w, h, pixels));
std::vector<unsigned char> bytes;
{
std::ifstream ifs(tmp_path.string(), std::ios::binary);
bytes.assign(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
}
boost::system::error_code ec;
boost::filesystem::remove(tmp_path, ec);
REQUIRE_FALSE(bytes.empty());
return std::make_shared<std::vector<unsigned char>>(std::move(bytes));
}
TEST_CASE("TextureDisplacement: decode_height_texture round-trips an 8-bit grayscale PNG", "[TextureDisplacement]")
{
TextureDisplacementLayer layer;
layer.image_data = make_flat_gray_png(128, 4, 4);
DecodedHeightTexture tex = decode_height_texture(layer);
REQUIRE_FALSE(tex.empty());
CHECK(tex.width == 4);
CHECK(tex.height == 4);
REQUIRE_THAT(tex.sample(Vec2f(0.5f, 0.5f)), WithinAbs(128.0 / 255.0, 1.0 / 255.0));
}
TEST_CASE("TextureDisplacement: an empty layer list leaves the mesh unchanged", "[TextureDisplacement]")
{
const indexed_triangle_set cube = its_make_cube(10., 10., 10.);
const std::vector<TextureDisplacementLayer> layers; // none
TextureDisplacementFacetsData facets{}; // all empty
const indexed_triangle_set result = build_texture_displacement(cube, layers, facets);
REQUIRE(result.vertices.size() == cube.vertices.size());
REQUIRE(result.indices.size() == cube.indices.size());
for (size_t i = 0; i < cube.vertices.size(); ++i)
for (int c = 0; c < 3; ++c)
CHECK(result.vertices[i](c) == cube.vertices[i](c));
}
TEST_CASE("TextureDisplacement: fully painting a mesh displaces every vertex along its own normal", "[TextureDisplacement]")
{
const indexed_triangle_set cube = its_make_cube(10., 10., 10.);
const TriangleMesh cube_mesh(cube);
TriangleSelector selector(cube_mesh);
for (int f = 0; f < int(cube.indices.size()); ++f)
selector.set_facet(f, EnforcerBlockerType::ENFORCER);
TextureDisplacementFacetsData facets{};
facets[0] = selector.serialize();
TextureDisplacementLayer layer;
layer.slot = 0;
layer.depth_mm = 2.0f;
layer.tiling_scale = 5.0f;
layer.image_data = make_flat_gray_png(255); // sample() == 1.0 everywhere -> full depth_mm displacement
const indexed_triangle_set result = build_texture_displacement(cube, {layer}, facets);
REQUIRE(result.vertices.size() == cube.vertices.size());
for (size_t i = 0; i < cube.vertices.size(); ++i) {
const float moved = (result.vertices[i] - cube.vertices[i]).norm();
CHECK_THAT(moved, WithinAbs(layer.depth_mm, 1e-3f));
}
}
TEST_CASE("TextureDisplacement: boundary vertices shared with unpainted triangles are pinned", "[TextureDisplacement]")
{
// A small triangle fan around a central vertex O, with 4 outer points A/B/C/D forming 4
// triangles T0..T3 in the XY plane. Only T0, T1, T2 are painted, T3 is left unpainted:
// O: touches all 4 triangles (incl. unpainted T3) -> boundary, must NOT move
// A: touches T0 (painted) and T3 (unpainted) -> boundary, must NOT move
// D: touches T2 (painted) and T3 (unpainted) -> boundary, must NOT move
// B: touches only T0 and T1 (both painted) -> interior, SHOULD move
// C: touches only T1 and T2 (both painted) -> interior, SHOULD move
indexed_triangle_set fan;
fan.vertices = { {0.f, 0.f, 0.f}, {1.f, 0.f, 0.f}, {0.f, 1.f, 0.f}, {-1.f, 0.f, 0.f}, {0.f, -1.f, 0.f} };
fan.indices = { {0, 1, 2}, {0, 2, 3}, {0, 3, 4}, {0, 4, 1} };
const TriangleMesh fan_mesh(fan);
TriangleSelector selector(fan_mesh);
selector.set_facet(0, EnforcerBlockerType::ENFORCER);
selector.set_facet(1, EnforcerBlockerType::ENFORCER);
selector.set_facet(2, EnforcerBlockerType::ENFORCER);
// facet 3 (T3) is left at its default EnforcerBlockerType::NONE.
TextureDisplacementFacetsData facets{};
facets[0] = selector.serialize();
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 result = build_texture_displacement(fan, {layer}, facets);
// Find each named vertex's post-bake position by matching the original (pinned vertices keep
// their exact original position; moved ones won't match any original position anymore).
auto still_at_original_position = [&](const Vec3f &original) {
for (const Vec3f &v : result.vertices)
if ((v - original).norm() < 1e-6f)
return true;
return false;
};
CHECK(still_at_original_position(fan.vertices[0])); // O: boundary
CHECK(still_at_original_position(fan.vertices[1])); // A: boundary
CHECK(still_at_original_position(fan.vertices[4])); // D: boundary
CHECK_FALSE(still_at_original_position(fan.vertices[2])); // B: interior, must have moved
CHECK_FALSE(still_at_original_position(fan.vertices[3])); // C: interior, must have moved
}