Major rework to draw rendering system. We are making a SDF first rendering system with tesselated stuff only as a fallback strategy for specific situations where SDF is particularly poorly suited Co-authored-by: Zachary Levy <zachary@sunforge.is> Reviewed-on: #17
331 lines
12 KiB
Odin
331 lines
12 KiB
Odin
package tess
|
||
|
||
import "core:math"
|
||
|
||
import draw ".."
|
||
|
||
SMOOTH_CIRCLE_ERROR_RATE :: 0.1
|
||
|
||
auto_segments :: proc(radius: f32, arc_degrees: f32) -> int {
|
||
if radius <= 0 do return 4
|
||
phys_radius := radius * draw.GLOB.dpi_scaling
|
||
acos_arg := clamp(2 * math.pow(1 - SMOOTH_CIRCLE_ERROR_RATE / phys_radius, 2) - 1, -1, 1)
|
||
theta := math.acos(acos_arg)
|
||
if theta <= 0 do return 4
|
||
full_circle_segments := int(math.ceil(2 * math.PI / theta))
|
||
segments := int(f32(full_circle_segments) * arc_degrees / 360.0)
|
||
min_segments := max(int(math.ceil(f64(arc_degrees / 90.0))), 4)
|
||
return max(segments, min_segments)
|
||
}
|
||
|
||
// ----- Internal helpers -----
|
||
|
||
// Color is premultiplied: the tessellated fragment shader passes it through directly
|
||
// and the blend state is ONE, ONE_MINUS_SRC_ALPHA.
|
||
solid_vertex :: proc(position: draw.Vec2, color: draw.Color) -> draw.Vertex {
|
||
return draw.Vertex{position = position, color = draw.premultiply_color(color)}
|
||
}
|
||
|
||
emit_rectangle :: proc(x, y, width, height: f32, color: draw.Color, vertices: []draw.Vertex, offset: int) {
|
||
vertices[offset + 0] = solid_vertex({x, y}, color)
|
||
vertices[offset + 1] = solid_vertex({x + width, y}, color)
|
||
vertices[offset + 2] = solid_vertex({x + width, y + height}, color)
|
||
vertices[offset + 3] = solid_vertex({x, y}, color)
|
||
vertices[offset + 4] = solid_vertex({x + width, y + height}, color)
|
||
vertices[offset + 5] = solid_vertex({x, y + height}, color)
|
||
}
|
||
|
||
extrude_line :: proc(
|
||
start, end_pos: draw.Vec2,
|
||
thickness: f32,
|
||
color: draw.Color,
|
||
vertices: []draw.Vertex,
|
||
offset: int,
|
||
) -> int {
|
||
direction := end_pos - start
|
||
delta_x := direction[0]
|
||
delta_y := direction[1]
|
||
length := math.sqrt(delta_x * delta_x + delta_y * delta_y)
|
||
if length < 0.0001 do return 0
|
||
|
||
scale := thickness / (2 * length)
|
||
perpendicular := draw.Vec2{-delta_y * scale, delta_x * scale}
|
||
|
||
p0 := start + perpendicular
|
||
p1 := start - perpendicular
|
||
p2 := end_pos - perpendicular
|
||
p3 := end_pos + perpendicular
|
||
|
||
vertices[offset + 0] = solid_vertex(p0, color)
|
||
vertices[offset + 1] = solid_vertex(p1, color)
|
||
vertices[offset + 2] = solid_vertex(p2, color)
|
||
vertices[offset + 3] = solid_vertex(p0, color)
|
||
vertices[offset + 4] = solid_vertex(p2, color)
|
||
vertices[offset + 5] = solid_vertex(p3, color)
|
||
|
||
return 6
|
||
}
|
||
|
||
// ----- Public draw -----
|
||
|
||
pixel :: proc(layer: ^draw.Layer, pos: draw.Vec2, color: draw.Color) {
|
||
vertices: [6]draw.Vertex
|
||
emit_rectangle(pos[0], pos[1], 1, 1, color, vertices[:], 0)
|
||
draw.prepare_shape(layer, vertices[:])
|
||
}
|
||
|
||
triangle :: proc(
|
||
layer: ^draw.Layer,
|
||
v1, v2, v3: draw.Vec2,
|
||
color: draw.Color,
|
||
origin: draw.Vec2 = {},
|
||
rotation: f32 = 0,
|
||
) {
|
||
if !draw.needs_transform(origin, rotation) {
|
||
vertices := [3]draw.Vertex{solid_vertex(v1, color), solid_vertex(v2, color), solid_vertex(v3, color)}
|
||
draw.prepare_shape(layer, vertices[:])
|
||
return
|
||
}
|
||
bounds_min := draw.Vec2{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
|
||
transform := draw.build_pivot_rotation(bounds_min, origin, rotation)
|
||
local_v1 := v1 - bounds_min
|
||
local_v2 := v2 - bounds_min
|
||
local_v3 := v3 - bounds_min
|
||
vertices := [3]draw.Vertex {
|
||
solid_vertex(draw.apply_transform(transform, local_v1), color),
|
||
solid_vertex(draw.apply_transform(transform, local_v2), color),
|
||
solid_vertex(draw.apply_transform(transform, local_v3), color),
|
||
}
|
||
draw.prepare_shape(layer, vertices[:])
|
||
}
|
||
|
||
// Draw an anti-aliased triangle via extruded edge quads.
|
||
// Interior vertices get the full premultiplied color; outer fringe vertices get BLANK (0,0,0,0).
|
||
// The rasterizer linearly interpolates between them, producing a smooth 1-pixel AA band.
|
||
// `aa_px` controls the extrusion width in logical pixels (default 1.0).
|
||
// This proc emits 21 vertices (3 interior + 6 edge quads × 3 verts each).
|
||
triangle_aa :: proc(
|
||
layer: ^draw.Layer,
|
||
v1, v2, v3: draw.Vec2,
|
||
color: draw.Color,
|
||
aa_px: f32 = draw.DFT_FEATHER_PX,
|
||
origin: draw.Vec2 = {},
|
||
rotation: f32 = 0,
|
||
) {
|
||
// Apply rotation if needed, then work in world space.
|
||
p0, p1, p2: draw.Vec2
|
||
if !draw.needs_transform(origin, rotation) {
|
||
p0 = v1
|
||
p1 = v2
|
||
p2 = v3
|
||
} else {
|
||
bounds_min := draw.Vec2{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
|
||
transform := draw.build_pivot_rotation(bounds_min, origin, rotation)
|
||
p0 = draw.apply_transform(transform, v1 - bounds_min)
|
||
p1 = draw.apply_transform(transform, v2 - bounds_min)
|
||
p2 = draw.apply_transform(transform, v3 - bounds_min)
|
||
}
|
||
|
||
// Compute outward edge normals (unit length, pointing away from triangle interior).
|
||
// Winding-independent: we check against the centroid to ensure normals point outward.
|
||
centroid_x := (p0.x + p1.x + p2.x) / 3.0
|
||
centroid_y := (p0.y + p1.y + p2.y) / 3.0
|
||
|
||
edge_normal :: proc(edge_start, edge_end: draw.Vec2, centroid_x, centroid_y: f32) -> draw.Vec2 {
|
||
delta_x := edge_end.x - edge_start.x
|
||
delta_y := edge_end.y - edge_start.y
|
||
length := math.sqrt(delta_x * delta_x + delta_y * delta_y)
|
||
if length < 0.0001 do return {0, 0}
|
||
inverse_length := 1.0 / length
|
||
// Perpendicular: (-delta_y, delta_x) normalized
|
||
normal_x := -delta_y * inverse_length
|
||
normal_y := delta_x * inverse_length
|
||
// Midpoint of the edge
|
||
midpoint_x := (edge_start.x + edge_end.x) * 0.5
|
||
midpoint_y := (edge_start.y + edge_end.y) * 0.5
|
||
// If normal points toward centroid, flip it
|
||
if normal_x * (centroid_x - midpoint_x) + normal_y * (centroid_y - midpoint_y) > 0 {
|
||
normal_x = -normal_x
|
||
normal_y = -normal_y
|
||
}
|
||
return {normal_x, normal_y}
|
||
}
|
||
|
||
normal_01 := edge_normal(p0, p1, centroid_x, centroid_y)
|
||
normal_12 := edge_normal(p1, p2, centroid_x, centroid_y)
|
||
normal_20 := edge_normal(p2, p0, centroid_x, centroid_y)
|
||
|
||
extrude_distance := aa_px * draw.GLOB.dpi_scaling
|
||
|
||
// Outer fringe vertices: each edge vertex extruded outward
|
||
outer_0_01 := p0 + normal_01 * extrude_distance
|
||
outer_1_01 := p1 + normal_01 * extrude_distance
|
||
outer_1_12 := p1 + normal_12 * extrude_distance
|
||
outer_2_12 := p2 + normal_12 * extrude_distance
|
||
outer_2_20 := p2 + normal_20 * extrude_distance
|
||
outer_0_20 := p0 + normal_20 * extrude_distance
|
||
|
||
// Premultiplied interior color (solid_vertex does premul internally).
|
||
// Outer fringe is BLANK = {0,0,0,0} which is already premul.
|
||
transparent := draw.BLANK
|
||
|
||
// 3 interior + 6 × 3 edge-quad = 21 vertices
|
||
vertices: [21]draw.Vertex
|
||
|
||
// Interior triangle
|
||
vertices[0] = solid_vertex(p0, color)
|
||
vertices[1] = solid_vertex(p1, color)
|
||
vertices[2] = solid_vertex(p2, color)
|
||
|
||
// Edge quad: p0→p1 (2 triangles)
|
||
vertices[3] = solid_vertex(p0, color)
|
||
vertices[4] = solid_vertex(p1, color)
|
||
vertices[5] = solid_vertex(outer_1_01, transparent)
|
||
vertices[6] = solid_vertex(p0, color)
|
||
vertices[7] = solid_vertex(outer_1_01, transparent)
|
||
vertices[8] = solid_vertex(outer_0_01, transparent)
|
||
|
||
// Edge quad: p1→p2 (2 triangles)
|
||
vertices[9] = solid_vertex(p1, color)
|
||
vertices[10] = solid_vertex(p2, color)
|
||
vertices[11] = solid_vertex(outer_2_12, transparent)
|
||
vertices[12] = solid_vertex(p1, color)
|
||
vertices[13] = solid_vertex(outer_2_12, transparent)
|
||
vertices[14] = solid_vertex(outer_1_12, transparent)
|
||
|
||
// Edge quad: p2→p0 (2 triangles)
|
||
vertices[15] = solid_vertex(p2, color)
|
||
vertices[16] = solid_vertex(p0, color)
|
||
vertices[17] = solid_vertex(outer_0_20, transparent)
|
||
vertices[18] = solid_vertex(p2, color)
|
||
vertices[19] = solid_vertex(outer_0_20, transparent)
|
||
vertices[20] = solid_vertex(outer_2_20, transparent)
|
||
|
||
draw.prepare_shape(layer, vertices[:])
|
||
}
|
||
|
||
triangle_lines :: proc(
|
||
layer: ^draw.Layer,
|
||
v1, v2, v3: draw.Vec2,
|
||
color: draw.Color,
|
||
thickness: f32 = draw.DFT_STROKE_THICKNESS,
|
||
origin: draw.Vec2 = {},
|
||
rotation: f32 = 0,
|
||
temp_allocator := context.temp_allocator,
|
||
) {
|
||
vertices := make([]draw.Vertex, 18, temp_allocator)
|
||
defer delete(vertices, temp_allocator)
|
||
write_offset := 0
|
||
|
||
if !draw.needs_transform(origin, rotation) {
|
||
write_offset += extrude_line(v1, v2, thickness, color, vertices, write_offset)
|
||
write_offset += extrude_line(v2, v3, thickness, color, vertices, write_offset)
|
||
write_offset += extrude_line(v3, v1, thickness, color, vertices, write_offset)
|
||
} else {
|
||
bounds_min := draw.Vec2{min(v1.x, v2.x, v3.x), min(v1.y, v2.y, v3.y)}
|
||
transform := draw.build_pivot_rotation(bounds_min, origin, rotation)
|
||
transformed_v1 := draw.apply_transform(transform, v1 - bounds_min)
|
||
transformed_v2 := draw.apply_transform(transform, v2 - bounds_min)
|
||
transformed_v3 := draw.apply_transform(transform, v3 - bounds_min)
|
||
write_offset += extrude_line(transformed_v1, transformed_v2, thickness, color, vertices, write_offset)
|
||
write_offset += extrude_line(transformed_v2, transformed_v3, thickness, color, vertices, write_offset)
|
||
write_offset += extrude_line(transformed_v3, transformed_v1, thickness, color, vertices, write_offset)
|
||
}
|
||
|
||
if write_offset > 0 {
|
||
draw.prepare_shape(layer, vertices[:write_offset])
|
||
}
|
||
}
|
||
|
||
triangle_fan :: proc(
|
||
layer: ^draw.Layer,
|
||
points: []draw.Vec2,
|
||
color: draw.Color,
|
||
origin: draw.Vec2 = {},
|
||
rotation: f32 = 0,
|
||
temp_allocator := context.temp_allocator,
|
||
) {
|
||
if len(points) < 3 do return
|
||
|
||
triangle_count := len(points) - 2
|
||
vertex_count := triangle_count * 3
|
||
vertices := make([]draw.Vertex, vertex_count, temp_allocator)
|
||
defer delete(vertices, temp_allocator)
|
||
|
||
if !draw.needs_transform(origin, rotation) {
|
||
for i in 1 ..< len(points) - 1 {
|
||
idx := (i - 1) * 3
|
||
vertices[idx + 0] = solid_vertex(points[0], color)
|
||
vertices[idx + 1] = solid_vertex(points[i], color)
|
||
vertices[idx + 2] = solid_vertex(points[i + 1], color)
|
||
}
|
||
} else {
|
||
bounds_min := draw.Vec2{max(f32), max(f32)}
|
||
for point in points {
|
||
bounds_min.x = min(bounds_min.x, point.x)
|
||
bounds_min.y = min(bounds_min.y, point.y)
|
||
}
|
||
transform := draw.build_pivot_rotation(bounds_min, origin, rotation)
|
||
for i in 1 ..< len(points) - 1 {
|
||
idx := (i - 1) * 3
|
||
vertices[idx + 0] = solid_vertex(draw.apply_transform(transform, points[0] - bounds_min), color)
|
||
vertices[idx + 1] = solid_vertex(draw.apply_transform(transform, points[i] - bounds_min), color)
|
||
vertices[idx + 2] = solid_vertex(draw.apply_transform(transform, points[i + 1] - bounds_min), color)
|
||
}
|
||
}
|
||
|
||
draw.prepare_shape(layer, vertices)
|
||
}
|
||
|
||
triangle_strip :: proc(
|
||
layer: ^draw.Layer,
|
||
points: []draw.Vec2,
|
||
color: draw.Color,
|
||
origin: draw.Vec2 = {},
|
||
rotation: f32 = 0,
|
||
temp_allocator := context.temp_allocator,
|
||
) {
|
||
if len(points) < 3 do return
|
||
|
||
triangle_count := len(points) - 2
|
||
vertex_count := triangle_count * 3
|
||
vertices := make([]draw.Vertex, vertex_count, temp_allocator)
|
||
defer delete(vertices, temp_allocator)
|
||
|
||
if !draw.needs_transform(origin, rotation) {
|
||
for i in 0 ..< triangle_count {
|
||
idx := i * 3
|
||
if i % 2 == 0 {
|
||
vertices[idx + 0] = solid_vertex(points[i], color)
|
||
vertices[idx + 1] = solid_vertex(points[i + 1], color)
|
||
vertices[idx + 2] = solid_vertex(points[i + 2], color)
|
||
} else {
|
||
vertices[idx + 0] = solid_vertex(points[i + 1], color)
|
||
vertices[idx + 1] = solid_vertex(points[i], color)
|
||
vertices[idx + 2] = solid_vertex(points[i + 2], color)
|
||
}
|
||
}
|
||
} else {
|
||
bounds_min := draw.Vec2{max(f32), max(f32)}
|
||
for point in points {
|
||
bounds_min.x = min(bounds_min.x, point.x)
|
||
bounds_min.y = min(bounds_min.y, point.y)
|
||
}
|
||
transform := draw.build_pivot_rotation(bounds_min, origin, rotation)
|
||
for i in 0 ..< triangle_count {
|
||
idx := i * 3
|
||
if i % 2 == 0 {
|
||
vertices[idx + 0] = solid_vertex(draw.apply_transform(transform, points[i] - bounds_min), color)
|
||
vertices[idx + 1] = solid_vertex(draw.apply_transform(transform, points[i + 1] - bounds_min), color)
|
||
vertices[idx + 2] = solid_vertex(draw.apply_transform(transform, points[i + 2] - bounds_min), color)
|
||
} else {
|
||
vertices[idx + 0] = solid_vertex(draw.apply_transform(transform, points[i + 1] - bounds_min), color)
|
||
vertices[idx + 1] = solid_vertex(draw.apply_transform(transform, points[i] - bounds_min), color)
|
||
vertices[idx + 2] = solid_vertex(draw.apply_transform(transform, points[i + 2] - bounds_min), color)
|
||
}
|
||
}
|
||
}
|
||
|
||
draw.prepare_shape(layer, vertices)
|
||
}
|