I have written a basic raytracer which keeps track of screen space. Each fragment has an associated pixel radius. When a ray dir hits the geometry when extruded by some distance from an eye, I compute the normal vector N for the hit, and combine it with four more rays. In pseudocode:
def distance := shortestDistanceToSurface(sdf, eye, dir, pixelRadius)
def p := eye + dir * distance
def N := estimateNormal(sdf, p)
def glance := distance * glsl.dot(dir, N)
def dx := (dirPX / glsl.dot(dirPX, N) - dirNX / glsl.dot(dirNX, N)) * glance
def dy := (dirPY / glsl.dot(dirPY, N) - dirNY / glsl.dot(dirNY, N)) * glance
Here, dirPX, dirNX, dirPY, and dirNY are rays which are offset by dir by the pixel radius in screen space in each of the four directions, but still aiming at the same reference point. This gives dx and dy, which are partial derivatives across the pixel indicating the rate at which the hit moves along the surface of the geometry as the rays move through screen space.
Because I track screen space, I can use pre-filtered samplers, as discussed by Inigo Quilez. They look great. However, now I want to add reflection (and refraction), which means that I need to recurse, and I'm not sure how to compute these rays and track screen space.
The essential problem is that, in order to figure out what color of light is being reflected at a certain place on the geometry, I need to not just take a point sample, but examine the whole screen space which is reflected. I can use the partial derivatives to give me four new points on the geometry which approximate an ellipse which is the projection of the original pixel from the screen:
def px := dx * pixelRadius
def py := dy * pixelRadius
def pPX := p + px
def pNX := p - px
def pPY := p + py
def pNY := p - py
And I can compute an approximate pixel radius by smushing the ellipse into a circle. I know that this ruins certain kinds of desirable anisotropic blur, but what's a raytracer without cheating?
def nextRadius := (glsl.length(dx) * glsl.length(dy)).squareRoot() * pixelRadius
However, I don't know where to reflect those points into the geometry; I don't know where to focus their rays. If I have to make a choice of focus, then it will be arbitrary, and depending on where the geometry reflects its own image, then this could arbitrarily blur or moiré the reflected images.
Do I need to take the second partial derivatives? I can approximate them just like the first derivatives, and then I can use them to adjust the normal N with slight changes, just like with the hit p. The normals then guide the focus of the ellipse, and map it to an approximate conic section. I'm worried about three things:
I worry about the cost of doing a couple extra vector additions and multiplications, which is probably negligible;
And also about whether the loss in precision, which is already really bad when doing these cheap derivatives, is going to be too lossy over multiple reflections;
And finally, how I'm supposed to handle situations where screen space explodes; when I have a mirrored sphere, how am I supposed to sample over big wedges of reflected space and e.g. average a checkerboard pattern into a grey?
And while it's not a worry, I simply don't know how to take four vectors and quickly fit a convincing cone to them, but this might be a mere problem of spending some time doing algebra on a whiteboard.
Edit: In John Amanatides' 1984 paper Ray Tracing with Cones, the curvature information is indeed computed, and used to fit an estimated cone onto the reflected ray. In Homan Igehy's 1999 paper Tracing Ray Differentials, only the first-order derivatives are used, and second derivatives are explicitly ignored.
Are there other alternatives, maybe? I've experimented with discarding the pixel radius after one reflection and just taking point samples, and they look horrible, with lots of aliasing and noise. Perhaps there is a field-of-view or depth-of-field approximation which can be computed on a per-material basis. As usual, multisampling can help, but I want an analytic solution so I don't waste so much CPU needlessly.
(sdf is a signed distance function and I am doing sphere tracing; the same routine both computes distance and also normals. glsl is the GLSL standard library.)
I won't accept my own answer, but I'll explain what I've done so that I can put this question down for now. I ended up going with an approach similar to Amanatides, computing a cone around each ray.
Each time I compute a normal vector, I also compute the mean curvature. Normal vectors are computed using a well-known trick. Let p be a zero of the SDF as in my question, let epsilon be a reasonable offset to use for numerical differentiation, and then let vp and vn be vectors whose components are evaluations of the SDF near p but perturbed at each component by epsilon. In pseudocode:
def justX := V(1.0, 0.0, 0.0)
def justY := V(0.0, 1.0, 0.0)
def justZ := V(0.0, 0.0, 1.0)
def vp := V(sdf(p + justX * epsilon), sdf(p + justY * epsilon), sdf(p + justZ * epsilon))
def vn := V(sdf(p - justX * epsilon), sdf(p - justY * epsilon), sdf(p - justZ * epsilon))
Now, by clever abuse of finite difference coefficients, we can compute both first and second derivatives at the same time. The third coefficient, sdf(p), is already zero by assumption. This gives our normal vector N, which is the gradient, and our mean curvature H, which is the Laplacian.
def N := glsl.normalize(vp - vn)
def H := sum(vp + vn) / (epsilon * epsilon)
We can estimate Gaussian curvature from mean curvature. While mean curvature tells our cone how much to expand or contract, Gaussian curvature is always non-negative and measures how much extra area (spherical excess) is added to the cone's area of intersection. Gaussian curvature is given with K instead of H, and after substituting:
def K := H * H
Now we're ready to adjust the computation of fragment width. Let's assume that, in addition to the pixelRadius in screen space and distance from the camera to the geometry, we also have dradius, the rate of change in pixel radius over distance. We can take a dot product to compute a glance factor just like before, and the trigonometry is similar:
def glance := glsl.dot(dir, N).abs().reciprocal()
def fradius := (pixelRadius + dradius * distance) * glance * (1.0 + K)
def fwidth := fradius * 2.0
And now we have fwidth just like in GLSL. Finally, when it comes time to reflect, we'll want to adjust the change in radius by integrating our second-derivative curvature into our first derivative:
def dradiusNew := dradius + H * fradius
The results are satisfactory. The fragments might be a little too big; it's hard to tell if something is overly blurry or just properly antialiased. I wonder whether Amanatides used a different set of equations to handle curvature; I spent far too long at a whiteboard deriving what ended up being pleasantly simple operations.
This image was not supersampled; each pixel had one fragment with one ray and one cone.
Related
I am dealing with clipping of quadratic Beziér curves. Clipping is a standard graphics task. Typically, no matter what we display on a screen, we only want to render the part that fits into the screen bounds, as an optimization.
For straight lines, there is something called Cohen-Sutherland algorithm, and a slightly extended version of this algorithm is the Sutherland–Hodgman algorithm, where the first solution is for dealing with lines and the second one for polygons.
Essentially, the algorithms split the computer screen into tik-tac-toe -like squares, where the central square is what fits on the screen, and we special case each of left/right and above/below. After, when one end of the line is right off the screen and the other is not, we replace the x coordinate for this point with the screen's max value of x, and calculate the y value for it. This becomes the new endpoint of the clipped line. Pretty simple and it works well.
With Beziér curves, the same approach can be taken, only in addition to the ends, we need to consider control points. In the case of a quadratic curve, there is only one control.
To clip the curve, we can do something very similar to Cohen-Sutherland. Only, depending on the situation, we might need to cut the original curve into up to five (5) pieces. Just like both ends of a straight line might be offscreen, while the center is visible, the same situation needs to be handled with curves, yet here we only need to deal with the hull [height] of the curve causing a mid-section to be invisible. Therefore, we might end up with two new curves, after the clipping.
Finding one of the coordinates for these curves is pretty easy. It is still the min/max coordinate for one of the axis, and the value of the other coordinate. There is prior art for this, for example even calculate x for y is a good starting point. We want to adopt the formula so vectors turn into separate x and y coordinates, but the rest is doable.
Next, however, we still have an unsolved problem these one or two new curves, are completely new quadratic curves and each will have therefore a new control point.
There is a thread at split quadratic curve into two where the author seems to be doing kind of what I need, albeit in a slightly different way. There is an accepted answer, yet I could not get the results to work.
I want to end-up with a function like:
function clipSegment(sx, sy, cx, cy, ex, ey, bounds) {
let curves: {sx, sy, cx, cy, ex, ey}[] = [];
...
return curves;
}
It should take coordinates and the bounds object that would have both min and max for both x and y coordinates. I think that Cohen-Sutherland approach with squares and bit-codes should work here just as well. We get more cases for curves, but everything is doable. My problem is the new control point coordinates. For example, we could calculating t from one of the coordinates, doing something like:
function getQuadraticPoint(t, sx, sy, cp1x, cp1y, ex, ey) {
const x = (1 - t) * (1 - t) * sx + 2 * (1 - t) * t * cp1x + t * t * ex;
const y = (1 - t) * (1 - t) * sy + 2 * (1 - t) * t * cp1y + t * t * ey;
return { x, y };
}
Once we have the new start and/or beginning, how do we get the new control points?
Some developers I found online, working on similar problems, recommended just working with t and changing the interval from t from 0 to 1 to 0 to t. This however won't work easily for Canvas 2D API. The 2D Path thing needs the control point and the end point [after the pen move to the beginning with moveTo].
I believe that the quadratic Beziér case should have a closed-form solution. Yet, I have not figured out what it is. Any ideas?
I'm interested in a fast way to calculate the rotation-independent center of a simple, convex, (non-intersecting) 2D polygon.
The example below (on the left) shows the mean center (sum of all points divided by the total), and the desired result on the right.
Some options I've already considered.
bound-box center (depends on rotation, and ignores points based on their relation to the axis).
Straight skeleton - too slow to calculate.
I've found a way which works reasonably well, (weight the points by the edge-lengths) - but this means a square-root call for every edge - which I'd like to avoid.(Will post as an answer, even though I'm not entirely satisfied with it).
Note, I'm aware of this questions similarity with:What is the fastest way to find the "visual" center of an irregularly shaped polygon?
However having to handle convex polygons increases the complexity of the problem significantly.
The points of the polygon can be weighted by their edge length which compensates for un-even point distribution.
This works for convex polygons too but in that case the center point isn't guaranteed to be inside the polygon.
Psudo-code:
def poly_center(poly):
sum_center = (0, 0)
sum_weight = 0.0
for point in poly:
weight = ((point - point.next).length +
(point - point.prev).length)
sum_center += point * weight
sum_weight += weight
return sum_center / sum_weight
Note, we can pre-calculate all edge lengths to halve the number of length calculations, or reuse the previous edge-length for half+1 length calculations. This is just written as an example to show the logic.
Including this answer for completeness since its the best method I've found so far.
There is no much better way than the accumulation of coordinates weighted by the edge length, which indeed takes N square roots.
If you accept an approximation, it is possible to skip some of the vertices by curve simplification, as follows:
decide of a deviation tolerance;
start from vertex 0 and jump to vertex M (say M=N/2);
check if the deviation along the polyline from 0 to M exceeds the tolerance (for this, compute the height of the triangle formed by the vertices 0, M/2, M);
if the deviation is exceeded, repeat recursively with 0, M/4, M/2 and M/2, 3M/4, M;
if the deviation is not exceeded, assume that the shape is straight between 0 and M.
continue until the end of the polygon.
Where the points are dense (like the left edge on your example), you should get some speedup.
I think its easiest to do something with the center of masses of the delaunay triangulation of the polygon points. i.e.
def _centroid_poly(poly):
T = spatial.Delaunay(poly).simplices
n = T.shape[0]
W = np.zeros(n)
C = 0
for m in range(n):
sp = poly[T[m,:],:]
W[m] = spatial.ConvexHull(sp).volume
C += W[m] +np.mean(sp, axis = 0)
return C / np.sum(W)
This works well for me!
Given three co-planar (2D) points (X1, Y1), (X2, Y2), and (X3, Y3), which represent (respectively ...) "1=where I was, 2=where I am, and 3=where I am going," I need a simple algorithm that will tell me, e.g.
Veer to the right
Make a slight left turn
Turn to the left
In other words, (a) is the turn to the left or to the right; and (b) how sharp is the turn (letting me be arbitrary about this).
For the first part, I've already learned about how to use (see wikipedia: Graham Scan, and question #26315401 here) cross-product to determine whether the turn is to the left or to the right, based on whether the path is counterclockwise.
And, I'm sure that ATAN2() will be at the core of determining how sharp the turn is.
But I can't .. quite .. wrap my head around the proper math which will work in all orientations. (Especially when the angle crosses the zero-line. (A bearing of 350 degrees to 10 degrees is a gap of 20 degrees, not 340, etc.)
Okay, I'm tired. [... of bangin' my head against the wall this mornin'.] "Every time I think I've got it, I'm not sure." So, okay, it's time to ask ... :-)
When you calculate direction change angles with Atan2, don't bother about absolute angles. You have not to calculate two bearings and subtract them - Atan2 can give you relative angle between the first and the second vectors in range -Pi..Pi (-180..180) (range might depend on programming language).
x12 = x2-x1
y12 = y2-y1
x23 = x3-x2
y23 = y3-y2
DirChange = Atan2(x12*y23-x23*y12, x12*x23+y12*y23)
Some explanations: we can calculate sine of vector-vector angle through cross product and vector norms (|A| = Sqrt(A.x*A.x + A.y*A.y)):
Sin(A_B) = (A x B) / (|A|*|B|)
and cosine of vector-vector angle through dot (scalar) product and vector norms:
Cos(A_B) = (A * B) / (|A|*|B|)
Imagine that Atan2 calculates angle with sine and cosine of this angle, excluding common denominator (product of norms)
A_B = Atan2(Sin(A_B), Cos(A_B))
Example in Delphi:
var
P1, P2, P3: TPoint;
x12, y12, x23, y23: Integer;
DirChange: Double;
begin
P1 := Point(0, 0);
P2 := Point(1, 0);
P3 := Point(2, 1);
x12 := P2.X - P1.X;
y12 := P2.Y - P1.Y;
x23 := P3.X - P2.X;
y23 := P3.Y - P2.Y;
DirChange := Math.ArcTan2(x12 * y23 - x23 * y12, x12 * x23 + y12* y23);
Memo1.Lines.Add(Format('%f radians %f degrees',
[DirChange, RadToDeg(DirChange)]));
Output:
0.79 radians 45.00 degrees (left turn)
for your example data set (1,1), (3,2), and (6,3)
-0.14 radians -8.13 degrees (right turn)
EDITED - THE FOLLOWING IS WRONG. My ORIGINAL response was as follows ...
I'm not coming up with the expected answers when I try to use your response.
Let's say the points are: (1,1), (3,2), and (6,3). A gentle right turn.
Using a spreadsheet, I come up with: X12=2, X23=3, Y12=1, Y23=3, and the ATAN2 result (in degrees) is 101.3. A very sharp turn of more than 90 degrees. The spreadsheet formula (listing X1,Y1,X2,Y2,X3,Y3,X12,X23,Y12,Y23 and answer) on row 2, is:
=DEGREES(ATAN2(G2*J2-H2*I2; G2*I2+H2*J2))
(The spreadsheet, OpenOffice, lists "X" as the first parameter to ATAN2.)
Are you certain that the typo is on my end?
AND, AS A MATTER OF FACT (SO TO SPEAK), "YES, IT WAS!"
(Heh, I actually had said it myself. It just didn't occur to me to, like, duh, swap them already.)
My spreadsheet's version of the ATAN2 function specifies the X parameter first. Most programming languages (Delphi, Perl, PHP ...) specify Y first, and that's how the (correct!) answer was given.
When I edited the formula, reversing the parameters to meet the spreadsheet's definition, the problem went away, and I was able to reproduce the values from the (edited) reply. Here's the corrected formula, with the parameters reversed:
=DEGREES(ATAN2(G2*I2+H2*J2; G2*J2-H2*I2))
^^== X ==^^ ^^== Y ==^^
Again, this change in formula was needed because this spreadsheet's implementation of ATAN2 is backwards from that of most programming language implementations. The answer that was originally given, which lists Y first, is correct for most programming languages.
Well, I seem to still be having a bit of a problem . . .
If the points are very far away from each other in nearly a straight line, I am coming up with "large angles." Example:
P1: (0.60644,0.30087) ..
P2: (0.46093,0.30378) ..
P3: (0.19335,0.30087)
The X-coordinate increases but the Y-coordinate remains almost the same.
x12=-0.145507 .. y12=-0.00290698
x23=-0.267578125 .. y23=0.002906976
(I inverted the Y differences because the coordinates are in quadrant-IV, where Y increases downward.)
x=-0.000354855 .. y=-0.00120083
ans= -106.462 (degrees)
Since the points are very nearly co-linear, I expected the answer to be very small. As you can see, it's more than 106 degrees.
I'm trying to find the best way to get the most distant point of a circle from a specified point in 2D space. What I have found so far, is how to get the distance between the point and the circle position, but I'm not entirely sure how to expand this to find the most distant point of the circle.
The known variables are:
Point a
Point b (circle position)
Radius r (circle radius)
To find the distance between the point and the circle position, I have found this:
xd = x2 - x1
yd = y2 - y1
Distance = SquareRoot(xd * xd + yd * yd)
It seems to me, this is part of the solution. How would this be expanded to get the position of Point x in the below image?
As an additional but optional part of the question: I have read in some places that it would be possible to get the distance portion without using the Square Root, which is very performance intensive and should be avoided if fast code is necessary. In my case, I would be doing this calculation quite often; Any comments on this within the context of the main question would be welcome too.
What about this?
Calculate A-B.
We now have a vector pointing from the center of the circle towards A (if B is the origin, skip this and just consider point A a vector).
Normalize.
Now we have a well defined length (the length is 1)
If the circle is not of unit radius, multiply by radius. If it is unit radius, skip this.
Now we have the correct length.
Invert sign (can be done in one step with 3., just multiply with the negative radius)
Now our vector points in the correct direction.
Add B (if B is the origin, skip this).
Now our vector is offset correctly so its endpoint is the point we want.
(Alternatively, you could calculate B-A to save the negation, but then you have to do one more operation to offset the origin correctly.)
By the way, it works the same in 3D, except the circle would be a sphere, and the vectors would have 3 components (or 4, if you use homogenous coords, in this case remember -- for correctness -- setting w to 0 when "turning points into vectors" and to 1 at the end when making a point from the vector).
EDIT:
(in reply of pseudocode)
Assuming you have a vec2 class which is a struct of two float numbers with operators for vector subtraction and scalar multiplicaion (pretty trivial, around a dozen lines of code) and a function normalize which needs to be no more than a shorthand for multiplying with inv_sqrt(x*x+y*y), the pseudocode (my pseudocode here is something like a C++/GLSL mix) could look something like this:
vec2 most_distant_on_circle(vec2 const& B, float r, vec2 const& A)
{
vec2 P(A - B);
normalize(P);
return -r * P + B;
}
Most math libraries that you'd use should have all of these functions and types built-in. HLSL and GLSL have them as first type primitives and intrinsic functions. Some GPUs even have a dedicated normalize instruction.
Since I was 13 and playing around with AMOS 3D I've been wanting to learn how to code 3D graphics. Now, 10 years later, I finally think I have have accumulated enough maths to give it a go.
I have followed various tutorials, and defined screenX (and screenY, equivalently) as
screenX = (pointX * cameraX) / distance
(Plus offsets and scaling.)
My problem is with what the distance variable actually refers to. I have seen distance being defined as the difference in z between the camera and the point. However, that cannot be completely right though, since x and y have the same effect as z on the actual distance from the camera to the point. I implemented distance as the actual distance, but the result gives a somewhat skewed perspective, as if it had "too much" perspective.
My "actual distance" implementation was along the lines of:
distance = new Vector(pointX, pointY, cameraZ - pointZ).magnitude()
Playing around with the code, I added an extra variable to my equation, a perspectiveCoefficient as follows:
distance = new Vector(pointX * perspectiveCoefficient,
pointY * perspectiveCoefficient, cameraZ - pointZ).magnitude()
For some reason, that is beyond me, I tend to get the best result setting the perspectiveCoefficient to 1/sqrt(2).
My 3D test cube is at http://vega.soi.city.ac.uk/~abdv866/3dcubetest/3dtest.svg. (Tested in Safari and FF.) It prompts you for a perspectiveCoefficient, where 0 gives a perspective without taking x/y distance into consideration, and 1 gives you a perspective where x, y and z distance is equally considered. It defaults to 1/sqrt(2). The cube can be rotated about x and y using the arrow keys. (For anyone interested, the relevant code is in update() in the View.js file.)
Grateful for any ideas on this.
Usually, projection is done on the Z=0 plane from an eye position behind this plane. The projected point is the intersection of the line (Pt,Eye) with the Z=0 plane. At the end you get something like:
screenX = scaling * pointX / (1 + pointZ/eyeDist)
screenY = scaling * pointY / (1 + pointZ/eyeDist)
I assume here the camera is at (0,0,0) and eye at (0,0,-eyeDist). If eyeDist becomes infinite, you obtain a parallel projection.