Skip to content

Fix negative-focal-length lens reflection; use ideal ray-slope thin-lens law (#102)#106

Merged
beneaze merged 2 commits into
mainfrom
fix/negative-focal-length-lens-reflection
Jul 10, 2026
Merged

Fix negative-focal-length lens reflection; use ideal ray-slope thin-lens law (#102)#106
beneaze merged 2 commits into
mainfrom
fix/negative-focal-length-lens-reflection

Conversation

@beneaze

@beneaze beneaze commented Jul 10, 2026

Copy link
Copy Markdown
Member

Fixes #102.

Problem

Entering a negative focal length on a lens made it act as a mirror — rays bounced back toward the source instead of transmitting and diverging.

Root cause & fix

LensElement deflected rays with the angle form θ_out = θ_in − arctan(y/f) (via atan2). This both (a) sent rays backward for f < 0, and (b) is not actually the correct ideal-lens law.

This PR switches to the ray-slope law used by the web engine:

tan(θ_out) = tan(θ_in) − y/f     # exit direction = normalize(normal + slope·tangent)

Why this is the right law, not just a patch:

  • Correct ideal lens. A parallel bundle at any incidence angle now focuses to a single point in the focal plane at f·tan(θ_in), for every ray height y. The old arctan form only focused on-axis bundles and smeared off-axis ones (~4 mm spread at 17° incidence → now 0).
  • Never reflects. The exit direction is built as normal·1 + tangent·slope, so its forward component is always positive for either sign of f. This fully resolves [Bug report] Negative focal length lenses reflecting instead of diverging rays #102 and removes an extreme-grazing backward-ray artifact the arctan form could still produce.
  • Desktop ↔ web parity. Now byte-identical (2e-16) to optiverse-web's GeometricEngine.thinLens / optiverse_engine._thin_lens across a grid of (f, θ_in, y). Converging-lens behavior at normal incidence is unchanged.

transform_q derives its exit direction from the same slope law, so the Gaussian ABCD matches the web's GeometricEngine.lensQ.

Verification

  • New test test_oblique_parallel_bundle_focuses_to_point locks in the f·tanθ focusing property (fails on the old arctan form).
  • test_negative_focal_length_diverges_forward ([Bug report] Negative focal length lenses reflecting instead of diverging rays #102 regression): -50 mm lens transmits forward and diverges.
  • Engine-level trace (source → -50 mm lens): all rays forward, zero backward.
  • Cross-engine grid comparison: desktop == web to machine epsilon.
  • All tests/raytracing/ pass; ruff + mypy clean.

optiverse-web

No port needed — the web engine already uses this ray-slope law; this PR brings the desktop into agreement with it.

beneaze and others added 2 commits July 10, 2026 11:32
The thin-lens deflection used the two-argument math.atan2(y, f) to
compute the exact focusing angle. For f < 0 (diverging lenses), atan2
returns an angle in the rear hemisphere, so theta_out landed in the
backward-facing cone and the reconstructed direction pointed opposite
the propagation direction — the lens behaved like a mirror.

Switch to the single-argument math.atan(y / f), matching the documented
formula theta_out = theta_in - arctan(y/f). This keeps the deflection in
(-pi/2, pi/2) for either sign of f, so a negative-focal-length lens stays
transmissive and diverges the beam (virtual focus on the incident side at
|f|). Applied the same fix to the Gaussian-beam q transform for parity.

Adds a regression test tracing an off-axis collimated ray through a
-50mm lens: it must transmit forward and diverge with slope |y/f|.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gine

Replaces the arctan angle form (θ_out = θ_in − arctan(y/f)) with the
ray-slope law tan(θ_out) = tan(θ_in) − y/f, building the exit direction as
normalize(normal + slope·tangent).

Why this is the correct ideal-lens law, not just a fix:
- A parallel bundle at ANY incidence angle now focuses to a single point in
  the focal plane at f·tan(θ_in), for every ray height y. The arctan form
  only focused on-axis bundles and smeared off-axis ones (verified: ~4 mm
  spread at 17°, now 0).
- The exit direction's forward component is always positive, so the lens is
  structurally transmissive for either sign of f. This fully resolves #102
  (negative-f reflection) and also removes the extreme-grazing backward-ray
  artifact the arctan form could still produce.
- Byte-identical (2e-16) to the web tracers across a grid of (f, θ_in, y),
  so desktop and web now agree. Converging-lens behavior at normal incidence
  is unchanged.

transform_q derives its exit direction from the same slope law so the
Gaussian ABCD matches web GeometricEngine.lensQ. Drops the now-unused math
import.

Adds a regression test asserting an oblique parallel bundle focuses to one
point; the #102 test now documents the slope-law rationale.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@beneaze beneaze changed the title Fix negative focal length lens reflecting instead of diverging (#102) Fix negative-focal-length lens reflection; use ideal ray-slope thin-lens law (#102) Jul 10, 2026
@beneaze
beneaze merged commit 624eba2 into main Jul 10, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug report] Negative focal length lenses reflecting instead of diverging rays

1 participant