Skip to main content

Light

The Light struct represents a light source that illuminates the scene. RayTracer Studio supports multiple colored lights (up to 4) and area lights for soft shadows.

Definition

cpp/include/Light.h
struct Light {
Vec3 position; // Light position in world space
Vec3 color; // Light color (RGB, 0-1)
float intensity; // Light brightness multiplier (0-2)
float radius; // Area light radius for soft shadows (0 = point light)

Light()
: position(Vec3(2.0f, 2.0f, -1.0f))
, color(Vec3(1.0f, 1.0f, 1.0f))
, intensity(1.0f)
, radius(0.5f) {}

Light(const Vec3& pos, const Vec3& col, float intens, float rad = 0.5f)
: position(pos)
, color(col)
, intensity(intens)
, radius(rad) {}

// Sample point on area light for soft shadows
Vec3 getSamplePointDisk(float u, float v, const Vec3& target) const;
};

Point Light vs Area Light

Point Light (radius = 0)

Emits light from a single infinitesimal point, creating hard shadows:

            ╲ │ ╱
╲│╱
────●──── Light Position
╱│╲
╱ │ ╲
↙ ↓ ↘ Light rays from single point

Shadow: ████████████ (sharp edge)

Area Light (radius > 0)

Emits light from a spherical area, creating soft shadows:

           ╲   │   ╱
╲ │ ╱
╱───(●)───╲ Light with radius
╱ │ ╲
╱ │ ╲
↙ ↓ ↘ Light rays from area

Shadow: ░░▓▓████▓▓░░ (soft penumbra)

Area Light Sampling

For soft shadows, we sample multiple points on the light's surface:

Vec3 getSamplePointDisk(float u, float v, const Vec3& target) const {
if (radius <= 0.0f) return position;

// Direction from light to target
Vec3 toTarget = (target - position).normalize();

// Create orthonormal basis on disk facing target
Vec3 up = (std::abs(toTarget.y) < 0.9f) ? Vec3(0, 1, 0) : Vec3(1, 0, 0);
Vec3 right = toTarget.cross(up).normalize();
up = right.cross(toTarget).normalize();

// Random point in unit disk (concentric mapping for uniform distribution)
float r = std::sqrt(u);
float theta = 2.0f * PI * v;
float dx = r * std::cos(theta);
float dy = r * std::sin(theta);

return position + right * (dx * radius) + up * (dy * radius);
}

This creates a disk-shaped sampling area that always faces the point being shaded:

         ┌─────┐
│ ○ ○ │
Target ◄─│○ ● ○│── Disk samples face target
│ ○ ○ │
└─────┘

Multiple Lights

The scene stores lights in a dynamic array, supporting up to 4 simultaneous lights:

class Scene {
std::vector<Light> lights;

// Add a new light
int addLight(float x, float y, float z, float r, float g, float b, float intensity);

// Remove a light by index
void removeLight(int index);

// Update individual light properties
void setLightPosition(int index, float x, float y, float z);
void setLightColor(int index, float r, float g, float b);
void setLightIntensity(int index, float intensity);
void setLightRadius(int index, float radius); // For soft shadows
};

Lighting Calculation

For each surface point, contributions from all lights are accumulated:

Vec3 calculateLocalLighting(const Ray& ray, const HitRecord& hit) const {
Vec3 color(0, 0, 0);
Vec3 viewDir = (ray.origin - hit.point).normalize();

// Iterate over ALL lights
for (const auto& light : lights) {
Vec3 lightDir = (light.position - hit.point).normalize();

// Shadow check (soft or hard depending on settings)
float shadowFactor = calculateShadowFactor(hit.point, light);

// Diffuse (Lambertian)
float diff = std::max(0.0f, hit.normal.dot(lightDir));
Vec3 diffuse = hit.material.color * diff * hit.material.diffuse;

// Specular (Blinn-Phong) - uses light color
Vec3 halfDir = (lightDir + viewDir).normalize();
float spec = std::pow(std::max(0.0f, hit.normal.dot(halfDir)), hit.material.shininess);
Vec3 specular = light.color * spec * hit.material.specularIntensity;

// Accumulate with intensity and shadow
color = color + (diffuse + specular) * light.intensity * shadowFactor;
}

// Add ambient (not affected by individual lights)
color = color + hit.material.color * hit.material.ambient;

return color;
}

Light Properties

Position

Where the light is located in 3D space:

AxisRangeEffect
X-5 to 5Left/right
Y-5 to 5Up/down
Z-5 to 5Forward/back

Color

Light color affects the appearance of illuminated surfaces:

PresetRGBEffect
White(1.0, 1.0, 1.0)Natural colors
Warm(1.0, 0.9, 0.7)Sunset/incandescent
Cool(0.8, 0.9, 1.0)Moonlight/LED
Red(1.0, 0.3, 0.3)Dramatic accent
Green(0.3, 1.0, 0.4)Spooky/sci-fi
Blue(0.3, 0.5, 1.0)Cold/underwater
// Surface color is multiplied by light color
Vec3 litColor = surfaceColor * lightColor;

Intensity

A multiplier for brightness (0.0 to 2.0):

ValueEffect
0.0Off
0.5Dim (50%)
1.0Normal
1.5Bright (150%)
2.0Very bright

Radius (for Soft Shadows)

Controls the size of the area light:

ValueEffect
0.0Point light (hard shadows)
0.1-0.3Small area (subtle softness)
0.5Medium area (default)
1.0-1.5Large area (very soft shadows)

Larger radius = softer shadow edges.

Shadow Rays

Each light casts its own shadows. With soft shadows enabled, multiple rays are cast:

float calculateShadowFactor(const Vec3& point, const Light& light) const {
if (!softShadowsEnabled || light.radius <= 0.0f) {
// Hard shadows
return isInShadowHard(point, light.position) ? 0.3f : 1.0f;
}

// Soft shadows - sample multiple points on area light
int litSamples = 0;
for (int i = 0; i < shadowSamples; i++) {
Vec3 samplePos = light.getSamplePointDisk(random(), random(), point);
if (!isInShadowHard(point, samplePos)) {
litSamples++;
}
}

float visibility = float(litSamples) / float(shadowSamples);
return 0.3f + visibility * 0.7f; // 0.3 to 1.0
}

With multiple lights, a point may be:

  • Fully lit: Visible from all lights
  • Partially lit: Visible from some lights (colored shadows!)
  • In shadow: Blocked from all lights (only ambient)

UI Controls

Light Tab

  • Light Selector: Tabs for each light (Light 1, Light 2, etc.)
  • Add (+) / Remove (−): Up to 4 lights
  • Color presets: White, Warm, Cool, Red, Green, Blue
  • Intensity slider: 0% to 200%
  • Position sliders: X, Y, Z coordinates

View Tab (Soft Shadows)

  • Soft Shadows toggle: Enable/disable
  • Light Size slider: Controls radius for all lights
  • Shadow Samples: 4, 9, 16, or 25 rays per light

API Reference

// Add a new light (returns index)
addLight(x, y, z, r, g, b, intensity: number): number

// Remove light at index
removeLight(index: number): void

// Position
setLightPosition(index: number, x, y, z: number): void
getLightX/Y/Z(index: number): number

// Color
setLightColor(index: number, r, g, b: number): void
getLightR/G/B(index: number): number

// Intensity
setLightIntensity(index: number, intensity: number): void
getLightIntensity(index: number): number

// Radius (for soft shadows)
setLightRadius(index: number, radius: number): void
getLightRadius(index: number): number

// Utility
getLightCount(): number
resetLights(): void

Creative Uses

Two-Point Lighting

Classic film lighting setup:

  • Key light: Main light, bright, white/warm
  • Fill light: Softer, dimmer, from opposite side

Colored Accent Lighting

  • Red and blue lights from opposite sides create dramatic contrast
  • Green light from below for spooky effect

Soft vs Hard Shadows

  • Use hard shadows (radius = 0) for harsh sunlight
  • Use soft shadows (radius > 0) for indoor/overcast lighting