Knife Through Mountains: A Procedural Shape Technique
When generating procedural shapes, there's an elegant technique I call "knife through mountains." Imagine a mountain range viewed from the side - jagged peaks and valleys stretching across the horizon. Now imagine slicing horizontally through it with a knife. The resulting silhouette is organic, irregular, and natural-looking.
The Core Idea
The technique works by generating 1D terrain (heights that vary along a line) and then "slicing" through it at a threshold level. Points where the terrain is above the threshold contribute to the shape; points below create indentations.
For a blob-like shape, we wrap this terrain around a circle. Each angle around the center has a terrain height, and the slice determines the radius at that angle.
radius = baseRadius + (terrainHeight - threshold) × amplitudeWhen terrainHeight > threshold, the shape bulges outward. When terrainHeight < threshold, it indents inward.
Generating the Terrain
The terrain needs to look natural - smooth in places but with irregular features. The key is layering multiple "octaves" of randomness, each at a different frequency and amplitude.
Control Points and Interpolation
Start with a set of random control points around the circle:
const numPoints = 6; // Base roughness
const heights: number[] = [];
for (let i = 0; i < numPoints; i++) {
heights.push(random()); // 0 to 1
}To get the height at any angle, interpolate between the nearest control points using cosine interpolation for smooth curves:
function getHeight(angle: number): number {
const normalizedAngle = angle / (Math.PI * 2); // 0 to 1
const index = normalizedAngle * numPoints;
const i0 = Math.floor(index) % numPoints;
const i1 = (i0 + 1) % numPoints;
const t = index - Math.floor(index);
// Cosine interpolation for smooth transitions
const smoothT = (1 - Math.cos(t * Math.PI)) / 2;
return heights[i0] * (1 - smoothT) + heights[i1] * smoothT;
}Cosine interpolation produces smoother results than linear interpolation. The curve eases in and out of each control point rather than making sharp turns.
Adding Octaves for Detail
A single layer of control points produces smooth, blobby shapes. To add natural-looking detail, layer multiple octaves:
function generateTerrain(
seed: number,
numOctaves: number,
baseRoughness: number,
) {
const random = createSeededRandom(seed);
const octaves: { heights: number[]; numPoints: number }[] = [];
for (let oct = 0; oct < numOctaves; oct++) {
// Each octave doubles the frequency
const numPoints = baseRoughness * Math.pow(2, oct);
const heights: number[] = [];
for (let i = 0; i < numPoints; i++) {
heights.push(random());
}
octaves.push({ heights, numPoints });
}
return octaves;
}When sampling the terrain, combine all octaves with decreasing influence:
function getTerrainHeight(angle: number, octaves: Octave[]): number {
let totalHeight = 0;
let amplitude = 1;
for (const octave of octaves) {
const height = interpolateOctave(angle, octave);
totalHeight += height * amplitude;
amplitude *= 0.5; // Each octave contributes half as much
}
// Normalize to 0-1 range
const maxPossible = 2 * (1 - Math.pow(0.5, octaves.length));
return totalHeight / maxPossible;
}This is the same principle behind fractal noise and terrain generation:
- Octave 1 (6 points): Large-scale shape - major bulges and indentations
- Octave 2 (12 points): Medium detail - secondary bumps
- Octave 3 (24 points): Fine detail - small irregularities
- Octave 4 (48 points): Micro detail - subtle texture
Each layer adds detail at a smaller scale while preserving the overall form.
The Slice
With terrain generated, the slice converts heights to radii:
const centerX = 50;
const centerY = 50;
const points: Point[] = [];
for (let i = 0; i < resolution; i++) {
const angle = (i / resolution) * Math.PI * 2;
const terrainHeight = getTerrainHeight(angle, octaves);
// The slice
const heightDiff = terrainHeight - threshold;
const radius = baseRadius + heightDiff * amplitude * 2;
points.push({
x: centerX + Math.cos(angle) * radius,
y: centerY + Math.sin(angle) * radius,
});
}The threshold parameter is the knife height. Changing it shifts the entire silhouette:
- Low threshold (0.2): Most terrain is "above" the knife, creating a large, bulbous shape
- Mid threshold (0.5): Balanced - some parts bulge out, some indent
- High threshold (0.8): Most terrain is "below" the knife, creating a small shape with spiky protrusions where peaks poke through
Converting to SVG
The points form a closed polygon. For a basic implementation, connect them with line segments:
let path = `M${points[0].x},${points[0].y}`;
for (let i = 1; i < points.length; i++) {
path += ` L${points[i].x},${points[i].y}`;
}
path += " Z";With sufficient resolution (64-128 points), the line segments are small enough to appear smooth. For even smoother results, you could use Catmull-Rom splines or cubic bezier curves, but high resolution usually suffices.
Parameters and Their Effects
The technique has intuitive parameters:
| Parameter | Effect |
|---|---|
| Seed | Different random terrain - completely changes the shape |
| Roughness | Base control points - more = more peaks and valleys |
| Octaves | Detail layers - more = finer, more natural-looking edges |
| Amplitude | How much radius varies - higher = more dramatic bulges/indents |
| Threshold | Knife height - shifts between bulbous and spiky shapes |
| Base Radius | Minimum size - the shape's overall scale |
Why This Works
The technique produces natural-looking shapes because it mimics how natural boundaries form. Coastlines, cloud edges, and organic forms often result from threshold processes - where some continuous field (elevation, moisture, density) crosses a critical value.
The multi-octave terrain provides self-similarity across scales, which is characteristic of natural forms. Large features have smaller features on top of them, which have even smaller features, and so on.
Variations
Multiple Shapes
Generate several shapes at different thresholds from the same terrain:
const thresholds = [0.3, 0.5, 0.7];
const paths = thresholds.map((t) => generateShape(terrain, t));This creates nested, concentric-ish shapes that share a visual relationship.
Asymmetric Terrain
Weight the octaves differently on different sides:
const eastAmplitude = angle > Math.PI ? 1.2 : 0.8;
totalHeight += height * amplitude * eastAmplitude;This creates directional bias, useful for shapes that should look wind-blown or stretched.
Animated Threshold
Animate the threshold over time to create a "breathing" or "pulsing" effect:
const threshold = 0.5 + Math.sin(time * 0.5) * 0.2;The shape smoothly morphs between bulbous and spiky states.
Comparison to Other Techniques
vs. Perlin/Simplex noise contours: The knife-through-mountains technique is simpler to implement and understand. True 2D noise fields can create more complex shapes (islands, archipelagos, holes), but require marching squares or similar algorithms to extract contours.
vs. Polar coordinate blobs: Simple polar blobs (radius = baseRadius + noise(angle)) are a special case of this technique with one octave. Adding multiple octaves and the threshold concept gives more control and variety.
vs. Metaballs/implicit surfaces: Metaballs create smooth, blobby shapes through field composition. They're better for shapes that should merge and split. The terrain slice technique is better for single, irregular shapes with natural-looking edges.
Implementation Notes
Seeded Randomness
Use a seeded random number generator so the same seed always produces the same shape:
function createRandom(seed: number) {
let state = seed;
return () => {
state = (state * 1664525 + 1013904223) % 4294967296;
return state / 4294967296;
};
}This simple LCG (Linear Congruential Generator) is fast and sufficient for this use case.
Pre-generate Random Values
Generate all random values upfront before using them. This prevents the RNG consumption order from affecting results when parameters change:
// Good: pre-generate all values
const octaveData = computed(() => {
const random = createRandom(seed.value);
// Generate all heights here...
});
// Then use octaveData in the shape generationResolution vs. File Size
Higher resolution means more points in the SVG path, which increases file size. For most uses, 64-128 points is a good balance. For very large shapes or close-up viewing, consider 256.
Conclusion
The knife-through-mountains technique is a simple, intuitive way to generate organic shapes. By layering octaves of interpolated random values and slicing at a threshold, you get natural-looking silhouettes with predictable, tweakable parameters.
The mental model - a knife cutting through a mountain range - makes the parameters intuitive: roughness controls how many mountains, octaves add detail, amplitude controls how tall they are, and threshold is where you cut.
Try the interactive demo to see how each parameter affects the result.