mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-15 20:01:31 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbaed8a512 | ||
|
|
597b2abac4 | ||
|
|
ba4de8c1fc | ||
|
|
7bce53a773 | ||
|
|
e65113af6d | ||
|
|
91ab04e823 |
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
@@ -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;
|
||||
@@ -45,6 +50,14 @@ uniform sampler2D height_tex;
|
||||
uniform vec2 height_tex_texel;
|
||||
uniform float depth_mm;
|
||||
uniform float tiling_scale;
|
||||
// Tile extent along v, in mm. Zero or negative means "the same as tiling_scale" - the square tile
|
||||
// every layer had before the two axes could differ - which is also what layer_tile_mm() does on the
|
||||
// CPU side, so an unset uniform behaves exactly as this shader always did.
|
||||
uniform float tiling_scale_v;
|
||||
// Mirror the texture along one axis of the tile. Both default to false, which GLSL guarantees for an
|
||||
// uninitialised bool uniform, so a shader whose caller has not set them behaves exactly as before.
|
||||
uniform bool mirror_u;
|
||||
uniform bool mirror_v;
|
||||
// Height map width / height. Scales the v axis so a non-square image keeps its proportions
|
||||
// instead of being squeezed into a square tile - mirrors libslic3r's apply_uv_transform().
|
||||
uniform float tex_aspect;
|
||||
@@ -54,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.
|
||||
@@ -162,13 +181,29 @@ void projection_axes(vec3 p, vec3 n, out vec3 t, out vec3 b, out vec2 units_per_
|
||||
}
|
||||
}
|
||||
|
||||
// Tile extents in mm, u then v, with the v fallback layer_tile_mm() applies.
|
||||
vec2 tile_mm()
|
||||
{
|
||||
float su = (tiling_scale > 1e-6) ? tiling_scale : 1.0;
|
||||
return vec2(su, (tiling_scale_v > 1e-6) ? tiling_scale_v : su);
|
||||
}
|
||||
|
||||
// +1 or -1 per axis, matching layer_mirror_sign() on the CPU side.
|
||||
vec2 mirror_sign()
|
||||
{
|
||||
return vec2(mirror_u ? -1.0 : 1.0, mirror_v ? -1.0 : 1.0);
|
||||
}
|
||||
|
||||
vec2 project_uv(vec3 p, vec3 n)
|
||||
{
|
||||
vec2 planar = projection_raw(p, n);
|
||||
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||
float cs = cos(rotation_rad);
|
||||
float sn = sin(rotation_rad);
|
||||
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
|
||||
// Extents divide after the rotation, so a tile whose two sides differ stays a rotated rectangle
|
||||
// instead of shearing. With a square tile the two orders agree exactly. The mirror is a sign on
|
||||
// the same division, in the tile's own frame.
|
||||
r *= mirror_sign() / tile_mm();
|
||||
// After the rotation, so the rotation stays a rotation rather than becoming a shear.
|
||||
r.y *= tex_aspect;
|
||||
return r + uv_offset;
|
||||
@@ -237,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
|
||||
@@ -270,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,
|
||||
@@ -289,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);
|
||||
if (v_dot_n > 0.05 && sweep > 0.5 * tiling_scale * height_tex_texel.x) {
|
||||
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
|
||||
@@ -322,18 +360,23 @@ void main()
|
||||
float hU = texture2D(height_tex, uv + vec2(0.0, height_tex_texel.y)).r;
|
||||
|
||||
vec2 dh_duv = vec2((hR - hL) / (2.0 * height_tex_texel.x), (hU - hD) / (2.0 * height_tex_texel.y));
|
||||
float inv_tiling = (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0);
|
||||
vec2 tile = tile_mm();
|
||||
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
|
||||
|
||||
float cs = cos(rotation_rad);
|
||||
float sn = sin(rotation_rad);
|
||||
// One uv unit is tiling_scale mm along u but tiling_scale / tex_aspect mm along v, so the v
|
||||
// component of the gradient carries the extra factor before being rotated back into t/b.
|
||||
vec2 g = vec2(dh_duv.x, dh_duv.y * tex_aspect);
|
||||
// One uv unit is tile.x mm along u but tile.y / tex_aspect mm along v, so each component of
|
||||
// the uv-space gradient carries its own factor before being rotated back into t/b.
|
||||
// Each component carries its own extent and its own mirror sign, both of which project_uv()
|
||||
// applied on the way in; without the sign a mirrored layer would light as though it were not.
|
||||
vec2 ms = mirror_sign();
|
||||
vec2 g = vec2(dh_duv.x * ms.x / tile.x, dh_duv.y * tex_aspect * ms.y / tile.y);
|
||||
// ...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);
|
||||
}
|
||||
@@ -353,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);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,16 @@ uniform bool volume_mirrored;
|
||||
uniform int mode;
|
||||
uniform float checker_freq;
|
||||
uniform float tiling_scale;
|
||||
// Tile extent along v, in mm. Zero or negative means "the same as tiling_scale" - see
|
||||
// layer_tile_mm() on the CPU side, so an unset uniform behaves exactly as this shader always did.
|
||||
uniform float tiling_scale_v;
|
||||
// Mirror along one tile axis; false by default, so an unset uniform is the old behaviour.
|
||||
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;
|
||||
@@ -79,13 +88,30 @@ vec2 projection_raw(vec3 p, vec3 n)
|
||||
return vec2(box_face_sign(n.z) * p.x, -p.y);
|
||||
}
|
||||
|
||||
|
||||
// Tile extents in mm, u then v, with the v fallback layer_tile_mm() applies.
|
||||
vec2 tile_mm()
|
||||
{
|
||||
float su = (tiling_scale > 1e-6) ? tiling_scale : 1.0;
|
||||
return vec2(su, (tiling_scale_v > 1e-6) ? tiling_scale_v : su);
|
||||
}
|
||||
|
||||
// +1 or -1 per axis, matching layer_mirror_sign() on the CPU side.
|
||||
vec2 mirror_sign()
|
||||
{
|
||||
return vec2(mirror_u ? -1.0 : 1.0, mirror_v ? -1.0 : 1.0);
|
||||
}
|
||||
|
||||
vec2 project_uv(vec3 p, vec3 n)
|
||||
{
|
||||
vec2 planar = projection_raw(p, n);
|
||||
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||
float cs = cos(rotation_rad);
|
||||
float sn = sin(rotation_rad);
|
||||
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
|
||||
// Extents divide after the rotation, so a tile whose two sides differ stays a rotated rectangle
|
||||
// instead of shearing. With a square tile the two orders agree exactly. The mirror is a sign on
|
||||
// the same division, in the tile's own frame.
|
||||
r *= mirror_sign() / tile_mm();
|
||||
r.y *= tex_aspect; // after the rotation, so the rotation stays a rotation rather than a shear
|
||||
return r + uv_offset;
|
||||
}
|
||||
@@ -105,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;
|
||||
|
||||
@@ -113,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);
|
||||
|
||||
@@ -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;
|
||||
@@ -104,6 +109,14 @@ uniform sampler2D height_tex;
|
||||
uniform vec2 height_tex_texel; // (1/width, 1/height) of height_tex
|
||||
uniform float depth_mm;
|
||||
uniform float tiling_scale;
|
||||
// Tile extent along v, in mm. Zero or negative means "the same as tiling_scale" - the square tile
|
||||
// every layer had before the two axes could differ - which is also what layer_tile_mm() does on the
|
||||
// CPU side, so an unset uniform behaves exactly as this shader always did.
|
||||
uniform float tiling_scale_v;
|
||||
// Mirror the texture along one axis of the tile. Both default to false, which GLSL guarantees for an
|
||||
// uninitialised bool uniform, so a shader whose caller has not set them behaves exactly as before.
|
||||
uniform bool mirror_u;
|
||||
uniform bool mirror_v;
|
||||
// Height map width / height. Scales the v axis so a non-square image keeps its proportions
|
||||
// instead of being squeezed into a square tile - mirrors libslic3r's apply_uv_transform().
|
||||
uniform float tex_aspect;
|
||||
@@ -113,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
|
||||
@@ -228,13 +247,29 @@ void projection_axes(vec3 p, vec3 n, out vec3 t, out vec3 b, out vec2 units_per_
|
||||
}
|
||||
}
|
||||
|
||||
// Tile extents in mm, u then v, with the v fallback layer_tile_mm() applies.
|
||||
vec2 tile_mm()
|
||||
{
|
||||
float su = (tiling_scale > 1e-6) ? tiling_scale : 1.0;
|
||||
return vec2(su, (tiling_scale_v > 1e-6) ? tiling_scale_v : su);
|
||||
}
|
||||
|
||||
// +1 or -1 per axis, matching layer_mirror_sign() on the CPU side.
|
||||
vec2 mirror_sign()
|
||||
{
|
||||
return vec2(mirror_u ? -1.0 : 1.0, mirror_v ? -1.0 : 1.0);
|
||||
}
|
||||
|
||||
vec2 project_uv(vec3 p, vec3 n)
|
||||
{
|
||||
vec2 planar = projection_raw(p, n);
|
||||
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||
float cs = cos(rotation_rad);
|
||||
float sn = sin(rotation_rad);
|
||||
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
|
||||
// Extents divide after the rotation, so a tile whose two sides differ stays a rotated rectangle
|
||||
// instead of shearing. With a square tile the two orders agree exactly. The mirror is a sign on
|
||||
// the same division, in the tile's own frame.
|
||||
r *= mirror_sign() / tile_mm();
|
||||
// After the rotation, so the rotation stays a rotation rather than becoming a shear.
|
||||
r.y *= tex_aspect;
|
||||
return r + uv_offset;
|
||||
@@ -305,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
|
||||
@@ -351,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,
|
||||
@@ -370,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);
|
||||
if (v_dot_n > 0.05 && sweep > 0.5 * tiling_scale * height_tex_texel.x) {
|
||||
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
|
||||
@@ -405,23 +443,28 @@ void main()
|
||||
// Central difference, per uv unit (not per texel).
|
||||
vec2 dh_duv = vec2((hR - hL) / (2.0 * height_tex_texel.x), (hU - hD) / (2.0 * height_tex_texel.y));
|
||||
|
||||
// uv -> mm is 1/tiling_scale for the triplanar projection, so this turns the uv-space
|
||||
// gradient into a real surface slope.
|
||||
float inv_tiling = (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0);
|
||||
// uv -> mm is the reciprocal of the tile extents, applied per component below, which is what
|
||||
// turns the uv-space gradient into a real surface slope.
|
||||
vec2 tile = tile_mm();
|
||||
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
|
||||
// uv was rotated by project_uv() while t/b are the unrotated model axes, so rotate the
|
||||
// gradient back into the axes' frame.
|
||||
float cs = cos(rotation_rad);
|
||||
float sn = sin(rotation_rad);
|
||||
// One uv unit is tiling_scale mm along u but tiling_scale / tex_aspect mm along v, so the v
|
||||
// component of the gradient carries the extra factor before being rotated back into t/b.
|
||||
vec2 g = vec2(dh_duv.x, dh_duv.y * tex_aspect);
|
||||
// One uv unit is tile.x mm along u but tile.y / tex_aspect mm along v, so each component of
|
||||
// the uv-space gradient carries its own factor before being rotated back into t/b.
|
||||
// Each component carries its own extent and its own mirror sign, both of which project_uv()
|
||||
// applied on the way in; without the sign a mirrored layer would light as though it were not.
|
||||
vec2 ms = mirror_sign();
|
||||
vec2 g = vec2(dh_duv.x * ms.x / tile.x, dh_duv.y * tex_aspect * ms.y / tile.y);
|
||||
// ...and back out of raw-projection units into millimetres of travel along t / b, which is a
|
||||
// no-op except for the spherical longitude (see projection_axes()). After the inverse rotation,
|
||||
// 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);
|
||||
}
|
||||
@@ -441,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);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,16 @@ uniform bool volume_mirrored;
|
||||
uniform int mode; // 0 checker, 1 distortion
|
||||
uniform float checker_freq; // checker squares per uv unit (one uv unit == one texture tile)
|
||||
uniform float tiling_scale;
|
||||
// Tile extent along v, in mm. Zero or negative means "the same as tiling_scale" - see
|
||||
// layer_tile_mm() on the CPU side, so an unset uniform behaves exactly as this shader always did.
|
||||
uniform float tiling_scale_v;
|
||||
// Mirror along one tile axis; false by default, so an unset uniform is the old behaviour.
|
||||
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;
|
||||
@@ -90,13 +99,30 @@ vec2 projection_raw(vec3 p, vec3 n)
|
||||
return vec2(box_face_sign(n.z) * p.x, -p.y);
|
||||
}
|
||||
|
||||
|
||||
// Tile extents in mm, u then v, with the v fallback layer_tile_mm() applies.
|
||||
vec2 tile_mm()
|
||||
{
|
||||
float su = (tiling_scale > 1e-6) ? tiling_scale : 1.0;
|
||||
return vec2(su, (tiling_scale_v > 1e-6) ? tiling_scale_v : su);
|
||||
}
|
||||
|
||||
// +1 or -1 per axis, matching layer_mirror_sign() on the CPU side.
|
||||
vec2 mirror_sign()
|
||||
{
|
||||
return vec2(mirror_u ? -1.0 : 1.0, mirror_v ? -1.0 : 1.0);
|
||||
}
|
||||
|
||||
vec2 project_uv(vec3 p, vec3 n)
|
||||
{
|
||||
vec2 planar = projection_raw(p, n);
|
||||
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||
float cs = cos(rotation_rad);
|
||||
float sn = sin(rotation_rad);
|
||||
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
|
||||
// Extents divide after the rotation, so a tile whose two sides differ stays a rotated rectangle
|
||||
// instead of shearing. With a square tile the two orders agree exactly. The mirror is a sign on
|
||||
// the same division, in the tile's own frame.
|
||||
r *= mirror_sign() / tile_mm();
|
||||
r.y *= tex_aspect; // after the rotation, so the rotation stays a rotation rather than a shear
|
||||
return r + uv_offset;
|
||||
}
|
||||
@@ -117,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;
|
||||
|
||||
@@ -125,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.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "TextureDisplacement.hpp"
|
||||
|
||||
#include <Eigen/Core>
|
||||
#include <Eigen/SVD>
|
||||
#include <Eigen/Geometry>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -722,7 +723,10 @@ V2Resolution recommend_v2_resolution(const indexed_triangle_set
|
||||
const DecodedHeightTexture &tex = decode_height_texture(layer);
|
||||
if (tex.width <= 0)
|
||||
continue;
|
||||
const float texel = layer.tiling_scale / float(tex.width);
|
||||
// Texels are square in both axes (the aspect correction is what keeps them so), so the finer
|
||||
// of the two tile extents sets the finest texel the refinement has to resolve.
|
||||
const Vec2f tile = layer_tile_mm(layer);
|
||||
const float texel = std::min(tile.x(), tile.y()) / float(tex.width);
|
||||
if (out.texel_mm <= 0.f || texel < out.texel_mm) {
|
||||
out.texel_mm = texel;
|
||||
out.pixels_per_edge = analyze_texture_detail(layer).pixels_per_edge;
|
||||
@@ -2164,7 +2168,7 @@ namespace {
|
||||
// of them was most of the per-sample arithmetic.
|
||||
struct UVTransform
|
||||
{
|
||||
float scale, cs, sn, aspect;
|
||||
float inv_u, inv_v, cs, sn, aspect;
|
||||
Vec2f offset;
|
||||
|
||||
// `normalized_uv` drops the two steps that only mean something for a coordinate measured on the
|
||||
@@ -2172,7 +2176,18 @@ struct UVTransform
|
||||
// rotation and offset. See apply_uv_transform(); TextureProjectionMethod::ModelUV is what needs it.
|
||||
UVTransform(const TextureDisplacementLayer &layer, float aspect_, bool normalized_uv = false)
|
||||
{
|
||||
scale = (normalized_uv || layer.tiling_scale <= 1e-6f) ? 1.f : (1.f / layer.tiling_scale);
|
||||
// The mirror folds into the reciprocal: it is a negation of the same coordinate, so there is
|
||||
// nothing to do per sample beyond carrying the sign here. A normalized uv has no millimetre
|
||||
// tiling to divide by, but a mirror is still a mirror, so the sign applies either way.
|
||||
const Vec2f sign = layer_mirror_sign(layer);
|
||||
if (normalized_uv) {
|
||||
inv_u = sign.x();
|
||||
inv_v = sign.y();
|
||||
} else {
|
||||
const Vec2f tile = layer_tile_mm(layer);
|
||||
inv_u = sign.x() / tile.x();
|
||||
inv_v = sign.y() / tile.y();
|
||||
}
|
||||
const float rad = layer.rotation_deg * float(M_PI) / 180.f;
|
||||
cs = std::cos(rad);
|
||||
sn = std::sin(rad);
|
||||
@@ -2181,9 +2196,11 @@ struct UVTransform
|
||||
}
|
||||
Vec2f operator()(const Vec2f &planar) const
|
||||
{
|
||||
const Vec2f scaled = planar * scale;
|
||||
Vec2f rotated(scaled.x() * cs - scaled.y() * sn, scaled.x() * sn + scaled.y() * cs);
|
||||
// See apply_uv_transform() for why the aspect correction follows the rotation.
|
||||
// See apply_uv_transform() for why both the tile extents and the aspect correction follow
|
||||
// the rotation rather than preceding it.
|
||||
Vec2f rotated(planar.x() * cs - planar.y() * sn, planar.x() * sn + planar.y() * cs);
|
||||
rotated.x() *= inv_u;
|
||||
rotated.y() *= inv_v;
|
||||
if (aspect > 0.f && aspect != 1.f)
|
||||
rotated.y() *= aspect;
|
||||
return rotated + offset;
|
||||
@@ -2203,26 +2220,43 @@ inline Vec3f triplanar_weights(const Vec3f &normal)
|
||||
|
||||
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect, bool normalized_uv)
|
||||
{
|
||||
// A uv that is already normalized has no millimetre scale to divide by and no aspect to correct -
|
||||
// see the header. UVTransform does the same, and the two must stay in step.
|
||||
const float scale = (normalized_uv || layer.tiling_scale <= 1e-6f) ? 1.f : (1.f / layer.tiling_scale);
|
||||
const Vec2f scaled = planar * scale;
|
||||
|
||||
const float rad = layer.rotation_deg * float(M_PI) / 180.f;
|
||||
const float cs = std::cos(rad);
|
||||
const float sn = std::sin(rad);
|
||||
Vec2f rotated(scaled.x() * cs - scaled.y() * sn, scaled.x() * sn + scaled.y() * cs);
|
||||
Vec2f rotated(planar.x() * cs - planar.y() * sn, planar.x() * sn + planar.y() * cs);
|
||||
|
||||
// The tile extents. Dividing here rather than before the rotation is what makes a non-square
|
||||
// tile a rotated *rectangle* on the surface: scaling the two axes of an already-rotated
|
||||
// coordinate by different amounts would be a shear, so the rotation has to come first and the
|
||||
// extents divide in the tile's own frame. With a square tile the two orders agree exactly,
|
||||
// R being linear - which is why this is not a change in behaviour for any existing layer.
|
||||
//
|
||||
// The mirror rides along as a sign on the same division. It is in the tile's own frame too, so
|
||||
// it mirrors along the tile's axes rather than the world's, and because the offset is what
|
||||
// positions the tile, mirroring about the tile's middle is a matter of the offset the gizmo
|
||||
// writes alongside it, not of anything here.
|
||||
// A uv that is already normalized has no millimetre scale to divide by - see the header - but it
|
||||
// can still be mirrored, so the sign is applied in both cases and only the extents are skipped.
|
||||
// UVTransform does the same, and the two must stay in step.
|
||||
const Vec2f sign = layer_mirror_sign(layer);
|
||||
if (normalized_uv) {
|
||||
rotated.x() *= sign.x();
|
||||
rotated.y() *= sign.y();
|
||||
} else {
|
||||
const Vec2f tile = layer_tile_mm(layer);
|
||||
rotated.x() *= sign.x() / tile.x();
|
||||
rotated.y() *= sign.y() / tile.y();
|
||||
}
|
||||
|
||||
// Non-square textures. Without this the [0,1] square of uv covers the whole image whatever its
|
||||
// proportions, so a 2:1 image is squeezed into a square tile and every feature in it comes out
|
||||
// half as wide as it should be. `tiling_scale` is the tile's size along u; the tile is
|
||||
// `tiling_scale * height / width` mm along v, which is exactly what keeps texels square - so
|
||||
// dividing v by that extent is the same as multiplying it by width / height. A square texture has
|
||||
// aspect 1 and is untouched, which is why this changes nothing for the shipped library.
|
||||
// proportions, so a 2:1 image is squeezed into one tile and every feature in it comes out half
|
||||
// as wide as it should be. The tile is `layer_tile_mm().y() * height / width` mm along v, which
|
||||
// is exactly what keeps texels square - so dividing v by that extent is the same as multiplying
|
||||
// it by width / height. A square texture has aspect 1 and is untouched, which is why this
|
||||
// changes nothing for the shipped library.
|
||||
//
|
||||
// Applied after the rotation, not before: scaling one axis of an already-rotated coordinate is a
|
||||
// shear, and doing it the other way round would make "Rotation" skew the pattern instead of
|
||||
// turning it.
|
||||
// Applied after the rotation, like the extents, and for the same reason: scaling one axis of an
|
||||
// already-rotated coordinate before turning it would be a shear.
|
||||
if (!normalized_uv && aspect > 0.f && aspect != 1.f)
|
||||
rotated.y() *= aspect;
|
||||
|
||||
@@ -3566,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<Matrix3d> 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)
|
||||
@@ -5783,7 +5840,13 @@ std::string texture_displacement_layers_to_json(const std::vector<TextureDisplac
|
||||
|
||||
j["depth_mm"] = l.depth_mm;
|
||||
j["tiling_scale"] = l.tiling_scale;
|
||||
j["rotation_deg"] = l.rotation_deg;
|
||||
// The v extent and the mirroring. Written unconditionally, including the <= 0 "same as u"
|
||||
// sentinel, so a project says plainly that the tile was square rather than leaving a reader
|
||||
// to infer it from a missing key.
|
||||
j["tiling_scale_v"] = l.tiling_scale_v;
|
||||
j["mirror_u"] = l.mirror_u;
|
||||
j["mirror_v"] = l.mirror_v;
|
||||
j["rotation_deg"] = l.rotation_deg;
|
||||
j["offset"] = vec2_to_json(l.offset);
|
||||
j["invert"] = l.invert;
|
||||
j["midlevel"] = l.midlevel;
|
||||
@@ -5891,6 +5954,12 @@ bool texture_displacement_layers_from_json(const std::string
|
||||
|
||||
read(j, "depth_mm", l.depth_mm);
|
||||
read(j, "tiling_scale", l.tiling_scale);
|
||||
// Absent in a project written before the tile could be rectangular or mirrored, and
|
||||
// then each keeps its default: -1 for the v extent, which layer_tile_mm() reads as
|
||||
// "square", and false for the mirrors. So an older project loads as what it was.
|
||||
read(j, "tiling_scale_v", l.tiling_scale_v);
|
||||
read(j, "mirror_u", l.mirror_u);
|
||||
read(j, "mirror_v", l.mirror_v);
|
||||
read(j, "rotation_deg", l.rotation_deg);
|
||||
if (const auto o = j.find("offset"); o != j.end())
|
||||
l.offset = vec2_from_json(*o, l.offset);
|
||||
|
||||
@@ -225,7 +225,17 @@ struct TextureDisplacementLayer
|
||||
std::shared_ptr<std::vector<unsigned char>> image_data;
|
||||
|
||||
float depth_mm = 0.4f; // maximum displacement along the surface normal, in mm
|
||||
float tiling_scale = 12.5f; // size of one texture tile, in mm
|
||||
float tiling_scale = 12.5f; // size of one texture tile along u, in mm
|
||||
// Size of one tile along v, in mm. Anything <= 0 means "whatever tiling_scale is", which is both
|
||||
// the default and what a layer created before the tile could be non-square meant - so an older
|
||||
// undo snapshot still comes back as the square tile it was.
|
||||
float tiling_scale_v = -1.f;
|
||||
// Mirror the texture along one axis of the tile. Set by dragging a width or height handle back
|
||||
// through the tile's middle, which is how a side handle behaves in an image editor: the extent
|
||||
// cannot go negative, so the sign lives here instead. Both default to off, which is what every
|
||||
// layer written before this meant.
|
||||
bool mirror_u = false;
|
||||
bool mirror_v = false;
|
||||
float rotation_deg = 0.f;
|
||||
Vec2f offset = Vec2f::Zero();
|
||||
bool invert = false;
|
||||
@@ -362,7 +372,8 @@ struct TextureDisplacementLayer
|
||||
static_cast<int>(tile_method), static_cast<int>(projection_method), lscm_seam_angle_deg, islands,
|
||||
static_cast<int>(blend_mode), midlevel, island_padding_mm, lscm_seam_edges, view_project_right,
|
||||
view_project_up, smoothing, edge_smoothing, edge_smoothing_amount, auto_connect_islands, island_groups,
|
||||
lscm_uv_overrides, view_project_projective, view_project_matrix, color_enabled);
|
||||
lscm_uv_overrides, view_project_projective, view_project_matrix, color_enabled, tiling_scale_v,
|
||||
mirror_u, mirror_v);
|
||||
}
|
||||
template<class Archive> void load(Archive &ar)
|
||||
{
|
||||
@@ -374,7 +385,7 @@ struct TextureDisplacementLayer
|
||||
tile_method_int, projection_method_int, lscm_seam_angle_deg, islands, blend_mode_int, midlevel,
|
||||
island_padding_mm, lscm_seam_edges, view_project_right, view_project_up, smoothing, edge_smoothing,
|
||||
edge_smoothing_amount, auto_connect_islands, island_groups, lscm_uv_overrides, view_project_projective,
|
||||
view_project_matrix, color_enabled);
|
||||
view_project_matrix, color_enabled, tiling_scale_v, mirror_u, mirror_v);
|
||||
image_data = blob.empty() ? nullptr : std::make_shared<std::vector<unsigned char>>(blob.begin(), blob.end());
|
||||
tile_method = static_cast<TextureTileMethod>(tile_method_int);
|
||||
projection_method = static_cast<TextureProjectionMethod>(projection_method_int);
|
||||
@@ -383,6 +394,25 @@ struct TextureDisplacementLayer
|
||||
};
|
||||
|
||||
|
||||
// The layer's tile extents in mm, u first. The v extent falls back to the u one whenever
|
||||
// tiling_scale_v is unset, so "unset means square" is written down once and every consumer - the
|
||||
// bake, the preview shaders, the UV pane and the placement gizmo - agrees on what a layer with no
|
||||
// v extent means.
|
||||
inline Vec2f layer_tile_mm(const TextureDisplacementLayer &layer)
|
||||
{
|
||||
const float su = (layer.tiling_scale > 1e-6f) ? layer.tiling_scale : 1.f;
|
||||
return Vec2f(su, (layer.tiling_scale_v > 1e-6f) ? layer.tiling_scale_v : su);
|
||||
}
|
||||
|
||||
// +1 or -1 per axis, for the layer's mirroring. A separate factor from the extents rather than a
|
||||
// signed extent: the extents are a size the panel shows and the refinement measures texels against,
|
||||
// and letting one of them go negative would mean every one of those places had to remember to take
|
||||
// the magnitude.
|
||||
inline Vec2f layer_mirror_sign(const TextureDisplacementLayer &layer)
|
||||
{
|
||||
return Vec2f(layer.mirror_u ? -1.f : 1.f, layer.mirror_v ? -1.f : 1.f);
|
||||
}
|
||||
|
||||
// Settings that apply to the whole layer stack rather than to one layer, held per ModelVolume next
|
||||
// to texture_displacement_layers and consumed by build_texture_displacement().
|
||||
struct TextureDisplacementOptions
|
||||
@@ -668,14 +698,19 @@ Vec2f project_planar(const Vec3f &position, const Vec3f &normal);
|
||||
// projection method, without going through project_texture_displacement_uv()'s own dispatch
|
||||
// (which only knows how to compute the *analytic* methods from a single vertex + normal).
|
||||
// `aspect` is the height map's width / height. It scales the v axis so a non-square image is not
|
||||
// squeezed into a square tile: `tiling_scale` is the tile's size along u, and the tile is
|
||||
// `tiling_scale * height / width` mm along v, which keeps texels square. 1 (the default) is the
|
||||
// square case and leaves the coordinate exactly as it always was.
|
||||
// squeezed into a square tile: the tile is `layer_tile_mm().y() * height / width` mm along v, which
|
||||
// keeps texels square. 1 (the default) is the square case and leaves the coordinate exactly as it
|
||||
// always was.
|
||||
//
|
||||
// The tile extents divide *after* the rotation, not before. With one extent that made no difference
|
||||
// (R is linear, so R(p / s) == R(p) / s), but with two it is what keeps a non-square tile a rotated
|
||||
// rectangle on the surface instead of a sheared parallelogram. The layer's mirroring is a sign on
|
||||
// that same division.
|
||||
//
|
||||
// `normalized_uv` says the coordinate is already a uv rather than a millimetre measurement on the
|
||||
// model - which is what TextureProjectionMethod::ModelUV hands in. Then neither the tiling scale (mm
|
||||
// model - which is what TextureProjectionMethod::ModelUV hands in. Then neither the tile extents (mm
|
||||
// per tile is meaningless for it) nor the aspect correction (the artist's layout already accounts for
|
||||
// the image's proportions) applies, and only the rotation and offset are left.
|
||||
// the image's proportions) applies, and only the rotation, the mirroring and the offset are left.
|
||||
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect = 1.f,
|
||||
bool normalized_uv = false);
|
||||
|
||||
@@ -1047,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.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -508,25 +508,120 @@ private:
|
||||
// is where the live adjustment is applied. Called whenever the panel moves or reverts it.
|
||||
void invalidate_adjust_textures();
|
||||
|
||||
// "Adjust Texture" mode: instead of painting, dragging an on-canvas handle changes the active
|
||||
// layer's offset. The handle is a flat panel lying in the paint patch's own tangent plane
|
||||
// (a "pan" - drag anywhere on it for free 2D movement), plus two arrows along the patch's
|
||||
// own U/V axes that constrain the drag to just that one axis for precise nudging. Anchored to
|
||||
// the centroid/average-normal of the active layer's current paint patch (see
|
||||
// libslic3r::compute_layer_paint_anchor()), so nothing is drawn if it has nothing painted yet.
|
||||
// "Adjust Texture" mode: instead of painting, the active layer's placement is a control lying
|
||||
// on the model. The gizmo is one tile of the texture in the paint patch's own tangent plane:
|
||||
// where it sits is the layer's offset, how it is turned is rotation_deg, how big it is is the
|
||||
// tile extents - so each handle is one of the values it edits rather than a stand-in for it.
|
||||
// Anchored to the centroid/average-normal of the active layer's paint patch (see
|
||||
// libslic3r::compute_layer_paint_anchor()), so nothing is drawn until something is painted.
|
||||
//
|
||||
// NOTE: the drag direction/sign below is this session's best-effort reasoning about which way
|
||||
// the texture should appear to move as the handle is dragged - it could not be visually
|
||||
// confirmed while writing it (no way to render/see pixels in this environment), so it may
|
||||
// need a one-line sign flip once actually tested.
|
||||
// Drawn through ImGui's foreground draw list rather than the "flat" shader, which is what gives
|
||||
// it antialiased sub-pixel strokes, rounded joins, a contrast halo and the numeric readout. The
|
||||
// cost is that render_adjust_texture_gizmo() has to be called from inside the gizmo's ImGui
|
||||
// frame (see on_render_input_window()), not from on_render() with the rest of the GL drawing.
|
||||
// Move is the whole interior, free in both axes. AxisMove is the four arrows, each holding a
|
||||
// move to one of the tile's axes. EdgeScale is the four edge bars, one tile extent each, and
|
||||
// CornerScale the four corner grips, which take both extents at once.
|
||||
//
|
||||
// The arrows and the bars share the same four rays out of the tile, so they are kept apart by
|
||||
// distance rather than direction: the bar hugs its edge, then a clear gap, then the arrow well
|
||||
// outside it. adjust_pick() leans on that - see the order it tests them in.
|
||||
enum class AdjustHandle { None, Move, AxisMove, EdgeScale, CornerScale, Rotate };
|
||||
|
||||
// The gizmo's geometry for one frame, in screen pixels. Worked out once and shared by the
|
||||
// drawing and the hit test, so the two cannot disagree about where a handle is. Everything
|
||||
// except the frame itself is a fixed number of pixels, which is what keeps the control equally
|
||||
// usable on a 5 mm part and a 500 mm one - the old handle scaled with the volume's bounding box
|
||||
// and so was unusable at both ends.
|
||||
struct AdjustFrame
|
||||
{
|
||||
bool valid = false;
|
||||
Vec2d center = Vec2d::Zero();
|
||||
// +u+v, +u-v, -u-v, -u+v. A scale grip sits on each.
|
||||
std::array<Vec2d, 4> corner{};
|
||||
// +u, -u, +v, -v: the middle of each edge, and the two ends of the bar that sits just
|
||||
// outside it. The bar's ends are kept rather than recomputed so the drawing and the hit test
|
||||
// cannot end up with it in two different places.
|
||||
std::array<Vec2d, 4> edge_mid{};
|
||||
std::array<Vec2d, 4> bar_a{};
|
||||
std::array<Vec2d, 4> bar_b{};
|
||||
// The axis arrow further out on the same ray: where its shaft starts and where its chevron
|
||||
// ends. Kept, like the bar's ends, so the drawing and the hit test cannot disagree.
|
||||
std::array<Vec2d, 4> arm_base{};
|
||||
std::array<Vec2d, 4> arm_tip{};
|
||||
std::array<Vec2d, 4> arm_dir{}; // unit, pointing outward from the centre
|
||||
// A ring is placed off every corner, but only one is drawn and only that one can be picked:
|
||||
// four marks for a single operation is clutter, and whichever corner the cursor is near is
|
||||
// the one worth offering. `rotate_shown` says which, and see update_rotate_corner() for why
|
||||
// it does not simply track the nearest.
|
||||
std::array<Vec2d, 4> rotate_at{};
|
||||
int rotate_shown = 0;
|
||||
// The tile projects to less than ADJUST_MIN_TILE_PX across, so the frame was drawn at that
|
||||
// floor instead. It is dashed to say so, and the readout still gives the true millimetres.
|
||||
bool clamped = false;
|
||||
// What the frame was built from, in mesh-local mm: the tile's own (rotated) edge directions
|
||||
// and its extents. The drag arithmetic reuses these rather than recomputing them, so it
|
||||
// cannot end up working against a different tile than the one on screen.
|
||||
Vec3f e_u = Vec3f::UnitX(), e_v = Vec3f::UnitY();
|
||||
Vec2f tile_mm = Vec2f::Ones(); // (u, v) as stored on the layer
|
||||
Vec2f half_planar = Vec2f::Ones(); // half extents in planar mm, v already aspect-corrected
|
||||
Vec2f center_planar = Vec2f::Zero();
|
||||
};
|
||||
|
||||
bool update_adjust_anchor(); // recomputes m_adjust_anchor_pos/normal; false if nothing painted
|
||||
// The selected instance's volume-to-world transform, which the bake frame is derived from.
|
||||
bool adjust_volume_to_world(Transform3d &out) const;
|
||||
// The anchor, carried from mesh-local into the bake frame. Converted on every use rather than
|
||||
// stored converted: the object can be moved, rotated or scaled while this mode is open, and a
|
||||
// stored copy would quietly describe the shape it used to be. The expensive part - finding the
|
||||
// paint patch's centroid - stays cached in the local members.
|
||||
bool adjust_anchor_in_frame(Vec3f &pos, Vec3f &normal) const;
|
||||
// Drops every scrap of live gizmo state. Called from each of the several ways out of the mode -
|
||||
// the panel's toggle, collapsing the card, switching to seam marking, closing the gizmo - so a
|
||||
// stale hover highlight or a half-finished drag cannot survive into the next time it is opened.
|
||||
void end_adjust_texture_mode();
|
||||
bool on_mouse_adjust_texture(const wxMouseEvent &mouse_event);
|
||||
void render_adjust_texture_gizmo();
|
||||
// This frame's gizmo geometry, or an invalid frame when there is nothing to draw.
|
||||
AdjustFrame adjust_frame() const;
|
||||
// Which handle is under `mouse`, with `index` set to the arm or corner it belongs to. Tested
|
||||
// outermost first; see the hit-area board of the design for the order and the radii.
|
||||
AdjustHandle adjust_pick(const Vec2d &mouse, const AdjustFrame &f, int &index) const;
|
||||
// Chooses which corner carries the rotate ring. The nearest to the cursor, but it only moves once
|
||||
// the newcomer is clearly nearer, because without that margin the ring flits between two corners
|
||||
// every time the cursor wanders along the line equidistant from both. Never moves during a drag.
|
||||
void update_rotate_corner(const AdjustFrame &f, const Vec2d &mouse);
|
||||
|
||||
// Where the tile's centre sits, in planar mm relative to the anchor, for a layer's placement.
|
||||
// Not the patch centroid: the gizmo *is* the texture's placement, so it has to travel as the
|
||||
// offset changes - pinning it to the centroid is what made the old handle look broken, the
|
||||
// texture sliding while the handle stayed put.
|
||||
static Vec2f adjust_handle_planar(const TextureDisplacementLayer &layer, float aspect);
|
||||
// The exact inverse: the offset that puts the tile's centre at `planar`. Rotating or resizing
|
||||
// the tile changes where a given offset places it, so every drag recomputes the offset from the
|
||||
// centre it wants to keep - which is what makes rotate and scale act *in place* rather than
|
||||
// swinging the tile around the anchor.
|
||||
static Vec2f adjust_offset_for_planar(const Vec2f &planar, float rot_deg, const Vec2f &tile_mm,
|
||||
const Vec2f &mirror_sign, float aspect);
|
||||
// The mesh-local-to-world transform of the selected instance, the (unrotated) tangent basis and
|
||||
// the lifted anchor the whole gizmo is built on. False when there is nothing to build on - which
|
||||
// is the single place that decides the gizmo is not drawable, so the drawing, the hit test and
|
||||
// the drag cannot disagree about it.
|
||||
bool adjust_basis(Transform3d &trafo, Vec3f &u_axis, Vec3f &v_axis, Vec3f &base) const;
|
||||
// Screen-space quad of a tile with the given placement. Used for the pre-drag ghost, which is
|
||||
// drawn from the captured start placement rather than from the layer, the layer having already
|
||||
// been moved by then.
|
||||
bool adjust_quad_for(const Vec2f ¢er_planar, float rot_deg, const Vec2f &tile_mm,
|
||||
std::array<Vec2d, 4> &out) const;
|
||||
// Where the cursor meets the drag plane, in planar mm relative to the anchor. The plane is
|
||||
// deliberately the *fixed* anchor plane rather than the tile's current position: measuring a
|
||||
// drag against a plane the drag itself moves is a feedback loop.
|
||||
bool adjust_planar_under(const Vec2d &mouse, Vec2f &planar) const;
|
||||
// Draws a small '+'/'-' next to the mouse over the 3D view while painting/selecting, so it is
|
||||
// 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: box_projection_axes(), so dragging
|
||||
// Bake-frame 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;
|
||||
|
||||
@@ -1118,23 +1213,47 @@ private:
|
||||
// See the "Adjust Texture" block of private methods above.
|
||||
bool m_adjust_texture_mode = false;
|
||||
bool m_adjust_anchor_valid = false;
|
||||
Vec3f m_adjust_anchor_pos = Vec3f::Zero(); // mesh-local
|
||||
Vec3f m_adjust_anchor_normal = Vec3f::UnitZ(); // mesh-local
|
||||
// Mesh-local, as compute_layer_paint_anchor() produces them. Everything else about the gizmo
|
||||
// works in the bake frame - world orientation and scale, origin at the volume's own, which is
|
||||
// where the texture is projected (texture_displacement_bake_frame()) - so these go through
|
||||
// adjust_anchor_in_frame() at the point of use. A tile is so many *printed* millimetres whatever
|
||||
// the instance's scale, which is why measuring the gizmo in mesh-local millimetres made its tile
|
||||
// differ from the rendered one by exactly that scale.
|
||||
Vec3f m_adjust_anchor_pos = Vec3f::Zero();
|
||||
Vec3f m_adjust_anchor_normal = Vec3f::UnitZ();
|
||||
|
||||
// Pan: free drag anywhere on the flat panel, moves offset along both axes. AxisU/AxisV: drag
|
||||
// the corresponding arrow, moves offset along only that one axis.
|
||||
enum class AdjustHandle { None, Pan, AxisU, AxisV };
|
||||
AdjustHandle m_adjust_drag_handle = AdjustHandle::None;
|
||||
Vec2f m_adjust_drag_start_offset = Vec2f::Zero();
|
||||
// Anchor-relative planar position (see project_planar()) of the point under the mouse at the
|
||||
// moment the current drag started; every subsequent frame's delta is measured against this,
|
||||
// rather than accumulated frame-to-frame, to avoid drift.
|
||||
Vec2f m_adjust_drag_start_planar = Vec2f::Zero();
|
||||
// What the cursor is over, and what is being dragged. `index` picks the arm or the corner
|
||||
// (0..3, in AdjustFrame's order); it is -1 for Move and Rotate, which have only one of each.
|
||||
// Only ever one of each is set, so two handles can never both look live.
|
||||
// Which corner currently carries the rotate ring. Persists between frames, which is what gives
|
||||
// update_rotate_corner() something to apply its margin against.
|
||||
int m_adjust_rotate_corner = 0;
|
||||
AdjustHandle m_adjust_hover_kind = AdjustHandle::None;
|
||||
int m_adjust_hover_index = -1;
|
||||
AdjustHandle m_adjust_drag_kind = AdjustHandle::None;
|
||||
int m_adjust_drag_index = -1;
|
||||
|
||||
// Lazily-built unit quad (the pan panel) and unit line-with-arrowhead (reused, rotated, for
|
||||
// both the U and V axis arrows), transformed into place at render time.
|
||||
GLModel m_adjust_panel_glmodel;
|
||||
GLModel m_adjust_arrow_glmodel;
|
||||
// Everything the live drag is measured against, captured once on mouse-down. Every frame's
|
||||
// delta is taken from these rather than accumulated frame to frame, so a long drag cannot
|
||||
// drift away from the cursor.
|
||||
Vec2f m_adjust_drag_start_center = Vec2f::Zero(); // tile centre, planar mm
|
||||
Vec2f m_adjust_drag_start_grab = Vec2f::Zero(); // the grabbed point, planar mm
|
||||
// How far the grab landed from the handle's own position, in planar mm. A scale drag reads the
|
||||
// extents straight off the cursor, so without subtracting this the tile would jump by however
|
||||
// far inside the generous hit radius the click happened to fall.
|
||||
Vec2f m_adjust_drag_grab_bias = Vec2f::Zero();
|
||||
float m_adjust_drag_start_rot = 0.f;
|
||||
Vec2f m_adjust_drag_start_tile = Vec2f::Ones(); // (u, v) extents as stored
|
||||
float m_adjust_drag_start_angle = 0.f; // planar angle of the grab, radians
|
||||
// The mirroring the layer had when the drag began. A width or height handle dragged back through
|
||||
// the tile's middle toggles its axis against this, rather than against whatever the last frame
|
||||
// left behind, so dragging back and forth across the middle cannot ratchet the flag.
|
||||
bool m_adjust_drag_start_mirror_u = false;
|
||||
bool m_adjust_drag_start_mirror_v = false;
|
||||
|
||||
// The live value to put in the readout pill, built by the drag and drawn by the renderer in the
|
||||
// same frame. Empty when no drag is running, which is also when no pill is drawn.
|
||||
std::string m_adjust_readout;
|
||||
|
||||
std::map<std::string, wxString> m_desc;
|
||||
|
||||
|
||||
@@ -297,9 +297,12 @@ void UVEditorCanvas::set_island_fill_colors(std::vector<ColorRGBA> colors)
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void UVEditorCanvas::set_uv_transform(float tiling_scale, float rotation_deg, bool tile_enabled, bool tile_mirrored)
|
||||
void UVEditorCanvas::set_uv_transform(float tiling_scale, float tiling_scale_v, const Vec2f &mirror_sign,
|
||||
float rotation_deg, bool tile_enabled, bool tile_mirrored)
|
||||
{
|
||||
m_tiling_scale = (std::abs(tiling_scale) > 1e-6f) ? tiling_scale : 1.f;
|
||||
m_tiling_scale_v = (tiling_scale_v > 1e-6f) ? tiling_scale_v : m_tiling_scale;
|
||||
m_mirror_sign = mirror_sign;
|
||||
m_rotation_deg = rotation_deg;
|
||||
m_tile_enabled = tile_enabled;
|
||||
m_tile_mirrored = tile_mirrored;
|
||||
@@ -661,11 +664,18 @@ Vec2f UVEditorCanvas::island_centroid(int island) const
|
||||
|
||||
Vec2f UVEditorCanvas::uv_delta_to_unwrap(const Vec2f &delta_uv) const
|
||||
{
|
||||
// apply_uv_transform() maps unwrap -> uv as uv = R(unwrap / tiling) + offset, so the inverse of
|
||||
// a *delta* (the translation drops out) is unwrap = tiling * R^-1(delta_uv).
|
||||
// apply_uv_transform() maps unwrap -> uv as uv = S^-1 R(unwrap) + offset, with S the tile
|
||||
// extents, so the inverse of a *delta* (the translation drops out) is unwrap = R^-1(S delta_uv).
|
||||
// The extents therefore multiply *before* the inverse rotation, which is the mirror image of the
|
||||
// forward order; with a square tile the two are interchangeable, with a rectangular one they are
|
||||
// not.
|
||||
const float rad = m_rotation_deg * float(M_PI) / 180.f;
|
||||
const float cs = std::cos(rad), sn = std::sin(rad);
|
||||
return Vec2f(delta_uv.x() * cs + delta_uv.y() * sn, -delta_uv.x() * sn + delta_uv.y() * cs) * m_tiling_scale;
|
||||
// The mirror is its own sign on the same axis, and a negation is its own inverse, so it comes
|
||||
// back out here exactly as it went in.
|
||||
const Vec2f scaled(delta_uv.x() * m_tiling_scale * m_mirror_sign.x(),
|
||||
delta_uv.y() * m_tiling_scale_v * m_mirror_sign.y());
|
||||
return Vec2f(scaled.x() * cs + scaled.y() * sn, -scaled.x() * sn + scaled.y() * cs);
|
||||
}
|
||||
|
||||
Vec2f UVEditorCanvas::snap_correction(int island) const
|
||||
@@ -2180,7 +2190,10 @@ UVEditorPanel::UVEditorPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY)
|
||||
void UVEditorPanel::apply_state(const UVEditorCanvas::PaneState &s)
|
||||
{
|
||||
const wxString name = s.has_layer ? s.layer_name : _L("No layer mapped with Unwrap");
|
||||
const wxString tile = s.has_layer ? wxString::Format(_L("%.1f mm tile"), s.tile_mm) : wxString();
|
||||
// A square tile reads as one number; only a genuinely rectangular one is worth two.
|
||||
const wxString tile = !s.has_layer ? wxString() :
|
||||
std::abs(s.tile_mm_v - s.tile_mm) < 0.005f ? wxString::Format(_L("%.1f mm tile"), s.tile_mm) :
|
||||
wxString::Format(_L("%.1f x %.1f mm tile"), s.tile_mm, s.tile_mm_v);
|
||||
const bool relayout = m_layer_name->GetLabel() != name || m_tile->GetLabel() != tile;
|
||||
m_layer_name->SetLabel(name);
|
||||
m_tile->SetLabel(tile);
|
||||
|
||||
@@ -91,7 +91,10 @@ public:
|
||||
// space, back into the unwrap's mm space - which is where a TextureIsland's offset actually
|
||||
// lives (see apply_uv_transform()). The tile settings come along because the background has to
|
||||
// repeat exactly the way the height sampler does, or the pane would stop showing what gets baked.
|
||||
void set_uv_transform(float tiling_scale, float rotation_deg, bool tile_enabled, bool tile_mirrored);
|
||||
// `tiling_scale_v` is the tile's extent along v; pass <= 0 for a square tile, matching
|
||||
// layer_tile_mm().
|
||||
void set_uv_transform(float tiling_scale, float tiling_scale_v, const Vec2f &mirror_sign,
|
||||
float rotation_deg, bool tile_enabled, bool tile_mirrored);
|
||||
|
||||
// Same 8-bit grayscale pixels build_texture_displacement()'s height sampling uses, shown
|
||||
// beneath the wireframe (expanded to RGBA on upload) so the unwrap can be checked against the
|
||||
@@ -129,7 +132,8 @@ public:
|
||||
{
|
||||
bool has_layer = false; // an active layer mapped with Unwrap
|
||||
wxString layer_name;
|
||||
float tile_mm = 0.f;
|
||||
float tile_mm = 0.f; // tile extent along u
|
||||
float tile_mm_v = 0.f; // along v; equal to tile_mm for a square tile
|
||||
std::vector<unsigned char> thumbnail_rgb; // thumbnail_px square, 3 bytes per pixel; empty for none
|
||||
int thumbnail_px = 0;
|
||||
float seam_angle_deg = 40.f;
|
||||
@@ -292,8 +296,10 @@ private:
|
||||
GLModel m_grid_glmodel;
|
||||
float m_grid_step = 0.f; // the UV step m_grid_glmodel was built for; 0 = not built
|
||||
|
||||
float m_tiling_scale = 1.f;
|
||||
float m_rotation_deg = 0.f;
|
||||
float m_tiling_scale = 1.f;
|
||||
float m_tiling_scale_v = 1.f; // already resolved: never the "unset" sentinel, see set_uv_transform()
|
||||
Vec2f m_mirror_sign = Vec2f::Ones(); // +1 or -1 per axis, see layer_mirror_sign()
|
||||
float m_rotation_deg = 0.f;
|
||||
bool m_tile_enabled = true;
|
||||
bool m_tile_mirrored = false;
|
||||
bool m_snap_enabled = false;
|
||||
|
||||
Reference in New Issue
Block a user