Home                   Reel                  Pulse (Senior Thesis Film)                 Projects & Breakdown                 Resume                 Contact
Fish









  





Creative Goal

This sequence introduces the opening of PULSE through underwater motion, fish behavior, and a glowing energy sphere.
Inspired by the quiet and mysterious atmosphere of the natural underwater world, my goal was to create a calm yet charged opening that slowly draws the viewer into the rhythm of the film.








Pre-Production


Reference Keywords
:  Schooling Fish  /  Energy Orb  /  Underwater Light Rays  /  Underwater Bubbles  /  Beat-Driven Motion





Previs






The previs stage helped define the camera framing and shot flow for each moment,
ensuring that the final renders captured the intended rhythm, atmosphere, and key reactions around the energy sphere at the right moments.











Previs to Final Render







Production

Overall OBJ Node set up
*All setups in this scene were built by me. The node workflow was custom-made for this project.



















Shot1
















Basically, the simulation network inside each shot follows the structure shown above.

1. magical particles inside the sphere | 2. the fish school simulation | 3. and bubbles emitted from the sphere.

First, I’ll  break down the particles inside the sphere.


















The key technical point of this particle simulation was creating a custom-designed velocity field first,
then using that velocity volume to drive the particles inside the POP Network.
This was done using *POP Advect by Volumes.

Since the timing of the particle motion needed to match a specific beat in the music,
I had to custom-remap the source velocity before running the volume simulation.


















Here, the source velocity values are remapped to match the specific frames I set.














Next, this is the internal POP Network setup.
It connects the custom velocity field, the animated emitter particles,
and the sphere surface used as the collision object for the simulation.
The key points here are bringing in the external velocity volume through *POP Advect by Volumes,
Separating the particles that make contact with the collision object.
And for those particles, I used *POP Wind and *POP Drag to create a delayed, suspended motion.



















Next is the post-processing stage for the cached particles.
The main points in this stage are remapping the color and alpha based on nage for rendering,
and setting the pscale for the final particle size.










The next simulation is the flock of fish.
















Let’s look at this setup through Fish Group 01.
For this part, I created the simulation for the instancing points customly with a SOP Solver, rather than using a POP Network.
This setup required a lot of VEX code, mathematical thinking, and some basic linear algebra knowledge, such as using the cross product.
Before going into the fish instancing setup itself, I will first explain the instancing points.















The logic behind this custom SOP Solver setup is simple:
particles emitted from the source follow an animated target while avoiding the collision object at the same time.
For the external SOP Solver settings, the initial state needs to be turned off.
This is because the solver only needs to bring in the initial input once.













the source points need to follow the target.
If I use the desired velocity directly toward the target, the motion feels too artificial,
almost like the points are instantly snapping toward the goal direction.
Instead, I wanted the particles to keep their current momentum while gradually steering toward the target in a more natural way.
For that, I used a steering force: steering force = desired velocity - current velocity
This defines the force needed to turn the current motion toward the desired direction.






 
 









So, using the source point position @P as the current location and the target point position target_pos,
I first created the desired vector. Then, to control the speed of that vector within the range I wanted,
I extracted its length, clamped it, and multiplied it back by the normalized desired direction.
This gives me the final steering force by subtracting the current velocity from the desired velocity:

steering force = desired velocity - current velocity

To make the fish school feel more natural, I also needed variation in the steering speed.
Some points should follow the target faster, while others should move more slowly.

















The issue here is that @ptnum / float(@numpt - 1) creates a fixed normalized value for each point,
so the ramp lookup does not change over time. As a result, a point assigned a faster value will always remain fast,
and a slower point will always remain slow.

To solve this, I added time variation to shift the ramp lookup position over time.
In other words, by adding a time-based offset to the random value,
the strength of the steering force changes naturally throughout the simulation.

Finally, I used a steering force scale parameter to control the overall movement style.
A lower scale makes the fish move in a wider, more indirect path,
while a higher scale makes them follow the target more tightly and directly.

Next, I added a clustering force to pull the fish school toward the center of its bounding box.





















To do this, I created a vector by subtracting each point’s current position from the center of the bounding box,
then added that direction back into the point motion.

The issue is that each point has a different distance from the center.
So, to prevent farther points from moving faster, I normalized the force before applying it.
This way, the fish can cluster toward the center at a more consistent speed.

Next, I added turbulent noise to create more variation in the motion.

















I used curl noise because it creates a flow-like motion where vectors naturally swirl around each other
instead of moving in a straight, uniform direction. This helped give the fish movement a more fluid and organic feel.














As you can see, the points now maintain a reasonable cluster while still showing noisy, varied motion.
However, the velocity of each point can accumulate over time and become too fast.
To prevent that, I added a drag_velocity step to slow down and dampen the motion.
















By combining this drag step with weaker turbulent noise and a lower clustering force,
the overall motion becomes looser and more natural. Next, I added a point-to-point avoidance behavior.
The basic idea is to generate a repulsion force whenever particles get closer than a certain distance from each other.







 










I searched for nearby points around point A, then calculated the direction from each neighbor back toward A.
After adding and normalizing those vectors, I applied the result directly to @P as a repulsion offset.
This allowed the points to push away from each other when they became too close.























I connected the randomized radius value to pscale to vary the size of each instance.
Then, using a search radius proportional to each point’s size, I applied a repulsion offset to push nearby points away and prevent overlapping.













As shown in the vector visualization, when the spheres get close to each other, you can see an outward repulsion force being applied.









Next, I created an avoiding force in VEX so the fish could move around the central sphere instead of passing through it.
The sphere was connected as the third input and used as the collision object.

For each point, I measured its distance from the sphere’s surface.
Then, I used the normal direction of the closest surface point to push the fish away from the sphere.

The closer a point gets to the surface, the stronger the avoiding force becomes.
I controlled this strength with a ramp, then clamped the final force to keep the motion within a natural speed range.














To correct any penetration that the avoiding force could not fully prevent,
I added a post-solve step to reposition the points outside the collision object.








Using the closest surface position and normal, I used a dot product to detect
whether any points had penetrated inside the collision object.

If penetration was detected, I pushed the points back out along the surface normal
with a small distance-based offset to keep the collision behavior stable.

At this point, the basic custom solver setup was complete.
Finally, I instanced the fish using Copy to Points and set the @up attribute for orientation.












When I calculated the orientation using a fixed global up vector {0,1,0},
the side vector could suddenly flip when the velocity direction changed drastically.

This caused the fish orientation to pop or rotate unnaturally.

To solve this, I used dihedral() to calculate the rotation between the previous velocity direction and the current velocity direction.
This allowed the up vector to smoothly follow the fish’s motion instead of being recalculated from a fixed global direction.
















More specifically, I calculated the rotation change between the previous velocity and the current velocity,
then allowed the up vector to gradually follow that change.
I used dihedral() to create the rotation based on the velocity change,
while also calculating a correction rotation toward the global up direction.

Finally, I blended those two rotations with slerp() and applied the result with qrotate().
This kept the fish turning naturally while preventing excessive twisting.
And with that, the three fish movement setups were complete.



















Now, for the last of the three simulations, I will talk about the bubbles emitting from the sphere.














Before running the source volume simulation, I custom-set the volume source and key attributes to create the desired volume shape.

Since the volume needed to start from the sphere and spread outward,
I set the velocity direction outward to define the main flow of the simulation.
I also applied animated noise to Cd and used it as a mask for threshold-based deletion,
creating a less uniform source density and distribution.

Finally, to prevent volume from being generated inside the sphere,
I used a dot product in VEX to remove points located in the inner area.
This cleaned up unnecessary internal sources and made the outward direction and shape of the volume clearer.













After that, I used Volume Rasterize and a simple Pyro setup to complete the volume simulation
that would become the base for the bubble geometry.











After that, I refined the shape using nodes like *VDB Smooth, then converted the volume into polygons.

The main issue here was motion blur.
Since the geometry was generated from a volume source, the points did not have proper v values.

To solve this, I ran an additional POP Network simulation and transferred
the velocity back onto the geometry, which allowed the motion blur to work properly.














This bubble simulation is built with three layers:
(1) the main bubbles shown here,
(2) the mid-size bubbles that follow the main bubble motion,
(3) and a separate small-bubble setup timed to rise together with the main bubbles.

For the small bubbles, I did not use a separate volume simulation.
Instead, I created the base motion with only a particle simulation,
then instanced the bubble geometry onto those points.














This completes the simulation stage.
Next, I will go over the lighting and render setup, then move on to the post-production pipeline.



















Back in the OBJ view, you can see that I kept the lighting setup as simple as possible,
using only an environment light and a top light.

Since the main visual focus was the glow from the particle simulation, I kept the environment light fairly low.
Then, I placed a point light above the scene to create the feeling of sunlight coming from the water surface,
with a soft gradient fading downward.

I also tested a few different ways to create dramatic light rays.
As you can see in the final render of Shot 03, the fish naturally blocked parts of the light,
creating a nice contrast between the occluded and non-occluded areas.
This produced a ray-like effect even without heavy extra settings.


Later in Nuke, I used the Volume Rays node to increase the density of the ray layer.












This is my Karma setup.

I usually prefer to separate each shot into its own scene file.
However, because the thesis schedule was very tight and these three shots were connected,
I needed to keep comparing them and adjusting the render settings together.

So toward the end, I managed the three shots in one file and only saved them out separately when sending them to the render farm.








To control each element separately in compositing, I created custom ID attributes
in Houdini and exported them as separate AOV passes through Karma Render Vars.
Then, in Nuke, I used those passes to isolate the fish areas and apply additional color correction and compositing adjustments.












Post Production












The top image shows the raw rendered EXR, and the bottom image shows the final comp.
The main focus in this compositing stage was creating atmosphere with the rays and adding depth through the fish grade.


















The comp stage played a huge role in creating the underwater atmosphere and bringing out the particle glow.
For this process, I needed several additional render layers beyond the main combined render.
Because of that, I spent a lot of time working with AOVs and Karma render settings.


















Finally, I did the color grading in DaVinci Resolve.
PULSE was my first time using the software, and through this process I realized that
color grading also needs clear references and planning from the pre-production stage.

Because the film had several different scenes, each with different backgrounds and lighting setups,
it was difficult to find one consistent grading direction at first.

In the end, I decided to connect the scenes with a saturated, cinematic look.
I also added a subtle bluish tone across the film to support the overall mood of the music.
















And here are the three final renders!

Please give me a small round of applause, even if it is just in your heart.
This was honestly not easy, especially because so many parts of this process were new to me.
But at the same time, I really enjoyed it.

The biggest technical highlight of this scene was building the fish flocking system with a custom SOP Solver.

Before this project, I had mostly worked with more specific solvers like POP and Pyro.
So I wanted to take this chance to better understand the basic logic behind a solver itself.

At first, I was using the word “solver” without fully understanding what the actual solving process meant.
But as I kept breaking it down step by step, it became one of the most interesting parts of the project.

I also really enjoyed using mathematical concepts like
dot product, cross product, and quaternions directly in VEX to solve visual problems.
It felt like solving a math problem, but in a very visual and creative way.

If I had more time, I would probably add more background bubbles to give the scene even more depth.
But for now, this wraps up the fish scene!!!