OverviewFilesCommitsIssuesDocsReleasesPeers

ifsrenderer-linux / blender / test_camera.py

93 lines · python ref:
"""
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()