Harbor

branch main
showing the latest snapshot on main
camera_test.odin 7.8 KB · Plain text
tests/camera_test.odin 0644 Raw
package gpu_tests

import gpu ".."
import "core:math"
import "core:testing"

// --- Easing ---

@(test)
test_ease_boundaries :: proc(t: ^testing.T) {
	// All easing types should return 0 at t=0 and 1 at t=1
	types := []gpu.Ease_Type {
		.LINEAR,
		.SINE_IN,
		.SINE_OUT,
		.SINE_IN_OUT,
		.QUAD_IN,
		.QUAD_OUT,
		.QUAD_IN_OUT,
		.CUBIC_IN,
		.CUBIC_OUT,
		.CUBIC_IN_OUT,
		.EXPO_IN,
		.EXPO_OUT,
		.EXPO_IN_OUT,
	}
	for et in types {
		v0 := gpu.ease(0, et)
		v1 := gpu.ease(1, et)
		testing.expectf(t, math.abs(v0) < 1e-4, "ease(%v, 0) = %f, want ~0", et, v0)
		testing.expectf(t, math.abs(v1 - 1) < 1e-4, "ease(%v, 1) = %f, want ~1", et, v1)
	}
}

@(test)
test_ease_clamping :: proc(t: ^testing.T) {
	// Values outside [0,1] should be clamped
	testing.expect(t, gpu.ease(-1) == 0, "ease(-1) should be 0")
	testing.expect(t, gpu.ease(2) == 1, "ease(2) should be 1")
}

@(test)
test_ease_monotonic :: proc(t: ^testing.T) {
	// Standard easing types should be monotonically increasing
	types := []gpu.Ease_Type {
		.LINEAR,
		.SINE_IN,
		.SINE_OUT,
		.SINE_IN_OUT,
		.QUAD_IN,
		.QUAD_OUT,
		.QUAD_IN_OUT,
		.CUBIC_IN,
		.CUBIC_OUT,
		.CUBIC_IN_OUT,
	}
	for et in types {
		prev: f32 = 0
		for i in 1 ..= 10 {
			s := f32(i) / 10.0
			v := gpu.ease(s, et)
			testing.expectf(
				t,
				v >= prev - 1e-6,
				"ease(%v) not monotonic at %f: %f < %f",
				et,
				s,
				v,
				prev,
			)
			prev = v
		}
	}
}

// --- Camera Lerp 2D ---

@(test)
test_camera_lerp_2d_endpoints :: proc(t: ^testing.T) {
	from := gpu.Camera2D {
		offset   = {0, 0},
		target   = {0, 0},
		rotation = 0,
		zoom     = 1,
	}
	to := gpu.Camera2D {
		offset   = {100, 200},
		target   = {50, 50},
		rotation = 90,
		zoom     = 2,
	}

	at0 := gpu.camera_lerp_2d(from, to, 0)
	testing.expect(t, at0.offset.x == 0 && at0.offset.y == 0, "lerp2d t=0 offset")
	testing.expect(t, at0.zoom == 1, "lerp2d t=0 zoom")

	at1 := gpu.camera_lerp_2d(from, to, 1)
	testing.expect(t, math.abs(at1.offset.x - 100) < 1e-4, "lerp2d t=1 offset.x")
	testing.expect(t, math.abs(at1.rotation - 90) < 1e-4, "lerp2d t=1 rotation")
	testing.expect(t, math.abs(at1.zoom - 2) < 1e-4, "lerp2d t=1 zoom")
}

@(test)
test_camera_lerp_2d_midpoint :: proc(t: ^testing.T) {
	from := gpu.Camera2D {
		target = {0, 0},
		zoom   = 1,
	}
	to := gpu.Camera2D {
		target = {10, 20},
		zoom   = 3,
	}

	mid := gpu.camera_lerp_2d(from, to, 0.5)
	testing.expect(t, math.abs(mid.target.x - 5) < 1e-4, "lerp2d midpoint target.x")
	testing.expect(t, math.abs(mid.target.y - 10) < 1e-4, "lerp2d midpoint target.y")
	testing.expect(t, math.abs(mid.zoom - 2) < 1e-4, "lerp2d midpoint zoom")
}

// --- Camera Lerp 3D ---

@(test)
test_camera_lerp_3d_endpoints :: proc(t: ^testing.T) {
	from := gpu.Camera3D {
		position = {0, 0, 5},
		target   = {0, 0, 0},
		up       = {0, 1, 0},
		fovy     = 60,
		near     = 0.1,
		far      = 100,
	}
	to := gpu.Camera3D {
		position = {10, 5, 0},
		target   = {5, 2, -3},
		up       = {0, 1, 0},
		fovy     = 90,
		near     = 0.5,
		far      = 500,
	}

	at0 := gpu.camera_lerp_3d(from, to, 0)
	testing.expect(t, math.abs(at0.position.x) < 1e-4, "lerp3d t=0 pos.x")
	testing.expect(t, math.abs(at0.fovy - 60) < 1e-4, "lerp3d t=0 fovy")

	at1 := gpu.camera_lerp_3d(from, to, 1)
	testing.expect(t, math.abs(at1.position.x - 10) < 1e-4, "lerp3d t=1 pos.x")
	testing.expect(t, math.abs(at1.fovy - 90) < 1e-4, "lerp3d t=1 fovy")
}

@(test)
test_camera_lerp_3d_up_normalized :: proc(t: ^testing.T) {
	from := gpu.Camera3D {
		up = {0, 1, 0},
	}
	to := gpu.Camera3D {
		up = {1, 0, 0},
	}

	mid := gpu.camera_lerp_3d(from, to, 0.5)
	up_len := math.sqrt(mid.up.x * mid.up.x + mid.up.y * mid.up.y + mid.up.z * mid.up.z)
	testing.expect(t, math.abs(up_len - 1) < 1e-4, "lerp3d up should be normalized")
}

// --- Coordinate Conversion 2D ---

@(test)
test_screen_world_2d_identity :: proc(t: ^testing.T) {
	// Default camera (no offset, no rotation, zoom=1) should be identity
	cam := gpu.Camera2D {
		zoom = 1,
	}
	world := gpu.get_screen_to_world_2d({100, 200}, cam)
	testing.expect(t, math.abs(world.x - 100) < 1e-4, "identity screen->world x")
	testing.expect(t, math.abs(world.y - 200) < 1e-4, "identity screen->world y")
}

@(test)
test_screen_world_2d_roundtrip :: proc(t: ^testing.T) {
	cam := gpu.Camera2D {
		offset   = {50, 30},
		target   = {100, 200},
		rotation = 45,
		zoom     = 1.5,
	}
	original := gpu.Vec2{300, 150}
	screen := gpu.get_world_to_screen_2d(original, cam)
	back := gpu.get_screen_to_world_2d(screen, cam)
	testing.expectf(
		t,
		math.abs(back.x - original.x) < 0.1,
		"roundtrip x: got %f want %f",
		back.x,
		original.x,
	)
	testing.expectf(
		t,
		math.abs(back.y - original.y) < 0.1,
		"roundtrip y: got %f want %f",
		back.y,
		original.y,
	)
}

@(test)
test_world_to_screen_2d_zoom :: proc(t: ^testing.T) {
	cam := gpu.Camera2D {
		zoom = 2,
	}
	screen := gpu.get_world_to_screen_2d({10, 20}, cam)
	testing.expect(t, math.abs(screen.x - 20) < 1e-4, "zoom 2x should double x")
	testing.expect(t, math.abs(screen.y - 40) < 1e-4, "zoom 2x should double y")
}

// --- Camera Path ---

@(test)
test_camera_path_2d_empty :: proc(t: ^testing.T) {
	path := gpu.make_camera_path_2d({})
	cam := gpu.camera_path_eval_2d(path, 0)
	// Should return default camera
	testing.expect(t, cam.zoom == 1, "empty path should return default camera")
}

@(test)
test_camera_path_2d_single :: proc(t: ^testing.T) {
	kf := [?]gpu.Camera_Keyframe_2D{{camera = gpu.Camera2D{target = {5, 5}, zoom = 2}, time = 0}}
	path := gpu.make_camera_path_2d(kf[:])
	cam := gpu.camera_path_eval_2d(path, 0)
	testing.expect(t, math.abs(cam.target.x - 5) < 1e-4, "single keyframe target.x")
	testing.expect(t, math.abs(cam.zoom - 2) < 1e-4, "single keyframe zoom")
}

@(test)
test_camera_path_2d_interpolation :: proc(t: ^testing.T) {
	kf := [?]gpu.Camera_Keyframe_2D {
		{camera = gpu.Camera2D{target = {0, 0}, zoom = 1}, time = 0, easing = .LINEAR},
		{camera = gpu.Camera2D{target = {100, 0}, zoom = 2}, time = 2, easing = .LINEAR},
	}
	path := gpu.make_camera_path_2d(kf[:])

	// At t=1 (midpoint)
	cam := gpu.camera_path_eval_2d(path, 1)
	testing.expectf(
		t,
		math.abs(cam.target.x - 50) < 1e-2,
		"path midpoint target.x: got %f want 50",
		cam.target.x,
	)
	testing.expectf(
		t,
		math.abs(cam.zoom - 1.5) < 1e-2,
		"path midpoint zoom: got %f want 1.5",
		cam.zoom,
	)

	// Before start
	cam_start := gpu.camera_path_eval_2d(path, -1)
	testing.expect(t, math.abs(cam_start.target.x) < 1e-4, "before start should clamp")

	// After end
	cam_end := gpu.camera_path_eval_2d(path, 10)
	testing.expect(t, math.abs(cam_end.target.x - 100) < 1e-4, "after end should clamp")
}

@(test)
test_camera_path_3d_interpolation :: proc(t: ^testing.T) {
	kf := [?]gpu.Camera_Keyframe_3D {
		{
			camera = gpu.Camera3D {
				position = {0, 0, 5},
				target = {0, 0, 0},
				up = {0, 1, 0},
				fovy = 60,
				near = 0.1,
				far = 100,
			},
			time = 0,
			easing = .LINEAR,
		},
		{
			camera = gpu.Camera3D {
				position = {10, 0, 5},
				target = {10, 0, 0},
				up = {0, 1, 0},
				fovy = 60,
				near = 0.1,
				far = 100,
			},
			time = 4,
			easing = .LINEAR,
		},
	}
	path := gpu.make_camera_path_3d(kf[:])
	cam := gpu.camera_path_eval_3d(path, 2)
	testing.expectf(
		t,
		math.abs(cam.position.x - 5) < 1e-2,
		"3d path midpoint pos.x: got %f want 5",
		cam.position.x,
	)
}

// --- Camera View/Proj Matrix ---

@(test)
test_camera_view_matrix_identity_look :: proc(t: ^testing.T) {
	// Camera at origin looking down -Z
	cam := gpu.Camera3D {
		position = {0, 0, 0},
		target   = {0, 0, -1},
		up       = {0, 1, 0},
	}
	view := gpu.camera_view_matrix(cam)
	// Should be close to identity (with Z negated for right-handed)
	testing.expect(t, math.abs(view[0, 0] - 1) < 1e-4, "view[0,0] should be 1")
	testing.expect(t, math.abs(view[1, 1] - 1) < 1e-4, "view[1,1] should be 1")
	testing.expect(t, math.abs(view[2, 2] - 1) < 1e-4, "view[2,2] should be 1")
	testing.expect(t, math.abs(view[3, 3] - 1) < 1e-4, "view[3,3] should be 1")
}