---
title: "How to add an underwater with Claude (tested prompt and script)"
description: "At one second the shot sinks underwater: the bottle wobbles in slow ripples, everything turns sea green and darker toward the bottom, and slanting shafts of light flicker down from the top. Install one skill and ask Claude Code, or run the Python script we tested yourself."
url: https://www.recharm.com/effects/underwater
---

# How to add an underwater with Claude

At one second the shot sinks underwater: the bottle wobbles in slow ripples, everything turns sea green and darker toward the bottom, and slanting shafts of light flicker down from the top.

Made by the script on this page, from our own test clip.

## Get this effect

1. ### Switch to Code in your [Claude Desktop App](https://claude.com/download)

2. ### Install the skill (once)

   ```
   Install the Recharm video effects skill: download https://recharm-growth-staging.s3.us-east-2.amazonaws.com/website/downloads/recharm-video-effects.zip and unzip it into ~/.claude/skills
   ```

3. ### Ask for the effect

   ```
   Add an underwater to input.mp4
   Second the shot dips underwater: 1.0
   Seconds the dip takes to reach full strength: 0.4
   ```

## Several videos, several effects, one ask

Put your clips in the folder, mention them with @, and ask for everything at once. Claude runs one tested script after another on each clip, then joins them.

Add an underwater to **@clip1.mp4**, put a sale badge on **@clip2.mp4**, give **@clip3.mp4** a VHS look, then join all three with crossfades.

## Settings you can change

Ask Claude to change any of these, or edit them at the top of the script.

| Setting      | Default          | What it does                                            |
| ------------ | ---------------- | ------------------------------------------------------- |
| `START`      | `1.0`            | Second the shot dips underwater.                        |
| `DIP`        | `0.4`            | Seconds the dip takes to reach full strength.           |
| `WAVE`       | `9`              | How far the ripples push the picture, in pixels.        |
| `WAVELENGTH` | `0.22`           | Length of one ripple, as a share of the frame height.   |
| `SPEED`      | `0.8`            | Ripples per second passing each point.                  |
| `WATER`      | `(10, 120, 135)` | Sea green the picture is washed toward.                 |
| `TINT`       | `0.55`           | How much of the water color goes in, 0 to 1.            |
| `HAZE`       | `1.5`            | Blur of the murky water, in pixels.                     |
| `RAYS`       | `0.22`           | Brightness of the shafts of light from the top, 0 to 1. |
| `FLICKER`    | `0.14`           | How much the light flickers up and down, 0 to 1.        |

## Without the skill

Don’t want to install the skill? Paste this prompt into Claude Code and Claude writes the script itself.

```
Make input.mp4 look like it was filmed underwater from 1.0 seconds on. Leave the first second untouched, then ease the effect in over 0.4 seconds (smoothstep) and keep it to the end. For each frame, with Python, numpy and OpenCV: 1) Ripple the picture with cv2.remap: move each pixel up to 9 pixels across and down using sine waves about 0.22 of the frame height long (a sine of y for the sideways push, a sine of x for the up-and-down push, plus a half-strength shorter wave on each), drifting at 0.8 waves per second; reflect at the borders so no black edges show. 2) Blur by a 1.5 pixel Gaussian for murky water. 3) Tint it sea green: take each pixel's gray level, multiply sea green (10, 120, 135) by gray/128, and mix that in at 55%. 4) Darken toward the bottom, from 0% at the top to 35% at the bottom edge. 5) Add soft diagonal shafts of pale aqua light (150, 255, 240) slanting down from the top at about 0.22 strength, swaying slowly and fading out by 85% of the way down. 6) Flicker the whole frame's brightness up and down by about 14% with two sine waves at 2.7 and 6.1 per second. This effect has no text; any text would be drawn with Pillow, not ffmpeg's drawtext filter. Keep the size, frame rate (30fps) and audio, encode as H.264 yuv420p and save it as output.mp4.
```

## Run it without Claude

This is the exact script we tested. Save it as `underwater.py` and run:

```
python3 underwater.py input.mp4 output.mp4
```

```
#!/usr/bin/env python3
"""Underwater: from one second in, the picture ripples in slow waves, turns sea green and flickers under shafts of light.

Usage: python3 underwater.py input.mp4 output.mp4
Needs ffmpeg, numpy and OpenCV to bend each frame (pip install numpy opencv-python-headless).
"""
import json
import subprocess
import sys

import cv2
import numpy as np

START = 1.0                 # second the shot dips underwater
DIP = 0.4                   # seconds the dip takes to reach full strength
WAVE = 9                    # how far the ripples push the picture, in pixels
WAVELENGTH = 0.22           # length of one ripple, as a share of the frame height
SPEED = 0.8                 # ripples per second passing each point
WATER = (10, 120, 135)      # sea green the picture is washed toward
TINT = 0.55                 # how much of the water color goes in, 0-1
HAZE = 1.5                  # blur of the murky water, in pixels
RAYS = 0.22                 # brightness of the shafts of light from the top, 0-1
FLICKER = 0.14              # how much the light flickers up and down, 0-1

def strength(t):
    """0 before START, easing up to 1 over DIP seconds."""
    u = min(max((t - START) / DIP, 0), 1)
    return u * u * (3 - 2 * u)

def ripple_maps(t, k):
    """Where each output pixel reads from: two sets of sine waves, across and down, drifting over time."""
    phase = 2 * np.pi * SPEED * t
    dx = np.sin(2 * np.pi * ys / (WAVELENGTH * h) + phase) + 0.5 * np.sin(2 * np.pi * xs / (0.6 * WAVELENGTH * h) - 1.3 * phase)
    dy = np.sin(2 * np.pi * xs / (WAVELENGTH * h) + 0.8 * phase) + 0.5 * np.sin(2 * np.pi * ys / (0.7 * WAVELENGTH * h) - phase)
    return (xs + k * WAVE * dx / 1.5).astype(np.float32), (ys + k * WAVE * dy / 1.5).astype(np.float32)

def light(t):
    """Soft shafts of light slanting down from the top, swaying, fading with depth; plus an overall flicker."""
    sway = 0.04 * np.sin(2 * np.pi * 0.3 * t)
    u = (xs + 0.35 * ys) / w + sway
    shafts = np.clip(np.sin(2 * np.pi * 2.3 * u) * 0.6 + np.sin(2 * np.pi * 5.1 * u + 1.7 * t) * 0.4, 0, 1) ** 2
    shafts *= np.clip(1 - ys / (0.85 * h), 0, 1)
    flicker = 1 + FLICKER * (0.6 * np.sin(2 * np.pi * 2.7 * t) + 0.4 * np.sin(2 * np.pi * 6.1 * t + 1.0))
    return shafts, flicker

def underwater(rgb, t):
    k = strength(t)
    mx, my = ripple_maps(t, k)
    img = cv2.remap(rgb, mx, my, cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT).astype(np.float32)
    img = cv2.GaussianBlur(img, (0, 0), max(k * HAZE, 0.01))
    gray = img.mean(axis=2, keepdims=True)
    water = gray / 128 * np.array(WATER, np.float32)                  # keep the shading, swap in the sea color
    img = img * (1 - k * TINT) + water * k * TINT
    depth = 1 - 0.35 * k * (ys / h)[..., None]                          # darker toward the bottom
    shafts, flicker = light(t)
    rays = k * RAYS * shafts[..., None] * np.array([150, 255, 240], np.float32)
    img = (img * depth + rays) * (1 + k * (flicker - 1))
    return np.clip(img, 0, 255).astype(np.uint8)

def probe(path):
    info = json.loads(subprocess.check_output(["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
                                               "stream=width,height,r_frame_rate", "-of", "json", path]))
    s = info["streams"][0]
    num, den = s["r_frame_rate"].split("/")
    return s["width"], s["height"], s["r_frame_rate"], float(num) / float(den)

src, out = sys.argv[1:3]
w, h, rate, fps = probe(src)
ys, xs = np.mgrid[0:h, 0:w].astype(np.float32)
reader = subprocess.Popen(["ffmpeg", "-v", "error", "-i", src, "-f", "rawvideo", "-pix_fmt", "rgb24", "-"],
                          stdout=subprocess.PIPE)
writer = subprocess.Popen(["ffmpeg", "-y", "-v", "error", "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{w}x{h}",
                           "-r", rate, "-i", "-", "-i", src, "-map", "0:v", "-map", "1:a?", "-c:v", "libx264",
                           "-crf", "18", "-pix_fmt", "yuv420p", "-c:a", "copy", "-shortest", out],
                          stdin=subprocess.PIPE)
i = 0
while raw := reader.stdout.read(w * h * 3):
    t = i / fps
    if t >= START:
        raw = underwater(np.frombuffer(raw, np.uint8).reshape(h, w, 3), t).tobytes()
    writer.stdin.write(raw)
    i += 1
writer.stdin.close()
if reader.wait() or writer.wait():
    sys.exit("ffmpeg failed")
```

## More distortion and glitch

- [Anaglyph 3D **Anaglyph 3D**](https://www.recharm.com/effects/anaglyph-3d)
- [Black hole **Black hole**](https://www.recharm.com/effects/black-hole)
- [Bulge and pinch **Bulge and pinch**](https://www.recharm.com/effects/bulge-pinch)
- [Datamosh **Datamosh**](https://www.recharm.com/effects/datamosh)
- [Disintegration **Disintegration**](https://www.recharm.com/effects/disintegration)

## Find 3x _winning ads_

Recharm cuts your ad footage into tagged, searchable clips: hooks, testimonials, product shots. Find the shots that win, add the effect with Claude, and ship 3x more ad edits.

[Start 14-day free trial](https://www.recharm.com/get-started)
