mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-10 12:30:10 +00:00
Compare commits
2 Commits
main
...
feature/te
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3514249197 | ||
|
|
7f2598d0d6 |
@@ -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}")
|
||||
|
||||
81
resources/shaders/110/texture_displacement_bump.fs
Normal file
81
resources/shaders/110/texture_displacement_bump.fs
Normal 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);
|
||||
}
|
||||
30
resources/shaders/110/texture_displacement_bump.vs
Normal file
30
resources/shaders/110/texture_displacement_bump.vs
Normal 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;
|
||||
}
|
||||
92
resources/shaders/140/texture_displacement_bump.fs
Normal file
92
resources/shaders/140/texture_displacement_bump.fs
Normal 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);
|
||||
}
|
||||
32
resources/shaders/140/texture_displacement_bump.vs
Normal file
32
resources/shaders/140/texture_displacement_bump.vs
Normal 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;
|
||||
}
|
||||
@@ -442,6 +442,8 @@ set(lisbslic3r_sources
|
||||
Tesselate.cpp
|
||||
Tesselate.hpp
|
||||
TextConfiguration.hpp
|
||||
TextureDisplacement.cpp
|
||||
TextureDisplacement.hpp
|
||||
Thread.cpp
|
||||
Thread.hpp
|
||||
Time.cpp
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
226
src/libslic3r/TextureDisplacement.cpp
Normal file
226
src/libslic3r/TextureDisplacement.cpp
Normal 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
|
||||
120
src/libslic3r/TextureDisplacement.hpp
Normal file
120
src/libslic3r/TextureDisplacement.hpp
Normal 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_
|
||||
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -27,7 +27,8 @@ enum class PainterGizmoType {
|
||||
FDM_SUPPORTS,
|
||||
SEAM,
|
||||
MM_SEGMENTATION,
|
||||
FUZZY_SKIN
|
||||
FUZZY_SKIN,
|
||||
TEXTURE_DISPLACEMENT
|
||||
};
|
||||
|
||||
class TriangleSelectorGUI : public TriangleSelector {
|
||||
|
||||
362
src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp
Normal file
362
src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp
Normal 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
|
||||
70
src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp
Normal file
70
src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp
Normal 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_
|
||||
@@ -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 "";
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ public:
|
||||
Seam,
|
||||
FuzzySkin,
|
||||
MmSegmentation,
|
||||
TextureDisplacement,
|
||||
Emboss,
|
||||
Svg,
|
||||
Measure,
|
||||
|
||||
88
src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.cpp
Normal file
88
src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.cpp
Normal 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
|
||||
51
src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.hpp
Normal file
51
src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.hpp
Normal 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_
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
136
tests/libslic3r/test_texture_displacement.cpp
Normal file
136
tests/libslic3r/test_texture_displacement.cpp
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user