Needs["OpenCascadeLink`"];
SeedRandom[1];
reflect[vector_,
normal_] = -(vector - 2 (vector - Projection[vector, normal])) //
Simplify;
shape = OpenCascadeShape[
RegionUnion @@
Table[Ball[{Cos[i], Sin[i], 0}, .4], {i, 0, 2 Pi, Pi/6.}]];
bm = OpenCascadeShapeSurfaceMeshToBoundaryMesh[shape,
"ShapeSurfaceMeshOptions" -> {"AngularDeflection" -> .1}];
R = BoundaryMeshRegion[bm];
R2 = RegionBoundary[R];
dist = RegionDistance[R2];
proj = RegionNearest[R2];
pt0 = RandomPoint[R, 1][[1]];
v0 = {1., 1., 0.};
d0 = 0.01*Norm[v0];
sol = NDSolveValue[{r''[t] == {0, 0, 0}, r[0] == pt0, r'[0] == v0,
WhenEvent[dist@r[t] <= d0,
r'[t] -> reflect[r'[t], r[t] - proj@r[t]]]}, r, {t, 0, 100},
MaxStepSize -> 0.01];
ani = Animate[
Show[Graphics3D[{{FaceForm[Opacity[.2], Darker@Cyan], EdgeForm[],
R2}}, Boxed -> False],
ParametricPlot3D[sol@t, {t, 0, c}, Mesh -> {{c}},
MeshStyle -> {AbsolutePointSize[8], Red},
Method -> {"BoundaryOffset" -> False},
PlotStyle -> {Opacity[.9], White}, PlotPoints -> 400,
PerformanceGoal -> "Quality", PlotRange -> All] /. Line -> Arrow,
ViewPoint -> {1, 1, .8}, Background -> Gray], {c, $MachineEpsilon,
100}, AnimationRate -> 1, ControlPlacement -> Bottom]

- Test another type of bounce and another obstacle surfaces.
SeedRandom[1];
reflect[vector_,
normal_] = -(vector - 2 (vector - Projection[vector, normal])) //
Simplify;
surf = ExampleData[{"Geometry3D", "UtahTeapot"}, "Region"];
cube = TransformedRegion[Cuboid @@ Transpose@RegionBounds[surf],
ScalingTransform[{1.2, 1.2, 1.2}, RegionCentroid[surf]]] //
DiscretizeRegion // RegionBoundary;
reg = RegionUnion[surf, cube];
dist = RegionDistance[reg];
proj = RegionNearest[reg];
pt0 = RegionCentroid[reg];
v0 = {1., 1., 0.2};
d0 = 0.01*Norm[v0];
sol = NDSolveValue[{r''[t] == {0, 0, -9.8}, r[0] == pt0, r'[0] == v0,
WhenEvent[dist@r[t] <= d0,
r'[t] -> reflect[r'[t], r[t] - proj@r[t]]]}, r, {t, 0, 20},
MaxStepSize -> 0.001]
ani = Animate[
Show[Graphics3D[{{FaceForm[Opacity[.2], Cyan], EdgeForm[],
surf}, {FaceForm[Opacity[.3], LightGray], EdgeForm[], cube}},
Boxed -> False],
ParametricPlot3D[sol@t, {t, 0, c}, Mesh -> {{c}},
MeshStyle -> {AbsolutePointSize[8], Red},
Method -> {"BoundaryOffset" -> False},
PlotStyle -> {Opacity[.9], White}, PlotPoints -> 400,
PerformanceGoal -> "Quality", PlotRange -> All] /. Line -> Arrow,
Background -> LightGray], {c, $MachineEpsilon, 20},
AnimationRate -> 1, ControlPlacement -> Bottom]
