/* * Created by Brett Terpstra 6920201 on 16/10/22. * Copyright (c) 2022 Brett Terpstra. All Rights Reserved. * * The general class for all things raytracing! */ #ifndef STEP_2_RAYTRACING_H #define STEP_2_RAYTRACING_H #include "engine/math/vectors.h" #include "engine/image/image.h" #include "engine/util/parser.h" #include "world.h" #include #include #include #include namespace Raytracing { class Camera { private: /* Image details */ const Image image; const PRECISION_TYPE aspectRatio; /* Camera details */ PRECISION_TYPE viewportHeight; PRECISION_TYPE viewportWidth; PRECISION_TYPE focalLength = 1.0; const PRECISION_TYPE NEAR_PLANE = 0.1; const PRECISION_TYPE FAR_PLANE = 500; PRECISION_TYPE tanFovHalf; PRECISION_TYPE frustumLength; Vec4 position{0, 0, 0}; Vec4 horizontalAxis; Vec4 verticalAxis; Vec4 imageOrigin; Vec4 up{0, 1, 0}; public: Camera(PRECISION_TYPE fov, const Image& image): image(image), aspectRatio(double(image.getWidth()) / double(image.getHeight())) { // scale the viewport height based on the camera's FOV tanFovHalf = tan(degreeeToRadian(fov) / 2); viewportHeight = (2.0 * tanFovHalf); // with must respect the aspect ratio of the image, otherwise we'd get funky results viewportWidth = (aspectRatio * viewportHeight); frustumLength = FAR_PLANE - NEAR_PLANE; // horizontal direction from the camera. used to translate the camera horizontalAxis = (Vec4{viewportWidth, 0, 0, 0}); // virtual direction, also used to translate the camera verticalAxis = (Vec4{0, viewportHeight, 0, 0}); // lower left of the camera's view port. used to project our vectors from image space to world space imageOrigin = (position - horizontalAxis / 2 - verticalAxis / 2 - Vec4(0, 0, focalLength, 0)); tlog << viewportHeight << "\n"; tlog << viewportWidth << "\n"; tlog << "\n"; tlog << horizontalAxis << "\n"; tlog << verticalAxis << "\n"; tlog << imageOrigin << "\n"; } /** * Projects an xy coord into world space * @param x image x coord * @param y image y coord * @return a Ray projected from camera position out into the world based on the relative position in the image. */ Ray projectRay(PRECISION_TYPE x, PRECISION_TYPE y); void setPosition(const Vec4& pos) { this->position = pos; } /** * Creates a projection matrix for use in the OpenGL pipeline. * @return Mat4x4 containing a standard perspective projection matrix */ [[nodiscard]] inline Mat4x4 project() const { Mat4x4 project{emptyMatrix}; // this should be all it takes to create a mostly correct projection matrix // the values are transposed because my matrix implementation is terrible. // This is set up in such a way that it is 1:1 with the CPU ray projection. Meaning when you move the camera in "Debug" mode, // the rays will be projected from that position and camera look direction. project.m00(float(1.0 / (aspectRatio * tanFovHalf))); project.m11(float(1.0 / tanFovHalf)); project.m22(float(-((FAR_PLANE + NEAR_PLANE) / frustumLength))); project.m32(-1); project.m23(float(-((2 * NEAR_PLANE * FAR_PLANE) / frustumLength))); //project.m33(0); return project; // use GLM to debug issues with ^ //glm::mat4 projectG = glm::perspective(glm::radians(90.0f), (float)aspectRatio, 0.1f, (float)1000); //return Mat4x4{projectG}; } /** * Creates a view matrix containing the camera rotation and inverse position. * the view matrix is used to transform world coordinates into camera space, * which can than be transformed into screen space using the projection matrix. * @param yaw yaw of the camera * @param pitch pitch of the camera * @param roll NOT SUPPORTED * @return Mat4x4 containing rotation in the first 3x3 values and -position in the last column */ Mat4x4 view(PRECISION_TYPE yaw, PRECISION_TYPE pitch); [[nodiscard]] inline Vec4 getPosition() const { return position; }; [[nodiscard]] inline Vec4 getImageOrigin() const { return imageOrigin; } [[nodiscard]] inline Vec4 getHorizontalAxis() const { return horizontalAxis; } [[nodiscard]] inline Vec4 getVerticalAxis() const { return verticalAxis; } // the camera's position must be set with setPosition(Vec4); // uses an internal up vector, assumed to be {0, 1, 0} // will make the camera look at provided position with respects to the current camera position. // TODO: update the view matrix. Requires that the view matrix be stored in the camera. void lookAt(const Vec4& lookAtPos); }; static Random rnd{-1.0, 1.0}; struct RayCasterImageBounds { int width, height, x, y; }; class RayCaster { private: const unsigned int system_threads = std::thread::hardware_concurrency(); int maxBounceDepth; int raysPerPixel; unsigned int finishedThreads = 0; Camera& camera; Image& image; World& world; // yes this is actually the only sync we need between the threads // and compared to the actual runtime of the raytracing it's very small! std::mutex queueSync; // the queue containing the image bounds to be rendered. std::queue* unprocessedQuads = nullptr; std::vector> executors{}; /** * Does the actual ray casting algorithm. Simulates up to maxBounceDepth ray depth. * @param ray ray to begin with * @return the overall average color of the ray */ Vec4 raycast(const Ray& ray); /** * * @param imageBounds bounds to work on * @param loopX the current x position to work on, between 0 and imageBounds.width * @param loopY the current y position to work on, between 0 and imageBounds.height */ void runRaycastingAlgorithm(RayCasterImageBounds imageBounds, int loopX, int loopY); /** * Creates the queue with the provided bounds */ void setupQueue(const std::vector& bounds); public: RayCaster(Camera& c, Image& i, World& world, Parser& p): camera(c), image(i), world(world) { world.generateBVH(); maxBounceDepth = std::stoi(p.getOptionValue("--maxRayDepth")); raysPerPixel = std::stoi(p.getOptionValue("--raysPerPixel")); } inline void updateRayInfo(int maxBounce, int perPixel) { raysPerPixel = perPixel; maxBounceDepth = maxBounce; } /** * divides the screen into image bounds * @param threads number of threads that will determine how many cuts to the screen is required * @return a list of bounds */ std::vector partitionScreen(int threads = -1); /** * Updates the thread value based on conditions, used for setting up the actual threads for execution. * @param threads reference to the value which will be updated. */ inline void updateThreadValue(int& threads) const { if (threads < 0 || threads == 1) threads = 1; else { if (threads == 0) threads = (int) system_threads; } } /** * Creates a random vector in the unit sphere. */ inline static Vec4 randomUnitVector() { return Vec4(rnd.getDouble(), rnd.getDouble(), rnd.getDouble()).normalize(); } /** * Runs the std::thread implementation * @param threads number of threads to use */ void runSTDThread(int threads = -1); /** * Runs the OpenMP implementation * @param threads number of threads to use */ void runOpenMP(int threads = -1); /** * ran by MPI * @param bounds bounds that get processed by this process */ void runMPI(std::queue bounds); /** * Blocking call that waits for all the threads to finish exeuction */ inline void join() { for (auto& p : executors) p->join(); } /** * Joins all joinable threads and clears the thread list. */ void deleteThreads() { for (auto& p : executors) { // wait for all threads to exit before trying to delete them. try { if (p->joinable()) p->join(); } catch (std::exception& e) {} } // since executors contains the only reference to the unique_ptr it will be deleted automatically executors.clear(); } ~RayCaster() { deleteThreads(); delete (unprocessedQuads); } }; } #endif //STEP_2_RAYTRACING_H