├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── LICENSE.txt ├── README.md ├── cmake └── sol-config.cmake.in ├── contrib └── CMakeLists.txt ├── include └── sol │ ├── algorithms │ └── path_tracer.h │ ├── bsdfs.h │ ├── cameras.h │ ├── color.h │ ├── geometry.h │ ├── image.h │ ├── lights.h │ ├── render_job.h │ ├── renderer.h │ ├── samplers.h │ ├── scene.h │ ├── shapes.h │ ├── textures.h │ └── triangle_mesh.h ├── src ├── CMakeLists.txt ├── algorithms │ └── path_tracer.cpp ├── bsdfs.cpp ├── cameras.cpp ├── formats │ ├── exr.cpp │ ├── exr.h │ ├── jpeg.cpp │ ├── jpeg.h │ ├── obj.cpp │ ├── obj.h │ ├── png.cpp │ ├── png.h │ ├── tiff.cpp │ └── tiff.h ├── image.cpp ├── lights.cpp ├── render_job.cpp ├── scene.cpp ├── scene_loader.cpp ├── scene_loader.h ├── shapes.cpp ├── textures.cpp └── triangle_mesh.cpp └── test ├── CMakeLists.txt ├── data ├── copyright.txt ├── cornell_box.mtl ├── cornell_box.obj ├── cornell_box.toml ├── cornell_box_glossy.mtl ├── cornell_box_glossy.obj ├── cornell_box_glossy.toml ├── cornell_box_specular.mtl ├── cornell_box_specular.obj ├── cornell_box_specular.toml ├── cornell_box_water.mtl ├── cornell_box_water.obj └── cornell_box_water.toml └── driver.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | build/ 3 | install/ 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "contrib/tomlplusplus"] 2 | path = contrib/tomlplusplus 3 | url = git@github.com:marzer/tomlplusplus.git 4 | [submodule "contrib/tinyexr"] 5 | path = contrib/tinyexr 6 | url = git@github.com:syoyo/tinyexr.git 7 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.11) 2 | project(sol VERSION 1.0) 3 | 4 | add_subdirectory(contrib) 5 | add_subdirectory(src) 6 | 7 | # Make sure to only build tests when building this project, 8 | # and not when importing it into another one. 9 | if (CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) 10 | include(CTest) 11 | if (BUILD_TESTING) 12 | add_subdirectory(test) 13 | endif () 14 | endif () 15 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2021 Arsène Pérard-Gayot 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SoL 2 | 3 | SoL (for _Speed of Light_, or sun in Spanish) is a small rendering library written in C++20. 4 | Its goal is to strike a good balance between performance and usability, 5 | and allow easy experimentation for rendering researchers. 6 | SoL is provided as a library that can be embedded in other projects. 7 | 8 | ## Building 9 | 10 | The recommended way to build SoL is by cloning the meta-repository [Solar](https://github.com/madmann91/solar), 11 | which downloads and installs all dependencies automatically. 12 | 13 | If you prefer to do this manually, you will need Git, CMake, 14 | [proto](https://github.com/madmann91/proto), [par](https://github.com/madmann91/par), 15 | and [bvh](https://github.com/madmann91/bvh) (`v2` branch). 16 | Additionally, SoL can use libpng, libjpeg, and libtiff when those are present on the system. 17 | These libraries are not required to build a working version of SoL, but without them, it can only load and save EXR image files. 18 | 19 | Before building, make sure you have downloaded all the submodules, by running: 20 | 21 | git submodule update --init --recursive 22 | 23 | Once all submodules have been downloaded and the dependencies have been downloaded and installed, type: 24 | 25 | mkdir build 26 | cd build 27 | cmake \ 28 | -Dproto_DIR= \ 29 | -Dpar_DIR= \ 30 | -Dbvh_DIR= \ 31 | -DSOL_MULTITHREADING_FRAMEWORK= \ 32 | -DCMAKE_BUILD_TYPE= 33 | cmake --build . 34 | -------------------------------------------------------------------------------- /cmake/sol-config.cmake.in: -------------------------------------------------------------------------------- 1 | # This file provides the imported target sol::sol, which can be used in a 2 | # call to `target_link_libraries` to pull the SoL library and all 3 | # its dependencies into another target. 4 | 5 | include(CMakeFindDependencyMacro) 6 | find_dependency(TIFF) 7 | find_dependency(JPEG) 8 | find_dependency(PNG) 9 | find_dependency(TBB) 10 | find_dependency(bvh) 11 | find_dependency(proto) 12 | find_dependency(Threads) 13 | @SOL_DEPENDS_TBB@ 14 | @SOL_DEPENDS_OMP@ 15 | include("${CMAKE_CURRENT_LIST_DIR}/sol-targets.cmake") 16 | -------------------------------------------------------------------------------- /contrib/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(tomlplusplus INTERFACE) 2 | target_include_directories(tomlplusplus INTERFACE $) 3 | 4 | add_library(tinyexr INTERFACE) 5 | target_include_directories(tinyexr INTERFACE $) 6 | -------------------------------------------------------------------------------- /include/sol/algorithms/path_tracer.h: -------------------------------------------------------------------------------- 1 | #ifndef SOL_ALGORITHMS_PATH_TRACER_H 2 | #define SOL_ALGORITHMS_PATH_TRACER_H 3 | 4 | #include "sol/renderer.h" 5 | #include "sol/color.h" 6 | 7 | #include 8 | 9 | #if defined(SOL_ENABLE_TBB) 10 | #include 11 | #elif defined(SOL_ENABLE_OMP) 12 | #include 13 | #else 14 | #include 15 | #endif 16 | 17 | namespace sol { 18 | 19 | class Sampler; 20 | 21 | namespace detail { 22 | 23 | struct PathTracerConfig { 24 | size_t max_path_len = 64; ///< Maximum path length 25 | size_t min_rr_path_len = 3; ///< Minimum path length to enable Russian Roulette 26 | float min_survival_prob = 0.05f; ///< Minimum Russian Roulette survival probability (must be in `[0, 1]`) 27 | float max_survival_prob = 0.75f; ///< Maximum Russian Roulette survival probability (must be in `[0, 1]`) 28 | float ray_offset = 1.e-5f; ///< Ray offset, in order to avoid self-intersections. Usually scene-dependent. 29 | }; 30 | 31 | } // namespace detail 32 | 33 | class PathTracer final : public Renderer { 34 | public: 35 | using Config = detail::PathTracerConfig; 36 | 37 | PathTracer(const Scene& scene, const Config& config = {}) 38 | : Renderer("PathTracer", scene), config_(config) 39 | {} 40 | 41 | void render(Image&, size_t, size_t) const override; 42 | 43 | private: 44 | Color trace_path(Sampler&, proto::Rayf) const; 45 | 46 | #if defined(SOL_ENABLE_TBB) 47 | par::tbb::Executor executor_; 48 | #elif defined(SOL_ENABLE_OMP) 49 | par::omp::DynamicExecutor executor_; 50 | #else 51 | par::SequentialExecutor executor_; 52 | #endif 53 | Config config_; 54 | }; 55 | 56 | } // namespace sol 57 | 58 | #endif 59 | -------------------------------------------------------------------------------- /include/sol/bsdfs.h: -------------------------------------------------------------------------------- 1 | #ifndef SOL_BSDFS_H 2 | #define SOL_BSDFS_H 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #include "sol/color.h" 11 | #include "sol/geometry.h" 12 | 13 | namespace sol { 14 | 15 | class Sampler; 16 | class Texture; 17 | class ColorTexture; 18 | 19 | /// Sample returned by a BSDF, including direction, pdf, and color. 20 | struct BsdfSample { 21 | proto::Vec3f in_dir; ///< Sampled direction 22 | float pdf; ///< Probability density function, evaluated for the direction 23 | float cos; ///< Cosine term of the rendering equation 24 | Color color; ///< Color of the sample (BSDF value) 25 | }; 26 | 27 | /// BSDF represented as a black box that can be sampled and evaluated. 28 | class Bsdf { 29 | public: 30 | const enum class Tag { 31 | DiffuseBsdf, 32 | PhongBsdf, 33 | MirrorBsdf, 34 | GlassBsdf, 35 | InterpBsdf 36 | } tag; 37 | 38 | /// Classification of BSDF shapes 39 | const enum class Type { 40 | Diffuse = 0, ///< Mostly diffuse, i.e no major features, mostly uniform 41 | Glossy = 1, ///< Mostly glossy, i.e hard for Photon Mapping 42 | Specular = 2 ///< Purely specular, i.e merging/connections are not possible 43 | } type; 44 | 45 | Bsdf(Tag tag, Type type) 46 | : tag(tag), type(type) 47 | {} 48 | 49 | virtual ~Bsdf() {} 50 | 51 | /// Evaluates the material for the given pair of directions and surface point. 52 | virtual Color eval( 53 | [[maybe_unused]] const proto::Vec3f& in_dir, 54 | [[maybe_unused]] const SurfaceInfo& surf_info, 55 | [[maybe_unused]] const proto::Vec3f& out_dir) const 56 | { 57 | return Color::black(); 58 | } 59 | 60 | /// Samples the material given a surface point and an outgoing direction. 61 | /// This may fail to return a sample, for instance if random sampling generated an 62 | /// incorrect direction, or if the surface configuration makes it impossible to generate 63 | /// a proper direction. 64 | virtual std::optional sample( 65 | [[maybe_unused]] Sampler& sampler, 66 | [[maybe_unused]] const SurfaceInfo& surf_info, 67 | [[maybe_unused]] const proto::Vec3f& out_dir, 68 | [[maybe_unused]] bool is_adjoint = false) const 69 | { 70 | return std::nullopt; 71 | } 72 | 73 | /// Returns the probability to sample the given input direction (sampled using the sample function). 74 | virtual float pdf( 75 | [[maybe_unused]] const proto::Vec3f& in_dir, 76 | [[maybe_unused]] const SurfaceInfo& surf_info, 77 | [[maybe_unused]] const proto::Vec3f& out_dir) const 78 | { 79 | return 0.0f; 80 | } 81 | 82 | virtual proto::fnv::Hasher& hash(proto::fnv::Hasher&) const = 0; 83 | virtual bool equals(const Bsdf&) const = 0; 84 | 85 | protected: 86 | // Utility function to check the validity of a `BsdfSample`. 87 | // It prevents corner cases that will cause issues (zero pdf, direction parallel/under the surface). 88 | // When `ExpectBelowSurface` is true, it expects the direction to be under the surface, otherwise above. 89 | template 90 | static std::optional validate_sample(const SurfaceInfo& surf_info, const BsdfSample& bsdf_sample) { 91 | bool is_below_surface = proto::dot(bsdf_sample.in_dir, surf_info.face_normal) < 0; 92 | return bsdf_sample.pdf > 0 && (is_below_surface == ExpectBelowSurface) 93 | ? std::make_optional(bsdf_sample) : std::nullopt; 94 | } 95 | }; 96 | 97 | /// Purely diffuse (Lambertian) BSDF. 98 | class DiffuseBsdf final : public Bsdf { 99 | public: 100 | DiffuseBsdf(const ColorTexture&); 101 | 102 | std::optional sample(Sampler&, const SurfaceInfo&, const proto::Vec3f&, bool) const override; 103 | Color eval(const proto::Vec3f&, const SurfaceInfo&, const proto::Vec3f&) const override; 104 | float pdf(const proto::Vec3f&, const SurfaceInfo&, const proto::Vec3f&) const override; 105 | 106 | proto::fnv::Hasher& hash(proto::fnv::Hasher&) const override; 107 | bool equals(const Bsdf&) const override; 108 | 109 | private: 110 | const ColorTexture& kd_; 111 | }; 112 | 113 | /// Specular part of the modified (physically correct) Phong. 114 | class PhongBsdf final : public Bsdf { 115 | public: 116 | PhongBsdf(const ColorTexture&, const Texture&); 117 | 118 | std::optional sample(Sampler&, const SurfaceInfo&, const proto::Vec3f&, bool) const override; 119 | Color eval(const proto::Vec3f&, const SurfaceInfo&, const proto::Vec3f&) const override; 120 | float pdf(const proto::Vec3f&, const SurfaceInfo&, const proto::Vec3f&) const override; 121 | 122 | proto::fnv::Hasher& hash(proto::fnv::Hasher&) const override; 123 | bool equals(const Bsdf&) const override; 124 | 125 | private: 126 | static Color eval(const proto::Vec3f&, const SurfaceInfo&, const proto::Vec3f&, const Color&, float); 127 | 128 | const ColorTexture& ks_; 129 | const Texture& ns_; 130 | }; 131 | 132 | /// Perfect mirror BSDF. 133 | class MirrorBsdf final : public Bsdf { 134 | public: 135 | MirrorBsdf(const ColorTexture&); 136 | 137 | std::optional sample(Sampler&, const SurfaceInfo&, const proto::Vec3f&, bool) const override; 138 | proto::fnv::Hasher& hash(proto::fnv::Hasher&) const override; 139 | bool equals(const Bsdf&) const override; 140 | 141 | private: 142 | const ColorTexture& ks_; 143 | }; 144 | 145 | /// BSDF that can represent glass or any separation between two mediums with different indices. 146 | class GlassBsdf final : public Bsdf { 147 | public: 148 | GlassBsdf( 149 | const ColorTexture& ks, 150 | const ColorTexture& kt, 151 | const Texture& eta); 152 | 153 | std::optional sample(Sampler&, const SurfaceInfo&, const proto::Vec3f&, bool) const override; 154 | proto::fnv::Hasher& hash(proto::fnv::Hasher&) const override; 155 | bool equals(const Bsdf&) const override; 156 | 157 | private: 158 | const ColorTexture& ks_; 159 | const ColorTexture& kt_; 160 | const Texture& eta_; 161 | }; 162 | 163 | /// A BSDF that interpolates between two Bsdfs. 164 | class InterpBsdf final : public Bsdf { 165 | public: 166 | InterpBsdf(const Bsdf*, const Bsdf*, const Texture&); 167 | 168 | std::optional sample(Sampler&, const SurfaceInfo&, const proto::Vec3f&, bool) const override; 169 | RgbColor eval(const proto::Vec3f&, const SurfaceInfo&, const proto::Vec3f&) const override; 170 | float pdf(const proto::Vec3f&, const SurfaceInfo&, const proto::Vec3f&) const override; 171 | 172 | proto::fnv::Hasher& hash(proto::fnv::Hasher&) const override; 173 | bool equals(const Bsdf&) const override; 174 | 175 | private: 176 | static Type infer_type(Type, Type); 177 | 178 | const Bsdf* a_; 179 | const Bsdf* b_; 180 | const Texture& k_; 181 | }; 182 | 183 | } // namespace sol 184 | 185 | #endif 186 | -------------------------------------------------------------------------------- /include/sol/cameras.h: -------------------------------------------------------------------------------- 1 | #ifndef SOL_CAMERAS_H 2 | #define SOL_CAMERAS_H 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | namespace sol { 10 | 11 | /// Structure that holds the local geometry information on a camera lens. 12 | struct LensGeometry { 13 | float cos; ///< Cosine between the local camera direction and the image plane normal 14 | float dist; ///< Distance between the camera and the point on the image plane 15 | float area; ///< Local pixel area divided by total area 16 | }; 17 | 18 | /// Base class for cameras. 19 | /// By convention, uv-coordinates on the image plane are in the range `[-1, 1]`. 20 | class Camera { 21 | public: 22 | virtual ~Camera() {} 23 | 24 | /// Generates a ray for a point on the image plane, represented by uv-coordinates. 25 | virtual proto::Rayf generate_ray(const proto::Vec2f& uv) const = 0; 26 | /// Projects a point onto the image plane and returns the corresponding uv-coordinates. 27 | virtual proto::Vec2f project(const proto::Vec3f& point) const = 0; 28 | /// Returns a point onto the image plane from uv-coordinates. 29 | virtual proto::Vec3f unproject(const proto::Vec2f& uv) const = 0; 30 | /// Returns the lens geometry at a given point on the image plane, represented by its uv-coordinates. 31 | virtual LensGeometry geometry(const proto::Vec2f& uv) const = 0; 32 | }; 33 | 34 | /// A perspective camera based on the pinhole camera model. 35 | class PerspectiveCamera final : public Camera { 36 | public: 37 | PerspectiveCamera( 38 | const proto::Vec3f& eye, 39 | const proto::Vec3f& dir, 40 | const proto::Vec3f& up, 41 | float horz_fov, 42 | float aspect_ratio); 43 | 44 | proto::Rayf generate_ray(const proto::Vec2f&) const override; 45 | proto::Vec2f project(const proto::Vec3f&) const override; 46 | proto::Vec3f unproject(const proto::Vec2f&) const override; 47 | LensGeometry geometry(const proto::Vec2f&) const override; 48 | 49 | private: 50 | proto::Vec3f eye_; 51 | proto::Vec3f dir_; 52 | proto::Vec3f right_; 53 | proto::Vec3f up_; 54 | float w_, h_; 55 | }; 56 | 57 | } // namespace sol 58 | 59 | #endif 60 | -------------------------------------------------------------------------------- /include/sol/color.h: -------------------------------------------------------------------------------- 1 | #ifndef SOL_COLOR_H 2 | #define SOL_COLOR_H 3 | 4 | #include 5 | 6 | #include 7 | 8 | namespace sol { 9 | 10 | /// Color encoded using three floating-point values, using the sRGB color space. 11 | struct RgbColor { 12 | float r, g, b; 13 | 14 | RgbColor() = default; 15 | explicit constexpr RgbColor(float rgb) : r(rgb), g(rgb), b(rgb) {} 16 | explicit constexpr RgbColor(float r, float g, float b) : r(r), g(g), b(b) {} 17 | 18 | RgbColor& operator += (const RgbColor& other) { return *this = *this + other; } 19 | RgbColor& operator -= (const RgbColor& other) { return *this = *this - other; } 20 | RgbColor& operator *= (const RgbColor& other) { return *this = *this * other; } 21 | RgbColor& operator /= (const RgbColor& other) { return *this = *this / other; } 22 | RgbColor& operator *= (float other) { return *this = *this * other; } 23 | RgbColor& operator /= (float other) { return *this = *this * other; } 24 | 25 | RgbColor operator + (const RgbColor& other) const { 26 | return RgbColor(r + other.r, g + other.g, b + other.b); 27 | } 28 | 29 | RgbColor operator - (const RgbColor& other) const { 30 | return RgbColor(r - other.r, g - other.g, b - other.b); 31 | } 32 | 33 | RgbColor operator * (const RgbColor& other) const { 34 | return RgbColor(r * other.r, g * other.g, b * other.b); 35 | } 36 | 37 | RgbColor operator / (const RgbColor& other) const { 38 | return RgbColor(r / other.r, g / other.g, b / other.b); 39 | } 40 | 41 | RgbColor operator * (float other) const { return RgbColor(r * other, g * other, b * other); } 42 | RgbColor operator / (float other) const { return *this * (1.0f / other); } 43 | 44 | friend RgbColor operator * (float other, const RgbColor& color) { return color * other; } 45 | friend RgbColor operator / (float other, const RgbColor& color) { return RgbColor(other / color.r, other / color.g, other / color.b); } 46 | 47 | bool operator == (const RgbColor& other) const { 48 | return r == other.r && g == other.g && b == other.b; 49 | } 50 | 51 | template 52 | Hasher& hash(Hasher& hasher) const { 53 | return hasher.combine(r).combine(g).combine(b); 54 | } 55 | 56 | static constexpr float default_gamma() { return 2.2f; } 57 | 58 | float luminance() const { return r * 0.2126f + g * 0.7152f + b * 0.0722f; } 59 | bool is_black() const { return r == 0.0f && g == 0.0f && b == 0.0f; } 60 | bool is_constant() const { return r == g && r == b; } 61 | 62 | static constexpr RgbColor black() { return constant(0); } 63 | static constexpr RgbColor constant(float c) { return RgbColor(c); } 64 | }; 65 | 66 | inline RgbColor lerp(const RgbColor& a, const RgbColor& b, float t) { 67 | return RgbColor( 68 | proto::lerp(a.r, b.r, t), 69 | proto::lerp(a.g, b.g, t), 70 | proto::lerp(a.b, b.b, t)); 71 | } 72 | 73 | inline RgbColor lerp(const RgbColor& a, const RgbColor& b, const RgbColor& c, float u, float v) { 74 | return RgbColor( 75 | proto::lerp(a.r, b.r, c.r, u, v), 76 | proto::lerp(a.g, b.g, c.g, u, v), 77 | proto::lerp(a.b, b.b, c.b, u, v)); 78 | } 79 | 80 | using Color = RgbColor; 81 | 82 | } // namespace sol 83 | 84 | #endif 85 | -------------------------------------------------------------------------------- /include/sol/geometry.h: -------------------------------------------------------------------------------- 1 | #ifndef SOL_GEOMETRY_H 2 | #define SOL_GEOMETRY_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace sol { 9 | 10 | class Bsdf; 11 | class Light; 12 | 13 | /// Surface information for a specific point on a surface. 14 | /// This information is required to perform various shading operations. 15 | struct SurfaceInfo { 16 | bool is_front_side; ///< True if the point is on the front of the surface 17 | proto::Vec3f point; ///< Hit point in world coordinates 18 | proto::Vec2f tex_coords; ///< Texture coordinates 19 | proto::Vec2f surf_coords; ///< Coordinates on the surface (depends on the surface type) 20 | proto::Vec3f face_normal; ///< Geometric normal 21 | proto::Mat3x3f local; ///< Local coordinates at the hit point, w.r.t shading normal 22 | 23 | proto::Vec3f normal() const { return local.col(2); } 24 | }; 25 | 26 | /// Result of intersecting a ray with a scene node. 27 | struct Hit { 28 | SurfaceInfo surf_info; ///< Surface information at the hit point 29 | const Light* light; ///< Light source at the hit point, if any (can be null) 30 | const Bsdf* bsdf; ///< BSDF at the hit point, if any (can be null) 31 | }; 32 | 33 | class Geometry { 34 | public: 35 | virtual ~Geometry() = default; 36 | 37 | /// Intersects the node with a ray, returns either a `Hit` that corresponds 38 | /// to the closest intersection along the ray, or nothing. 39 | /// If an intersection is found, the `tmax` parameter of the ray is updated. 40 | virtual std::optional intersect_closest(proto::Rayf&) const = 0; 41 | /// Tests if a given ray intersects the node or not. 42 | virtual bool intersect_any(const proto::Rayf&) const = 0; 43 | }; 44 | 45 | } // namespace sol 46 | 47 | #endif 48 | -------------------------------------------------------------------------------- /include/sol/image.h: -------------------------------------------------------------------------------- 1 | #ifndef SOL_IMAGE_H 2 | #define SOL_IMAGE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "sol/color.h" 12 | 13 | namespace sol { 14 | 15 | /// Image represented as a list of floating-point channels, each having the same width and height. 16 | /// An image can have an arbitrary number of channels, but some image formats only support 3 or 4 channels. 17 | /// By convention, the top-left corner of the image is at (0, 0). 18 | struct Image { 19 | template using Word = std::make_unsigned_t>; 20 | 21 | public: 22 | /// File formats for image `load()` and `save()` functions. 23 | enum class Format { 24 | Auto, Png, Jpeg, Tiff, Exr 25 | }; 26 | 27 | using Channel = std::unique_ptr; 28 | using Channels = std::vector; 29 | 30 | Image() = default; 31 | Image(const Image&) = delete; 32 | Image(Image&&) = default; 33 | Image(size_t width, size_t height, size_t channel_count) 34 | : width_(width), height_(height) 35 | { 36 | channels_.resize(channel_count); 37 | for (size_t i = 0; i < channel_count; ++i) 38 | channels_[i] = std::make_unique(width * height); 39 | } 40 | 41 | Image& operator = (const Image&) = delete; 42 | Image& operator = (Image&&) = default; 43 | 44 | size_t width() const { return width_; } 45 | size_t height() const { return height_; } 46 | size_t channel_count() const { return channels_.size(); } 47 | 48 | RgbColor rgb_at(size_t x, size_t y) const { 49 | assert(channel_count() == 3); 50 | auto i = y * width_ + x; 51 | return RgbColor(channels_[0][i], channels_[1][i], channels_[2][i]); 52 | } 53 | 54 | void accumulate(size_t x, size_t y, const RgbColor& color) { 55 | assert(channel_count() == 3); 56 | auto i = y * width_ + x; 57 | channels_[0][i] += color.r; 58 | channels_[1][i] += color.g; 59 | channels_[2][i] += color.b; 60 | } 61 | 62 | Channel& channel(size_t i) { return channels_[i]; } 63 | const Channel& channel(size_t i) const { return channels_[i]; } 64 | 65 | /// Resets every pixel in the image to the given value. 66 | void clear(float value = 0.0f); 67 | 68 | /// Scales every pixel in the image by the given value. 69 | void scale(float value); 70 | 71 | /// Saves the image to a file, using the given format. 72 | /// If format is `Auto`, the function uses the EXR format for the image. 73 | bool save(const std::string_view& path, Format format = Format::Auto) const; 74 | 75 | /// Loads an image from a file, using the given format hint. 76 | /// If the format is `Auto`, the function will try to auto-detect which format the image is in. 77 | static std::optional load(const std::string_view& path, Format format = Format::Auto); 78 | 79 | /// Converts an unsigned integer type to an image component. 80 | template 81 | static proto_always_inline float word_to_component(Word word) { return word * (1.0f / 255.0f); } 82 | 83 | /// Converts a component to an unsigned integer type. 84 | template 85 | static proto_always_inline Word component_to_word(float f, float gamma = RgbColor::default_gamma()) { 86 | static_assert(sizeof(Word) < sizeof(uint32_t)); 87 | if constexpr (PerformGammaCorrect) 88 | f = std::pow(f, 1.0f / gamma); 89 | return std::min( 90 | uint32_t{std::numeric_limits>::max()}, 91 | static_cast(std::max(f * (static_cast(std::numeric_limits>::max()) + 1.0f), 0.0f))); 92 | } 93 | 94 | private: 95 | size_t width_ = 0; 96 | size_t height_ = 0; 97 | std::vector> channels_; 98 | }; 99 | 100 | } // namespace sol 101 | 102 | #endif 103 | -------------------------------------------------------------------------------- /include/sol/lights.h: -------------------------------------------------------------------------------- 1 | #ifndef SOL_LIGHTS_H 2 | #define SOL_LIGHTS_H 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "sol/color.h" 12 | #include "sol/shapes.h" 13 | 14 | namespace sol { 15 | 16 | class Sampler; 17 | class ColorTexture; 18 | 19 | /// Result from sampling the area of a light source from another surface point. 20 | struct LightAreaSample { 21 | proto::Vec3f pos; ///< Position on the light source 22 | Color intensity; ///< Intensity along the direction 23 | float pdf_from; ///< Probability to sample the point on the light from another point 24 | float pdf_area; ///< Probability to sample the point on the light 25 | float pdf_dir; ///< Probability to sample the direction between the light source and the surface 26 | float cos; ///< Cosine between the direction and the light source geometry 27 | }; 28 | 29 | /// Result from sampling a light source to get a point and a direction. 30 | struct LightEmissionSample { 31 | proto::Vec3f pos; ///< Position on the light source 32 | proto::Vec3f dir; ///< Direction of the ray going outwards from the light 33 | Color intensity; ///< Intensity along the direction 34 | float pdf_area; ///< Probability to sample the point on the light 35 | float pdf_dir; ///< Probability to sample the direction 36 | float cos; ///< Cosine between the direction and the light source geometry 37 | }; 38 | 39 | /// Emission value for a given point on the surface of the light, and a given direction. 40 | struct EmissionValue { 41 | Color intensity; ///< Intensity of the light source at the given point 42 | float pdf_from; ///< Probability to sample the point on the light from another point 43 | float pdf_area; ///< Probability to sample the point on the light 44 | float pdf_dir; ///< Probability to sample the direction 45 | }; 46 | 47 | class Light { 48 | public: 49 | const enum class Tag { 50 | PointLight, 51 | UniformTriangleLight, 52 | UniformSphereLight 53 | } tag; 54 | 55 | Light(Tag tag) 56 | : tag(tag) 57 | {} 58 | 59 | virtual ~Light() {} 60 | 61 | /// Samples the area of a light source from the given point on another surface. 62 | virtual std::optional sample_area(Sampler&, const proto::Vec3f& from) const = 0; 63 | 64 | /// Samples the emissive surface of the light. 65 | virtual std::optional sample_emission(Sampler&) const = 0; 66 | 67 | /// Computes the emission value of this light, from a given point on a surface, 68 | /// for a given point on the light, and a given direction. 69 | /// The direction should be oriented outwards (from the light _to_ the surface). 70 | virtual EmissionValue emission(const proto::Vec3f& from, const proto::Vec3f& dir, const proto::Vec2f& uv) const = 0; 71 | 72 | /// Returns the probability to sample the given point on the light source from another point on a surface. 73 | virtual float pdf_from(const proto::Vec3f& from, const proto::Vec2f& uv) const = 0; 74 | 75 | /// Returns true if the light source has an area (i.e. it can be hit when intersecting a ray with the scene). 76 | virtual bool has_area() const = 0; 77 | 78 | virtual proto::fnv::Hasher& hash(proto::fnv::Hasher&) const = 0; 79 | virtual bool equals(const Light&) const = 0; 80 | 81 | protected: 82 | // Utility function to create a `LightSample`. 83 | // Just like its counterpart for `BsdfSample`, this prevents corner cases for pdfs or cosines. 84 | template 85 | static std::optional make_sample(const LightSample& light_sample) { 86 | return light_sample.pdf_area > 0 && light_sample.pdf_dir > 0 && light_sample.cos > 0 87 | ? std::make_optional(light_sample) : std::nullopt; 88 | } 89 | }; 90 | 91 | /// A single-point light. 92 | class PointLight final : public Light { 93 | public: 94 | PointLight(const proto::Vec3f&, const Color&); 95 | 96 | std::optional sample_area(Sampler&, const proto::Vec3f&) const override; 97 | std::optional sample_emission(Sampler&) const override; 98 | EmissionValue emission(const proto::Vec3f&, const proto::Vec3f&, const proto::Vec2f&) const override; 99 | float pdf_from(const proto::Vec3f&, const proto::Vec2f&) const override; 100 | 101 | bool has_area() const override { return false; } 102 | 103 | proto::fnv::Hasher& hash(proto::fnv::Hasher&) const override; 104 | bool equals(const Light&) const override; 105 | 106 | private: 107 | proto::Vec3f pos_; 108 | Color intensity_; 109 | }; 110 | 111 | /// An area light in the shape of an object given as parameter. 112 | /// The light emission profile is diffuse (i.e. follows the cosine 113 | /// between the normal of the light surface and the emission direction). 114 | template 115 | class AreaLight final : public Light { 116 | public: 117 | AreaLight(const SamplableShape&, const ColorTexture&); 118 | 119 | std::optional sample_area(Sampler&, const proto::Vec3f&) const override; 120 | std::optional sample_emission(Sampler&) const override; 121 | EmissionValue emission(const proto::Vec3f&, const proto::Vec3f&, const proto::Vec2f&) const override; 122 | float pdf_from(const proto::Vec3f&, const proto::Vec2f&) const override; 123 | 124 | bool has_area() const override { return true; } 125 | 126 | proto::fnv::Hasher& hash(proto::fnv::Hasher&) const override; 127 | bool equals(const Light&) const override; 128 | 129 | private: 130 | static Tag infer_tag(const UniformTriangle&) { return Tag::UniformTriangleLight; } 131 | static Tag infer_tag(const UniformSphere&) { return Tag::UniformSphereLight; } 132 | 133 | SamplableShape shape_; 134 | const ColorTexture& intensity_; 135 | }; 136 | 137 | using UniformTriangleLight = AreaLight; 138 | using UniformSphereLight = AreaLight; 139 | 140 | } // namespace sol 141 | 142 | #endif 143 | -------------------------------------------------------------------------------- /include/sol/render_job.h: -------------------------------------------------------------------------------- 1 | #ifndef SOL_RENDER_JOB_H 2 | #define SOL_RENDER_JOB_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace sol { 10 | 11 | struct Image; 12 | struct Scene; 13 | class Renderer; 14 | 15 | /// A rendering job, with accompanying scene data and renderer. 16 | /// Rendering jobs should only be controlled from a single thread 17 | /// (i.e. calling `wait/start/cancel` from different threads is undefined behavior). 18 | struct RenderJob { 19 | size_t sample_count = 0; ///< Number of samples to render (0 = unlimited, until cancellation). 20 | size_t samples_per_frame = 1; ///< Number of samples per frame (larger = higher throughput but higher latency) 21 | const Renderer& renderer; ///< Rendering algorithm to use. 22 | Image& output; ///< Output image, where samples are accumulated. 23 | 24 | RenderJob(const Renderer& renderer, Image& output); 25 | RenderJob(const RenderJob&) = delete; 26 | RenderJob(RenderJob&&); 27 | ~RenderJob(); 28 | 29 | /// Starts the rendering job, producing samples into the output image. 30 | /// This function takes a callback that is called after a frame has been rendered. 31 | /// The next frame will only start after that callback returns, and if the returned value is `true`. 32 | /// If the returned value is false, the job is cancelled. 33 | void start(std::function&& frame_end = {}); 34 | 35 | /// Waits for this rendering job to finish, or until the given amount of milliseconds has passed. 36 | /// If the function is given 0 milliseconds as timeout, it will wait indefinitely without any timeout. 37 | /// Returns true if the rendering job is over, otherwise false. 38 | bool wait(size_t timeout_ms = 0); 39 | 40 | /// Explicitly cancels the rendering job. 41 | /// This might require waiting for some frames to finish rendering. 42 | void cancel(); 43 | 44 | private: 45 | std::thread render_thread_; 46 | std::mutex mutex_; 47 | std::condition_variable done_cond_; 48 | bool is_done_ = true; 49 | }; 50 | 51 | } // namespace sol 52 | 53 | #endif 54 | -------------------------------------------------------------------------------- /include/sol/renderer.h: -------------------------------------------------------------------------------- 1 | #ifndef SOL_RENDERER_H 2 | #define SOL_RENDERER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include "sol/samplers.h" 14 | #include "sol/scene.h" 15 | 16 | namespace sol { 17 | 18 | struct Image; 19 | 20 | /// Base class for all rendering algorithms. 21 | class Renderer { 22 | public: 23 | Renderer(const std::string_view& name, const Scene& scene) 24 | : name_(name), scene_(scene) 25 | {} 26 | 27 | virtual ~Renderer() {} 28 | 29 | const std::string& name() const { return name_; } 30 | 31 | /// Renders the samples starting at the given index into the given image. 32 | /// Since the behavior is entirely deterministic, this `sample_index` 33 | /// variable can be used to retrace a particular set of samples. 34 | virtual void render(Image& image, size_t sample_index, size_t sample_count = 1) const = 0; 35 | 36 | protected: 37 | /// Processes each pixel of the given range `[0,w]x[0,h]` in parallel. 38 | template 39 | static inline void for_each_pixel(Executor& executor, size_t w, size_t h, const F& f) { 40 | par::for_each(executor, par::range_2d(size_t{0}, w, size_t{0}, h), f); 41 | } 42 | 43 | /// Generates a seed suitable to initialize a sampler, given a frame index, and a pixel position (2D). 44 | static inline uint32_t pixel_seed(size_t frame_index, size_t x, size_t y) { 45 | return proto::fnv::Hasher().combine(x).combine(y).combine(frame_index); 46 | } 47 | 48 | /// Samples the area within a pixel, using the given sampler. 49 | /// Returns the coordinates of the pixel in camera space (i.e. `[-1, 1]^2`). 50 | static inline proto::Vec2f sample_pixel(Sampler& sampler, size_t x, size_t y, size_t w, size_t h) { 51 | return proto::Vec2f( 52 | (x + sampler()) * (2.0f / static_cast(w)) - 1.0f, 53 | 1.0f - (y + sampler()) * (2.0f / static_cast(h))); 54 | } 55 | 56 | /// Computes the balance heuristic given the probability density values for two techniques. 57 | static inline float balance_heuristic(float x, float y) { 58 | // More robust than x / (x + y), for when x, y = +-inf 59 | return 1.0f / (1.0f + y / x); 60 | } 61 | 62 | std::string name_; 63 | const Scene& scene_; 64 | }; 65 | 66 | } // namespace sol 67 | 68 | #endif 69 | -------------------------------------------------------------------------------- /include/sol/samplers.h: -------------------------------------------------------------------------------- 1 | #ifndef SOL_SAMPLERS_H 2 | #define SOL_SAMPLERS_H 3 | 4 | #include 5 | #include 6 | 7 | namespace sol { 8 | 9 | /// Random number source. 10 | class Sampler { 11 | public: 12 | /// Generates a new floating-point value in the range [0, 1] from this sampler. 13 | virtual float operator () () = 0; 14 | 15 | protected: 16 | ~Sampler() {} 17 | }; 18 | 19 | /// PCG-based random number generator (see http://www.pcg-random.org), compatible with the standard library. 20 | struct PcgGenerator { 21 | using result_type = uint32_t; 22 | static constexpr uint32_t min() { return std::numeric_limits::min(); } 23 | static constexpr uint32_t max() { return std::numeric_limits::max(); } 24 | static constexpr uint64_t inc = 1; 25 | 26 | PcgGenerator(uint64_t init_state) { 27 | seed(init_state); 28 | } 29 | 30 | void seed(uint64_t init_state) { 31 | state = 0; 32 | (*this)(); 33 | state += init_state; 34 | (*this)(); 35 | } 36 | 37 | uint32_t operator () () { 38 | auto old_state = state; 39 | state = old_state * UINT64_C(6364136223846793005) + inc; 40 | uint32_t xorshifted = ((old_state >> 18) ^ old_state) >> 27; 41 | uint32_t rot = old_state >> 59; 42 | return (xorshifted >> rot) | (xorshifted << ((-rot) & UINT32_C(31))); 43 | } 44 | 45 | uint64_t state; 46 | }; 47 | 48 | /// Template to create samplers from a generator compatible with the standard-library. 49 | template 50 | class StdRandomSampler final : public Sampler { 51 | public: 52 | template 53 | StdRandomSampler(Args&&... args) 54 | : distrib_(0, 1), gen_(std::forward(args)...) 55 | {} 56 | 57 | float operator () () override final { return distrib_(gen_); } 58 | 59 | private: 60 | std::uniform_real_distribution distrib_; 61 | Generator gen_; 62 | }; 63 | 64 | using Mt19937Sampler = StdRandomSampler; ///< Mersenne-twister-based sampler. 65 | using PcgSampler = StdRandomSampler; ///< PCG-based sampler. 66 | 67 | } // namespace sol 68 | 69 | #endif 70 | -------------------------------------------------------------------------------- /include/sol/scene.h: -------------------------------------------------------------------------------- 1 | #ifndef SOL_SCENE_H 2 | #define SOL_SCENE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | namespace sol { 13 | 14 | class Bsdf; 15 | class Light; 16 | class Texture; 17 | class Camera; 18 | class Image; 19 | class Geometry; 20 | 21 | namespace detail { 22 | 23 | struct SceneDefaults { 24 | float fov = 60.0f; 25 | float aspect_ratio = 1.0f; 26 | proto::Vec3f eye_pos = proto::Vec3f(0, 0, 0); 27 | proto::Vec3f dir_vector = proto::Vec3f(0, 0, 1); 28 | proto::Vec3f up_vector = proto::Vec3f(0, 1, 0); 29 | }; 30 | 31 | } // namespace detail 32 | 33 | /// Owning collection of lights, BSDFs, textures and geometric objects that make up a scene. 34 | struct Scene { 35 | Scene(); 36 | 37 | Scene(Scene&&); 38 | ~Scene(); 39 | 40 | std::unique_ptr root; 41 | std::unique_ptr camera; 42 | 43 | template using unique_vector = std::vector>; 44 | 45 | unique_vector bsdfs; 46 | unique_vector lights; 47 | unique_vector textures; 48 | unique_vector images; 49 | 50 | using Defaults = detail::SceneDefaults; 51 | 52 | /// Loads the given scene file, using the given configuration to deduce missing values. 53 | static std::optional load( 54 | const std::string& file_name, 55 | const Defaults& defaults = {}, 56 | std::ostream* err_out = nullptr); 57 | }; 58 | 59 | } // namespace sol 60 | 61 | #endif 62 | -------------------------------------------------------------------------------- /include/sol/shapes.h: -------------------------------------------------------------------------------- 1 | #ifndef SOL_SHAPES_H 2 | #define SOL_SHAPES_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace sol { 9 | 10 | class Sampler; 11 | 12 | /// Shape surface sample. 13 | struct ShapeSample { 14 | proto::Vec2f surf_coords; ///< Surface coordinates of the sample on the surface. 15 | proto::Vec3f pos; ///< World position of the sample. 16 | proto::Vec3f normal; ///< Surface normal at the sample position on the surface. 17 | float pdf; ///< Sampling probability when sampling the area of the shape (in _area_ measure). 18 | }; 19 | 20 | /// A shape sample obtained by sampling a shape from a point on another surface. 21 | struct DirectionalShapeSample final : ShapeSample { 22 | /// Sampling probability when sampling the area of the shape from the surface point (in _area_ measure). 23 | float pdf_from; 24 | 25 | explicit DirectionalShapeSample(const ShapeSample& other) 26 | : ShapeSample(other), pdf_from(other.pdf) 27 | {} 28 | }; 29 | 30 | /// Mixin for all samplable shapes. 31 | template 32 | struct SamplableShape { 33 | Shape shape; 34 | 35 | SamplableShape(const Shape& shape) 36 | : shape(shape) 37 | {} 38 | 39 | /// Samples the surface of the shape from a given point on another surface. 40 | ShapeSample sample(Sampler& sampler) const; 41 | DirectionalShapeSample sample(Sampler& sampler, const proto::Vec3f&) const; 42 | 43 | template 44 | Hasher& hash(Hasher& hasher) const { return shape.hash(hasher); } 45 | bool operator == (const SamplableShape& other) const { return shape == other.shape; } 46 | }; 47 | 48 | /// A uniformly-sampled triangle. 49 | struct UniformTriangle final : SamplableShape { 50 | proto::Vec3f normal; 51 | float inv_area; 52 | 53 | UniformTriangle(const proto::Trianglef& triangle) 54 | : SamplableShape(triangle), normal(triangle.normal()), inv_area(1.0f / triangle.area()) 55 | {} 56 | 57 | using SamplableShape::sample; 58 | ShapeSample sample_at(proto::Vec2f) const; 59 | DirectionalShapeSample sample_at(const proto::Vec2f&, const proto::Vec3f&) const; 60 | }; 61 | 62 | /// A uniformly-sampled sphere. 63 | struct UniformSphere final : SamplableShape { 64 | float inv_area; 65 | 66 | UniformSphere(const proto::Spheref& sphere) 67 | : SamplableShape(sphere), inv_area(1.0f / sphere.area()) 68 | {} 69 | 70 | using SamplableShape::sample; 71 | ShapeSample sample_at(const proto::Vec2f&) const; 72 | DirectionalShapeSample sample_at(const proto::Vec2f&, const proto::Vec3f&) const; 73 | }; 74 | 75 | } // namespace sol 76 | 77 | #endif 78 | -------------------------------------------------------------------------------- /include/sol/textures.h: -------------------------------------------------------------------------------- 1 | #ifndef SOL_TEXTURES_H 2 | #define SOL_TEXTURES_H 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include "sol/color.h" 12 | #include "sol/image.h" 13 | 14 | namespace sol { 15 | 16 | /// Available border modes for image textures. 17 | struct BorderMode { 18 | enum class Tag { Clamp, Repeat, Mirror, End }; 19 | static constexpr size_t tag_count() { return static_cast(Tag::End); } 20 | 21 | struct Clamp { 22 | static constexpr Tag tag() { return Tag::Clamp; } 23 | proto::Vec2f operator () (const proto::Vec2f&) const; 24 | }; 25 | 26 | struct Repeat { 27 | static constexpr Tag tag() { return Tag::Repeat; } 28 | proto::Vec2f operator () (const proto::Vec2f&) const; 29 | }; 30 | 31 | struct Mirror { 32 | static constexpr Tag tag() { return Tag::Mirror; } 33 | proto::Vec2f operator () (const proto::Vec2f&) const; 34 | }; 35 | }; 36 | 37 | /// Available filters for an image texture. 38 | struct ImageFilter { 39 | enum class Tag { Nearest, Bilinear, End }; 40 | static constexpr size_t tag_count() { return static_cast(Tag::End); } 41 | 42 | struct Nearest { 43 | static constexpr Tag tag() { return Tag::Nearest; } 44 | template 45 | Color operator () (const proto::Vec2f&, size_t, size_t, F&&) const; 46 | }; 47 | 48 | struct Bilinear { 49 | static constexpr Tag tag() { return Tag::Bilinear; } 50 | template 51 | Color operator () (const proto::Vec2f&, size_t, size_t, F&&) const; 52 | }; 53 | }; 54 | 55 | /// Abstract texture that produces a floating-point value from a UV coordinate. 56 | class Texture { 57 | public: 58 | const enum class Tag { 59 | ConstantTexture = 0, 60 | ConstantColorTexture = 1, 61 | ImageTextureBegin = 2, 62 | ImageTextureEnd = 63 | ImageTextureBegin + BorderMode::tag_count() * ImageFilter::tag_count() 64 | } tag; 65 | 66 | Texture(Tag tag) 67 | : tag(tag) 68 | {} 69 | 70 | virtual ~Texture() {} 71 | 72 | /// Produces a value, given a particular texture coordinate. 73 | virtual float sample(const proto::Vec2f& uv) const = 0; 74 | 75 | virtual proto::fnv::Hasher& hash(proto::fnv::Hasher&) const = 0; 76 | virtual bool equals(const Texture&) const = 0; 77 | 78 | protected: 79 | template 80 | static constexpr Tag make_image_texture_tag() { 81 | return static_cast( 82 | static_cast(Tag::ImageTextureBegin) + 83 | static_cast(ImageFilterType::tag()) * BorderMode::tag_count() + 84 | static_cast(BorderModeType::tag())); 85 | } 86 | }; 87 | 88 | /// Abstract texture that produces a color from a UV coordinate. Can be used as a 89 | /// regular (i.e. scalar) texture that returns the luminance of the color of each 90 | /// sample. 91 | class ColorTexture : public Texture { 92 | public: 93 | ColorTexture(Tag tag) 94 | : Texture(tag) 95 | {} 96 | 97 | float sample(const proto::Vec2f& uv) const override final { 98 | return sample_color(uv).luminance(); 99 | } 100 | 101 | virtual Color sample_color(const proto::Vec2f& uv) const = 0; 102 | }; 103 | 104 | /// Constant texture that evaluates to the same scalar everywhere. 105 | class ConstantTexture final : public Texture { 106 | public: 107 | ConstantTexture(float constant) 108 | : Texture(Tag::ConstantTexture), constant_(constant) 109 | {} 110 | 111 | float sample(const proto::Vec2f&) const override { return constant_; } 112 | 113 | proto::fnv::Hasher& hash(proto::fnv::Hasher& hasher) const override { 114 | return hasher.combine(tag).combine(constant_); 115 | } 116 | 117 | bool equals(const Texture& other) const override { 118 | return other.tag == tag && static_cast(other).constant_ == constant_; 119 | } 120 | 121 | private: 122 | float constant_; 123 | }; 124 | 125 | /// Constant texture that evaluates to the same color everywhere. 126 | class ConstantColorTexture final : public ColorTexture { 127 | public: 128 | ConstantColorTexture(const Color& color) 129 | : ColorTexture(Tag::ConstantColorTexture), color_(color) 130 | {} 131 | 132 | Color sample_color(const proto::Vec2f&) const override { return color_; } 133 | 134 | proto::fnv::Hasher& hash(proto::fnv::Hasher& hasher) const override { 135 | return color_.hash(hasher.combine(tag)); 136 | } 137 | 138 | bool equals(const Texture& other) const override { 139 | return other.tag == tag && static_cast(other).color_ == color_; 140 | } 141 | 142 | private: 143 | Color color_; 144 | }; 145 | 146 | template struct ConstantTextureSelector {}; 147 | template <> struct ConstantTextureSelector { using Type = ConstantTexture; }; 148 | template <> struct ConstantTextureSelector { using Type = ConstantColorTexture; }; 149 | 150 | /// Helper to choose a constant texture type based on the constant's type. 151 | template using ConstantTextureType = typename ConstantTextureSelector::Type; 152 | 153 | /// Texture made of an image, using the given filter and border handling mode. 154 | template 155 | class ImageTexture final : public ColorTexture { 156 | public: 157 | ImageTexture( 158 | const Image& image, 159 | ImageFilter&& filter = ImageFilter(), 160 | BorderMode&& border_mode = BorderMode()) 161 | : ColorTexture(make_image_texture_tag()) 162 | , image_(image) 163 | , filter_(std::move(filter)) 164 | , border_mode_(std::move(border_mode)) 165 | {} 166 | 167 | Color sample_color(const proto::Vec2f& uv) const override; 168 | 169 | proto::fnv::Hasher& hash(proto::fnv::Hasher& hasher) const override { 170 | return hasher.combine(tag).combine(&image_); 171 | } 172 | 173 | bool equals(const Texture& other) const override { 174 | return other.tag == tag && &static_cast(other).image() == &image_; 175 | } 176 | 177 | const Image& image() const { return image_; } 178 | 179 | private: 180 | const Image& image_; 181 | 182 | ImageFilter filter_; 183 | BorderMode border_mode_; 184 | }; 185 | 186 | } // namespace sol 187 | 188 | #endif 189 | -------------------------------------------------------------------------------- /include/sol/triangle_mesh.h: -------------------------------------------------------------------------------- 1 | #ifndef SOL_TRIANGLE_MESH_H 2 | #define SOL_TRIANGLE_MESH_H 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "sol/geometry.h" 10 | 11 | namespace sol { 12 | 13 | class Light; 14 | 15 | /// Triangle mesh with an underlying acceleration data structure to speed up intersection tests. 16 | class TriangleMesh : public Geometry { 17 | public: 18 | TriangleMesh( 19 | std::vector&& indices, 20 | std::vector&& vertices, 21 | std::vector&& normals, 22 | std::vector&& tex_coords, 23 | std::vector&& bsdfs, 24 | std::unordered_map&& lights); 25 | ~TriangleMesh(); 26 | 27 | std::optional intersect_closest(proto::Rayf&) const override; 28 | bool intersect_any(const proto::Rayf&) const override; 29 | 30 | /// Returns the number of triangles in the mesh. 31 | size_t triangle_count() const { return indices_.size() / 3; } 32 | 33 | /// Returns the vertex indices of a triangle located a given triangle index. 34 | std::tuple triangle_indices(size_t triangle_index) const { 35 | return std::tuple { 36 | indices_[triangle_index * 3 + 0], 37 | indices_[triangle_index * 3 + 1], 38 | indices_[triangle_index * 3 + 2] 39 | }; 40 | } 41 | 42 | const std::vector& normals() const { return normals_; } 43 | const std::vector& tex_coords() const { return tex_coords_; } 44 | const std::vector& bsdfs() const { return bsdfs_; } 45 | 46 | private: 47 | struct BvhData; 48 | template 49 | std::unique_ptr build_bvh(Executor&, const std::vector&) const; 50 | template 51 | std::vector build_triangles(Executor&, const std::vector&) const; 52 | 53 | std::vector indices_; 54 | std::vector triangles_; 55 | std::vector normals_; 56 | std::vector tex_coords_; 57 | std::vector bsdfs_; 58 | std::unordered_map lights_; 59 | std::unique_ptr bvh_data_; 60 | }; 61 | 62 | } // namespace sol 63 | 64 | #endif 65 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(SOL_MULTITHREADING_FRAMEWORK None CACHE STRING "Multithreading framework to use in SoL") 2 | set_property(CACHE SOL_MULTITHREADING_FRAMEWORK PROPERTY STRINGS None TBB OpenMP) 3 | 4 | add_library(sol 5 | formats/png.cpp 6 | formats/jpeg.cpp 7 | formats/tiff.cpp 8 | formats/exr.cpp 9 | formats/obj.cpp 10 | algorithms/path_tracer.cpp 11 | triangle_mesh.cpp 12 | image.cpp 13 | cameras.cpp 14 | lights.cpp 15 | bsdfs.cpp 16 | scene.cpp 17 | scene_loader.cpp 18 | shapes.cpp 19 | textures.cpp 20 | render_job.cpp) 21 | 22 | set_target_properties(sol PROPERTIES 23 | CXX_STANDARD 20 24 | ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib 25 | LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) 26 | 27 | find_package(proto REQUIRED) 28 | find_package(bvh REQUIRED) 29 | find_package(Threads REQUIRED) 30 | 31 | if (SOL_MULTITHREADING_FRAMEWORK STREQUAL "TBB") 32 | find_package(TBB REQUIRED) 33 | if (TBB_FOUND) 34 | target_compile_definitions(sol PUBLIC -DSOL_ENABLE_TBB) 35 | target_link_libraries(sol PUBLIC TBB::tbb) 36 | set(SOL_DEPENDS_TBB "find_dependency(TBB)") 37 | message(STATUS "Building SoL with TBB") 38 | endif () 39 | elseif (SOL_MULTITHREADING_FRAMEWORK STREQUAL "OpenMP") 40 | find_package(OpenMP REQUIRED) 41 | if (OpenMP_FOUND) 42 | target_compile_definitions(sol PUBLIC -DSOL_ENABLE_OMP) 43 | target_link_libraries(sol PUBLIC OpenMP::OpenMP_CXX) 44 | set(SOL_DEPENDS_OMP "find_dependency(OpenMP)") 45 | message(STATUS "Building SoL with OpenMP") 46 | endif () 47 | else () 48 | message(STATUS "Building SoL without multithreading support") 49 | endif () 50 | 51 | target_link_libraries(sol PUBLIC proto::proto par::par Threads::Threads) 52 | target_link_libraries(sol PRIVATE bvh::bvh tomlplusplus tinyexr) 53 | target_include_directories(sol PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) 54 | target_include_directories(sol PUBLIC 55 | $ 56 | $) 57 | 58 | set_target_properties(sol PROPERTIES INTERPROCEDURAL_OPTIMIZATION_RELEASE ON) 59 | 60 | # Add optional image libraries 61 | find_package(PNG QUIET) 62 | if (PNG_FOUND) 63 | message(STATUS "Adding PNG support") 64 | target_compile_definitions(sol PUBLIC -DSOL_ENABLE_PNG) 65 | target_link_libraries(sol PRIVATE PNG::PNG) 66 | endif () 67 | 68 | find_package(JPEG QUIET) 69 | if (JPEG_FOUND) 70 | message(STATUS "Adding JPEG support") 71 | target_compile_definitions(sol PRIVATE -DSOL_ENABLE_JPEG) 72 | target_link_libraries(sol PRIVATE JPEG::JPEG) 73 | endif () 74 | 75 | find_package(TIFF QUIET) 76 | if (TIFF_FOUND) 77 | message(STATUS "Adding TIFF support") 78 | target_compile_definitions(sol PRIVATE -DSOL_ENABLE_TIFF) 79 | target_link_libraries(sol PRIVATE TIFF::TIFF) 80 | endif () 81 | 82 | include(CMakePackageConfigHelpers) 83 | 84 | set(CONTRIB_TARGETS tinyexr tomlplusplus) 85 | 86 | # Allow using this library by locating the config file from the build directory 87 | export(TARGETS sol ${CONTRIB_TARGETS} NAMESPACE sol:: FILE ../sol-targets.cmake) 88 | configure_package_config_file( 89 | ../cmake/sol-config.cmake.in 90 | ../sol-config.cmake 91 | INSTALL_DESTINATION . 92 | INSTALL_PREFIX ${CMAKE_CURRENT_BINARY_DIR}) 93 | write_basic_package_version_file(${CMAKE_CURRENT_BINARY_DIR}/../sol-config-version.cmake COMPATIBILITY SameMajorVersion) 94 | 95 | # Allow using this library by installing it 96 | configure_package_config_file( 97 | ../cmake/sol-config.cmake.in 98 | ../sol-config-install.cmake 99 | INSTALL_DESTINATION lib/cmake) 100 | install(FILES ${CMAKE_CURRENT_BINARY_DIR}/../sol-config-install.cmake RENAME sol-config.cmake DESTINATION lib/cmake) 101 | install(FILES ${CMAKE_CURRENT_BINARY_DIR}/../sol-config-version.cmake DESTINATION lib/cmake) 102 | install(DIRECTORY ../include DESTINATION .) 103 | install(TARGETS sol ${CONTRIB_TARGETS} EXPORT sol-targets) 104 | install(EXPORT sol-targets NAMESPACE sol:: DESTINATION lib/cmake) 105 | -------------------------------------------------------------------------------- /src/algorithms/path_tracer.cpp: -------------------------------------------------------------------------------- 1 | #include "sol/algorithms/path_tracer.h" 2 | #include "sol/image.h" 3 | #include "sol/cameras.h" 4 | #include "sol/bsdfs.h" 5 | #include "sol/lights.h" 6 | 7 | namespace sol { 8 | 9 | void PathTracer::render(Image& image, size_t sample_index, size_t sample_count) const { 10 | using Sampler = PcgSampler; 11 | Renderer::for_each_pixel( 12 | executor_, image.width(), image.height(), 13 | [&] (size_t x, size_t y) { 14 | auto color = Color::black(); 15 | for (size_t i = 0; i < sample_count; ++i) { 16 | Sampler sampler(Renderer::pixel_seed(sample_index + i, x, y)); 17 | auto ray = scene_.camera->generate_ray( 18 | Renderer::sample_pixel(sampler, x, y, image.width(), image.height())); 19 | color += trace_path(sampler, ray); 20 | } 21 | image.accumulate(x, y, color); 22 | }); 23 | } 24 | 25 | static inline const Light* pick_light(Sampler& sampler, const Scene& scene) { 26 | // TODO: Sample lights adaptively 27 | auto light_index = std::min(static_cast(sampler() * scene.lights.size()), scene.lights.size() - 1); 28 | return scene.lights[light_index].get(); 29 | } 30 | 31 | Color PathTracer::trace_path(Sampler& sampler, proto::Rayf ray) const { 32 | static constexpr bool disable_mis = false; 33 | static constexpr bool disable_nee = false; 34 | static constexpr bool disable_rr = false; 35 | 36 | auto light_pick_prob = 1.0f / scene_.lights.size(); 37 | auto pdf_prev_bounce = 0.0f; 38 | auto throughput = Color::constant(1.0f); 39 | auto color = Color::black(); 40 | 41 | for (size_t path_len = 0; path_len < config_.max_path_len; path_len++) { 42 | auto hit = scene_.root->intersect_closest(ray); 43 | if (!hit) 44 | break; 45 | 46 | auto out_dir = -ray.dir; 47 | 48 | // Direct hits on a light source 49 | if (hit->light && hit->surf_info.is_front_side) { 50 | // Convert the bounce pdf from solid angle to area measure 51 | auto pdf_prev_bounce_area = 52 | pdf_prev_bounce * proto::dot(out_dir, hit->surf_info.normal()) / (ray.tmax * ray.tmax); 53 | 54 | auto emission = hit->light->emission(ray.org, out_dir, hit->surf_info.surf_coords); 55 | auto mis_weight = pdf_prev_bounce != 0.0f ? 56 | Renderer::balance_heuristic(pdf_prev_bounce_area, emission.pdf_from * light_pick_prob) : 1.0f; 57 | if constexpr (disable_mis || disable_nee) 58 | mis_weight = pdf_prev_bounce != 0 ? 0 : 1; 59 | color += throughput * emission.intensity * mis_weight; 60 | } 61 | 62 | if (!hit->bsdf) 63 | break; 64 | 65 | // Evaluate direct lighting 66 | bool skip_nee = disable_nee || hit->bsdf->type == Bsdf::Type::Specular; 67 | if (!skip_nee) { 68 | auto light = pick_light(sampler, scene_); 69 | if (auto light_sample = light->sample_area(sampler, hit->surf_info.point)) { 70 | auto in_dir = light_sample->pos - hit->surf_info.point; 71 | auto cos_surf = proto::dot(in_dir, hit->surf_info.normal()); 72 | auto shadow_ray = proto::Rayf::between_points(hit->surf_info.point, light_sample->pos, config_.ray_offset); 73 | 74 | if (!scene_.root->intersect_any(shadow_ray)) { 75 | // Normalize the incoming direction 76 | auto inv_light_dist = 1.0f / proto::length(in_dir); 77 | cos_surf *= inv_light_dist; 78 | in_dir *= inv_light_dist; 79 | 80 | auto pdf_bounce = light->has_area() ? hit->bsdf->pdf(in_dir, hit->surf_info, out_dir) : 0.0f; 81 | auto pdf_light = light_sample->pdf_from * light_pick_prob; 82 | auto geom_term = light_sample->cos * inv_light_dist * inv_light_dist; 83 | 84 | auto mis_weight = light->has_area() ? 85 | Renderer::balance_heuristic(pdf_light, pdf_bounce * geom_term) : 1.0f; 86 | 87 | if constexpr (disable_mis) 88 | mis_weight = 1; 89 | 90 | color += 91 | light_sample->intensity * 92 | throughput * 93 | hit->bsdf->eval(in_dir, hit->surf_info, out_dir) * 94 | (geom_term * cos_surf * mis_weight / pdf_light); 95 | } 96 | } 97 | } 98 | 99 | // Russian Roulette 100 | auto survival_prob = 1.0f; 101 | if (!disable_rr && path_len >= config_.min_rr_path_len) { 102 | survival_prob = proto::clamp( 103 | throughput.luminance(), 104 | config_.min_survival_prob, 105 | config_.max_survival_prob); 106 | if (sampler() >= survival_prob) 107 | break; 108 | } 109 | 110 | // Bounce 111 | auto bsdf_sample = hit->bsdf->sample(sampler, hit->surf_info, out_dir); 112 | if (!bsdf_sample) 113 | break; 114 | 115 | throughput *= bsdf_sample->color * (bsdf_sample->cos / (bsdf_sample->pdf * survival_prob)); 116 | ray = proto::Rayf(hit->surf_info.point, bsdf_sample->in_dir, config_.ray_offset); 117 | pdf_prev_bounce = skip_nee ? 0.0f : bsdf_sample->pdf; 118 | } 119 | return color; 120 | } 121 | 122 | } // namespace sol 123 | -------------------------------------------------------------------------------- /src/bsdfs.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include "sol/bsdfs.h" 6 | #include "sol/samplers.h" 7 | #include "sol/textures.h" 8 | 9 | namespace sol { 10 | 11 | static float reflect_cosine( 12 | const proto::Vec3f& in_dir, 13 | const proto::Vec3f& normal, 14 | const proto::Vec3f& out_dir) 15 | { 16 | // The minus sign and negative dot are here because `out_dir` 17 | // is going out of the surface by convention. 18 | return -proto::negative_dot(in_dir, proto::reflect(out_dir, normal)); 19 | } 20 | 21 | static float fresnel_factor(float eta, float cos_i, float cos_t) { 22 | // Evaluates the Fresnel factor given the ratio between two different media 23 | // and the given cosines for the incoming/transmitted rays. 24 | auto rs = (eta * cos_i - cos_t) / (eta * cos_i + cos_t); 25 | auto rp = (cos_i - eta * cos_t) / (cos_i + eta * cos_t); 26 | return (rs * rs + rp * rp) * 0.5f; 27 | } 28 | 29 | // Diffuse BSDF -------------------------------------------------------------------- 30 | 31 | DiffuseBsdf::DiffuseBsdf(const ColorTexture& kd) 32 | : Bsdf(Tag::DiffuseBsdf, Type::Diffuse), kd_(kd) 33 | {} 34 | 35 | Color DiffuseBsdf::eval(const proto::Vec3f&, const SurfaceInfo& surf_info, const proto::Vec3f&) const { 36 | return kd_.sample_color(surf_info.tex_coords) * std::numbers::inv_pi_v; 37 | } 38 | 39 | std::optional DiffuseBsdf::sample(Sampler& sampler, const SurfaceInfo& surf_info, const proto::Vec3f& out_dir, bool) const { 40 | auto [in_dir, pdf] = proto::sample_cosine_hemisphere(sampler(), sampler()); 41 | auto local_in_dir = surf_info.local * in_dir; 42 | return validate_sample(surf_info, BsdfSample { 43 | .in_dir = local_in_dir, 44 | .pdf = pdf, 45 | .cos = in_dir[2], 46 | .color = eval(local_in_dir, surf_info, out_dir) 47 | }); 48 | } 49 | 50 | float DiffuseBsdf::pdf(const proto::Vec3f& in_dir, const SurfaceInfo& surf_info, const proto::Vec3f&) const { 51 | return proto::cosine_hemisphere_pdf(proto::positive_dot(in_dir, surf_info.normal())); 52 | } 53 | 54 | proto::fnv::Hasher& DiffuseBsdf::hash(proto::fnv::Hasher& hasher) const { 55 | return hasher.combine(tag).combine(&kd_); 56 | } 57 | 58 | bool DiffuseBsdf::equals(const Bsdf& other) const { 59 | return other.tag == tag && &static_cast(other).kd_ == &kd_; 60 | } 61 | 62 | // Phong BSDF ---------------------------------------------------------------------- 63 | 64 | PhongBsdf::PhongBsdf(const ColorTexture& ks, const Texture& ns) 65 | : Bsdf(Tag::PhongBsdf, Type::Glossy), ks_(ks), ns_(ns) 66 | {} 67 | 68 | Color PhongBsdf::eval(const proto::Vec3f& in_dir, const SurfaceInfo& surf_info, const proto::Vec3f& out_dir) const { 69 | return eval(in_dir, surf_info, out_dir, ks_.sample_color(surf_info.tex_coords), ns_.sample(surf_info.tex_coords)); 70 | } 71 | 72 | std::optional PhongBsdf::sample(Sampler& sampler, const SurfaceInfo& surf_info, const proto::Vec3f& out_dir, bool) const { 73 | auto ks = ks_.sample_color(surf_info.tex_coords); 74 | auto ns = ns_.sample(surf_info.tex_coords); 75 | auto basis = proto::ortho_basis(proto::reflect(-out_dir, surf_info.normal())); 76 | auto [in_dir, pdf] = proto::sample_cosine_power_hemisphere(ns, sampler(), sampler()); 77 | auto local_in_dir = basis * in_dir; 78 | auto cos = proto::positive_dot(local_in_dir, surf_info.normal()); 79 | return validate_sample(surf_info, BsdfSample { 80 | .in_dir = local_in_dir, 81 | .pdf = pdf, 82 | .cos = cos, 83 | .color = eval(local_in_dir, surf_info, out_dir, ks, ns) 84 | }); 85 | } 86 | 87 | float PhongBsdf::pdf(const proto::Vec3f& in_dir, const SurfaceInfo& surf_info, const proto::Vec3f& out_dir) const { 88 | return proto::cosine_power_hemisphere_pdf(ns_.sample(surf_info.tex_coords), reflect_cosine(in_dir, surf_info.normal(), out_dir)); 89 | } 90 | 91 | proto::fnv::Hasher& PhongBsdf::hash(proto::fnv::Hasher& hasher) const { 92 | return hasher.combine(tag).combine(&ks_).combine(&ns_); 93 | } 94 | 95 | bool PhongBsdf::equals(const Bsdf& other) const { 96 | return 97 | other.tag == tag && 98 | &static_cast(other).ks_ == &ks_ && 99 | &static_cast(other).ns_ == &ns_; 100 | } 101 | 102 | Color PhongBsdf::eval(const proto::Vec3f& in_dir, const SurfaceInfo& surf_info, const proto::Vec3f& out_dir, const Color& ks, float ns) { 103 | return ks * std::pow(reflect_cosine(in_dir, surf_info.normal(), out_dir), ns) * (ns + 2.0f) * (0.5f * std::numbers::inv_pi_v); 104 | } 105 | 106 | // Mirror BSDF --------------------------------------------------------------------- 107 | 108 | MirrorBsdf::MirrorBsdf(const ColorTexture& ks) 109 | : Bsdf(Tag::MirrorBsdf, Type::Specular), ks_(ks) 110 | {} 111 | 112 | std::optional MirrorBsdf::sample(Sampler&, const SurfaceInfo& surf_info, const proto::Vec3f& out_dir, bool) const { 113 | return validate_sample(surf_info, BsdfSample { 114 | .in_dir = proto::reflect(-out_dir, surf_info.normal()), 115 | .pdf = 1.0f, 116 | .cos = 1.0f, 117 | .color = ks_.sample_color(surf_info.tex_coords) 118 | }); 119 | } 120 | 121 | proto::fnv::Hasher& MirrorBsdf::hash(proto::fnv::Hasher& hasher) const { 122 | return hasher.combine(tag).combine(&ks_); 123 | } 124 | 125 | bool MirrorBsdf::equals(const Bsdf& other) const { 126 | return other.tag == tag && &static_cast(other).ks_ == &ks_; 127 | } 128 | 129 | // Glass BSDF ---------------------------------------------------------------------- 130 | 131 | GlassBsdf::GlassBsdf( 132 | const ColorTexture& ks, 133 | const ColorTexture& kt, 134 | const Texture& eta) 135 | : Bsdf(Tag::GlassBsdf, Type::Specular), ks_(ks), kt_(kt), eta_(eta) 136 | {} 137 | 138 | std::optional GlassBsdf::sample(Sampler& sampler, const SurfaceInfo& surf_info, const proto::Vec3f& out_dir, bool is_adjoint) const { 139 | auto eta = eta_.sample(surf_info.tex_coords); 140 | eta = surf_info.is_front_side ? eta : 1.0f / eta; 141 | auto cos_i = proto::dot(out_dir, surf_info.normal()); 142 | auto cos2_t = 1.0f - eta * eta * (1.0f - cos_i * cos_i); 143 | if (cos2_t > 0.0f) { 144 | auto cos_t = std::sqrt(cos2_t); 145 | if (sampler() > fresnel_factor(eta, cos_i, cos_t)) { 146 | // Refraction 147 | auto refract_dir = (eta * cos_i - cos_t) * surf_info.normal() - eta * out_dir; 148 | auto adjoint_fix = is_adjoint ? eta * eta : 1.0f; 149 | return validate_sample(surf_info, BsdfSample { 150 | .in_dir = refract_dir, 151 | .pdf = 1.0f, 152 | .cos = 1.0f, 153 | .color = kt_.sample_color(surf_info.tex_coords) * adjoint_fix 154 | }); 155 | } 156 | } 157 | 158 | // Reflection 159 | return validate_sample(surf_info, BsdfSample { 160 | .in_dir = proto::reflect(-out_dir, surf_info.normal()), 161 | .pdf = 1.0f, 162 | .cos = 1.0f, 163 | .color = ks_.sample_color(surf_info.tex_coords) 164 | }); 165 | } 166 | 167 | proto::fnv::Hasher& GlassBsdf::hash(proto::fnv::Hasher& hasher) const { 168 | return hasher.combine(tag).combine(&ks_).combine(&kt_).combine(&eta_); 169 | } 170 | 171 | bool GlassBsdf::equals(const Bsdf& other) const { 172 | return 173 | other.tag == tag && 174 | &static_cast(other).ks_ == &ks_ && 175 | &static_cast(other).kt_ == &kt_ && 176 | &static_cast(other).eta_ == &eta_; 177 | } 178 | 179 | // Interpolation BSDF -------------------------------------------------------------- 180 | 181 | InterpBsdf::InterpBsdf(const Bsdf* a, const Bsdf* b, const Texture& k) 182 | : Bsdf(Tag::InterpBsdf, infer_type(a->type, b->type)), a_(a), b_(b), k_(k) 183 | {} 184 | 185 | RgbColor InterpBsdf::eval(const proto::Vec3f& in_dir, const SurfaceInfo& surf_info, const proto::Vec3f& out_dir) const { 186 | return lerp( 187 | a_->eval(in_dir, surf_info, out_dir), 188 | b_->eval(in_dir, surf_info, out_dir), 189 | k_.sample(surf_info.tex_coords)); 190 | } 191 | 192 | std::optional InterpBsdf::sample(Sampler& sampler, const SurfaceInfo& surf_info, const proto::Vec3f& out_dir, bool is_adjoint) const { 193 | auto k = k_.sample(surf_info.tex_coords); 194 | auto target = b_, other = a_; 195 | if (sampler() > k) { 196 | std::swap(target, other); 197 | k = 1.0f - k; 198 | } 199 | if (auto sample = target->sample(sampler, surf_info, out_dir, is_adjoint)) { 200 | sample->pdf = proto::lerp(other->pdf(sample->in_dir, surf_info, out_dir), sample->pdf, k); 201 | sample->color = lerp(other->eval(sample->in_dir, surf_info, out_dir), sample->color, k); 202 | return sample; 203 | } 204 | return std::nullopt; 205 | } 206 | 207 | float InterpBsdf::pdf(const proto::Vec3f& in_dir, const SurfaceInfo& surf_info, const proto::Vec3f& out_dir) const { 208 | return proto::lerp( 209 | a_->pdf(in_dir, surf_info, out_dir), 210 | b_->pdf(in_dir, surf_info, out_dir), 211 | k_.sample(surf_info.tex_coords)); 212 | } 213 | 214 | proto::fnv::Hasher& InterpBsdf::hash(proto::fnv::Hasher& hasher) const { 215 | return hasher.combine(tag).combine(a_).combine(b_).combine(&k_); 216 | } 217 | 218 | bool InterpBsdf::equals(const Bsdf& other) const { 219 | return 220 | other.tag == tag && 221 | static_cast(other).a_ == a_ && 222 | static_cast(other).b_ == b_ && 223 | &static_cast(other).k_ == &k_; 224 | } 225 | 226 | Bsdf::Type InterpBsdf::infer_type(Type a, Type b) { 227 | if (a == Type::Diffuse || b == Type::Diffuse) return Type::Diffuse; 228 | if (a == Type::Glossy || b == Type::Glossy ) return Type::Glossy; 229 | return Type::Specular; 230 | } 231 | 232 | } // namespace sol 233 | -------------------------------------------------------------------------------- /src/cameras.cpp: -------------------------------------------------------------------------------- 1 | #include "sol/cameras.h" 2 | 3 | namespace sol { 4 | 5 | PerspectiveCamera::PerspectiveCamera( 6 | const proto::Vec3f& eye, 7 | const proto::Vec3f& dir, 8 | const proto::Vec3f& up, 9 | float horz_fov, 10 | float aspect_ratio) 11 | : eye_(eye) 12 | { 13 | dir_ = proto::normalize(dir); 14 | right_ = proto::normalize(proto::cross(dir, up)); 15 | up_ = proto::cross(right_, dir_); 16 | 17 | w_ = std::tan(horz_fov * std::numbers::pi_v / 360.0f); 18 | h_ = w_ / aspect_ratio; 19 | right_ *= w_; 20 | up_ *= h_; 21 | } 22 | 23 | proto::Rayf PerspectiveCamera::generate_ray(const proto::Vec2f& uv) const { 24 | return proto::Rayf(eye_, proto::normalize(dir_ + uv[0] * right_ + uv[1] * up_)); 25 | } 26 | 27 | proto::Vec2f PerspectiveCamera::project(const proto::Vec3f& point) const { 28 | auto d = proto::normalize(point - eye_); 29 | return proto::Vec2f(dot(d, right_) / (w_ * w_), dot(d, up_) / (h_ * h_)); 30 | } 31 | 32 | proto::Vec3f PerspectiveCamera::unproject(const proto::Vec2f& uv) const { 33 | return eye_ + dir_ + uv[0] * right_ + uv[1] * up_; 34 | } 35 | 36 | LensGeometry PerspectiveCamera::geometry(const proto::Vec2f& uv) const { 37 | auto d = std::sqrt(1.0f + uv[0] * uv[0] * w_ * w_ + uv[1] * uv[1] * h_ * h_); 38 | return LensGeometry { 1.0f / d, d, 1.0f / (4.0f * w_ * h_) }; 39 | } 40 | 41 | } // namespace sol 42 | -------------------------------------------------------------------------------- /src/formats/exr.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define TINYEXR_IMPLEMENTATION 4 | #include 5 | 6 | #include "formats/exr.h" 7 | 8 | namespace sol::exr { 9 | 10 | std::optional load(const std::string_view& path) { 11 | std::string path_string(path); 12 | 13 | EXRVersion version; 14 | if (ParseEXRVersionFromFile(&version, path_string.c_str()) != 0 || version.multipart) 15 | return std::nullopt; 16 | 17 | EXRHeader exr_header; 18 | InitEXRHeader(&exr_header); 19 | 20 | const char* err = nullptr; 21 | if (ParseEXRHeaderFromFile(&exr_header, &version, path_string.c_str(), &err) != 0) { 22 | FreeEXRErrorMessage(err); 23 | return std::nullopt; 24 | } 25 | 26 | for (int i = 0; i < exr_header.num_channels; ++i) 27 | exr_header.requested_pixel_types[i] = TINYEXR_PIXELTYPE_FLOAT; 28 | 29 | EXRImage exr_image; 30 | InitEXRImage(&exr_image); 31 | 32 | if (LoadEXRImageFromFile(&exr_image, &exr_header, path_string.c_str(), &err) != 0) { 33 | FreeEXRHeader(&exr_header); 34 | FreeEXRErrorMessage(err); 35 | return std::nullopt; 36 | } 37 | 38 | Image image(exr_image.width, exr_image.height, exr_header.num_channels); 39 | for (int i = 0; i < exr_header.num_channels; ++i) 40 | std::memcpy(image.channel(i).get(), exr_image.images[i], sizeof(float) * exr_image.width * exr_image.height); 41 | 42 | FreeEXRImage(&exr_image); 43 | FreeEXRHeader(&exr_header); 44 | return std::make_optional(std::move(image)); 45 | } 46 | 47 | bool save(const Image& image, const std::string_view& path) { 48 | std::string path_string(path); 49 | 50 | EXRHeader exr_header; 51 | EXRImage exr_image; 52 | InitEXRHeader(&exr_header); 53 | InitEXRImage(&exr_image); 54 | 55 | std::vector images(image.channel_count()); 56 | if (image.channel_count() >= 3) { 57 | images[0] = image.channel(2).get(); 58 | images[1] = image.channel(1).get(); 59 | images[2] = image.channel(0).get(); 60 | for (size_t i = 3; i < image.channel_count(); ++i) 61 | images[i] = image.channel(i).get(); 62 | } else { 63 | for (size_t i = 0; i < image.channel_count(); ++i) 64 | images[i] = image.channel(i).get(); 65 | } 66 | 67 | exr_image.images = reinterpret_cast(images.data()); 68 | exr_image.width = image.width(); 69 | exr_image.height = image.height(); 70 | 71 | std::vector channel_infos(image.channel_count()); 72 | exr_header.num_channels = image.channel_count(); 73 | exr_header.channels = channel_infos.data(); 74 | 75 | size_t name_len = sizeof(EXRChannelInfo::name) - 1; 76 | for (size_t i = 0; i < image.channel_count(); ++i) { 77 | auto channel_name = "Channel_" + std::to_string(i); 78 | // Note: That syntax here makes sure that the name is a proper C-string 79 | std::strncpy(exr_header.channels[i].name, channel_name.c_str(), name_len)[name_len] = 0; 80 | } 81 | if (image.channel_count() >= 3) { 82 | if (image.channel_count() >= 4) 83 | std::strncpy(exr_header.channels[3].name, "A", name_len)[name_len] = 0; 84 | std::strncpy(exr_header.channels[2].name, "R", name_len)[name_len] = 0; 85 | std::strncpy(exr_header.channels[1].name, "G", name_len)[name_len] = 0; 86 | std::strncpy(exr_header.channels[0].name, "B", name_len)[name_len] = 0; 87 | } 88 | 89 | std::vector pixel_types(image.channel_count(), TINYEXR_PIXELTYPE_FLOAT); 90 | exr_header.pixel_types = pixel_types.data(); 91 | exr_header.requested_pixel_types = pixel_types.data(); 92 | 93 | const char* err = nullptr; 94 | if (SaveEXRImageToFile(&exr_image, &exr_header, path_string.c_str(), &err) != 0) { 95 | FreeEXRErrorMessage(err); 96 | return false; 97 | } 98 | 99 | return true; 100 | } 101 | 102 | } // namespace sol::exr 103 | -------------------------------------------------------------------------------- /src/formats/exr.h: -------------------------------------------------------------------------------- 1 | #ifndef SOL_FORMATS_EXR_H 2 | #define SOL_FORMATS_EXR_H 3 | 4 | #include 5 | 6 | #include "sol/image.h" 7 | 8 | namespace sol::exr { 9 | 10 | std::optional load(const std::string_view&); 11 | bool save(const Image&, const std::string_view&); 12 | 13 | } // namespace sol::exr 14 | 15 | #endif 16 | -------------------------------------------------------------------------------- /src/formats/jpeg.cpp: -------------------------------------------------------------------------------- 1 | #ifdef SOL_ENABLE_JPEG 2 | #include 3 | #include 4 | 5 | #include 6 | #endif 7 | 8 | #include "formats/jpeg.h" 9 | 10 | namespace sol::jpeg { 11 | 12 | #ifdef SOL_ENABLE_JPEG 13 | static void error_exit(j_common_ptr) { throw std::runtime_error("Cannot load/save JPEG image file"); } 14 | static void output_message(j_common_ptr) {} 15 | 16 | std::optional load(const std::string_view& path) { 17 | std::string path_string(path); 18 | auto file = fopen(path_string.c_str(), "rb"); 19 | if (!file) 20 | return std::nullopt; 21 | std::unique_ptr file_ptr(file, fclose); 22 | 23 | jpeg_error_mgr jerr; 24 | jpeg_std_error(&jerr); 25 | jerr.error_exit = error_exit; 26 | jerr.output_message = output_message; 27 | 28 | std::optional final_image; 29 | jpeg_decompress_struct decompress_info; 30 | try { 31 | decompress_info.err = &jerr; 32 | jpeg_create_decompress(&decompress_info); 33 | std::unique_ptr 34 | decompress_ptr(&decompress_info, jpeg_destroy_decompress); 35 | 36 | jpeg_stdio_src(&decompress_info, file); 37 | 38 | jpeg_read_header(&decompress_info, TRUE); 39 | jpeg_start_decompress(&decompress_info); 40 | size_t width = decompress_info.output_width; 41 | size_t height = decompress_info.output_height; 42 | size_t channel_count = decompress_info.output_components; 43 | Image image(width, height, channel_count); 44 | 45 | auto row = std::make_unique(image.width() * image.channel_count()); 46 | for (size_t y = 0; y < height; y++) { 47 | auto row_ptr = row.get(); 48 | jpeg_read_scanlines(&decompress_info, &row_ptr, 1); 49 | for (size_t x = 0; x < width; ++x) { 50 | for (size_t i = 0; i < channel_count; ++i) { 51 | image.channel(i)[y * width + x] = 52 | Image::word_to_component(row[x * channel_count + i]); 53 | } 54 | } 55 | } 56 | jpeg_finish_decompress(&decompress_info); 57 | jpeg_destroy_decompress(&decompress_info); 58 | final_image = std::make_optional(std::move(image)); 59 | } catch (std::exception& e) { 60 | // Do nothing 61 | } 62 | 63 | jpeg_destroy_decompress(&decompress_info); 64 | return final_image; 65 | } 66 | 67 | bool save(const Image& image, const std::string_view& path) { 68 | std::string path_string(path); 69 | auto file = fopen(path_string.c_str(), "wb"); 70 | if (!file) 71 | return false; 72 | std::unique_ptr file_ptr(file, fclose); 73 | 74 | jpeg_error_mgr jerr; 75 | jpeg_std_error(&jerr); 76 | jerr.error_exit = error_exit; 77 | jerr.output_message = output_message; 78 | 79 | size_t channel_count = 3; 80 | jpeg_compress_struct compress_info; 81 | try { 82 | compress_info.err = &jerr; 83 | jpeg_create_compress(&compress_info); 84 | std::unique_ptr 85 | compress_ptr(&compress_info, jpeg_destroy_compress); 86 | 87 | jpeg_stdio_dest(&compress_info, file); 88 | 89 | compress_info.image_width = image.width(); 90 | compress_info.image_height = image.height(); 91 | compress_info.input_components = channel_count; 92 | compress_info.in_color_space = JCS_RGB; 93 | jpeg_set_defaults(&compress_info); 94 | jpeg_start_compress(&compress_info, TRUE); 95 | 96 | auto row = std::make_unique(image.width() * channel_count); 97 | for (size_t y = 0; y < image.height(); y++) { 98 | std::fill(row.get(), row.get() + image.width() * channel_count, 0); 99 | for (size_t x = 0; x < image.width(); ++x) { 100 | for (size_t i = 0, n = std::min(image.channel_count(), channel_count); i < n; ++i) { 101 | row[x * channel_count + i] = 102 | Image::component_to_word(image.channel(i)[y * image.width() + x]); 103 | } 104 | } 105 | auto row_ptr = row.get(); 106 | jpeg_write_scanlines(&compress_info, &row_ptr, 1); 107 | } 108 | jpeg_finish_compress(&compress_info); 109 | return true; 110 | } catch(std::exception& e) { 111 | return false; 112 | } 113 | } 114 | #else 115 | std::optional load(const std::string_view&) { return std::nullopt; } 116 | bool save(const Image&, const std::string_view&) { return false; } 117 | #endif // SOL_ENABLE_JPEG 118 | 119 | } // namespace sol::jpeg 120 | -------------------------------------------------------------------------------- /src/formats/jpeg.h: -------------------------------------------------------------------------------- 1 | #ifndef SOL_FORMATS_JPEG_H 2 | #define SOL_FORMATS_JPEG_H 3 | 4 | #include 5 | 6 | #include "sol/image.h" 7 | 8 | namespace sol::jpeg { 9 | 10 | std::optional load(const std::string_view&); 11 | bool save(const Image&, const std::string_view&); 12 | 13 | } // namespace sol::jpeg 14 | 15 | #endif 16 | -------------------------------------------------------------------------------- /src/formats/obj.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | 15 | #include "sol/color.h" 16 | #include "sol/textures.h" 17 | #include "sol/bsdfs.h" 18 | #include "sol/lights.h" 19 | #include "sol/triangle_mesh.h" 20 | 21 | #include "formats/obj.h" 22 | 23 | namespace sol::obj { 24 | 25 | // Maximum line length allowed in OBJ and MTL files. 26 | static constexpr size_t max_line_len = 1024; 27 | 28 | struct Index { 29 | int v, n, t; 30 | 31 | size_t hash() const { 32 | return proto::fnv::Hasher().combine(v).combine(n).combine(t); 33 | } 34 | 35 | bool operator == (const Index& other) const { 36 | return v == other.v && n == other.n && t == other.t; 37 | } 38 | }; 39 | 40 | struct Face { 41 | std::vector indices; 42 | size_t material; 43 | }; 44 | 45 | struct Group { 46 | std::vector faces; 47 | }; 48 | 49 | struct Object { 50 | std::vector groups; 51 | }; 52 | 53 | struct Material { 54 | RgbColor ka = RgbColor::black(); 55 | RgbColor kd = RgbColor::black(); 56 | RgbColor ks = RgbColor::black(); 57 | RgbColor ke = RgbColor::black(); 58 | RgbColor tf = RgbColor::black(); 59 | float ns = 0.0f; 60 | float ni = 0.0f; 61 | float tr = 0.0f; 62 | float d = 0.0f; 63 | int illum = 0; 64 | std::string map_ka; 65 | std::string map_kd; 66 | std::string map_ks; 67 | std::string map_ns; 68 | std::string map_ke; 69 | std::string map_d; 70 | std::string bump; 71 | }; 72 | 73 | using MaterialLib = std::unordered_map; 74 | 75 | struct File { 76 | std::vector objects; 77 | std::vector vertices; 78 | std::vector normals; 79 | std::vector tex_coords; 80 | std::vector materials; 81 | std::unordered_set mtl_files; 82 | 83 | size_t face_count() const { 84 | size_t count = 0; 85 | for (auto& object : objects) { 86 | for (auto& group : object.groups) 87 | count += group.faces.size(); 88 | } 89 | return count; 90 | } 91 | }; 92 | 93 | inline void remove_eol(char* ptr) { 94 | int i = 0; 95 | while (ptr[i]) i++; 96 | i--; 97 | while (i > 0 && std::isspace(ptr[i])) { 98 | ptr[i] = '\0'; 99 | i--; 100 | } 101 | } 102 | 103 | inline char* strip_text(char* ptr) { 104 | while (*ptr && !std::isspace(*ptr)) { ptr++; } 105 | return ptr; 106 | } 107 | 108 | inline char* strip_spaces(char* ptr) { 109 | while (std::isspace(*ptr)) { ptr++; } 110 | return ptr; 111 | } 112 | 113 | inline std::optional parse_index(char** ptr) { 114 | char* base = *ptr; 115 | 116 | // Detect end of line (negative indices are supported) 117 | base = strip_spaces(base); 118 | if (!std::isdigit(*base) && *base != '-') 119 | return std::nullopt; 120 | 121 | Index idx = {0, 0, 0}; 122 | idx.v = std::strtol(base, &base, 10); 123 | 124 | base = strip_spaces(base); 125 | if (*base == '/') { 126 | base++; 127 | 128 | if (*base != '/') 129 | idx.t = std::strtol(base, &base, 10); 130 | 131 | base = strip_spaces(base); 132 | if (*base == '/') { 133 | base++; 134 | idx.n = std::strtol(base, &base, 10); 135 | } 136 | } 137 | 138 | *ptr = base; 139 | return std::make_optional(idx); 140 | } 141 | 142 | inline proto::Vec3f parse_vec3(char* ptr) { 143 | proto::Vec3f v; 144 | v[0] = std::strtof(ptr, &ptr); 145 | v[1] = std::strtof(ptr, &ptr); 146 | v[2] = std::strtof(ptr, &ptr); 147 | return v; 148 | } 149 | 150 | inline RgbColor parse_rgb_color(char* ptr) { 151 | RgbColor color; 152 | color.r = std::max(std::strtof(ptr, &ptr), 0.0f); 153 | color.g = std::max(std::strtof(ptr, &ptr), 0.0f); 154 | color.b = std::max(std::strtof(ptr, &ptr), 0.0f); 155 | return color; 156 | } 157 | 158 | inline proto::Vec2f parse_vec2(char* ptr) { 159 | proto::Vec2f v; 160 | v[0] = std::strtof(ptr, &ptr); 161 | v[1] = std::strtof(ptr, &ptr); 162 | return v; 163 | } 164 | 165 | static File parse_obj(std::istream& is, const std::string& file_name, bool is_strict) { 166 | File file; 167 | 168 | // Add dummy elements to account for the fact that indices start at 1 in the file. 169 | file.objects.emplace_back(); 170 | file.objects[0].groups.emplace_back(); 171 | file.materials.emplace_back("#dummy"); 172 | file.vertices.emplace_back(0); 173 | file.normals.emplace_back(0); 174 | file.tex_coords.emplace_back(0); 175 | 176 | char line[max_line_len]; 177 | size_t line_count = 0; 178 | size_t material_index = 0; 179 | 180 | while (is.getline(line, max_line_len)) { 181 | line_count++; 182 | char* ptr = strip_spaces(line); 183 | 184 | // Skip comments and empty lines 185 | if (*ptr == '\0' || *ptr == '#') 186 | continue; 187 | 188 | remove_eol(ptr); 189 | 190 | // Test each command in turn, the most frequent first 191 | if (*ptr == 'v') { 192 | switch (ptr[1]) { 193 | case ' ': 194 | case '\t': file.vertices .push_back(parse_vec3(ptr + 1)); break; 195 | case 'n': file.normals .push_back(parse_vec3(ptr + 2)); break; 196 | case 't': file.tex_coords.push_back(parse_vec2(ptr + 2)); break; 197 | default: 198 | if (is_strict) 199 | throw SourceError(file_name, { line_count, 1 }, "Invalid vertex"); 200 | break; 201 | } 202 | } else if (*ptr == 'f' && std::isspace(ptr[1])) { 203 | Face face; 204 | ptr += 2; 205 | while (true) { 206 | auto index = parse_index(&ptr); 207 | if (!index) 208 | break; 209 | face.indices.push_back(*index); 210 | } 211 | face.material = material_index; 212 | 213 | // Convert relative indices to absolute 214 | bool is_valid = face.indices.size() >= 3; 215 | for (size_t i = 0; i < face.indices.size(); i++) { 216 | face.indices[i].v = (face.indices[i].v < 0) ? file.vertices.size() + face.indices[i].v : face.indices[i].v; 217 | face.indices[i].t = (face.indices[i].t < 0) ? file.tex_coords.size() + face.indices[i].t : face.indices[i].t; 218 | face.indices[i].n = (face.indices[i].n < 0) ? file.normals.size() + face.indices[i].n : face.indices[i].n; 219 | is_valid &= face.indices[i].v > 0 && face.indices[i].t >= 0 && face.indices[i].n >= 0; 220 | } 221 | 222 | if (is_valid) 223 | file.objects.back().groups.back().faces.push_back(face); 224 | else if (is_strict) 225 | throw SourceError(file_name, { line_count, 1 }, "Invalid face"); 226 | } else if (*ptr == 'g' && std::isspace(ptr[1])) { 227 | file.objects.back().groups.emplace_back(); 228 | } else if (*ptr == 'o' && std::isspace(ptr[1])) { 229 | file.objects.emplace_back(); 230 | file.objects.back().groups.emplace_back(); 231 | } else if (!std::strncmp(ptr, "usemtl", 6) && std::isspace(ptr[6])) { 232 | ptr = strip_spaces(ptr + 6); 233 | char* base = ptr; 234 | ptr = strip_text(ptr); 235 | 236 | std::string material(base, ptr); 237 | if (auto it = std::find(file.materials.begin(), file.materials.end(), material); it != file.materials.end()) { 238 | material_index = it - file.materials.begin(); 239 | } else { 240 | material_index = file.materials.size(); 241 | file.materials.push_back(material); 242 | } 243 | } else if (!std::strncmp(ptr, "mtllib", 6) && std::isspace(ptr[6])) { 244 | ptr = strip_spaces(ptr + 6); 245 | char* base = ptr; 246 | ptr = strip_text(ptr); 247 | file.mtl_files.emplace(base, ptr); 248 | } else if (*ptr == 's' && std::isspace(ptr[1])) { 249 | // Ignore smooth commands 250 | } else if (is_strict) 251 | throw SourceError(file_name, { line_count, 1 }, "Unknown command"); 252 | } 253 | 254 | return file; 255 | } 256 | 257 | static File parse_obj(const std::string& file_name, bool is_strict) { 258 | std::ifstream is(file_name); 259 | if (!is) 260 | throw std::runtime_error("Cannot open OBJ file '" + file_name + "'"); 261 | return parse_obj(is, file_name, is_strict); 262 | } 263 | 264 | static void parse_mtl(std::istream& stream, const std::string& file_name, MaterialLib& material_lib, bool is_strict = false) { 265 | char line[max_line_len]; 266 | size_t line_count = 0; 267 | 268 | auto* material = &material_lib["#dummy"]; 269 | while (stream.getline(line, max_line_len)) { 270 | line_count++; 271 | char* ptr = strip_spaces(line); 272 | 273 | // Skip comments and empty lines 274 | if (*ptr == '\0' || *ptr == '#') 275 | continue; 276 | 277 | remove_eol(ptr); 278 | if (!std::strncmp(ptr, "newmtl", 6) && std::isspace(ptr[6])) { 279 | ptr = strip_spaces(ptr + 7); 280 | char* base = ptr; 281 | ptr = strip_text(ptr); 282 | std::string name(base, ptr); 283 | if (is_strict && material_lib.contains(name)) 284 | throw SourceError(file_name, { line_count, 1 }, "Redefinition of material '" + name + "'"); 285 | material = &material_lib[name]; 286 | } else if (ptr[0] == 'K' && std::isspace(ptr[2])) { 287 | if (ptr[1] == 'a') 288 | material->ka = parse_rgb_color(ptr + 3); 289 | else if (ptr[1] == 'd') 290 | material->kd = parse_rgb_color(ptr + 3); 291 | else if (ptr[1] == 's') 292 | material->ks = parse_rgb_color(ptr + 3); 293 | else if (ptr[1] == 'e') 294 | material->ke = parse_rgb_color(ptr + 3); 295 | else 296 | goto unknown_command; 297 | } else if (ptr[0] == 'N' && std::isspace(ptr[2])) { 298 | if (ptr[1] == 's') 299 | material->ns = std::strtof(ptr + 3, &ptr); 300 | else if (ptr[1] == 'i') 301 | material->ni = std::strtof(ptr + 3, &ptr); 302 | else 303 | goto unknown_command; 304 | } else if (ptr[0] == 'T' && std::isspace(ptr[2])) { 305 | if (ptr[1] == 'f') 306 | material->tf = parse_rgb_color(ptr + 3); 307 | else if (ptr[1] == 'r') 308 | material->tr = std::strtof(ptr + 3, &ptr); 309 | else 310 | goto unknown_command; 311 | } else if (ptr[0] == 'd' && std::isspace(ptr[1])) { 312 | material->d = std::strtof(ptr + 2, &ptr); 313 | } else if (!std::strncmp(ptr, "illum", 5) && std::isspace(ptr[5])) { 314 | material->illum = std::strtof(ptr + 6, &ptr); 315 | } else if (!std::strncmp(ptr, "map_Ka", 6) && std::isspace(ptr[6])) { 316 | material->map_ka = std::string(strip_spaces(ptr + 7)); 317 | } else if (!std::strncmp(ptr, "map_Kd", 6) && std::isspace(ptr[6])) { 318 | material->map_kd = std::string(strip_spaces(ptr + 7)); 319 | } else if (!std::strncmp(ptr, "map_Ks", 6) && std::isspace(ptr[6])) { 320 | material->map_ks = std::string(strip_spaces(ptr + 7)); 321 | } else if (!std::strncmp(ptr, "map_Ke", 6) && std::isspace(ptr[6])) { 322 | material->map_ke = std::string(strip_spaces(ptr + 7)); 323 | } else if (!std::strncmp(ptr, "map_Ns", 6) && std::isspace(ptr[6])) { 324 | material->map_ns = std::string(strip_spaces(ptr + 7)); 325 | } else if (!std::strncmp(ptr, "map_d", 5) && std::isspace(ptr[5])) { 326 | material->map_d = std::string(strip_spaces(ptr + 6)); 327 | } else if (!std::strncmp(ptr, "bump", 4) && std::isspace(ptr[4])) { 328 | material->bump = std::string(strip_spaces(ptr + 4)); 329 | } else 330 | goto unknown_command; 331 | continue; 332 | 333 | unknown_command: 334 | if (is_strict) 335 | throw SourceError(file_name, { line_count, 1 }, "Unknown command '" + std::string(ptr) + "'"); 336 | } 337 | } 338 | 339 | static void parse_mtl(const std::string& file_name, MaterialLib& material_lib, bool is_strict) { 340 | std::ifstream is(file_name); 341 | if (!is) { 342 | // Accept missing material files in non-strict mode 343 | if (!is_strict) return; 344 | throw std::runtime_error("Cannot open MTL file '" + file_name + "'"); 345 | } 346 | parse_mtl(is, file_name, material_lib, is_strict); 347 | } 348 | 349 | template 350 | static const Texture* get_texture( 351 | SceneLoader& scene_loader, 352 | const std::string& file_name, 353 | const T& constant, 354 | bool is_strict) 355 | { 356 | using ImageTextureType = ImageTexture; 357 | if (file_name != "") { 358 | if (auto image = scene_loader.load_image(file_name)) 359 | return scene_loader.get_or_insert_texture(*image); 360 | else if (is_strict) 361 | throw std::runtime_error("Cannot load image '" + file_name + "'"); 362 | } 363 | return scene_loader.get_or_insert_texture>(constant); 364 | } 365 | 366 | static const ColorTexture* get_color_texture( 367 | SceneLoader& scene_loader, 368 | const std::string& file_name, 369 | const RgbColor& color, 370 | bool is_strict) 371 | { 372 | return static_cast(get_texture(scene_loader, file_name, color, is_strict)); 373 | } 374 | 375 | static const Bsdf* convert_material(SceneLoader& scene_loader, const Material& material, bool is_strict) { 376 | switch (material.illum) { 377 | case 5: { 378 | auto ks = get_color_texture(scene_loader, material.map_ks, material.ks, is_strict); 379 | return scene_loader.get_or_insert_bsdf(*ks); 380 | } 381 | case 7: { 382 | auto ks = get_color_texture(scene_loader, material.map_ks, material.ks, is_strict); 383 | auto kt = scene_loader.get_or_insert_texture(material.tf); 384 | auto ni = scene_loader.get_or_insert_texture(1.0f / material.ni); 385 | return scene_loader.get_or_insert_bsdf(*ks, static_cast(*kt), *ni); 386 | } 387 | default: { 388 | // A mix of Phong and diffuse 389 | const Bsdf* diff = nullptr, *spec = nullptr; 390 | float diff_k = 0, spec_k = 0; 391 | 392 | if (material.ks != RgbColor::black() || material.map_ks != "") { 393 | auto ks = get_color_texture(scene_loader, material.map_ks, material.ks, is_strict); 394 | auto ns = get_texture(scene_loader, material.map_ns, material.ns, is_strict); 395 | spec = scene_loader.get_or_insert_bsdf(*ks, *ns); 396 | spec_k = material.map_ks != "" ? 1.0f : material.ks.luminance(); 397 | } 398 | 399 | if (material.kd != RgbColor::black() || material.map_kd != "") { 400 | auto kd = get_color_texture(scene_loader, material.map_kd, material.kd, is_strict); 401 | diff = scene_loader.get_or_insert_bsdf(*kd); 402 | diff_k = material.map_kd != "" ? 1.0f : material.kd.luminance(); 403 | } 404 | 405 | if (spec && diff) { 406 | auto k = scene_loader.get_or_insert_texture(spec_k / (diff_k + spec_k)); 407 | return scene_loader.get_or_insert_bsdf(diff, spec, *k); 408 | } 409 | return diff ? diff : spec; 410 | } 411 | } 412 | } 413 | 414 | static void check_materials(File& file, const MaterialLib& material_lib, bool is_strict) { 415 | for (auto& material : file.materials) { 416 | if (!material_lib.contains(material)) { 417 | if (is_strict) 418 | throw std::runtime_error("Cannot find material named '" + material + "'"); 419 | material = "#dummy"; 420 | } 421 | } 422 | } 423 | 424 | static std::unique_ptr build_mesh( 425 | SceneLoader& scene_loader, 426 | const File& file, 427 | const MaterialLib& material_lib, 428 | bool is_strict) 429 | { 430 | auto hash = [] (const Index& idx) { return idx.hash(); }; 431 | std::unordered_map index_map(file.vertices.size(), std::move(hash)); 432 | 433 | std::vector indices; 434 | std::vector vertices; 435 | std::vector normals; 436 | std::vector tex_coords; 437 | std::vector bsdfs; 438 | std::vector normals_to_fix; 439 | std::unordered_map lights; 440 | 441 | vertices.reserve(file.vertices.size()); 442 | normals.reserve(file.normals.size()); 443 | tex_coords.reserve(file.tex_coords.size()); 444 | indices.reserve(file.face_count() * 3); 445 | 446 | for (auto& object : file.objects) { 447 | for (auto& group : object.groups) { 448 | for (auto& face : group.faces) { 449 | // Make a unique vertex for each possible combination of 450 | // vertex, texture coordinate, and normal index. 451 | for (auto& index : face.indices) { 452 | if (index_map.emplace(index, index_map.size()).second) { 453 | // Mark this normal so that we can fix it later, 454 | // if it is missing from the OBJ file. 455 | if (index.n == 0) 456 | normals_to_fix.push_back(normals.size()); 457 | vertices .push_back(file.vertices [index.v]); 458 | normals .push_back(file.normals [index.n]); 459 | tex_coords.push_back(file.tex_coords[index.t]); 460 | } 461 | } 462 | 463 | // Get a BSDF for the face 464 | assert(material_lib.contains(file.materials[face.material])); 465 | auto& material = material_lib.find(file.materials[face.material])->second; 466 | auto bsdf = convert_material(scene_loader, material, is_strict); 467 | bool is_emissive = material.ke != Color::black() || material.map_ke != ""; 468 | 469 | // Add triangles to the mesh 470 | size_t first_index = index_map[face.indices[0]]; 471 | size_t cur_index = index_map[face.indices[1]]; 472 | for (size_t i = 1, n = face.indices.size() - 1; i < n; ++i) { 473 | auto next_index = index_map[face.indices[i + 1]]; 474 | 475 | if (is_emissive) { 476 | proto::Trianglef triangle(vertices[first_index], vertices[cur_index], vertices[next_index]); 477 | auto intensity = get_color_texture(scene_loader, material.map_ke, material.ke, is_strict); 478 | auto light = scene_loader.get_or_insert_light(triangle, *intensity); 479 | lights.emplace(indices.size() / 3, light); 480 | } 481 | 482 | indices.push_back(first_index); 483 | indices.push_back(cur_index); 484 | indices.push_back(next_index); 485 | bsdfs.push_back(bsdf); 486 | 487 | cur_index = next_index; 488 | } 489 | } 490 | } 491 | } 492 | 493 | // Fix normals that are missing. 494 | if (!normals_to_fix.empty()) { 495 | std::vector smooth_normals(normals.size(), proto::Vec3f(0)); 496 | for (size_t i = 0; i < indices.size(); i += 3) { 497 | auto normal = proto::Trianglef( 498 | vertices[indices[i + 0]], 499 | vertices[indices[i + 1]], 500 | vertices[indices[i + 2]]).normal(); 501 | smooth_normals[indices[i + 0]] += normal; 502 | smooth_normals[indices[i + 1]] += normal; 503 | smooth_normals[indices[i + 2]] += normal; 504 | } 505 | for (auto normal_index : normals_to_fix) 506 | normals[normal_index] = proto::normalize(smooth_normals[normal_index]); 507 | } 508 | 509 | return std::make_unique( 510 | std::move(indices), 511 | std::move(vertices), 512 | std::move(normals), 513 | std::move(tex_coords), 514 | std::move(bsdfs), 515 | std::move(lights)); 516 | } 517 | 518 | std::unique_ptr load(SceneLoader& scene_loader, const std::string_view& file_name) { 519 | static constexpr bool is_strict = false; 520 | 521 | auto file = parse_obj(std::string(file_name), is_strict); 522 | MaterialLib material_lib; 523 | material_lib.emplace("#dummy", Material {}); 524 | 525 | for (auto& mtl_file : file.mtl_files) { 526 | std::error_code err_code; 527 | auto full_path = std::filesystem::absolute(file_name, err_code).parent_path().string() + "/" + mtl_file; 528 | parse_mtl(full_path, material_lib, is_strict); 529 | } 530 | 531 | check_materials(file, material_lib, is_strict); 532 | return build_mesh(scene_loader, file, material_lib, is_strict); 533 | } 534 | 535 | } // namespace sol::obj 536 | -------------------------------------------------------------------------------- /src/formats/obj.h: -------------------------------------------------------------------------------- 1 | #ifndef SOL_FORMATS_OBJ_H 2 | #define SOL_FORMATS_OBJ_H 3 | 4 | #include 5 | 6 | #include "scene_loader.h" 7 | #include "sol/triangle_mesh.h" 8 | 9 | namespace sol::obj { 10 | 11 | std::unique_ptr load(SceneLoader&, const std::string_view&); 12 | 13 | } // namespace sol::obj 14 | 15 | #endif 16 | -------------------------------------------------------------------------------- /src/formats/png.cpp: -------------------------------------------------------------------------------- 1 | #ifdef SOL_ENABLE_PNG 2 | #include 3 | 4 | #include 5 | #endif 6 | 7 | #include "formats/png.h" 8 | 9 | namespace sol::png { 10 | 11 | #ifdef SOL_ENABLE_PNG 12 | std::optional load(const std::string_view& path) { 13 | std::string path_string(path); 14 | auto file = fopen(path_string.c_str(), "rb"); 15 | if (!file) 16 | return std::nullopt; 17 | std::unique_ptr file_ptr(file, fclose); 18 | 19 | // Read signature 20 | png_byte sig[8]; 21 | if (fread(sig, 1, 8, file) != 8 || !png_sig_cmp(sig, 0, 8)) 22 | return std::nullopt; 23 | 24 | png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); 25 | if (!png_ptr) 26 | return std::nullopt; 27 | 28 | png_infop info_ptr = png_create_info_struct(png_ptr); 29 | if (!info_ptr) { 30 | png_destroy_read_struct(&png_ptr, nullptr, nullptr); 31 | return std::nullopt; 32 | } 33 | 34 | if (setjmp(png_jmpbuf(png_ptr))) { 35 | png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); 36 | return std::nullopt; 37 | } 38 | 39 | png_set_sig_bytes(png_ptr, 8); 40 | png_init_io(png_ptr, file); 41 | png_read_info(png_ptr, info_ptr); 42 | 43 | size_t width = png_get_image_width(png_ptr, info_ptr); 44 | size_t height = png_get_image_height(png_ptr, info_ptr); 45 | 46 | auto color_type = png_get_color_type(png_ptr, info_ptr); 47 | auto bit_depth = png_get_bit_depth(png_ptr, info_ptr); 48 | 49 | // Expand paletted and grayscale images to RGB, transform palettes to 8 bit per channel 50 | if (color_type & PNG_COLOR_TYPE_PALETTE) 51 | png_set_palette_to_rgb(png_ptr); 52 | if ((color_type & PNG_COLOR_TYPE_GRAY) || (color_type & PNG_COLOR_TYPE_GRAY_ALPHA)) 53 | png_set_gray_to_rgb(png_ptr); 54 | if (bit_depth == 16) 55 | png_set_strip_16(png_ptr); 56 | 57 | size_t channel_count = color_type & PNG_COLOR_MASK_ALPHA ? 4 : 3; 58 | 59 | Image image(width, height, channel_count); 60 | auto row_bytes = std::make_unique(width * channel_count); 61 | for (size_t y = 0; y < height; y++) { 62 | png_read_row(png_ptr, row_bytes.get(), nullptr); 63 | for (size_t x = 0; x < width; x++) { 64 | for (size_t i = 0; i < channel_count; ++i) { 65 | image.channel(i)[y * width + x] = 66 | Image::word_to_component<8>(row_bytes[x * channel_count + i]); 67 | } 68 | } 69 | } 70 | 71 | png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); 72 | return std::make_optional(std::move(image)); 73 | } 74 | 75 | bool save(const Image& image, const std::string_view& path) { 76 | std::string path_string(path); 77 | auto file = fopen(path_string.c_str(), "wb"); 78 | if (!file) 79 | return false; 80 | std::unique_ptr file_ptr(file, fclose); 81 | 82 | png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); 83 | if (!png_ptr) 84 | return false; 85 | 86 | png_infop info_ptr = png_create_info_struct(png_ptr); 87 | if (!info_ptr) { 88 | png_destroy_read_struct(&png_ptr, nullptr, nullptr); 89 | return false; 90 | } 91 | 92 | if (setjmp(png_jmpbuf(png_ptr))) { 93 | png_destroy_write_struct(&png_ptr, &info_ptr); 94 | return false; 95 | } 96 | 97 | auto channel_count = proto::clamp(image.channel_count(), 3, 4); 98 | auto row_bytes = std::make_unique(image.width() * channel_count); 99 | png_init_io(png_ptr, file); 100 | 101 | png_set_IHDR( 102 | png_ptr, info_ptr, image.width(), image.height(), 8, 103 | channel_count == 3 ? PNG_COLOR_TYPE_RGB : PNG_COLOR_TYPE_RGBA, 104 | PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); 105 | 106 | png_write_info(png_ptr, info_ptr); 107 | 108 | for (size_t y = 0; y < image.height(); y++) { 109 | std::fill(row_bytes.get(), row_bytes.get() + image.width() * channel_count, 0); 110 | for (size_t x = 0; x < image.width(); x++) { 111 | for (size_t i = 0, n = std::min(image.channel_count(), channel_count); i < n; ++i) { 112 | row_bytes[x * channel_count + i] = 113 | Image::component_to_word<8>(image.channel(i)[y * image.width() + x]); 114 | } 115 | } 116 | png_write_row(png_ptr, row_bytes.get()); 117 | } 118 | 119 | png_write_end(png_ptr, info_ptr); 120 | png_destroy_write_struct(&png_ptr, &info_ptr); 121 | return true; 122 | } 123 | #else 124 | std::optional load(const std::string_view&) { return std::nullopt; } 125 | bool save(const Image&, const std::string_view&) { return false; } 126 | #endif // SOL_ENABLE_PNG 127 | 128 | } // namespace sol::png 129 | -------------------------------------------------------------------------------- /src/formats/png.h: -------------------------------------------------------------------------------- 1 | #ifndef SOL_FORMATS_PNG_H 2 | #define SOL_FORMATS_PNG_H 3 | 4 | #include 5 | 6 | #include "sol/image.h" 7 | 8 | namespace sol::png { 9 | 10 | std::optional load(const std::string_view&); 11 | bool save(const Image&, const std::string_view&); 12 | 13 | } // namespace sol::png 14 | 15 | #endif 16 | -------------------------------------------------------------------------------- /src/formats/tiff.cpp: -------------------------------------------------------------------------------- 1 | #ifdef SOL_ENABLE_TIFF 2 | #include 3 | #endif 4 | 5 | #include "formats/tiff.h" 6 | 7 | namespace sol::tiff { 8 | 9 | #ifdef SOL_ENABLE_TIFF 10 | static void error_handler(const char*, const char*, va_list) { 11 | throw std::runtime_error("Cannot load/save TIFF image file"); 12 | } 13 | 14 | std::optional load(const std::string_view& path) { 15 | std::string path_string(path); 16 | 17 | TIFFSetErrorHandler(error_handler); 18 | TIFFSetWarningHandler(error_handler); 19 | try { 20 | auto tiff = TIFFOpen(path_string.c_str(), "r"); 21 | if (!tiff) 22 | return std::nullopt; 23 | std::unique_ptr tiff_ptr(tiff, TIFFClose); 24 | 25 | uint32_t width, height; 26 | TIFFGetField(tiff, TIFFTAG_IMAGEWIDTH, &width); 27 | TIFFGetField(tiff, TIFFTAG_IMAGELENGTH, &height); 28 | 29 | auto pixels = std::make_unique(width * height); 30 | if (!TIFFReadRGBAImageOriented(tiff, width, height, pixels.get(), ORIENTATION_TOPLEFT)) 31 | return std::nullopt; 32 | 33 | Image image(width, height, 4); 34 | for (size_t y = 0; y < height; y++) { 35 | for (size_t x = 0; x < width; x++) { 36 | auto pixel = pixels[y * width + x]; 37 | image.channel(0)[y * width + x] = Image::word_to_component<8>(pixel & 0xFF); 38 | image.channel(1)[y * width + x] = Image::word_to_component<8>((pixel >> 8) & 0xFF); 39 | image.channel(2)[y * width + x] = Image::word_to_component<8>((pixel >> 16) & 0xFF); 40 | image.channel(3)[y * width + x] = Image::word_to_component<8>((pixel >> 24) & 0xFF); 41 | } 42 | } 43 | return std::make_optional(std::move(image)); 44 | } catch (std::exception& e) { 45 | return std::nullopt; 46 | } 47 | } 48 | 49 | bool save(const Image& image, const std::string_view& path) { 50 | std::string path_string(path); 51 | 52 | TIFFSetErrorHandler(error_handler); 53 | TIFFSetWarningHandler(error_handler); 54 | 55 | try { 56 | auto tiff = TIFFOpen(path_string.c_str(), "w"); 57 | if (!tiff) 58 | return false; 59 | std::unique_ptr tiff_ptr(tiff, TIFFClose); 60 | 61 | uint32_t width = image.width(), height = image.height(); 62 | size_t channel_count = std::min(image.channel_count(), size_t{3}); 63 | TIFFSetField(tiff, TIFFTAG_IMAGEWIDTH, width); 64 | TIFFSetField(tiff, TIFFTAG_IMAGELENGTH, height); 65 | TIFFSetField(tiff, TIFFTAG_BITSPERSAMPLE, 32); 66 | TIFFSetField(tiff, TIFFTAG_SAMPLESPERPIXEL, channel_count); 67 | TIFFSetField(tiff, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_IEEEFP); 68 | TIFFSetField(tiff, TIFFTAG_ORIENTATION, ORIENTATION_TOPLEFT); 69 | TIFFSetField(tiff, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG); 70 | TIFFSetField(tiff, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB); 71 | TIFFSetField(tiff, TIFFTAG_FILLORDER, FILLORDER_MSB2LSB); 72 | TIFFSetField(tiff, TIFFTAG_ROWSPERSTRIP, TIFFDefaultStripSize(tiff, static_cast(-1))); 73 | TIFFSetField(tiff, TIFFTAG_COMPRESSION, COMPRESSION_LZW); 74 | TIFFSetField(tiff, TIFFTAG_PREDICTOR, 1); 75 | 76 | auto row = std::make_unique(channel_count * width); 77 | for (size_t y = 0; y < height; y++) { 78 | for (size_t x = 0; x < width; x++) { 79 | for (size_t i = 0; i < channel_count; ++i) 80 | row[x * channel_count + i] = image.channel(i)[y * width + x]; 81 | } 82 | TIFFWriteScanline(tiff, row.get(), y, 0); 83 | } 84 | return true; 85 | } catch (std::exception& e) { 86 | return false; 87 | } 88 | } 89 | #else 90 | std::optional load(const std::string_view&) { return std::nullopt; } 91 | bool save(const Image&, const std::string_view&) { return false; } 92 | #endif // SOL_ENABLE_TIFF 93 | 94 | } // namespace sol::tiff 95 | -------------------------------------------------------------------------------- /src/formats/tiff.h: -------------------------------------------------------------------------------- 1 | #ifndef SOL_FORMATS_TIFF_H 2 | #define SOL_FORMATS_TIFF_H 3 | 4 | #include 5 | 6 | #include "sol/image.h" 7 | 8 | namespace sol::tiff { 9 | 10 | std::optional load(const std::string_view&); 11 | bool save(const Image&, const std::string_view&); 12 | 13 | } // namespace sol::tiff 14 | 15 | #endif 16 | -------------------------------------------------------------------------------- /src/image.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "sol/image.h" 5 | 6 | #include "formats/png.h" 7 | #include "formats/jpeg.h" 8 | #include "formats/tiff.h" 9 | #include "formats/exr.h" 10 | 11 | namespace sol { 12 | 13 | void Image::clear(float value) { 14 | for (auto& channel : channels_) 15 | std::fill(channel.get(), channel.get() + width_ * height_, value); 16 | } 17 | 18 | void Image::scale(float value) { 19 | for (auto& channel : channels_) { 20 | std::transform(channel.get(), channel.get() + width_ * height_, channel.get(), 21 | [&] (float x) { return x * value; }); 22 | } 23 | } 24 | 25 | bool Image::save(const std::string_view& path, Format format) const { 26 | if (format == Format::Auto) 27 | format = Format::Exr; 28 | switch (format) { 29 | case Format::Png: return png ::save(*this, path); 30 | case Format::Jpeg: return jpeg::save(*this, path); 31 | case Format::Tiff: return tiff::save(*this, path); 32 | case Format::Exr: return exr ::save(*this, path); 33 | default: 34 | assert(false); 35 | return false; 36 | } 37 | } 38 | 39 | std::optional Image::load(const std::string_view& path, Format format) { 40 | switch (format) { 41 | case Format::Png: return png ::load(path); 42 | case Format::Jpeg: return jpeg::load(path); 43 | case Format::Tiff: return tiff::load(path); 44 | case Format::Exr: return exr ::load(path); 45 | default: 46 | if (auto image = png ::load(path)) return image; 47 | if (auto image = jpeg::load(path)) return image; 48 | if (auto image = tiff::load(path)) return image; 49 | if (auto image = exr ::load(path)) return image; 50 | return std::nullopt; 51 | } 52 | } 53 | 54 | } // namespace sol 55 | -------------------------------------------------------------------------------- /src/lights.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "sol/lights.h" 4 | #include "sol/textures.h" 5 | #include "sol/samplers.h" 6 | 7 | namespace sol { 8 | 9 | // Point Light --------------------------------------------------------------------- 10 | 11 | PointLight::PointLight(const proto::Vec3f& pos, const Color& intensity) 12 | : Light(Tag::PointLight), pos_(pos), intensity_(intensity) 13 | {} 14 | 15 | std::optional PointLight::sample_area(Sampler&, const proto::Vec3f&) const { 16 | return std::make_optional(LightAreaSample { 17 | .pos = pos_, 18 | .intensity = intensity_, 19 | .pdf_from = 1.0f, 20 | .pdf_area = 1.0f, 21 | .pdf_dir = proto::uniform_sphere_pdf(), 22 | .cos = 1.0f 23 | }); 24 | } 25 | 26 | std::optional PointLight::sample_emission(Sampler& sampler) const { 27 | auto [dir, pdf_dir] = proto::sample_uniform_sphere(sampler(), sampler()); 28 | return std::make_optional(LightEmissionSample { 29 | .pos = pos_, 30 | .dir = dir, 31 | .intensity = intensity_, 32 | .pdf_area = 1.0f, 33 | .pdf_dir = pdf_dir, 34 | .cos = 1.0f 35 | }); 36 | } 37 | 38 | EmissionValue PointLight::emission(const proto::Vec3f&, const proto::Vec3f&, const proto::Vec2f&) const { 39 | return EmissionValue { Color::black(), 1.0f, 1.0f, 1.0f }; 40 | } 41 | 42 | float PointLight::pdf_from(const proto::Vec3f&, const proto::Vec2f&) const { 43 | return 1.0f; 44 | } 45 | 46 | proto::fnv::Hasher& PointLight::hash(proto::fnv::Hasher& hasher) const { 47 | return pos_.hash(intensity_.hash(hasher.combine(tag))); 48 | } 49 | 50 | bool PointLight::equals(const Light& other) const { 51 | return 52 | other.tag == tag && 53 | static_cast(other).pos_ == pos_ && 54 | static_cast(other).intensity_ == intensity_; 55 | } 56 | 57 | // Area Light ---------------------------------------------------------------------- 58 | 59 | template 60 | AreaLight::AreaLight(const Shape& shape, const ColorTexture& intensity) 61 | : Light(infer_tag(shape)), shape_(shape), intensity_(intensity) 62 | {} 63 | 64 | template 65 | std::optional AreaLight::sample_area(Sampler& sampler, const proto::Vec3f& from) const { 66 | auto sample = shape_.sample(sampler, from); 67 | auto dir = proto::normalize(from - sample.pos); 68 | auto cos = proto::positive_dot(dir, sample.normal); 69 | return cos > 0 ? std::make_optional(LightAreaSample { 70 | .pos = sample.pos, 71 | .intensity = intensity_.sample_color(sample.surf_coords), 72 | .pdf_from = sample.pdf_from, 73 | .pdf_area = sample.pdf, 74 | .pdf_dir = proto::cosine_hemisphere_pdf(cos), 75 | .cos = cos 76 | }) : std::nullopt; 77 | } 78 | 79 | template 80 | std::optional AreaLight::sample_emission(Sampler& sampler) const { 81 | auto sample = shape_.sample(sampler); 82 | auto [dir, pdf_dir] = proto::sample_cosine_hemisphere(sampler(), sampler()); 83 | auto cos = dir[2]; 84 | return cos > 0 ? std::make_optional(LightEmissionSample { 85 | .pos = sample.pos, 86 | .dir = proto::ortho_basis(sample.normal) * dir, 87 | .intensity = intensity_.sample_color(sample.surf_coords), 88 | .pdf_area = sample.pdf, 89 | .pdf_dir = pdf_dir, 90 | .cos = cos 91 | }) : std::nullopt; 92 | } 93 | 94 | template 95 | EmissionValue AreaLight::emission(const proto::Vec3f& from, const proto::Vec3f& dir, const proto::Vec2f& uv) const { 96 | auto sample = shape_.sample_at(uv, from); 97 | auto cos = proto::dot(dir, sample.normal); 98 | if (cos <= 0) 99 | return EmissionValue { Color::black(), 1.0f, 1.0f, 1.0f }; 100 | return EmissionValue { 101 | .intensity = intensity_.sample_color(uv), 102 | .pdf_from = sample.pdf_from, 103 | .pdf_area = sample.pdf, 104 | .pdf_dir = proto::cosine_hemisphere_pdf(cos) 105 | }; 106 | } 107 | 108 | template 109 | float AreaLight::pdf_from(const proto::Vec3f& from, const proto::Vec2f& uv) const { 110 | return shape_.sample_at(uv, from).pdf_from; 111 | } 112 | 113 | template 114 | proto::fnv::Hasher& AreaLight::hash(proto::fnv::Hasher& hasher) const { 115 | return shape_.hash(hasher).combine(&intensity_); 116 | } 117 | 118 | template 119 | bool AreaLight::equals(const Light& other) const { 120 | return 121 | other.tag == tag && 122 | static_cast(other).shape_ == shape_ && 123 | &static_cast(other).intensity_ == &intensity_; 124 | } 125 | 126 | template class AreaLight; 127 | template class AreaLight; 128 | 129 | } // namespace sol 130 | -------------------------------------------------------------------------------- /src/render_job.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "sol/render_job.h" 4 | #include "sol/scene.h" 5 | #include "sol/image.h" 6 | #include "sol/renderer.h" 7 | 8 | namespace sol { 9 | 10 | RenderJob::RenderJob(const Renderer& renderer, Image& output) 11 | : renderer(renderer), output(output) 12 | {} 13 | 14 | RenderJob::RenderJob(RenderJob&& other) 15 | : sample_count(other.sample_count) 16 | , samples_per_frame(other.samples_per_frame) 17 | , renderer(other.renderer) 18 | , output(other.output) 19 | {} 20 | 21 | RenderJob::~RenderJob() = default; 22 | 23 | void RenderJob::start(std::function&& frame_end) { 24 | is_done_ = false; 25 | render_thread_ = std::thread([&] { 26 | for (size_t i = 0; sample_count == 0 || i < sample_count; i += samples_per_frame) { 27 | size_t j = i + samples_per_frame; 28 | if (i + j > sample_count && sample_count != 0) 29 | j = sample_count - i; 30 | 31 | renderer.render(output, i, j); 32 | if ((frame_end && !frame_end(*this)) || is_done_) 33 | break; 34 | } 35 | 36 | std::unique_lock lock(mutex_); 37 | is_done_ = true; 38 | done_cond_.notify_one(); 39 | }); 40 | } 41 | 42 | bool RenderJob::wait(size_t timeout_ms) { 43 | if (is_done_ || !render_thread_.joinable()) 44 | return true; 45 | std::unique_lock lock(mutex_); 46 | if (timeout_ms != 0) { 47 | using namespace std::chrono_literals; 48 | auto target = std::chrono::system_clock::now() + timeout_ms * 1ms; 49 | if (!done_cond_.wait_until(lock, target, [&] { return is_done_; })) 50 | return false; 51 | } else 52 | done_cond_.wait(lock, [&] { return is_done_; }); 53 | render_thread_.join(); 54 | return true; 55 | } 56 | 57 | void RenderJob::cancel() { 58 | is_done_ = true; 59 | } 60 | 61 | } // namespace sol 62 | -------------------------------------------------------------------------------- /src/scene.cpp: -------------------------------------------------------------------------------- 1 | #include "sol/scene.h" 2 | #include "sol/bsdfs.h" 3 | #include "sol/lights.h" 4 | #include "sol/cameras.h" 5 | #include "sol/textures.h" 6 | #include "sol/geometry.h" 7 | 8 | namespace sol { 9 | 10 | Scene::Scene() = default; 11 | Scene::~Scene() = default; 12 | Scene::Scene(Scene&&) = default; 13 | 14 | } // namespace sol 15 | -------------------------------------------------------------------------------- /src/scene_loader.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "scene_loader.h" 5 | #include "formats/obj.h" 6 | 7 | #include "sol/cameras.h" 8 | #include "sol/bsdfs.h" 9 | #include "sol/lights.h" 10 | #include "sol/image.h" 11 | #include "sol/geometry.h" 12 | 13 | namespace sol { 14 | 15 | static proto::Vec3f parse_vec3(toml::node_view node, const proto::Vec3f& default_val) { 16 | if (auto array = node.as_array(); array) { 17 | return proto::Vec3f( 18 | (*array)[0].value_or(default_val[0]), 19 | (*array)[1].value_or(default_val[1]), 20 | (*array)[2].value_or(default_val[2])); 21 | } 22 | return default_val; 23 | } 24 | 25 | SceneLoader::SceneLoader(Scene& scene, const Scene::Defaults& defaults, std::ostream* err_out) 26 | : scene_(scene), defaults_(defaults), err_out_(err_out) 27 | {} 28 | 29 | SceneLoader::~SceneLoader() = default; 30 | 31 | bool SceneLoader::load(const std::string& file_name) { 32 | try { 33 | load_and_throw_on_error(file_name); 34 | return true; 35 | } catch (toml::parse_error& error) { 36 | // Use the same error message syntax for TOML++ errors and the loaders' error messages 37 | if (err_out_) (*err_out_) << SourceError::from_toml(error).what(); 38 | } catch (std::exception& e) { 39 | if (err_out_) (*err_out_) << e.what(); 40 | } 41 | return false; 42 | } 43 | 44 | void SceneLoader::load_and_throw_on_error(const std::string& file_name) { 45 | std::ifstream is(file_name); 46 | if (!is) 47 | throw std::runtime_error("Cannot open scene file '" + file_name + "'"); 48 | 49 | std::error_code err_code; 50 | auto base_dir = std::filesystem::absolute(file_name, err_code).parent_path().string(); 51 | 52 | auto table = toml::parse(is, file_name); 53 | if (auto camera = table["camera"].as_table()) 54 | scene_.camera = create_camera(*camera); 55 | if (auto geoms = table["objects"].as_array()) { 56 | for (auto& geom : *geoms) { 57 | if (auto table = geom.as_table()) 58 | create_geom(*table, base_dir); 59 | } 60 | } 61 | auto root_name = table["root"].value_or(""); 62 | if (!geoms_.contains(root_name)) 63 | throw std::runtime_error("Root geometry named '" + root_name + "' cannot be found"); 64 | scene_.root = std::move(geoms_[root_name]); 65 | } 66 | 67 | const Image* SceneLoader::load_image(const std::string& file_name) { 68 | std::error_code err_code; 69 | auto full_name = std::filesystem::absolute(file_name, err_code).string(); 70 | if (auto it = images_.find(full_name); it != images_.end()) 71 | return it->second; 72 | if (auto image = Image::load(full_name)) { 73 | auto image_ptr = scene_.images.emplace_back(new Image(std::move(*image))).get(); 74 | images_.emplace(full_name, image_ptr); 75 | return image_ptr; 76 | } 77 | return nullptr; 78 | } 79 | 80 | void SceneLoader::insert_geom(const std::string& name, std::unique_ptr&& geom) { 81 | if (!geoms_.emplace(name, std::move(geom)).second) 82 | throw std::runtime_error("Duplicate geometry found with name '" + name + "'"); 83 | } 84 | 85 | std::unique_ptr SceneLoader::create_camera(const toml::table& table) { 86 | auto type = table["type"].value_or(""); 87 | if (type == "perspective") { 88 | auto eye = parse_vec3(table["eye"], defaults_.eye_pos); 89 | auto dir = parse_vec3(table["dir"], defaults_.dir_vector); 90 | auto up = parse_vec3(table["up"], defaults_.up_vector); 91 | auto fov = table["fov"].value_or(defaults_.fov); 92 | auto ratio = table["aspect"].value_or(defaults_.aspect_ratio); 93 | return std::make_unique(eye, dir, up, fov, ratio); 94 | } 95 | throw SourceError::from_toml(table.source(), "Unknown camera type '" + type + "'"); 96 | } 97 | 98 | void SceneLoader::create_geom(const toml::table& table, const std::string& base_dir) { 99 | auto name = table["name"].value_or(""); 100 | auto type = table["type"].value_or(""); 101 | if (type == "import") { 102 | auto file = table["file"].value_or(""); 103 | if (file.ends_with(".obj")) 104 | return insert_geom(name, obj::load(*this, base_dir + "/" + file)); 105 | throw SourceError::from_toml(table.source(), "Unknown file format for '" + file + "'"); 106 | } 107 | throw SourceError::from_toml(table.source(), "Unknown node type '" + type + "'"); 108 | } 109 | 110 | std::optional Scene::load(const std::string& file_name, const Defaults& defaults, std::ostream* err_out) { 111 | Scene scene; 112 | SceneLoader loader(scene, defaults, err_out); 113 | return loader.load(file_name) ? std::make_optional(std::move(scene)) : std::nullopt; 114 | } 115 | 116 | } // namespace sol 117 | -------------------------------------------------------------------------------- /src/scene_loader.h: -------------------------------------------------------------------------------- 1 | #ifndef SOL_SCENE_LOADER_H 2 | #define SOL_SCENE_LOADER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | #include 13 | 14 | #include "sol/scene.h" 15 | 16 | namespace sol { 17 | 18 | /// Exception used internally to represent an error in a source file 19 | struct SourceError : public std::runtime_error { 20 | struct Pos { size_t row, col; }; 21 | 22 | SourceError(const std::string_view& file_name, const Pos& pos, const std::string_view& message) 23 | : std::runtime_error(format(file_name, pos, message)) 24 | {} 25 | 26 | static std::string format(const std::string_view& file_name, const Pos& pos, const std::string_view& message) { 27 | return 28 | std::string(message) + " (in " + 29 | std::string(file_name) + ":" + 30 | std::to_string(pos.row) + ":" + 31 | std::to_string(pos.col) + ")"; 32 | } 33 | 34 | static SourceError from_toml(const toml::source_region& source, const std::string_view& message) { 35 | return SourceError(*source.path, { source.begin.line, source.begin.column }, message); 36 | } 37 | 38 | static SourceError from_toml(const toml::parse_error& error) { 39 | return from_toml(error.source(), error.description()); 40 | } 41 | }; 42 | 43 | /// Internal object used by scene file loaders. 44 | /// This object performs hash-consing of scene objects, 45 | /// so that the same objects are re-used whenever possible. 46 | struct SceneLoader { 47 | public: 48 | SceneLoader(Scene&, const Scene::Defaults&, std::ostream*); 49 | ~SceneLoader(); 50 | 51 | // Loads the given file into the scene. 52 | bool load(const std::string& file_name); 53 | 54 | // Loads the given file into the scene. 55 | // May throw an exception, and the error output stream is not written to. 56 | void load_and_throw_on_error(const std::string& file_name); 57 | 58 | // Loads an image or returns an already loaded one. 59 | const Image* load_image(const std::string& file_name); 60 | 61 | // Creates a new BSDF or returns an existing one. 62 | template 63 | const Bsdf* get_or_insert_bsdf(Args&&... args) { 64 | return get_or_insert(bsdfs_, scene_.bsdfs, std::forward(args)...); 65 | } 66 | 67 | // Creates a new texture or returns an existing one. 68 | template 69 | const Texture* get_or_insert_texture(Args&&... args) { 70 | return get_or_insert(textures_, scene_.textures, std::forward(args)...); 71 | } 72 | 73 | // Creates a new light or returns an existing one. 74 | template 75 | const Light* get_or_insert_light(Args&&... args) { 76 | return get_or_insert(lights_, scene_.lights, std::forward(args)...); 77 | } 78 | 79 | void insert_geom(const std::string&, std::unique_ptr&&); 80 | 81 | private: 82 | std::unique_ptr create_camera(const toml::table&); 83 | void create_geom(const toml::table&, const std::string&); 84 | 85 | template 86 | static const U* get_or_insert(Set& set, Container& container, Args&&... args) { 87 | T t(std::forward(args)...); 88 | if (auto it = set.find(&t); it != set.end()) 89 | return *it; 90 | auto new_t = new T(std::move(t)); 91 | container.emplace_back(new_t); 92 | set.insert(new_t); 93 | return new_t; 94 | } 95 | 96 | struct Hash { 97 | template 98 | size_t operator () (const T* t) const { 99 | proto::fnv::Hasher hasher; 100 | return t->hash(hasher); 101 | } 102 | }; 103 | 104 | struct Compare { 105 | template 106 | bool operator () (const T* left, const T* right) const { 107 | return left->equals(*right); 108 | } 109 | }; 110 | 111 | Scene& scene_; 112 | Scene::Defaults defaults_; 113 | 114 | std::unordered_set bsdfs_; 115 | std::unordered_set lights_; 116 | std::unordered_set textures_; 117 | 118 | std::unordered_map images_; 119 | std::unordered_map> geoms_; 120 | 121 | std::ostream* err_out_; 122 | }; 123 | 124 | } // namespace sol 125 | 126 | #endif 127 | -------------------------------------------------------------------------------- /src/shapes.cpp: -------------------------------------------------------------------------------- 1 | #include "sol/shapes.h" 2 | #include "sol/samplers.h" 3 | 4 | namespace sol { 5 | 6 | template 7 | ShapeSample SamplableShape::sample(Sampler& sampler) const { 8 | auto uv = proto::Vec2f(sampler(), sampler()); 9 | return static_cast(this)->sample_at(uv); 10 | } 11 | 12 | template 13 | DirectionalShapeSample SamplableShape::sample(Sampler& sampler, const proto::Vec3f& from) const { 14 | auto uv = proto::Vec2f(sampler(), sampler()); 15 | return static_cast(this)->sample_at(uv, from); 16 | } 17 | 18 | // Uniform Triangle ---------------------------------------------------------------- 19 | 20 | ShapeSample UniformTriangle::sample_at(proto::Vec2f uv) const { 21 | if (uv[0] + uv[1] > 1) 22 | uv = proto::Vec2f(1) - uv; 23 | auto pos = proto::lerp(shape.v0, shape.v1, shape.v2, uv[0], uv[1]); 24 | return ShapeSample { uv, pos, normal, inv_area }; 25 | } 26 | 27 | DirectionalShapeSample UniformTriangle::sample_at(const proto::Vec2f& uv, const proto::Vec3f&) const { 28 | return DirectionalShapeSample(sample_at(uv)); 29 | } 30 | 31 | // Uniform Sphere ------------------------------------------------------------------ 32 | 33 | ShapeSample UniformSphere::sample_at(const proto::Vec2f& uv) const { 34 | auto dir = std::get<0>(proto::sample_uniform_sphere(uv[0], uv[1])); 35 | auto pos = shape.center + dir * shape.radius; 36 | return ShapeSample { uv, pos, dir, inv_area }; 37 | } 38 | 39 | DirectionalShapeSample UniformSphere::sample_at(const proto::Vec2f& uv, const proto::Vec3f&) const { 40 | return DirectionalShapeSample(sample_at(uv)); 41 | } 42 | 43 | template struct SamplableShape; 44 | template struct SamplableShape; 45 | 46 | } // namespace sol 47 | -------------------------------------------------------------------------------- /src/textures.cpp: -------------------------------------------------------------------------------- 1 | #include "sol/textures.h" 2 | 3 | namespace sol { 4 | 5 | proto::Vec2f BorderMode::Clamp::operator () (const proto::Vec2f& uv) const { 6 | return proto::clamp(uv, proto::Vec2f(0), proto::Vec2f(1)); 7 | } 8 | 9 | proto::Vec2f BorderMode::Repeat::operator () (const proto::Vec2f& uv) const { 10 | auto u = uv[0] - std::floor(uv[0]); 11 | auto v = uv[1] - std::floor(uv[1]); 12 | return proto::Vec2f(u, v); 13 | } 14 | 15 | proto::Vec2f BorderMode::Mirror::operator () (const proto::Vec2f& uv) const { 16 | auto u = std::fmod(uv[0], 2.0f); 17 | auto v = std::fmod(uv[0], 2.0f); 18 | u = u > 1.0f ? 2.0f - u : u; 19 | v = v > 1.0f ? 2.0f - v : v; 20 | return proto::Vec2f(u, v); 21 | } 22 | 23 | template 24 | Color ImageFilter::Nearest::operator () (const proto::Vec2f& uv, size_t width, size_t height, F&& f) const { 25 | size_t x = proto::clamp(uv[0] * width, 0, width - 1); 26 | size_t y = proto::clamp(uv[1] * height, 0, height - 1); 27 | return f(x, y); 28 | } 29 | 30 | template 31 | Color ImageFilter::Bilinear::operator () (const proto::Vec2f& uv, size_t width, size_t height, F&& f) const { 32 | auto i = uv[0] * (width - 1); 33 | auto j = uv[1] * (height - 1); 34 | auto u = i - std::floor(i); 35 | auto v = j - std::floor(j); 36 | auto x0 = proto::clamp(i, 0, width - 1); 37 | auto y0 = proto::clamp(j, 0, height - 1); 38 | auto x1 = proto::clamp(x0 + 1, 0, width - 1); 39 | auto y1 = proto::clamp(y0 + 1, 0, height - 1); 40 | auto p00 = f(x0, y0); 41 | auto p10 = f(x1, y0); 42 | auto p01 = f(x0, y1); 43 | auto p11 = f(x1, y1); 44 | return lerp(lerp(p00, p10, u), lerp(p01, p11, u), v); 45 | } 46 | 47 | template 48 | Color ImageTexture::sample_color(const proto::Vec2f& uv) const { 49 | auto fixed_uv = border_mode_(uv); 50 | return filter_(fixed_uv, image_.width(), image_.height(), 51 | [&] (size_t i, size_t j) { return image_.rgb_at(i, j); }); 52 | } 53 | 54 | template class ImageTexture; 55 | template class ImageTexture; 56 | template class ImageTexture; 57 | template class ImageTexture; 58 | template class ImageTexture; 59 | template class ImageTexture; 60 | 61 | } // namespace sol 62 | -------------------------------------------------------------------------------- /src/triangle_mesh.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #if defined(SOL_ENABLE_TBB) 9 | #include 10 | #include 11 | #elif defined(SOL_ENABLE_OMP) 12 | #include 13 | #include 14 | #else 15 | #include 16 | #include 17 | #endif 18 | #include 19 | #include 20 | #include 21 | 22 | #include "sol/triangle_mesh.h" 23 | #include "sol/lights.h" 24 | 25 | namespace sol { 26 | 27 | using Bvh = bvh::Bvh; 28 | struct TriangleMesh::BvhData { Bvh bvh; }; 29 | 30 | #if defined(SOL_ENABLE_TBB) 31 | template 32 | using TopDownScheduler = bvh::tbb::ParallelTopDownScheduler; 33 | using Executor = par::tbb::Executor; 34 | #elif defined(SOL_ENABLE_OMP) 35 | template 36 | using TopDownScheduler = bvh::omp::ParallelTopDownScheduler; 37 | using Executor = par::omp::StaticExecutor; 38 | #else 39 | template 40 | using TopDownScheduler = bvh::SequentialTopDownScheduler; 41 | using Executor = par::SequentialExecutor; 42 | #endif 43 | 44 | TriangleMesh::TriangleMesh( 45 | std::vector&& indices, 46 | std::vector&& vertices, 47 | std::vector&& normals, 48 | std::vector&& tex_coords, 49 | std::vector&& bsdfs, 50 | std::unordered_map&& lights) 51 | : indices_(std::move(indices)) 52 | , normals_(std::move(normals)) 53 | , tex_coords_(std::move(tex_coords)) 54 | , bsdfs_(std::move(bsdfs)) 55 | , lights_(std::move(lights)) 56 | { 57 | Executor executor; 58 | bvh_data_ = build_bvh(executor, vertices); 59 | triangles_ = build_triangles(executor, vertices); 60 | } 61 | 62 | TriangleMesh::~TriangleMesh() = default; 63 | 64 | std::optional TriangleMesh::intersect_closest(proto::Rayf& ray) const { 65 | auto hit_info = bvh::SingleRayTraverser::traverse(ray, bvh_data_->bvh, 66 | [&] (proto::Rayf& ray, const Bvh::Node& leaf) { 67 | std::optional> hit_info; 68 | for (size_t i = leaf.first_index, n = i + leaf.prim_count; i < n; ++i) { 69 | if (auto uv = triangles_[i].intersect(ray)) 70 | hit_info = std::make_optional(std::tuple { i, uv->first, uv->second }); 71 | } 72 | return hit_info; 73 | }); 74 | 75 | if (!hit_info) 76 | return std::nullopt; 77 | 78 | auto [permuted_index, u, v] = *hit_info; 79 | auto face_normal = triangles_[permuted_index].normal(); 80 | auto triangle_index = bvh_data_->bvh.prim_indices[permuted_index]; 81 | auto [i0, i1, i2] = triangle_indices(triangle_index); 82 | 83 | auto normal = proto::lerp(normals_[i0], normals_[i1], normals_[i2], u, v); 84 | auto tex_coords = proto::lerp(tex_coords_[i0], tex_coords_[i1], tex_coords_[i2], u, v); 85 | 86 | // Flip normals based on the side of the triangle 87 | bool is_front_side = proto::dot(face_normal, ray.dir) < 0; 88 | if (!is_front_side) { 89 | face_normal = -face_normal; 90 | normal = -normal; 91 | } 92 | 93 | SurfaceInfo surf_info; 94 | surf_info.is_front_side = is_front_side; 95 | surf_info.point = ray.point_at(ray.tmax); 96 | surf_info.tex_coords = tex_coords; 97 | surf_info.surf_coords = proto::Vec2f(u, v); 98 | surf_info.face_normal = face_normal; 99 | surf_info.local = proto::ortho_basis(proto::normalize(normal)); 100 | 101 | const Light* light = nullptr; 102 | if (auto it = lights_.find(triangle_index); it != lights_.end()) 103 | light = it->second; 104 | return std::make_optional(Hit { surf_info, light, bsdfs_[triangle_index] }); 105 | } 106 | 107 | bool TriangleMesh::intersect_any(const proto::Rayf& init_ray) const { 108 | auto ray = init_ray; 109 | return bvh::SingleRayTraverser::traverse(ray, bvh_data_->bvh, 110 | [&] (proto::Rayf& ray, const Bvh::Node& leaf) { 111 | for (size_t i = leaf.first_index, n = i + leaf.prim_count; i < n; ++i) { 112 | if (triangles_[i].intersect(ray)) 113 | return true; 114 | } 115 | return false; 116 | }); 117 | } 118 | 119 | template 120 | std::unique_ptr TriangleMesh::build_bvh(Executor& executor, const std::vector& vertices) const { 121 | using Builder = bvh::SweepSahBuilder; 122 | 123 | TopDownScheduler top_down_scheduler; 124 | 125 | auto bboxes = std::make_unique(triangle_count()); 126 | auto centers = std::make_unique(triangle_count()); 127 | 128 | // Compute bounding boxes and centers in parallel 129 | auto global_bbox = par::transform_reduce( 130 | executor, par::range_1d(size_t{0}, triangle_count()), proto::BBoxf::empty(), 131 | [] (proto::BBoxf left, const proto::BBoxf& right) { return left.extend(right); }, 132 | [&] (size_t i) -> proto::BBoxf { 133 | auto triangle = proto::Trianglef( 134 | vertices[indices_[i * 3 + 0]], 135 | vertices[indices_[i * 3 + 1]], 136 | vertices[indices_[i * 3 + 2]]); 137 | auto bbox = triangle.bbox(); 138 | centers[i] = triangle.center(); 139 | return bboxes[i] = bbox; 140 | }); 141 | 142 | auto bvh = Builder::build(top_down_scheduler, executor, global_bbox, bboxes.get(), centers.get(), triangle_count()); 143 | bvh::TopologyModifier topo_modifier(bvh, bvh.parents(executor)); 144 | bvh::SequentialReinsertionOptimizer::optimize(topo_modifier); 145 | return std::make_unique(BvhData { std::move(bvh) }); 146 | } 147 | 148 | template 149 | std::vector TriangleMesh::build_triangles(Executor& executor, const std::vector& vertices) const { 150 | // Build a permuted array of triangles, so as to avoid indirections when intersecting the mesh with a ray. 151 | std::vector triangles(triangle_count()); 152 | par::for_each(executor, par::range_1d(size_t{0}, triangle_count()), [&] (size_t i) { 153 | auto j = bvh_data_->bvh.prim_indices[i]; 154 | triangles[i] = proto::PrecomputedTrianglef( 155 | vertices[indices_[j * 3 + 0]], 156 | vertices[indices_[j * 3 + 1]], 157 | vertices[indices_[j * 3 + 2]]); 158 | }); 159 | return triangles; 160 | } 161 | 162 | } // namespace sol 163 | -------------------------------------------------------------------------------- /test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_executable(driver driver.cpp) 2 | target_link_libraries(driver PUBLIC sol) 3 | set_target_properties(driver PROPERTIES 4 | CXX_STANDARD 20 5 | INTERPROCEDURAL_OPTIMIZATION_RELEASE ON) 6 | 7 | add_test(NAME driver_cornell_box COMMAND driver ${CMAKE_CURRENT_SOURCE_DIR}/data/cornell_box.toml) 8 | -------------------------------------------------------------------------------- /test/data/copyright.txt: -------------------------------------------------------------------------------- 1 | These are modified versions of the Cornell Box scenes from the 2 | "McGuire Computer Graphics Archive" (https://casual-effects.com/data/). 3 | Those are licensed under CC-BY 3.0, with the following (original) copyright text: 4 | 5 | # A set of eight Cornell Boxes in OBJ format. 6 | # Note that the real box is not a perfect cube, so 7 | # the faces are imperfect in this data set. 8 | # 9 | # Created by Guedis Cardenas and Morgan McGuire at Williams College, 2011 10 | # Attribution 3.0 Unported (CC BY 3.0) License 11 | # http://creativecommons.org/licenses/by/3.0/ 12 | # 13 | # http://graphics.cs.williams.edu/data 14 | # 15 | -------------------------------------------------------------------------------- /test/data/cornell_box.mtl: -------------------------------------------------------------------------------- 1 | # The original Cornell Box in OBJ format. 2 | # Note that the real box is not a perfect cube, so 3 | # the faces are imperfect in this data set. 4 | # 5 | # Created by Guedis Cardenas and Morgan McGuire at Williams College, 2011 6 | # Released into the Public Domain. 7 | # 8 | # http://graphics.cs.williams.edu/data 9 | # http://www.graphics.cornell.edu/online/box/data.html 10 | # 11 | 12 | newmtl leftWall 13 | Ns 10.0000 14 | Ni 1.5000 15 | illum 2 16 | Ka 0.63 0.065 0.05 # Red 17 | Kd 0.63 0.065 0.05 18 | Ks 0 0 0 19 | Ke 0 0 0 20 | 21 | 22 | newmtl rightWall 23 | Ns 10.0000 24 | Ni 1.5000 25 | illum 2 26 | Ka 0.14 0.45 0.091 # Green 27 | Kd 0.14 0.45 0.091 28 | Ks 0 0 0 29 | Ke 0 0 0 30 | 31 | 32 | newmtl floor 33 | Ns 10.0000 34 | Ni 1.0000 35 | illum 2 36 | Ka 0.725 0.71 0.68 # White 37 | Kd 0.725 0.71 0.68 38 | Ks 0 0 0 39 | Ke 0 0 0 40 | 41 | 42 | newmtl ceiling 43 | Ns 10.0000 44 | Ni 1.0000 45 | illum 2 46 | Ka 0.725 0.71 0.68 # White 47 | Kd 0.725 0.71 0.68 48 | Ks 0 0 0 49 | Ke 0 0 0 50 | 51 | 52 | newmtl backWall 53 | Ns 10.0000 54 | Ni 1.0000 55 | illum 2 56 | Ka 0.725 0.71 0.68 # White 57 | Kd 0.725 0.71 0.68 58 | Ks 0 0 0 59 | Ke 0 0 0 60 | 61 | 62 | newmtl shortBox 63 | Ns 10.0000 64 | Ni 1.0000 65 | illum 2 66 | Ka 0.725 0.71 0.68 # White 67 | Kd 0.725 0.71 0.68 68 | Ks 0 0 0 69 | Ke 0 0 0 70 | 71 | 72 | newmtl tallBox 73 | Ns 10.0000 74 | Ni 1.0000 75 | illum 2 76 | Ka 0.725 0.71 0.68 # White 77 | Kd 0.725 0.71 0.68 78 | Ks 0 0 0 79 | Ke 0 0 0 80 | 81 | newmtl light 82 | Ns 10.0000 83 | Ni 1.0000 84 | illum 2 85 | Ka 0.78 0.78 0.78 # White 86 | Kd 0.78 0.78 0.78 87 | Ks 0 0 0 88 | Ke 17 12 4 89 | -------------------------------------------------------------------------------- /test/data/cornell_box.obj: -------------------------------------------------------------------------------- 1 | # The original Cornell Box in OBJ format. 2 | # Note that the real box is not a perfect cube, so 3 | # the faces are imperfect in this data set. 4 | # 5 | # Created by Guedis Cardenas and Morgan McGuire at Williams College, 2011 6 | # Released into the Public Domain. 7 | # 8 | # http://graphics.cs.williams.edu/data 9 | # http://www.graphics.cornell.edu/online/box/data.html 10 | # 11 | 12 | mtllib cornell_box.mtl 13 | 14 | ## Object floor 15 | v -1.01 0.00 0.99 16 | v 1.00 0.00 0.99 17 | v 1.00 0.00 -1.04 18 | v -0.99 0.00 -1.04 19 | 20 | g floor 21 | usemtl floor 22 | f -4 -3 -2 -1 23 | 24 | ## Object ceiling 25 | v -1.02 1.99 0.99 26 | v -1.02 1.99 -1.04 27 | v 1.00 1.99 -1.04 28 | v 1.00 1.99 0.99 29 | 30 | g ceiling 31 | usemtl ceiling 32 | f -4 -3 -2 -1 33 | 34 | ## Object backwall 35 | v -0.99 0.00 -1.04 36 | v 1.00 0.00 -1.04 37 | v 1.00 1.99 -1.04 38 | v -1.02 1.99 -1.04 39 | 40 | g backWall 41 | usemtl backWall 42 | f -4 -3 -2 -1 43 | 44 | ## Object rightwall 45 | v 1.00 0.00 -1.04 46 | v 1.00 0.00 0.99 47 | v 1.00 1.99 0.99 48 | v 1.00 1.99 -1.04 49 | 50 | g rightWall 51 | usemtl rightWall 52 | f -4 -3 -2 -1 53 | 54 | ## Object leftWall 55 | v -1.01 0.00 0.99 56 | v -0.99 0.00 -1.04 57 | v -1.02 1.99 -1.04 58 | v -1.02 1.99 0.99 59 | 60 | g leftWall 61 | usemtl leftWall 62 | f -4 -3 -2 -1 63 | 64 | ## Object shortBox 65 | usemtl shortBox 66 | 67 | # Top Face 68 | v 0.53 0.60 0.75 69 | v 0.70 0.60 0.17 70 | v 0.13 0.60 0.00 71 | v -0.05 0.60 0.57 72 | f -4 -3 -2 -1 73 | 74 | # Left Face 75 | v -0.05 0.00 0.57 76 | v -0.05 0.60 0.57 77 | v 0.13 0.60 0.00 78 | v 0.13 0.00 0.00 79 | f -4 -3 -2 -1 80 | 81 | # Front Face 82 | v 0.53 0.00 0.75 83 | v 0.53 0.60 0.75 84 | v -0.05 0.60 0.57 85 | v -0.05 0.00 0.57 86 | f -4 -3 -2 -1 87 | 88 | # Right Face 89 | v 0.70 0.00 0.17 90 | v 0.70 0.60 0.17 91 | v 0.53 0.60 0.75 92 | v 0.53 0.00 0.75 93 | f -4 -3 -2 -1 94 | 95 | # Back Face 96 | v 0.13 0.00 0.00 97 | v 0.13 0.60 0.00 98 | v 0.70 0.60 0.17 99 | v 0.70 0.00 0.17 100 | f -4 -3 -2 -1 101 | 102 | # Bottom Face 103 | v 0.53 0.00 0.75 104 | v 0.70 0.00 0.17 105 | v 0.13 0.00 0.00 106 | v -0.05 0.00 0.57 107 | f -12 -11 -10 -9 108 | 109 | g shortBox 110 | usemtl shortBox 111 | 112 | ## Object tallBox 113 | usemtl tallBox 114 | 115 | # Top Face 116 | v -0.53 1.20 0.09 117 | v 0.04 1.20 -0.09 118 | v -0.14 1.20 -0.67 119 | v -0.71 1.20 -0.49 120 | f -4 -3 -2 -1 121 | 122 | # Left Face 123 | v -0.53 0.00 0.09 124 | v -0.53 1.20 0.09 125 | v -0.71 1.20 -0.49 126 | v -0.71 0.00 -0.49 127 | f -4 -3 -2 -1 128 | 129 | # Back Face 130 | v -0.71 0.00 -0.49 131 | v -0.71 1.20 -0.49 132 | v -0.14 1.20 -0.67 133 | v -0.14 0.00 -0.67 134 | f -4 -3 -2 -1 135 | 136 | # Right Face 137 | v -0.14 0.00 -0.67 138 | v -0.14 1.20 -0.67 139 | v 0.04 1.20 -0.09 140 | v 0.04 0.00 -0.09 141 | f -4 -3 -2 -1 142 | 143 | # Front Face 144 | v 0.04 0.00 -0.09 145 | v 0.04 1.20 -0.09 146 | v -0.53 1.20 0.09 147 | v -0.53 0.00 0.09 148 | f -4 -3 -2 -1 149 | 150 | # Bottom Face 151 | v -0.53 0.00 0.09 152 | v 0.04 0.00 -0.09 153 | v -0.14 0.00 -0.67 154 | v -0.71 0.00 -0.49 155 | f -8 -7 -6 -5 156 | 157 | g tallBox 158 | usemtl tallBox 159 | 160 | ## Object light 161 | v -0.24 1.98 0.16 162 | v -0.24 1.98 -0.22 163 | v 0.23 1.98 -0.22 164 | v 0.23 1.98 0.16 165 | 166 | g light 167 | usemtl light 168 | f -4 -3 -2 -1 169 | -------------------------------------------------------------------------------- /test/data/cornell_box.toml: -------------------------------------------------------------------------------- 1 | root = "CornellBox" 2 | 3 | [camera] 4 | type = "perspective" 5 | eye = [0, 0.9, 2.5] 6 | dir = [0, 0, -1] 7 | up = [0, 1, 0] 8 | fov = 60 9 | 10 | [[objects]] 11 | name = "CornellBox" 12 | type = "import" 13 | file = "cornell_box.obj" 14 | -------------------------------------------------------------------------------- /test/data/cornell_box_glossy.mtl: -------------------------------------------------------------------------------- 1 | # The original Cornell Box in OBJ format. 2 | # Note that the real box is not a perfect cube, so 3 | # the faces are imperfect in this data set. 4 | # 5 | # Created by Guedis Cardenas and Morgan McGuire at Williams College, 2011 6 | # Released into the Public Domain. 7 | # 8 | # http://graphics.cs.williams.edu/data 9 | # http://www.graphics.cornell.edu/online/box/data.html 10 | # 11 | 12 | newmtl leftWall 13 | Ns 10.0000 14 | Ni 1.5000 15 | illum 2 16 | Ka 0.63 0.065 0.05 # Red 17 | Kd 0.63 0.065 0.05 18 | Ks 0 0 0 19 | Ke 0 0 0 20 | 21 | 22 | newmtl rightWall 23 | Ns 10.0000 24 | Ni 1.5000 25 | illum 2 26 | Ka 0.14 0.45 0.091 # Green 27 | Kd 0.14 0.45 0.091 28 | Ks 0 0 0 29 | Ke 0 0 0 30 | 31 | 32 | newmtl floor 33 | Ns 1000.0000 34 | Ni 1.0000 35 | illum 2 36 | Ka 0.725 0.71 0.68 # White 37 | Kd 0.725 0.71 0.68 38 | Ks 1 1 1 39 | Ke 0 0 0 40 | 41 | 42 | newmtl ceiling 43 | Ns 10.0000 44 | Ni 1.0000 45 | illum 2 46 | Ka 0.725 0.71 0.68 # White 47 | Kd 0.725 0.71 0.68 48 | Ks 0 0 0 49 | Ke 1 0.9 0.7 50 | 51 | 52 | newmtl backWall 53 | Ns 10.0000 54 | Ni 1.0000 55 | illum 2 56 | Ka 0.725 0.71 0.68 # White 57 | Kd 0.725 0.71 0.68 58 | Ks 0 0 0 59 | Ke 0 0 0 60 | 61 | 62 | newmtl shortBox 63 | Ns 10.0000 64 | Ni 1.0000 65 | illum 2 66 | Ka 0.725 0.71 0.68 # White 67 | Kd 0.725 0.71 0.68 68 | Ks 0 0 0 69 | Ke 0 0 0 70 | 71 | 72 | newmtl tallBox 73 | Ns 500.0000 74 | Ni 1.0000 75 | illum 2 76 | Ka 0.725 0.71 0.68 # White 77 | Kd 0.725 0.71 0.68 78 | Ks 0.725 0.71 0.68 79 | Ke 0 0 0 80 | 81 | newmtl light 82 | Ns 10.0000 83 | Ni 1.0000 84 | illum 2 85 | Ka 0.78 0.78 0.78 # White 86 | Kd 0.78 0.78 0.78 87 | Ks 0 0 0 88 | Ke 17 12 4 89 | -------------------------------------------------------------------------------- /test/data/cornell_box_glossy.obj: -------------------------------------------------------------------------------- 1 | # The original Cornell Box in OBJ format. 2 | # Note that the real box is not a perfect cube, so 3 | # the faces are imperfect in this data set. 4 | # 5 | # Created by Guedis Cardenas and Morgan McGuire at Williams College, 2011 6 | # Released into the Public Domain. 7 | # 8 | # http://graphics.cs.williams.edu/data 9 | # http://www.graphics.cornell.edu/online/box/data.html 10 | # 11 | 12 | mtllib cornell_box_glossy.mtl 13 | 14 | ## Object floor 15 | v -1.01 0.00 0.99 16 | v 1.00 0.00 0.99 17 | v 1.00 0.00 -1.04 18 | v -0.99 0.00 -1.04 19 | 20 | g floor 21 | usemtl floor 22 | f -4 -3 -2 -1 23 | 24 | ## Object ceiling 25 | v -1.02 1.99 0.99 26 | v -1.02 1.99 -1.04 27 | v 1.00 1.99 -1.04 28 | v 1.00 1.99 0.99 29 | 30 | g ceiling 31 | usemtl ceiling 32 | f -4 -3 -2 -1 33 | 34 | ## Object backwall 35 | v -0.99 0.00 -1.04 36 | v 1.00 0.00 -1.04 37 | v 1.00 1.99 -1.04 38 | v -1.02 1.99 -1.04 39 | 40 | g backWall 41 | usemtl backWall 42 | f -4 -3 -2 -1 43 | 44 | ## Object rightwall 45 | v 1.00 0.00 -1.04 46 | v 1.00 0.00 0.99 47 | v 1.00 1.99 0.99 48 | v 1.00 1.99 -1.04 49 | 50 | g rightWall 51 | usemtl rightWall 52 | f -4 -3 -2 -1 53 | 54 | ## Object leftWall 55 | v -1.01 0.00 0.99 56 | v -0.99 0.00 -1.04 57 | v -1.02 1.99 -1.04 58 | v -1.02 1.99 0.99 59 | 60 | g leftWall 61 | usemtl leftWall 62 | f -4 -3 -2 -1 63 | 64 | ## Object shortBox 65 | usemtl shortBox 66 | 67 | # Top Face 68 | v 0.53 0.60 0.75 69 | v 0.70 0.60 0.17 70 | v 0.13 0.60 0.00 71 | v -0.05 0.60 0.57 72 | f -4 -3 -2 -1 73 | 74 | # Left Face 75 | v -0.05 0.00 0.57 76 | v -0.05 0.60 0.57 77 | v 0.13 0.60 0.00 78 | v 0.13 0.00 0.00 79 | f -4 -3 -2 -1 80 | 81 | # Front Face 82 | v 0.53 0.00 0.75 83 | v 0.53 0.60 0.75 84 | v -0.05 0.60 0.57 85 | v -0.05 0.00 0.57 86 | f -4 -3 -2 -1 87 | 88 | # Right Face 89 | v 0.70 0.00 0.17 90 | v 0.70 0.60 0.17 91 | v 0.53 0.60 0.75 92 | v 0.53 0.00 0.75 93 | f -4 -3 -2 -1 94 | 95 | # Back Face 96 | v 0.13 0.00 0.00 97 | v 0.13 0.60 0.00 98 | v 0.70 0.60 0.17 99 | v 0.70 0.00 0.17 100 | f -4 -3 -2 -1 101 | 102 | # Bottom Face 103 | v 0.53 0.00 0.75 104 | v 0.70 0.00 0.17 105 | v 0.13 0.00 0.00 106 | v -0.05 0.00 0.57 107 | f -12 -11 -10 -9 108 | 109 | g shortBox 110 | usemtl shortBox 111 | 112 | ## Object tallBox 113 | usemtl tallBox 114 | 115 | # Top Face 116 | v -0.53 1.20 0.09 117 | v 0.04 1.20 -0.09 118 | v -0.14 1.20 -0.67 119 | v -0.71 1.20 -0.49 120 | f -4 -3 -2 -1 121 | 122 | # Left Face 123 | v -0.53 0.00 0.09 124 | v -0.53 1.20 0.09 125 | v -0.71 1.20 -0.49 126 | v -0.71 0.00 -0.49 127 | f -4 -3 -2 -1 128 | 129 | # Back Face 130 | v -0.71 0.00 -0.49 131 | v -0.71 1.20 -0.49 132 | v -0.14 1.20 -0.67 133 | v -0.14 0.00 -0.67 134 | f -4 -3 -2 -1 135 | 136 | # Right Face 137 | v -0.14 0.00 -0.67 138 | v -0.14 1.20 -0.67 139 | v 0.04 1.20 -0.09 140 | v 0.04 0.00 -0.09 141 | f -4 -3 -2 -1 142 | 143 | # Front Face 144 | v 0.04 0.00 -0.09 145 | v 0.04 1.20 -0.09 146 | v -0.53 1.20 0.09 147 | v -0.53 0.00 0.09 148 | f -4 -3 -2 -1 149 | 150 | # Bottom Face 151 | v -0.53 0.00 0.09 152 | v 0.04 0.00 -0.09 153 | v -0.14 0.00 -0.67 154 | v -0.71 0.00 -0.49 155 | f -8 -7 -6 -5 156 | 157 | g tallBox 158 | usemtl tallBox 159 | 160 | ## Object light 161 | v -0.24 1.98 0.16 162 | v -0.24 1.98 -0.22 163 | v 0.23 1.98 -0.22 164 | v 0.23 1.98 0.16 165 | -------------------------------------------------------------------------------- /test/data/cornell_box_glossy.toml: -------------------------------------------------------------------------------- 1 | root = "CornellBox" 2 | 3 | [camera] 4 | type = "perspective" 5 | eye = [0, 0.9, 2.5] 6 | dir = [0, 0, -1] 7 | up = [0, 1, 0] 8 | fov = 60 9 | 10 | [[objects]] 11 | name = "CornellBox" 12 | type = "import" 13 | file = "cornell_box_glossy.obj" 14 | -------------------------------------------------------------------------------- /test/data/cornell_box_specular.mtl: -------------------------------------------------------------------------------- 1 | # The sphere Cornell Box as seen in Henrik Jensen's 2 | # "Realistic Image Synthesis Using Photon Mapping" (Page 107 Fig. 9.10) in OBJ format. 3 | # Note that the real box is not a perfect cube, so 4 | # the faces are imperfect in this data set. 5 | # 6 | # Created by Guedis Cardenas and Morgan McGuire at Williams College, 2011 7 | # Released into the Public Domain. 8 | # 9 | # http://graphics.cs.williams.edu/data 10 | # http://www.graphics.cornell.edu/online/box/data.html 11 | # http://www.cs.cmu.edu/~djames/15-864/pics/cornellBox.jpg 12 | # 13 | 14 | 15 | newmtl leftSphere 16 | Ka 0.01 0.01 0.01 17 | Kd 0.01 0.01 0.01 18 | Ks 0.95 0.95 0.95 19 | Ns 1000 20 | illum 5 21 | 22 | newmtl rightSphere 23 | Ka 0.01 0.01 0.01 24 | Kd 0.01 0.01 0.01 25 | Ks 1.0 1.0 1.0 26 | Tf 1.00 1.00 1.00 27 | Ns 200 28 | Ni 1.8 29 | illum 7 30 | 31 | newmtl floor 32 | Ns 10.0000 33 | Ni 1.5000 34 | illum 2 35 | Ka 0.7250 0.7100 0.6800 36 | Kd 0.7250 0.7100 0.6800 37 | Ks 0 0 0 38 | 39 | newmtl ceiling 40 | Ns 10.0000 41 | Ni 1.5000 42 | illum 2 43 | Ka 0.7250 0.7100 0.6800 44 | Kd 0.7250 0.7100 0.6800 45 | Ks 0.0000 0.0000 0.0000 46 | 47 | newmtl backWall 48 | Ns 10.0000 49 | Ni 1.5000 50 | illum 2 51 | Ka 0.7250 0.7100 0.6800 52 | Kd 0.7250 0.7100 0.6800 53 | Ks 0.0000 0.0000 0.0000 54 | Ke 0.0000 0.0000 0.0000 55 | 56 | newmtl rightWall 57 | #Ns 10.0000 58 | #illum 2 59 | Ka 0.161 0.133 0.427 # Blue 60 | Kd 0.161 0.133 0.427 61 | #Ks 0 0 0 62 | Ks 0.95 0.95 0.95 63 | Ns 1000 64 | illum 5 65 | 66 | newmtl leftWall 67 | Ns 10.0000 68 | illum 2 69 | Ka 0.6300 0.0650 0.0500 70 | Kd 0.6300 0.0650 0.0500 71 | Ks 0 0 0 72 | 73 | newmtl light 74 | Ns 10.0000 75 | Ni 1.5000 76 | d 1.0000 77 | Tr 0.0000 78 | Tf 1.0000 1.0000 1.0000 79 | illum 2 80 | Ka 0.7800 0.7800 0.7800 81 | Kd 0.7800 0.7800 0.7800 82 | Ke 100 100 100 83 | Ks 0 0 0 84 | -------------------------------------------------------------------------------- /test/data/cornell_box_specular.toml: -------------------------------------------------------------------------------- 1 | root = "CornellBox" 2 | 3 | [camera] 4 | type = "perspective" 5 | eye = [0, 0.9, 2.5] 6 | dir = [0, 0, -1] 7 | up = [0, 1, 0] 8 | fov = 60 9 | 10 | [[objects]] 11 | name = "CornellBox" 12 | type = "import" 13 | file = "cornell_box_specular.obj" 14 | -------------------------------------------------------------------------------- /test/data/cornell_box_water.mtl: -------------------------------------------------------------------------------- 1 | # The water Cornell Box as seen in Henrik Jensen's 2 | # "Realistic Image Synthesis Using Photon Mapping" (Page 109 Fig. 9.14) in OBJ format. 3 | # Note that the real box is not a perfect cube, so 4 | # the faces are imperfect in this data set. 5 | # 6 | # Created by Guedis Cardenas and Morgan McGuire at Williams College, 2011 7 | # Released into the Public Domain. 8 | # 9 | # http://graphics.cs.williams.edu/data 10 | # http://www.graphics.cornell.edu/online/box/data.html 11 | # 12 | newmtl leftSphere 13 | Ka 0.01 0.01 0.01 14 | Kd 0.01 0.01 0.01 15 | Ks 0.95 0.95 0.95 16 | Ns 1000 17 | illum 5 18 | 19 | newmtl rightSphere 20 | Ka 0.01 0.01 0.01 21 | Kd 0.01 0.01 0.01 22 | Ks 1 1 1 23 | Tf 1 1 1 24 | Ns 200 25 | Ni 2.5 26 | illum 7 27 | 28 | newmtl floor 29 | Ns 10.0000 30 | Ni 1.5000 31 | illum 2 32 | Ka 0.7250 0.7100 0.6800 33 | Kd 0.7250 0.7100 0.6800 34 | Ks 0 0 0 35 | 36 | newmtl ceiling 37 | Ns 10.0000 38 | Ni 1.5000 39 | illum 2 40 | Ka 0.7250 0.7100 0.6800 41 | Kd 0.7250 0.7100 0.6800 42 | Ks 0.0000 0.0000 0.0000 43 | 44 | newmtl backWall 45 | Ns 10.0000 46 | Ni 1.5000 47 | illum 2 48 | Ka 0.7250 0.7100 0.6800 49 | Kd 0.7250 0.7100 0.6800 50 | Ks 0.0000 0.0000 0.0000 51 | Ke 0.0000 0.0000 0.0000 52 | 53 | newmtl rightWall 54 | Ns 10.0000 55 | illum 2 56 | Ka 0.161 0.133 0.427 # Blue 57 | Kd 0.161 0.133 0.427 58 | Ks 0 0 0 59 | 60 | newmtl leftWall 61 | Ns 1000.0000 62 | illum 2 63 | Ka 0.6300 0.0650 0.0500 64 | Kd 0.6300 0.0650 0.0500 65 | Ks 1.6300 1.0650 1.0500 66 | 67 | newmtl light 68 | Ns 10.0000 69 | Ni 1.5000 70 | d 1.0000 71 | Tr 0.0000 72 | Tf 1.0000 1.0000 1.0000 73 | illum 2 74 | Ka 0.7800 0.7800 0.7800 75 | Kd 0.7800 0.7800 0.7800 76 | Ke 10 10 10 77 | Ks 0 0 0 78 | 79 | newmtl water 80 | Ka 0.01 0.01 0.01 81 | Kd 0.01 0.01 0.01 82 | Ks 0.8 0.8 1 83 | Tf 0.8 0.8 1 84 | Ns 200 85 | Ni 1.33 86 | illum 7 87 | -------------------------------------------------------------------------------- /test/data/cornell_box_water.toml: -------------------------------------------------------------------------------- 1 | root = "CornellBox" 2 | 3 | [camera] 4 | type = "perspective" 5 | eye = [0, 0.9, 2.5] 6 | dir = [0, 0, -1] 7 | up = [0, 1, 0] 8 | fov = 60 9 | 10 | [[objects]] 11 | name = "CornellBox" 12 | type = "import" 13 | file = "cornell_box_water.obj" 14 | -------------------------------------------------------------------------------- /test/driver.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | static const std::unordered_set valid_algorithms = { "path_tracer" }; 15 | 16 | struct Options { 17 | std::string scene_file; 18 | std::string out_file = "render.exr"; 19 | sol::Image::Format out_format = sol::Image::Format::Auto; 20 | 21 | std::string algorithm = "path_tracer"; 22 | 23 | size_t output_width = 1080; 24 | size_t output_height = 720; 25 | size_t samples_per_pixel = 16; 26 | 27 | size_t max_path_len = 64; 28 | size_t min_rr_path_len = 3; 29 | float min_survival_prob = 0.05f; 30 | float max_survival_prob = 0.75f; 31 | float ray_offset = 1e-5f; 32 | }; 33 | 34 | static void usage() { 35 | Options default_options; 36 | std::cout << 37 | "Usage: driver [options] scene.toml\n" 38 | "Available options:\n" 39 | " -h --help Shows this message\n" 40 | " -o --output Sets the output image file name (default: '" 41 | << default_options.out_file << "')\n" 42 | " -f --format Sets the output image format (default: auto)\n" 43 | " -a --algorithm Sets the rendering algorithm to use (default: '" 44 | << default_options.algorithm << "')\n" 45 | " -s --samples Sets the number of samples per pixel (default: " 46 | << default_options.samples_per_pixel << ")\n" 47 | " -w --width Sets the output width, in pixels (default: " 48 | << default_options.output_width << ")\n" 49 | " -h --height Sets the output height, in pixels (default: " 50 | << default_options.output_height << ")\n" 51 | " --max-path-len Sets the maximum path length (default: " 52 | << default_options.max_path_len << ")\n" 53 | " --min-survival-prob Sets the minimum Russian Roulette survival probability (default: " 54 | << default_options.min_survival_prob << ")\n" 55 | " --max-survival-prob Sets the maximum Russian Roulette survival probability (default: " 56 | << default_options.max_survival_prob << ")\n" 57 | " --min-rr-path-len Sets the minimum path length after which Russian Roulette starts (default: " 58 | << default_options.min_rr_path_len << ")\n" 59 | " --ray-offset Sets the ray offset used to avoid self intersections (default: " 60 | << default_options.ray_offset << ")\n" 61 | "\nValid image formats:\n" 62 | " auto, png, jpeg, exr, tiff\n" 63 | "\nValid algorithms:\n "; 64 | for (auto it = valid_algorithms.begin(); it != valid_algorithms.end();) { 65 | std::cout << *it; 66 | auto next = std::next(it); 67 | if (next != valid_algorithms.end()) 68 | std::cout << ", "; 69 | it = next; 70 | } 71 | std::cout << "\n" << std::endl; 72 | } 73 | 74 | bool must_have_arg(int i, int argc, char** argv) { 75 | if (i == argc - 1) { 76 | std::cerr << "Missing argument for option '" << argv[i] << "'" << std::endl; 77 | return false; 78 | } 79 | return true; 80 | } 81 | 82 | static std::optional parse_options(int argc, char** argv) { 83 | Options options; 84 | for (int i = 1; i < argc; ++i) { 85 | if (argv[i][0] == '-') { 86 | using namespace std::literals::string_view_literals; 87 | if (argv[i] == "-h"sv || argv[i] == "--help"sv) { 88 | usage(); 89 | return std::nullopt; 90 | } else if (argv[i] == "-o"sv) { 91 | if (!must_have_arg(i++, argc, argv)) 92 | return std::nullopt; 93 | options.out_file = argv[i]; 94 | } else if (argv[i] == "-f"sv) { 95 | if (!must_have_arg(i++, argc, argv)) 96 | return std::nullopt; 97 | if (argv[i] == "auto"sv) options.out_format = sol::Image::Format::Auto; 98 | else if (argv[i] == "png"sv) options.out_format = sol::Image::Format::Png; 99 | else if (argv[i] == "jpeg"sv) options.out_format = sol::Image::Format::Jpeg; 100 | else if (argv[i] == "tiff"sv) options.out_format = sol::Image::Format::Tiff; 101 | else if (argv[i] == "exr"sv) options.out_format = sol::Image::Format::Exr; 102 | else { 103 | std::cerr << "Unknown image format '" << argv[i] << "'" << std::endl; 104 | return std::nullopt; 105 | } 106 | } else if (argv[i] == "-a"sv || argv[i] == "--algorithm"sv) { 107 | if (!must_have_arg(i++, argc, argv)) 108 | return std::nullopt; 109 | options.algorithm = argv[i]; 110 | if (!valid_algorithms.contains(options.algorithm)) { 111 | std::cerr << "Unknown rendering algorithm '" << argv[i] << "'" << std::endl; 112 | return std::nullopt; 113 | } 114 | } else if (argv[i] == "-w"sv || argv[i] == "--width"sv) { 115 | if (!must_have_arg(i++, argc, argv)) 116 | return std::nullopt; 117 | options.output_width = std::strtoul(argv[i], NULL, 10); 118 | } else if (argv[i] == "-h"sv || argv[i] == "--height"sv) { 119 | if (!must_have_arg(i++, argc, argv)) 120 | return std::nullopt; 121 | options.output_height = std::strtoul(argv[i], NULL, 10); 122 | } else if (argv[i] == "-spp"sv || argv[i] == "--samples-per-pixel"sv) { 123 | if (!must_have_arg(i++, argc, argv)) 124 | return std::nullopt; 125 | options.samples_per_pixel = std::strtoul(argv[i], NULL, 10); 126 | } else if (argv[i] == "--max-path-len"sv) { 127 | if (!must_have_arg(i++, argc, argv)) 128 | return std::nullopt; 129 | options.max_path_len = std::strtoul(argv[i], NULL, 10); 130 | } else if (argv[i] == "--min-survival-prob"sv) { 131 | if (!must_have_arg(i++, argc, argv)) 132 | return std::nullopt; 133 | options.min_survival_prob = std::strtof(argv[i], NULL); 134 | } else if (argv[i] == "--max-survival-prob"sv) { 135 | if (!must_have_arg(i++, argc, argv)) 136 | return std::nullopt; 137 | options.max_survival_prob = std::strtof(argv[i], NULL); 138 | } else if (argv[i] == "--min-rr-path-len"sv) { 139 | if (!must_have_arg(i++, argc, argv)) 140 | return std::nullopt; 141 | options.min_rr_path_len = std::strtoul(argv[i], NULL, 10); 142 | } else if (argv[i] == "--ray-offset"sv) { 143 | if (!must_have_arg(i++, argc, argv)) 144 | return std::nullopt; 145 | options.ray_offset = std::strtof(argv[i], NULL); 146 | } else { 147 | std::cerr << "Unknown option '" << argv[i] << "'" << std::endl; 148 | return std::nullopt; 149 | } 150 | } else if (options.scene_file.empty()) { 151 | options.scene_file = argv[i]; 152 | } else { 153 | std::cerr << "Too many input files" << std::endl; 154 | return std::nullopt; 155 | } 156 | } 157 | if (options.scene_file.empty()) { 158 | std::cerr << 159 | "Missing scene file\n" 160 | "Type 'driver -h' to show usage" << std::endl; 161 | return std::nullopt; 162 | } 163 | return std::make_optional(options); 164 | } 165 | 166 | static bool save_image(const sol::Image& image, const Options& options) { 167 | auto format = options.out_format; 168 | if (format == sol::Image::Format::Auto) { 169 | // Try to guess the format from the file extension 170 | if (options.out_file.ends_with(".png")) 171 | format = sol::Image::Format::Png; 172 | if (options.out_file.ends_with(".jpg") || options.out_file.ends_with(".jpeg")) 173 | format = sol::Image::Format::Jpeg; 174 | if (options.out_file.ends_with(".tiff")) 175 | format = sol::Image::Format::Tiff; 176 | if (options.out_file.ends_with(".exr")) 177 | format = sol::Image::Format::Exr; 178 | } 179 | if (!image.save(options.out_file, format)) { 180 | if (format != sol::Image::Format::Auto && 181 | image.save(options.out_file, sol::Image::Format::Auto)) { 182 | std::cout << "Image could not be saved in the given format, so the default format was used instead" << std::endl; 183 | return true; 184 | } 185 | std::cout << "Could not save image to '" << options.out_file << "'" << std::endl; 186 | return false; 187 | } 188 | std::cout << "Image was saved to '" << options.out_file << "'" << std::endl; 189 | return true; 190 | } 191 | 192 | int main(int argc, char** argv) { 193 | auto options = parse_options(argc, argv); 194 | if (!options) 195 | return 1; 196 | 197 | sol::Scene::Defaults scene_defaults; 198 | scene_defaults.aspect_ratio = 199 | static_cast(options->output_width) / 200 | static_cast(options->output_height); 201 | 202 | std::ostringstream err_stream; 203 | auto scene = sol::Scene::load(options->scene_file, scene_defaults, &err_stream); 204 | if (!scene) { 205 | std::cerr << err_stream.str() << std::endl; 206 | return 1; 207 | } 208 | 209 | std::cout 210 | << "Scene summary:\n" 211 | << " " << scene->bsdfs.size() << " BSDF(s)\n" 212 | << " " << scene->lights.size() << " light(s)\n" 213 | << " " << scene->textures.size() << " texture(s)\n" 214 | << " " << scene->images.size() << " image(s)\n"; 215 | 216 | std::unique_ptr renderer; 217 | assert(options->algorithm == "path_tracer"); 218 | renderer = std::make_unique(*scene, sol::PathTracer::Config { 219 | .max_path_len = options->max_path_len, 220 | .min_rr_path_len = options->min_rr_path_len, 221 | .min_survival_prob = options->min_survival_prob, 222 | .max_survival_prob = options->max_survival_prob, 223 | .ray_offset = options->ray_offset 224 | }); 225 | 226 | sol::Image output(options->output_width, options->output_height, 3); 227 | sol::RenderJob render_job(*renderer, output); 228 | 229 | render_job.sample_count = options->samples_per_pixel; 230 | render_job.samples_per_frame = options->samples_per_pixel; 231 | 232 | auto render_start = std::chrono::system_clock::now(); 233 | render_job.start(); 234 | std::cout << "Rendering started..." << std::endl; 235 | render_job.wait(); 236 | auto render_end = std::chrono::system_clock::now(); 237 | auto rendering_ms = std::chrono::duration_cast(render_end - render_start).count(); 238 | std::cout << "Rendering finished in " << rendering_ms << "ms" << std::endl; 239 | 240 | if (!options->out_file.empty()) { 241 | output.scale(1.0f / static_cast(render_job.sample_count)); 242 | if (!save_image(output, *options)) 243 | return 1; 244 | } 245 | return 0; 246 | } 247 | --------------------------------------------------------------------------------