No workaround — Beams visible

Workaround 1 — Looks fine to me

angle  = acos(view_vector.z)
diff   = abs(tan(angle) − tan(angle + 0.01)) // Width of an angular band of 0.01 radians
adjust = 1 / diff

Workaround 2 — Looks indistinguishable from workaround 1

diff   = abs(tan(angle) − tan(angle + 0.1)) // Width of an angular band of 0.1 radians
adjust = 1 / diff

Workaround 3 (bad)

vx       = view_vector.x / view_vector.z
vy       = view_vector.y / view_vector.z
radius   = sqrt(vx*vx + vy*vy)        // distance of the projected pixel from the view center
constant = 2 * π
area     = constant * radius*radius   // number of pixels inside this radius
area     = constant * (vx*vx + vy*vy) // faster way to calculate the same
adjust   = 1 / (1 + area)

Here the compensation is too powerful, and there are now horizontal and vertical beams instead of diagonal ones

Workaround 4 — Looks indistinguishable from workarounds 1 and 2

area     = vx*vx + vy*vy              // Ignore the constant
adjust   = 1 / (1 + area)             // Still don’t understand how to justify the 1 in “1 + ”, but it seems to work — why not 5? Why not 0.001?

Workaround 5 — Just testing

diff   = abs(tan(angle) − tan(angle + 10⁻⁶)) // Width of an angular band of 0.000001 radians
adjust = 1 / diff

Looks like the choice of the constant used in the tan calculation is not particularly important.

Conclusion

The weight for each sample can be calculated as follows:
Vz     = view_vector.z
angle  = acos(Vz)
band   = 0.0001                            // Radians. Any number between 10−5 and 10−3 seems to work fine
diff   = abs(tan(angle) − tan(angle+band)) // Width of an angular band
adjust = 1 / diff
power  = max(0, normal · view_vector)      // This is the cosine (Lambertian reflectance)
weight = power * adjust   // This is the adjusted cosine, accounting for edge skew caused by rectilinear projection
However, the following calculation produces identical results with much cheaper operations:
Vx     = view_vector.x
Vy     = view_vector.y
vx     = Vx / Vz
vy     = Vy / Vz
area   = vx*vx + vy*vy
adjust = 1 / (1 + area)
weight = power * adjust   // This is the adjusted cosine, accounting for edge skew caused by rectilinear projection
After asking Maxima to simplify (Vx/Vz)^2 + (Vy/Vz)^2, I discovered that the above calculation can be simplified to:
area = (Vx*Vx + Vy*Vy) / (Vz*Vz)           // Saves one division
and then, after asking it about 1 / (1 + (Vx*Vx+Vy*Vy)/(Vz*Vz)), I discovered this alternative:
adjust = Vz*Vz / (Vx*Vx + Vy*Vy + Vz*Vz)   // Saves two divisions
and because I knew that view_vector just happens to be a unit vector,
adjust = Vz*Vz                             // Saves all divisions
so the final result is:
power  = max(0, normal · view_vector)      // This is the cosine (Lambertian reflectance)
Vz     = view_vector.z
weight = power * Vz*Vz    // This is the adjusted cosine, accounting for edge skew caused by rectilinear projection
That is way simpler than I expected.

Why though?

Let’s simplify
adjust = 1 / abs(tan(acos(Vz)) − tan(acos(Vz)+band))
Why does this lead into the same result as this?
adjust = someconstant * Vz * Vz
According to definition…
acos(x) = atan(sqrt(1−x2) / x)
So,
tan(acos(x)) = sqrt(1−x2) / x
So the goal we want to figure out is:
abs(sqrt(1−x2) / x − sqrt(1−(x+band)2) / (x+band)) = someconstant * x2
What is the relationship between band and someconstant?
Copyright © Joel Yliluoma 2020