diff --git a/.gitignore b/.gitignore index 704289a2204..d8716520a91 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ build-linux/* deps/build-linux/* **/.DS_Store **/.idea/ +/deps/ diff --git a/src/libslic3r/Config.cpp b/src/libslic3r/Config.cpp index 2cfb0740ca1..47900cdc717 100644 --- a/src/libslic3r/Config.cpp +++ b/src/libslic3r/Config.cpp @@ -29,6 +29,9 @@ namespace Slic3r { +std::map config_images; +png::BackendPng no_image; // A non-loaded image (IsOK() should always be false) for avoiding returning nullptr. + // Escape \n, \r and backslash std::string escape_string_cstyle(const std::string &str) { @@ -460,6 +463,22 @@ void ConfigBase::apply_only(const ConfigBase &other, const t_config_option_keys } else my_opt->set(other_opt); } + // ConfigOption *path_opt = this->option("fuzzy_skin_displacement_map", false); // tries to use deleted = operator? + // std::string path_opt = "/home/owner/Maps/wall/Bricks076A_1K-JPG/Bricks076A_1K_Displacement-512-RGBA.png"; // this->option("fuzzy_skin_displacement_map", false)->value; + // dynamic_cast(*this->option("fuzzy_skin_displacement_map"))->value; + /* + if (path_opt != nullptr) { + std::string this_displacement_map_path = path_opt; + if (this_displacement_map_path != displacement_img.GetPath()) { + if (this_displacement_map_path != "") { + this->m_displacement_img.LoadFile(this_displacement_map_path); + } + else { + m_displacement_img.Destroy(); + } + } + } + */ } // Are the two configs equal? Ignoring options not present in both configs. diff --git a/src/libslic3r/Config.hpp b/src/libslic3r/Config.hpp index b8c046ceba0..9ddcd64d1d3 100644 --- a/src/libslic3r/Config.hpp +++ b/src/libslic3r/Config.hpp @@ -16,6 +16,7 @@ #include "clonable_ptr.hpp" #include "Exception.hpp" #include "Point.hpp" +#include "PNGReadWrite.hpp" // ideally some kind of cache or interface (IBackendImage or something) would be used instead of BackendPng directly. #include #include @@ -81,6 +82,10 @@ extern bool unescape_strings_cstyle(const std::string &str, std::vector< extern std::string escape_ampersand(const std::string& str); +extern std::map config_images; +// ^ consider: std::map> config_images; // use `new` and pointers but let delete get called automatically +extern png::BackendPng no_image; // A non-loaded image (IsOK() should always be false) for avoiding returning nullptr. + namespace ConfigHelpers { inline bool looks_like_enum_value(std::string value) { @@ -2065,9 +2070,50 @@ class ConfigBase : public ConfigOptionResolver static size_t load_from_gcode_string_legacy(ConfigBase& config, const char* str, ConfigSubstitutionContext& substitutions); + /*! + Get a pointer to the displacement map. The return is always non-null. + If the param is false but the function was not properly called on the main thread + beforehand with true, an exception will be raised (It will appear in the GUI, but it + is an implementation error and should be fixed before release). + + @param opt_key_str The string equivalent to the opt_key that will also be used as the + caching key in config_images. + @param main_thread If true, the call must be made outside of a multithreading context. + In threads should be called with false to avoid multithreading issues, + by forcing an exception if not already loaded. + */ + const png::BackendPng* opt_image(std::string opt_key_str, bool main_thread) const { + // ConfigOption* path_opt = this->option("fuzzy_skin_displacement_map", false); + // if (path_opt == nullptr) { + // return &no_image; + // } + // std::string path = dynamic_cast(path_opt->get()); // incorrect? + const std::string& path = this->opt_string(opt_key_str).empty() + ? std::string("") // print_config_def.get("fuzzy_skin_displacement_map")->get_default_value()->value + : this->opt_string(opt_key_str); + if (path == "") { + return &no_image; + } + + auto pos = config_images.find(path); // std::map::const_iterator pos = + if (pos == config_images.end()) { + if (!main_thread) { + throw ConfigurationError(opt_key_str + " has a new value." + " opt_image(\"" + opt_key_str + "\", true) must" + " be accessed within the main thread first to preload it" + " (This is a problem with implementation not a runtime error)."); + // return nullptr; + } + config_images[path] = png::BackendPng(); + config_images[path].LoadFile(path); + } + // else Some other function should clear config_images cache in case the same image file changed on storage. + return &config_images[path]; + } private: // Set a configuration value from a string. bool set_deserialize_raw(const t_config_option_key& opt_key_src, const std::string& value, ConfigSubstitutionContext& substitutions, bool append); + // png::BackendPng m_displacement_img; //!< This must be loaded from fuzzy_skin_displacement_map (see apply_config; Using a cached image accessed by the option key may be better). }; // Configuration store with dynamic number of configuration values. diff --git a/src/libslic3r/Layer.cpp b/src/libslic3r/Layer.cpp index d273fde9632..0f11682203a 100644 --- a/src/libslic3r/Layer.cpp +++ b/src/libslic3r/Layer.cpp @@ -176,7 +176,8 @@ void Layer::make_perimeters() && config.infill_overlap == other_config.infill_overlap && config.fuzzy_skin == other_config.fuzzy_skin && config.fuzzy_skin_thickness == other_config.fuzzy_skin_thickness - && config.fuzzy_skin_point_dist == other_config.fuzzy_skin_point_dist) + && config.fuzzy_skin_point_dist == other_config.fuzzy_skin_point_dist + && config.fuzzy_skin_displacement_map == other_config.fuzzy_skin_displacement_map) { other_layerm->perimeters.clear(); other_layerm->fills.clear(); diff --git a/src/libslic3r/LayerRegion.cpp b/src/libslic3r/LayerRegion.cpp index fd29d6d54ce..463154486bc 100644 --- a/src/libslic3r/LayerRegion.cpp +++ b/src/libslic3r/LayerRegion.cpp @@ -81,10 +81,12 @@ void LayerRegion::make_perimeters(const SurfaceCollection &slices, SurfaceCollec &slices, this->layer()->height, this->flow(frPerimeter), - ®ion_config, - &this->layer()->object()->config(), + ®ion_config, // PrintRegionConfig* + // &this->layer()->object()->config(), // PrintObjectConfig* + this->layer()->object(), // PrintObject* &print_config, spiral_vase, + this->layer()->print_z, // output: &this->perimeters, diff --git a/src/libslic3r/PNGReadWrite.cpp b/src/libslic3r/PNGReadWrite.cpp index 51bf7de7c3e..9a84c08d07f 100644 --- a/src/libslic3r/PNGReadWrite.cpp +++ b/src/libslic3r/PNGReadWrite.cpp @@ -1,13 +1,28 @@ #include "PNGReadWrite.hpp" +#include "Config.hpp" // for ConfigurationError #include +// Get the correct sleep function: +#ifdef _WIN32 +#include +#else +#include +#endif +#include + #include +#include #include +#include +#include + + #include #include + namespace Slic3r { namespace png { struct PNGDescr { @@ -58,6 +73,406 @@ static void png_read_callback(png_struct *png_ptr, reader->read(static_cast(outBytes), byteCountToRead); } + +bool BackendPng::IsOk() const { + //if (this->image_path == "") { + if (this->m_pixel_size < 1) { + return false; + } + if (this->cols < 1) { + return false; + } + if (this->rows < 1) { + return false; + } + return true; +} + +bool BackendPng::dump() const { + std::cerr<<"[BackendImage] \"" << this->GetPath() << "\" (OK:" << (this->IsOk()?"true":"false") << ") dump:" <IsOk()) return false; + for (size_t y=0; yGetHeight(); y++) { + for (size_t x=0; xGetWidth(); x++) { + if (this->m_color) { + std::cout << (x==0?"":", ") + << std::to_string(this->GetRed(x, y)) << "," + << std::to_string(this->GetGreen(x, y)) << "," + << std::to_string(this->GetBlue(x, y)); + ; + } + else { + std::cout << (x==0?"":",") << std::to_string(this->GetLuma(x, y)); + } + } + std::cout << std::endl; + } + std::cout << std::endl; + return true; +} + +std::string BackendPng::GetPath() const { + return this->image_path; +} + +bool BackendPng::reinitialize(bool force) { + if (this->busy) { + if (!force) { + std::cerr << "[BackendPng::reinitialize] cannot run while busy loading." << std::endl; + return false; + } + // else do not show an error. Assume busy was set by the caller. + } + // Swap each buf with a new a tmp vector to reduce the buffer capacity (memory usage): + // std::vector().swap(this->png->buf); + if (png && info) { + png_destroy_info_struct(png, &info); + this->png = nullptr; + this->info = nullptr; + } + if (png) { + png_destroy_read_struct(&png, nullptr, nullptr); + this->png = nullptr; + } + /* + if (info) { + std::cerr << "[reinitialize] Error: Unexpected png info will be deleted." << std::endl; + delete this->info; + this->info = nullptr; + } + // Don't do this. The compiler says: + // - "invalid use of incomplete type ‘struct png_info_def’" "png.h:484:16: note: forward declaration of ‘struct png_info_def’" + // - "neither the destructor nor the class-specific ‘operator delete’ will be called, even if they are declared when the class is defined" + */ + this->image_path = ""; + this->m_pixel_size = 0; // cause IsOk() to return false. + this->error_shown = false; + this->m_color = false; + return true; +} + +void BackendPng::Destroy() { + this->reinitialize(true); +} + +bool BackendPng::load_png_file(std::string path) { + bool was_busy = this->busy; + if (this->image_path == path) { + // another thread must have already loaded it. + return true; + } + if (was_busy) { + // It is already loading. + return false; + } + this->busy = true; // Set to false before *every* return (or throw, which also requires first setting this->error_shown = true). + + if (path == "") { + this->busy = false; + this->error_shown = true; + throw ConfigurationError( + "The fuzzy_skin_displacement_map is blank but load_png_file was called." + " This is a programming error, not a configuration error." + ); + } + else if ((this->GetPath() != "")) { + // Only reinitialize if the old path isn't "". + this->reinitialize(true); + } + // Private since format-specific + // Load data using STL: See + std::vector vec; // since ImageGreyscale uses template class Image with uint8_t for PxT (pixel type) + std::ifstream file(path, std::ios::binary); + if (file.fail()) { + this->image_path = ""; + this->busy = false; + this->error_shown = true; + throw ConfigurationError( + std::string("The fuzzy_skin_displacement_map \"") + + path + std::string("\" does not exist. Change the path and re-slice to clear the invalid state.") + ); + // return false; + } + file.unsetf(std::ios::skipws); // Do not skip \n + std::streampos fileSize; + file.seekg(0, std::ios::end); + fileSize = file.tellg(); + file.seekg(0, std::ios::beg); + vec.reserve(fileSize); + vec.insert(vec.begin(), + std::istream_iterator(file), + std::istream_iterator()); + // Now translate the bytes to a png structure if that is proper: + png::ReadBuf rb{static_cast(vec.data()), static_cast(fileSize)}; // pre-C++11: (void*)&pixels_[0] + // Note: There is no real problem since fileSize is positive but + // Having no cast results in the compiler warning: "narrowing conversion of + // ‘fileSize.std::fpos<__mbstate_t>::operator std::streamoff()’ + // from ‘std::streamoff’ {aka ‘long int’} to ‘size_t’ {aka ‘long unsigned int’} [-Wnarrowing]" + if (load_png_stream(rb, path, true)) { + // ^ true to keep it busy (Prevent potential multithreading-related crash where busy is false but path is not set. + this->image_path = path; + this->busy = false; + return true; + } + this->busy = false; + return false; +} +bool BackendPng::load_png_stream(IStream &in_buf, std::string optional_stated_path, bool next_busy) { + // optional_stated_path is only for error messages. + this->busy = true; // also set to true by load_png, the usual caller + this->reinitialize(next_busy); + // based on the decode_png from this file + static const constexpr int PNG_SIG_BYTES = 8; + + std::vector sig(PNG_SIG_BYTES, 0); + in_buf.read(sig.data(), PNG_SIG_BYTES); + std::string path_msg = ""; + if (optional_stated_path != "") + path_msg = " \"" + optional_stated_path + "\""; + if (!png_check_sig(sig.data(), PNG_SIG_BYTES)) { + this->error_shown = true; + this->busy = next_busy; + throw ConfigurationError( + std::string("The fuzzy_skin_displacement_map") + + path_msg + std::string(" is not a PNG file. Change the path and re-slice to clear the invalid state.") + ); + // return false; + } + this->png = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + if (!this->png) { + this->error_shown = true; + this->busy = next_busy; + throw ConfigurationError( + std::string("The fuzzy_skin_displacement_map") + + path_msg + std::string(" is not a readable PNG file. Change the path and re-slice to clear the invalid state.") + ); + // return false; + } + this->info = png_create_info_struct(this->png); + if(!this->info) { + this->error_shown = true; + this->busy = next_busy; + throw ConfigurationError( + std::string("The fuzzy_skin_displacement_map") + + path_msg + std::string(" is not a valid PNG file. Change the path and re-slice to clear the invalid state.") + ); + // return false; + } + + png_set_read_fn(this->png, static_cast(&in_buf), png_read_callback); + + // Tell that we have already read the first bytes to check the signature + png_set_sig_bytes(this->png, PNG_SIG_BYTES); + + png_read_info(this->png, this->info); + + this->cols = png_get_image_width(this->png, this->info); + this->rows = png_get_image_height(this->png, this->info); + png_byte color_type = png_get_color_type(this->png, this->info); + png_byte bit_depth = png_get_bit_depth(this->png, this->info); + // ^ png_byte is typedef unsigned char usually, so displaying it + // as a string should use std::to_string. + // ^ png_get_bit_depth gets bits *per channel*! + // Therefore derive m_pixel_size from color_type also: + if (color_type == PNG_COLOR_TYPE_RGBA && bit_depth == 8) { + this->m_pixel_size = 4; + this->m_color = true; + } else if (color_type == PNG_COLOR_TYPE_RGB && bit_depth == 8) { + this->m_pixel_size = 3; + this->m_color = true; + } else if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth == 8) { + this->m_pixel_size = 1; + this->m_color = false; + } + /*else if (color_type == PNG_COLOR_TYPE_GRAY_ALPHA && bit_depth == 8) { + this->m_pixel_size = 2; + this->m_color = false; + } + */ + // ^ TODO: Allow GRAY_ALPHA somehow (causes sequential buffer overflow + // or "libpng error: IDAT: invalid window size (libpng)" to be thrown by libpng on load. + + if (this->m_pixel_size == 0) { + this->error_shown = true; + std::string type_msg = get_type_message(color_type); + + this->busy = next_busy; + throw ConfigurationError( + std::string("The fuzzy_skin_displacement_map") + + path_msg + std::string(" is not a grayscale/truecolor PNG file.") + + std::string(" The image is ") + std::to_string(static_cast(bit_depth)) + + std::string("bpc") + type_msg + + std::string(". Change the path and re-slice to clear the invalid state.") + ); + // return false; + } + + this->m_stride = m_pixel_size * this->cols; + + this->buf.resize(this->rows * this->cols * this->m_pixel_size); + + auto readbuf = static_cast(this->buf.data()); + for (size_t r = 0; r < this->rows; ++r) + png_read_row(this->png, readbuf + r * this->m_stride, nullptr); + if (this->GetWidth() * this->GetHeight() <= 16) this->dump(); // for debug only + this->busy = false; + return true; +} + +bool BackendPng::load_png_stream(const ReadBuf &in_buf, std::string optional_stated_path, bool next_busy) { + // based on template bool decode_png(const ReadBuf &in_buf, Img &out_img) from PNGReadWrite.hpp + struct ReadBufStream: public IStream { + const ReadBuf &rbuf_ref; size_t pos = 0; + + explicit ReadBufStream(const ReadBuf &buf): rbuf_ref{buf} {} + + size_t read(std::uint8_t *outp, size_t amount) override + { + if (amount > rbuf_ref.sz - pos) return 0; + + auto buf = static_cast(rbuf_ref.buf); + std::copy(buf + pos, buf + (pos + amount), outp); + pos += amount; + + return amount; + } + + bool is_ok() const override { return pos < rbuf_ref.sz; } + } stream{in_buf}; + return this->load_png_stream(stream, optional_stated_path, next_busy); +} + +bool BackendPng::LoadFile(std::string path) { + if (this->image_path == path) { + // another thread must have already loaded it. + return true; + } + double delay = .25; + double total_delay = 0.0; + + double delay_timeout = 120; + // ^ How many seconds to wait for other thread(s)...OR milliseconds depending on sleep function used?? + // - Values below 60 cause fuzz in images >= 1024x1024 due to not loaded yet on i7-12000F on WD SSD. + // TODO: Adjust if milliseconds not seconds (See comment above)? + + while (this->busy) { + // wait for the other thread. + if (this->error_shown) { + return false; // Another thread already failed to load the image. + } + if (total_delay >= delay_timeout) { + std::cerr << "[BackendImage::LoadFile] waiting for other thread(s) timed out." + " To avoid this, implement caching (for example, see config_images)" + " and use the main thread only (for example, see image_opt)." << std::endl; + // FIXME: Find a way to avoid IsOK() is false after this if the image was still loading in another thread and will have succeeded. + break; + } + sleep(delay); + total_delay += delay; + } + if (total_delay > 0.0) { + if (this->GetPath() == path) { + // Another thread already loaded the correct image. + return true; + } + else if (this->IsOk()) { + // Another thread probably already loaded the correct image. + return true; + } + else if (this->error_shown) { + // Assume another thread already failed, + // otherwise a load_ method may throw more than once + // (display more than one error dialog) when the path is + // not blank but this->GetPath() is blank (this->IsOk() is false) + // and there is more than one thread that may call load on the + // same BackendPng. + return false; + } + std::cerr << "[BackendPng::LoadFile] The thread is in an unknown state." << std::endl; + // return true; + } + // TODO: Support other formats if another headless (non-wx) image loader besides libpng is in the project. + return this->load_png_file(path); +} + + +size_t BackendPng::GetWidth() const { + return this->cols; +} + +size_t BackendPng::GetHeight() const { + return this->rows; +} + +bool BackendPng::clamp(size_t& x, size_t& y) const { + bool was_in_bounds = true; + if (x >= this->GetWidth()) { + was_in_bounds = false; + x %= this->GetWidth(); + } + if (y >= this->GetHeight()) { + was_in_bounds = false; + y %= this->GetHeight(); + } + return was_in_bounds; +} + +std::string BackendPng::get_type_message(png_byte color_type) const { + std::string type_msg = ""; + if (color_type == PNG_COLOR_TYPE_PALETTE) { + type_msg = " with indexed color"; + } else if (color_type == PNG_COLOR_TYPE_GRAY) { + type_msg = " greyscale"; + } else if (color_type == PNG_COLOR_TYPE_RGB) { + type_msg = " RGB"; + } else if (color_type == PNG_COLOR_TYPE_RGBA) { + type_msg = " RGBA"; + } else if (color_type == PNG_COLOR_TYPE_GA) { + type_msg = " greyscale+alpha"; + } else { + type_msg = "type " + std::to_string(static_cast(color_type)); + } + return type_msg; +} + +uint8_t BackendPng::GetRed(size_t x, size_t y) const { + // PNG stores each pixel in RGBA order unless png_set_bgr is called, + // or png_set_swap_alpha is called to move A to beginning + // (See ). + this->clamp(x, y); + return buf[y * this->m_stride + x * this->m_pixel_size]; +} + +uint8_t BackendPng::GetGreen(size_t x, size_t y) const { + this->clamp(x, y); + if (!this->m_color) + return buf[y * this->m_stride + x * this->m_pixel_size]; + return buf[y * this->m_stride + x * this->m_pixel_size + 1]; +} + +uint8_t BackendPng::GetBlue(size_t x, size_t y) const { + this->clamp(x, y); + if (!this->m_color) + return buf[y * this->m_stride + x * this->m_pixel_size]; + return buf[y * this->m_stride + x * this->m_pixel_size + 2]; +} + +uint8_t BackendPng::GetLuma(size_t x, size_t y) const { + this->clamp(x, y); + if (this->m_pixel_size < 3) { // GRAY or GRAY_ALPHA + return buf[y * this->m_stride + x * this->m_pixel_size]; + } + size_t start = y * this->m_stride + x * this->m_pixel_size; + // To get something close to perceptible luminance, use the multipliers from + // Rec. 709 such as used in HDTV. The multipliers total 1.0, so no additional + // math is necessary to convert back to a byte range. + return static_cast( + static_cast(buf[start]) * .2126f // R + + static_cast(buf[start+1]) * .7152f // G + + static_cast(buf[start+2]) * .0722f // B + ); +} + + bool decode_png(IStream &in_buf, ImageGreyscale &out_img) { static const constexpr int PNG_SIG_BYTES = 8; diff --git a/src/libslic3r/PNGReadWrite.hpp b/src/libslic3r/PNGReadWrite.hpp index 01e1f474500..308fad04ba6 100644 --- a/src/libslic3r/PNGReadWrite.hpp +++ b/src/libslic3r/PNGReadWrite.hpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace Slic3r { namespace png { @@ -28,7 +29,7 @@ using ImageGreyscale = Image; // TODO (if needed): implement transformation of rgb images into grayscale... bool decode_png(IStream &stream, ImageGreyscale &out_img); -// TODO (if needed) +// Use BackendPng instead. Image gets messy to load since static_cast(...) on this doesn't work. // struct RGB { uint8_t r, g, b; }; // using ImageRGB = Image; // bool decode_png(IStream &stream, ImageRGB &img); @@ -39,6 +40,54 @@ struct ReadBuf { const void *buf = nullptr; const size_t sz = 0; }; bool is_png(const ReadBuf &pngbuf); +/*! +Implement a drop-in replacement, in most or all use cases, for wxImage but for backend use. +This class is based on the decode_png and elements from Image implementations, both from PNGReadWrite.*. +If this class is extended, it should imitate wxImage as much as possible for consistency between GUI and +CLI code. Otherwise an interface should be created and this would be the backend implementation of that. +*/ +class BackendPng { +private: + png_struct *png = nullptr; + png_info *info = nullptr; + std::string image_path; + size_t m_pixel_size = 0; + size_t m_stride; + size_t cols; + size_t rows; + bool m_color; + bool error_shown = false; + bool busy = false; + std::vector buf; + bool reinitialize(bool force); + bool load_png_file(std::string path); + bool load_png_stream(IStream &in_buf, std::string optional_stated_path, bool next_busy); + bool load_png_stream(const ReadBuf& in_buff, std::string optional_stated_path, bool next_busy); + bool clamp(size_t& x, size_t& y) const; + std::string get_type_message(png_byte color_type) const; + bool dump() const; +public: + BackendPng() = default; + BackendPng(const BackendPng&) = delete; + // (BackendPng&&) = delete; + // BackendPng& operator=(const BackendPng&) = delete; + // BackendPng& operator=(BackendPng&&) = delete; + void Destroy(); + ~BackendPng() + { + this->Destroy(); + } + bool IsOk() const; + std::string GetPath() const; + bool LoadFile(std::string path); + size_t GetWidth() const; + size_t GetHeight() const; + uint8_t GetRed(size_t x, size_t y) const; + uint8_t GetGreen(size_t x, size_t y) const; + uint8_t GetBlue(size_t x, size_t y) const; + uint8_t GetLuma(size_t x, size_t y) const; +}; + template bool decode_png(const ReadBuf &in_buf, Img &out_img) { struct ReadBufStream: public IStream { diff --git a/src/libslic3r/PerimeterGenerator.cpp b/src/libslic3r/PerimeterGenerator.cpp index 01d3c592a69..8553625081d 100644 --- a/src/libslic3r/PerimeterGenerator.cpp +++ b/src/libslic3r/PerimeterGenerator.cpp @@ -2,9 +2,20 @@ #include "ClipperUtils.hpp" #include "ExtrusionEntityCollection.hpp" #include "ShortestPath.hpp" +#include "PNGReadWrite.hpp" +#include "Print.hpp" // PrintObject declaration etc #include #include +// typedef unsigned char Byte; + +// Get the correct sleep function: +#ifdef _WIN32 +#include +#else +#include +#endif +#include namespace Slic3r { @@ -131,27 +142,160 @@ class PerimeterGeneratorLoop { bool is_internal_contour() const; }; -// Thanks Cura developers for this function. -static void fuzzy_polygon(Polygon &poly, double fuzzy_skin_thickness, double fuzzy_skin_point_dist) +static inline double surface_offset(double offset, double max_offset, double fuzzy_skin_thickness) { + return (offset) * (fuzzy_skin_thickness * 2.) / max_offset - fuzzy_skin_thickness; +} + +/*! +Map a surface point to a 2d U (horizontal) value on a texture map, but expressed in millimeters due to the value's usage in PrusaSlicer. +The side matters since the texture should be wrapped around the whole object, not just one side, starting with the left, +to match the behavior of cube maps as used in the graphics field. +To reduce the number of calculations, the U value is not conformed to 0.0 to 1.0 like usual U values. +It is not 3D cube mapping but it will work for any point that is neither on the top nor bottom +(It would work but visibly behave like square mapping rather than cube mapping in those cases). + +@param center The center of the bounding_box, pre-calculated for caching or customization. +@param bounding_box The extents of the model from a top view. +@param flat_point The surface point from a top view (variance in depth relative to center only matters if it puts the point in a different side). +@param normal_radians The normal of the flat_point expressed in radians from the top view (X-Y plane). +*/ +static inline double cubemap_side_u(const Point& center, const BoundingBox& bounding_box, const Point& flat_point, const double normal_radians) { + // double normal_radians = atan2(flat_point.y() - center.y(), flat_point.x() - center.x()); + double angle_deg = normal_radians * 180. / 3.14159; + // -180 < angle_deg <= 180. + // int side = 0; + double previous_sides_total_length = 0.0; + double relative_offset; + if (angle_deg > 135. || angle_deg <= -135.) { // left (side 0) + relative_offset = bounding_box.size().y() - (flat_point.y() - bounding_box.min.y()); + // ^ inverse since the left side *of* the left side is at the back, which has a larger y value than the front. + } else if (angle_deg < -45.) { // front (2nd side in cube mapping) + // side = 1; + previous_sides_total_length = bounding_box.size().y(); // left's length is its y size. + relative_offset = flat_point.x() - bounding_box.min.x(); + } + else if (angle_deg <= 45.) { // right + // side = 2; + previous_sides_total_length = + bounding_box.size().y() // left's length is its y size. + + bounding_box.size().x() // front's length is its x size (width). + ; + relative_offset = flat_point.y() - bounding_box.min.y(); + } else { // (angle_deg > 45. && angle_deg <= 135) { // back + // side = 3; + previous_sides_total_length = + bounding_box.size().y() * 2. // length of left + right (same, so * 2) + + bounding_box.size().x() // front's length is its x size (width). + ; + relative_offset = bounding_box.size().x() - (flat_point.x() - bounding_box.min.x()); + // ^ inverse since the left side *of* the back is the right. + } + return previous_sides_total_length + relative_offset; +} + +/*! +\brief Create a fuzzy polygon from an existing polygon. + +Create a fuzzy polygon from an existing polygon. If a displacement map is used +(object->config().displacement_img().IsOk()), the spacing is fuzzy_skin_point_dist per pixel. +In that case z is used with fuzzy_skin_point_dist to determine the y pixel in the displacement map. +Otherwise, distance between points gets a random change of +- 1/4 and z is ignored. + +Thanks Cura developers for this function. PrusaSlicer community member Poikilos initially implemented displacement_img and cube mapping. + +@param poly The original polygon (a single perimeter). +@param fuzzy_skin_thickness The maximum random amount to affect the depth of the surface (total of in or out change). +@param z The location of the perimeter starting from the bottom of the model (used only for mapping). +@param object The PrintObject* used to access the bounding_box. Both that and displacement_img are used for mapping, but only if + displacement_img->IsOk() (loaded/unloaded from fuzzy_skin_displacement_map path by apply_config). +@param displacement_img The map used when fuzzy map is enabled in PrintRegionConfig (not in PrintObjectConfig in object) + requiring this extra argument in every deeper call. +*/ +static void fuzzy_polygon(Polygon &poly, double fuzzy_skin_thickness, double fuzzy_skin_point_dist, const double z, const PrintObject* object, const png::BackendPng* displacement_img) { + // This function must recieve scaled(value) for each double argument, such as to improve float accuracy. const double min_dist_between_points = fuzzy_skin_point_dist * 3. / 4.; // hardcoded: the point distance may vary between 3/4 and 5/4 the supplied value const double range_random_point_dist = fuzzy_skin_point_dist / 2.; - double dist_left_over = double(rand()) * (min_dist_between_points / 2) / double(RAND_MAX); // the distance to be traversed on the line before making the first new point + double dist_left_over = 0.0; // randomized below if there is no displacement map to align. Point* p0 = &poly.points.back(); Points out; out.reserve(poly.points.size()); + // In graphics terms, U wraps around the layer (and is mapped to the X-axis of the image). + // In PrusaSlicer the vertical axis is Z, but when working with images the vertical axis is V (mapped to Y in the image). + // Normally U and V are from 0 to 1, but in this case they are pixel locations (from 0 to width or height minus 1). + // TODO: Offset U such that the left edge of the image is at a good seam (probably point closest to 0,0, or + // in other words the front left corner, since the seam matters most in the case of box-like things like miniature buildings). + double pixel_u = 0.0; + double resolution = fuzzy_skin_point_dist; + double pixel_v = z / fuzzy_skin_point_dist; // Match v and u scale to fix y to x proportions (Each fuzzy_skin_point_dist spans 1 pixel on x). + // ^ Note: z and fuzzy_skin_point_dist must both have been scaled using scale(value) for this to work correctly. + double pixel_x = 0.0; + double pixel_y = 0.0; + BoundingBox bounding_box; + bool mapped = (displacement_img != nullptr) && displacement_img->IsOk(); // Check if the option is being used at all. + if (object == nullptr) { + if (mapped) { + mapped = false; + std::cerr << "Error: object is nullptr but displacement_img IsOk. This should never happen" + " (and object should only be nullptr in test(s)), and mapping has been set back to false." << std::endl; + } + } + + Point center; + if (mapped) { + // Only access `object` if mapped (or tests that set object to nullptr will cause an exception). + bounding_box = object->bounding_box(); + center = bounding_box.center(); + resolution = fuzzy_skin_point_dist; // A lower value can sharpen edges, but if a 1024x1024 image uses too high of a divisor (like 32 for a 200mm high model using 16MB RAM) the program will have increased slicing time by 32x (and likely have an OOM crash)! + pixel_y = pixel_v; + pixel_y = static_cast((static_cast(pixel_v+.5)) % displacement_img->GetHeight()); // +.5 to round; "Clamp" the texture using the "repeat" method (in graphics terms). + pixel_y = displacement_img->GetHeight() - pixel_y; // Flip it so the bottom pixel (GetHeight()-1) is at the first layer(s) (z=~0) of the print. + // std::cerr << "z=" << z << " / fuzzy_skin_point_dist=" << fuzzy_skin_point_dist << " and mapped becomes " << pixel_y << "" << std::endl; // debug only (and messy since multithreaded) + } + else { + dist_left_over = double(rand()) * (min_dist_between_points / 2.) / double(RAND_MAX); // the distance to be traversed on the line before making the first new point + } + double total_dist = 0.0; // Keep track of total travel for displacement_img mapping. + const double rand_max_d = double(RAND_MAX); for (Point &p1 : poly.points) { // 'a' is the (next) new point between p0 and p1 Vec2d p0p1 = (p1 - *p0).cast(); double p0p1_size = p0p1.norm(); - // so that p0p1_size - dist_last_point evaulates to dist_left_over - p0p1_size + // so that p0p1_size - dist_last_point evaluates to dist_left_over - p0p1_size double dist_last_point = dist_left_over + p0p1_size * 2.; - for (double p0pa_dist = dist_left_over; p0pa_dist < p0p1_size; - p0pa_dist += min_dist_between_points + double(rand()) * range_random_point_dist / double(RAND_MAX)) - { - double r = double(rand()) * (fuzzy_skin_thickness * 2.) / double(RAND_MAX) - fuzzy_skin_thickness; - out.emplace_back(*p0 + (p0p1 * (p0pa_dist / p0p1_size) + perp(p0p1).cast().normalized() * r).cast()); - dist_last_point = p0pa_dist; + if (mapped) { + for (double p0pa_dist = dist_left_over; p0pa_dist < p0p1_size; + p0pa_dist += resolution) + { + // First get the side for cubic mapping. + // a. Get the flat (non-fuzzy, or .5 offset) point first, to determine the 2D in-between point for mapping. + double radius = surface_offset(.5, 1.0, fuzzy_skin_thickness); // The initial value is a "neutral" radius, *only* for calculating flat_point. + Point flat_point = *p0 + (p0p1 * (p0pa_dist / p0p1_size) + perp(p0p1).cast().normalized() * radius).cast(); + Point normal_point = *p0 + (p0p1 * (p0pa_dist / p0p1_size) + perp(p0p1).cast().normalized() * (radius+1.0)).cast(); + // ^ should be the same math as below. + double normal_radians = atan2(normal_point.y() - flat_point.y(), normal_point.x() - flat_point.x()); + // b. determine the face: + pixel_u = cubemap_side_u(center, bounding_box, flat_point, normal_radians) / fuzzy_skin_point_dist; + pixel_x = (double)((int)(pixel_u+.5) % displacement_img->GetWidth()); // +.5 to round; "Clamp" the texture using the "repeat" method (in graphics terms). + radius = surface_offset(255.0 - double(displacement_img->GetLuma((int)(pixel_x+.5), (int)(pixel_y+.5))), 255.0, fuzzy_skin_thickness); + // ^ Adding +.5 before casting to int is effectively the same as rounding. + // ^ Using GetBlue, GetRed or GetGreen doesn't matter since the image should have been loaded as gray. + // ^ max is 255 since it is image data (assumes 1 byte per channel as is expected in most cases including all known wx cases). + // ^ inverse (255 - luma) due to existing behavior of surface_offset (code was moved to there unmodified from 'else' case below). + out.emplace_back(*p0 + (p0p1 * (p0pa_dist / p0p1_size) + perp(p0p1).cast().normalized() * radius).cast()); + dist_last_point = p0pa_dist; + // pixel_u = total_dist / fuzzy_skin_point_dist; // This would always start at the seam, which is only reliable in models with double two-way symmetry where seam is "Aligned". + total_dist += resolution; + } + } + else { + for (double p0pa_dist = dist_left_over; p0pa_dist < p0p1_size; + p0pa_dist += min_dist_between_points + double(rand()) * range_random_point_dist / double(RAND_MAX)) + { + double radius = surface_offset(rand(), rand_max_d, fuzzy_skin_thickness); + out.emplace_back(*p0 + (p0p1 * (p0pa_dist / p0p1_size) + perp(p0p1).cast().normalized() * radius).cast()); + dist_last_point = p0pa_dist; + } } dist_left_over = p0p1_size - dist_last_point; p0 = &p1; @@ -169,7 +313,7 @@ static void fuzzy_polygon(Polygon &poly, double fuzzy_skin_thickness, double fuz using PerimeterGeneratorLoops = std::vector; -static ExtrusionEntityCollection traverse_loops(const PerimeterGenerator &perimeter_generator, const PerimeterGeneratorLoops &loops, ThickPolylines &thin_walls) +static ExtrusionEntityCollection traverse_loops(const PerimeterGenerator &perimeter_generator, const PerimeterGeneratorLoops &loops, ThickPolylines &thin_walls, const PrintObject* object) { // loops is an arrayref of ::Loop objects // turn each one into an ExtrusionLoop object @@ -195,7 +339,14 @@ static ExtrusionEntityCollection traverse_loops(const PerimeterGenerator &perime const Polygon &polygon = loop.fuzzify ? fuzzified : loop.polygon; if (loop.fuzzify) { fuzzified = loop.polygon; - fuzzy_polygon(fuzzified, scaled(perimeter_generator.config->fuzzy_skin_thickness.value), scaled(perimeter_generator.config->fuzzy_skin_point_dist.value)); + fuzzy_polygon( + fuzzified, + scaled(perimeter_generator.config->fuzzy_skin_thickness.value), + scaled(perimeter_generator.config->fuzzy_skin_point_dist.value), + scaled(perimeter_generator.z_of_current_layer), + object, + perimeter_generator.config->opt_image("fuzzy_skin_displacement_map", false) + ); } if (perimeter_generator.config->overhangs && perimeter_generator.layer_id > perimeter_generator.object_config->raft_layers && ! ((perimeter_generator.object_config->support_material || perimeter_generator.object_config->support_material_enforce_layers > 0) && @@ -257,7 +408,7 @@ static ExtrusionEntityCollection traverse_loops(const PerimeterGenerator &perime } else { const PerimeterGeneratorLoop &loop = loops[idx.first]; assert(thin_walls.empty()); - ExtrusionEntityCollection children = traverse_loops(perimeter_generator, loop.children, thin_walls); + ExtrusionEntityCollection children = traverse_loops(perimeter_generator, loop.children, thin_walls, object); out.entities.reserve(out.entities.size() + children.entities.size() + 1); ExtrusionLoop *eloop = static_cast(coll.entities[idx.first]); coll.entities[idx.first] = nullptr; @@ -317,8 +468,10 @@ void PerimeterGenerator::process() m_lower_slices_polygons = offset(*this->lower_slices, float(scale_(+nozzle_diameter/2))); } - // we need to process each island separately because we might have different - // extra perimeters for each one + // We need to process each island separately because we might have different + // extra perimeters for each one. + // - this->object should only be accessed if a map is used, otherwise test(s) must be modified + // to generate an object and ensure `object` param of the PerimeterGenerator constructor is not nullptr. for (const Surface &surface : this->slices->surfaces) { // detect how many perimeters must be generated for this island int loop_number = this->config->perimeters + surface.extra_perimeters - 1; // 0-indexed loops @@ -476,7 +629,7 @@ void PerimeterGenerator::process() } } // at this point, all loops should be in contours[0] - ExtrusionEntityCollection entities = traverse_loops(*this, contours.front(), thin_walls); + ExtrusionEntityCollection entities = traverse_loops(*this, contours.front(), thin_walls, this->object); // if brim will be printed, reverse the order of perimeters so that // we continue inwards after having finished the brim // TODO: add test for perimeter order diff --git a/src/libslic3r/PerimeterGenerator.hpp b/src/libslic3r/PerimeterGenerator.hpp index 0b3501d364f..9ce31648529 100644 --- a/src/libslic3r/PerimeterGenerator.hpp +++ b/src/libslic3r/PerimeterGenerator.hpp @@ -7,6 +7,8 @@ #include "Polygon.hpp" #include "PrintConfig.hpp" #include "SurfaceCollection.hpp" +#include "PNGReadWrite.hpp" +#include "Print.hpp" // PrintObject declaration etc namespace Slic3r { @@ -22,37 +24,46 @@ class PerimeterGenerator { Flow overhang_flow; Flow solid_infill_flow; const PrintRegionConfig *config; - const PrintObjectConfig *object_config; + const PrintObjectConfig *object_config; // Deprecate since object is here now (Would require refactoring in several files)? + const PrintObject *object; const PrintConfig *print_config; // Outputs: ExtrusionEntityCollection *loops; ExtrusionEntityCollection *gap_fill; SurfaceCollection *fill_surfaces; + const double z_of_current_layer; - PerimeterGenerator( + PerimeterGenerator( // also used in test_perimeters // Input: - const SurfaceCollection* slices, + const SurfaceCollection* slices, double layer_height, Flow flow, const PrintRegionConfig* config, - const PrintObjectConfig* object_config, + // const PrintObjectConfig* object_config, + const PrintObject* object, // must not be null if map such as fuzzy_skin_displacement_map is used const PrintConfig* print_config, const bool spiral_vase, + double z, + // Output: // Loops with the external thin walls ExtrusionEntityCollection* loops, // Gaps without the thin walls ExtrusionEntityCollection* gap_fill, // Infills without the gap fills - SurfaceCollection* fill_surfaces) + SurfaceCollection* fill_surfaces + ) : slices(slices), lower_slices(nullptr), layer_height(layer_height), layer_id(-1), perimeter_flow(flow), ext_perimeter_flow(flow), overhang_flow(flow), solid_infill_flow(flow), - config(config), object_config(object_config), print_config(print_config), + config(config), print_config(print_config), + object_config(&object->config()), + object(object), m_spiral_vase(spiral_vase), m_scaled_resolution(scaled(print_config->gcode_resolution.value)), loops(loops), gap_fill(gap_fill), fill_surfaces(fill_surfaces), - m_ext_mm3_per_mm(-1), m_mm3_per_mm(-1), m_mm3_per_mm_overhang(-1) + m_ext_mm3_per_mm(-1), m_mm3_per_mm(-1), m_mm3_per_mm_overhang(-1), + z_of_current_layer(z) {} void process(); diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index f171cb14dd1..7d9daaced46 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -426,7 +426,7 @@ static std::vector s_Preset_print_options { "solid_infill_below_area", "only_retract_when_crossing_perimeters", "infill_first", "ironing", "ironing_type", "ironing_flowrate", "ironing_speed", "ironing_spacing", "max_print_speed", "max_volumetric_speed", "avoid_crossing_perimeters_max_detour", - "fuzzy_skin", "fuzzy_skin_thickness", "fuzzy_skin_point_dist", + "fuzzy_skin", "fuzzy_skin_thickness", "fuzzy_skin_point_dist", "fuzzy_skin_displacement_map", #ifdef HAS_PRESSURE_EQUALIZER "max_volumetric_extrusion_rate_slope_positive", "max_volumetric_extrusion_rate_slope_negative", #endif /* HAS_PRESSURE_EQUALIZER */ diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index b21ed563180..7190da03f7a 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -1302,6 +1302,17 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(0.8)); + def = this->add("fuzzy_skin_displacement_map", coString); + def->label = L("Fuzzy skin displacement map"); + def->category = L("Fuzzy Skin"); + def->tooltip = L("The fuzzy skin value will be from the mapped pixel brightness rather than random. " + "The mapping is similar to cylindrical mapping within the scope of each perimeter, " + "but the pixel offset is constant (based on fuzzy_skin_point_dist) " + "to avoid changes in image scale along the Z axis."); + def->sidetext = L("PNG file path (If used, point distance is mm per pixel where small decimals on big models may use more than 16G RAM)."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionString()); + def = this->add("gap_fill_enabled", coBool); def->label = L("Fill gaps"); def->category = L("Layers and Perimeters"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 0c1060b7de6..514a84aaa03 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -557,6 +557,7 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionEnum, fuzzy_skin)) ((ConfigOptionFloat, fuzzy_skin_thickness)) ((ConfigOptionFloat, fuzzy_skin_point_dist)) + ((ConfigOptionString, fuzzy_skin_displacement_map)) ((ConfigOptionBool, gap_fill_enabled)) ((ConfigOptionFloat, gap_fill_speed)) ((ConfigOptionFloatOrPercent, infill_anchor)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index eeaf1b13cf6..d9040fa88f8 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -147,6 +147,8 @@ void PrintObject::make_perimeters() if (! region.config().extra_perimeters || region.config().perimeters == 0 || region.config().fill_density == 0 || this->layer_count() < 2) continue; + //const png::BackendPng* tmp = + region.config().opt_image("fuzzy_skin_displacement_map", true); // Preload the image in each region before threading (opt_image caches 1 per path). BOOST_LOG_TRIVIAL(debug) << "Generating extra perimeters for region " << region_id << " in parallel - start"; tbb::parallel_for( tbb::blocked_range(0, m_layers.size() - 1), @@ -648,6 +650,7 @@ bool PrintObject::invalidate_state_by_config_options( || opt_key == "fuzzy_skin" || opt_key == "fuzzy_skin_thickness" || opt_key == "fuzzy_skin_point_dist" + || opt_key == "fuzzy_skin_displacement_map" || opt_key == "overhangs" || opt_key == "thin_walls" || opt_key == "thick_bridges") { diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 96151d8c7ff..af50fd1172b 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1456,6 +1456,7 @@ void TabPrint::build() optgroup->append_single_option_line("fuzzy_skin", category_path + "fuzzy-skin-type"); optgroup->append_single_option_line("fuzzy_skin_thickness", category_path + "fuzzy-skin-thickness"); optgroup->append_single_option_line("fuzzy_skin_point_dist", category_path + "fuzzy-skin-point-distance"); + optgroup->append_single_option_line("fuzzy_skin_displacement_map", category_path + "fuzzy-skin-displacement-map"); page = add_options_page(L("Infill"), "infill"); category_path = "infill_42#"; diff --git a/tests/fff_print/test_perimeters.cpp b/tests/fff_print/test_perimeters.cpp index a3f11113aa0..2d3286bced3 100644 --- a/tests/fff_print/test_perimeters.cpp +++ b/tests/fff_print/test_perimeters.cpp @@ -37,6 +37,7 @@ SCENARIO("Perimeter nesting", "[Perimeters]") }; FullPrintConfig config; + // PrintObject* object_ptr = new PrintObject(...); auto test = [&config](const TestData &data) { SurfaceCollection slices; @@ -45,14 +46,17 @@ SCENARIO("Perimeter nesting", "[Perimeters]") ExtrusionEntityCollection loops; ExtrusionEntityCollection gap_fill; SurfaceCollection fill_surfaces; + double z = 0.0; PerimeterGenerator perimeter_generator( &slices, 1., // layer height Flow(1., 1., 1.), static_cast(&config), - static_cast(&config), + // static_cast(&config), + nullptr, // object_ptr, // PrintObject*; nullptr is ok only if no map (such as fuzzy_skin_displacement_map) since maps require bounding_box for mapping. static_cast(&config), false, // spiral_vase + z, // output: &loops, &gap_fill, &fill_surfaces); perimeter_generator.process();