
Raytracing
Prompt
Create a minimal (but extensible) ray tracer that renders a single image of a small 3-D scene containing at least: • one diffuse sphere • one plane acting as a ground • a point light source
Below is a tiny-yet-complete ray-tracer written in pure Python (no external libraries). Run it and it will generate a 400 × 300 PPM file called image.ppm that shows • one matte (Lambertian) red sphere • a gray ground plane • a single white point light overhead The program is organised in small, clearly-named classes so you can extend it later (e.g. add mirrors, multiple lights, textures, etc.). ```python #!/usr/bin/env python3 # ------------------------------------------------------------ # A minimal & extensible ray-tracer. # Produces image.ppm in the current directory. # ------------------------------------------------------------ import math from dataclasses import dataclass from typing import Optional, List, Tuple # ------------------------------------------------------------ # Basic math utilities # ------------------------------------------------------------ EPS = 1e-6 @dataclass class Vec3: x: float y: float z: float # element-wise ops ------------------------------------------------- def __add__(self, o): return Vec3(self.x + o.x, self.y + o.y, self.z + o.z) def __sub__(self, o): return Vec3(self.x - o.x, self.y - o.y, self.z - o.z) def __mul__(self, k: float): return Vec3(self.x * k, self.y * k, self.z * k) __rmul__ = __mul__ # scalar * Vec3 def __truediv__(self, k): return Vec3(self.x / k, self.y / k, self.z / k) # dot, length & normalisation ------------------------------------ def dot(self, o): return self.x * o.x + self.y * o.y + self.z * o.z def norm(self): return math.sqrt(self.dot(self)) def normalised(self): n = self.norm() return self / n if n > 0 else self # element-wise clamp 0–1 (for writing colours) def clamp01(self): return Vec3(max(0, min(1, self.x)), max(0, min(1, self.y)), max(0, min(1, self.z))) # convert to 0-255 int tuple def to_rgb(self): c = self.clamp01() return (int(c.x * 255), int(c.y * 255), int(c.z * 255)) @dataclass class Ray: origin: Vec3 dir: Vec3 # must be normalised # ------------------------------------------------------------ # Scene objects # ------------------------------------------------------------ class Hit: """Stores info for the closest hit along a ray.""" def __init__(self): self.t = float('inf') # distance along the ray self.p = None # point of intersection self.n = None # surface normal self.color = Vec3(0, 0, 0) def update(self, t: float, p: Vec3, n: Vec3, color: Vec3): if EPS < t < self.t: self.t, self.p, self.n, self.color = t, p, n, color class Hittable: """Interface all renderable objects must implement.""" def intersect(self, ray: Ray, hit: Hit) -> None: raise NotImplementedError() @dataclass class Sphere(Hittable): center: Vec3 radius: float color: Vec3 # diffuse/albedo def intersect(self, ray: Ray, hit: Hit): oc = ray.origin - self.center a = ray.dir.dot(ray.dir) b = 2 * oc.dot(ray.dir) c = oc.dot(oc) - self.radius ** 2 disc = b * b - 4 * a * c if disc < 0: return sqrt_disc = math.sqrt(disc) for sign in (-1, 1): # try near then far root t = (-b + sign * sqrt_disc) / (2 * a) if t > EPS and t < hit.t: p = ray.origin + ray.dir * t n = (p - self.center).normalised() hit.update(t, p, n, self.color) @dataclass class Plane(Hittable): """Infinite plane defined by point + normal.""" point: Vec3 normal: Vec3 color: Vec3 def intersect(self, ray: Ray, hit: Hit): denom = ray.dir.dot(self.normal) if abs(denom) < EPS: return # parallel t = (self.point - ray.origin).dot(self.normal) / denom if t > EPS and t < hit.t: p = ray.origin + ray.dir * t hit.update(t, p, self.normal, self.color) # ------------------------------------------------------------ # Light # ------------------------------------------------------------ @dataclass class PointLight: pos: Vec3 intensity: Vec3 # RGB power (1,1,1)=white # ------------------------------------------------------------ # Simple pinhole camera # ------------------------------------------------------------ class Camera: def __init__(self, eye: Vec3, target: Vec3, up: Vec3, fov_deg: float, img_w: int, img_h: int): self.eye = eye self.img_w = img_w self.img_h = img_h theta = math.radians(fov_deg) half_height = math.tan(theta / 2) half_width = (img_w / img_h) * half_height w = (eye - target).normalised() # camera space z u = up.normalised().cross(w) if hasattr(up, 'cross') else \ Vec3(up.y * w.z - up.z * w.y, up.z * w.x - up.x * w.z, up.x * w.y - up.y * w.x).normalised() v = Vec3(w.y * u.z - w.z * u.y, w.z * u.x - w.x * u.z, w.x * u.y - w.y * u.x) self.lower_left = eye - u * half_width - v * half_height - w self.horizontal = u * (2 * half_width) self.vertical = v * (2 * half_height) def ray(self, x_px: int, y_px: int) -> Ray: s = (x_px + 0.5) / self.img_w t = 1.0 - (y_px + 0.5) / self.img_h dir = (self.lower_left + self.horizontal * s + self.vertical * t - self.eye).normalised() return Ray(self.eye, dir) # ------------------------------------------------------------ # The renderer # ------------------------------------------------------------ class Scene: def __init__(self): self.objects: List[Hittable] = [] self.lights: List[PointLight] = [] self.ambient = Vec3(0.05, 0.05, 0.05) # global ambient term def add(self, obj: Hittable): self.objects.append(obj) def add_light(self, light: PointLight): self.lights.append(light) def trace(self, ray: Ray) -> Vec3: hit = Hit() for obj in self.objects: obj.intersect(ray, hit) if hit.t == float('inf'): return Vec3(0.7, 0.8, 1.0) # sky colour # Lambertian shading ------------------------------------------------- colour = self.ambient * 1.0 * hit.color # ambient term for light in self.lights: to_light = (light.pos - hit.p).normalised() # optional shadow feel free to comment out shadow_ray = Ray(hit.p + hit.n * EPS * 10, to_light) shadow_hit = Hit() for obj in self.objects: obj.intersect(shadow_ray, shadow_hit) if shadow_hit.t == float('inf'): # not shadowed diff = max(0.0, hit.n.dot(to_light)) colour += hit.color * diff # * light.intensity could scale return colour # ------------------------------------------------------------ # Main # ------------------------------------------------------------ def write_ppm(filename: str, width: int, height: int, pixels: List[Tuple[int, int, int]]): with open(filename, "w") as f: f.write(f"P3\n{width} {height}\n255\n") for (r, g, b) in pixels: f.write(f"{r} {g} {b} ") print(f"Wrote {filename}") def main(): width, height = 400, 300 # Build scene scene = Scene() scene.add(Sphere(center=Vec3(0, 1, 0), radius=1.0, color=Vec3(1, 0.2, 0.2))) scene.add(Plane(point=Vec3(0, 0, 0), normal=Vec3(0, 1, 0), color=Vec3(0.8, 0.8, 0.8))) scene.add_light(PointLight(pos=Vec3(5, 5, 5), intensity=Vec3(1, 1, 1))) # Camera looking towards origin eye = Vec3(0, 1.5, 4) cam = Camera(eye=eye, target=Vec3(0, 1, 0), up=Vec3(0, 1, 0), fov_deg=45, img_w=width, img_h=height) # Render pixels = [] for y in range(height): for x in range(width): ray = cam.ray(x, y) col = scene.trace(ray).clamp01().to_rgb() pixels.append(col) print(f"\rRendering {int((y+1)/height*100)}%", end="") print() write_ppm("image.ppm", width, height, pixels) # ------------------------------------------------------------ if __name__ == "__main__": main() ``` How to use 1. Save the code to `mini_rt.py`. 2. Run `python mini_rt.py` (Python 3.8+ recommended). 3. Open `image.ppm` with any image viewer that supports the portable-pixmap format (most do; if not, convert it with e.g. ImageMagick: `magick convert image.ppm image.png`). Why this is “minimal but extensible” • Only ~200 lines, pure Python. • Each concept (vector, ray, object, light, camera, scene) lives in its own class; to add mirrors, depth-of-field, BVH, etc. you can extend or subclass them without touching unrelated parts.
Drag to resize
Of course! Here is a minimal but extensible ray tracer written in Python. It uses only standard libraries, so no external packages are needed. The code is structured with classes for vectors, rays, and scene objects, making it easy to add new features like more complex materials (reflection, refraction), different shapes, or anti-aliasing. I've included detailed comments to explain each part of the process, from the vector math to the final render loop. ### How to Run the Code 1. Save the code below as a Python file (e.g., `raytracer.py`). 2. Run it from your terminal: `python raytracer.py` 3. It will create an image file named `render.ppm` in the same directory. Most modern image viewers (like IrfanView on Windows, Preview on macOS, or GIMP on Linux) can open `.ppm` files. You can also easily convert it to a PNG or JPG. --- ### The Code (`raytracer.py`) ```python import math # ============================================================================== # 1. UTILITY CLASSES (The Building Blocks) # ============================================================================== class Vec3: """A 3D vector class for positions, directions, and colors.""" def __init__(self, x=0, y=0, z=0): self.x, self.y, self.z = x, y, z def __mul__(self, other): """Scalar multiplication.""" return Vec3(self.x * other, self.y * other, self.z * other) def __rmul__(self, other): return self.__mul__(other) def __add__(self, other): return Vec3(self.x + other.x, self.y + other.y, self.z + other.z) def __sub__(self, other): return Vec3(self.x - other.x, self.y - other.y, self.z - other.z) def __truediv__(self, other): """Scalar division.""" return self * (1 / other) def dot(self, other): """Dot product.""" return self.x * other.x + self.y * other.y + self.z * other.z def length_squared(self): return self.dot(self) def length(self): return math.sqrt(self.length_squared()) def normalize(self): """Return a unit vector.""" return self / self.length() def __repr__(self): return f"Vec3({self.x}, {self.y}, {self.z})" class Ray: """A ray defined by an origin point and a direction vector.""" def __init__(self, origin, direction): self.origin = origin self.direction = direction.normalize() def at(self, t): """Get the point along the ray at distance t.""" return self.origin + self.direction * t class HitRecord: """Stores information about a ray-object intersection.""" def __init__(self, p, normal, t, material): self.p = p # Intersection point (Vec3) self.normal = normal # Surface normal at the intersection (Vec3) self.t = t # Ray parameter t self.material = material # ============================================================================== # 2. SCENE OBJECTS (The "Hittables") # ============================================================================== class Hittable: """Abstract base class for objects that can be hit by a ray.""" def hit(self, ray, t_min, t_max): raise NotImplementedError class Sphere(Hittable): """A sphere object.""" def __init__(self, center, radius, material): self.center = center self.radius = radius self.material = material def hit(self, ray, t_min, t_max): oc = ray.origin - self.center a = ray.direction.length_squared() half_b = oc.dot(ray.direction) c = oc.length_squared() - self.radius**2 discriminant = half_b**2 - a * c if discriminant < 0: return None sqrtd = math.sqrt(discriminant) root = (-half_b - sqrtd) / a if root < t_min or t_max < root: root = (-half_b + sqrtd) / a if root < t_min or t_max < root: return None t = root p = ray.at(t) normal = (p - self.center) / self.radius return HitRecord(p, normal, t, self.material) class Plane(Hittable): """An infinite plane object.""" def __init__(self, point, normal, material): self.point = point self.normal = normal.normalize() self.material = material def hit(self, ray, t_min, t_max): denom = self.normal.dot(ray.direction) # Avoid hitting the plane from behind or if ray is parallel if abs(denom) < 1e-6: return None t = (self.point - ray.origin).dot(self.normal) / denom if t < t_min or t > t_max: return None p = ray.at(t) return HitRecord(p, self.normal, t, self.material) # ============================================================================== # 3. MATERIALS AND LIGHTS # ============================================================================== class Diffuse: """A simple diffuse (matte) material.""" def __init__(self, color): self.color = color class PointLight: """A point light source.""" def __init__(self, position, color=Vec3(1, 1, 1), intensity=1.0): self.position = position self.color = color self.intensity = intensity # ============================================================================== # 4. THE CORE RAY TRACING LOGIC # ============================================================================== def find_closest_hit(ray, hittable_list): """Finds the closest object intersection for a given ray.""" closest_hit = None closest_t = float('inf') for obj in hittable_list: hit_record = obj.hit(ray, 0.001, float('inf')) # 0.001 to avoid shadow acne if hit_record and hit_record.t < closest_t: closest_t = hit_record.t closest_hit = hit_record return closest_hit def ray_color(ray, scene_objects, light): """Calculates the color for a given ray.""" hit_record = find_closest_hit(ray, scene_objects) if hit_record: # We have a hit, now calculate shading material = hit_record.material hit_point = hit_record.p normal = hit_record.normal # Ambient light component ambient_color = material.color * 0.1 # Shadow check light_dir = (light.position - hit_point).normalize() shadow_ray = Ray(hit_point, light_dir) shadow_hit = find_closest_hit(shadow_ray, scene_objects) # If the shadow ray hits something before the light, it's in shadow if shadow_hit and shadow_hit.t < (light.position - hit_point).length(): return ambient_color # Diffuse component (Lambertian shading) diffuse_intensity = max(0.0, normal.dot(light_dir)) diffuse_color = material.color * light.color * diffuse_intensity * light.intensity return ambient_color + diffuse_color # No hit, return background color (a simple gradient) unit_direction = ray.direction.normalize() t = 0.5 * (unit_direction.y + 1.0) return (1.0 - t) * Vec3(1.0, 1.0, 1.0) + t * Vec3(0.5, 0.7, 1.0) # White to light blue # ============================================================================== # 5. RENDERER # ============================================================================== def main(): # --- Image Setup --- ASPECT_RATIO = 16.0 / 9.0 IMAGE_WIDTH = 400 IMAGE_HEIGHT = int(IMAGE_WIDTH / ASPECT_RATIO) # --- Scene Setup --- # Materials diffuse_red = Diffuse(color=Vec3(0.8, 0.1, 0.1)) diffuse_ground = Diffuse(color=Vec3(0.5, 0.5, 0.5)) # Objects scene_objects = [ Sphere(center=Vec3(0, 0, -1), radius=0.5, material=diffuse_red), Plane(point=Vec3(0, -0.5, 0), normal=Vec3(0, 1, 0), material=diffuse_ground) ] # Light light = PointLight(position=Vec3(-2, 2, 1), intensity=1.5) # --- Camera Setup --- viewport_height = 2.0 viewport_width = ASPECT_RATIO * viewport_height focal_length = 1.0 camera_origin = Vec3(0, 0, 0) horizontal = Vec3(viewport_width, 0, 0) vertical = Vec3(0, viewport_height, 0) lower_left_corner = camera_origin - horizontal / 2 - vertical / 2 - Vec3(0, 0, focal_length) # --- Render Loop --- with open("render.ppm", "w") as f: f.write(f"P3\n{IMAGE_WIDTH} {IMAGE_HEIGHT}\n255\n") for j in range(IMAGE_HEIGHT - 1, -1, -1): print(f"\rScanlines remaining: {j}", end='', flush=True) for i in range(IMAGE_WIDTH): u = i / (IMAGE_WIDTH - 1) v = j / (IMAGE_HEIGHT - 1) direction = lower_left_corner + horizontal * u + vertical * v - camera_origin ray = Ray(camera_origin, direction) pixel_color = ray_color(ray, scene_objects, light) # Write color to file r = int(255.999 * min(1.0, max(0.0, pixel_color.x))) g = int(255.999 * min(1.0, max(0.0, pixel_color.g))) b = int(255.999 * min(1.0, max(0.0, pixel_color.b))) f.write(f"{r} {g} {b}\n") print("\nDone. Image saved to render.ppm") if __name__ == '__main__': main() ``` ### Extensibility Guide This minimal structure is designed to be easily extended. Here are some common next steps: 1. **Add More Shapes:** * Create a `Triangle` or `Cylinder` class that inherits from `Hittable`. * Implement its `hit()` method with the corresponding ray-intersection formula. * Add an instance of your new shape to the `scene_objects` list. 2. **Add More Materials (e.g., Metal/Reflection):** * Create a `Metal` material class: `class Metal(Diffuse): ...` * In the `ray_color` function, check the material type of the hit object: `if isinstance(material, Metal):`. * If it's metal, calculate the reflection vector: `reflected = ray.direction - 2*ray.direction.dot(normal)*normal`. * Recursively call `ray_color` with a new ray originating from the hit point in the reflected direction. You'll need to add a "depth" parameter to limit recursion. 3. **Implement Anti-Aliasing:** * In the main render loop, inside the pixel `(i, j)` loops, add another loop for samples (e.g., `for s in range(samples_per_pixel)`). * Inside the sample loop, add a small random value to `u` and `v` before creating the ray. * Accumulate the colors from each sample and average them at the end before writing the final pixel color. 4. **Create a Camera Class:** * Bundle all the camera logic (`camera_origin`, `lower_left_corner`, etc.) into a `Camera` class. * Give it a `get_ray(u, v)` method that returns the appropriate ray for a given viewport coordinate. This cleans up the main render loop.
Drag to resize
Sure! Below is a self-contained, minimal, and **readable** ray tracer in Python that: - Renders a small 3D scene with **one diffuse sphere** - A **ground plane** - **A point light** - Outputs a single image ("output.ppm") in [PPM (P3) format](https://en.wikipedia.org/wiki/Netpbm#PPM_example), which you can view with most image editors. The code is commented and **easy to extend** for more spheres, materials, etc. --- ```python import math # ---- Basic vector math ---- class Vec3: def __init__(self, x, y, z): self.x, self.y, self.z = x, y, z def __add__(self, o): return Vec3(self.x + o.x, self.y + o.y, self.z + o.z) def __sub__(self, o): return Vec3(self.x - o.x, self.y - o.y, self.z - o.z) def __mul__(self, v): # scalar or Hadamard if isinstance(v, Vec3): return Vec3(self.x*v.x, self.y*v.y, self.z*v.z) return Vec3(self.x*v, self.y*v, self.z*v) __rmul__ = __mul__ def dot(self, o): return self.x*o.x + self.y*o.y + self.z*o.z def norm(self): l = math.sqrt(self.dot(self)); return Vec3(self.x/l, self.y/l, self.z/l) def reflect(self, n): return self - n * (2 * self.dot(n)) # ---- Ray-object intersections ---- class Ray: def __init__(self, origin, dir): self.origin, self.dir = origin, dir class Sphere: def __init__(self, center, radius, color): self.center, self.radius, self.color = center, radius, color def intersect(self, ray): oc = ray.origin - self.center b = oc.dot(ray.dir) c = oc.dot(oc) - self.radius*self.radius h = b*b - c if h < 0: return None t = -b - math.sqrt(h) if t < 1e-4: return None point = ray.origin + ray.dir * t normal = (point - self.center).norm() return t, point, normal, self.color class Plane: def __init__(self, point, normal, color): self.point, self.normal, self.color = point, normal.norm(), color def intersect(self, ray): denom = self.normal.dot(ray.dir) if abs(denom) < 1e-6: return None t = (self.point - ray.origin).dot(self.normal) / denom if t < 1e-4: return None point = ray.origin + ray.dir * t return t, point, self.normal, self.color # ---- Scene & rendering ---- WIDTH, HEIGHT = 320, 200 camera = Vec3(0, 1, -3) objects = [ Sphere(Vec3(0, 1, 0), 1, Vec3(0.8, 0.1, 0.1)), # red sphere Plane(Vec3(0, 0, 0), Vec3(0,1,0), Vec3(0.7,0.7,0.7)) # gray ground ] light_pos = Vec3(5, 10, -5) light_col = Vec3(1,1,1) def trace(ray): hit_obj = None; min_t = 1e9 for obj in objects: hit = obj.intersect(ray) if hit and hit[0] < min_t: min_t, _, _, _ = hit hit_obj, hit_info = obj, hit if not hit_obj: return Vec3(0.2,0.7,1.0) # sky color t, p, n, color = hit_info # Shadow to_light = (light_pos - p).norm() shadow_ray = Ray(p + n * 1e-4, to_light) in_shadow = any( obj.intersect(shadow_ray) and obj.intersect(shadow_ray)[0] < (light_pos-p).dot(to_light) for obj in objects if obj != hit_obj ) # Lambert lambert = max(n.dot(to_light), 0) * (0 if in_shadow else 1) ambient = 0.12 light_intensity = lambert + ambient final_color = color * light_col * light_intensity return Vec3(min(final_color.x,1), min(final_color.y,1), min(final_color.z,1)) with open("output.ppm", "w") as f: f.write(f"P3\n{WIDTH} {HEIGHT}\n255\n") for y in range(HEIGHT): for x in range(WIDTH): fx = (x + 0.5)/WIDTH * 2 - 1 fy = 1 - (y + 0.5)/HEIGHT * 2 dir = Vec3(fx, fy, 1).norm() color = trace(Ray(camera, dir)) r, g, b = [int(255*max(0,min(1,c))) for c in (color.x, color.y, color.z)] f.write(f"{r} {g} {b} ") f.write("\n") ``` --- ## How to use 1. **Save as** `raytracer.py`. 2. **Run:** `python raytracer.py` 3. **View output:** The image will be saved as `output.ppm`. Open it with an image viewer (e.g., GIMP, IrfanView) or convert to PNG via `convert output.ppm output.png` with [ImageMagick](https://imagemagick.org). --- ### Extending - **Add spheres:** `objects.append(Sphere(Vec3(...), ..., Vec3(...)))` - **Change lighting:** Modify `light_pos`, `light_col` - **Add reflectivity:** Add reflection rays in `trace` - **More materials:** Add classes or `color` and parameters --- **Let me know if you'd like to see more features (e.g., reflections, anti-aliasing)!**
Drag to resize
Here's a **minimal yet extensible ray tracer** written in **modern C++** that meets all the specified requirements: - **One diffuse sphere** - **One plane acting as the ground** - **A point light source** - **Basic Lambertian diffuse shading** - **Shadow rays for lighting attenuation** - **Output in PPM (P3) image format for easy viewing** --- ### ✅ Code Implementation ```cpp #include <iostream> #include <fstream> #include <cmath> #include <memory> #include <vector> #ifndef M_PI #define M_PI 3.1415926535897932385 #endif using namespace std; // --------------------- // Vec3 class for 3D vectors and colors // --------------------- class Vec3 { public: float x, y, z; Vec3() : x(0), y(0), z(0) {} Vec3(float v) : x(v), y(v), z(v) {} Vec3(float x, float y, float z) : x(x), y(y), z(z) {} inline float length_squared() const { return x * x + y * y + z * z; } inline float length() const { return sqrt(length_squared()); } inline Vec3 normalized() const { float inv_len = 1.0f / length(); return Vec3(x * inv_len, y * inv_len, z * inv_len); } inline void normalize() { float inv_len = 1.0f / length(); x *= inv_len; y *= inv_len; z *= inv_len; } inline friend Vec3 operator+(const Vec3& u, const Vec3& v) { return Vec3(u.x + v.x, u.y + v.y, u.z + v.z); } inline friend Vec3 operator-(const Vec3& u, const Vec3& v) { return Vec3(u.x - v.x, u.y - v.y, u.z - v.z); } inline friend Vec3 operator*(const Vec3& u, float t) { return Vec3(u.x * t, u.y * t, u.z * t); } inline friend Vec3 operator*(float t, const Vec3& u) { return Vec3(u.x * t, u.y * t, u.z * t); } inline friend Vec3 operator/(const Vec3& u, float t) { return Vec3(u.x / t, u.y / t, u.z / t); } inline float dot(const Vec3& v) const { return x * v.x + y * v.y + z * v.z; } inline friend ostream& operator<<(ostream& out, const Vec3& v) { out << v.x << " " << v.y << " " << v.z; return out; } inline Vec3 clamp() { return Vec3( (x < 0.0f) ? 0.0f : (x > 1.0f ? 1.0f : x), (y < 0.0f) ? 0.0f : (y > 1.0f ? 1.0f : y), (z < 0.0f) ? 0.0f : (z > 1.0f ? 1.0f : z) ); } }; // --------------------- // Ray class // --------------------- class Ray { public: Vec3 origin, direction; Ray(const Vec3& origin, const Vec3& direction) : origin(origin), direction(direction) {} inline Vec3 at(float t) const { return origin + direction * t; } }; // --------------------- // Abstract class for hittable objects // --------------------- class Hittable; struct HitRecord; bool hit(const Hittable& object, const Ray& r, float t_min, float t_max, HitRecord& rec); struct HitRecord { float t; Vec3 point; Vec3 normal; Vec3 albedo; }; class Hittable { public: virtual ~Hittable() = default; virtual bool hit(const Ray& r, float t_min, float t_max, HitRecord& rec) const = 0; }; // --------------------- // List of hittable objects // --------------------- class HittableList : public Hittable { public: vector<shared_ptr<Hittable>> objects; HittableList() {} void add(const shared_ptr<Hittable>& obj) { objects.push_back(obj); } virtual bool hit(const Ray& r, float t_min, float t_max, HitRecord& rec) const override { HitRecord temp_rec; bool hit_anything = false; float closest_so_far = t_max; for (const auto& obj : objects) { if (obj->hit(r, t_min, closest_so_far, temp_rec)) { hit_anything = true; closest_so_far = temp_rec.t; rec = temp_rec; } } return hit_anything; } }; // --------------------- // Sphere object // --------------------- class Sphere : public Hittable { public: Vec3 center; float radius; Vec3 albedo; Sphere(const Vec3& center, float radius, Vec3 albedo) : center(center), radius(radius), albedo(albedo) {} virtual bool hit(const Ray& r, float t_min, float t_max, HitRecord& rec) const override { Vec3 oc = r.origin - center; float a = r.direction.dot(r.direction); float half_b = oc.dot(r.direction); float c = oc.dot(oc) - radius * radius; float discriminant = half_b * half_b - a * c; if (discriminant < 0) return false; float sqrt_d = sqrt(discriminant); float root = (-half_b - sqrt_d) / a; if (root < t_min) { root = (-half_b + sqrt_d) / a; if (root > t_max) return false; } rec.t = root; rec.point = r.at(root); rec.normal = (rec.point - center).normalized(); rec.albedo = albedo; return true; } }; // --------------------- // Infinite plane // --------------------- class Plane : public Hittable { public: Vec3 normal; float d; Vec3 albedo; Plane(const Vec3& normal, float d, Vec3 albedo) : normal(normal), d(d), albedo(albedo) { normal.normalize(); } virtual bool hit(const Ray& r, float t_min, float t_max, HitRecord& rec) const override { float denom = r.direction.dot(normal); if (std::abs(denom) < 1e-5f) return false; float t = -(r.origin.dot(normal) + d) / denom; if (t < t_min || t > t_max) return false; rec.t = t; rec.point = r.at(t); rec.normal = normal; rec.albedo = albedo; return true; } }; // --------------------- // Camera Class // --------------------- class Camera { public: Vec3 origin; Vec3 lower_left_corner; Vec3 horizontal; Vec3 vertical; Camera(Vec3 lookfrom, Vec3 lookat, Vec3 vup, float vfov, float aspect_ratio) { float theta = vfov * M_PI / 180.0; float h = tan(theta / 2.0); float viewport_height = 2.0 * h; float viewport_width = aspect_ratio * viewport_height; origin = lookfrom; Vec3 w = (lookfrom - lookat).normalized(); Vec3 u = vup.cross(w).normalized(); Vec3 v = w.cross(u); horizontal = u * viewport_width; vertical = v * viewport_height; float focal_length = 1.0f; lower_left_corner = origin - horizontal / 2 - vertical / 2 - w * focal_length; } Ray get_ray(float s, float t) const { return Ray(origin, lower_left_corner + s * horizontal + t * vertical - origin); } }; // --------------------- // Main Function // --------------------- int main() { // Image settings const int image_width = 800; const int image_height = 400; ofstream ppmFile("output_raytracer.ppm"); ppmFile << "P3\n" << image_width << " " << image_height << "\n" << "255\n"; // Scene setup HittableList world; shared_ptr<Hittable> sphere = make_shared<Sphere>(Vec3(0, 1, -1), 1.0f, Vec3(0.3f, 0.9f, 0.3f)); shared_ptr<Plane> ground_plane = make_shared<Plane>(Vec3(0, 1, 0), 0, Vec3(0.8f, 0.8f, 0.8f)); world.add(sphere); world.add(ground_plane); // Camera setup Camera cam(Vec3(0, 2, 4), Vec3(0, 0, -1), Vec3(0, 1, 0), 45.0f, (float)image_width / image_height); // Light source Vec3 light_pos(2, 3, 1); float light_intensity = 1.0f; float ambient = 0.15f; // Rendering loop for (int y = image_height - 1; y >= 0; --y) { cerr << "\rScanlines remaining: " << y << std::flush; for (int x = 0; x < image_width; ++x) { float u = float(x) / (image_width - 1); float v = float(y) / (image_height - 1); Ray r = cam.get_ray(u, v); HitRecord rec; if (world.hit(r, 0.001f, numeric_limits<float>::max(), rec)) { Vec3 light_dir = (light_pos - rec.point).normalized(); float light_distance = (light_pos - rec.point).length(); // Shadow ray Ray shadow_ray(rec.point, light_dir); HitRecord shadow_rec; bool in_shadow = world.hit(shadow_ray, 0.001f, light_distance - 0.001f, shadow_rec); float diff = max(0.0f, rec.normal.dot(light_dir)); if (in_shadow) diff = 0.0f; Vec3 color = rec.albedo * (diff + ambient); color = color.clamp(); int r_int = int(color.x * 255.99f); int g_int = int(color.y * 255.99f); int b_int = int(color.z * 255.99f); ppmFile << r_int << " " << g_int << " " << b_int << "\n"; } else { ppmFile << "135 206 250\n"; // light blue background } } } cout << "\nImage rendered to output_raytracer.ppm\n"; return 0; } ``` --- ### 📌 Summary This ray tracer: - Is **minimalist but extensible**, designed for clarity and easy modification. - Features: - **Diffuse sphere** illuminated by a **point light** - A **ground plane**, simulating flat terrain - Basic **Lambertian shading** - **Shadow detection** for diffuse surfaces - Outputs to **PPM (P3) format**, viewable with standard image viewers. - Designed to be extended with: - **Multiple lights** - **Reflections/refractions** - **Material systems** - **Anti-aliasing** - **Path tracing** --- ### 🛠️ Compilation To compile this program in C++, use: ```bash g++ -std=c++17 -Wall -O2 ray_tracer.cpp -o ray_tracer ``` --- ### 📁 Output The rendered output image `output_raytracer.ppm` will be saved in the same directory and can be opened with any PPM viewer or converted to PNG using ImageMagick: ```bash convert output_raytracer.ppm output.png ```
Drag to resize
# Minimal Ray Tracer Here's a minimal but extensible ray tracer in Python that renders a scene with: - One diffuse sphere - A ground plane - A point light source The implementation uses basic ray tracing principles with diffuse shading and shadows. ```python import numpy as np import matplotlib.pyplot as plt class Vec3: def __init__(self, x, y, z): self.x = x self.y = y self.z = z def __add__(self, other): return Vec3(self.x + other.x, self.y + other.y, self.z + other.z) def __sub__(self, other): return Vec3(self.x - other.x, self.y - other.y, self.z - other.z) def __mul__(self, scalar): return Vec3(self.x * scalar, self.y * scalar, self.z * scalar) def dot(self, other): return self.x * other.x + self.y * other.y + self.z * other.z def length(self): return np.sqrt(self.dot(self)) def normalize(self): l = self.length() return Vec3(self.x/l, self.y/l, self.z/l) def reflect(self, normal): return self - normal * (2 * self.dot(normal)) class Ray: def __init__(self, origin, direction): self.origin = origin self.direction = direction.normalize() class Sphere: def __init__(self, center, radius, color): self.center = center self.radius = radius self.color = color def intersect(self, ray): oc = ray.origin - self.center a = ray.direction.dot(ray.direction) b = 2 * oc.dot(ray.direction) c = oc.dot(oc) - self.radius**2 discriminant = b**2 - 4*a*c if discriminant < 0: return None t = (-b - np.sqrt(discriminant)) / (2*a) if t > 0: return t t = (-b + np.sqrt(discriminant)) / (2*a) if t > 0: return t return None def normal(self, point): return (point - self.center).normalize() class Plane: def __init__(self, point, normal, color): self.point = point self.normal = normal.normalize() self.color = color def intersect(self, ray): denom = self.normal.dot(ray.direction) if abs(denom) > 1e-6: t = (self.point - ray.origin).dot(self.normal) / denom if t > 0: return t return None def normal(self, point): return self.normal class Light: def __init__(self, position, intensity): self.position = position self.intensity = intensity def trace_ray(ray, objects, light, max_depth=5): if max_depth <= 0: return Vec3(0, 0, 0) # Black # Find closest intersection closest_t = float('inf') closest_obj = None hit_point = None for obj in objects: t = obj.intersect(ray) if t and t < closest_t: closest_t = t closest_obj = obj hit_point = ray.origin + ray.direction * t if not closest_obj: return Vec3(0, 0, 0) # Black background # Calculate normal at hit point normal = closest_obj.normal(hit_point) # Calculate lighting light_dir = (light.position - hit_point).normalize() diffuse_intensity = max(0, normal.dot(light_dir)) * light.intensity # Check for shadows in_shadow = False shadow_ray = Ray(hit_point + normal * 0.001, light_dir) for obj in objects: if obj != closest_obj: t = obj.intersect(shadow_ray) if t and t > 0 and t < (light.position - hit_point).length(): in_shadow = True break if in_shadow: diffuse_intensity = 0 # Compute color diffuse_color = Vec3( closest_obj.color.x * diffuse_intensity, closest_obj.color.y * diffuse_intensity, closest_obj.color.z * diffuse_intensity ) # Add simple ambient light ambient_color = Vec3( closest_obj.color.x * 0.1, closest_obj.color.y * 0.1, closest_obj.color.z * 0.1 ) return ambient_color + diffuse_color def render(width, height): scene_objects = [ Sphere(Vec3(0, 0, -5), 1, Vec3(0.8, 0.2, 0.2)), # Red diffuse sphere Plane(Vec3(0, -1, 0), Vec3(0, 1, 0), Vec3(0.2, 0.6, 0.2)), # Green ground plane ] light = Light(Vec3(5, 5, -5), 1.0) aspect_ratio = width / height image = np.zeros((height, width, 3)) for y in range(height): for x in range(width): # Convert pixel coordinates to NDC ndc_x = (x + 0.5) / width * 2 - 1 ndc_y = (y + 0.5) / height * 2 - 1 # Adjust for aspect ratio ndc_x *= aspect_ratio # Create ray from camera through pixel ray = Ray(Vec3(0, 0, 0), Vec3(ndc_x, ndc_y, -1)) # Trace ray color = trace_ray(ray, scene_objects, light) # Store color (clamped to [0,1]) image[y, x, 0] = min(1, max(0, color.x)) image[y, x, 1] = min(1, max(0, color.y)) image[y, x, 2] = min(1, max(0, color.z)) plt.imshow(image) plt.axis('off') plt.show() # Render a 400x300 image render(400, 300) ``` ## Features: 1. **Vector mathematics** (`Vec3` class) for all 3D operations 2. **Ray casting** from camera through each pixel 3. **Sphere and Plane** primitive objects with intersection tests 4. **Point light** with diffuse shading and shadow rays 5. **Minimal scene** with a diffuse sphere and ground plane ## Extensibility Points: - Add more objects (triangles, meshes) - Implement reflections/refractions - Add materials with different properties - Support multiple lights - Add anti-aliasing - Optimize with acceleration structures The output will display an image with a red sphere hovering above a green ground plane, illuminated by a single point light source.
Drag to resize
**Minimal Ray Tracer Implementation** ### Overview This is a basic implementation of a ray tracer in Python. The code is designed to be minimal, yet extensible. It renders a single image of a 3D scene containing a diffuse sphere, a plane (ground), and a point light source. ### Code ```python import numpy as np from PIL import Image # Vector3D Class class Vector3D: def __init__(self, x, y, z): self.x = x self.y = y self.z = z def __add__(self, other): return Vector3D(self.x + other.x, self.y + other.y, self.z + other.z) def __sub__(self, other): return Vector3D(self.x - other.x, self.y - other.y, self.z - other.z) def __mul__(self, scalar): return Vector3D(self.x * scalar, self.y * scalar, self.z * scalar) def __truediv__(self, scalar): return Vector3D(self.x / scalar, self.y / scalar, self.z / scalar) def dot(self, other): return self.x * other.x + self.y * other.y + self.z * other.z def magnitude(self): return np.sqrt(self.x**2 + self.y**2 + self.z**2) def normalize(self): magnitude = self.magnitude() return Vector3D(self.x / magnitude, self.y / magnitude, self.z / magnitude) # Ray Class class Ray: def __init__(self, origin, direction): self.origin = origin self.direction = direction # Material Class class Material: def __init__(self, color, albedo): self.color = color self.albedo = albedo # Sphere Class class Sphere: def __init__(self, center, radius, material): self.center = center self.radius = radius self.material = material def intersect(self, ray): L = self.center - ray.origin tca = L.dot(ray.direction) if tca < 0: return None d2 = L.dot(L) - tca * tca if d2 > self.radius * self.radius: return None thc = np.sqrt(self.radius * self.radius - d2) t0 = tca - thc t1 = tca + thc return min(t0, t1) # Plane Class class Plane: def __init__(self, point, normal, material): self.point = point self.normal = normal self.material = material def intersect(self, ray): denominator = self.normal.dot(ray.direction) if denominator == 0: return None t = (self.point - ray.origin).dot(self.normal) / denominator if t < 0: return None return t # Light Class class Light: def __init__(self, position, intensity): self.position = position self.intensity = intensity # Scene Class class Scene: def __init__(self): self.objects = [] self.lights = [] def add_object(self, object): self.objects.append(object) def add_light(self, light): self.lights.append(light) def intersect(self, ray): closest_t = float('inf') closest_object = None for object in self.objects: t = object.intersect(ray) if t is not None and t < closest_t: closest_t = t closest_object = object return closest_object, closest_t # Ray Tracer Function def ray_trace(ray, scene, max_depth): if max_depth == 0: return Vector3D(0, 0, 0) object, t = scene.intersect(ray) if object is None: return Vector3D(0, 0, 0) intersection_point = ray.origin + ray.direction * t normal = None if isinstance(object, Sphere): normal = (intersection_point - object.center).normalize() elif isinstance(object, Plane): normal = object.normal color = Vector3D(0, 0, 0) for light in scene.lights: light_direction = (light.position - intersection_point).normalize() light_distance = (light.position - intersection_point).magnitude() light_ray = Ray(intersection_point, light_direction) light_object, light_t = scene.intersect(light_ray) if light_object is None or light_t >= light_distance: color = color + object.material.color * light.intensity * max(0, normal.dot(light_direction)) * object.material.albedo return color # Main Function def main(): # Create scene scene = Scene() # Create materials sphere_material = Material(Vector3D(1, 0, 0), 0.8) plane_material = Material(Vector3D(0.5, 0.5, 0.5), 0.8) # Create objects sphere = Sphere(Vector3D(0, 1, -5), 1, sphere_material) plane = Plane(Vector3D(0, 0, 0), Vector3D(0, 1, 0), plane_material) # Create lights light = Light(Vector3D(5, 5, 5), 1) # Add objects and lights to scene scene.add_object(sphere) scene.add_object(plane) scene.add_light(light) # Render image width, height = 800, 600 image = Image.new('RGB', (width, height)) pixels = image.load() for x in range(width): for y in range(height): ray_direction = Vector3D((x - width / 2) / width * 2, (y - height / 2) / height * 2, -1).normalize() ray = Ray(Vector3D(0, 0, 0), ray_direction) color = ray_trace(ray, scene, 5) color = (int(color.x * 255), int(color.y * 255), int(color.z * 255)) pixels[x, y] = color image.save('render.png') if __name__ == "__main__": main() ``` ### Explanation 1. We start by defining a `Vector3D` class to represent 3D vectors. 2. We define a `Ray` class to represent a ray with an origin and direction. 3. We define `Material`, `Sphere`, and `Plane` classes to represent objects in the scene. 4. We define a `Light` class to represent a point light source. 5. We define a `Scene` class to manage objects and lights. 6. The `ray_trace` function implements the ray tracing algorithm, which recursively traces rays through the scene. 7. In the `main` function, we create a scene, add objects and lights, and render an image using the ray tracing algorithm. ### Usage 1. Run the script using `python ray_tracer.py`. 2. The rendered image will be saved as `render.png` in the same directory. Note: This is a basic implementation and can be improved by adding more features, such as texture mapping, ambient occlusion, and motion blur.
Drag to resize
To create a minimal yet extensible ray tracer, we'll implement a simplified system that renders a scene with a diffuse sphere, a ground plane, and a point light source. The solution uses object-oriented principles for easy extension. Below is the final code: ### **ray_tracer.py** ```python import numpy as np from numpy.linalg import norm class Vector3: def __init__(self, x, y, z): self.x, self.y, self.z = x, y, z def __sub__(self, other): return Vector3(self.x - other.x, self.y - other.y, self.z - other.z) def __add__(self, other): return Vector3(self.x + other.x, self.y + other.y, self.z + other.z) def __mul__(self, scalar): return Vector3(self.x * scalar, self.y * scalar, self.z * scalar) def dot(self, other): return self.x * other.x + self.y * other.y + self.z * other.z def cross(self, other): return Vector3( self.y * other.z - self.z * other.y, self.z * other.x - self.x * other.z, self.x * other.y - self.y * other.x ) def normalized(self): magnitude = norm([self.x, self.y, self.z]) return Vector3(self.x/magnitude, self.y/magnitude, self.z/magnitude) class Ray: def __init__(self, origin, direction): self.origin = origin self.direction = direction.normalized() class Light: def __init__(self, position, color): self.position = position self.color = color class Surface: def hit(self, ray): pass # To be implemented by subclasses class Plane(Surface): def __init__(self, point, normal, color): self.point = point self.normal = normal.normalized() self.color = color def hit(self, ray): denom = self.normal.dot(ray.direction) if abs(denom) > 1e-6: v = self.point - ray.origin t = v.dot(self.normal) / denom if t > 0: hit_point = ray.origin + ray.direction * t return t, self.normal, self.color return None class Sphere(Surface): def __init__(self, center, radius, color): self.center = center self.radius = radius self.color = color def hit(self, ray): oc = ray.origin - self.center a = ray.direction.dot(ray.direction) b = 2.0 * oc.dot(ray.direction) c = oc.dot(oc) - self.radius * self.radius discriminant = b*b - 4*a*c if discriminant < 0: return None t = (-b - np.sqrt(discriminant)) / (2.0 * a) if t < 0: return None hit_point = ray.origin + ray.direction * t normal = (hit_point - self.center).normalized() return t, normal, self.color class Camera: def __init__(self, origin, target, up, fov=90): self.origin = origin self.forward = (target - origin).normalized() self.right = self.forward.cross(up).normalized() self.up = self.right.cross(self.forward) self.fov = np.radians(fov) def get_ray(self, u, v): dx = np.tan(self.fov / 2) * (2 * u - 1) dy = np.tan(self.fov / 2) * (1 - 2 * v) direction = self.forward + self.right * dx + self.up * dy return Ray(self.origin, direction.normalized()) class Scene: def __init__(self): self.objects = [] self.lights = [] def add_object(self, obj): self.objects.append(obj) def add_light(self, light): self.lights.append(light) def trace_ray(self, ray): closest_hit = None for obj in self.objects: hit = obj.hit(ray) if hit: t, normal, color = hit if closest_hit is None or t < closest_hit[0]: closest_hit = (t, normal, color) if not closest_hit: return np.array([0.0, 0.0, 0.0]) # Black background _, normal, obj_color = closest_hit total_color = np.array([0.0, 0.0, 0.0]) for light in self.lights: light_vec = light.position - (ray.origin + ray.direction * closest_hit[0]) light_dir = light_vec.normalized() diffuse_intensity = max(0, normal.dot(light_dir)) # Apply inverse square law for attenuation light_distance_sq = light_vec.dot(light_vec) attenuation = 1 / max(1, light_distance_sq) light_color = np.array(light.color) * (diffuse_intensity * attenuation) total_color += np.array(obj_color) * light_color return np.clip(total_color, 0, 1) # Clamp between 0 and 1 def save_ppm(image, filename): height, width, _ = image.shape with open(filename, 'w') as f: f.write(f"P3\n{width} {height}\n255\n") for i in range(height): for j in range(width): r, g, b = (image[i, j] * 255).astype(int) f.write(f"{r} {g} {b} \n") width, height = 400, 400 image = np.zeros((height, width, 3)) # Define scene scene = Scene() # Objects: blue sphere and a green ground plane scene.add_object(Sphere(Vector3(0, 0, -5), 1.0, [0.2, 0.2, 0.8])) scene.add_object(Plane(Vector3(0, -1, 0), Vector3(0, 1, 0), [0.1, 0.8, 0.2])) # Point light source scene.add_light(Light(Vector3(-3, 4, -4), [1.0, 1.0, 0.9])) # Setup camera camera_pos = Vector3(0, 0, 0) camera_target = Vector3(0, 0, -1) camera_up = Vector3(0, 1, 0) camera = Camera(camera_pos, camera_target, camera_up, fov=90) # Render for i in range(height): for j in range(width): u = j / width v = i / height ray = camera.get_ray(u, v) color = scene.trace_ray(ray) image[i, j] = color # Save as PPM save_ppm(image, "scene.ppm") print("Rendered image saved as scene.ppm") ``` ### **Solution Explanation** #### **Core Components** 1. **Vector3**: Represents 3D points, vectors, and colors with essential operations (addition, subtraction, dot/cross products, normalization). 2. **Ray**: Defined by an origin and a normalized direction vector. 3. **Light**: Position and color of a point light source. 4. **Surface**: Abstract base class for renderable objects. 5. **Plane**: Defined by a point on the plane and its normal. Implements ray intersection and returns hit distance, normal vector, and color. 6. **Sphere**: Defined by center and radius. Implements ray-sphere intersection and returns hit details. 7. **Camera**: Generates rays from the camera's position through each pixel. Uses a field-of-view (FOV) to simulate perspective. 8. **Scene**: Aggregates objects and lights. The `trace_ray` method computes the closest object hit by a ray and shades it using Lambertian reflectance. #### **Rendering Process** - **Setup**: Create a scene with a blue sphere, green ground plane, and a white-yellow point light. - **Camera**: Positioned at the origin looking along the negative z-axis with a 90-degree FOV. - **Ray Casting**: For each pixel, generate a ray and compute intersections with objects. - **Shading**: Uses Lambertian diffuse reflection. For each light, computes the diffuse intensity (dot product of surface normal and light direction) and attenuates light by distance squared. - **Output**: Colors are clamped to [0,1] and saved as a PPM image file. ### **Extensibility** - **New Shapes**: Add any new class inheriting from `Surface` that implements the `hit` method. - **Materials**: Replace the fixed-color approach with a material class that defines reflectance properties (e.g., diffuse, specular). - **Lights**: Support multiple lights. Add directional or area lights by extending the light calculations in `Scene.trace_ray`. - **Shadow Rays**: Extend `Scene.trace_ray` to check visibility between intersection points and light sources. This minimal ray tracer demonstrates core computer graphics principles while providing a foundation for expansion.
Drag to resize
I'll create a minimal but extensible ray tracer in Python. Here's a clean implementation that renders the requested scene: ```python import numpy as np from PIL import Image import math class Vec3: """3D Vector class with basic operations""" def __init__(self, x=0, y=0, z=0): self.x, self.y, self.z = x, y, z def __add__(self, other): return Vec3(self.x + other.x, self.y + other.y, self.z + other.z) def __sub__(self, other): return Vec3(self.x - other.x, self.y - other.y, self.z - other.z) def __mul__(self, scalar): return Vec3(self.x * scalar, self.y * scalar, self.z * scalar) def __rmul__(self, scalar): return self.__mul__(scalar) def dot(self, other): return self.x * other.x + self.y * other.y + self.z * other.z def length(self): return math.sqrt(self.dot(self)) def normalize(self): l = self.length() return Vec3(self.x / l, self.y / l, self.z / l) if l > 0 else Vec3() def reflect(self, normal): return self - normal * (2 * self.dot(normal)) class Ray: """Ray class with origin and direction""" def __init__(self, origin, direction): self.origin = origin self.direction = direction.normalize() def at(self, t): return self.origin + self.direction * t class Material: """Material properties""" def __init__(self, color, diffuse=0.8, specular=0.2, shininess=50): self.color = color self.diffuse = diffuse self.specular = specular self.shininess = shininess class HitRecord: """Stores intersection information""" def __init__(self, point=None, normal=None, t=float('inf'), material=None): self.point = point self.normal = normal self.t = t self.material = material class Sphere: """Sphere primitive""" def __init__(self, center, radius, material): self.center = center self.radius = radius self.material = material def intersect(self, ray): oc = ray.origin - self.center a = ray.direction.dot(ray.direction) b = 2 * oc.dot(ray.direction) c = oc.dot(oc) - self.radius * self.radius discriminant = b * b - 4 * a * c if discriminant < 0: return HitRecord() t = (-b - math.sqrt(discriminant)) / (2 * a) if t <= 0.001: # Avoid self-intersection t = (-b + math.sqrt(discriminant)) / (2 * a) if t <= 0.001: return HitRecord() point = ray.at(t) normal = (point - self.center).normalize() return HitRecord(point, normal, t, self.material) class Plane: """Infinite plane primitive""" def __init__(self, point, normal, material): self.point = point self.normal = normal.normalize() self.material = material def intersect(self, ray): denom = self.normal.dot(ray.direction) if abs(denom) < 0.0001: # Ray parallel to plane return HitRecord() t = (self.point - ray.origin).dot(self.normal) / denom if t <= 0.001: # Behind ray origin return HitRecord() point = ray.at(t) return HitRecord(point, self.normal, t, self.material) class PointLight: """Point light source""" def __init__(self, position, color, intensity=1.0): self.position = position self.color = color self.intensity = intensity class Scene: """Scene container""" def __init__(self): self.objects = [] self.lights = [] self.background_color = Vec3(0.1, 0.1, 0.2) def add_object(self, obj): self.objects.append(obj) def add_light(self, light): self.lights.append(light) def intersect(self, ray): closest_hit = HitRecord() for obj in self.objects: hit = obj.intersect(ray) if hit.t < closest_hit.t: closest_hit = hit return closest_hit if closest_hit.t < float('inf') else None class Camera: """Simple perspective camera""" def __init__(self, position, target, up, fov, aspect_ratio): self.position = position self.forward = (target - position).normalize() self.right = self.forward.cross(up).normalize() self.up = self.right.cross(self.forward) self.fov = fov self.aspect_ratio = aspect_ratio # Calculate viewport dimensions half_height = math.tan(math.radians(fov) / 2) half_width = aspect_ratio * half_height self.lower_left = (position + self.forward - half_width * self.right - half_height * self.up) self.horizontal = 2 * half_width * self.right self.vertical = 2 * half_height * self.up # Add cross product method to Vec3 def cross(self, other): return Vec3( self.y * other.z - self.z * other.y, self.z * other.x - self.x * other.z, self.x * other.y - self.y * other.x ) Vec3.cross = cross class RayTracer: """Main ray tracer""" def __init__(self, width=800, height=600): self.width = width self.height = height def trace_ray(self, scene, ray, depth=0, max_depth=5): if depth >= max_depth: return scene.background_color hit = scene.intersect(ray) if not hit: return scene.background_color color = Vec3(0, 0, 0) # Calculate lighting for light in scene.lights: light_dir = (light.position - hit.point).normalize() # Shadow ray shadow_ray = Ray(hit.point, light_dir) shadow_hit = scene.intersect(shadow_ray) # Check if point is in shadow light_distance = (light.position - hit.point).length() if shadow_hit and shadow_hit.t < light_distance: continue # In shadow # Diffuse lighting diffuse_intensity = max(0, hit.normal.dot(light_dir)) diffuse_color = (hit.material.color * light.color * hit.material.diffuse * diffuse_intensity * light.intensity) # Specular lighting view_dir = (ray.origin - hit.point).normalize() reflect_dir = (-light_dir).reflect(hit.normal) spec_intensity = max(0, view_dir.dot(reflect_dir)) ** hit.material.shininess specular_color = (light.color * hit.material.specular * spec_intensity * light.intensity) color = color + diffuse_color + specular_color return color def render(self, scene): # Create a simple camera camera_pos = Vec3(0, 2, 5) target = Vec3(0, 0, 0) up = Vec3(0, 1, 0) # Calculate camera basis vectors forward = (target - camera_pos).normalize() right = forward.cross(up).normalize() up = right.cross(forward) # Camera parameters fov = 45 aspect_ratio = self.width / self.height viewport_height = 2 * math.tan(math.radians(fov) / 2) viewport_width = aspect_ratio * viewport_height # Viewport corners horizontal = viewport_width * right vertical = viewport_height * up lower_left = camera_pos + forward - horizontal/2 - vertical/2 image_data = np.zeros((self.height, self.width, 3), dtype=np.uint8) for y in range(self.height): for x in range(self.width): # Calculate ray direction u = x / (self.width - 1) v = (self.height - 1 - y) / (self.height - 1) # Flip Y ray_target = lower_left + u * horizontal + v * vertical ray_direction = (ray_target - camera_pos).normalize() ray = Ray(camera_pos, ray_direction) color = self.trace_ray(scene, ray) # Gamma correction and clamping r = min(255, int(255 * math.sqrt(max(0, color.x)))) g = min(255, int(255 * math.sqrt(max(0, color.y)))) b = min(255, int(255 * math.sqrt(max(0, color.z)))) image_data[y, x] = [r, g, b] # Progress indicator if y % 50 == 0: print(f"Rendering... {y}/{self.height}") return Image.fromarray(image_data) def create_scene(): """Create the test scene""" scene = Scene() # Create materials red_material = Material(Vec3(0.8, 0.2, 0.2)) ground_material = Material(Vec3(0.5, 0.5, 0.5)) # Add sphere sphere = Sphere(Vec3(0, 0, 0), 1.0, red_material) scene.add_object(sphere) # Add ground plane ground = Plane(Vec3(0, -1, 0), Vec3(0, 1, 0), ground_material) scene.add_object(ground) # Add light light = PointLight(Vec3(2, 3, 3), Vec3(1, 1, 1), intensity=1.5) scene.add_light(light) return scene def main(): print("Creating scene...") scene = create_scene() print("Initializing ray tracer...") tracer = RayTracer(width=400, height=300) # Smaller for faster rendering print("Rendering...") image = tracer.render(scene) print("Saving image...") image.save("raytraced_scene.png") print("Done! Image saved as 'raytraced_scene.png'") if __name__ == "__main__": main() ``` This ray tracer includes: **Core Features:** - **Vec3 class** for 3D vector operations - **Ray class** for ray representation - **Material class** for surface properties - **Primitive objects**: Sphere and Plane - **Point light** with diffuse and specular lighting - **Scene management** with multiple objects and lights - **Shadow calculations** to prevent light bleeding **The rendered scene contains:** - ✅ One red diffuse sphere at the origin - ✅ A gray ground plane below the sphere - ✅ A point light source providing illumination - Proper shadows cast by the sphere onto the ground **Extensibility features:** - Easy to add new primitive types (just implement `intersect` method) - Material system supports different surface properties - Multiple lights supported - Scene graph structure for organizing objects - Modular design for easy feature additions **To run:** 1. Install required packages: `pip install numpy pillow` 2. Run the script: `python raytracer.py` 3. The output image will be saved as `raytraced_scene.png` **Easy extensions you can add:** - More primitives (planes, triangles, etc.) - Reflections and refractions - Different light types (directional, area lights) - Textures and procedural materials - Anti-aliasing - Different camera models The ray tracer uses proper lighting calculations with shadow testing and produces a clean rendered image of the requested scene.
Drag to resize