"""
Tests for the viewport camera basis.
These exist because a mirror in the view basis is invisible in a still image of a
symmetric fractal and invisible at the centre of the screen, so it survived every
check until someone orbited and said it "doesn't rotate properly". The previous
camera test asserted the mirror as CORRECT.
Ground truth here is Blender's own projection, not my reasoning about handedness:
the viewport overlay has to land where Blender would put the same world point, or
it does not track the scene.
These need mathutils, so they run inside Blender:
blender -b --python-expr "import unittest,sys; sys.path.insert(0,'blender'); \\
unittest.main(module='test_viewport_camera', argv=['x'])"
"""
import math
import unittest
try:
from mathutils import Matrix, Vector
except ImportError: # pragma: no cover - only meaningful inside Blender
Matrix = Vector = None
import ifs_render
W = H = 512
LENS = 50.0
def _orbit_view(degrees, radius=3.0):
"""A camera orbiting the origin at `degrees`, world +Z up, looking inward."""
a = math.radians(degrees)
eye = Vector((radius * math.sin(a), -radius * math.cos(a), 0.0))
forward = (Vector((0, 0, 0)) - eye).normalized()
right = forward.cross(Vector((0, 0, 1))).normalized()
up = right.cross(forward)
world = Matrix(((right.x, up.x, -forward.x, eye.x),
(right.y, up.y, -forward.y, eye.y),
(right.z, up.z, -forward.z, eye.z),
(0, 0, 0, 1)))
return world.inverted()
def _blender_screen(view, point):
fov = 2.0 * math.atan(18.0 / LENS)
f = 1.0 / math.tan(fov / 2)
n, fr = 0.1, 100.0
proj = Matrix(((f, 0, 0, 0), (0, f, 0, 0),
(0, 0, (fr + n) / (n - fr), 2 * fr * n / (n - fr)),
(0, 0, -1, 0)))
v = proj @ view @ Vector((point[0], point[1], point[2], 1.0))
if v.w <= 0:
return None
return ((v.x / v.w + 1) * 0.5 * W, (v.y / v.w + 1) * 0.5 * H)
def _our_screen(view, point):
"""Exactly what ifs_viewport._view_projection does, including the negated up."""
world = view.inverted()
eye = world.translation
forward = -Vector(world.col[2][:3])
up = Vector(world.col[1][:3])
fov = math.degrees(2.0 * math.atan(18.0 / LENS))
vp = ifs_render.view_projection(
(eye.x, eye.y, eye.z), (forward.x, forward.y, forward.z),
(up.x, up.y, up.z), fov_degrees=fov, engine_fov_quirk=False, flip_x=True)
return ifs_render.project_point(vp, point, W, H)
@unittest.skipIf(Matrix is None, "needs mathutils (run inside Blender)")
class ViewportTracksBlender(unittest.TestCase):
# project_point subtracts half a pixel, as the kernel does.
TOLERANCE = 1.0
def test_matches_blenders_projection_around_a_full_orbit(self):
probe = (1.0, 0.0, 0.0) # asymmetric, or a mirror is invisible
for degrees in range(0, 360, 15):
view = _orbit_view(degrees)
expected = _blender_screen(view, probe)
actual = _our_screen(view, probe)
with self.subTest(orbit=degrees):
self.assertIsNotNone(actual, "point was discarded")
self.assertIsNotNone(expected)
self.assertLess(abs(expected[0] - actual[0]), self.TOLERANCE)
self.assertLess(abs(expected[1] - actual[1]), self.TOLERANCE)
def test_a_mirrored_basis_would_fail_this(self):
# Guards the guard: with the up negation removed the test must break, or
# it is not actually testing for the mirror.
view = _orbit_view(0)
world = view.inverted()
eye = world.translation
forward = -Vector(world.col[2][:3])
up = Vector(world.col[1][:3])
fov = math.degrees(2.0 * math.atan(18.0 / LENS))
vp = ifs_render.view_projection( # flip_x omitted - the old bug
(eye.x, eye.y, eye.z), (forward.x, forward.y, forward.z),
(up.x, up.y, up.z), fov_degrees=fov, engine_fov_quirk=False)
mirrored = ifs_render.project_point(vp, (1.0, 0.0, 0.0), W, H)
correct = _our_screen(view, (1.0, 0.0, 0.0))
self.assertGreater(abs(mirrored[0] - correct[0]), 100.0,
"the two bases are indistinguishable, so this proves nothing")
def test_vertical_axis_is_not_flipped(self):
# The orbit probe sits at z=0, so its screen y never moves and a vertical
# flip hides from that test completely. This is the one that catches it.
view = _orbit_view(0)
high = _our_screen(view, (0.0, 0.0, 1.0))
low = _our_screen(view, (0.0, 0.0, -1.0))
self.assertGreater(high[1], low[1], "world +Z must be up on screen")
if __name__ == "__main__":
unittest.main()