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 multipliers
- `MF_DirtBlend` — overlays a world-space dirt layer driven by a scalar mask
- `MF_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 `FunctionInput` instead.
- Use `UsesMaterialAttributes = true` on functions that pack a full PBR set via `SetMaterialAttributes`. 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 `StaticBool` parameters with a `StaticSwitch` node rather than a runtime `If`. 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.*