DreamShaderLang
Plugin referenceTools

VirtualFunction tools

VirtualFunction tools

VirtualFunction tools

DreamShader » Tools » VirtualFunction tools

The editor actions that generate, open and refresh a VirtualFunction declaration for an existing UMaterialFunction asset, plus the startup service that keeps those declarations in sync with the assets they name.

Attaches toUMaterialFunction, UMaterialFunctionMaterialLayer, UMaterialFunctionMaterialLayerBlend
Menu locationContent Browser context menu ▸ DreamShader ▸ VirtualFunction, and the Material Editor DreamShader toolbar combo
Writes<SourceDirectory>/VirtualFunctions/<Name>.dsh, UTF-8 without BOM
Since1.2.0 (the declaration) · 1.2.1 (the menu and the .dsh writer) · 1.2.2 (reuse, OpenVirtualFunction, startup sync)

Context menu

The VirtualFunction section is built fresh on every right-click. Before building it, the editor searches every project source file for a VirtualFunction declaration whose resolved asset matches the selected UMaterialFunction. The entries you get depend on the answer.

When a declaration already exists

Entry nameLabelTooltipIconEffect
DreamShader.OpenVirtualFunctionOpenVirtualFunction"Open the existing DreamShader VirtualFunction definition in VSCode."Icons.OpenInExternalEditorOpens the file that holds the declaration, positioned at the declaration's line and column
DreamShader.CopyVirtualFunctionReferenceCopy Virtual Function Reference"Copy a DreamShader Graph call that references this existing VirtualFunction."GenericCommands.CopyCopies a call built from the declared name, not from the asset name

No other entry is offered — in particular there is no Create, because one already exists.

When no declaration exists

Entry nameLabelTooltipIconEffect
DreamShader.CopyVirtualFunctionCopyVirtualFunction"Copy a complete DreamShader VirtualFunction declaration for this Material Function."GenericCommands.CopyCopies the whole declaration to the clipboard
DreamShader.CreateVirtualFunctionCreateVirtualFunction"Create a .dsh file containing the VirtualFunction declaration."Icons.SaveWrites the declaration to a new .dsh and opens it
DreamShader.CopyVirtualFunctionPrototypeCopy extern Prototype (2.0) (since 2.0.0)"Copy the 2.0 extern prototype of this Material Function, with its /// @asset line, for a .dss or a .dsh."GenericCommands.CopyCopies the .dss spelling of the declaration — see Editor integration
DreamShader.CreateVirtualFunctionPrototypeCreate extern Prototype (2.0) (since 2.0.0)"Create a .dsh file holding the 2.0 extern prototype of this Material Function, for .dss sources to #include."Icons.SaveWrites that prototype to a new .dsh and opens it
DreamShader.CopyVirtualFunctionCallCopyVirtualFunctionCall"Copy a DreamShader Graph call example for this VirtualFunction."GenericCommands.CopyCopies a call example to the clipboard

The match is on the declaration's resolved asset object path, compared case-insensitively; the first match wins. Two spellings count as a declaration: a 1.x VirtualFunction block in any source, and (since 2.0.0) an extern prototype with a /// @asset line in a .dsh or .dss. Because the lookup reads the files, a declaration written by hand a moment ago is found immediately — nothing is cached from startup.

[!WARNING] The lookup re-enumerates and re-reads every source (.dsm, .dsh, .dsf, .dss, .dsi, .dsp) under every source root, outside each root's Packages folder, from disk, on every right-click of a Material Function asset; a .dsh or .dss that mentions both extern and @asset is also parsed. The cost scales with the number of source files and is paid before the context menu appears. There is no cache and no way to disable it short of -NoDreamShaderEditorBridge, which removes the menu entirely.

Actions

Runtime substitutions are shown as {Placeholder} throughout this page.

ActionSuccess toastFailure toasts
CopyVirtualFunctionCopied VirtualFunction definition for {Asset}. — the full text is also logged at DisplayDreamShader could not find the selected Material Function. · DreamShader failed to build VirtualFunction: {Error}
CreateVirtualFunctionCreated VirtualFunction file: {File}asset gone · build failure · DreamShader failed to create directory: {Directory} · DreamShader failed to write VirtualFunction file: {File} · Created VirtualFunction file but could not open it: {File}
OpenVirtualFunctionOpened VirtualFunction definition: {File}asset gone · DreamShader could not find a VirtualFunction definition for {Asset}. · DreamShader could not open VirtualFunction file: {File}
Copy Virtual Function ReferenceCopied VirtualFunction reference for {Name}. — the declared name, not the asset nameasset gone · not found · DreamShader failed to build VirtualFunction reference: {Error}
CopyVirtualFunctionCallCopied VirtualFunction call for {Asset}.asset gone · DreamShader failed to build VirtualFunction call: {Error}

[!NOTE] CreateVirtualFunction on an asset that already has a declaration silently redirects to OpenVirtualFunction. It never writes a duplicate file. The toast you get is Opened VirtualFunction definition: {File}, not a "created" message. Create extern Prototype does the same when either spelling exists.

Where the file is written

AspectValue
Directory<SourceDirectory>/VirtualFunctions/ — DShader/VirtualFunctions/ by default
Preferred file name<sanitized MaterialFunction name>.dsh
On collision<sanitized name>_<crc32 of the asset's object path>.dsh, the CRC printed as eight lowercase hex digits
EncodingUTF-8 without BOM
After writingopened through the preferred-editor chain — VSCode, then the OS default editor, then Notepad

A .dsh written here is picked up by the source-directory watcher like any other header, so the materials that import it recompile automatically.

The generated declaration

VirtualFunction(Name="<SanitizedName>")
{
    Options = {
        Asset = <asset-literal>;
        Description = "Generated from <full object path>";
    }

    Inputs = {
        [opt ]<type> <Name>[ = <default>][ [ Description="…"; SortPriority=<n>; ] ];
        …
    }

    Outputs = {
        <type> <Name>[ [ Description="…"; SortPriority=<n>; ] ];
        …
    }
}

The body is tab-indented. A declaration is produced only when the asset exposes at least one output.

Asset literal

Object path begins withEmitted as
/Game/Path(Game, "<rest>")
/Engine/Path(Engine, "<rest>")
a content plugin's mount point (longest match over the enabled content plugins)Path(Plugins.<PluginName>, "<rest>")
anything elsethe raw object path, unwrapped

See Path for the accepted root spellings.

Input types

The mapping from Unreal's function-input types to DreamShaderLang tokens is exhaustive:

EFunctionInputTypeToken
FunctionInput_Scalarfloat
FunctionInput_Vector2float2
FunctionInput_Vector3float3
FunctionInput_Vector4float4
FunctionInput_Texture2DTexture2D
FunctionInput_TextureCubeTextureCube
FunctionInput_Texture2DArrayTexture2DArray
FunctionInput_VolumeTextureVolumeTexture
FunctionInput_MaterialAttributesMaterialAttributes
FunctionInput_SubstrateSubstrate
FunctionInput_StaticBoolStaticBool — kept distinct so callers can pass a static bool
FunctionInput_Boolbool
(any other value)float4

Optional inputs and defaults

An input is prefixed opt when the asset marks it as using its preview value as the default. The default literal is rendered from that preview value:

Input typeDefault literal
Scalarthe float value
StaticBool, Booltrue / false
Vector2float2(x, y)
Vector3float3(x, y, z)
Vector4float4(x, y, z, w)
anything else(empty — the = suffix is omitted entirely)

[!WARNING] A texture or material-attributes input marked optional gets the opt prefix but no default value, because there is no literal syntax for one. Supply the argument at the call site, or edit the declaration.

Metadata suffix

KeyEmitted when
Descriptionthe asset's description for that parameter is non-empty
SortPrioritythe asset's sort priority differs from the parameter's own index

Identifier sanitization

RuleDetail
First charactermust be a letter or _; anything else is replaced with _
Later charactersmust be alphanumeric or _; anything else is replaced with _
Runs of _consecutive underscores collapse to one
All-underscore resultbecomes DreamShaderSymbol
Empty inputbecomes DreamShaderSymbol
A name that degraded to DreamShaderSymbolfalls back to Input<N>, Output<N> or VirtualFunction<N>, <N> being the 1-based index
Duplicate names in one declarationuniquified with _2, _3, …

Non-ASCII letters are preserved — the sanitizer is Unicode-aware, not ASCII-only.

Inside emitted string literals, \, ", carriage return, line feed and tab are escaped.

The call example

CopyVirtualFunctionCall and Copy Virtual Function Reference both produce:

<FunctionName>(<arg>, <arg>, …, OutputIndex=0)
RuleDetail
Argument per inputthe input's sanitized, uniquified name
Optional inputsthe literal token default
Trailing argumentOutputIndex=0, always appended, even for a single-output function
ErrorCause
VirtualFunction name cannot be empty.the declared name was empty
VirtualFunction '{Name}' does not expose any outputs.the function exposes no outputs
No MaterialFunction asset was provided.a null asset reached the builder
MaterialFunction '{Name}' does not have a valid package path.the asset has no package to build a literal from
MaterialFunction '{Name}' does not expose any outputs.the declaration builder found no outputs

The copied text is a template for 1.x text. Replace the placeholder argument names with real Graph values, and change OutputIndex when you want a different output. See Calls. default as an argument is a legacy rule (L25): 1.x text only, and dsc migrate rewrites it to named arguments.

Startup sync service

Once per editor session, at bridge startup, DreamShader re-reads every VirtualFunction declaration in the project, rebuilds it from the live asset, and writes the file back when the text changed.

[!WARNING] There is no watcher for this. Editing a UMaterialFunction's inputs or outputs does not refresh the declaration; nor does saving the .dsh. The declarations are re-synchronized only on the next editor start.

AspectBehaviour
Scanned filesevery source under the writable source roots — the project's own; plugin roots are skipped — excluding each root's Packages folder
Refused filesany source containing preprocessor directives — DSH9001 (since 1.9.0)
Keyword matchingthe bare word VirtualFunction, matched case-sensitively, with identifier-boundary checks
Skipped regionsquoted strings with \ escapes, // line comments and /* */ block comments
Block extractiona balanced ( … ) followed by a balanced { … }; an optional trailing ; is absorbed into the block's range
Validationeach extracted block is parsed on its own by the legacy front end (since 2.0.0; the 1.x parser before) and must yield exactly one VirtualFunction, whose Options.Asset must resolve
Not synceda 2.0 extern prototype with /// @asset: the lookup finds it, sync never rewrites it (since 2.0.0)
Comparisonon normalized text — CRLF and CR become LF, then the whole text is trimmed — so line-ending and surrounding-whitespace differences never trigger a rewrite
Rewrite orderreplacements are applied back to front, by descending start offset, so earlier offsets stay valid
Write-backthe whole file, UTF-8 without BOM

Because the scanner is a hand-rolled lexer rather than the front end, it finds declarations anywhere a file may legally hold one — including inside a .dsm alongside a Shader block.

Conditional sources are refused (since 1.9.0)

A source containing any preprocessor directive is not synced. It is reported once, as DSH9001, and left exactly as written.

The reason is the write-back, not the read. Sync records StartIndex / EndIndex as byte offsets and splices the rebuilt declaration over that range. The preprocessor guarantees line-count conservation — which buys line alignment and nothing more, because a cut line is replaced by a shorter empty one. Every byte offset past the first cut therefore points at the wrong place, and there is no offset map in the contract to correct with. Were a preprocessed string to reach SaveStringToFile, every dead branch in the file would be flattened permanently.

This is the same accident as Adopt, and worse in one respect: Adopt is a menu item a person chooses, while this service runs unattended at bridge startup across every writable source in the project.

[!NOTE] The gate is reached only after a VirtualFunction substring test, so a conditional material that holds no declaration — which sync would never have rewritten anyway — does not report an error at every editor start.

The read-only users of the same scanner are unaffected and keep scanning the raw text: the context menu, Open Virtual Function, and the copied call example all see the union over every branch. That matches the rule the dependency graph follows, and it is safe for the same reason — reading every branch over-reports, and over-reporting is harmless.

Sync diagnostics

Every message below is recorded with stage virtualFunctionSync, code virtual-function-sync, source DreamShader VirtualFunction, and severity error — the only severity the sync produces. They reach diagnostics.json, the sharded diagnostics/ directory and bridge.db.

MessageCause
DreamShader could not read VirtualFunction source file '{File}'.the file could not be read; reported at line 1, column 1
VirtualFunction attributes are missing a closing ')'.the attribute list is unbalanced
VirtualFunction body is missing a closing '}'.the body braces are unbalanced
VirtualFunction declaration is invalid: {ParserError}the extracted block did not parse — {ParserError} is the message of the legacy front end's first error, without its code — or did not contain exactly one VirtualFunction
VirtualFunction '{Name}' asset reference is invalid: {Error}Options.Asset could not be resolved; the raw literal is recorded as the asset path
VirtualFunction '{Name}' references missing MaterialFunction '{Path}'.the asset path resolved but the object failed to load
VirtualFunction '{Name}' could not be refreshed from MaterialFunction '{Path}': {Error}the declaration builder failed for the live asset
DreamShader failed to update VirtualFunction source file '{File}'.the rewritten file could not be written; reported at line 1, column 1

Expected exactly one VirtualFunction block. is the parser error text carried by the fourth row when a block contains zero or several declarations.

The conditional-source refusal obeys the paragraph above like every other row: its record still carries stage virtualFunctionSync, code virtual-function-sync and severity error. Its DSH9001 travels in the message text, exactly as DSH8149 does on the Adopt gate — the wire contract is unchanged.

It is nonetheless raised through the same FailWith helper the coded diagnostics use, and that is a deliberate trade rather than an inconsistency: gen-diagnostics.ps1 discovers codes by scanning for exactly that call shape, so a refusal written any other way would exist in the source and not in this manual. DSH9001 is the first entry in the DSH9xxx tools range, which was empty before it.

Reporting

LogWhen
DreamShader refreshed {Count} VirtualFunction definition(s) in '{File}'.a file was rewritten
DreamShader scanned {Scanned} VirtualFunction definition(s), refreshed {Refreshed}, reported {Issues} issue(s).at the end, only when something happened

Files that hold at least one declaration and produced no diagnostics have their diagnostics cleared; files that produced diagnostics have them set.

Example

Right-click /Game/Functions/MF_Weathering (a UMaterialFunction with one scalar and one texture input, and one output) ▸ DreamShader ▸ CreateVirtualFunction. The editor writes <Project>/DShader/VirtualFunctions/MF_Weathering.dsh and opens it:

VirtualFunction(Name="MF_Weathering")
{
	Options = {
		Asset = Path(Game, "Functions/MF_Weathering");
		Description = "Generated from /Game/Functions/MF_Weathering.MF_Weathering";
	}

	Inputs = {
		opt float Amount = 0.5 [
			Description="Weathering strength";
		];
		Texture2D Mask;
	}

	Outputs = {
		float3 Result;
	}
}

CopyVirtualFunctionCall on the same asset copies:

MF_Weathering(default, Mask, OutputIndex=0)

Using it from a material:

import "VirtualFunctions/MF_Weathering.dsh";

Shader(Name="Materials/M_Rock")
{
    Properties = {
        Texture2D MaskTex = Path(Game, "Textures/T_Mask");
    }

    Outputs = {
        float3 Color;
        Base.BaseColor = Color;
    }

    Graph = {
        Color = MF_Weathering(0.8, MaskTex, OutputIndex=0);
    }
}

See also

  • VirtualFunction — the declaration's grammar and its Options keys
  • Options — every key the Options section accepts
  • Editor integration — where the menu attaches and what else lives there
  • Calls — calling a VirtualFunction from a Graph
  • Inputs / Outputs / Results — opt, defaults and metadata in a declaration
  • Path — the Path(Root, "…") literal the Asset key uses
  • Import — how a material picks up the generated .dsh
  • Preprocessor — why a conditional source is refused with DSH9001
  • Divergence — Adopt, the other source writer, refused on the same grounds
  • Decompiler — the same declaration builder, used for every called function
  • Bridge — where the sync diagnostics are published
  • Diagnostics index — every message, by stage

On this page