3D Bowling Ball Ray Tracer

Author: Cristian Ambriz

Language: C/C++

Graphics Library: X11

Project Description

This project implements a ray tracing renderer that creates photorealistic bowling balls. The program uses custom ray-sphere intersection algorithms, procedural Perlin noise textures, and multi-light setups to render realistic 3D bowling balls with finger holes and internal layering.

Bowling ball reference Final render showing three colored bowling balls

Key Features

Development Progress

Update 1: Multiple Clips and Holes System (October 31, 2025) Utilizing lab8 Ray Tracing Sphere Code

Implemented an array-based clipping system to support multiple cuts per object. This allowed the creation of three finger holes in each bowling ball. As well as a struct for holes to define their positions and different sizes.

struct Hole {
    Flt x, y, z;
    Flt outerR, innerR;
    };
    
    // Pre-positioned holes
    Hole holes[3] = {
        /*  x   ,   y   ,   z   , outerR, innerR */
        { -35.0f, 120.0f, 150.0f, 20.0f, 15.0f },
        { 35.0f, 120.0f, 150.0f, 20.0f, 15.0f },
        { 0.0f, 40.0f, 180.0f, 24.0f, 19.0f }
    };
    
    // Radius size for fingers/thumb 
    const Flt holeRadDiff[3] = {
        holes[0].outerR - holes[0].innerR,
        holes[1].outerR - holes[1].innerR,
        holes[2].outerR - holes[2].innerR
    };
    
struct Object {
    Clip clip[MAX_CLIPS];
    int nclips;
};

// Three finger/thumb holes
for (int h = 0; h < 3; ++h) {
    vecMake(holes[h].x, holes[h].y, holes[h].z, o->clip[h].center);
    o->clip[h].radius = holes[h].outerR;
    o->clip[h].inside = 1;
}
Bowling ball with finger holes

Update 2: Layered Shell System (November 3, 2025)

Created 20 sphere objects to use like shells with progressively smaller radii to show depthness. Each shell has interpolated hole sizes that taper from the outer surface to the inner core. 20 shells to reduce the threading visual effect while maintaining visual depth.

const int nshells = 20;
const Flt liner = 0.5; // spacing between each shell
const Flt divShells = 1.0f / (Flt)nshells;

// Create 20 shells for depth
for (int i = 0; i < nshells; i++) {
    o = &g.object[g.nobjects++];
    o->type = TYPE_SPHERE;
    vecCopy(ballCenters[ballNum], o->center);
    o->radius = ballR - (i + 1) * liner; // Each shell slightly smaller (199.5, 197.5, 195.0, ...)
    o->perlin = ballNum; // Store which ball for color
    o->nclips = 4; // 3 holes + 1 diagonal cut
    
    // Interpolate spacing between each shell
    Flt t = (i + 1) * divShells; // shell index from 0 ---> 1 until each shell is clipped
    Flt depth = 1.0f - t; // flip it and multiply by finger/thumb holes radius to simulate depthness
    
    // Create tapered hole sizes
    for (int h = 0; h < 3; h++) {
        // Position holes depending on color ball     // finger, thumbs
        // clip radius of each hole to create depthness (19.75, 23.75)
        vecMake(updateHoles[h].x, updateHoles[h].y, updateHoles[h].z, o->clip[h].center);
        o->clip[h].radius = updateHoles[h].innerR + holeRadDiff[h] * depth;
    }
    
    // Diagonal cut
    vecMake(ballCenters[ballNum][0] + 150.0, 300.0, 150.0, 
            o->clip[3].center);
    o->clip[3].radius = 120.0f;
    o->clip[3].inside = 1;
}
Bowling ball showing internal layers

Update 3: Perlin Noise Texture (November 9, 2025)

Utilizing lab3's Perlin Noise Implementation

Implemented procedural Perlin noise texture generation using three octaves of noise at different scales. This creates realistic marble-like swirls and color variation on the ball's surface. The algorithm uses a turbulence function to create banding effects and intensity-based color mapping for vein-like patterns.

if (o->surface == SURF_PERLIN) {
    const float minvalue = -1.0f;
    const float maxvalue = 1.0f;
    const float range = maxvalue - minvalue; // 2.0

    float p[3];

    // Use multiple octaves of noise for more detail
    const float scale1 = 0.015f;  // Large swirls
    const float scale2 = 0.04f;   // Medium detail
    const float scale3 = 0.1f;    // Fine detail

    // Calculate noise at each scale
    p[0] = closehit.p[0] * scale1;
    p[1] = closehit.p[1] * scale1;
    p[2] = closehit.p[2] * scale1;
    float n1 = noise3(p);

    p[0] = closehit.p[0] * scale2;
    p[1] = closehit.p[1] * scale2;
    p[2] = closehit.p[2] * scale2;
    float n2 = noise3(p);

    p[0] = closehit.p[0] * scale3;
    p[1] = closehit.p[1] * scale3;
    p[2] = closehit.p[2] * scale3;
    float n3 = noise3(p);

    // Weighted combination [-1, 1]
    float value = (n1 * 0.6f + n2 * 0.3f + n3 * 0.1f);
    
    //source for added turbulence: https://www.pbr-book.org/3ed-2018/Texture/Noise#Turbulence
    // turbulence/banding
        float t = 0.5f * (value + 1.0f);     // -> [0,1]
        t = fabsf(t - 0.5f) * 2.0f;          // bands
        t = powf(t, 0.7f);                   // contrast
        value = (t * 2.0f) - 1.0f;           // back to ~[-1,1] for old mapping
        :x
    // Calculate vein intensity
    float denom = (maxvalue - value);
    if (denom < 0.0001f) denom = 0.0001f;
    float k = range / denom; // Pattern vein intensity

    // Pick tint color (0-255 scale)
    float tintR, tintG, tintB;
    if (o->perlin == 0) {       
        // Green ball
        tintR = 12.75f; tintG = 102.0f; tintB = 25.5f;
    } 
    else if (o->perlin == 1) {  
        // Blue ball
        tintR = 25.5f; tintG = 76.5f; tintB = 204.0f;
    } 
    else {                      
        // Red ball
        tintR = 153.0f; tintG = 25.5f; tintB = 12.5f;
    }

    // Convert to [0,1] and clamp
    float outR = (k * tintR) / 255.0f;
    float outG = (k * tintG) / 255.0f;
    float outB = (k * tintB) / 255.0f;

    // Clamp values
    if (outR > 1.0f) outR = 1.0f;
    else if (outR < 0.0f) outR = 0.0f;
    
    if (outG > 1.0f) outG = 1.0f;
    else if (outG < 0.0f) outG = 0.0f;
    
    if (outB > 1.0f) outB = 1.0f;
    else if (outB < 0.0f) outB = 0.0f;
    
    closehit.color[0] = outR;
    closehit.color[1] = outG;
    closehit.color[2] = outB;
}
Bowling ball with Perlin noise marble texture

Update 4: Multiple Balls and Color Variants (November 20-29, 2024)

Expanded the renderer to create three bowling balls simultaneously, each with unique colors. Implemented color-specific Perlin noise patterns so each ball (green, blue, red) has its own marble color scheme. Optimized lighting with 3-point setup (key, fill, rim) plus ambient light. Added diagonal cutting plane to show internal structure.

// Define hole positions (relative to ball center)
Hole holes[3] = {
    { -35.0f, 120.0f, 150.0f, 20.0f, 15.0f },  // Left finger
    { 35.0f, 120.0f, 150.0f, 20.0f, 15.0f },   // Right finger
    { 0.0f, 40.0f, 180.0f, 24.0f, 19.0f }      // Thumb
};

// Pre-positioned ball centers
Vec ballCenters[3] = {
    {-450.0f, 200.0f, 0.0f},  // Left ball - Green
    {0.0f, 200.0f, 0.0f},     // Middle ball - Blue
    {450.0f, 200.0f, 0.0f}    // Right ball - Red
};

// Create all 3 balls
for (int ballNum = 0; ballNum < 3; ballNum++) {
    // Adjust hole positions for each ball
    Hole updateHoles[3];
    for (int h = 0; h < 3; h++) {
        updateHoles[h].x = holes[h].x + ballCenters[ballNum][0];
        updateHoles[h].y = holes[h].y + ballCenters[ballNum][1];
        updateHoles[h].z = holes[h].z + ballCenters[ballNum][2];
        updateHoles[h].outerR = holes[h].outerR;
        updateHoles[h].innerR = holes[h].innerR;
    }
    
    // Create outer sphere
    o = &g.object[g.nobjects++];
    o->type = TYPE_SPHERE;
    vecCopy(ballCenters[ballNum], o->center);
    o->radius = ballR;
    o->perlin = ballNum;  // Tag for color selection
    o->nclips = 4;
    
    // Add holes
    for (int h = 0; h < 3; ++h) {
        vecMake(updateHoles[h].x, updateHoles[h].y, updateHoles[h].z, 
                o->clip[h].center);
        o->clip[h].radius = updateHoles[h].outerR;
        o->clip[h].inside = 1;
    }
    
    // Create shells, inner core...
}

// Lighting setup
g.nlights = 0;

// Key Light - main light
vecMake(400.0, 500.0, 600.0, g.lightPos[g.nlights]);
vecMake(0.8, 0.8, 0.75, g.lightCol[g.nlights]);
++g.nlights;

// Fill light - soft fill
vecMake(-500.0, 300.0, 400.0, g.lightPos[g.nlights]);
vecMake(0.4, 0.4, 0.45, g.lightCol[g.nlights]);
++g.nlights;

// Back light - rim light
vecMake(0.0, 400.0, -600.0, g.lightPos[g.nlights]);
vecMake(0.3, 0.3, 0.35, g.lightCol[g.nlights]);
++g.nlights;

// Ambient light
vecMake(0.15, 0.15, 0.15, g.ambient);
Three colored bowling balls with ray tracing