Harbor

branch main
showing the latest snapshot on main
draw3d.odin 12.4 KB · Plain text
renderer/draw3d.odin 0644 Raw
package renderer

import "core:log"
import "core:math"
import glsl "core:math/linalg/glsl"
import bk "../backend"
import "../resource"

MAX_DRAW_3D_COMMANDS :: 1024

Push_Constants_3D :: struct {
	proj_view: glsl.mat4x4,
	model:     glsl.mat4x4,
}

// Lit 3D push constants: model matrix + material properties.
// Odin aligns to 16 bytes (mat4x4 alignment), so actual size is 128 bytes.
Push_Constants_3D_Lit :: struct {
	model:       glsl.mat4x4,  // 64 bytes, offset 0
	base_color:  [4]f32,       // 16 bytes, offset 64
	metallic:    f32,          // 4 bytes,  offset 80
	roughness:   f32,          // 4 bytes,  offset 84
	reflectance: f32,          // 4 bytes,  offset 88
	_pad:        f32,          // 4 bytes,  offset 92
	emissive:    [4]f32,       // 16 bytes, offset 96
}
// Actual: 128 bytes (16-byte aligned, padded after emissive)

Draw_Command_3D :: struct {
	mesh_id:         u32,
	texture_id:      u32,
	normal_map_id:   u32,
	layout_variant:  bk.Layout_Variant,
	model:           glsl.mat4x4,
	double_sided:    bool,
	// Material properties (used by lit path)
	base_color:      [4]f32,
	metallic:        f32,
	roughness:       f32,
	reflectance:     f32,
	emissive:        [4]f32,
}

Draw_Queue_3D :: struct {
	commands:       [MAX_DRAW_3D_COMMANDS]Draw_Command_3D,
	count:          u32,
	proj_view:      glsl.mat4x4,
	resource_state: ^resource.Resource_State,
}

reset_draw_queue_3d :: proc(q: ^Draw_Queue_3D) {
	q.count = 0
}

push_draw_command_3d :: proc(
	q: ^Draw_Queue_3D,
	mesh_id, texture_id: u32,
	layout_variant: bk.Layout_Variant = .PNU,
	model: glsl.mat4x4,
	double_sided: bool = false,
	base_color: [4]f32 = {1, 1, 1, 1},
	metallic: f32 = 0.0,
	roughness: f32 = 0.5,
	reflectance: f32 = 0.5,
	emissive: [4]f32 = {0, 0, 0, 0},
	normal_map_id: u32 = resource.NORMAL_TEXTURE_ID,
) {
	if q.count >= MAX_DRAW_3D_COMMANDS {
		log.warnf("gpu/renderer: draw queue full (%d), command dropped", MAX_DRAW_3D_COMMANDS)
		return
	}
	q.commands[q.count] = {
		mesh_id        = mesh_id,
		texture_id     = texture_id,
		normal_map_id  = normal_map_id,
		layout_variant = layout_variant,
		model          = model,
		double_sided   = double_sided,
		base_color     = base_color,
		metallic       = metallic,
		roughness      = roughness,
		reflectance    = reflectance,
		emissive       = emissive,
	}
	q.count += 1
}

flush_draw_queue_3d :: proc(
	q: ^Draw_Queue_3D,
	b: ^bk.Backend,
	ctx: bk.Frame_Context,
	pipelines_culled:  [bk.MAX_LAYOUT_VARIANTS]bk.Pipeline_Handle,
	pipelines_no_cull: [bk.MAX_LAYOUT_VARIANTS]bk.Pipeline_Handle,
) {
	if q.count == 0 {
		return
	}

	// Sort by (layout_variant, double_sided) to minimize pipeline switches.
	cmds := q.commands[:q.count]
	for i in 1..<len(cmds) {
		j := i
		for j > 0 && draw_sort_key_3d(&cmds[j]) < draw_sort_key_3d(&cmds[j - 1]) {
			cmds[j], cmds[j - 1] = cmds[j - 1], cmds[j]
			j -= 1
		}
	}

	current_variant: bk.Layout_Variant = cmds[0].layout_variant
	current_double_sided: bool = cmds[0].double_sided
	current_pipeline := select_pipeline(pipelines_culled, pipelines_no_cull, current_variant, current_double_sided)
	pipeline_valid := current_pipeline != {}
	if pipeline_valid {
		b.bind_graphics_pipeline(ctx, current_pipeline)
	}

	last_texture_id: u32 = max(u32)

	for i in 0..<q.count {
		draw := &q.commands[i]

		if draw.layout_variant != current_variant || draw.double_sided != current_double_sided {
			current_variant = draw.layout_variant
			current_double_sided = draw.double_sided
			current_pipeline = select_pipeline(pipelines_culled, pipelines_no_cull, current_variant, current_double_sided)
			pipeline_valid = current_pipeline != {}
			if pipeline_valid {
				b.bind_graphics_pipeline(ctx, current_pipeline)
			}
			last_texture_id = max(u32)
		}

		if !pipeline_valid { continue }

		if draw.texture_id != last_texture_id {
			ds := resource.get_texture_descriptor_set(q.resource_state, draw.texture_id)
			b.bind_descriptor_set(ctx, current_pipeline, ds, 0)
			last_texture_id = draw.texture_id
		}

		pc := Push_Constants_3D{
			proj_view = q.proj_view,
			model     = draw.model,
		}
		b.push_constants(ctx, current_pipeline, {.Vertex}, 0, size_of(Push_Constants_3D), &pc)

		vb, ib, index_count, mesh_ok := resource.get_mesh_buffers(q.resource_state, draw.mesh_id)
		if !mesh_ok { continue }

		b.bind_vertex_buffer(ctx, vb)
		b.bind_index_buffer(ctx, ib)
		b.draw_indexed(ctx, u32(index_count), 1, 0, 0, 0)
	}

	q.count = 0
}

// Lit 3D flush: uses PBR pipeline with light UBO at set=1
flush_draw_queue_3d_lit :: proc(
	q: ^Draw_Queue_3D,
	b: ^bk.Backend,
	ctx: bk.Frame_Context,
	pipelines_culled:  [bk.MAX_LAYOUT_VARIANTS]bk.Pipeline_Handle,
	pipelines_no_cull: [bk.MAX_LAYOUT_VARIANTS]bk.Pipeline_Handle,
	light_descriptor_set: bk.Descriptor_Handle,
) {
	if q.count == 0 {
		return
	}

	cmds := q.commands[:q.count]
	for i in 1..<len(cmds) {
		j := i
		for j > 0 && draw_sort_key_3d(&cmds[j]) < draw_sort_key_3d(&cmds[j - 1]) {
			cmds[j], cmds[j - 1] = cmds[j - 1], cmds[j]
			j -= 1
		}
	}

	current_variant: bk.Layout_Variant = cmds[0].layout_variant
	current_double_sided: bool = cmds[0].double_sided
	current_pipeline := select_pipeline(pipelines_culled, pipelines_no_cull, current_variant, current_double_sided)
	pipeline_valid := current_pipeline != {}
	if pipeline_valid {
		b.bind_graphics_pipeline(ctx, current_pipeline)
		if light_descriptor_set != bk.Descriptor_Handle(0) {
			b.bind_descriptor_set(ctx, current_pipeline, light_descriptor_set, 1)
		}
	}

	last_texture_id: u32 = max(u32)
	last_normal_map_id: u32 = max(u32)

	for i in 0..<q.count {
		draw := &q.commands[i]

		if draw.layout_variant != current_variant || draw.double_sided != current_double_sided {
			current_variant = draw.layout_variant
			current_double_sided = draw.double_sided
			current_pipeline = select_pipeline(pipelines_culled, pipelines_no_cull, current_variant, current_double_sided)
			pipeline_valid = current_pipeline != {}
			if pipeline_valid {
				b.bind_graphics_pipeline(ctx, current_pipeline)
				if light_descriptor_set != bk.Descriptor_Handle(0) {
					b.bind_descriptor_set(ctx, current_pipeline, light_descriptor_set, 1)
				}
			}
			last_texture_id = max(u32)
			last_normal_map_id = max(u32)
		}

		if !pipeline_valid { continue }

		if draw.texture_id != last_texture_id {
			ds := resource.get_texture_descriptor_set(q.resource_state, draw.texture_id)
			b.bind_descriptor_set(ctx, current_pipeline, ds, 0)
			last_texture_id = draw.texture_id
		}

		// Bind normal map at set 3 for variants with tangent data
		if variant_has_tangent(current_variant) && draw.normal_map_id != last_normal_map_id {
			nds := resource.get_texture_descriptor_set(q.resource_state, draw.normal_map_id)
			b.bind_descriptor_set(ctx, current_pipeline, nds, 3)
			last_normal_map_id = draw.normal_map_id
		}

		pc := Push_Constants_3D_Lit{
			model       = draw.model,
			base_color  = draw.base_color,
			metallic    = draw.metallic,
			roughness   = draw.roughness,
			reflectance = draw.reflectance,
			emissive    = draw.emissive,
		}
		b.push_constants(ctx, current_pipeline, {.Vertex}, 0, size_of(Push_Constants_3D_Lit), &pc)

		vb, ib, index_count, mesh_ok := resource.get_mesh_buffers(q.resource_state, draw.mesh_id)
		if !mesh_ok { continue }

		b.bind_vertex_buffer(ctx, vb)
		b.bind_index_buffer(ctx, ib)
		b.draw_indexed(ctx, u32(index_count), 1, 0, 0, 0)
	}

	q.count = 0
}

// Lit 3D flush with shadow maps: PBR pipeline with light UBO at set=1 and shadow at set=2
flush_draw_queue_3d_lit_shadowed :: proc(
	q: ^Draw_Queue_3D,
	b: ^bk.Backend,
	ctx: bk.Frame_Context,
	pipelines_culled:  [bk.MAX_LAYOUT_VARIANTS]bk.Pipeline_Handle,
	pipelines_no_cull: [bk.MAX_LAYOUT_VARIANTS]bk.Pipeline_Handle,
	light_descriptor_set: bk.Descriptor_Handle,
	shadow_descriptor_set: bk.Descriptor_Handle,
) {
	if q.count == 0 {
		return
	}

	cmds := q.commands[:q.count]
	for i in 1..<len(cmds) {
		j := i
		for j > 0 && draw_sort_key_3d(&cmds[j]) < draw_sort_key_3d(&cmds[j - 1]) {
			cmds[j], cmds[j - 1] = cmds[j - 1], cmds[j]
			j -= 1
		}
	}

	current_variant: bk.Layout_Variant = cmds[0].layout_variant
	current_double_sided: bool = cmds[0].double_sided
	current_pipeline := select_pipeline(pipelines_culled, pipelines_no_cull, current_variant, current_double_sided)
	pipeline_valid := current_pipeline != {}
	if pipeline_valid {
		b.bind_graphics_pipeline(ctx, current_pipeline)
		b.bind_descriptor_set(ctx, current_pipeline, light_descriptor_set, 1)
		b.bind_descriptor_set(ctx, current_pipeline, shadow_descriptor_set, 2)
	}

	last_texture_id: u32 = max(u32)
	last_normal_map_id: u32 = max(u32)

	for i in 0..<q.count {
		draw := &q.commands[i]

		if draw.layout_variant != current_variant || draw.double_sided != current_double_sided {
			current_variant = draw.layout_variant
			current_double_sided = draw.double_sided
			current_pipeline = select_pipeline(pipelines_culled, pipelines_no_cull, current_variant, current_double_sided)
			pipeline_valid = current_pipeline != {}
			if pipeline_valid {
				b.bind_graphics_pipeline(ctx, current_pipeline)
				b.bind_descriptor_set(ctx, current_pipeline, light_descriptor_set, 1)
				b.bind_descriptor_set(ctx, current_pipeline, shadow_descriptor_set, 2)
			}
			last_texture_id = max(u32)
			last_normal_map_id = max(u32)
		}

		if !pipeline_valid { continue }

		if draw.texture_id != last_texture_id {
			ds := resource.get_texture_descriptor_set(q.resource_state, draw.texture_id)
			b.bind_descriptor_set(ctx, current_pipeline, ds, 0)
			last_texture_id = draw.texture_id
		}

		// Bind normal map at set 3 for variants with tangent data
		if variant_has_tangent(current_variant) && draw.normal_map_id != last_normal_map_id {
			nds := resource.get_texture_descriptor_set(q.resource_state, draw.normal_map_id)
			b.bind_descriptor_set(ctx, current_pipeline, nds, 3)
			last_normal_map_id = draw.normal_map_id
		}

		pc := Push_Constants_3D_Lit{
			model       = draw.model,
			base_color  = draw.base_color,
			metallic    = draw.metallic,
			roughness   = draw.roughness,
			reflectance = draw.reflectance,
			emissive    = draw.emissive,
		}
		b.push_constants(ctx, current_pipeline, {.Vertex}, 0, size_of(Push_Constants_3D_Lit), &pc)

		vb, ib, index_count, mesh_ok := resource.get_mesh_buffers(q.resource_state, draw.mesh_id)
		if !mesh_ok { continue }

		b.bind_vertex_buffer(ctx, vb)
		b.bind_index_buffer(ctx, ib)
		b.draw_indexed(ctx, u32(index_count), 1, 0, 0, 0)
	}

	q.count = 0
}

// --- Camera Math ---

// Perspective projection for Vulkan: depth [0,1], Y-flipped for Vulkan NDC
// Odin matrix indexing: m[row, col], column-major storage (matches GLSL)
perspective :: proc(fovy_degrees, aspect, near, far: f32) -> glsl.mat4x4 {
	fovy_rad := fovy_degrees * math.PI / 180.0
	f := 1.0 / math.tan(fovy_rad * 0.5)

	m: glsl.mat4x4
	m[0, 0] = f / aspect
	m[1, 1] = -f  // Y-flip for Vulkan
	m[2, 2] = far / (near - far)
	m[2, 3] = (near * far) / (near - far)
	m[3, 2] = -1.0
	return m
}

// Right-handed look-at matrix. Camera looks down -Z.
// m[row, col] convention, column-major storage.
look_at :: proc(eye, target, world_up: glsl.vec3) -> glsl.mat4x4 {
	f := normalize(target - eye)       // forward (into screen)
	r := normalize(cross(f, world_up)) // right
	u := cross(r, f)                   // true up

	m: glsl.mat4x4
	m[0, 0] =  r.x;  m[0, 1] =  r.y;  m[0, 2] =  r.z
	m[1, 0] =  u.x;  m[1, 1] =  u.y;  m[1, 2] =  u.z
	m[2, 0] = -f.x;  m[2, 1] = -f.y;  m[2, 2] = -f.z
	m[0, 3] = -dot(r, eye)
	m[1, 3] = -dot(u, eye)
	m[2, 3] =  dot(f, eye)
	m[3, 3] = 1.0
	return m
}

// --- Draw command helpers ---

// Sort key: layout_variant in high bits, double_sided in low bit.
draw_sort_key_3d :: proc(cmd: ^Draw_Command_3D) -> u16 {
	return u16(cmd.layout_variant) << 1 | u16(cmd.double_sided)
}

// Returns true for layout variants that include tangent data (and need normal map at set 3).
variant_has_tangent :: proc(v: bk.Layout_Variant) -> bool {
	return v == .PNUT || v == .PNUCT
}

// Select pipeline from arrays based on variant and cull mode.
select_pipeline :: proc(
	culled:  [bk.MAX_LAYOUT_VARIANTS]bk.Pipeline_Handle,
	no_cull: [bk.MAX_LAYOUT_VARIANTS]bk.Pipeline_Handle,
	variant: bk.Layout_Variant,
	double_sided: bool,
) -> bk.Pipeline_Handle {
	arr := no_cull if double_sided else culled
	return arr[variant]
}

// --- Vector math helpers ---

@(private)
normalize :: proc(v: glsl.vec3) -> glsl.vec3 {
	l := math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z)
	if l < 1e-8 {
		return {0, 0, 0}
	}
	return v / l
}

@(private)
cross :: proc(a, b: glsl.vec3) -> glsl.vec3 {
	return {
		a.y * b.z - a.z * b.y,
		a.z * b.x - a.x * b.z,
		a.x * b.y - a.y * b.x,
	}
}

@(private)
dot :: proc(a, b: glsl.vec3) -> f32 {
	return a.x * b.x + a.y * b.y + a.z * b.z
}