mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-01 05:58:19 -06:00
M7.6: extrusion + primitives
Extruded text keeps per-character 3D: each glyph's outline is extruded in its own space and placed by its animated matrix; shape layers extrude their fill outlines (gradients use their mid colour). Shadow-map bias scales with the texel footprint at the receiver and the PCF reach (no acne on curved casters), identically on CPU and GPU. Docs: Advanced 3D in architecture.md, parity table updated (3D, Text). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
dec091e575
commit
79ad7e1843
@@ -197,7 +197,8 @@ fn shadow_factor(m: u32, p: vec3<f32>, n_l: f32) -> f32 {
|
||||
v = q.y * focal / q.z + c;
|
||||
}
|
||||
let z = q.z;
|
||||
let bias = extra.x * (1.0 + 2.0 * (1.0 - clamp(n_l, 0.0, 1.0))) + abs(z) * 0.0015;
|
||||
let texel = select(abs(z) / focal, 1.0 / focal, ortho);
|
||||
let bias = extra.x * texel * (1.0 + extra.y) * (1.0 + 3.0 * (1.0 - clamp(n_l, 0.0, 1.0)));
|
||||
let step = extra.y / 2.0;
|
||||
let sz = i32(size);
|
||||
var lit = 0.0;
|
||||
|
||||
@@ -460,12 +460,12 @@ pub(crate) fn shadow_maps(s: &mut Scene, draft: bool) {
|
||||
0 => {
|
||||
let eye = [centre[0] - dir[0] * radius * 2.0, centre[1] - dir[1] * radius * 2.0, centre[2] - dir[2] * radius * 2.0];
|
||||
let focal = (size as f64 * 0.5 / radius) as f32;
|
||||
maps.push(ShadowMap { view: basis(eye, dir, hint), focal, ortho: true, size, offset: 0, bias: 1.5 / focal, radius: soft });
|
||||
maps.push(ShadowMap { view: basis(eye, dir, hint), focal, ortho: true, size, offset: 0, bias: 1.5, radius: soft });
|
||||
}
|
||||
1 => {
|
||||
let half = (l.cos_outer.clamp(-1.0, 1.0).acos() as f64 + 0.05).min(1.45);
|
||||
let focal = (size as f64 * 0.5 / half.tan()) as f32;
|
||||
maps.push(ShadowMap { view: basis(pos, dir, hint), focal, ortho: false, size, offset: 0, bias: 1.0, radius: soft });
|
||||
maps.push(ShadowMap { view: basis(pos, dir, hint), focal, ortho: false, size, offset: 0, bias: 1.5, radius: soft });
|
||||
}
|
||||
_ => {
|
||||
let fs = size / 2;
|
||||
@@ -477,7 +477,7 @@ pub(crate) fn shadow_maps(s: &mut Scene, draft: bool) {
|
||||
([0.0, 0.0, 1.0], [0.0, 1.0, 0.0]),
|
||||
([0.0, 0.0, -1.0], [0.0, 1.0, 0.0]),
|
||||
] {
|
||||
maps.push(ShadowMap { view: basis(pos, f, d), focal: fs as f32 * 0.5, ortho: false, size: fs, offset: 0, bias: 1.0, radius: soft });
|
||||
maps.push(ShadowMap { view: basis(pos, f, d), focal: fs as f32 * 0.5, ortho: false, size: fs, offset: 0, bias: 1.5, radius: soft });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ pub struct ShadowMap {
|
||||
pub ortho: bool,
|
||||
pub size: u32,
|
||||
pub offset: u32,
|
||||
/// Depth bias (world units).
|
||||
/// Depth bias in texels (scaled by the texel footprint at the receiver).
|
||||
pub bias: f32,
|
||||
/// PCF spread (texels).
|
||||
pub radius: f32,
|
||||
@@ -531,33 +531,35 @@ pub fn extrusion_params(ctx: &EvalCtx, layer: &Layer) -> Option<model::extrude::
|
||||
})
|
||||
}
|
||||
|
||||
/// Extruded meshes of a text or shape layer: (mesh in layer space, straight colour).
|
||||
pub fn extruded_meshes(ctx: &EvalCtx, layer: &Layer, p: &model::extrude::ExtrudeParams) -> Vec<(Primitive, [f32; 4])> {
|
||||
let mut groups: Vec<(Vec<kurbo::BezPath>, [f32; 4])> = vec![];
|
||||
/// Extruded meshes of a text or shape layer: (mesh, straight colour, mesh space → layer
|
||||
/// space). Text extrudes each character in its own space, so per-character 3D animation
|
||||
/// (position, rotation, scale in Z) carries over.
|
||||
pub fn extruded_meshes(ctx: &EvalCtx, layer: &Layer, p: &model::extrude::ExtrudeParams) -> Vec<(Primitive, [f32; 4], Mat4)> {
|
||||
let mut groups: Vec<(Vec<kurbo::BezPath>, [f32; 4], Mat4)> = vec![];
|
||||
match layer.source {
|
||||
LayerSource::Text => {
|
||||
if let Some(tg) = crate::text::text_geom(ctx, layer) {
|
||||
for g in tg.glyphs.iter().filter(|g| g.apply_fill && g.fill[3] > 0.0) {
|
||||
groups.push((vec![g.path()], g.fill));
|
||||
groups.push((vec![g.local.clone()], g.fill, g.m));
|
||||
}
|
||||
}
|
||||
}
|
||||
LayerSource::Shape => {
|
||||
if let Some(c) = layer.props.sub("contents") {
|
||||
groups = crate::shapes::fill_outlines(ctx, layer, c);
|
||||
groups = crate::shapes::fill_outlines(ctx, layer, c).into_iter().map(|(ps, c)| (ps, c, Mat4::IDENTITY)).collect();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
groups
|
||||
.into_iter()
|
||||
.filter_map(|(paths, color)| {
|
||||
.filter_map(|(paths, color, m)| {
|
||||
let cs: Vec<Vec<[f64; 2]>> = paths.iter().flat_map(|bp| contours(bp, 0.25)).collect();
|
||||
let outlines = model::extrude::group_contours(cs);
|
||||
if outlines.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some((model::extrude::extrude(&outlines, p), color))
|
||||
Some((model::extrude::extrude(&outlines, p), color, m))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -567,7 +569,7 @@ fn extruded_layer(r: &Renderer, ctx: &EvalCtx, layer: &Layer, p: &model::extrude
|
||||
let opacity = (ctx.opacity(layer) as f32 * r.opacity_mul()).clamp(0.0, 1.0);
|
||||
let world = ctx.world_matrix(layer);
|
||||
let lin = b.s.linear_io;
|
||||
for (mesh, c) in extruded_meshes(ctx, layer, p) {
|
||||
for (mesh, c, local) in extruded_meshes(ctx, layer, p) {
|
||||
let l = |v: f32| if lin { v } else { srgb_to_linear(v) };
|
||||
let m = b.material(Material {
|
||||
base: [l(c[0]), l(c[1]), l(c[2]), c[3]],
|
||||
@@ -576,7 +578,8 @@ fn extruded_layer(r: &Renderer, ctx: &EvalCtx, layer: &Layer, p: &model::extrude
|
||||
double_sided: false,
|
||||
..base
|
||||
});
|
||||
b.emit(&mesh, m, &|_| world);
|
||||
let xf = world * local;
|
||||
b.emit(&mesh, m, &|_| xf);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -212,7 +212,9 @@ pub fn shadow_factor(m: &ShadowMap, texels: &[f32], p: [f32; 3], n_l: f32) -> f3
|
||||
let Some((u, v, z)) = shadow_coord(m, p) else { return 1.0 };
|
||||
let size = m.size as i64;
|
||||
// Slope-scaled bias (world units are pixels).
|
||||
let bias = m.bias * (1.0 + 2.0 * (1.0 - n_l.clamp(0.0, 1.0))) + z.abs() * 0.0015;
|
||||
// Texel footprint (world units) × PCF reach, steeper for grazing light.
|
||||
let texel = if m.ortho { 1.0 / m.focal } else { z.abs() / m.focal };
|
||||
let bias = m.bias * texel * (1.0 + m.radius) * (1.0 + 3.0 * (1.0 - n_l.clamp(0.0, 1.0)));
|
||||
let step = m.radius / 2.0;
|
||||
let mut lit = 0.0;
|
||||
for j in -2..=2 {
|
||||
|
||||
@@ -308,7 +308,7 @@ fn extruded_text_renders_as_a_mesh() {
|
||||
let params = scene::extrusion_params(&ctx, l).unwrap();
|
||||
let meshes = scene::extruded_meshes(&ctx, l, ¶ms);
|
||||
assert!(!meshes.is_empty());
|
||||
assert!(meshes.iter().all(|(m, _)| m.positions.iter().any(|q| (q[2] - 30.0).abs() < 1e-3)));
|
||||
assert!(meshes.iter().all(|(m, _, _)| m.positions.iter().any(|q| (q[2] - 30.0).abs() < 1e-3)));
|
||||
let img = render(&p, cid);
|
||||
assert!(img.data.iter().filter(|q| q[3] > 0.5).count() > 200);
|
||||
}
|
||||
|
||||
+29
-1
@@ -69,7 +69,35 @@ the **source** (solid, footage frame, text, shape contents, or a nested comp), a
|
||||
then **effects** in order, then **layer styles**, then the **transform** into comp space (with motion blur sub-samples
|
||||
when enabled), the **track matte**, and finally **blends** into the accumulator. Adjustment layers
|
||||
apply their effects to the accumulator. Runs of 3D layers are composited per pixel through the
|
||||
active camera, with lights, shadows and depth of field.
|
||||
active camera, with lights, shadows and depth of field (Classic 3D), or rasterised as meshes by
|
||||
the Advanced 3D renderer (below).
|
||||
|
||||
**Advanced 3D** (Composition Settings ▸ 3D Renderer ▸ Advanced 3D, `comp.renderer`;
|
||||
`crates/render/src/three_d/adv`, `crates/model`). A run of 3D layers becomes one scene of
|
||||
world-space triangles: 3D model layers (glTF 2.0 / OBJ footage, mapped from +Y-up model units
|
||||
into the layer by Geometry Options ▸ Model Scale; skins and animation clips played along layer
|
||||
time), primitive layers (Layer ▸ New ▸ Cube/Sphere/Plane/Torus/Cone/Cylinder, parametric in
|
||||
Geometry Options), text and shape layers with Geometry Options ▸ Extrusion Depth (glyph and
|
||||
fill outlines flattened, triangulated with holes and extruded with Angular/Concave/Convex
|
||||
bevels, per character so per-character 3D carries over), and a textured card for every other
|
||||
3D layer. Shading is physically based in linear light (glTF metallic-roughness; Cook–Torrance
|
||||
GGX specular, Smith–Schlick geometry, Schlick Fresnel, Lambert diffuse, after Karis 2013):
|
||||
parallel, spot and point lights with After Effects' falloffs, ambient lights, image-based
|
||||
light from an Environment light whose Source (or the comp's Environment Layer, Layer ▸
|
||||
Environment Layer) is an equirectangular image (cosine irradiance map; roughness-indexed mip
|
||||
chain with the split-sum environment BRDF fit), and percentage-closer-filtered shadow maps
|
||||
(spot: perspective, parallel: orthographic over the scene, point: six cube faces). Cards keep
|
||||
Material Options (Accepts Lights/Shadows, Casts Shadows On/Only, Ambient, Diffuse, Specular);
|
||||
models and primitives add Base Color, Metallic, Roughness, Emissive. Rasterisation runs at 2×2
|
||||
supersampling with a reversed-Z depth buffer; opaque triangles resolve visibility first, the
|
||||
transparent ones are sorted back to front and blended. The software rasteriser
|
||||
(`adv::raster`) is the reference and the headless/CI path; `effectcraft-gpu` implements
|
||||
`Accelerator::raster_3d` with a wgpu render pipeline (`advanced3d.wgsl`) that repeats the
|
||||
shading step for step, and tests compare the two. Resolve, depth of field (a gather blur by
|
||||
each pixel's circle of confusion from the camera's Depth of Field settings) and the conversion
|
||||
back to the working encoding run on the CPU for both. Not yet: motion blur, track mattes and
|
||||
blend modes inside an Advanced 3D run, collapsed 3D precomps (drawn flattened), morph targets,
|
||||
extruded strokes.
|
||||
|
||||
A **layer cache** keeps each layer's finished pixels (source, masks and effects) keyed by a hash of
|
||||
its evaluated inputs, excluding the transform. Static and transform-only layers render once;
|
||||
|
||||
+4
-4
@@ -74,7 +74,7 @@ library groundwork for mask tracking and Warp Stabilizer.
|
||||
| Wall clock, five parallel agents | 42–50 h | **≈ 8–10 h at today's measured pace; ≈ 25 h by the audit's conservative figures** |
|
||||
|
||||
What is left is concentrated in large systems: Roto Brush and Refine Edge, the 3D camera tracker,
|
||||
Advanced 3D (models, PBR, image-based lighting), the Warp Stabilizer and mask-tracking UI on top of
|
||||
the Warp Stabilizer and mask-tracking UI on top of
|
||||
the new tracking library, a JavaScript scripting object model, SVG/PSD import, WebM, a disk cache,
|
||||
the missing effect categories (3D Channel, Immersive Video, OCIO), native macOS menus, and
|
||||
wasm threads.
|
||||
@@ -91,9 +91,9 @@ wasm threads.
|
||||
| Shapes | 68% | 3.9 | taper and wave strokes (pen tool for shape paths and vertex editing landed in M6.5) |
|
||||
| Compositions | 67% | 7.4 | marker dialog, flowchart, Essential Graphics |
|
||||
| Animation | 65% | 10.4 | puppet, Wiggler/Smoother/Motion Sketch (motion-path handles and the graph editor transform box landed in M5.8) |
|
||||
| Text | ≈ 85% | 1.5 | vertical type is basic (upright characters, no tate-chu-yoko), no OpenType feature panel, no text on 3D bevels; per-character styles, paragraph settings, on-canvas editing and the `sourceText` style API landed (M9.9–M9.10) |
|
||||
| Text | ≈ 86% | 1.3 | vertical type is basic (upright characters, no tate-chu-yoko), no OpenType feature panel, no extruded strokes (extruded, bevelled text landed in M7.6); per-character styles, paragraph settings, on-canvas editing and the `sourceText` style API landed (M9.9–M9.10) |
|
||||
| Web | 60% | 2.5 | threads, browser storage, audio |
|
||||
| 3D | 58% | 20.5 | Advanced 3D (models, PBR, image-based light), 3D camera tracker, multi-view layouts |
|
||||
| 3D | 72% | 12.0 | 3D camera tracker, multi-view layouts, Advanced 3D motion blur and blend modes, cameras/lights from models; Advanced 3D (glTF/OBJ models, primitives, extruded text and shapes, PBR, image-based light, shadow maps, GPU rasteriser) landed in M7.4–M7.6 |
|
||||
| Effects | 58% | 13.6 | Effect Controls widgets (angle dial, point crosshair, eyedropper, curves), GPU versions of more effects, 41 missing effects |
|
||||
| Interface | 55% | 10.4 | drag-to-dock and floating panels, preferences, native macOS menus (viewer rulers/snapping/channels/snapshots landed in M0.13) |
|
||||
| Project | 53% | 8.1 | 8/16/32-bit pipeline and colour management, auto-save, proxies, folder moves |
|
||||
@@ -132,4 +132,4 @@ Mocha and Cineware are third-party and not counted.
|
||||
The engine underneath (keyframes, expressions, shape operators, text animators, Classic 3D, all
|
||||
38 blend modes, the render queue) is deep. Most of what is missing is interactive tooling in the
|
||||
viewer, settings that are stored but not yet rendered, about 69 menu entries that are still
|
||||
disabled, and the large systems: tracking, puppet, paint, roto, Advanced 3D and GPU rendering.
|
||||
disabled, and the large systems: tracking, puppet, paint and roto (Advanced 3D landed in M7.4–M7.6).
|
||||
|
||||
Reference in New Issue
Block a user