mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-28 18:22:06 +01:00
* Layer styles: chiselled bevel facets are flat at any edge angle (#1926) Chisel Hard and Soft shaded the facets of an edge at 30, 60, 120 or 150 degrees in regular cross-stripes (a comb along the edge), while 0, 45, 90 and 135 degree edges were clean. The height of a chisel is a linear ramp of the distance to the edge, and that distance was measured to the nearest pixel centre of a thresholded mask: an oblique edge is a pixel staircase, so the distance ripples with its period (0.19 px at 60 degrees), and the shading is a gradient of it, scaled by depth. Chisel bevels now measure to the edge itself. A sharp anti-aliased edge pixel (partly covered, touching an empty and a full pixel) places the edge by its coverage along its own normal (edge_offset inverts the area a straight edge cuts off a pixel), and a pixel measures to the line of the edge pixels around the foot of its perpendicular, not to the centre of the nearest one (edge_distance). The ripple drops to 0.01 px. Hard-edged masks and soft ramps have no sharp edge pixel and give exactly the plain fields. The GPU compositor takes the bevel distance fields from the CPU through new FieldKind::BevelInside / BevelOutside (the glow fields are unchanged); distance_field takes the reach so values past it are not refined, and the band halo of the bevel fields grows to reach + 9. * Chisel bevel: edge normals from a line fit, exact at hard stretches (#1926) A follow-up to the previous commit, from measuring how far it was from the best it can do. The 3x3 coverage gradient gives an edge's direction to 1-3 degrees, and the error is systematic, so every distance tilted the same way and left a 0.010-0.017 px ripple. With the true normals the ripple is 0.002 to 0.004 px, so the normal was the limit. refine_edge_normals fits a line through the edge points (pixel centre moved to its edge) of the sharp pixels within 3 px that agree to 25 degrees, and takes the line's normal where the points really are on a line (spread along it at least 0.7 px, off it at most 0.1 px). Corners and tight curves keep the gradient's normal. On straight edges it reaches the ideal-normal floor. The inside field now seeds from the empty pixels, as the plain field does, and measures to the sharp pixel next to the nearest empty one, capped by the plain distance (which never falls short of the true one). A circle's pole, or a rounded rectangle on whole pixels, has a stretch of edge exactly on a pixel boundary with no sharp pixel; the distance to the sharp pixels alone was up to 0.29 px too long there. A mask without sharp pixels still gives exactly the plain field. Measured on anti-aliased shapes: ripple on straight edges 0.0105 -> 0.0038 (0.0172 -> 0.0021 at 75 degrees); a disc of radius 70, inside, worst error 0.29 -> 0.02. The band halo of the bevel fields is reach + 9 and the banded test now sees a halo that is too small (the old one passed at reach + 3). * Chisel bevel: sparse sharp-edge storage, no full-map copies (#1926) Measured before sending the fix: the new bevel fields made a bevel size change in the GPU canvas about twice as slow (12 MP: 84 -> 161 ms per step), and nearly all of a step is the CPU effect maps. The cost of the normal refinement was memory, not arithmetic: a Vec<Option<SharpEdge>> is 16 bytes for every pixel, refine_edge_normals copied the whole map twice for a few thousand edge pixels, and local_coverage cloned the whole map and read nine pixels for each. SharpEdges keeps a 4 byte slot per pixel and a short list of the edge pixels. The candidates are found by a row by row pass over which pixels are covered (an edge pixel is covered and has an empty neighbour), and the coverage is worked out where it is needed by the same formula as local_coverage. The refinement works on the list. Nothing else changes: the fields are bit-identical (FNV fingerprints of 27 maps at 5 reaches and of whole bevel_maps at 3 sizes, 216 of 216 equal), and the rendered PNGs of the issue scene and a realistic scene are byte-identical. Release, 12 MP ellipse, size 8, one thread: new fields 408 -> 351 ms (old fields 290); the sharp pass 48 -> 19 ms and the PCA step 32 -> 1 ms. GPU canvas, ms per size change, main / before / after: 12 MP ellipse 84 / 161 / 96, 300 stars 122 / 246 / 166, text 91 / 184 / 139, 36 MP ellipse 295 / 522 / 336. What is left on edge-heavy content is the neighbour search and edge_distance, which this commit does not touch.