Commit 85939344 authored by Tomáš Iser's avatar Tomáš Iser
Browse files

Initial commit: NPGR010 path tracer skeleton

Source code from the main branch of the course repository, with an
updated CMakeLists.txt (CMake 3.16+, required OpenMP, Release by
default, automatic Homebrew libomp on macOS) and macOS build
instructions.
parents
Loading
Loading
Loading
Loading

.gitignore

0 → 100644
+9 −0
Original line number Diff line number Diff line
/build

# Rendered images
*.bmp
*.hdr
*.pfm

# macOS
.DS_Store

BUILD_MACOS.md

0 → 100644
+95 −0
Original line number Diff line number Diff line
# Building the NPGR010 path tracer on macOS

Tested on macOS 26 (Apple Silicon), Apple clang 17, CMake 4.4, libomp 23.
Works the same on Intel Macs (Homebrew just lives in `/usr/local` instead of `/opt/homebrew`).

All commands are typed in the **Terminal** app (Cmd+Space → "Terminal").

## 1. Install Apple's compiler (Command Line Tools)

```bash
xcode-select --install
```

A dialog pops up; click *Install*. You do **not** need the full Xcode app.
Check it worked:

```bash
clang++ --version
```

## 2. Install Homebrew (package manager)

Skip if `brew --version` already works. Otherwise follow https://brew.sh:

```bash
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
```

At the end the installer prints two "Next steps" commands (adding `brew` to your
`~/.zprofile`). **Run them**, then open a new Terminal window.

## 3. Install CMake and OpenMP

```bash
brew install cmake libomp
```

Why `libomp`? The renderer uses OpenMP (`#include <omp.h>`) to render on all CPU
cores. Apple's clang supports OpenMP but does not ship the runtime library, so
without it you get `fatal error: 'omp.h' file not found`.

Homebrew installs libomp as "keg-only" (not in the default search paths). The
project's `CMakeLists.txt` asks `brew` where it is, so you don't have to.

## 4. Configure and build

From the project folder (the one containing `CMakeLists.txt`):

```bash
cmake -S . -B build
cmake --build build
```

- The first command (*configure*) checks your compiler and libraries and writes
  build files into a separate `build/` folder.
- The second command compiles. After editing source files, only this one is needed.
- The build is optimized (Release) by default — a path tracer is many times
  slower without optimizations. For debugging, configure with
  `-DCMAKE_BUILD_TYPE=Debug` instead.

During configuration you should see `-- Found OpenMP: TRUE`. If it fails with
`Could NOT find OpenMP`, run `brew install libomp`, delete the `build` folder
and configure again.

## 5. Run

```bash
./build/NPGR010 -h
./build/NPGR010 -s 0 -i 16 -o test.bmp
open test.bmp
```

`open` shows the image in Preview. `.hdr` output (the default) can also be
opened in Preview; `.bmp` is the simplest to view anywhere.

## Troubleshooting

| Symptom | Fix |
|---|---|
| `brew: command not found` | Run the two "Next steps" lines from the Homebrew installer, open a new Terminal. |
| `xcrun: error: invalid active developer path` | Step 1 is missing (often after a macOS upgrade): `xcode-select --install`. |
| `Could NOT find OpenMP` | `brew install libomp`, then `rm -rf build` and redo step 4. |
| Rendering is very slow | You configured with `-DCMAKE_BUILD_TYPE=Debug`; reconfigure without it. |
| macOS blocks the program as "unidentified developer" | Only happens for binaries downloaded from elsewhere — build it yourself instead. |

## Using an IDE

No extra settings are needed — just open the folder:

- **VS Code**: install the *C/C++* and *CMake Tools* extensions and open the
  folder; pick the *Release* variant for fast rendering.
- **CLion**: open the folder; it picks up `CMakeLists.txt` automatically.
- **Xcode**: `cmake -S . -B build-xcode -G Xcode`, then
  `open build-xcode/NPGR010.xcodeproj` and choose the Release scheme
  (*Product → Scheme → Edit Scheme → Run → Build Configuration*).

CMakeLists.txt

0 → 100644
+27 −0
Original line number Diff line number Diff line
cmake_minimum_required(VERSION 3.16)

project(NPGR010 LANGUAGES CXX)

# Default to an optimized build (single-config generators: Makefiles, Ninja)
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
    set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()

add_executable(NPGR010 src/pg3render.cpp)

target_compile_features(NPGR010 PRIVATE cxx_std_17)

# macOS: Apple clang has no OpenMP runtime; use Homebrew's keg-only libomp
if(APPLE AND NOT OpenMP_ROOT)
    execute_process(
        COMMAND brew --prefix libomp
        OUTPUT_VARIABLE HOMEBREW_LIBOMP
        OUTPUT_STRIP_TRAILING_WHITESPACE
        ERROR_QUIET)
    if(EXISTS "${HOMEBREW_LIBOMP}")
        set(OpenMP_ROOT "${HOMEBREW_LIBOMP}")
    endif()
endif()

find_package(OpenMP REQUIRED)
target_link_libraries(NPGR010 PRIVATE OpenMP::OpenMP_CXX)

README.md

0 → 100644
+47 −0
Original line number Diff line number Diff line
# NPGR010 - Homeworks

A simple C++17 path tracer used as the starting point for the NPGR010 homeworks.

## Requirements

- A C++17 compiler with OpenMP (GCC, Clang, or MSVC)
- CMake 3.16 or newer

macOS users: Apple's compiler needs a few extra steps, see [BUILD_MACOS.md](BUILD_MACOS.md).

## Building

From the project folder:

```
cmake -S . -B build
cmake --build build
```

The first command configures the project into the `build` subdirectory, the second
one compiles it. After editing the sources, only the second command is needed.

The build is optimized (Release) by default. For a debug build, configure with
`-DCMAKE_BUILD_TYPE=Debug`. With Visual Studio, choose the configuration when
building instead, e.g. `cmake --build build --config Release`.

## Running

```
./build/NPGR010 -h
./build/NPGR010 -s 0 -i 16 -o test.bmp
```

With Visual Studio, the executable is in `build\Release\NPGR010.exe`.

Output can be saved as `.hdr` (default), `.pfm`, or `.bmp`.

## Viewing the results

Use the `.hdr` images. They keep the full range of light values, unlike
`.bmp`, which is clamped and gamma-corrected for quick previews only.
Open them in [tev](https://github.com/Tom94/tev), an HDR image viewer
that lets you change the exposure and compare two images. Downloads for
Windows, Linux and macOS are on its
[releases page](https://github.com/Tom94/tev/releases); on macOS you can
also run `brew install --cask tev`.

src/camera.hpp

0 → 100644
+102 −0
Original line number Diff line number Diff line
#pragma once

#include <vector>
#include <cmath>
#include "math.hpp"
#include "ray.hpp"

class Camera
{
public:

    void Setup(
        const Vec3f &aPosition,
        const Vec3f &aForward,
        const Vec3f &aUp,
        const Vec2f &aResolution,
        float       aHorizontalFOV)
    {
        const Vec3f forward = Normalize(aForward);
        const Vec3f up      = Normalize(Cross(aUp, -forward));
        const Vec3f left    = Cross(-forward, up);

        mPosition   = aPosition;
        mForward    = forward;
        mResolution = aResolution;

        const Vec3f pos(
            Dot(up, aPosition),
            Dot(left, aPosition),
            Dot(-forward, aPosition));

        Mat4f worldToCamera = Mat4f::Indetity();
        worldToCamera.SetRow(0, up,       -pos.x);
        worldToCamera.SetRow(1, left,     -pos.y);
        worldToCamera.SetRow(2, -forward, -pos.z);

        const Mat4f perspective = Mat4f::Perspective(aHorizontalFOV, 0.1f, 10000.f);
        const Mat4f worldToNScreen = perspective * worldToCamera;
        const Mat4f nscreenToWorld = Invert(worldToNScreen);

        mWorldToRaster  =
            Mat4f::Scale(Vec3f(aResolution.x * 0.5f, aResolution.y * 0.5f, 0)) *
            Mat4f::Translate(Vec3f(1.f, 1.f, 0)) * worldToNScreen;

        mRasterToWorld  = nscreenToWorld *
            Mat4f::Translate(Vec3f(-1.f, -1.f, 0)) *
            Mat4f::Scale(Vec3f(2.f / aResolution.x, 2.f / aResolution.y, 0));

        const float tanHalfAngle = std::tan(aHorizontalFOV * PI_F / 360.f);
        mPixelArea = 4.f * Sqr(tanHalfAngle) / Sqr(aResolution.x);
    }

    int RasterToIndex(const Vec2f &aPixelCoords) const
    {
        return int(std::floor(aPixelCoords.x) + std::floor(aPixelCoords.y) * mResolution.x);
    }

    Vec2f IndexToRaster(const int &aPixelIndex) const
    {
        const float y = std::floor(aPixelIndex / mResolution.x);
        const float x = float(aPixelIndex) - y * mResolution.x;
        return Vec2f(x, y);
    }

    Vec3f RasterToWorld(const Vec2f &aRasterXY) const
    {
        return mRasterToWorld.TransformPoint(Vec3f(aRasterXY.x, aRasterXY.y, 0));
    }

    Vec2f WorldToRaster(const Vec3f &aWorldPos) const
    {
        Vec3f temp = mWorldToRaster.TransformPoint(aWorldPos);
        return Vec2f(temp.x, temp.y);
    }

    // returns false when raster position is outside screen space
    bool CheckRaster(const Vec2f &aRasterPos) const
    {
        return aRasterPos.x >= 0 && aRasterPos.y >= 0 &&
            aRasterPos.x < mResolution.x && aRasterPos.y < mResolution.y;
    }

    Ray GenerateRay(const Vec2f &aRasterXY) const
    {
        const Vec3f worldRaster = RasterToWorld(aRasterXY);

        Ray res;
        res.origin  = mPosition;
        res.direction  = Normalize(worldRaster - mPosition);
        res.offset = 0;
        return res;
    }

public:

    Vec3f mPosition;
    Vec3f mForward;
    Vec2f mResolution;
    Mat4f mRasterToWorld;
    Mat4f mWorldToRaster;
    float mPixelArea;
};
Loading