layer.create: the instance3d op - a 2D copy of a 3D model layer (#2100)

Layer > Create > Create 3D Layer Instance, as After Effects (public docs):
3D model layers take no masks or effects, so the command puts a comp-sized
solid at the top of the layer stack with CC Composite referencing the
selected model, and switches the model off (back on to keep its shadow) -
a 2D copy of its render, through the comp's camera, that 2D effects can be
applied to.

- CC Composite (ec.channel.cccomposite) gains an optional Layer parameter:
  with a layer chosen it takes the original's place; In Front replaces the
  solid's pixels, alpha included, the other Composite Original modes blend
  with them. The GPU compositor runs such an instance on the CPU.
- FxHost serves a referenced model or primitive layer as After Effects
  does - the layer rendered on its own through the comp's camera
  (Renderer::solo_3d); the owner keeps no cache entry for it (it moves with
  the camera and lights, without a trace in its own properties).
- layer.create {op: instance3d} skips layers that are not 3D model or
  primitive layers (the ones that take no effects); one undo step, the copy
  selected, the model's video off.

Tests: tests_instance3d.rs (behaviour, pixels, undo/redo, serde, 2D effects
on the copy), a CC Composite layer-parameter unit test; docs/effects.md
regenerated (3033 parameters).
This commit is contained in:
Minher0
2026-10-25 12:46:58 +00:00
parent 991904888a
commit 82cebba68f
8 changed files with 302 additions and 8 deletions
+52 -3
View File
@@ -171,7 +171,9 @@ const CC_COMPOSITE_MODES: [&str; 16] = [
"Stencil Alpha",
];
/// Composites the original stack input over/under/with the result of preceding effects.
/// Composites the original stack input over/under/with the result of preceding effects. With a
/// layer chosen (Create 3D Layer Instance's solid), that layer takes the original's place — a 2D
/// copy of it, rendered through the comp's camera when it is a 3D model layer.
fn cc_composite(ctx: &EffectCtx, mut b: Buf) -> Buf {
let op = (ctx.params.f("opacity") as f32 / 100.0).clamp(0.0, 1.0);
let m = ctx.params.e("compositeOriginal");
@@ -193,11 +195,24 @@ fn cc_composite(ctx: &EffectCtx, mut b: Buf) -> Buf {
15 => BlendMode::StencilAlpha,
_ => BlendMode::Normal,
};
let original = ctx.env.host.and_then(|h| h.original()).map(|o| crate::util::fit_buffer(o, b.img.width, b.img.height, b.offset, b.scale));
// The chosen layer takes the original's place (a 2D copy of it); the layer's own original
// otherwise, as before.
let referenced = ctx.layer_param("layer", true).map(|o| crate::util::fit_layer(ctx, &b, &o, false));
// A chosen layer in front replaces the layer's pixels, alpha included: the copy of a 3D
// layer is transparent where the model isn't. The other modes composite with them.
let replace = referenced.is_some() && m == 0;
let original =
referenced.or_else(|| ctx.env.host.and_then(|h| h.original()).map(|o| crate::util::fit_buffer(o, b.img.width, b.img.height, b.offset, b.scale)));
b.img.data.par_iter_mut().enumerate().for_each(|(i, px)| {
let result = *px;
let orig = original.as_ref().and_then(|o| o.data.get(i)).copied().unwrap_or(result).map(|v| v * op);
let mut o = if m == 1 { blend_pixel(BlendMode::Normal, orig, result, 0.5) } else { blend_pixel(mode, result, orig, 0.5) };
let mut o = if replace {
orig
} else if m == 1 {
blend_pixel(BlendMode::Normal, orig, result, 0.5)
} else {
blend_pixel(mode, result, orig, 0.5)
};
o[3] = o[3].clamp(0.0, 1.0);
if rgb_only {
let (c, _) = unpremul(o);
@@ -323,6 +338,7 @@ pub fn specs() -> Vec<EffectSpec> {
"ec.channel.cccomposite",
"CC Composite",
vec![
layer_param("layer", "Layer"),
p("opacity", "Opacity", num(100.0), pct()),
p("compositeOriginal", "Composite Original", Value::Enum(0), popup(&CC_COMPOSITE_MODES)),
p("rgbOnly", "RGB Only", Value::Bool(false), ParamUi::Checkbox),
@@ -423,6 +439,39 @@ pub(crate) mod tests {
assert!((add.img.get(0, 0)[0] - 0.6).abs() < 1e-5);
}
#[test]
fn cc_composite_layer_takes_the_originals_place() {
// Create 3D Layer Instance's solid: the chosen layer takes the original's place. In
// Front replaces the pixels — alpha included, the copy of a 3D layer is transparent
// where the model isn't — and the other modes composite with them.
let solid = || Image::filled(4, 4, [1.0, 1.0, 1.0, 1.0]);
let model = |write: bool| {
let mut img = Image::new(4, 4);
if write {
img.set(1, 1, [1.0, 0.0, 0.0, 1.0]);
}
img
};
let out = with_host(model(true), |env| run_fx("ec.channel.cccomposite", &[("layer", Value::Layer(Some(7)))], solid(), 0.0, env));
assert_eq!(out.img.get(1, 1), [1.0, 0.0, 0.0, 1.0], "the chosen layer's pixel");
assert_eq!(out.img.get(3, 3), [0.0; 4], "transparent where the chosen layer isn't");
// Half opacity keeps the copy's shape at half strength.
let out =
with_host(model(true), |env| run_fx("ec.channel.cccomposite", &[("layer", Value::Layer(Some(7))), ("opacity", num(50.0))], solid(), 0.0, env));
let p = out.img.get(1, 1);
assert!((p[0] - 0.5).abs() < 1e-6 && (p[3] - 0.5).abs() < 1e-6, "{p:?}");
assert_eq!(out.img.get(3, 3), [0.0; 4]);
// In Back: the layer's own pixels over the chosen layer.
let out = with_host(model(true), |env| {
run_fx("ec.channel.cccomposite", &[("layer", Value::Layer(Some(7))), ("compositeOriginal", Value::Enum(1))], solid(), 0.0, env)
});
assert_eq!(out.img.get(1, 1), [1.0, 1.0, 1.0, 1.0]);
assert_eq!(out.img.get(3, 3), [1.0, 1.0, 1.0, 1.0]);
// No layer chosen: the layer's own original, as before.
let out = with_host(model(true), |env| run_fx("ec.channel.cccomposite", &[], Image::filled(4, 4, [0.5, 0.5, 0.5, 1.0]), 0.0, env));
assert_eq!(out.img.get(0, 0), [0.5, 0.5, 0.5, 1.0]);
}
#[test]
fn compound_arithmetic_copy_self_identity_and_add() {
let img = sample();
+49 -3
View File
@@ -1,14 +1,17 @@
//! Layer ▸ Create: Convert to Editable Text (Photoshop type layers), Create Shapes from Text,
//! Create Masks from Text and Create Shapes from Vector Layer (SVG footage).
//! Create Masks from Text, Create Shapes from Vector Layer (SVG footage) and Create 3D Layer
//! Instance (a 2D copy of a 3D model layer that 2D effects can be applied to).
use effectcraft_color::Label;
use effectcraft_keyframe::Value as KV;
use effectcraft_project::build::{self, Ids};
use effectcraft_project::{ItemKind, LayerSource, Solid};
use serde_json::{Value, json};
use super::{CommandSpec, bad, has_layers, layers_p, str_p};
use crate::{EngineError, Result, Session, cmd};
const OPS: &str = "editableText|shapesFromText|masksFromText|shapesFromVector";
const OPS: &str = "editableText|shapesFromText|masksFromText|shapesFromVector|instance3d";
fn create(s: &mut Session, p: &Value) -> Result<Value> {
let op = str_p(p, "op").ok_or_else(|| bad("layer.create", format!("missing `op` ({OPS})")))?;
@@ -151,6 +154,49 @@ fn create(s: &mut Session, p: &Value) -> Result<Value> {
})?;
created.push(id.0);
}
"instance3d" => {
// After Effects' Create 3D Layer Instance (Layer ▸ Create): 3D model layers take
// no masks or effects, so a comp-sized solid goes at the top of the layer stack
// with CC Composite referencing the model — a 2D copy of its render, through the
// comp's camera, that 2D effects can be applied to. The model is switched off,
// as After Effects does (back on to keep its shadow).
if !src.source.is_model() {
skipped.push(format!("{}: not a 3D model layer", src.name));
continue;
}
let id = s.edit("Create 3D Layer Instance", None, |proj, st| {
let name = comp.unique_layer_name(&format!("{} Instance", src.name));
let folder = proj.folder_named("Solids").unwrap_or_else(|| proj.add_item("Solids", Label::Yellow, None, ItemKind::Folder));
let sid = proj.add_item(
&name,
src.label,
Some(folder),
ItemKind::Solid(Solid { color: [1.0, 1.0, 1.0], width: comp.width, height: comp.height, pixel_aspect: 1.0 }),
);
let mut l = build::layer(proj, &comp, &name, LayerSource::Solid { item: sid }, (comp.width, comp.height), None);
// CC Composite referencing the 3D layer: the solid shows its render (the 2D
// copy).
let fx = effectcraft_effects::find("ec.channel.cccomposite").ok_or_else(|| bad("layer.create", "the CC Composite effect is missing"))?;
let mut g = effectcraft_effects::instantiate(fx, &mut Ids(&mut proj.next_id), fx.name, [comp.width as f64, comp.height as f64]);
if let Some(pr) = g.get_mut("layer") {
pr.value = KV::Layer(Some(lid.0));
}
if let Some(fxg) = l.props.sub_mut("effects") {
fxg.children.push(g.into());
}
let c = proj.comp_mut(cid).ok_or(EngineError::NoComp)?;
// The model is switched off (After Effects turns the referenced layer off);
// the copy goes at the top of the stack, selected.
if let Some(m) = c.layers.iter_mut().find(|x| x.id == lid) {
m.switches.video = false;
}
let id = l.id;
c.layers.insert(0, l);
st.selected_layers = vec![id];
Ok(id)
})?;
created.push(id.0);
}
other => return Err(bad("layer.create", format!("`{other}` is not available yet (supported: {OPS})"))),
}
}
@@ -198,5 +244,5 @@ fn insert_above(
}
pub fn specs() -> Vec<CommandSpec> {
vec![cmd!("layer.create", "Create", [], None, "{op: editableText|shapesFromText|masksFromText|shapesFromVector, layers?}", has_layers, create)]
vec![cmd!("layer.create", "Create", [], None, "{op: editableText|shapesFromText|masksFromText|shapesFromVector|instance3d, layers?}", has_layers, create)]
}
+2
View File
@@ -1305,6 +1305,8 @@ mod tests_fuzz_open;
#[cfg(test)]
mod tests_import_project;
#[cfg(test)]
mod tests_instance3d;
#[cfg(test)]
mod tests_key_metadata_order;
#[cfg(test)]
mod tests_keylight;
+157
View File
@@ -0,0 +1,157 @@
//! Create 3D Layer Instance (Layer ▸ Create): a comp-sized solid at the top of the layer stack
//! with CC Composite referencing the selected 3D model layer — a 2D copy of its render that 2D
//! effects can be applied to (behaviour, pixels, undo/redo, serde).
use std::sync::Arc;
use effectcraft_keyframe::Value as KV;
use effectcraft_project::{Footage, FootageKind, GroupKind, ItemId, Layer, LayerId, Project};
use effectcraft_render::{FootageSource, RenderOpts};
use effectcraft_time::{FrameRate, Tick};
use serde_json::json;
use crate::{Importer, Session};
fn session() -> Session {
let mut s = Session { footage: Arc::new(DiskModels), importer: Some(Arc::new(ModelImporter)), ..Default::default() };
s.execute("comp.new", json!({"name": "Scene", "width": 640, "height": 360, "frameRate": 30, "duration": 2})).unwrap();
s
}
fn cid(s: &Session) -> ItemId {
s.active_comp_id().unwrap()
}
fn layer_in(s: &Session, comp: ItemId, id: u64) -> Layer {
s.project.comp(comp).unwrap().layer(LayerId(id)).unwrap().clone()
}
fn px(img: &effectcraft_raster::Image, x: u32, y: u32) -> [f32; 4] {
img.data[(y * img.width + x) as usize]
}
fn close(a: &effectcraft_raster::Image, b: &effectcraft_raster::Image, tol: f32) -> bool {
a.data.iter().zip(b.data.iter()).all(|(p, q)| p.iter().zip(q.iter()).all(|(x, y)| (x - y).abs() <= tol))
}
struct DiskModels;
impl FootageSource for DiskModels {
fn frame(&self, _: ItemId, _: &Footage, _: Tick) -> Option<Arc<effectcraft_raster::Image>> {
None
}
fn model(&self, _: ItemId, f: &Footage) -> Option<Arc<effectcraft_model::Model>> {
let bytes = std::fs::read(&f.path).ok()?;
effectcraft_model::load(&f.path, &bytes, &|_| None).ok().map(Arc::new)
}
}
struct ModelImporter;
impl Importer for ModelImporter {
fn probe(&self, path: &str) -> Result<Footage, String> {
effectcraft_model::format_of(path).ok_or("not a model")?;
Ok(Footage { path: path.into(), kind: FootageKind::Model, frame_rate: FrameRate::FPS_30, codec: "glTF".into(), ..Default::default() })
}
}
/// A red triangle in the Z = 0 plane (buffer embedded as a data URI).
const TRIANGLE: &str = r#"{
"asset": {"version": "2.0"},
"scenes": [{"nodes": [0]}],
"nodes": [{"mesh": 0}],
"meshes": [{"primitives": [{"attributes": {"POSITION": 0, "NORMAL": 1}, "material": 0}]}],
"materials": [{"pbrMetallicRoughness": {"baseColorFactor": [1.0, 0.0, 0.0, 1.0]}}],
"buffers": [{"byteLength": 72, "uri": "data:application/octet-stream;base64,AACAvwAAgL8AAAAAAACAPwAAgL8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/"}],
"bufferViews": [
{"buffer": 0, "byteOffset": 0, "byteLength": 36},
{"buffer": 0, "byteOffset": 36, "byteLength": 36}
],
"accessors": [
{"bufferView": 0, "componentType": 5126, "type": "VEC3", "count": 3, "min": [-1.0, -1.0, 0.0], "max": [1.0, 1.0, 0.0]},
{"bufferView": 1, "componentType": 5126, "type": "VEC3", "count": 3}
]
}"#;
fn triangle_model(s: &mut Session) -> u64 {
let dir = std::env::temp_dir().join(format!("ec-instance3d-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("Triangle.gltf");
std::fs::write(&path, TRIANGLE).unwrap();
s.execute("layer.newModel", json!({"path": path.to_string_lossy(), "name": "Triangle"})).unwrap()["layer"].as_u64().unwrap()
}
#[test]
fn instance3d_creates_a_2d_copy_of_the_model() {
let mut s = session();
let model = triangle_model(&mut s);
// The model renders through the comp's camera.
let before = s.render(cid(&s), Tick::ZERO, RenderOpts::default());
assert!(px(&before, 320, 180)[3] > 0.9, "the model covers the centre: {:?}", px(&before, 320, 180));
assert!(px(&before, 4, 4)[3] < 0.1, "the comp is transparent around it: {:?}", px(&before, 4, 4));
// Create 3D Layer Instance.
s.execute("layer.select", json!({"layers": [model]})).unwrap();
let r = s.execute("layer.create", json!({"op": "instance3d"})).unwrap();
let solid = r["layers"][0].as_u64().unwrap();
let comp = cid(&s);
let l = layer_in(&s, comp, solid);
assert_eq!(l.name, "Triangle Instance");
assert_eq!(s.active_comp().unwrap().layers[0].id.0, solid, "at the top of the layer stack");
// CC Composite referencing the 3D layer.
let g = l.effects().unwrap().groups().next().unwrap().clone();
assert!(matches!(&g.kind, GroupKind::Effect { effect } if effect == "ec.channel.cccomposite"), "{:?}", g.kind);
assert_eq!(g.get("layer").unwrap().value, KV::Layer(Some(model)));
// The model is switched off, the copy selected.
assert!(!layer_in(&s, comp, model).switches.video, "the referenced 3D layer is off, as After Effects does");
assert_eq!(s.state.selected_layers, vec![LayerId(solid)]);
// The comp renders the same through the copy: a 2D copy of the 3D layer, transparent
// around it.
let after = s.render(comp, Tick::ZERO, RenderOpts::default());
assert!(close(&before, &after, 2e-2), "the copy shows the model:\n{:?}\n{:?}", px(&before, 320, 180), px(&after, 320, 180));
assert!(px(&after, 320, 180)[3] > 0.9 && px(&after, 4, 4)[3] < 0.1, "{:?} {:?}", px(&after, 320, 180), px(&after, 4, 4));
// Undo removes the copy and switches the model back on; redo returns it.
s.execute("edit.undo", json!({})).unwrap();
assert!(s.active_comp().unwrap().layer(LayerId(solid)).is_none());
assert!(layer_in(&s, comp, model).switches.video);
s.execute("edit.redo", json!({})).unwrap();
assert!(s.active_comp().unwrap().layer(LayerId(solid)).is_some());
assert!(!layer_in(&s, comp, model).switches.video);
// Serde round-trips the layer parameter.
let j = serde_json::to_string(&*s.project).unwrap();
let back: Project = serde_json::from_str(&j).unwrap();
assert_eq!(back, *s.project);
// 2D effects can be applied to the copy (the model layer itself takes none): Fill in blue
// recolours the triangle.
s.execute("effect.apply", json!({"layer": solid, "effect": "ec.generate.fill"})).unwrap();
s.execute("prop.set", json!({"layer": solid, "path": "effects/#2/color", "value": [0.0, 0.0, 1.0, 1.0]})).unwrap();
let with_fx = s.render(comp, Tick::ZERO, RenderOpts::default());
let p = px(&with_fx, 320, 180);
assert!(p[2] > 0.9 && p[0] < 0.1, "the fill recolours the copy: {p:?}");
}
#[test]
fn instance3d_also_takes_3d_primitives() {
let mut s = session();
let prim = s.execute("layer.new3dPrimitive", json!({"kind": "cube", "width": 200, "height": 200, "depth": 200})).unwrap()["layer"].as_u64().unwrap();
let before = s.render(cid(&s), Tick::ZERO, RenderOpts::default());
let r = s.execute("layer.create", json!({"op": "instance3d", "layers": [prim]})).unwrap();
let solid = r["layers"][0].as_u64().unwrap();
let l = layer_in(&s, cid(&s), solid);
assert_eq!(l.name, "Cube Instance");
assert!(!layer_in(&s, cid(&s), prim).switches.video);
let after = s.render(cid(&s), Tick::ZERO, RenderOpts::default());
assert!(close(&before, &after, 2e-2), "the copy shows the cube");
}
#[test]
fn instance3d_needs_a_3d_layer() {
let mut s = session();
let solid = s.execute("layer.newSolid", json!({"color": "#808080", "width": 100, "height": 100})).unwrap()["layer"].as_u64().unwrap();
let err = s.execute_checked("layer.create", json!({"op": "instance3d", "layers": [solid]}));
assert!(err.is_err());
let text = s.execute("layer.newText", json!({"text": "hi"})).unwrap()["layer"].as_u64().unwrap();
assert!(s.execute_checked("layer.create", json!({"op": "instance3d", "layers": [text]})).is_err());
// A 3D-switched text layer is not a model layer either (it has 2D pixels of its own).
s.execute("layer.setSwitch", json!({"layers": [text], "switch": "threeD", "value": true})).unwrap();
assert!(s.execute_checked("layer.create", json!({"op": "instance3d", "layers": [text]})).is_err());
}
+6
View File
@@ -247,6 +247,12 @@ fn point(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
p.f[0] = pr.color("highlightColor");
}
"ec.channel.cccomposite" => {
// A chosen layer (Create 3D Layer Instance's solid) runs on the CPU: its pixels — a
// referenced 3D model rendered through the comp's camera — replace the layer's own
// original, alpha included.
if ctx.layer_param("layer", true).is_some() {
return None;
}
if let Some(source) = ctx.env.host.and_then(|h| h.original()) {
let fitted = effectcraft_effects::util::fit_buffer(source, b.img.width, b.img.height, b.offset, b.scale);
original = Some(e.g.upload_image(&fitted)?);
+7
View File
@@ -68,6 +68,13 @@ impl Dependencies<'_> {
if self.path.len() > crate::MAX_FX_DEPTH || self.path.contains(&id) {
return None;
}
// A referenced 3D model or primitive layer renders through the comp's camera and lights
// (an effect layer parameter reads it that way — Create 3D Layer Instance's CC
// Composite): those move its pixels without a trace in its own properties, so the owner
// keeps no cache entry for it.
if layer.source.is_model() {
return None;
}
self.remaining = self.remaining.checked_sub(1)?;
if (self.other_times || super::reads_other_times(layer)) && super::has_expression(&layer.props) {
return None;
+27
View File
@@ -277,6 +277,14 @@ impl EffectHost for FxHost<'_, '_, '_> {
return None;
}
let sub = self.r.nested();
// A 3D model or primitive layer has no 2D source of its own: an effect referencing one
// reads it as After Effects does — the layer rendered on its own through the comp's
// camera (Create 3D Layer Instance's CC Composite), in the layer's effect space (the
// comp, centred on the layer's anchor).
if other.source.is_model() {
let buf = sub.solo_3d(self.ctx, other);
return Some(self.pixels(other, buf, false));
}
// The cached layer buffer is shared; effects get their own copy.
let buf = if masks_and_effects { (*sub.content_buf(self.ctx, other)?).clone() } else { sub.source(self.ctx, other)? };
Some(self.pixels(other, buf, true))
@@ -1493,6 +1501,25 @@ impl<'a> Renderer<'a> {
self.source(ctx, layer)
}
/// A 3D model or primitive layer rendered on its own, through the comp's camera: what an
/// effect layer parameter reads when it references one, as After Effects, which renders 3D
/// layers through the active camera. The comp's pixels at this render's scale; the layer's
/// blend mode, track matte and video switch don't apply (a source read, not a composite —
/// the solo clone of a 3D track matte, see `three_d::adv`).
fn solo_3d(&self, ctx: &EvalCtx, layer: &Layer) -> Buf {
let s = self.opts.scale;
let w = ((ctx.comp.width as f64 * s).ceil() as u32).max(1);
let h = ((ctx.comp.height as f64 * s).ceil() as u32).max(1);
let mut solo = layer.clone();
solo.track_matte = None;
solo.blend_mode = effectcraft_color::BlendMode::Normal;
solo.preserve_transparency = false;
solo.switches.video = true;
let mut canvas = Image::new(w, h);
three_d::compose::draw_run(self, ctx, &[&solo], &mut canvas);
Buf { img: canvas, offset: [0.0; 2], scale: s }
}
/// Layer pixels after source → masks → effects (no layer styles).
pub fn content_buf(&self, ctx: &EvalCtx, layer: &Layer) -> Option<Arc<Buf>> {
self.content_buf_timed(ctx, layer, None).map(|(b, _)| b)
+2 -2
View File
@@ -3,7 +3,7 @@
<!-- Generated from the effect registry (crates/effects/src/catalog.rs); do not edit by hand.
Regenerate: UPDATE_DOCS=1 cargo test -p effectcraft-effects --lib effects_doc_is_current -->
EffectCraft ships 307 effects with 3032 parameters in total, grouped into the same categories as the Effects & Presets panel. Every effect is our own implementation, written from public behaviour descriptions and standard image-processing literature. Parameter names, order, popup options, units, defaults and ranges follow the reference application so that projects, expressions and muscle memory carry over.
EffectCraft ships 307 effects with 3033 parameters in total, grouped into the same categories as the Effects & Presets panel. Every effect is our own implementation, written from public behaviour descriptions and standard image-processing literature. Parameter names, order, popup options, units, defaults and ranges follow the reference application so that projects, expressions and muscle memory carry over.
- **GPU**: 280 effects also run on the GPU compositor with identical results.
- **32**: 307 effects process 32-bit float (HDR, overbright) pixels without clamping.
@@ -69,7 +69,7 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|---|---|---:|:-:|:-:|---|
| Arithmetic | `ec.channel.arithmetic` | 5 | GPU | 32 | Implemented |
| Blend | `ec.channel.blend` | 4 | GPU | 32 | Implemented |
| CC Composite | `ec.channel.cccomposite` | 3 | GPU | 32 | Implemented |
| CC Composite | `ec.channel.cccomposite` | 4 | GPU | 32 | Implemented |
| Calculations | `ec.channel.calculations` | 9 | GPU | 32 | Implemented |
| Channel Combiner | `ec.channel.combiner` | 6 | GPU | 32 | Implemented |
| Compound Arithmetic | `ec.channel.compoundarithmetic` | 6 | GPU | 32 | Implemented |