Skip to content

kids.kapish.splines.paths

unity

Authoring components and mesh generation for spline paths, built on kids.kapish.splines.

Installation

Add to your Unity project's package manifest:

json
{
  "kids.kapish.splines.paths": "file:../../src/unity/kids.kapish.splines.paths"
}

Requires Unity 6000.0+, kids.kapish.splines, kids.kapish.terrains, and com.unity.splines.

Architecture

Where the split falls

kids.kapish.splines is the headless core — sample a SplineContainer into Strip ribbon data (StripCalculator). This package is the Unity-side layer on top of it: MonoBehaviour authoring components, Scene-view preview, and mesh generation. Take kids.kapish.splines alone if you only need strip data for gameplay; take this package when you want rendered paths in a scene.

kids.kapish.splines        spline → Strip (data + sampling)

kids.kapish.splines.paths  Strip → Mesh + components (this package)

Components

  • PathMeshStrip — owns a spline, calls StripCalculator to get strips, bakes meshes via PathMeshBuilder, holds the baked chunks. Rebuild() / TearDown(). The editor adds a save-to-asset step (#if UNITY_EDITOR).
  • SplineGeometrySourcePath — draws the strip in the Scene view without baking. Fast authoring loop: tweak settings, watch the ribbon, bake when happy.
  • SplineManager — operates over every gizmo and mesh beneath it: refresh, rebuild-all, tear-down-all, optional always-on preview.

Cross-section profiles

The mesh shape is driven by a PathProfile — a cross-section swept along the strip. A profile yields an ordered Left→Right list of PathProfilePoints (Across, Up, U); PathMeshBuilder densifies between them by WidthSubdivisions and sweeps the result ring-by-ring along the coarse samples, stitching consecutive rings with the same flip-flop diagonal alternation used before.

  • Across rides the strip's terrain-conformed Left/Right edges (LerpUnclamped, so values outside [0,1] extend past the edges for verges/kerbs). Up offsets along the sample frame's up vector.
  • FlatPathProfile (the default) is just [(0,0), (1,0)] — with WidthSubdivisions it reproduces the original even strip byte-for-byte, so this refactor is behaviour-preserving. CrownPathProfile is a worked non-flat example.
  • Profiles serialise polymorphically on PathMeshSettings.Profile via [SerializeReference], so they're inspector-selectable and swappable per path.

Known limitation (first cut): triangle normals still use the flat per-face + "always face up" flip from the even-strip era. That's correct for flat and gently-shaped profiles (crowns, shallow channels) but will misface near-vertical or overhanging profile faces (kerb walls, deep channels). Those want the analytic swept-surface normal (cross(pathTangent, crossSegmentDir)) instead of the flip heuristic — a follow-up once profiles grow vertical faces. Edge skirts are unchanged and still attach to the conformed Across 0/1 edges.

Object mode

Cross-section sweep is one mode over the shared Strip substrate; object mode is the sibling — tile template prefabs along the path and cage-deform each tile so it bends with the curve (boardwalks, fences, kerbs, sleepers) rather than sweeping a flat profile. PathObjectMeshBuilder consumes the same strip frame; PathObjectStrip bakes the result as one mesh per material.

  • Cage deformation. Each template's combined bounds define a unit cage: a vertex's normalised position maps Z → the tile's arc span (sampled continuously along the strip, so it bends), X → across (the strip's left↔right, or a fixed authored width when StretchToWidth is off), Y → up (height above the strip surface, sitting the template's lowest point on the bed). The template normal is rotated into the per-vertex frame basis {across, up, tangent}. Continuous arc sampling is what makes winding paths bend smoothly instead of faceting per tile.
  • Seeded + edit-stable. Template choice per tile comes from a per-index hash Float01(seed, index) — same seed + path gives the same layout every rebuild, and per-index (not running-sequence) hashing keeps it stable under edits. Re-roll by changing Seed.
  • Weighted templates. PathObjectTemplate carries a Weight; the pick is a seeded cumulative-weight walk.
  • Tiling. TileLength (or the first template's Z bounds × scale) sets the nominal tile span. CompressToFitcount = round(total / nominal), step = total / count so a whole number tiles the path with no end gap; off → floor at the exact nominal length.
  • Output. Triangles are grouped by source material into one MeshBuilder each, so a multi-material template bakes to one mesh + GameObject per material under Generated Objects. Meshes are saved as assets (editor-time bake, like PathMeshStrip).
  • Known gap. Sharp (≈90°) corners shear the cage; not special-cased yet (a future refinement — for now keep tiles short through tight turns, or break the spline).

PathMeshBuilder and mesh assembly

PathMeshBuilder turns a Strip into List<Mesh> chunks. It is a path-geometry generator, not a general mesh builder: it lays out width subdivisions, the diagonal triangulation pattern, edge skirts, and chunk splitting, and it emits flat per-face normals with an "always face up" flip plus unwelded vertices (one per triangle corner) for crisp flat shading on the path surface.

Shared-work note: the bulk of PathMeshBuilder (strip → triangles, skirts, normals) is path-specific and doesn't belong in a generic builder. Only the final assembly step (new Mesh + index-format selection + SetVertices/SetTriangles/SetNormals/SetColors) overlaps with kids.kapish.meshes' MeshBuilder. That accumulator is currently 2D-leaning (welded verts, RecalculateNormals, multi-UV vector fills) and does not accept custom per-vertex normals, so it can't host the path's flat-shaded geometry as-is. Consolidating means extending MeshBuilder into a full 3D accumulator (custom normals + skip-recalc + tangents) and then delegating the assembly tail of PathMeshBuilder to it — see the shared backlog.

File Structure

Runtime/
├── SplineManager.cs            # Bulk manager component
├── SplineGeometrySourcePath.cs              # Scene-view strip preview
├── PathMeshStrip.cs          # Spline → baked path mesh
├── PathMeshBuilder.cs        # Strip → mesh chunks (cross-section sweep)
├── PathMeshSettings.cs       # Mesh-gen config
├── PathObjectStrip.cs        # Spline → baked, cage-deformed template meshes
├── PathObjectMeshBuilder.cs  # Strip + templates → deformed meshes (object mode)
├── PathObjectSettings.cs     # Object-mode config
├── PathObjectTemplate.cs     # One weighted template prefab
└── Carrot.Splines.Paths.asmdef

Editor/
├── PathManagerEditor.cs
├── PathMeshStripEditor.cs
├── PathObjectStripEditor.cs
└── Carrot.Splines.Paths.Editor.asmdef

Dependencies

DependencyKind
kids.kapish.splinesruntime — Strip / StripCalculator
kids.kapish.terrainsruntime — terrain conforming via ITerrainSampler
com.unity.splinesruntime — SplineContainer

Build

Import via Unity Package Manager. Requires Unity 6000.0+.


Usage Guide

Authoring components and mesh generation for spline paths.

Authoring a path

  1. Add a SplineContainer (Unity Splines) and draw your path.
  2. Add a PathMeshStrip component (Add Component → Carrot/Splines/Path Mesh Strip).
  3. Tune StripSettings (width, spacing, terrain conforming) and the mesh settings.
  4. Add a SplineGeometrySourcePath to preview the ribbon live in the Scene view while you tweak.
  5. Hit Rebuild in the inspector to bake the mesh.

For a network of paths, put a SplineManager on a parent object to rebuild/tear-down everything beneath it at once.

Baking from code

csharp
using Carrot.Splines;        // Strip, StripCalculator (kids.kapish.splines)
using Carrot.Splines.Paths;  // PathMeshBuilder, PathMeshSettings (this package)
using UnityEngine;
using UnityEngine.Splines;

SplineContainer container = GetComponent<SplineContainer>();

Strip strip = StripCalculator.Compute(container, 0, new StripSettings { Width = 4f });

var meshSettings = new PathMeshSettings
{
    TargetSegmentLength = 5f,
    WidthSubdivisions = 2,
    EnableEdgeSkirt = true,
    ChunkLength = 50f,
};

List<Mesh> chunks = PathMeshBuilder.Build(strip, meshSettings, transform.worldToLocalMatrix);

Driving the component at runtime

csharp
using Carrot.Splines.Paths;

PathMeshStrip path = GetComponent<PathMeshStrip>();
path.Rebuild();                  // regenerate after moving the spline
int verts = path.TotalVertexCount;
// path.TearDown();              // release baked chunks

Object mode — boardwalk planks

Instead of a swept mesh, tile template prefabs along the path and cage-deform each tile so it follows the curve. Add a PathObjectStrip component (Add Component → Carrot/Splines/Path Object Strip) to a SplineContainer, populate ObjectSettings.Templates, and hit Rebuild.

csharp
using Carrot.Splines.Paths;
using UnityEngine;

var objects = GetComponent<PathObjectStrip>();
objects.ObjectSettings.Seed = 7;             // change to re-roll the whole layout
objects.ObjectSettings.TileLength = 0.6f;    // plank pitch (0 = the template's own Z length)
objects.ObjectSettings.CompressToFit = true; // stretch tiles slightly so a whole number fits exactly
objects.ObjectSettings.StretchToWidth = true; // map each plank across the full path width

objects.ObjectSettings.Templates.Add(new PathObjectTemplate { Source = plankCommon, Weight = 10f, Scale = 1f });
objects.ObjectSettings.Templates.Add(new PathObjectTemplate { Source = plankMossy,  Weight = 2f,  Scale = 1f });
objects.ObjectSettings.Templates.Add(new PathObjectTemplate { Source = plankBroken, Weight = 0.5f, Scale = 1f }); // rare

objects.Rebuild();

Templates are weighted, so plankBroken (weight 0.5) shows up occasionally among the common planks. Because template choice is seeded per tile, the layout is identical every rebuild — find a Seed you like and it stays. Each plank prefab should be modelled facing local +Z (the path tangent), centred on X (across), +Y up. On a winding path the planks bend to follow it, because each tile's vertices sample the strip frame continuously along their arc span.

Marsh boardwalk: drop the spline along the route, set TileLength to your plank pitch, and add a couple of low-weight weathered/broken variants for character. Sharp ~90° corners aren't special-cased yet — keep the spline's turns gentle, or break it at hard corners.

Tips

  • Just need strip data? — depend on kids.kapish.splines alone (no mesh generation pulled in). This package is only for rendered paths.
  • Model layout drifts on rebuild? — it shouldn't; it's seeded. If it does, something is feeding a changing Seed. Edit-stability is per-index, so lengthening the path only adds/changes planks past the edit, not before it.
  • Preview before you bakeSplineGeometrySourcePath shows the exact samples the calculator produces; bake once you're happy.
  • ChunkingChunkLength splits long paths into multiple meshes for vertex limits and culling.
  • Flat shading — path meshes use per-face normals with an upward flip, so the surface reads crisply lit regardless of slope.

Carrot