Bezier Generator

The bezier generator is the default and most configurable of the centerline generators. It samples a set of random grid-jittered corner anchors, sorts them by angle around their centroid to form a simple star-shaped polygon, and assembles a closed cubic Bézier spline through those corners. Handle lengths are proportional to rad * chord and adaptively clamped so that no handle overshoots a nearby corner. An optional per-environment style draw broadens the shape family within a single batch. Any smooth curve that still self-crosses after clamping is rescued by its provably simple corner polygon, which the downstream XPBD solver re-rounds.

Sample tracks produced by the Bezier generator

Sample tracks produced by the Bezier generator at the default configuration.

How It Works

The generator runs a fixed sequence of Warp kernels in a single pass — no host-side retry loop, no per-environment Python branching:

  1. Optional style draw. When style_sampling=True, a dedicated kernel draws per-environment rad, scale, and handle_clamp_frac values from their respective *_range fields using RNG salt 2741. A range of None collapses to the corresponding scalar, so individual knobs can stay fixed while others vary. This Python branch is resolved at CUDA-graph capture time; the per-environment values are device arrays and the captured graph remains static.

  2. Corner count. _corner_count_sample_k draws count[e] uniformly from [min_num_points, max_num_points] for each environment using RNG salt 6151.

  3. Corner positions. _corner_sample_k (or _corner_sample_style_k when style-sampling is active) samples max_num_points grid cells from a num_cells × num_cells grid where num_cells = int(1 / (min_point_distance * 2)). Each cell selection uses bounded duplicate rejection (up to eight retries per corner) to spread corners across distinct cells, then adds per-axis sub-cell jitter. Coordinates are multiplied by scale (or by the per-environment scale[e] in style mode). RNG salt for positions: 9781.

  4. Prune-then-sort. _ccw_sort_k sorts only the first count[e] corners by atan2 angle around the centroid of those same kept corners (not all max_num_points slots), writing NaN past the live count. Sorting after pruning matters: sorting all max_num_points points and keeping a prefix would compute the centroid over unpruned slots, producing angular wedges around the wrong centroid that can close as figure-eight loops.

  5. Vertex tangents. _vertex_tangents_k computes each corner’s tangent as a normalized blend of the outgoing unit edge direction u_out and the incoming unit edge direction u_in, weighted by p = atan(edgy) / π + 0.5. At the default edgy=0, p=0.5 gives a symmetric bisector tangent. It also records each corner’s shorter incident edge length for the adaptive clamp (step 6).

  6. Cubic Bézier assembly. _assemble_k emits num_points_per_segment dense samples for each closed edge (corner i to corner (i+1) % count[e]), including the wrap-around closing edge. For each segment the handle lengths are computed as:

    \[h_0 = \min\!\bigl(\texttt{rad} \cdot \text{chord},\; \texttt{handle\_clamp\_frac} \cdot \min_{\text{adj}}(e_0)\bigr)\]
    \[h_1 = \min\!\bigl(\texttt{rad} \cdot \text{chord},\; \texttt{handle\_clamp\_frac} \cdot \min_{\text{adj}}(e_1)\bigr)\]

    where \(\min_{\text{adj}}(e)\) is the length of the shorter incident edge at that corner. The cubic control points are then:

    \[P_1 = c_0 + \hat{t}_0 \cdot h_0, \qquad P_2 = c_1 - \hat{t}_1 \cdot h_1\]

    and the segment is evaluated via the standard Bernstein basis (see Math section). In style mode _assemble_style_k reads rad[e] and handle_clamp_frac[e] from the per-environment device arrays drawn in step 1.

  7. Dense-to-N arc-resample. _arc_resample_inplace converts the dense max_num_points × num_points_per_segment loop to exactly num_points arc-uniformly spaced samples, building cumulative arc length in float64 and interpolating with a searchsorted lookup.

  8. Self-intersection check and fallback. self_intersections_inplace counts proper crossings on the num_points-point resampled loop. For environments where the Bézier curve self-crosses, _assemble_polygon_selected_k emits the straight corner polygon (equivalent to handle_clamp_frac=0) and _arc_resample_selected_inplace overwrites only those rows. _select_vec2_k then writes the Bézier result for non-crossers and the polygon result for crossers into the output buffer.

  9. Generation flag. All environments are marked out_valid_wp=1 unconditionally. The generation gate is always True; geometric validity is decided later by the shared post-relax inflation gate.

Math

Cubic Bézier segment. Each corner-to-corner segment is a standard degree-3 Bézier curve parameterised by \(t \in [0, 1]\):

\[B(t) = (1-t)^3 P_0 + 3(1-t)^2 t\, P_1 + 3(1-t)t^2 P_2 + t^3 P_3\]

where \(P_0 = c_0\) and \(P_3 = c_1\) are the adjacent corner anchors, and \(P_1\), \(P_2\) are the interior control (handle) points constructed below.

Vertex tangent blend. The tangent direction at corner \(i\) is:

\[p = \frac{\arctan(\texttt{edgy})}{\pi} + 0.5, \qquad \hat{t}_i = \operatorname{normalize}\!\bigl(p\,\hat{u}^{\text{out}}_i + (1-p)\,\hat{u}^{\text{in}}_i\bigr)\]

where \(\hat{u}^{\text{out}}_i\) is the unit vector from corner \(i\) to corner \(i{+}1\) and \(\hat{u}^{\text{in}}_i\) is the unit vector from corner \(i{-}1\) to corner \(i\). At edgy=0, \(p = 0.5\) and the tangent bisects the two edge directions symmetrically. As edgy grows, \(p \to 1\) and the tangent aligns with the outgoing edge, sharpening the exit from each corner.

Handle construction. For each segment from \(c_0\) to \(c_1\) with chord \(\|c_1 - c_0\|\):

\[h_i = \min\!\bigl(\texttt{rad} \cdot \|c_1 - c_0\|,\; \texttt{handle\_clamp\_frac} \cdot l_{\min,i}\bigr)\]

where \(l_{\min,i}\) is the shorter of the two edges incident at corner \(i\). The interior control points follow:

\[P_1 = c_0 + \hat{t}_0 \cdot h_0, \qquad P_2 = c_1 - \hat{t}_1 \cdot h_1\]

The clamp prevents \(h_i\) from exceeding handle_clamp_frac times the shorter adjacent edge, so a long handle cannot overshoot a nearby corner and cause a self-crossing.

Parameters

All fields are part of TrackGenConfig (see the Configuration reference for the full list, defaults, and validation rules).

min_num_points

Minimum corner-anchor count per environment. Must be >= 2.

max_num_points

Maximum corner-anchor count per environment. A wider range adds more per-environment shape diversity. Also sets the buffer stride P.

num_points_per_segment

Dense Bézier samples emitted per corner-to-corner segment before arc-resampling. Must be >= 2; higher values give the arc-resampler a finer cumulative-arc-length grid, reducing interpolation error.

min_point_distance

Minimum grid-cell spacing for corner sampling. Controls the corner grid as num_cells = int(1 / (min_point_distance * 2)). Must be in (0, 0.5].

min_angle

Minimum interior angle (radians) at constrained corners. Used by the gate-angle validity kernel; not applied directly to centerline generation on the current single-pass path.

rad

Handle length as a fraction of the segment chord — the primary curviness dial. Only live when rad <= handle_clamp_frac; if rad exceeds the clamp, handle_clamp_frac binds every segment and rad has no independent effect.

edgy

Corner-tangent blend weight. Converted as p = atan(edgy) / π + 0.5. At the default 0.0, p = 0.5 gives a symmetric bisector tangent; larger positive values push p toward 1, making turns asymmetrically sharper (outgoing-edge-dominant).

scale

Isotropic scale multiplier applied to sampled corner positions. Scales all track coordinates; interacts with spacing and half_width through the coordinate range.

handle_clamp_frac

Adaptive handle clamp. Each corner’s handle is capped at handle_clamp_frac × (its shorter incident edge) to prevent overshoot past a nearby corner. Set equal to rad (the default 0.4) so the clamp only trims genuine overshoot; set below rad to bind every handle regardless of rad (near-polygonal tracks); set very large to disable.

style_sampling

Opt-in per-environment Bézier style randomisation (default False). When True, each environment draws its own rad, scale, and handle_clamp_frac from the *_range fields, so a single batch spans a family of styles.

rad_range

(lo, hi) range for per-environment rad when style_sampling=True. None collapses to the scalar rad for all environments.

scale_range

(lo, hi) range for per-environment scale when style_sampling=True. None collapses to the scalar scale for all environments.

handle_clamp_frac_range

(lo, hi) range for per-environment handle_clamp_frac when style_sampling=True. None collapses to the scalar handle_clamp_frac for all environments.

What Makes It Distinct

The Bezier generator is the default and most knob-rich of the six generators:

  • Widest style control. Nine primary knobs (min_num_points, max_num_points, num_points_per_segment, min_point_distance, min_angle, rad, edgy, scale, handle_clamp_frac) plus the opt-in style_sampling family are all exclusive to the corner-Bézier representation. No other generator exposes per-env handle geometry or a per-batch style sweep.

  • Cubic Bézier representation. Compared to the hull generator (which uses a closed uniform Catmull-Rom spline) and the polar generator (also Catmull-Rom), the Bézier formulation gives explicit, per-corner handle control through rad and edgy. The hull generator adds radial midpoint displacement for pronounced lobes and pinches; the Bézier generator keeps the shape entirely corner-determined.

  • Star-shaped corner families. Like the hull generator the Bézier generator samples Cartesian corners and angle-sorts them. Unlike the polar generator, which starts from polar control knots and is smooth and centered by construction, and unlike the voronoi generator, which snaps anchors to a pre-sampled site field, the Bézier generator’s shapes are anchored to sampled grid corners and are roughly star-shaped around the corner centroid. Unlike the checkpoint generator — which steers a bounded-turn path through radial checkpoints and produces organically curving loops — the Bézier generator is purely geometric and not trajectory-based.

  • Lowest relaxation burden at the default config. At the default handle_clamp_frac=0.4 (matching rad=0.4) and fat-band settings the valid yield is approximately 99.9 % end-to-end, and the generator produces consistently smooth, well-spaced centerlines before relaxation begins.

Fallback and Validity

Local polygon fallback. After arc-resampling to num_points, any environment whose Bézier centerline contains at least one proper self-crossing is redirected to its provably simple corner polygon (straight line segments between the sorted corners). Only the affected environments are reprocessed; non-crossing environments are unaffected. The XPBD bead-chain solver then re-rounds the polygon fallback through its bending and separation corrections.

Generation-stage validity. The generator writes out_valid_wp=1 for every environment unconditionally. The generation stage does not gate on geometry. Turning number, track thickness, NaN presence, and width-floor checks are all evaluated later by the shared post-relax inflation validity gate (_validity_k inside inflate_warp). The final Track.valid field therefore reflects post-relax geometric quality, not generation success.