OverviewFilesCommitsIssuesDocsReleasesPeers

ifsrenderer-linux commits / 434e6d8

Composite over the viewport, and stop mangling the camera FOV
Two bugs from the first live-viewport pass, both reported from actually using it. **The 3D viewport went blank.** The tonemap drew an OPAQUE fullscreen quad, so it painted over the grid, the objects and the gizmos - "normal viewport doesn't work" is exactly what that looks like. Alpha is now the tonemapped density, so empty space is transparent and the fractal composites over the scene with ALPHA blending, restored to NONE afterwards so the viewport's state is left as found. **The camera FOV was wrong.** `perspective()` unconditionally applied Camera.cs's `1 + FieldOfView % 179`, which is right for documents authored against it and wrong for a real focal length: a 50mm lens is 39.60 degrees and the quirk rendered it as 40.60. The quirk is now opt-out and stays the default, since documents depend on it; the viewport passes engine_fov_quirk=False and gets the angle it asked for. Verified round-tripping 35/50/85mm - implied FOV now equals the input exactly, where before every one was a degree off. The horizontal angle is the one the kernel wants, because its projection uses aspect 1 and applies width/height itself. Verified the shaders still compile and that gpu.state.blend_set exists and round-trips. Whether the composite reads well over a lit scene is a judgement call that needs a real viewport. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
434e6d8fa5f68be22ae4da477401721202a20aad
by plankatron <fleeky@gmail.com> · 2026-08-16T23:00:13.000Z

Changed files (2) +27 -10

blender/ifs_render.py +15 -7
……@@ -96,15 +96,22 @@ def look_at(position, target, up) -> list[list[float]]:
9696 ]
9797
9898
99def perspective(fov_degrees: float, aspect: float, near: float) -> list[list[float]]:
99def perspective(fov_degrees: float, aspect: float, near: float,
100 engine_fov_quirk: bool = True) -> list[list[float]]:
100101 """
101102 `Matrix4x4.CreatePerspectiveFieldOfView` with far = +infinity.
102103
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.
106112 """
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)
108115 y_scale = 1.0 / math.tan(fov * 0.5)
109116 x_scale = y_scale / aspect
110117 return [
……@@ -120,7 +127,8 @@ def matmul(a: list[list[float]], b: list[list[float]]) -> list[list[float]]:
120127 for r in range(4)]
121128
122129
123def view_projection(position, forward, up, fov_degrees=60.0, near=0.001):
130def view_projection(position, forward, up, fov_degrees=60.0, near=0.001,
131 engine_fov_quirk=True):
124132 """
125133 The engine's view-projection, flattened in .NET's row-major memory order.
126134
……@@ -130,7 +138,7 @@ def view_projection(position, forward, up, fov_degrees=60.0, near=0.001):
130138 """
131139 target = (position[0] - forward[0], position[1] - forward[1], position[2] - forward[2])
132140 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))
134142 return tuple(value for row in combined for value in row)
135143
136144
blender/ifs_viewport.py +12 -3
……@@ -65,8 +65,12 @@ void main()
6565 vec4 h = texelFetch(accum_tex, c, 0);
6666 float peak = max(float(texelFetch(max_tex, ivec2(0, 0), 0).x), 1.0);
6767 float d = max(h.w, 1e-12);
68 float s = log(1.0 + brightness * h.w / peak) / log(10.0) / d;
69 fragColor = vec4(clamp(h.rgb * s, 0.0, 1.0), 1.0);
68 // Alpha IS the tonemapped density, so empty space is transparent and the
69 // fractal COMPOSITES over the viewport - grid, objects and gizmos stay visible.
70 // An opaque quad here blanks the whole 3D view.
71 float lum = log(1.0 + brightness * h.w / peak) / log(10.0);
72 float s = lum / d;
73 fragColor = vec4(clamp(h.rgb * s, 0.0, 1.0), clamp(lum, 0.0, 1.0));
7074}
7175"""
7276
……@@ -239,10 +243,13 @@ class Session:
239243 eye = world.translation
240244 forward = -Vector(world.col[2][:3]) # Blender cameras look down -Z
241245 up = Vector(world.col[1][:3])
246 # Horizontal FOV from the viewport's focal length against Blender's 36mm
247 # sensor. The kernel's projection uses aspect 1 and applies width/height
248 # itself, so the horizontal angle is the one it wants.
242249 fov = math.degrees(2.0 * math.atan(18.0 / max(lens, 1.0)))
243250 return ifs_render.view_projection(
244251 (eye.x, eye.y, eye.z), (forward.x, forward.y, forward.z),
245 (up.x, up.y, up.z), fov_degrees=fov)
252 (up.x, up.y, up.z), fov_degrees=fov, engine_fov_quirk=False)
246253
247254 # ---------------------------------------------------------------------- draw
248255
……@@ -306,7 +313,9 @@ class Session:
306313 self.tonemap.uniform_sampler('max_tex', self.maxbuf)
307314 self.tonemap.uniform_int('out_size', (self.width, self.height))
308315 self.tonemap.uniform_float('brightness', self.brightness)
316 gpu.state.blend_set('ALPHA')
309317 self.batch.draw(self.tonemap)
318 gpu.state.blend_set('NONE') # leave the viewport's state as we found it
310319
311320 self.frames += 1
312321 # Keep animating: without this the viewport only refines when nudged.