package font import "core:mem" import "core:math" // ============================================================================ // SDF (SIGNED DISTANCE FIELD) RENDERING (Phase 6) // ============================================================================ @(private) cuberoot :: proc(x: f32) -> f32 { if x < 0 { return -math.pow(-x, 1.0 / 3.0) } else { return math.pow(x, 1.0 / 3.0) } } // Solve cubic equation: x^3 + a*x^2 + b*x + c = 0 // Returns number of roots (1 or 3), stores roots in r @(private) solve_cubic :: proc(a, b, c: f32, r: [^]f32) -> i32 { s := -a / 3 p := b - a * a / 3 q := a * (2 * a * a - 9 * b) / 27 + c p3 := p * p * p d := q * q + 4 * p3 / 27 if d >= 0 { z := math.sqrt(d) u := (-q + z) / 2 v := (-q - z) / 2 u = cuberoot(u) v = cuberoot(v) r[0] = s + u + v return 1 } else { u := math.sqrt(-p / 3) v := math.acos(-math.sqrt(-27 / p3) * q / 2) / 3 // p3 must be negative since d is negative m := math.cos(v) n := math.cos(v - 3.141592 / 2) * 1.732050808 r[0] = s + u * 2 * m r[1] = s - u * (m + n) r[2] = s - u * (m - n) return 3 } } // Find intersections between a ray and a quadratic Bezier curve // Returns number of hits (0, 1, or 2) @(private) ray_intersect_bezier :: proc(orig: [2]f32, ray: [2]f32, q0: [2]f32, q1: [2]f32, q2: [2]f32, hits: ^[2][2]f32) -> i32 { q0perp := q0[1] * ray[0] - q0[0] * ray[1] q1perp := q1[1] * ray[0] - q1[0] * ray[1] q2perp := q2[1] * ray[0] - q2[0] * ray[1] roperp := orig[1] * ray[0] - orig[0] * ray[1] a := q0perp - 2 * q1perp + q2perp b := q1perp - q0perp c := q0perp - roperp s0: f32 = 0 s1: f32 = 0 num_s: i32 = 0 if a != 0.0 { discr := b * b - a * c if discr > 0.0 { rcpna := -1 / a d := math.sqrt(discr) s0 = (b + d) * rcpna s1 = (b - d) * rcpna if s0 >= 0.0 && s0 <= 1.0 { num_s = 1 } if d > 0.0 && s1 >= 0.0 && s1 <= 1.0 { if num_s == 0 { s0 = s1 } num_s += 1 } } } else { // 2*b*s + c = 0 // s = -c / (2*b) s0 = c / (-2 * b) if s0 >= 0.0 && s0 <= 1.0 { num_s = 1 } } if num_s == 0 { return 0 } else { rcp_len2 := 1 / (ray[0] * ray[0] + ray[1] * ray[1]) rayn_x := ray[0] * rcp_len2 rayn_y := ray[1] * rcp_len2 q0d := q0[0] * rayn_x + q0[1] * rayn_y q1d := q1[0] * rayn_x + q1[1] * rayn_y q2d := q2[0] * rayn_x + q2[1] * rayn_y rod := orig[0] * rayn_x + orig[1] * rayn_y q10d := q1d - q0d q20d := q2d - q0d q0rd := q0d - rod hits[0][0] = q0rd + s0 * (2.0 - 2.0 * s0) * q10d + s0 * s0 * q20d hits[0][1] = a * s0 + b if num_s > 1 { hits[1][0] = q0rd + s1 * (2.0 - 2.0 * s1) * q10d + s1 * s1 * q20d hits[1][1] = a * s1 + b return 2 } else { return 1 } } } @(private) point_equal :: proc(a: [2]f32, b: [2]f32) -> bool { return a[0] == b[0] && a[1] == b[1] } // Compute winding number by counting edge crossings from (-infinity, y) to (x, y) @(private) compute_crossings_x :: proc(x, y_in: f32, nverts: i32, verts: [^]Vertex) -> i32 { ray: [2]f32 = {1, 0} winding: i32 = 0 // Make sure y never passes through a vertex of the shape y := y_in y_frac := math.mod(y, 1.0) if y_frac < 0.01 { y += 0.01 } else if y_frac > 0.99 { y -= 0.01 } orig: [2]f32 = {x, y} // Test a ray from (-infinity, y) to (x, y) #no_bounds_check for i: i32 = 0; i < nverts; i += 1 { if verts[i].type == VLINE { x0 := i32(verts[i - 1].x) y0 := i32(verts[i - 1].y) x1 := i32(verts[i].x) y1 := i32(verts[i].y) if y > f32(min(y0, y1)) && y < f32(max(y0, y1)) && x > f32(min(x0, x1)) { x_inter := (y - f32(y0)) / f32(y1 - y0) * f32(x1 - x0) + f32(x0) if x_inter < x { winding += 1 if y0 < y1 else -1 } } } if verts[i].type == VCURVE { x0 := i32(verts[i - 1].x) y0 := i32(verts[i - 1].y) x1 := i32(verts[i].cx) y1 := i32(verts[i].cy) x2 := i32(verts[i].x) y2 := i32(verts[i].y) ax := min(x0, min(x1, x2)) ay := min(y0, min(y1, y2)) by := max(y0, max(y1, y2)) if y > f32(ay) && y < f32(by) && x > f32(ax) { q0: [2]f32 = {f32(x0), f32(y0)} q1: [2]f32 = {f32(x1), f32(y1)} q2: [2]f32 = {f32(x2), f32(y2)} hits: [2][2]f32 if point_equal(q0, q1) || point_equal(q1, q2) { // Degenerate curve, treat as line lx0 := i32(verts[i - 1].x) ly0 := i32(verts[i - 1].y) lx1 := i32(verts[i].x) ly1 := i32(verts[i].y) if y > f32(min(ly0, ly1)) && y < f32(max(ly0, ly1)) && x > f32(min(lx0, lx1)) { x_inter := (y - f32(ly0)) / f32(ly1 - ly0) * f32(lx1 - lx0) + f32(lx0) if x_inter < x { winding += 1 if ly0 < ly1 else -1 } } } else { num_hits := ray_intersect_bezier(orig, ray, q0, q1, q2, &hits) if num_hits >= 1 { if hits[0][0] < 0 { winding += -1 if hits[0][1] < 0 else 1 } } if num_hits >= 2 { if hits[1][0] < 0 { winding += -1 if hits[1][1] < 0 else 1 } } } } } } return winding } // Generate a signed distance field bitmap for a glyph get_glyph_sdf :: proc( info: ^Font_Info, scale: f32, glyph: i32, padding: i32, onedge_value: u8, pixel_dist_scale: f32, width: ^i32, height: ^i32, xoff: ^i32, yoff: ^i32, allocator := context.allocator, ) -> [^]u8 { // Use arena allocator for intermediate allocations (optimization) // Stack-based backing to avoid heap allocation overhead arena_backing: [256 * 1024]u8 arena: mem.Arena mem.arena_init(&arena, arena_backing[:]) context.allocator = mem.arena_allocator(&arena) scale_x := scale scale_y := scale if scale == 0 { return nil } ix0, iy0, ix1, iy1: i32 get_glyph_bitmap_box_subpixel(info, glyph, scale, scale, 0.0, 0.0, &ix0, &iy0, &ix1, &iy1) // If empty, return nil if ix0 == ix1 || iy0 == iy1 { return nil } ix0 -= padding iy0 -= padding ix1 += padding iy1 += padding w := ix1 - ix0 h := iy1 - iy0 if width != nil { width^ = w } if height != nil { height^ = h } if xoff != nil { xoff^ = ix0 } if yoff != nil { yoff^ = iy0 } // Invert for y-downwards bitmaps scale_y = -scale_y // Distance from singular values (in the same units as the pixel grid) eps: f32 : 1.0 / 1024 eps2: f32 : eps * eps vertices_temp: ^Vertex = nil num_verts := get_glyph_shape(info, glyph, cast(^^Vertex)&vertices_temp) verts := ([^]Vertex)(vertices_temp) // Use caller's allocator for the returned bitmap data_ptr, _ := mem.alloc(int(w * h), allocator = allocator) data := ([^]u8)(data_ptr) // precompute uses arena (context.allocator) precompute_ptr, _ := mem.alloc(int(num_verts) * size_of(f32)) precompute := ([^]f32)(precompute_ptr) // Precompute scaled vertex positions to avoid per-pixel multiplication Scaled_Vert :: struct { x, y, cx, cy: f32 } sv_ptr, _ := mem.alloc(int(num_verts) * size_of(Scaled_Vert)) sv := ([^]Scaled_Vert)(sv_ptr) for i: i32 = 0; i < num_verts; i += 1 { sv[i] = { x = f32(verts[i].x) * scale_x, y = f32(verts[i].y) * scale_y, cx = f32(verts[i].cx) * scale_x, cy = f32(verts[i].cy) * scale_y, } } j := num_verts - 1 for i: i32 = 0; i < num_verts; i += 1 { if verts[i].type == VLINE { dx := sv[j].x - sv[i].x dy := sv[j].y - sv[i].y dist := math.sqrt(dx * dx + dy * dy) precompute[i] = 0.0 if dist < eps else 1.0 / dist } else if verts[i].type == VCURVE { bx := sv[i].x - 2 * sv[i].cx + sv[j].x by := sv[i].y - 2 * sv[i].cy + sv[j].y len2 := bx * bx + by * by precompute[i] = 1.0 / len2 if len2 >= eps2 else 0.0 } else { precompute[i] = 0.0 } j = i } for y := iy0; y < iy1; y += 1 { for x := ix0; x < ix1; x += 1 { min_dist: f32 = 999999.0 sx := f32(x) + 0.5 sy := f32(y) + 0.5 x_gspace := sx / scale_x y_gspace := sy / scale_y winding := compute_crossings_x(x_gspace, y_gspace, num_verts, verts) #no_bounds_check for i: i32 = 0; i < num_verts; i += 1 { x0 := sv[i].x y0 := sv[i].y if verts[i].type == VLINE && precompute[i] != 0.0 { x1 := sv[i - 1].x y1 := sv[i - 1].y dist2 := (x0 - sx) * (x0 - sx) + (y0 - sy) * (y0 - sy) if dist2 < min_dist * min_dist { min_dist = math.sqrt(dist2) } dist := abs((x1 - x0) * (y0 - sy) - (y1 - y0) * (x0 - sx)) * precompute[i] if dist < min_dist { // Check position along line dx := x1 - x0 dy := y1 - y0 px := x0 - sx py := y0 - sy t := -(px * dx + py * dy) / (dx * dx + dy * dy) if t >= 0.0 && t <= 1.0 { min_dist = dist } } } else if verts[i].type == VCURVE { x2 := sv[i - 1].x y2 := sv[i - 1].y x1 := sv[i].cx y1 := sv[i].cy box_x0 := min(min(x0, x1), x2) box_y0 := min(min(y0, y1), y2) box_x1 := max(max(x0, x1), x2) box_y1 := max(max(y0, y1), y2) // Coarse culling against bbox to avoid computing cubic unnecessarily if sx > box_x0 - min_dist && sx < box_x1 + min_dist && sy > box_y0 - min_dist && sy < box_y1 + min_dist { num: i32 = 0 ax := x1 - x0 ay := y1 - y0 bx := x0 - 2 * x1 + x2 by := y0 - 2 * y1 + y2 mx := x0 - sx my := y0 - sy res: [3]f32 = {0, 0, 0} a_inv := precompute[i] if a_inv == 0.0 { // If a_inv is 0, it's 2nd degree so use quadratic formula qa := 3 * (ax * bx + ay * by) qb := 2 * (ax * ax + ay * ay) + (mx * bx + my * by) qc := mx * ax + my * ay if abs(qa) < eps2 { // If a is 0, it's linear if abs(qb) >= eps2 { res[num] = -qc / qb num += 1 } } else { discriminant := qb * qb - 4 * qa * qc if discriminant >= 0 { root := math.sqrt(discriminant) res[0] = (-qb - root) / (2 * qa) res[1] = (-qb + root) / (2 * qa) num = 2 } } } else { cb := 3 * (ax * bx + ay * by) * a_inv cc := (2 * (ax * ax + ay * ay) + (mx * bx + my * by)) * a_inv cd := (mx * ax + my * ay) * a_inv num = solve_cubic(cb, cc, cd, &res[0]) } dist2 := (x0 - sx) * (x0 - sx) + (y0 - sy) * (y0 - sy) if dist2 < min_dist * min_dist { min_dist = math.sqrt(dist2) } if num >= 1 && res[0] >= 0.0 && res[0] <= 1.0 { t := res[0] it := 1.0 - t px := it * it * x0 + 2 * t * it * x1 + t * t * x2 py := it * it * y0 + 2 * t * it * y1 + t * t * y2 dist2 = (px - sx) * (px - sx) + (py - sy) * (py - sy) if dist2 < min_dist * min_dist { min_dist = math.sqrt(dist2) } } if num >= 2 && res[1] >= 0.0 && res[1] <= 1.0 { t := res[1] it := 1.0 - t px := it * it * x0 + 2 * t * it * x1 + t * t * x2 py := it * it * y0 + 2 * t * it * y1 + t * t * y2 dist2 = (px - sx) * (px - sx) + (py - sy) * (py - sy) if dist2 < min_dist * min_dist { min_dist = math.sqrt(dist2) } } if num >= 3 && res[2] >= 0.0 && res[2] <= 1.0 { t := res[2] it := 1.0 - t px := it * it * x0 + 2 * t * it * x1 + t * t * x2 py := it * it * y0 + 2 * t * it * y1 + t * t * y2 dist2 = (px - sx) * (px - sx) + (py - sy) * (py - sy) if dist2 < min_dist * min_dist { min_dist = math.sqrt(dist2) } } } } } if winding == 0 { min_dist = -min_dist // If outside the shape, value is negative } val := f32(onedge_value) + pixel_dist_scale * min_dist if val < 0 { val = 0 } else if val > 255 { val = 255 } data[(y - iy0) * w + (x - ix0)] = u8(val) } } // precompute and verts allocated from arena, will be freed with arena_backing return data } // Generate a signed distance field bitmap for a codepoint get_codepoint_sdf :: proc( info: ^Font_Info, scale: f32, codepoint: i32, padding: i32, onedge_value: u8, pixel_dist_scale: f32, width: ^i32, height: ^i32, xoff: ^i32, yoff: ^i32, allocator := context.allocator, ) -> [^]u8 { return get_glyph_sdf(info, scale, find_glyph_index(info, codepoint), padding, onedge_value, pixel_dist_scale, width, height, xoff, yoff, allocator) } // Free a signed distance field bitmap free_sdf :: proc(bitmap: [^]u8) { mem.free(bitmap) }