Spite: Catharsis

This project ran for a month and a half, including a pre-production phase where we planned, iterated, and eventually built one of the most technically diverse game slices I’ve been part of. I worked across VFX, shaders, procedural tool creation, and animation scripting — and for the first time, I saw my work connect across systems in a production setting. It was also the project where I made my very first team-based VFX, which gave me a huge love for stylized feedback.


Here’s a breakdown of what I made and what I learned — both technically and personally:

💧 First VFX – Water Projectile

This was the first VFX I ever made on a team, and it honestly changed how I saw feedback in games. It’s a stylized water projectile that might look simple now, but it taught me a ton about how meshes, motion, and materials all come together.

I learned to layer meshes for motion, connect HLSL into material logic, and wrestle with UV seams and polar distortion (I got very stuck there at one point). I also tried Substance 3D Designer for the first time — I loved designing textures just for this effect. It felt like a proper production step, and it’s still a milestone for me.
🪨 Procedural Rubble Tool – Houdini RBD with File Pattern Automation

I wanted to build something that non-technical teammates could use — so I made this tool in Houdini to automatically take a folder of FBX files and turn them into falling rubble with physics.

The tool uses a filepattern node to pull in all the files (using $HIP/geo/myRubbleFolder/*.fbx), loops over them using For-Each by file path, and loads them into a file SOP with a detail() expression:
detail("../filepattern1", "filelist", @iteration, "string")

Each piece is then fractured with RBD Material Fracture and dropped into a simple gravity sim. The result? Just drop in a bunch of meshes and hit go.
🤖 FK Hand Tool – Maya Rigging Automation with Python

This tool was made to automate finger rigging in Maya. Instead of setting up each FK chain by hand, it builds controller hierarchies starting from any selected knuckle. It saved our team a ton of time and reduced errors in the rigging stage.

🧠 Technical Breakdown:
  • Uses Maya’s Python API to find joint hierarchies starting from selection
  • Automatically creates FK controllers and zeroed offset groups
  • Snaps each controller to its joint (position + rotation)
  • Applies parent constraints (maintain offset off)
  • Parents controllers hierarchically (clean FK chain)
  • Includes a custom UI so animators can launch it easily

📜 Full Script (click to expand):
View Python code
import maya.cmds as cmds

def create_fk_finger_from_knuckle(scale=1.0, ctrl_shape="cube"):
    selection = cmds.ls(selection=True, type='joint')
    if not selection:
        cmds.error("Please select a knuckle (joint) to start.")
        return

    knuckle = selection[0]
    joints = cmds.listRelatives(knuckle, allDescendents=True, type="joint")
    if not joints:
        cmds.error("No child joints found under the selected knuckle.")
        return

    joints.reverse()
    joints.insert(0, knuckle)
    joints.pop()

    controllers = []
    offset_groups = []

    for joint in joints:
        ctrl_name = f"{joint}_ctrl"
        ctrl = cmds.circle(name=ctrl_name, degree=1)[0]
        cmds.matchTransform(ctrl, joint, pos=True, rot=True)
        cmds.scale(scale, scale, scale, ctrl)
        cmds.rotate(0, "90deg", 0, ctrl)
        offset_grp = cmds.group(ctrl, name=f"{joint}_offset_grp")
        cmds.delete(cmds.parentConstraint(joint, offset_grp))
        cmds.makeIdentity(ctrl, apply=True, rotate=True, translate=True, scale=True)
        cmds.parentConstraint(ctrl, joint, mo=True)
        controllers.append(ctrl)
        offset_groups.append(offset_grp)

    for i in range(1, len(offset_groups)):
        cmds.parent(offset_groups[i], controllers[i - 1])

    if controllers:
        cmds.parentConstraint(controllers[0], knuckle, mo=True)

    print(f"FK setup complete starting from knuckle: {knuckle}")

class Window(object):
    def __init__(self):
        self.window="Window"
        self.title="FK Tool"
        self.size=(500,500)

        if cmds.window(self.window, ex=True):
            cmds.deleteUI(self.window, wnd=True)

        self.window = cmds.window(self.window, t=self.title, wh=self.size)
        cmds.columnLayout(adjustableColumn=True, mar=10, bgc=(0.5,0.5,1))
        cmds.button(l="Generate FK Chain", c=self.CreateFKFingerCommand, bgc=(1,0.5,0.5))
        cmds.showWindow()

    def CreateFKFingerCommand(self, *args):
        create_fk_finger_from_knuckle()
        print("FK chain generated.")

Window()
❤️ Health Shader – Animated Pulse Feedback with UV Distortion

This shader was all about building something that feels alive. It reacts to a healthValue and pulses over time like a glowing orb that’s been infused with heartbeat logic.

Key techniques:
  • Custom HeartPumpWave() to simulate pulse
  • Polar UV distortion and swirl via RotateUV()
  • Scrolling vein masks, alpha fading, and line glow
float HeartPumpWave(float t) {
  return 0.9 + 0.2 * (sin(t * 0.6 * 6.28) + sin(t * 1.2 * 6.28));
}
⚔️ Weapon Swipe VFX – Flipbook Integration & Communication Lessons

This effect came from a simple idea: “what if a sword swipe felt like it cracked the air?” I simulated a slash in EmberGen, rendered it as a flipbook, and hooked it into our custom DirectX 11 engine using an HLSL shader.

Flipbook logic snippet:
int frame = (int)(time * playbackSpeed) % totalFrames;
float2 uvOffset = CalculateFlipbookOffset(frame, gridSize);
float4 flipbookSample = tex.Sample(sampler, uv + uvOffset);
What didn’t work was our communication. I failed to clearly explain the implementation to my TA partner — they made something different, and we didn’t align early enough. The effect shipped, but not in the form I envisioned.

That mistake taught me more than the shader did. Next time, I’ll communicate clearer, show visuals sooner, and avoid making assumptions — even when the idea seems obvious in my head.