A GPU-accelerated 3D neural network fish tank simulation where fish evolve to graze on thousands of food particles using field-of-view based density sensors. Built with Taichi for high-performance physics and Pygame for 3D rendering.
![]() |
![]() |
![]() |
|---|---|---|
| Normal Mode | Density Mode | Flow Mode |
What is this simulating?
An ecosystem where fish must survive by efficiently grazing on a dense cloud of 10,000+ food particles. Each fish is controlled by a neural network that evolves through natural selection to optimize foraging behavior.
- Overview
- Features
- Installation
- Quick Start
- How It Works
- Visualization Modes
- Training System
- File Structure
- Configuration
- Controls
- Technical Details
This simulation models an ecosystem where fish must survive by grazing on a dense cloud of 10,000+ tiny food particles. Each fish is controlled by a neural network that processes sensory information about food density in its field of view. Through evolutionary training, fish learn to efficiently navigate and feed in this environment.
- GPU-Accelerated Physics: Uses Taichi to handle collision detection and sensor calculations for thousands of entities at 60 FPS
- Field-of-View Sensing: Fish detect food density in 5 sectors within a 120° cone, mimicking realistic vision
- Metabolic Simulation: Hunger increases based on fish size and speed, creating survival pressure
- Multiple Visualization Modes: View the simulation as normal 3D graphics, density heatmaps, or velocity flow fields
- Offline Training: Train fish at maximum speed without rendering, then load trained models into the live simulation
- 10,000 Food Particles: Tiny, low-nutrition droplets that respawn after being eaten
- 150 Fish: Neural network-controlled agents with genetic variation
- Dynamic Hunger: Larger and faster fish get hungry more quickly
- Reproduction: Fish reproduce when they have sufficient energy
- Evolution: Genetic algorithms select and mutate successful fish
- Normal Mode: Fish as colored 3D triangles, food as white particles
- Density Mode: Volumetric gas-like heatmap showing food distribution
- Flow Mode: Fish rendered as velocity vectors showing movement patterns
- Live Graphs: Real-time hunger and consumption rate tracking
- 3D Camera: Free-flying camera with mouse and keyboard controls
- Headless Training: Run simulation at maximum speed without rendering
- Model Persistence: Save and load trained neural networks
- Training Logs: JSON logs and PNG plots of fitness progression
- Live Loading: Load trained models into running simulation
- Python 3.8+
- CUDA-capable GPU (recommended) or CPU fallback
pip install taichi pygame numpy matplotlibpython bottom_feeder.pypython train_feeder_offline.pyThis will run 50 epochs of training and save models to trained_models/.
- Run
python bottom_feeder.py - Press L to load the latest trained model
- Watch the trained fish perform!
The simulation is built on a modular architecture with clear separation of concerns:
┌─────────────────┐
│ bottom_feeder │ Main loop, input handling
└────────┬────────┘
│
┌────▼────────────────────────────┐
│ feeder_world.py │
│ Simulation State Management │
│ - Fish population │
│ - Food respawning │
│ - Reproduction logic │
└────┬────────────────────────────┘
│
┌────▼────────────────────────────┐
│ feeder_physics.py (Taichi) │
│ GPU-Accelerated Calculations │
│ - Sensor updates │
│ - Collision detection │
│ - Density grid computation │
└────┬────────────────────────────┘
│
┌────▼────────────────────────────┐
│ feeder_entities.py │
│ Fish & Food Classes │
│ - Neural network forward pass │
│ - Movement physics │
│ - Hunger/energy mechanics │
└─────────────────────────────────┘
Each fish has a feedforward neural network with:
- 5 Density Sensors: Food density in FOV sectors (Far Left, Left, Center, Right, Far Right)
- Normalized Speed: Current speed / max speed
- Normalized Hunger: Current hunger / max hunger
- Normalized Size: Current size / max size
- Normalized Energy: Current energy / typical max
- Previous Yaw Rate: Last frame's turning rate (horizontal)
- Previous Pitch Rate: Last frame's turning rate (vertical)
- Bias: Always 1.0
Hidden Layer
- 8 neurons with tanh activation
- Target Yaw Rate: Desired horizontal turning speed (-1 to 1)
- Target Pitch Rate: Desired vertical turning speed (-1 to 1)
- Target Speed: Desired swimming speed (0 to 1)
Input Layer (13) → Hidden Layer (8) → Output Layer (3)
[tanh activation]
The network weights are the fish's "genetics" and are:
- Initialized randomly for new fish
- Inherited with mutations during reproduction
- Evolved through selection pressure (fitness = food eaten)
The simulation uses three main GPU kernels:
1. update_sensors()
- Iterates through all food particles for each fish
- Calculates angle between fish's forward vector and food direction
- Determines which FOV sector the food falls into
- Accumulates density values weighted by distance
# Pseudocode
for each fish:
for each food particle:
if particle in sensor range:
angle = acos(forward_vector · direction_to_food)
if angle < FOV_half_angle:
sector = map_angle_to_sector(angle)
density = (SENSOR_RANGE - distance) / SENSOR_RANGE
fish_sensors[fish_id][sector] += density2. check_collisions()
- Checks if food particles are within fish eating radius
- Marks eaten food as inactive
- Increments fish eaten counter for fitness tracking
3. update_density_grid()
- Maps food positions to a 30×30×30 grid
- Counts food particles per cell
- Used for density visualization mode
Fish have a 120° forward-facing cone divided into 5 sectors:
Far Left Left Center Right Far Right
◢ ◢ ▲ ◣ ◣
◢ ◢ │ ◣ ◣
◢ ◢ │ ◣ ◣
◢──────◢───┼───◣──────◣
120° FOV Cone
│
FISH
This mimics realistic vision where fish can only see what's in front of them, creating more interesting foraging behavior.
Hunger increases each frame based on:
hunger_rate = HUNGER_BASE_RATE + size * HUNGER_SIZE_FACTOR + speed * HUNGER_SPEED_FACTOR- Base Rate: 0.5 per second
- Size Factor: 0.1 × size (larger fish need more food)
- Speed Factor: 0.02 × speed (faster swimming burns energy)
When hunger reaches 100, the fish dies.
The simulation uses a custom 3D camera with perspective projection:
- Camera Position: Calculated from spherical coordinates (distance, angle_x, angle_y)
- View Transformation: Convert world coordinates to camera space
- Perspective Division: Project 3D points to 2D screen coordinates
- Depth Sorting: Sort all drawable objects back-to-front for proper alpha blending
Each fish is rendered as a 3D triangle pointing in its direction of travel:
Nose (forward)
▲
╱ ╲
╱ ╲
╱ ╲
╱ ╲
╱─────────╲
Left Right
(base) (base)
Triangle Construction:
- Calculate fish's forward, right, and up vectors from yaw/pitch
- Nose: Position + forward × size × 1.2
- Left: Position + (backward × 0.4 + right × 0.7) × size
- Right: Position + (backward × 0.4 - right × 0.7) × size
- Project all three points to screen space
- Draw filled polygon with color based on neural network weights
Color Encoding: Fish color is generated from their neural network weights using a hash function, so genetically similar fish have similar colors.
Press P to cycle through three different visualization modes, each revealing different aspects of the simulation:
What you see: Standard 3D view with fish as colored triangles and food as white particles.
Purpose: Watch the simulation as it naturally appears, observing fish behavior and population dynamics.
Visual Elements:
- Fish: Rendered as 3D triangles pointing in their direction of travel
- Triangle color is generated from neural network weights (genetics)
- Genetically similar fish have similar colors
- Size represents fish size (larger fish = larger triangles)
- Food: 10,000 white particles scattered throughout the world
- Particles fade based on distance from camera
- Eaten food disappears and respawns after 5 seconds
- World Boundaries: Wireframe box showing simulation limits
- Live Graphs (toggle with G):
- Average hunger across all fish
- Food consumption rate (smoothed)
How Fish Rendering Works: Each fish is drawn as a 3D triangle constructed from three points:
- Nose: Position + forward vector × size × 1.2
- Left Wing: Position + (backward + right) × size × 0.7
- Right Wing: Position + (backward - right) × size × 0.7
The triangle is projected to 2D screen space and drawn with depth-based fading.
What you see: Volumetric gas-like heatmap showing food distribution in 3D space.
Purpose: Understand where food is concentrated and how fish create "paths" by eating.
Visual Elements:
- Color Gradient: Blue (sparse) → Cyan → Green → Yellow → Red (dense)
- Smooth Clouds: Overlapping semi-transparent spheres create a continuous gas-like appearance
- No Fish: Fish are hidden to focus on food distribution
- No Individual Food: Particles are aggregated into a density field
How It Works:
- Grid Mapping: The world is divided into a 30×30×30 grid (27,000 cells)
- Food Counting: Each food particle is mapped to its grid cell
- Density Calculation: Count how many food particles are in each cell
- Visualization:
- Only cells with food are rendered
- Each cell is drawn as a soft, multi-layered cloud
- Cloud size and opacity scale with food density
- Clouds overlap and blend to create smooth gradients
- Color Mapping:
- Intensity = cell_count / max_count
- Color transitions through spectrum based on intensity
Performance: Optimized to only process ~100-500 active cells instead of all 27,000.
Use Cases:
- Identify food clustering patterns
- See how fish "carve paths" through the food cloud
- Understand spatial distribution of resources
- Observe how food respawning affects density over time
What you see: Hierarchical tree structure showing fish movement flow from fine details to main trunks.
Purpose: Analyze collective movement patterns at multiple scales, from local details to global trends.
Visual Elements:
- Hierarchical Branches: Connected lines forming a tree-like structure
- Color Gradient: Blue (fine branches) → Cyan → Green → Yellow → Red (main trunks)
- Line Thickness: Thicker lines = more accumulated fish flow
- Brightness: Brighter = stronger flow relative to sibling branches
- 4 Hierarchical Levels: From finest (16×16×16) to coarsest (2×2×2 = 4 main trunks)
- No Individual Fish: Fish are hidden to show aggregated patterns
How It Works:
-
Multi-Scale Grid Hierarchy (4 levels):
- Level 1 (16×16×16): Finest branches - minimum 2 fish required
- Level 2 (8×8×8): Medium branches - minimum 4 fish required
- Level 3 (4×4×4): Large branches - minimum 8 fish required
- Level 4 (2×2×2): Main trunks - minimum 15 fish required (4 total trunks)
-
Velocity Accumulation: For each grid level:
- Map each fish to its grid cell
- Calculate velocity vector (vx, vy, vz) from yaw, pitch, and speed
- Accumulate velocities in each cell
- Average the velocities:
avg_velocity = sum_velocities / fish_count
-
Temporal Smoothing (larger branches move slower):
- Finest branches (smooth = 0.1): Keep 10% old + 90% new → Very responsive
- Medium branches (smooth = 0.3): Keep 30% old + 70% new → Moderate
- Large branches (smooth = 0.6): Keep 60% old + 40% new → Slower
- Main trunks (smooth = 0.85): Keep 85% old + 15% new → Very slow, stable
- Empty cells decay over time to prevent ghost branches
-
Hierarchical Connection:
- Fine branches connect to their parent cell in the next coarser level
- Lines drawn from child cell center → parent cell center
- Creates a true tree structure where branches merge into trunks
- Main trunks (level 4) extend in their flow direction
-
Relative Flow Strength:
- For each cell, find all sibling cells (cells with the same parent)
- Calculate:
relative_strength = this_cell_count / max_sibling_count - Adjust brightness:
40% to 100%based on relative strength - Bright branches = dominant flow direction
- Dim branches = weaker, secondary flow
-
Rendering:
- Coarse levels (dark, thick) drawn first (background)
- Fine levels (light, thin) drawn on top (foreground)
- Line thickness increases with level and fish count
- Color gradient shows hierarchy level
- Brightness shows relative importance within siblings
What It Means:
-
Blue/Cyan branches: Local, detailed movement patterns - update quickly
-
Green branches: Regional aggregated flow - moderate stability
-
Yellow/Red trunks: Overall directional trends - very stable
-
Bright lines: Strong flow in that direction (many fish)
-
Dim lines: Weak flow in that direction (fewer fish)
-
Thick lines: High accumulated fish count
-
Thin lines: Low accumulated fish count
Use Cases:
- Identify overall migration patterns (red/yellow trunks)
- See how local movements (blue branches) feed into global trends
- Compare flow strength between different regions
- Understand multi-scale collective behavior
- Observe how fish respond to food distribution at different scales
Advantages:
- Multi-Scale Analysis: See both details and big picture simultaneously
- Clear Hierarchy: Tree structure shows how local flows aggregate
- Temporal Stability: Larger branches move slowly, easier to track trends
- Flow Strength: Brightness immediately shows dominant vs. minor flows
- No Clutter: Aggregation reduces 150 fish to ~50-100 meaningful lines
Press P to cycle: Normal → Density → Flow → Normal
Each mode updates in real-time, so you can switch mid-simulation to analyze different aspects of the same moment.
The train_feeder_offline.py script runs a genetic algorithm:
For each epoch (1-50):
1. Reset all fish (age=0, hunger=0, energy=20)
2. Run simulation for 30 seconds
3. Calculate fitness for each fish (fitness = food eaten)
4. Select top 20% of fish as "survivors"
5. Repopulate to 150 fish by cloning survivors with mutations
6. Save best fish neural network to trained_models/
7. Log statistics to training_logs/
fitness = total_food_eatenSimple but effective: fish that eat more food survive and reproduce.
When creating offspring:
for each weight in neural_network:
if random() < mutation_probability (0.05):
weight += random_normal(0, mutation_strength=0.1)The training system now tracks 18+ metrics across 8 comprehensive graphs to provide deep insight into evolutionary progress:
-
Fitness Evolution:
- Mean (Green): Average food eaten. Increases as fish learn to hunt.
- Median (Orange): The "middle" fish. If Mean > Median, a few "super-fish" are skewing the average.
- Max (Blue): The value of the absolute best fish.
- Shaded Area: Standard deviation (population diversity).
-
Population Food Consumption:
- Tracks total food eaten by the entire population per epoch.
- A rising curve means the ecosystem as a whole is becoming more efficient at extracting resources.
-
Fitness Distribution:
- compares Top 10% vs Bottom 10% performance.
- Large gap = high inequality (some geniuses, some idiots).
- Small gap = converged population (everyone acts similarly).
-
Population Size:
- Tracks number of surviving fish against the target (150).
- In early training, this might dip if fish are too stupid to eat.
- In late training, it should stay stable at 150.
-
Behavior (Speed vs Hunger):
- Speed (Blue): Are fish evolving to swim fast?
- Hunger (Red): Are they constantly starving?
- Correlation: Fast swimming burns energy → increases hunger. Evolution tries to find the sweet spot.
-
Energy Levels:
- Green Line (40): Reproduction threshold.
- If Average Energy (Yellow) is below this line, the population is struggling to reproduce.
-
Neural Network Weights:
- Mean Weight (Purple): Shifts in average brain connections.
- Stability: If the curve flattens, the "brain structure" has stabilized (converged).
-
Evolution Dynamics:
- Weight Change (Purple): How much the brain changes each generation. High = searching for solutions. Low = optimizing.
- Diversity (Red): How different the fish are from each other. Zero = clones.
In the live simulation:
- Press L
- Latest model from
trained_models/is loaded - Population is replaced with 150 clones of the trained fish
- Each clone has slight mutations for diversity
3D_Bottom_feeder/
│
├── bottom_feeder.py # Main entry point, game loop
├── feeder_constants.py # All simulation parameters
├── feeder_physics.py # Taichi GPU kernels
├── feeder_entities.py # Fish and Food classes
├── feeder_world.py # Simulation state management
├── feeder_renderer.py # 3D rendering and visualization
├── feeder_logger.py # Training statistics logging
├── train_feeder_offline.py # Headless training script
│
├── trained_models/ # Saved neural networks (.pkl)
├── training_logs/ # Training statistics (.json, .png)
│
└── Archieve/ # Original simulation (reference)
├── neural_network.py # Neural network implementation
├── utils.py # Helper functions
└── ...
- Initializes Pygame and Taichi
- Main game loop (input → update → render)
- Handles keyboard/mouse input
- Manages visualization mode switching
- Collects statistics for graphs
- All tunable parameters in one place
- World dimensions, fish counts, food settings
- Hunger rates, reproduction thresholds
- Camera settings, visualization modes
@ti.data_orientedclass with Taichi fields- GPU kernels for parallel computation
- Handles 10,000+ food × 150 fish interactions per frame
FeederFish: Position, velocity, neural network, hunger, energyFeederFood: Position, active state, respawn timer- Fish update logic (neural network forward pass, movement)
- Manages fish and food populations
- Synchronizes Python data ↔ Taichi fields
- Handles reproduction and food respawning
- Save/load simulation state
Camera3D: 3D perspective projectiondraw_feeder_world(): Main rendering dispatcher- Mode-specific rendering functions
- Graph and HUD drawing
Edit feeder_constants.py to customize:
WORLD_WIDTH = 2000
WORLD_HEIGHT = 2000
WORLD_DEPTH = 2000INITIAL_FISH_COUNT = 150
MAX_FOOD = 10000HUNGER_BASE_RATE = 0.5 # Lower = fish survive longer
REPRO_MIN_ENERGY = 40.0 # Lower = easier reproduction
FOV_ANGLE = 120.0 # Degrees of vision
SENSOR_RANGE = 400.0 # How far fish can sense foodDENSITY_GRID_RES = 30 # Higher = finer density grid (slower)
VIS_MODE_NORMAL = 0
VIS_MODE_DENSITY = 1
VIS_MODE_FLOW = 2- Mouse Drag: Rotate camera view
- W/A/S/D: Move camera forward/left/back/right
- Q/E: Move camera down/up
- R/F: Zoom in/out
- Shift: Hold for faster camera movement
- P: Cycle visualization modes (Normal → Density → Flow)
- G: Toggle graphs on/off
- F5: Save current simulation state
- F6: Load latest saved state
- L: Load latest trained model
- ESC: Quit
- Taichi GPU Kernels: All physics runs on GPU in parallel
- Spatial Optimization: Only active food particles are processed
- Surface Caching: Rendered clouds are cached and reused
- Vectorized Projection: NumPy arrays for batch food projection
- Depth Sorting: Only visible objects are sorted
Python (CPU) Taichi (GPU)
───────────── ────────────
FeederFish.x,y,z ──sync→ fish_pos field
FeederFish.yaw,pitch ──sync→ fish_rot field
FeederFish.size ──sync→ fish_size field
──compute→ update_sensors()
──compute→ check_collisions()
fish_sensors field ←─read── Python
fish_eaten_count ←─read── Python
FeederFish.update() uses sensor data
FeederFish.fitness += eaten_count
- Taichi Fields: ~50 MB (10k food + 500 max fish)
- Pygame Surfaces: ~10-20 MB (cached clouds)
- Python Objects: ~5 MB (fish/food instances)
- Total: ~65-75 MB
- Taichi kernels: ~2-3ms
- Python fish updates: ~3-4ms
- Rendering (Normal): ~6-8ms
- Rendering (Density): ~8-12ms
- Rendering (Flow): ~4-6ms
- Implement spatial hashing for 100k+ food particles
- Add predator species
- Implement NEAT (NeuroEvolution of Augmenting Topologies)
- Multi-GPU support
- WebGL/Three.js web version
- VR support
This project is licensed under the Apache License 2.0.
See the LICENSE file for the full license text, or visit https://www.apache.org/licenses/LICENSE-2.0 for details.
- Taichi: GPU-accelerated computing framework
- Pygame: Graphics and input handling
- NumPy: Numerical computations
- Matplotlib: Training visualization
If you use this code in your research, please cite:
@software{bottom_feeder_2024,
title={3D Bottom Feeder Simulation},
author={Jason Hoford},
year={2025},
url={https://github.com/yourusername/3d-bottom-feeder}
}Made with ❤️ and Taichi




