/* * Created by Brett Terpstra 6920201 on 16/10/22. * Copyright (c) 2022 Brett Terpstra. All Rights Reserved. */ #include "engine/raytracing.h" #include #include #include #include namespace Raytracing { extern Signals* RTSignal; Ray Camera::projectRay(PRECISION_TYPE x, PRECISION_TYPE y) { // transform the x and y to points from image coords to be inside the camera's viewport. double transformedX = (x / (image.getWidth() - 1)); auto transformedY = (y / (image.getHeight() - 1)); // then generate a ray which extends out from the camera position in the direction with respects to its position on the image return {position, imageOrigin + transformedX * horizontalAxis + transformedY * verticalAxis - position}; } void Camera::lookAt(const Vec4& lookAtPos) { // standard camera lookAt function auto w = (position - lookAtPos).normalize(); auto u = (Vec4::cross(up, w)).normalize(); auto v = Vec4::cross(w, u); horizontalAxis = viewportWidth * u; verticalAxis = viewportHeight * v; imageOrigin = position - horizontalAxis / 2 - verticalAxis / 2 - w; } void Camera::setRotation(const PRECISION_TYPE yaw, const PRECISION_TYPE pitch) { // TODO: } Mat4x4 Camera::view(PRECISION_TYPE yaw, PRECISION_TYPE pitch) { Mat4x4 view; pitch = degreeeToRadian(pitch); yaw = degreeeToRadian(yaw); PRECISION_TYPE cosPitch = std::cos(pitch); PRECISION_TYPE cosYaw = std::cos(yaw); PRECISION_TYPE sinPitch = std::sin(pitch); PRECISION_TYPE sinYaw = std::sin(yaw); auto x = Vec4{cosYaw, 0, -sinYaw}; // forward auto y = Vec4{sinYaw * sinPitch, cosPitch, cosYaw * sinPitch}; // right auto z = Vec4{sinYaw * cosPitch, -sinPitch, cosPitch * cosYaw}; // up // we can actually take those x, y, z vectors and use them to compute the raytracer camera settings viewportHeight = 2 * tanFovHalf; viewportWidth = aspectRatio * viewportHeight; // exactly the same as the look at function. horizontalAxis = viewportWidth * x; verticalAxis = viewportHeight * y; imageOrigin = position - horizontalAxis / 2 - verticalAxis / 2 - z; view.m00(float(x.x())); view.m01(float(x.y())); view.m02(float(x.z())); view.m03(float(x.w())); view.m10(float(y.x())); view.m11(float(y.y())); view.m12(float(y.z())); view.m13(float(y.w())); view.m20(float(z.x())); view.m21(float(z.y())); view.m22(float(z.z())); view.m23(float(z.w())); // view matrix are inverted, dot product to simulate translate matrix multiplication view.m03(-float(Vec4::dot(x, position))); view.m13(-float(Vec4::dot(y, position))); view.m23(-float(Vec4::dot(z, position))); view.m33(1); return view; } struct RayData { Ray ray; int depth; Vec4 color; }; Vec4 Raycaster::raycast(const Ray& ray) { Ray localRay = ray; Vec4 color {1.0, 1.0, 1.0}; for (int CURRENT_BOUNCE = 0; CURRENT_BOUNCE < maxBounceDepth; CURRENT_BOUNCE++){ if (RTSignal->haltExecution || RTSignal->haltRaytracing) return color; while (RTSignal->pauseRaytracing) // sleep for 1/60th of a second, or about 1 frame. std::this_thread::sleep_for(std::chrono::milliseconds(16)); auto hit = world.checkIfHit(localRay, 0.001, infinity); if (hit.first.hit) { auto object = hit.second; auto scatterResults = object->getMaterial()->scatter(localRay, hit.first); // if the material scatters the ray, ie casts a new one, if (scatterResults.scattered) { // attenuate the recursive raycast by the material's color color = color * scatterResults.attenuationColor; localRay = scatterResults.newRay; } else { // if we don't scatter, we don't need to keep looping color = {0.0, 0.0, 0.0}; break; } } else { // since we didn't hit, we hit the sky. color = color * Vec4{0.5, 0.7, 1.0}; // if we don't hit we cannot keep looping. break; } } return color; } void Raycaster::runSTDThread(int threads){ for (int i = 0; i < threads; i++) { executors.push_back(std::make_unique([this, i, threads]() -> void { // run through all the quadrants std::stringstream str; str << "Threading of #"; str << (i+1); profiler::start("Raytracer Results", str.str()); int j = 0; while (true) { RaycasterImageBounds imageBoundingData; // get the function for the quadrant queueSync.lock(); if (unprocessedQuads->empty()) { queueSync.unlock(); break; } imageBoundingData = unprocessedQuads->front(); unprocessedQuads->pop(); queueSync.unlock(); // the run it for (int kx = 0; kx <= imageBoundingData.width; kx++) { for (int ky = 0; ky < imageBoundingData.height; ky++) { try { int x = imageBoundingData.x + kx; int y = imageBoundingData.y + ky; Raytracing::Vec4 color; // TODO: profile for speed; for (int s = 0; s < raysPerPixel; s++) { // simulate anti aliasing by generating rays with very slight random directions color = color + raycast(camera.projectRay(x + rnd.getDouble(), y + rnd.getDouble())); } PRECISION_TYPE sf = 1.0 / raysPerPixel; // apply pixel color with gamma correction image.setPixelColor(x, y, {std::sqrt(sf * color.r()), std::sqrt(sf * color.g()), std::sqrt(sf * color.b())}); if (RTSignal->haltExecution || RTSignal->haltRaytracing) return; while (RTSignal->pauseRaytracing) // sleep for 1/60th of a second, or about 1 frame. std::this_thread::sleep_for(std::chrono::milliseconds(16)); } catch (std::exception& error) { flog << "Possibly fatal error in the multithreaded raytracer!\n"; flog << error.what() << "\n"; } } } j++; } finishedThreads++; profiler::end("Raytracer Results", str.str()); })); } } void Raycaster::run(bool multithreaded, int threads) { if (threads == 0) threads = system_threads; // calculate the max divisions we can have per side, then expand by a factor of 4. // the reason to do this is that some of them will finish far quciker than others. The now free threads can keep working. // to do it without a queue like this leads to most threads finishing and a single thread being the critical path which isn't optimally efficient. int divs = int(std::log(threads) / std::log(2)) * 4; // if we are running single threaded, disable everything special // the reason we run single threaded in a seperate thread is because the GUI requires its own set of updating commands // which cannot be blocked by the raytracer, otherwise it would become unresponsive. if (!multithreaded){ threads = 1; divs = 1; } ilog << "Starting multithreaded raytracer with " << threads << " threads!\n"; delete(unprocessedQuads); unprocessedQuads = new std::queue(); // we need to subdivide the image for the threads, since this is really quick it's fine to due sequentially for (int dx = 0; dx < divs; dx++) { for (int dy = 0; dy < divs; dy++) { unprocessedQuads->push({ image.getWidth() / divs, image.getHeight() / divs, (image.getWidth() / divs) * dx, (image.getHeight() / divs) * dy }); } } runSTDThread(threads); } }