diff --git a/crates/gpu/src/shaders/advanced3d.wgsl b/crates/gpu/src/shaders/advanced3d.wgsl index 9ae81ff51..12100cc38 100644 --- a/crates/gpu/src/shaders/advanced3d.wgsl +++ b/crates/gpu/src/shaders/advanced3d.wgsl @@ -197,7 +197,8 @@ fn shadow_factor(m: u32, p: vec3, 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; diff --git a/crates/render/src/three_d/adv/raster.rs b/crates/render/src/three_d/adv/raster.rs index 36cec108c..972a627bb 100644 --- a/crates/render/src/three_d/adv/raster.rs +++ b/crates/render/src/three_d/adv/raster.rs @@ -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 }); } } } diff --git a/crates/render/src/three_d/adv/scene.rs b/crates/render/src/three_d/adv/scene.rs index 1d3acc3ce..14251745d 100644 --- a/crates/render/src/three_d/adv/scene.rs +++ b/crates/render/src/three_d/adv/scene.rs @@ -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 Vec<(Primitive, [f32; 4])> { - let mut groups: Vec<(Vec, [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, [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> = 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); } } diff --git a/crates/render/src/three_d/adv/shade.rs b/crates/render/src/three_d/adv/shade.rs index a8ab5e707..16d052872 100644 --- a/crates/render/src/three_d/adv/shade.rs +++ b/crates/render/src/three_d/adv/shade.rs @@ -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 { diff --git a/crates/render/src/three_d/adv/tests.rs b/crates/render/src/three_d/adv/tests.rs index 23db2cbc8..47ad6178d 100644 --- a/crates/render/src/three_d/adv/tests.rs +++ b/crates/render/src/three_d/adv/tests.rs @@ -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); } diff --git a/docs/architecture.md b/docs/architecture.md index 7e2921e01..c9dfc023f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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; diff --git a/docs/parity.md b/docs/parity.md index af0c9420f..3a1b09320 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -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).