mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-12 10:21:17 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56b559d36a |
@@ -72,17 +72,21 @@ varying float weight;
|
||||
varying float island_active;
|
||||
varying vec2 vertex_uv;
|
||||
|
||||
// The cylinder's own frame, built exactly as libslic3r's project_cylindrical() builds it - including
|
||||
// the handedness, which comes out left-handed for an axis of +Z. Copied rather than "corrected", so
|
||||
// the preview wraps the texture the same way round as the bake.
|
||||
// The cylinder's own frame, built exactly as libslic3r's project_cylindrical() builds it - right
|
||||
// handed, so the angle taken from it grows anticlockwise about the axis, which seen from outside the
|
||||
// cylinder is to the viewer's right rather than mirrored.
|
||||
void cylinder_frame(out vec3 up, out vec3 right, out vec3 fwd)
|
||||
{
|
||||
up = (length(patch_axis) > 1e-8) ? normalize(patch_axis) : vec3(0.0, 0.0, 1.0);
|
||||
vec3 arbitrary = (abs(up.z) < 0.9) ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
|
||||
right = normalize(cross(up, arbitrary));
|
||||
fwd = normalize(cross(right, up));
|
||||
fwd = normalize(cross(up, right));
|
||||
}
|
||||
|
||||
// Which side of a world plane a face is on, with zero counting as the positive side. Not GLSL's own
|
||||
// sign(), which returns 0 there and would collapse the coordinate onto a line.
|
||||
float box_face_sign(float n) { return n >= 0.0 ? 1.0 : -1.0; }
|
||||
|
||||
// The raw, millimetre-valued projection of `p` (a position in the texture frame), before the layer's
|
||||
// tiling/rotation/aspect/offset - a term-for-term transcription of libslic3r's project_planar(),
|
||||
// project_cylindrical() and project_spherical(). `n` is read by the planar mode only.
|
||||
@@ -94,7 +98,7 @@ vec2 projection_raw(vec3 p, vec3 n)
|
||||
vec3 rel = p - patch_center;
|
||||
float x = dot(rel, right);
|
||||
float y = dot(rel, fwd);
|
||||
return vec2(atan(y, x) * sqrt(x * x + y * y), dot(rel, up));
|
||||
return vec2(atan(y, x) * sqrt(x * x + y * y), -dot(rel, up));
|
||||
}
|
||||
if (projection_mode == 2) { // Spherical: (longitude, latitude) * radius
|
||||
vec3 rel = p - patch_center;
|
||||
@@ -102,10 +106,18 @@ vec2 projection_raw(vec3 p, vec3 n)
|
||||
if (radius < 1e-8)
|
||||
return vec2(0.0);
|
||||
vec3 dir = rel / radius;
|
||||
return vec2(atan(dir.y, dir.x), asin(clamp(dir.z, -1.0, 1.0))) * radius;
|
||||
return vec2(atan(dir.y, dir.x), -asin(clamp(dir.z, -1.0, 1.0))) * radius;
|
||||
}
|
||||
vec3 an = abs(n); // Triplanar: drop the dominant normal axis
|
||||
return (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy);
|
||||
// Triplanar: drop the dominant normal axis. v is negated so the image's first pixel row (v = 0)
|
||||
// sits at the top of the model instead of upside down, and u is flipped on the far side of each
|
||||
// plane so the two faces sharing it are not mirror images. libslic3r's box_plane_yz() and its two
|
||||
// siblings, term for term.
|
||||
vec3 an = abs(n);
|
||||
if (an.x >= an.y && an.x >= an.z)
|
||||
return vec2(box_face_sign(n.x) * p.y, -p.z);
|
||||
if (an.y >= an.x && an.y >= an.z)
|
||||
return vec2(-box_face_sign(n.y) * p.x, -p.z);
|
||||
return vec2(box_face_sign(n.z) * p.x, -p.y);
|
||||
}
|
||||
|
||||
// The two surface directions projection_raw()'s u and v run along at `p`, plus how many raw units one
|
||||
@@ -124,7 +136,7 @@ void projection_axes(vec3 p, vec3 n, out vec3 t, out vec3 b, out vec2 units_per_
|
||||
vec2 xy = vec2(dot(rel, right), dot(rel, fwd));
|
||||
float r = length(xy);
|
||||
t = (r > 1e-6) ? (fwd * xy.x - right * xy.y) / r : right; // circumferential: u runs along it
|
||||
b = up; // v is the distance along the axis
|
||||
b = -up; // v is the distance *against* the axis
|
||||
return;
|
||||
}
|
||||
if (projection_mode == 2) {
|
||||
@@ -133,20 +145,20 @@ void projection_axes(vec3 p, vec3 n, out vec3 t, out vec3 b, out vec2 units_per_
|
||||
vec3 dir = (r > 1e-8) ? rel / r : vec3(0.0, 0.0, 1.0);
|
||||
float c = length(dir.xy); // cos(latitude)
|
||||
t = (c > 1e-6) ? vec3(-dir.y, dir.x, 0.0) / c : vec3(1.0, 0.0, 0.0);
|
||||
b = cross(dir, t); // increasing latitude, unit length
|
||||
b = -cross(dir, t); // v grows away from the pole, unit length
|
||||
units_per_mm = vec2(1.0 / max(c, 1e-3), 1.0);
|
||||
return;
|
||||
}
|
||||
vec3 an = abs(n);
|
||||
if (an.x >= an.y && an.x >= an.z) { // planar = p.yz
|
||||
t = vec3(0.0, 1.0, 0.0);
|
||||
b = vec3(0.0, 0.0, 1.0);
|
||||
} else if (an.y >= an.x && an.y >= an.z) { // planar = p.xz
|
||||
t = vec3(1.0, 0.0, 0.0);
|
||||
b = vec3(0.0, 0.0, 1.0);
|
||||
} else { // planar = p.xy
|
||||
t = vec3(1.0, 0.0, 0.0);
|
||||
b = vec3(0.0, 1.0, 0.0);
|
||||
vec3 an = abs(n); // the same signed basis projection_raw() uses
|
||||
if (an.x >= an.y && an.x >= an.z) { // planar = (+-y, -z)
|
||||
t = vec3(0.0, box_face_sign(n.x), 0.0);
|
||||
b = vec3(0.0, 0.0, -1.0);
|
||||
} else if (an.y >= an.x && an.y >= an.z) { // planar = (+-x, -z)
|
||||
t = vec3(-box_face_sign(n.y), 0.0, 0.0);
|
||||
b = vec3(0.0, 0.0, -1.0);
|
||||
} else { // planar = (+-x, -y)
|
||||
t = vec3(box_face_sign(n.z), 0.0, 0.0);
|
||||
b = vec3(0.0, -1.0, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,14 +36,20 @@ varying vec4 world_pos;
|
||||
varying float distortion;
|
||||
varying vec2 vertex_uv;
|
||||
|
||||
// Right handed (up x right), like libslic3r's project_cylindrical(), so the angle taken from it grows
|
||||
// anticlockwise about the axis - to the viewer's right when seen from outside the cylinder.
|
||||
void cylinder_frame(out vec3 up, out vec3 right, out vec3 fwd)
|
||||
{
|
||||
up = (length(patch_axis) > 1e-8) ? normalize(patch_axis) : vec3(0.0, 0.0, 1.0);
|
||||
vec3 arbitrary = (abs(up.z) < 0.9) ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
|
||||
right = normalize(cross(up, arbitrary));
|
||||
fwd = normalize(cross(right, up));
|
||||
fwd = normalize(cross(up, right));
|
||||
}
|
||||
|
||||
// Which side of a world plane a face is on, with zero counting as the positive side. Not GLSL's own
|
||||
// sign(), which returns 0 there and would collapse the coordinate onto a line.
|
||||
float box_face_sign(float n) { return n >= 0.0 ? 1.0 : -1.0; }
|
||||
|
||||
// Term for term libslic3r's project_planar() / project_cylindrical() / project_spherical(), in mm.
|
||||
vec2 projection_raw(vec3 p, vec3 n)
|
||||
{
|
||||
@@ -53,7 +59,7 @@ vec2 projection_raw(vec3 p, vec3 n)
|
||||
vec3 rel = p - patch_center;
|
||||
float x = dot(rel, right);
|
||||
float y = dot(rel, fwd);
|
||||
return vec2(atan(y, x) * sqrt(x * x + y * y), dot(rel, up));
|
||||
return vec2(atan(y, x) * sqrt(x * x + y * y), -dot(rel, up));
|
||||
}
|
||||
if (projection_mode == 2) {
|
||||
vec3 rel = p - patch_center;
|
||||
@@ -61,10 +67,16 @@ vec2 projection_raw(vec3 p, vec3 n)
|
||||
if (radius < 1e-8)
|
||||
return vec2(0.0);
|
||||
vec3 dir = rel / radius;
|
||||
return vec2(atan(dir.y, dir.x), asin(clamp(dir.z, -1.0, 1.0))) * radius;
|
||||
return vec2(atan(dir.y, dir.x), -asin(clamp(dir.z, -1.0, 1.0))) * radius;
|
||||
}
|
||||
// v negated so the image's first pixel row sits at the top of the model, u flipped on the far side
|
||||
// of each plane so the two faces sharing it are not mirror images - see libslic3r's box_plane_yz().
|
||||
vec3 an = abs(n);
|
||||
return (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy);
|
||||
if (an.x >= an.y && an.x >= an.z)
|
||||
return vec2(box_face_sign(n.x) * p.y, -p.z);
|
||||
if (an.y >= an.x && an.y >= an.z)
|
||||
return vec2(-box_face_sign(n.y) * p.x, -p.z);
|
||||
return vec2(box_face_sign(n.z) * p.x, -p.y);
|
||||
}
|
||||
|
||||
vec2 project_uv(vec3 p, vec3 n)
|
||||
|
||||
@@ -138,17 +138,21 @@ in vec2 vertex_uv;
|
||||
|
||||
out vec4 out_color;
|
||||
|
||||
// The cylinder's own frame, built exactly as libslic3r's project_cylindrical() builds it - including
|
||||
// the handedness, which comes out left-handed for an axis of +Z. Copied rather than "corrected", so
|
||||
// the preview wraps the texture the same way round as the bake.
|
||||
// The cylinder's own frame, built exactly as libslic3r's project_cylindrical() builds it - right
|
||||
// handed, so the angle taken from it grows anticlockwise about the axis, which seen from outside the
|
||||
// cylinder is to the viewer's right rather than mirrored.
|
||||
void cylinder_frame(out vec3 up, out vec3 right, out vec3 fwd)
|
||||
{
|
||||
up = (length(patch_axis) > 1e-8) ? normalize(patch_axis) : vec3(0.0, 0.0, 1.0);
|
||||
vec3 arbitrary = (abs(up.z) < 0.9) ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
|
||||
right = normalize(cross(up, arbitrary));
|
||||
fwd = normalize(cross(right, up));
|
||||
fwd = normalize(cross(up, right));
|
||||
}
|
||||
|
||||
// Which side of a world plane a face is on, with zero counting as the positive side. Not GLSL's own
|
||||
// sign(), which returns 0 there and would collapse the coordinate onto a line.
|
||||
float box_face_sign(float n) { return n >= 0.0 ? 1.0 : -1.0; }
|
||||
|
||||
// The raw, millimetre-valued projection of `p` (a position in the texture frame), before the layer's
|
||||
// tiling/rotation/aspect/offset - a term-for-term transcription of libslic3r's project_planar(),
|
||||
// project_cylindrical() and project_spherical(). `n` is read by the planar mode only.
|
||||
@@ -160,7 +164,7 @@ vec2 projection_raw(vec3 p, vec3 n)
|
||||
vec3 rel = p - patch_center;
|
||||
float x = dot(rel, right);
|
||||
float y = dot(rel, fwd);
|
||||
return vec2(atan(y, x) * sqrt(x * x + y * y), dot(rel, up));
|
||||
return vec2(atan(y, x) * sqrt(x * x + y * y), -dot(rel, up));
|
||||
}
|
||||
if (projection_mode == 2) { // Spherical: (longitude, latitude) * radius
|
||||
vec3 rel = p - patch_center;
|
||||
@@ -168,10 +172,18 @@ vec2 projection_raw(vec3 p, vec3 n)
|
||||
if (radius < 1e-8)
|
||||
return vec2(0.0);
|
||||
vec3 dir = rel / radius;
|
||||
return vec2(atan(dir.y, dir.x), asin(clamp(dir.z, -1.0, 1.0))) * radius;
|
||||
return vec2(atan(dir.y, dir.x), -asin(clamp(dir.z, -1.0, 1.0))) * radius;
|
||||
}
|
||||
vec3 an = abs(n); // Triplanar: drop the dominant normal axis
|
||||
return (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy);
|
||||
// Triplanar: drop the dominant normal axis. v is negated so the image's first pixel row (v = 0)
|
||||
// sits at the top of the model instead of upside down, and u is flipped on the far side of each
|
||||
// plane so the two faces sharing it are not mirror images. libslic3r's box_plane_yz() and its two
|
||||
// siblings, term for term.
|
||||
vec3 an = abs(n);
|
||||
if (an.x >= an.y && an.x >= an.z)
|
||||
return vec2(box_face_sign(n.x) * p.y, -p.z);
|
||||
if (an.y >= an.x && an.y >= an.z)
|
||||
return vec2(-box_face_sign(n.y) * p.x, -p.z);
|
||||
return vec2(box_face_sign(n.z) * p.x, -p.y);
|
||||
}
|
||||
|
||||
// The two surface directions projection_raw()'s u and v run along at `p`, plus how many raw units one
|
||||
@@ -190,7 +202,7 @@ void projection_axes(vec3 p, vec3 n, out vec3 t, out vec3 b, out vec2 units_per_
|
||||
vec2 xy = vec2(dot(rel, right), dot(rel, fwd));
|
||||
float r = length(xy);
|
||||
t = (r > 1e-6) ? (fwd * xy.x - right * xy.y) / r : right; // circumferential: u runs along it
|
||||
b = up; // v is the distance along the axis
|
||||
b = -up; // v is the distance *against* the axis
|
||||
return;
|
||||
}
|
||||
if (projection_mode == 2) {
|
||||
@@ -199,20 +211,20 @@ void projection_axes(vec3 p, vec3 n, out vec3 t, out vec3 b, out vec2 units_per_
|
||||
vec3 dir = (r > 1e-8) ? rel / r : vec3(0.0, 0.0, 1.0);
|
||||
float c = length(dir.xy); // cos(latitude)
|
||||
t = (c > 1e-6) ? vec3(-dir.y, dir.x, 0.0) / c : vec3(1.0, 0.0, 0.0);
|
||||
b = cross(dir, t); // increasing latitude, unit length
|
||||
b = -cross(dir, t); // v grows away from the pole, unit length
|
||||
units_per_mm = vec2(1.0 / max(c, 1e-3), 1.0);
|
||||
return;
|
||||
}
|
||||
vec3 an = abs(n);
|
||||
if (an.x >= an.y && an.x >= an.z) { // planar = p.yz
|
||||
t = vec3(0.0, 1.0, 0.0);
|
||||
b = vec3(0.0, 0.0, 1.0);
|
||||
} else if (an.y >= an.x && an.y >= an.z) { // planar = p.xz
|
||||
t = vec3(1.0, 0.0, 0.0);
|
||||
b = vec3(0.0, 0.0, 1.0);
|
||||
} else { // planar = p.xy
|
||||
t = vec3(1.0, 0.0, 0.0);
|
||||
b = vec3(0.0, 1.0, 0.0);
|
||||
vec3 an = abs(n); // the same signed basis projection_raw() uses
|
||||
if (an.x >= an.y && an.x >= an.z) { // planar = (+-y, -z)
|
||||
t = vec3(0.0, box_face_sign(n.x), 0.0);
|
||||
b = vec3(0.0, 0.0, -1.0);
|
||||
} else if (an.y >= an.x && an.y >= an.z) { // planar = (+-x, -z)
|
||||
t = vec3(-box_face_sign(n.y), 0.0, 0.0);
|
||||
b = vec3(0.0, 0.0, -1.0);
|
||||
} else { // planar = (+-x, -y)
|
||||
t = vec3(box_face_sign(n.z), 0.0, 0.0);
|
||||
b = vec3(0.0, -1.0, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,14 +47,20 @@ in vec2 vertex_uv;
|
||||
|
||||
out vec4 out_color;
|
||||
|
||||
// Right handed (up x right), like libslic3r's project_cylindrical(), so the angle taken from it grows
|
||||
// anticlockwise about the axis - to the viewer's right when seen from outside the cylinder.
|
||||
void cylinder_frame(out vec3 up, out vec3 right, out vec3 fwd)
|
||||
{
|
||||
up = (length(patch_axis) > 1e-8) ? normalize(patch_axis) : vec3(0.0, 0.0, 1.0);
|
||||
vec3 arbitrary = (abs(up.z) < 0.9) ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
|
||||
right = normalize(cross(up, arbitrary));
|
||||
fwd = normalize(cross(right, up));
|
||||
fwd = normalize(cross(up, right));
|
||||
}
|
||||
|
||||
// Which side of a world plane a face is on, with zero counting as the positive side. Not GLSL's own
|
||||
// sign(), which returns 0 there and would collapse the coordinate onto a line.
|
||||
float box_face_sign(float n) { return n >= 0.0 ? 1.0 : -1.0; }
|
||||
|
||||
// Term for term libslic3r's project_planar() / project_cylindrical() / project_spherical(), in mm.
|
||||
vec2 projection_raw(vec3 p, vec3 n)
|
||||
{
|
||||
@@ -64,7 +70,7 @@ vec2 projection_raw(vec3 p, vec3 n)
|
||||
vec3 rel = p - patch_center;
|
||||
float x = dot(rel, right);
|
||||
float y = dot(rel, fwd);
|
||||
return vec2(atan(y, x) * sqrt(x * x + y * y), dot(rel, up));
|
||||
return vec2(atan(y, x) * sqrt(x * x + y * y), -dot(rel, up));
|
||||
}
|
||||
if (projection_mode == 2) {
|
||||
vec3 rel = p - patch_center;
|
||||
@@ -72,10 +78,16 @@ vec2 projection_raw(vec3 p, vec3 n)
|
||||
if (radius < 1e-8)
|
||||
return vec2(0.0);
|
||||
vec3 dir = rel / radius;
|
||||
return vec2(atan(dir.y, dir.x), asin(clamp(dir.z, -1.0, 1.0))) * radius;
|
||||
return vec2(atan(dir.y, dir.x), -asin(clamp(dir.z, -1.0, 1.0))) * radius;
|
||||
}
|
||||
// v negated so the image's first pixel row sits at the top of the model, u flipped on the far side
|
||||
// of each plane so the two faces sharing it are not mirror images - see libslic3r's box_plane_yz().
|
||||
vec3 an = abs(n);
|
||||
return (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy);
|
||||
if (an.x >= an.y && an.x >= an.z)
|
||||
return vec2(box_face_sign(n.x) * p.y, -p.z);
|
||||
if (an.y >= an.x && an.y >= an.z)
|
||||
return vec2(-box_face_sign(n.y) * p.x, -p.z);
|
||||
return vec2(box_face_sign(n.z) * p.x, -p.y);
|
||||
}
|
||||
|
||||
vec2 project_uv(vec3 p, vec3 n)
|
||||
|
||||
@@ -1729,8 +1729,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
||||
//plate->pattern_file = it->second->pattern_file;
|
||||
plate->no_light_thumbnail_file = it->second->no_light_thumbnail_file;
|
||||
plate->top_file = it->second->top_file;
|
||||
plate->pick_file = it->second->pick_file;
|
||||
plate->pattern_bbox_file = it->second->pattern_bbox_file;
|
||||
plate->pick_file = it->second->pick_file.empty();
|
||||
plate->pattern_bbox_file = it->second->pattern_bbox_file.empty();
|
||||
plate->config = it->second->config;
|
||||
|
||||
if (!plate->thumbnail_file.empty())
|
||||
|
||||
@@ -3295,7 +3295,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
m_is_role_based_fan_on.fill(false);
|
||||
m_role_based_fan_marker_layer.fill(-1);
|
||||
|
||||
m_fan_mover.reset();
|
||||
m_fan_mover.release();
|
||||
m_ordering_cache.clear();
|
||||
|
||||
// Belt printer: initialize belt-specific writer via virtual hook.
|
||||
|
||||
@@ -491,6 +491,57 @@ V2Resolution recommend_v2_resolution(const indexed_triangle_set
|
||||
return out;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Which side of a world plane a face is on, with an exactly zero component counted as the positive
|
||||
// side. The tie never shows: a normal component of zero carries zero tri-planar blend weight (see
|
||||
// triplanar_weights()), so the plane it orients contributes nothing at that point.
|
||||
inline float box_face_sign(float n) { return n >= 0.f ? 1.f : -1.f; }
|
||||
|
||||
// The box projection's 2D basis for one of the three world planes, oriented the way a viewer
|
||||
// standing outside the model sees that face.
|
||||
//
|
||||
// v runs *down* the image, because that is what v means to the sampler: v = 0 is the texture's first
|
||||
// pixel row, i.e. its top (DecodedHeightTexture::sample(); the UV editor negates Y on screen for the
|
||||
// same reason, see UVEditorCanvas::render()). So v = -z on the four upright planes and v = -y on the
|
||||
// horizontal one, which is what puts the top of an image at the top of the model rather than
|
||||
// upside down.
|
||||
//
|
||||
// u then has to depend on which *side* of the plane the face is on, or one of the two faces sharing
|
||||
// a plane comes out left-right mirrored: with a single u per plane, a portrait image reads upright on
|
||||
// +X and 180 degrees round on -X. Fixing that is the sign below, chosen so that u x v == -normal,
|
||||
// i.e. (right, down, into the surface) is right-handed for a viewer outside:
|
||||
//
|
||||
// face u v face u v face u v
|
||||
// +X +y -z +Y -x -z +Z +x -y
|
||||
// -X -y -z -Y +x -z -Z -x -y
|
||||
//
|
||||
// The sign flips where its own normal component crosses zero, and the blend weight of the plane it
|
||||
// belongs to is exactly zero there, so the blended tri-planar sum stays continuous across the flip -
|
||||
// it is not a return of the hard axis switch described on TextureProjectionMethod::Triplanar.
|
||||
inline Vec2f box_plane_yz(const Vec3f &p, float nx) { return Vec2f(box_face_sign(nx) * p.y(), -p.z()); }
|
||||
inline Vec2f box_plane_xz(const Vec3f &p, float ny) { return Vec2f(-box_face_sign(ny) * p.x(), -p.z()); }
|
||||
inline Vec2f box_plane_xy(const Vec3f &p, float nz) { return Vec2f(box_face_sign(nz) * p.x(), -p.y()); }
|
||||
} // namespace
|
||||
|
||||
void box_projection_axes(const Vec3f &normal, Vec3f &u_axis, Vec3f &v_axis)
|
||||
{
|
||||
// The same table as box_plane_yz()/box_plane_xz()/box_plane_xy(), handed out as a pair of world
|
||||
// directions for the plane the normal is closest to. Two shapes of one table, because the bake's
|
||||
// hot path wants the closed form and the GUI wants the axes; the test suite checks them against
|
||||
// each other so they cannot drift apart.
|
||||
const Vec3f n = normal.cwiseAbs();
|
||||
if (n.x() >= n.y() && n.x() >= n.z()) {
|
||||
u_axis = box_face_sign(normal.x()) * Vec3f::UnitY();
|
||||
v_axis = -Vec3f::UnitZ();
|
||||
} else if (n.y() >= n.x() && n.y() >= n.z()) {
|
||||
u_axis = -box_face_sign(normal.y()) * Vec3f::UnitX();
|
||||
v_axis = -Vec3f::UnitZ();
|
||||
} else {
|
||||
u_axis = box_face_sign(normal.z()) * Vec3f::UnitX();
|
||||
v_axis = -Vec3f::UnitY();
|
||||
}
|
||||
}
|
||||
|
||||
Vec2f project_planar(const Vec3f &position, const Vec3f &normal)
|
||||
{
|
||||
// Planar-project onto the two axes orthogonal to the dominant component of `normal`. Called
|
||||
@@ -501,23 +552,26 @@ Vec2f project_planar(const Vec3f &position, const Vec3f &normal)
|
||||
// other face in the same patch - exactly the bug an earlier version of this feature had).
|
||||
const Vec3f n = normal.cwiseAbs();
|
||||
if (n.x() >= n.y() && n.x() >= n.z())
|
||||
return Vec2f(position.y(), position.z());
|
||||
return box_plane_yz(position, normal.x());
|
||||
if (n.y() >= n.x() && n.y() >= n.z())
|
||||
return Vec2f(position.x(), position.z());
|
||||
return Vec2f(position.x(), position.y());
|
||||
return box_plane_xz(position, normal.y());
|
||||
return box_plane_xy(position, normal.z());
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Wrapped around patch_axis, centered at patch_center. u is the arc length (mm) around the axis at
|
||||
// this point's own radius, v is the signed distance along the axis - a reasonable approximation
|
||||
// for roughly cylindrical selections, not an exact fit for arbitrary geometry.
|
||||
// this point's own radius, v is the distance along the axis, measured *against* it - a reasonable
|
||||
// approximation for roughly cylindrical selections, not an exact fit for arbitrary geometry.
|
||||
Vec2f project_cylindrical(const Vec3f &position, const Vec3f &patch_center, const Vec3f &patch_axis)
|
||||
{
|
||||
Vec3f up = patch_axis;
|
||||
up = (up.norm() > 1e-8f) ? Vec3f(up.normalized()) : Vec3f::UnitZ();
|
||||
const Vec3f arbitrary = (std::abs(up.dot(Vec3f::UnitZ())) < 0.9f) ? Vec3f::UnitZ() : Vec3f::UnitX();
|
||||
const Vec3f right = Vec3f(up.cross(arbitrary).normalized());
|
||||
const Vec3f fwd = Vec3f(right.cross(up).normalized());
|
||||
// up x right, not right x up: that makes (right, fwd, up) right-handed, so the angle below grows
|
||||
// anticlockwise about the axis - which, seen from outside the cylinder, is to the viewer's right.
|
||||
// The other way round the texture wraps on mirrored, as if seen through the model from behind.
|
||||
const Vec3f fwd = Vec3f(up.cross(right).normalized());
|
||||
|
||||
const Vec3f rel = position - patch_center;
|
||||
const float along_axis = rel.dot(up);
|
||||
@@ -526,7 +580,9 @@ Vec2f project_cylindrical(const Vec3f &position, const Vec3f &patch_center, cons
|
||||
const float radius = std::sqrt(x * x + y * y);
|
||||
const float angle = std::atan2(y, x);
|
||||
|
||||
return Vec2f(angle * radius, along_axis);
|
||||
// Negated, like every other projection's v: v = 0 is the texture's top row, so v has to grow
|
||||
// *down* the axis for the top of the image to sit at the top of the model.
|
||||
return Vec2f(angle * radius, -along_axis);
|
||||
}
|
||||
|
||||
// Longitude/latitude around patch_center. u/v are scaled by this point's own distance from the
|
||||
@@ -542,7 +598,9 @@ Vec2f project_spherical(const Vec3f &position, const Vec3f &patch_center)
|
||||
const Vec3f dir = rel / radius;
|
||||
const float longitude = std::atan2(dir.y(), dir.x());
|
||||
const float latitude = std::asin(std::clamp(dir.z(), -1.f, 1.f));
|
||||
return Vec2f(longitude, latitude) * radius;
|
||||
// Longitude already grows to the right as seen from outside; latitude grows toward the north
|
||||
// pole, so it is negated to put the image's top row (v = 0) at the top of the model.
|
||||
return Vec2f(longitude, -latitude) * radius;
|
||||
}
|
||||
|
||||
// CGAL's LSCM parameterizer expects a clean mesh with no isolated (unreferenced) vertices - but
|
||||
@@ -837,9 +895,26 @@ std::vector<int> segment_into_charts(const indexed_triangle_set &mesh, const std
|
||||
// solve at all, and why this also serves as the fallback for a chart LSCM cannot handle.
|
||||
std::vector<Vec2f> project_to_tangent_plane(const indexed_triangle_set &chart, const Vec3f &normal)
|
||||
{
|
||||
const Vec3f seed = (std::abs(normal.z()) < 0.9f) ? Vec3f::UnitZ() : Vec3f::UnitX();
|
||||
const Vec3f u = Vec3f(seed.cross(normal).normalized());
|
||||
const Vec3f v = Vec3f(normal.cross(u).normalized());
|
||||
// Oriented like the box projection (see box_projection_axes()), so a chart reads the right way up
|
||||
// and unmirrored instead of being flattened to whichever basis a cross product happened to give.
|
||||
// For a chart whose normal *is* a world axis - a cube face, which is the common case once the
|
||||
// seam angle has cut the patch along its sharp edges - this is exactly the box basis, so such a
|
||||
// face unwraps to the same orientation Triplanar would have given it.
|
||||
Vec3f u, v;
|
||||
box_projection_axes(normal, u, v);
|
||||
// The box axes lie in the tangent plane already when the normal is a world axis; for any other
|
||||
// normal, u is squared up against it and v follows. u x v == -normal keeps the chart the way
|
||||
// round a viewer outside the model sees it (a plain normal x u would mirror it).
|
||||
u = u - normal * normal.dot(u);
|
||||
if (u.norm() <= 1e-6f) {
|
||||
// Out of reach for a unit normal - the box basis puts u perpendicular to the normal's own
|
||||
// dominant axis, so the two can never be within 45 degrees of each other - but a caller
|
||||
// handing over an unnormalized normal should still get a usable basis rather than a NaN.
|
||||
const Vec3f seed = (std::abs(normal.z()) < 0.9f) ? Vec3f::UnitZ() : Vec3f::UnitX();
|
||||
u = seed.cross(normal);
|
||||
}
|
||||
u = Vec3f(u.normalized());
|
||||
v = Vec3f(u.cross(normal).normalized());
|
||||
|
||||
std::vector<Vec2f> uvs(chart.vertices.size());
|
||||
for (size_t i = 0; i < chart.vertices.size(); ++i)
|
||||
@@ -1814,8 +1889,10 @@ float sample_layer_height(const DecodedHeightTexture &texture, const TextureDisp
|
||||
return texture.sample(uv, layer.tile_enabled, layer.tile_method);
|
||||
}
|
||||
// Flat projection onto the captured projector plane. Single-valued per point, so unlike
|
||||
// blended triplanar it is one sample, and it is what "project from view" places.
|
||||
return sample_at(Vec2f(position.dot(layer.view_project_right), position.dot(layer.view_project_up)));
|
||||
// blended triplanar it is one sample, and it is what "project from view" places. The
|
||||
// projector's up axis is negated because v runs *down* the image (v = 0 is its first pixel
|
||||
// row), the same reason the projective branch above measures window y downward.
|
||||
return sample_at(Vec2f(position.dot(layer.view_project_right), -position.dot(layer.view_project_up)));
|
||||
case TextureProjectionMethod::LSCM: // no usable unwrap for this patch - fall back to Triplanar
|
||||
case TextureProjectionMethod::Triplanar:
|
||||
default: break;
|
||||
@@ -1830,14 +1907,15 @@ float sample_layer_height(const DecodedHeightTexture &texture, const TextureDisp
|
||||
const float w_sum = w.x() + w.y() + w.z();
|
||||
if (w_sum < 1e-8f)
|
||||
// Degenerate normal: no axis is meaningfully dominant, so no blend is meaningful either.
|
||||
return sample_at(Vec2f(position.x(), position.y()));
|
||||
return sample_at(box_plane_xy(position, normal.z()));
|
||||
w /= w_sum;
|
||||
|
||||
// Each plane drops the axis it is named for, matching project_planar()'s own convention (which
|
||||
// the GUI's on-canvas placement gizmo also relies on).
|
||||
return w.x() * sample_at(Vec2f(position.y(), position.z())) +
|
||||
w.y() * sample_at(Vec2f(position.x(), position.z())) +
|
||||
w.z() * sample_at(Vec2f(position.x(), position.y()));
|
||||
// Each plane drops the axis it is named for and is oriented for the side of that plane the normal
|
||||
// points to, matching project_planar()'s own convention (which the GUI's on-canvas placement
|
||||
// gizmo also relies on). See box_plane_yz() for why the orientation is not simply (y, z).
|
||||
return w.x() * sample_at(box_plane_yz(position, normal.x())) +
|
||||
w.y() * sample_at(box_plane_xz(position, normal.y())) +
|
||||
w.z() * sample_at(box_plane_xy(position, normal.z()));
|
||||
}
|
||||
|
||||
bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDisplacementLayer &layer,
|
||||
@@ -1899,7 +1977,8 @@ bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDispla
|
||||
out = texture.sample_color(uv, layer.tile_enabled, layer.tile_method);
|
||||
return true;
|
||||
} else {
|
||||
const Vec2f p(position.dot(layer.view_project_right), position.dot(layer.view_project_up));
|
||||
// Negated up, exactly as sample_layer_height() does it.
|
||||
const Vec2f p(position.dot(layer.view_project_right), -position.dot(layer.view_project_up));
|
||||
if (!covered(p))
|
||||
return false;
|
||||
out = sample_at(p);
|
||||
@@ -1915,7 +1994,7 @@ bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDispla
|
||||
Vec3f w = triplanar_weights(normal);
|
||||
const float w_sum = w.x() + w.y() + w.z();
|
||||
if (w_sum < 1e-8f) {
|
||||
const Vec2f p(position.x(), position.y());
|
||||
const Vec2f p = box_plane_xy(position, normal.z());
|
||||
if (!covered(p))
|
||||
return false;
|
||||
out = sample_at(p);
|
||||
@@ -1926,8 +2005,8 @@ bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDispla
|
||||
// A blend of three planes is only "not covered" where *every* contributing plane is outside the
|
||||
// placement; where some are, the covered ones are renormalised so the colour does not fade toward
|
||||
// black at the edge of an untiled tri-planar layer.
|
||||
const std::array<Vec2f, 3> planes = { Vec2f(position.y(), position.z()), Vec2f(position.x(), position.z()),
|
||||
Vec2f(position.x(), position.y()) };
|
||||
const std::array<Vec2f, 3> planes = { box_plane_yz(position, normal.x()), box_plane_xz(position, normal.y()),
|
||||
box_plane_xy(position, normal.z()) };
|
||||
Vec3f acc = Vec3f::Zero();
|
||||
float acc_w = 0.f;
|
||||
for (int i = 0; i < 3; ++i)
|
||||
|
||||
@@ -524,11 +524,24 @@ struct TextureColorSettings
|
||||
bool empty() const { return palette.empty(); }
|
||||
};
|
||||
|
||||
// The two world directions the box projection takes its u and v along on a face whose outward
|
||||
// normal is `normal`: the plane is the one that normal is closest to, and the pair is oriented as a
|
||||
// viewer standing outside the model sees it - u to their right, v *down* the image, which is the
|
||||
// direction v runs in the texture (v = 0 is the image's first pixel row, see
|
||||
// DecodedHeightTexture::sample()). So an image comes out the way it looks in a file manager rather
|
||||
// than upside down, and it is not mirrored on the three faces facing the other way.
|
||||
//
|
||||
// Exposed because the GUI needs the same basis for the on-canvas "adjust texture placement" gizmo
|
||||
// (its ring's zero angle and its offset handle's plane), and it must not be a second, drifting copy
|
||||
// of the table.
|
||||
void box_projection_axes(const Vec3f &normal, Vec3f &u_axis, Vec3f &v_axis);
|
||||
|
||||
// Raw dominant-axis planar projection of `position` (in mm, not yet scaled/rotated/offset by any
|
||||
// layer), dropping the axis position that best aligns with `normal`. Exposed on its own (rather
|
||||
// than only inline inside project_texture_displacement_uv()) so GUI code - the on-canvas
|
||||
// "adjust texture placement" gizmo - can map a dragged 3D point into the exact same 2D space
|
||||
// tiling_scale/rotation_deg/offset operate in, without duplicating the axis-selection logic.
|
||||
// layer), dropping the axis position that best aligns with `normal`. Equivalent to projecting onto
|
||||
// box_projection_axes()' pair. Exposed on its own (rather than only inline inside
|
||||
// project_texture_displacement_uv()) so GUI code - the on-canvas "adjust texture placement" gizmo -
|
||||
// can map a dragged 3D point into the exact same 2D space tiling_scale/rotation_deg/offset operate
|
||||
// in, without duplicating the axis-selection logic.
|
||||
Vec2f project_planar(const Vec3f &position, const Vec3f &normal);
|
||||
|
||||
// Applies a layer's tiling_scale/rotation_deg/offset to an already-projected planar coordinate
|
||||
|
||||
@@ -21,9 +21,7 @@ namespace libvgcode {
|
||||
// the triangles that share it: the shader runs 8 times per segment instead of 24.
|
||||
static constexpr const std::array<uint8_t, 8> VERTEX_DATA = { 0, 1, 2, 3, 4, 5, 6, 7 };
|
||||
|
||||
// 16-bit indices: GCN 1/2 GPUs have no 8-bit index support, and some of their drivers
|
||||
// emulate it wrongly in instanced draws.
|
||||
static constexpr const std::array<uint16_t, 24> INDEX_DATA = {
|
||||
static constexpr const std::array<uint8_t, 24> INDEX_DATA = {
|
||||
0, 1, 2, // front spike
|
||||
0, 2, 3, // front spike
|
||||
0, 3, 4, // right/bottom body
|
||||
@@ -39,7 +37,7 @@ void SegmentTemplate::init()
|
||||
if (m_vao_id != 0)
|
||||
return;
|
||||
|
||||
m_size_in_bytes_gpu += VERTEX_DATA.size() * sizeof(uint8_t) + INDEX_DATA.size() * sizeof(uint16_t);
|
||||
m_size_in_bytes_gpu += (VERTEX_DATA.size() + INDEX_DATA.size()) * sizeof(uint8_t);
|
||||
|
||||
int curr_vertex_array;
|
||||
glsafe(glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &curr_vertex_array));
|
||||
@@ -63,7 +61,7 @@ void SegmentTemplate::init()
|
||||
// and restored together with the vao.
|
||||
glsafe(glGenBuffers(1, &m_ibo_id));
|
||||
glsafe(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ibo_id));
|
||||
glsafe(glBufferData(GL_ELEMENT_ARRAY_BUFFER, INDEX_DATA.size() * sizeof(uint16_t), INDEX_DATA.data(), GL_STATIC_DRAW));
|
||||
glsafe(glBufferData(GL_ELEMENT_ARRAY_BUFFER, INDEX_DATA.size() * sizeof(uint8_t), INDEX_DATA.data(), GL_STATIC_DRAW));
|
||||
|
||||
glsafe(glBindBuffer(GL_ARRAY_BUFFER, curr_array_buffer));
|
||||
glsafe(glBindVertexArray(curr_vertex_array));
|
||||
@@ -96,7 +94,7 @@ void SegmentTemplate::render(size_t count)
|
||||
glsafe(glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &curr_vertex_array));
|
||||
|
||||
glsafe(glBindVertexArray(m_vao_id));
|
||||
glsafe(glDrawElementsInstanced(GL_TRIANGLES, static_cast<GLsizei>(INDEX_DATA.size()), GL_UNSIGNED_SHORT,
|
||||
glsafe(glDrawElementsInstanced(GL_TRIANGLES, static_cast<GLsizei>(INDEX_DATA.size()), GL_UNSIGNED_BYTE,
|
||||
nullptr, static_cast<GLsizei>(count)));
|
||||
glsafe(glBindVertexArray(curr_vertex_array));
|
||||
}
|
||||
|
||||
@@ -1252,8 +1252,9 @@ std::vector<Vec2f> GLGizmoTextureDisplacement::compute_layer_vertex_uvs(const in
|
||||
uv[vi] = Vec2f(-1e6f, -1e6f);
|
||||
continue;
|
||||
}
|
||||
// Negated up, as sample_layer_height()'s affine branch has it: v runs down the image.
|
||||
const Vec2f planar(patch.vertices[vi].dot(layer.view_project_right),
|
||||
patch.vertices[vi].dot(layer.view_project_up));
|
||||
-patch.vertices[vi].dot(layer.view_project_up));
|
||||
uv[vi] = apply_uv_transform(planar, layer, aspect);
|
||||
}
|
||||
return uv;
|
||||
@@ -3292,19 +3293,10 @@ Vec3f GLGizmoTextureDisplacement::adjust_handle_center(const TextureDisplacement
|
||||
|
||||
void GLGizmoTextureDisplacement::adjust_tangent_basis(Vec3f &u_axis, Vec3f &v_axis) const
|
||||
{
|
||||
// Mirrors project_planar()'s own dominant-axis choice exactly, so the ring's "0 degrees" and
|
||||
// the offset handle's plane always agree with what project_texture_displacement_uv() does.
|
||||
const Vec3f n = m_adjust_anchor_normal.cwiseAbs();
|
||||
if (n.x() >= n.y() && n.x() >= n.z()) {
|
||||
u_axis = Vec3f::UnitY();
|
||||
v_axis = Vec3f::UnitZ();
|
||||
} else if (n.y() >= n.x() && n.y() >= n.z()) {
|
||||
u_axis = Vec3f::UnitX();
|
||||
v_axis = Vec3f::UnitZ();
|
||||
} else {
|
||||
u_axis = Vec3f::UnitX();
|
||||
v_axis = Vec3f::UnitY();
|
||||
}
|
||||
// libslic3r's own basis, asked for rather than restated here, so the ring's "0 degrees" and the
|
||||
// offset handle's plane always agree with what project_texture_displacement_uv() does - including
|
||||
// v pointing *down* the texture and u flipping on the far side of each plane.
|
||||
box_projection_axes(m_adjust_anchor_normal, u_axis, v_axis);
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::render_adjust_texture_gizmo()
|
||||
|
||||
@@ -423,8 +423,8 @@ private:
|
||||
// obvious whether the next stroke adds paint (default) or erases it (Shift). Uses ImGui's
|
||||
// foreground draw list, so it must be called from inside the gizmo's ImGui frame.
|
||||
void render_paint_cursor_hint();
|
||||
// Mesh-local tangent-plane basis at m_adjust_anchor_normal, matching project_planar()'s
|
||||
// dominant-axis convention so dragging on-canvas maps consistently onto offset.
|
||||
// Mesh-local tangent-plane basis at m_adjust_anchor_normal: box_projection_axes(), so dragging
|
||||
// on-canvas maps consistently onto offset whichever face the anchor is on.
|
||||
void adjust_tangent_basis(Vec3f &u_axis, Vec3f &v_axis) const;
|
||||
|
||||
// The plane a drag is measured against: the paint patch's anchor, lifted clear of the surface.
|
||||
|
||||
@@ -2043,40 +2043,3 @@ TEST_CASE("3MF XML entries declaring more than an int can hold fail to load", "[
|
||||
CHECK_FALSE(parser.check_3mf_from_prusa(path));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Loading a gcode.3mf from a stream keeps the plate's pick and pattern file paths", "[3mf][Regression]")
|
||||
{
|
||||
Model model;
|
||||
REQUIRE(load_stl((std::string(TEST_DATA_DIR) + "/test_3mf/Prusa.stl").c_str(), &model));
|
||||
model.add_default_instances();
|
||||
ScopedTemporaryDir backup_dir("orca_pick");
|
||||
model.set_backup_path(backup_dir.string());
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
|
||||
auto plate = std::make_unique<PlateData>();
|
||||
plate->plate_index = 0;
|
||||
plate->pick_file = (backup_dir.path() / "pick.png").string();
|
||||
plate->pattern_bbox_file = (backup_dir.path() / "plate.json").string();
|
||||
|
||||
ScopedTemporaryFile temp(".3mf");
|
||||
const std::string test_file = temp.string();
|
||||
StoreParams store_params;
|
||||
store_params.path = test_file.c_str();
|
||||
store_params.model = &model;
|
||||
store_params.config = &config;
|
||||
store_params.plate_data_list.push_back(plate.get());
|
||||
store_params.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence;
|
||||
REQUIRE(store_bbs_3mf(store_params));
|
||||
|
||||
boost::nowide::ifstream in(test_file, std::ios::binary);
|
||||
Model dst_model;
|
||||
DynamicPrintConfig dst_config;
|
||||
PlateDataPtrs dst_plates;
|
||||
Semver file_version;
|
||||
dst_model.set_backup_path(backup_dir.string());
|
||||
REQUIRE(load_gcode_3mf_from_stream(in, &dst_config, &dst_model, &dst_plates, &file_version));
|
||||
REQUIRE(dst_plates.size() == 1);
|
||||
CHECK(dst_plates.front()->pick_file == "Metadata/pick_1.png");
|
||||
CHECK(dst_plates.front()->pattern_bbox_file == "Metadata/plate_1.json");
|
||||
release_PlateData_list(dst_plates);
|
||||
}
|
||||
|
||||
@@ -2174,3 +2174,318 @@ TEST_CASE("whether a layer's texture has colour agrees with its decode", "[Textu
|
||||
|
||||
CHECK_FALSE(height_texture_has_color(TextureDisplacementLayer{}));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// UV orientation: which way up a texture lands on the model.
|
||||
//
|
||||
// v = 0 is the height map's *first* pixel row, i.e. its top (DecodedHeightTexture::sample(), and
|
||||
// the UV editor negates Y on screen for the same reason). So "the image is the right way up" is the
|
||||
// statement that the row a point samples gets *lower* as the point moves up the face, and "it is
|
||||
// not left-right mirrored" is that the column gets higher as the point moves to the right of a
|
||||
// viewer standing outside the model. Both are measured below by sampling a ramp whose value says
|
||||
// which row (or column) it came from, which settles the question without having to reason about the
|
||||
// uv transform at all.
|
||||
|
||||
static constexpr int RAMP_N = 64; // the ramp texture's size, in texels
|
||||
static constexpr float RAMP_TILING = 40.f; // mm per tile
|
||||
static constexpr float CUBE_HALF = 5.f; // half extent of the cube the probes walk over
|
||||
// How far a probe steps off a face centre. Short on purpose: the cylindrical and spherical u is an
|
||||
// arc length (angle * radius), so a long step also changes the radius, and that term swamps the
|
||||
// angular one. It is symmetric about the face centre, so at a short step it cancels between the two
|
||||
// readings a check compares.
|
||||
static constexpr float RAMP_STEP = 1.f;
|
||||
// What one step's worth of travel is in ramp rows: 2 * RAMP_STEP mm of separation, RAMP_N texels per
|
||||
// RAMP_TILING mm. Halved as the margin a check demands, so "flat" cannot pass as either direction.
|
||||
static constexpr double RAMP_MARGIN = RAMP_N * RAMP_STEP / RAMP_TILING;
|
||||
|
||||
// A height map whose rows (or columns) carry a linear ramp: row r holds 3 + 4r, so the sampled
|
||||
// height identifies the row. Built directly rather than through a PNG because these tests are about
|
||||
// the projection, not the decoder - decode_height_texture() has its own tests.
|
||||
static DecodedHeightTexture ramp_texture(bool by_row)
|
||||
{
|
||||
DecodedHeightTexture t;
|
||||
t.width = t.height = RAMP_N;
|
||||
t.pixels.resize(size_t(RAMP_N) * size_t(RAMP_N));
|
||||
for (int y = 0; y < RAMP_N; ++y)
|
||||
for (int x = 0; x < RAMP_N; ++x)
|
||||
t.pixels[size_t(y) * size_t(RAMP_N) + size_t(x)] = uint8_t(3 + 4 * (by_row ? y : x));
|
||||
return t;
|
||||
}
|
||||
|
||||
// uv 0.5 at the cube's centre, and a tile wide enough that every point the probes visit stays well
|
||||
// inside the one tile - so no reading is a wrapped-around neighbour of the one it is compared with.
|
||||
static TextureDisplacementLayer ramp_layer(TextureProjectionMethod method)
|
||||
{
|
||||
TextureDisplacementLayer layer;
|
||||
layer.projection_method = method;
|
||||
layer.tiling_scale = RAMP_TILING;
|
||||
layer.offset = Vec2f(0.5f, 0.5f);
|
||||
layer.depth_mm = 1.f;
|
||||
return layer;
|
||||
}
|
||||
|
||||
// Which row (or column) of `ramp` the layer reads at `p`. Fractional, because a point between two
|
||||
// texels reads the ramp between them; only the direction it moves in matters here.
|
||||
static double ramp_index(const DecodedHeightTexture &ramp, const TextureDisplacementLayer &layer, const Vec3f &p,
|
||||
const Vec3f &n, const Vec3f ¢er = Vec3f(Vec3f::Zero()),
|
||||
const Vec3f &axis = Vec3f(Vec3f::UnitZ()))
|
||||
{
|
||||
return (double(sample_layer_height(ramp, layer, p, n, center, axis, nullptr)) * 255.0 - 3.0) / 4.0;
|
||||
}
|
||||
|
||||
// How a viewer standing outside the model sees a face: their right, and their up. Up is world +Z on
|
||||
// the four upright faces; on the top and bottom, where +Z is edge on, it is world +Y.
|
||||
static void outside_viewer_frame(const Vec3f &n, Vec3f &right, Vec3f &up)
|
||||
{
|
||||
up = (std::abs(n.z()) < 0.9f) ? Vec3f(0.f, 0.f, 1.f) : Vec3f(0.f, 1.f, 0.f);
|
||||
right = Vec3f((-n).cross(up).normalized());
|
||||
}
|
||||
|
||||
static const std::array<std::pair<const char *, Vec3f>, 6> CUBE_FACES = { {
|
||||
{ "+X", Vec3f(1.f, 0.f, 0.f) },
|
||||
{ "-X", Vec3f(-1.f, 0.f, 0.f) },
|
||||
{ "+Y", Vec3f(0.f, 1.f, 0.f) },
|
||||
{ "-Y", Vec3f(0.f, -1.f, 0.f) },
|
||||
{ "+Z", Vec3f(0.f, 0.f, 1.f) },
|
||||
{ "-Z", Vec3f(0.f, 0.f, -1.f) },
|
||||
} };
|
||||
|
||||
TEST_CASE("the box projection lands a texture upright and unmirrored on all six faces of a cube",
|
||||
"[TextureDisplacement]")
|
||||
{
|
||||
const DecodedHeightTexture rows = ramp_texture(true);
|
||||
const DecodedHeightTexture cols = ramp_texture(false);
|
||||
const TextureDisplacementLayer layer = ramp_layer(TextureProjectionMethod::Triplanar);
|
||||
|
||||
for (const auto &[name, n] : CUBE_FACES) {
|
||||
DYNAMIC_SECTION("face " << name)
|
||||
{
|
||||
Vec3f right, up;
|
||||
outside_viewer_frame(n, right, up);
|
||||
const Vec3f centre = n * CUBE_HALF;
|
||||
|
||||
// Upright: the row read near the top of the face is nearer the image's first row.
|
||||
const double row_up = ramp_index(rows, layer, centre + up * RAMP_STEP, n);
|
||||
const double row_down = ramp_index(rows, layer, centre - up * RAMP_STEP, n);
|
||||
INFO("row at the top of the face " << row_up << ", at the bottom " << row_down);
|
||||
CHECK(row_down - row_up > RAMP_MARGIN);
|
||||
|
||||
// Unmirrored: the column read to the viewer's right is further into the image. Without a
|
||||
// per-side sign this fails on exactly the three faces facing away - -X, +Y and -Z - which
|
||||
// is the mirroring a bare v flip leaves behind.
|
||||
const double col_left = ramp_index(cols, layer, centre - right * RAMP_STEP, n);
|
||||
const double col_right = ramp_index(cols, layer, centre + right * RAMP_STEP, n);
|
||||
INFO("column at the left of the face " << col_left << ", at the right " << col_right);
|
||||
CHECK(col_right - col_left > RAMP_MARGIN);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("every projection method lands a texture upright on an upright face", "[TextureDisplacement]")
|
||||
{
|
||||
const DecodedHeightTexture rows = ramp_texture(true);
|
||||
// ViewProjected is one flat projection from the captured camera rather than a per-face map, so its
|
||||
// "up" is the projector's own (view_project_up), which the default capture puts at +Y.
|
||||
const auto method = GENERATE(TextureProjectionMethod::Triplanar, TextureProjectionMethod::Cylindrical,
|
||||
TextureProjectionMethod::Spherical, TextureProjectionMethod::ViewProjected);
|
||||
const TextureDisplacementLayer layer = ramp_layer(method);
|
||||
|
||||
for (const auto &[name, n] : CUBE_FACES) {
|
||||
// Only the four upright faces: on the top and bottom a cylinder's v is constant (they are its
|
||||
// caps) and a sphere's is at an extremum (they are its poles), so neither says anything about
|
||||
// which way up the image is.
|
||||
if (std::abs(n.z()) > 0.9f)
|
||||
continue;
|
||||
DYNAMIC_SECTION("method " << int(method) << ", face " << name)
|
||||
{
|
||||
const Vec3f up = (method == TextureProjectionMethod::ViewProjected) ? layer.view_project_up :
|
||||
Vec3f(0.f, 0.f, 1.f);
|
||||
const Vec3f centre = n * CUBE_HALF;
|
||||
const double row_up = ramp_index(rows, layer, centre + up * RAMP_STEP, n);
|
||||
const double row_down = ramp_index(rows, layer, centre - up * RAMP_STEP, n);
|
||||
INFO("row at the top " << row_up << ", at the bottom " << row_down);
|
||||
CHECK(row_down - row_up > RAMP_MARGIN);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("the cylindrical and spherical wraps run left to right as seen from outside", "[TextureDisplacement]")
|
||||
{
|
||||
const DecodedHeightTexture cols = ramp_texture(false);
|
||||
const auto method = GENERATE(TextureProjectionMethod::Cylindrical, TextureProjectionMethod::Spherical);
|
||||
const TextureDisplacementLayer layer = ramp_layer(method);
|
||||
|
||||
for (const auto &[name, n] : CUBE_FACES) {
|
||||
// The caps and poles are out, as above - u is undefined on the axis. So is whichever face the
|
||||
// atan2 branch cut runs through, where u jumps from +pi to -pi: that is a real seam in both
|
||||
// projections, and a reading taken across it compares two opposite sides of the model.
|
||||
if (std::abs(n.z()) > 0.9f)
|
||||
continue;
|
||||
const bool on_the_cut = (method == TextureProjectionMethod::Cylindrical) ? (n.y() < -0.9f) : (n.x() < -0.9f);
|
||||
if (on_the_cut)
|
||||
continue;
|
||||
DYNAMIC_SECTION("method " << int(method) << ", face " << name)
|
||||
{
|
||||
Vec3f right, up;
|
||||
outside_viewer_frame(n, right, up);
|
||||
const Vec3f centre = n * CUBE_HALF;
|
||||
const double col_left = ramp_index(cols, layer, centre - right * RAMP_STEP, n);
|
||||
const double col_right = ramp_index(cols, layer, centre + right * RAMP_STEP, n);
|
||||
INFO("column at the left " << col_left << ", at the right " << col_right);
|
||||
CHECK(col_right - col_left > RAMP_MARGIN);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("an unwrapped cube's flat charts are laid out the right way up", "[TextureDisplacement]")
|
||||
{
|
||||
indexed_triangle_set cube = its_make_cube(2. * CUBE_HALF, 2. * CUBE_HALF, 2. * CUBE_HALF);
|
||||
for (Vec3f &v : cube.vertices)
|
||||
v -= Vec3f(CUBE_HALF, CUBE_HALF, CUBE_HALF);
|
||||
|
||||
const PatchUnwrap unwrap = compute_patch_unwrap(cube, LSCM_DEFAULT_SEAM_ANGLE_DEG);
|
||||
REQUIRE_FALSE(unwrap.empty());
|
||||
REQUIRE(unwrap.chart_count == 6); // one flat chart per face, cut at the 90-degree edges
|
||||
|
||||
// Each chart carries one face. The four upright ones span world Z, and their v has to grow
|
||||
// downwards like every other projection's - whatever the packing then did with the island.
|
||||
int upright_charts = 0;
|
||||
for (int c = 0; c < unwrap.chart_count; ++c) {
|
||||
int top = -1, bottom = -1;
|
||||
for (size_t i = 0; i < unwrap.uvs.size(); ++i) {
|
||||
if (unwrap.vertex_chart[i] != c)
|
||||
continue;
|
||||
const float z = cube.vertices[size_t(unwrap.source_vertex[i])].z();
|
||||
if (top < 0 || z > cube.vertices[size_t(unwrap.source_vertex[size_t(top)])].z())
|
||||
top = int(i);
|
||||
if (bottom < 0 || z < cube.vertices[size_t(unwrap.source_vertex[size_t(bottom)])].z())
|
||||
bottom = int(i);
|
||||
}
|
||||
REQUIRE(top >= 0);
|
||||
REQUIRE(bottom >= 0);
|
||||
const float v_top = unwrap.uvs[size_t(top)].y(), v_bottom = unwrap.uvs[size_t(bottom)].y();
|
||||
if (std::abs(v_top - v_bottom) < 1e-3f)
|
||||
continue; // a cap: the whole chart is at one height, so it has no up
|
||||
++upright_charts;
|
||||
DYNAMIC_SECTION("chart " << c)
|
||||
{
|
||||
INFO("v at the chart's topmost vertex " << v_top << ", at its bottommost " << v_bottom);
|
||||
CHECK(v_bottom - v_top > 1.f);
|
||||
}
|
||||
}
|
||||
CHECK(upright_charts == 4); // the four sides; the top and bottom faces are the caps skipped above
|
||||
}
|
||||
|
||||
TEST_CASE("project_planar agrees with the box projection's own axes", "[TextureDisplacement]")
|
||||
{
|
||||
// The two express the same table in different shapes - one a pair of world axes for the GUI, the
|
||||
// other the closed form the bake's hot path uses - so they are checked against each other here
|
||||
// rather than left to drift apart.
|
||||
const Vec3f p(2.f, -3.f, 7.f);
|
||||
for (const auto &[name, n] : CUBE_FACES) {
|
||||
DYNAMIC_SECTION("face " << name)
|
||||
{
|
||||
Vec3f u_axis, v_axis;
|
||||
box_projection_axes(n, u_axis, v_axis);
|
||||
const Vec2f planar = project_planar(p, n);
|
||||
CHECK_THAT(planar.x(), WithinAbs(p.dot(u_axis), 1e-5f));
|
||||
CHECK_THAT(planar.y(), WithinAbs(p.dot(v_axis), 1e-5f));
|
||||
// Right-handed for a viewer outside the model: u to their right, v down, so u x v points
|
||||
// into the surface. Getting this wrong is exactly what mirrors a face.
|
||||
CHECK_THAT(u_axis.cross(v_axis).dot(n), WithinAbs(-1.f, 1e-5f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// An image with a different value in each quadrant, so one bake says both which way up it landed
|
||||
// and whether it came out mirrored. Distinct enough that a wrong quadrant cannot be mistaken for the
|
||||
// right one: top-left 255, top-right 170, bottom-left 85, bottom-right 0.
|
||||
static std::shared_ptr<std::vector<unsigned char>> make_quadrant_marker_png(size_t w = 32, size_t h = 32)
|
||||
{
|
||||
std::vector<uint8_t> pixels(w * h);
|
||||
for (size_t y = 0; y < h; ++y)
|
||||
for (size_t x = 0; x < w; ++x)
|
||||
pixels[y * w + x] = (y < h / 2) ? ((x < w / 2) ? 255 : 170) : ((x < w / 2) ? 85 : 0);
|
||||
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("baking an asymmetric image puts the top of the image at the top of the model", "[TextureDisplacement]")
|
||||
{
|
||||
// An upright wall in the plane y = 0, facing -Y, tessellated so that each of the image's four
|
||||
// quadrants gets a vertex at its centre. A flat sheet rather than a cube because every vertex
|
||||
// normal is then exactly the face normal, which is what makes the expected depth below an exact
|
||||
// number instead of a tri-planar blend of three of them.
|
||||
constexpr int STEPS = 4; // cells per side, so 5x5 vertices
|
||||
constexpr float SPAN = 20.f; // mm, the wall's width and height
|
||||
indexed_triangle_set wall;
|
||||
for (int j = 0; j <= STEPS; ++j)
|
||||
for (int i = 0; i <= STEPS; ++i)
|
||||
wall.vertices.emplace_back(-SPAN / 2.f + SPAN * float(i) / float(STEPS), 0.f,
|
||||
SPAN * float(j) / float(STEPS));
|
||||
const auto at = [](int i, int j) { return j * (STEPS + 1) + i; };
|
||||
for (int j = 0; j < STEPS; ++j)
|
||||
for (int i = 0; i < STEPS; ++i) {
|
||||
// Wound so that (+x) x (+z) = -y: the wall faces -Y, away from the origin.
|
||||
wall.indices.emplace_back(at(i, j), at(i + 1, j), at(i + 1, j + 1));
|
||||
wall.indices.emplace_back(at(i, j), at(i + 1, j + 1), at(i, j + 1));
|
||||
}
|
||||
|
||||
TextureDisplacementFacetsData facets{};
|
||||
facets[0] = paint_whole_mesh(wall);
|
||||
|
||||
TextureDisplacementLayer layer;
|
||||
layer.slot = 0;
|
||||
layer.projection_method = TextureProjectionMethod::Triplanar;
|
||||
layer.image_data = make_quadrant_marker_png();
|
||||
layer.depth_mm = 2.f;
|
||||
// The wall is 20 mm across and one tile is 40 mm, so it covers the middle half of the image in
|
||||
// both axes: the quadrant boundaries land on x = 0 and z = 10, and the probed vertices sit a
|
||||
// whole number of texels away from both.
|
||||
layer.tiling_scale = 40.f;
|
||||
layer.offset = Vec2f(0.5f, 0.75f);
|
||||
// A single decal rather than a repeat, so that getting the orientation wrong cannot be masked by
|
||||
// the wrap bringing the flipped coordinate back into the image at a plausible-looking row. With
|
||||
// tiling off, uv stays inside [0.25, 0.75] in both axes only while v runs the right way.
|
||||
layer.tile_enabled = false;
|
||||
|
||||
const indexed_triangle_set result = build_texture_displacement(wall, { layer }, facets, classic_options());
|
||||
REQUIRE(result.vertices.size() == wall.vertices.size());
|
||||
|
||||
// Seen from outside this face the viewer's right is +X and their up is +Z, so the image's
|
||||
// top-left quadrant has to land at low x and high z. Each entry: the vertex, and the image value
|
||||
// whose quadrant it must have read.
|
||||
const std::array<std::tuple<const char *, int, int, int>, 4> expected = { {
|
||||
{ "top-left (255)", 1, 3, 255 },
|
||||
{ "top-right (170)", 3, 3, 170 },
|
||||
{ "bottom-left (85)", 1, 1, 85 },
|
||||
{ "bottom-right (0)", 3, 1, 0 },
|
||||
} };
|
||||
|
||||
for (const auto &[name, i, j, value] : expected) {
|
||||
DYNAMIC_SECTION("quadrant " << name)
|
||||
{
|
||||
const size_t vi = size_t(at(i, j));
|
||||
// The whole displacement is along -Y, so this is the relief's height at that vertex.
|
||||
const float raised = wall.vertices[vi].y() - result.vertices[vi].y();
|
||||
INFO("vertex at " << wall.vertices[vi].x() << ", " << wall.vertices[vi].z());
|
||||
CHECK_THAT(raised, WithinAbs(layer.depth_mm * float(value) / 255.f, 1e-3f));
|
||||
// Nothing moved sideways or vertically: a mirrored or rotated basis would still have
|
||||
// displaced along the normal, so this is here to catch the relief leaning instead.
|
||||
CHECK_THAT(result.vertices[vi].x(), WithinAbs(wall.vertices[vi].x(), 1e-4f));
|
||||
CHECK_THAT(result.vertices[vi].z(), WithinAbs(wall.vertices[vi].z(), 1e-4f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user