logo
Kurashizu Blogwhere ideas flow
  • Home
  • News
  • Blog
  • About
  • Home
  • News
  • Blog
  • About

© 2026 Kurashizu. All rights reserved.

Admin·New Post·Service Status
Powered by Cloudflare·02.11.26
← Back to news

Curveball

Original article
mightyburger.net|22 points|2 comments|by toilet|Jul 4, 2026

Curveball

Curveball is my custom-built curve generation utility designed specifically for Neverball.

Depending on your preference, you can use it as a web tool or run it as a standalone desktop application by downloading a release from GitHub or compiling the source code yourself. If you already have the Rust toolchain installed, compiling is a nightmare compiling is actually quite straightforward.

The Motivation

While developing a new level set for the open-source title Neverball, I realized I needed more complex geometric shapes than what was readily available.

The game already includes a tool called curve.c, which served the needs of most existing level sets. However, it had a significant limitation: it could only produce circular arcs. (The only exception was Nevermania, where the creator, fwp, likely utilized custom scripts).

To bridge this gap, I began writing various scripts. Eventually, I decided to consolidate these fragmented scripts into a single, cohesive tool featuring a visualizer, similar to the approach taken by curve.js.

Evolution of the Tool

  • Basic standalone scripts
  • Abstracted logic for efficiency
  • Integrated visualizer
  • Future community-driven plugins

Understanding "Brushes"

In Neverball, levels are constructed using "brushes." The way these are defined is somewhat unconventional: a brush is essentially the intersection of multiple halfspaces.

To visualize this, imagine the brush initially occupies the entire universe. You then repeatedly "slice" the universe in half, discarding the unwanted portion until only a small, specific piece remains.

While this seems counterintuitive, it is done for technical efficiency, particularly to simplify the collision detection code.

The Data Format

These shapes are stored in text files using the Quake map format. Each "cut" is defined by three points on a plane. The sequence of these points is critical; an incorrect order will result in the wrong half of the universe being deleted.

Example: Data for a simple cube

{ "classname" "worldspawn" // brush 0 
{ 
( 0 0 64 ) ( 0 64 64 ) ( 64 0 64 ) mtrl/invisible 0 0 0 0.5 0.5 0 0 0 
( 0 0 0 ) ( 0 0 64 ) ( 64 0 64 ) mtrl/invisible 0 0 0 0.5 0.5 0 0 0 
( 0 0 64 ) ( 0 0 0 ) ( 0 64 64 ) mtrl/invisible 0 0 0 0.5 0.5 0 0 0 
( 64 0 64 ) ( 64 64 64 ) ( 64 64 0 ) mtrl/invisible 0 0 0 0.5 0.5 0 0 0 
( 64 64 0 ) ( 64 64 64 ) ( 0 64 64 ) mtrl/invisible 0 0 0 0.5 0.5 0 0 0 
( 0 0 0 ) ( 64 0 0 ) ( 64 64 0 ) mtrl/invisible 0 0 0 0.5 0.5 0 0 0 
} 
}

Solving for Convexity

Because of the halfspace logic, every individual brush must be convex. Consequently, complex curves must be approximated using a series of smaller, convex brushes.

ToolMethodCapability
curve.cManual point selectionCircular arcs only
CurveballConvex Hull AlgorithmComplex, varied curves

To automate this, I utilized the chull crate in Rust. By providing chull with a set of points, the algorithm determines the optimal way to construct the shape. This allowed me to prototype new generators rapidly. While this made manual texture assignment difficult, I simply use Trenchbroom to paint textures across multiple faces efficiently.

The Core Concept: Extrusions

The breakthrough in my design was the use of extrusions. Instead of defining every point, you define a 2D profile and extrude it along a 3D path.

By copying the profile along the path and running the vertices of adjacent sections through chull, the curve is formed. This creates a powerful combinatorial system: if I define mmm profiles and nnn paths, I can generate m⋅nm \cdot nm⋅n unique curves.

Extrusion Concept

Handling Orientation

A major challenge was rotating the profile as it moved along the path (essential for "revolve" paths). I initially discovered that knowing the current position and the direction of travel is insufficient for proper rotation.

To solve this, I define a Frenet Frame consisting of three vectors: T\mathbf{T}T (Tangent), N\mathbf{N}N (Normal), and B\mathbf{B}B (Binormal). To rotate a point, I multiply it by the matrix: [T,N,B]\left[ \mathbf{T}, \mathbf{N}, \mathbf{B} \right][T,N,B]

While one could use complex methods like rotation-minimizing frames or integrating Frenet-Serret formulas, the paths in Neverball are simple enough that I can define these vectors directly.

Implementation in Rust

The extrude function is the heart of the tool:

pub fn extrude<PRF, PTH>(
    n: u32, 
    profile: &PRF, 
    path: &PTH, 
    profile_orientation: ProfileOrientation,
) -> CurveResult<Vec<Brush>> 
where 
    PRF: Profile, 
    PTH: Path,
  • Profile: Generates a vector of points in a 2D plane (which can change along the path).
  • Path: Generates a point and a corresponding Frenet Frame.

Special Cases

While most shapes are handled by extrude(), Curveball also supports three specific types: Curve Classic, Curve Slope, and Rayto.

I implemented these separately to maintain a similar interface to curve.c, even though Curve Classic and Rayto could technically be achieved via clever use of the extrusion system. Curve Slope is the true outlier; it cannot be made via standard extrusion and instead consists of brushes that resemble triangular slices.

I have several levels currently on hold that require these specific shapes, and I am eager to see how the Neverball community utilizes Curveball to expand the game's geometry!