Unreal Engine
DousenUE is an editor-only plugin for Unreal Engine 5. It keeps Unreal in step with the rest of your pipeline without anyone touching the Content Browser by hand: an asset exported from a DCC lands in Unreal (or offers to), asset names are re-checked against your project's conventions on the way in, and assets can be handed off between Unreal and any other app on the bridge in either direction.
The plugin talks to exactly two things, both over HTTP/JSON — the Dousen server's REST API (DousenCore, default port 8080) and the DousenDesktop bridge relay (default port 8766). There is no gRPC, no separate socket protocol to install, and no feature here works without a valid Dousen license behind the DousenCore instance it talks to.
This page covers the plugin as it exists in Source/ today. A couple of design docs bundled in the plugin folder (README.md, kickstart_ue.md) describe an earlier plan for an in-editor task list with Jira status buttons — that never shipped in the actual C++ and isn't part of the current plugin. Task management lives entirely in DousenDesktop; see Jira tasks.
Installing and configuring
-
Copy the plugin into your project.
Copy the
DousenUEfolder into your project'sPlugins/directory. (The authoritative source lives in thedousen-io/dousenUE.gitsubmodule, mounted atDousenDesktop/plugins/DousenUEin the Dousen monorepo — copy or symlink from there.) - Regenerate your project files.
- Open the project in Unreal Engine 5. The plugin is editor-only and loads automatically with the editor.
- Edit
Config/BaseDousenUE.iniunder the section[/Script/DousenUE.DousenProjectSettings]before relying on conventions or the material catalog — both stay off untilProjectIdis set.
| Key | Type | Default | Meaning |
|---|---|---|---|
| ServerScheme | string | https | Scheme for the DousenCore host. Defaults to https so an unconfigured remote host never silently falls back to cleartext — see the loopback rule below. |
| ServerHost | string | localhost:8080 | Host and port of the DousenCore REST API, e.g. your-host:8080. |
| BridgeRelayPort | int | 8766 | Port of the DousenDesktop HTTP bridge relay (BridgeHttpRelay). Matches DousenDesktop's own default. |
| ProjectId | string (UUID) | empty | Your Dousen project's UUID. Conventions fetching, material-catalog reporting, and their refresh timers are all skipped entirely while this is empty — not just disabled, never even attempted. |
| bAutoReimportOnBridgeEvent | bool | true | Automatically reimport (or offer to import) a mesh or texture when a mesh.export/texture.export bridge event arrives while the editor is open. Gates only the reimport step — DCC registration and Open in DCC keep working regardless. |
| MaterialCatalog | soft object path | unset | Path to a UDousenMaterialCatalog Data Asset living in your project's /Game/ content. Empty skips material-catalog reporting entirely (fail-open to legacy import behavior). See Material catalog and the Nanite gate. |
The plugin ships with a starter Config/BaseDousenUE.ini:
INI[/Script/DousenUE.DousenProjectSettings]
; Use https for any non-loopback DousenCore host. http is only accepted for
; localhost / 127.0.0.0/8 / ::1; cleartext to a remote host is refused (DOU-192).
ServerScheme=http
ServerHost=localhost:8080
BridgeRelayPort=8766
; DousenCore project UUID — set this to your project's ID to enable conventions
ProjectId=
; Soft path to the TA-authored material catalog Data Asset (must live in /Game
; project content), e.g.
; MaterialCatalog=/Game/Pipeline/DA_DousenMaterialCatalog.DA_DousenMaterialCatalog
;MaterialCatalog=
Cleartext HTTP only works to loopback (DOU-192). ServerScheme=http is accepted only when ServerHost resolves to localhost, 127.0.0.0/8, or ::1 — exactly why the shipped default (localhost:8080) is safe as-is. Point ServerHost at any other machine and the plugin refuses the request outright rather than send credentials or JWTs unencrypted, logging "Refusing to contact DousenCore over cleartext HTTP on a non-loopback host. Set [DousenUE] ServerScheme=https…". Switch to ServerScheme=https for anything beyond your own machine.
Panels
The plugin registers two dockable Nomad tabs under the editor's Window menu, titled exactly Dousen and Dousen Bridge:
- Dousen — the CV tag search panel: a Tag Search area with dynamic key/value query rows, a + to add a row, and Search / Clear buttons.
- Dousen Bridge — a live table of the last 50 bridge events (columns: Time, Type, From, File — hover the File cell for the full path), newest first, with a status line reading "Polling <relay URL>" or "No poller — bridge events unavailable." and a Clear button.
Neither panel is a task or Jira UI, and the Bridge panel's event table isn't the same list that drives Open in DCC — that submenu is populated separately from GET /bridge/dccs (below). Task assignment, Jira status, and checkout/submit all live in DousenDesktop, not in Unreal.
Automatic asset sync
Unreal already ships an Auto Reimport that watches source files on disk and refreshes an asset when its .fbx or .tga changes. Dousen's asset sync is deliberately not that. A file-watcher only ever knows "a file changed" — it has no idea where the asset belongs in your project, what its textures are, or how it should be set up. FAssetSyncManager acts on the bridge export event instead, which carries the pipeline metadata a disk watcher never sees, and uses it to land the export correctly, not just freshly:
- Placed by convention. The destination package is computed from the source asset's resolved naming-convention name and path — the same resolvers as Naming conventions on import — so a brand-new asset is created at the convention-correct
/Gamelocation rather than dumped into a monitored folder. Matching an event to an existing asset is a direct package-path lookup, never a scan of the whole/Gametree. - Textures wired into material slots. When the event carries a material assignment, each incoming texture is classified to a channel and bound into the correct slot of a generated material instance (see the material catalog and the classifier note below) — not left as loose texture assets for someone to hook up by hand.
- Nanite set from the catalog. The mesh's Nanite state is resolved from its assigned materials on import — the import half of the Nanite gate.
- CV tags carried through.
ApplyCvMetadatastamps the event's CV tags onto the asset's registry metadata (see CV tag search), so the asset arrives searchable.
None of that is derivable from a file path alone, which is exactly why Unreal's stock watcher can't do it — it all rides on the bridge event's metadata. The reimport of an already-existing asset is the one part that overlaps with stock Auto Reimport; everything above is the part that doesn't.
How the event reaches the editor
Getting the event into the editor is deliberately unremarkable — the value is in the handling above, not the transport. Two paths feed FAssetSyncManager:
- Push, while the editor is open. The plugin runs a small loopback-only TCP command server (auto-picking a free port from
8767) and advertises it on registration. DousenDesktop pushes the command there the instant an export happens and gets a synchronous acknowledgement once the asset has actually landed on disk — so the pipeline knows the import completed and can reflect that back to the artist and to other connected apps. Stock Auto Reimport is fire-and-forget; nothing downstream ever learns it ran. This ack loop is the one piece of the transport that's a genuine capability rather than plumbing. - Poll, as the fallback.
FBridgePollerpollsGET /bridge/events?since=<timestamp>every 2 seconds for the case where the push didn't apply. A 10-second dedup window drops the poll's copy of anything the push already handled, so nothing imports twice. If the relay stops answering, the poller backs off exponentially (2min(failures,4) ticks, capped at 16 — roughly 32 seconds at the 2-second base) and recovers on its own, logging"Bridge relay unreachable (failure #N) — retry in ~Ns"underLogDousenUEeach time it backs off further.
What happens when an event lands depends on whether the computed target package already exists:
- Package exists —
FReimportManager::Reimport()updates the existing asset in place (deferred one tick to the game thread to sidestep a TaskGraph reentrancy issue with a synchronous reimport), the package is saved, and a toast reads "[Dousen] Reimported <asset>" (visible 4 seconds). - No package yet, interactive path (the poller, editor open) — instead of importing outright, a toast offers "[Dousen] <file> exported — import to UE?" with Import and Dismiss buttons (visible 12 seconds), so nothing lands in the project without someone choosing it.
- No package yet, command path — used by DousenDesktop's push and by headless import, where there's no one present to click a toast — imports unprompted, no toast, and reports the result back over the command connection.
Naming-convention violations are checked on the same event and surface as their own toast, "[Dousen] Naming: <message>" (8 seconds) — a violation doesn't block the import itself, it's advisory.
When an event also carries a material assignment (see the material catalog), the plugin classifies each incoming texture by filename to a channel — BaseColor, Normal, ARM (packed occlusion/roughness/metallic), Roughness, Metallic, AO, Emissive, Opacity, Height, or Mask — plus its sRGB and compression settings. That classifier (ClassifyTexture/SlotForTexture) is deliberately kept identical to DousenDesktop's python/dousen/texture_classify.py (both carry an explicit "mirrors … — change together" comment), so a texture gets the same channel guess whether that guess happens DCC-side at export or here at import.
Naming conventions on import
FConventionsConfig parses the compiled conventions JSON fetched from DousenCore's GET /api/projects/:id/conventions/compiled. The fetch runs once at editor startup and then on a 300-second (5-minute) timer — but only while ProjectId is set; with it empty, neither the initial fetch nor the timer is ever scheduled.
Three resolver methods do the actual work, all driven off the tag map attached to a bridge event, plus a fourth used purely for the naming-violation toast above:
| Method | What it does |
|---|---|
| ResolveName | Fills a template like SM_{asset_name}_{variant} — prefix, taxonomy prefixes, asset name, taxonomy suffixes, variant, LOD, number, suffix, joined with _ — from the tags on the event. |
| ResolveEnginePath | Walks the project's taxonomy tree, joining the path segments of whichever nodes are flagged as folders, to produce a /Game/... destination. |
| ResolveSourcePath | Mirrors the engine path by default. If an asset type's convention instead sets a source-path override with its own template, renders that template's {{ key }} placeholders against the tags — for source files that don't live in the same shape as the imported asset. |
| ValidateName | Checks a name against the asset type's convention and returns the list of violations, if any — this is what produces the "[Dousen] Naming: …" toast. |
This is what lets asset sync (above) compute a destination path deterministically instead of searching the content tree, and it's the plugin's second line of defense on naming — the first is the naming.convention validator that already ran inside the DCC before export. A name that somehow slipped past that check (or was renamed after export, before publishing) still gets caught here on the way into Unreal.
Open in DCC
FDousenContentBrowserExtension adds a context-menu extender for exactly three classes — StaticMesh, SkeletalMesh, and Texture2D — no others. Right-clicking one of these in the Content Browser adds a Dousen section to the Common Asset Actions group with two entries:
- Open in DCC — a submenu listing every DCC app currently registered with the bridge relay. If none are registered it shows a disabled "Open in DCC (no DCCs registered)" entry instead. Picking one publishes a
dcc.openbridge event carrying the asset name, a Jira key field (always empty from Unreal — there's no task panel here to source one from), and the chosen DCC; that app opens the asset via its own bridge subscription. - Show Dousen Tags — pops up a window titled "Dousen Tags — <asset>" listing every
dousen_*anddousen_cv_*metadata tag on the asset, sorted, or "No Dousen tags on this asset." if there are none. Useful for confirming exactly what conventions and CV tags landed on an asset without digging through the Asset Registry.
The DCC list comes from GET /bridge/dccs, fetched once at plugin startup and refreshed on a 60-second timer (which also re-sends Unreal's own registration, in case DousenDesktop restarts) — so a DCC that connects to the bridge after the editor is already open shows up in the submenu within a minute.
Unreal as a bridge participant
The plugin registers itself with the relay as DCC id unreal / display name Unreal Engine, including the local command-server port from above, and re-registers (a heartbeat, effectively) every 30 seconds — comfortably inside the relay's 90-second registration TTL (see the Bridge API reference) — and unregisters cleanly on editor shutdown.
Saving a Static Mesh, Skeletal Mesh, or Texture2D asset publishes mesh.export or texture.export; saving a Material or Material Instance publishes material.export; saving a Landscape actor publishes asset.export — each carrying the asset's path, class, and package filename, so other tools and DousenCore's asset tracking see the change immediately. Incoming dcc.open events targeted at unreal (or with no target at all) resolve the requested asset path through the Asset Registry and navigate the Content Browser straight to it.
Material catalog and the Nanite gate
Material catalog integration is implemented end-to-end — Unreal, Substance Painter, Substance Designer, Blender, Maya, 3ds Max, and Houdini all read it — but it's still pending a full workstation-based end-to-end verification pass before it's considered production-hardened. Expect edge-case behavior (unresolved materials, mixed assignments) to be refined.
A Dousen Material Catalog is a UDousenMaterialCatalog Data Asset that a TA authors once per project (add one from the Content Browser: Add → Miscellaneous → Data Asset → Dousen Material Catalog). Because the plugin itself ships with no project content, the catalog instance has to live under your project's own /Game/ tree — point MaterialCatalog at it. Each entry describes one master or parent material:
| Field | Meaning |
|---|---|
| Id | Stable, TA-edited slug (lowercase, underscores). DCC payloads reference materials by this id. Auto-filled by Auto Populate From Materials below. |
| DisplayName | Human-readable name shown in DCC-side material pickers. |
| Material | Soft reference to the actual master/parent UMaterialInterface. |
| bAllowNanite | If false, meshes assigned this material must not enable Nanite. Drives both the import-time Nanite gate below and the DCC-side Nanite/NonNanite validator gate. |
| TextureSlots | Per-slot declarations: ParamName, a Content semantic (Albedo, Normal, Roughness, Metallic, ARM, ORM, AO, Emissive, Opacity, Height, Mask, or Custom), bSRGB, a Compression preset (Default, NormalMap, Masks, Grayscale, HDR), and MatchKeywords for auto-matching filenames. |
| ScalarParams | Per-param declarations: Name, Default, Min, Max, Label. |
| VectorParams | Per-param declarations: Name, Default color, Label. |
The Auto Populate From Materials editor button (AutoPopulateFromMaterials) speeds up authoring: for every entry with a Material assigned, it fills in Id/DisplayName when empty and enumerates that material's actual texture/scalar/vector parameters, guessing each texture slot's content/sRGB/compression/keywords from its parameter name using the same keyword table ClassifyTexture uses. It never clobbers a field you've already filled in and never removes a declaration you added by hand — so the TA workflow is: assign the material, click the button, then review the guesses and set bAllowNanite by hand (it's never guessed).
Once configured, the plugin reports the catalog to DousenCore with POST /api/projects/:id/material-catalog (admin-gated on DousenCore's side, like the rest of the pipeline-config endpoints): once at startup, again on the same 5-minute conventions-refresh timer, and again immediately whenever the catalog asset itself is saved. Each report is skipped if a CRC32 hash of the serialized catalog JSON matches the last successful report — so an unchanged catalog isn't re-sent every five minutes, and a failed POST retries on the very next cycle rather than being silently dropped. DCC-side material pickers (Substance Painter/Designer, Blender, Maya, 3ds Max, Houdini) read the same catalog back via GET on that endpoint, so artists choose from a materials list scoped to what's actually approved for the project instead of a raw asset browse — see DCC addons.
How the import-side Nanite gate works
This is the import half of the single Nanite gate described from the export side in Asset validation. When the plugin assigns catalog materials to a reimported mesh, it ANDs bAllowNanite across every material slot the mesh resolves to:
- If every resolved slot's material allows Nanite,
NaniteSettings.bEnabledis turned on for that mesh. - If any resolved slot's material disallows it, Nanite is turned off for the whole mesh.
- If a slot's assignment doesn't resolve to any catalog entry at all, the plugin leaves the mesh's existing Nanite setting untouched rather than guessing — an unresolved material is treated as "don't know," not as "safe" (this replaced an earlier fail-open bug that defaulted Nanite on).
This gating only ever runs when the import event actually carries a material assignment — the legacy flat texture.export path (no catalog, textures matched by filename only) never touches Nanite at all.
Material Instances created from catalog entries go through the same naming-convention resolver as every other asset, under asset type material_instance, falling back to a legacy MI_ prefix if the project's conventions don't define that asset type yet. Their texture, scalar, and vector parameters are copied straight from the catalog entry's declared slots onto a new Material Instance parented to that entry's master material.
CV tag search
On import or reimport, ApplyCvMetadata stamps whatever CV tags a bridge event or sidecar carries onto the asset's package metadata as dousen_cv_<key> asset-registry tags — sourced upstream from DousenDesktop's local, always-on CV auto-tagger and its review dialog before export. The Dousen panel's Tag Search area pre-populates its key dropdown with the five known keys:
dousen_cv_object_classdousen_cv_poly_tierdousen_cv_has_uvsdousen_cv_is_skinneddousen_cv_style_hint
Add a row per tag you want to filter on, fill in a value, and Search builds an AND query (an FARFilter over every key/value pair, recursive under /Game) and syncs the Content Browser's selection to every match, reporting "N assets matched". Clear resets the query. Pair it with Show Dousen Tags (above) when you want to see what's actually stamped on one specific asset first, rather than searching blind.
Headless import
You don't have to keep Unreal open to receive assets. DousenDesktop's importer treats the editor as "active" if the plugin has polled the bridge within the last 6 seconds:
- Editor active — the export is pushed straight to the plugin's local command server (see asset sync above) rather than spawning a second Unreal process, which would only fight the running one.
- Editor not active — and only when "Auto-import via UnrealEditor-Cmd when UE is not running" is enabled (off by default) with an executable path configured — DousenDesktop launches
UnrealEditor-Cmdunattended (-run=Python -unattended -nopause -nosplash) with a temporary Python script that reads its import parameters from a JSON sidecar (passed via an environment variable, not the command line, to avoid an injection risk) and imports throughunreal.AssetToolsHelpers.
Both the toggle and the executable path are configured on the DousenDesktop side, under Settings → Unreal.
Connecting to the bridge
The plugin authenticates to two different servers with two different bearer tokens: a DousenCore JWT (cached from your desktop login, or a DOUSEN_TOKEN environment override) for the REST API on port 8080, and the bridge relay's own per-session token (rotated every time DousenDesktop restarts, read from a file or a DOUSEN_BRIDGE_TOKEN environment override) for everything on port 8766 — see the Bridge API reference for the token file locations and rotation behavior. Plain HTTP to DousenCore is refused outside loopback per the cleartext rule above; the bridge relay itself is loopback-only by construction.
If the relay goes quiet, the poller backs off automatically as described in asset sync and recovers on its own once DousenDesktop is reachable again — no restart of the editor required.
Diagnostics
Everything the plugin does is logged to the editor's Output Log under the LogDousenUE category (and mirrored to the project's Saved/Logs/). Filter on it first when an import, a naming check, or a material-catalog report doesn't happen as expected — see Troubleshooting for the common cases.