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())