| … | … | @@ -96,15 +96,22 @@ def look_at(position, target, up) -> list[list[float]]: |
| 96 | 96 | ] |
| 97 | 97 | |
| 98 | 98 | |
| 99 | | def perspective(fov_degrees: float, aspect: float, near: float) -> list[list[float]]: |
| 99 | def perspective(fov_degrees: float, aspect: float, near: float, |
| 100 | engine_fov_quirk: bool = True) -> list[list[float]]: |
| 100 | 101 | """ |
| 101 | 102 | `Matrix4x4.CreatePerspectiveFieldOfView` with far = +infinity. |
| 102 | 103 | |
| 103 | | The engine passes `1 + FieldOfView % 179` degrees, aspect 1 and near 0.001 |
| 104 | | (Camera.cs:105). At far = inf the third column degenerates to M33 = -1 and |
| 105 | | M43 = -near. |
| 104 | At far = inf the third column degenerates to M33 = -1 and M43 = -near. |
| 105 | |
| 106 | `engine_fov_quirk` reproduces Camera.cs:105, which passes |
| 107 | `1 + FieldOfView % 179` rather than the angle itself. Documents are authored |
| 108 | against that behaviour so it is the default and must stay - but it mangles a |
| 109 | real focal length (a 50mm lens is ~39.6 degrees, which the quirk turns into |
| 110 | 40.6), so anything driving the camera from OUTSIDE a document, like the |
| 111 | Blender viewport, passes False and gets the angle it asked for. |
| 106 | 112 | """ |
| 107 | | fov = math.radians(1.0 + (fov_degrees % 179.0)) |
| 113 | fov = math.radians(1.0 + (fov_degrees % 179.0)) if engine_fov_quirk \ |
| 114 | else math.radians(fov_degrees) |
| 108 | 115 | y_scale = 1.0 / math.tan(fov * 0.5) |
| 109 | 116 | x_scale = y_scale / aspect |
| 110 | 117 | return [ |
| … | … | @@ -120,7 +127,8 @@ def matmul(a: list[list[float]], b: list[list[float]]) -> list[list[float]]: |
| 120 | 127 | for r in range(4)] |
| 121 | 128 | |
| 122 | 129 | |
| 123 | | def view_projection(position, forward, up, fov_degrees=60.0, near=0.001): |
| 130 | def view_projection(position, forward, up, fov_degrees=60.0, near=0.001, |
| 131 | engine_fov_quirk=True): |
| 124 | 132 | """ |
| 125 | 133 | The engine's view-projection, flattened in .NET's row-major memory order. |
| 126 | 134 | |
| … | … | @@ -130,7 +138,7 @@ def view_projection(position, forward, up, fov_degrees=60.0, near=0.001): |
| 130 | 138 | """ |
| 131 | 139 | target = (position[0] - forward[0], position[1] - forward[1], position[2] - forward[2]) |
| 132 | 140 | view = look_at(position, target, (-up[0], -up[1], -up[2])) |
| 133 | | combined = matmul(view, perspective(fov_degrees, 1.0, near)) |
| 141 | combined = matmul(view, perspective(fov_degrees, 1.0, near, engine_fov_quirk)) |
| 134 | 142 | return tuple(value for row in combined for value in row) |
| 135 | 143 | |
| 136 | 144 | |