Disclaimer: This repository is "vibe coded". Please use with caution.
A Linux desktop Live2D model viewer that renders Live2D characters as transparent Wayland overlay windows using the wlr-layer-shell protocol.
Live2D models float on your desktop with click-through transparency — only the model itself receives input. Built with the Live2D Cubism SDK for Native, OpenGL (EGL), and native Wayland (no X11, no GLFW windowing).
compressed.mp4
- Wayland-native overlay — renders as a layer-shell surface, always on top of your desktop
- Click-through transparency — only the Live2D model area receives pointer events; everything else passes through
- Multi-compositor support — works on any Wayland compositor supporting
wlr-layer-shell(Hyprland, Sway, river, etc.) - Multi-output support — switch between monitors (Hyprland)
- Interactive — responds to mouse drag, tap, and scroll input
- Motion & expression — supports idle animations, lip-sync, eye-blink, physics, and expressions
- Configurable model directory — load models from any path via CLI flag or XDG config
- JSON configuration — customize behavior via
config.json(default model, emotion timeout, additional model dirs, scale/position, window size) - IPC control — Unix domain socket API for external control (model switching, expressions, motions, lipsync, zoom, position, look-at direction)
| Dependency | Notes |
|---|---|
| Linux with Wayland compositor | Must support wlr-layer-shell-v1 (Hyprland, Sway, river, etc.) |
| Live2D Cubism SDK for Native | Download from live2d.com/sdk (proprietary, not bundled) |
| C++ compiler | GCC or Clang with C++14 support |
| CMake | >= 3.16 |
| pkg-config | For finding Wayland/EGL libraries |
| Wayland development libraries | wayland-client, wayland-egl, wayland-cursor |
| EGL & OpenGL | libegl-dev, libgl-dev or equivalent |
| wayland-scanner | Usually part of wayland-protocols or wayland dev packages |
| curl, unzip | For downloading third-party dependencies (GLEW) |
Arch Linux:
sudo pacman -S --needed base-devel cmake pkgconf wayland wayland-protocols libglvnd egl-wayland curl unzipUbuntu / Debian:
sudo apt install build-essential cmake pkg-config libwayland-dev wayland-protocols libegl-dev libgl-dev curl unzipFedora:
sudo dnf install gcc-c++ cmake pkgconf-pkg-config wayland-devel wayland-protocols-devel mesa-libEGL-devel mesa-libGL-devel curl unzipgit clone https://github.com/shinkuan/waifuland.git
cd waifuland- Go to https://www.live2d.com/en/sdk/about/
- Download Cubism SDK for Native
- Extract it into the project root directory:
waifuland/ ├── CubismSdkForNative-5-r.5/ <-- extracted SDK here ├── CMakeLists.txt ├── src/ └── ... - If the folder name differs from
CubismSdkForNative-5-r.5, updateSDK_ROOT_PATHinCMakeLists.txt.
Use the install script (recommended):
chmod +x install.sh
./install.shOr build manually:
cd thirdParty && bash scripts/setup_glew && cd ..
mkdir -p build && cd build
cmake ..
make -j$(nproc)The binary will be at build/bin/waifuland.
# Run with default model directory (~/.config/waifuland/models/)
./build/bin/waifuland
# Run with a custom model directory
./build/bin/waifuland --models_dir /path/to/your/models
# Run with a custom config file
./build/bin/waifuland --config /path/to/config.jsonWaifuland reads a JSON config file from $XDG_CONFIG_HOME/waifuland/config.json (fallback: ~/.config/waifuland/config.json). Override the path with --config <path>.
Example config.json:
{
"additional_model_dirs": [
"/home/user/extra-models",
"/opt/shared-models"
],
"default_model": "MyFavoriteModel",
"emotion_timeout": 5,
"model_scale": 1.0,
"model_x": 0.0,
"model_y": 0.0,
"window_width": 1900,
"window_height": 1000
}| Key | Type | Default | Description |
|---|---|---|---|
additional_model_dirs |
string[] | [] |
Extra directories to scan for models (in addition to the default models dir) |
default_model |
string | "" |
Name of the model subfolder to load first on startup |
emotion_timeout |
float | 5 |
Seconds before expression reverts to default. Set to -1 to never revert |
model_scale |
float | 1.0 |
Initial model scale |
model_x |
float | 0.0 |
Initial model X offset |
model_y |
float | 0.0 |
Initial model Y offset |
window_width |
int | 1900 |
Render target width in pixels |
window_height |
int | 1000 |
Render target height in pixels |
All fields are optional. Missing fields use their default values. If the config file doesn't exist, all defaults are used.
| Input | Action |
|---|---|
| Left-click Upper Body | Trigger expression change (if any) |
| Left-click Lower Body | Trigger motion (if any) |
| Left-click drag | Drag the model / trigger hit areas |
| Right-click | Switch to next model |
| Middle-click | Switch skin (if any) |
| Scroll wheel | Zoom in/out |
Waifuland runs on any Wayland compositor that supports the wlr-layer-shell protocol. Some features require compositor-specific IPC and are only available on Hyprland:
| Feature | Hyprland | Sway | Other wlroots |
|---|---|---|---|
| Overlay rendering | Yes | Yes | Yes |
| Click-through transparency | Yes | Yes | Yes |
| Mouse drag / tap / scroll | Yes | Yes | Yes |
| Global cursor tracking (eyes follow cursor anywhere) | Yes | No | No |
| Cross-monitor drag | Yes | No | No |
| Move to focused monitor | Yes | Yes | No |
The compositor is auto-detected at startup. Feature availability is logged to the console.
You can hide and show your Live2D model (along with its click-through input region) by sending a SIGUSR1 signal to the background process. When hidden, it uses virtually no resources.
kill -SIGUSR1 $(pgrep -x waifuland)
# Or
killall -s SIGUSR1 waifuland
# Or using the included toggle command:
./build/bin/waifuland toggleMove the model to whichever monitor currently has focus by sending a SIGUSR2 signal. This works on Hyprland and Sway.
kill -SIGUSR2 $(pgrep -x waifuland)
# Or
killall -s SIGUSR2 waifuland
# Or using the included focus command:
./build/bin/waifuland focusHyprland keybind examples
Add these to your ~/.config/hypr/hyprland.conf:
# Toggle waifuland visibility with Super + W
bind = SUPER, W, exec, killall -s SIGUSR1 waifuland
# Move waifuland to focused monitor with Super + Shift + W
bind = SUPER SHIFT, W, exec, killall -s SIGUSR2 waifulandWaifuland exposes a Unix domain socket for external control by scripts and programs.
- Socket path:
$XDG_RUNTIME_DIR/waifuland.sock(fallback:/tmp/waifuland.sock) - Protocol: Newline-delimited JSON — send
{"command":"<name>", ...}\n, receive{"ok":true, ...}\n - Requires:
socat(install via your package manager)
A convenience CLI client waifuland-ctl is included in the project root:
./waifuland-ctl <command> [--key value ...]Returns current model, zoom, position, and visibility.
./waifuland-ctl get_status{
"ok": true,
"model": "MyModel",
"model_index": 0,
"model_count": 3,
"hidden": false,
"zoom": 1.0,
"x": 0.0,
"y": 0.0
}./waifuland-ctl get_available_models{
"ok": true,
"models": ["MyModel", "AnotherModel", "ThirdModel"]
}./waifuland-ctl get_current_model{
"ok": true,
"model": "MyModel",
"index": 0
}# By name
./waifuland-ctl set_model --name "AnotherModel"
# By index
./waifuland-ctl set_model --index 2{
"ok": true
}./waifuland-ctl next_model{
"ok": true
}./waifuland-ctl prev_model{
"ok": true
}./waifuland-ctl get_expressions{
"ok": true,
"expressions": ["happy.exp3.json", "angry.exp3.json", "sad.exp3.json"]
}./waifuland-ctl set_expression --id "happy.exp3.json"{
"ok": true
}./waifuland-ctl get_motions{
"ok": true,
"motions": [
{"group": "Idle", "index": 0, "file": "idle_01.motion3.json"},
{"group": "TapBody", "index": 0, "file": "tap_01.motion3.json"}
]
}# Play a specific motion by group and index
./waifuland-ctl do_motion --group "TapBody" --index 0
# Play with custom priority (default: 2 = Normal)
./waifuland-ctl do_motion --group "Idle" --index 0 --priority 3
# Play a random TapBody motion (omit group)
./waifuland-ctl do_motion{
"ok": true
}Value range: 0.0 (closed) to 1.0 (fully open). Send continuously for real-time lipsync.
./waifuland-ctl set_mouth_y --value 0.8{
"ok": true
}Value range: 0.1 to 10.0.
./waifuland-ctl set_model_zoom --value 1.5{
"ok": true
}./waifuland-ctl set_model_position --x 0.5 --y -0.3{
"ok": true
}./waifuland-ctl get_model_position{
"ok": true,
"x": 0.5,
"y": -0.3,
"zoom": 1.5
}toggle_hidden — Toggle window visibility
./waifuland-ctl toggle_hidden{
"ok": true,
"hidden": true
}./waifuland-ctl switch_skin{
"ok": true
}# Set look direction (x, y range: -1.0 to 1.0)
./waifuland-ctl set_look --x 0.5 --y 0.3
# Reset to default (follow cursor)
./waifuland-ctl set_look --reset{
"ok": true
}You can also communicate directly with the socket without waifuland-ctl:
# Using socat
echo '{"command":"get_status"}' | socat - UNIX-CONNECT:$XDG_RUNTIME_DIR/waifuland.sock
# Using Python
python3 -c "
import socket, json
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect('$XDG_RUNTIME_DIR/waifuland.sock')
sock.send(b'{\"command\":\"get_status\"}\n')
print(sock.recv(4096).decode())
sock.close()
"Place Live2D models in your models directory. Each model should be in its own subfolder containing a .model3.json file:
~/.config/waifuland/models/
├── MyModel/
│ ├── MyModel.model3.json
│ ├── MyModel.moc3
│ ├── textures/
│ └── motions/
└── AnotherModel/
└── ...
The default models directory is $XDG_CONFIG_HOME/waifuland/models/ (fallback: ~/.config/waifuland/models/). Override it with the --models_dir flag.
waifuland/
├── src/ # Application source code
│ ├── main.cpp # Entry point, CLI argument parsing
│ ├── LAppConfig.* # JSON config file reader (singleton)
│ ├── LAppWayland.* # Wayland client setup (display, compositor, EGL, layer-shell)
│ ├── LAppWaylandRegion.* # Input region management (click-through transparency)
│ ├── LAppDelegate.* # Main app controller, render loop, input handling
│ ├── LAppLive2DManager.* # Model lifecycle management
│ ├── LAppView.* # View/projection matrices, rendering coordination
│ ├── LAppModel.* # Individual Live2D model instance
│ ├── LAppIPC.* # IPC socket server for external control
│ └── LAppDefine.* # Global constants and configuration
├── protocol/ # Wayland protocol XML files
│ ├── xdg-shell.xml
│ └── wlr-layer-shell-unstable-v1.xml
├── thirdParty/ # Third-party dependencies (GLEW, stb)
├── cmake/ # CMake helper scripts
└── CMakeLists.txt # Build configuration
- No GLFW windowing — Wayland surfaces are created directly via
wl_compositorandzwlr_layer_shell_v1for overlay behavior that GLFW cannot provide. - EGL rendering — OpenGL context is managed through EGL, bound directly to the Wayland display.
- Layer-shell overlay — the application renders as a Wayland layer surface, sitting above normal windows.
- Input region masking — only the model's bounding area accepts input; the rest of the surface is fully transparent and click-through.
- Live2D Cubism SDK for Native — Live2D model rendering
- GLEW — OpenGL extension loading
- stb_image — Image loading
- wlr-layer-shell — Wayland overlay protocol
- Wayland / EGL / OpenGL — Display and rendering stack
This project includes code adapted from the Live2D Cubism SDK samples, which are subject to the Live2D Open Software License.
The Live2D Cubism SDK (Core library) is proprietary and must be downloaded separately. See Live2D SDK License.