-
-
Notifications
You must be signed in to change notification settings - Fork 92
Graphic Visual Effects
📁 Source example:
snes-examples/graphics/Effects/GradientColors
Gradient effect is visual tricks: a smooth transition of colors across the screen from top to bottom. You can see it everywhere — from the sky in Donkey Kong Country to the menus of Chrono Trigger.
On the SNES, the screen is drawn scanline by scanline, top to bottom. The key insight is: if you change a palette color between two scanlines, every horizontal band can display a different shade — giving a seamless gradient with zero tile cost.
The SNES stores all active colors in CGRAM (Color Generator RAM). It holds 256 entries (0–255), each encoded as a 15-bit BGR value — 5 bits per channel (Red, Green, Blue), giving values from 0 to 31.
In PVSnesLib, the RGB15(r, g, b) macro builds a BGR15 color, and setPaletteColor(index, color) writes it to CGRAM by addressing REG_CGADD ($2121) and CGRAM_PALETTE ($2122).
Palette entry 0 is special: it is the backdrop color — the color shown on any pixel not covered by a background layer or sprite. Changing it mid-frame is the foundation of the gradient trick.
The effect works by updating palette entry 0 at each scanline during the active display period. All background layers are disabled, so the entire screen shows only the backdrop color. The HDMA (Horizontal DMA) controller feeds one color from the gradient table directly into CGRAM at every scanline — the PPU picks up the new color for the very next line it draws, producing a smooth color ramp from top to bottom with zero CPU cost.
The gradient effect needs no tile graphics — just a color table. This is where gfx4snes and its -n option come in.
The input bitmap must be exactly 256×224 pixels — the full NTSC active display resolution of the SNES. Each row of pixels represents one scanline, so the vertical color progression in your image maps directly to the on-screen gradient. Use an indexed-color mode image with the colors arranged top-to-bottom in the order you want them displayed. Recommended tools: Aseprite, GraphicsGale, or GIMP (avoid tools that mangle palette ordering).
Passing -n to gfx4snes instructs it to skip tile output entirely and instead generate an assembly data file named <filename>_grad_data.asm. This file contains the gradient color table as SNES assembly data, ready to be included directly into your project. The Makefile rule looks like:
$(GFXCONV) -n -i gradient.pngThe generated gradient_grad_data.asm must then be included inside your project's data.asm file with a standard .include directive:
.include "gradient_grad_data.asm"This makes the gradient color table available as a labeled symbol that your C code can reference via an extern declaration to iterate over the colors at runtime.
PVSnesLib exposes HDMA gradient setup through setModeHdmaColor(), which takes a pointer to the gradient color table generated by gfx4snes -n and configures the HDMA channel to feed one color per scanline directly into CGRAM:
// Set up HDMA to feed the gradient palette into CGRAM automatically,
// one color entry per scanline, for the full 224-line frame.
setModeHdmaColor((u8 *)&hdmaGradgradientList);Called once during initialization (or whenever you want to activate the gradient), the HDMA engine handles all per-scanline color writes in hardware, leaving the CPU entirely free for game logic.
📁 Source example:
snes-examples/graphics/Effects/Fading
Screen fading is often used for transitions in SNES games. The display smoothly dims to black before a new scene loads, then gently brightens to reveal the next screen. You can find this technique in virtually every classic SNES title.
On the SNES, this effect is performed through a single master brightness register (REG_INIDISP) that controls the luminance of the entire display at once. There is no need to touch individual pixels or palette entries — the hardware does all the work.
PVSnesLib exposes two functions for this: setFadeEffect() and setFadeEffectEx(), both declared in include/snes/video.h.
// Perform a fade with a default speed (1 frame per brightness step)
void setFadeEffect(u8 mode);
// Perform a fade with a custom number of frames per brightness step
void setFadeEffectEx(u8 mode, u8 framesNumber);Both functions are blocking: they wait until the full fade (all 16 brightness steps) has completed before returning to your code.
setFadeEffect(mode) performs a fade using a default speed of 1 VBlank per brightness step.
void setFadeEffect(u8 mode);Parameters:
-
mode—FADE_IN(2) orFADE_OUT(1)
Duration: 16 steps × 1 frame = 16 frames (~0.27 seconds at 60 Hz NTSC).
// Fade out: screen goes from full brightness to black
setFadeEffect(FADE_OUT);
// ... swap graphics, load a new level, etc. ...
// Fade in: screen returns from black to full brightness
setFadeEffect(FADE_IN);This is the simplest way to add a transition to your game. Use it when you want a quick, snappy cut between scenes.
setFadeEffectEx(mode, framesNumber) works exactly like setFadeEffect() but lets you control how many VBlanks to wait between each brightness step.
void setFadeEffectEx(u8 mode, u8 framesNumber);Parameters:
-
mode—FADE_IN(2) orFADE_OUT(1) -
framesNumber— number of VBlanks to wait between each of the 16 brightness steps
Total fade duration: framesNumber × 16 frames.
// Slow, dramatic fade out over ~1 second
setFadeEffectEx(FADE_OUT, 4);
// ... load new scene assets ...
// Slow fade in over ~1 second
setFadeEffectEx(FADE_IN, 4);Use setFadeEffectEx() when you want a cinematic, gradual transition — for example at the start of the game, between story scenes, or after a boss is defeated.
📁 Source example:
snes-examples/graphics/Effects/WindowCircular
The mosaic effect is a hardware-accelerated screen pixelation filter built directly into the SNES Picture Processing Unit (PPU). When enabled on a background layer, the PPU samples the colour of the upper-left pixel of every N×N block and tiles that colour across the entire block, making the image look like it is made of large square pixels. The block size ranges from 1×1 (no visible effect, single pixel) to 16×16 (maximum pixelation). You simply write a value to one register and the hardware does everything else.
On the SNES, this effect is performed through a single mosaic size and background register (REG_MOSAIC) that controls the size and background used for the effect.
setMosaicEffect(mode, bgNumbers) performs a mosaic effect using a default speed of 1 VBlank per mosaic step.
void setMosaicEffect(u8 mode, u8 bgNumbers);Parameters:
-
mode—MOSAIC_INfor normal to mosaic,MOSAIC_OUTfor mosaic to normal -
bgNumbers—MOSAIC_BG1toMOSAIC_BG4depending of which background to use for effect
Use setMosaicEffect() to gradually increasing the mosaic size during a scene change pixelates the image into a blur, then a new scene fades in while the size decreases back to 1×1.
📁 Source example:
snes-examples/graphics/Effects/WindowCircular
The animated circular wipe (also called an iris in / iris out) is one of the most iconic screen-transition effects on the Super Nintendo. You can see it in Super Mario World every time a level ends or the player gets a game-over: a circular mask shrinks toward the player's position until the screen goes fully black, or grows from a point to reveal a new level.
Despite looking complex, the effect is entirely achieved with the SNES Window hardware and HDMA (Horizontal-blank DMA).
Frame 0 Frame 8 Frame 16 Frame 24
+-----------+ +-----------+ +-----------+ +-----------+
|###########| |###/---\###| |#/-------\#| |/----------\|
|###########| |###| |###| || || || ||
|###########| |###\---/###| |#\-------/#| |\----------/|
|###########| |###########| |###########| |###########|
+-----------+ +-----------+ +-----------+ +-----------+
Closed r=0 Small circle Medium circle Full screen
The SNES PPU provides two hardware windows (Window 1 and Window 2). Each window is defined by two horizontal coordinates written to registers (REG_WH0 and REG_WH1 that control the Window 1 left position (X1) and right position (X2), REG_WH2 and REG_WH3 for the Window 2 left (X1) and right (X2) positions).
The inside-window region spans pixels from X1 to X2, inclusive. Setting X2 < X1 produces an empty (zero-width) window. The window can be independently applied per background layer and per OBJ, and can be configured to show or hide the content inside or outside the window.
By default these registers apply the same window shape to every scanline. By using HDMA, we can load different X1/X2 values for every horizontal line — which lets us draw any shape we want, line by line.
Declared in <snes/dma.h>:
void setModeHdmaWindow(u8 bgrnd, u8 bgrndmask, u8 *hdmatableL, u8 *hdmatableR);Parameters:
-
bgrnd- Which background layers get the window mask. CombineMSWIN_BG1..MSWIN_BG4with\|. | -
bgrndmask- Per-layer inside/outside logic. UseMSWIN1_BGxMSKENABLE(show inside) orMSWIN1_BGxMSKOUT(show outside). | -
hdmatableL- HDMA table for Window 1 left edge (REG_WH0) — one byte per scanline | -
hdmatableR- HDMA table for Window 1 right edge (REG_WH1) — one byte per scanline |
This function configures HDMA channels 4 and 5 to feed WH0 and WH1 every scanline, and writes REG_W12SEL, REG_W34SEL, and REG_TMW according to the mask arguments.
setModeHdmaWindow() expects two separate, single-byte-per-scanline tables in direct repeat-mode format:
Byte 0: (line_count | 0x80) -- bit7=1 means repeat; value=number of scanlines
Byte 1: X edge for scanline 0
Byte 2: X edge for scanline 1
...
Byte 224: X edge for scanline 223
Byte 225: 0x00 -- table terminator
With 224 scanlines handled in one block, the header byte is 0x80 | 224 = 0xE0.
void setModeHdmaWindowEx(u8 bgrnd, u8 bgrndmask, u8 *hdmatableLR);Parameters:
-
bgrnd- Which background layers get the window mask. CombineMSWIN_BG1..MSWIN_BG4with\| -
bgrndmask- Per-layer inside/outside logic. UseMSWIN1_BGxMSKENABLE(show inside) orMSWIN1_BGxMSKOUT(show outside) -
hdmatableLR- HDMA table for Window 1 left and right edge (REG_WH0andREG_WH1) — two bytes per scanline
To stop the effect:
void setModeHdmaWindowReset(u8 channels);
// Stop the window HDMA and clear window registers:
setModeHdmaWindowReset(HDMA_CHANNEL4 | HDMA_CHANNEL5);It is important to have two version of the tables, the one used for calculation and another for the hmda. Without that, you will have flickeing on screen. This is the reason why you have this trick in the example.
...
extern u8 hdma_table_LR[224 * 3 + 1];
u8 hdma_table_LRB[224 * 3 + 1];
...
... // calculate new value in hdma_table_LR
// to avoid glitch on screen after calculating new values
memcpy(hdma_table_LRB,hdma_table_LR,224*3+1);
...Use setModeHdmaWindowEx() when you want a nice gradual opening screen — for example at the start of the level in a game, as we saw it on ancient computer games like Lode Runner on Apple II ;-) .