Skip to content

Commit

Permalink
Add a minimal displacement map implementation related to feature requ…
Browse files Browse the repository at this point in the history
…est issue prusa3d#8649.
  • Loading branch information
Poikilos committed Dec 24, 2022
1 parent 7eb2113 commit e4726b4
Show file tree
Hide file tree
Showing 11 changed files with 300 additions and 17 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ build-linux/*
deps/build-linux/*
**/.DS_Store
**/.idea/
/deps/
3 changes: 2 additions & 1 deletion src/libslic3r/Layer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
3 changes: 2 additions & 1 deletion src/libslic3r/LayerRegion.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ void LayerRegion::make_perimeters(const SurfaceCollection &slices, SurfaceCollec
// output:
&this->perimeters,
&this->thin_fills,
fill_surfaces
fill_surfaces,
this->layer()->print_z
);

if (this->layer()->lower_layer != nullptr)
Expand Down
243 changes: 233 additions & 10 deletions src/libslic3r/PerimeterGenerator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,21 @@
#include "ClipperUtils.hpp"
#include "ExtrusionEntityCollection.hpp"
#include "ShortestPath.hpp"
#include "PNGReadWrite.hpp"

#include <cmath>
#include <cassert>
#include <fstream>
#include <vector>
// typedef unsigned char Byte;

// Get the correct sleep function:
#ifdef _WIN32
#include <Windows.h>
#else
#include <unistd.h>
#endif
#include <cstdlib>

namespace Slic3r {

Expand Down Expand Up @@ -131,27 +143,213 @@ 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)
bool BackendImage::LoadGreyscalePng(std::string path) {
this->busy = true; // every return (and throw) statement must set this to false.
this->error_shown = false;
// Private since format-specific
// Load data using STL: See <https://stackoverflow.com/a/21802936/4541104>
std::vector <uint8_t> 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;
throw ConfigurationError(
"The fuzzy_skin_displacement_map \""+path+"\" does not exist. Change the path and re-slice to clear the invalid data."
);
// 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<uint8_t>(file),
std::istream_iterator<uint8_t>());
// Now translate the bytes to a png structure if that is proper:
png::ReadBuf rb{static_cast<void*>(vec.data()), fileSize}; // pre-C++11: (void*)&pixels_[0]
if (!png::decode_png(rb, this->image)) {
this->image_path = "";
this->busy = false;
this->error_shown = true;
throw ConfigurationError(
std::string("Error: fuzzy_skin_displacement_map=\"")
+ path + "\" wasn't recognized as an 8-bit Greyscale PNG file. Change the path and re-slice to clear the invalid data."
);
// return false;
}
this->image_path = path;
// this->dump(); // for debug only
this->busy = false;
return true;
}

bool BackendImage::LoadFile(std::string path)
{
double delay = .1;
double total_delay = 0.0;
double delay_timeout = 100; // how many seconds to wait for other thread(s)
while (this->busy) {
// wait for the other thread.
if (total_delay >= delay_timeout) {
std::cerr << "[BackendImage::LoadFile] waiting for other thread(s) timed out." << 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->path() == 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 the caller may throw more than once
// (display more than one error dialog) when the path is
// not blank but this->path() is blank (this->IsOk() is false).
return false;
}
}
// TODO: Support other formats if another headless (non-wx) image loader besides libpng is in the project.
return this->LoadGreyscalePng(path);
}
bool BackendImage::dump() {
std::cerr<<"[BackendImage] \"" << this->path() << "\" (OK:" << (this->IsOk()?"true":"false") << ") dump:" <<std::endl;
if (!this->IsOk()) return false;
for (size_t y=0; y<this->GetHeight(); y++) {
for (size_t x=0; x<this->GetWidth(); x++) {
std::cout << (x==0?"":",") << std::to_string(this->GetRed(x, y));
}
std::cout << std::endl;
}
std::cout << std::endl;
return true;
}
std::string BackendImage::path()
{
return this->image_path;
}
size_t BackendImage::GetWidth()
{
return this->image.cols;
}
size_t BackendImage::GetHeight()
{
return this->image.rows;
}
bool BackendImage::Clamp(size_t& x, size_t& y) {
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;
}
uint8_t BackendImage::GetRed(size_t x, size_t y)
{
this->Clamp(x, y);
return this->image.get(y, x);
}
uint8_t BackendImage::GetGreen(size_t x, size_t y)
{
this->Clamp(x, y);
return this->image.get(y, x);
}
uint8_t BackendImage::GetBlue(size_t x, size_t y)
{
this->Clamp(x, y);
return this->image.get(y, x);
}
bool BackendImage::IsOk()
{
if (this->image_path == "") {
return false;
}
return true;
}
void BackendImage::Destroy()
{
std::vector<uint8_t>().swap(this->image.buf); // Swap buf with a new a tmp vector to reduce the buffer capacity (memory usage).
this->image_path = "";
}

BackendImage displacement_img;

// Thanks Cura developers for this function. PrusaSlicer community member Poikilos implemented displacement_img.
static void fuzzy_polygon(Polygon &poly, double fuzzy_skin_thickness, double fuzzy_skin_point_dist, const double z)
{
// This function must recieve scaled<double>(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<double>(value) for this to work correctly.
double pixel_x = 0.0;
double pixel_y = 0.0;
bool mapped = displacement_img.IsOk(); // Check if the option is being used at all.
if (mapped) {
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 = (double)(((int)(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.
for (Point &p1 : poly.points)
{ // 'a' is the (next) new point between p0 and p1
Vec2d p0p1 = (p1 - *p0).cast<double>();
double p0p1_size = p0p1.norm();
// so that p0p1_size - dist_last_point evaulates 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<double>().normalized() * r).cast<coord_t>());
dist_last_point = p0pa_dist;
if (mapped) {
for (double p0pa_dist = dist_left_over; p0pa_dist < p0p1_size;
p0pa_dist += resolution)
{
pixel_x = (double)((int)(pixel_u+.5) % displacement_img.GetWidth()); // +.5 to round; "Clamp" the texture using the "repeat" method (in graphics terms).
double value = double(displacement_img.GetBlue((int)(pixel_x+.5), (int)(pixel_y+.5))) / 255.0;
double r = (1.0 - value) * (fuzzy_skin_thickness * 2.) - 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).
// Future: Check if loaded as RGB, and in that case, use it as a normal map instead (usefulness of that would need to be determined).
out.emplace_back(*p0 + (p0p1 * (p0pa_dist / p0p1_size) + perp(p0p1).cast<double>().normalized() * r).cast<coord_t>());
dist_last_point = p0pa_dist;
pixel_u = total_dist / fuzzy_skin_point_dist;
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 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<double>().normalized() * r).cast<coord_t>());
dist_last_point = p0pa_dist;
}
}
dist_left_over = p0p1_size - dist_last_point;
p0 = &p1;
Expand All @@ -171,6 +369,31 @@ using PerimeterGeneratorLoops = std::vector<PerimeterGeneratorLoop>;

static ExtrusionEntityCollection traverse_loops(const PerimeterGenerator &perimeter_generator, const PerimeterGeneratorLoops &loops, ThickPolylines &thin_walls)
{
std::string this_displacement_map_path = perimeter_generator.config->fuzzy_skin_displacement_map.value;
if (this_displacement_map_path != displacement_img.path()) {
if (this_displacement_map_path != "") {
displacement_img.LoadFile(this_displacement_map_path);
/*
This check is too strict: It causes large images to fail due to multi-threading issues (probably state of IsOk() when called after delay_timeout).
if (!displacement_img.LoadFile(this_displacement_map_path))
{
// cerr << "[traverse_loops] Error: Loading fuzzy_skin_displacement_map=\""
// << this_displacement_map_path
// << "\" failed." << endl;
throw ConfigurationError(
std::string("Error: fuzzy_skin_displacement_map=\"")
+ this_displacement_map_path + "\" wasn't recognized as an 8-bit Greyscale PNG file. Change the path and re-slice to clear the invalid data."
);
}
else {
cerr << "[traverse_loops] Loaded fuzzy_skin_displacement_map=\"" << this_displacement_map_path << "\"" << std::endl;
}
*/
}
else {
displacement_img.Destroy();
}
}
// loops is an arrayref of ::Loop objects
// turn each one into an ExtrusionLoop object
ExtrusionEntityCollection coll;
Expand All @@ -195,7 +418,7 @@ 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<float>(perimeter_generator.config->fuzzy_skin_thickness.value), scaled<float>(perimeter_generator.config->fuzzy_skin_point_dist.value));
fuzzy_polygon(fuzzified, scaled<double>(perimeter_generator.config->fuzzy_skin_thickness.value), scaled<double>(perimeter_generator.config->fuzzy_skin_point_dist.value), scaled<double>(perimeter_generator.z_of_current_layer));
}
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) &&
Expand Down
48 changes: 45 additions & 3 deletions src/libslic3r/PerimeterGenerator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,47 @@
#include "Polygon.hpp"
#include "PrintConfig.hpp"
#include "SurfaceCollection.hpp"
#include "PNGReadWrite.hpp"

namespace Slic3r {

class BackendImage
{
private:
// PNGDescr* descr;
png::ImageGreyscale image;
std::string image_path;
bool LoadPng(std::string path);
bool Clamp(size_t& x, size_t& y);
bool busy;
bool error_shown; // Another thread already has shown an error so don't try load again (prevent multiple error dialogs).
bool dump(); // dump rgb values as CSV for debugging (HUGE stdout dump)
public:
BackendImage() :
// descr(nullptr),
error_shown(false),
busy(false)
{}
~BackendImage() {
/*
if (descr != nullptr) {
delete descr;
}
*/
}
bool LoadGreyscalePng(std::string path);
bool LoadFile(std::string path);
std::string path();
size_t GetWidth();
size_t GetHeight();

uint8_t GetRed(size_t x, size_t y);
uint8_t GetGreen(size_t x, size_t y);
uint8_t GetBlue(size_t x, size_t y);
bool IsOk();
void Destroy();
};

class PerimeterGenerator {
public:
// Inputs:
Expand All @@ -28,10 +66,11 @@ class PerimeterGenerator {
ExtrusionEntityCollection *loops;
ExtrusionEntityCollection *gap_fill;
SurfaceCollection *fill_surfaces;
const double z_of_current_layer;

PerimeterGenerator(
// Input:
const SurfaceCollection* slices,
const SurfaceCollection* slices,
double layer_height,
Flow flow,
const PrintRegionConfig* config,
Expand All @@ -44,15 +83,18 @@ class PerimeterGenerator {
// Gaps without the thin walls
ExtrusionEntityCollection* gap_fill,
// Infills without the gap fills
SurfaceCollection* fill_surfaces)
SurfaceCollection* fill_surfaces,
double z
)
: 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),
m_spiral_vase(spiral_vase),
m_scaled_resolution(scaled<double>(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();
Expand Down
Loading

0 comments on commit e4726b4

Please sign in to comment.