An open-source cross-platform modelling editor for 2D puppet animation, with first class pen and touch support, designed to interoperate with the Live2D Cubism source format (.cmo3). Runs on Windows, macOS, Linux, and Android tablets with pen/touch input. Supports importing and exporting both CMO3 and MOC3 formats.
Status: Early development. Prebuilt downloads are on the Releases page.
Q: Can I support the Umamo project financially?
A: Umamo is completely open source and free, but if you would like to support the project you can join the Patreon or make a one time donation through Ko-Fi. All received funds go directly to paying hosting bills, hardware for testing, and platform developer fees. Excess funds will be banked for the future or distributed to other open source projects working with the Umamo Project.
Q: Does Umamo do animations yet?
A: Not yet. This is a long term stretch goal. Getting the modeling, UV editor, and physics interface perfected first will be important for animation later.
Q: Umamo doesn't support a newer version of Cubism/Krita/Clip Studio Paint file formats yet. Can you add support?
A: Yes, please search if the issue has already been reported and if not, open a new issue that describes the problem.
Q: Umamo corrupted my project and I lost my work! HELP!
A: Currently Umamo is alpha level software available for testing. Please make sure to make regular backups of your files. In fact, make second copies of your files explicitly for playing around with. Do not use the original copies.
Every tagged version publishes desktop builds on the Releases page. There are two different releases per platform:
| File | Note |
|---|---|
umamo-windows-x64-<version>.msi |
Windows installer. Installs for your user alone, with no administrator rights needed. |
umamo-macos-arm64-<version>.dmg |
Apple silicon Macs. Open it and drag Umamo to Applications. |
umamo-linux-<arch>-<version>.deb |
Debian 12+, Ubuntu 22.04+, Linux Mint 21+. |
umamo-linux-<arch>-<version>.rpm |
Fedora and other RPM distributions. |
umamo-<target>-<version>.zip/.tar.gz |
Java SDK not required, just unpack it and run it directly. |
umamo-<target>-<version>.jar |
You will need Java SDK 21 or higher to run. When Java's default would give Umamo less than 3 GB of memory, it restarts itself with room for up to half of your computer's memory. To always allow half, start it from a terminal: java -XX:MaxRAMPercentage=50 -jar umamo-<target>-<version>.jar. |
Targets: linux-x64, linux-arm64, windows-x64, macos-arm64, macos-x64. Check your download against the release's signed SHA256SUMS.txt (see Verifying downloads).
Signing. The MacOS downloads are signed and notarized by Apple, and the Linux RPM and the release's checksums are signed with the Umamo release key. Windows builds are not signed yet due to the high cost.
- Windows: SmartScreen might show "Windows protected your PC" for the installer and the archive version's
Umamo.exe. Choose More info -> Run anyway.
- Windows:
- Run
umamo-windows-x64-<version>.msi.- Umamo installs per user to:
%LOCALAPPDATA%\Programs\umamo,- A start menu entry will be added and
*.umafiles will be associated automatically. - Note: There is no confirmation screen, when the installer finishes it will exit without confirming.
- A start menu entry will be added and
- Uninstall through Windows Settings -> Apps.
- The zip archive version is portable and can be run directly after extracting:
Umamo\Umamo.exe.
- Umamo installs per user to:
- Run
- Debian, Ubuntu, Linux Mint:
sudo apt install ./umamo-linux-<arch>-<version>.deb.- Uninstall:
sudo apt remove umamo.
- Uninstall:
- Fedora: Import the Umamo release key once:
sudo rpm --import https://umamo.org/keys/umamo-signing-key.ascsudo dnf install --setopt=localpkg_gpgcheck=1 ./umamo-linux-x64-<version>.rpm.- Uninstall:
sudo dnf remove umamo.
- Uninstall:
- Other Linux:
tar xzf umamo-linux-x64-<version>.tar.gzand runumamo/bin/umamo. - MacOS: Open
umamo-macos-arm64-<version>.dmgand drag Umamo to Applications, or unzipUmamo.appand move it there.
The DEB and RPM install Umamo to /opt/umamo, add it to the application menu, and make it the app that opens .uma files. If you registered the tarball's umamo.desktop by hand before, remove that first (below), or the menu will show Umamo twice.
To upgrade, install the new version over the old one. Your customized settings will be kept on upgrade and uninstall.
If you need to reset your settings, then delete settings.json from:
- Linux:
~/.config/umamo - MacOS:
~/Library/Application Support/umamo - Windows:
%APPDATA%\umamo
The Umamo release key signs the RPM and every release's SHA256SUMS.txt. Its fingerprint is 67754C0DC58743270D574066F5499AE5C80A0F1C; the key is on umamo.org and in this repository at app/desktop/packaging/umamo-signing-key.asc. Download SHA256SUMS.txt and SHA256SUMS.txt.asc beside your files, then:
gpg --fetch-keys https://umamo.org/keys/umamo-signing-key.asc
gpg --verify SHA256SUMS.txt.asc SHA256SUMS.txt
sha256sum --check --ignore-missing SHA256SUMS.txtgpg --verify names the key it checked; compare its fingerprint with the one above. Every file also carries a build provenance attestation from GitHub: gh attestation verify <file> --repo umamoorg/umamo confirms it was built by this repository's release workflow.
The installers register the .uma type, and on MacOS Umamo.app declares it itself, so Finder can open .uma files with it. The portable zip and tarball register nothing on their own.
- Linux: The archives come with freedesktop.org files to register the mimetype manually. Put
umamo/bin/umamoon yourPATH(or edit theExec=line inumamo.desktopto its full path), then register them for your user:
xdg-mime install --novendor umamo/lib/app/resources/umamo-uma.xml
xdg-desktop-menu install --novendor umamo/lib/app/resources/umamo.desktopxdg-mime uninstall and xdg-desktop-menu uninstall with the same files to undo it.
Go to Help -> Open Log Folder to open the log folder location. When submitting bug reports, please attach the newest file. Log file locations:
- Linux:
~/.local/share/umamo/logs(or$XDG_DATA_HOME/umamo/logs) - MacOS:
~/Library/Application Support/umamo/logs - Windows:
%LOCALAPPDATA%\umamo\logs
Umamo is a Kotlin Multiplatform project (Gradle, Kotlin DSL) targeting desktop and Android from one shared codebase.
| Platform | Note |
|---|---|
| Windows | Regularly built and tested |
| Linux | Regularly built and tested |
| MacOS | Working, minimally tested. |
| Android | Not yet, incomplete graphics layer implementation is blocking it. |
| iPadOS | Application and UI loads, but there is no puppet renderer and JDOM is incompatible. |
- JDK 21 (LTS). The build is pinned to a 21 toolchain. Point
JAVA_HOMEat a JDK 21 — e.g.export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64. (Newer JDKs run Gradle fine, but are ahead of the Android toolchain.) Use a distribution JDK, not Homebrew's::desktop:createDistributableruns Compose'scheckRuntime, which hard-errors on a Homebrew JDK unless you pass-Pcompose.desktop.packaging.checkJdkVendor=false. - Android SDK with
platforms;android-36andbuild-tools;36.0.0. SetANDROID_HOME(or createlocal.propertieswithsdk.dir=…). Only needed for the Android target.
The Gradle wrapper (./gradlew) pins the Gradle version so no system Gradle install required.
./gradlew :desktop:run #Launch the desktop editor
./gradlew :android:installDebug #Deploy to a connected Android tablet
./gradlew build #Everything
./gradlew test #Unit tests
./gradlew ktlintFormat #Format all files according to .editorconfig
./gradlew -q :cli:run --args="dump model.moc3" #Dump a CMO3/MOC3's contents to stdout
./gradlew -q :cli:run --args="convert model.cmo3 out.moc3" #Convert between formats(Overwrites WITHOUT confirmation)
./gradlew -q :cli:run --args="diff a.cmo3 b.moc3" #Compare two models semantically
# Cross-target the desktop build (default: host). -Pumamo.target swaps the bundled Skiko + LWJGL
# natives: linux-x64 | linux-arm64 | windows-x64 | macos-x64 | macos-arm64.
./gradlew :desktop:packageUberJarForCurrentOS -Pumamo.target=windows-x64
# Self-contained app image with a bundled JRE (what the release workflow ships).
# → app/desktop/build/compose/binaries/main/app/
./gradlew :desktop:createDistributable-Pumamo.target is meaningless for createDistributable and must not be combined with it: an app image jlinks the host JDK into itself, so cross-resolving the natives would bundle one platform's Skiko/LWJGL inside another platform's runtime. Only the uber jar is portable that way. Pass -Pumamo.requireTargetIsHost=true (as CI does) to turn a mismatch into a build failure.
Two things differ from a native Linux box (neither applies on a real desktop/GPU):
- Use the system JDK 21, not Homebrew's. Homebrew's
openjdk@21has a few issues resolving libraries so it is easier to use the system installed JDK. Installsudo apt install openjdk-21-jdkand pointJAVA_HOMEat it:export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64. - Opt into the GPU. Mesa defaults to the
llvmpipesoftware rasteriser on WSL; select the hardware D3D12 path explicitly. Compose's own Skiko renderer can't make a hardware GL context under WSLg, so let it fall back to software.
MESA_LOADER_DRIVER_OVERRIDE=d3d12 GALLIUM_DRIVER=d3d12 SKIKO_RENDER_API=SOFTWARE \
./gradlew :desktop:runBy default the desktop build bundles the natives for the machine running it. Pass -Pumamo.target=<os>-<arch> to bundle a different platform's natives instead which is useful for testing a Windows build from a WSL2/Linux checkout. Recognized targets: linux-x64, linux-arm64, windows-x64, macos-x64, macos-arm64.
# Produce a Windows-native runnable uber-jar from WSL2/Linux:
./gradlew :desktop:packageUberJarForCurrentOS -Pumamo.target=windows-x64
# → app/desktop/build/compose/jars/This swaps both the Compose/Skiko renderer native and the LWJGL GL natives so the jar runs on the target. It is not a cross-compiler: the result is portable JVM bytecode plus the target's natives, so you still need a matching JVM on the target to launch it.
Don't combine -Pumamo.target with :desktop:run — that would load the target's natives in your host JVM and fail. The override is for producing artifacts and not running locally.
Reusable library modules live under module/, runnable application targets under app/. Everything else at the repo root (gradle/, docs/, build files) is intentionally not a module. A module's Gradle path (:format) is kept flat and independent of its folder. See: settings.gradle.kts
| Module | Directory | Targets | Role |
|---|---|---|---|
:format |
module/format |
jvm + android | Cubism file family + art I/O (CMO3 read/write, …) |
:reimport |
module/reimport |
jvm + android | Source-art binding + non-destructive reconcile |
:storage |
module/storage |
jvm + android | App config/data dirs + file IO (okio) + native file dialogs (FileKit) |
:settings |
module/settings |
jvm + android | JSON settings: defaults←user merge, dotted get/set, change Flow → :storage |
:runtime |
module/runtime |
jvm + android | Puppet model + CMO3 ingest |
:render |
module/render |
jvm + android | Deformation eval (CPU) + renderer + morph-blend shaders (expect/actual: LWJGL / GLES) → :runtime |
:ui |
module/ui |
jvm + android | Compose Multiplatform editor shell |
:desktop |
app/desktop |
jvm | Desktop entrypoint (Compose Desktop + LWJGL viewport) |
:android |
app/android |
android | Android entrypoint (Activity + Compose + GLSurfaceView) |
Shared logic lives in commonMain; platform code in jvmMain/androidMain via expect/actual.
- Action registry. Every editor operation is a named
Commandin a centralCommandRegistry(:ui,org.umamo.ui.action); menus, the command palette, and key bindings all dispatch through it, so handlers are never hardcoded in widgets. A command palette lists the registry with adaptive search. - Key bindings. Chords are stored logically, by key position plus a platform "primary" modifier (CMD on macOS, CTRL elsewhere), so shortcuts survive non-QWERTY layouts and map per OS. They resolve through a
Keymap; a built-in default (defaultKeymap) along with editable user-persisted keymaps and presets(settingsinput.keybinding.preset). - Settings. A layered JSON tree: bundled defaults ← user overrides, addressed by dotted keys (
:settings), persisted via:storageand emitting change events the UI reacts to.
Editor-format knowledge for CMO3, MOC3, and CLIP are reverse-engineered by black box observation. KRA is ported directly from Krita's own open-source GPLv3 code which is license-compatible with Umamo. All are written up under docs/format/.
| Format | Read | Write | Note |
|---|---|---|---|
| PSD | 〇 | - | Photoshop (Read only) |
| CLIP | 〇 | - | Clip Studio Paint (Read only) |
| KRA | 〇 | - | Krita (Read only) - ZIP + maindoc.xml + tiled LZF rasters; ported from open source. |
| UMA | 〇 | 〇 | Umamo Model File |
| CMO3 | 〇 | 〇 | Cubism Editor Model File - Compatible up to Cubism 5.4 |
| MOC3 | 〇 | 〇 | Cubism Distribution Model File - Compatible up to Cubism 5.4 |
Reverse engineered file formats have documentation in the docs/format/ folder.
The :cli module (app/cli) is a headless dump, convert, diff, atlas packing, and art meshing tool over the same CODEC and conversion stack the editor uses. Run it through Gradle; relative paths resolve against the repository root. Always pass -q so Gradle's own build output stays out of the tool's output. The tool writes data to stdout and diagnostics to stderr. The convert command will overwrite WITHOUT confirmation.
./gradlew -q :cli:run --args="dump model.moc3" #Static-rig summary
./gradlew -q :cli:run --args="dump model.moc3 --sections" #Container tier: per-section presence + counts
./gradlew -q :cli:run --args="dump model.cmo3" #CAFF entry table + main.xml overview
./gradlew -q :cli:run --args="dump model.cmo3 --xml" #Decompressed main.xml, byte-for-byte (Redirectable)
./gradlew -q :cli:run --args="dump model.cmo3 --puppet" #Import to PuppetModel and summarize (Either format)
./gradlew -q :cli:run --args="dump art.psd" #Artwork canvas, folders, per-layer inventory + trimmed bounds
./gradlew -q :cli:run --args="extract model.cmo3 out/" #Unpack the archive (Plaintext main.xml + layer PNGs) into out/model/
./gradlew -q :cli:run --args="atlas art.psd out/" #Pack the layers into out/art/page_NN.png + placements.txt
./gradlew -q :cli:run --args="atlas art.clip out/ --preview" #Also recomposite the canvas from the packed pages.
./gradlew -q :cli:run --args="mesh art.psd out/ --preview" #Auto-mesh every layer: out/art-mesh/mesh.txt + wireframe PNGs
./gradlew -q :cli:run --args="convert in.cmo3 out.cmo3" #Resave (An unedited main.xml will reemit byte-identical.)
./gradlew -q :cli:run --args="convert in.moc3 out.moc3" #Rebake (Fresh synthesis, NOT byte-identical by design.)
./gradlew -q :cli:run --args="convert in.cmo3 out.moc3" #Full family: moc3 + model3.json + cdi3.json + textures
./gradlew -q :cli:run --args="convert in.moc3 out.cmo3" #Fresh-graph synthesis (Always reports MissingSourceArt)
./gradlew -q :cli:run --args="diff a.cmo3 b.moc3" #Semantic per-entity diff of two modelsA moc3 input may also be given as its .model3.json manifest; textures and sidecars(cdi3/physics3/pose3/userdata3) are discovered through the manifest's file references.
dump, atlas, and mesh accept layered source artwork (.psd, .clip, .kra) or any flat raster, read as a one-layer document.
Packs a document's layers into atlas pages, writing page_NN.png and placements.txt to a new subdirectory named after the input. It reads every packed tile back out of its page and exits non-zero if any byte differs. Layers it cannot pack (no opaque pixels, or larger than a page) are reported rather than dropped silently.
| Option | Effect | Default |
|---|---|---|
--page-size=N |
Page side to pack against. | 4096 |
--gutter=N |
Transparent spacing around every tile. | 2 |
--extrude=N |
Edge pixels replicated into the gutter. | 2 |
--rotate |
Allow quarter-turning tiles to pack tighter. | Off |
--visible-only |
Skip layers hidden by their own or a folder's eye. | Off |
--no-shrink |
Keep full --page-size pages instead of cropping them. |
Off |
--preview |
Also write preview.png: the canvas recomposited from the packed pages (blend modes and layer opacity ignored). |
Off |
Runs the art mesher over a document's raster layers, printing a report and writing it to mesh.txt in a new <
A539
code><input>-mesh subdirectory. It exits non-zero if the mesher withheld a mesh after a failed structural check.
| Option | Effect | Default |
|---|---|---|
--layer=NAME |
Mesh only the layer with this exact name. | Every raster layer |
--preset=NAME |
standard, fine, or coarse: The settings the overrides below start from. |
standard |
--visible-only |
Skip layers hidden by their own or a folder's eye. | Off |
--fill-holes |
Mesh over holes in the art (lace, a spider web) as one solid sheet instead of cutting them out. | Off |
--no-strand-rungs |
Mesh thin strands from their outline alone instead of as rung strips. | Off |
--preview |
Also write NNN-<layer>.png: each mesh over its art (outline magenta, interior edges blue, triangles under the minimum angle orange, vertices refinement added green). |
Off |
--scale=N |
Magnify previews N times. | 1 |
Each setting override replaces one value of the preset. Distances are in art pixels.
| Override | Sets | Standard | Fine | Coarse |
|---|---|---|---|---|
--alpha-threshold=N |
The least alpha (1-255) counted as art. | 16 | 16 | 16 |
--outline-spacing=N |
The distance between outline vertices. | 32 | 20 | 48 |
--interior-spacing=N |
The distance between inner-ring and lattice vertices. | 48 | 28 | 72 |
--outer-margin=N |
How far outside the art the outline runs. | 4 | 3 | 6 |
--inner-margin=N |
How far inside the art's edge the inner ring runs; 0 for none. | 8 | 6 | 12 |
--minimum-margin=N |
The closest the outline may come to any art pixel. | 2 | 1.5 | 3 |
--minimum-outline-points=N |
The fewest vertices one outline ring may have. | 6 | 8 | 4 |
--vertex-budget=N |
The most vertices one mesh may have. | 1024 | 1024 | 1024 |
--minimum-angle=N |
The angle, in degrees, refinement brings every triangle up to; 0 turns refinement off. | 20 | 24 | 15 |
--interior-thinning-limit=N |
Over the vertex budget, how many outline spacings the interior may thin to before the outline thins with it. | 4 | 4 | 4 |
--strand-width=N |
The art width below which a stretch of art is a strand, meshed as rungs. | 40 | 26 | 60 |
The report has one row per layer, then the run's smallest triangle angles in 5-degree buckets.
| Column | Meaning |
|---|---|
order, name, raster |
The layer's order, name, and raster size. |
vertices, triangles |
The mesh's vertex and triangle counts. |
outline |
Outline rings / outline vertices. |
refined |
Outline chords split because they came too close to the art or cut it off. |
inner |
Inner rings / inner-ring vertices. |
lattice |
Interior lattice vertices. |
strands |
Strands / rungs / centerline vertices. |
steiner |
Vertices refinement added toward the minimum angle. |
minAngle |
The smallest triangle angle, in degrees. |
under5, under10, under20 |
Triangles whose smallest angle is under 5, 10, and 20 degrees. |
unfixable |
Triangles left under the minimum angle that refinement could not help: an angle the outline itself makes, or a spot too tight to split. |
milliseconds, notices |
The time taken and the mesher's notices. |
All tests can be run with ./gradlew test or individual tests can be run like so:
./gradlew :interop:jvmTest --tests "org.umamo.interop.PuppetModelDiffTest"
Each CODEC and reader have tests that run against real samples that you can supply. Note: The /test/corpus is intentionally gitignored to prevent accidentally committing private data. If you don't supply samples then the tests will automatically skip.
Put your samples at test/corpus/ and they are picked up automatically:
test/corpus/cmo3/ EricaTamamo.cmo3 (the model the CMO3 gate's exact counts are pinned to)
test/corpus/moc3/ Put each model's files in a separate subdirectory.
Artwork:
test/corpus/clip/ test/corpus/psd/ test/corpus/krita/ test/corpus/tiff/ test/corpus/jpeg/
./gradlew :format:jvmTest #Corpus automatic discovery.
./gradlew :format:jvmTest -Dcmo3.sample=/path/to/Model.cmo3 #Point at one explicitly.
./gradlew :format:jvmTest --tests "org.umamo.format.cmo3.tools.ModelGenerator" -Dcmo3.generate=true -Dcmo3.gensample=/path/to/Model.cmo3 --rerun #Generate new CMO3 model and registration. Make sure to run ktlintFormat afterwards to clean up.
./gradlew :ui:jvmTest --tests "*.EditGrabPerfProbeTest" -i #Edit-mode perf probe over moc3.perfSample (test/corpus/moc3/modelF by default). Print-only: -i (--info) shows the rows, or read build/test-results.
./gradlew :render:jvmTest --tests "*.EditGrabRenderPerfProbeTest" -i #Its render-thread twin (needs a GL context).Umamo is an independent, community-developed project. It is not affiliated with, authorized by, endorsed by, or sponsored by Live2D Inc. (formerly Cybernoids Co., Ltd.) or any of its affiliates.
"Live2D®" and "Cubism" are trademarks of Live2D Inc. in Japan and/or other countries; all other trademarks are the property of their respective owners. These marks are used here only nominatively, to describe file-format compatibility, and no endorsement, affiliation, or sponsorship is implied.
WANTED: Human translations for various languages.(Japanese, Chinese, Korean, Spanish, French, German, etc.)
The English strings are located at: module/ui/src/commonMain/composeResources/values/strings.xml
At the moment I'm not accepting large code contributions from outside contributers. Small fixes are accepted, but please keep in mind that at moment the development speed on this project is rapid. I'm might make your change obsolete by the time I get around to reviewing it.
(Once large changes are accepted in the future.) If you are planning a large change that touches many files then break it down into a stack of smaller pull requests that build on each other. For example, if you're adding a new export format:
- PR 1: Add the underlying data structures/parser with tests, no UI wiring yet.
- PR 2: Add the export logic that consumes those structures, merged on top of PR 1.
- PR 3: Wire up the UI/CLI flag that exposes the feature, merged on top of PR 2.
Each PR should be reviewable and mergeable on its own instead of being submitted as one giant diff. This makes review faster and makes it easier to bisect regressions.
Umamo's editor-format layer (.cmo3/.moc3 read/write, .clip ingest) is a clean-room reimplementation built from black-box observation of file outputs and independently produced documentation.
By contributing you affirm:
- No use of official Live2D/Cubism source or decompiled binaries as a basis, knowledge only from observing file outputs and public, independently authored documentation.
- You are not bound by a Live2D agreement your contribution would breach, and will disclose any such license before contributing.
- Are not a past or present employee of Live2D or Cybernoids.
If you have been exposed to Live2D source code or decompiled binaries, please do not contribute.
Large Language Models(LLM) are used to assist with rubber ducking and housekeeping tasks. Assisted tasks include planning development road maps, function documentation, and helping to keep the developer focused. AI has not been and will never be used to decompile or otherwise discover the source code of protected software.
GPLv3 See LICENSE