DreamShaderLang
Getting Started

Project Layout

How DShader/ is organized, how a plugin contributes a source root of its own, which directories the plugin owns, and the package rules that decide what gets compiled.

DreamShaderLang sources live in one directory under the project root — DShader by default, set by Project Settings ▸ DreamPlugin ▸ Dream Shader ▸ Paths ▸ Source Directory. A relative path resolves against the project directory.

The plugin creates the source directory, its Packages subdirectory and the generated-shader directory at module startup, on the first editor launch, whether or not you have authored anything.

Since since 1.6.0 that is no longer the only root: every enabled plugin that ships a DShader folder contributes a source root of its own — see Plugin source roots.

The tree

M_Sample.dsm
F_Tint.dsf
Common.dsh
Color.dsh
dreamshader.package.json
Noise.dsh
DreamShader.code-workspace
dreamshader.lock.json

Plugin source roots

since 1.6.0

A plugin can carry the .dsm / .dsf / .dsh files that build its materials instead of parking them in the project's tree. Root="Plugin.<Name>" has been able to write assets into a plugin since 1.2.0; the source half was what was missing.

M_Toon.dsm
Toon.dsh
MoonToon.uplugin
AspectRule
Discoveryno configuration needed. The toggle is Project Settings ▸ … ▸ Paths ▸ Scan Plugin Source Directories (default on)
Default asset targeta plugin-root file with no Root= generates into its own plugin's mount point/MoonToon, not /Game. Only an absent or whitespace-only Root is defaulted, so Root="/" opts back into /Game
Importsnever cross roots. A file resolves its imports against its own root and that root's Packages folder; crossing one deliberately means import "Plugin.MoonToon:Shared/Toon.dsh"; — see Imports
Writabilityonly the project root is writable. VirtualFunction sync skips files under a plugin root: a plugin ships its definitions as authored, and the editor no longer rewrites them
Watcherone watch per root, so Auto Compile On Save fires for a plugin's sources the same way it does for the project's
WorkspaceDreamShader.code-workspace lists one folders entry per root — the project as ".", then Plugin: <Name> for each plugin root
Gen pagea plugin-root file is labelled with its root name in the row subtitle, and the search box matches root names — typing a plugin's name filters to everything it ships

A plugin DShader folder that overlaps an existing root is ignored with a warning, rather than handing the same file to two owners. That happens when Source Directory is pointed at a plugin folder, or at something containing one.

The root list is cached and rebuilt only when Source Directory or the scan toggle changes, or when DreamShader Gen ▸ Refresh is pressed — which is what picks up a DShader folder you just created, or a plugin mounted mid-session. The watcher is still registered once at startup, so Auto Compile On Save for a root that appeared mid-session starts working on the next editor start.

Directory roles

DirectoryOwnerContents
DShader/youThe source root. Any layout below it works; the ones here are conventions, not rules.
DShader/Materials/youMaterial .dsm files, usually one Shader each.
DShader/Functions/youReusable .dsf function files.
DShader/Shared/youProject-local .dsh headers.
DShader/VirtualFunctions/the editorWhere Create Virtual Function writes a declaration for an existing UMaterialFunction.
DShader/Decompiled/the editorWhere Export DSM / Export DSF write, under Materials, Functions, Layers and LayerBlends.
DShader/Packages/the extensionInstalled shared libraries. Always the literal Packages subdirectory of the source root; not separately configurable.
DShader/DreamShader.code-workspacethe pluginRewritten from scratch on every Open Dream Shader Workspace.
DShader/dreamshader.lock.jsonthe extensionRecords installed package revisions. Neither read nor written by the plugin.
Intermediate/DreamShader/GeneratedShaders/the pluginThe generated .ush helper includes, mounted at /DreamShaderGenerated.

DreamShader.code-workspace is serialized from a fixed object on every invocation — it is never read, merged or preserved. Hand-added launch, tasks, extensions or extra settings entries are destroyed. Keep per-user configuration in DShader/.vscode/settings.json, which the command does not touch.

Naming conventions

KindConvention
Material fileM_*.dsm
Material function fileF_*.dsf
Shared headernamed by domain — Texture.dsh, Color.dsh
Package entryLibrary/<Name>.dsh

Extensions are compared case-insensitively, so M_Water.DSM is a material file. Identifiers are a different matter: non-ASCII characters in namespace and function names are replaced when the plugin sanitizes them into HLSL and asset identifiers, so keep those ASCII.

Imports

Prefer a small number of stable entry imports per material:

import "Shared/Common.dsh";
import "@typedreammoon/dream-noise/Library/Noise.dsh";

A specifier is tried against three candidates in order: the importing file's own directory, the owning root's source directory, then that root's Packages directory. The first candidate that exists and stays inside its own containment root wins — a file inside a package cannot climb out with ../. A specifier with no extension gets .dsh appended, so import "…/Noise" can never resolve to a .dsf.

An unqualified specifier since 1.6.0 never leaves its own root, so installing a plugin cannot change what an existing import means. To cross one deliberately, write import "Plugin.MoonToon:Shared/Toon.dsh";. Full rules: Imports and Namespaces.

Keeping imports shallow also keeps rebuilds honest: the source hash covers the whole inlined import closure, so a header edit invalidates every file that pulls it in.

What is compiled, and what is not

The plugin has two enumerators, and they exclude different things.

EnumeratorExtensionsExcludesUsed by
Full source enumeration.dsm, .dsh, .dsfeverything under any root's Packages folderstartup in-memory generation, the commandlet's compile -All, cook, the Gen page list, VirtualFunction sync
Material source enumeration.dsm, .dsfonly .dsm files under DShader/PackagesRecompile DSM, the auto-compile queue, the dependency graph

The Packages exclusion is complete for .dsm but only partial for .dsf. A .dsf shipped inside a package is compiled in an interactive editor session — the file watcher and Recompile DSM pick it up — and is not compiled by compile -All, by cook, or by the Gen page. A package that ships .dsf function assets therefore works locally and silently produces nothing in a headless build. Ship library code as .dsh headers, or copy the .dsf out of Packages into your own source tree.

Examples/**/*.dsm inside a package is never compiled by any path and never appears in the Gen page. To use an example, copy it out of DShader/Packages into DShader/.

Package .dsh headers are fully importable, but are never scanned for VirtualFunction declarations — a VirtualFunction shipped in a package is never validated against its asset.

Source control

ItemCommit?
.dsm / .dsf / .dshyes — this is the material logic
dreamshader.lock.jsonyes, if the team installs packages
Config/DefaultEngine.iniyes — the project settings live there and are shared
DShader/Packages/team's choice: vendored, or reinstalled from the lock file
Generated .uasset filesusually no — with the default backend the editor writes none anyway
Intermediate/DreamShader/no

Standardize Source Directory across the team so imports resolve identically on every machine. A plugin's own DShader folder is committed with the plugin — it is part of the same deliverable as its Content/.

Next

On this page