diff --git a/RayTracer/shape/Triangle.cpp b/RayTracer/shape/Triangle.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..9aa2e942bd29e42c5873d97173477a772bff170a
--- /dev/null
+++ b/RayTracer/shape/Triangle.cpp
@@ -0,0 +1,65 @@
+#pragma once
+
+#include "Triangle.h"
+
+namespace shapes {
+Triangle::Triangle(util::Vertex p1, util::Vertex p2, util::Vertex p3,
+                   const std::shared_ptr<material::Material>& material)
+    : p1(p1), p2(p2), p3(p3), material(material) {
+	recalculateBB();
+}
+
+util::AxisAlignedBoundingBox Triangle::bounds() const {
+	return bb;
+}
+void Triangle::recalculateBB() {
+	const util::Vec3 xx = p1.position;
+	const util::Vec3 yy = p2.position;
+	const util::Vec3 zz = p3.position;
+	const util::Vec3 minBound =
+	    util::Vec3(std::min<float>({xx.x(), yy.x(), zz.x()}),
+	               std::min<float>({xx.y(), yy.y(), zz.y()}),
+	               std::min<float>({xx.z(), yy.z(), zz.z()}));
+	const util::Vec3 maxBound =
+	    util::Vec3(std::max<float>({xx.x(), yy.x(), zz.x()}),
+	               std::max<float>({xx.y(), yy.y(), zz.y()}),
+	               std::max<float>({xx.z(), yy.z(), zz.z()}));
+	bb = util::AxisAlignedBoundingBox(minBound, maxBound);
+}
+
+std::optional<cam::Hit> Triangle::intersect(const cam::Ray& r) const {
+	util::Vec3 e1 = p2.position - p1.position;
+	util::Vec3 e2 = p3.position - p1.position;
+	util::Vec3 pvec = util::cross(r.d, e2);
+	float det = util::dot(e1, pvec);
+	if (det < cam::epsilon) return std::nullopt;
+
+	util::Vec3 tvec = r.x0 - p1.position;
+	float u = util::dot(tvec, pvec) / det;
+	if (u < 0 || u > 1) return std::nullopt;
+
+	util::Vec3 qvec = util::cross(tvec, e1);
+	float v = util::dot(r.d, qvec) / det;
+	if (v < 0 || u + v > 1) return std::nullopt;
+
+	float t = util::dot(e2, qvec) / det;
+	util::Vec3 hit = r.x0 + r.d * t;
+
+	float w = 1 - u - v;
+	/*
+	std::cout << p1().normal << std::endl;
+	std::cout << p2().normal << std::endl;
+	std::cout << p3().normal << std::endl;
+	std::cout << u << std::endl;
+	std::cout << v << std::endl;
+	std::cout << w << std::endl;*/
+	// auto bary_normal =
+	//    (u * p1().normal + v * p2().normal + w * p3().normal).normalize();
+	auto cross_normal =
+	    util::cross(p2.position - p1.position, p3.position - p1.position)
+	        .normalize();
+	// if (util::dot(bary_normal, cross_normal) < 0)
+	//	std::cout << "Hm" << std::endl;
+	return std::optional<cam::Hit>(cam::Hit(hit, cross_normal, t, material));
+}
+}  // namespace shapes
diff --git a/RayTracer/shape/Triangle.h b/RayTracer/shape/Triangle.h
new file mode 100644
index 0000000000000000000000000000000000000000..03d5e98e469e1ef2fa9f1d65c9111af62bca4324
--- /dev/null
+++ b/RayTracer/shape/Triangle.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include <memory>
+#include <optional>
+
+#include "../camera/Hit.h"
+#include "../tools/AxisAlignedBoundingBox.h"
+#include "../tools/Vertex.h"
+
+namespace shapes {
+class Triangle {
+   public:
+	Triangle(util::Vertex p1, util::Vertex p2, util::Vertex p3,
+	         const std::shared_ptr<material::Material>& material);
+	std::optional<cam::Hit> intersect(const cam::Ray& r) const;
+	util::AxisAlignedBoundingBox bounds() const;
+	void recalculateBB();
+
+	util::SurfacePoint sampleLight() const;
+	util::Vec3 calculateLightEmission(const util::SurfacePoint& p,
+	                                  const util::Vec3& d) const;
+
+	const std::shared_ptr<material::Material>& material;
+
+   private:
+	const util::Vertex p1;
+	const util::Vertex p2;
+	const util::Vertex p3;
+	util::AxisAlignedBoundingBox bb;
+};
+}  // namespace shapes
diff --git a/RayTracer/shape/TriangleMesh.cpp b/RayTracer/shape/TriangleMesh.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..667bc1be0fe9ecaa7d3ca358f93f0af210aecf60
--- /dev/null
+++ b/RayTracer/shape/TriangleMesh.cpp
@@ -0,0 +1,56 @@
+#pragma once
+
+#include "TriangleMesh.h"
+
+#include <sstream>
+#include <string>
+
+#include "../tools/ObjectLoader.h"
+
+namespace shapes {
+TriangleMesh::TriangleMesh(std::vector<Triangle> triangles)
+    : triangles(triangles),
+      material(nullptr),
+      hierarchy(Group(util::identity())) {
+}
+TriangleMesh::TriangleMesh(std::istream& in,
+                           const std::shared_ptr<material::Material>& material)
+    : material(material), hierarchy(Group(util::identity())) {
+	triangles = util::loadObj(in, material);
+}
+std::optional<cam::Hit> TriangleMesh::intersect(const cam::Ray& r) const {
+	std::optional<cam::Hit> result = std::nullopt;
+
+	for (auto tri : triangles) {
+		// if (tri.bounds().intersects(r)) {
+		std::optional<cam::Hit> temp = tri.intersect(r);
+		if (temp) {
+			if (r.in_range(temp->scalar())) {
+				if (!result) {
+					result = temp;
+				} else if (result->scalar() > temp->scalar()) {
+					result = temp;
+				}
+			}
+		}
+		//}
+	}
+	return result;
+}
+util::AxisAlignedBoundingBox TriangleMesh::bounds() const {
+	return util::AxisAlignedBoundingBox(util::Vec3(-2), util::Vec3(2));
+}
+
+util::SurfacePoint TriangleMesh::sampleLight() const {
+	return util::SurfacePoint(util::Vec3(), 0, material);
+}
+util::Vec3 TriangleMesh::calculateLightEmission(const util::SurfacePoint& p,
+                                                const util::Vec3& d) const {
+	return util::Vec3();
+}
+util::AxisAlignedBoundingBox TriangleMesh::initBB() {
+	util::AxisAlignedBoundingBox init = triangles[0].bounds();
+	for (auto tri : triangles) init + tri.bounds();
+	return init;
+}
+}  // namespace shapes
diff --git a/RayTracer/shape/TriangleMesh.h b/RayTracer/shape/TriangleMesh.h
new file mode 100644
index 0000000000000000000000000000000000000000..d612b7bad6b6a32a4267ee66e65237f61ba84432
--- /dev/null
+++ b/RayTracer/shape/TriangleMesh.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include <vector>
+
+#include "../tools/Vertex.h"
+#include "Group.h"
+#include "Light.h"
+#include "Shape.h"
+#include "Triangle.h"
+
+namespace shapes {
+class TriangleMesh : public Light, public Shape {
+   public:
+	TriangleMesh(std::vector<Triangle> triangles);
+	TriangleMesh(std::istream& in,
+	             const std::shared_ptr<material::Material>& material);
+	std::optional<cam::Hit> intersect(const cam::Ray& r) const override;
+	util::AxisAlignedBoundingBox bounds() const override;
+
+	util::SurfacePoint sampleLight() const override;
+	util::Vec3 calculateLightEmission(const util::SurfacePoint& p,
+	                                  const util::Vec3& d) const override;
+
+   public:
+	std::shared_ptr<material::Material> material;
+	std::vector<Triangle> triangles;
+
+   private:
+	Group hierarchy;
+	util::AxisAlignedBoundingBox initBB();
+};
+}  // namespace shapes
diff --git a/RayTracer/tools/ObjectLoader.cpp b/RayTracer/tools/ObjectLoader.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d9c25d20a736f5b2fa778bfd1c00fb65a2740510
--- /dev/null
+++ b/RayTracer/tools/ObjectLoader.cpp
@@ -0,0 +1,76 @@
+#pragma once
+
+#include "ObjectLoader.h"
+
+#include <sstream>
+
+#include "Vertex.h"
+
+namespace util {
+std::vector<shapes::Triangle> loadObj(
+    std::istream& in, const std::shared_ptr<material::Material>& material) {
+	std::vector<Vertex> vertices;
+	std::vector<Vec3> v;
+	std::vector<Vec3> vt;
+	std::vector<Vec3> vn;
+
+	std::string lineStr;
+	while (std::getline(in, lineStr)) {
+		std::istringstream lineSS(lineStr);
+		std::string lineType;
+		lineSS >> lineType;
+
+		// vertex
+		if (lineType == "v") {
+			float x = 0, y = 0, z = 0;
+			lineSS >> x >> y >> z;
+			v.push_back(Vec3(x, y, z));
+		}
+
+		// texture
+		if (lineType == "vt") {
+			float u = 0, v = 0;
+			lineSS >> u >> v;
+			vt.push_back(Vec3(u, v, 0));
+		}
+
+		// normal
+		if (lineType == "vn") {
+			float i = 0, j = 0, k = 0;
+			lineSS >> i >> j >> k;
+			vn.push_back(Vec3(i, j, k).normalize());
+		}
+
+		// polygon
+		if (lineType == "f") {
+			std::string refStr;
+			while (lineSS >> refStr) {
+				std::istringstream ref(refStr);
+				std::string vStr, vtStr, vnStr;
+				std::getline(ref, vStr, '/');
+				std::getline(ref, vtStr, '/');
+				std::getline(ref, vnStr, '/');
+				int currentv = atoi(vStr.c_str());
+				int currentvt = atoi(vtStr.c_str());
+				int currentvn = atoi(vnStr.c_str());
+				currentv = (currentv >= 0 ? currentv : v.size() + currentv);
+				currentvt =
+				    (currentvt >= 0 ? currentvt : vt.size() + currentvt);
+				currentvn =
+				    (currentvn >= 0 ? currentvn : vn.size() + currentvn);
+				Vertex vert;
+				vert.position = v[currentv - 1];
+				vert.texcoord = v[currentvt - 1];
+				vert.normal = v[currentvn - 1];
+				vertices.push_back(vert);
+			}
+		}
+	}
+	std::vector<shapes::Triangle> triangles;
+	for (int i = 0; i < vertices.size(); i += 3) {
+		triangles.push_back(
+		    {vertices[i + 0], vertices[i + 1], vertices[i + 2], material});
+	}
+	return triangles;
+}
+}  // namespace util
\ No newline at end of file
diff --git a/RayTracer/tools/ObjectLoader.h b/RayTracer/tools/ObjectLoader.h
new file mode 100644
index 0000000000000000000000000000000000000000..ec4db0d9bfd193bf442afd6118dceab419f0391c
--- /dev/null
+++ b/RayTracer/tools/ObjectLoader.h
@@ -0,0 +1,12 @@
+#pragma once
+
+#include <fstream>
+#include <vector>
+
+#include "../shape/Triangle.h"
+
+namespace util {
+std::vector<shapes::Triangle> loadObj(
+    std::istream& in,
+    const std::shared_ptr<material::Material>& material = nullptr);
+}  // namespace util
\ No newline at end of file