Packaging shader logic into named, reusable blocks is the kind of discipline that separates a rushed prototype from a maintainable game codebase. Material Functions in Unreal Engine 5 give you exactly that — a container for any node sub-graph you want to call from multiple Materials without copy-pasting. Every tweak propagates automatically to every asset that references the function.
What Material Functions are (and why solo devs need them)
A Material Function is a UE5 asset (MaterialFunction) that holds a self-contained node graph with explicit inputs and outputs. You build it once in the Material Function editor, then place FunctionInput and FunctionOutput nodes at its boundary. Any Material that references it via a Call Material Function node gets the encapsulated logic compiled inline — there is no runtime overhead compared to duplicating the nodes by hand.
The productivity payoff is immediate. If you have 40 GLB props from BitSoul's marketplace all using a dirt-layer blend, fixing the blend formula in one Material Function updates every prop simultaneously. Without functions, the same fix is 40 manual edits.
When to reach for a Material Function:
- Any node cluster you've duplicated across two or more Materials
- PBR channel packing logic you want to standardize (ORM maps, normal strength)
- Blend operations driven by world position, vertex color, or a mask texture
- Utility math you'd otherwise re-build per Material (triplanar projection, HSV shift)
![]()
Building your first Material Function in UE5
Create a Material Function in the Content Browser → Add → Material & Textures → Material Function. Name it with a MF_ prefix by convention — MF_DirtBlend, MF_PBRCore, MF_EmissiveMask.
Inside the editor you have three key boundary node types:
| Node | Purpose |
|---|---|
FunctionInput | Exposes a parameter slot on the Call node |
FunctionOutput | Sends a value back to the parent Material |
SetMaterialAttributes | Packs full PBR channels into a single wire |
A minimal dirt-blend function in pseudocode:
FunctionInput: BaseColor (Vector3)
FunctionInput: DirtMask (Scalar, 0–1)
FunctionInput: DirtColor (Vector3, default = 0.05, 0.04, 0.03)
LinearInterpolate(BaseColor, DirtColor, DirtMask)
↓
FunctionOutput: BlendedColor
Pin the FunctionInput nodes to a LinearInterpolate, then route the result to FunctionOutput. Set a Preview Value on each input so the function renders correctly when edited in isolation.
Exposing scalar inputs as Material Parameters
Inside a function you can use ScalarParameter and VectorParameter nodes exactly like in a regular Material. These bubble up as exposed parameters when a calling Material Instance is opened, so you can vary DirtIntensity per-prop in the Instance without touching the function graph:
ScalarParameter(DirtIntensity, default=0.5)
↓
Multiply(DirtMask, DirtIntensity)
↓
FunctionOutput: ScaledDirtMask
Connecting Material Functions to your GLB asset library
Once your GLB props are in engine, create one Master Material per surface category — metal, painted plastic, stone, fabric — and have each one call the relevant Material Functions. All per-asset variation lives in Material Instances that override scalar and vector parameters, not in duplicated Material graphs.
A three-layer master setup that covers most game environments:
MF_PBRCore— passes BaseColor, Roughness, Metallic, and Normal through with optional intensity multipliersMF_DirtBlend— overlays a world-space dirt layer driven by a scalar maskMF_EmissiveMask— gates emissive glow through a packed texture channel (often stored in the ORM alpha)
Each function is a separate asset; swap or update any layer independently. When you pull new free props from bitsoulhosting.com/marketplace, they slot into this system with a single Material Instance assignment — no rework required.
![]()
Optimizing Material Function shader complexity
Material Functions compile inline — the HLSL they generate is identical to hand-placed nodes. There is no call overhead, but there is also no automatic reduction in instruction count. Keep these rules in mind:
- Avoid texture samples inside functions where possible. Textures sampled inside a function count against the Material's sampler limit (16 in SM5). Pass the already-sampled vector as a
FunctionInputinstead. - Use
UsesMaterialAttributes = trueon functions that pack a full PBR set viaSetMaterialAttributes. This avoids a separate wire per channel and keeps the call node compact. - Check shader complexity after adding each function call: Viewport → View Mode → Shader Complexity. A function that looks lightweight can add 50+ instructions if it branches on a dynamic condition.
- Cull dead branches at compile time using
StaticBoolparameters with aStaticSwitchnode rather than a runtimeIf. The compiler drops the unused branch entirely — critical for mobile and VR budgets.
Approximate instruction-count targets to benchmark against:
Simple surface < 60 instructions
Standard PBR 60–120 instructions
Complex layered 120–200 instructions ← profile before shipping
Mobile budget < 80 instructions
Functions that stay under these ceilings are safe to stack. Above 200 instructions, profile on target hardware before committing to the design.
Material Functions are among the highest-ROI patterns in UE5 shader work — build them early and every new 3D asset you import snaps into a coherent, maintainable shader system without redundant graph maintenance. Browse and download game-ready reusable GLB assets at bitsoulhosting.com/marketplace to put your new function library to work immediately.
This post is part of the Ultimate Guide to Free 3D Game Assets — BitSoul's complete reference for formats, texturing, rigging, optimization, and engine integration.
Need drop-in assets for this workflow? Grab the Architecture Western Bundle on the BitSoul marketplace and drop them straight into your project.