Godot 4 · 3D · GDScript · Game Systems

Shatter
Transition System

// personal reference · glass break scene transition · 3D shards · mockup for battle transition

01 Concept & Design Decisions

Shatter transition captures the current scene as a screenshot, applies it to flat 3D shard pieces, then shatters them apart revealing the next scene underneath. Inspired by Code Monkey's Unity screen shatter tutorial.

Current Use Built as mockup for Town → Village transition. Reuse later for battle transition when battle system is ready. Same system, different scenes.

// Key design decisions

decisionchoicereason
Shard meshFlat pre-cut quadsSimple, no Voronoi fracture needed
Shard count25–35 piecesVisual sweet spot, light on PC
UV mappingTriplanar materialNo Blender needed, seamless texture
Shard materialUNSHADEDNext scene lighting won't affect shards
Next sceneLive, not frozenAtmosphere shows through gaps — dramatic
Black backgroundOptional (boolean)Use for day→night transitions
Shard spin3D angular velocityCinematic feel
ExplosionRadiant from centerRealistic physics feel
The Key Trick — Unshaded Material Since shards use UNSHADED material, the next scene's lighting (day/night/storm) does NOT affect the shard pieces. Screenshot texture always shows exactly as captured regardless of Village night lighting. This solves the lighting bleed problem completely.

02 Settings & Toggles SWITCHABLE

# =============================================
# SHATTER TRANSITION SETTINGS
# =============================================

# Number of shard pieces (25-35 recommended)
var SHARD_COUNT: int = 30

# How far shards fly outward (3D units)
var BLAST_POWER: float = 15.0

# Random angular spin on each shard
var SPIN_STRENGTH: float = 8.0

# How long shards stay before fading (seconds)
var SHARD_LIFETIME: float = 1.5

# How long fade out takes (seconds)
var FADE_DURATION: float = 0.4

# Show solid black background between shards
# true = black then fade to next scene (good for day→night)
# false = next scene visible through gaps immediately (dramatic)
var USE_BLACK_BACKGROUND: bool = false

# If USE_BLACK_BACKGROUND = true, how long to fade black to transparent
var BLACK_FADE_DURATION: float = 0.5

# =============================================

03 Full Transition Flow

// USE_BLACK_BACKGROUND = false (default)

Trigger transition
Capture Town screenshot
Load Village underneath
Shatter plays
Village visible through gaps
Shards fade + queue_free

// USE_BLACK_BACKGROUND = true

Trigger transition
Capture Town screenshot
Load Village underneath
Shatter plays over black
Black fades transparent
Village revealed + cleanup
Village loads before shatter starts Player never sees loading. Shatter animation covers it. By the time pieces fly away, Village is already fully loaded underneath.

04 Node Structure

Main (Node3D)
├── TownScene (Node3D) ← current scene
├── VillageScene (Node3D) ← loaded underneath, live
└── ShatterTransition (Node3D) ← spawned on trigger
├── ExplosionCenter (Node3D) ← blast origin point
├── BlackBackground (MeshInstance3D) ← optional
├── Shard_01 (RigidBody3D)
    ├── MeshInstance3D (flat quad)
    └── CollisionShape3D (box)
├── Shard_02 (RigidBody3D)
├── Shard_03 (RigidBody3D)
└── ... (25-35 total)
Shard Collision Layers Set shards to NOT collide with each other — only fall through space. This skips thousands of physics calculations per frame. Shards only need to collide with nothing, just fly outward and fall.

05 Screen Capture SUBVIEWPORT

Capture the current scene as a static image texture. Apply to all shards. This is the "frozen frame" that shatters apart.

# Capture current viewport as ImageTexture
func capture_screenshot() -> ImageTexture:
    var image = get_viewport().get_texture().get_image()
    var texture = ImageTexture.create_from_image(image)
    return texture

# Store it before loading next scene
var screenshot = capture_screenshot()
# then load Village, then apply screenshot to shards
Order matters Capture screenshot FIRST before loading Village. Otherwise you capture Village instead of Town.

06 Shard Setup

Each shard is a RigidBody3D with a flat quad mesh. Start frozen so they don't fall before transition triggers. Arranged in a grid covering the screen area.

# Generate shards procedurally at runtime
# No need for pre-made scene — just create flat quads in code

func create_shards(screenshot: ImageTexture):
    var cols = 6   # 6x5 = 30 shards
    var rows = 5
    var shard_w = screen_width / cols
    var shard_h = screen_height / rows

    for row in rows:
        for col in cols:
            var shard = RigidBody3D.new()
            shard.freeze = true  # frozen until blast

            # position each shard in grid covering screen
            shard.position = Vector3(
                (col * shard_w) - (screen_width / 2),
                (row * shard_h) - (screen_height / 2),
                0
            )

            # flat quad mesh
            var mesh = QuadMesh.new()
            mesh.size = Vector2(shard_w, shard_h)

            var mesh_instance = MeshInstance3D.new()
            mesh_instance.mesh = mesh
            mesh_instance.material_override = create_shard_material(screenshot)

            var col_shape = CollisionShape3D.new()
            var box = BoxShape3D.new()
            box.size = Vector3(shard_w, shard_h, 0.01)
            col_shape.shape = box

            shard.add_child(mesh_instance)
            shard.add_child(col_shape)
            add_child(shard)

07 Unshaded Material KEY TRICK

UNSHADED mode means next scene lighting (day/night/storm) does NOT affect shard appearance. Screenshot always looks exactly as captured.

func create_shard_material(screenshot: ImageTexture) -> StandardMaterial3D:
    var mat = StandardMaterial3D.new()

    # KEY: unshaded — no lighting affects this material
    mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED

    # Apply screenshot as albedo texture
    mat.albedo_texture = screenshot

    # Triplanar mapping — seamless texture across all shards
    # no UV unwrapping needed, projects from world space
    mat.uv1_triplanar = true
    mat.uv1_world_triplanar = true

    # Enable transparency for fade out later
    mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
    mat.albedo_color = Color(1, 1, 1, 1)  # fully opaque at start

    return mat
Why Triplanar? Each shard is a separate mesh with its own local UV space. Without triplanar, each shard would show the full screenshot instead of just its portion. Triplanar projects from world space — each shard automatically shows only the correct portion of the screenshot matching its screen position.

08 Explosion Impulse Code

Radiant explosion from center point. Closer pieces fly faster. Each piece also gets random 3D angular spin.

func blast_shards():
    var explosion_origin: Vector3 = $ExplosionCenter.global_position

    for shard in get_children():
        if not shard is RigidBody3D: continue

        # unfreeze
        shard.freeze = false

        # radiant direction from blast origin to shard
        var direction: Vector3 = shard.global_position - explosion_origin
        var distance: float = direction.length()

        # closer = faster (inverse distance)
        var impulse: Vector3 = direction.normalized() * (BLAST_POWER / max(distance, 0.1))

        # add slight forward push (toward camera) for dramatic effect
        impulse.z += BLAST_POWER * 0.3

        shard.apply_central_impulse(impulse)

        # random 3D angular spin
        var spin = Vector3(
            randf_range(-SPIN_STRENGTH, SPIN_STRENGTH),
            randf_range(-SPIN_STRENGTH, SPIN_STRENGTH),
            randf_range(-SPIN_STRENGTH, SPIN_STRENGTH)
        )
        shard.apply_torque_impulse(spin)

09 Black Background Option OPTIONAL

When enabled — solid black plane sits between shards and next scene. After shatter, black fades transparent revealing next scene. Good for dramatic day→night transitions.

func setup_black_background():
    if not USE_BLACK_BACKGROUND: return

    var bg_mesh = QuadMesh.new()
    bg_mesh.size = Vector2(screen_width * 2, screen_height * 2)  # oversized

    var bg_mat = StandardMaterial3D.new()
    bg_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
    bg_mat.albedo_color = Color(0, 0, 0, 1)  # solid black
    bg_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA

    $BlackBackground.mesh = bg_mesh
    $BlackBackground.material_override = bg_mat
    $BlackBackground.position.z = -0.1  # slightly behind shards

# fade black to transparent after shatter done
func fade_black_background():
    if not USE_BLACK_BACKGROUND: return

    var tween = create_tween()
    tween.tween_property(
        $BlackBackground.material_override,
        "albedo_color:a",
        0.0,  # fade to fully transparent
        BLACK_FADE_DURATION
    )
    await tween.finished
    # village now fully visible

10 Cleanup & Memory

After shards finish flying, fade them out and queue_free the entire shatter container. Never leave physics bodies running after transition ends.

func cleanup_shards():
    # wait for shards to fly away
    await get_tree().create_timer(SHARD_LIFETIME).timeout

    # fade out all shards simultaneously
    for shard in get_children():
        if not shard is RigidBody3D: continue
        var mat = shard.get_child(0).material_override
        var tween = create_tween()
        tween.tween_property(mat, "albedo_color:a", 0.0, FADE_DURATION)

    # wait for fade to finish
    await get_tree().create_timer(FADE_DURATION).timeout

    # purge everything — shards, collisions, meshes all freed
    queue_free()

11 Full Transition Manager SYSTEM

# ShatterTransition.gd — attach to ShatterTransition node
extends Node3D

# =============================================
# SETTINGS
# =============================================
var SHARD_COUNT: int = 30
var BLAST_POWER: float = 15.0
var SPIN_STRENGTH: float = 8.0
var SHARD_LIFETIME: float = 1.5
var FADE_DURATION: float = 0.4
var USE_BLACK_BACKGROUND: bool = false
var BLACK_FADE_DURATION: float = 0.5

# =============================================
# MAIN TRIGGER
# =============================================
func start_transition(next_scene_path: String):
    # 1. capture screenshot FIRST before loading next scene
    var screenshot = capture_screenshot()

    # 2. load next scene underneath
    var next_scene = load(next_scene_path).instantiate()
    get_parent().add_child(next_scene)
    get_parent().move_child(next_scene, 0)  # put behind everything

    # 3. setup black background if enabled
    setup_black_background()

    # 4. create shards on top
    create_shards(screenshot)

    # 5. blast shards apart
    blast_shards()

    # 6. fade black if enabled
    fade_black_background()

    # 7. cleanup after transition
    cleanup_shards()

    # 8. remove old Town scene
    get_parent().get_node("TownScene").queue_free()

// How to trigger from Town scene

# In Town.gd — when player hits exit trigger
func _on_exit_triggered():
    var shatter = load("res://transitions/ShatterTransition.tscn").instantiate()
    get_parent().add_child(shatter)
    shatter.start_transition("res://scenes/Village.tscn")

12 Summary

Build Order Get screenshot → apply to one shard → triplanar correct → then multiply to 30 shards → then add blast → then cleanup. Test each step before adding next.