← Back to all posts

QuickTile Studio: From First Commit to an Asset Store Package

Published:
unitytoolingcandyrush

QuickTile Studio is a Unity editor tool that turns a material into a seamless, tileable texture and exports it as a sprite-ready PNG. It is listed on the Unity Asset Store as version 1.0.0.

The repository was created on January 13, 2026. By January 18 it held a restructured package, a demo scene, a changelog and a PDF manual. From the git history and the store listing:

Repository created2026-01-13 (UTC)
Package restructure and PDF manual2026-01-18
Commits35, including the initial commit
Shipped C#20 files, 3,017 lines (13 runtime files, 7 editor files)
Demo textures43.9 MB down to 4.6 MB
Store listing1.0.0, Unity 6000.3.2f1, Built-in and URP compatible, HDRP not, 5.3 MB, release date February 20, 2026

Where it came from

CandyRush needs ground textures that repeat without visible seams. On October 29, 2025, a "Seamless Tiling Preview" component landed in the game's repository: four quads in a 2x2 grid with mirrored UVs, an orthographic camera framing them, and a Randomize button. It grew through two pull requests. #113 (November 14) added rotation, eight lock flags, material tiling and offset controls, and capture to a sprite with the importer set to Repeat wrap. #150 (December 13) added live slider updates, a JSON settings file saved beside every capture, and a second tool for organic blob patches. An AI coding agent wrote those commits under my direction; I set the requirements and reviewed the results.

By January the tool had nothing CandyRush-specific in it. That is the test I use for whether an internal tool can be a product: delete the game, and see whether the tool still makes sense on its own. This one did.

The technique: mirror, then capture 2x2

Mirroring is the image-editor trick for seamless tiles, done with UVs. Four quads share one material:

  • top-left: normal UVs
  • top-right: flipped horizontally
  • bottom-left: flipped vertically
  • bottom-right: flipped on both axes

Every internal edge meets its own reflection, so the pixels match across it. The outer edges match too: the left edge of the top-left quad and the right edge of the top-right quad both sample the same column of the source. So the captured 2x2 block is one full period of the pattern, and it tiles with an ordinary Repeat wrap mode. That is why capture writes the PNG with wrapMode = Repeat and no further processing.

The core of it, from QuickTileGenerator:

// Base UV corners (vertex order: BL, BR, TL, TR)
Vector2 bl = new(0, 0), br = new(1, 0), tl = new(0, 1), tr = new(1, 1);

// Apply mirroring by swapping UV corners
if (sx < 0) { (bl, br) = (br, bl); (tl, tr) = (tr, tl); }
if (sy < 0) { (bl, tl) = (tl, bl); (br, tr) = (tr, br); }

// Apply UV rotation around center (0.5, 0.5)
if (rot != 0f)
{
    bl = RotateUV(bl, rot); br = RotateUV(br, rot);
    tl = RotateUV(tl, rot); tr = RotateUV(tr, rot);
}

Two decisions the history records

The 2x2 block is the product, not the center quad

The first day included a stripped-down proof of concept (commit message: "PoC my brain hurts"). It used one center quad as the source of truth, surrounded by rings of mirrored neighbours (3x3, 5x5, 7x7), with the camera framing only the center quad.

The preview looked seamless. The capture would not have been: a single quad's edges only match because its neighbours are flipped. Captured alone, it has seams on all four sides. About six hours later a commit message reads: "2x2 necessary for seamless tiling. at some point I knew this and forgot." The CandyRush tool had used 2x2 since October. The proof of concept was deleted on January 15.

The lesson I took: verify the property the output depends on, not the property the preview shows.

Rotate the sampling, not the geometry

In CandyRush #113, rotation was applied to a parent GameObject, which keeps the mirror symmetry intact at any angle in the preview. But under a fixed, axis-aligned capture camera, rotated geometry means the captured square is in general no longer one period of the pattern.

QuickTile Studio rotates the UVs instead, after mirroring, around the center of each quad. Shared edges still sample the same UV points, and the 2x2 period stays aligned with the capture square at every angle. The code comment states the requirement directly: rotation is "baked into texture sampling rather than world-space, which is required for correct tiling across independent chunks."

Turning "looks right" into a score

Mirroring guarantees that pixels line up. It does not guarantee the result looks good. The eye finds the fold lines, the center cross of the 2x2, wherever a strong feature reflects into itself. With five continuous parameters (tiling X and Y, offset X and Y, rotation) and a list of materials, clicking Randomize until something looks right does not scale.

So the package has two scanners. Random Sampling explores the whole parameter range. Guided Refinement searches a small neighbourhood around the current values: exhaustively, in odometer order, if the neighbourhood fits the evaluation budget, otherwise by random sampling.

Each candidate is rendered once and scored by three heuristics, all looking at the fold lines:

HeuristicWhat it measures
Fold Line Variancecontrast along the fold lines
Fold Gradientbrightness gradient perpendicular to the folds
Fold Edge Detectionedge strength in a band around the folds

The details that make it usable in an editor:

  • Reproducible. The candidate generator takes a seed (default 12345), so the same scan settings produce the same candidates.
  • Responsive. The scan is an IEnumerable stepped from EditorApplication.update, five evaluations per tick. The editor stays interactive, Cancel works mid-scan, and finishing or cancelling restores the generator's original parameters.
  • One render, many scores. Each candidate renders at scan resolution (default 256 px), and every enabled heuristic scores the same pixels. A top-K list is kept per heuristic, so the heuristics can be compared on one scan.
  • Nothing is lost. Candidates above the acceptance threshold (default 0.7) are saved as JSON, optionally with the image.
  • Smooth playback. In Guided Refinement, sampled candidates are sorted so the top inspector slider changes least often. Watching a scan run looks like the sliders sweeping rather than jumping.
  • Extensible. Heuristics are discovered by reflection, so adding one is a single class:
[TileHeuristic(DefaultEnabled = true, Order = 3)]
public sealed class FoldEdgeDetectionHeuristic : ITileQualityHeuristic
{
    public string Name => "Fold Edge Detection";
    public TileScanScore Evaluate(Color32[] px, int w, int h) { /* ... */ }
}

The scanning logic lives in the runtime assembly and the windows in the editor assembly, each with its own .asmdef, so the scoring code has no dependency on UnityEditor.

To be plain about the limits: the heuristics are proxies for perception. I have no measured study of how well they agree with human judgement. The tool presents them that way: they rank candidates for a person to review.

What I cut to ship

The fastest path to a package was removing things:

  • The blob-patch generator. It came over in the first commit (a 794-line FeatureGenerator) and was removed the same day. It was a second product.
  • A hosted WebGL version. A browser demo on my site was considered, recorded in a scratch notes file, and deferred. On January 15 the scratch notes were replaced with a TODO.md.
  • A hard URP dependency. The package defaults to the Built-in render pipeline, and URP materials ship as an included .unitypackage that URP users import. One package covers both pipelines without forcing a dependency on either.
  • Size in the generator itself. The first-commit generator was 697 lines; the shipped QuickTileGenerator is 226.

Packaging, January 14 to 18

The last stretch was product work rather than feature work:

  • a rename "to prep for asset store" on January 14
  • a demo scene with photorealistic and stylized material sets
  • a restructure into one Assets/QuickTileStudio folder with separate Runtime and Editor assemblies under the IrsikSoftware.QuickTileStudio namespace (111 files touched)
  • demo textures re-exported from 43.9 MB to 4.6 MB, about 90% smaller; the demo assets, not the code, were most of the package's weight
  • a changelog in Keep a Changelog format and a manual, exported to PDF on January 18

The store listing went live with version 1.0.0 and a release date of February 20, 2026.

A related small tool: LogSmith

The same month I tagged the first public releases of LogSmith, a logging package for Unity that installs as a UPM git URL.

0.1.0 was tagged on January 3 at 02:08 UTC. 0.2.0 followed 54 minutes later as a deliberate breaking change: the static entry point was renamed from LogSmith to Log. A class with the same name as its namespace forces callers into LogSmith.LogSmith whenever names collide. The release shipped with a find-and-replace migration table:

LogSmith.GetLogger -> Log.GetLogger
LogSmith.Logger    -> Log.Logger
LogSmith.Router    -> Log.Router

The cheapest time for a breaking rename is before anyone depends on the name.

What generalizes

Test an internal tool by deleting its host. If it still makes sense without the game, it can be a product.

Verify the output's invariant, not the preview's. The center-quad preview looked seamless and would have captured seams. The 2x2 period is the property that matters.

Automate what construction can't guarantee, and label proxies as proxies. Mirroring settles pixel continuity. The scanners address perception, and they rank candidates for a person rather than claiming to judge.

Cut scope early. The second generator, the WebGL demo and the render pipeline dependency all waited, and the package shipped.