/************************************************************ Title : A Photo-realistic Renderer Based on Photon Mapping Students : Davood Rasti Alireza Rasti Version : 0.01 Date : 23-06-2012 Updated : 01-10-2026 *************************************************************/ #include "./3rdparty/stb_collection/stb_image_write.h" #include "./prrpm.h" #include #include #include #include #include #include #include #include #include using namespace PRRPM; // Parse a complete numeric token so malformed scene values cannot silently become zero. template Value parseNumber(std::string_view token) { Value value{}; const char* tokenEnd = token.data() + token.size(); const auto result = std::from_chars(token.data(), tokenEnd, value); if (result.ec != std::errc{} || result.ptr != tokenEnd) throw std::invalid_argument("invalid numeric value '" + std::string(token) + "'"); return value; } std::vector tokenizer(const std::string& line) { std::vector stringList; std::string normalizedLine = line; // Scene files accept whitespace as well as '=', ',' and ';' as separators. for (char& character : normalizedLine) { if (character == '=' || character == ',' || character == ';') character = ' '; } std::istringstream lineStream(normalizedLine); std::string token; while (lineStream >> token) stringList.push_back(token); return stringList; } bool loadSceneFile(const std::string& fileName) { std::ifstream modelFile(fileName); if (!modelFile) { std::cerr << "Unable to open scene file: " << fileName << '\n'; return false; } std::string code; PRRPM::Triangle triangle; PRRPM::Sphere sphere; std::string lineToRead; std::size_t lineNumber = 0; while (std::getline(modelFile, lineToRead)) { ++lineNumber; try { std::vector stringList; stringList = tokenizer(lineToRead); if (stringList.size() < 1) continue; code = stringList[0]; if (code[0] == '*') break; else if (code[0] == '/') continue; // Each non-comment line begins with a directive such as imagesize, light, t, or s. else if (code == "imagesize" && stringList.size() >= 3) { imageWidth = parseNumber(stringList[1]); imageHeight = parseNumber(stringList[2]); } else if (code == "light" && stringList.size() >= 2) { if (stringList[1] == "on" || stringList[1] == "off") isLightOn = stringList[1] == "on"; // Accept either the full numeric light record or the compact "light pos" form. else if (stringList.size() >= 11) { PRRPM::LightSource lightSource{}; lightSource.type = parseNumber(stringList[1]); lightSource.position = Vector3d(parseNumber(stringList[2]), parseNumber(stringList[3]), parseNumber(stringList[4])); lightSource.intensity = Vector3d(parseNumber(stringList[5]), parseNumber(stringList[6]), parseNumber(stringList[7])); lightSource.size = Vector3d(parseNumber(stringList[8]), parseNumber(stringList[9]), parseNumber(stringList[10])); lightSourcesList.push_back(lightSource); } else if (stringList[1] == "pos" && stringList.size() >= 5) { PRRPM::LightSource lightSource{}; lightSource.type = 0; lightSource.position = Vector3d(parseNumber(stringList[2]), parseNumber(stringList[3]), parseNumber(stringList[4])); lightSource.intensity = Vector3d(1.0f, 1.0f, 1.0f); lightSourcesList.push_back(lightSource); } } else if ((code == "numberOfPhotons" || code == "numberOfPhotonPass" || code == "numberOfBounces" || code == "numberOfPhotonBounces") && stringList.size() >= 2) { const ui32 value = parseNumber(stringList[1]); if (code == "numberOfPhotons") numberOfPhotons = value; else if (code == "numberOfPhotonPass") numberOfPhotonPass = value; else numberOfBounces = value; } else if (code == "photonCasting" && stringList.size() >= 2) isPhotonMapping = stringList[1] == "on"; else if (code == "gui" && stringList.size() >= 2) { if (stringList[1] == "off") isGUI = false; else if (stringList[1] == "on") isGUI = true; } else if (code == "t" && stringList.size() >= 13) { triangle.point1.x = parseNumber(stringList[1]); triangle.point1.y = parseNumber(stringList[2]); triangle.point1.z = parseNumber(stringList[3]); triangle.point2.x = parseNumber(stringList[4]); triangle.point2.y = parseNumber(stringList[5]); triangle.point2.z = parseNumber(stringList[6]); triangle.point3.x = parseNumber(stringList[7]); triangle.point3.y = parseNumber(stringList[8]); triangle.point3.z = parseNumber(stringList[9]); triangle.color.x = parseNumber(stringList[10]); triangle.color.y = parseNumber(stringList[11]); triangle.color.z = parseNumber(stringList[12]); triangle.materialID = stringList.size() >= 14 ? parseNumber(stringList[13]) : 0; // The ray-triangle test uses the precomputed face normal. PRRPM::Vector3d edge1 = triangle.point2 - triangle.point1; PRRPM::Vector3d edge2 = triangle.point3 - triangle.point1; triangle.normal = edge1.cross(edge2); triangle.normal.normalize(); triangleList.push_back(triangle); } else if (code == "s" && stringList.size() >= 8) { sphere.position.x = parseNumber(stringList[1]); sphere.position.y = parseNumber(stringList[2]); sphere.position.z = parseNumber(stringList[3]); sphere.radius = parseNumber(stringList[4]); sphere.color.x = parseNumber(stringList[5]); sphere.color.y = parseNumber(stringList[6]); sphere.color.z = parseNumber(stringList[7]); sphere.materialID = stringList.size() >= 9 ? parseNumber(stringList[8]) : 0; sphereList.push_back(sphere); } } catch (const std::invalid_argument& error) { // Include the source location so scene-file typos are easy to find. std::cerr << fileName << ':' << lineNumber << ": " << error.what() << '\n'; return false; } } return true; } // Advance one queued secondary ray. Status values are -2 (finished), 0 (diffuse), // 1 (mirror), and 2 (dielectric); storing these states makes recursion unnecessary. void nextRayTracingPass(ui32 index) { if (secondIntersectionPointsList[index].nubmerOfPhotonAndMaterialID == -2) return; PRRPM::Ray ray; ray.direction = secondIntersectionPointsList[index].fluxAndDirection; ray.direction.normalize(); ray.origin = secondIntersectionPointsList[index].positionAndOrigin; ray.objectID = -1; ray.distance = maxSceneDepth; PRRPM::Vector3d color; intersectObjects(&ray); if (ray.objectID != -1) { if (getObjectMaterial(ray.objectID, ray.objectType) == 0) { if (secondIntersectionPointsList[index].radiusSquare != -1) color = getObjectColor(ray.objectID, ray.objectType); else color = secondIntersectionPointsList[index].color * getObjectColor(ray.objectID, ray.objectType); int cellID = computeCellID(ray.intersectionPosition); if (cellID >= 0 && cellID < gridSize) { secondIntersectionPointsList[index].color = color; secondIntersectionPointsList[index].positionAndOrigin = ray.intersectionPosition; secondIntersectionPointsList[index].normal = getSurfaceNormal(ray); secondIntersectionPointsList[index].cellID = cellID; // secondIntersectionPointsList[index].materialID = 0; secondIntersectionPointsList[index].radiusSquare = irridianceRadiusSquare; secondIntersectionPointsList[index].nubmerOfPhotonAndMaterialID = 0; secondIntersectionPointsList[index].fluxAndDirection = PRRPM::Vector3d(); } } else if (getObjectMaterial(ray.objectID, ray.objectType) == 1) { PRRPM::Vector3d normal = getSurfaceNormal(ray); secondIntersectionPointsList[index].positionAndOrigin = ray.intersectionPosition; secondIntersectionPointsList[index].fluxAndDirection = ray.direction - normal * normal.dot(ray.direction) * 2.0f; secondIntersectionPointsList[index].nubmerOfPhotonAndMaterialID = 1; } else if (getObjectMaterial(ray.objectID, ray.objectType) == 2) { secondIntersectionPointsList[index].positionAndOrigin = ray.intersectionPosition; secondIntersectionPointsList[index].normal = getSurfaceNormal(ray); secondIntersectionPointsList[index].fluxAndDirection = ray.direction; if (secondIntersectionPointsList[index].radiusSquare == -1) secondIntersectionPointsList[index].color = secondIntersectionPointsList[index].color * getObjectColor(ray.objectID, ray.objectType); else secondIntersectionPointsList[index].color = getObjectColor(ray.objectID, ray.objectType); secondIntersectionPointsList[index].nubmerOfPhotonAndMaterialID = 2; } } else secondIntersectionPointsList[index].nubmerOfPhotonAndMaterialID = -2; } // Replace each dielectric hit with its reflected and refracted paths, then discard finished paths. void calculateReflectionFractionForAllPoints() { PRRPM::ui32 step = 0; for (PRRPM::ui32 iPoint = 0; iPoint < secondIntersectionPointsList.size(); ++iPoint) { IntersectionPoint intersectionPoint = secondIntersectionPointsList[iPoint]; if (intersectionPoint.nubmerOfPhotonAndMaterialID == 2) { PRRPM::Vector3d reflectionRayDirection; PRRPM::Vector3d refractionRayDirection; f32 cosOutAngle; f32 reflectionIntensity; ++step; calculateReflectionFractionDirection(intersectionPoint.fluxAndDirection, intersectionPoint.normal, &reflectionRayDirection, &refractionRayDirection, &cosOutAngle, &reflectionIntensity); if (cosOutAngle < 0) { secondIntersectionPointsList[iPoint] = intersectionPoint; secondIntersectionPointsList[iPoint].fluxAndDirection = reflectionRayDirection; secondIntersectionPointsList[iPoint].radiusSquare = -1; ++iPoint; continue; } secondIntersectionPointsList[iPoint - 1] = intersectionPoint; secondIntersectionPointsList[iPoint - 1].fluxAndDirection = refractionRayDirection; secondIntersectionPointsList[iPoint - 1].color = secondIntersectionPointsList[iPoint].color * (1.0 - reflectionIntensity); secondIntersectionPointsList[iPoint - 1].radiusSquare = -1; secondIntersectionPointsList[iPoint] = intersectionPoint; secondIntersectionPointsList[iPoint].fluxAndDirection = reflectionRayDirection; // secondIntersectionPointsList[iPoint].nubmerOfPhotonAndMaterialID = -2; secondIntersectionPointsList[iPoint].color = secondIntersectionPointsList[iPoint].color * reflectionIntensity; secondIntersectionPointsList[iPoint].radiusSquare = -1; } } std::sort(secondIntersectionPointsList.begin(), secondIntersectionPointsList.end(), compareByMetalID); PRRPM::ui32 numberOfNonDiffusedPoint = int(std::count_if( secondIntersectionPointsList.begin(), secondIntersectionPointsList.end(), isMetalIDOut)); secondIntersectionPointsList.erase(secondIntersectionPointsList.begin(), secondIntersectionPointsList.begin() + numberOfNonDiffusedPoint); } // Cast one primary camera ray per pixel. Diffuse hits are photon-map gather points; // mirror and dielectric hits are queued for the later non-recursive passes. void firstRayTracingPass() { PRRPM::f32 screenRatio = (PRRPM::f32)imageWidth / (PRRPM::f32)imageHeight; PRRPM::f32 inverseWidth = 1.0f / (PRRPM::f32)imageWidth; PRRPM::f32 inverseHeight = 1.0f / (PRRPM::f32)imageHeight; PRRPM::f32 inverseWidthOverTwo = inverseWidth / 4.0f; PRRPM::f32 inverseHeightOverTwo = inverseHeight / 2.0f; for (ui32 y = 0; y < imageHeight; ++y) { Ray ray; unsigned int yPixel = y * imageWidth; for (ui32 x = 0; x < imageWidth; ++x) { ray.direction.x = ((x + inverseWidthOverTwo) * inverseWidth - 0.5) * screenRatio; ray.direction.y = ((y + inverseHeightOverTwo) * inverseHeight - 0.5); ray.direction.z = screenRatio; ray.direction.normalize(); ray.origin = Vector3d(0.0, 0.0, 0.0); ray.objectID = -1; ray.distance = maxSceneDepth; intersectObjects(&ray); if (ray.objectID != -1) { PRRPM::ui32 index = x + (y * imageWidth); if (getObjectMaterial(ray.objectID, ray.objectType) == 0) { PRRPM::Vector3d color = getObjectColor(ray.objectID, ray.objectType); int cellID = computeCellID(ray.intersectionPosition); if (cellID >= 0 && cellID < gridSize) { intersectionPointsList[index].color = color; intersectionPointsList[index].positionAndOrigin = ray.intersectionPosition; intersectionPointsList[index].normal = getSurfaceNormal(ray); intersectionPointsList[index].pixelIndex = x + yPixel; intersectionPointsList[index].cellID = cellID; // intersectionPointsList[index].materialID = 0; intersectionPointsList[index].radiusSquare = irridianceRadiusSquare; intersectionPointsList[index].nubmerOfPhotonAndMaterialID = 0; intersectionPointsList[index].fluxAndDirection = Vector3d(); } } else if (getObjectMaterial(ray.objectID, ray.objectType) == 1) { PRRPM::Vector3d normal = getSurfaceNormal(ray); intersectionPointsList[index].positionAndOrigin = ray.intersectionPosition; intersectionPointsList[index].fluxAndDirection = ray.direction - normal * normal.dot(ray.direction) * 2.0f; intersectionPointsList[index].pixelIndex = x + yPixel; intersectionPointsList[index].nubmerOfPhotonAndMaterialID = 1; } else if (getObjectMaterial(ray.objectID, ray.objectType) == 2) { intersectionPointsList[index].positionAndOrigin = ray.intersectionPosition; intersectionPointsList[index].normal = getSurfaceNormal(ray); intersectionPointsList[index].pixelIndex = x + yPixel; intersectionPointsList[index].fluxAndDirection = ray.direction; intersectionPointsList[index].color = getObjectColor(ray.objectID, ray.objectType); intersectionPointsList[index].nubmerOfPhotonAndMaterialID = 2; } } } } } void prepareIntersectionPointsList() { // -1 cell/pixel indices mark pixels that did not produce a valid gather point. intersectionPointsList.resize((imageWidth * imageHeight)); PRRPM::IntersectionPoint intersectionPoint; intersectionPoint.color = PRRPM::Vector3d(); intersectionPoint.positionAndOrigin = PRRPM::Vector3d(); intersectionPoint.normal = PRRPM::Vector3d(); intersectionPoint.pixelIndex = -1; intersectionPoint.cellID = -1; intersectionPoint.radiusSquare = irridianceRadiusSquare; intersectionPoint.nubmerOfPhotonAndMaterialID = 0; intersectionPoint.fluxAndDirection = PRRPM::Vector3d(); std::fill(intersectionPointsList.begin(), intersectionPointsList.begin() + (imageWidth * imageHeight), intersectionPoint); } void rayTracingPass() { firstRayTracingPass(); for (ui32 iPoint = 0; iPoint < intersectionPointsList.size(); ++iPoint) { if (intersectionPointsList[iPoint].nubmerOfPhotonAndMaterialID != 0) { PRRPM::IntersectionPoint intersectionPoint = intersectionPointsList[iPoint]; intersectionPoint.nubmerOfPhotonAndMaterialID = -2; secondIntersectionPointsList.push_back(intersectionPoint); secondIntersectionPointsList.push_back(intersectionPointsList[iPoint]); } } // Seed the work list with secondary paths, then advance it a fixed number of bounces. calculateReflectionFractionForAllPoints(); for (ui32 iBounce = 1; iBounce < 5; ++iBounce) { for (ui32 iPoints = 0; iPoints < secondIntersectionPointsList.size(); ++iPoints) nextRayTracingPass(iPoints); for (ui32 iPoint = 0; iPoint < secondIntersectionPointsList.size(); ++iPoint) { if (secondIntersectionPointsList[iPoint].nubmerOfPhotonAndMaterialID == 0) intersectionPointsList.push_back(secondIntersectionPointsList[iPoint]); } std::sort(secondIntersectionPointsList.begin(), secondIntersectionPointsList.end(), compareByCellID); ui32 numberOfNonDiffusedPoint = int(std::count_if( secondIntersectionPointsList.begin(), secondIntersectionPointsList.end(), isCellIDOut)); secondIntersectionPointsList.erase( secondIntersectionPointsList.end() - (secondIntersectionPointsList.size() - numberOfNonDiffusedPoint), secondIntersectionPointsList.end()); std::vector tempsecondIntersectionPointsList; for (ui32 iPoint = 0; iPoint < secondIntersectionPointsList.size(); ++iPoint) { PRRPM::IntersectionPoint intersectionPoint; intersectionPoint.nubmerOfPhotonAndMaterialID = -2; tempsecondIntersectionPointsList.push_back(intersectionPoint); tempsecondIntersectionPointsList.push_back(secondIntersectionPointsList[iPoint]); } secondIntersectionPointsList.clear(); secondIntersectionPointsList = tempsecondIntersectionPointsList; calculateReflectionFractionForAllPoints(); } } void findPointAndComputeFlux(const Ray& photonRay, const PRRPM::Vector3d& color) { // Gather this photon into nearby camera gather points and shrink their radii as they // accumulate. PRRPM::Vector3d surfaceNormal = getSurfaceNormal(photonRay); for (std::size_t iNeighbor = 0; iNeighbor < std::size(neighborsDirection); ++iNeighbor) { int cellID = computeCellID(photonRay.intersectionPosition + neighborsDirection[iNeighbor]); if (cellID < 0 || cellID >= gridSize) continue; const int firstCell = hastValueList[cellID]; if (firstCell >= 0) { std::size_t pointIndex = static_cast(firstCell); while (true) { Vector3d distanceVector = intersectionPointsList[pointIndex].positionAndOrigin - photonRay.intersectionPosition; f32 distance = distanceVector.dot(distanceVector); IntersectionPoint* intersectionPoint = &intersectionPointsList[pointIndex]; f32 dotNormals = intersectionPoint->normal.dot(surfaceNormal); if (dotNormals > -0.001 && distance <= intersectionPoint->radiusSquare) { f32 radiusReductionCoefficient = (intersectionPointsList[pointIndex].nubmerOfPhotonAndMaterialID * alpha + alpha) / (intersectionPoint->nubmerOfPhotonAndMaterialID * alpha + 1.0); intersectionPoint->radiusSquare *= radiusReductionCoefficient; ++intersectionPoint->nubmerOfPhotonAndMaterialID; intersectionPoint->fluxAndDirection = (intersectionPoint->fluxAndDirection + intersectionPoint->color * color * MathUtility::InverstPI) * radiusReductionCoefficient; } ++pointIndex; if (pointIndex >= intersectionPointsList.size() || intersectionPointsList[pointIndex].cellID != cellID) break; } } } } void photonCasting(std::default_random_engine& randomNumberGenerator, const LightSource& lightSource) { std::uniform_real_distribution uniform(0.0, 1.0); PRRPM::Ray photonRay; generatePhotonRayFromLighSource(&photonRay, lightSource, randomNumberGenerator); photonRay.objectID = -1; photonRay.distance = maxSceneDepth; ui8 bounces = 0; Vector3d color = lightSource.intensity; bool isRefraction = false; while (true) { intersectObjects(&photonRay); if (photonRay.objectID != -1) { Vector3d objectColor = getObjectColor(photonRay.objectID, photonRay.objectType); ui8 objectMaterialID = getObjectMaterial(photonRay.objectID, photonRay.objectType); if (objectMaterialID == 0) { // Diffuse hits contribute flux; the next direction is sampled from the hemisphere. findPointAndComputeFlux(photonRay, color); ++bounces; f32 reflectionProbability = std::max(objectColor.x, std::max(objectColor.y, objectColor.z)); if (bounces >= numberOfBounces || (bounces > 1 && uniform(randomNumberGenerator) <= reflectionProbability)) break; if (isRefraction) { color = lightSource.intensity; isRefraction = false; } else color = color * getObjectColor(photonRay.objectID, photonRay.objectType) / reflectionProbability; // * 0.7f; f32 r1 = MathUtility::TwoPI * uniform(randomNumberGenerator); f32 r2 = uniform(randomNumberGenerator); f32 r2s = sqrtf(r2); PRRPM::Vector3d tempNormal = getSurfaceNormal(photonRay); if (tempNormal.dot(photonRay.direction) >= 0) tempNormal = tempNormal * -1.0f; PRRPM::Vector3d w = tempNormal; PRRPM::Vector3d u = PRRPM::Vector3d(1, 0, 0); if (fabs(w.x) > 0.1) u = PRRPM::Vector3d(0, 1, 0); u = u.cross(w); u.normalize(); PRRPM::Vector3d v = w.cross(u); photonRay.direction = u * cosf(r1) * r2s + v * sinf(r1) * r2s + w * sqrtf(1.0f - r2); photonRay.direction.normalize(); photonRay.origin = photonRay.intersectionPosition; photonRay.objectID = -1; photonRay.distance = maxSceneDepth; continue; } else if (objectMaterialID == 1) { // Mirror material: reflect the photon and continue its path. PRRPM::Vector3d normal = getSurfaceNormal(photonRay); photonRay.direction = photonRay.direction - normal * normal.dot(photonRay.direction) * 2.0; photonRay.origin = photonRay.intersectionPosition; photonRay.objectID = -1; photonRay.distance = maxSceneDepth; ++bounces; if (bounces >= numberOfBounces) break; continue; } else if (objectMaterialID == 2) { // Dielectric material: choose reflection or refraction using Fresnel probability. PRRPM::Vector3d reflectionRayDirection; PRRPM::Vector3d refractionRayDirection; f32 cosOutAngle; f32 reflectionIntensity; calculateReflectionFractionDirection( photonRay.direction, getSurfaceNormal(photonRay), &reflectionRayDirection, &refractionRayDirection, &cosOutAngle, &reflectionIntensity); if (cosOutAngle < 0) { photonRay.origin = photonRay.intersectionPosition; photonRay.direction = reflectionRayDirection; photonRay.objectID = -1; photonRay.distance = maxSceneDepth; ++bounces; if (bounces >= numberOfBounces) break; continue; } photonRay.origin = photonRay.intersectionPosition; if (uniform(randomNumberGenerator) >= reflectionIntensity) { photonRay.direction = refractionRayDirection; color = getObjectColor(photonRay.objectID, photonRay.objectType) * lightSource.intensity; } else { photonRay.direction = reflectionRayDirection; color = getObjectColor(photonRay.objectID, photonRay.objectType); } photonRay.objectID = -1; photonRay.distance = maxSceneDepth; isRefraction = true; ++bounces; if (bounces >= numberOfBounces) break; continue; } else break; } else break; } } void photonCastingPass() { if (lightSourcesList.empty()) { std::cerr << "No light sources defined in the scene\n"; return; } // Each pass launches a fresh set of photons; progress is updated in place on the console. for (ui32 iPhotonPass = 0; iPhotonPass < numberOfPhotonPass; ++iPhotonPass) { std::default_random_engine randomNumberGenerator; for (ui32 iPhoton = 0; iPhoton < numberOfPhotons; ++iPhoton) { LightSource lightSource = lightSourcesList[iPhoton % lightSourcesList.size()]; photonCasting(randomNumberGenerator, lightSource); } std::cout << "\rPass: " << iPhotonPass + 1 << '/' << numberOfPhotonPass << std::flush; } std::cout << '\n'; } // Build the spatial hash table for efficient photon gathering. void buildHashTable() { // Sort gather points by cell so each hash entry can point to the start of its cell's range. std::sort(intersectionPointsList.begin(), intersectionPointsList.end(), compareByCellID); PRRPM::ui32 numberOfNonDiffusedPoint = int( std::count_if(intersectionPointsList.begin(), intersectionPointsList.end(), isCellIDOut)); intersectionPointsList.erase(intersectionPointsList.begin(), intersectionPointsList.begin() + numberOfNonDiffusedPoint); hastValueList.resize(gridSize); std::fill(hastValueList.begin(), hastValueList.begin() + gridSize, -1); for (std::size_t i = 0; i < intersectionPointsList.size(); ++i) { const int cellID = intersectionPointsList[i].cellID; if (hastValueList[cellID] == -1) hastValueList[cellID] = i; } } // Correct the flux at each gather point to compute the final irradiance. void fluxCorrection() { // Convert accumulated photon flux to irradiance using each gather point's final disk area. imageBufferList.resize((imageWidth * imageHeight)); std::fill(imageBufferList.begin(), imageBufferList.begin() + (imageWidth * imageHeight), Vector3d(0, 0, 0)); for (std::size_t i = 0; i < intersectionPointsList.size(); ++i) { const int pixelIndex = intersectionPointsList[i].pixelIndex; imageBufferList[pixelIndex] = imageBufferList[pixelIndex] + intersectionPointsList[i].fluxAndDirection * (1.0 / (MathUtility::PI * intersectionPointsList[i].radiusSquare * numberOfPhotons * numberOfPhotonPass)); } } void saveImage(void) { const std::size_t expectedPixelCount = static_cast(imageWidth) * imageHeight; if (imageBufferList.size() != expectedPixelCount) { std::cerr << "Image buffer size does not match image dimensions\n"; return; } // Convert linear floating-point RGB into display-encoded 8-bit RGB for the BMP writer. std::vector pixels(imageBufferList.size() * 3); const auto toByte = [](f32 channel) { channel = std::clamp(channel, 0.0f, 1.0f); const f32 srgb = channel <= 0.0031308f ? 12.92f * channel : 1.055f * std::pow(channel, 1.0f / 2.4f) - 0.055f; return static_cast(std::lround(srgb * 255.0f)); }; for (std::size_t y = 0; y < imageHeight; ++y) { // Camera rows increase upward; image files store the top row first. const std::size_t sourceRow = imageHeight - 1 - y; for (std::size_t x = 0; x < imageWidth; ++x) { const std::size_t sourceIndex = sourceRow * imageWidth + x; const std::size_t targetIndex = (y * imageWidth + x) * 3; pixels[targetIndex] = toByte(imageBufferList[sourceIndex].x); pixels[targetIndex + 1] = toByte(imageBufferList[sourceIndex].y); pixels[targetIndex + 2] = toByte(imageBufferList[sourceIndex].z); } } if (!stbi_write_bmp(outFileName.c_str(), static_cast(imageWidth), static_cast(imageHeight), 3, pixels.data())) std::cerr << "Failed to write image: " << outFileName << '\n'; } void cleanUp(void) { // delete[] imageBuffer; } void displayItroAndInfo(void) { std::cout << " Scene Information"; std::cout << "\n Number of triangles : " << triangleList.size(); std::cout << "\n Light : "; if (isLightOn) std::cout << "on"; else std::cout << "off"; std::cout << "\n Image size : " << imageWidth << " X " << imageHeight; std::cout << "\n Output file : " << outFileName << "\n\n"; } int main(int argc, char* argv[]) { if (argc != 3) { std::cerr << "Usage: " << argv[0] << " \n"; return 1; } const std::filesystem::path sceneFilePath = argv[1]; outFileName = argv[2]; if (!loadSceneFile(sceneFilePath.string())) return 1; displayItroAndInfo(); // Pipeline: trace camera paths, build the photon lookup table, cast photons, then write output. const auto renderingStart = std::chrono::steady_clock::now(); prepareIntersectionPointsList(); std::cout << "raytracing:1:start\n"; rayTracingPass(); std::cout << "raytracing:1:done\nphotontracing:start\n"; buildHashTable(); photonCastingPass(); std::cout << "photontracing:done\n"; // return 0; fluxCorrection(); const auto renderingEnd = std::chrono::steady_clock::now(); const double totalRenderingTime = std::chrono::duration(renderingEnd - renderingStart).count(); std::cout << "renderingtime:" << totalRenderingTime << std::endl; // Save result image buffer in file saveImage(); cleanUp(); std::cout << "done\n"; return 0; }