ChromaStack: the layer card, the swaps and the preview shader

Add layer asks which kind of layer it is making - a relief layer, a height
map pushed into the surface, or a ChromaStack picture - and the layer
keeps the answer, so a card never shows a control that cannot do anything
on it. Depth, Midlevel, Smoothing, Invert, Edge fade, Tile and Blend are
absent from a picture card: each would move the relief's steps off the
print's layer boundaries, or has nothing to act on. Tile size becomes
Picture size. A picture cannot be reordered, since anything printed over
it buries the colours, and an object can hold only one, refused when the
layer is added rather than when its swaps would overwrite the first one's.

The card is the feature in one block: the picture and the predicted print
side by side, how far the prediction lands from the picture, the base
measured from the painted face, a row per band with the filament's name
and an editable TD, and the colours the stack reaches as a ramp - computed
from the stack alone, so it answers whether these filaments can print this
picture before a solve is spent finding out they cannot.

The swaps go onto the object as height ranges with extruder and
layer_height set, so they are object-relative, saved in the project and
visible in the object list; ObjectList::rebuild_layer_ranges() mirrors
them in the tree without touching the selection, which inside a gizmo
would tear the gizmo down.

The colours exist nowhere else - they are blends of translucent filaments,
not filaments, so the scene's per-triangle filament assignment cannot
express them - so the gizmo draws them itself, with a shader that reads
each fragment's own print Z. A swap happens at one Z across the whole
plate, so that is what decides a surface's colour, a band edge lands on
the layer boundary however coarse the mesh is, and it still works after a
bake when the height map is attached to nothing on screen. On top of the
colour it shades like printed plastic: a wrapped terminator and a grazing
glow scaled by the top filament's TD, and layer lines on the walls, all
fading out through fwidth once a pixel covers more than half a layer.
This commit is contained in:
ExPikaPaka
2026-10-16 11:44:33 +02:00
parent e0a6000d50
commit 8baeba1341
10 changed files with 1566 additions and 127 deletions
+92
View File
@@ -0,0 +1,92 @@
#version 110
// What a ChromaStack print looks like, shaded per fragment.
//
// The colour is not a vertex attribute and not a texture on the surface: it is read from the
// fragment's own height. A filament swap happens at one print Z across the whole plate, so the layer
// a point sits in is what decides its colour - which means a band edge lands exactly on the layer
// boundary however coarse the mesh is, and the same shader is right before a bake (on the preview's
// displaced mesh) and after one (on the volume's own).
//
// On top of that it tries to look like printed plastic rather than like flat shaded colour: the
// terminator is wrapped and the grazing angles glow, because filament translucent enough to be worth
// stacking scatters light rather than reflecting it; and the walls carry layer lines, because that is
// the first thing the eye uses to read a print as a print.
#define INTENSITY_CORRECTION 0.6
const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);
#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION)
#define LIGHT_TOP_SPECULAR (0.125 * INTENSITY_CORRECTION)
#define LIGHT_TOP_SHININESS 20.0
const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074);
#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION)
#define INTENSITY_AMBIENT 0.3
// The stack's colours, one texel per printed layer, bottom to top. A texture rather than a uniform
// array because a base can be several hundred layers and no uniform array survives that.
uniform sampler2D palette;
uniform float palette_size; // texels in `palette`, i.e. the stack's total printed layers
uniform float base_z; // world Z of the bottom of the object
uniform float layer_height; // mm
// How translucent the stack reads, 0 to 1, from the transmission distance of the filament on top.
// It drives the wrap and the rim: an opaque stack should not glow.
uniform float translucency;
uniform float alpha;
varying vec3 clipping_planes_dots;
varying vec3 eye_normal;
varying vec3 eye_position;
varying vec3 world_normal;
varying float world_z;
void main()
{
if (any(lessThan(clipping_planes_dots, vec3(0.0))))
discard;
float z_rel = world_z - base_z;
float layers = max(z_rel / layer_height, 0.0);
// How much of a layer one pixel covers. Everything below is faded out as this grows: layer lines
// finer than a pixel are noise, and a colour read from one layer out of the dozen under a pixel
// flickers as the camera moves.
float px = fwidth(layers);
// The layer this fragment sits in, 1-based. Up close that is an exact texel of the palette; once a
// pixel spans a layer or more it slides to a continuous coordinate so the linear filter averages
// the bands it covers instead of picking one of them at random.
float stepped = clamp(ceil(layers - 1.0e-4), 1.0, palette_size) - 0.5;
float smooth_ = clamp(layers, 0.5, palette_size - 0.5);
float u = mix(stepped, smooth_, clamp(px - 0.5, 0.0, 1.0)) / palette_size;
vec3 albedo = texture2D(palette, vec2(u, 0.5)).rgb;
vec3 N = normalize(eye_normal);
vec3 V = normalize(-eye_position);
// Layer lines, on the walls only: a terrace is one layer's top surface and has none to show. The
// groove runs round the part at every layer boundary, so it is a function of height alone.
float wall = 1.0 - abs(world_normal.z);
float phase = fract(layers);
float groove = smoothstep(0.0, 0.16, phase) * smoothstep(1.0, 0.84, phase);
groove = mix(groove, 0.65, clamp(px * 1.5, 0.0, 1.0)); // fade to flat when too fine to resolve
float lines = mix(1.0, 0.84 + 0.16 * groove, wall * 0.9);
albedo *= lines;
// Diffuse, with a wrapped terminator: light entering translucent filament comes back out a little
// way round the curve, so the shaded side never goes as dark as an opaque surface's would.
float wrap = 0.25 + 0.45 * translucency;
float ndotl = dot(N, LIGHT_TOP_DIR);
float diffuse = max((ndotl + wrap) / (1.0 + wrap), 0.0) * LIGHT_TOP_DIFFUSE;
diffuse += max((dot(N, LIGHT_FRONT_DIR) + wrap) / (1.0 + wrap), 0.0) * LIGHT_FRONT_DIFFUSE;
// Specular, strongest along a layer line, which is what makes a printed wall glint in stripes
// rather than evenly.
float spec = LIGHT_TOP_SPECULAR * pow(max(dot(V, reflect(-LIGHT_TOP_DIR, N)), 0.0), LIGHT_TOP_SHININESS);
spec *= mix(1.0, 0.35 + 0.65 * groove, wall * 0.9);
// Scattering out of the edges: a translucent part lights up where it is seen at a grazing angle
// and where it is thin. Tinted by the filament, because what comes back out has been through it.
float rim = pow(1.0 - clamp(dot(N, V), 0.0, 1.0), 4.0);
vec3 color = albedo * (INTENSITY_AMBIENT + diffuse) + vec3(spec) + albedo * rim * translucency * 0.55;
gl_FragColor = vec4(color, alpha);
}
+40
View File
@@ -0,0 +1,40 @@
#version 110
uniform mat4 view_model_matrix;
uniform mat4 projection_matrix;
uniform mat3 view_normal_matrix;
uniform mat4 volume_world_matrix;
// The volume's normal matrix in world space, so the fragment shader can tell a wall from a terrace -
// which is what decides whether layer lines show at all.
uniform mat3 world_normal_matrix;
// Clipping plane, x = min z, y = max z (the FFF/SLA preview's top/bottom cut).
uniform vec2 z_range;
// Clipping plane, general orientation (the section view).
uniform vec4 clipping_plane;
// GLModel's P3N3 layout, which is what init_from(indexed_triangle_set) produces - this shader needs
// nothing per vertex that the mesh does not already carry, because the colour comes from the
// fragment's own height rather than from anything baked into the vertices.
attribute vec3 v_position;
attribute vec3 v_normal;
varying vec3 clipping_planes_dots;
varying vec3 eye_normal;
varying vec3 eye_position;
varying vec3 world_normal;
varying float world_z;
void main()
{
vec4 model_pos = vec4(v_position, 1.0);
vec4 world_pos = volume_world_matrix * model_pos;
world_z = world_pos.z;
world_normal = normalize(world_normal_matrix * v_normal);
eye_normal = normalize(view_normal_matrix * v_normal);
vec4 eye_pos = view_model_matrix * model_pos;
eye_position = eye_pos.xyz;
gl_Position = projection_matrix * eye_pos;
clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z);
}
+94
View File
@@ -0,0 +1,94 @@
#version 140
// What a ChromaStack print looks like, shaded per fragment.
//
// The colour is not a vertex attribute and not a texture on the surface: it is read from the
// fragment's own height. A filament swap happens at one print Z across the whole plate, so the layer
// a point sits in is what decides its colour - which means a band edge lands exactly on the layer
// boundary however coarse the mesh is, and the same shader is right before a bake (on the preview's
// displaced mesh) and after one (on the volume's own).
//
// On top of that it tries to look like printed plastic rather than like flat shaded colour: the
// terminator is wrapped and the grazing angles glow, because filament translucent enough to be worth
// stacking scatters light rather than reflecting it; and the walls carry layer lines, because that is
// the first thing the eye uses to read a print as a print.
#define INTENSITY_CORRECTION 0.6
const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);
#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION)
#define LIGHT_TOP_SPECULAR (0.125 * INTENSITY_CORRECTION)
#define LIGHT_TOP_SHININESS 20.0
const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074);
#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION)
#define INTENSITY_AMBIENT 0.3
// The stack's colours, one texel per printed layer, bottom to top. A texture rather than a uniform
// array because a base can be several hundred layers and no uniform array survives that.
uniform sampler2D palette;
uniform float palette_size; // texels in `palette`, i.e. the stack's total printed layers
uniform float base_z; // world Z of the bottom of the object
uniform float layer_height; // mm
// How translucent the stack reads, 0 to 1, from the transmission distance of the filament on top.
// It drives the wrap and the rim: an opaque stack should not glow.
uniform float translucency;
uniform float alpha;
in vec3 clipping_planes_dots;
in vec3 eye_normal;
in vec3 eye_position;
in vec3 world_normal;
in float world_z;
out vec4 out_color;
void main()
{
if (any(lessThan(clipping_planes_dots, vec3(0.0))))
discard;
float z_rel = world_z - base_z;
float layers = max(z_rel / layer_height, 0.0);
// How much of a layer one pixel covers. Everything below is faded out as this grows: layer lines
// finer than a pixel are noise, and a colour read from one layer out of the dozen under a pixel
// flickers as the camera moves.
float px = fwidth(layers);
// The layer this fragment sits in, 1-based. Up close that is an exact texel of the palette; once a
// pixel spans a layer or more it slides to a continuous coordinate so the linear filter averages
// the bands it covers instead of picking one of them at random.
float stepped = clamp(ceil(layers - 1.0e-4), 1.0, palette_size) - 0.5;
float smooth_ = clamp(layers, 0.5, palette_size - 0.5);
float u = mix(stepped, smooth_, clamp(px - 0.5, 0.0, 1.0)) / palette_size;
vec3 albedo = texture(palette, vec2(u, 0.5)).rgb;
vec3 N = normalize(eye_normal);
vec3 V = normalize(-eye_position);
// Layer lines, on the walls only: a terrace is one layer's top surface and has none to show. The
// groove runs round the part at every layer boundary, so it is a function of height alone.
float wall = 1.0 - abs(world_normal.z);
float phase = fract(layers);
float groove = smoothstep(0.0, 0.16, phase) * smoothstep(1.0, 0.84, phase);
groove = mix(groove, 0.65, clamp(px * 1.5, 0.0, 1.0)); // fade to flat when too fine to resolve
float lines = mix(1.0, 0.84 + 0.16 * groove, wall * 0.9);
albedo *= lines;
// Diffuse, with a wrapped terminator: light entering translucent filament comes back out a little
// way round the curve, so the shaded side never goes as dark as an opaque surface's would.
float wrap = 0.25 + 0.45 * translucency;
float ndotl = dot(N, LIGHT_TOP_DIR);
float diffuse = max((ndotl + wrap) / (1.0 + wrap), 0.0) * LIGHT_TOP_DIFFUSE;
diffuse += max((dot(N, LIGHT_FRONT_DIR) + wrap) / (1.0 + wrap), 0.0) * LIGHT_FRONT_DIFFUSE;
// Specular, strongest along a layer line, which is what makes a printed wall glint in stripes
// rather than evenly.
float spec = LIGHT_TOP_SPECULAR * pow(max(dot(V, reflect(-LIGHT_TOP_DIR, N)), 0.0), LIGHT_TOP_SHININESS);
spec *= mix(1.0, 0.35 + 0.65 * groove, wall * 0.9);
// Scattering out of the edges: a translucent part lights up where it is seen at a grazing angle
// and where it is thin. Tinted by the filament, because what comes back out has been through it.
float rim = pow(1.0 - clamp(dot(N, V), 0.0, 1.0), 4.0);
vec3 color = albedo * (INTENSITY_AMBIENT + diffuse) + vec3(spec) + albedo * rim * translucency * 0.55;
out_color = vec4(color, alpha);
}
+40
View File
@@ -0,0 +1,40 @@
#version 140
uniform mat4 view_model_matrix;
uniform mat4 projection_matrix;
uniform mat3 view_normal_matrix;
uniform mat4 volume_world_matrix;
// The volume's normal matrix in world space, so the fragment shader can tell a wall from a terrace -
// which is what decides whether layer lines show at all.
uniform mat3 world_normal_matrix;
// Clipping plane, x = min z, y = max z (the FFF/SLA preview's top/bottom cut).
uniform vec2 z_range;
// Clipping plane, general orientation (the section view).
uniform vec4 clipping_plane;
// GLModel's P3N3 layout, which is what init_from(indexed_triangle_set) produces - this shader needs
// nothing per vertex that the mesh does not already carry, because the colour comes from the
// fragment's own height rather than from anything baked into the vertices.
in vec3 v_position;
in vec3 v_normal;
out vec3 clipping_planes_dots;
out vec3 eye_normal;
out vec3 eye_position;
out vec3 world_normal;
out float world_z;
void main()
{
vec4 model_pos = vec4(v_position, 1.0);
vec4 world_pos = volume_world_matrix * model_pos;
world_z = world_pos.z;
world_normal = normalize(world_normal_matrix * v_normal);
eye_normal = normalize(view_normal_matrix * v_normal);
vec4 eye_pos = view_model_matrix * model_pos;
eye_position = eye_pos.xyz;
gl_Position = projection_matrix * eye_pos;
clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z);
}
+2
View File
@@ -114,6 +114,8 @@ std::pair<bool, std::string> GLShadersManager::init()
valid &= append_shader("mm_gouraud", { prefix + "mm_gouraud.vs", prefix + "mm_gouraud.fs" });
// Fast shaded preview for the texture displacement gizmo (see libslic3r/TextureDisplacement.hpp).
valid &= append_shader("texture_displacement_shaded", { prefix + "texture_displacement_shaded.vs", prefix + "texture_displacement_shaded.fs" });
// ChromaStack: the printed colours on the model, read per fragment from its own height.
valid &= append_shader("chromastack", { prefix + "chromastack.vs", prefix + "chromastack.fs" });
// UV-check overlay for the same gizmo: a procedural checker or a distortion heatmap over the
// painted patch, to sanity-check the unwrap.
valid &= append_shader("texture_displacement_uvcheck", { prefix + "texture_displacement_uvcheck.vs", prefix + "texture_displacement_uvcheck.fs" });
+31
View File
@@ -2875,6 +2875,37 @@ void ObjectList::del_layer_from_object(const int obj_idx, const t_layer_height_r
changed_object(obj_idx);
}
void ObjectList::rebuild_layer_ranges(const int obj_idx)
{
if (obj_idx < 0 || obj_idx >= int(m_objects->size()))
return;
const wxDataViewItem obj_item = m_objects_model->GetItemById(obj_idx);
if (!obj_item.IsOk())
return;
const t_layer_config_ranges& ranges = object(obj_idx)->layer_config_ranges;
// Adding and deleting tree items fires selection events on GTK; suppress them as every other
// bulk edit of the tree here does.
const bool prevent_events = m_prevent_list_events;
m_prevent_list_events = true;
wxDataViewItem root = m_objects_model->GetLayerRootItem(obj_item);
if (root.IsOk()) {
m_objects_model->DeleteChildren(root);
if (ranges.empty())
m_objects_model->Delete(root);
else {
for (const auto& range : ranges)
add_layer_item(range.first, root);
Expand(root);
}
} else {
add_layer_root_item(obj_item);
}
m_prevent_list_events = prevent_events;
changed_object(obj_idx);
}
void ObjectList::del_layers_from_object(const int obj_idx)
{
object(obj_idx)->layer_config_ranges.clear();
+5
View File
@@ -324,6 +324,11 @@ public:
void del_settings_from_config(const wxDataViewItem& parent_item);
void del_instances_from_object(const int obj_idx);
void del_layer_from_object(const int obj_idx, const t_layer_height_range& layer_range);
// Mirrors an object's height ranges in the tree after they have been rewritten from outside the
// list - the texture displacement gizmo writes a whole set at once for a ChromaStack stack's
// filament swaps. Deliberately leaves the selection alone: changing it while a gizmo is open
// tears the gizmo down under the user.
void rebuild_layer_ranges(const int obj_idx);
void del_layers_from_object(const int obj_idx);
bool del_from_cut_object(bool is_connector, bool is_model_part = false, bool is_negative_volume = false);
bool del_subobject_from_object(const int obj_idx, const int idx, const int type);
File diff suppressed because it is too large Load Diff
@@ -235,7 +235,11 @@ private:
ModelVolume* texture_volume();
const ModelVolume* texture_volume() const;
void add_texture_layer();
// `chroma` picks the kind: a relief layer, a height map pushed into the surface, or a ChromaStack
// picture, a colour picture made of filament swaps. The kind is a property of the layer and is
// fixed when it is created - there is no panel-wide mode, so a plaque can carry a picture on its
// face and a knurl on its rim at the same time.
void add_texture_layer(bool chroma = false);
void remove_texture_layer(int slot);
// Moves the layer in `slot` to position `to_index` of the slot-ordered stack (an insertion index,
// 0..layer count), which is the order blend modes apply in. Layers keep their data; what moves is
@@ -399,6 +403,109 @@ private:
void set_layer_texture(TextureDisplacementLayer &layer, const TextureLibraryEntry &entry);
void import_custom_texture(TextureDisplacementLayer &layer);
// ---- ChromaStack (see libslic3r/ChromaStack.hpp) ----
//
// A ChromaStack layer is an ordinary texture displacement layer whose height map is computed rather
// than imported: the picture the user picked is kept in `chroma_source`, the stack in
// `chroma`, and `image_data` holds whatever the last solve produced. Everything downstream -
// projection, preview, bake - therefore needs no changes at all, and this is the whole of the
// feature's GUI side: pick a picture, edit the stack, solve, hand the filament swaps to the
// object's height ranges.
// The project's loaded filaments as the colour model sees them: colour from the project, TD from
// each filament's preset. Index is the filament index the swaps are written from.
static std::vector<ChromaFilament> chroma_filaments();
// The layer height the print profile is set to, mm; 0 when there is no print preset. The solve is
// only valid for the layer height it was run at, so the panel compares it against the stack's.
static float print_layer_height();
// Where the active layer's painted patch sits, in the object's own printed coordinates - which is
// what the stack has to be lined up with, because a filament swap happens at a print Z and the
// relief grows upward from this patch.
struct ChromaPatch
{
bool valid = false;
float top_mm = 0.f; // the patch's surface above the bottom of the object
float spread_mm = 0.f; // how far from flat the patch is in Z; 0 is a horizontal face
// The patch's extent in the plane the texture is projected from, in the bake frame - i.e. the
// (x, y) the Z-facing plane of the tri-planar projection samples by. This is what the picture
// has to be placed against: tiling_scale and offset are in that space, not in the patch's.
Vec2f planar_min = Vec2f::Zero();
Vec2f planar_max = Vec2f::Zero();
};
ChromaPatch chroma_patch(const TextureDisplacementLayer &layer) const;
// The painted face's height in layers, which is what the stack's base has to be: -1 when nothing
// is painted yet or there is no layer height to divide by.
int chroma_measured_base(const TextureDisplacementLayer &layer) const;
// Extracting the painted patch is far too expensive to repeat once a frame, which is how often the
// panel wants the number. The paint's own timestamp and the transform the patch was measured in are
// between them everything the answer depends on.
mutable std::array<ChromaPatch, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_patch_cache{};
mutable std::array<uint64_t, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_patch_stamp{};
mutable std::array<Transform3d, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_patch_trafo{};
// The patch is measured in two frames at once - the instance's, for the print height, and the
// bake's, for the placement - so both are part of what the cached answer depends on.
mutable std::array<Transform3d, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_patch_bake{};
// Picks a colour picture for the layer, turns ChromaStack on, and seeds a stack from the loaded
// filaments and the patch if the layer has none yet. Solves straight away, so the relief appears
// without a second click.
void chroma_pick_picture(TextureDisplacementLayer &layer);
// Re-runs the solve and replaces the layer's height map with its result. False (with the reason
// shown to the user) when the layer has no picture, no stack or no relief layers.
bool chroma_solve_layer(TextureDisplacementLayer &layer);
// Writes the stack's filament swaps onto the object as height ranges, replacing whatever ranges it
// had. Takes its own undo snapshot.
void chroma_apply_swaps(const TextureDisplacementLayer &layer);
// The stack's swaps as the object's height ranges already express them: true when applying would
// change nothing, which is what the panel's hint keys off.
bool chroma_swaps_applied(const TextureDisplacementLayer &layer) const;
// The whole of a ChromaStack card between its header and More settings: the picture and the print
// it predicts, the base, the filaments and the colours they reach, and the two actions.
void render_chroma_card(TextureDisplacementLayer &layer, float card_pad, float label_w, float wrap_w);
// The surface drawn in the colours the stack will print, over whatever neutral surface is there:
// the live preview's displaced mesh before a bake, the volume's own mesh after one. Every triangle
// takes the palette colour of the printed layer its centroid falls in (chroma_level_at()), which
// is what the eye will see at that height - so the relief's terraces read as the picture, the walls
// between them show the bands they cut through, and looking straight down gives the blended colours
// the whole feature exists to produce.
//
// Nothing else in the application can show this: the colours are blends of translucent filaments
// that no single filament can make, so they cannot be expressed as the per-triangle filament
// assignment the 3D scene paints with. They exist only here and in the sliced preview.
void rebuild_chroma_colors(const TextureDisplacementLayer &layer);
bool render_chroma_colors(const TextureDisplacementLayer &layer);
GLModel m_chroma_color_model;
// The surface the overlay was built from. The mesh alone: the colours are not in it, so a change
// to the stack costs a palette upload rather than a mesh rebuild.
size_t m_chroma_color_key = 0;
// The stack's colours, one texel per printed layer, and what they were built from. A texture
// because a uniform array cannot hold a base of several hundred layers.
std::unique_ptr<GLTexture> m_chroma_palette_tex;
size_t m_chroma_palette_key = 0;
bool m_show_chroma_colors = true;
// What the last solve of each slot reported, for the panel. Not saved: it is a property of the
// solve, and a project is reopened with whatever height map the solve produced.
struct ChromaReport
{
bool valid = false;
double mean = 0.;
double max = 0.;
};
std::array<ChromaReport, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_report{};
// The predicted print, per slot: the palette colours the solve chose, as a panel thumbnail. The
// 3D view cannot show this - the colours do not exist until the print swaps filament - so it is
// the only place the user sees what the stack will actually produce.
std::array<std::unique_ptr<GLTexture>, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_preview;
// The source picture's thumbnail, beside the prediction, keyed by the image it was built from.
std::array<std::unique_ptr<GLTexture>, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_source_thumb;
std::array<const void *, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_source_key{};
// Per slot: the stack has been edited since the height map was solved, so the relief on screen is the
// previous stack's. Nothing else can show this - a stale height map looks like a perfectly good one.
std::array<bool, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_stale{};
// Paint / Erase, from the panel. See get_left_button_state_type().
bool m_erase_mode = false;
// Per slot: whether the layer's card shows every setting or only Depth, Tile size and Rotation.
+1
View File
@@ -4533,6 +4533,7 @@ void TabFilament::build()
optgroup->append_single_option_line("filament_adhesiveness_category", "material_basic_information#adhesiveness-category");
optgroup->append_single_option_line("filament_density", "material_basic_information#density");
optgroup->append_single_option_line("filament_transmission_distance", "material_basic_information#transmission-distance");
optgroup->append_single_option_line("filament_shrink", "material_basic_information#shrinkage-xy");
optgroup->append_single_option_line("filament_shrinkage_compensation_z", "material_basic_information#shrinkage-z");
optgroup->append_single_option_line("filament_cost", "material_basic_information#price");