A ray tracer written from scratch in pure Java - no graphics libraries, just math and careful design.
We built this engine in a university software engineering course and kept going after it ended. It renders glass that bends light the way real glass does, soft shadows, depth of field and glossy reflections, and it handles models with hundreds of thousands of triangles thanks to two spatial indexes and multithreading.
Before and after
Anti-aliasing - one ray per pixel against 121 jittered rays per pixel:
Bloom - bright highlights spill a soft glow over their surroundings:
Light and materials - recursive ray tracing with Phong shading; glass with refraction, total internal reflection and Fresnel reflectance; glossy and frosted surfaces; soft shadows from area lights; shadows tinted by the glass they pass through; depth of field.
Geometry - spheres, planes, polygons, triangles, tubes and cylinders; smooth-shaded meshes; surfaces of revolution; textured OBJ/MTL models; scenes described in XML.
Speed and image quality - a regular grid and an adaptive octree; multithreading; jittered and adaptive super-sampling; bloom, FXAA, color grading and sharpening filters.
flowchart TD
A["Camera.renderImage()"] --> B["Pixels are handed out to worker threads"]
B --> C{"Adaptive<br/>super-sampling?"}
C -- yes --> D["Quadtree: refine only where<br/>the colors in a pixel differ"]
C -- no --> E["Jittered N×N samples"]
D --> F["Primary rays, leaving from a lens<br/>aperture when depth of field is on"]
E --> F
F --> G["Closest hit from the acceleration structure:<br/>linear scan · regular grid · octree"]
G -- miss --> H["Background"]
G -- hit --> I["Direct light: emission + diffuse + specular,<br/>through soft, tinted shadows"]
I --> J{"Reflective or<br/>transparent?"}
J -- yes --> K["Reflected and refracted rays<br/>(Snell's law, Fresnel, glossy scattering)"]
K --> G
J -- no --> L["Average the samples"]
H --> L
L --> M["Post-processing: bloom · color grading · FXAA"]
M --> N["PNG"]
Each pixel is sampled by several rays. A ray asks the acceleration structure for its closest hit, gathers the light that reaches that point, and spawns reflected and refracted rays until their contribution fades.
The scene - every shape is an Intersectable, and so is a group of shapes:
classDiagram
direction TB
class Intersectable {
<<abstract>>
+calcIntersections(Ray, double) List~Intersection~
+getBoundingBox() AABB
}
class Geometry {
<<abstract>>
+getNormal(Point) Vector
+getEmission(Point) Color
}
class LightSource {
<<interface>>
+getIntensity(Point) Color
+getL(Point) Vector
+getRadius() double
}
Intersectable <|-- Geometry
Intersectable <|-- Geometries
Geometries o-- "*" Intersectable
Geometry <|-- Sphere
Geometry <|-- Plane
Geometry <|-- Polygon
Geometry <|-- Cylinder
Polygon <|-- Triangle
Triangle <|-- SmoothTriangle
Triangle <|-- TexturedTriangle
Lathe ..> SmoothTriangle : builds
LightSource <|.. DirectionalLight
LightSource <|.. PointLight
PointLight <|-- SpotLight
Scene o-- Geometries
Scene o-- "*" LightSource
The renderer - each concern sits behind its own abstraction:
classDiagram
direction TB
class Camera {
+getBuilder()$ Builder
+renderImage() Camera
+writeToImage(String)
}
class AccelerationStructure {
<<interface>>
+findClosestIntersection(Ray) Intersection
+traverse(Ray, double, IntersectionVisitor)
}
class PostProcessFilter {
<<interface>>
+apply(BufferedImage) BufferedImage
}
Builder ..> Camera : builds
Camera --> RayTracerBase
Camera --> TargetArea
Camera --> PostProcessingEngine
TargetArea --> SamplingPattern
RayTracerBase <|-- SimpleRayTracer
SimpleRayTracer --> AccelerationStructure
AccelerationStructure <|.. LinearScan
AccelerationStructure <|.. RegularGrid
AccelerationStructure <|.. AdaptiveGrid
PostProcessingEngine o-- "*" PostProcessFilter
| Pattern | Where | Why |
|---|---|---|
| Builder | Camera.Builder |
Many optional settings, validated once |
| Strategy | AccelerationStructure, SamplingPattern |
Swap the spatial index or sampling scheme without touching the shading code |
| Composite | Geometries |
A model or a whole scene is intersected like a single shape |
| Template Method | Intersectable.calcIntersections, PixelFilter.apply |
The base class fixes the contract, subclasses fill in one step |
| Visitor | AccelerationStructure.IntersectionVisitor |
Shadow rays stop as soon as the light is blocked |
| Pipeline | PostProcessingEngine |
Filters compose in any order |
Single thread, one ray per pixel, on an Intel Core i5-13420H laptop. All three strategies render identical images.
| Scene | Objects | Image | Linear scan | Regular grid | Octree |
|---|---|---|---|---|---|
| Utah teapot + 400 glass bubbles | 1,392 | 1000×1000 | 23.0 s | 0.89 s | 1.15 s |
| Laptop on a glass table | 130,351 | 400×400 | 43.6 min | 0.83 s | 0.98 s |
On top of that, ten threads render 6.7× faster than one, and adaptive super-sampling takes half the time of a fixed 5×5 grid.
- 132 JUnit tests, designed with equivalence partitioning and boundary value analysis, cover the math, every shape, lights, the camera, sampling, filters, the scene loaders and reference renders.
- The grid and the octree are checked against brute force on thousands of random rays.
- The XML loader is tested against XXE attacks.
- GitHub Actions builds and tests every push. Heavy renders and benchmarks are kept in a separate
showcasegroup.
You need JDK 25 and Maven 3.9 or later.
git clone https://github.com/itzik123/ISE5786_8332_7818.git
cd ISE5786_8332_7818
mvn testRendered images are written to images/. To render one of the showcase scenes:
mvn test -Pshowcase -Dtest=SceneShowcaseTest#testGlassCubeThe animation is rendered the same way, one PNG per frame in images/checkmate:
mvn test -Pshowcase -Dtest=CheckmateAnimationTestScene scene = new Scene("Demo").setAmbientLight(new AmbientLight(new Color(25, 25, 30)));
scene.geometries.add(
new Sphere(new Point(0, 0, -100), 50)
.setEmission(new Color(20, 40, 120))
.setMaterial(new Material().setKd(0.5).setKs(0.5).setShininess(100).setKr(0.2)),
new Plane(new Point(0, -50, 0), Vector.AXIS_Y)
.setEmission(new Color(30, 30, 35))
.setMaterial(new Material().setKd(0.5).setKr(0.4)
.setBlurRadius(0.05).setBlurGlassRays(16))); // glossy floor
scene.lights.add(new SpotLight(new Color(900, 700, 500),
new Point(100, 150, 50), new Vector(-1, -1.5, -1.5)).setKl(0.0005).setKq(0.00005));
Camera.getBuilder()
.setRayTracer(scene, RayTracerType.ADAPTIVE_GRID)
.setLocation(new Point(0, 40, 400)).setDirection(new Point(0, 0, -100))
.setVpSize(200, 200).setVpDistance(400)
.setResolution(800, 800)
.setAntiAliasing(3, new Jittered()) // 3×3 rays per pixel
.setMultithreading(-2) // all cores but two
.addPostProcessFilter(new BloomFilter())
.build()
.renderImage()
.writeToImage("demo"); // images/demo.pngScenes can also be loaded from XML files - see scenes/.
Built in the Introduction to Software Engineering course (ISE 5786). The course lecturer, Dan
Zilberstein, provided the overall design and a few base classes, which keep his @author tag.
The HP laptop and E-45 aircraft models are by 3dhaupt and the cottage is by gerhald3d, all from
Free3D, and the workstation is a free model by its original author. These models are free for
personal use and are not included in this repository. The
teapot is Martin Newell's 1975 Utah teapot.
Released under the MIT License.








