2 patterns-earthbound-HDMA-Scanline-Shift
Balazs Horvath edited this page 2026-04-18 11:13:11 +02:00

HDMA Scanline Shift

Inspired by EarthBound's HDMA effects, this pattern shifts each scanline horizontally based on sin wave modulation.

Mathematical Formula


\text{shift}_{\text{amount}} = \sin(y \cdot k + t \cdot \omega) \cdot \text{max}_{\text{shift}}

x' = (x - \text{shift}_{\text{amount}}) \bmod \text{width}

Where:

  • y is the vertical position (0 to height)
  • k is the spatial frequency (vertical)
  • t is time (frame number)
  • \omega is the temporal frequency
  • \text{max}_{\text{shift}} is the maximum pixel shift

How It Works

HDMA (Horizontal Direct Memory Access) allows mid-frame register updates on the SNES. This pattern simulates that effect by:

flowchart TD
    A[Create coordinate grid] --> B[Calculate shift amount]
    B --> C[Apply shift using roll]
    C --> D[Color with phase-shifted sine waves]
    D --> E[Output animation]
    
    B --> B1[sin Y * k + t * omega]
    B --> B2[max_shift multiplier]
    C --> C1[torch.roll]
    C --> C2[Horizontal dimension]
    D --> D1[120 phase shifts]
    D --> D2[RGB channels]

Implementation

import torch

width, height = 512, 512
frames = []

for t in range(30):
    # Create coordinate grids
    x = torch.linspace(0, 2*torch.pi, width)
    y = torch.linspace(0, 2*torch.pi, height)
    X, Y = torch.meshgrid(x, y, indexing='ij')
    
    # HDMA-style scanline shift
    # Each row is shifted based on its vertical position and time
    shift_amount = torch.sin(Y * 2 + t/5) * 20  # Shift up to 20 pixels
    
    # Apply shift using roll
    shifted = torch.roll(X, shift_amount.int(), dims=1)
    
    # Color with pleasing Earthbound-style palette
    r = torch.sin(shifted + t/10)
    g = torch.sin(shifted + t/10 + 2*torch.pi/3)
    b = torch.sin(shifted + t/10 + 4*torch.pi/3)
    
    rgb = torch.stack([r, g, b], dim=-1)
    rgb = ((rgb + 1) / 2).clamp(0, 1)
    frames.append(rgb)

output_image = torch.stack(frames, dim=0)  # Shape: [30, H, W, 3]

Line-by-Line Explanation

x = torch.linspace(0, 2*torch.pi, width)
y = torch.linspace(0, 2*torch.pi, height)

Creates coordinate arrays ranging from 0 to 2π across the image dimensions.

X, Y = torch.meshgrid(x, y, indexing='ij')

Creates 2D coordinate grids. indexing='ij' uses matrix indexing (row, column).

shift_amount = torch.sin(Y * 2 + t/5) * 20

Calculates the horizontal shift for each row:

  • Y * 2: Spatial frequency - how quickly the shift changes vertically
  • t/5: Temporal frequency - how fast the animation plays
  • * 20: Maximum shift in pixels
shifted = torch.roll(X, shift_amount.int(), dims=1)

Applies the shift by rolling the X coordinate array horizontally. .int() converts to integer pixels.

r = torch.sin(shifted + t/10)
g = torch.sin(shifted + t/10 + 2*torch.pi/3)
b = torch.sin(shifted + t/10 + 4*torch.pi/3)

Color cycling with 120° phase shifts between channels for smooth color transitions.

Customization

Change Shift Amount

shift_amount = torch.sin(Y * 2 + t/5) * 40  # Larger shift

Change Animation Speed

shift_amount = torch.sin(Y * 2 + t/10) * 20  # Slower

Change Spatial Frequency

shift_amount = torch.sin(Y * 4 + t/5) * 20  # More frequent vertical variation

Change Color Palette

# Warm sunset
r = shifted * 0.8 + 0.2
g = shifted * 0.4 + 0.3
b = shifted * 0.2 + 0.6

Performance Notes

  • torch.roll() is efficient for circular shifts
  • Precompute coordinate grids if generating many frames
  • Use integer shifts for pixel-perfect results

References