[UE 5.8] Substrate Toon Profile Override in Material Instance fails to apply after restarting the Editor

Summary

When using the experimental Substrate Toon Shading feature in UE 5.8, assigning a Toon Profile via the Material Property Overrides in a Material Instance works correctly during the initial session. However, after restarting the Unreal Editor, the overridden Toon Profile fails to apply or render correctly.

The issue seems to be related to shader caching (DDC) or initialization, as the overridden profile is completely ignored upon startup. The rendering only returns to normal if the shader is manually recompiled (e.g., making a dummy change to the master material).

What Type of Bug are you experiencing?

Editor

Steps to Reproduce

Enable Substrate in the Project Settings.

Create a Base Material using the Substrate Toon Shading model.

Create a Material Instance (MI) from the Base Material.

Open the MI, go to ‘Material Property Overrides’, enable ‘Toon Profile’, and assign a Toon Profile Data Asset.

Apply the MI to a mesh and observe that it renders correctly in the viewport.

Save all assets and close the Unreal Editor.

Relaunch the Editor and open the project.

Observe the mesh with the MI applied.

Expected Result

The overridden Toon Profile should load and render correctly immediately after opening the project, retaining the serialized override data.

Observed Result

The Toon Profile override is not applied or rendered correctly upon project load. The rendering only returns to normal if the shader is manually recompiled (e.g., making a dummy change to the master material).

Affects Versions

5.8

Platform(s)

Windows

Was able to remedy this for now with a temporary python script added to the projects init_unreal.py. The MI overrides don’t work because the Toon Profile assigned to the Master Material is not being loaded at all on startup. The script essentially makes the dummy changes and then clears the dirty flags.

You need to enable Unreal’s Python Editor Script Plugin, Add your actual master material paths to the top and paste the script into your init_unreal file (YourProject/Content/Python/init_unreal.py) or create one there if you don’t have.

Script:

import unreal

TOON_PROFILE_REFRESH_MATERIALS = (
“/Game/Path/To/M_YourMasterMaterial”,
“/Game/Path/To/M_AnotherMasterMaterial”,
“/Game/Path/To/M_AdditionalMasterMaterial”,
)

TOON_PROFILE_REFRESH_DELAY_TICKS = 180
_toon_profile_refresh_tick_count = 0
_toon_profile_refresh_tick_handle = None

def _refresh_material_toon_profile(material_path):
material = unreal.load_asset(material_path)

if material is None:
    unreal.log_warning(
        "[ToonProfileRefresh] Material not found: {}".format(material_path)
    )
    return False

toon_nodes = [
    expression
    for expression in unreal.MaterialEditingLibrary.get_material_expressions(
        material
    )
    if isinstance(
        expression,
        unreal.MaterialExpressionSubstrateToonBSDF
    )
]

node_profiles = [
    (node, node.get_editor_property("toon_profile"))
    for node in toon_nodes
    if node.get_editor_property("toon_profile")
]

if not node_profiles:
    unreal.log_warning(
        "[ToonProfileRefresh] No assigned Toon Profile found in {}".format(
            material_path
        )
    )
    return False

# Clear the Toon Profiles and compile once.
try:
    for node, _profile in node_profiles:
        node.set_editor_property("toon_profile", None)

    unreal.MaterialEditingLibrary.recompile_material(material)

finally:
    # Always restore the original Toon Profiles.
    for node, profile in node_profiles:
        node.set_editor_property("toon_profile", profile)

# Compile again with the restored profiles.
unreal.MaterialEditingLibrary.recompile_material(material)

# The material finishes in its original state, so remove the artificial
# dirty flag without saving the asset.
editor_assets = unreal.get_editor_subsystem(
    unreal.EditorAssetSubsystem
)
editor_assets.set_dirty_flag(material, False)

unreal.log(
    "[ToonProfileRefresh] Refreshed {} Toon node(s) in {}".format(
        len(node_profiles),
        material_path
    )
)

return True

def _run_toon_profile_startup_refresh():
refreshed_count = 0

for material_path in TOON_PROFILE_REFRESH_MATERIALS:
    try:
        refreshed_count += int(
            _refresh_material_toon_profile(material_path)
        )
    except Exception as error:
        unreal.log_error(
            "[ToonProfileRefresh] Failed for {}: {}".format(
                material_path,
                error
            )
        )

unreal.log(
    "[ToonProfileRefresh] Complete: {}/{} materials".format(
        refreshed_count,
        len(TOON_PROFILE_REFRESH_MATERIALS)
    )
)

def _on_toon_profile_startup_tick(_delta_seconds):
global _toon_profile_refresh_tick_count
global _toon_profile_refresh_tick_handle

_toon_profile_refresh_tick_count += 1

if _toon_profile_refresh_tick_count < TOON_PROFILE_REFRESH_DELAY_TICKS:
    return

unreal.unregister_slate_post_tick_callback(
    _toon_profile_refresh_tick_handle
)
_toon_profile_refresh_tick_handle = None
_run_toon_profile_startup_refresh()

_toon_profile_refresh_tick_handle = unreal.register_slate_post_tick_callback(
_on_toon_profile_startup_tick
)
unreal.log(“[ToonProfileRefresh] Scheduled startup refresh”)