#pragma once
#include "shape.h"
////////////////////////////////////////////////////////////////////////
// MeshTriangle: A triangle with per-vertex normals, UVs, and tangents.
// Supports smooth shading, texture lookups, alpha testing, and normal mapping.
////////////////////////////////////////////////////////////////////////
class MeshTriangle : public Shape {
const float EPS = 1e-7f;
public:
// Vertex positions
vec3 v0, v1, v2;
// Per-vertex normals (for smooth shading)
vec3 n0, n1, n2;
// Per-vertex texture coordinates
vec2 uv0, uv1, uv2;
// Per-vertex tangents (for normal mapping)
vec3 t0, t1, t2;
// Precomputed geometric face normal
vec3 Ng;
// Constructor WITH tangents
MeshTriangle(const vec3& a, const vec3& b, const vec3& c,
const vec3& na, const vec3& nb, const vec3& nc,
const vec2& ta, const vec2& tb, const vec2& tc,
const vec3& tan0, const vec3& tan1, const vec3& tan2,
Material* m)
: Shape(m), v0(a), v1(b), v2(c),
n0(na), n1(nb), n2(nc),
uv0(ta), uv1(tb), uv2(tc),
t0(tan0), t1(tan1), t2(tan2)
{
vec3 cross_product = cross(v1 - v0, v2 - v0);
float len = length(cross_product);
Ng = (len > 1e-8f) ? cross_product / len : vec3(0, 1, 0);
}
// Constructor WITHOUT tangents (backward compatible)
MeshTriangle(const vec3& a, const vec3& b, const vec3& c,
const vec3& na, const vec3& nb, const vec3& nc,
const vec2& ta, const vec2& tb, const vec2& tc,
Material* m)
: Shape(m), v0(a), v1(b), v2(c),
n0(na), n1(nb), n2(nc),
uv0(ta), uv1(tb), uv2(tc),
t0(0), t1(0), t2(0)
{
vec3 cross_product = cross(v1 - v0, v2 - v0);
float len = length(cross_product);
Ng = (len > 1e-8f) ? cross_product / len : vec3(0, 1, 0);
}
std::optional<Intersection> intersect(Ray r) override {
// Möller–Trumbore intersection
vec3 e1 = v1 - v0;
vec3 e2 = v2 - v0;
vec3 h = cross(r.direction, e2);
float a = dot(e1, h);
if (abs(a) < EPS)
return std::nullopt;
float f = 1.0f / a;
vec3 s = r.origin - v0;
float u = f * dot(s, h);
if (u < 0.0f || u > 1.0f)
return std::nullopt;
vec3 q = cross(s, e1);
float v = f * dot(r.direction, q);
if (v < 0.0f || u + v > 1.0f)
return std::nullopt;
float t = f * dot(e2, q);
if (t < EPS)
return std::nullopt;
float w = 1.0f - u - v;
// Alpha test: check texture alpha at hit UV before accepting the hit
if (mat && mat->tex) {
vec2 hitUV = w * uv0 + u * uv1 + v * uv2;
if (mat->tex->sampleAlpha(hitUV) < 0.5f) {
return std::nullopt;
}
}
// Add opacity map test
if (mat && mat->texOpacity) {
vec2 hitUV = w * uv0 + u * uv1 + v * uv2;
if (mat->texOpacity->sampleFloat(hitUV) < 0.5f) {
return std::nullopt;
}
}
Intersection result;
result.t = t;
result.position = r.evaluate(t);
result.Ng = Ng;
result.hasNg = true;
// Barycentric interpolation of per-vertex normals
vec3 interpN = w * n0 + u * n1 + v * n2;
float len = length(interpN);
if (len > 1e-6f) {
result.normal = interpN / len;
}
else {
result.normal = Ng;
}
// Ensure the shading normal faces the incoming ray
if (dot(result.normal, r.direction) > 0.0f) {
result.normal = -result.normal;
}
// Barycentric interpolation of UVs
result.uv = w * uv0 + u * uv1 + v * uv2;
// Interpolate tangent for normal mapping
vec3 interpT = w * t0 + u * t1 + v * t2;
float tLen = length(interpT);
// Compute tangent for normal mapping (only if material has normal map)
if (mat && mat->texBump) {
result.tangent = computeTangent(result.normal);
result.hasTangent = true;
}
else {
result.hasTangent = false;
}
result.shape = this;
return result;
}
// Returns interpolated UV at barycentric coords (u,v)
vec2 interpolateUV(float u, float v) const {
float w = 1.0f - u - v;
return w * uv0 + u * uv1 + v * uv2;
}
// Compute barycentrics for a given point on the triangle
vec3 barycentrics(const vec3& p) const {
vec3 e0 = v1 - v0;
vec3 e1 = v2 - v0;
vec3 ep = p - v0;
float d00 = dot(e0, e0);
float d01 = dot(e0, e1);
float d11 = dot(e1, e1);
float d20 = dot(ep, e0);
float d21 = dot(ep, e1);
float denom = d00 * d11 - d01 * d01;
float u = (d11 * d20 - d01 * d21) / denom;
float v = (d00 * d21 - d01 * d20) / denom;
float w = 1.0f - u - v;
return vec3(w, u, v);
}
// Surface area of this triangle (needed for area light sampling)
float area() const {
return 0.5f * length(cross(v1 - v0, v2 - v0));
}
SimpleBox boundingBox() override {
SimpleBox box;
box.extend(v0);
box.extend(v1);
box.extend(v2);
return box;
}
vec3 computeTangent(const vec3& N) const {
vec3 e1 = v1 - v0;
vec3 e2 = v2 - v0;
vec2 duv1 = uv1 - uv0;
vec2 duv2 = uv2 - uv0;
float denom = duv1.x * duv2.y - duv2.x * duv1.y;
if (abs(denom) < 1e-8f) {
vec3 up = (abs(N.y) < 0.9f) ? vec3(0, 1, 0) : vec3(1, 0, 0);
return normalize(cross(up, N));
}
float f = 1.0f / denom;
vec3 T = f * (duv2.y * e1 - duv1.y * e2);
return normalize(T - dot(T, N) * N);
}
};
#pragma once
#include <vector>
#include <string>
#include <cmath>
#include <algorithm>
#include <numeric>
#include "geom.h"
#include "stb_image.h"
////////////////////////////////////////////////////////////////////////
// IBL (Image-Based Lighting): Loads an HDR environment map and provides
// importance sampling based on luminance distribution.
//
// The environment map is stored in equirectangular (lat-long) format.
// A 2D CDF is built from the luminance of each pixel (weighted by
// sin(theta) to correct for the solid angle distortion at the poles).
//
// Usage:
// 1. Call load("path/to/env.hdr")
// 2. Call build() to construct the importance sampling tables
// 3. Use sample(e1, e2) to importance-sample a direction
// 4. Use lookup(direction) to evaluate the environment radiance
// 5. Use pdf(direction) for the sampling PDF
////////////////////////////////////////////////////////////////////////
class EnvironmentMap {
public:
// Raw HDR pixel data (RGB floats, row-major, top-to-bottom)
std::vector<float> pixels; // width * height * 3
int width = 0;
int height = 0;
// Importance sampling structures
std::vector<float> marginalCDF; // CDF over rows (height entries)
std::vector<float> marginalPDF; // PDF for each row
std::vector<std::vector<float>> conditionalCDF; // CDF within each row
std::vector<std::vector<float>> conditionalPDF; // PDF for each pixel in row
float totalPower = 0.0f;
float rotationOffset = 0.0f;
bool loaded = false;
EnvironmentMap() = default;
////////////////////////////////////////////////////////////////////////
// Load an HDR file (uses stb_image - expects .hdr format).
// NOTE: stb_image.h must be included with STB_IMAGE_IMPLEMENTATION
// in exactly one .cpp file (you already do this in raytrace.cpp).
// Here we just declare the function and rely on the linker.
////////////////////////////////////////////////////////////////////////
bool load(const std::string& path) {
// stb_image loads HDR as float RGB
// We need to declare this extern since stb_image is compiled in raytrace.cpp
int n;
// stbi_set_flip_vertically_on_load is already called in your Texture loader,
// but for environment maps we want top = +Y, so don't flip.
stbi_set_flip_vertically_on_load(false);
float* data = stbi_loadf(path.c_str(), &width, &height, &n, 3);
stbi_set_flip_vertically_on_load(true); // Restore for other textures
if (!data) {
fprintf(stderr, "Failed to load HDR environment map: %s\n", path.c_str());
return false;
}
pixels.assign(data, data + width * height * 3);
stbi_image_free(data);
printf("Loaded HDR environment map: %s (%dx%d)\n", path.c_str(), width, height);
loaded = true;
return true;
}
////////////////////////////////////////////////////////////////////////
// Build importance sampling tables from the luminance distribution.
// Must be called // Must be called
// Build importance sampling tables from the luminance distribution.
////////////////////////////////////////////////////////////////////////
}
return true;
loaded = true;
printf("Loaded HDR environment map: %s (%dx%d)\n", path.c_str(), width, height);
stbi_image_free(data);
pixels.assign(data, data + width * height * 3);
}
return false;
fprintf(stderr, "Failed to load HDR environment map: %s\n", path.c_str());
if (!data) {
stbi_set_flip_vertically_on_load(true); // Restore for other textures
float* data = stbi_loadf(path.c_str(), &width, &height, &n, 3);
stbi_set_flip_vertically_on_load(false);
// but for environment maps we want top = +Y, so don't flip.
// stbi_set_flip_vertically_on_load is already called in your Texture loader,
int n;
// We need to declare this extern since stb_image is compiled in raytrace.cpp
// stb_image loads HDR as float RGB
bool load(const std::string& path) {
////////////////////////////////////////////////////////////////////////
// Here we just declare the function and rely on the linker.
// in exactly one .cpp file (you already do this in raytrace.cpp).
// NOTE: stb_image.h must be included with STB_IMAGE_IMPLEMENTATION
// Load an HDR file (uses stb_image - expects .hdr format).
////////////////////////////////////////////////////////////////////////
EnvironmentMap() = default;
bool loaded = false;
float rotationOffset = 0.0f;
float totalPower = 0.0f;
std::vector<std::vector<float>> conditionalPDF; // PDF for each pixel in row
std::vector<std::vector<float>> conditionalCDF; // CDF within each row
std::vector<float> marginalPDF; // PDF for each row
std::vector<float> marginalCDF; // CDF over rows (height entries)
// Importance sampling structures
int height = 0;
int width = 0;
std::vector<float> pixels; // width * height * 3
// Raw HDR pixel data (RGB floats, row-major, top-to-bottom)
public:
class EnvironmentMap {
////////////////////////////////////////////////////////////////////////
// 5. Use pdf(direction) for the sampling PDF
// 4. Use lookup(direction) to evaluate the environment radiance
// 3. Use sample(e1, e2) to importance-sample a direction
// 2. Call build() to construct the importance sampling tables
// 1. Call load("path/to/env.hdr")
// Usage:
//
// sin(theta) to correct for the solid angle distortion at the poles).
// A 2D CDF is built from the luminance of each pixel (weighted by
// The environment map is stored in equirectangular (lat-long) format.
//
// importance sampling based on luminance distribution.
// IBL (Image-Based Lighting): Loads an HDR environment map and provides
////////////////////////////////////////////////////////////////////////
#include "stb_image.h"
#include "geom.h"
#include <numeric>
#include <algorithm>
#include <cmath>
#include <string>
#include <vector>
#pragma once
};
}
return normalize(T - dot(T, N) * N);
vec3 T = f * (duv2.y * e1 - duv1.y * e2);
float f = 1.0f / denom;
}
return normalize(cross(up, N));
vec3 up = (abs(N.y) < 0.9f) ? vec3(0, 1, 0) : vec3(1, 0, 0);
if (abs(denom) < 1e-8f) {
float denom = duv1.x * duv2.y - duv2.x * duv1.y;
vec2 duv2 = uv2 - uv0;
vec2 duv1 = uv1 - uv0;
vec3 e2 = v2 - v0;
vec3 e1 = v1 - v0;
vec3 computeTangent(const vec3& N) const {
}
return box;
box.extend(v2);
box.extend(v1);
box.extend(v0);
SimpleBox box;
SimpleBox boundingBox() override {
}
return 0.5f * length(cross(v1 - v0, v2 - v0));
float area() const {
// Surface area of this triangle (needed for area light sampling)
}
return vec3(w, u, v);
float w = 1.0f - u - v;
float v = (d00 * d21 - d01 * d20) / denom;
float u = (d11 * d20 - d01 * d21) / denom;
float denom = d00 * d11 - d01 * d01;
float d21 = dot(ep, e1);
float d20 = dot(ep, e0);
float d11 = dot(e1, e1);
float d01 = dot(e0, e1);
float d00 = dot(e0, e0);
vec3 ep = p - v0;
vec3 e1 = v2 - v0;
vec3 e0 = v1 - v0;
vec3 barycentrics(const vec3& p) const {
// Compute barycentrics for a given point on the triangle
}
return w * uv0 + u * uv1 + v * uv2;
float w = 1.0f - u - v;
vec2 interpolateUV(float u, float v) const {
// Returns interpolated UV at barycentric coords (u,v)
}
return result;
result.shape = this;
}
result.hasTangent = false;
else {
}
result.hasTangent = true;
result.tangent = computeTangent(result.normal);
if (mat && mat->texBump) {
// Compute tangent for normal mapping (only if material has normal map)
float tLen = length(interpT);
vec3 interpT = w * t0 + u * t1 + v * t2;
// Interpolate tangent for normal mapping
result.uv = w * uv0 + u * uv1 + v * uv2;
// Barycentric interpolation of UVs
}
result.normal = -result.normal;
if (dot(result.normal, r.direction) > 0.0f) {
// Ensure the shading normal faces the incoming ray
}
result.normal = Ng;
else {
}
result.normal = interpN / len;
if (len > 1e-6f) {
float len = length(interpN);
vec3 interpN = w * n0 + u * n1 + v * n2;
// Barycentric interpolation of per-vertex normals
result.hasNg = true;
result.Ng = Ng;
result.position = r.evaluate(t);
result.t = t;
Intersection result;
}
}
return std::nullopt;
if (mat->texOpacity->sampleFloat(hitUV) < 0.5f) {
vec2 hitUV = w * uv0 + u * uv1 + v * uv2;
if (mat && mat->texOpacity) {
// Add opacity map test
}
}
return std::nullopt;
if (mat->tex->sampleAlpha(hitUV) < 0.5f) {
vec2 hitUV = w * uv0 + u * uv1 + v * uv2;
if (mat && mat->tex) {
// Alpha test: check texture alpha at hit UV before accepting the hit
float w = 1.0f - u - v;
return std::nullopt;
if (t < EPS)
float t = f * dot(e2, q);
return std::nullopt;
if (v < 0.0f || u + v > 1.0f)
float v = f * dot(r.direction, q);
vec3 q = cross(s, e1);
return std::nullopt;
if (u < 0.0f || u > 1.0f)
float u = f * dot(s, h);
vec3 s = r.origin - v0;
float f = 1.0f / a;
return std::nullopt;
if (abs(a) < EPS)
float a = dot(e1, h);
vec3 h = cross(r.direction, e2);
vec3 e2 = v2 - v0;
vec3 e1 = v1 - v0;
// Möller–Trumbore intersection
std::optional<Intersection> intersect(Ray r) override {
}
Ng = (len > 1e-8f) ? cross_product / len : vec3(0, 1, 0);
float len = length(cross_product);
vec3 cross_product = cross(v1 - v0, v2 - v0);
{
t0(0), t1(0), t2(0)
uv0(ta), uv1(tb), uv2(tc),
n0(na), n1(nb), n2(nc),
: Shape(m), v0(a), v1(b), v2(c),
Material* m)
const vec2& ta, const vec2& tb, const vec2& tc,
const vec3& na, const vec3& nb, const vec3& nc,
MeshTriangle(const vec3& a, const vec3& b, const vec3& c,
// Constructor WITHOUT tangents (backward compatible)
}
Ng = (len > 1e-8f) ? cross_product / len : vec3(0, 1, 0);
float len = length(cross_product);
vec3 cross_product = cross(v1 - v0, v2 - v0);
{
t0(tan0), t1(tan1), t2(tan2)
uv0(ta), uv1(tb), uv2(tc),
n0(na), n1(nb), n2(nc),
: Shape(m), v0(a), v1(b), v2(c),
Material* m)
const vec3& tan0, const vec3& tan1, const vec3& tan2,
const vec2& ta, const vec2& tb, const vec2& tc,
const vec3& na, const vec3& nb, const vec3& nc,
MeshTriangle(const vec3& a, const vec3& b, const vec3& c,
// Constructor WITH tangents
vec3 Ng;
// Precomputed geometric face normal
vec3 t0, t1, t2;
// Per-vertex tangents (for normal mapping)
vec2 uv0, uv1, uv2;
// Per-vertex texture coordinates
vec3 n0, n1, n2;
// Per-vertex normals (for smooth shading)
vec3 v0, v1, v2;
// Vertex positions
public:
const float EPS = 1e-7f;
class MeshTriangle : public Shape {
////////////////////////////////////////////////////////////////////////
// Supports smooth shading, texture lookups, alpha testing, and normal mapping.
// MeshTriangle: A triangle with per-vertex normals, UVs, and tangents.
////////////////////////////////////////////////////////////////////////
#include "shape.h"
#pragma onceRefracted Reports
Coursework write-ups from the graphics track — what was built, what it got wrong first, and the reading that fixed it. Each report is the log of one project, kept in the order the work happened.
Swaroop Kishen. 14 reports across 3 courses.
Latest report CS 599, Project 4 Path Tracing Submitted 6 November 2026CS 599
Ray Traced Real Time Rendering
Hardware ray tracing under Vulkan — bringing the offline estimator onto the GPU, one validated resource at a time.
- 1 First Light 20 Sep 2026
- 2 Scanline 11 Oct 2026
- 3 Ray Casting 24 Oct 2026
- 4 Path Tracing 6 Nov 2026
CS 562
Advanced Real Time Rendering
A real-time deferred pipeline: G-buffers, shadow maps, image-based lighting, and the approximations that buy them a frame budget.
- 1 Deferred Shading 9 Feb 2026
- 2 Shadow Mapping 1 Mar 2026
- 3 PBS and IBL 28 Mar 2026
- 4 Screen Space Ambient Occlusion 4 Apr 2026
- 5 Screen Space Directional Occlusion 18 Apr 2026
CS 500
Ray Tracing
An offline path tracer built from the intersection query up — BVH traversal, Monte Carlo integration, and the BRDFs that make the estimator converge.
- 1 Ray Casting 4 Feb 2026
- 2 Path Tracing 1 Mar 2026
- 3 Reflections 14 Mar 2026
- 4 Transmission 29 Mar 2026
- 5 Large Scale Scene Rendering with IBL 14 Apr 2026