Letterbox, maintain render-resolution's aspect ratio

This commit is contained in:
2026-02-05 17:00:21 +01:00
parent 3b32097c56
commit cb19295575
6 changed files with 153 additions and 47 deletions

View File

@@ -121,7 +121,7 @@ get_camera_projection_matrix :: proc(renderer: ^Renderer, camera: ^Camera) -> (l
case Camera_Perspective: { case Camera_Perspective: {
projection_matrix *= linalg.matrix4_perspective( projection_matrix *= linalg.matrix4_perspective(
linalg.to_radians(f32(t.fov_degrees)), linalg.to_radians(f32(t.fov_degrees)),
get_aspect_ratio(renderer), get_aspect_ratio(renderer, .Render),
camera.near, camera.near,
camera.far, camera.far,
flip_z_axis = true, flip_z_axis = true,
@@ -129,7 +129,7 @@ get_camera_projection_matrix :: proc(renderer: ^Renderer, camera: ^Camera) -> (l
} }
case Camera_Orthographic: { case Camera_Orthographic: {
half_h := t.height / 2 half_h := t.height / 2
half_w := half_h * get_aspect_ratio(renderer) half_w := half_h * get_aspect_ratio(renderer, .Render)
projection_matrix *= linalg.matrix_ortho3d( projection_matrix *= linalg.matrix_ortho3d(
-half_w, half_w, -half_w, half_w,

View File

@@ -161,6 +161,8 @@ create_mesh_from_primitive :: proc(renderer: ^Renderer, primitive_mesh_type: Pri
vertices := make([dynamic]f32, 0) vertices := make([dynamic]f32, 0)
indices := make([dynamic]u32, 0) indices := make([dynamic]u32, 0)
defer delete(vertices)
defer delete(indices)
for y in 0..=Y_SEGMENTS { for y in 0..=Y_SEGMENTS {
y_segment := f32(y) / f32(Y_SEGMENTS) y_segment := f32(y) / f32(Y_SEGMENTS)
@@ -234,3 +236,12 @@ create_mesh_from_primitive :: proc(renderer: ^Renderer, primitive_mesh_type: Pri
return {}, false return {}, false
} }
delete_mesh :: proc(renderer: ^Renderer, mesh: ^Mesh) {
assert(renderer != nil)
assert(mesh != nil)
when RENDER_BACKEND_OPENGL {
opengl_delete_mesh(renderer, mesh)
}
}

View File

@@ -243,10 +243,12 @@ delete_pass :: proc(pass: ^Pass) {
} }
@(private) delete_scene_pass :: proc(pass: ^Scene_Pass) { @(private) delete_scene_pass :: proc(pass: ^Scene_Pass) {
assert(pass.draw_commands != nil)
delete(pass.draw_commands) delete(pass.draw_commands)
} }
@(private) delete_post_processing_pass :: proc(pass: ^Post_Processing_Pass) { @(private) delete_post_processing_pass :: proc(pass: ^Post_Processing_Pass) {
assert(pass.post_processing_nodes != nil)
delete(pass.post_processing_nodes) delete(pass.post_processing_nodes)
} }
@@ -294,9 +296,10 @@ execute_pass :: proc(renderer: ^Renderer, pass: ^Pass, view_matrix, projection_m
should_clear_depth := should_write_depth should_clear_depth := should_write_depth
should_clear_color := true should_clear_color := true
should_scissor :: false
set_clear_color(renderer, RGBA_Color { 0, 0, 0, 0 }) set_clear_color(renderer, RGBA_Color { 0, 0, 0, 0 })
clear_screen(renderer, should_clear_color, should_clear_depth, should_scissor)
clear_screen(renderer, should_clear_color, should_clear_depth)
apply_depth(renderer, should_test_depth, should_write_depth) apply_depth(renderer, should_test_depth, should_write_depth)
apply_blend_mode(renderer, t.blend_mode) apply_blend_mode(renderer, t.blend_mode)

View File

@@ -4,7 +4,7 @@ import "core:mem"
import "core:log" import "core:log"
import "core:fmt" import "core:fmt"
PIPELINE_MAX_PASSES :: 8 PIPELINE_MAX_PASSES :: 16
Pipeline :: struct { Pipeline :: struct {
passes: [PIPELINE_MAX_PASSES]^Pass, passes: [PIPELINE_MAX_PASSES]^Pass,
@@ -40,10 +40,11 @@ add_pass_to_pipeline :: proc(renderer: ^Renderer, pass: ^Pass) -> bool {
pipeline := &renderer.pipeline pipeline := &renderer.pipeline
if pipeline.amount_of_passes == PIPELINE_MAX_PASSES { assert(pipeline.amount_of_passes < PIPELINE_MAX_PASSES)
log.warnf("Failed to add scene-pass '%v' to renderer's pipeline; hit max capacity (%v).", pass.name, PIPELINE_MAX_PASSES) // if pipeline.amount_of_passes == PIPELINE_MAX_PASSES {
return false // log.warnf("Failed to add scene-pass '%v' to renderer's pipeline; hit max capacity (%v).", pass.name, PIPELINE_MAX_PASSES)
} // return false
// }
pipeline.passes[pipeline.amount_of_passes] = pass pipeline.passes[pipeline.amount_of_passes] = pass
pipeline.amount_of_passes += 1 pipeline.amount_of_passes += 1

View File

@@ -12,7 +12,10 @@ RENDER_BACKEND_DIRECTX11 :: #config(RENDER_BACKEND_DIRECTX11, false)
RENDER_BACKEND_METAL :: #config(RENDER_BACKEND_METAL, false) RENDER_BACKEND_METAL :: #config(RENDER_BACKEND_METAL, false)
Renderer :: struct { Renderer :: struct {
render_resolution: Resolution,
viewport: Viewport, viewport: Viewport,
backbuffer: Backbuffer,
surface_ptr: rawptr, surface_ptr: rawptr,
vsync: bool, vsync: bool,
backend: rawptr, backend: rawptr,
@@ -37,19 +40,44 @@ Polygon_Mode :: enum {
Point, Point,
} }
Viewport :: struct { Resolution :: struct {
x, y, width, height: u16, width, height: u16,
} }
get_aspect_ratio :: proc(renderer: ^Renderer) -> f32 { Backbuffer :: struct {
resolution: Resolution,
}
Viewport :: struct {
x, y: u16,
res: Resolution,
}
Aspect_Ratio_Type :: enum { // TODO: SS - Rename?
Render,
Backbuffer,
Viewport,
}
get_aspect_ratio :: proc(renderer: ^Renderer, type: Aspect_Ratio_Type) -> f32 {
assert(renderer != nil) assert(renderer != nil)
viewport := &renderer.viewport res: Resolution
return f32(viewport.width) / f32(viewport.height) switch type {
case .Render: res = renderer.render_resolution
case .Backbuffer: res = renderer.backbuffer.resolution
case .Viewport: res = renderer.viewport.res
}
return f32(res.width) / f32(res.height)
} }
create :: proc(surface_ptr: rawptr) -> (^Renderer, bool) { create :: proc(render_resolution: Resolution, surface_ptr: rawptr) -> (^Renderer, bool) {
if render_resolution.width == 0 || render_resolution.height == 0 {
return nil, false
}
renderer := new(Renderer) renderer := new(Renderer)
renderer.render_resolution = render_resolution
renderer.surface_ptr = surface_ptr renderer.surface_ptr = surface_ptr
when RENDER_BACKEND_OPENGL { when RENDER_BACKEND_OPENGL {
@@ -119,21 +147,37 @@ create :: proc(surface_ptr: rawptr) -> (^Renderer, bool) {
return renderer, true return renderer, true
} }
set_viewport :: proc(renderer: ^Renderer, x, y, width, height: u16) { set_backbuffer_resolution :: proc(renderer: ^Renderer, res: Resolution) {
log.infof("Setting viewport to %v:%v, %vx%v.", x, y, width, height) assert(renderer != nil)
renderer.backbuffer.resolution = res
}
renderer.viewport = { set_viewport :: proc {
x = x, set_viewport_with_viewport,
y = y, set_viewport_with_x_y_w_h,
width = width, }
height = height,
} set_viewport_with_viewport :: proc(renderer: ^Renderer, viewport: Viewport) {
assert(renderer != nil)
// log.infof("Setting viewport to %v:%v, %vx%v.", viewport.x, viewport.y, viewport.res.width, viewport.res.height)
renderer.viewport = viewport
when RENDER_BACKEND_OPENGL { when RENDER_BACKEND_OPENGL {
opengl_viewport_changed(renderer) opengl_viewport_changed(renderer)
} }
} }
set_viewport_with_x_y_w_h :: proc(renderer: ^Renderer, x, y, width, height: u16) {
assert(renderer != nil)
set_viewport_with_viewport(renderer, Viewport {
x, y,
{ width, height }
})
}
set_vsync :: proc(renderer: ^Renderer, on: bool) { set_vsync :: proc(renderer: ^Renderer, on: bool) {
assert(renderer != nil) assert(renderer != nil)
@@ -158,9 +202,9 @@ set_vsync :: proc(renderer: ^Renderer, on: bool) {
} }
} }
@(private) clear_screen :: proc(renderer: ^Renderer, clear_color: bool, clear_depth: bool) { @(private) clear_screen :: proc(renderer: ^Renderer, clear_color, clear_depth, scissor: bool) {
when RENDER_BACKEND_OPENGL { when RENDER_BACKEND_OPENGL {
opengl_clear_screen(renderer, clear_color, clear_depth) opengl_clear_screen(renderer, clear_color, clear_depth, scissor)
} }
} }
@@ -173,9 +217,6 @@ render_frame :: proc(renderer: ^Renderer, texture_to_present: ^Texture, clear_co
camera_view_matrix, _ := get_camera_view_matrix(renderer.active_camera) camera_view_matrix, _ := get_camera_view_matrix(renderer.active_camera)
camera_projection_matrix, _ := get_camera_projection_matrix(renderer, renderer.active_camera) camera_projection_matrix, _ := get_camera_projection_matrix(renderer, renderer.active_camera)
set_clear_color(renderer, clear_color)
clear_screen(renderer, true, true)
for i in 0 ..< renderer.pipeline.amount_of_passes { for i in 0 ..< renderer.pipeline.amount_of_passes {
execute_pass(renderer, renderer.pipeline.passes[i], camera_view_matrix, camera_projection_matrix) execute_pass(renderer, renderer.pipeline.passes[i], camera_view_matrix, camera_projection_matrix)
} }
@@ -189,9 +230,38 @@ render_frame :: proc(renderer: ^Renderer, texture_to_present: ^Texture, clear_co
// Disable depth // Disable depth
apply_depth(renderer, false, false) apply_depth(renderer, false, false)
compute_letterboxed_viewport :: proc(backbuffer: Resolution, render: Resolution) -> Viewport {
bb_w, bb_h := expand_values(backbuffer)
r_w, r_h := expand_values(render)
bb_aspect := f32(bb_w) / f32(bb_h)
r_aspect := f32(r_w) / f32(r_h)
if bb_aspect > r_aspect {
// Pillarbox.
h := bb_h
w := u16(f32(h) * r_aspect)
x := (bb_w - w) / 2
return { x, 0, { w, h } }
} else {
// Letterbox.
w := bb_w
h := u16(f32(w) / r_aspect)
y := (bb_h - h) / 2
return { 0, y, { w, h } }
}
}
// Clear // Clear
set_viewport(renderer, 0, 0, renderer.backbuffer.resolution.width, renderer.backbuffer.resolution.height)
set_clear_color(renderer, RGB_Color{ 0, 0, 0 })
clear_screen(renderer, true, false, false)
set_viewport(renderer, compute_letterboxed_viewport(renderer.backbuffer.resolution, renderer.render_resolution))
set_clear_color(renderer, clear_color) set_clear_color(renderer, clear_color)
clear_screen(renderer, true, true) clear_screen(renderer, true, true, true)
// Create a temporary Material. // Create a temporary Material.
mat := Material { mat := Material {
@@ -230,8 +300,6 @@ execute_post_processing_node :: proc(renderer: ^Renderer, node: ^Post_Processing
defer bind_render_target(renderer, nil) defer bind_render_target(renderer, nil)
apply_depth(renderer, false, false) apply_depth(renderer, false, false)
set_clear_color(renderer, RGBA_Color { 0, 0, 0, 0 })
clear_screen(renderer, true, false)
// fmt.printfln("TODO: SS - Execute post-processing node '%v' (VS: '%v', FS: '%v').", "NAME", node.program.vertex_shader.path, node.program.fragment_shader.path) // fmt.printfln("TODO: SS - Execute post-processing node '%v' (VS: '%v', FS: '%v').", "NAME", node.program.vertex_shader.path, node.program.fragment_shader.path)
@@ -252,14 +320,14 @@ execute_post_processing_node :: proc(renderer: ^Renderer, node: ^Post_Processing
} }
destroy :: proc(renderer: ^Renderer) { destroy :: proc(renderer: ^Renderer) {
delete_mesh(renderer, &renderer.default_cube_mesh)
delete_mesh(renderer, &renderer.default_quad_mesh)
delete_mesh(renderer, &renderer.default_sphere_mesh)
when RENDER_BACKEND_OPENGL { when RENDER_BACKEND_OPENGL {
opengl_destroy(renderer) opengl_destroy(renderer)
} }
// TODO: SS - Destroy 'default_cube_mesh'
// TODO: SS - Destroy 'default_quad_mesh'
// TODO: SS - Destroy 'default_sphere_mesh'
assert(renderer != nil) assert(renderer != nil)
free(renderer) free(renderer)
} }

View File

@@ -90,21 +90,23 @@ when RENDER_BACKEND_OPENGL {
return true return true
} }
@(private) opengl_apply_renderer_viewport :: proc(renderer: ^Renderer) {
opengl_viewport_changed :: proc(renderer: ^Renderer) {
gl.Viewport( gl.Viewport(
i32(renderer.viewport.x), i32(renderer.viewport.y), i32(renderer.viewport.x), i32(renderer.viewport.y),
i32(renderer.viewport.width), i32(renderer.viewport.height) i32(renderer.viewport.res.width), i32(renderer.viewport.res.height)
) )
} }
opengl_viewport_changed :: proc(renderer: ^Renderer) {
opengl_apply_renderer_viewport(renderer)
}
opengl_set_vsync :: proc(renderer: ^Renderer, on: bool) -> bool { opengl_set_vsync :: proc(renderer: ^Renderer, on: bool) -> bool {
if win.wglSwapIntervalEXT == nil { if win.wglSwapIntervalEXT == nil {
fmt.printfln("'wglSwapIntervalEXT' is nil.") fmt.printfln("'wglSwapIntervalEXT' is nil.")
return false return false
} }
// Kommer inte in hit.
win.wglSwapIntervalEXT(on ? 1 : 0) win.wglSwapIntervalEXT(on ? 1 : 0)
return true return true
@@ -121,7 +123,17 @@ when RENDER_BACKEND_OPENGL {
gl.ClearColor(r, g, b, a) gl.ClearColor(r, g, b, a)
} }
opengl_clear_screen :: proc(renderer: ^Renderer, clear_color: bool, clear_depth: bool) { opengl_clear_screen :: proc(renderer: ^Renderer, clear_color, clear_depth, scissor: bool) {
if scissor {
gl.Enable(gl.SCISSOR_TEST)
gl.Scissor(
i32(renderer.viewport.x),
i32(renderer.viewport.y),
i32(renderer.viewport.res.width),
i32(renderer.viewport.res.height)
)
}
flags := u32(0) flags := u32(0)
if clear_color { if clear_color {
flags |= gl.COLOR_BUFFER_BIT flags |= gl.COLOR_BUFFER_BIT
@@ -130,6 +142,10 @@ when RENDER_BACKEND_OPENGL {
flags |= gl.DEPTH_BUFFER_BIT flags |= gl.DEPTH_BUFFER_BIT
} }
gl.Clear(flags) gl.Clear(flags)
if scissor {
gl.Disable(gl.SCISSOR_TEST)
}
} }
opengl_swap_buffers :: proc(renderer: ^Renderer) { opengl_swap_buffers :: proc(renderer: ^Renderer) {
@@ -208,6 +224,18 @@ when RENDER_BACKEND_OPENGL {
return m, true return m, true
} }
opengl_delete_mesh :: proc(renderer: ^Renderer, mesh: ^Mesh) {
if mesh == nil { return }
gl.DeleteBuffers(1, &mesh.backend.vbo)
gl.DeleteBuffers(1, &mesh.backend.ebo)
gl.DeleteVertexArrays(1, &mesh.backend.vao)
mesh.backend.vbo = 0
mesh.backend.ebo = 0
mesh.backend.vao = 0
}
opengl_create_shader :: proc(renderer: ^Renderer, type: Shader_Type, path: string, data: []u8) -> (Shader_OpenGL, bool) { opengl_create_shader :: proc(renderer: ^Renderer, type: Shader_Type, path: string, data: []u8) -> (Shader_OpenGL, bool) {
handle: u32 handle: u32
@@ -544,12 +572,7 @@ when RENDER_BACKEND_OPENGL {
if rt == nil { if rt == nil {
gl.BindFramebuffer(gl.FRAMEBUFFER, 0) gl.BindFramebuffer(gl.FRAMEBUFFER, 0)
gl.Viewport( opengl_apply_renderer_viewport(renderer)
0,
0,
i32(renderer.viewport.width),
i32(renderer.viewport.height),
)
return return
} }