Google Summer of Code 2010 project
The goal of this project was to improve GeoGebra3D and extend it with new features. It spanned different areas, described in detail below. While there were several separate goals set at the outset of the project, the eventual focus was on function plots.
- Developer: André Eriksson, student, KTH Royal Institute of Technology, Stockholm, Sweden
- Mentor: Mathieu Blossier <mathieu@…>, mathematics teacher and teacher educator at the University of Rouen in France
Blog
Here is André's Blog
Drawing curves
The existing implementation for drawing parametrized curves in GeoGebra3D was limited and in need of development. When designing a new solution we wanted it to have the following features:
- Dynamic level of detail depending on the curvature. The initial implementation sampled the curve at fixed intervals, which led to unnecessarily high detail in areas with low curvature and insufficient detail in areas with high curvature.
- Dynamic level of detail depending on the view. We wanted high fidelity and speed regardless of how zoomed in the camera was.
- Persistent data. The initial implementation reevaluated every data point every time the view was changed.
- Efficient culling. We didn't want to draw parts of the curve outside the viewing volume.
- Eliminate the need for the user to specify an interval for the curve parameter.
After doing some research, we've come to the conclusion that the last point is very hard to implement (since figuring out which parts of the curve to draw involves searching over an infinite interval), and have decided not to attempt to implement it for now.
In order to address the other points, we built an implementation based on a binary tree over the curve parameter. Each node in the tree consists of a parameter value and the corresponding value of the curve evaluated at that point. New data points are evaluated and inserted into the tree continuously when they are needed. When the view is changed, the tree is traversed recursively until the sufficient level of detail is reached.
Drawing surfaces
Enable surface plots on the form z=f(x,y). A basic implementation was written by Mathieu, but it needed improvement. I initially wrote a simple top-down triangle quadtree-based implementation, where the mesh was created from scratch each update. The mesh was then refined recursively by comparing the normals of adjacent triangles. This implementation didn't take into account mesh topology, which led to cracks in the mesh wherever the detail level of two adjacent triangles was different. I realized that in order to achieve a continuous mesh, an unnecessary amount of splits would have to be performed. For this reason I abandoned this approach in favor of one closer to a triangle bintree.
There are a few properties that a good implementation of surface plots should have:
- Level of detail should depend on curvature. Areas that are locally near linear should have less detail while areas with high curvature should have more.
- Level of detail should depend on the view. Detail should always be high in the viewing volume and low outside.
- It should be fast. Ideally any changes in detail should be real-time for usability reasons.
- It should look good. This means that the mesh has to be continuous, as cracks will otherwise ruin the appearance of the graph.
- The user shouldn't have to specify intervals for the parameters.
The current implementation achieves (most) of these goals by means of an hierarchal data structure based on diamonds. It's heavily influenced on ROAM ( http://www.cognigraph.com/ROAM_homepage/ ), which is an efficient approach to real-time adaptive terrain.
The algorithm starts by bootstrapping a fairly simple square mesh. This mesh is then continuously refined by means of a series of split and merge operations on the "active diamonds". A split operation increases triangle count by replacing a diamond with four children, while a merge operation is basically its inverse. There is one priority queue for split operations and one for merge operations, sorted by an error value assigned to each diamond. Which of the two operations is performed depends on how detailed the mesh currently is compared to the desired level of detail.
A diamond is defined uniquely by one vertex, its center point. Each diamond also contains references to four other diamond: two parents that are at one level above the vertex, as well as two other diamonds at even higher levels. These four vertices form a square (or a parallelogram if the parameter intervals aren't equal in length) around the diamond's vertex.
Triangles are saved in a data structure (DrawList), as a number of floats in a FloatBuffer (actually two float buffers, one for vertices and one for normals). The triangles are stored in a contiguous chunk in the buffers in order to facilitate rendering with vertex buffers/VBOs.
Which error measure should be used has not yet been decided. The original naïve approach simply set the error for each diamond as the minimum distance between the vertex of a diamond and a line between its two parents' vertices. However, there are some problems with this approach. After performing some experiments, it appears that either the total error according to this measure tends to a non-zero asymptote (depending on the function) as detail level increases, or at least it tends to zero very slowly. This makes it hard to use total error as a guideline for refinement. Another idea is to use some kind of measure of volume that takes into account all five points in a diamond.
Each update, a specified number of splits/merges are performed. Whether a split or merge is performed depends on a few factors. There's both a minimum triangle count and a goal for the total error. If either of these have not been met, a split is performed. If the minimum triangle count has been met and the current total error is lower than the goal a merge is performed. Otherwise, nothing is done.
One of the major advantages of the above approach compared to a top-down approach is that there is no minimum amount of work that has to be done in an update. The mesh can be updated continuously in small increments until the desired level of detail is reached, which facilitates real-time updates.
Other areas
There were a few other goals set in the project, that I unfortunately did not have the time to work on. Hopefully I will have the chance to work on some of them in a future project. A list:
- Drawing text - to implement a better way to draw text in the 3D perspective. This will probably be done by using JLaTeXMath.
- Graphical enhancements - to improve the speed and visual quality of GeoGebra3D. One part of this will be to implement Vertex Buffer Objects. Another could be implementing shaders.
- Drawing polygons and polyhedra using triangulation
- Texture mapping - to implement loading and mapping of textures, preferably to any object.
Documentation
Surface Plots
Currently, surface plots can be created by using either the command 'f(<variable 1>, <variable 2>)=<some function>' or the command 'Function[<some function>, <variable 1>, <minimum value for variable 1>, <maximum value for variable 1>, <variable 2>, <minimum value for variable 2>,<maximum value for variable 2>] (for example 'f(x,y)=sqrt(x x+y y)' or Function[sqrt(a a+b b),a,-10,10,b,-10,10]). While the code I have built supports parametrized surfaces (eg f(u,v)=(g(u,v),h(u,v),i(u,v))), as of now there is no interface exposing this to the user. (Instructions on how to implement are included this in this document)
The algorithm that handles surface plots is built on an algorithm for real-time level of detail(LoD) for 3D terrain, called ROAM (http://www.cognigraph.com/ROAM_homepage/). In short, the algorithm works by keeping a triangle mesh in memory, which it continuously updates depending on the view and current level of detail.
The basic 'unit' in the mesh is the diamond, represented by the class SurfaceMeshDiamond. Each diamond is uniquely identified by its midpoint vertex. This vertex is surrounded by four other vertices, belonging to other diamonds. A diamond can be seen as two isosceles triangles sharing one side (namely the base), thereby forming a parallelogram. The diamonds are structured so that each diamond has four 'child' diamonds. Each child diamond has two 'parent' diamonds, and each of its two triangles occupies half of one of the parents' base triangles.
Two operations are performed on diamonds: splits and merges. When split, a diamond is taken from the mesh and replaced by its four base diamonds, thereby increasing level of detail. A merge is the inverse of a split - four child diamonds are replaced by their common parent, thus reducing level of detail. In order to avoid cracks in the mesh, both the split and merge operations are implemented recursively, as several nearby diamonds often need to be split/merged to retain the mesh topology. Both split and merge operations are handled by SurfaceMeshDiamond.
Each diamond has an associated error value, which is a measure of how much the local area of the mesh it represents differs from the 'actual' function. This has to be an approximation, since calculating the exact value would require integration, which would be a) slow and b) not possible in many situations. The error calculation has to be fast since we want to rapidly create new diamonds. For this reason an error measure is used, that is the total volume of the two pyramids, formed by linking the four edge vertices with the middle vertex. This is used since it doesn't require the function to be evaluated at any extra points, and also seems to give good results. Technically, a diamond has two error values, one for each of its triangles. This is used to calculate a more exact total error value of the mesh. For some diamonds in certain meshes, one or more vertices are undefined (due to for example singularities). In these situations, the usual error calculation can't be used. Instead an error defined as a * SurfaceMesh.undefErrorConst is used, where a is the area of the ingoing diamond in the parameter space.
The algorithm is run in real time, with a set number of split or merge operations performed each frame. Which of the operations is to be performed depends on the current triangle count and total (visible) error (This is decided by SurfaceMesh.tooCoarse(), which is described in more detail under the section on SurfaceMesh). There are two priority queues for diamonds that are to be split and merged, respectively. The split merge queue contains all active diamonds in the mesh, while the merge priority queue contains all their parents. When it has been decided whether diamonds are to be split or merged, the appropriate priority queue is popped, and the resulting diamond is handled. Since priority queues are somewhat slow, bucket-based 'almost-' priority queues are used instead. Each queue consists of a fixed number of buckets. When diamonds are inserted, they are weighed according to the error and placed in a bucket accordingly. When popping a diamond, the first diamond in the highest occupied bucket is used. These are described in more detail below.
The diamonds are not used when drawing the mesh - instead the representation of the mesh used for rendering is a list of triangles (stored in the class SurfTriList). Triangles are added and removed to this list as diamonds are merged and split. A more detailed description of SurfTriList can be found below.
Another important aspect is frustum culling. Culling info is updated in a top-down fashion starting from the root node and its children. Each diamond is marked as either OUT, SOMEIN or ALLIN, meaning that it is outside, partly inside or completely inside the viewing volume, respectively. Since the view is often rotated around the origin, the mesh is anisotropic, and the viewing volume is defined as the smallest sphere centered on the origin that contains the entire viewing frustum. This means that the culling status of a diamond can be determined by simply comparing the radius of the viewing 'sphere' to the diamond's smallest and largest distances to the origin. Furthermore, in all relevant cases, this point will be one of the diamond's vertices, which means that we frustum culling can be really fast. Furthermore, if a diamond is marked as ALLIN or OUT, any diamond that's 'contained' in it is (pretty much) guaranteed to have the same culling status, meaning that we don't even have to test such diamonds. Culling for diamonds where vertices are NaN/infinite is difficult, since there's really no way of telling how they are supposed to behave. For this reason, these diamonds are never culled, and might be refined even though they're not visible.
There is support for rendering functions that are not defined for any specific interval but continue into infinity (note that this is not generally possible for parametric plots - they should always come with a specified interval). However, it appears that GeoFunctionNVar does not contain any information on whether a function should be 'limited' or not, so I've chosen to place a flag (unlimitedRange) in DrawFunction2Var that can be set to true if a function is to be 'unlimited'. 'Unlimited' functions work by replacing the mesh with a bigger one whenever the radius of the viewing sphere is big enough to almost fit the entire old mesh (or a smaller one when the mesh is way too big for the current view). This approach does mean that there is a visible jerk whenever the mesh is replaced, but there are also a few advantages to this approach compared to having one mesh that is constantly expanded. Mainly it's a lot easier to write - the second approach would require a lot of extra code. It's also more memory friendly, as diamonds at levels of detail far from the current level would otherwise crop up and drain extra memory (of course this could be worked around, but it would mean extra coding as well). If anyone would like to implement smooth transitions don't hesitate to contact me - I have some ideas on how it could be done.
SurfaceMesh
This class contains the main logic behind the algorithm. It handles creation of the base mesh, optimization, splits and merges. It also contains parameters that can be used to tune the level of detail of the mesh.
The base mesh setup is lifted from ROAM. The visible part of the mesh consists of one diamond and its children. This diamond in turn has parents and neighbors, but these are flagged not to be displayed or refined, and are arranged in a circular pattern to avoid null references.
As for the detail parameters, here's a short list with descriptions:
- private final double errorCoeffs
- Parameters for controlling the desired error per area unit. This error E is set according to E=errorCoeffs[0]+errorCoeffs[1]*r+errorCoeffs[2]*r^2+errorCoeffs[3]*sqrt(r), where r is the radius of the viewing sphere.
- private final int triGoal
- The desired triangle count.
- private final int minTriCount
- The minimum triangle count for any mesh. This has to be nonzero since the error measure might be zero for certain base meshes even though there is actually a large error.
- private final int initialRefinement
- The amount of splits/merges performed when the mesh is created.
- private final int stepRefinement
- The amount of splits/merges performed on each update.
- private final int maxTriangles
- The maximum triangle count allowed. Memory is allocated to fit this many triangles.
- public static final double undefErrorConst
- A proportionality constant for setting the error of diamonds where one or more vertices are undefined.
- private static final int maxLevel
- The maximum level of refinement for any mesh. Not very important but it makes certain meshes less computationally expensive.
SurfaceMesh decides whether to split or merge diamonds with a call to the function tooCoarse(), which compares the current total [visble] error and triangle count to preset values. Its first priority is to get the triangle count up to minTriCount. As long as the total triangle count < minTriCount, the mesh will continue to be refined no matter what. The second criteria checked is the total triangle count. If it is larger than maxTriangles, refinement will stop. After that, the total error is compared to a calculated desired error that depends on the total visible area and the viewing radius. The desired error E is calculated according to E = a*(c0+c1*r+c2*r^2+c3*sqrt(r)), where a is the total visible area, r is the radius of the viewing sphere, and c0/c1/c2/c3 are the values in the array SurfaceMesh.errorCoeffs. If the error is less than the goal, true is returned (except when the triangle count is higher than triGoal). If the error is greater than the goal, false is always returned. When tooCoarse() returns true followed by false (or the other way), the refinement is aborted. This is done to avoid wasting time optimizing a mesh that is already satisfactory, since this will only happen when the detail goals are satisfied.
Meshes are only refined until the desired detail criteria have been met once. A flag (SurfaceMesh.doUpdates) is then toggled, and any attempts to optimize the mesh after this are ignored. This flag is reset by calling turnOnUpdates(). This is done by DrawSurface2Var whenever the view is significantly changed.
SurfaceMeshDiamond
This class represents the basic unit in the mesh, the diamond. It contains one vertex, as well as references to other diamonds whose vertices represent the diamond's corners. These other diamonds are two parents (stored in parents[]) and two ancestors from higher levels (ancestors[]; ancestors[0] would be the diamond's parent if it were part of a quadtree). It also contains an array of four references to child diamonds. If any of the diamond's children have never been used, the corresponding reference(s) are null.
SurfaceMeshDiamond handles the logic for frustum culling and error/area/bounding calculations. Other than that, its main purpose is to act as a container for various data. Despite being a fairly large class, SurfaceMeshDiamond doesn't contain any functionality critical to understanding the plotting system (other than what has already been described above). Its code should also be fairly self-documenting, which is why I choose not to go into great detail about it.
Triangle normals are calculated for each new diamond in estimateNormal(), by evaluating the function at two nearby points on the surface (the distance is specified by SurfaceMesh.normalDelta), taking a cross product, and normalizing. This means that the function is evaluated a total of three times for every diamond, which could lead to very complicated functions being drawn slowly. Since normals are only used to give the appearance of a correctly shaded surface, perhaps in such cases this calculation could be replaced by one that uses the cross product of the distances to a parent and ancestor vertex instead.
Priority Queues
As noted above, the 'priority queues' used in the plots are not real priority queues, but approximate priority queues using buckets. Split and merge queues are different classes (SplitQueue and MergeQueue, found in SurfaceMesh.java). Both inherit from BucketPriorityQueue (found in its own file), which handles the logic behind adding/removing elements to/from the queues. The queues override the function clamp(), which associates a bucket index (a positive integer less than the number of buckets) to a floating point value. In the code, the total error for a diamond is used as this float value, so the diamonds are sorted according to error. The first bucket (index 0) is used as a 'storage bucket', where diamonds that are culled are placed. Diamonds in this bucket are never split. This is done to avoid performing operations on diamonds that are outside the viewing volume. As for the particular functions used in Split-/MergeQueue.clamp(), they have been found by trial and error. It's hard to find a good error measure, since there's no upper bound on the possible error of a diamond, while errors have to be assigned to a fixed number of buckets. Errors also tend to fluctuate a lot, with errors sometimes being very small. Any linear mapping tends to clump together diamonds in the first and last buckets, which is bad for obvious reasons (if all diamonds are in the same bucket, splitting/merging will basically be random). I've found exponential functions to work much better, which is why they're used for both buckets. Note that both these functions will tend to sort most diamonds in the highest bucket initially, since initial errors often are very large. This does not really matter, since all diamonds in the initial stages of the mesh will be refined eventually, anyway.
TriList
This class is used to store the triangles used for rendering the mesh. It essentially consists of two large float arrays (or FloatBuffers), which hold vertex and normal data, respectively. Each triangle is represented as nine contiguous floats in each of the buffers. Furthermore, the triangles are guaranteed to always be stored sequentially without gaps in the vertex array. This means that the class is well suited for VBOs/vertex arrays. TriList isn't exclusive to surface meshes - it can be used for storing any triangle mesh.
With each element in a TriList is associated an instance of the class TriListElem. This class contains index information as well as pointers to the next and previous elements in the TriList (meaning that TriList is effectively a doubly linked list). TriList supports adding/removing triangles, as well as hiding/showing triangles. The latter two functions work by moving the triangle data to/from the associated TriListElem (so that no data is destroyed). TriListElems only store triangle data when they are hidden, in order to avoid redundancy.
An overridden version of TriList, called SurfTriList (found in SurfaceMesh.java) is used in the actual code. Its main purpose is to act as a middleman between SurfaceMesh and TriList, calculating triangle data from a diamond reference and triangle index. However, it also keeps track of the total visible surface area and error (used for level of detail).
DrawFunction2Var
This class is responsible for creating and calling optimize() in SurfaceMesh. It is rather tiny, but a few things about it are worth noting.
The update functions in DrawFunction2Var (realtimeUpdate(), updateForView(), updateForItself()) are called every frame, every time the view is changed, and every time the function is changed respectively. The SurfaceMesh.optimize() calls are issued only from realtimeUpdate(). It also calculates the current bounding radius and draws the mesh. updateForView() has two functions. Firstly, if the zoom level has been changed, the doUpdate flag in SurfaceMesh is reset, so that updates are again performed. Secondly, if the function being drawn is unbound, the current viewing sphere radius is compared to the radius when the current SurfaceMesh was created. If the new radius is larger than the old radius times a constant (unlimitedScaleFactor), the SurfaceMesh is scrapped and replaced by a new one with a surface radius of r*unlimitedScaleFactor, where r is the current radius. The same thing (but reversed) happens if the current radius is too small. updateForItself() scraps the old SurfaceMesh and replaces it with a new one.
Parametric plots
As stated above, the SurfaceMesh is equipped to handle parametric surface plots as well. Since an interface for parametric plots does not yet exist, I have not been able to expose this functionality to the user. I will instead describe in detail how this should be done.
As it stands, SurfaceMesh accepts an instance of GeoFunctionNVar as its function. Since parameter values and vertices are kept separated, adding surface plots would basically be as simple as letting GeoFunctionNVar handle functions f: R^2 -> R^3, (u,v) -> (g(u,v),h(u,v),i(u,v)). If this was handled in GeoFunctionNVar, there should be no need to change anything in SurfaceMesh.
One issue with parametric surface plots is that unlike ordinary height fields, they can't really be constructed without the user specifying intervals for the parameters. The reason for this is twofold; firstly, telling if a function such as (sin(u)*cos(v),sin(u)*sin(v),cos(u)) (a sphere) is periodic, or 'repeats itself', as the parameters tend to infinity is very hard without numerically. If this isn't handled, functions such as this will continue to be drawn 'onto' itself indefinitely. The second reason is that it's also very hard to tell 'where' in the parameter space to start rendering the function. For example, assuming that the camera is near the origin, for the function (u, v, 1), the natural area to start rendering the function would be in a neighborhood of (u,v)=(0,0), while for the function (u-1e20,v+1e20,1) (which is the same function except for a change of variables), the natural place to look for the function would be in a neighborhood of (u,v)=(1e20,-1e20). Finding the latter function would require either looking for 'small values' of the function parameters, or simply searching over a huge part of all the possible values (and there are a lot, so this would be very slow). For these reasons, I would recommend against adding a f(u,v)=(g(u,v),h(u,v),i(u,v)) command, and instead implementing something like Surface[g(u,v),h(u,v),i(u,v),u,interval start, interval end,v, interval start, interval end].
Curve Plots
Curve plots area created by using the command Curve[<some function>, <some function>, <some function>, <variable name>, <beginning of interval>, <end of interval>] (eg. Curve[t,cos(t),sin(t),t,-100,100]). The class used for drawing curve plots is called CurveTree. Each point in the CurveTree is represented by an instance of the class CurveTreeNode.
Since there seems to be a shortage of literature on the subject of curve plots, the algorithm for drawing curves was developed from scratch for this project. The algorithm used makes use of a binary tree (over the parameter interval), where each node contains data for one vertex in the tree. The tree is traversed top-down on every update, with new points being created dynamically as determined by refinement criteria (discussed in detail later).
Just as with parametric surface plots, there's appears to be no hope for allowing users to create curve plots without specifying intervals in which they should be drawn. Firstly, it is very hard to detect and handle periodic functions. A function that is periodic but isn't detected as such would continue to be drawn on top of itself indefinitely. Secondly, finding the intervals for the parameter for which the resulting function is visible in the viewing volume is very hard. For an illustration of this, see the section on parametric surface plots.
The algorithm that handles the refinement of the curve can be seen as acting on a set of connected segments, replacing one segment with two wherever the curve is not yet refined enough. In order for the tree to be representable as a set of segments even when the tree that holds the points only has less than two elements, two extra points have to be added (CurveTree.start and CurveTree.end). These two points also make sure that the endpoints of the curve, as specified by the user, are always visible. Every point in the representation of the curve can be regarded as belonging to a level n (corresponding to the depth of the point in the binary tree). The level of the root is defined as 1. Points at one level can be spaced of a minimum distance of dt/n apart, where dt is the size of the interval specified by the user. When a segment (a,b) is refined by the algorithm, one point (c), is added between a and b, and the segment (a,b) is replaced by the two segments (a,c),(c,b). Thus no previously computed points are wasted as the set is refined.
As a way to avoid problems that may arise with functions that appear to be almost linear when sampled at only tree points, the tree is automatically and uniformly refined down to a specified level (CurveTree.initialLevels) in the constructor of CurveTree, before the normal refinement routine is called. Note that the number of points in the curve grows exponentially with initialLevel, so high values will cause significant slowdowns. A value less than 10 should be sufficient.
A top-down approach to updates is taken, meaning that the curve is refined from the root every update. This can lead to visible jerks with high detail curves, which is why updates are not performed unless necessary. Just as with surface plots, users are expected to rotate the view much more often than they zoom, so an anisotropic approach is taken to level of detail, meaning that the tree is only updated when the zoom level changes.
The regular refinement of the mesh is handled by the function CurveTree.refine() (accessed through CurveTree.beginRefinement()). It recursively acts on two line segments defined by three consecutive points on the curve. Initially, it is called with the points start, root, and end, where root is the root of the tree. It refined depending on several criteria. Firstly, a forced refinement is performed for the first CurveTree.forcedLevels levels, in order to avoid problems with functions that appear nearly linear when sampled at few points. Note however that a simple form of culling is performed, so that segments outside the viewing sphere are never refined. At depths greater than forcedLevels, a combination of different tests are used to decide whether to refine or not.
The first and arguably most important of these tests (performed in angleTooSharp() simply tests if the angle between the two segments is large enough (ideally, since the functions that are drawn most often have continuous first derivatives, the angle difference should always be close to pi). It does this by comparing the dot product of the directions of the two segments with a specified constant (pCosThreshold). If the cosine of the angle is smaller than than pCosThreshold, the two segments are refined, unless they are not in the viewing volume, or their length is too small. If the cosine of the angle is larger than pCosThreshold, the tangents of the three points that define the two segments are compared. If the cosine of the angle between two points of a segment is smaller than another constant (tCosThreshold}), the segment is refined (again, only of it is visible and long enough).
The test to decide if two a segment is small enough is performed in distanceLargeEnough(), and essentially involves comparing the segment length to a minimum distance (defined by minParamDist) as well as a value depending on the scale defined by the EuclideanView3D used. If the length is in the interval ]minParamDist, distanceFactor/scale[, the segment is refined (CurveTree.distanceFactor is another level of detail constant).
As a way to avoid storing more data than necessary, no list of used points is kept throughout the refinement. Instead points are drawn one by one by calling PlotterBrush.addPointToCurve3D().
Tangents are approximated at each point by evaluating the function at a small distance (defined by CurveTree.deltaParam) from the point, and using the direction of a vector between these two points.
Just as with the surface plots, I've included a list of the important level of detail parameters found in CurveTree:
- public static final double discontinuityThreshold
- used by plotterBrush to judge if the curve has gone through a singularity between two consecutively added points. If the cosine of the angle between two points' tangents is greater than discontinuityThreshold, no line is drawn between the points.
- public static final double deltaParam
- the difference in the paramater value used for tangent estimations.
- public static final int initialLevels
- the number levels calculated in the constructor.
- public static final int forcedLevels
- a limit for the minimum amount of refinement done. All the points in the first forcedLevels levels of the tree are always drawn (unless culled).
- public static final double pCosThreshold
- the cosine of the minimum angle between two segments for them to not be refined.
- public static final double tCosThreshold
- the cosine of the minimum angle between the tangents of the two points in a line segment for it not to be refined.
- public static final double minParamDist
- the minimum allowed distance parameter-wise between two points.
- public static final double distanceFactor
- a scale factor used to decide the maximum distance between two points.
