{"id":17772133,"url":"https://github.com/dranikpg/simple-rays","last_synced_at":"2026-03-09T06:31:24.449Z","repository":{"id":111494709,"uuid":"338408522","full_name":"dranikpg/simple-rays","owner":"dranikpg","description":"Simple ray tracer written in Rust","archived":false,"fork":false,"pushed_at":"2021-02-12T20:15:41.000Z","size":1180,"stargazers_count":195,"open_issues_count":1,"forks_count":7,"subscribers_count":7,"default_branch":"main","last_synced_at":"2025-07-14T00:10:29.783Z","etag":null,"topics":["ray-tracing","rust"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dranikpg.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-02-12T19:07:05.000Z","updated_at":"2025-01-13T08:01:20.000Z","dependencies_parsed_at":"2023-03-13T13:39:53.699Z","dependency_job_id":null,"html_url":"https://github.com/dranikpg/simple-rays","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/dranikpg/simple-rays","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dranikpg%2Fsimple-rays","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dranikpg%2Fsimple-rays/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dranikpg%2Fsimple-rays/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dranikpg%2Fsimple-rays/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dranikpg","download_url":"https://codeload.github.com/dranikpg/simple-rays/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dranikpg%2Fsimple-rays/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30284774,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-09T02:57:19.223Z","status":"ssl_error","status_checked_at":"2026-03-09T02:56:26.373Z","response_time":61,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ray-tracing","rust"],"created_at":"2024-10-26T21:38:04.444Z","updated_at":"2026-03-09T06:31:24.201Z","avatar_url":"https://github.com/dranikpg.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"## Simple ray tracer written in Rust from scratch\n\nI've just finished my first semester at the Faculty of ~~Applied~~\nMathematics ~~and Computer Science~~ at the Belarusian SU. I missed conventional\nprogramming a bit and I'm curious to see how I can apply any of my new\nknowledge. Finding exciting yet widely applied and simple topic for a side\nproject turned out to be not that simple. Linear algebra is broadly used in\ncomputer graphics, where I previously came across matrices, transformations and\nprojections when working with OpenGL, but back then I had only a somewhat\nshallow understanding of those topics. I've never done any *ray tracing*, so my\nchoice simply fell on it. Besides that it somewhat overhyped now and has very\nlittle to almost no theory - just vectors and geometry. I haven't read any posts\nor books specifically on ray tracing, so my approach surely won't be the fastest\nor most optimal, but I think it will be quite interesting to come up with a\nworking one.\n\nI've chosen Rust 🦀 because it's my favourite programming language and I haven't\nused it for a while. So let's start!\n\n### Everything starts with a ray\n\nI've defined some utils for working with floating point numbers, a Point and\nVector struct and some ~~ugly~~ convenient macros for creating vectors and\npoints. I first used `f32` for all calculations, but then found out that\nprecision degrades very quickly, especially for extremely sharp angles. At that\npoint I had already marked the Point struct as `Copy`, so I kept the marker,\neven though its new size of 24 bytes exceeded twice the machine word. This is\njust a general rule of thumb and I bet it has almost no effect in this case, as\nthe average number of arguments in generally low.\n\nRaytracing has obviously something to do with casting rays. A ray is just a line\nwith a positive direction. A line in space can be defined by a point and a\ndirection vector. What matters is that we can describe every point on this line\nwith a single number.\n\n```rust\nimpl Line {\n    pub fn at(\u0026self, t: f64) -\u003e Point {\n        self.origin + t * self.direction\n    }\n}\n```\n\nTo create an image we have to find out how to intersect rays with objects in\nspace. Many might associate ray tracing with spheres, because they nicely\ndemonstrate many visual effects. But I won't cover any of those effects. So\nwe'll focus just on triangles. Besides that triangles can be used to approximate\nalmost any shape and 3d models consists of them.\n**But how do we find the intersection of a line and a triangle?**\n\n### The plane\n\nFirst, we have to define the plane which contains the triangle. A plane can be\ndefined by a single point and a _normal vector_. The normal vector is actually\nthe cross product of any two vectors in the plane.\n\n\u003cdiv style=\"width:400px\"\u003e\n\n![Plane](images/sc_plane.png)\n\n\n\u003c/div\u003e\n\nSo lets implement the _cross product_ for vectors:\n\n```rust\nimpl Vector {\n    pub fn cross(\u0026self, v2: Vector) -\u003e Vector {\n        Vector {\n            x: (self.y * v2.z - self.z * v2.y),\n            y: -(self.x * v2.z - self.z * v2.x),\n            z: (self.x * v2.y - self.y * v2.x),\n        }\n    }\n}\n```\n\nThen we can turn our point-normal pair into a well known equation\n`Ax + By + Cz + D = 0`, so our Plane constructor looks like:\n\n```rust\nimpl Plane {\n    pub fn new(p: Point, v1: Vector, v2: Vector) -\u003e MathResult\u003cSelf\u003e {\n        match v1.cross(v2) {\n            v if v.is_zero() =\u003e Err(MathError::CollinearVectors),\n            Vector { x, y, z } =\u003e Ok(Plane {\n                a: x,\n                b: y,\n                c: z,\n                d: -(x * p.x + y * p.y + z * p.z),\n            })\n        }\n    }\n}\n```\n\nTo find the intersection of a line and a plane, we can express the coordinates\nof all points on the line in terms of our \"line parameter\", and then substitute\nthose relations into the planes equation. By solving the equation for the \"line\nparameter\" we get the point of intersection.\n\n### Does the triangle contain it?\n\nThere are many ways to determine whether a triangle contains a point in two\ndimensions, so I first thought of equivalent approaches: introducing a two\ndimensional coordinate system on the plane or looking for intersections with the\ntriangles sides.\n\nOne approach is based on the fact, that if a point lies inside a triangle, then\nit lies in the same half plane for each side of the triangle (right or left half\nplane, depends on the order of traversal). We can generalize this property for\nthe third dimension be reviewing one other property for vectors on the plane:\nthe cross product of a vector with all vectors pointing right of it will point\nupwards, and for all vectors left of it - downwards (or downwards/upwards if the\norder of vectors in the cross product is flipped).\n\n\u003cdiv style=\"width:300px;\"\u003e\n\n![vector halves](images/sc_vec_halves.png)\n\n\u003c/div\u003e\n\nSo if a point lies inside a triangle, then all cross products of each side with\nthe corresponding vector, connecting the vertex and the point of intersection,\npoint in the same direction:\n\n\u003cdiv style=\"display:flex; justify-content: space-around; \"\u003e\n\n\u003cdiv style=\"width:400px; \"\u003e\n\n![triangle 1](images/sc_tri1.png)\n\n\u003c/div\u003e\n\n\n\u003cdiv style=\"width:400px;\"\u003e\n\n![triangle 2](images/sc_tri2.png)\n\n\u003c/div\u003e\n\n\u003c/div\u003e\n\n\nThat results in a simple implementation using the cross product that we already\ndefined:\n\n```rust\nimpl Triangle {\n    fn is_inside(\u0026self, pt: Point) -\u003e bool {\n        self.vertices.iter().enumerate()\n            // calculate the cross products for each vertex\n            .map(|(pos, vertex)| -\u003e Vector {\n                let next_vertex = self.vertices[(pos + 1) % 3];\n                vector!(cross   vector!(vertex, pt),\n                                vector!(vertex, next_vertex))\n            })\n            // check if each vector is codirectional with the sum of the previous ones\n            // that is equivalent to all of them being pairwise codirectional\n            .fold(Some(vector!()), |last_opt, v| -\u003e Option\u003cVector\u003e {\n                match last_opt {\n                    Some(last) =\u003e if v.is_codirectional(last) { Some(v + last) } else { None }\n                    None =\u003e None,\n                }\n            }).is_some()\n    }\n}\n```\n\nA ray intersects a triangle only if it intersects the plane containing the\ntriangle and the point of intersection lies inside the triangle. Now we know how\nto verify both conditions, so lets move on to *casting rays*.\n\n### How to cast rays?\n\nTo create an image, we have to cast rays from an \"eye\" (or camera, or origin)\nthrough an imaginary grid. Finding the best approach to generate such a grid\nturned out to be an interesting task. For the sake of simplicity we'll say that\nour eye always looks at the origin and its rotation is locked. If it were to be\nan airplane, we'd say that its roll is always zero :)\n\n![Airplane](https://upload.wikimedia.org/wikipedia/commons/thumb/c/c1/Yaw_Axis_Corrected.svg/250px-Yaw_Axis_Corrected.svg.png)\n\nLets also say, that the global Y axis is the one that points \"up\". That means,\nthat if the angle between the eye and the Oxz plane is not 90 degrees (a\ntop-down projection), then \"up\" on the image always points \"up\" in the 3d world.\nNow we can see that the plane formed by the \"eye\" ray and the Y axis will always\nbe perpendicular to the Oxz plane.\n\nGiven those relations, there is a simple way to define the imaginary grid plane\nwith a 2d coordinate system fixed at the global origin:\n\n* Because our rotation is always zero, our grid will be perpendicular to the\n  \"eye\"-Y axis plane, so we can define our *local X axis* (Vx on the image) as\n  the cross product of the Y axis and the \"eye\" ray.\n* Our *local Y axis* (Vy on the image) can be defined as the cross product\n  between the \"eye\" ray and the local X axis.\n\n![](images/sc_ray.png)\n\nBecause I want the axes to point how I'm used to, ~~we~~ I have to \"flip\" x and\ny when converting image coordinates into rays:\n\n```rust\nfn create_ray(env: \u0026Environment, (x, y): (u32, u32)) -\u003e Line {\n    let interpolated = |cur: u32, max: u32| -\u003e f64 {\n        2f64 * (cur as f64 / max as f64) - 1f64\n    };\n    let vx = vector!(cross env.origin, vector!(axis y)).normalized();\n    let vy = vector!(cross env.origin, vx).normalized();\n    let pt = vector!()\n        + interpolated(y, IMAGE_SIZE.1) * env.grid_size * vx\n        + interpolated(x, IMAGE_SIZE.0) * env.grid_size * vy;\n    Line {\n        direction: vector!(env.origin, pt),\n        origin: env.origin,\n    }\n}\n```\n\n### Turn the lights on!\n\nWe already know how to find intersections of lines and triangles. But what about\nthe _brightness_? In our world all objects will be opaque and there will be only\none source of light - the sun. To find out whether a point on a triangle is lit\nby the sun, we have to cast another ray. If the ray from the intersection point\nto the sun intersects any other triangle, then our pixel is covered by a shadow.\nBecause we don't consider on which side the \"eye\" ray intersected the plane, we\nhave to make sure that both the sun and the \"eye\" are in the same half-space,\nbounded by the surface plane. Without this check both sides of each triangle\nwould have the exact same lighting, which is obviously not true for\nnon-transparent shapes. Two points are in the same half-space if their\ncorresponding values for the plane formula are of the same sign.\n\nAs far as I know, real raytracing casts many more rays to compute reflections,\nrefractions, scattering etc. Instead, I decided to implement something *similar*\nto the _Phong shading_ model, which I'm familiar with from OpenGL. In this\nmodel, the brightness of a \"pixel\" consists of its _ambient_, _diffuse_ and\nspecular components:\n\n* Every pixel is at least as bright as the _ambient_ brightness, even if it lies\n  in a shadow\n* A pixel is brighter if it directly faces the sun, and darker if the angle\n  between the surface normal and the sun ray is greater. How much this\n  brightness varies in relation to the angle is the defined by the _diffuse_\n  part.\n* Specular lighting indicates how rough or even a material is\n  (the small bright spot on spheres), but it won't be used here\n\nInstead of finding the angle between the surface normal and the \"sun\" ray, we'll\nfind just the cosine. The closer the absolute value is to 1, the closer the\nnormal is to being aligned with the \"sun\" ray. We can find the cosine using the\ndot product.\n\nIn our case we will use the ambient color only for covered pixels.\n\n```rust\nfn compute_lights(env: \u0026Environment, surface: \u0026Triangle, pt: Point) -\u003e f32 {\n    let sun_ray = Line {\n        direction: vector!(pt, env.sun),\n        origin: pt,\n    };\n    let covered = env.surfaces.iter()\n        .filter(|sf| !sf.triangle.contains(pt))\n        .map(|sf| sf.triangle.intersect(\u0026sun_ray))\n        // check if any intersection lies on the positive direction of the ray\n        .any(|opt| opt.map(|t| t \u003e= -FLOAT_EPS).unwrap_or(false));\n    let different_halves = surface.plane.subs(env.origin)\n        * surface.plane.subs(env.sun) \u003c= 0.0;\n    if covered || different_halves {\n        env.ambient_light\n    } else {\n        let normal = surface.plane.normal();\n        let cos = sun_ray.direction.cos(normal).abs() as f32;\n        (1.0 - env.diffuse_light) + cos * env.diffuse_light\n    }\n}\n```\n\n### Lets cast finally\n\nIt's pretty clear that from all the intersections, we have to discard those,\nwhich are behind our \"eye\", and choose the closest one from the remaining. Then\nwe compute the brightness at the point of intersection and multiply it by the\nsurface color:\n\n```rust\nfn cast_ray(env: \u0026Environment, ray: \u0026Line) -\u003e [u8; 3] {\n    let intersection_opt = env.surfaces.iter()\n        .map(|sf: \u0026ColoredSurface| sf.triangle.intersect(ray).map(|t| (t, sf)))\n        .filter(Option::is_some).map(Option::unwrap)\n        // check if it lies on the positive direction of the ray\n        .filter(|is| is.0 \u003e= -FLOAT_EPS)\n        // find closest to the origin\n        .min_by(|a, b| a.0.partial_cmp(\u0026b.0).unwrap());\n    if let Some((ray_param, surface)) = intersection_opt {\n        let brightness = compute_lights(\u0026env, \u0026surface.triangle, ray.at(ray_param));\n        surface.color.iter()\n            .map(|c| (*c as f32 * brightness) as u8).try_collect().unwrap()\n    } else {\n        VOID_COLOR\n    }\n}\n```\n\n### Last steps\n\nNow we know how to cast rays, but how do we generate an image? The easiest way\nis just to save the ray casting results in an byte array (RBG, 3 bytes per\npixel) and then convert it into a png. We'd also like to import 3d models to\nbuild more complex shapes. Oh, and using Rust and not going multi-threaded would\nbe kind of lame.\n\nI'm using:\n\n* The [image ](https://crates.io/crates/image) crate to write the color array to\n  a png file.\n* [obj-rs](https://crates.io/crates/obj-rs) to parse wavefront obj files and\n  turn them into triangles.\n* [scoped_threadpool](https://crates.io/crates/scoped_threadpool) to avoid\n  cluttering the code with `Arc`s when they're unnecessary\n* And of course the good old [num_cpus](https://crates.io/crates/num_cpus)\n\n### Results:\n\nThe results might seem not that impressive visually, but we're actually\nrendering 3d models (with shadows!) in less than just 400 lines of pure Rust\nwithout any graphics or maths libraries.\n\nLow poly wavefront from [Kenney](https://kenney.nl/assets/pirate-kit):\n\n![Example](images/att7.gif)\n\nMoving sun:\n\n![Example 1](images/att3.gif)\n\nBy using sine in the interpolation function in `create_ray`, we can create some\nspace curvature :)\n\n![Example 2](images/att4.gif)\n\nMy first three triangles (and an odd rotation bug):\n\n![Example 4](images/att1.gif)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdranikpg%2Fsimple-rays","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdranikpg%2Fsimple-rays","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdranikpg%2Fsimple-rays/lists"}