package renderer import "core:math" import "core:mem" import glsl "core:math/linalg/glsl" import "../resource" import bk "../backend" MAX_QUAD_COUNT :: 10000 MAX_VERTEX_COUNT :: MAX_QUAD_COUNT * 4 MAX_INDEX_COUNT :: MAX_QUAD_COUNT * 6 CIRCLE_SEGMENTS :: 64 VERTEX_SIZE :: 48 // matches gpu.Vertex layout // Mirrors gpu.Vertex layout to avoid circular import Draw2D_Vertex :: struct { position: [3]f32, normal: [3]f32, tex_coord: [2]f32, color: [4]f32, } Batch_2D :: struct { vertex_buffers: [bk.MAX_FRAMES_IN_FLIGHT]bk.Buffer_Handle, index_buffers: [bk.MAX_FRAMES_IN_FLIGHT]bk.Buffer_Handle, vertices: [dynamic]Draw2D_Vertex, indices: [dynamic]u32, gfx_pipeline: bk.Pipeline_Handle, proj_view: glsl.mat4x4, screen_proj: glsl.mat4x4, current_texture_id: u32, resource_state: ^resource.Resource_State, // Per-frame state backend: ^bk.Backend, frame_ctx: bk.Frame_Context, frame_index: u32, // GPU buffer write offsets vertex_buf_offset: int, index_buf_offset: int, // Lit rendering support lit_pipeline: bk.Pipeline_Handle, light_desc_set: bk.Descriptor_Handle, use_lit: bool, // Back-pointer for deferred render pass parent_state: ^Renderer_State, } init_batch_2d :: proc(b: ^bk.Backend, res_state: ^resource.Resource_State) -> (batch: Batch_2D, ok: bool) { batch.resource_state = res_state // Create per-frame vertex and index buffers for i in 0.. 0 && batch.backend != nil { flush_batch_2d(batch) } batch.current_texture_id = texture_id } // --- Shape generation --- batch_add_rectangle :: proc(batch: ^Batch_2D, x, y, w, h: f32, color: [4]f32) { if len(batch.vertices) + 4 > MAX_VERTEX_COUNT || len(batch.indices) + 6 > MAX_INDEX_COUNT { return } base := u32(len(batch.vertices)) append(&batch.vertices, Draw2D_Vertex{ position = {x, y, 0}, normal = {0, 0, 1}, color = color }, Draw2D_Vertex{ position = {x + w, y, 0}, normal = {0, 0, 1}, color = color }, Draw2D_Vertex{ position = {x + w, y + h, 0}, normal = {0, 0, 1}, color = color }, Draw2D_Vertex{ position = {x, y + h, 0}, normal = {0, 0, 1}, color = color }, ) append(&batch.indices, base, base + 1, base + 2, base + 2, base + 3, base) } batch_add_textured_quad :: proc(batch: ^Batch_2D, x, y, w, h: f32, u0, v0, u1, v1: f32, color: [4]f32) { if len(batch.vertices) + 4 > MAX_VERTEX_COUNT || len(batch.indices) + 6 > MAX_INDEX_COUNT { return } base := u32(len(batch.vertices)) append(&batch.vertices, Draw2D_Vertex{ position = {x, y, 0}, normal = {0, 0, 1}, tex_coord = {u0, v0}, color = color }, Draw2D_Vertex{ position = {x + w, y, 0}, normal = {0, 0, 1}, tex_coord = {u1, v0}, color = color }, Draw2D_Vertex{ position = {x + w, y + h, 0}, normal = {0, 0, 1}, tex_coord = {u1, v1}, color = color }, Draw2D_Vertex{ position = {x, y + h, 0}, normal = {0, 0, 1}, tex_coord = {u0, v1}, color = color }, ) append(&batch.indices, base, base + 1, base + 2, base + 2, base + 3, base) } batch_add_textured_quad_rotated :: proc(batch: ^Batch_2D, x, y, w, h: f32, u0, v0, u1, v1: f32, rotation: f32, color: [4]f32) { if len(batch.vertices) + 4 > MAX_VERTEX_COUNT || len(batch.indices) + 6 > MAX_INDEX_COUNT { return } base := u32(len(batch.vertices)) // Rotate around the top-left corner (x, y) cx := x + w * 0.5 cy := y + h * 0.5 rad := rotation * math.PI / 180.0 cos_r := math.cos(rad) sin_r := math.sin(rad) rotate :: proc(px, py, cx, cy, cos_r, sin_r: f32) -> [3]f32 { dx := px - cx dy := py - cy return {cx + dx * cos_r - dy * sin_r, cy + dx * sin_r + dy * cos_r, 0} } append(&batch.vertices, Draw2D_Vertex{ position = rotate(x, y, cx, cy, cos_r, sin_r), normal = {0, 0, 1}, tex_coord = {u0, v0}, color = color }, Draw2D_Vertex{ position = rotate(x + w, y, cx, cy, cos_r, sin_r), normal = {0, 0, 1}, tex_coord = {u1, v0}, color = color }, Draw2D_Vertex{ position = rotate(x + w, y + h, cx, cy, cos_r, sin_r), normal = {0, 0, 1}, tex_coord = {u1, v1}, color = color }, Draw2D_Vertex{ position = rotate(x, y + h, cx, cy, cos_r, sin_r), normal = {0, 0, 1}, tex_coord = {u0, v1}, color = color }, ) append(&batch.indices, base, base + 1, base + 2, base + 2, base + 3, base) } batch_add_circle :: proc(batch: ^Batch_2D, cx, cy, radius: f32, color: [4]f32) { vert_needed := 1 + CIRCLE_SEGMENTS idx_needed := CIRCLE_SEGMENTS * 3 if len(batch.vertices) + vert_needed > MAX_VERTEX_COUNT || len(batch.indices) + idx_needed > MAX_INDEX_COUNT { return } base := u32(len(batch.vertices)) // Center vertex append(&batch.vertices, Draw2D_Vertex{ position = {cx, cy, 0}, normal = {0, 0, 1}, color = color }) // Perimeter vertices for i in 0.. [3]f32 { dx := px - cx dy := py - cy return {cx + dx * cos_r - dy * sin_r, cy + dx * sin_r + dy * cos_r, 0} } batch_add_triangle :: proc(batch: ^Batch_2D, x1, y1, x2, y2, x3, y3: f32, color: [4]f32) { if len(batch.vertices) + 3 > MAX_VERTEX_COUNT || len(batch.indices) + 3 > MAX_INDEX_COUNT { return } base := u32(len(batch.vertices)) append(&batch.vertices, Draw2D_Vertex{ position = {x1, y1, 0}, normal = {0, 0, 1}, color = color }, Draw2D_Vertex{ position = {x2, y2, 0}, normal = {0, 0, 1}, color = color }, Draw2D_Vertex{ position = {x3, y3, 0}, normal = {0, 0, 1}, color = color }, ) append(&batch.indices, base, base + 1, base + 2) } batch_add_rectangle_rotated :: proc(batch: ^Batch_2D, cx, cy, half_w, half_h: f32, color: [4]f32, rotation: f32 = 0) { if len(batch.vertices) + 4 > MAX_VERTEX_COUNT || len(batch.indices) + 6 > MAX_INDEX_COUNT { return } base := u32(len(batch.vertices)) cos_r := math.cos(rotation) sin_r := math.sin(rotation) append(&batch.vertices, Draw2D_Vertex{ position = rotate_point(cx - half_w, cy - half_h, cx, cy, cos_r, sin_r), normal = {0, 0, 1}, color = color }, Draw2D_Vertex{ position = rotate_point(cx + half_w, cy - half_h, cx, cy, cos_r, sin_r), normal = {0, 0, 1}, color = color }, Draw2D_Vertex{ position = rotate_point(cx + half_w, cy + half_h, cx, cy, cos_r, sin_r), normal = {0, 0, 1}, color = color }, Draw2D_Vertex{ position = rotate_point(cx - half_w, cy + half_h, cx, cy, cos_r, sin_r), normal = {0, 0, 1}, color = color }, ) append(&batch.indices, base, base + 1, base + 2, base + 2, base + 3, base) } batch_add_ellipse :: proc(batch: ^Batch_2D, cx, cy, rx, ry: f32, color: [4]f32, rotation: f32 = 0) { vert_needed := 1 + CIRCLE_SEGMENTS idx_needed := CIRCLE_SEGMENTS * 3 if len(batch.vertices) + vert_needed > MAX_VERTEX_COUNT || len(batch.indices) + idx_needed > MAX_INDEX_COUNT { return } base := u32(len(batch.vertices)) cos_r := math.cos(rotation) sin_r := math.sin(rotation) append(&batch.vertices, Draw2D_Vertex{ position = {cx, cy, 0}, normal = {0, 0, 1}, color = color }) for i in 0.. MAX_VERTEX_COUNT || len(batch.indices) + idx_needed > MAX_INDEX_COUNT { return } base := u32(len(batch.vertices)) cos_r := math.cos(rotation) sin_r := math.sin(rotation) for i in 0.. MAX_VERTEX_COUNT || len(batch.indices) + idx_needed > MAX_INDEX_COUNT { return } base := u32(len(batch.vertices)) cos_r := math.cos(rotation) sin_r := math.sin(rotation) // Rectangle body (4 verts, 2 tris) append(&batch.vertices, Draw2D_Vertex{ position = rotate_point(cx - radius, cy - half_length, cx, cy, cos_r, sin_r), normal = {0, 0, 1}, color = color }, Draw2D_Vertex{ position = rotate_point(cx + radius, cy - half_length, cx, cy, cos_r, sin_r), normal = {0, 0, 1}, color = color }, Draw2D_Vertex{ position = rotate_point(cx + radius, cy + half_length, cx, cy, cos_r, sin_r), normal = {0, 0, 1}, color = color }, Draw2D_Vertex{ position = rotate_point(cx - radius, cy + half_length, cx, cy, cos_r, sin_r), normal = {0, 0, 1}, color = color }, ) append(&batch.indices, base, base + 1, base + 2, base + 2, base + 3, base) // Top semicircle (center at cx, cy - half_length) top_base := u32(len(batch.vertices)) top_cy := cy - half_length append(&batch.vertices, Draw2D_Vertex{ position = rotate_point(cx, top_cy, cx, cy, cos_r, sin_r), normal = {0, 0, 1}, color = color }) for i in 0..=half_segs { angle := math.PI + f32(i) * math.PI / f32(half_segs) px := cx + radius * math.cos(angle) py := top_cy + radius * math.sin(angle) append(&batch.vertices, Draw2D_Vertex{ position = rotate_point(px, py, cx, cy, cos_r, sin_r), normal = {0, 0, 1}, color = color }) } for i in 0.. MAX_VERTEX_COUNT || len(batch.indices) + 6 > MAX_INDEX_COUNT { return } base := u32(len(batch.vertices)) cos_r := math.cos(rotation) sin_r := math.sin(rotation) append(&batch.vertices, Draw2D_Vertex{ position = rotate_point(cx, cy - half_h, cx, cy, cos_r, sin_r), normal = {0, 0, 1}, color = color }, Draw2D_Vertex{ position = rotate_point(cx + half_w, cy, cx, cy, cos_r, sin_r), normal = {0, 0, 1}, color = color }, Draw2D_Vertex{ position = rotate_point(cx, cy + half_h, cx, cy, cos_r, sin_r), normal = {0, 0, 1}, color = color }, Draw2D_Vertex{ position = rotate_point(cx - half_w, cy, cx, cy, cos_r, sin_r), normal = {0, 0, 1}, color = color }, ) append(&batch.indices, base, base + 1, base + 2, base + 2, base + 3, base) } batch_add_regular_polygon :: proc(batch: ^Batch_2D, cx, cy, radius: f32, sides: int, color: [4]f32, rotation: f32 = 0) { n := max(sides, 3) vert_needed := 1 + n idx_needed := n * 3 if len(batch.vertices) + vert_needed > MAX_VERTEX_COUNT || len(batch.indices) + idx_needed > MAX_INDEX_COUNT { return } base := u32(len(batch.vertices)) cos_r := math.cos(rotation) sin_r := math.sin(rotation) append(&batch.vertices, Draw2D_Vertex{ position = {cx, cy, 0}, normal = {0, 0, 1}, color = color }) for i in 0.. MAX_VERTEX_COUNT || len(batch.indices) + idx_needed > MAX_INDEX_COUNT { return } base := u32(len(batch.vertices)) cos_r := math.cos(rotation) sin_r := math.sin(rotation) append(&batch.vertices, Draw2D_Vertex{ position = {cx, cy, 0}, normal = {0, 0, 1}, color = color }) for i in 0..=segs { angle := start_angle + f32(i) * sweep_angle / f32(segs) px := cx + radius * math.cos(angle) py := cy + radius * math.sin(angle) append(&batch.vertices, Draw2D_Vertex{ position = rotate_point(px, py, cx, cy, cos_r, sin_r), normal = {0, 0, 1}, color = color }) } for i in 0.. MAX_VERTEX_COUNT || len(batch.indices) + idx_needed > MAX_INDEX_COUNT { return } base := u32(len(batch.vertices)) cos_r := math.cos(rotation) sin_r := math.sin(rotation) for i in 0..=segs { angle := start_angle + f32(i) * sweep_angle / f32(segs) cos_a := math.cos(angle) sin_a := math.sin(angle) op := rotate_point(cx + outer_r * cos_a, cy + outer_r * sin_a, cx, cy, cos_r, sin_r) ip := rotate_point(cx + inner_r * cos_a, cy + inner_r * sin_a, cx, cy, cos_r, sin_r) append(&batch.vertices, Draw2D_Vertex{ position = op, normal = {0, 0, 1}, color = color }, Draw2D_Vertex{ position = ip, normal = {0, 0, 1}, color = color }, ) } for i in 0.. MAX_VERTEX_COUNT || len(batch.indices) + 6 > MAX_INDEX_COUNT { return } dx := x2 - x1 dy := y2 - y1 length := math.sqrt(dx * dx + dy * dy) if length < 0.0001 { return } // Perpendicular, scaled by half thickness ht := thickness * 0.5 / length px := -dy * ht py := dx * ht base := u32(len(batch.vertices)) append(&batch.vertices, Draw2D_Vertex{ position = {x1 - px, y1 - py, 0}, normal = {0, 0, 1}, color = color }, Draw2D_Vertex{ position = {x1 + px, y1 + py, 0}, normal = {0, 0, 1}, color = color }, Draw2D_Vertex{ position = {x2 + px, y2 + py, 0}, normal = {0, 0, 1}, color = color }, Draw2D_Vertex{ position = {x2 - px, y2 - py, 0}, normal = {0, 0, 1}, color = color }, ) append(&batch.indices, base, base + 1, base + 2, base + 2, base + 3, base) } batch_add_text :: proc(batch: ^Batch_2D, x, y: f32, text: string, color: [4]f32, scale: f32 = 1.0) { base_font_add_text(batch, x, y, text, color, scale) } text_width :: proc(text: string, scale: f32 = 1.0) -> f32 { return base_font_text_width(text, scale) } text_height :: proc(text: string, scale: f32 = 1.0) -> f32 { return base_font_text_height(text, scale) } // --- Projection --- update_screen_projection :: proc(batch: ^Batch_2D, width, height: f32) { batch.screen_proj = ortho_2d(0, width, 0, height) batch.proj_view = batch.screen_proj } set_projection_2d :: proc(batch: ^Batch_2D, proj: glsl.mat4x4) { batch.proj_view = proj } reset_projection_2d :: proc(batch: ^Batch_2D) { batch.proj_view = batch.screen_proj } // Orthographic projection for screen-space 2D (Y-down, top-left origin). // Vulkan NDC has Y+ down so bottom=0,top=height gives Y-down directly. // D3D NDC has Y+ up so we swap bottom/top to achieve the same Y-down result. ortho_2d :: proc(left, right, bottom, top: f32) -> glsl.mat4x4 { b, t: f32 when bk.GPU_BACKEND == "vulkan" || bk.GPU_BACKEND == "opengl" { b = bottom t = top } else { // D3D: flip Y so Y=0 is top of screen (Y-down like Vulkan) b = top t = bottom } rl := right - left tb := t - b // Clip: X[-1,1], Y[-1,1], Z[0,1] // Odin matrix: m[row, col] m: glsl.mat4x4 m[0, 0] = 2.0 / rl m[1, 1] = 2.0 / tb m[2, 2] = 0.5 m[0, 3] = -(right + left) / rl m[1, 3] = -(t + b) / tb m[2, 3] = 0.5 m[3, 3] = 1.0 return m }