Merge pull request #69 from SteamSmith/feat/bake-auto-rebake

feat(bake): Auto-rebake keeps one mesh map up to date
This commit is contained in:
BFlat
2026-11-02 12:45:03 +07:00
committed by GitHub
18 changed files with 391 additions and 13 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ end to end today (engine + UI + automation + tests), not what is planned.
| Layer stack | 16 | 97% | Paint, fill, folder and **instance** layers; 32 per-channel blend modes and opacities; white/black/painted masks with **mask with height**; generator, fill, levels (with a histogram of its input), invert/threshold, color selection, compare, blur, sharpen and HSL effects on masks and layer content; anchor points; smart masks; mask view (Alt+click); Alt+click a material channel to keep it alone; duplicate, group, drag to reorder, rename, color tags, copy/cut/paste (also as instance) across sets, flatten, instantiate across sets, save layers as smart materials; **30 library filters** (adjustments incl. histogram scan, range and shift, blur/sharpen/high pass, bevel, glow, edge detect, warps, mosaic, baked lighting, material finishes); **geometry masks** (a layer on chosen mesh parts) | A few niche filters of the reference app's catalog |
| Painting tools | 14 | 90% | Paint, eraser, projection with image stencils, polygon fill, **path** and **lasso fill**, smudge, clone, color picker, quick mask, Shift+click lines with preview, 2D view painting; size, flow, stroke opacity, spacing, hardness, angle, follow path, jitters, alignment, angle fade, backface culling, lazy mouse, X/Y/Z mirror and **radial symmetry**, pen pressure, procedural and image alphas, **text** in any installed font (as an alpha or a decal), **particle brush** | Pen tilt (not exposed by the windowing layer) |
| Materials and assets | 8 | 90% | 63 procedural materials (incl. car paint, velvet, brushed steel, skin, wax, ice, snow, bark), 21 smart materials, 18 smart masks, **67 procedural brush alphas**, **18 brush presets** plus user presets, 23 procedurals, 6 environments plus imported HDR/EXR panoramas, image resources, a user smart material library, a tabbed shelf with search and **drag and drop** onto layers and the viewport | A smaller bundled library than the reference app's |
| Baking | 10 | 100% | AO, curvature (also from the normal map), thickness, world normal, position, ID, normal, height, **bent normals, opacity, color map and world-space direction**; **high-poly meshes**, cage, frontal/rear distances, average normals, **match by mesh name**; per-texture-set selection, common and per-map parameters, progress and cancel; high-poly base color textures embedded or in files beside a `.gltf`, FBX or USD layer | — |
| Baking | 10 | 100% | AO, curvature (also from the normal map), thickness, world normal, position, ID, normal, height, **bent normals, opacity, color map and world-space direction**; **high-poly meshes**, cage, frontal/rear distances, average normals, **match by mesh name**; per-texture-set selection, common and per-map parameters, progress and cancel; **Auto-rebake** (`bake.autoRebake`: one map bakes itself again in the background when its bake settings, the low poly or the high polys change); high-poly base color textures embedded or in files beside a `.gltf`, FBX or USD layer | — |
| Viewport and display | 12 | 95% | PBR with image-based lighting, 6 procedural environments or imported HDR/EXR panoramas with exposure, rotation, blur, opacity, ACES/linear tone mapping, HDR pipeline with bloom, vignette, sharpen and grain, key-light shadows, 4× MSAA plus idle temporal accumulation, the baked normal map under the painted normal, channel / mesh-map / mask / UV checker views, 3D and 2D wireframes of the mesh's own polygons, brush cursor, axis gizmos, subsurface scattering blur, **depth of field**, saved cameras and cameras from glTF, FBX, COLLADA and USD files, displacement (also in shadows and the path tracers), **path-traced Rendering mode** (F10, on the GPU or the CPU) with environment importance sampling and an edge-aware **denoiser** | Hardware ray tracing |
| Shader | 6 | 100% | Metallic/roughness and specular/glossiness PBR, **clear coat, sheen, anisotropy and subsurface scattering** (viewport and path tracer), height-to-normal, painted normals, parallax occlusion, emissive, opacity (dithered), AO intensity | — |
| Export | 8 | 92% | 10 presets (incl. Godot 4 and **OpenPBR** with every shading lobe) plus **user output templates** (per-channel packing, converted maps, JSON import/export), PNG/JPEG/TGA/TIFF/BMP/WEBP/EXR/HDR, 8/16/32-bit, padding modes, size override, OpenGL/DirectX normals, **colour space** of the colour maps (sRGB, Linear Rec.709, ACEScg, ACES2065-1; data maps never converted), `$udim` tiles, **MaterialX** (`file.exportTextures` with `materialX`: a `.mtlx` OpenPBR material per set, `<UDIM>` aware), OBJ, textured GLB (with clearcoat/sheen) and USD (with clear coat) mesh export, zip download in the browser | Sheen and anisotropy in USD (UsdPreviewSurface has no inputs for them; GLB and OpenPBR maps carry them) |
+176
View File
@@ -0,0 +1,176 @@
//! Auto-rebake: one mesh map that bakes itself again when what it was baked from changes.
//!
//! The map is chosen with `bake.autoRebake`. From then on, [`Session::auto_rebake_due`] names the
//! texture sets whose bake inputs (the set's bake settings except the list of maps, the low poly,
//! the high polys and the cage) differ from those of their last bake of that map.
//! The apps poll it each frame and bake the due sets in the background; `bake.autoRebake.run`
//! does the same thing once, blocking, for agents and scripts.
//!
//! A set only takes part once it has the map baked at its current output size: Auto-rebake
//! keeps a bake up to date, it doesn't start one, and it never leaves a set's maps at two sizes.
//! The choice isn't part of the project.
use std::collections::BTreeMap;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::sync::Arc;
use serde_json::{Value, json};
use texturecraft_bake::Progress;
use texturecraft_core::BakedMap;
use crate::bake_cmds::BakeTask;
use crate::commands::{CommandSpec, always, bad, boolp};
use crate::{Result, Session, cmd};
/// Which map rebakes itself, and what each texture set was last baked from.
#[derive(Clone, Debug, Default)]
pub struct AutoRebake {
pub map: Option<BakedMap>,
/// Fingerprint of each set's bake inputs when it last baked [`AutoRebake::map`].
seen: BTreeMap<usize, u64>,
/// Fingerprint taken when a set's bake started, kept when that bake finishes.
pending: BTreeMap<usize, u64>,
}
pub fn specs() -> Vec<CommandSpec> {
vec![
cmd!(
"bake.autoRebake",
"Auto-rebake",
[],
None,
"{map?: map id (e.g. \"normal_base\", \"height\") to keep up to date, or null / off: true to turn it off; no params: just report} → {map, due: [texture sets that need a rebake]}",
always,
auto_rebake
),
cmd!(
"bake.autoRebake.run",
"Run Auto-rebake",
[],
None,
"{cpu?: true (skip the GPU)} → {rebaked: [texture sets]}: bakes the Auto-rebake map of every set whose bake inputs changed since it was last baked, now",
always,
auto_rebake_run
),
]
}
impl Session {
/// A fingerprint of everything a bake of texture set `ts` is made from, except which maps it
/// makes.
pub fn bake_inputs_key(&self, ts: usize) -> Option<u64> {
let set = self.doc.project.texture_sets.get(ts)?;
let mut settings = set.bake.clone();
settings.maps.clear();
settings.enabled = true;
let mut h = DefaultHasher::new();
serde_json::to_string(&settings).ok()?.hash(&mut h);
(Arc::as_ptr(&self.mesh) as usize).hash(&mut h);
for m in &self.bake_meshes.high {
(Arc::as_ptr(&m.mesh) as usize).hash(&mut h);
}
self.bake_meshes.cage.as_ref().map(|c| Arc::as_ptr(&c.mesh) as usize).hash(&mut h);
Some(h.finish())
}
/// Texture sets whose Auto-rebake map is out of date. A set seen for the first time (a project
/// just opened, a bake from before Auto-rebake was on) is taken as up to date.
pub fn auto_rebake_due(&mut self) -> Vec<usize> {
let Some(map) = self.auto_rebake.map else { return Vec::new() };
let mut due = Vec::new();
for ts in 0..self.doc.project.texture_sets.len() {
let Some(set) = self.doc.project.texture_sets.get(ts).filter(|t| t.bake.enabled) else { continue };
let columns = set.tile_grid().map_or(1, |g| g.cols);
let baked = self.doc.baked.get(&ts).is_some_and(|b| b.maps.contains_key(&map) && b.resolution == set.bake.resolution.saturating_mul(columns));
let Some(key) = self.bake_inputs_key(ts).filter(|_| baked) else { continue };
match self.auto_rebake.seen.get(&ts) {
None => {
self.auto_rebake.seen.insert(ts, key);
}
Some(seen) if *seen != key => due.push(ts),
Some(_) => {}
}
}
due
}
/// Call right before building the bake task of `ts`: remembers what it is baked from, so a
/// finished bake of the Auto-rebake map marks the set up to date (and a change made while it
/// ran still triggers another).
pub fn begin_bake(&mut self, ts: usize) {
if let Some(key) = self.bake_inputs_key(ts) {
self.auto_rebake.pending.insert(ts, key);
}
}
/// The bake task of `ts` restricted to one map (Auto-rebake's: the other maps stay as they are).
pub fn bake_task_for(&self, ts: usize, only: Option<BakedMap>) -> Result<BakeTask> {
let mut task = self.bake_task(ts)?;
if let Some(m) = only {
task.settings.maps = std::iter::once(m).collect();
}
Ok(task)
}
/// Records a finished bake: when it made the Auto-rebake map, the set is up to date.
pub(crate) fn note_baked(&mut self, ts: usize, mut made: impl Iterator<Item = BakedMap>) {
let key = self.auto_rebake.pending.remove(&ts);
if let (Some(key), Some(map)) = (key, self.auto_rebake.map)
&& made.any(|m| m == map)
{
self.auto_rebake.seen.insert(ts, key);
}
}
/// Records bakes of `sets` that were cancelled, failed or never started: Auto-rebake leaves
/// those sets alone until their bake inputs change again, instead of retrying every frame.
pub fn bake_abandoned(&mut self, sets: impl IntoIterator<Item = usize>) {
for ts in sets {
if let Some(key) = self.auto_rebake.pending.remove(&ts).or_else(|| self.bake_inputs_key(ts)) {
self.auto_rebake.seen.insert(ts, key);
}
}
}
/// Bakes every due set's Auto-rebake map now (blocking); returns the sets.
pub fn auto_rebake_now(&mut self, cpu: bool) -> Result<Vec<usize>> {
let due = self.auto_rebake_due();
let map = self.auto_rebake.map;
for &ts in &due {
self.begin_bake(ts);
let mut task = self.bake_task_for(ts, map)?;
if cpu {
task.tracer = None;
}
let out = task.run(&Progress::new())?;
self.apply_bake(ts, out)?;
}
Ok(due)
}
}
fn state(s: &mut Session) -> Value {
json!({ "map": s.auto_rebake.map.map(BakedMap::id), "due": s.auto_rebake_due() })
}
fn auto_rebake(s: &mut Session, p: &Value) -> Result<Value> {
if boolp(p, "off") == Some(true) || p.get("map").is_some_and(Value::is_null) {
s.auto_rebake = AutoRebake::default();
} else if let Some(id) = p.get("map") {
let map = id.as_str().and_then(BakedMap::parse).ok_or_else(|| bad("bake.autoRebake", format!("unknown mesh map {id}")))?;
if s.auto_rebake.map != Some(map) {
s.auto_rebake = AutoRebake { map: Some(map), ..AutoRebake::default() };
}
}
Ok(state(s))
}
fn auto_rebake_run(s: &mut Session, p: &Value) -> Result<Value> {
if s.auto_rebake.map.is_none() {
return Err(bad("bake.autoRebake.run", "no Auto-rebake map is set; run bake.autoRebake {map} first"));
}
let rebaked = s.auto_rebake_now(boolp(p, "cpu") == Some(true))?;
s.update();
Ok(json!({ "rebaked": rebaked }))
}
+5 -1
View File
@@ -177,6 +177,7 @@ impl Session {
/// Store a finished bake (one undo step).
pub fn apply_bake(&mut self, ts: usize, out: BakeOutput) -> Result<()> {
self.log.push(format!("Baked texture set {ts} ({})", out.tracer));
let made: Vec<BakedMap> = out.maps.keys().copied().collect();
self.edit("Bake mesh maps", |s| {
let maps: BTreeSet<BakedMap> = out.maps.keys().copied().collect();
let entry = s.doc.baked.entry(ts).or_default();
@@ -186,11 +187,14 @@ impl Session {
}
s.doc.project.texture_set_mut(ts)?.baked.extend(maps);
Ok(())
})
})?;
self.note_baked(ts, made.into_iter());
Ok(())
}
/// Bake texture set `ts` now (blocking), on the CPU when `cpu`; returns where the rays ran.
pub fn bake_now(&mut self, ts: usize, cpu: bool) -> Result<String> {
self.begin_bake(ts);
let mut task = self.bake_task(ts)?;
if cpu {
task.tracer = None;
+1
View File
@@ -68,6 +68,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
v.extend(crate::uv_tile_cmds::specs());
v.extend(crate::template_cmds::specs());
v.extend(crate::project_cmds::specs());
v.extend(crate::auto_rebake::specs());
v.extend(crate::bake_cmds::specs());
v.extend(crate::materialx_cmds::specs());
v.extend(crate::file_cmds::specs());
+1
View File
@@ -8,6 +8,7 @@
#![forbid(unsafe_code)]
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
pub mod auto_rebake;
pub mod bake_cmds;
pub mod brush_preset_cmds;
pub mod camera_cmds;
+3
View File
@@ -320,6 +320,8 @@ pub struct Session {
pub mesh_source: Option<(String, Vec<u8>)>,
/// High-definition meshes and cage for baking (saved inside the project).
pub bake_meshes: crate::BakeMeshes,
/// The map that bakes itself again when its inputs change (not saved).
pub auto_rebake: crate::auto_rebake::AutoRebake,
pub caches: Vec<SetCache>,
pub(crate) undo: Vec<(String, Document)>,
pub(crate) redo: Vec<(String, Document)>,
@@ -375,6 +377,7 @@ impl Session {
bvh: Arc::new(bvh),
mesh_source,
bake_meshes: Default::default(),
auto_rebake: Default::default(),
caches,
undo: Vec::new(),
redo: Vec::new(),
+119
View File
@@ -0,0 +1,119 @@
//! Auto-rebake: one mesh map bakes itself again when its bake inputs change.
use std::sync::Arc;
use serde_json::{Value, json};
use texturecraft_engine::core::BakedMap;
use texturecraft_engine::geom::primitives;
use texturecraft_engine::{Session, execute};
/// A plane with a high poly just above it, baked once with height and opacity.
fn baked_session(tag: &str) -> (Session, std::path::PathBuf) {
let mesh = primitives::builtin("plane").unwrap();
let mut obj = String::from("o Part_high\n");
for p in &mesh.positions {
obj += &format!("v {} {} {}\n", p.x, p.y + 0.002, p.z);
}
for t in mesh.indices.chunks_exact(3) {
obj += &format!("f {} {} {}\n", t[0] + 1, t[1] + 1, t[2] + 1);
}
let dir = std::env::temp_dir().join(format!("tc_auto_rebake_{tag}_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("hp.obj");
std::fs::write(&path, obj).unwrap();
let mut s = Session::new_project("t", mesh, "builtin:plane", None, 256).unwrap();
execute(&mut s, "bake.highPoly.add", &json!({ "path": path.display().to_string() })).unwrap();
execute(&mut s, "bake.settings", &json!({ "resolution": 128, "dilation": 0, "maps": ["height", "opacity"] })).unwrap();
execute(&mut s, "bake.run", &json!({ "cpu": true })).unwrap();
(s, dir)
}
fn due(s: &mut Session) -> Vec<u64> {
execute(s, "bake.autoRebake", &Value::Null).unwrap()["due"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect()
}
fn image(s: &Session, map: BakedMap) -> Arc<Vec<f32>> {
s.doc.baked[&0].maps[&map].clone()
}
#[test]
fn a_changed_bake_input_rebakes_only_the_chosen_map() {
let (mut s, dir) = baked_session("rebake");
// Off by default; nothing is due and the run command says what to do first.
assert_eq!(execute(&mut s, "bake.autoRebake", &Value::Null).unwrap()["map"], Value::Null);
assert!(execute(&mut s, "bake.autoRebake.run", &Value::Null).is_err());
let r = execute(&mut s, "bake.autoRebake", &json!({ "map": "height" })).unwrap();
assert_eq!(r["map"], "height");
assert!(due(&mut s).is_empty(), "turning it on takes the current bake as up to date");
// Which maps are ticked is not a bake input; a setting is.
execute(&mut s, "bake.settings", &json!({ "maps": ["height", "opacity", "ambient_occlusion"] })).unwrap();
assert!(due(&mut s).is_empty());
execute(&mut s, "bake.settings", &json!({ "maxFrontal": 0.2, "maps": ["height", "opacity"] })).unwrap();
assert_eq!(due(&mut s), [0]);
let (height, opacity) = (image(&s, BakedMap::Height), image(&s, BakedMap::Opacity));
let r = execute(&mut s, "bake.autoRebake.run", &json!({ "cpu": true })).unwrap();
assert_eq!(r["rebaked"], json!([0]));
assert!(!Arc::ptr_eq(&height, &image(&s, BakedMap::Height)), "the Auto-rebake map was baked again");
assert!(Arc::ptr_eq(&opacity, &image(&s, BakedMap::Opacity)), "the other maps were left alone");
assert!(due(&mut s).is_empty(), "and the set is up to date");
assert_eq!(execute(&mut s, "bake.autoRebake.run", &Value::Null).unwrap()["rebaked"], json!([]));
// A manual bake of the map also counts; a changed high poly list is a change again.
execute(&mut s, "bake.settings", &json!({ "antialiasing": 2 })).unwrap();
assert_eq!(due(&mut s), [0]);
s.begin_bake(0);
let out = s.bake_task_for(0, None).unwrap().run(&texturecraft_engine::bake::Progress::new()).unwrap();
s.apply_bake(0, out).unwrap();
assert!(due(&mut s).is_empty(), "a manual bake of the map marks it up to date");
execute(&mut s, "bake.highPoly.remove", &json!({ "all": true })).unwrap();
assert_eq!(due(&mut s), [0]);
// A different output size is left to a manual bake: the maps would end up at two sizes.
execute(&mut s, "bake.settings", &json!({ "resolution": 256 })).unwrap();
assert!(due(&mut s).is_empty());
// Hostile input, and turning it off.
for bad in [json!({ "map": "nonsense" }), json!({ "map": 3 })] {
assert!(execute(&mut s, "bake.autoRebake", &bad).is_err(), "{bad}");
}
assert_eq!(execute(&mut s, "bake.autoRebake", &json!({ "map": null })).unwrap()["map"], Value::Null);
assert!(due(&mut s).is_empty());
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn a_cancelled_or_failed_bake_is_not_queued_again_until_the_inputs_change() {
let (mut s, dir) = baked_session("cancel");
execute(&mut s, "bake.autoRebake", &json!({ "map": "height" })).unwrap();
assert!(due(&mut s).is_empty());
execute(&mut s, "bake.settings", &json!({ "maxFrontal": 0.2 })).unwrap();
assert_eq!(due(&mut s), [0]);
// What the app does: start the bake, cancel it, record the abandoned bake.
s.begin_bake(0);
let mut task = s.bake_task_for(0, Some(BakedMap::Height)).unwrap();
task.tracer = None;
let progress = texturecraft_engine::bake::Progress::new();
progress.cancel();
assert!(task.run(&progress).is_err(), "a cancelled bake fails");
s.bake_abandoned([0]);
assert!(due(&mut s).is_empty(), "not queued again on the next frame");
// The next change makes it due again; a bake that never started (queued behind a failed
// one) is recorded the same way.
execute(&mut s, "bake.settings", &json!({ "maxFrontal": 0.3 })).unwrap();
assert_eq!(due(&mut s), [0]);
s.bake_abandoned([0]);
assert!(due(&mut s).is_empty());
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn a_set_without_the_map_baked_is_left_alone() {
let mut s = Session::new_project("t", primitives::builtin("plane").unwrap(), "builtin:plane", None, 128).unwrap();
execute(&mut s, "bake.autoRebake", &json!({ "map": "height" })).unwrap();
execute(&mut s, "bake.settings", &json!({ "maxFrontal": 0.3 })).unwrap();
assert!(due(&mut s).is_empty(), "Auto-rebake keeps a bake up to date, it doesn't start one");
}
+5
View File
@@ -679,6 +679,11 @@ pub const TABLE: &[(&str, &str)] = &[
("Restore every default shortcut", "استعادة كل الاختصارات الافتراضية"),
("Return to painting mode", "العودة إلى نمط الرسم"),
("Return to this step", "العودة إلى هذه الخطوة"),
("Auto-rebake", "إعادة الخبز تلقائيًا"),
(
"Bake this map again by itself when its bake settings, the low poly or the high polys change",
"يعيد خبز هذه الخريطة تلقائيًا عند تغيّر إعدادات الخبز أو المجسّم منخفض الدقة أو المجسّمات عالية الدقة",
),
("Right-click a layer > Create smart material", "انقر بزر الفأرة الأيمن على طبقة > إنشاء مادة ذكية"),
("Rotation", "التدوير"),
("Rotation (degrees)", "التدوير (بالدرجات)"),
+5
View File
@@ -721,6 +721,11 @@ pub const TABLE: &[(&str, &str)] = &[
("Restore every default shortcut", "Alle Standard-Tastenkürzel wiederherstellen"),
("Return to painting mode", "Zurück zum Malmodus"),
("Return to this step", "Zu diesem Schritt zurückkehren"),
("Auto-rebake", "Automatisch neu backen"),
(
"Bake this map again by itself when its bake settings, the low poly or the high polys change",
"Diese Map automatisch neu backen, wenn sich ihre Bake-Einstellungen, das Low-Poly- oder die High-Poly-Meshes ändern",
),
("Right-click a layer > Create smart material", "Rechtsklick auf eine Ebene > Smart-Material erstellen"),
("Rotation", "Drehung"),
("Rotation (degrees)", "Drehung (Grad)"),
+5
View File
@@ -709,6 +709,11 @@ pub const TABLE: &[(&str, &str)] = &[
("Restore every default shortcut", "Restaurar todos los atajos predeterminados"),
("Return to painting mode", "Volver al modo de pintura"),
("Return to this step", "Volver a este paso"),
("Auto-rebake", "Rehornear automáticamente"),
(
"Bake this map again by itself when its bake settings, the low poly or the high polys change",
"Vuelve a hornear este mapa automáticamente cuando cambian sus ajustes de horneado, la malla low poly o las high poly",
),
("Right-click a layer > Create smart material", "Clic derecho en una capa > Crear material inteligente"),
("Rotation", "Rotación"),
("Rotation (degrees)", "Rotación (grados)"),
+5
View File
@@ -724,6 +724,11 @@ pub const TABLE: &[(&str, &str)] = &[
("Restore every default shortcut", "Restaurer tous les raccourcis par défaut"),
("Return to painting mode", "Revenir au mode peinture"),
("Return to this step", "Revenir à cette étape"),
("Auto-rebake", "Précalcul automatique"),
(
"Bake this map again by itself when its bake settings, the low poly or the high polys change",
"Précalcule à nouveau cette carte automatiquement quand ses réglages de précalcul, le maillage low poly ou les maillages high poly changent",
),
("Right-click a layer > Create smart material", "Clic droit sur un calque > Créer un matériau intelligent"),
("Rotation", "Rotation"),
("Rotation (degrees)", "Rotation (degrés)"),
+5
View File
@@ -718,6 +718,11 @@ pub const TABLE: &[(&str, &str)] = &[
("Restore every default shortcut", "Ripristina tutte le scorciatoie predefinite"),
("Return to painting mode", "Torna alla modalità pittura"),
("Return to this step", "Torna a questo passaggio"),
("Auto-rebake", "Bake automatico"),
(
"Bake this map again by itself when its bake settings, the low poly or the high polys change",
"Esegue di nuovo il bake di questa mappa quando cambiano le sue impostazioni di bake, la mesh low poly o le mesh high poly",
),
("Right-click a layer > Create smart material", "Clic destro su un livello > Crea materiale intelligente"),
("Rotation", "Rotazione"),
("Rotation (degrees)", "Rotazione (gradi)"),
+5
View File
@@ -700,6 +700,11 @@ pub const TABLE: &[(&str, &str)] = &[
("Restore every default shortcut", "すべてのショートカットをデフォルトに戻す"),
("Return to painting mode", "ペイントモードに戻る"),
("Return to this step", "このステップに戻る"),
("Auto-rebake", "自動再ベイク"),
(
"Bake this map again by itself when its bake settings, the low poly or the high polys change",
"ベイク設定、ローポリ、またはハイポリが変わると、このマップを自動的にベイクし直します",
),
("Right-click a layer > Create smart material", "レイヤーを右クリック > スマートマテリアルを作成"),
("Rotation", "回転"),
("Rotation (degrees)", "回転 (度)"),
+5
View File
@@ -691,6 +691,11 @@ pub const TABLE: &[(&str, &str)] = &[
("Restore every default shortcut", "모든 기본 단축키를 복원합니다"),
("Return to painting mode", "페인팅 모드로 돌아가기"),
("Return to this step", "이 단계로 돌아가기"),
("Auto-rebake", "자동 다시 베이크"),
(
"Bake this map again by itself when its bake settings, the low poly or the high polys change",
"베이크 설정, 로우 폴리 또는 하이 폴리가 바뀌면 이 맵을 자동으로 다시 베이크합니다",
),
("Right-click a layer > Create smart material", "레이어 마우스 오른쪽 클릭 > 스마트 머티리얼 만들기"),
("Rotation", "회전"),
("Rotation (degrees)", "회전 (도)"),
+5
View File
@@ -706,6 +706,11 @@ pub const TABLE: &[(&str, &str)] = &[
("Restore every default shortcut", "Restaurar todos os atalhos padrão"),
("Return to painting mode", "Voltar ao modo de pintura"),
("Return to this step", "Voltar a esta etapa"),
("Auto-rebake", "Refazer bake automaticamente"),
(
"Bake this map again by itself when its bake settings, the low poly or the high polys change",
"Refaz o bake deste mapa automaticamente quando as configurações de bake, a malha low poly ou as high poly mudam",
),
("Right-click a layer > Create smart material", "Clique com o botão direito em uma camada > Criar material inteligente"),
("Rotation", "Rotação"),
("Rotation (degrees)", "Rotação (graus)"),
+2
View File
@@ -637,6 +637,8 @@ pub const TABLE: &[(&str, &str)] = &[
("Restore every default shortcut", "恢复所有默认快捷键"),
("Return to painting mode", "返回绘画模式"),
("Return to this step", "返回到此步骤"),
("Auto-rebake", "自动重新烘焙"),
("Bake this map again by itself when its bake settings, the low poly or the high polys change", "当烘焙设置、低模或高模发生变化时,自动重新烘焙此贴图"),
("Right-click a layer > Create smart material", "右键单击图层 > 创建智能材质"),
("Rotation", "旋转"),
("Rotation (degrees)", "旋转(度)"),
+32 -10
View File
@@ -51,6 +51,8 @@ pub struct BakeJob {
pub queue: Vec<usize>,
pub current: Option<(usize, Arc<Progress>, std::sync::mpsc::Receiver<texturecraft_engine::Result<texturecraft_engine::bake::BakeOutput>>)>,
pub total: usize,
/// Bake only this map (an Auto-rebake); None bakes every ticked map.
pub only: Option<texturecraft_engine::core::BakedMap>,
}
/// The browser build's notice when WebGPU is unavailable and wgpu fell back to WebGL2.
@@ -188,7 +190,7 @@ impl TextureCraftApp {
return;
}
let total = sets.len();
self.bake = Some(BakeJob { queue: sets, current: None, total });
self.bake = Some(BakeJob { queue: sets, current: None, total, only: None });
}
fn poll_bake(&mut self, ctx: &egui::Context) {
@@ -200,19 +202,19 @@ impl TextureCraftApp {
let ts = *ts;
job.current = None;
if let Err(e) = self.session.apply_bake(ts, out) {
self.session.bake_abandoned([ts]);
self.toast(e.to_string(), true);
}
}
Ok(Err(e)) => {
job.current = None;
let msg = e.to_string();
self.bake = None;
self.cancel_bake();
self.toast(msg, true);
return;
}
Err(std::sync::mpsc::TryRecvError::Empty) => return,
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
self.bake = None;
self.cancel_bake();
self.toast(i18n::tr("The bake worker stopped"), true);
return;
}
@@ -225,8 +227,9 @@ impl TextureCraftApp {
self.toast(i18n::tr("Mesh maps baked"), false);
return;
}
let ts = job.queue.remove(0);
match self.session.bake_task(ts) {
let (ts, only) = (job.queue.remove(0), job.only);
self.session.begin_bake(ts);
match self.session.bake_task_for(ts, only) {
Ok(task) => {
let progress = Arc::new(Progress::new());
let p = progress.clone();
@@ -235,17 +238,35 @@ impl TextureCraftApp {
job.current = Some((ts, progress, rx));
}
}
Err(e) => self.toast(e.to_string(), true),
Err(e) => {
self.session.bake_abandoned([ts]);
self.toast(e.to_string(), true);
}
}
}
}
/// Auto-rebake: when the chosen map's bake inputs changed, bake it again in the background
/// (one set after another, only that map), unless a bake or a stroke is running.
fn drive_auto_rebake(&mut self) {
if self.bake.is_some() || self.session.stroke_active() || self.session.auto_rebake.map.is_none() {
return;
}
let due = self.session.auto_rebake_due();
if !due.is_empty() {
let total = due.len();
self.bake = Some(BakeJob { queue: due, current: None, total, only: self.session.auto_rebake.map });
}
}
/// Stops the bake job (also after an error). The set being baked and the queued ones wait for
/// their next bake input change before Auto-rebake tries them again.
pub fn cancel_bake(&mut self) {
if let Some(job) = self.bake.take()
&& let Some((_, p, _)) = job.current
{
let Some(job) = self.bake.take() else { return };
if let Some((_, p, _)) = &job.current {
p.cancel.store(true, Ordering::Relaxed);
}
self.session.bake_abandoned(job.current.map(|(ts, ..)| ts).into_iter().chain(job.queue));
}
/// Resize texture set `ts` to `resolution` on a worker thread (the document changes when
@@ -381,6 +402,7 @@ impl eframe::App for TextureCraftApp {
web_files::dropped(self, ctx, f);
}
self.poll_bake(ctx);
self.drive_auto_rebake();
self.poll_resize(ctx);
let scale = self.ui.prefs.ui_scale.clamp(0.5, 3.0);
if (ctx.zoom_factor() - scale).abs() > 1e-3 {
+11 -1
View File
@@ -723,7 +723,17 @@ fn bake_panel(app: &mut TextureCraftApp, ui: &mut Ui) {
ui.separator();
match selected {
None => common_params(app, ui, &mut b),
Some(m) => map_params(ui, m, &mut b),
Some(m) => {
let mut auto = app.session.auto_rebake.map == Some(m);
if ui
.checkbox(&mut auto, tr("Auto-rebake"))
.on_hover_text(tr("Bake this map again by itself when its bake settings, the low poly or the high polys change"))
.changed()
{
app.run("bake.autoRebake", if auto { json!({ "map": m.id() }) } else { json!({ "off": true }) });
}
map_params(ui, m, &mut b)
}
}
if b.direction == [0.0; 3] {
b.direction = before.direction;