<aside>

Overview

A small UE5 study : Geometry Script builds the parametric window frame, and a Voronoi shader turns the glass into a mosaic

</aside>

Proc_StainedGlass_01.mp4


1. Window Shape

Proc_StainedGlass - Shape.mp4

The window shape is procedurally generated with Geometry Script. I start from two rounded rectangles (top and bottom), mirror them across the vertical axis so they meet in a pointed center, then extrude and boolean to form the frame. UVs are generated procedurally with a simple planar projection.


2. Glass Material

Proc_StainedGlass - Material.mp4

This is a single-pass Voronoi mosaic in a custom node(HLSL). I scale UVs to make a grid, then for each pixel I check the 3×3 neighbor cells and place one jittered seed per cell. I keep the closest and second-closest distances (d1, d2), which gives me the standard Voronoi structure. When a candidate becomes the closest, I convert that seed back to UV and sample the source texture. Every pixel inside that cell reuses that color, which creates the mosaic look. I also mix in an optional per-cell random tint to push a stained-glass vibe without new textures.

for (int y = -1; y <= 1; ++y)
{

    for (int x = -1; x <= 1; ++x)
    {
        float2 g = float2(x, y);
        float2 cell = iUV + g;
     
        // generate a hash
        float2 h;
        h.x = sin(dot(cell, float2(127.1, 311.7)) + Seed * 6.2831853);
        h.y = sin(dot(cell, float2(269.5, 183.3)) + Seed * 4.1231056);
        h = frac(h * 43758.5453);

        // Center jitter around (0.5, 0.5)
        float2 r = (h - 0.5) * Jitter + 0.5;

       
        diff = (g + r) - fUV;
        dist2 = dot(diff, diff);

        // Track nearest and 2nd nearest
        if (dist2 < d1)
        {
            d2 = d1;
            d1 = dist2;

            nearestSeedUV = (cell + r) / Tiles;
            bestCellP = cell;
        }
        else if (dist2 < d2)
        {
            d2 = dist2;
        }
    }
}

// sample color from nearest seed location
float3 col = Texture2DSample(Tex, TexSampler, nearestSeedUV).rgb;

// random color hash
float3 RanCol ;
RanCol.x = frac(sin(dot(bestCellP, float2(12.9898, 78.233)) + Seed*2.0) * 43758.5453);
RanCol.y = frac(sin(dot(bestCellP, float2(39.3467, 11.135)) + Seed*3.0) * 43758.5453);
RanCol.z = frac(sin(dot(bestCellP, float2(73.1560, 52.235)) + Seed*5.0) * 43758.5453);

col = lerp(col, (col * RanCol) + 0.01, RandomColorBias);