Appearance
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
StripCalculatorto get strips, bakes meshes viaPathMeshBuilder, 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/Rightedges (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)]— withWidthSubdivisionsit reproduces the original even strip byte-for-byte, so this refactor is behaviour-preserving.CrownPathProfileis a worked non-flat example.- Profiles serialise polymorphically on
PathMeshSettings.Profilevia[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 conformedAcross0/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
StretchToWidthis 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 changingSeed. - Weighted templates.
PathObjectTemplatecarries aWeight; the pick is a seeded cumulative-weight walk. - Tiling.
TileLength(or the first template's Z bounds × scale) sets the nominal tile span.CompressToFit→count = round(total / nominal),step = total / countso a whole number tiles the path with no end gap; off →floorat the exact nominal length. - Output. Triangles are grouped by source material into one
MeshBuildereach, so a multi-material template bakes to one mesh + GameObject per material underGenerated Objects. Meshes are saved as assets (editor-time bake, likePathMeshStrip). - 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 withkids.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 extendingMeshBuilderinto a full 3D accumulator (custom normals + skip-recalc + tangents) and then delegating the assembly tail ofPathMeshBuilderto 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.asmdefDependencies
| Dependency | Kind |
|---|---|
kids.kapish.splines | runtime — Strip / StripCalculator |
kids.kapish.terrains | runtime — terrain conforming via ITerrainSampler |
com.unity.splines | runtime — SplineContainer |
Build
Import via Unity Package Manager. Requires Unity 6000.0+.
Usage Guide
Authoring components and mesh generation for spline paths.
Authoring a path
- Add a
SplineContainer(Unity Splines) and draw your path. - Add a PathMeshStrip component (Add Component → Carrot/Splines/Path Mesh Strip).
- Tune
StripSettings(width, spacing, terrain conforming) and the mesh settings. - Add a SplineGeometrySourcePath to preview the ribbon live in the Scene view while you tweak.
- 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 chunksObject 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
TileLengthto 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.splinesalone (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 bake —
SplineGeometrySourcePathshows the exact samples the calculator produces; bake once you're happy. - Chunking —
ChunkLengthsplits 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.