Skip to content

Optional Helper Scripts

r2Nexus edited this page Jun 6, 2026 · 2 revisions

Optional Helper Scripts

This page contains optional helper scripts/tools that make the Blender part of the workflow less annoying.

None of this is strictly required. You can set up the materials and reload textures manually if you want to. The helper script just adds a small panel inside Blender so you do not need to keep re-running random script snippets every time you open the file.

StS2 Tool Kit

The StS2 Tool Kit is a small Blender add-on that adds a panel to the right side of the 3D viewport.

It can:

  • Apply recommended material settings for 2D puppet-style image planes.
  • Make materials unlit.
  • Change texture filtering.
  • Reload external images after you update/export them again from Krita.
  • Optionally only affect selected objects.

This is mainly meant for the Krita → Blender → Godot workflow described in this guide.

How to Install

The script is provided as plain source code so you can inspect it before using it.

  1. Copy the script below.
  2. Save it as sts2_tool_kit.py.
  3. Open Blender.
  4. Go to Edit.
  5. Open Preferences.
  6. Go to Add-ons.
  7. Click Install....
  8. Select sts2_tool_kit.py.
  9. Enable the add-on.
  10. In the 3D viewport, press N to open the right sidebar.
  11. Open the StS2 tab.

You should now see a panel called StS2 Tool Kit.

What the Options Do

Alpha Threshold

Controls how much transparency gets cut off.

Lower values keep softer edges. Higher values make cleaner but sharper cutouts.

For this workflow, values around 0.45 worked reasonably well for me, but you may need to adjust it depending on your art.

Texture Filtering

Controls how Blender displays image textures.

Options:

  • Linear - smooth texture filtering
  • Closest - pixelated texture filtering
  • Cubic - smoother texture filtering
  • Smart - lets Blender decide automatically

For most painted character art, Linear is probably fine.

Selected Objects Only

If enabled, the tool only changes materials used by selected objects.

If disabled, the tool searches through all objects in the file and changes any materials it can find.

This is useful if you only want to test settings on a few parts before applying them everywhere.

Make Materials Unlit

Rebuilds the image materials as simple unlit materials.

This is useful because the final character should look like flat 2D art, not like a shaded 3D model.

If this is disabled, the tool keeps the current material node setup and only changes the basic transparency/filtering settings.

Buttons

Apply Material Settings

Applies the current material settings.

Use this after importing your image planes, or after changing options like Alpha Threshold, Texture Filtering, or Make Materials Unlit.

Reload Images

Reloads external image files and then reapplies the current material settings.

This is useful when you update/export your character parts from Krita and want Blender to refresh them without manually reloading every texture.

Script

Click to expand the script
bl_info = {
    "name": "StS2 Tool Kit",
    "author": "lovecMC",
    "version": (1, 0, 4),
    "blender": (3, 6, 0),
    "location": "View3D > Sidebar > StS2",
    "description": "Simple tools for 2D puppet materials and image reloading.",
    "category": "Material",
}

import bpy


class STS2ToolKitSettings(bpy.types.PropertyGroup):
    alpha_threshold: bpy.props.FloatProperty(
        name="Alpha Threshold",
        description="Lower keeps softer edges. Higher makes cleaner but sharper cutouts.",
        default=0.45,
        min=0.0,
        max=1.0,
    )

    texture_filtering: bpy.props.EnumProperty(
        name="Texture Filtering",
        description="How Blender displays image textures",
        items=[
            ("Linear", "Linear", "Smooth texture filtering"),
            ("Closest", "Closest", "Pixelated texture filtering"),
            ("Cubic", "Cubic", "Smoother texture filtering"),
            ("Smart", "Smart", "Blender decides automatically"),
        ],
        default="Linear",
    )

    selected_only: bpy.props.BoolProperty(
        name="Selected Objects Only",
        description="Only edit materials used by selected objects",
        default=False,
    )

    make_unlit: bpy.props.BoolProperty(
        name="Make Materials Unlit",
        description="Rebuild image materials as simple unlit materials",
        default=False,
    )


def get_materials(context, selected_only):
    found = set()
    objects = context.selected_objects if selected_only else bpy.data.objects

    for obj in objects:
        data = getattr(obj, "data", None)
        materials = getattr(data, "materials", None)

        if not materials:
            continue

        for material in materials:
            if material and material.as_pointer() not in found:
                found.add(material.as_pointer())
                yield material


def find_image_texture(material):
    if not material.use_nodes or not material.node_tree:
        return None

    for node in material.node_tree.nodes:
        if node.type == "TEX_IMAGE" and node.image:
            return node

    return None


def apply_basic_material_settings(material, settings):
    image_nodes_changed = 0

    # Alpha Clip usually exports to Godot as masked / alpha scissor transparency.
    if hasattr(material, "blend_method"):
        material.blend_method = "CLIP"

    if hasattr(material, "alpha_threshold"):
        material.alpha_threshold = settings.alpha_threshold

    # 2D puppet parts usually need to be visible from both sides.
    if hasattr(material, "use_backface_culling"):
        material.use_backface_culling = False

    if hasattr(material, "show_transparent_back"):
        material.show_transparent_back = False

    # Keep the current shader setup. Only change image filtering.
    if material.use_nodes and material.node_tree:
        for node in material.node_tree.nodes:
            if node.type == "TEX_IMAGE" and hasattr(node, "interpolation"):
                node.interpolation = settings.texture_filtering
                image_nodes_changed += 1

    return image_nodes_changed


def make_material_unlit(material, settings):
    old_image_node = find_image_texture(material)

    if old_image_node is None:
        return False

    image = old_image_node.image

    material.use_nodes = True
    material.blend_method = "CLIP"
    material.alpha_threshold = settings.alpha_threshold

    if hasattr(material, "use_backface_culling"):
        material.use_backface_culling = False

    if hasattr(material, "show_transparent_back"):
        material.show_transparent_back = False

    nodes = material.node_tree.nodes
    links = material.node_tree.links
    nodes.clear()

    image_node = nodes.new("ShaderNodeTexImage")
    image_node.location = (-400, 0)
    image_node.image = image
    image_node.interpolation = settings.texture_filtering

    transparent_node = nodes.new("ShaderNodeBsdfTransparent")
    transparent_node.location = (-150, 100)

    unlit_node = nodes.new("ShaderNodeBackground")
    unlit_node.location = (-150, -100)
    unlit_node.inputs["Strength"].default_value = 1.0

    mix_node = nodes.new("ShaderNodeMixShader")
    mix_node.location = (100, 0)

    output_node = nodes.new("ShaderNodeOutputMaterial")
    output_node.location = (350, 0)

    links.new(image_node.outputs["Alpha"], mix_node.inputs["Fac"])
    links.new(transparent_node.outputs["BSDF"], mix_node.inputs[1])
    links.new(image_node.outputs["Color"], unlit_node.inputs["Color"])
    links.new(unlit_node.outputs["Background"], mix_node.inputs[2])
    links.new(mix_node.outputs["Shader"], output_node.inputs["Surface"])

    return True


def apply_material_settings(context, settings):
    changed = 0
    skipped = 0

    for material in get_materials(context, settings.selected_only):
        if settings.make_unlit:
            if make_material_unlit(material, settings):
                changed += 1
            else:
                skipped += 1
        else:
            apply_basic_material_settings(material, settings)
            changed += 1

    return changed, skipped


def reload_images(context):
    reloaded = 0
    skipped = 0

    for image in bpy.data.images:
        is_external_file = (
            image.source == "FILE"
            and image.filepath
            and image.packed_file is None
        )

        if not is_external_file:
            skipped += 1
            continue

        # Do not overwrite unsaved image edits made inside Blender.
        if image.is_dirty:
            skipped += 1
            continue

        try:
            image.reload()
            reloaded += 1
        except RuntimeError:
            skipped += 1

    if context.screen:
        for area in context.screen.areas:
            area.tag_redraw()

    return reloaded, skipped


class STS2_OT_apply_material_settings(bpy.types.Operator):
    bl_idname = "sts2.apply_material_settings"
    bl_label = "Apply Material Settings"
    bl_description = "Apply alpha clip, texture filtering, and optionally make materials unlit"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        settings = context.scene.sts2_tool_kit
        changed, skipped = apply_material_settings(context, settings)

        if changed == 0:
            self.report({"WARNING"}, "No materials found.")
            return {"CANCELLED"}

        if settings.make_unlit:
            self.report(
                {"INFO"},
                f"Updated {changed} unlit material(s). Skipped {skipped}.",
            )
        else:
            self.report(
                {"INFO"},
                f"Updated {changed} material(s).",
            )

        return {"FINISHED"}


class STS2_OT_reload_images(bpy.types.Operator):
    bl_idname = "sts2.reload_images"
    bl_label = "Reload Images"
    bl_description = "Reload images and apply the current material settings"
    bl_options = {"REGISTER", "UNDO"}

    def execute(self, context):
        settings = context.scene.sts2_tool_kit

        reloaded, image_skipped = reload_images(context)
        changed, material_skipped = apply_material_settings(context, settings)

        self.report(
            {"INFO"},
            f"Reloaded {reloaded} image(s). Updated {changed} material(s).",
        )

        return {"FINISHED"}


class STS2_PT_tool_kit(bpy.types.Panel):
    bl_label = "StS2 Tool Kit"
    bl_idname = "STS2_PT_tool_kit"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = "StS2"

    def draw(self, context):
        layout = self.layout
        settings = context.scene.sts2_tool_kit

        layout.prop(settings, "alpha_threshold")
        layout.prop(settings, "texture_filtering")
        layout.prop(settings, "selected_only")
        layout.prop(settings, "make_unlit")

        layout.separator()

        layout.operator("sts2.apply_material_settings", icon="MATERIAL")
        layout.operator("sts2.reload_images", icon="FILE_REFRESH")


classes = (
    STS2ToolKitSettings,
    STS2_OT_apply_material_settings,
    STS2_OT_reload_images,
    STS2_PT_tool_kit,
)


def register():
    for cls in classes:
        bpy.utils.register_class(cls)

    bpy.types.Scene.sts2_tool_kit = bpy.props.PointerProperty(
        type=STS2ToolKitSettings,
    )


def unregister():
    del bpy.types.Scene.sts2_tool_kit

    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)


if __name__ == "__main__":
    register()