Skip to content

Writing plugins

Plugins from the Plugins menu run inside Pixel Painter’s own Python and Qt process. They are for trusted TD tools: a plugin can parent a PySide window to Pixel Painter, add a layer through the public API, and affect the open project. Treat a script from somebody else like a plugin: do not run it unless you trust its author.

Put a top-level .py file in Documents\PixelPainter\plugins. Its filename appears in the Plugins menu without .py. Clicking it imports the file once for the current session and calls its required run() function. Clicking it again calls the same function again, so a script can reuse a dialog in a module global.

"""A small trusted host script that adds a paint layer."""
from PySide6.QtWidgets import QDialog, QHBoxLayout, QLabel, QLineEdit, QPushButton, QVBoxLayout
from shiboken6 import isValid
from pixelpaint import ui
from pixelpaint.api import ProjectAPI
_window = None
class LayerTool(QDialog):
def __init__(self, parent):
super().__init__(parent)
self.setWindowTitle("Layer Tool")
self.name = QLineEdit("Layer")
self.status = QLabel("Open a project first.")
add = QPushButton("Add Layer")
add.clicked.connect(self.add_layer)
buttons = QHBoxLayout()
buttons.addStretch()
buttons.addWidget(add)
layout = QVBoxLayout(self)
layout.addWidget(self.name)
layout.addWidget(self.status)
layout.addLayout(buttons)
def add_layer(self):
texture_set = ProjectAPI().get_active_texture_set()
if texture_set is None:
self.status.setText("Open a project before adding a layer.")
return
name = self.name.text().strip() or "Layer"
texture_set.add_layer(name, "paint")
self.status.setText(f"Added: {name}")
def run():
global _window
if _window is None or not isValid(_window):
_window = LayerTool(ui.get_main_window())
_window.show()
_window.raise_()
_window.activateWindow()

The dialog has the real main window as its Qt parent, so it follows the app’s theme, focus and lifetime. TextureSetAPI.add_layer() is the same service used by Pixel Painter’s own UI, so normal signals and undo integration remain intact.

Use only documented public imports for a tool that should survive updates:

from pixelpaint import ui
from pixelpaint.api import ProjectAPI, TextureSetAPI
from pixelpaint.viewport.preview import PreviewScene, PreviewViewport, box_mesh

The last line is a 3D preview for a tool’s own test mesh: Pixel Painter’s own viewport, lit by its current environment, showing a PreviewScene you fill yourself. It never shows or changes the open project.

pixelpaint.ui.window, pixelpaint.core, and private attributes are host internals. They may be useful in a studio, but they are not guaranteed to stay compatible between versions.

stop() is optional. Pixel Painter calls it for every script it loaded after the artist confirms quitting and before Qt destroys the host window. Use it to remove a menu, close a worker, or call ui.delete_ui_element() for UI owned by the script.

Launch Pixel Painter through a studio .bat or launcher to add shared folders. On Windows, each list uses ; and keeps the declared order:

Terminal window
set PIXELPAINT_STARTUP_PATH=P:\pipeline\pixelpaint\startup
set PIXELPAINT_PLUGINS_PATH=P:\pipeline\pixelpaint\plugins
set PYTHONPATH=P:\pipeline;P:\pipeline\thirdparty\python_packages;%PYTHONPATH%
start "" "C:\Program Files\PixelPainter\bin\PixelPainter.exe"

Documents\PixelPainter\startup and PIXELPAINT_STARTUP_PATH run each top-level .py entry, in local-folder then shared-folder order and then filename order, once after the main window opens. Each entry defines the same run() hook as a manual plugin; a broken entry is logged and does not prevent the next one from running. Use Startup to add a studio menu whose actions lazily import tools from PYTHONPATH.

PIXELPAINT_PLUGINS_PATH adds folders to the manual Plugins menu. It does not autorun those files. Empty, duplicate, and unavailable entries are ignored; unavailable folders are logged. Pixel Painter’s own packages and Qt take precedence over shared-drive imports. To recover from a broken startup tool, launch once with PIXELPAINT_SKIP_STARTUP=1.

A larger tool can keep its entry, helpers and assets together in one folder:

material_helper/
plugin.json
plugin.py
dialog.py
assets/

plugin.json supplies display metadata without importing the tool while the Plugins menu opens:

{
"manifest_version": 1,
"id": "studio.material-helper",
"name": "Material Helper",
"entry": "plugin.py",
"version": "1.0",
"description": "Creates a studio material layer"
}

entry must be a relative .py file inside that folder. id is a stable ASCII identifier using letters, digits, dots, underscores or hyphens. The entry still defines run() and optional stop(); it may use normal relative imports such as from .dialog import MaterialDialog. Duplicate ids and invalid manifests are not loaded; the Plugins menu offers details with the reason.