```python
import bpy, bmesh, math, random, os
from mathutils import Vector, Matrix, noise

random.seed(11)

# ---------------------------------------------------------------- clean scene
for ob in list(bpy.data.objects):
    bpy.data.objects.remove(ob, do_unlink=True)
for blk in (bpy.data.meshes, bpy.data.materials, bpy.data.lights, bpy.data.cameras):
    for it in list(blk):
        blk.remove(it)

scene = bpy.context.scene
scene.render.engine = 'CYCLES'
try:
    scene.cycles.device = 'GPU'
    prefs = bpy.context.preferences.addons.get('cycles')
    if prefs:
        for t in ('OPTIX', 'CUDA', 'HIP', 'METAL', 'ONEAPI'):
            try:
                prefs.preferences.compute_device_type = t
                break
            except Exception:
                pass
except Exception:
    pass
scene.cycles.samples = 64
scene.cycles.use_denoising = True
scene.cycles.transparent_max_bounces = 24
scene.cycles.max_bounces = 12
scene.render.resolution_x = 1536
scene.render.resolution_y = 1536
scene.render.film_transparent = False
try:
    scene.view_settings.view_transform = 'AgX'
except Exception:
    pass
for lk in ('AgX - Punchy', 'Punchy', 'AgX - Medium High Contrast', 'None'):
    try:
        scene.view_settings.look = lk
        break
    except Exception:
        continue

C = bpy.context.collection

# ---------------------------------------------------------------- helpers
def out_type(node, t, idx=0):
    return [s for s in node.outputs if s.type == t][idx]

def in_type(node, t, idx=0):
    return [s for s in node.inputs if s.type == t][idx]

def sset(node, name, val):
    if name in node.inputs:
        try:
            node.inputs[name].default_value = val
        except Exception:
            pass

def new_mat(name):
    m = bpy.data.materials.new(name)
    m.use_nodes = True
    nt = m.node_tree
    for n in list(nt.nodes):
        nt.nodes.remove(n)
    out = nt.nodes.new('ShaderNodeOutputMaterial'); out.location = (700, 0)
    bsdf = nt.nodes.new('ShaderNodeBsdfPrincipled'); bsdf.location = (380, 0)
    nt.links.new(bsdf.outputs[0], out.inputs[0])
    return m, nt, bsdf

def set_blend(mat):
    for attr, val in (('blend_method', 'BLEND'), ('surface_render_method', 'BLENDED'),
                      ('shadow_method', 'HASHED')):
        try:
            setattr(mat, attr, val)
        except Exception:
            pass
    try:
        mat.use_backface_culling = False
    except Exception:
        pass

def make_mesh_obj(name, verts, faces, mat, uvs=None, smooth=True):
    me = bpy.data.meshes.new(name)
    me.from_pydata([tuple(v) for v in verts], [], faces)
    me.validate(verbose=False)
    if uvs is not None:
        uvl = me.uv_layers.new(name="UVMap")
        for poly in me.polygons:
            for li in poly.loop_indices:
                vi = me.loops[li].vertex_index
                uvl.data[li].uv = uvs[vi]
    if smooth:
        for p in me.polygons:
            p.use_smooth = True
    ob = bpy.data.objects.new(name, me)
    C.objects.link(ob)
    if mat:
        ob.data.materials.append(mat)
    return ob

def look_at(obj, target):
    d = Vector(target) - obj.location
    obj.rotation_euler = d.to_track_quat('-Z', 'Y').to_euler()

def add_area(name, loc, target, energy, size, color):
    ld = bpy.data.lights.new(name, type='AREA')
    ld.size = size
    ld.energy = energy
    ld.color = color
    ob = bpy.data.objects.new(name, ld)
    ob.location = loc
    C.objects.link(ob)
    look_at(ob, target)
    return ob

# ---------------------------------------------------------------- fish spine
L0, L1 = -1.45, 1.65

def prof(t):
    t = min(max(t, 0.0), 1.0)
    e1, e2 = 0.38, 0.85
    tm = e1 / (e1 + e2)
    fm = (tm ** e1) * ((1 - tm) ** e2)
    return ((t ** e1) * ((1 - t) ** e2)) / fm

def spine(t):
    x = L0 + (L1 - L0) * t
    amp = 0.10 + 0.30 * (t ** 1.6)
    y = amp * math.sin(5.5 * t - 1.2)
    z = 0.07 * math.sin(3.0 * t)
    return Vector((x, y, z))

def radii(t):
    f = prof(t)
    return max(0.52 * f + 0.02, 0.075), max(0.82 * f + 0.03, 0.115)

def frame(t):
    dt = 1e-3
    t0 = max(0.0, t - dt); t1 = min(1.0, t + dt)
    T = (spine(t1) - spine(t0))
    if T.length < 1e-9:
        T = Vector((1, 0, 0))
    T.normalize()
    S = T.cross(Vector((0, 0, 1)))
    if S.length < 1e-6:
        S = Vector((0, -1, 0))
    S.normalize()
    U = S.cross(T).normalized()
    return T, S, U

# ---------------------------------------------------------------- materials
ORANGE = (1.0, 0.30, 0.025, 1.0)

# body -------------------------------------------------------------
mat_body, nt, bs = new_mat("GoldfishBody")
tc = nt.nodes.new('ShaderNodeTexCoord'); tc.location = (-1200, 0)
mp = nt.nodes.new('ShaderNodeMapping'); mp.location = (-1000, 200)
mp.inputs['Scale'].default_value = (0.62, 1.0, 1.0)
nt.links.new(tc.outputs['Object'], mp.inputs['Vector'])
vor = nt.nodes.new('ShaderNodeTexVoronoi'); vor.location = (-800, 200)
vor.voronoi_dimensions = '3D'
try:
    vor.feature = 'DISTANCE_TO_EDGE'
except Exception:
    pass
sset(vor, 'Scale', 14.0)
sset(vor, 'Randomness', 0.85)
nt.links.new(mp.outputs['Vector'], vor.inputs['Vector'])
edge = nt.nodes.new('ShaderNodeValToRGB'); edge.location = (-600, 220)
edge.color_ramp.elements[0].position = 0.0
edge.color_ramp.elements[0].color = (0, 0, 0, 1)
edge.color_ramp.elements[1].position = 0.06
edge.color_ramp.elements[1].color = (1, 1, 1, 1)
nt.links.new(vor.outputs['Distance'], edge.inputs['Fac'])

sepz = nt.nodes.new('ShaderNodeSeparateXYZ'); sepz.location = (-1000, -220)
nt.links.new(tc.outputs['Object'], sepz.inputs['Vector'])
mr = nt.nodes.new('ShaderNodeMapRange'); mr.location = (-820, -240)
mr.inputs['From Min'].default_value = -0.85
mr.inputs['From Max'].default_value = 0.85
nt.links.new(sepz.outputs['Z'], mr.inputs['Value'])
grad = nt.nodes.new('ShaderNodeValToRGB'); grad.location = (-620, -240)
cr = grad.color_ramp
cr.elements[0].position = 0.10
cr.elements[0].color = (1.0, 0.80, 0.42, 1)
cr.elements[1].position = 0.92
cr.elements[1].color = (0.72, 0.135, 0.012, 1)
e_mid = cr.elements.new(0.48); e_mid.color = ORANGE
e_mid2 = cr.elements.new(0.30); e_mid2.color = (1.0, 0.50, 0.09, 1)
nt.links.new(mr.outputs['Result'], grad.inputs['Fac'])

inv = nt.nodes.new('ShaderNodeMath'); inv.location = (-420, 240)
inv.operation = 'SUBTRACT'
inv.inputs[0].default_value = 1.0
nt.links.new(edge.outputs['Color'], inv.inputs[1])
mul = nt.nodes.new('ShaderNodeMath'); mul.location = (-260, 240)
mul.operation = 'MULTIPLY'; mul.inputs[1].default_value = 0.65
nt.links.new(inv.outputs[0], mul.inputs[0])

mixc = nt.nodes.new('ShaderNodeMix'); mixc.location = (-60, 0)
mixc.data_type = 'RGBA'; mixc.blend_type = 'MIX'
nt.links.new(mul.outputs[0], in_type(mixc, 'VALUE'))
ca, cb = [s for s in mixc.inputs if s.type == 'RGBA'][:2]
nt.links.new(grad.outputs['Color'], ca)
cb.default_value = (0.42, 0.085, 0.01, 1)
nt.links.new(out_type(mixc, 'RGBA'), bs.inputs['Base Color'])

bump = nt.nodes.new('ShaderNodeBump'); bump.location = (100, -320)
bump.inputs['Strength'].default_value = 0.32
bump.inputs['Distance'].default_value = 0.03
nt.links.new(edge.outputs['Color'], bump.inputs['Height'])
nt.links.new(bump.outputs['Normal'], bs.inputs['Normal'])
sset(bs, 'Roughness', 0.26)
sset(bs, 'Coat Weight', 0.45)
sset(bs, 'Coat Roughness', 0.08)
sset(bs, 'Subsurface Weight', 0.15)
sset(bs, 'Subsurface Radius', (0.6, 0.18, 0.06))
sset(bs, 'IOR', 1.4)

# fins -------------------------------------------------------------
def make_fin_mat(name, edge_alpha=0.07):
    m, nt, bs = new_mat(name)
    tc = nt.nodes.new('ShaderNodeTexCoord'); tc.location = (-1100, 0)
    sep = nt.nodes.new('ShaderNodeSeparateXYZ'); sep.location = (-900, 0)
    nt.links.new(tc.outputs['UV'], sep.inputs['Vector'])
    # alpha : 1 at base -> low at outer edge
    ar = nt.nodes.new('ShaderNodeValToRGB'); ar.location = (-700, 200)
    ar.color_ramp.elements[0].position = 0.02
    ar.color_ramp.elements[0].color = (1, 1, 1, 1)
    ar.color_ramp.elements[1].position = 1.0
    ar.color_ramp.elements[1].color = (edge_alpha,) * 3 + (1,)
    em = ar.color_ramp.elements.new(0.45); em.color = (0.72, 0.72, 0.72, 1)
    nt.links.new(sep.outputs['X'], ar.inputs['Fac'])
    nt.links.new(ar.outputs['Color'], bs.inputs['Alpha'])
    # colour : deep orange base -> pale translucent edge
    cg = nt.nodes.new('ShaderNodeValToRGB'); cg.location = (-700, -160)
    cg.color_ramp.elements[0].position = 0.0
    cg.color_ramp.elements[0].color = (1.0, 0.30, 0.03, 1)
    cg.color_ramp.elements[1].position = 1.0
    cg.color_ramp.elements[1].color = (1.0, 0.72, 0.42, 1)
    cm = cg.color_ramp.elements.new(0.5); cm.color = (1.0, 0.46, 0.12, 1)
    nt.links.new(sep.outputs['X'], cg.inputs['Fac'])
    # fin rays
    wv = nt.nodes.new('ShaderNodeTexWave'); wv.location = (-900, -450)
    wv.wave_type = 'BANDS'
    try:
        wv.bands_direction = 'Y'
    except Exception:
        pass
    sset(wv, 'Scale', 7.0)
    sset(wv, 'Distortion', 0.6)
    sset(wv, 'Detail', 1.0)
    nt.links.new(tc.outputs['UV'], wv.inputs['Vector'])
    wr = nt.nodes.new('ShaderNodeValToRGB'); wr.location = (-700, -450)
    wr.color_ramp.elements[0].position = 0.35
    wr.color_ramp.elements[1].position = 0.62
    nt.links.new(wv.outputs['Color'], wr.inputs['Fac'])
    mx = nt.nodes.new('ShaderNodeMix'); mx.location = (-420, -200)
    mx.data_type = 'RGBA'; mx.blend_type = 'MIX'
    in_type(mx, 'VALUE').default_value = 0.30
    sa, sb = [s for s in mx.inputs if s.type == 'RGBA'][:2]
    nt.links.new(cg.outputs['Color'], sa)
    nt.links.new(wr.outputs['Color'], sb)
    mul2 = nt.nodes.new('ShaderNodeMix'); mul2.location = (-200, -200)
    mul2.data_type = 'RGBA'; mul2.blend_type = 'MULTIPLY'
    in_type(mul2, 'VALUE').default_value = 1.0
    ma, mb = [s for s in mul2.inputs if s.type == 'RGBA'][:2]
    nt.links.new(cg.outputs['Color'], ma)
    nt.links.new(out_type(mx, 'RGBA'), mb)
    nt.links.new(out_type(mul2, 'RGBA'), bs.inputs['Base Color'])
    bmp = nt.nodes.new('ShaderNodeBump'); bmp.location = (100, -450)
    bmp.inputs['Strength'].default_value = 0.18
    bmp.inputs['Distance'].default_value = 0.02
    nt.links.new(wr.outputs['Color'], bmp.inputs['Height'])
    nt.links.new(bmp.outputs['Normal'], bs.inputs['Normal'])
    sset(bs, 'Roughness', 0.22)
    sset(bs, 'Subsurface Weight', 0.25)
    sset(bs, 'Subsurface Radius', (0.5, 0.2, 0.08))
    sset(bs, 'Coat Weight', 0.2)
    set_blend(m)
    return m

mat_tail = make_fin_mat("TailFin", 0.05)
mat_fin = make_fin_mat("SideFin", 0.10)

# eyes / mouth ------------------------------------------------------
mat_eye, nte, bse = new_mat("EyeBall")
sset(bse, 'Base Color', (0.12, 0.06, 0.02, 1))
sset(bse, 'Roughness', 0.14); sset(bse, 'Coat Weight', 1.0); sset(bse, 'Coat Roughness', 0.03)

mat_iris, nti, bsi = new_mat("EyeIris")
sset(bsi, 'Base Color', (0.55, 0.20, 0.02, 1))
sset(bsi, 'Roughness', 0.2); sset(bsi, 'Coat Weight', 1.0)

mat_pup, ntp, bsp = new_mat("EyePupil")
sset(bsp, 'Base Color', (0.01, 0.005, 0.005, 1))
sset(bsp, 'Roughness', 0.05); sset(bsp, 'Coat Weight', 1.0)

mat_glint, ntg, bsg = new_mat("EyeGlint")
sset(bsg, 'Base Color', (1, 1, 1, 1))
sset(bsg, 'Emission Color', (1, 1, 1, 1))
sset(bsg, 'Emission Strength', 6.0)

mat_mouth, ntm, bsm = new_mat("Mouth")
sset(bsm, 'Base Color', (0.42, 0.08, 0.03, 1))
sset(bsm, 'Roughness', 0.35)

# environment -------------------------------------------------------
mat_sand, nts, bss = new_mat("Sand")
tcs = nts.nodes.new('ShaderNodeTexCoord'); tcs.location = (-800, 0)
nz = nts.nodes.new('ShaderNodeTexNoise'); nz.location = (-600, 0)
sset(nz, 'Scale', 26.0); sset(nz, 'Detail', 6.0)
nts.links.new(tcs.outputs['Object'], nz.inputs['Vector'])
sr = nts.nodes.new('ShaderNodeValToRGB'); sr.location = (-400, 0)
sr.color_ramp.elements[0].color = (0.20, 0.24, 0.24, 1)
sr.color_ramp.elements[1].color = (0.44, 0.44, 0.38, 1)
nts.links.new(nz.outputs['Fac'], sr.inputs['Fac'])
nts.links.new(sr.outputs['Color'], bss.inputs['Base Color'])
sb = nts.nodes.new('ShaderNodeBump'); sb.location = (100, -300)
sb.inputs['Strength'].default_value = 0.5
nts.links.new(nz.outputs['Fac'], sb.inputs['Height'])
nts.links.new(sb.outputs['Normal'], bss.inputs['Normal'])
sset(bss, 'Roughness', 0.75)

mat_rock, ntr, bsr = new_mat("Rock")
sset(bsr, 'Base Color', (0.16, 0.19, 0.20, 1))
sset(bsr, 'Roughness', 0.65)

mat_plant, ntpl, bspl = new_mat("Plant")
sset(bspl, 'Base Color', (0.06, 0.30, 0.16, 1))
sset(bspl, 'Roughness', 0.45)
sset(bspl, 'Subsurface Weight', 0.3)
sset(bspl, 'Subsurface Radius', (0.1, 0.4, 0.15))
sset(bspl, 'Alpha', 0.92)
set_blend(mat_plant)

mat_surf, ntsu, bssu = new_mat("WaterSurface")
sset(bssu, 'Base Color', (0.28, 0.62, 0.72, 1))
sset(bssu, 'Roughness', 0.10)
sset(bssu, 'Alpha', 0.28)
sset(bssu, 'Emission Color', (0.35, 0.70, 0.82, 1))
sset(bssu, 'Emission Strength', 0.55)
set_blend(mat_surf)

mat_bub, ntb, bsb = new_mat("Bubble")
sset(bsb, 'Base Color', (0.85, 0.95, 1.0, 1))
sset(bsb, 'Roughness', 0.05)
sset(bsb, 'Alpha', 0.32)
sset(bsb, 'Transmission Weight', 0.5)
sset(bsb, 'IOR', 1.33)
set_blend(mat_bub)

# ---------------------------------------------------------------- body mesh
NT, NC = 64, 32
bverts, buvs, bfaces = [], [], []
for i in range(NT + 1):
    t = i / NT
    P = spine(t); T, S, U = frame(t); ry, rz = radii(t)
    for k in range(NC):
        th = 2 * math.pi * k / NC
        st, ct = math.sin(th), math.cos(th)
        rzz = rz * (1.0 + 0.09 * max(0.0, -st))
        p = P + S * (ry * ct) + U * (rzz * st)
        bverts.append(p)
        buvs.append((k / NC, t))
for i in range(NT):
    for k in range(NC):
        a = i * NC + k
        b = i * NC + (k + 1) % NC
        c = (i + 1) * NC + (k + 1) % NC
        d = (i + 1) * NC + k
        bfaces.append((a, b, c, d))
T0, S0, U0 = frame(0.0)
T1, S1, U1 = frame(1.0)
nose_i = len(bverts)
bverts.append(spine(0.0) - T0 * 0.05); buvs.append((0.5, 0.0))
tail_i = len(bverts)
bverts.append(spine(1.0) + T1 * 0.03); buvs.append((0.5, 1.0))
for k in range(NC):
    bfaces.append((nose_i, (k + 1) % NC, k))
    base = NT * NC
    bfaces.append((tail_i, base + k, base + (k + 1) % NC))
body = make_mesh_obj("Goldfish_Body", bverts, bfaces, mat_body, uvs=buvs)
sub = body.modifiers.new('sub', 'SUBSURF'); sub.levels = 1; sub.render_levels = 1

# ---------------------------------------------------------------- membranes
def make_membrane(name, base_pts, tip_pts, ns, mat, curl=None, thick=0.017, ease=1.0, subs=2):
    na = len(base_pts)
    verts, uvs, faces = [], [], []
    for i in range(na):
        av = i / (na - 1)
        b = base_pts[i]; tp = tip_pts[i]
        for j in range(ns + 1):
            s = j / ns
            p = b + (tp - b) * (s ** ease)
            if curl is not None:
                p = p + curl(av, s)
            verts.append(p); uvs.append((s, av))
    for i in range(na - 1):
        for j in range(ns):
            a = i * (ns + 1) + j
            faces.append((a, a + 1, a + ns + 2, a + ns + 1))
    ob = make_mesh_obj(name, verts, faces, mat, uvs=uvs)
    sd = ob.modifiers.new('sld', 'SOLIDIFY'); sd.thickness = thick; sd.offset = 0.0
    sf = ob.modifiers.new('sub', 'SUBSURF'); sf.levels = 1; sf.render_levels = subs
    return ob

# --- tail (big flowing veiltail)
P1 = spine(1.0)
na = 46
tb, tt = [], []
for i in range(na):
    a = -1 + 2 * i / (na - 1)
    b = P1 + U1 * (0.18 * a) - T1 * 0.12
    phi = a * 1.30
    Lb = 1.62 * (0.60 + 0.40 * abs(a) ** 1.5) * (1 - 0.42 * abs(a) ** 14) * (1 + 0.055 * math.sin(a * 9.0))
    d = (T1 * math.cos(phi) + U1 * math.sin(phi)).normalized()
    tb.append(b); tt.append(b + d * Lb)

def tail_curl(av, s):
    a = av * 2 - 1
    return S1 * (0.36 * math.sin(2.6 * a + 0.8)) * (s ** 1.8) + U1 * (0.11 * math.sin(3.4 * a)) * (s * s)

tail = make_membrane("Fin_Tail", tb, tt, 18, mat_tail, curl=tail_curl, thick=0.022, subs=2)

# --- dorsal fin
na = 34
db, dt = [], []
for i in range(na):
    av = i / (na - 1)
    t = 0.28 + 0.42 * av
    P = spine(t); T, S, U = frame(t); ry, rz = radii(t)
    b = P + U * (rz * 0.90)
    h = 0.92 * (math.sin(math.pi * (av ** 0.8)) ** 0.55)
    db.append(b)
    dt.append(b + U * h + T * (0.22 * av + 0.05))

def dcurl(av, s):
    return Vector((0.0, 0.06 * math.sin(4.5 * av + 1.0) * s * s, 0.0))

dorsal = make_membrane("Fin_Dorsal", db, dt, 12, mat_fin, curl=dcurl, thick=0.02)

# --- anal fin
na = 24
ab, at = [], []
for i in range(na):
    av = i / (na - 1)
    t = 0.68 + 0.22 * av
    P = spine(t); T, S, U = frame(t); ry, rz = radii(t)
    b = P - U * (rz * 0.90)
    h = 0.50 * (math.sin(math.pi * (av ** 0.85)) ** 0.6)
    ab.append(b)
    at.append(b - U * h + T * (0.30 * av + 0.05))
anal = make_membrane("Fin_Anal", ab, at, 10, mat_fin, curl=dcurl, thick=0.018)

# --- pectoral + pelvic pairs
def side_fan(name, t0, sgn, out_mix, length, spread, mat, drop):
    P = spine(t0); T, S, U = frame(t0); ry, rz = radii(t0)
    A = P + S * sgn * (ry * out_mix[0]) + U * (rz * out_mix[1])
    dirv = (S * sgn * spread[0] + T * spread[1] + U * spread[2]).normalized()
    nn = 20
    bb, tp = [], []
    for i in range(nn):
        av = i / (nn - 1)
        b = A + T * (0.10 * (2 * av - 1)) + U * (0.03 * (2 * av - 1))
        ln = length * (0.55 + 0.45 * math.sin(math.pi * av) ** 0.8)
        bb.append(b)
        tp.append(b + dirv * ln)

    def curl(av, s):
        return (U * (drop * math.sin(math.pi * av)) + S * sgn * 0.07) * (s * s)

    return make_membrane(name, bb, tp, 10, mat, curl=curl, thick=0.014)

fins = []
for sgn, tag in ((1, "L"), (-1, "R")):
    fins.append(side_fan("Fin_Pect_" + tag, 0.26, sgn, (0.85, -0.15),
                         0.78, (0.55, 0.78, -0.28), mat_fin, 0.14))
    fins.append(side_fan("Fin_Pelvic_" + tag, 0.54, sgn, (0.48, -0.86),
                         0.44, (0.34, 0.55, -0.80), mat_fin, -0.05))

# ---------------------------------------------------------------- eyes
def ball(name, loc, r, mat, scale=(1, 1, 1)):
    bpy.ops.mesh.primitive_uv_sphere_add(segments=32, ring_count=16, radius=r, location=loc)
    ob = bpy.context.active_object
    ob.name = name
    ob.scale = scale
    ob.data.materials.clear()
    ob.data.materials.append(mat)
    for p in ob.data.polygons:
        p.use_smooth = True
    return ob

t_eye = 0.135
Pe = spine(t_eye); Te, Se, Ue = frame(t_eye); rye, rze = radii(t_eye)
eyes = []
for sgn in (1, -1):
    ctr = Pe + Se * sgn * (rye * 0.80) + Ue * (rze * 0.42)
    eb = ball("Eye_%s" % ("L" if sgn > 0 else "R"), ctr, 0.165, mat_iris, (1.0, 0.80, 1.0))
    eyes.append(eb)
    pc = ctr + Se * sgn * 0.085
    eyes.append(ball("Pupil_%s" % ("L" if sgn > 0 else "R"), pc, 0.105, mat_pup))
    gc = ctr + Se * sgn * 0.145 + Ue * 0.055 - Te * 0.045
    eyes.append(ball("Glint_%s" % ("L" if sgn > 0 else "R"), gc, 0.030, mat_glint))

# mouth
Pm = spine(0.015)
mouth = ball("Mouth", Pm - Vector((0.03, 0, 0.04)), 0.105, mat_mouth, (0.55, 1.0, 0.55))

# ---------------------------------------------------------------- sand floor
n, size = 40, 9.0
sv, su, sf = [], [], []
for i in range(n + 1):
    for j in range(n + 1):
        x = -size / 2 + size * i / n
        y = -size / 2 + size * j / n
        z = -2.55 + 0.26 * noise.noise(Vector((x * 0.30, y * 0.30, 0.0))) \
            + 0.07 * noise.noise(Vector((x * 1.3, y * 1.3, 3.0)))
        z -= 0.10 * math.exp(-((x * x + y * y) * 0.05))
        sv.append(Vector((x, y, z))); su.append((i / n, j / n))
for i in range(n):
    for j in range(n):
        a = i * (n + 1) + j
        sf.append((a, a + 1, a + n + 2, a + n + 1))
sand = make_mesh_obj("SeaFloor", sv, sf, mat_sand, uvs=su)

# rocks
for k in range(7):
    ang = random.uniform(0, 6.28)
    rad = random.uniform(1.8, 3.6)
    loc = (math.cos(ang) * rad, 1.0 + math.sin(ang) * rad * 0.6, -2.45 + random.uniform(-0.05, 0.05))
    bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=2, radius=random.uniform(0.16, 0.34), location=loc)
    ob = bpy.context.active_object
    ob.name = "Rock_%d" % k
    ob.scale = (1.0, random.uniform(0.7, 1.2), random.uniform(0.45, 0.7))
    ob.rotation_euler = (0, 0, random.uniform(0, 3.14))
    ob.data.materials.clear(); ob.data.materials.append(mat_rock)
    for p in ob.data.polygons:
        p.use_smooth = True

# water surface
n2, size2 = 26, 9.0
wv, wu, wf = [], [], []
for i in range(n2 + 1):
    for j in range(n2 + 1):
        x = -size2 / 2 + size2 * i / n2
        y = -size2 / 2 + size2 * j / n2
        z = 3.25 + 0.09 * math.sin(x * 1.6 + y * 0.7) + 0.06 * math.sin(y * 2.3 - x * 0.5)
        wv.append(Vector((x, y, z))); wu.append((i / n2, j / n2))
for i in range(n2):
    for j in range(n2):
        a = i * (n2 + 1) + j
        wf.append((a, a + 1, a + n2 + 2, a + n2 + 1))
surf = make_mesh_obj("WaterSurface", wv, wf, mat_surf, uvs=wu)
surf.visible_shadow = False

# plants
def make_blade(name, base, height, width, dirv, seed):
    ns = 18
    verts, uvs, faces = [], [], []
    perp = Vector((-dirv.y, dirv.x, 0.0))
    if perp.length < 1e-6:
        perp = Vector((1, 0, 0))
    perp.normalize()
    for j in range(ns + 1):
        v = j / ns
        w = width * (1 - 0.88 * v ** 1.4) * (0.45 + 0.55 * math.sin(math.pi * min(1.0, 0.18 + v * 0.9)))
        c = base + Vector((dirv.x * height * 0.55 * v * v, dirv.y * height * 0.55 * v * v, height * v))
        c = c + Vector((0.13 * math.sin(v * 4.0 + seed), 0.13 * math.cos(v * 3.4 + seed), 0.0)) * v
        verts.append(c - perp * w * 0.5); uvs.append((0.0, v))
        verts.append(c + perp * w * 0.5); uvs.append((1.0, v))
    for j in range(ns):
        a = 2 * j
        faces.append((a, a + 1, a + 3, a + 2))
    ob = make_mesh_obj(name, verts, faces, mat_plant, uvs=uvs)
    sd = ob.modifiers.new('sld', 'SOLIDIFY'); sd.thickness = 0.012; sd.offset = 0
    sf = ob.modifiers.new('sub', 'SUBSURF'); sf.levels = 1; sf.render_levels = 2
    return ob

clusters = [Vector((-2.9, 2.2, -2.45)), Vector((2.9, 2.6, -2.45)), Vector((0.6, 3.2, -2.45)),
            Vector((-3.4, 0.4, -2.45))]
bi = 0
for ci, cpos in enumerate(clusters):
    for k in range(5):
        off = Vector((random.uniform(-0.28, 0.28), random.uniform(-0.28, 0.28), 0))
        d = Vector((random.uniform(-1, 1), random.uniform(-1, 1), 0)).normalized()
        make_blade("Plant_%d_%d" % (ci, k), cpos + off, random.uniform(1.4, 2.6),
                   random.uniform(0.16, 0.28), d, random.uniform(0, 6.0))
        bi += 1

# bubbles
for k in range(16):
    x = -1.9 + random.uniform(-0.5, 0.9) + 0.25 * k * 0.1
    y = -0.6 + random.uniform(-0.8, 0.8)
    z = 0.4 + k * 0.17 + random.uniform(-0.1, 0.1)
    b = ball("Bubble_%d" % k, (x + 0.18 * math.sin(k), y, z), random.uniform(0.030, 0.085), mat_bub)
    b.visible_shadow = False

# ---------------------------------------------------------------- world
world = bpy.data.worlds.new("Water")
scene.world = world
world.use_nodes = True
wnt = world.node_tree
for nd in list(wnt.nodes):
    wnt.nodes.remove(nd)
wout = wnt.nodes.new('ShaderNodeOutputWorld'); wout.location = (400, 0)
bgn = wnt.nodes.new('ShaderNodeBackground'); bgn.location = (200, 0)
bgn.inputs['Strength'].default_value = 0.85
geo = wnt.nodes.new('ShaderNodeNewGeometry'); geo.location = (-600, 0)
sep = wnt.nodes.new('ShaderNodeSeparateXYZ'); sep.location = (-400, 0)
wnt.links.new(geo.outputs['Incoming'], sep.inputs['Vector'])
wr2 = wnt.nodes.new('ShaderNodeValToRGB'); wr2.location = (-200, 0)
wr2.color_ramp.elements[0].position = 0.20
wr2.color_ramp.elements[0].color = (0.010, 0.055, 0.085, 1)
wr2.color_ramp.elements[1].position = 0.85
wr2.color_ramp.elements[1].color = (0.10, 0.42, 0.52, 1)
mide = wr2.color_ramp.elements.new(0.55); mide.color = (0.035, 0.20, 0.30, 1)
wnt.links.new(sep.outputs['Z'], wr2.inputs['Fac'])
wnt.links.new(wr2.outputs['Color'], bgn.inputs['Color'])
wnt.links.new(bgn.outputs['Background'], wout.inputs['Surface'])

# ---------------------------------------------------------------- lights
sund = bpy.data.lights.new("SunShafts", type='SUN')
sund.energy = 2.0
sund.angle = 0.12
sund.color = (1.0, 0.94, 0.80)
sun = bpy.data.objects.new("SunShafts", sund)
C.objects.link(sun)
sun.location = (-3.0, -2.0, 6.0)
look_at(sun, (0.4, 0.4, 0.0))

add_area("Key", (-4.6, -6.2, 5.2), (0.1, 0.0, 0.25), 850.0, 5.0, (1.0, 0.96, 0.88))
add_area("Fill", (5.6, -3.6, 0.8), (0.4, 0.0, 0.1), 260.0, 6.0, (0.45, 0.78, 1.0))
add_area("Rim", (4.2, 5.0, 3.0), (0.6, 0.2, 0.3), 520.0, 4.0, (0.65, 0.95, 1.0))
add_area("Bounce", (-0.5, -2.0, -2.1), (0.0, 0.0, 0.4), 130.0, 5.0, (0.35, 0.75, 0.80))

# ---------------------------------------------------------------- camera
cam_data = bpy.data.cameras.new("HeroCam")
cam_data.lens = 52.0
cam = bpy.data.objects.new("HeroCam", cam_data)
C.objects.link(cam)
cam.location = (-2.15, -9.3, 1.15)
target = Vector((0.42, 0.0, 0.18))
look_at(cam, target)
cam_data.dof.use_dof = True
cam_data.dof.focus_distance = (target - cam.location).length
cam_data.dof.aperture_fstop = 3.2
scene.camera = cam

# ---------------------------------------------------------------- save
try:
    path = bpy.data.filepath
    if not path:
        path = os.path.join(os.getcwd(), "goldfish_scene.blend")
    bpy.ops.wm.save_as_mainfile(filepath=path)
except Exception as e:
    try:
        bpy.ops.wm.save_as_mainfile(filepath="/tmp/goldfish_scene.blend")
    except Exception:
        pass
```