Photon Engine 2.0.0-beta
A physically based renderer.
Loading...
Searching...
No Matches
TSphere.ipp
Go to the documentation of this file.
1#pragma once
2
4#include "Math/constant.h"
6#include "Math/TVector2.h"
7#include "Math/TMatrix2.h"
8
9#include <Common/assertion.h>
10
11#include <cmath>
12#include <algorithm>
13#include <type_traits>
14
15namespace ph::math
16{
17
18template<typename T>
20{
21 return TSphere(1);
22}
23
24template<typename T>
25inline TSphere<T>::TSphere(const T radius) :
26 m_radius(radius)
27{
28 PH_ASSERT_GE(radius, T(0));
29}
30
31template<typename T>
33 const TLineSegment<T>& segment,
34 real* const out_hitT) const
35{
36 //return isIntersectingNaive(segment, out_hitT);
37 return isIntersectingHearnBaker(segment, out_hitT);
38}
39
40//template<typename T>
41//inline bool TSphere<T>::isIntersectingRefined(
42// const TLineSegment<T>& segment,
43// T* const out_hitT) const
44//{
45// const auto segmentOriginToCentroid = math::TVector3<T>(0) - segment.getOrigin();
46// const auto approxHitT = segmentOriginToCentroid.dot(segment.getDirection());
47//
48// const TLineSegment<T> shiftedSegment(
49// segment.getPoint(approxHitT),
50// segment.getDirection(),
51// segment.getMinT() - approxHitT,
52// segment.getMaxT() - approxHitT);
53// if(!isIntersecting(shiftedSegment, out_hitT))
54// {
55// return false;
56// }
57//
58// *out_hitT += approxHitT;
59// return true;
60//}
61
62template<typename T>
63inline bool TSphere<T>::isInside(const TVector3<T>& point) const
64{
65 // Checks whether the point is within radius (sphere center = origin)
66 return point.lengthSquared() < math::squared(m_radius);
67}
68
69template<typename T>
71 const TLineSegment<T>& segment,
72 real* const out_hitT) const
73{
74 PH_ASSERT_GE(m_radius, T(0));
75 PH_ASSERT(out_hitT);
76
77 // Construct a ray/line then use its parametric form to test against the sphere and solve for t
78 //
79 // ray origin: o
80 // ray direction: d
81 // sphere center: c
82 // sphere radius: r
83 // intersection point: p
84 // vector dot: *
85 // ray equation: o + td (t is a scalar variable)
86 //
87 // To find the intersection point, the length of vector (td - oc) must equals r.
88 // This is equivalent to (td - oc)*(td - oc) = r^2. After reformatting, we have
89 //
90 // t^2(d*d) - 2t(d*oc) + (oc*oc) - r^2 = 0 --- (1)
91 //
92 // Solving equation (1) for t will yield the intersection point (o + td).
93
94 // FIXME: T may be more precise than float64
95
96 const Vector3D rayO(segment.getOrigin());
97 const Vector3D rayD(segment.getDir());
98
99 // Vector from ray origin (o) to sphere center (c)
100 const Vector3D oc = Vector3D(0).sub(rayO);
101
102 // a in equation (1)
103 const float64 a = rayD.dot(rayD);
104
105 // b in equation (1) (-2 is cancelled while solving t)
106 const float64 b = rayD.dot(oc);
107
108 // c in equation (1)
109 const float64 c = oc.dot(oc) - static_cast<float64>(m_radius) * m_radius;
110
111 float64 D = b * b - a * c;
112 if(D < 0.0)
113 {
114 return false;
115 }
116 else
117 {
118 D = std::sqrt(D);
119
120 const float64 rcpA = 1.0 / a;
121
122 // t = (b +- D) / a
123 // t1 must be <= t2 (rcpA & D always > 0)
124 const float64 t1 = (b - D) * rcpA;
125 const float64 t2 = (b + D) * rcpA;
126
127 PH_ASSERT_MSG(t1 <= t2, "\n"
128 "t1 = " + std::to_string(t1) + "\n"
129 "t2 = " + std::to_string(t2) + "\n"
130 "(a, b, c) = (" + std::to_string(a) + ", " + std::to_string(b) + ", " + std::to_string(c) + ")\n"
131 "ray-origin = " + rayO.toString() + "\n"
132 "ray-direction = " + rayD.toString());
133
134 // We now know that the ray/line intersects the sphere, but it may not be the case for the line
135 // segment. We test all cases with t1 <= t2 being kept in mind:
136
137 float64 tClosest;
138
139 // t1 is smaller than t2, we test t1 first
140 if(segment.getMinT() <= t1 && t1 <= segment.getMaxT())
141 {
142 tClosest = t1;
143 }
144 // test t2
145 else if(segment.getMinT() <= t2 && t2 <= segment.getMaxT())
146 {
147 tClosest = t2;
148 }
149 // no intersection found in [t_min, t_max] (NaN aware)
150 else
151 {
152 return false;
153 }
154
155 PH_ASSERT_IN_RANGE_INCLUSIVE(tClosest, segment.getMinT(), segment.getMaxT());
156
157 *out_hitT = static_cast<real>(tClosest);
158 return true;
159 }
160}
161
162// References:
163// [1] Ray Tracing Gems Chapter 7: Precision Improvements for Ray/Sphere Intersection
164// [2] Best reference would be code for chapter 7 with method 5 (our line direction is not normalized)
165// https://github.com/Apress/ray-tracing-gems/
166// [3] https://github.com/NVIDIAGameWorks/Falcor (Source/Falcor/Utils/Geometry/IntersectionHelpers.slang)
167//
168template<typename T>
169inline bool TSphere<T>::isIntersectingHearnBaker(
170 const TLineSegment<T>& segment,
171 real* const out_hitT) const
172{
173 PH_ASSERT_GE(m_radius, T(0));
174 PH_ASSERT(out_hitT);
175
176 // We use the same notation as `isIntersectingNaive()` for the geometries
177
178 const Vector3D rayO(segment.getOrigin());
179 const Vector3D rayD(segment.getDir());
180 const Vector3D unitRayD = rayD.normalize();
181
182 // Vector from sphere center (c) to ray origin (o)
183 // (vector f in Ray Tracing Gems, note this is the negated version of oc in `isIntersectingNaive()`)
184 const Vector3D co = rayO;// rayO - Vector3D(0);
185
186 const float64 r2 = static_cast<float64>(m_radius) * m_radius;
187
188 // a in equation (1) in `isIntersectingNaive()`
189 // (same as in RT Gems)
190 const float64 a = rayD.dot(rayD);
191
192 // b term in RT Gems eq. 8 without the negative sign (handled in q later to save one negation)
193 const float64 b = co.dot(rayD);
194
195 // vector fd in RT Gems eq. 5 (curvy_L^2)
196 const Vector3D fd = co - unitRayD * co.dot(unitRayD);
197
198 // Basically the discriminant term in RT Gems eq. 5 with 4s and 2s eliminated
199 const float64 D = a * (r2 - fd.dot(fd));
200 if(D < 0.0)
201 {
202 return false;
203 }
204 else
205 {
206 // c in equation (1) in `isIntersectingNaive()`
207 // (same as in RT Gems)
208 const float64 c = co.dot(co) - r2;
209
210 const float64 sqrtD = std::sqrt(D);
211
212 // q term in RT Gems eq. 10
213 const float64 q = (b >= 0.0) ? -sqrtD - b : sqrtD - b;
214
215 // t0 & t1 term in RT Gems eq. 10 (here we let t0 <= t1)
216 // (unlike `isIntersectingNaive()`, we cannot determine which one is smaller due to its formulation)
217 float64 t0 = c / q;
218 float64 t1 = q / a;
219 if(t0 > t1)
220 {
221 std::swap(t0, t1);
222 }
223
224 PH_ASSERT_MSG(t0 <= t1, "\n"
225 "t0 = " + std::to_string(t0) + "\n"
226 "t1 = " + std::to_string(t1) + "\n"
227 "(a, b, c) = (" + std::to_string(a) + ", " + std::to_string(b) + ", " + std::to_string(c) + ")\n"
228 "ray-origin = " + rayO.toString() + "\n"
229 "ray-direction = " + rayD.toString());
230
231 // We now know that the ray/line intersects the sphere, but it may not be the case for the line
232 // segment. We test all cases with t0 <= t1 being kept in mind:
233
234 float64 tClosest;
235
236 // t0 is smaller than t1, we test t0 first
237 if(segment.getMinT() <= t0 && t0 <= segment.getMaxT())
238 {
239 tClosest = t0;
240 }
241 // test t1
242 else if(segment.getMinT() <= t1 && t1 <= segment.getMaxT())
243 {
244 tClosest = t1;
245 }
246 // no intersection found in [t_min, t_max] (NaN aware)
247 else
248 {
249 return false;
250 }
251
252 PH_ASSERT_IN_RANGE_INCLUSIVE(tClosest, segment.getMinT(), segment.getMaxT());
253
254 *out_hitT = static_cast<real>(tClosest);
255 return true;
256 }
257}
258
259template<typename T>
260inline T TSphere<T>::getRadius() const
261{
262 return m_radius;
263}
264
265template<typename T>
266inline T TSphere<T>::getArea() const
267{
268 return constant::four_pi<T> * m_radius * m_radius;
269}
270
271template<typename T>
273{
274 constexpr auto EXPANSION_RATIO = T(0.0001);
275
276 return TAABB3D<T>(
277 TVector3<T>(-m_radius, -m_radius, -m_radius),
278 TVector3<T>( m_radius, m_radius, m_radius))
279 .expand(TVector3<T>(EXPANSION_RATIO * m_radius));
280}
281
282template<typename T>
283inline bool TSphere<T>::mayOverlapVolume(const TAABB3D<T>& volume) const
284{
285 // Intersection test for solid box and hollow sphere.
286 // Reference: Jim Arvo's algorithm in Graphics Gems 2
287
288 const T radius2 = math::squared(m_radius);
289
290 // These variables are gonna store minimum and maximum squared distances
291 // from the sphere's center to the AABB volume.
292 T minDist2 = T(0);
293 T maxDist2 = T(0);
294
295 T a, b;
296
297 a = math::squared(volume.getMinVertex().x());
298 b = math::squared(volume.getMaxVertex().x());
299 maxDist2 += std::max(a, b);
300 if (T(0) < volume.getMinVertex().x()) minDist2 += a;
301 else if(T(0) > volume.getMaxVertex().x()) minDist2 += b;
302
303 a = math::squared(volume.getMinVertex().y());
304 b = math::squared(volume.getMaxVertex().y());
305 maxDist2 += std::max(a, b);
306 if (T(0) < volume.getMinVertex().y()) minDist2 += a;
307 else if(T(0) > volume.getMaxVertex().y()) minDist2 += b;
308
309 a = math::squared(volume.getMinVertex().z());
310 b = math::squared(volume.getMaxVertex().z());
311 maxDist2 += std::max(a, b);
312 if (T(0) < volume.getMinVertex().z()) minDist2 += a;
313 else if(T(0) > volume.getMaxVertex().z()) minDist2 += b;
314
315 return minDist2 <= radius2 && radius2 <= maxDist2;
316}
317
318template<typename T>
319inline TVector3<T> TSphere<T>::sampleToSurfaceArchimedes(const std::array<T, 2>& sample) const
320{
321 PH_ASSERT_IN_RANGE_INCLUSIVE(sample[0], static_cast<T>(0), static_cast<T>(1));
322 PH_ASSERT_IN_RANGE_INCLUSIVE(sample[1], static_cast<T>(0), static_cast<T>(1));
323 PH_ASSERT_GE(m_radius, static_cast<T>(0));
324
325 const T y = static_cast<T>(2) * (sample[0] - static_cast<T>(0.5));
326 const T phi = constant::two_pi<T> * sample[1];
327 const T r = std::sqrt(std::max(static_cast<T>(1) - y * y, static_cast<T>(0)));
328
329 const auto localUnitPos = TVector3<T>(
330 r * std::sin(phi),
331 y,
332 r * std::cos(phi));
333
334 return localUnitPos * m_radius;
335}
336
337template<typename T>
339 const std::array<T, 2>& sample, T* const out_pdfA) const
340{
341 PH_ASSERT(out_pdfA);
342
343 *out_pdfA = uniformSurfaceSamplePdfA();
344
345 return sampleToSurfaceArchimedes(sample);
346}
347
348template<typename T>
349inline TVector3<T> TSphere<T>::sampleToSurfaceAbsCosThetaWeighted(const std::array<T, 2>& sample) const
350{
351 PH_ASSERT_LE(static_cast<T>(0), sample[0]); PH_ASSERT_LE(sample[0], static_cast<T>(1));
352 PH_ASSERT_LE(static_cast<T>(0), sample[1]); PH_ASSERT_LE(sample[1], static_cast<T>(1));
353
354 const auto ySign = math::sign(sample[1] - static_cast<T>(0.5));
355 const auto reusedSample1 = ySign * static_cast<T>(2) * (sample[1] - static_cast<T>(0.5));
356
357 const T phi = constant::two_pi<T> * sample[0];
358 const T yValue = ySign * std::sqrt(reusedSample1);
359 const T yRadius = std::sqrt(static_cast<T>(1) - yValue * yValue);// TODO: y*y is in fact valueB?
360
361 const auto localUnitPos = TVector3<T>(
362 std::sin(phi) * yRadius,
363 yValue,
364 std::cos(phi) * yRadius);
365
366 return localUnitPos.mul(m_radius);
367}
368
369template<typename T>
371 const std::array<T, 2>& sample, T* const out_pdfA) const
372{
373 const auto localPos = sampleToSurfaceAbsCosThetaWeighted(sample);
374
375 PH_ASSERT_GE(localPos.y(), static_cast<T>(0));
376 const T cosTheta = localPos.y() / m_radius;
377
378 // PDF_A is |cos(theta)|/(pi*r^2)
379 PH_ASSERT(out_pdfA);
380 *out_pdfA = std::abs(cosTheta) / (constant::pi<real> * m_radius * m_radius);
381
382 return localPos;
383}
384
385template<typename T>
387{
388 // PDF_A is 1/(4*pi*r^2)
389 return static_cast<T>(1) / getArea();
390}
391
392template<typename T>
394{
395 using namespace math::constant;
396
397 const TVector2<T>& phiTheta = surfaceToPhiTheta(surface);
398
399 return {
400 phiTheta.x() / two_pi<T>, // [0, 1]
401 (pi<T> - phiTheta.y()) / pi<T>};// [0, 1]
402}
403
404template<typename T>
406{
407 using namespace math::constant;
408
409 const T phi = latLong01.x() * two_pi<T>;
410 const T theta = (static_cast<T>(1) - latLong01.y()) * pi<T>;
411
412 return {phi, theta};
413}
414
415template<typename T>
417{
418 return phiThetaToSurface(latLong01ToPhiTheta(latLong01));
419}
420
421template<typename T>
423{
424 using namespace math::constant;
425
426 PH_ASSERT_GT(m_radius, 0);
427
428 const math::TVector3<T>& unitDir = surface.div(m_radius);
429
430 const T cosTheta = math::clamp(unitDir.y(), static_cast<T>(-1), static_cast<T>(1));
431
432 const T theta = std::acos(cosTheta); // [ 0, pi]
433 const T phiRaw = std::atan2(unitDir.x(), unitDir.z()); // [-pi, pi]
434 const T phi = phiRaw >= static_cast<T>(0) ? phiRaw : two_pi<T> + phiRaw;// [ 0, 2*pi]
435
436 return {phi, theta};
437}
438
439template<typename T>
441{
442 const T phi = phiTheta.x();
443 const T theta = phiTheta.y();
444
445 const T zxPlaneRadius = std::sin(theta);
446
447 const auto localUnitPos = TVector3<T>(
448 zxPlaneRadius * std::sin(phi),
449 std::cos(theta),
450 zxPlaneRadius * std::cos(phi));
451
452 return localUnitPos * m_radius;
453}
454
455template<typename T>
456template<typename SurfaceToUv>
458 const TVector3<T>& surface,
459 SurfaceToUv surfaceToUv,
460 T hInRadians) const
461{
462 static_assert(std::is_invocable_r_v<TVector2<T>, SurfaceToUv, TVector3<T>>,
463 "A surface to UV mapper must accept Vector3 position and return Vector2 "
464 "UV or any type that is convertible to the mentioned ones");
465
466 PH_ASSERT_GT(hInRadians, 0);
467
468 const math::TVector3<T>& normal = surface.div(m_radius);
469 hInRadians = std::min(hInRadians, math::to_radians<T>(45));
470
471 // Calculate displacement vectors on hit normal's tangent plane
472 // (with small angle approximation)
473
474 const T delta = m_radius * std::tan(hInRadians);
475
476 const auto& hitBasis = math::TOrthonormalBasis3<T>::makeFromUnitY(normal);
477 const math::TVector3<T>& dx = hitBasis.getXAxis().mul(delta);
478 const math::TVector3<T>& dz = hitBasis.getZAxis().mul(delta);
479
480 // Compute partial derivatives with 2nd-order approximation
481
482 // Find delta positions on the sphere from displacement vectors
483 const math::TVector3<T>& negX = surface.sub(dx).normalize().mul(m_radius);
484 const math::TVector3<T>& posX = surface.add(dx).normalize().mul(m_radius);
485 const math::TVector3<T>& negZ = surface.sub(dz).normalize().mul(m_radius);
486 const math::TVector3<T>& posZ = surface.add(dz).normalize().mul(m_radius);
487
488 // Find delta uvw vectors
489 const math::TVector2<T>& negXuv = surfaceToUv(negX);
490 const math::TVector2<T>& posXuv = surfaceToUv(posX);
491 const math::TVector2<T>& negZuv = surfaceToUv(negZ);
492 const math::TVector2<T>& posZuv = surfaceToUv(posZ);
493
494 const math::TMatrix2<T> uvwDiff(
495 posXuv.x() - negXuv.x(), posXuv.y() - negXuv.y(),
496 posZuv.x() - negZuv.x(), posZuv.y() - negZuv.y());
497 const auto xDiff = posX - negX;
498 const auto zDiff = posZ - negZ;
499 const std::array<std::array<T, 2>, 3> bs = {
500 xDiff.x(), zDiff.x(),
501 xDiff.y(), zDiff.y(),
502 xDiff.z(), zDiff.z() };
503
504 // Calculate positional partial derivatives
505 math::TVector3<T> dPdU, dPdV;
506 std::array<std::array<T, 2>, 3> xs;
507 if(uvwDiff.solve(bs, &xs))
508 {
509 dPdU.x() = xs[0][0]; dPdV.x() = xs[0][1];
510 dPdU.y() = xs[1][0]; dPdV.y() = xs[1][1];
511 dPdU.z() = xs[2][0]; dPdV.z() = xs[2][1];
512 }
513 else
514 {
515 dPdU = hitBasis.getZAxis();
516 dPdV = hitBasis.getXAxis();
517 }
518
519 return {dPdU, dPdV};
520}
521
522}// end namespace ph::math
A 3-D Axis-Aligned Bounding Box (AABB).
Definition TAABB3D.h:32
const TVector3< T > & getMaxVertex() const
Get the corner vertex of the maximum (+++) octant.
Definition TAABB3D.ipp:152
TAABB3D & expand(const TVector3< T > &amount)
Makes the bounds grow by some amount.
Definition TAABB3D.ipp:225
const TVector3< T > & getMinVertex() const
Get the corner vertex of the minimum (—) octant.
Definition TAABB3D.ipp:146
Represents a line segment in space.
Definition TLineSegment.h:25
T getMinT() const
Definition TLineSegment.ipp:94
const TVector3< T > & getOrigin() const
Definition TLineSegment.ipp:82
const TVector3< T > & getDir() const
Definition TLineSegment.ipp:88
T getMaxT() const
Definition TLineSegment.ipp:100
Represents a 2x2 matrix.
Definition TMatrix2.h:16
bool solve(const std::array< T, 2 > &b, std::array< T, 2 > *out_x) const
Solves linear systems of the form Ax = b.
Definition TMatrix2.ipp:103
static TOrthonormalBasis3 makeFromUnitY(const TVector3< T > &unitYAxis)
Definition TOrthonormalBasis3.ipp:16
A sphere in 3-D space.
Definition TSphere.h:20
static TSphere makeUnit()
Definition TSphere.ipp:19
TVector2< T > surfaceToPhiTheta(const TVector3< T > &surface) const
Map Cartesian to spherical coordinates on the surface of the sphere.
Definition TSphere.ipp:422
bool isInside(const TVector3< T > &point) const
Same as isIntersecting(), except the cost is slightly higher to reduce numerical error.
Definition TSphere.ipp:63
TVector3< T > latLong01ToSurface(const TVector2< T > &latLong01) const
Definition TSphere.ipp:416
TVector2< T > surfaceToLatLong01(const TVector3< T > &surface) const
Definition TSphere.ipp:393
T getArea() const
Definition TSphere.ipp:266
TAABB3D< T > getAABB() const
Definition TSphere.ipp:272
TVector3< T > phiThetaToSurface(const TVector2< T > &phiTheta) const
Map spherical to Cartesian coordinates on the surface of the sphere.
Definition TSphere.ipp:440
TVector2< T > latLong01ToPhiTheta(const TVector2< T > &latLong01) const
Definition TSphere.ipp:405
T uniformSurfaceSamplePdfA() const
Definition TSphere.ipp:386
T getRadius() const
Definition TSphere.ipp:260
TVector3< T > sampleToSurfaceArchimedes(const std::array< T, 2 > &sample) const
Definition TSphere.ipp:319
TVector3< T > sampleToSurfaceAbsCosThetaWeighted(const std::array< T, 2 > &sample) const
Definition TSphere.ipp:349
bool mayOverlapVolume(const TAABB3D< T > &volume) const
Conservatively checks whether this sphere overlaps a volume. By conservative, it means true can be re...
Definition TSphere.ipp:283
std::pair< TVector3< T >, TVector3< T > > surfaceDerivativesWrtUv(const TVector3< T > &surface, SurfaceToUv surfaceToUv, T hInRadians=to_radians< T >(1)) const
Calculate dPdU and dPdV with finite difference.
Definition TSphere.ipp:457
bool isIntersecting(const TLineSegment< T > &segment, real *out_hitT) const
Checks whether the segment is interseting with this sphere.
Definition TSphere.ipp:32
Represents a 2-D vector.
Definition TVector2.h:19
T & x()
Definition TVector2.ipp:38
T & y()
Definition TVector2.ipp:44
Represents a 3-D vector.
Definition TVector3.h:17
T & y()
Definition TVector3.ipp:189
T & z()
Definition TVector3.ipp:195
T & x()
Definition TVector3.ipp:183
Derived mul(const Derived &rhs) const
Definition TArithmeticArrayBase.ipp:98
T dot(const Derived &rhs) const
Definition TVectorNBase.ipp:14
Derived sub(const Derived &rhs) const
Definition TArithmeticArrayBase.ipp:62
Derived div(const Derived &rhs) const
Definition TArithmeticArrayBase.ipp:134
T lengthSquared() const
Definition TVectorNBase.ipp:44
Derived add(const Derived &rhs) const
Definition TArithmeticArrayBase.ipp:26
constexpr T four_pi
Value of .
Definition constant.h:39
constexpr T two_pi
Value of .
Definition constant.h:27
constexpr T pi
Value of .
Definition constant.h:15
Math functions and utilities.
Definition TransformInfo.h:10
T squared(const T value)
Definition math.h:59
int sign(const T value)
Extract the sign of value.
Definition math.h:154
T to_radians(const T degrees)
Convert degrees to radians.
Definition math.h:140
TVector3< float64 > Vector3D
Definition math_fwd.h:54
T clamp(const T value, const T lowerBound, const T upperBound)
Clamps a value to [lowerBound, upperBound]. None of value, lowerBound and upperBound can be NaN,...
Definition math.h:77