From fbaed8a5121dad1549f0f227e04aad17031eb5d4 Mon Sep 17 00:00:00 2001 From: ExPikaPaka Date: Thu, 15 Oct 2026 15:45:47 +0200 Subject: [PATCH] Project the texture in the model's own orientation, and stop the preview tiling The bake frame took its orientation from the world, so turning a part on the plate slid the decoration across its surface. Turning a part is a print-time decision - clearance, supports, warping - and it should not be a design one. The frame now takes its orientation from the model and keeps world scale, so a tile is still tiling_scale printed millimetres whatever the instance's scale. The rotation is separated out by polar decomposition rather than by assuming the linear part is a rotation times a scale, since a sheared or mirrored placement is legal here. A reflection is deliberately left with the scale, so a mirrored instance still makes the frame's determinant negative and the bake still flips the normals it derives. The preview shaders rebuild that frame themselves from a world position, so they take the same rotation through a uniform; without it the fast preview and the bake would disagree, which is the class of bug this is fixing. The gizmo's own way back out to world carries the rotation too, which it did not need to when the frame was world-aligned. Tiling in the fast preview: - the sampler wrap mode was set on the height texture but not on the colour texture, which sits on its own unit and is sampled at the same uv. So with Tile off the relief stopped at one placement while the colour carried on repeating around it. - outside a single placement the colour then came out black, because a zero texture border cannot say "no colour here", only "the colour is black here". The shader now tests the uv the way sample_layer_color() does and leaves the surface its own colour. The placement gizmo is a tile of a planar projection, so it is offered only for the mappings that have one. An unwrap cuts the surface into islands and packs them, and a model's own UV channel is whatever its author drew: one tile lands there as an arbitrary curved patch with no rectangle to take hold of. Those two say so and point at the UV editor instead. --- .../110/texture_displacement_shaded.fs | 38 ++++++--- .../110/texture_displacement_uvcheck.fs | 7 +- .../140/texture_displacement_shaded.fs | 38 ++++++--- .../140/texture_displacement_uvcheck.fs | 7 +- src/libslic3r/TextureDisplacement.cpp | 34 ++++++-- src/libslic3r/TextureDisplacement.hpp | 4 + .../GUI/Gizmos/GLGizmoTextureDisplacement.cpp | 81 +++++++++++++++---- 7 files changed, 166 insertions(+), 43 deletions(-) diff --git a/resources/shaders/110/texture_displacement_shaded.fs b/resources/shaders/110/texture_displacement_shaded.fs index b4a9dd0fc3..06723a2d6a 100644 --- a/resources/shaders/110/texture_displacement_shaded.fs +++ b/resources/shaders/110/texture_displacement_shaded.fs @@ -36,6 +36,11 @@ uniform int palette_b[64]; uniform float prefer_pure_de; // PREFER_PURE_DE: how much better than a single filament a mix must be uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height uniform bool has_color_tex; +// False means the texture is a single placement, not a repeat. The sampler's wrap mode can express +// that for the *height* - a zero border reads as no displacement - but not for colour, because a +// zero border is indistinguishable from the image genuinely being black there. So the uv is tested +// instead, which is what sample_layer_color() does on the CPU side. +uniform bool tile_enabled; uniform bool volume_mirrored; uniform mat4 view_model_matrix; @@ -62,6 +67,12 @@ uniform bool invert; uniform float midlevel; // the height that means "don't move"; needed by the parallax step uniform vec3 eye_model_pos; // camera position in the texture frame (world minus tex_anchor) uniform vec3 tex_anchor; // the volume's origin in world space: the texture frame's origin +// The placement's rotation and its inverse, so this shader turns a world position into the texture +// frame exactly as texture_displacement_bake_frame() does. Both are passed rather than one +// transposed here, because GLSL 110 has no transpose(). Identity leaves the old world-aligned +// behaviour, which is what an unset uniform gives. +uniform mat3 tex_rot; // world -> texture frame +uniform mat3 tex_rot_inv; // texture frame -> world uniform bool use_vertex_uv; // 2x3 affine (lin = (m00, m01, m10, m11), tr = (m02, m12)) applied to the dragged island's uv; see the // 140 variant. Identity when nothing is dragged. @@ -261,9 +272,12 @@ void main() // World millimetres throughout, like the bake - see the 140 variant. vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz))); - vec3 tex_pos = world_pos.xyz - tex_anchor; // the frame the texture is projected in, as the bake does + vec3 tex_pos = tex_rot * (world_pos.xyz - tex_anchor); // the frame the texture is projected in, as the bake does if (volume_mirrored) triangle_normal = -triangle_normal; + // The same normal in that frame. triangle_normal itself stays in world, because the lighting at + // the end of this shader needs it there. + vec3 tex_normal = tex_rot * triangle_normal; // Where the colour is read from. Both branches below already compute the uv this fragment's // *height* came from - including the parallax-marched one on the triplanar path - and the colour @@ -294,14 +308,14 @@ void main() } else if (weight > 0.0) { vec3 t, b; vec2 units_per_mm; - projection_axes(tex_pos, triangle_normal, t, b, units_per_mm); + projection_axes(tex_pos, tex_normal, t, b, units_per_mm); // Parallax occlusion mapping: march the view ray through the height shell and shade at the // first point where it drops below the displaced surface (see header). float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0); - vec3 view_dir = normalize(eye_model_pos - tex_pos); - float v_dot_n = dot(view_dir, triangle_normal); - vec2 uv = project_uv(tex_pos, triangle_normal); + vec3 view_dir = normalize(tex_rot * eye_model_pos - tex_pos); + float v_dot_n = dot(view_dir, tex_normal); + vec2 uv = project_uv(tex_pos, tex_normal); // The shell the displaced surface lives inside, as signed heights along the normal. Taken from // both ends of h in [0, 1] so it stays correct for an inverted layer or a raised midlevel, @@ -313,18 +327,18 @@ void main() // How far, in mm, sweeping the ray across the shell slides the sample point sideways. Below half // a texel there is no parallax to find and the march would be pure cost - which is the common // case of looking straight down at a surface. - float sweep = length(view_dir - triangle_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4); + float sweep = length(view_dir - tex_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4); if (v_dot_n > 0.05 && sweep > 0.5 * min(tile_mm().x, tile_mm().y) * height_tex_texel.x) { // A point at ray parameter s (model_pos + view_dir * s) sits at height s * v_dot_n above the // undisplaced surface. Start at the top of the shell, where the ray is outside the surface // by construction, and step inward; the crossing is what this pixel actually sees. float s = h_hi / v_dot_n; float ds = (h_hi - h_lo) / (v_dot_n * float(PARALLAX_STEPS)); - vec2 prev_uv = project_uv(tex_pos + view_dir * s, triangle_normal); + vec2 prev_uv = project_uv(tex_pos + view_dir * s, tex_normal); float prev_gap = h_hi - amp * (H_AT(prev_uv) - midlevel); // >= 0 by construction for (int i = 0; i < PARALLAX_STEPS; ++i) { s -= ds; - vec2 cur_uv = project_uv(tex_pos + view_dir * s, triangle_normal); + vec2 cur_uv = project_uv(tex_pos + view_dir * s, tex_normal); float gap = s * v_dot_n - amp * (H_AT(cur_uv) - midlevel); if (gap <= 0.0) { // Crossed between the last two samples - interpolating the hit is what stops it @@ -360,7 +374,9 @@ void main() // ...and back out of raw-projection units into millimetres along t / b; see the 140 variant. vec2 slope = amplitude * vec2(g.x * cs + g.y * sn, -g.x * sn + g.y * cs) * units_per_mm; - vec3 gradient = slope.x * t + slope.y * b; + // t and b came out of the texture frame, so the gradient is in it too and has to come back + // to world before it perturbs the normal the lighting uses. + vec3 gradient = tex_rot_inv * (slope.x * t + slope.y * b); gradient -= triangle_normal * dot(triangle_normal, gradient); triangle_normal = normalize(triangle_normal - gradient); } @@ -380,7 +396,9 @@ void main() // Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as // the same material under the same light, and the relief this preview exists to show is unaffected. vec3 albedo = uniform_color.rgb; - if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0) + bool uv_covered = tile_enabled || + (all(greaterThanEqual(color_uv, vec2(0.0))) && all(lessThan(color_uv, vec2(1.0)))); + if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0 && uv_covered) albedo = palette_rgb[nearest_palette_entry(texture2D(color_tex, color_uv).rgb)]; gl_FragColor = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a); } diff --git a/resources/shaders/110/texture_displacement_uvcheck.fs b/resources/shaders/110/texture_displacement_uvcheck.fs index 0d3b52358e..2c8d395b7c 100644 --- a/resources/shaders/110/texture_displacement_uvcheck.fs +++ b/resources/shaders/110/texture_displacement_uvcheck.fs @@ -23,6 +23,9 @@ uniform float tiling_scale_v; uniform bool mirror_u; uniform bool mirror_v; uniform vec3 tex_anchor; // the volume's origin in world space +// The placement's rotation, so this overlay projects the same frame the bake does. Identity, which +// is what an unset uniform gives, leaves the old world-aligned behaviour. +uniform mat3 tex_rot; // world -> texture frame uniform float rotation_rad; uniform vec2 uv_offset; uniform bool use_vertex_uv; @@ -128,7 +131,7 @@ void main() // World space anchored at the volume's origin, like the bake and the shaded preview. vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz))); - vec3 tex_pos = world_pos.xyz - tex_anchor; + vec3 tex_pos = tex_rot * (world_pos.xyz - tex_anchor); if (volume_mirrored) triangle_normal = -triangle_normal; @@ -136,7 +139,7 @@ void main() if (mode == 1) { base = heatmap(distortion); } else { - vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, triangle_normal); + vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, tex_rot * triangle_normal); vec2 c = floor(uv * checker_freq); float check = mod(c.x + c.y, 2.0); base = (check < 0.5) ? vec3(0.22, 0.23, 0.26) : vec3(0.82, 0.83, 0.86); diff --git a/resources/shaders/140/texture_displacement_shaded.fs b/resources/shaders/140/texture_displacement_shaded.fs index ecf074661a..5bea73acf8 100644 --- a/resources/shaders/140/texture_displacement_shaded.fs +++ b/resources/shaders/140/texture_displacement_shaded.fs @@ -95,6 +95,11 @@ uniform int palette_b[64]; uniform float prefer_pure_de; // PREFER_PURE_DE: how much better than a single filament a mix must be uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height uniform bool has_color_tex; +// False means the texture is a single placement, not a repeat. The sampler's wrap mode can express +// that for the *height* - a zero border reads as no displacement - but not for colour, because a +// zero border is indistinguishable from the image genuinely being black there. So the uv is tested +// instead, which is what sample_layer_color() does on the CPU side. +uniform bool tile_enabled; uniform bool volume_mirrored; uniform mat4 view_model_matrix; @@ -121,6 +126,12 @@ uniform bool invert; uniform float midlevel; // the height that means "don't move"; needed by the parallax step uniform vec3 eye_model_pos; // camera position in the texture frame (world minus tex_anchor) uniform vec3 tex_anchor; // the volume's origin in world space: the texture frame's origin +// The placement's rotation and its inverse, so this shader turns a world position into the texture +// frame exactly as texture_displacement_bake_frame() does. Both are passed rather than one +// transposed here, because GLSL 110 has no transpose(). Identity leaves the old world-aligned +// behaviour, which is what an unset uniform gives. +uniform mat3 tex_rot; // world -> texture frame +uniform mat3 tex_rot_inv; // texture frame -> world uniform bool use_vertex_uv; // true: sample at vertex_uv with a derived tangent frame (LSCM) // A 2x3 affine (columns packed as lin = (m00, m01, m10, m11), tr = (m02, m12)) applied to the uv of // the island currently being dragged in the UV editor (island_active > 0.5). Identity when nothing is @@ -329,9 +340,12 @@ void main() // printed part whatever the instance's scale or rotation, so the preview has to project from the // world position and perturb the world normal. vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz))); - vec3 tex_pos = world_pos.xyz - tex_anchor; // the frame the texture is projected in, as the bake does + vec3 tex_pos = tex_rot * (world_pos.xyz - tex_anchor); // the frame the texture is projected in, as the bake does if (volume_mirrored) triangle_normal = -triangle_normal; + // The same normal in that frame. triangle_normal itself stays in world, because the lighting at + // the end of this shader needs it there. + vec3 tex_normal = tex_rot * triangle_normal; // Where the colour is read from. Both branches below already compute the uv this fragment's // *height* came from - including the parallax-marched one on the triplanar path - and the colour @@ -375,14 +389,14 @@ void main() // closed-form uv here, unlike the LSCM case. vec3 t, b; vec2 units_per_mm; - projection_axes(tex_pos, triangle_normal, t, b, units_per_mm); + projection_axes(tex_pos, tex_normal, t, b, units_per_mm); // Parallax occlusion mapping: march the view ray through the height shell and shade at the // first point where it drops below the displaced surface (see header). float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0); - vec3 view_dir = normalize(eye_model_pos - tex_pos); - float v_dot_n = dot(view_dir, triangle_normal); - vec2 uv = project_uv(tex_pos, triangle_normal); + vec3 view_dir = normalize(tex_rot * eye_model_pos - tex_pos); + float v_dot_n = dot(view_dir, tex_normal); + vec2 uv = project_uv(tex_pos, tex_normal); // The shell the displaced surface lives inside, as signed heights along the normal. Taken from // both ends of h in [0, 1] so it stays correct for an inverted layer or a raised midlevel, @@ -394,18 +408,18 @@ void main() // How far, in mm, sweeping the ray across the shell slides the sample point sideways. Below half // a texel there is no parallax to find and the march would be pure cost - which is the common // case of looking straight down at a surface. - float sweep = length(view_dir - triangle_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4); + float sweep = length(view_dir - tex_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4); if (v_dot_n > 0.05 && sweep > 0.5 * min(tile_mm().x, tile_mm().y) * height_tex_texel.x) { // A point at ray parameter s (model_pos + view_dir * s) sits at height s * v_dot_n above the // undisplaced surface. Start at the top of the shell, where the ray is outside the surface // by construction, and step inward; the crossing is what this pixel actually sees. float s = h_hi / v_dot_n; float ds = (h_hi - h_lo) / (v_dot_n * float(PARALLAX_STEPS)); - vec2 prev_uv = project_uv(tex_pos + view_dir * s, triangle_normal); + vec2 prev_uv = project_uv(tex_pos + view_dir * s, tex_normal); float prev_gap = h_hi - amp * (H_AT(prev_uv) - midlevel); // >= 0 by construction for (int i = 0; i < PARALLAX_STEPS; ++i) { s -= ds; - vec2 cur_uv = project_uv(tex_pos + view_dir * s, triangle_normal); + vec2 cur_uv = project_uv(tex_pos + view_dir * s, tex_normal); float gap = s * v_dot_n - amp * (H_AT(cur_uv) - midlevel); if (gap <= 0.0) { // Crossed between the last two samples - interpolating the hit is what stops it @@ -448,7 +462,9 @@ void main() // because units_per_mm is expressed in the t/b frame rather than in uv. vec2 slope = amplitude * vec2(g.x * cs + g.y * sn, -g.x * sn + g.y * cs) * units_per_mm; - vec3 gradient = slope.x * t + slope.y * b; + // t and b came out of the texture frame, so the gradient is in it too and has to come back + // to world before it perturbs the normal the lighting uses. + vec3 gradient = tex_rot_inv * (slope.x * t + slope.y * b); gradient -= triangle_normal * dot(triangle_normal, gradient); triangle_normal = normalize(triangle_normal - gradient); } @@ -468,7 +484,9 @@ void main() // Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as // the same material under the same light, and the relief this preview exists to show is unaffected. vec3 albedo = uniform_color.rgb; - if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0) + bool uv_covered = tile_enabled || + (all(greaterThanEqual(color_uv, vec2(0.0))) && all(lessThan(color_uv, vec2(1.0)))); + if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0 && uv_covered) albedo = palette_rgb[nearest_palette_entry(texture(color_tex, color_uv).rgb)]; out_color = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a); } diff --git a/resources/shaders/140/texture_displacement_uvcheck.fs b/resources/shaders/140/texture_displacement_uvcheck.fs index 0e0e9ed6b1..d06586bc73 100644 --- a/resources/shaders/140/texture_displacement_uvcheck.fs +++ b/resources/shaders/140/texture_displacement_uvcheck.fs @@ -32,6 +32,9 @@ uniform float tiling_scale_v; uniform bool mirror_u; uniform bool mirror_v; uniform vec3 tex_anchor; // the volume's origin in world space +// The placement's rotation, so this overlay projects the same frame the bake does. Identity, which +// is what an unset uniform gives, leaves the old world-aligned behaviour. +uniform mat3 tex_rot; // world -> texture frame uniform float rotation_rad; uniform vec2 uv_offset; uniform bool use_vertex_uv; @@ -140,7 +143,7 @@ void main() // World space anchored at the volume's origin, like the bake and the shaded preview. vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz))); - vec3 tex_pos = world_pos.xyz - tex_anchor; + vec3 tex_pos = tex_rot * (world_pos.xyz - tex_anchor); if (volume_mirrored) triangle_normal = -triangle_normal; @@ -148,7 +151,7 @@ void main() if (mode == 1) { base = heatmap(distortion); } else { - vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, triangle_normal); + vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, tex_rot * triangle_normal); vec2 c = floor(uv * checker_freq); float check = mod(c.x + c.y, 2.0); // Two distinct greys, plus a faint tint on one set so orientation is readable at a glance. diff --git a/src/libslic3r/TextureDisplacement.cpp b/src/libslic3r/TextureDisplacement.cpp index ece72bc16c..62915dcbca 100644 --- a/src/libslic3r/TextureDisplacement.cpp +++ b/src/libslic3r/TextureDisplacement.cpp @@ -1,6 +1,7 @@ #include "TextureDisplacement.hpp" #include +#include #include #include #include @@ -3599,13 +3600,36 @@ void texture_displacement_patch_frame(const indexed_triangle_set &patch, const s (an.y() <= an.x() && an.y() <= an.z()) ? Vec3f::UnitY() : Vec3f::UnitZ(); } +Matrix3d texture_displacement_frame_rotation(const Transform3d &volume_to_world) +{ + // Separated out by polar decomposition rather than by assuming the linear part is a clean + // rotation times a scale: a sheared or mirrored placement is legal here. A reflection is left + // behind with the scale on purpose, so a mirrored instance still makes the bake frame's + // determinant negative and the bake still knows to flip the normals it derives. + const Matrix3d linear = volume_to_world.linear(); + Eigen::JacobiSVD svd(linear, Eigen::ComputeFullU | Eigen::ComputeFullV); + Matrix3d rotation = svd.matrixU() * svd.matrixV().transpose(); + if (rotation.determinant() < 0.0) { + Matrix3d u = svd.matrixU(); + u.col(2) *= -1.0; + rotation = u * svd.matrixV().transpose(); + } + return rotation.transpose(); // world -> texture frame +} + Transform3d texture_displacement_bake_frame(const Transform3d &volume_to_world) { - // World orientation and scale, but the origin moved to where the volume's own origin sits: the - // texture then rides with the model when it is moved about the plate (and a single, untiled stamp - // starts on the model rather than at the plate's corner), while a tile is still `tiling_scale` - // printed millimetres whatever the instance's scale. - return Eigen::Translation3d(-volume_to_world.translation()) * volume_to_world; + // The model's own orientation, world scale, and the origin moved to where the volume's origin + // sits. So the texture rides with the model however it is moved *and* however it is turned, + // while a tile is still `tiling_scale` printed millimetres whatever the instance's scale, and a + // single untiled stamp starts on the model rather than at the plate's corner. + // + // Orientation used to come from the world too, which meant turning a part on the plate slid the + // decoration across its surface. Turning a part is a print-time decision - clearance, supports, + // warping - and it should not be a design one. + Transform3d unrotate = Transform3d::Identity(); + unrotate.linear() = texture_displacement_frame_rotation(volume_to_world); + return unrotate * Eigen::Translation3d(-volume_to_world.translation()) * volume_to_world; } Transform3d texture_displacement_volume_to_world(const ModelVolume &volume) diff --git a/src/libslic3r/TextureDisplacement.hpp b/src/libslic3r/TextureDisplacement.hpp index efd6b071d6..0947f5d168 100644 --- a/src/libslic3r/TextureDisplacement.hpp +++ b/src/libslic3r/TextureDisplacement.hpp @@ -1082,6 +1082,10 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set Transform3d texture_displacement_volume_to_world(const ModelVolume &volume); // The frame the bake and the previews project the texture in: `volume_to_world` with its translation // removed, i.e. world orientation and scale about the volume's own origin. See build_texture_displacement(). +// The rotation that carries a world direction into the texture's frame, i.e. the inverse of the +// placement's own rotation. Exposed because the preview shaders rebuild the frame themselves from a +// world position and have to turn it the same way the bake does, or the two disagree. +Matrix3d texture_displacement_frame_rotation(const Transform3d &volume_to_world); Transform3d texture_displacement_bake_frame(const Transform3d &volume_to_world); // Area-weighted vertex normals of `its` - the directions the bake both projects and displaces along. diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp index e4ce74e437..5f9d03621c 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp @@ -1609,14 +1609,20 @@ void GLGizmoTextureDisplacement::render_shaded_preview_mesh() const GLint wrap = !layer->tile_enabled ? no_tile_wrap : (layer->tile_method == TextureTileMethod::MirroredRepeat) ? GL_MIRRORED_REPEAT : GL_REPEAT; - glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrap)); - glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrap)); + // The colour image is a second texture on its own unit, sampled at the same uv, so it needs the + // same wrap or the two disagree: the relief stops at one placement while the colour carries on + // repeating around it, which is what "Tile off still tiles" looked like. + const auto set_tile_wrap = [wrap]() { + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrap)); + glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrap)); #if !SLIC3R_OPENGL_ES - if (wrap == GL_CLAMP_TO_BORDER) { - static const GLfloat border[4] = { 0.f, 0.f, 0.f, 0.f }; - glsafe(::glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border)); - } + if (wrap == GL_CLAMP_TO_BORDER) { + static const GLfloat border[4] = { 0.f, 0.f, 0.f, 0.f }; + glsafe(::glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border)); + } #endif // !SLIC3R_OPENGL_ES + }; + set_tile_wrap(); shader->set_uniform("height_tex", 0); shader->set_uniform("height_tex_texel", Vec2f(1.f / float(tex->get_width()), 1.f / float(tex->get_height()))); shader->set_uniform("depth_mm", layer->depth_mm); @@ -1624,6 +1630,9 @@ void GLGizmoTextureDisplacement::render_shaded_preview_mesh() shader->set_uniform("tiling_scale_v", layer_tile_mm(*layer).y()); shader->set_uniform("mirror_u", layer->mirror_u); shader->set_uniform("mirror_v", layer->mirror_v); + // The wrap mode above covers the height; the colour needs to be told as well, since "no texture + // here" and "the texture is black here" are the same sample but not the same result. + shader->set_uniform("tile_enabled", layer->tile_enabled); // Read off the uploaded texture rather than the decoded one: they are the same image, and this is // the aspect the sampler will actually see. shader->set_uniform("tex_aspect", float(tex->get_width()) / float(tex->get_height())); @@ -1641,6 +1650,12 @@ void GLGizmoTextureDisplacement::render_shaded_preview_mesh() const Vec3d tex_anchor = trafo_matrix.translation(); shader->set_uniform("tex_anchor", Vec3f(tex_anchor.cast())); shader->set_uniform("eye_model_pos", Vec3f((camera.get_position() - tex_anchor).cast())); + // The same turn texture_displacement_bake_frame() makes, so the preview projects the frame the + // bake will. Both directions: the shader needs the inverse to bring its gradient back to world, + // and GLSL 110 has no transpose(). + const Matrix3d tex_rot = texture_displacement_frame_rotation(trafo_matrix); + shader->set_uniform("tex_rot", Matrix3f(tex_rot.cast())); + shader->set_uniform("tex_rot_inv", Matrix3f(tex_rot.transpose().cast())); // When set, the shader samples at the per-vertex uv baked into the mesh (LSCM) rather than // projecting; see rebuild_shaded_preview_mesh(). shader->set_uniform("use_vertex_uv", m_shaded_preview_uses_vertex_uv); @@ -1676,6 +1691,7 @@ void GLGizmoTextureDisplacement::render_shaded_preview_mesh() shader->set_uniform("color_tex", 1); glsafe(::glActiveTexture(GL_TEXTURE1)); glsafe(::glBindTexture(GL_TEXTURE_2D, (GLuint) color_tex->get_id())); + set_tile_wrap(); // the same tiling the height is sampled with, see above glsafe(::glActiveTexture(GL_TEXTURE0)); } // The live UV-editor island drag rides this 2x3 affine (identity except mid-drag); only the flagged @@ -2182,6 +2198,7 @@ void GLGizmoTextureDisplacement::render_uvcheck_mesh() shader->set_uniform("view_normal_matrix", view_normal_matrix); shader->set_uniform("volume_mirrored", trafo_matrix.matrix().determinant() < 0.0); shader->set_uniform("tex_anchor", Vec3f(trafo_matrix.translation().cast())); // see the preview shader + shader->set_uniform("tex_rot", Matrix3f(texture_displacement_frame_rotation(trafo_matrix).cast())); shader->set_uniform("mode", m_uv_check_mode == UVCheckMode::Distortion ? 1 : 0); shader->set_uniform("checker_freq", 4.f); // squares per texture tile shader->set_uniform("tiling_scale", layer->tiling_scale); @@ -3574,18 +3591,37 @@ bool GLGizmoTextureDisplacement::adjust_basis(Transform3d &trafo, Vec3f &u_axis, { if (!m_adjust_anchor_valid) return false; - const ModelObject *mo = m_c->selection_info()->model_object(); - const ModelVolume *mv = texture_volume(); - if (mo == nullptr || mv == nullptr || active_layer() == nullptr) + const ModelObject *mo = m_c->selection_info()->model_object(); + const ModelVolume *mv = texture_volume(); + const TextureDisplacementLayer *layer = active_layer(); + if (mo == nullptr || mv == nullptr || layer == nullptr) + return false; + // The two mappings that take their uv from a *layout* rather than from a projection. An unwrap + // cuts the surface into islands and packs them, and a model's own UV channel is whatever its + // author drew, so one uv tile lands on the surface as an arbitrary curved region - there is no + // rectangle there for a handle to be, and drawing one would claim a placement that does not + // exist. Those have the UV editor and the model's own layout instead. + // + // Box is exact. Cylinder and Sphere share the planar frame but curve it around an axis, so the + // handle is an approximation there: it moves the texture correctly, the frame is only roughly + // where the tile falls. + // + // Gating here rather than at the button means a mapping changed while the mode is open simply + // stops drawing, instead of drawing somewhere wrong. + if (layer->projection_method == TextureProjectionMethod::LSCM || + layer->projection_method == TextureProjectionMethod::ModelUV) return false; Transform3d volume_to_world; if (!adjust_volume_to_world(volume_to_world)) return false; - // Everything the gizmo computes is already in the bake frame, and that frame is the world one - // with its origin moved to the volume's: so getting a point of it onto the screen is a - // translation and nothing more. No rotation or scale here is the whole point - they are what the - // tile's millimetres must not be measured through. + // Everything the gizmo computes is already in the bake frame, so this is only what carries a + // point of that frame back out to world space for projection: the placement's rotation, and the + // origin. Deliberately no scale - that is what a tile's millimetres must not be measured + // through, and leaving it out here is what keeps the drawn tile the size the texture actually + // tiles at. The rotation has to be here, though, since the frame itself now carries the model's + // orientation; when it did not, this was a pure translation. trafo = Transform3d::Identity(); + trafo.linear() = texture_displacement_frame_rotation(volume_to_world).transpose(); trafo.translation() = volume_to_world.translation(); adjust_tangent_basis(u_axis, v_axis); base = adjust_plane_point(); @@ -8318,11 +8354,28 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float { "blend_multiply.png", from_u8(blend_multiply) } }); ImGui::SameLine(0.f, gap_s); + // See adjust_basis(): the handle is a tile of a planar projection, which only Box + // mapping is. Leaving the mode switched on under another mapping would show a + // control that cannot move anything, so it is dropped here too. + const bool adjust_na = layer.projection_method == TextureProjectionMethod::LSCM || + layer.projection_method == TextureProjectionMethod::ModelUV; + if (adjust_na && m_adjust_texture_mode) { + m_adjust_texture_mode = false; + end_adjust_texture_mode(); + } if (icon_toggle(842, "texture_displacement_adjust.svg", m_adjust_texture_mode, icon_sm, _L("Adjust placement"), _L("Adjust placement - drag the tile on the model to move the texture, " "its edges to set the tile's width and height, a corner for both, " "or the ring to rotate it. Shift holds a move to one axis, and holds " - "a corner's proportions."))) { + "a corner's proportions."), + {}, + adjust_na ? _L("Not for a mapping that comes from a UV layout. An unwrap " + "cuts the surface into islands and packs them, and a model's " + "own UV channel is whatever its author drew, so one tile " + "lands on the surface as an arbitrary curved patch with no " + "rectangle to take hold of. Place it in the UV editor " + "instead, or with Tile size and Rotation.") : + wxString())) { if (!m_adjust_texture_mode) { update_model_object(); // flush any pending strokes before anchoring if (update_adjust_anchor())