diff --git a/light/ambientlight.cpp b/light/ambientlight.cpp new file mode 100644 index 0000000..0bde3bb --- /dev/null +++ b/light/ambientlight.cpp @@ -0,0 +1,5 @@ +#include "light/ambientlight.h" + +Light::Illumination AmbientLight::illuminate(Scene const &scene, Ray const &ray) const { + return {this->color * this->intensity, -ray.normal}; +} diff --git a/light/ambientlight.h b/light/ambientlight.h new file mode 100644 index 0000000..87212aa --- /dev/null +++ b/light/ambientlight.h @@ -0,0 +1,16 @@ +#ifndef AMBIENTLIGHT_H +#define AMBIENTLIGHT_H + +#include "light/light.h" + +class AmbientLight : public Light { + +public: + // Constructor + AmbientLight(float intensity, Color color = Color(1, 1, 1)) : Light(intensity, color) {} + + // Light functions + Illumination illuminate(Scene const &scene, Ray const &ray) const override; +}; + +#endif diff --git a/light/spotlight.cpp b/light/spotlight.cpp new file mode 100644 index 0000000..0828a8e --- /dev/null +++ b/light/spotlight.cpp @@ -0,0 +1,38 @@ +#include "light/spotlight.h" +#include "scene/scene.h" + +SpotLight::SpotLight(Vector3d const &position, Vector3d const &direction, float alphaMin, float alphaMax, float intensity, Color const &color) + : Light(intensity, color), position(position), direction(normalized(direction)), alphaMin(alphaMin), alphaMax(alphaMax) {} + +Light::Illumination SpotLight::illuminate(Scene const &scene, Ray const &ray) const { + Vector3d const target = ray.origin + (ray.length - LGT_EPS) * ray.direction; + + // Illumination object + Illumination illum; + illum.direction = normalized(target - this->position); + + // Precompute the distance from the light source + float const distance = length(target - this->position); + + // Define a secondary ray from the surface point to the light source + Ray lightRay; + lightRay.origin = target; + lightRay.direction = -illum.direction; + lightRay.length = distance - LGT_EPS; + + // Determine the angle of the inner cone + float const alpha = std::fabs(std::acos(dotProduct(illum.direction, this->direction)) * 180.0f / float(PI)); + + // If the target is within the cone... + if (this->alphaMax > alpha) { + // ... and not in shadow ... + if (!scene.findOcclusion(lightRay)) { + // ... compute the attenuation and light color ... + illum.color = 1.0f / (distance * distance) * this->color * this->intensity; + // ... then compute the falloff towards the edge of the cone + if (this->alphaMin < alpha) + illum.color *= 1.0f - (alpha - this->alphaMin) / (this->alphaMax - this->alphaMin); + } + } + return illum; +} diff --git a/light/spotlight.h b/light/spotlight.h new file mode 100644 index 0000000..830f17f --- /dev/null +++ b/light/spotlight.h @@ -0,0 +1,27 @@ +#ifndef SPOTLIGHT_H +#define SPOTLIGHT_H + +#include "light/light.h" + +class SpotLight : public Light { + +public: + // Constructor + SpotLight(Vector3d const &position, Vector3d const &direction, float alphaMin, float alphaMax, float intensity, + Color const &color = Color(1, 1, 1)); + + // Set + void setDirection(Vector3d const &direction) { this->direction = normalized(direction); } + void setPosition(Vector3d const &position) { this->position = position; } + void setAlphaMax(float alphaMax) { this->alphaMax = alphaMax; } + void setAlphaMin(float alphaMin) { this->alphaMin = alphaMin; } + + // Light functions + Illumination illuminate(Scene const &scene, Ray const &ray) const override; + +protected: + Vector3d position, direction; + float alphaMin, alphaMax; +}; + +#endif diff --git a/primitive/objmodel.cpp b/primitive/objmodel.cpp new file mode 100644 index 0000000..d29e552 --- /dev/null +++ b/primitive/objmodel.cpp @@ -0,0 +1,43 @@ +#include "primitive/objmodel.h" +#include "primitive/box.h" +#include "primitive/triangle.h" +#include "scene/scene.h" + +ObjModel::ObjModel(std::shared_ptr const &shader) : Primitive(shader), boundingBox(Vector3d(-INFINITY, -INFINITY, -INFINITY), Vector3d(INFINITY, INFINITY, INFINITY), shader) {} + +void ObjModel::loadObj(char const *fileName, Vector3d const &scale, Vector3d const &translation) { + // Load faces + this->primitives = Scene::loadObj(fileName, scale, translation, shader()); + + // Extent of box + Vector3d minVert(INFINITY, INFINITY, INFINITY); + Vector3d maxVert(-INFINITY, -INFINITY, -INFINITY); + + // For each face, update the extent + for (const auto &primitive : this->primitives) { + minVert = Vector3d(std::min(minVert.x, primitive->minimumBounds(0)), std::min(minVert.y, primitive->minimumBounds(1)), std::min(minVert.z, primitive->minimumBounds(2))); + maxVert = Vector3d(std::max(maxVert.x, primitive->maximumBounds(0)), std::max(maxVert.y, primitive->maximumBounds(1)), std::max(maxVert.z, primitive->maximumBounds(2))); + } + + // Update the bounding box + boundingBox.setCenter(0.5f * (maxVert + minVert)); + boundingBox.setSize(maxVert - minVert + Vector3d(SPLT_EPS, SPLT_EPS, SPLT_EPS)); +} + +bool ObjModel::intersect(Ray &ray) const { + // Ray box intersection + Ray boxRay = ray; + if (boundingBox.intersect(boxRay)) { + // ray primitive intersection + bool hit = false; + for (const auto &p : this->primitives) { + hit |= p->intersect(ray); + } + return hit; + } + return false; +} + +float ObjModel::minimumBounds(int dimension) const { return this->boundingBox.minimumBounds(dimension); } + +float ObjModel::maximumBounds(int dimension) const { return this->boundingBox.maximumBounds(dimension); } diff --git a/primitive/objmodel.h b/primitive/objmodel.h new file mode 100644 index 0000000..daa632c --- /dev/null +++ b/primitive/objmodel.h @@ -0,0 +1,29 @@ +#ifndef OBJMODEL_H +#define OBJMODEL_H + +#include "primitive/box.h" +#include + +class ObjModel : public Primitive { +public: + // Constructor + ObjModel(std::shared_ptr const &shader); + ~ObjModel() override{}; + + // Load object data + void loadObj(char const *fileName, Vector3d const &scale = Vector3d(1, 1, 1), + Vector3d const &translation = Vector3d(0, 0, 0)); + + // Primitive functions + bool intersect(Ray &ray) const override; + + // Bounding box + float minimumBounds(int dimension) const override; + float maximumBounds(int dimension) const override; + +protected: + Box boundingBox; + std::vector> primitives; +}; + +#endif diff --git a/scene/scene.cpp b/scene/scene.cpp index 68e0924..955ce22 100644 --- a/scene/scene.cpp +++ b/scene/scene.cpp @@ -42,7 +42,167 @@ std::vector> Scene::loadObj(char const *fileName, Vec std::vector> faces; std::vector> indices; - // IMPLEMENT ME + // Open file from disk + std::ifstream file; + file.open(fileName); + if (!file.is_open()) { + std::cout << "(Scene): Could not open .obj file: " << fileName << std::endl; + return std::vector>(); + } + + // Print the file name + std::cout << "(Scene): Loading \"" << fileName << "\"" << std::endl; + + // Actual model data + std::vector vData; + std::vector tangentData; + std::vector bitangentData; + std::vector normalData; + std::vector vnData; + std::vector vtData; + + // Read vertices, normals, textures, and faces from the file + std::string line; + while (getline(file, line)) { + std::stringstream lineStream(trim(line)); + std::string type; + lineStream >> type; + + // Vertices + if (type == "v") { + float x, y, z; + lineStream >> x >> y >> z; + vData.emplace_back(componentProduct(Vector3d(x, y, z), scale) + translation); + tangentData.emplace_back(); + bitangentData.emplace_back(); + normalData.emplace_back(); + } + + // Texture coordinates + if (type == "vt") { + float u, v; + lineStream >> u >> v; + vtData.emplace_back(flipU ? 1.0f - u : u, flipV ? 1.0f - v : v); + } + + // Normals + if (type == "vn") { + float a, b, c; + lineStream >> a >> b >> c; + vnData.emplace_back(normalized(componentQuotient( + Vector3d(a, b, c), + scale))); // Division needed for preventing stretched normals, normals' = (transform^-1)^T * normals + } + + // Faces + if (type == "f") { + std::string vertex[3]; + std::array vertInd = {-1, -1, -1}; + std::array texInd = {-1, -1, -1}; + std::array normInd = {-1, -1, -1}; + lineStream >> vertex[0] >> vertex[1] >> vertex[2]; + + // triangulate polygons, like quads (which must be given in triangle fan notation) + while (!vertex[2].empty()) { + auto triangle = std::make_shared(shader); + + for (int i = 0; i < 3; ++i) { + std::stringstream vertexSteam(vertex[i]); + std::string reference; + + // vertex index + getline(vertexSteam, reference, '/'); + try { + vertInd[i] = stoi(reference) - 1; + triangle->setVertex(i, vData.at(vertInd[i])); + } catch (...) { + std::cout << "Error: vertex index invalid on line \"" << line << "\"" << std::endl; + } + + // texture index + if (getline(vertexSteam, reference, '/')) { + if (!reference.empty()) { + try { + texInd[i] = stoi(reference) - 1; + triangle->setSurface(i, vtData.at(texInd[i])); + } catch (...) { + std::cout << "Error: texture coordinate index invalid on line \"" << line << "\"" << std::endl; + } + } + + // normal index + if (getline(vertexSteam, reference, '/')) { + try { + normInd[i] = stoi(reference) - 1; + triangle->setNormal(i, vnData.at(normInd[i])); + } catch (...) { + std::cout << "Error: normal index invalid on line \"" << line << "\"" << std::endl; + } + } + } + } + + // calculate and accumulate tangent and bitangent vectors + if (std::all_of(vertInd.begin(), vertInd.end(), [](int i) { return i > -1; }) && + std::all_of(texInd.begin(), texInd.end(), [](int i) { return i > -1; })) { + for (int i = 0; i < 3; i++) { + const Vector3d deltaPos1 = vData.at(vertInd[(i + 1) % 3]) - vData.at(vertInd[i]); + const Vector3d deltaPos2 = vData.at(vertInd[(i + 2) % 3]) - vData.at(vertInd[i]); + + const Vector2d deltaUV1 = vtData.at(texInd[(i + 1) % 3]) - vtData.at(texInd[i]); + const Vector2d deltaUV2 = vtData.at(texInd[(i + 2) % 3]) - vtData.at(texInd[i]); + + const float r = 1.0f / (deltaUV1.u * deltaUV2.v - deltaUV1.v * deltaUV2.u); + tangentData[vertInd[i]] += (deltaPos1 * deltaUV2.v - deltaPos2 * deltaUV1.v) * r; + bitangentData[vertInd[i]] += (deltaPos2 * deltaUV1.u - deltaPos1 * deltaUV2.u) * r; + + normalData[vertInd[i]] += crossProduct(tangentData[vertInd[i]], bitangentData[vertInd[i]]); + } + } + + faces.push_back(triangle); + indices.push_back(vertInd); + + // get the next triangle + if (lineStream.eof()) + break; + + vertex[1] = vertex[2]; + lineStream >> vertex[2]; + } + } + } + + // Close the file + file.close(); + + // set the normalized tangents and bitangents + for (int i = 0; i < faces.size(); i++) { + for (int j = 0; j < 3; j++) { + Vector3d tangent = normalized(tangentData[indices[i][j]]); + const Vector3d bitangent = normalized(bitangentData[indices[i][j]]); + // try to use the normal from the obj file, if it doesn't exist, use the computed normal + Vector3d normal = normalized(normalData[indices[i][j]]); + if (vnData.size() > 0) + normal = dynamic_cast(faces[i].get())->getNormal(j); + + // gram-schmidt orthogonalization + tangent = normalized(tangent - normal * dotProduct(normal, tangent)); + // check handedness of coordinate system + if (dotProduct(crossProduct(normal, tangent), bitangent) < 0.0f) + tangent *= -1.0f; + + dynamic_cast(faces[i].get())->setTangent(j, tangent); + dynamic_cast(faces[i].get())->setBitangent(j, bitangent); + dynamic_cast(faces[i].get())->setNormal(j, normal); + } + } + + // Debug output + std::cout << " -> " << vData.size() << " vertices parsed" << std::endl; + std::cout << " -> " << vnData.size() << " normals parsed" << std::endl; + std::cout << " -> " << vtData.size() << " uv-positions parsed" << std::endl; + std::cout << " -> " << faces.size() << " primitives parsed" << std::endl; return faces; }