Ben Traje
← Back to unity

Procedural Stripe Shaders in Unity: Dynamic Patterns with HLSL

18 Aug 26 (2mo ago)

Creating dynamic, customizable patterns for 3D objects often requires stepping beyond standard image textures. Relying on static Albedo maps means creating a new image file for every possible color combination or stripe layout. By utilizing Unity’s Shader Graph and HLSL (High-Level Shader Language), you can generate resolution-independent, mathematically perfect stripes that can be adjusted on the fly.

Here is a comprehensive guide to building a procedural stripe generator using a Custom Function Node.

Why Procedural Shaders Over Textures?

  • Resolution Independence: Mathematical stripes never pixelate, regardless of how close the camera gets to the object.
  • Dynamic Customization: Variables like color, stripe count, and gap thickness can be linked to C# scripts, allowing players to customize objects in real-time.
  • Memory Efficiency: A few lines of math calculate infinitely faster and take up significantly less disk space than multiple 4K texture maps.

The Setup: Custom Function Node

While Shader Graph is excellent for visual scripting, calculating dynamic loops and evenly distributed segments using standard nodes quickly turns into an unreadable web. The most efficient approach is packing HLSL code inside a Custom Function Node.

1. Node Configuration

  1. Open your Lit Shader Graph.
  2. Right-click and select Create Node > Custom Function.
  3. In the Graph Inspector (Node Settings), change the Type to String.
  4. Name the function (e.g., DynamicStripes).

2. Inputs and Outputs

To keep the graph clean, this specific script handles all internal coloring and gap settings internally, requiring only one input coordinate.

  • Inputs: Add a Float named UV_Y. (In your graph, route a UV Node into a Split Node, and plug the G channel into this input).
  • Outputs: Add a Vector 4 named OutColor.

The HLSL Code

Paste the following code into the Body text box of your Custom Function Node.

This script is built to handle up to 5 stripes. It dynamically calculates the available space on the object, subtracts the defined top/bottom margins (EdgeGap) and internal spacing (InnerGap), and divides the remaining area equally among the active stripes.

// ==========================================
// --- HARDCODED SETTINGS ---
// ==========================================

int StripeCount = 3;   // Switch between 1 and 5
float Smoothness = 0.01; // Anti-aliasing for stripe edges

// Margin and Gap Configuration
float EdgeGap = 0.15;  // Top and bottom secured white margins
float InnerGap = 0.04; // White space between the stripes

// Background Color
float4 BaseColor = float4(1.0, 1.0, 1.0, 1.0); 

// Stripe Colors (RGBA)
float4 Color1 = float4(1.0, 0.85, 0.75, 1.0); // Peach
float4 Color2 = float4(0.88, 0.95, 0.88, 1.0); // Mint
float4 Color3 = float4(1.0, 0.92, 0.65, 1.0); // Yellow
float4 Color4 = float4(0.75, 0.88, 0.95, 1.0); // Blue
float4 Color5 = float4(0.95, 0.8, 0.85, 1.0); // Pink

// ==========================================
// --- CORE LOGIC ---
// ==========================================

// 1. Calculate available space and divide equally
float SafeHeight = 1.0 - (2.0 * EdgeGap);
float TotalGaps = max(0.0, float(StripeCount) - 1.0) * InnerGap;
float StripeH = (SafeHeight - TotalGaps) / max(1.0, float(StripeCount));

// 2. Define exact start and end coordinates
float Start1 = EdgeGap;
float End1   = Start1 + StripeH;

float Start2 = End1 + InnerGap;
float End2   = Start2 + StripeH;

float Start3 = End2 + InnerGap;
float End3   = Start3 + StripeH;

float Start4 = End3 + InnerGap;
float End4   = Start4 + StripeH;

float Start5 = End4 + InnerGap;
float End5   = Start5 + StripeH;

// 3. Generate precise isolation masks
float mask1 = smoothstep(Start1, Start1 + Smoothness, UV_Y) - smoothstep(End1, End1 + Smoothness, UV_Y);
float mask2 = smoothstep(Start2, Start2 + Smoothness, UV_Y) - smoothstep(End2, End2 + Smoothness, UV_Y);
float mask3 = smoothstep(Start3, Start3 + Smoothness, UV_Y) - smoothstep(End3, End3 + Smoothness, UV_Y);
float mask4 = smoothstep(Start4, Start4 + Smoothness, UV_Y) - smoothstep(End4, End4 + Smoothness, UV_Y);
float mask5 = smoothstep(Start5, Start5 + Smoothness, UV_Y) - smoothstep(End5, End5 + Smoothness, UV_Y);

// 4. Composite the final image
float4 finalColor = BaseColor;
if (StripeCount >= 1) finalColor = lerp(finalColor, Color1, clamp(mask1, 0.0, 1.0));
if (StripeCount >= 2) finalColor = lerp(finalColor, Color2, clamp(mask2, 0.0, 1.0));
if (StripeCount >= 3) finalColor = lerp(finalColor, Color3, clamp(mask3, 0.0, 1.0));
if (StripeCount >= 4) finalColor = lerp(finalColor, Color4, clamp(mask4, 0.0, 1.0));
if (StripeCount >= 5) finalColor = lerp(finalColor, Color5, clamp(mask5, 0.0, 1.0));

OutColor = finalColor;

Breaking Down the Mechanics

The Double smoothstep Masking

Typically, a single smoothstep acts as a threshold, turning a color "on" from a specific coordinate upward. To create isolated bands, the code uses mathematical subtraction:

  • smoothstep(Start) activates the color at the bottom boundary.
  • - smoothstep(End) deactivates the color at the top boundary.

This ensures the stripe exists strictly within those two variables. The clamp function in the final lerp statements ensures that floating-point inaccuracies during this subtraction do not generate visual artifacts or inverted colors.

Object Space vs. UV Space

This implementation relies on UV Coordinates.

  • UV Mapping guarantees a perfect $0$ to $1$ gradient from the bottom of the object to the top.
  • Object Space Position evaluates actual 3D coordinates. If a 3D model's pivot point is precisely in the center, its bottom half relies on negative Y values. While Object Space avoids UV seams, the math must be offset to accommodate negative coordinates (e.g., -0.5 to 0.5 for a $1$-unit tall object). Sticking to UV space is recommended for isolated 2D-style patterns.