I'm trying to get some rays to bounce in a circle. But I want to be able to control the reflections, i.e. the direction the rays bounce in the circle. I have a MWE below, and it is severely limited by RegionIntersection. Even running one ray for 10 bounces takes 19 seconds. Yes. That is 2 seconds per bounce!

(* Starting point *)
p0 = {0, 1};
(* Initial direction of light *)
d0 = {0, -1};

(* Radius of sphere *)
radius = 50;

(* Break the sphere into 500 lines *)
points = 500;
boundary1 = N[CirclePoints[radius, points]];

(* These are the distinct edges *)
edge1 = Table[
   RotateRight[boundary1, i][[;; 2]], {i, Length@boundary1}];
lines = Line[#] & /@ edge1;

(* These are their normals *)
norm1 = N[Normalize@(RotationTransform[Pi/2]@(#[[2]] - #[[1]]))] & /@

raytracing[{p0_, d0_}] := Module[{},

   (* Find intersection *)
   intersection =
    N[RegionIntersection[HalfLine[p0 + d0, d0], #]] & /@ lines;

   (* Find position of the intersection *)
   intersectionedge = Position[intersection, _?(# != {} &)];
   intersectionedge = intersectionedge[[1, 1]];
   (* Store point where this occured *)
   p1 = intersection[[intersectionedge]][[1, 1]];
   (* Find the normal to the line segment making up the circle *)
   n = norm1[[intersectionedge]];

   (* Find the normal, and rotate it slightly (to get the random bounce effect) *)
   limit1 = Normalize[RotationMatrix[Pi/3].(n)];
   limit2 = Normalize[RotationMatrix[-Pi/3].(n)];
   (* Find the random direction our ray travels now *)
   d1 = Normalize[{RandomReal[{limit1[[1]], limit2[[1]]}],
      RandomReal[{n[[2]], limit2[[2]]}]}];

   Return[{p1, d1}]


results = NestList[raytracing, {p0, d0}, 10];
resultsplot = results[[;; , {1}]];
resultsplot = Flatten[results[[;; , {1}]], 1];
Show[ListPlot[resultsplot, Joined -> True,
  PlotRange -> {{-50, 50}, {-50, 50}}, AspectRatio -> 1,
  Frame -> True], Graphics[Circle[{0, 0}, 50]]]

my attempt

  • 2
    $\begingroup$ Is this a sphere or a circle? BIG DIFFERENCE!! $\endgroup$ – David G. Stork Apr 16 at 1:25
  • 1
    $\begingroup$ There's also a big difference between a colon and a semi-colon. $\endgroup$ – TRiG Apr 17 at 9:50
  • $\begingroup$ There's an easy way to do this on your GPU - shadertoy.com lets you do it right in your browser with shaders... $\endgroup$ – Dagelf Apr 18 at 9:17
  • $\begingroup$ @Dagelf, in fact what I do is write a prototype in Mathematica first, and once it's working there, move to an OpenGL implementation, which is what Shadertoy uses. $\endgroup$ – J. M.'s discontentment May 19 at 2:58

Update: Extended to Include 3D Shapes

I have extended the workflow to include using 3D shapes including an imported 3D CAD object at the end of this answer.

Original Post

Here is a slight adaptation to my answer to your previous question here. It uses region functions, but not RegionIntersection. Rather it relies on the ray advancing to within the collision margin and using RegionNearest to approximate a reflection angle. It also counts the hits so that you could use it decay the photons as well. I have not added any scattering component and I did not join the lines. Below we will setup a simple but more complex geometry to see how it generalizes.

(* Create and Discretize Region *)
disks = RegionUnion[Disk[{-1, 0}, 0.5], Disk[{1, 0}, 0.5], 
   Disk[{0, -1}, 0.5], Disk[{0, 1}, 0.5], Disk[{0, 0}, 0.25]];
region = RegionDifference[Disk[], disks];
R2 = RegionBoundary@DiscretizeRegion[region, AccuracyGoal -> 5];
(* Set up Region Operators *)
rdf = RegionDistance[R2];
rnf = RegionNearest[R2];
(* Time Increment *)
dt = 0.001;
(* Collision Margin *)
margin = 1.02 dt;
(* Starting Point for Emission *)
sp = 0.85 Normalize[{1, 1}];
(* Conditional Particle Advancer *)
advance[r_, x_, v_, c_] := 
 Block[{xnew = x + dt v}, {rdf[xnew], xnew, v, c}] /; r > margin
advance[r_, x_, v_, c_] := 
 Block[{xnew = x , vnew = v, normal = Normalize[x - rnf[x]]},
   vnew = Normalize[v - 2 v.normal normal];
   xnew += dt vnew;
   {rdf[xnew], xnew, vnew, c + 1}] /; r <= margin

Now, setup and run the simulation and display the results.

(* Setup and run simulation *)
nparticles = 1000;
ntimesteps = 2500;
tabres = Table[
    advance @@ # &, {rdf[sp], 
     sp, {Cos[2 Pi #], Sin[2 Pi #]} &@RandomReal[], 0}, 
    ntimesteps], {i, 1, nparticles}];
frames = Table[
     Epilog -> ({ColorData["Rainbow", (#4 - 1)/10], 
          Disk[#2, 0.01]} & @@@ tabres[[All, i]]), 
     AspectRatio -> Automatic], {i, 1, ntimesteps, 50}];


It took about 20s to solve the 1000 photons system on my laptop. Rendering the animation took additional time.

Extended Workflow to Include 3D Shapes

Mathematica 12.1 introduced a link to the open source 3D CAD package, OpenCascade, as described here. Being a 3D CAD modeler, OpenCascade does a pretty good job preserving sharp features efficiently. I will describe a couple of workflows to incorporate this new feature to perform 3D Raytracing with a simple solver.

Using OpenCascadeLink to Create 3D Shapes

Through experimentation, I found that I needed to invert the surface normals to get the RegionDistance and RegionNearest functions to work properly. This can be done relatively simply by creating a cavity in a bounding object with the shape of interest. Here, we will create a rectangular toroidal conduit and perform the necessary differencing operation to create the cavity.

(* Load Needed Packages *)
(* Create a swept annular conduit *)
pp = Polygon[{{0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}}];
shape = OpenCascadeShape[pp];
axis = {{2, 0, 0}, {2, 1, 0}};
sweep = OpenCascadeShapeRotationalSweep[shape, axis, -3 \[Pi]/2];
bmsweep = OpenCascadeShapeSurfaceMeshToBoundaryMesh[sweep];
(* Visualize Sweep *)
Show[Graphics3D[{{Red, pp}, {Blue, Thick, Arrow[axis]}}], 
 bmsweep["Wireframe"], Boxed -> False]
(* Create Padded Bounding Box as Main Body *)
shapebb = 
   Cuboid @@ 
     CoordinateBounds[Transpose@bmsweep["Bounds"], Scaled[.05]]]];
(* Difference Padded BB from sweep in OpenCascade *)
diff = OpenCascadeShapeDifference[shapebb, sweep];
(* Visualize Differenced Model *)
bmeshdiff = OpenCascadeShapeSurfaceMeshToBoundaryMesh[diff];
(* Create Mesh Regions *)
bmr = BoundaryMeshRegion[bmsweep];
mrd = MeshRegion[bmeshdiff];

Toroidal Cavity

Now, execute the simulation workflow:

(* Set up Region Operators on Differenced Geometry *)
rdf = RegionDistance[mrd];
rnf = RegionNearest[mrd];
(* Setup and run simulation *)
(* Time Increment *)
dt = 0.004;
(* Collision Margin *)
margin = 1.004 dt;
(* Conditional Particle Advancer *)
advance[r_, x_, v_, c_] := 
 Block[{xnew = x + dt v}, {rdf[xnew], xnew, v, c}] /; r > margin
advance[r_, x_, v_, c_] := 
 Block[{xnew = x , vnew = v, normal = Normalize[x - rnf[x]]},
   vnew = Normalize[v - 2 v.normal normal];
   xnew += dt vnew;
   {rdf[xnew], xnew, vnew, c + 1}] /; r <= margin
(* Starting Point for Emission *)
sp = {3, 0.5, 1};
nparticles = 2000;
ntimesteps = 2000;
tabres = Table[
    advance @@ # &, {rdf[sp], 
     sp, { Cos[2 Pi #[[1]]] Sin[Pi #[[2]]], 
        Sin[ Pi #[[2]]] Sin[2 Pi #[[1]]], Cos[ Pi #[[2]]]} &@
      First@RandomReal[1, {1, 2}], 0}, ntimesteps], {i, 1, 
frames = Table[
    Graphics3D[{White, EdgeForm[Thin], Opacity[0.25], bmr, 
      Join~({ColorData["Rainbow", (#4 - 1)/10], Sphere[#2, 0.025]} & @@@
         tabres[[All, i]]), Boxed -> False, 
     PlotRange -> RegionBounds[bmr], 
     ViewPoint -> {1.5729625965895664`, -2.8428921412097794`, \
     ViewVertical -> {-0.26122960866834294`, -0.9511858016078727`, 
       0.16433095379316984`}], {i, 1, ntimesteps, 66}];

Toroidal Animation

The simulation looks relatively reasonable. It will not be so fast as to be able to perform the simulations interactively, but a 2,000 particle simulation takes a minute or two. There is still plenty of room for optimization too.

Using Imported CAD

I created a hemispherical "mirror" in the SolidWorks 3D CAD package and saved the geometry as an ACIS step file. In my case, the default export was in $mm$ so I wanted to rescale back to meters. I thought RegionResize would be the approach, but it did not preserve sharpe features as shown in the following:

(* Write a ACIS step file in Current Notebook Directory *)
steptxt = 
file = OpenWrite["hemimirror2.step"];
WriteString[file, steptxt];
(* Import step file Using OpenCascade *)
shape2 = OpenCascadeShapeImport[
bmesh2 = OpenCascadeShapeSurfaceMeshToBoundaryMesh[shape2]
(* Convert into MeshRegion *)
mrd = MeshRegion[bmesh2, PlotTheme -> "Lines"];
(* Scale to Meters *)
mrd = RegionPlot3D[RegionResize[mrd, 1/1000], Mesh -> All, 
  PlotStyle -> None, Boxed -> False]

Mesh distortion

As you can see, RegionResize did not keep sharp feature edges on a simple uniform scaling. It is straight forward to rescale a BoundaryMesh as shown here:

(* Import step file Using OpenCascade *)
shape2 = OpenCascadeShapeImport["hemimirror2.step"];
bmesh2 = OpenCascadeShapeSurfaceMeshToBoundaryMesh[shape2]
(* Scale coordinates to meters using ToBoundaryMesh *)
bmesh2 = ToBoundaryMesh["Coordinates" -> bmesh2["Coordinates"]/1000, 
  "BoundaryElements" -> bmesh2["BoundaryElements"]]
mrd = MeshRegion[bmesh2, PlotTheme -> "Lines"]

BMESH Rescale

The simple rescaling on the BoundaryMesh preserves the sharp edges. Now, exectute the workflow on the imported CAD.

(* Set up Region Operators on Differenced Geometry *)
rdf = RegionDistance[mrd];
rnf = RegionNearest[mrd];
(* Setup and run simulation *)
(* Time Increment *)
dt = 0.002;
(* Collision Margin *)
margin = 1.004 dt;
(* Conditional Particle Advancer *)
advance[r_, x_, v_, c_] := 
 Block[{xnew = x + dt v}, {rdf[xnew], xnew, v, c}] /; r > margin
advance[r_, x_, v_, c_] := 
 Block[{xnew = x , vnew = v, normal = Normalize[x - rnf[x]]},
   vnew = Normalize[v - 2 v.normal normal];
   xnew += dt vnew;
   {rdf[xnew], xnew, vnew, c + 1}] /; r <= margin
(* Starting Point for Emission *)
sp = {0.5, 0.25, 0};
nparticles = 2000;
ntimesteps = 4000;
tabres = Table[
    advance @@ # &, {rdf[sp], 
     sp, { Cos[2 Pi #[[1]]] Sin[Pi #[[2]]], 
        Sin[ Pi #[[2]]] Sin[2 Pi #[[1]]], Cos[ Pi #[[2]]]} &@
      First@RandomReal[1, {1, 2}], 0}, ntimesteps], {i, 1, 
frames = Table[
    Graphics3D[{White, EdgeForm[Thin], Opacity[0.25], mrd, 
      Join~({ColorData["Rainbow", (#4 - 1)/10], 
          Sphere[#2, 0.0125]} & @@@ tabres[[All, i]]), Boxed -> False,
      PlotRange -> RegionBounds[mrd], 
     ViewPoint -> {0.8544727985513026`, 
       2.0153230313799515`, -2.5803777467117928`}, 
     ViewVertical -> {-0.028824747767816083`, 0.9942988180484538`, 
       0.10265960424416963`}], {i, 1, ntimesteps, 250}];

Imported CAD simulation

So, the workflow with some subtle workarounds is able to perform some sort of raytracing 3D shapes including third party CAD packages. It is only a quick and dirty prototype with room for improvement, but it's a start.

| improve this answer | |
  • 8
    $\begingroup$ A great answer. $\endgroup$ – A little mouse on the pampas Apr 16 at 13:47
  • 1
    $\begingroup$ @PleaseCorrectGrammarMistakes Thank you very much! I created a 3D workflow in case you are interested. It still can be improved, but it looks feasible. $\endgroup$ – Tim Laska Apr 18 at 15:24
  • 1
    $\begingroup$ Great answer, +1! A small nitpick: if your second code block, epilog is defined but never used (you seem to apply the same expression manually on the next line). $\endgroup$ – Victor K. Apr 18 at 22:07
  • 1
    $\begingroup$ @VictorK. Thank you and good catch! I removed the unused epilog function. $\endgroup$ – Tim Laska Apr 18 at 23:39

EDIT 01:

The original code had an issue when the angle of the ray is counterclockwise from the normal of the circle, which I didn't catch. The code should be correct now, I think.

RegionIntersection and friends are really nice functions if you just need to find a couple of values, but it looks to me like RegionIntersection will be called 500 times (since you have 500 edge lines). I think because it's such a general algorithm that has to take into account many possible geometries, it's not super fast. Certainly if you have to call it 10,000 times, it's going to be a bit slow. If you really want to use RegionIntersection, you should ask for the intersection between a line and a circle, and it will be much faster. However, I think the very best way is to go back to basics and calculate the intersections manually.

I set up a function intersect which takes an initial point and vector and calculates the intersection with the circle of centre {0, 0} and radius 50. It returns the point of intersection.

reflect calculates the new angle based on the normal vector of the circle at the point it strikes. I add a random amount based on the limits, and then check with a While loop to make sure I'm not reflecting outside the sphere. This is probably an inefficient way of doing this, but I didn't feel like fixing it. I'm sure you can find a better way. This function returns a point in the direction of the reflected line.

Finally, generate takes a number of generations to propagate, a custom starting point and starting angle, as well as the limits, and propagates the reflections. It returns a list of points for plotting.

centre = {0., 0.};
radius = 50.;
intersect[p0_, v_] := Module[{
   u, d, n},
  u = p0 - centre;
  d = (u.v)/Norm[v];
  n = Sqrt[radius^2 + d^2 - u.u];
  p0 + (n - d)/Norm[v]*v
reflect[{p0_, p1_}, limits_] := Module[{
   p, theta},
  p = p1 - p0;
  theta = ArcCos[(p.p1)/(Norm[p] Norm[p1])] + RandomReal[limits];
   theta >= π/2 ∨ theta <= -π/2,
   theta = ArcCos[(p.p1)/(Norm[p] Norm[p1])] + RandomReal[limits];
  If[p[[2]]*p1[[1]] > p[[1]]*p1[[2]], theta = -theta];
  {{Cos[theta], -Sin[theta]}, {Sin[theta], Cos[theta]}}.-p1
generate[n_, initpt_, initv_, limits_] := Module[{
   points, vec
  points = {initpt};
  vec = initv;
   AppendTo[points, intersect[points[[i]], vec]];
   vec = reflect[{points[[i]], points[[i + 1]]}, limits],
   {i, n}
   Circle[centre, radius],
   Arrow[pts = 
     generate[rays, initpt, {Cos[initangle], Sin[initangle]}, 
 {{rays, 20, "Number of Rays:"}, 1, 500, 1, Appearance -> "Labeled"},
 {{limits, {0, 0}, "Limits:"}, -π, π, 
  Appearance -> {"Labeled", "Paired"}, ControlType -> IntervalSlider, 
  Method -> "Push"},
 {{initpt, {0, 20}}, Locator},
 Control[{{initangle, π/4, "Initial Angle:"}, 
   Experimental`AngularSlider[Dynamic[initangle]] &}]

Reflections in a circle.

You can move the starting point just by clicking and dragging the locator. The rest of the parameters are customizable above the plot. It seems reasonable fast up to around 500 reflections on my computer. If you want more performance, there's a few things that could be changed. My use of AppendTo probably isn't the fastest, though I'm not sure if it makes much difference on only 500 elements.

I did a circle since that's what you have in your example, but a sphere would be a bit more work. If you want to break the circle into line segments like in your code rather than calculating the intersection between a line and a circle, there are algorithms built originally for video games that can rapidly calculate the intersection of 2 lines. I found one and was able to do reflections inside of arbitrary, n-sided polygons quite rapidly. If it's important that it be a collection of lines rather than a uniform circle, let me know and I can edit my answer.

| improve this answer | |

The ray-tracing task in general is just searching for reflection angles and corresponding interaction points. As it was pointed out by @MassDefect, the faster way is making these calculations as it is without any Region's. Let's set the initial statement:

r = 1; (*Radius of the circle*)
{x0,y0} = {0,0}; (*Center of the circle*)
pt = {r Cos[5.1 Pi/4], r Sin[5.1 Pi/4]}; (*First reflection point*)
alpha = 8 Pi/18; (*First ray angle*)

beg = {-((Last@pt - Tan[alpha] * First@pt)/Tan[alpha]), 0}; (*Ray starting point*)
pts = {pt}; (*List of interaction points*)
phi = Pi - Arg[First@pt + I Last@pt]; (*central angle of interaction point*)
i = Pi - alpha - phi; (*incidence angle obtained from one of the triangles*)
beta = Pi - alpha - 2 i;
alpha = N[Pi - beta]; (*The new ray angle from another triangle*)
eq = {
y == x Tan[alpha] + (Last@pt - Tan[alpha]*First@pt), (*ray equation*)
(x - x0)^2 + (y - y0)^2 == r^2,
x != First@pt} (*This allows taking right point from the solution*)
sol = NSolve[eq, {x, y}, Reals];
pt = {x, y} /. sol[[1]];
pts = Join[pts, {pt}],
{i, 1, 1500}]]

This gives 1500 bounces in less than 14 sec on my PC.

Drawing the solution:

   Black, Circle[{x0, y0}, r],
   Red, PointSize[Medium], Point@pts,
   Green, Arrow@pts,
   Blue, Arrow@{beg, pts[[1]]}}, Axes -> True, Ticks -> None]

enter image description here

| improve this answer | |

Not an answer but too long for a comment.

RegionIntersection is not very fast on its own and I think it is probably meant for computing full-dimensional intersections. Morever, RegionIntersection is high-level code and you Map over your list of lines. That is orders of magnitude slower than computing ray-line intersection in compiled and vectorized code. (It might work already much better if you put all lines into a single MeshRegion.)

But there is another problem: You test for intersection of the ray with each boundary edge. If you have $n$ boundary edges, then the cost is proportional to n. In computer graphics, one groups many lines (or triangles in 3D) together into clusters. Each cluster has a simple bounding volume (typically an axis-aligned cuboid). Then, in order to check whether a ray hits a certain line or (triangle), one first checks whether the ray hits the bounding box. So if 100 lines are contained in a given bounding box and the ray does not hit the box then you saved 99 collision tests. But it is even getting better: One can apply this grouping technique again to these bounding boxes! And further to the bounding boxes of clusters of bounding boxes and so on. In the end, one can organize this hierarchy into a tree structure, a so-called bounding volume hierarchy. Then the query for a single ray-line (ray-triangle) intersection costs only roughly $O(\log(n))$ instead of $O(n)$, which makes a huge difference. Building the bounding volume hierarchy has a certain cost, but it has to be built only once for each scene.

| improve this answer | |
  • 1
    $\begingroup$ As a side comment, symbolic RegionIntersection of even simplest things such as two line segments results absurdly complicated answers. There's definitely room for improvement! $\endgroup$ – kirma May 14 at 20:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.