mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
Merge branch 'main' into worktree-agent-a4b0610bd14276508
This commit is contained in:
commit
a4f4d72f27
360 files changed
+28430
-578
No files matched your search
@@ -9,11 +9,13 @@
|
||||
//! effectcraft-cli get <comp> <layer> <path> [--time S] read a property
|
||||
//! effectcraft-cli set <comp> <layer> <path> <value> [--time S] [--expression E]
|
||||
//! effectcraft-cli render-frame [--comp C] [--time S|--frame N] [--max-side PX|--scale K] [--out F.png]
|
||||
//! effectcraft-cli render [--comp C] --out FILE [--format h264|prores|webm|png|jpeg|tiff|exr|gif|wav|aiff] [--start S] [--end S]
|
||||
//! effectcraft-cli render [--comp C] --out FILE [--format h264|hevc|av1|prores|webm|png|jpeg|tiff|exr|gif|wav|aiff] [--start S] [--end S]
|
||||
//! [--work-area] [--fps N] [--resolution full|half|third|quarter|K] [--quality best|draft] [--channels rgb|rgba]
|
||||
//! [--jpeg-quality N] [--bitrate KBPS] [--prores proxy|lt|standard|hq|4444|4444xq] [--audio auto|on|off]
|
||||
//! [--profile main|main10] [--level auto|4.1] [--rate-control bitrate|quality] [--video-quality 1-100]
|
||||
//! [--keyint FRAMES] [--webm-codec vp9|av1] [--audio-bitrate KBPS] [--opus-app audio|voice]
|
||||
//! effectcraft-cli render F.ecproj --queue render the project's Render Queue
|
||||
//! effectcraft-cli bench [--comp C] [--time S] [--scale K] [--n N] [--play N] [--gpu] render timings
|
||||
//! effectcraft-cli bench [--comp C] [--time S] [--scale K] [--n N] [--play N] [--gpu [--adv3d]] render timings
|
||||
//! (--gpu: CPU vs GPU ms/frame for every comp at Full and Half)
|
||||
//! effectcraft-cli script FILE.jsx [F.ecproj] | --eval CODE run an After Effects-style script
|
||||
//! effectcraft-cli mcp [--bridge PORT] MCP server on stdio
|
||||
@@ -44,8 +46,12 @@ const USAGE: &str = "usage: effectcraft-cli <info|commands|exec|run|props|get|se
|
||||
render-frame [--comp C] [--time S | --frame N] [--max-side PX | --scale K] [--out F.png]
|
||||
render [--comp C] --out FILE [--format F] [--start S] [--end S] [--work-area] [--fps N]
|
||||
[--resolution full|half|third|quarter|K] [--quality best|draft] [--channels rgb|rgba]
|
||||
[--jpeg-quality N] [--bitrate KBPS] [--prores PROFILE] [--audio auto|on|off] | --queue
|
||||
bench [--comp C] [--time S] [--scale K] [--n N] [--play N] [--gpu] per-layer/effect render timings;
|
||||
[--jpeg-quality N] [--bitrate KBPS] [--prores PROFILE] [--audio auto|on|off]
|
||||
[--profile main|main10] [--level auto|4.1] [--rate-control bitrate|quality] [--video-quality N]
|
||||
[--keyint FRAMES] [--webm-codec vp9|av1] [--audio-bitrate KBPS] [--opus-app audio|voice] | --queue
|
||||
(formats h264|hevc|av1|prores|webm|png|jpeg|tiff|exr|gif|wav|aiff;
|
||||
--profile..--keyint: HEVC / AV1, --audio-bitrate/--opus-app: WebM Opus)
|
||||
bench [--comp C] [--time S] [--scale K] [--n N] [--play N] [--gpu [--adv3d]] per-layer/effect render timings;
|
||||
--play N renders N consecutive frames with/without the layer cache;
|
||||
--gpu compares CPU and GPU ms/frame for every comp at Full and Half
|
||||
script FILE.jsx [F.ecproj] | --eval CODE run JavaScript with the After Effects-style object model
|
||||
@@ -85,6 +91,14 @@ const VALUED: &[&str] = &[
|
||||
"--bitrate",
|
||||
"--prores",
|
||||
"--audio",
|
||||
"--profile",
|
||||
"--level",
|
||||
"--rate-control",
|
||||
"--video-quality",
|
||||
"--keyint",
|
||||
"--webm-codec",
|
||||
"--audio-bitrate",
|
||||
"--opus-app",
|
||||
];
|
||||
|
||||
struct Args {
|
||||
@@ -546,6 +560,22 @@ fn render(args: &Args, json_out: bool) -> Result<(), Failure> {
|
||||
if let Some(v) = args.num("--bitrate")? {
|
||||
p["bitrate"] = json!(v);
|
||||
}
|
||||
for (flag, key) in [
|
||||
("--profile", "profile"),
|
||||
("--level", "level"),
|
||||
("--rate-control", "rateControl"),
|
||||
("--webm-codec", "webmCodec"),
|
||||
("--opus-app", "opusApplication"),
|
||||
] {
|
||||
if let Some(v) = args.opt(flag) {
|
||||
p[key] = json!(v);
|
||||
}
|
||||
}
|
||||
for (flag, key) in [("--video-quality", "quality"), ("--keyint", "keyframeInterval"), ("--audio-bitrate", "audioBitrate")] {
|
||||
if let Some(v) = args.num(flag)? {
|
||||
p[key] = json!(v);
|
||||
}
|
||||
}
|
||||
// Render exactly this item: unqueue whatever the project's queue already holds.
|
||||
for k in 0..s.project.render_queue.len() {
|
||||
let _ = s.execute("renderQueue.setRender", json!({"index": k + 1, "render": false}));
|
||||
@@ -786,6 +816,109 @@ fn adjustment_bench_comp(p: &mut effectcraft_engine::project::Project) -> Option
|
||||
Some(p.add_item("Adjustment Layers (bench)", effectcraft_engine::color::Label::None, None, ItemKind::Comp(comp.into())))
|
||||
}
|
||||
|
||||
/// `bench --gpu`'s Advanced 3D comp (1920×1080): a ground plane, PBR primitives, extruded
|
||||
/// bevelled text, a shadow-casting spot light, a point light and an environment light, seen
|
||||
/// through a depth-of-field camera (hexagonal iris, highlights) with a motion-blurred sphere.
|
||||
fn adv3d_bench_comp(p: &mut effectcraft_engine::project::Project) -> Option<ItemId> {
|
||||
use effectcraft_engine::keyframe::{Keyframe, TextDoc, Value};
|
||||
use effectcraft_engine::project::build::{self, Ids};
|
||||
use effectcraft_engine::project::{Comp, ItemKind, Layer, LayerSource, LightKind, PrimitiveKind, Renderer as R3, Solid};
|
||||
let (w, h) = (1920u32, 1080u32);
|
||||
let mut comp = Comp::new(w, h, effectcraft_time::FrameRate::FPS_30, Tick::from_seconds_f64(10.0));
|
||||
comp.renderer = R3::Advanced3D;
|
||||
comp.enable_motion_blur = true;
|
||||
comp.motion_blur_samples = 8;
|
||||
let mut layers: Vec<Layer> = vec![];
|
||||
let mut add = |p: &mut effectcraft_engine::project::Project, comp: &Comp, src: LayerSource, edit: &dyn Fn(&mut Layer)| {
|
||||
let mut l = build::layer(p, comp, "L", src, (w, h), None);
|
||||
edit(&mut l);
|
||||
layers.insert(0, l);
|
||||
};
|
||||
fn set(l: &mut Layer, path: &str, v: Value) {
|
||||
if let Some(pr) = l.props.prop_mut(path) {
|
||||
pr.value = v;
|
||||
}
|
||||
}
|
||||
add(p, &comp, LayerSource::Primitive { kind: PrimitiveKind::Plane }, &|l| {
|
||||
set(l, "geometryOptions/width", Value::Scalar(6000.0));
|
||||
set(l, "geometryOptions/height", Value::Scalar(6000.0));
|
||||
set(l, "transform/position", Value::Vec3([960.0, 800.0, 0.0]));
|
||||
set(l, "transform/rotationX", Value::Scalar(90.0));
|
||||
});
|
||||
for (i, kind) in [PrimitiveKind::Sphere, PrimitiveKind::Torus, PrimitiveKind::Cube, PrimitiveKind::Sphere].into_iter().enumerate() {
|
||||
add(p, &comp, LayerSource::Primitive { kind }, &|l| {
|
||||
let x = 400.0 + 380.0 * i as f64;
|
||||
set(l, "geometryOptions/radius", Value::Scalar(140.0));
|
||||
set(l, "geometryOptions/tubeRadius", Value::Scalar(45.0));
|
||||
for k in ["width", "height", "depth"] {
|
||||
set(l, &format!("geometryOptions/{k}"), Value::Scalar(220.0));
|
||||
}
|
||||
set(l, "transform/position", Value::Vec3([x, 640.0, 300.0 * i as f64 - 300.0]));
|
||||
set(l, "transform/rotationX", Value::Scalar(35.0));
|
||||
set(l, "transform/rotationY", Value::Scalar(25.0));
|
||||
set(l, "materialOptions/metallic", Value::Scalar(if i % 2 == 1 { 100.0 } else { 0.0 }));
|
||||
set(l, "materialOptions/roughness", Value::Scalar(30.0));
|
||||
set(l, "materialOptions/castsShadows", Value::Enum(1));
|
||||
if i == 0 {
|
||||
l.switches.motion_blur = true;
|
||||
if let Some(pr) = l.props.prop_mut("transform/position") {
|
||||
pr.keys = vec![
|
||||
Keyframe::new(Tick::ZERO, Value::Vec3([x, 640.0, -300.0])),
|
||||
Keyframe::new(Tick::from_seconds_f64(10.0), Value::Vec3([x + 300.0 * 60.0, 640.0, -300.0])),
|
||||
];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
add(p, &comp, LayerSource::Text, &|l| {
|
||||
l.switches.three_d = true;
|
||||
let doc = TextDoc { text: "EffectCraft".into(), size: 180.0, fill: [0.95, 0.75, 0.2, 1.0], apply_fill: true, ..Default::default() };
|
||||
set(l, "text/sourceText", Value::Text(Box::new(doc)));
|
||||
let mut next = 900_000u64;
|
||||
l.props.children.push(build::extrusion_geometry_options(&mut Ids(&mut next)).into());
|
||||
set(l, "geometryOptions/extrusionDepth", Value::Scalar(40.0));
|
||||
set(l, "geometryOptions/bevelStyle", Value::Enum(1));
|
||||
set(l, "transform/position", Value::Vec3([960.0, 260.0, 200.0]));
|
||||
set(l, "transform/rotationY", Value::Scalar(-15.0));
|
||||
});
|
||||
let env = p.add_item(
|
||||
"Environment (bench)",
|
||||
effectcraft_engine::color::Label::None,
|
||||
None,
|
||||
ItemKind::Solid(Solid { color: [0.55, 0.65, 0.9], width: 64, height: 32, pixel_aspect: 1.0 }),
|
||||
);
|
||||
add(p, &comp, LayerSource::Solid { item: env }, &|l| {
|
||||
l.environment = true;
|
||||
l.switches.three_d = true;
|
||||
});
|
||||
add(p, &comp, LayerSource::Light { kind: LightKind::Environment }, &|l| set(l, "lightOptions/intensity", Value::Scalar(40.0)));
|
||||
add(p, &comp, LayerSource::Light { kind: LightKind::Spot }, &|l| {
|
||||
set(l, "transform/position", Value::Vec3([500.0, -600.0, -900.0]));
|
||||
set(l, "transform/poi", Value::Vec3([960.0, 640.0, 0.0]));
|
||||
set(l, "lightOptions/castsShadows", Value::Bool(true));
|
||||
set(l, "lightOptions/coneAngle", Value::Scalar(100.0));
|
||||
set(l, "lightOptions/shadowDiffusion", Value::Scalar(8.0));
|
||||
});
|
||||
add(p, &comp, LayerSource::Light { kind: LightKind::Point }, &|l| {
|
||||
set(l, "transform/position", Value::Vec3([1500.0, 200.0, -500.0]));
|
||||
set(l, "lightOptions/intensity", Value::Scalar(50.0));
|
||||
});
|
||||
add(p, &comp, LayerSource::Camera, &|l| {
|
||||
let zoom = match l.props.prop_mut("cameraOptions/zoom").map(|pr| pr.value.clone()) {
|
||||
Some(Value::Scalar(z)) => z,
|
||||
_ => 2666.7,
|
||||
};
|
||||
set(l, "cameraOptions/dof", Value::Bool(true));
|
||||
set(l, "cameraOptions/focusDistance", Value::Scalar(zoom));
|
||||
set(l, "cameraOptions/aperture", Value::Scalar(120.0));
|
||||
set(l, "cameraOptions/irisShape", Value::Enum(4));
|
||||
set(l, "cameraOptions/highlightGain", Value::Scalar(20.0));
|
||||
set(l, "cameraOptions/highlightThreshold", Value::Scalar(200.0));
|
||||
});
|
||||
comp.layers = layers;
|
||||
Some(p.add_item("Advanced 3D (bench)", effectcraft_engine::color::Label::None, None, ItemKind::Comp(comp.into())))
|
||||
}
|
||||
|
||||
/// `bench --gpu`: CPU vs GPU ms/frame for every comp (or `--comp`) at Full and Half. "cold"
|
||||
/// renders everything (no layer cache); "warm" reuses the layer cache (playback / scrubbing of
|
||||
/// unchanged layers: compositing cost); "viewer" is the GPU display path without readback;
|
||||
@@ -796,14 +929,17 @@ fn bench_gpu(s: &Session, args: &Args) -> Result<(), Failure> {
|
||||
let gpu = effectcraft_gpu::Gpu::headless().ok_or_else(|| Failure::Error("--gpu: no usable GPU adapter".into()))?;
|
||||
let n = args.num("--n")?.unwrap_or(10.0).max(1.0) as usize;
|
||||
// Every comp, plus an adjustment-layer comp built for the benchmark (the main comp under a
|
||||
// full-frame adjustment layer with a GPU effect stack).
|
||||
// full-frame adjustment layer with a GPU effect stack) and an Advanced 3D comp (`--adv3d`:
|
||||
// that one only).
|
||||
let mut project = (*s.project).clone();
|
||||
project.settings.gpu_acceleration = true;
|
||||
let comps: Vec<ItemId> = match args.opt("--comp") {
|
||||
_ if args.flag("--adv3d") => adv3d_bench_comp(&mut project).into_iter().collect(),
|
||||
Some(_) => s.active_comp_id().into_iter().collect(),
|
||||
None => {
|
||||
let mut v: Vec<ItemId> = s.project.comps().map(|(id, _)| *id).collect();
|
||||
v.extend(adjustment_bench_comp(&mut project));
|
||||
v.extend(adv3d_bench_comp(&mut project));
|
||||
v
|
||||
}
|
||||
};
|
||||
@@ -864,7 +1000,17 @@ fn bench_gpu(s: &Session, args: &Args) -> Result<(), Failure> {
|
||||
// Agreement: share of pixels where the GPU frame differs from the CPU reference by
|
||||
// more than 1/255 on any channel.
|
||||
let (a, b) = (mk(Backend::Cpu, Some(&cache)), mk(Backend::Gpu, Some(&gcache)));
|
||||
let off = a.data.iter().zip(&b.data).filter(|(p, q)| (0..4).any(|c| (p[c] - q[c]).abs() > 1.0 / 255.0 + 1e-6)).count();
|
||||
let off = a
|
||||
.data
|
||||
.iter()
|
||||
.zip(&b.data)
|
||||
.filter(|(p, q)| {
|
||||
(0..4).any(|c| {
|
||||
let d = (p[c] - q[c]).abs();
|
||||
d.is_nan() || d > 1.0 / 255.0 + 1e-6
|
||||
})
|
||||
})
|
||||
.count();
|
||||
let pct = 100.0 * off as f64 / a.data.len().max(1) as f64;
|
||||
let speedup = cpu_warm / gpu_warm.max(1e-9);
|
||||
eprintln!(
|
||||
|
||||
Reference in new issue
Block a user