mesh-avatar-studio
일러스트를 코딩 에이전트와 로컬 편집기로 2D 메쉬 애니메이션 아바타로 변환
계층형 자산 컬렉션을 위한 결정적 생성기와 픽셀 편집기, 생성 시 그레이스케일 색상 매핑 및 OpenSea 메타데이터 내장
A deterministic generator and pixel editor for layered asset collections. Grayscale value-maps colorized at generation time, with OpenSea metadata built in.
디자인C# · PythonGitHub @corderro-artz온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
A deterministic generator and pixel editor for layered asset collections.
Most layered generators composite fixed images from an indexed stack. nfty puts a grayscale
colorizer at the core instead: a single value-map is cast to any color by rolling a hue and a
saturation over it, with every pixel's own lightness preserved. Those layers are ordered and
flattened into one image — the shape its namesake, the web3 NFT collection, is built from — so a
handful of drawings becomes thousands of distinct assets.README 전문 보기 ↗A deterministic generator and pixel editor for layered asset collections.
Most layered generators composite fixed images from an indexed stack. nfty puts a grayscale colorizer at the core instead: a single value-map is cast to any color by rolling a hue and a saturation over it, with every pixel's own lightness preserved. Those layers are ordered and flattened into one image — the shape its namesake, the web3 NFT collection, is built from — so a handful of drawings becomes thousands of distinct assets. A pixel editor is built into the application, so the art and the collection it belongs to are authored in one environment rather than two. The end result is an expansive, reusable asset catalog, or a finished drop with OpenSea metadata written beside every image.
None of it is approximate. The same book and the same seed produce byte-identical output on any machine, in any locale, on any CPU architecture, and the program counts the exact number of distinct assets a book admits before you ask it for any. The demo built into the binary admits 7,155,000 of them, out of twenty-nine 32×32 sprites.
| Rise: paint depth into a rolled recipe | The pop-out viewer, 2D beside 3D |
|---|---|
Using the app rather than working on it? Read the User Manual instead — this file is for developers. Its source is in
docs/manual/.
nfty is two front-ends over one engine. Nfty.Core holds the whole of it — the archive formats, the
colorizer, the roller, the rules engine, the rarity math and the text reports — and carries no UI or
CLI dependency, so the command line and the Avalonia desktop app produce identical bytes from
identical inputs. A collection is authored as a small tree of ZIP archives, generated against a
string seed, and published as images plus two parallel sets of per-asset metadata.
| Capability | Details |
|---|---|
| Value-map colorization | One grayscale drawing rolls a hue and saturation per asset; lightness is the art's, color is the collection's |
| Three layer kinds | Dynamic (rolled color), Static (one fixed color, no RNG), Custom (full-color RGBA, composited as-is) |
| Counted output space | UniqueSpace reports the exact number of distinct assets a book admits, and says which direction it is bounded in |
| Deterministic runs | SplitMix64 from a string seed; identical output across locales, machines and CPU architectures |
| Optional layers | A per-recipe chance that a layer is left out entirely, added without changing any existing seed's output |
| Incompatibility rules | Exclude and require constraints between named variants, enforced by re-rolling inside a bounded retry budget |
| Built-in pixel editor | Brush, eraser, shapes, line, flood fill, select-and-move, undo/redo — over a value-map or a color raster |
| Dual metadata | Standards-pure OpenSea metadata/NNNN.json plus a rich nfty/NNNN.json with DNA, seed, rarity and per-layer color |
| Export and sealing | Four independent content axes, a plan you read before it writes, and AES-256-GCM sealing for a view-only handoff |
| Extend | Re-open a cooked Set, add to it, and recompute rarity across the whole collection |
| Open formats | Every archive is a ZIP with a manifest.json; the custom extension is a renamed .zip |
| Part | What it is | Where to start |
|---|---|---|
App (Nfty.Desktop) | Avalonia desktop app: Explorer, pixel editor, Set browser, cook and export dialogs | Get started |
CLI (Nfty.Cli) | Every operation headless, for scripts and CI | Command line |
Ticket (Nfty.Ticket) | Dependency-free runtime API: roll an asset's identity from a Menu and a seed | Ticket and Menu API |
Menu (.mnu) | A CookBook flattened with its art, so a runtime rolls and renders with no .cbk | Roll assets at runtime |
Rise (Nfty.Model) | Pixel art to a 3D model (glTF): depth painting, models inside Sets | Make a 3D model |
| Viewer | Pop-out window for one asset or model, 2D and 3D | View an asset in its own window |
More in the User Manual: keyboard shortcuts, settings and the FAQ.
| Requirement | Version | Notes |
|---|---|---|
| .NET SDK | 10.0 | Pinned by global.json; needed to build, not to run a release |
| .NET desktop runtime | 10.0 | Only for the two framework-dependent download shapes |
| OS | Windows, macOS, Linux | The desktop head is Avalonia; the CLI is platform-agnostic |
| Screen | 1280×720 or larger | The app's window minimum; it opens at 1366×768 |
| Python | 3.10+ | Optional — the manual, the demo generator and the icon build are Python tools |
| Category | Packages |
|---|---|
| Imaging | SixLabors.ImageSharp — pinned to 3.1.11 on purpose; 4.0.0 requires a build-time license key |
| CLI | System.CommandLine |
| UI | Avalonia, Avalonia.Desktop, Avalonia.Themes.Fluent, Avalonia.Skia, Avalonia.HarfBuzz, CommunityToolkit.Mvvm |
| Composition | Microsoft.Extensions.DependencyInjection |
| Tests | xunit.v3, Avalonia.Headless.XUnit, coverlet.collector |
Package versions live in Directory.Packages.props and never in a
.csproj.
Prebuilt Windows builds are on the releases page. Every download carries both front-ends, the demo CookBook, and the licenses.
| Zip | Size | .NET 10 needed | For |
|---|---|---|---|
| portable | 85 MB | No | Unzip anywhere and run. Start here if you are unsure. |
| single-file | 77 MB | No | One .exe. Unpacks itself on first launch, so that launch is slower. |
| framework-dependent | 15 MB | Yes | A small folder; needs the .NET 10 desktop runtime. |
| single-file-net10 | 15 MB | Yes | One .exe with the runtime left out. |
Zips are named nfty-<version>-<rid>-<shape>.zip (tools/release/build.py makes them). The app is
Nfty.Desktop.exe; the command line sits in the cli folder.
nfty keeps its settings in a .nfty folder beside the executable, so a copy carries its own
recent-files list, palette and thread setting and writes nothing elsewhere. Move or delete the folder and nothing is
left behind.
To build from source:
git clone https://github.com/corderro-artz/nfty.git
cd nfty
dotnet build nfty.sln
Note: Native AOT is deliberately unsupported. It links, but the resulting app cannot resolve its own views (
ViewLocatorbuilds a type name at runtime) or read a.cbk(System.Text.Jsonreflection). A release must not contain a binary that starts and then does nothing.
A first collection, end to end, against the demo CookBook built into the program — no file to find and no archive to download:
dotnet run --project src/Nfty.Cli -- demo . # writes ChestDemo.cbk here
dotnet run --project src/Nfty.Cli -- inspect ChestDemo.cbk # what is in it
dotnet run --project src/Nfty.Cli -- validate ChestDemo.cbk # is it sound
dotnet run --project src/Nfty.Cli -- stats ChestDemo.cbk # what odds its weights imply
dotnet run --project src/Nfty.Cli -- generate ChestDemo.cbk --count 8 --seed hello --out ./out
Roll one asset at runtime from a Menu, with no .cbk present:
dotnet run --project src/Nfty.Cli -- menu ChestDemo.cbk --out chest.mnu
dotnet run --project src/Nfty.Cli -- serve chest.mnu --seed hello --out one.png
Every command takes --threads N (or the NFTY_THREADS variable); output is identical at any count.
The desktop app is the same engine with a UI on top:
dotnet run --project src/Nfty.Desktop
Its Landing screen carries Open the demo CookBook, which unpacks that same book beside the app and opens it. The demo is yours to break, and reopening it keeps whatever you did to it.
The demo is a small collection of layered chests: two Recipes, seven Ingredients covering all three layer kinds, two optional layers, one incompatibility rule, and 7,155,000 distinct assets out of twenty-nine 32×32 sprites — which is the argument for value-map colorization, made in a file you can open.
The domain is a cooking metaphor, and the words are the model — the file extensions follow them.
| Term | File | What it is |
|---|---|---|
| CookBook | .cbk | The top-level container: canvas size, collection metadata, weighted Recipes |
| Recipe | .rcp | A complete template for one character type — an ordered layer stack plus its incompatibility rules |
| Ingredient | .igt | One layer, or trait-category, with its weighted variant images |
| Variant | — | A single image with a weight and a name, held inside an Ingredient |
| Set | .set | The generated output: images, per-item metadata, rarity, and the seed that made it |
| Kitchen | .ktn | A workspace folder of loose parts; its contents are discovered by scanning, never recorded |
Generating a CookBook rolls a Recipe per asset, so one book yields a mixed collection. .cbk nests
.rcp nests .igt, so the archive layers mirror the domain layers.
| Kind | Source art | At generation time |
|---|---|---|
| Dynamic | Grayscale value-map | Rolls a hue and saturation per asset from a weighted range |
| Static | Grayscale value-map | Applies one fixed color, deterministically, consuming no RNG |
| Custom | Full-color RGBA | Composited exactly as-is; never recolored |
Dynamic and static take their value from the grayscale map and only their hue and saturation from
the color, which is what multiplies the output space. Custom carries no colorization at all, and
Validator refuses one that does.
A Recipe's layer stack is ordered bottom-to-top, and that order is the paint order — depth 1
paints first and sits furthest back. Because it is a list rather than a stored z, two layers can
never share a depth.
nfty move ingredient cat.rcp --id shades --to 3
…or drag a row in the desktop app. Reordering produces a different collection, not a re-render: the generator consumes one roll per layer in order, so moving a layer moves which roll reaches it.
Rejecting duplicates is the default, and it is a promise: no two assets in a Set are the same, and a run that cannot fill its count from the unique DNA space fails and states the real maximum.
It is not the only way. Allow repeats in the Cook dialog, or --unlimited on the command line,
keeps every roll — so any count is producible from any book, and identity is the token number as
ERC-721 defines it. The rules are still enforced and the weights still decide rarity: a low-weight
variant stays uncommon, a layer with a low appearance chance is rarer still, and the rarest asset in
the drop is the low-weight variant of a low-chance layer.
The two modes are not interchangeable for reproducibility. The same book and seed give a different
collection under each, because rejecting a duplicate spends a roll the other mode does not — which is
why set.json records uniqueDna alongside the seed.
nfty/
├── src/
│ ├── Nfty.Core/ The engine — no UI or CLI dependency (net10.0)
│ │ ├── Model/ Immutable domain records
│ │ ├── Formats/ ZIP + manifest IO, Validator
│ │ ├── Imaging/ Conversion, colorization, compositing, preview
│ │ ├── Generation/ RNG, rollers, DNA, rules engine, orchestrator
│ │ ├── Editing/ Layer depth, region edits, image import
│ │ ├── Output/ Set writer, dual metadata, extend loader
│ │ ├── Publish/ Export planning and AES-256-GCM sealing
│ │ └── Stats/ Rarity, and the reports both front-ends print
│ ├── Nfty.Ticket/ Roll + Menu runtime, no dependencies (net10.0)
│ ├── Nfty.Model/ Rise: pixel art to a 3D model, glTF (net10.0)
│ ├── Nfty.Cli/ System.CommandLine wiring (net10.0)
│ ├── Nfty.App/ Avalonia GUI — ViewModels, Views, Themes
│ └── Nfty.Desktop/ Desktop head — window, clipboard, pickers
├── tests/ Tests across five xunit.v3 projects
│ └── fixtures/ Archives an older build wrote, and still reads
├── docs/
│ ├── manual/ The end-user manual (Material for MkDocs)
│ ├── design/archive/ A dated visual record — history, not spec
│ └── superpowers/ Design specs, newest first
├── tools/ Demo, icons, docs capture, release build
└── nfty.sln
Source: docs/diagrams/generation-pipeline.mmd
Both rejections re-enter at the recipe roll and share one MaxRerollsPerAsset budget per asset. Spending it throws RuleConflictException or UniqueSpaceExhaustedException, depending on the cause. Dedup runs before the render, so a collision never costs a composited canvas — and it is the step Allow repeats turns off, which is why that mode can fill any count from any book.
The DNA is a SHA-256 over the recipe id, each layer's variant id, and the quantized color of
each colorized layer. Quantizing folds a continuous color space into something countable, which is
what lets stats tell you how many unique DNA a book can produce before you try to mint them.
Nfty.Core so the CLI and the GUI cannot
disagree about it.StringComparer.Ordinal; the seed hash is read little-endian rather than native-endian; anything
that sums stays sequential, because floating-point addition is not associative.GenerateStreaming is the sequential oracle the parallel path is tested
against at 1, 2, 3 and 8 threads.Exact, AtLeast, AtMost, Unknown — a
product built on an under-count bounds the truth in neither direction, and a bool could not say
so.Nfty.Core hands back live Image<Rgba32>; LoadedCookBook and
GeneratedSet are IDisposable and free the whole tree.dotnet run --project src/Nfty.Cli -- --help
| Command | What it does |
|---|---|
demo <dir> | Write out the built-in demo CookBook — embedded, so it works on a copy with nothing beside it |
inspect <file> | Print the tree of a .cbk / .rcp / .igt / .set / .tin / .mnu, or list a .ktn. --voxel adds a partial-alpha report |
validate <cbk> | Report every problem found, not just the first |
stats <cbk> | The odds the weights imply, per Recipe and per Variant |
preview <file> | Render a PNG exactly as generation would — one variant, or a whole rolled stack |
generate <cbk> | Generate a Set |
extend <cbk> <set> | Grow an existing Set, recomputing rarity across all of it |
menu <cbk> --out <mnu> | Write a .mnu Menu: the book's weights, rules, colorization and art, flattened so a runtime host can roll and render an asset with no .cbk present |
serve <mnu> | Roll (--seed) or render (--ticket) one asset from a .mnu Menu, through the same render generate uses |
model <png|igt> | Rise a picture (or one Ingredient variant, with its stored depth) into a 3D model: GLB, glTF + .bin, or glTF with the buffer inline. --front N --back M raises every opaque pixel at once |
export <set> | Publish a cooked Set: choose what goes with it, and in what shape |
new ingredient|recipe|cookbook|kitchen | Build an archive from a manifest and its parts |
add variant|ingredient|recipe|rule | Append to an existing archive |
move ingredient|recipe | Reorder a Recipe's layers, or a book's recipe listing |
remove rule | Remove one incompatibility rule, addressed by its 1-based position |
Errors surface as a message, never a stack trace — add --verbose for the trace.
Layered art only works if the pieces register against each other, so both front-ends can composite a layer against the ones it will actually sit between:
nfty preview cat.rcp --seed alpha # the whole stack, one deterministic roll
nfty preview cat.rcp --seed alpha --only body,shades # just those layers, at their real depths
export --seal is AES-256-GCM over PBKDF2-SHA256 at 600,000 iterations, and the view-only mark is
HMAC-bound to a second key derived from the same passphrase, so a third party cannot strip it. A
sealed export takes its own extension (.tin) rather than being a .set whose bytes are ciphertext,
because a recipient double-clicking it has to be told what it is before opening it.
There is deliberately no --passphrase flag anywhere — argv is visible to every process, lands in
shell history and is captured by CI logs. --key names a source instead: env:NAME, file:PATH,
stdin or prompt, and an unprefixed value is an error rather than a guess.
Note: Against the person holding the passphrase, the view-only mark buys a refusal in the product and nothing more — the format is open source. It is a lock on a door, not a wall.
The same Nfty.Core engine behind an Avalonia UI: an Explorer over the open CookBook, a per-type
detail pane, a Set browser over cooked output, and an Ingredient Editor with a full paint stack.
.igt files from the open Kitchen,
under and over the art while you draw.Measured inside the real app with Nfty.Core/Diagnostics/Perf.cs
— named scopes with wall time and managed allocation, free when off, enabled by NFTY_PERF=1. It
measures whole user-facing operations rather than microbenchmarks, which is why it is not
BenchmarkDotNet.
| Operation | Before | After | What changed |
|---|---|---|---|
| Generate 200 assets @ 512px | 3,020 ms | 662 ms | Rendering parallelized; rolling stays sequential |
| Write 200 assets @ 512px | 2,078 ms | 454 ms | Per-asset writes parallelized; rarity finished first |
| Set browser resize (8 steps) | 1,213 ms / 90 MB | 342 ms / 24 MB | Row chunking caches its last answer |
Thumbnail decode scales with the canvas the author chose — ~0.5 ms from a 64px source, 7.1 ms from a 1000px one — so a screenful of forty large tiles was ~280 ms of frozen UI per scroll. It decodes off the UI thread now. A ZIP archive is not thread-safe, so archive extraction stays sequential and only the decode runs wide, and anything that sums stays sequential too: a different addition order is a different number, and rarity and the DNA-space count are both promises.
dotnet build nfty.sln # build everything
dotnet test nfty.sln # run all tests
dotnet run --project src/Nfty.Cli -- --help
dotnet run --project src/Nfty.Desktop
dotnet test tests/Nfty.Core.Tests # one project
dotnet test --filter FullyQualifiedName~DnaTests # one class
dotnet test --filter FullyQualifiedName~DnaTests.Same_selection_same_dna
Tests are named Snake_case_sentences, which is what --filter matches. Fixtures are built in
memory from tiny synthetic images; there are no golden-image files, and every image assertion
reads a pixel. tests/fixtures/ is the one exception and the only place real archives are read from
disk — their value is that an older build wrote them and they still read, so they are never
regenerated to make a failing test pass.
Visual work is verified from a rendered frame, never from the markup:
NFTY_CAPTURE=1 NFTY_CAPTURE_DIR=./frames dotnet test tests/Nfty.App.Tests \
--filter FullyQualifiedName~VisualCapture
…then look at the PNGs. Nearly every GUI defect this project has fixed was found that way and would
have been missed by reading code. Three companion sweeps catch what a frame cannot:
DarkModeContrastTests scores every text run against the surface it lands on in both themes,
ThemeResourceTests proves every DynamicResource the markup names resolves in both, and
MinimumWindowFitTests drives every page at the smallest window the app allows.
TreatWarningsAsErrors and GenerateDocumentationFile are on, so a
warning or an undocumented public member fails the build. Zero warnings, always.Themes/Icons.axaml comes from
assets/icons/*.svg via python tools/icons/build.py; the demo .cbk comes from
tools/demo/. Edit the generator, re-run it, commit both — a test fails if the pair disagrees.Themes/Tokens.axaml, and a new color goes in
both theme dictionaries.docs/design/archive/. It is a dated record; the app has moved past it
by design, and a difference is not a defect.python tools/release/build.py <version> win-x64 # writes four archives to .release/
gh release create v<version> .release/*.zip
<Version> in Directory.Build.props, stated once and
carried by every assembly in the solution.ci.yml — builds and tests on Windows, audits the dependency
graph for advisories separately, and re-runs the engine and CLI on Ubuntu, macOS (arm64) and
Windows, because determinism is promised across architectures.docs.yml publishes docs/manual/ to GitHub Pages. The
site is built there rather than committed.Note: ImageSharp must stay at 3.1.11. Version 4.0.0 requires a build-time license key (
sixlabors.lic), which is account-specific and is not in this repository.
Note: The manual needs its own toolchain:
pip install -r docs/requirements.txt, thenmkdocs serve.docs_dirisdocs/manual, notdocs/, so the archived mockups and design specs stay out of the published site.
Note: GUI tests render through Avalonia's headless backend with Skia, so they run on Windows in CI. The engine and CLI carry the determinism guarantees and are the ones re-run cross-platform.
Note: Sealing needs
AesGcm, which is a platform capability rather than a language feature.Seal.IsSupportedanswers for the current machine.
| Resource | Link |
|---|---|
| Repository | github.com/corderro-artz/nfty |
| User manual | vaporsoft.dev/nfty |
| Latest release | GitHub releases |
| Developer briefing | CLAUDE.md · AGENTS.md |
| Deferred work | BACKLOG.md |
| Design specs | docs/superpowers/ |
| CI workflow | .github/workflows/ci.yml |
| Docs workflow | .github/workflows/docs.yml |
| Actions | GitHub Actions |
| Issues | GitHub issues |
| Pull requests | GitHub pull requests |
| License | LICENSE |
Read CLAUDE.md first. It is the real briefing — the domain model, the file formats, and the invariants that are load-bearing but not obvious from the code. Several of its rules are the kind you would otherwise only learn by breaking something. AGENTS.md is the short version.
main for the change.dotnet build nfty.sln and dotnet test nfty.sln before opening a pull request; both must be
clean, and a warning is a failure.MIT. The interface is set in IBM Plex, bundled with the app under the SIL Open Font License — that license travels with any redistribution of the binaries, which is why every release archive carries a copy.
Copyright © 2026 Corderro Artz / Vaporsoft.
set chance|depth|variant|recipe|cookbook|voxel-ready| Change a layer's absent chance, a variant's depth map or weight, a recipe's weight, book metadata; snap partial alpha |
remove ingredient|variant|recipe | Remove a layer, variant or recipe |
sheet grid <png> | Read, set or clear the grid a spritesheet carries |
| Vaporsoft | vaporsoft.dev |