You are participating in Blender-Bench, a public benchmark where AI models complete the same Blender task.

Your result will be compared head-to-head with another model. Anyone on the internet can vote based on two equally important published views: a 2D rendered presentation and an interactive 3D scene exported to GLB. Treat neither view as optional; make the result clear and compelling in both.

Complete the request as well as you can. The benchmark does not prescribe a workflow or visual style; use whichever Blender techniques you consider appropriate. Make the important requirements evident in the published result, save a complete scene, and present the work clearly in every requested output.

The 2D presentation is rendered by the benchmark from your saved scene using Cycles at 1536 x 1536, 64 samples, denoising enabled, and GPU acceleration when available with a CPU fallback. It is the native-fidelity view of the Blender scene, so you may use Blender's full feature set. Preview and validate your materials, lighting, color management, and camera composition in Cycles, and do not rely on EEVEE-only effects for the final appearance.

The interactive 3D view is a best-effort GLB export containing geometry, compatible materials, the contestant camera, and supported lights. Because glTF cannot represent every Blender feature, native effects such as particles, hair or curves, volumes, simulations, and some procedural geometry may appear only in the 2D render. The benchmark may also generate a separate preview GLB by baking supported procedural material inputs to textures; this does not convert unsupported geometry or guarantee exact parity with Blender. When practical, keep the core form and scene readable in the 3D view while using the 2D presentation to show native Blender features faithfully.

## Harness Instructions

You are writing a single Python script for Blender 5.1 using the bpy API. The scene starts empty. Your script must build everything the user's prompt asks for.

You are building a real three-dimensional scene, and evaluators judge the whole scene and your final render side by side — an interactive 3D view and a benchmark-generated 360° camera orbit rendered in Cycles from your saved scene appear next to your presentation render, so forms that only read correctly from one angle will show. Build genuine 3D first, then present it well: before you finish, set up a camera and lighting to frame a single, well-composed presentation image of your result — a good scene that happens to have a well-placed camera and lights. The final benchmark capture overrides the scene render settings and renders in Cycles at 1536x1536, 64 samples, with denoising and GPU acceleration (CPU fallback). Preview and validate materials, lighting, color management, and composition in Cycles; do not rely on EEVEE-only effects. Use whatever Blender features best serve the result; a best-effort GLB export is also provided for interactive inspection, and features glTF cannot represent are not expected to survive it.

You get one attempt: the script runs once, headless, with no feedback and no chance to iterate, so write robust code. Reply with exactly one fenced Python code block containing the complete script and nothing else.


## Task

Complete the following Blender task:

<task>

A single goldfish as the hero of the shot: rounded body, a big flowing tail fin,
dorsal and side fins, and an eye on each side, in an unmistakable goldfish orange.

The floor is a shape any child would name instantly. The ceiling is life: a gentle
S-curve in the body as if mid-swim, translucency toward the fin edges, a subtle
scale pattern, and a calm watery backdrop that makes the orange pop.

</task>


[System]
You are participating in Blender-Bench, a public benchmark where AI models complete the same Blender task.

Your result will be compared head-to-head with another model. Anyone on the internet can vote based on two equally important published views: a 2D rendered presentation and an interactive 3D scene exported to GLB. Treat neither view as optional; make the result clear and compelling in both.

Complete the request as well as you can. The benchmark does not prescribe a workflow or visual style; use whichever Blender techniques you consider appropriate. Make the important requirements evident in the published result, save a complete scene, and present the work clearly in every requested output.

The 2D presentation is rendered by the benchmark from your saved scene using Cycles at 1536 x 1536, 64 samples, denoising enabled, and GPU acceleration when available with a CPU fallback. It is the native-fidelity view of the Blender scene, so you may use Blender's full feature set. Preview and validate your materials, lighting, color management, and camera composition in Cycles, and do not rely on EEVEE-only effects for the final appearance.

The interactive 3D view is a best-effort GLB export containing geometry, compatible materials, the contestant camera, and supported lights. Because glTF cannot represent every Blender feature, native effects such as particles, hair or curves, volumes, simulations, and some procedural geometry may appear only in the 2D render. The benchmark may also generate a separate preview GLB by baking supported procedural material inputs to textures; this does not convert unsupported geometry or guarantee exact parity with Blender. When practical, keep the core form and scene readable in the 3D view while using the 2D presentation to show native Blender features faithfully.

## Harness Instructions

You are writing a single Python script for Blender 5.1 using the bpy API. The scene starts empty. Your script must build everything the user's prompt asks for.

You are building a real three-dimensional scene, and evaluators judge the whole scene and your final render side by side — an interactive 3D view and a benchmark-generated 360° camera orbit rendered in Cycles from your saved scene appear next to your presentation render, so forms that only read correctly from one angle will show. Build genuine 3D first, then present it well: before you finish, set up a camera and lighting to frame a single, well-composed presentation image of your result — a good scene that happens to have a well-placed camera and lights. The final benchmark capture overrides the scene render settings and renders in Cycles at 1536x1536, 64 samples, with denoising and GPU acceleration (CPU fallback). Preview and validate materials, lighting, color management, and composition in Cycles; do not rely on EEVEE-only effects. Use whatever Blender features best serve the result; a best-effort GLB export is also provided for interactive inspection, and features glTF cannot represent are not expected to survive it.

You get one attempt: the script runs once, headless, with no feedback and no chance to iterate, so write robust code. Reply with exactly one fenced Python code block containing the complete script and nothing else.

[User]
## Task

Complete the following Blender task:

<task>

A single goldfish as the hero of the shot: rounded body, a big flowing tail fin,
dorsal and side fins, and an eye on each side, in an unmistakable goldfish orange.

The floor is a shape any child would name instantly. The ceiling is life: a gentle
S-curve in the body as if mid-swim, translucency toward the fin edges, a subtle
scale pattern, and a calm watery backdrop that makes the orange pop.

</task>

[System]
You are participating in Blender-Bench, a public benchmark where AI models complete the same Blender task.

Your result will be compared head-to-head with another model. Anyone on the internet can vote based on two equally important published views: a 2D rendered presentation and an interactive 3D scene exported to GLB. Treat neither view as optional; make the result clear and compelling in both.

Complete the request as well as you can. The benchmark does not prescribe a workflow or visual style; use whichever Blender techniques you consider appropriate. Make the important requirements evident in the published result, save a complete scene, and present the work clearly in every requested output.

The 2D presentation is rendered by the benchmark from your saved scene using Cycles at 1536 x 1536, 64 samples, denoising enabled, and GPU acceleration when available with a CPU fallback. It is the native-fidelity view of the Blender scene, so you may use Blender's full feature set. Preview and validate your materials, lighting, color management, and camera composition in Cycles, and do not rely on EEVEE-only effects for the final appearance.

The interactive 3D view is a best-effort GLB export containing geometry, compatible materials, the contestant camera, and supported lights. Because glTF cannot represent every Blender feature, native effects such as particles, hair or curves, volumes, simulations, and some procedural geometry may appear only in the 2D render. The benchmark may also generate a separate preview GLB by baking supported procedural material inputs to textures; this does not convert unsupported geometry or guarantee exact parity with Blender. When practical, keep the core form and scene readable in the 3D view while using the 2D presentation to show native Blender features faithfully.

## Harness Instructions

You are writing a single Python script for Blender 5.1 using the bpy API. The scene starts empty. Your script must build everything the user's prompt asks for.

You are building a real three-dimensional scene, and evaluators judge the whole scene and your final render side by side — an interactive 3D view and a benchmark-generated 360° camera orbit rendered in Cycles from your saved scene appear next to your presentation render, so forms that only read correctly from one angle will show. Build genuine 3D first, then present it well: before you finish, set up a camera and lighting to frame a single, well-composed presentation image of your result — a good scene that happens to have a well-placed camera and lights. The final benchmark capture overrides the scene render settings and renders in Cycles at 1536x1536, 64 samples, with denoising and GPU acceleration (CPU fallback). Preview and validate materials, lighting, color management, and composition in Cycles; do not rely on EEVEE-only effects. Use whatever Blender features best serve the result; a best-effort GLB export is also provided for interactive inspection, and features glTF cannot represent are not expected to survive it.

You get one attempt: the script runs once, headless, with no feedback and no chance to iterate, so write robust code. Reply with exactly one fenced Python code block containing the complete script and nothing else.

[User]
## Task

Complete the following Blender task:

<task>

A single goldfish as the hero of the shot: rounded body, a big flowing tail fin,
dorsal and side fins, and an eye on each side, in an unmistakable goldfish orange.

The floor is a shape any child would name instantly. The ceiling is life: a gentle
S-curve in the body as if mid-swim, translucency toward the fin edges, a subtle
scale pattern, and a calm watery backdrop that makes the orange pop.

</task>

payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com" "https://js.stripe.com" "https://*.js.stripe.com" "https://hooks.stripe.com")

```python
import bpy
import math
import mathutils

# Clear existing mesh objects
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)

# ------------------------------------------------------------
# Helper function to create a material
# ------------------------------------------------------------
def create_goldfish_material(name, color, with_scales=True):
    mat = bpy.data.materials.new(name)
    mat.use_nodes = True
    nodes = mat.node_tree.nodes
    links = mat.node_tree.links
    nodes.clear()

    # Output node
    output = nodes.new(type='ShaderNodeOutputMaterial')
    output.location = (400, 0)

    # Principled BSDF
    bsdf = nodes.new(type='ShaderNodeBsdfPrincipled')
    bsdf.location = (100, 0)
    links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])

    # Base color orange
    if with_scales:
        # Mix with scale pattern
        # Use voronoi for scale look
        voronoi = nodes.new(type='ShaderNodeTexVoronoi')
        voronoi.location = (-300, 100)
        voronoi.feature = 'F1'
        voronoi.inputs['Scale'].default_value = 30.0
        voronoi.inputs['Randomness'].default_value = 0.3

        # Color ramp to create scale outlines
        ramp = nodes.new(type='ShaderNodeValToRGB')
        ramp.location = (-100, 100)
        # Set ramp colors: dark orange to bright orange
        ramp.color_ramp.elements[0].color = (0.6, 0.2, 0.0, 1.0)
        ramp.color_ramp.elements[0].position = 0.2
        ramp.color_ramp.elements[1].color = (1.0, 0.5, 0.0, 1.0)
        links.new(voronoi.outputs['Distance'], ramp.inputs['Fac'])

        # Mix with base color
        mix = nodes.new(type='ShaderNodeMixRGB')
        mix.location = (100, 200)
        mix.blend_type = 'OVERLAY'
        mix.inputs['Fac'].default_value = 0.5
        mix.inputs['Color1'].default_value = (1.0, 0.55, 0.0, 1.0)  # typical goldfish orange
        links.new(ramp.outputs['Color'], mix.inputs['Color2'])
        links.new(mix.outputs['Color'], bsdf.inputs['Base Color'])
    else:
        bsdf.inputs['Base Color'].default_value = (1.0, 0.55, 0.0, 1.0)

    # Add some roughness
    bsdf.inputs['Roughness'].default_value = 0.4
    bsdf.inputs['Specular'].default_value = 0.5

    return mat

def create_fin_material(name, base_color=(1.0, 0.5, 0.0, 1.0), transparent_edges=True):
    mat = bpy.data.materials.new(name)
    mat.use_nodes = True
    nodes = mat.node_tree.nodes
    links = mat.node_tree.links
    nodes.clear()

    output = nodes.new(type='ShaderNodeOutputMaterial')
    output.location = (400, 0)

    bsdf = nodes.new(type='ShaderNodeBsdfPrincipled')
    bsdf.location = (100, 0)
    links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])

    bsdf.inputs['Base Color'].default_value = base_color
    bsdf.inputs['Roughness'].default_value = 0.6
    bsdf.inputs['Transmission'].default_value = 0.2  # slight translucency

    if transparent_edges:
        # Use a gradient texture from edge to center
        # Add a geometry node to get distance from edge
        geo = nodes.new(type='ShaderNodeNewGeometry')
        geo.location = (-200, 100)

        # Capture attribute: we can use the 'Pointiness' from geometry? Not directly.
        # Alternative: use a procedural gradient based on UV coordinates.
        # We'll use a UV map with a gradient texture.
        # Simplest: use the 'Object' texture coordinate with a gradient.
        tex_coord = nodes.new(type='ShaderNodeTexCoord')
        tex_coord.location = (-400, 100)
        gradient = nodes.new(type='ShaderNodeTexGradient')
        gradient.location = (-200, 100)
        gradient.gradient_type = 'SPHERICAL'
        links.new(tex_coord.outputs['Object'], gradient.inputs['Vector'])

        # Invert gradient to get edges transparent
        ramp = nodes.new(type='ShaderNodeValToRGB')
        ramp.location = (0, 100)
        ramp.color_ramp.elements[0].color = (0,0,0,1)
        ramp.color_ramp.elements[0].position = 0.1
        ramp.color_ramp.elements[1].color = (1,1,1,1)
        ramp.color_ramp.elements[1].position = 0.9
        links.new(gradient.outputs['Fac'], ramp.inputs['Fac'])

        # Mix with transparency
        transparent = nodes.new(type='ShaderNodeBsdfTransparent')
        transparent.location = (100, -100)

        mix_shader = nodes.new(type='ShaderNodeMixShader')
        mix_shader.location = (250, 0)
        links.new(ramp.outputs['Color'], mix_shader.inputs['Fac'])
        links.new(bsdf.outputs['BSDF'], mix_shader.inputs[1])
        links.new(transparent.outputs['BSDF'], mix_shader.inputs[2])
        links.new(mix_shader.outputs['Shader'], output.inputs['Surface'])

    return mat

# ------------------------------------------------------------
# Build goldfish mesh
# ------------------------------------------------------------
# Start with a sphere
bpy.ops.mesh.primitive_uv_sphere_add(segments=24, ring_count=16, radius=1, location=(0,0,0))
fish_base = bpy.context.active_object
fish_base.name = "Goldfish_Body_Base"

# Scale to elongate along X, flatten Y (laterally compressed), increase height Z
fish_base.scale = (2.5, 0.6, 1.8)
bpy.ops.object.transform_apply(scale=True)

# Add lattice for S-curve deform
lattice = bpy.data.lattices.new("S_Curve_Lattice")
lattice_obj = bpy.data.objects.new("S_Curve_Lattice", lattice)
bpy.context.collection.objects.link(lattice_obj)
lattice_obj.location = (0,0,0)

# Set lattice subdivisions
lattice.points_u = 8
lattice.points_v = 4
lattice.points_w = 4

# Add lattice modifier to fish
mod = fish_base.modifiers.new(name="Lattice", type='LATTICE')
mod.object = lattice_obj

# Select lattice and enter edit mode to shape points
bpy.context.view_layer.objects.active = lattice_obj
bpy.ops.object.mode_set(mode='EDIT')
lattice_points = lattice_obj.data.points
# Get the boundaries
# We'll create an S-curve along the U direction (X)
# For each row in U, we adjust V and W uniformly? To keep cross-section symmetric, we shift all points with same U up or down in Z.
# We iterate over all points, group by U index.
u_steps = lattice.points_u
v_steps = lattice.points_v
w_steps = lattice.points_w
# Map U index to Z offset: sine wave
import math
for u in range(u_steps):
    t = u / (u_steps - 1)  # 0 to 1
    # Gentle S: use sine-like but with two bends
    z_offset = 0.3 * math.sin(2 * math.pi * t)  # one period -> S shape
    for v in range(v_steps):
        for w in range(w_steps):
            idx = u * v_steps * w_steps + v * w_steps + w
            point = lattice_points[idx]
            # Original position relative to lattice (normalized -0.5 to 0.5 in each axis)
            # We want to shift Z coordinate
            # point.co_deform is a Vector, but we can directly edit point.co
            # Actually in edit mode, we modify point.co (relative to lattice origin, range -0.5..0.5)
            # But we need to ensure we don't shift V/W.
            point.co.z = point.co.z + z_offset
bpy.ops.object.mode_set(mode='OBJECT')

# Apply lattice modifier to make shape permanent
bpy.context.view_layer.objects.active = fish_base
bpy.ops.object.modifier_apply(modifier="Lattice")

# Delete lattice object
bpy.data.objects.remove(lattice_obj, do_unlink=True)

# Now fish_base has S-curve shape.
# Rename to body
fish_body = fish_base
fish_body.name = "Goldfish_Body"

# ------------------------------------------------------------
# Add fins
# ------------------------------------------------------------
# Tail fin: a large thin fan shape at the back (negative X end)
# Find the end vertex: the most negative X vertex
# We'll place a plane at that location, rotate, shape, and attach material.
tail_location = (-2.5, 0, 0)  # approximate
bpy.ops.mesh.primitive_grid_add(size=2, x_subdivisions=8, y_subdivisions=8, location=tail_location)
tail_fin = bpy.context.active_object
tail_fin.name = "Tail_Fin"
# Scale to make it wide and thin
tail_fin.scale = (0.2, 1.5, 1.0)
bpy.ops.object.transform_apply(scale=True)
# Rotate so it's perpendicular to body? Actually tail fin should trail behind, fanning out in Y and Z.
# Current grid is facing Z? By default grid is in XY plane. We want it in YZ plane (facing X).
bpy.ops.transform.rotate(value=math.radians(90), axis='Y')
# Shape: use proportional editing to make it fan-like
bpy.ops.object.mode_set(mode='EDIT')
# Move vertices to form a fan: center vertex at tail tip, pull outer edges.
# Or we can use a mesh from a curve? Simpler: Use simple shape: just a large plane with subdivision and then move vertices.
# We'll use a simpler approach: use a circle mesh extruded? Time.
# For now, just use the grid and shape with proportional editing moving outer loops inward.
bpy.ops.mesh.select_all(action='DESELECT')
# Select the farthest vertices in Y direction? Actually easier: we'll leave it as a rectangular fin for now.
# But we need a nice tail. Use a "fan" shape by creating a mesh from a curve.
# Alternative: Use a "mesh from curve" by creating a bezier curve for the fin outline, then fill.
# Let's do that for better look.

# Delete the grid
bpy.ops.object.delete(use_global=False)

# Create a bezier curve for tail fin profile
bpy.ops.curve.primitive_bezier_curve_add(enter_editmode=False, location=(-2.5, 0, 0))
curve_obj = bpy.context.active_object
curve_obj.name = "Tail_Curve"
# Change to 2D and fill
curve_obj.data.dimensions = '2D'
curve_obj.data.fill_mode = 'BOTH'
curve_obj.data.bevel_depth = 0.0  # no bevel
# Edit curve points
bpy.ops.object.mode_set(mode='EDIT')
# Get spline
spline = curve_obj.data.splines[0]
spline.bezier_points[0].co = mathutils.Vector((0, 0, 0))
spline.bezier_points[0].handle_left = mathutils.Vector((-0.3, -0.5, 0))
spline.bezier_points[0].handle_right = mathutils.Vector((0.3, -0.5, 0))
spline.bezier_points[0].radius = 1.0
spline.bezier_points[1].co = mathutils.Vector((0, 1.5, 0))
spline.bezier_points[1].handle_left = mathutils.Vector((0.1, 1.0, 0))
spline.bezier_points[1].handle_right = mathutils.Vector((-0.1, 1.0, 0))
# Add more points to make fan shape? Only two points give a straight line. We need more.
# We'll delete this and create a more complex curve.
bpy.ops.curve.delete(type='SELECT')
# Add a path curve with many points? Or use a bezier with more control points.
bpy.ops.curve.primitive_bezier_curve_add(enter_editmode=False, location=(-2.5, 0, 0))
curve_obj = bpy.context.active_object
curve_obj.name = "Tail_Curve_S"
bpy.ops.object.mode_set(mode='EDIT')
spline = curve_obj.data.splines[0]
# We'll make a series of points to outline a tail fin shape (like a fan)
points = [
    (0, 0, 0),           # tip
    (0.2, 0.5, 0),
    (0.3, 1.0, 0),
    (0.2, 1.5, 0),
    (0, 1.8, 0),
    (-0.2, 1.5, 0),
    (-0.3, 1.0, 0),
    (-0.2, 0.5, 0),
    (0, 0, 0)            # back to tip
]
spline.points.clear()
for i, p in enumerate(points):
    spline.points.add(1)
    spline.points[i].co = mathutils.Vector((p[0], p[1], p[2], 1))
# Set spline type to 'POLY'
spline.type = 'POLY'
# Set fill mode
bpy.ops.object.mode_set(mode='OBJECT')
curve_obj.data.dimensions = '2D'
curve_obj.data.fill_mode = 'BOTH'
curve_obj.data.use_fill_caps = True
# Now convert to mesh
bpy.ops.object.convert(target='MESH')
tail_fin = bpy.context.active_object
tail_fin.name = "Tail_Fin"
# Rotate to align with body (tail fin should be in YZ plane)
tail_fin.rotation_euler = (0, math.radians(90), 0)
bpy.ops.object.transform_apply(rotation=True)
# Add subdivision surface to smooth
mod = tail_fin.modifiers.new(name="Subsurf", type='SUBSURF')
mod.levels = 2
bpy.ops.object.modifier_apply(modifier="Subsurf")
# Add material
mat_tail = create_fin_material("Tail_Fin_Mat", base_color=(1.0, 0.5, 0.0, 1.0), transparent_edges=True)
if tail_fin.data.materials:
    tail_fin.data.materials[0] = mat_tail
else:
    tail_fin.data.materials.append(mat_tail)

# Dorsal fin: smaller fan on top (positive Z)
# Similar approach: curve outline, convert, rotate, etc.
dorsal_location = (0, 0, 2.0)
bpy.ops.curve.primitive_bezier_curve_add(enter_editmode=False, location=dorsal_location)
curve_dorsal = bpy.context.active_object
bpy.ops.object.mode_set(mode='EDIT')
spline = curve_dorsal.data.splines[0]
points_dorsal = [
    (0, 0, 0),
    (0.1, 0.3, 0),
    (0.15, 0.6, 0),
    (0.1, 0.9, 0),
    (0, 1.0, 0),
    (-0.1, 0.9, 0),
    (-0.15, 0.6, 0),
    (-0.1, 0.3, 0),
    (0, 0, 0)
]
spline.points.clear()
for i, p in enumerate(points_dorsal):
    spline.points.add(1)
    spline.points[i].co = mathutils.Vector((p[0], p[1], p[2], 1))
spline.type = 'POLY'
bpy.ops.object.mode_set(mode='OBJECT')
curve_dorsal.data.dimensions = '2D'
curve_dorsal.data.fill_mode = 'BOTH'
curve_dorsal.data.use_fill_caps = True
bpy.ops.object.convert(target='MESH')
dorsal_fin = bpy.context.active_object
dorsal_fin.name = "Dorsal_Fin"
# Rotate to lie flat on top? Actually dorsal fin stands up along Z, so it should be in XZ plane? We'll rotate to match.
dorsal_fin.rotation_euler = (math.radians(90), 0, 0)  # rotate around X to bring to XZ? Let's check.
# Default after conversion, the curve was in XY plane. We want it in YZ plane? No, dorsal fin is on top, spreading in X and Z? Actually dorsal fin runs along the back, it's a vertical fin. So outline should be in YZ (side view) but placed on top. Let's simple: create a plane, shape with proportional editing, add material.
# But using curve is okay. We'll adjust later by scaling and rotating.
# Let's just delete and use a simpler approach: use a grid and shape.
bpy.ops.object.delete(use_global=False)

# Use a circle for dorsal fin? Create a circle, scale, solidify?
# Time is tight. I'll use a simple plane with subdivision and shape it manually by moving vertices.
# For robustness, I'll create a plane, subdivide, and use proportional editing to get a fan shape.
bpy.ops.mesh.primitive_plane_add(size=1, location=(0, 0, 2.2))
dorsal_fin = bpy.context.active_object
dorsal_fin.name = "Dorsal_Fin"
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.subdivide(number_cuts=4)
# Select all vertices
bpy.ops.mesh.select_all(action='SELECT')
# Scale along Y to make it wider
bpy.ops.transform.resize(value=(1, 2, 1))
# Now shape: we want a fin that is curved and tapers to tip.
# Move the farthest Y vertices up (Z) to create a fin.
# Use proportional editing with falloff.
bpy.ops.transform.translate(value=(0, 0, 0.8), constraint_axis=(False, False, True), orient_type='GLOBAL')
# Exit edit mode
bpy.ops.object.mode_set(mode='OBJECT')
# Add subdivision
mod = dorsal_fin.modifiers.new(name="Subsurf", type='SUBSURF')
mod.levels = 2
bpy.ops.object.modifier_apply(modifier="Subsurf")
# Rotate to orient correctly? Already plane is in XY, we want it standing up in Z? Actually we want it to be vertical, so we need to rotate it to be in YZ or XZ? Typically dorsal fin is vertical, attached along the back. We'll rotate it so its plane is XZ (vertical). Rotate around X by 90 degrees.
dorsal_fin.rotation_euler = (math.radians(90), 0, 0)
bpy.ops.object.transform_apply(rotation=True)
# Now it's standing up. But we need it to be attached to the back at the top of the body. The fin's center is at (0,0,2.2). That's fine.
# Add material
mat_dorsal = create_fin_material("Dorsal_Fin_Mat", base_color=(1.0, 0.45, 0.0, 1.0), transparent_edges=True)
dorsal_fin.data.materials.append(mat_dorsal)

# Pectoral fins (side fins): two small fins on sides.
# Create a plane, shape, duplicate.
bpy.ops.mesh.primitive_plane_add(size=0.5, location=(1.0, 0.8, 0.5))
fin_side = bpy.context.active_object
fin_side.name = "Pectoral_Fin_Right"
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.subdivide(number_cuts=2)
# Scale and shape
bpy.ops.transform.resize(value=(1.5, 1, 1))
# Move some vertices to create a leaf shape
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.transform.translate(value=(0, 0, 0.3), orient_type='GLOBAL')
bpy.ops.object.mode_set(mode='OBJECT')
# Add subsurf
mod = fin_side.modifiers.new(name="Subsurf", type='SUBSURF')
mod.levels = 2
bpy.ops.object.modifier_apply(modifier="Subsurf")
# Rotate to angle
fin_side.rotation_euler = (0, 0, math.radians(30))
bpy.ops.object.transform_apply(rotation=True)
# Material
mat_side = create_fin_material("Side_Fin_Mat", base_color=(1.0, 0.5, 0.0, 1.0), transparent_edges=True)
fin_side.data.materials.append(mat_side)
# Duplicate for left side
bpy.ops.object.duplicate_move_linked(OBJECT='Pectoral_Fin_Right')
fin_left = bpy.context.active_object
fin_left.name = "Pectoral_Fin_Left"
fin_left.location.x = -1.0  # opposite side
fin_left.scale.x = -1  # mirror? but careful with normals. better set scale negative.
# Actually mirror by scaling X by -1
fin_left.scale.x = -1.0
bpy.ops.object.transform_apply(scale=True)
# Adjust rotation
fin_left.rotation_euler.z = math.radians(-30)  # opposite angle
bpy.ops.object.transform_apply(rotation=True)

# Eyes: two small spheres on sides of head.
bpy.ops.mesh.primitive_uv_sphere_add(segments=16, ring_count=8, radius=0.2, location=(2.2, 0.6, 0.6))
eye_right = bpy.context.active_object
eye_right.name = "Eye_Right"
# Eye material: white sclera, black pupil
mat_eye = bpy.data.materials.new("Eye_Mat")
mat_eye.use_nodes = True
nodes = mat_eye.node_tree.nodes
links = mat_eye.node_tree.links
nodes.clear()
out = nodes.new(type='ShaderNodeOutputMaterial')
diff = nodes.new(type='ShaderNodeBsdfDiffuse')
diff.inputs['Color'].default_value = (1,1,1,1)
links.new(diff.outputs['BSDF'], out.inputs['Surface'])
eye_right.data.materials.append(mat_eye)
# Pupil: add a smaller black sphere inside
bpy.ops.mesh.primitive_uv_sphere_add(segments=12, ring_count=6, radius=0.1, location=(2.2, 0.6, 0.6))
pupil_right = bpy.context.active_object
pupil_right.name = "Pupil_Right"
pupil_right.scale = (0.5, 1, 1)  # slightly oval
mat_pupil = bpy.data.materials.new("Pupil_Mat")
mat_pupil.use_nodes = True
nodes = mat_pupil.node_tree.nodes
nodes.clear()
out = nodes.new(type='ShaderNodeOutputMaterial')
diff = nodes.new(type='ShaderNodeBsdfDiffuse')
diff.inputs['Color'].default_value = (0,0,0,1)
links.new(diff.outputs['BSDF'], out.inputs['Surface'])
pupil_right.data.materials.append(mat_pupil)
# Duplicate for left eye
bpy.ops.object.duplicate_move_linked(OBJECT='Eye_Right')
eye_left = bpy.context.active_object
eye_left.name = "Eye_Left"
eye_left.location.x = -2.2
bpy.ops.object.duplicate_move_linked(OBJECT='Pupil_Right')
pupil_left = bpy.context.active_object
pupil_left.name = "Pupil_Left"
pupil_left.location.x = -2.2

# ------------------------------------------------------------
# Apply goldfish body material
# ------------------------------------------------------------
mat_body = create_goldfish_material("Goldfish_Body_Mat", color=(1.0, 0.55, 0.0, 1.0), with_scales=True)
if fish_body.data.materials:
    fish_body.data.materials[0] = mat_body
else:
    fish_body.data.materials.append(mat_body)

# ------------------------------------------------------------
# Floor: a circle that any child would name instantly
# ------------------------------------------------------------
bpy.ops.mesh.primitive_circle_add(vertices=64, radius=5, fill_type='TRIFAN', location=(0,0,-2))
floor = bpy.context.active_object
floor.name = "Floor"
floor.rotation_euler = (0, 0, 0)
# Material: light blue gradient or something
mat_floor = bpy.data.materials.new("Floor_Mat")
mat_floor.use_nodes = True
nodes = mat_floor.node_tree.nodes
links = mat_floor.node_tree.links
nodes.clear()
out = nodes.new(type='ShaderNodeOutputMaterial')
diff = nodes.new(type='ShaderNodeBsdfDiffuse')
diff.inputs['Color'].default_value = (0.2, 0.5, 0.8, 1)
links.new(diff.outputs['BSDF'], out.inputs['Surface'])
floor.data.materials.append(mat_floor)

# ------------------------------------------------------------
# Background: world gradient for calm watery backdrop
# ------------------------------------------------------------
world = bpy.data.worlds.new('World')
world.use_nodes = True
nodes = world.node_tree.nodes
links = world.node_tree.links
nodes.clear()
out_world = nodes.new(type='ShaderNodeOutputWorld')
bg = nodes.new(type='ShaderNodeBackground')
bg.inputs['Strength'].default_value = 1.0
links.new(bg.outputs['Background'], out_world.inputs['Surface'])

# Gradient texture
grad = nodes.new(type='ShaderNodeTexGradient')
grad.gradient_type = 'LINEAR'
coord = nodes.new(type='ShaderNodeTexCoord')
links.new(coord.outputs['Generated'], grad.inputs['Vector'])
# Map range to color from light blue to darker blue
color_ramp = nodes.new(type='ShaderNodeValToRGB')
color_ramp.color_ramp.elements[0].color = (0.3, 0.6, 0.9, 1.0)  # light sky
color_ramp.color_ramp.elements[0].position = 0.0
color_ramp.color_ramp.elements[1].color = (0.1, 0.2, 0.4, 1.0)  # deep water
color_ramp.color_ramp.elements[1].position = 1.0
links.new(grad.outputs['Fac'], color_ramp.inputs['Fac'])
links.new(color_ramp.outputs['Color'], bg.inputs['Color'])

# Assign to scene
bpy.context.scene.world = world

# ------------------------------------------------------------
# Lighting
# ------------------------------------------------------------
# Key light
bpy.ops.object.light_add(type='AREA', location=(4, 3, 6))
key_light = bpy.context.active_object
key_light.name = "Key_Light"
key_light.data.energy = 200
key_light.data.size = 2
# Backlight/rim light
bpy.ops.object.light_add(type='AREA', location=(-3, -2, 4))
rim_light = bpy.context.active_object
rim_light.name = "Rim_Light"
rim_light.data.energy = 100
rim_light.data.size = 2

# Fill light
bpy.ops.object.light_add(type='AREA', location=(0, -4, 3))
fill_light = bpy.context.active_object
fill_light.name = "Fill_Light"
fill_light.data.energy = 50
fill_light.data.size = 2

# ------------------------------------------------------------
# Camera
# ------------------------------------------------------------
bpy.ops.object.camera_add(location=(4, -5, 3))
camera = bpy.context.active_object
camera.name = "Main_Camera"
camera.rotation_euler = (math.radians(60), 0, math.radians(45))
bpy.context.scene.camera = camera

# ------------------------------------------------------------
# Group all fish parts into a collection for easy manipulation
# ------------------------------------------------------------
fish_collection = bpy.data.collections.new("Goldfish")
bpy.context.scene.collection.children.link(fish_collection)
# Move all fish parts to this collection
for obj in [fish_body, tail_fin, dorsal_fin, fin_side, fin_left, eye_right, eye_left, pupil_right, pupil_left]:
    if obj.name in bpy.context.scene.collection.objects:
        bpy.context.scene.collection.objects.unlink(obj)
    fish_collection.objects.link(obj)

# ------------------------------------------------------------
# Final viewport adjustments (optional)
# ------------------------------------------------------------
# Set camera to see the fish in a nice composition
# The fish is centered around origin. Camera looks at origin.
# Add an empty as target for camera to track? Not necessary.

# Ensure all objects are visible
bpy.context.view_layer.update()

print("Script completed successfully.")
```


```python
import bpy
import math
import mathutils

# Clear existing mesh objects
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)

# ------------------------------------------------------------
# Helper function to create a material
# ------------------------------------------------------------
def create_goldfish_material(name, color, with_scales=True):
    mat = bpy.data.materials.new(name)
    mat.use_nodes = True
    nodes = mat.node_tree.nodes
    links = mat.node_tree.links
    nodes.clear()

    # Output node
    output = nodes.new(type='ShaderNodeOutputMaterial')
    output.location = (400, 0)

    # Principled BSDF
    bsdf = nodes.new(type='ShaderNodeBsdfPrincipled')
    bsdf.location = (100, 0)
    links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])

    # Base color orange
    if with_scales:
        # Mix with scale pattern
        # Use voronoi for scale look
        voronoi = nodes.new(type='ShaderNodeTexVoronoi')
        voronoi.location = (-300, 100)
        voronoi.feature = 'F1'
        voronoi.inputs['Scale'].default_value = 30.0
        voronoi.inputs['Randomness'].default_value = 0.3

        # Color ramp to create scale outlines
        ramp = nodes.new(type='ShaderNodeValToRGB')
        ramp.location = (-100, 100)
        # Set ramp colors: dark orange to bright orange
        ramp.color_ramp.elements[0].color = (0.6, 0.2, 0.0, 1.0)
        ramp.color_ramp.elements[0].position = 0.2
        ramp.color_ramp.elements[1].color = (1.0, 0.5, 0.0, 1.0)
        links.new(voronoi.outputs['Distance'], ramp.inputs['Fac'])

        # Mix with base color
        mix = nodes.new(type='ShaderNodeMixRGB')
        mix.location = (100, 200)
        mix.blend_type = 'OVERLAY'
        mix.inputs['Fac'].default_value = 0.5
        mix.inputs['Color1'].default_value = (1.0, 0.55, 0.0, 1.0)  # typical goldfish orange
        links.new(ramp.outputs['Color'], mix.inputs['Color2'])
        links.new(mix.outputs['Color'], bsdf.inputs['Base Color'])
    else:
        bsdf.inputs['Base Color'].default_value = (1.0, 0.55, 0.0, 1.0)

    # Add some roughness
    bsdf.inputs['Roughness'].default_value = 0.4
    bsdf.inputs['Specular'].default_value = 0.5

    return mat

def create_fin_material(name, base_color=(1.0, 0.5, 0.0, 1.0), transparent_edges=True):
    mat = bpy.data.materials.new(name)
    mat.use_nodes = True
    nodes = mat.node_tree.nodes
    links = mat.node_tree.links
    nodes.clear()

    output = nodes.new(type='ShaderNodeOutputMaterial')
    output.location = (400, 0)

    bsdf = nodes.new(type='ShaderNodeBsdfPrincipled')
    bsdf.location = (100, 0)
    links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])

    bsdf.inputs['Base Color'].default_value = base_color
    bsdf.inputs['Roughness'].default_value = 0.6
    bsdf.inputs['Transmission'].default_value = 0.2  # slight translucency

    if transparent_edges:
        # Use a gradient texture from edge to center
        # Add a geometry node to get distance from edge
        geo = nodes.new(type='ShaderNodeNewGeometry')
        geo.location = (-200, 100)

        # Capture attribute: we can use the 'Pointiness' from geometry? Not directly.
        # Alternative: use a procedural gradient based on UV coordinates.
        # We'll use a UV map with a gradient texture.
        # Simplest: use the 'Object' texture coordinate with a gradient.
        tex_coord = nodes.new(type='ShaderNodeTexCoord')
        tex_coord.location = (-400, 100)
        gradient = nodes.new(type='ShaderNodeTexGradient')
        gradient.location = (-200, 100)
        gradient.gradient_type = 'SPHERICAL'
        links.new(tex_coord.outputs['Object'], gradient.inputs['Vector'])

        # Invert gradient to get edges transparent
        ramp = nodes.new(type='ShaderNodeValToRGB')
        ramp.location = (0, 100)
        ramp.color_ramp.elements[0].color = (0,0,0,1)
        ramp.color_ramp.elements[0].position = 0.1
        ramp.color_ramp.elements[1].color = (1,1,1,1)
        ramp.color_ramp.elements[1].position = 0.9
        links.new(gradient.outputs['Fac'], ramp.inputs['Fac'])

        # Mix with transparency
        transparent = nodes.new(type='ShaderNodeBsdfTransparent')
        transparent.location = (100, -100)

        mix_shader = nodes.new(type='ShaderNodeMixShader')
        mix_shader.location = (250, 0)
        links.new(ramp.outputs['Color'], mix_shader.inputs['Fac'])
        links.new(bsdf.outputs['BSDF'], mix_shader.inputs[1])
        links.new(transparent.outputs['BSDF'], mix_shader.inputs[2])
        links.new(mix_shader.outputs['Shader'], output.inputs['Surface'])

    return mat

# ------------------------------------------------------------
# Build goldfish mesh
# ------------------------------------------------------------
# Start with a sphere
bpy.ops.mesh.primitive_uv_sphere_add(segments=24, ring_count=16, radius=1, location=(0,0,0))
fish_base = bpy.context.active_object
fish_base.name = "Goldfish_Body_Base"

# Scale to elongate along X, flatten Y (laterally compressed), increase height Z
fish_base.scale = (2.5, 0.6, 1.8)
bpy.ops.object.transform_apply(scale=True)

# Add lattice for S-curve deform
lattice = bpy.data.lattices.new("S_Curve_Lattice")
lattice_obj = bpy.data.objects.new("S_Curve_Lattice", lattice)
bpy.context.collection.objects.link(lattice_obj)
lattice_obj.location = (0,0,0)

# Set lattice subdivisions
lattice.points_u = 8
lattice.points_v = 4
lattice.points_w = 4

# Add lattice modifier to fish
mod = fish_base.modifiers.new(name="Lattice", type='LATTICE')
mod.object = lattice_obj

# Select lattice and enter edit mode to shape points
bpy.context.view_layer.objects.active = lattice_obj
bpy.ops.object.mode_set(mode='EDIT')
lattice_points = lattice_obj.data.points
# Get the boundaries
# We'll create an S-curve along the U direction (X)
# For each row in U, we adjust V and W uniformly? To keep cross-section symmetric, we shift all points with same U up or down in Z.
# We iterate over all points, group by U index.
u_steps = lattice.points_u
v_steps = lattice.points_v
w_steps = lattice.points_w
# Map U index to Z offset: sine wave
import math
for u in range(u_steps):
    t = u / (u_steps - 1)  # 0 to 1
    # Gentle S: use sine-like but with two bends
    z_offset = 0.3 * math.sin(2 * math.pi * t)  # one period -> S shape
    for v in range(v_steps):
        for w in range(w_steps):
            idx = u * v_steps * w_steps + v * w_steps + w
            point = lattice_points[idx]
            # Original position relative to lattice (normalized -0.5 to 0.5 in each axis)
            # We want to shift Z coordinate
            # point.co_deform is a Vector, but we can directly edit point.co
            # Actually in edit mode, we modify point.co (relative to lattice origin, range -0.5..0.5)
            # But we need to ensure we don't shift V/W.
            point.co.z = point.co.z + z_offset
bpy.ops.object.mode_set(mode='OBJECT')

# Apply lattice modifier to make shape permanent
bpy.context.view_layer.objects.active = fish_base
bpy.ops.object.modifier_apply(modifier="Lattice")

# Delete lattice object
bpy.data.objects.remove(lattice_obj, do_unlink=True)

# Now fish_base has S-curve shape.
# Rename to body
fish_body = fish_base
fish_body.name = "Goldfish_Body"

# ------------------------------------------------------------
# Add fins
# ------------------------------------------------------------
# Tail fin: a large thin fan shape at the back (negative X end)
# Find the end vertex: the most negative X vertex
# We'll place a plane at that location, rotate, shape, and attach material.
tail_location = (-2.5, 0, 0)  # approximate
bpy.ops.mesh.primitive_grid_add(size=2, x_subdivisions=8, y_subdivisions=8, location=tail_location)
tail_fin = bpy.context.active_object
tail_fin.name = "Tail_Fin"
# Scale to make it wide and thin
tail_fin.scale = (0.2, 1.5, 1.0)
bpy.ops.object.transform_apply(scale=True)
# Rotate so it's perpendicular to body? Actually tail fin should trail behind, fanning out in Y and Z.
# Current grid is facing Z? By default grid is in XY plane. We want it in YZ plane (facing X).
bpy.ops.transform.rotate(value=math.radians(90), axis='Y')
# Shape: use proportional editing to make it fan-like
bpy.ops.object.mode_set(mode='EDIT')
# Move vertices to form a fan: center vertex at tail tip, pull outer edges.
# Or we can use a mesh from a curve? Simpler: Use simple shape: just a large plane with subdivision and then move vertices.
# We'll use a simpler approach: use a circle mesh extruded? Time.
# For now, just use the grid and shape with proportional editing moving outer loops inward.
bpy.ops.mesh.select_all(action='DESELECT')
# Select the farthest vertices in Y direction? Actually easier: we'll leave it as a rectangular fin for now.
# But we need a nice tail. Use a "fan" shape by creating a mesh from a curve.
# Alternative: Use a "mesh from curve" by creating a bezier curve for the fin outline, then fill.
# Let's do that for better look.

# Delete the grid
bpy.ops.object.delete(use_global=False)

# Create a bezier curve for tail fin profile
bpy.ops.curve.primitive_bezier_curve_add(enter_editmode=False, location=(-2.5, 0, 0))
curve_obj = bpy.context.active_object
curve_obj.name = "Tail_Curve"
# Change to 2D and fill
curve_obj.data.dimensions = '2D'
curve_obj.data.fill_mode = 'BOTH'
curve_obj.data.bevel_depth = 0.0  # no bevel
# Edit curve points
bpy.ops.object.mode_set(mode='EDIT')
# Get spline
spline = curve_obj.data.splines[0]
spline.bezier_points[0].co = mathutils.Vector((0, 0, 0))
spline.bezier_points[0].handle_left = mathutils.Vector((-0.3, -0.5, 0))
spline.bezier_points[0].handle_right = mathutils.Vector((0.3, -0.5, 0))
spline.bezier_points[0].radius = 1.0
spline.bezier_points[1].co = mathutils.Vector((0, 1.5, 0))
spline.bezier_points[1].handle_left = mathutils.Vector((0.1, 1.0, 0))
spline.bezier_points[1].handle_right = mathutils.Vector((-0.1, 1.0, 0))
# Add more points to make fan shape? Only two points give a straight line. We need more.
# We'll delete this and create a more complex curve.
bpy.ops.curve.delete(type='SELECT')
# Add a path curve with many points? Or use a bezier with more control points.
bpy.ops.curve.primitive_bezier_curve_add(enter_editmode=False, location=(-2.5, 0, 0))
curve_obj = bpy.context.active_object
curve_obj.name = "Tail_Curve_S"
bpy.ops.object.mode_set(mode='EDIT')
spline = curve_obj.data.splines[0]
# We'll make a series of points to outline a tail fin shape (like a fan)
points = [
    (0, 0, 0),           # tip
    (0.2, 0.5, 0),
    (0.3, 1.0, 0),
    (0.2, 1.5, 0),
    (0, 1.8, 0),
    (-0.2, 1.5, 0),
    (-0.3, 1.0, 0),
    (-0.2, 0.5, 0),
    (0, 0, 0)            # back to tip
]
spline.points.clear()
for i, p in enumerate(points):
    spline.points.add(1)
    spline.points[i].co = mathutils.Vector((p[0], p[1], p[2], 1))
# Set spline type to 'POLY'
spline.type = 'POLY'
# Set fill mode
bpy.ops.object.mode_set(mode='OBJECT')
curve_obj.data.dimensions = '2D'
curve_obj.data.fill_mode = 'BOTH'
curve_obj.data.use_fill_caps = True
# Now convert to mesh
bpy.ops.object.convert(target='MESH')
tail_fin = bpy.context.active_object
tail_fin.name = "Tail_Fin"
# Rotate to align with body (tail fin should be in YZ plane)
tail_fin.rotation_euler = (0, math.radians(90), 0)
bpy.ops.object.transform_apply(rotation=True)
# Add subdivision surface to smooth
mod = tail_fin.modifiers.new(name="Subsurf", type='SUBSURF')
mod.levels = 2
bpy.ops.object.modifier_apply(modifier="Subsurf")
# Add material
mat_tail = create_fin_material("Tail_Fin_Mat", base_color=(1.0, 0.5, 0.0, 1.0), transparent_edges=True)
if tail_fin.data.materials:
    tail_fin.data.materials[0] = mat_tail
else:
    tail_fin.data.materials.append(mat_tail)

# Dorsal fin: smaller fan on top (positive Z)
# Similar approach: curve outline, convert, rotate, etc.
dorsal_location = (0, 0, 2.0)
bpy.ops.curve.primitive_bezier_curve_add(enter_editmode=False, location=dorsal_location)
curve_dorsal = bpy.context.active_object
bpy.ops.object.mode_set(mode='EDIT')
spline = curve_dorsal.data.splines[0]
points_dorsal = [
    (0, 0, 0),
    (0.1, 0.3, 0),
    (0.15, 0.6, 0),
    (0.1, 0.9, 0),
    (0, 1.0, 0),
    (-0.1, 0.9, 0),
    (-0.15, 0.6, 0),
    (-0.1, 0.3, 0),
    (0, 0, 0)
]
spline.points.clear()
for i, p in enumerate(points_dorsal):
    spline.points.add(1)
    spline.points[i].co = mathutils.Vector((p[0], p[1], p[2], 1))
spline.type = 'POLY'
bpy.ops.object.mode_set(mode='OBJECT')
curve_dorsal.data.dimensions = '2D'
curve_dorsal.data.fill_mode = 'BOTH'
curve_dorsal.data.use_fill_caps = True
bpy.ops.object.convert(target='MESH')
dorsal_fin = bpy.context.active_object
dorsal_fin.name = "Dorsal_Fin"
# Rotate to lie flat on top? Actually dorsal fin stands up along Z, so it should be in XZ plane? We'll rotate to match.
dorsal_fin.rotation_euler = (math.radians(90), 0, 0)  # rotate around X to bring to XZ? Let's check.
# Default after conversion, the curve was in XY plane. We want it in YZ plane? No, dorsal fin is on top, spreading in X and Z? Actually dorsal fin runs along the back, it's a vertical fin. So outline should be in YZ (side view) but placed on top. Let's simple: create a plane, shape with proportional editing, add material.
# But using curve is okay. We'll adjust later by scaling and rotating.
# Let's just delete and use a simpler approach: use a grid and shape.
bpy.ops.object.delete(use_global=False)

# Use a circle for dorsal fin? Create a circle, scale, solidify?
# Time is tight. I'll use a simple plane with subdivision and shape it manually by moving vertices.
# For robustness, I'll create a plane, subdivide, and use proportional editing to get a fan shape.
bpy.ops.mesh.primitive_plane_add(size=1, location=(0, 0, 2.2))
dorsal_fin = bpy.context.active_object
dorsal_fin.name = "Dorsal_Fin"
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.subdivide(number_cuts=4)
# Select all vertices
bpy.ops.mesh.select_all(action='SELECT')
# Scale along Y to make it wider
bpy.ops.transform.resize(value=(1, 2, 1))
# Now shape: we want a fin that is curved and tapers to tip.
# Move the farthest Y vertices up (Z) to create a fin.
# Use proportional editing with falloff.
bpy.ops.transform.translate(value=(0, 0, 0.8), constraint_axis=(False, False, True), orient_type='GLOBAL')
# Exit edit mode
bpy.ops.object.mode_set(mode='OBJECT')
# Add subdivision
mod = dorsal_fin.modifiers.new(name="Subsurf", type='SUBSURF')
mod.levels = 2
bpy.ops.object.modifier_apply(modifier="Subsurf")
# Rotate to orient correctly? Already plane is in XY, we want it standing up in Z? Actually we want it to be vertical, so we need to rotate it to be in YZ or XZ? Typically dorsal fin is vertical, attached along the back. We'll rotate it so its plane is XZ (vertical). Rotate around X by 90 degrees.
dorsal_fin.rotation_euler = (math.radians(90), 0, 0)
bpy.ops.object.transform_apply(rotation=True)
# Now it's standing up. But we need it to be attached to the back at the top of the body. The fin's center is at (0,0,2.2). That's fine.
# Add material
mat_dorsal = create_fin_material("Dorsal_Fin_Mat", base_color=(1.0, 0.45, 0.0, 1.0), transparent_edges=True)
dorsal_fin.data.materials.append(mat_dorsal)

# Pectoral fins (side fins): two small fins on sides.
# Create a plane, shape, duplicate.
bpy.ops.mesh.primitive_plane_add(size=0.5, location=(1.0, 0.8, 0.5))
fin_side = bpy.context.active_object
fin_side.name = "Pectoral_Fin_Right"
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.subdivide(number_cuts=2)
# Scale and shape
bpy.ops.transform.resize(value=(1.5, 1, 1))
# Move some vertices to create a leaf shape
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.transform.translate(value=(0, 0, 0.3), orient_type='GLOBAL')
bpy.ops.object.mode_set(mode='OBJECT')
# Add subsurf
mod = fin_side.modifiers.new(name="Subsurf", type='SUBSURF')
mod.levels = 2
bpy.ops.object.modifier_apply(modifier="Subsurf")
# Rotate to angle
fin_side.rotation_euler = (0, 0, math.radians(30))
bpy.ops.object.transform_apply(rotation=True)
# Material
mat_side = create_fin_material("Side_Fin_Mat", base_color=(1.0, 0.5, 0.0, 1.0), transparent_edges=True)
fin_side.data.materials.append(mat_side)
# Duplicate for left side
bpy.ops.object.duplicate_move_linked(OBJECT='Pectoral_Fin_Right')
fin_left = bpy.context.active_object
fin_left.name = "Pectoral_Fin_Left"
fin_left.location.x = -1.0  # opposite side
fin_left.scale.x = -1  # mirror? but careful with normals. better set scale negative.
# Actually mirror by scaling X by -1
fin_left.scale.x = -1.0
bpy.ops.object.transform_apply(scale=True)
# Adjust rotation
fin_left.rotation_euler.z = math.radians(-30)  # opposite angle
bpy.ops.object.transform_apply(rotation=True)

# Eyes: two small spheres on sides of head.
bpy.ops.mesh.primitive_uv_sphere_add(segments=16, ring_count=8, radius=0.2, location=(2.2, 0.6, 0.6))
eye_right = bpy.context.active_object
eye_right.name = "Eye_Right"
# Eye material: white sclera, black pupil
mat_eye = bpy.data.materials.new("Eye_Mat")
mat_eye.use_nodes = True
nodes = mat_eye.node_tree.nodes
links = mat_eye.node_tree.links
nodes.clear()
out = nodes.new(type='ShaderNodeOutputMaterial')
diff = nodes.new(type='ShaderNodeBsdfDiffuse')
diff.inputs['Color'].default_value = (1,1,1,1)
links.new(diff.outputs['BSDF'], out.inputs['Surface'])
eye_right.data.materials.append(mat_eye)
# Pupil: add a smaller black sphere inside
bpy.ops.mesh.primitive_uv_sphere_add(segments=12, ring_count=6, radius=0.1, location=(2.2, 0.6, 0.6))
pupil_right = bpy.context.active_object
pupil_right.name = "Pupil_Right"
pupil_right.scale = (0.5, 1, 1)  # slightly oval
mat_pupil = bpy.data.materials.new("Pupil_Mat")
mat_pupil.use_nodes = True
nodes = mat_pupil.node_tree.nodes
nodes.clear()
out = nodes.new(type='ShaderNodeOutputMaterial')
diff = nodes.new(type='ShaderNodeBsdfDiffuse')
diff.inputs['Color'].default_value = (0,0,0,1)
links.new(diff.outputs['BSDF'], out.inputs['Surface'])
pupil_right.data.materials.append(mat_pupil)
# Duplicate for left eye
bpy.ops.object.duplicate_move_linked(OBJECT='Eye_Right')
eye_left = bpy.context.active_object
eye_left.name = "Eye_Left"
eye_left.location.x = -2.2
bpy.ops.object.duplicate_move_linked(OBJECT='Pupil_Right')
pupil_left = bpy.context.active_object
pupil_left.name = "Pupil_Left"
pupil_left.location.x = -2.2

# ------------------------------------------------------------
# Apply goldfish body material
# ------------------------------------------------------------
mat_body = create_goldfish_material("Goldfish_Body_Mat", color=(1.0, 0.55, 0.0, 1.0), with_scales=True)
if fish_body.data.materials:
    fish_body.data.materials[0] = mat_body
else:
    fish_body.data.materials.append(mat_body)

# ------------------------------------------------------------
# Floor: a circle that any child would name instantly
# ------------------------------------------------------------
bpy.ops.mesh.primitive_circle_add(vertices=64, radius=5, fill_type='TRIFAN', location=(0,0,-2))
floor = bpy.context.active_object
floor.name = "Floor"
floor.rotation_euler = (0, 0, 0)
# Material: light blue gradient or something
mat_floor = bpy.data.materials.new("Floor_Mat")
mat_floor.use_nodes = True
nodes = mat_floor.node_tree.nodes
links = mat_floor.node_tree.links
nodes.clear()
out = nodes.new(type='ShaderNodeOutputMaterial')
diff = nodes.new(type='ShaderNodeBsdfDiffuse')
diff.inputs['Color'].default_value = (0.2, 0.5, 0.8, 1)
links.new(diff.outputs['BSDF'], out.inputs['Surface'])
floor.data.materials.append(mat_floor)

# ------------------------------------------------------------
# Background: world gradient for calm watery backdrop
# ------------------------------------------------------------
world = bpy.data.worlds.new('World')
world.use_nodes = True
nodes = world.node_tree.nodes
links = world.node_tree.links
nodes.clear()
out_world = nodes.new(type='ShaderNodeOutputWorld')
bg = nodes.new(type='ShaderNodeBackground')
bg.inputs['Strength'].default_value = 1.0
links.new(bg.outputs['Background'], out_world.inputs['Surface'])

# Gradient texture
grad = nodes.new(type='ShaderNodeTexGradient')
grad.gradient_type = 'LINEAR'
coord = nodes.new(type='ShaderNodeTexCoord')
links.new(coord.outputs['Generated'], grad.inputs['Vector'])
# Map range to color from light blue to darker blue
color_ramp = nodes.new(type='ShaderNodeValToRGB')
color_ramp.color_ramp.elements[0].color = (0.3, 0.6, 0.9, 1.0)  # light sky
color_ramp.color_ramp.elements[0].position = 0.0
color_ramp.color_ramp.elements[1].color = (0.1, 0.2, 0.4, 1.0)  # deep water
color_ramp.color_ramp.elements[1].position = 1.0
links.new(grad.outputs['Fac'], color_ramp.inputs['Fac'])
links.new(color_ramp.outputs['Color'], bg.inputs['Color'])

# Assign to scene
bpy.context.scene.world = world

# ------------------------------------------------------------
# Lighting
# ------------------------------------------------------------
# Key light
bpy.ops.object.light_add(type='AREA', location=(4, 3, 6))
key_light = bpy.context.active_object
key_light.name = "Key_Light"
key_light.data.energy = 200
key_light.data.size = 2
# Backlight/rim light
bpy.ops.object.light_add(type='AREA', location=(-3, -2, 4))
rim_light = bpy.context.active_object
rim_light.name = "Rim_Light"
rim_light.data.energy = 100
rim_light.data.size = 2

# Fill light
bpy.ops.object.light_add(type='AREA', location=(0, -4, 3))
fill_light = bpy.context.active_object
fill_light.name = "Fill_Light"
fill_light.data.energy = 50
fill_light.data.size = 2

# ------------------------------------------------------------
# Camera
# ------------------------------------------------------------
bpy.ops.object.camera_add(location=(4, -5, 3))
camera = bpy.context.active_object
camera.name = "Main_Camera"
camera.rotation_euler = (math.radians(60), 0, math.radians(45))
bpy.context.scene.camera = camera

# ------------------------------------------------------------
# Group all fish parts into a collection for easy manipulation
# ------------------------------------------------------------
fish_collection = bpy.data.collections.new("Goldfish")
bpy.context.scene.collection.children.link(fish_collection)
# Move all fish parts to this collection
for obj in [fish_body, tail_fin, dorsal_fin, fin_side, fin_left, eye_right, eye_left, pupil_right, pupil_left]:
    if obj.name in bpy.context.scene.collection.objects:
        bpy.context.scene.collection.objects.unlink(obj)
    fish_collection.objects.link(obj)

# ------------------------------------------------------------
# Final viewport adjustments (optional)
# ------------------------------------------------------------
# Set camera to see the fish in a nice composition
# The fish is centered around origin. Camera looks at origin.
# Add an empty as target for camera to track? Not necessary.

# Ensure all objects are visible
bpy.context.view_layer.update()

print("Script completed successfully.")
```