mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-11 04:47:14 +00:00
Session::crypto_coverage returns the coverage of one Cryptomatte ID over the layer, read from the probe Session::crypto_probe_job keeps: at most max_side samples on the long side, each at the centre of its cell, or None until the probe is built. It is what the Cryptomatte eyedropper's hover highlight will draw. Tests cover the coverage of an ID from the kept probe and the cost of a highlight with six ranks.
148 lines
7.7 KiB
Rust
148 lines
7.7 KiB
Rust
use super::{EngineError, Image, ItemId, RenderOpts, Renderer, Result, Session, ThumbnailJob, Tick, Value, commands};
|
|
|
|
impl Session {
|
|
/// Render a comp frame with the session's footage + expression hosts.
|
|
pub fn render(&self, comp: ItemId, t: Tick, opts: RenderOpts) -> Image {
|
|
self.renderer(opts).comp_frame(comp, t)
|
|
}
|
|
|
|
/// The renderer [`Session::render`] uses: the session's footage, expressions, layer cache and
|
|
/// accelerator, and the render prefs (nested switches, draft shadows) over `opts`.
|
|
pub fn renderer(&self, opts: RenderOpts) -> Renderer<'_> {
|
|
let mut r = Renderer::new(&self.project, self.footage.as_ref(), self.render_opts(opts));
|
|
r.expr = self.expr.as_deref();
|
|
r.cache = Some(&self.layer_cache);
|
|
r.accel = self.accel.as_deref();
|
|
r
|
|
}
|
|
|
|
/// `opts` with the render prefs (nested switches, draft shadows), as [`Session::renderer`].
|
|
pub fn render_opts(&self, opts: RenderOpts) -> RenderOpts {
|
|
RenderOpts { nested_switches: self.prefs.general.switches_affect_nested_comps, draft_shadows: self.prefs.three_d.realtime_shadows, ..opts }
|
|
}
|
|
|
|
/// Whether `effect.cryptomatteAt` with params `p` answers from the kept probe (a few array
|
|
/// reads; `probeOnly` queries answer `pending` until then). Changes nothing: ask it every
|
|
/// frame, and start [`Session::crypto_probe_job`] only when a build will actually run.
|
|
pub fn crypto_probe_ready(&self, p: &Value) -> Result<bool> {
|
|
commands::crypto_probe_ready(self, p)
|
|
}
|
|
|
|
/// For the Cryptomatte eyedropper: `None` when `effect.cryptomatteAt` with params `p` answers
|
|
/// from the kept probe, else a job that builds that probe (it may render a precomp's ID pass)
|
|
/// off the UI thread, for the next query to use. Asking for a job supersedes a build in
|
|
/// flight for another probe (it then stores nothing): ask only to run it.
|
|
pub fn crypto_probe_job(&self, p: &Value) -> Result<Option<commands::CryptoProbeJob>> {
|
|
commands::crypto_probe_job(self, p)
|
|
}
|
|
|
|
/// The coverage of Cryptomatte ID `hash` (as `effect.cryptomatteAt` reports it, parsed) over
|
|
/// the layer of params `p`, at most `max_side` samples on the long side, from the kept probe:
|
|
/// `None` until [`Session::crypto_probe_job`] has built it.
|
|
pub fn crypto_coverage(&self, p: &Value, hash: u32, max_side: usize) -> Option<commands::CryptoCoverage> {
|
|
commands::crypto_coverage(self, p, hash, max_side)
|
|
}
|
|
|
|
/// Let go of the kept Cryptomatte probe (and its aux channels), and cancel a build in
|
|
/// flight: the eyedropper was disarmed. Edits, footage changes and Open / New do it too.
|
|
pub fn drop_crypto_probe(&self) {
|
|
if let Ok(mut slot) = self.crypto_probe.lock() {
|
|
slot.clear();
|
|
}
|
|
}
|
|
|
|
/// Whether a Cryptomatte probe is kept, and how many were built so far (for tests).
|
|
#[doc(hidden)]
|
|
pub fn crypto_probe_stats(&self) -> (bool, u64) {
|
|
self.crypto_probe.lock().map_or((false, 0), |s| (s.kept(), s.builds().1))
|
|
}
|
|
|
|
/// How many probe jobs ran so far, built or cancelled before they began (for tests: each
|
|
/// is a thread in the eyedropper).
|
|
#[doc(hidden)]
|
|
pub fn crypto_probe_jobs(&self) -> u64 {
|
|
self.crypto_probe.lock().map_or(0, |s| s.builds().0)
|
|
}
|
|
|
|
/// Render a comp frame (comp time `t`) as opaque 8-bit RGBA over the comp background, scaled
|
|
/// so the longest side is at most `max_side` pixels (0 = full size). Returns (width, height, rgba).
|
|
pub fn render_rgba8(&self, comp: ItemId, t: Tick, max_side: u32) -> Result<(u32, u32, Vec<u8>)> {
|
|
let c = self.project.comp(comp).ok_or(EngineError::NoComp)?;
|
|
let long = c.width.max(c.height).max(1) as f64;
|
|
let scale = if max_side == 0 { 1.0 } else { (max_side as f64 / long).min(1.0) };
|
|
let img = self.render(comp, t, RenderOpts { scale, backend: effectcraft_render::Backend::Auto, ..Default::default() });
|
|
Ok((img.width, img.height, img.to_rgba8_over(c.background)))
|
|
}
|
|
|
|
/// A Project panel thumbnail of an item (a comp at `t`, footage at its first frame), longest
|
|
/// side at most `max_side`, as a job that owns everything it reads: frontends run it off the
|
|
/// UI thread (a big comp takes far longer than a frame). `None` for items without pixels.
|
|
pub fn thumbnail_job(&self, item: ItemId, t: Tick, max_side: u32) -> Option<ThumbnailJob> {
|
|
let it = self.project.item(item)?;
|
|
let footage = self.footage.clone();
|
|
match &it.kind {
|
|
effectcraft_project::ItemKind::Comp(c) => {
|
|
let (project, expr, cache) = (self.project.clone(), self.expr.clone(), self.layer_cache.clone());
|
|
let long = c.width.max(c.height).max(1) as f64;
|
|
let scale = if max_side == 0 { 1.0 } else { (max_side as f64 / long).min(1.0) };
|
|
let opts = RenderOpts {
|
|
scale,
|
|
nested_switches: self.prefs.general.switches_affect_nested_comps,
|
|
draft_shadows: self.prefs.three_d.realtime_shadows,
|
|
..Default::default()
|
|
};
|
|
let bg = c.background;
|
|
Some(Box::new(move || {
|
|
let mut r = Renderer::new(&project, footage.as_ref(), opts);
|
|
r.expr = expr.as_deref();
|
|
r.cache = Some(&cache);
|
|
let img = r.comp_frame(item, t);
|
|
Some((img.width, img.height, img.to_rgba8_over(bg)))
|
|
}))
|
|
}
|
|
effectcraft_project::ItemKind::Footage(f) if f.has_video => {
|
|
let f = f.clone();
|
|
Some(Box::new(move || {
|
|
let img = effectcraft_render::cineon_frame(footage.frame(item, &f, Tick(0))?, &f);
|
|
// Point-sample down to at most `max_side` on the long side.
|
|
let step = (img.width.max(img.height) as f32 / max_side.max(1) as f32).ceil().max(1.0) as u32;
|
|
let (w, h) = (img.width.div_ceil(step), img.height.div_ceil(step));
|
|
let full = img.to_rgba8();
|
|
let mut px = Vec::with_capacity((w * h * 4) as usize);
|
|
for y in 0..h {
|
|
for x in 0..w {
|
|
let i = (((y * step) * img.width + x * step) * 4) as usize;
|
|
px.extend_from_slice(&full[i..i + 4]);
|
|
}
|
|
}
|
|
Some((w, h, px))
|
|
}))
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// [`Session::render_rgba8`], or with `transparent` the frame's own straight alpha (what a
|
|
/// render with RGB + Alpha channels writes) instead of compositing over the background.
|
|
pub fn render_rgba8_alpha(&self, comp: ItemId, t: Tick, max_side: u32, transparent: bool) -> Result<(u32, u32, Vec<u8>)> {
|
|
if !transparent {
|
|
return self.render_rgba8(comp, t, max_side);
|
|
}
|
|
let c = self.project.comp(comp).ok_or(EngineError::NoComp)?;
|
|
let long = c.width.max(c.height).max(1) as f64;
|
|
let scale = if max_side == 0 { 1.0 } else { (max_side as f64 / long).min(1.0) };
|
|
let img = self.render(comp, t, RenderOpts { scale, backend: effectcraft_render::Backend::Auto, ..Default::default() });
|
|
Ok((img.width, img.height, img.to_rgba8()))
|
|
}
|
|
|
|
/// Make the media layer and the layer cache let go of what they decoded from `footage`'s
|
|
/// files (it keys frames by path, so an overwritten file would keep showing its old pixels).
|
|
pub(crate) fn forget_decoded(&self, footage: &effectcraft_project::Footage) {
|
|
for p in std::iter::once(&footage.path).chain(&footage.sequence) {
|
|
self.footage.forget(p);
|
|
}
|
|
self.layer_cache.clear();
|
|
self.drop_crypto_probe();
|
|
}
|
|
}
|