"""
Tests for the camera convention.
The engine's projection is not the usual one and getting it wrong renders an EMPTY
image, which is indistinguishable from a broken pipeline. Two things make it
unusual, both load-bearing:
* The view matrix looks along `Position - Forward` (Camera.cs:104, whose comment
notes `CreateLookAt` has the other handedness). A visible point therefore has
NEGATIVE w after projection, and `project()` discards `p_hom.w >= 0.0`.
* .NET builds `view * projection` for row-vector maths and uploads it row-major;
GLSL reads those bytes column-major and computes `M * v`. The two conventions
cancel, so the bytes cross unchanged - "fixing" either one alone breaks it.
Run: python3 -m unittest discover -s blender
"""
import unittest
from ifs_host import sierpinski
from ifs_render import project_point, view_projection
WIDTH = HEIGHT = 512
EYE = (0.0, 0.0, -2.6)
FORWARD = (0.0, 0.0, 1.0)
UP = (0.0, 1.0, 0.0)
class Projection(unittest.TestCase):
def setUp(self):
self.vp = view_projection(EYE, FORWARD, UP)
def test_visible_points_have_negative_w(self):
# The property project() actually tests. If this flips, everything is
# discarded and the render is silently blank.
for point in ((0.0, 0.0, 0.0), (0.3, 0.2, 0.0), (-0.3, -0.2, 0.0)):
result = project_point(self.vp, point, WIDTH, HEIGHT)
self.assertIsNotNone(result, f"{point} was discarded")
self.assertLess(result[2], 0.0, f"{point} has w >= 0")
def test_the_demo_document_lands_inside_the_frame(self):
# Every corner of the gasket must be on screen, or the render is cropped
# and the "wrong" output still looks like a fractal.
corners = [(0.0, 0.75, 0.0), (-0.65, -0.375, 0.0), (0.65, -0.375, 0.0)]
projected = [project_point(self.vp, c, WIDTH, HEIGHT) for c in corners]
for corner, result in zip(corners, projected):
self.assertIsNotNone(result, f"{corner} is off screen")
x, y, _w = result
self.assertTrue(0 <= x < WIDTH and 0 <= y < HEIGHT, f"{corner} -> {x},{y}")
def test_the_gasket_is_the_right_way_up(self):
# The apex is at +y in world space and must be ABOVE the base on screen.
# A sign error in `up` flips it, and an upside-down gasket is still a
# perfectly convincing gasket.
apex = project_point(self.vp, (0.0, 0.75, 0.0), WIDTH, HEIGHT)
base = project_point(self.vp, (0.0, -0.375, 0.0), WIDTH, HEIGHT)
self.assertGreater(apex[1], base[1])
def test_the_gasket_is_not_mirrored(self):
left = project_point(self.vp, (-0.65, -0.375, 0.0), WIDTH, HEIGHT)
right = project_point(self.vp, (0.65, -0.375, 0.0), WIDTH, HEIGHT)
self.assertLess(left[0], right[0])
def test_the_centre_projects_to_the_centre(self):
x, y, _w = project_point(self.vp, (0.0, 0.0, 0.0), WIDTH, HEIGHT)
self.assertAlmostEqual(WIDTH / 2 - 0.5, x, places=3)
self.assertAlmostEqual(HEIGHT / 2 - 0.5, y, places=3)
def test_points_behind_the_camera_are_discarded(self):
# Behind the eye at z = -2.6 looking towards +z means z < -2.6.
self.assertIsNone(project_point(self.vp, (0.0, 0.0, -50.0), WIDTH, HEIGHT))
def test_points_far_outside_the_frustum_are_discarded(self):
self.assertIsNone(project_point(self.vp, (100.0, 0.0, 0.0), WIDTH, HEIGHT))
def test_the_demo_camera_matches_the_demo_document(self):
# sierpinski() and the render camera must agree, or the fractal is framed
# somewhere other than where it is drawn.
iterators, _camera, _settings = sierpinski()
for it in iterators:
translate = it.vec3_params[0]
corner = tuple(c * 2.0 for c in translate) # Translate is corner/2
self.assertIsNotNone(project_point(self.vp, corner, WIDTH, HEIGHT),
f"iterator corner {corner} is off screen")
if __name__ == "__main__":
unittest.main()