mirror of
https://github.com/storytold/texturecraft.git
synced 2026-11-06 18:42:19 +00:00
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:
+1
-1
@@ -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) |
|
||||
|
||||
@@ -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 }))
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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)", "التدوير (بالدرجات)"),
|
||||
|
||||
@@ -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)"),
|
||||
|
||||
@@ -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)"),
|
||||
|
||||
@@ -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)"),
|
||||
|
||||
@@ -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)"),
|
||||
|
||||
@@ -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)", "回転 (度)"),
|
||||
|
||||
@@ -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)", "회전 (도)"),
|
||||
|
||||
@@ -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)"),
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user