diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..efbb80e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM weckyy702/raychel_ci:latest + +#Copy source files and set the working director + +COPY . /usr/src/Raychel +WORKDIR /usr/src/Raychel + +ARG COMPILER + +RUN cmake -DCMAKE_CXX_COMPILER=${COMPILER} -DRAYCHEL_BUILD_TESTS=ON . +RUN cmake --build . --target all test + +LABEL Name=raychel Version=0.0.1 diff --git a/include/Raychel/Core/Deserialize.h b/include/Raychel/Core/Deserialize.h new file mode 100644 index 0000000..ec76d6c --- /dev/null +++ b/include/Raychel/Core/Deserialize.h @@ -0,0 +1,166 @@ +/** +* \file Deserializer.h +* \author Weckyy702 (weckyy702@gmail.com) +* \brief Header file for Deserializer class +* \date 2022-05-19 +* +* MIT License +* Copyright (c) [2022] [Weckyy702 (weckyy702@gmail.com | https://github.com/Weckyy702)] +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ +#ifndef RAYCHEL_DESERIALIZER_H +#define RAYCHEL_DESERIALIZER_H + +#include +#include +#include + +#include "SDFTransforms.h" +#include "Scene.h" + +namespace Raychel { + + namespace details { + + template + concept DeserializableWithoutTarget = requires() + { + // clang-format off + requires !has_target_v; + { do_deserialize(std::declval(), DeserializationTag{}) } -> std::same_as>; + // clang-format on + }; + + template + concept DeserializableWithTarget = requires() + { + // clang-format off + requires has_target_v; + { do_deserialize(std::declval(), std::declval(), DeserializationTag{}) } -> std::same_as>; + // clang-format on + }; + + template + concept Deserializable = DeserializableWithoutTarget || DeserializableWithTarget; + + template + struct IDeserializer + { + IDeserializer() = default; + + RAYCHEL_MAKE_NONCOPY_NONMOVE(IDeserializer) + + virtual std::optional + deserialize(std::istream& is, std::optional&& maybe_target) const noexcept = 0; + + virtual SerializableObjectData get_serializer() const noexcept = 0; + + virtual std::string_view contained_type_name() const noexcept = 0; + + virtual ~IDeserializer() = default; + }; + + template Object> + class Deserializer final : public IDeserializer + { + public: + Deserializer() = default; + + std::optional deserialize(std::istream& is, std::optional&& maybe_target) const noexcept final + { + if constexpr (has_target_v) { + return _deserialize_with_target(is, std::move(maybe_target)); + } else { + if (maybe_target.has_value()) { + Logger::warn("Type ", Logger::details::type_name(), " did not expect to have a target!\n"); + return std::nullopt; + } + return _deserialize_without_target(is); + } + } + + SerializableObjectData get_serializer() const noexcept final + { + return SerializableObjectData{SerializableObjectDescriptor{}}; + } + + std::string_view contained_type_name() const noexcept final + { + return serializable_type_name_for(); + } + + virtual ~Deserializer() = default; + + private: + static std::optional _deserialize_without_target(std::istream& is) noexcept + { + auto maybe_object = do_deserialize(is, DeserializationTag{}); + if (!maybe_object.has_value()) + return std::nullopt; + return Container{std::move(maybe_object).value()}; + } + + static std::optional + _deserialize_with_target(std::istream& is, std::optional maybe_target) noexcept + { + if (!maybe_target.has_value()) { + Logger::warn("Type ", Logger::details::type_name(), " expectec to have a target!\n"); + return std::nullopt; + } + auto maybe_object = do_deserialize(is, std::move(maybe_target).value(), DeserializationTag{}); + if (!maybe_object.has_value()) + return std::nullopt; + return Container{std::move(maybe_object).value()}; + } + }; + + template + using DeserializerPtr = std::unique_ptr>; + + template + auto object_deserializers() + { + std::vector> res{}; + + ((res.emplace_back(std::make_unique>())), ...); + + return res; + } + } // namespace details + + template + auto object_deserializers() + { + return details::object_deserializers(); + } + + template + auto material_deserializers() + { + return details::object_deserializers(); + } + + [[nodiscard]] Scene deserialize_scene( + std::istream& is, const std::vector>& object_deserializers, + const std::vector>& material_deserializers) noexcept; + +} //namespace Raychel + +#endif //!RAYCHEL_DESERIALIZER_H diff --git a/include/Raychel/Core/Raymarch.h b/include/Raychel/Core/Raymarch.h new file mode 100644 index 0000000..87e931a --- /dev/null +++ b/include/Raychel/Core/Raymarch.h @@ -0,0 +1,66 @@ +/** +* \file Raymarch.h +* \author Weckyy702 (weckyy702@gmail.com) +* \brief Header file for Raymarch class +* \date 2022-04-11 +* +* MIT License +* Copyright (c) [2022] [Weckyy702 (weckyy702@gmail.com | https://github.com/Weckyy702)] +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ +#ifndef RAYCHEL_RAYMARCH_H +#define RAYCHEL_RAYMARCH_H + +#include "Types.h" + +#include +#include +#include + +namespace Raychel { + + constexpr static auto no_hit = std::numeric_limits::max(); + + struct RaymarchResult + { + vec3 point{}; + double ray_depth{}; + std::size_t ray_steps{}; + std::size_t hit_index{no_hit}; + }; + + struct RaymarchOptions + { + std::size_t max_ray_steps{1'000}; + double max_ray_depth{100}; + double surface_epsilon{1e-3}; + }; + + [[nodiscard]] std::pair + evaluate_distance_field(const std::vector& surfaces, const vec3& point) noexcept; + + [[nodiscard]] RaymarchResult raymarch( + vec3 current_point, const vec3& direction, const std::vector& surfaces, RaymarchOptions options) noexcept; + + [[nodiscard]] vec3 get_normal(const vec3& point, const SDFContainer& surface, double normal_offset = 1e-6) noexcept; + +} // namespace Raychel + +#endif //!RAYCHEL_RAYMARCH_H diff --git a/include/Raychel/Core/SDFBooleans.h b/include/Raychel/Core/SDFBooleans.h new file mode 100644 index 0000000..2f59c65 --- /dev/null +++ b/include/Raychel/Core/SDFBooleans.h @@ -0,0 +1,87 @@ +/** +* \file SDFBooleans.h +* \author Weckyy702 (weckyy702@gmail.com) +* \brief Header file for boolean operations on SDFs +* \date 2022-05-27 +* +* MIT License +* Copyright (c) [2022] [Weckyy702 (weckyy702@gmail.com | https://github.com/Weckyy702)] +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ +#ifndef RAYCHEL_SDF_BOOLEANS_H +#define RAYCHEL_SDF_BOOLEANS_H + +#include "Types.h" + +#include + +namespace Raychel { + + template + struct Union + { + Target1 target1; + Target2 target2; + }; + + template + Union(T1, T2) -> Union; + + template + double evaluate_sdf(const Union& object, const vec3& p) noexcept + { + return std::min(evaluate_sdf(object.target1, p), evaluate_sdf(object.target2, p)); + } + + template + struct Difference + { + Target1 target1; + Target2 target2; + }; + + template + Difference(T1, T2) -> Difference; + + template + double evaluate_sdf(const Difference& object, const vec3& p) noexcept + { + return std::max(-evaluate_sdf(object.target1, p), evaluate_sdf(object.target2, p)); + } + + template + struct Intersection + { + Target1 target1; + Target2 target2; + }; + + template + Intersection(T1, T2) -> Intersection; + + template + double evaluate_sdf(const Intersection& object, const vec3& p) noexcept + { + return std::max(evaluate_sdf(object.target1, p), evaluate_sdf(object.target2, p)); + } + +} // namespace Raychel + +#endif //!RAYCHEL_SDF_BOOLEANS_H diff --git a/include/Raychel/Core/SDFContainer.h b/include/Raychel/Core/SDFContainer.h new file mode 100644 index 0000000..780972d --- /dev/null +++ b/include/Raychel/Core/SDFContainer.h @@ -0,0 +1,213 @@ +/** + * \file SDFContainer.h + * \author Weckyy702 (weckyy702@gmail.com) + * \brief Header file for SDFContainer class + * \date 2022-04-09 + * + * MIT License + * Copyright (c) [2022] [Weckyy702 (weckyy702@gmail.com | https://github.com/Weckyy702)] + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ +#ifndef RAYCHEL_SDF_CONTAINER_H +#define RAYCHEL_SDF_CONTAINER_H + +#include "Raychel/Core/SDFPrimitives.h" +#include "Types.h" + +#include "RaychelCore/Badge.h" +#include "RaychelCore/ClassMacros.h" + +#include +#include +#include + +namespace Raychel { + + template + constexpr bool has_custom_normal_v = requires(T t) + { + { + evaluate_normal(t, vec3{}) + } -> std::same_as; + }; + + namespace details { + + template + class TypeId + { + static char _; + + public: + static std::uintptr_t id() + { + return reinterpret_cast(&_); + } + }; + + template + char TypeId::_{}; + + struct ISDFContainerImpl + { + ISDFContainerImpl() = default; + + RAYCHEL_MAKE_NONCOPY_NONMOVE(ISDFContainerImpl) + + [[nodiscard]] virtual std::uintptr_t type_id() const noexcept = 0; + + virtual void debug_log() const noexcept = 0; + + virtual ~ISDFContainerImpl() = default; + }; + + template + class SDFContainerImpl final : public ISDFContainerImpl + { + public: + explicit SDFContainerImpl(T&& object) noexcept(std::is_nothrow_move_constructible_v) + : object_{std::forward(object)} + {} + + void debug_log() const noexcept override + { + std::cout << "SDFContainer with object type " << Logger::details::type_name() << " (type id " << type_id() + << ")"; + if constexpr (has_target_v) { + std::cout << " and target "; + if constexpr (std::is_same_v) { + object_.target.unsafe_impl()->debug_log(); + } else { + std::cout << Logger::details::type_name() << '\n'; + } + } else { + std::cout << '\n'; + } + } + + [[nodiscard]] std::uintptr_t type_id() const noexcept override + { + return TypeId::id(); + } + + [[nodiscard]] T& object() noexcept + { + return object_; + } + + [[nodiscard]] const T& object() const noexcept + { + return object_; + } + + ~SDFContainerImpl() override = default; + + private: + T object_; + }; + + template + struct Eval + { + static double eval(ISDFContainerImpl* ptr, const vec3& p) + { + auto& obj = get_ref(ptr); + return evaluate_sdf(obj, p); + } + + static vec3 get_normal(ISDFContainerImpl* ptr, const vec3& p) + { + if constexpr (has_custom_normal_v) { + auto& obj = get_ref(ptr); + return evaluate_normal(obj, p); + } + RAYCHEL_ASSERT_NOT_REACHED; + } + + static T& get_ref(ISDFContainerImpl* ptr) + { + return reinterpret_cast*>(ptr)->object(); + } + }; + } // namespace details + + class SDFContainer + { + using EvalFunction = double (*)(details::ISDFContainerImpl*, const vec3&); + using NormalFunction = vec3 (*)(details::ISDFContainerImpl*, const vec3&); + + public: + template + using Impl = details::SDFContainerImpl; + + template + requires(!std::is_same_v, SDFContainer>) //otherwise the move constructor would be hidden + explicit SDFContainer(T&& object) noexcept(std::is_nothrow_move_constructible_v) + : impl_{std::make_unique>(std::forward(object))}, + eval_{details::Eval::eval}, + get_normal_(details::Eval::get_normal), + has_custom_normal_{has_custom_normal_v} + {} + + RAYCHEL_MAKE_NONCOPY(SDFContainer) + + RAYCHEL_MAKE_DEFAULT_MOVE(SDFContainer) + + [[nodiscard]] double evaluate(const vec3& p) const noexcept + { + return eval_(impl_.get(), p); + } + + [[nodiscard]] bool has_custom_normal() const noexcept + { + return has_custom_normal_; + } + + [[nodiscard]] vec3 get_normal(const vec3& p) const noexcept + { + return get_normal_(impl_.get(), p); + } + + [[nodiscard]] auto type_id() const noexcept + { + return impl_->type_id(); + } + + [[nodiscard]] auto* unsafe_impl() const noexcept + { + return impl_.get(); + } + + ~SDFContainer() = default; + + private: + std::unique_ptr impl_{}; + EvalFunction eval_; + NormalFunction get_normal_; + bool has_custom_normal_ : 1 {}; + }; + + inline double evaluate_sdf(const SDFContainer& obj, const vec3& p) + { + return obj.evaluate(p); + } +} // namespace Raychel + +#endif //! RAYCHEL_SDF_CONTAINER_H diff --git a/include/Raychel/Core/SDFModifiers.h b/include/Raychel/Core/SDFModifiers.h new file mode 100644 index 0000000..3137625 --- /dev/null +++ b/include/Raychel/Core/SDFModifiers.h @@ -0,0 +1,84 @@ +/** +* \file SDFModifiers.h +* \author Weckyy702 (weckyy702@gmail.com) +* \brief Header file for modifiers on SDFs +* \date 2022-05-27 +* +* MIT License +* Copyright (c) [2022] [Weckyy702 (weckyy702@gmail.com | https://github.com/Weckyy702)] +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ +#ifndef RAYCHEL_SDF_MODIFIERS_H +#define RAYCHEL_SDF_MODIFIERS_H + +#include "Raychel/Core/Types.h" + +namespace Raychel { + + template + struct Hollow + { + Target target; + }; + + template + Hollow(T) -> Hollow; + + template + double evaluate_sdf(const Hollow& object, const vec3& p) noexcept + { + return std::abs(evaluate_sdf(object.target, p)); + } + + template + struct Rounded + { + Target target; + double radius; + }; + + template + Rounded(T, double) -> Rounded; + + template + double evaluate_sdf(const Rounded& object, const vec3& p) noexcept + { + return evaluate_sdf(object.target, p) - object.radius; + } + + template + struct Onion + { + Target target; + double thickness; + }; + + template + Onion(T, double) -> Onion; + + template + double evaluate_sdf(const Onion& object, const vec3& p) noexcept + { + return std::abs(evaluate_sdf(object.target, p)) - object.thickness; + } + +} // namespace Raychel + +#endif //!RAYCHEL_SDF_MODIFIERS_H diff --git a/include/Raychel/Core/SDFPrimitives.h b/include/Raychel/Core/SDFPrimitives.h new file mode 100644 index 0000000..2d6fd16 --- /dev/null +++ b/include/Raychel/Core/SDFPrimitives.h @@ -0,0 +1,105 @@ +/** +* \file SDFPrimitives.h +* \author Weckyy702 (weckyy702@gmail.com) +* \brief Header file for SDF primitives +* \date 2022-05-27 +* +* MIT License +* Copyright (c) [2022] [Weckyy702 (weckyy702@gmail.com | https://github.com/Weckyy702)] +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ +#ifndef RAYCHEL_SDF_PRIMITIVES_H +#define RAYCHEL_SDF_PRIMITIVES_H + +#include "Types.h" + +#include +#include +#include + +namespace Raychel { + + struct Sphere + { + double radius{1.0}; + }; + + inline double evaluate_sdf(const Sphere& object, const vec3& p) noexcept + { + return mag(p) - object.radius; + } + + //optimized normal calculation for spheres + inline vec3 evaluate_normal(const Sphere& /*unused*/, const vec3& p) noexcept + { + return normalize(p); + } + + bool do_serialize(std::ostream& os, const Sphere& object) noexcept; + + std::optional do_deserialize(std::istream& is, DeserializationTag) noexcept; + + struct Box + { + vec3 size{1, 1, 1}; + }; + + inline double evaluate_sdf(const Box& box, const vec3& p) noexcept + { + using std::abs, std::max, std::min; + const auto q = vec3{abs(p.x()), abs(p.y()), abs(p.z())} - box.size; + + return mag(vec3{max(q.x(), 0.0), max(q.y(), 0.0), max(q.z(), 0.0)}) + min(max(q.x(), max(q.y(), q.z())), 0.0); + } + + bool do_serialize(std::ostream& os, const Box& object) noexcept; + + std::optional do_deserialize(std::istream& is, DeserializationTag) noexcept; + + struct Plane + { + vec3 normal{0, 1, 0}; + }; + + inline double evaluate_sdf(const Plane& object, const vec3& p) noexcept + { + return std::abs(dot(object.normal, p)); + } + + inline vec3 evaluate_normal(const Plane& object, const vec3&) noexcept + { + return object.normal; + } + + bool do_serialize(std::ostream& os, const Plane& object) noexcept; + + std::optional do_deserialize(std::istream& is, DeserializationTag) noexcept; + + struct DeserializationErrorPlaceHolder + {}; + + inline double evaluate_sdf(const DeserializationErrorPlaceHolder /*unused*/, const vec3& /*unused*/) noexcept + { + return 1e9; + } + +} // namespace Raychel + +#endif //!RAYCHEL_SDF_PRIMITIVES_H diff --git a/include/Raychel/Core/SDFTransforms.h b/include/Raychel/Core/SDFTransforms.h new file mode 100644 index 0000000..8e36c3a --- /dev/null +++ b/include/Raychel/Core/SDFTransforms.h @@ -0,0 +1,115 @@ +/** +* \file SDFTransforms.h +* \author Weckyy702 (weckyy702@gmail.com) +* \brief Header file for SDFTransforms class +* \date 2022-05-27 +* +* MIT License +* Copyright (c) [2022] [Weckyy702 (weckyy702@gmail.com | https://github.com/Weckyy702)] +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ +#ifndef RAYCHEL_SDF_TRANSFORM_H +#define RAYCHEL_SDF_TRANSFORM_H + +#include "Types.h" +#include "SDFContainer.h" + +#include +#include + +namespace Raychel { + + template + struct Translate + { + Target target; + vec3 translation{}; + }; + + template + struct has_target> : std::true_type + {}; + + template + Translate(T, vec3) -> Translate; + + template + double evaluate_sdf(const Translate& object, const vec3& p) noexcept + { + return evaluate_sdf(object.target, p - object.translation); + } + + template + bool do_serialize(std::ostream& os, const Translate& object) noexcept + { + os << object.translation << '\n'; + return os.good(); + } + + template + std::optional> do_deserialize(std::istream& is, SDFContainer target, DeserializationTag>) noexcept + { + vec3 translation{}; + + if (!(is >> translation)) + return std::nullopt; + return Translate{std::move(target), translation}; + } + + template + struct Rotate + { + Target target; + Quaternion rotation{}; + }; + + template + struct has_target> : std::true_type + {}; + + template + Rotate(T, Quaternion) -> Rotate; + + template + double evaluate_sdf(const Rotate& object, const vec3& p) noexcept + { + return evaluate_sdf(object.target, p * inverse(object.rotation)); + } + + template + bool do_serialize(std::ostream& os, const Rotate& object) noexcept + { + os << object.rotation << '\n'; + return os.good(); + } + + template + std::optional> do_deserialize(std::istream& is, SDFContainer target, DeserializationTag>) noexcept + { + Quaternion rotation{}; + + if (!(is >> rotation)) + return std::nullopt; + return Rotate{std::move(target), rotation}; + } + +} // namespace Raychel + +#endif //!RAYCHEL_SDF_TRANSFORM_H diff --git a/include/Raychel/Core/SDFs.h b/include/Raychel/Core/SDFs.h new file mode 100644 index 0000000..2f361dc --- /dev/null +++ b/include/Raychel/Core/SDFs.h @@ -0,0 +1,36 @@ +/** +* \file SDFs.h +* \author Weckyy702 (weckyy702@gmail.com) +* \brief Header file for predefined signed distance functions +* \date 2022-04-12 +* +* MIT License +* Copyright (c) [2022] [Weckyy702 (weckyy702@gmail.com | https://github.com/Weckyy702)] +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ +#ifndef RAYCHEL_PREDEFINED_SIGNED_DISTANCE_FUNCTIONS_H +#define RAYCHEL_PREDEFINED_SIGNED_DISTANCE_FUNCTIONS_H + +#include "SDFBooleans.h" +#include "SDFModifiers.h" +#include "SDFPrimitives.h" +#include "SDFTransforms.h" + +#endif //!RAYCHEL_PREDEFINED_SIGNED_DISTANCE_FUNCTIONS_H diff --git a/include/Raychel/Core/Scene.h b/include/Raychel/Core/Scene.h new file mode 100644 index 0000000..b6cc422 --- /dev/null +++ b/include/Raychel/Core/Scene.h @@ -0,0 +1,141 @@ +/** + * \file Scene.h + * \author Weckyy702 (weckyy702@gmail.com) + * \brief Header file for Scene class + * \date 2022-04-09 + * + * MIT License + * Copyright (c) [2022] [Weckyy702 (weckyy702@gmail.com | https://github.com/Weckyy702)] + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ +#ifndef RAYCHEL_SCENE_H +#define RAYCHEL_SCENE_H + +#include "Raychel/Render/MaterialContainer.h" +#include "SDFContainer.h" +#include "Serialize.h" + +#include +#include +#include + +namespace Raychel { + + template + struct RaymarchableObject + { + std::size_t index_in_scene; + Object& object; + Material& material; + }; + + //Clang needs this deduction guide + template + RaymarchableObject(std::size_t, Object&, Material&) -> RaymarchableObject; + + class Scene + { + public: + Scene() = default; + + static Scene unsafe_from_data( + std::vector objects, std::vector> object_serializers, + std::vector materials, + std::vector> material_serializers) noexcept; + + template + auto add_object(Object&& object, Material&& material) noexcept + { + const auto where = std::lower_bound( + objects_.begin(), objects_.end(), details::TypeId::id(), [](const auto& cont, const auto& id) { + return cont.type_id() < id; + }); + + const auto obj = objects_.emplace(where, std::forward(object)); + const auto index = std::distance(objects_.begin(), obj); + + const auto mat = materials_.emplace(materials_.begin() + index, std::forward(material)); + + object_serializers_.emplace(object_serializers_.begin() + index, details::SerializableObjectDescriptor{}); + material_serializers_.emplace( + material_serializers_.begin() + index, details::SerializableObjectDescriptor{}); + + return RaymarchableObject{ + objects_.size() - 1U, + details::get_container_content(*obj), + details::get_container_content(*mat)}; + } + + void remove_object(std::size_t index) noexcept; + + template F> + requires(std::is_same_v, color>) void set_background_function(F&& f) noexcept + { + background_function_ = f; + } + + [[nodiscard]] const auto& objects() const noexcept + { + return objects_; + } + + [[nodiscard]] const auto& materials() const noexcept + { + return materials_; + } + + [[nodiscard]] const auto& background_function() const noexcept + { + return background_function_; + } + + [[nodiscard]] const auto& object_serializers() const noexcept + { + return object_serializers_; + } + + [[nodiscard]] const auto& material_serializers() const noexcept + { + return material_serializers_; + } + + private: + Scene( + std::vector objects, std::vector> object_serializers, + std::vector materials, std::vector> material_serializers) + : object_serializers_{std::move(object_serializers)}, + material_serializers_{std::move(material_serializers)}, + objects_{std::move(objects)}, + materials_{std::move(materials)} + {} + + std::vector> object_serializers_{}; + std::vector> material_serializers_{}; + + std::vector objects_{}; + std::vector materials_{}; + BackgroundFunction background_function_{}; + }; + + bool serialize_scene(const Scene& scene, std::ostream& os) noexcept; + +} // namespace Raychel + +#endif //! RAYCHEL_SCENE_H diff --git a/include/Raychel/Core/Serialize.h b/include/Raychel/Core/Serialize.h new file mode 100644 index 0000000..a201a9a --- /dev/null +++ b/include/Raychel/Core/Serialize.h @@ -0,0 +1,163 @@ +/** +* \file SerializableObjectData.h +* \author Weckyy702 (weckyy702@gmail.com) +* \brief Header file for SerializableObjectData class +* \date 2022-05-18 +* +* MIT License +* Copyright (c) [2022] [Weckyy702 (weckyy702@gmail.com | https://github.com/Weckyy702)] +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ +#ifndef RAYCHEL_SERIALIZABLE_OBJECT_DATA_H +#define RAYCHEL_SERIALIZABLE_OBJECT_DATA_H + +#include "Types.h" + +#include "RaychelCore/ClassMacros.h" +#include "RaychelCore/Raychel_assert.h" +#include "RaychelLogger/Logger.h" + +#include +#include +#include + +namespace Raychel { + + namespace details { + + template + auto& get_container_content(Container& container) noexcept + { + using Impl = typename Container::template Impl; + + auto* impl_interface = container.unsafe_impl(); + auto* impl = dynamic_cast(impl_interface); + RAYCHEL_ASSERT(impl != nullptr); + + return impl->object(); + } + + template + const auto& get_container_content(const Container& container) noexcept + { + using Impl = typename Container::template Impl; + + auto* impl_interface = container.unsafe_impl(); + auto* impl = dynamic_cast(impl_interface); + RAYCHEL_ASSERT(impl != nullptr); + return impl->object(); + } + + template + requires(has_target_v) constexpr std::string_view serializable_type_name_for() + { + constexpr auto full_type_name = Logger::details::type_name(); + constexpr auto bracket_index = full_type_name.find('<'); + static_assert(bracket_index != std::string_view::npos); + constexpr std::string_view base_type_name{full_type_name.begin(), full_type_name.begin() + bracket_index}; + + return base_type_name; + } + + template + constexpr std::string_view serializable_type_name_for() + { + return Logger::details::type_name(); + } + + // clang-format off + template + concept Serializable = requires(const Object& obj) + { + { do_serialize(std::declval(), obj) } -> std::same_as; + }; + // clang-format on + + template + struct SerializableObjectDescriptor + {}; + + template + bool serialize_internal(std::ostream&, const T&, std::size_t = 0) noexcept; + + template + bool serialize_internal(std::ostream&, const T&, std::size_t = 0) noexcept; + + template + requires(has_target_v) bool serialize_with_target( + std::ostream& os, const Object& object, std::size_t recursion_depth) noexcept + { + os << serializable_type_name_for() << "<> with "; + return os.good() && do_serialize(os, object) && serialize_internal(os, object.target, recursion_depth + 1); + } + + template + bool serialize_with_target(std::ostream& os, const Object& object, std::size_t /*unused*/) noexcept + { + os << serializable_type_name_for() << " with "; + return os.good() && do_serialize(os, object); + } + + template + bool serialize_internal(std::ostream& os, const Object& object, std::size_t recursion_depth) noexcept + { + for (std::size_t i{}; i != recursion_depth; ++i) + os << " "; + return serialize_with_target(os, object, recursion_depth); + } + + template + bool serialize_internal(std::ostream& os, const Object& /*unused*/, std::size_t /*unused*/) noexcept + { + constexpr auto type_name = Logger::details::type_name(); + Logger::warn( + "Object of type '", + type_name, + "' cannot be serialized. Make sure the function bool do_serialize(std::ostream&, const ", + type_name, + "&) exists and can be located by ADL\n"); + os << "__NONSERIALIZABLE__\n"; + return os.good(); + } + } // namespace details + + template + class SerializableObjectData + { + public: + template + explicit SerializableObjectData(details::SerializableObjectDescriptor&& /*unused*/) + : serialize_{[](std::ostream& os, const Container& container) -> bool { + return details::serialize_internal(os, details::get_container_content(container)); + }} + {} + + bool serialize(const Container& container, std::ostream& os) const noexcept + { + return serialize_(os, container); + } + + private: + std::function serialize_; + }; + +} // namespace Raychel + +#endif //!RAYCHEL_SERIALIZABLE_OBJECT_DATA_H diff --git a/include/Raychel/Core/Types.h b/include/Raychel/Core/Types.h new file mode 100644 index 0000000..9b19b98 --- /dev/null +++ b/include/Raychel/Core/Types.h @@ -0,0 +1,70 @@ +/** +* \file Types.h +* \author Weckyy702 (weckyy702@gmail.com) +* \brief Header file for Types class +* \date 2022-04-10 +* +* MIT License +* Copyright (c) [2022] [Weckyy702 (weckyy702@gmail.com | https://github.com/Weckyy702)] +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ +#ifndef RAYCHEL_TYPES_H +#define RAYCHEL_TYPES_H + +#include "RaychelMath/Transform.h" +#include "RaychelMath/color.h" +#include "RaychelMath/vec2.h" +#include "RaychelMath/vec3.h" + +#include + +namespace Raychel { + + using vec3 = basic_vec3; + using color = basic_color; + using Quaternion = basic_quaternion; + using Transform = basic_transform; + + using Size2D = basic_vec2; + + class SDFContainer; + + class Scene; + + struct RenderData; + + using BackgroundFunction = std::function; + + template + struct DeserializationTag + {}; + + template + struct has_target : std::false_type + {}; + + template + constexpr bool has_target_v = has_target::value && requires(T t){ + t.target; + }; + +} // namespace Raychel + +#endif //!RAYCHEL_TYPES_H diff --git a/include/Raychel/Core/Xoroshiro128+.h b/include/Raychel/Core/Xoroshiro128+.h new file mode 100644 index 0000000..a671b7c --- /dev/null +++ b/include/Raychel/Core/Xoroshiro128+.h @@ -0,0 +1,153 @@ +/** +* \file Xoroshiro128+.h +* \author Weckyy702 (weckyy702@gmail.com) +* \brief Header file for Xoroshiro128+ class +* \date 2022-04-24 +* +* MIT License +* Copyright (c) [2022] [Weckyy702 (weckyy702@gmail.com | https://github.com/Weckyy702)] +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ +#ifndef RAYCHEL_XOROSHIRO128_H +#define RAYCHEL_XOROSHIRO128_H + +#include +#include +#include +#include + +namespace Raychel { + + //This is an implementation of the xoroshiro128+ algorithm as described at https://prng.di.unimi.it/xoroshiro128plus.c ported to C++20 + + // template + + //TODO: template this once we figure out how jump() and long_jump() can be generalized + class Xoroshiro128 + { + struct TwoUint64s + { + std::uint64_t first, second; + }; + + public: + static constexpr std::int32_t A = 24, B = 16, C = 37; + + constexpr Xoroshiro128() = default; + + constexpr Xoroshiro128(std::uint64_t seed) : state_{seed, 0} + { + jump(); //jump through the domain a bit so we don't get weird results on the first call to next() + } + + constexpr Xoroshiro128(std::uint64_t s0, std::uint64_t s1) : state_{s0, s1} + {} + + static constexpr auto min() noexcept + { + return std::numeric_limits::min(); + } + + static constexpr auto max() noexcept + { + return std::numeric_limits::max(); + } + + template + [[nodiscard]] constexpr Result operator()() noexcept + { + return next(); + } + + template + constexpr Result next() noexcept + { + static_assert(sizeof(Result) <= sizeof(std::uint64_t), "Xoroshiro128+ cannot produce more than 64 random bits!"); + + auto [s0, s1] = state_; + const auto result = static_cast(s0 + s1); + + s1 ^= s0; + state_.first = std::rotl(s0, A) ^ s1 ^ (s1 << B); + state_.second = std::rotl(s1, C); + + return result; + } + + constexpr void jump() noexcept + { + uint64_t s0{}; + uint64_t s1{}; + + for (std::size_t b{}; b != 64U; ++b) { + if (short_jump_.first & std::uint64_t{1} << b) { + s0 ^= state_.first; + s1 ^= state_.second; + } + next(); + } + + for (std::size_t b{}; b != 64U; ++b) { + if (short_jump_.second & std::uint64_t{1} << b) { + s0 ^= state_.first; + s1 ^= state_.second; + } + next(); + } + + state_.first = s0; + state_.second = s1; + } + + constexpr void long_jump() noexcept + { + uint64_t s0{}; + uint64_t s1{}; + + for (std::size_t b{}; b != 64; ++b) { + if (long_jump_.first & std::uint64_t{1} << b) { + s0 ^= state_.first; + s1 ^= state_.second; + } + next(); + } + + for (std::size_t b{}; b != 64; ++b) { + if (long_jump_.second & std::uint64_t{1} << b) { + s0 ^= state_.first; + s1 ^= state_.second; + } + next(); + } + + state_.first = s0; + state_.second = s1; + } + + private: + TwoUint64s state_{123456789, 987654321}; + + static constexpr TwoUint64s short_jump_{0xdf900294d8f554a5, 0x170865df4b3201fc}; + static constexpr TwoUint64s long_jump_{0xd2a98b26625eee7b, 0xdddf9b1090aa7ac1}; + }; + +} // namespace Raychel + +#endif //!RAYCHEL_XOROSHIRO128_H diff --git a/include/Raychel/Core/ZigguratNormal.h b/include/Raychel/Core/ZigguratNormal.h new file mode 100644 index 0000000..d24ab1a --- /dev/null +++ b/include/Raychel/Core/ZigguratNormal.h @@ -0,0 +1,39 @@ +/** +* \file ZigguratNormal.h +* \author Weckyy702 (weckyy702@gmail.com) +* \brief Header file for ZigguratNormal class +* \date 2022-04-24 +* +* MIT License +* Copyright (c) [2022] [Weckyy702 (weckyy702@gmail.com | https://github.com/Weckyy702)] +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ +#ifndef RAYCHEL_ZIGGURAT_NORMAL_H +#define RAYCHEL_ZIGGURAT_NORMAL_H + +namespace Raychel { + + [[nodiscard]] double uniform_random() noexcept; + + [[nodiscard]] double ziggurat_normal() noexcept; + +} // namespace Raychel + +#endif //!RAYCHEL_ZIGGURAT_NORMAL_H diff --git a/include/Raychel/Render/Camera.h b/include/Raychel/Render/Camera.h new file mode 100644 index 0000000..d060bb1 --- /dev/null +++ b/include/Raychel/Render/Camera.h @@ -0,0 +1,43 @@ +/** +* \file Camera.h +* \author Weckyy702 (weckyy702@gmail.com) +* \brief Header file for Camera class +* \date 2022-04-11 +* +* MIT License +* Copyright (c) [2022] [Weckyy702 (weckyy702@gmail.com | https://github.com/Weckyy702)] +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ +#ifndef RAYCHEL_CAMERA_H +#define RAYCHEL_CAMERA_H + +#include "Raychel/Core/Types.h" + +namespace Raychel { + + struct Camera + { + Transform transform; + double zoom{1.0}; + }; + +} // namespace Raychel + +#endif //!RAYCHEL_CAMERA_H diff --git a/include/Raychel/Render/Denoise.h b/include/Raychel/Render/Denoise.h new file mode 100644 index 0000000..b7b3755 --- /dev/null +++ b/include/Raychel/Render/Denoise.h @@ -0,0 +1,50 @@ +/** +* \file Denoise.h +* \author Weckyy702 (weckyy702@gmail.com) +* \brief Header file for Denoise class +* \date 2022-05-06 +* +* MIT License +* Copyright (c) [2022] [Weckyy702 (weckyy702@gmail.com | https://github.com/Weckyy702)] +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ +#ifndef RAYCHEL_DENOISE_H +#define RAYCHEL_DENOISE_H + +#include "FatPixel.h" +#include "Framebuffer.h" + +namespace Raychel { + + struct DenoisingOptions + { + std::size_t half_patch_size{1}; + std::size_t half_search_window_size{6}; + double distance_threshold{1.}; + std::size_t num_scales{3}; + }; + + Framebuffer denoise_single_scale(const FatFramebuffer& input_pixels, DenoisingOptions options = {}) noexcept; + + Framebuffer denoise_multiscale(const FatFramebuffer& input_pixels, DenoisingOptions options = {}) noexcept; + +} // namespace Raychel + +#endif //!RAYCHEL_DENOISE_H diff --git a/include/Raychel/Render/FatPixel.h b/include/Raychel/Render/FatPixel.h new file mode 100644 index 0000000..4941f46 --- /dev/null +++ b/include/Raychel/Render/FatPixel.h @@ -0,0 +1,64 @@ +/** +* \file FatPixel.h +* \author Weckyy702 (weckyy702@gmail.com) +* \brief Header file for FatPixel class +* \date 2022-05-01 +* +* MIT License +* Copyright (c) [2022] [Weckyy702 (weckyy702@gmail.com | https://github.com/Weckyy702)] +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ +#ifndef RAYCHEL_FAT_PIXEL_H +#define RAYCHEL_FAT_PIXEL_H + +#include "RayHistogram.h" + +#include + +namespace Raychel { + + namespace details { + template + struct FatPixel + { + using Histogram = RayHistogram; + + FatPixel operator+(const FatPixel& other) const noexcept + { + return {noisy_color + other.noisy_color, histogram + other.histogram}; + } + + template T> + FatPixel operator/(T s) const noexcept + { + return {noisy_color / s, histogram / s}; + } + + color noisy_color{}; + Histogram histogram{}; + }; + + } // namespace details + + using FatPixel = details::FatPixel<30U>; + +} // namespace Raychel + +#endif //!RAYCHEL_FAT_PIXEL_H diff --git a/include/Raychel/Render/Framebuffer.h b/include/Raychel/Render/Framebuffer.h new file mode 100644 index 0000000..8b2bea6 --- /dev/null +++ b/include/Raychel/Render/Framebuffer.h @@ -0,0 +1,77 @@ +/** +* \file Framebuffer.h +* \author Weckyy702 (weckyy702@gmail.com) +* \brief Header file for Framebuffer class +* \date 2022-04-11 +* +* MIT License +* Copyright (c) [2022] [Weckyy702 (weckyy702@gmail.com | https://github.com/Weckyy702)] +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ +#ifndef RAYCHEL_FRAMEBUFFER_H +#define RAYCHEL_FRAMEBUFFER_H + +#include "FatPixel.h" +#include "Raychel/Core/Types.h" + +#include + +namespace Raychel { + + namespace details { + template + struct BasicFramebuffer + { + + constexpr Pixel& at(std::size_t x, std::size_t y) + { + return const_cast(const_cast(this)->at(x, y)); + } + + constexpr const Pixel& at(std::size_t x, std::size_t y) const + { + RAYCHEL_ASSERT((x < size.x()) && (y < size.y())); + + return pixel_data.at(x + (y * size.x())); + } + + const Size2D size{}; + std::vector pixel_data{}; + }; + + template + constexpr auto begin(const BasicFramebuffer& obj) + { + return obj.pixel_data.begin(); + } + + template + constexpr auto end(const BasicFramebuffer& obj) + { + return obj.pixel_data.end(); + } + } // namespace details + + using Framebuffer = details::BasicFramebuffer; + using FatFramebuffer = details::BasicFramebuffer; + +} // namespace Raychel + +#endif //!RAYCHEL_FRAMEBUFFER_H diff --git a/include/Raychel/Render/MaterialContainer.h b/include/Raychel/Render/MaterialContainer.h new file mode 100644 index 0000000..915bfaa --- /dev/null +++ b/include/Raychel/Render/MaterialContainer.h @@ -0,0 +1,130 @@ +/** +* \file MaterialContainer.h +* \author Weckyy702 (weckyy702@gmail.com) +* \brief Header file for MaterialContainer class +* \date 2022-04-10 +* +* MIT License +* Copyright (c) [2022] [Weckyy702 (weckyy702@gmail.com | https://github.com/Weckyy702)] +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ +#ifndef RAYCHEL_MATERIAL_CONTAINER_H +#define RAYCHEL_MATERIAL_CONTAINER_H + +#include "Materials.h" + +#include "Raychel/Core/Types.h" +#include "RaychelCore/Badge.h" +#include "RaychelCore/ClassMacros.h" + +#include + +namespace Raychel { + + namespace details { + + struct IMaterialContainerImpl + { + IMaterialContainerImpl() = default; + + RAYCHEL_MAKE_NONCOPY_NONMOVE(IMaterialContainerImpl) + + [[nodiscard]] virtual color get_surface_color_internal(const ShadingData& data) const noexcept = 0; + + [[nodiscard]] virtual double get_material_ior_internal() const noexcept = 0; + + virtual ~IMaterialContainerImpl() = default; + }; + + template + class MaterialContainerImpl final : public IMaterialContainerImpl + { + public: + explicit MaterialContainerImpl(T&& object) noexcept(std::is_nothrow_move_constructible_v) + : object_{std::forward(object)} + {} + + [[nodiscard]] color get_surface_color_internal(const ShadingData& data) const noexcept override + { + return get_surface_color(object_, data); + } + + [[nodiscard]] double get_material_ior_internal() const noexcept override + { + if constexpr (is_transparent_material_v) + return get_material_ior(object_); + return 1.0; + } + + [[nodiscard]] T& object() noexcept + { + return object_; + } + + [[nodiscard]] const T& object() const noexcept + { + return object_; + } + + private: + T object_; + }; + + } // namespace details + + class MaterialContainer + { + public: + template + using Impl = details::MaterialContainerImpl; + + template + requires(!std::is_same_v) explicit MaterialContainer(T&& object) noexcept( + std::is_nothrow_move_constructible_v) + : impl_{std::make_unique>(std::forward(object))} + {} + + RAYCHEL_MAKE_NONCOPY(MaterialContainer) + RAYCHEL_MAKE_DEFAULT_MOVE(MaterialContainer) + + [[nodiscard]] color get_surface_color(const ShadingData& data) const noexcept + { + return impl_->get_surface_color_internal(data); + } + + [[nodiscard]] double get_material_ior() const noexcept + { + return impl_->get_material_ior_internal(); + } + + [[nodiscard]] auto* unsafe_impl() const noexcept + { + return impl_.get(); + } + + ~MaterialContainer() = default; + + private: + std::unique_ptr impl_{}; + }; + +} //namespace Raychel + +#endif //!RAYCHEL_MATERIAL_CONTAINER_H diff --git a/include/Raychel/Render/Materials.h b/include/Raychel/Render/Materials.h new file mode 100644 index 0000000..d364a3e --- /dev/null +++ b/include/Raychel/Render/Materials.h @@ -0,0 +1,66 @@ +/** +* \file Materials.h +* \author Weckyy702 (weckyy702@gmail.com) +* \brief Header file for Materials class +* \date 2022-05-20 +* +* MIT License +* Copyright (c) [2022] [Weckyy702 (weckyy702@gmail.com | https://github.com/Weckyy702)] +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ +#ifndef RAYCHEL_MATERIALS_H +#define RAYCHEL_MATERIALS_H + +#include "Raychel/Core/Types.h" + +#include + +namespace Raychel { + + struct RenderState; + + struct ShadingData + { + vec3 position; + vec3 normal; + vec3 incoming_direction; + + const RenderState& state; + std::size_t recursion_depth; + }; + + template + struct is_transparent_material : std::false_type + {}; + + template + constexpr bool is_transparent_material_v = is_transparent_material::value; + + struct DeserializationErrorMaterial + {}; + + constexpr color get_surface_color(DeserializationErrorMaterial /*unused*/, const ShadingData& /*unused*/) noexcept + { + return color{1, 0, 1}; + } + +} // namespace Raychel + +#endif //!RAYCHEL_MATERIALS_H diff --git a/include/Raychel/Render/RayHistogram.h b/include/Raychel/Render/RayHistogram.h new file mode 100644 index 0000000..ea8e3c0 --- /dev/null +++ b/include/Raychel/Render/RayHistogram.h @@ -0,0 +1,153 @@ +/** +* \file RayHistogram.h +* \author Weckyy702 (weckyy702@gmail.com) +* \brief Header file for RayHistogram class +* \date 2022-04-30 +* +* MIT License +* Copyright (c) [2022] [Weckyy702 (weckyy702@gmail.com | https://github.com/Weckyy702)] +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ +#ifndef RAYCHEL_RAY_HISTOGRAM_H +#define RAYCHEL_RAY_HISTOGRAM_H + +#include "Raychel/Core/Types.h" + +#include +#include + +namespace Raychel { + + namespace details { + + struct BinData + { + std::size_t low_bin_index{}; + double low_bin_weight{}; + + std::size_t high_bin_index{}; + double high_bin_weight{}; + }; + }; // namespace details + + template + requires(NumBins > 2U) class RayHistogram + { + using BinnedChannel = std::array; + + public: + RayHistogram() = default; + + void add_sample(color c) noexcept + { + _add_channel(red_, c.r()); + _add_channel(green_, c.g()); + _add_channel(blue_, c.b()); + } + + const auto& red_channel() const noexcept + { + return red_; + } + + const auto& green_channel() const noexcept + { + return green_; + } + + const auto& blue_channel() const noexcept + { + return blue_; + } + + RayHistogram operator+(const RayHistogram& other) const noexcept + { + BinnedChannel union_red{}, union_green{}, union_blue{}; + + for (std::size_t i{}; i != NumBins; ++i) { + union_red[i] = red_[i] + other.red_[i]; + union_green[i] = green_[i] + other.green_[i]; + union_blue[i] = blue_[i] + other.blue_[i]; + } + + return {std::move(union_red), std::move(union_green), std::move(union_blue)}; + } + + template T> + RayHistogram operator/(T _s) const noexcept + { + const auto s = static_cast(_s); + + BinnedChannel new_red{}, new_green{}, new_blue{}; + for (std::size_t i{}; i != NumBins; ++i) { + new_red[i] = red_[i] / s; + new_green[i] = green_[i] / s; + new_blue[i] = blue_[i] / s; + } + + return {std::move(new_red), std::move(new_green), std::move(new_blue)}; + } + + private: + RayHistogram(BinnedChannel red, BinnedChannel green, BinnedChannel blue) + : red_{std::move(red)}, green_{std::move(green)}, blue_{std::move(blue)} + {} + + static void _add_channel(BinnedChannel& channel, double value) noexcept + { + const auto [low_bin, low_weight, high_bin, high_weight] = _get_bin_data(value); + channel.at(low_bin) += low_weight; + channel.at(high_bin) += high_weight; + } + + static details::BinData _get_bin_data(double value) noexcept + { + constexpr auto max_value = 7.5; + constexpr auto saturated_value = 2.5; + constexpr auto fbin_factor = static_cast(NumBins - 2U); + + auto v = std::max(value, 0.0); + v = std::pow(v, 1.0 / 2.2); + v /= max_value; + + v = std::min(saturated_value, v); + + const auto fbin = v * fbin_factor; + + const auto bin_low = static_cast(fbin); + if (bin_low < (NumBins - 2U)) { + const auto high_weight = std::fmod(fbin, 1.0); + const auto low_weight = 1.0 - high_weight; + + return {bin_low, low_weight, bin_low + 1U, high_weight}; + } + const auto high_weight = (v - 1) / (saturated_value - 1); + const auto low_weight = 1.0 - high_weight; + + return {NumBins - 2U, low_weight, NumBins - 1U, high_weight}; + } + + BinnedChannel red_{}; + BinnedChannel green_{}; + BinnedChannel blue_{}; + }; +} // namespace Raychel + +#endif //!RAYCHEL_RAY_HISTOGRAM_H diff --git a/include/Raychel/Render/RenderUtils.h b/include/Raychel/Render/RenderUtils.h new file mode 100644 index 0000000..52ed939 --- /dev/null +++ b/include/Raychel/Render/RenderUtils.h @@ -0,0 +1,55 @@ +/** +* \file RenderHelper.h +* \author Weckyy702 (weckyy702@gmail.com) +* \brief Header file for RenderHelper class +* \date 2022-04-12 +* +* MIT License +* Copyright (c) [2022] [Weckyy702 (weckyy702@gmail.com | https://github.com/Weckyy702)] +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ +#ifndef RAYCHEL_RENDER_UTILS_H +#define RAYCHEL_RENDER_UTILS_H + +#include "Renderer.h" + +namespace Raychel { + + struct RefractionData + { + vec3 surface_point; + vec3 incoming_direction; + vec3 normal; + + double material_ior; + double ior_variation; + + const RenderState& state; + std::size_t recursion_depth; + }; + + [[nodiscard]] color get_shaded_color(const RenderData& data) noexcept; + + [[nodiscard]] color get_diffuse_lighting(const ShadingData& data) noexcept; + + [[nodiscard]] color get_refraction(const RefractionData& data) noexcept; +} // namespace Raychel + +#endif //!RAYCHEL_RENDER_UTILS_H diff --git a/include/Raychel/Render/Renderer.h b/include/Raychel/Render/Renderer.h new file mode 100644 index 0000000..e3552e3 --- /dev/null +++ b/include/Raychel/Render/Renderer.h @@ -0,0 +1,95 @@ +/** +* \file Renderer.h +* \author Weckyy702 (weckyy702@gmail.com) +* \brief Header file for Renderer class +* \date 2022-04-11 +* +* MIT License +* Copyright (c) [2022] [Weckyy702 (weckyy702@gmail.com | https://github.com/Weckyy702)] +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ +#ifndef RAYCHEL_RENDERER_H +#define RAYCHEL_RENDERER_H + +#include "Camera.h" +#include "Framebuffer.h" +#include "MaterialContainer.h" +#include "Raychel/Core/SDFContainer.h" + +#include +#include + +namespace Raychel { + + struct RenderOptions + { + //Size of the output image + Size2D output_size{1280, 720}; + + //Maximum number of steps until raymarching terminates + std::size_t max_ray_steps{1'024}; + + //Maximum depth for recursive algorithms + std::size_t max_recursion_depth{6}; + + //Maximum number of light bounces for indirect lighting + std::size_t max_lighting_bounces{2}; + + //Number of samples per pixel for rendering. Dramatically increases render times! + std::size_t samples_per_pixel{128}; + + //If antialiasing is used. (Low performance impact) + bool do_aa{true}; + + //How many threads are used for rendering. If 0, the library will choose + std::size_t thread_count{0}; + + //Maximum distance a ray can travel + double max_ray_depth{500}; + + //Maximum distance between the ray and a surface + double surface_epsilon{1e-6}; + //Radius used for normal calculation. Should be smaller than surface_epsilon to avoid weirdness + double normal_epsilon{1e-12}; + //Offset along the surface normal to avoid shadow weirdness. Should be larger than surface_epsilon + double shading_epsilon{1e-5}; + }; + + struct RenderState + { + const std::vector& surfaces; + const std::vector& materials; + BackgroundFunction get_background{}; + RenderOptions options{}; + }; + + struct RenderData + { + vec3 origin, direction; + + const RenderState& state; + std::size_t recursion_depth; + }; + + FatFramebuffer render_scene(const Scene& scene, const Camera& camera, const RenderOptions& options = {}) noexcept; + +} // namespace Raychel + +#endif //!RAYCHEL_RENDERER_H diff --git a/misc/generate_ziggurat_tables.py b/misc/generate_ziggurat_tables.py new file mode 100644 index 0000000..5e6708c --- /dev/null +++ b/misc/generate_ziggurat_tables.py @@ -0,0 +1,47 @@ +from math import * + +C = 128 +R = 3.442619855899 +V = 9.91256303526217e-3 + +zig_x = [0 for i in range(C+1)] +zig_r = [0 for i in range(C)] + +#C code: +#static void zigNorInit(int iC, double dR, double dV) +#{ +# int i; double f; +# f = exp(-0.5 * R * R); +# zig_x[0] = V / f; +# zig_x[1] = R; +# zig_x[iC] = 0; +# for (i = 2; i < C; ++i) +# { +# zig_x[i] = sqrt(-2 * log(V / zig_x[i - 1] + f)); +# f = exp(-0.5 * zig_x[i] * zig_x[i]); +# } +# for (i = 0; i < C; ++i) +# zig_r[i] = zig_x[i + 1] / zig_x[i]; +#} + +def main(): + f = exp(-0.5 * R * R) + zig_x[0] = V / f + zig_x[1] = R + zig_x[C] = 0 + for i in range(2, C): + zig_x[i] = sqrt(-2 * log(V / zig_x[i - 1] + f)) + f = exp(-0.5 * zig_x[i] * zig_x[i]) + for i in range(C): + zig_r[i] = zig_x[i + 1] / zig_x[i] + + print("X:") + for x in zig_x: + print(f"{x}, ") + + print("R: ") + for r in zig_r: + print(f"{r}, ") + +if __name__ == "__main__": + main() diff --git a/src/Core/Deserialize.cpp b/src/Core/Deserialize.cpp new file mode 100644 index 0000000..c23f8c7 --- /dev/null +++ b/src/Core/Deserialize.cpp @@ -0,0 +1,267 @@ +/** +* \file Deserialize.cpp +* \author Weckyy702 (weckyy702@gmail.com) +* \brief Implementation file for Deserialize class +* \date 2022-05-19 +* +* MIT License +* Copyright (c) [2022] [Weckyy702 (weckyy702@gmail.com | https://github.com/Weckyy702)] +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ + +#include "Raychel/Core/Deserialize.h" +#include "Raychel/Core/SDFPrimitives.h" + +namespace Raychel { + + struct DeserializerState + { + std::istream& input_stream; + const std::vector>& object_deserializers; + const std::vector>& material_deserializers; + + std::vector objects{}; + std::vector> object_serializers{}; + + std::vector materials{}; + std::vector> material_serializers{}; + + bool is_in_object_section{true}; + }; + + template + struct ContainerAndSerializer + { + Container container; + SerializableObjectData serializer; + }; + + template + ContainerAndSerializer(T, SerializableObjectData) -> ContainerAndSerializer; + + enum class ParseObjectFromLineResult { + ok = 0, + entered_material_section, + no_type_name_separator, + empty_line, + }; + + static bool check_object_header(std::istream& is) noexcept + { + std::string line{}; + std::getline(is, line); + + if (line == "--BEGIN SURFACES--") + return true; + + Logger::warn("Incorrect surface section header '", line, "'\n"); + return false; + } + + static std::string get_shortened_line(std::istream& is) noexcept + { + std::string line{}; + if (!std::getline(is, line)) + return {}; + + //skip whitespace characters + auto current_char = line.begin(); + while (std::isspace(*current_char) != 0) { + current_char++; + } + + return {current_char, line.end()}; + } + + template + static details::IDeserializer* find_deserializer_for( + std::string_view type_name, const std::vector>& deserializers) noexcept + { + for (const auto& deserializer : deserializers) { + if (deserializer->contained_type_name() == type_name) { + return deserializer.get(); + } + } + Logger::warn("Could not find deserializer for type name '", type_name, "'\n"); + return nullptr; + } + + template + static std::optional> parse_object( + std::string_view type_name, std::string_view rest_of_line, + const std::vector>& deserializers) noexcept + { + const auto maybe_deserializer = find_deserializer_for(type_name, deserializers); + if (maybe_deserializer == nullptr) + return std::nullopt; + + std::stringstream line_stream{std::string{rest_of_line}}; //Hmmmmmmmmmmmmmmmmmmm + auto maybe_object = maybe_deserializer->deserialize(line_stream, std::nullopt); + if (!maybe_object.has_value()) { + Logger::warn("Could not deserialize object of type '", type_name, " with data '", rest_of_line, "'\n"); + return std::nullopt; + } + return ContainerAndSerializer{std::move(maybe_object).value(), maybe_deserializer->get_serializer()}; + } + + template + static std::optional> parse_targeted( + DeserializerState& state, std::string_view type_name, std::string_view rest_of_line, + const std::vector>& deserializers) noexcept + { + const auto maybe_deserializer = find_deserializer_for(type_name, deserializers); + if (maybe_deserializer == nullptr) + return std::nullopt; + + auto [maybe_target_and_serializer, result] = parse_object_from_line(state, deserializers); + if (result != ParseObjectFromLineResult::ok) + return std::nullopt; + if (!maybe_target_and_serializer.has_value()) + return std::nullopt; + + Container target = std::move(maybe_target_and_serializer).value().container; + + std::stringstream line_stream{std::string{rest_of_line}}; + auto maybe_object = maybe_deserializer->deserialize(line_stream, std::move(target)); + if (!maybe_object.has_value()) { + Logger::warn("Could not deserialize object of type '", type_name, " with data '", rest_of_line, "'\n"); + return std::nullopt; + } + + return ContainerAndSerializer{std::move(maybe_object).value(), maybe_deserializer->get_serializer()}; + } + + template + static std::optional> parse_type( + DeserializerState& state, std::string_view type_name, std::string_view rest_of_line, + const std::vector>& deserializers) noexcept + { + //If the type is has a target, recursively resolve that target + if (type_name.ends_with("<>")) { + type_name.remove_suffix(2U); + return parse_targeted(state, type_name, rest_of_line, deserializers); + } + return parse_object(type_name, rest_of_line, deserializers); + } + + template + static bool add_object( + std::optional> maybe_object, std::vector& containers, + std::vector>& serializers) noexcept + { + if (!maybe_object.has_value()) + return false; + auto [container, serializer] = std::move(maybe_object).value(); + containers.emplace_back(std::move(container)); + serializers.emplace_back(std::move(serializer)); + return true; + } + + template + static std::pair>, ParseObjectFromLineResult> parse_object_from_line( + DeserializerState& state, const std::vector>& deserializers) noexcept + { + const auto line = get_shortened_line(state.input_stream); + + if (line.empty()) + return {std::nullopt, ParseObjectFromLineResult::empty_line}; + + if (line == "--BEGIN MATERIALS--") { + return {std::nullopt, ParseObjectFromLineResult::entered_material_section}; + } + + const auto end_of_type_name_index = line.find(" with "); + if (end_of_type_name_index == std::string::npos) { + Logger::warn("Incorrect type name separator!\n"); + return {std::nullopt, ParseObjectFromLineResult::no_type_name_separator}; + } + const auto end_of_type_name = line.begin() + static_cast(end_of_type_name_index); + std::string_view type_name{line.begin(), end_of_type_name}; + std::string_view rest_of_line{end_of_type_name + 6, line.end()}; + + return std::make_pair(parse_type(state, type_name, rest_of_line, deserializers), ParseObjectFromLineResult::ok); + } + + static bool parse_line(DeserializerState& state) noexcept + { + if (state.is_in_object_section) { + auto [maybe_object, result] = parse_object_from_line(state, state.object_deserializers); + if (result == ParseObjectFromLineResult::entered_material_section) { + state.is_in_object_section = false; + return true; + } + if (result == ParseObjectFromLineResult::empty_line) + return true; + if (result != ParseObjectFromLineResult::ok) + return false; + return add_object(std::move(maybe_object), state.objects, state.object_serializers); + } + auto [maybe_material, result] = parse_object_from_line(state, state.material_deserializers); + if (result == ParseObjectFromLineResult::entered_material_section) { + Logger::warn("Entered material section twice!\n"); + return false; + } + if (result == ParseObjectFromLineResult::empty_line) + return true; + if (result != ParseObjectFromLineResult::ok) { + return false; + } + return add_object(std::move(maybe_material), state.materials, state.material_serializers); + } + + static void place_dummy(DeserializerState& state) noexcept + { + if (state.is_in_object_section) { + state.objects.emplace_back(DeserializationErrorPlaceHolder{}); + state.object_serializers.emplace_back(details::SerializableObjectDescriptor{}); + return; + } + state.materials.emplace_back(DeserializationErrorMaterial{}); + state.material_serializers.emplace_back((details::SerializableObjectDescriptor{})); + } + + Scene deserialize_scene( + std::istream& is, const std::vector>& object_deserializers, + const std::vector>& material_deserializers) noexcept + { + DeserializerState state{is, object_deserializers, material_deserializers}; + + if (!check_object_header(is)) + return {}; + + while (state.input_stream) { + if (!parse_line(state)) { + Logger::debug("Placing dummy\n"); + place_dummy(state); + } + } + + if (state.is_in_object_section) { + Logger::warn("Parser did not leave object section! Incorrect material section header?\n"); + } + + return Scene::unsafe_from_data( + std::move(state.objects), + std::move(state.object_serializers), + std::move(state.materials), + std::move(state.material_serializers)); + } + +} //namespace Raychel diff --git a/src/Core/Raymarch.cpp b/src/Core/Raymarch.cpp new file mode 100644 index 0000000..4cd2f04 --- /dev/null +++ b/src/Core/Raymarch.cpp @@ -0,0 +1,87 @@ +/** +* \file Raymarch.cpp +* \author Weckyy702 (weckyy702@gmail.com) +* \brief Implementation file for Raymarch class +* \date 2022-06-11 +* +* MIT License +* Copyright (c) [2022] [Weckyy702 (weckyy702@gmail.com | https://github.com/Weckyy702)] +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ + +#include "Raychel/Core/Raymarch.h" +#include "Raychel/Core/SDFContainer.h" + +namespace Raychel { + + std::pair evaluate_distance_field(const std::vector& surfaces, const vec3& point) noexcept + { + const auto surfaces_size = surfaces.size(); + + double min_distance{1e9}; + auto hit_index = no_hit; + for (std::size_t i{}; i != surfaces_size; ++i) { + const auto surface_distance = std::abs(surfaces[i].evaluate(point)); + + if (surface_distance < min_distance) { + hit_index = i; + min_distance = surface_distance; + } + } + return {min_distance, hit_index}; + } + + RaymarchResult raymarch( + vec3 current_point, const vec3& direction, const std::vector& surfaces, RaymarchOptions options) noexcept + { + double depth{}; + std::size_t step{}; + while (step != options.max_ray_steps && depth < options.max_ray_depth) { + const auto [max_distance, hit_index] = evaluate_distance_field(surfaces, current_point); + if (max_distance < options.surface_epsilon) { + return {current_point, depth, step, hit_index}; + } + current_point += direction * max_distance; + depth += max_distance; + ++step; + } + return {current_point, depth, step, no_hit}; + } + + vec3 get_normal(const vec3& point, const SDFContainer& surface, double normal_offset) noexcept + { + //This implements the tetrahedon sampling technique found at https://iquilezles.org/articles/normalsSDF/ + constexpr vec3 xyy{1, -1, -1}, yyx{-1, -1, 1}, yxy{-1, 1, -1}, xxx{1, 1, 1}; + + if (surface.has_custom_normal()) { + return surface.get_normal(point); + } + + // clang-format off + return normalize(vec3{ + xyy*surface.evaluate(point+xyy*normal_offset) + + yyx*surface.evaluate(point+yyx*normal_offset) + + yxy*surface.evaluate(point+yxy*normal_offset) + + xxx*surface.evaluate(point+xxx*normal_offset) + }); + // clang-format on + } + +} // namespace Raychel diff --git a/src/Core/SDFPrimitives.cpp b/src/Core/SDFPrimitives.cpp new file mode 100644 index 0000000..1a7988a --- /dev/null +++ b/src/Core/SDFPrimitives.cpp @@ -0,0 +1,78 @@ +/** +* \file SDFPrimitives.cpp +* \author Weckyy702 (weckyy702@gmail.com) +* \brief Implementation file for SDFPrimitives class +* \date 2022-05-27 +* +* MIT License +* Copyright (c) [2022] [Weckyy702 (weckyy702@gmail.com | https://github.com/Weckyy702)] +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ + +#include "Raychel/Core/SDFPrimitives.h" + +namespace Raychel { + + bool do_serialize(std::ostream& os, const Sphere& object) noexcept + { + os << object.radius << '\n'; + return os.good(); + } + + std::optional do_deserialize(std::istream& is, DeserializationTag) noexcept + { + double radius{}; + if (!(is >> radius)) + return std::nullopt; + return Sphere{radius}; + } + + bool do_serialize(std::ostream& os, const Box& object) noexcept + { + os << object.size << '\n'; + return os.good(); + } + + std::optional do_deserialize(std::istream& is, DeserializationTag /*unused*/) noexcept + { + vec3 size{}; + if (!(is >> size)) + return std::nullopt; + + return Box{size}; + } + + bool do_serialize(std::ostream& os, const Plane& object) noexcept + { + os << object.normal << '\n'; + return os.good(); + } + + std::optional do_deserialize(std::istream& is, DeserializationTag) noexcept + { + vec3 normal{}; + if (!(is >> normal)) + return std::nullopt; + if (normal == vec3{}) + return std::nullopt; + + return Plane{normalize(normal)}; + } +} //namespace Raychel diff --git a/src/Core/Scene.cpp b/src/Core/Scene.cpp new file mode 100644 index 0000000..733beb7 --- /dev/null +++ b/src/Core/Scene.cpp @@ -0,0 +1,75 @@ +/** +* \file Scene.cpp +* \author Weckyy702 (weckyy702@gmail.com) +* \brief Implementation file for Scene class +* \date 2022-05-17 +* +* MIT License +* Copyright (c) [2022] [Weckyy702 (weckyy702@gmail.com | https://github.com/Weckyy702)] +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ + +#include "Raychel/Core/Scene.h" + +namespace Raychel { + + void Scene::remove_object(std::size_t _index) noexcept + { + using Diff = std::iter_difference_t; + if (_index >= objects_.size()) { + return; + } + + RAYCHEL_ASSERT(_index < std::numeric_limits::max()) + + const auto index = static_cast(_index); + + objects_.erase(objects_.begin() + index); + materials_.erase(materials_.begin() + index); + object_serializers_.erase(object_serializers_.begin() + index); + material_serializers_.erase(material_serializers_.begin() + index); + } + + Scene Scene::unsafe_from_data( + std::vector objects, std::vector> object_serializers, + std::vector materials, + std::vector> material_serializers) noexcept + { + //These all need to be the exact same size + if (!(objects.size() == object_serializers.size() && objects.size() == materials.size() && + objects.size() == material_serializers.size())) { + Logger::warn( + "Unable to create Scene from invalid data! Data sizes:" + "\n Objects: ", + objects.size(), + "\n Object serializers: ", + object_serializers.size(), + "\n Materials: ", + materials.size(), + "\n Material serializers: ", + material_serializers.size(), + '\n'); + return Scene{}; + } + + return Scene{std::move(objects), std::move(object_serializers), std::move(materials), std::move(material_serializers)}; + } + +} //namespace Raychel diff --git a/src/Core/Serialize.cpp b/src/Core/Serialize.cpp new file mode 100644 index 0000000..61c1d44 --- /dev/null +++ b/src/Core/Serialize.cpp @@ -0,0 +1,55 @@ +/** +* \file Serialize.cpp +* \author Weckyy702 (weckyy702@gmail.com) +* \brief Implementation file for Serialize class +* \date 2022-05-19 +* +* MIT License +* Copyright (c) [2022] [Weckyy702 (weckyy702@gmail.com | https://github.com/Weckyy702)] +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ + +#include "Raychel/Core/Serialize.h" +#include "Raychel/Core/Scene.h" + +namespace Raychel { + + bool serialize_scene(const Scene& scene, std::ostream& os) noexcept + { + std::size_t i{}; + os << "--BEGIN SURFACES--\n"; + for (const auto& serializer : scene.object_serializers()) { + if (!serializer.serialize(scene.objects().at(i), os)) + return false; + ++i; + } + + i = 0U; + os << "--BEGIN MATERIALS--\n"; + for (const auto& serializer : scene.material_serializers()) { + if (!serializer.serialize(scene.materials().at(i), os)) + return false; + ++i; + } + + return true; + } + +} //namespace Raychel diff --git a/src/Core/ZigguratNormal.cpp b/src/Core/ZigguratNormal.cpp new file mode 100644 index 0000000..1e6493a --- /dev/null +++ b/src/Core/ZigguratNormal.cpp @@ -0,0 +1,212 @@ +/** +* \file ZigguratNormal.cpp +* \author Weckyy702 (weckyy702@gmail.com) +* \brief Implementation for the Ziggurat algorithm to approximate a normal distribution (taken from https://www.jstatsoft.org/article/download/v005i08/623) +* \date 2022-04-24 +* +* MIT License +* Copyright (c) [2022] [Weckyy702 (weckyy702@gmail.com | https://github.com/Weckyy702)] +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ + +#include "Raychel/Core/ZigguratNormal.h" +#include "Raychel/Core/Xoroshiro128+.h" + +#include "RaychelCore/Raychel_assert.h" +#include "RaychelMath/math.h" + +#include +#include +#include +#include + +namespace Raychel { + + namespace details { + + constexpr auto R = 3.6541528853610088; + + constexpr static std::array X_table{ + 3.7130862467425505, 3.442619855899, 3.2230849845811416, + 3.0832288582168683, 2.9786962526477803, 2.894344007021529, + 2.8231253505489105, 2.761169372387177, 2.7061135731218195, + 2.6564064112613597, 2.6109722484318474, 2.569033625924938, + 2.5300096723888275, 2.493454522095372, 2.4590181774118305, + 2.42642064553375, 2.3954342780110625, 2.3658713701176386, + 2.3375752413392368, 2.310413683698763, 2.2842740596774718, + 2.2590595738691985, 2.2346863955909795, 2.2110814088787034, + 2.188180432076049, 2.165926793748922, 2.1442701823603953, + 2.1231657086739766, 2.1025731351892385, 2.082456237992017, + 2.0627822745083084, 2.0435215366550676, 2.0246469733773855, + 2.006133869963472, 1.98795957412762, 1.9701032608543265, + 1.9525457295535567, 1.9352692282966228, 1.9182573008645099, + 1.901494653105151, 1.884967035707759, 1.8686611409944887, + 1.8525645117280911, 1.836665460258446, 1.8209529965961255, + 1.8054167642192285, 1.7900469825998586, 1.7748343955860695, + 1.7597702248995934, 1.7448461281138004, 1.7300541605637305, + 1.7153867407136676, 1.7008366185699169, 1.6863968467791681, + 1.672060754097601, 1.6578219209540241, 1.6436741568628686, + 1.6296114794706347, 1.615628095043161, 1.6017183802213781, + 1.5878768648905761, 1.5740982160230008, 1.560377222366169, + 1.5467087798599104, 1.5330878776740433, 1.5195095847659401, + 1.5059690368632033, 1.492461423781354, 1.4789819769899242, + 1.4655259573427108, 1.4520886428892246, 1.4386653166845635, + 1.42525125451406, 1.4118417124470577, 1.3984319141310053, + 1.3850170377326518, 1.3715922024273426, 1.3581524543301435, + 1.344692751753547, 1.3312079496656273, 1.317692783209414, + 1.3041418501286168, 1.2905495919261964, 1.2769102735601556, + 1.263217961454621, 1.2494664995730682, 1.2356494832633627, + 1.2217602305399964, 1.2077917504159497, 1.1937367078331287, + 1.1795873846639882, 1.1653356361647524, 1.1509728421488674, + 1.1364898520131608, 1.1218769225825422, 1.107123647534036, + 1.0922188769072774, 1.0771506248928957, 1.0619059636948243, + 1.0464709007640454, 1.0308302360681956, 1.0149673952513305, + 0.9988642334929836, 0.982500803515429, 0.9658550794011499, + 0.9489026255113064, 0.9316161966151508, 0.9139652510230323, + 0.8959153525809377, 0.8774274291129234, 0.8584568431938132, + 0.8389522142975774, 0.8188539067003573, 0.7980920606440569, + 0.7765839878947599, 0.7542306644540556, 0.7309119106424888, + 0.7064796113354365, 0.6807479186691546, 0.6534786387399752, + 0.6243585973360507, 0.5929629424714483, 0.5586921784081852, + 0.5206560387620606, 0.4774378372966898, 0.4265479863554235, + 0.36287143109703196, 0.27232086481396467, 0.0, + }; + + constexpr static std::array R_table{ + 0.9271586026096681, 0.9362302895738892, 0.9566079929529229, 0.9660963845448882, + 0.971681487982781, 0.9753938521821022, 0.9780541171685178, 0.980060694640489, + 0.9816315315239645, 0.9828963811271866, 0.9839375456663325, 0.9848098704733534, + 0.9855513792328944, 0.9861893030819736, 0.9867436799867864, 0.9872295978111943, + 0.9876586437103296, 0.9880398701570176, 0.9883804563121089, 0.9886861715693078, + 0.9889617072428545, 0.9892109183130244, 0.9894370025436909, 0.9896426351781105, + 0.9898300715969688, 0.9900012265183524, 0.9901577357834697, 0.9903010050508025, + 0.9904322485336944, 0.9905525200843218, 0.9906627383358567, 0.9907637071892196, + 0.9908561326209719, 0.9909406365607181, 0.991017768416579, 0.9910880146997187, + 0.991151807102165, 0.991209529308185, 0.9912615227624552, 0.9913080915739614, + 0.9913495066999154, 0.9913860095266759, 0.9914178149430195, 0.9914451139838447, + 0.9914680761085329, 0.9914868511670121, 0.9915015710974835, 0.9915123513923666, + 0.9915192923629307, 0.9915224802280646, 0.9915219880484646, 0.9915178765240442, + 0.9915101946694387, 0.9914989803800052, 0.9914842608986051, 0.9914660531916395, + 0.9914443642412228, 0.9914191912590011, 0.9913905218258715, 0.9913583339607497, + 0.9913225961204966, 0.9912832671321499, 0.9912402960576856, 0.991193621990624, + 0.991143173782899, 0.991088869699481, 0.9910306169972894, 0.9909683114239041, + 0.9909018366304913, 0.9908310634921467, 0.9907558493275227, 0.9906760370080955, + 0.9905914539457294, 0.9905019109452362, 0.9904072009063883, 0.990307097357238, + 0.990201352797563, 0.9900896968277136, 0.9899718340339569, 0.9898474415964779, + 0.9897161665803526, 0.9895776228628198, 0.9894313876418468, 0.9892769974609422, + 0.9891139436730952, 0.9889416672520418, 0.9887595528412437, 0.9885669219091597, + 0.9883630248526034, 0.9881470318569457, 0.9879180222809051, 0.987674972282531, + 0.9874167403388364, 0.9871420502305995, 0.9868494709610887, 0.9865373929461655, + 0.986203999644239, 0.9858472335755389, 0.98546475539409, 0.9850538942989907, + 0.9846115875710347, 0.9841343063494573, 0.9836179638544746, 0.9830578010168337, + 0.9824482427525728, 0.9817827157061126, 0.9810534148544756, 0.9802510014227667, + 0.9793642073274506, 0.9783793105963312, 0.9772794298852922, 0.9760435609386315, + 0.9746452378300764, 0.9730506368752245, 0.9712158326862985, 0.9690827290502092, + 0.9665728537853818, 0.9635775863118795, 0.959942176565901, 0.9554384188286962, + 0.9497153478809163, 0.9422042060159378, 0.9319193267489506, 0.9169927970716931, + 0.8934105197245976, 0.8507165493794344, 0.7504610213889943, 0.0, + }; + + //NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) + static thread_local std::uint32_t xorshift32_seed{2463534242U}; + [[nodiscard]] std::uint32_t random32() noexcept + { + xorshift32_seed ^= (xorshift32_seed << 13U); + xorshift32_seed ^= (xorshift32_seed >> 17U); + xorshift32_seed ^= (xorshift32_seed << 5U); + + return xorshift32_seed; + } + + //NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) + static thread_local std::uint64_t xorshift64_seed{88172645463325252U}; + [[nodiscard]] std::uint64_t random64() noexcept + { + xorshift64_seed ^= (xorshift64_seed << 13U); + xorshift64_seed ^= (xorshift64_seed >> 7U); + xorshift64_seed ^= (xorshift64_seed << 17U); + + return xorshift64_seed; + } + + //Fast way to generate "uniform" reals in the range [0; 1]. Credit to Inigo Quilez (https://iquilezles.org/articles/sfrand/) + [[nodiscard]] double uniform01() noexcept + { + //The idea is to generate 52 random bits for the mantissa and fix the exponent to 1023 to generate a random number between 1 and 2. + //We can then easily map that number into the [0; 1] range by subtracting 1 + static_assert(sizeof(std::uint64_t) == sizeof(double)); + + constexpr std::uint64_t fix_exponent{0x3FF0000000000000}; + + const auto mantissa = (random64() >> 12U); + + return std::bit_cast(mantissa | fix_exponent) - 1.0; + } + + [[nodiscard]] static double normal_tail(bool is_negative) noexcept + { + double x{}; + double y{}; + std::size_t guard{}; + do { + x = std::log(uniform01()) / R; + y = std::log(uniform01()); + + RAYCHEL_ASSERT(++guard != 16U); + } while (-2 * y < sq(x)); + + if (is_negative) { + return x - R; + } + return R - x; + } + } // namespace details + + double uniform_random() noexcept + { + return details::uniform01() * 2 - 1; + } + + double ziggurat_normal() noexcept + { + //The guard variable is just to prevent an infinte loop. It's not gonna matter in practice + for (std::size_t guard{}; guard != 16U; ++guard) { + const auto u = 2.0 * details::uniform01() - 1.0; + const auto i = details::random32() & 0x7FU; + /* first try the rectangular boxes */ + if (std::abs(u) < details::R_table.at(i)) { + return u * details::X_table.at(i); + } + /* bottom box: sample from the tail */ + if (i == 0) { + return details::normal_tail(u < 0); + } + /* is this a sample from the wedges? */ + const auto x = u * details::X_table.at(i); + const auto f0 = std::exp(-0.5 * (sq(details::X_table.at(i)) - sq(x))); + const auto f1 = std::exp(-0.5 * (sq(details::X_table.at(i + 1)) - sq(x))); + if (f1 + details::uniform01() * (f0 - f1) < 1.0) { + return x; + } + } + RAYCHEL_ASSERT_NOT_REACHED; + } + +} //namespace Raychel diff --git a/src/Render/Denoise.cpp b/src/Render/Denoise.cpp new file mode 100644 index 0000000..509dd86 --- /dev/null +++ b/src/Render/Denoise.cpp @@ -0,0 +1,389 @@ +/** +* \file Denoise.cpp +* \author Weckyy702 (weckyy702@gmail.com) +* \brief Implementation of the Ray Histogram Fusion algorithm by Mauricio Delbracio et al. +* \date 2022-05-02 +* +* MIT License +* Copyright (c) [2022] [Weckyy702 (weckyy702@gmail.com | https://github.com/Weckyy702)] +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ + +#include "Raychel/Render/Denoise.h" + +#include +#include +#include + +namespace Raychel { + + static std::size_t to_index(std::size_t x, std::size_t y, std::size_t image_width) + { + return x + (y * image_width); + } + + static std::size_t safe_sub(std::size_t a, std::size_t b) + { + if (b > a) { + return 0U; + } + return a - b; + } + + template + static double chi_squared_distance(const std::array& a, const std::array& b) + { + double sum{}; + double num_nonempty_bins{}; + + for (std::size_t i{}; i != NumBins; ++i) { + const auto divisor = a[i] + b[i]; + if (divisor != 0.0) { + sum += sq(a[i] - b[i]) / divisor; + ++num_nonempty_bins; + } + } + + if (num_nonempty_bins == 0.0) { + return 0.0; + } + + return sum / num_nonempty_bins; + } + + template + static auto chi_squared_distance(const RayHistogram& a, const RayHistogram& b) + { + //TODO: process the three channels in one loop + return Tuple{ + chi_squared_distance(a.red_channel(), b.red_channel()), + chi_squared_distance(a.green_channel(), b.green_channel()), + chi_squared_distance(a.blue_channel(), b.blue_channel())}; + } + + struct SearchWindow + { + [[nodiscard]] std::size_t width() const noexcept + { + return end_x - start_x; + } + + [[nodiscard]] std::size_t height() const noexcept + { + return end_y - start_y; + } + + [[nodiscard]] std::size_t area() const noexcept + { + return width() * height(); + } + + std::size_t start_x{}, start_y{}, end_x{}, end_y{}; + }; + + using Patch = SearchWindow; + + static SearchWindow + search_window_for_pixel(std::size_t x, std::size_t y, Size2D image_size, std::size_t half_search_window_size) noexcept + { + const auto start_x = safe_sub(x, half_search_window_size); + const auto start_y = safe_sub(y, half_search_window_size); + + const auto end_x = std::min(x + half_search_window_size, image_size.x()); + const auto end_y = std::min(y + half_search_window_size, image_size.y()); + + return {start_x, start_y, end_x, end_y}; + } + + static Patch patch_for_pixel(std::size_t x, std::size_t y, Size2D image_size, std::size_t half_patch_size) noexcept + { + return search_window_for_pixel(x, y, image_size, half_patch_size); + } + + static std::vector get_denoised_patch_with_search_window( + const SearchWindow& search_window, const Patch& this_patch, const std::vector& input_pixels, + const Size2D image_size, const DenoisingOptions& options) noexcept + { + std::vector> c{this_patch.area()}; + std::vector V(this_patch.area()); + + for (auto search_y = search_window.start_y; search_y != search_window.end_y; ++search_y) { + for (auto search_x = search_window.start_x; search_x != search_window.end_x; ++search_x) { + + const auto& other_patch = patch_for_pixel(search_x, search_y, image_size, options.half_patch_size); + + for (auto this_y = this_patch.start_y; this_y != this_patch.end_y; ++this_y) { + for (auto this_x = this_patch.start_x; this_x != this_patch.end_x; ++this_x) { + const auto index_in_image = to_index(this_x, this_y, image_size.x()); + const auto& this_pixel = input_pixels.at(index_in_image); + const auto index_in_patch = + to_index(this_x - this_patch.start_x, this_y - this_patch.start_y, this_patch.width()); + + for (auto other_y = other_patch.start_y; other_y != other_patch.end_y; ++other_y) { + for (auto other_x = other_patch.start_x; other_x != other_patch.end_x; ++other_x) { + + const auto& other_pixel = input_pixels.at(to_index(other_x, other_y, image_size.x())); + const auto d = chi_squared_distance(this_pixel.histogram, other_pixel.histogram); + + for (std::size_t i{}; i != 3U; ++i) { + if (d[i] < options.distance_threshold) { + V.at(index_in_patch)[i] += other_pixel.noisy_color[i]; + ++c.at(index_in_patch)[i]; + } + } + } + } + } + } + } + } + + for (std::size_t i{}; i != V.size(); ++i) { + for (std::size_t j{}; j != 3U; ++j) { + if (c.at(i)[j] != 0) { + V.at(i)[j] /= c.at(i)[j]; + } + } + } + return V; + } + + static void denoise_part( + std::vector& output, Size2D begin, Size2D end, const std::vector& input_pixels, Size2D image_size, + DenoisingOptions options) noexcept + { + std::vector N(input_pixels.size()); + + for (auto y = begin.y(); y != end.y(); ++y) { + for (auto x = begin.x(); x != end.x(); ++x) { + const auto this_patch = patch_for_pixel(x, y, image_size, options.half_patch_size); + const auto search_window = search_window_for_pixel(x, y, image_size, options.half_search_window_size); + + const auto V = + get_denoised_patch_with_search_window(search_window, this_patch, input_pixels, image_size, options); + + for (auto patch_y = this_patch.start_y; patch_y != this_patch.end_y; ++patch_y) { + for (auto patch_x = this_patch.start_x; patch_x != this_patch.end_x; ++patch_x) { + const auto index_in_patch = + to_index(patch_x - this_patch.start_x, patch_y - this_patch.start_y, this_patch.width()); + const auto index_in_image = to_index(patch_x, patch_y, image_size.x()); + + ++N.at(index_in_image); + + auto& p = output.at(index_in_image); + p += (V.at(index_in_patch) - p) / N.at(index_in_image); + } + } + } + } + } + + template < + typename Pixel, std::invocable Add = std::plus, + std::invocable Divide = std::divides> + details::BasicFramebuffer gaussian_subsample( + const details::BasicFramebuffer& input_pixels, std::size_t scale, Add&& adder = {}, Divide&& divider = {}) + { + //Because we use operator<<, specifying a scale larger than the number of bits in an unsigned int would result in UB. + RAYCHEL_ASSERT(scale < (sizeof(unsigned int) * 8)); + + if (scale == 0U) { + return input_pixels; + } + + const auto half_sample_window_size = (1U << (scale - 1)); + const auto scaling_factor = std::pow(0.5, static_cast(scale)); + const auto pixel_step = 1U << scale; + const auto scaled_size = input_pixels.size * scaling_factor; + + if (scaled_size == Size2D{}) { + return {}; + } + + std::vector output_pixels{scaled_size.x() * scaled_size.y(), Pixel{}}; + + for (std::size_t y{}; y != scaled_size.y(); ++y) { + for (std::size_t x{}; x != scaled_size.x(); ++x) { + const auto sample_patch = + patch_for_pixel(x * pixel_step, y * pixel_step, input_pixels.size, half_sample_window_size); + + std::size_t num_samples{}; + Pixel output_pixel{}; + + for (auto patch_y = sample_patch.start_y; patch_y != sample_patch.end_y; ++patch_y) { + for (auto patch_x = sample_patch.start_x; patch_x != sample_patch.end_x; ++patch_x) { + ++num_samples; + const auto& sample_pixel = input_pixels.pixel_data.at(to_index(patch_x, patch_y, input_pixels.size.x())); + + output_pixel = adder(output_pixel, sample_pixel); + } + } + output_pixel = divider(output_pixel, num_samples); + + output_pixels.at(to_index(x, y, scaled_size.x())) = output_pixel; + } + } + + return {scaled_size, output_pixels}; + } + + static void add_scaled_part( + const Raychel::Framebuffer& scaled_input, std::vector& output, std::size_t scale, std::size_t num_scales) + { + using namespace Raychel; + RAYCHEL_ASSERT(scale < (sizeof(unsigned int) * 8)); + + if (scale == 0U) { + std::copy(scaled_input.pixel_data.begin(), scaled_input.pixel_data.end(), output.begin()); + return; + } + + const auto scaling_factor = 1U << scale; + const auto step_size = std::pow(0.5, static_cast(scale)); + const auto correction_factor = 0.5 / static_cast(num_scales); + + for (std::size_t y{}; y != scaled_input.size.y(); ++y) { + for (std::size_t x{}; x != scaled_input.size.x(); ++x) { + const auto next_x = std::min(x + 1U, scaled_input.size.x() - 1U); + const auto next_y = std::min(y + 1U, scaled_input.size.y() - 1U); + + const auto top_left = scaled_input.at(x, y); + const auto top_right = scaled_input.at(next_x, y); + const auto bottom_left = scaled_input.at(x, next_y); + const auto bottom_right = scaled_input.at(next_x, next_y); + + const auto large_x = x * scaling_factor; + const auto large_y = y * scaling_factor; + + for (std::size_t part_y{}; part_y != scaling_factor; ++part_y) { + for (std::size_t part_x{}; part_x != scaling_factor; ++part_x) { + const auto relative_x = static_cast(part_x) * step_size; + const auto relative_y = static_cast(part_y) * step_size; + + const auto right_weight = std::fmod(relative_x * static_cast(scaling_factor), 1.0); + const auto left_weight = 1.0 - right_weight; + const auto bottom_weight = std::fmod(relative_y * static_cast(scaling_factor), 1.0); + const auto top_weight = 1.0 - bottom_weight; + + // clang-format off + output.at(to_index(large_x + part_x, large_y + part_y, scaled_input.size.x() * scaling_factor)) += ( + (top_left * (top_weight * left_weight)) + + (top_right * (top_weight * right_weight)) + + (bottom_left * (top_weight * left_weight)) + + (bottom_right * (bottom_weight * right_weight)) + ) * correction_factor; + // clang-format on + } + } + } + } + } + + static void denoise_threaded( + std::vector& output, const FatFramebuffer& input_pixels, unsigned int num_threads, + DenoisingOptions options) noexcept + { + constexpr Size2D patch_size{128, 128}; + + std::vector thread_pool{}; + thread_pool.reserve(num_threads); + + std::atomic_size_t current_patch_index{0U}; + + const auto num_patches_x = + static_cast(std::ceil(static_cast(input_pixels.size.x()) / static_cast(patch_size.x()))); + const auto num_patches_y = + static_cast(std::ceil(static_cast(input_pixels.size.y()) / static_cast(patch_size.y()))); + const auto max_patch_index = num_patches_x * num_patches_y - 1U; + + for (std::size_t i{}; i != num_threads; ++i) { + thread_pool.emplace_back([&, i] { + std::size_t patch_index{}; + do { + patch_index = current_patch_index.fetch_add(1U); + + const auto x_index = patch_index % num_patches_x; + const auto y_index = patch_index / num_patches_x; + + const Size2D patch_begin{ + std::min(x_index * patch_size.x(), input_pixels.size.x()), + std::min(y_index * patch_size.y(), input_pixels.size.y())}; + const Size2D patch_end{ + std::min(patch_begin.x() + patch_size.x(), input_pixels.size.x()), + std::min(patch_begin.y() + patch_size.y(), input_pixels.size.y())}; + + Logger::debug("Thread ", i, " got patch ", patch_index, " from ", patch_begin, " to ", patch_end, '\n'); + + denoise_part(output, patch_begin, patch_end, input_pixels.pixel_data, input_pixels.size, options); + } while (patch_index < max_patch_index); + }); + } + } + + void denoise_internal(std::vector& output, const FatFramebuffer& input_pixels, DenoisingOptions options) noexcept + { + const auto num_threads = std::thread::hardware_concurrency(); + + if (num_threads == 0U) { + denoise_part(output, Size2D{}, input_pixels.size, input_pixels.pixel_data, input_pixels.size, options); + return; + } + + denoise_threaded(output, input_pixels, num_threads, options); + } + + Framebuffer denoise_single_scale(const FatFramebuffer& input_pixels, DenoisingOptions options) noexcept + { + std::vector output(input_pixels.pixel_data.size()); + + denoise_internal(output, input_pixels, options); + + return {input_pixels.size, std::move(output)}; + } + + Framebuffer denoise_multiscale(const FatFramebuffer& input_pixels, DenoisingOptions options) noexcept + { + if (options.num_scales == 1U) { + return denoise_single_scale(input_pixels, options); + } + + RAYCHEL_TODO("Multiscale denoising"); + + auto scale = options.num_scales - 1U; + + std::vector u_old{}; + + while (scale != 0) { + //uS = Ds(u) + const auto scaled_input = gaussian_subsample(input_pixels, scale); + //TODO: verify that the number of samples is still the same + + std::vector scaled_output{scaled_input.pixel_data.size(), color{}}; + denoise_internal(scaled_output, scaled_input, options); + + add_scaled_part({input_pixels.size, scaled_output}, u_old, scale, options.num_scales); + + --scale; + } + + return {input_pixels.size, std::move(u_old)}; + } + +} // namespace Raychel diff --git a/src/Render/RenderUtils.cpp b/src/Render/RenderUtils.cpp new file mode 100644 index 0000000..814bbdc --- /dev/null +++ b/src/Render/RenderUtils.cpp @@ -0,0 +1,260 @@ +/** +* \file RenderUtils.cpp +* \author Weckyy702 (weckyy702@gmail.com) +* \brief Implementation file for RenderUtils class +* \date 2022-04-12 +* +* MIT License +* Copyright (c) [2022] [Weckyy702 (weckyy702@gmail.com | https://github.com/Weckyy702)] +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ + +#include "Raychel/Render/RenderUtils.h" +#include +#include "Raychel/Core/Raymarch.h" +#include "Raychel/Core/ZigguratNormal.h" + +#include "RaychelMath/vector.h" + +namespace Raychel { + + color get_shaded_color(const RenderData& data) noexcept + { + const auto get_background_color = [&] { + if (data.state.get_background) { + return data.state.get_background(data); + } + return color{data.direction.x(), data.direction.y(), data.direction.z()}; + }; + + if (data.recursion_depth >= data.state.options.max_recursion_depth) { + return get_background_color(); + } + + const auto& [surfaces, materials, _, options] = data.state; + const auto result = raymarch( + data.origin, data.direction, surfaces, {options.max_ray_steps, options.max_ray_depth, options.surface_epsilon}); + + if (result.hit_index == no_hit) { + return get_background_color(); + } + + const auto surface_normal = get_normal(result.point, surfaces[result.hit_index], options.normal_epsilon); + RAYCHEL_ASSERT(equivalent(mag_sq(surface_normal), 1.0)); + + return data.state.materials[result.hit_index].get_surface_color( + {.position = result.point + surface_normal * options.shading_epsilon, + .normal = surface_normal, + .incoming_direction = data.direction, + .state = data.state, + .recursion_depth = data.recursion_depth + 1U}); + } + + static vec3 get_random_direction_on_weighted_hemisphere(const vec3& normal) noexcept + { + vec3 test{}; + do { + test = normal + vec3{ziggurat_normal(), ziggurat_normal(), ziggurat_normal()}; + } while (test == vec3{}); + + test = normalize(test); + + if (dot(test, normal) < 0.0) { + test *= -1; + } + return test; + } + + color get_diffuse_lighting(const ShadingData& data) noexcept + { + const auto direction = get_random_direction_on_weighted_hemisphere(data.normal); + return get_shaded_color(RenderData{ + .origin = data.position, + .direction = direction, + .state = data.state, + .recursion_depth = std::max( + data.state.options.max_recursion_depth - data.state.options.max_lighting_bounces, data.recursion_depth)}) * + dot(direction, data.normal); + } + + [[nodiscard]] static double fresnel(const vec3& direction, vec3 normal, double interior_ior, double exterior_ior) + { + RAYCHEL_ASSERT(equivalent(mag_sq(direction), 1.0)); + RAYCHEL_ASSERT(equivalent(mag_sq(normal), 1.0)); + + auto cosi = dot(direction, normal); + + if (cosi > 0.0) { + std::swap(interior_ior, exterior_ior); + normal *= -1.0; + } else { + cosi *= -1.0; + } + + const auto etai = exterior_ior; + const auto etat = interior_ior; + + const auto sint = (exterior_ior / interior_ior) * std::sqrt(std::max(0.0, 1.0 - sq(cosi))); + + if (sint >= 1.0) { + return 1.0; //TIR + } + + const auto cost = std::sqrt(1.0 - sq(sint)); + + const auto Rs = ((etat * cosi) - (etai * cost)) / ((etat * cosi) + (etai * cost)); + const auto Rp = ((etai * cosi) - (etat * cost)) / ((etai * cosi) + (etat * cost)); + + return std::clamp((sq(Rs) + sq(Rp)) * 0.5, 0.0, 1.0); + } + + [[nodiscard]] static vec3 refract(const vec3& direction, vec3 normal, double interior_ior, double exterior_ior) + { + RAYCHEL_ASSERT(equivalent(mag_sq(direction), 1.0)); + RAYCHEL_ASSERT(equivalent(mag_sq(normal), 1.0)); + + auto cosi = dot(direction, normal); + + if (cosi > 0.0) { + std::swap(interior_ior, exterior_ior); + normal *= -1.0; + } else { + cosi *= -1.0; + } + + const auto eta = exterior_ior / interior_ior; + + const auto k = 1.0 - sq(eta) * (1.0 - sq(cosi)); + if (k < 0.0) { + return vec3{}; //TIR + } + return normalize((direction * eta) + (normal * (eta * cosi - std::sqrt(k)))); + } + + static std::size_t get_surrounding_object(const std::vector& surfaces, const vec3& point) noexcept + { + std::size_t hit_index{no_hit}; + double min_distance{-1e9}; + for (std::size_t i{}; i != surfaces.size(); ++i) { + const auto object_distance = surfaces[i].evaluate(point); + if (object_distance < 0.0 && object_distance > min_distance) { + min_distance = object_distance; + hit_index = i; + } + } + return hit_index; + } + + static double get_surrounding_ior( + const vec3& surface_point, const std::vector& surfaces, const std::vector& materials) + { + const auto closest_object_index = get_surrounding_object(surfaces, surface_point); + if (closest_object_index == no_hit) { + return 1.0; + } + return materials[closest_object_index].get_material_ior(); + } + + [[nodiscard]] static color get_reflective_component(const RefractionData& data, double reflection_factor) noexcept + { + if (reflection_factor < 0.01) { + return color{}; + } + return get_shaded_color( + {.origin = data.surface_point, + .direction = reflect(data.incoming_direction, data.normal), + .state = data.state, + .recursion_depth = data.recursion_depth}) * + reflection_factor; + } + + [[nodiscard]] static color get_refractive_component(const RefractionData& data, double ior_factor, double outer_ior) noexcept + { + const auto trace_direction = refract(data.incoming_direction, data.normal, data.material_ior * ior_factor, outer_ior); + const auto trace_origin = data.surface_point - ((2.0 * data.state.options.shading_epsilon) * data.normal); + + if (trace_direction == vec3{}) { + Logger::warn("Did not expect to reach ", __FILE__, ':', __LINE__, '\n'); + return color{0}; + } + + const auto& options = data.state.options; + const auto result = raymarch( + trace_origin, + trace_direction, + data.state.surfaces, + {options.max_ray_steps, options.max_ray_depth, options.surface_epsilon}); + + // RAYCHEL_ASSERT(result.hit_index != no_hit) + if (result.hit_index == no_hit) { + return color{0}; + } + + auto opposite_normal = get_normal(result.point, data.state.surfaces[result.hit_index], options.normal_epsilon); + const auto opposite_shading_point = result.point + (opposite_normal * options.shading_epsilon); + const auto out_direction = refract( + trace_direction, + opposite_normal, + data.material_ior, + get_surrounding_ior(data.surface_point, data.state.surfaces, data.state.materials)); + + //Total internal reflection + if (out_direction == vec3{}) { + return get_shaded_color(RenderData{ + .origin = opposite_shading_point, + .direction = reflect(trace_direction, opposite_normal), + .state = data.state, + .recursion_depth = data.recursion_depth}); + } + + return get_shaded_color(RenderData{ + .origin = opposite_shading_point, + .direction = out_direction, + .state = data.state, + .recursion_depth = data.recursion_depth}); + } + + [[nodiscard]] static color get_refractive_component(const RefractionData& data, double refraction_factor) noexcept + { + if (refraction_factor < 0.01) { + return color{}; + } + + const auto outer_ior = get_surrounding_ior(data.surface_point, data.state.surfaces, data.state.materials); + if (data.ior_variation == 0.0) { + return get_refractive_component(data, 1.0, outer_ior) * refraction_factor; + } + return color{ + get_refractive_component(data, 1.0 - data.ior_variation, outer_ior).r(), + get_refractive_component(data, 1, outer_ior).g(), + get_refractive_component(data, 1.0 + data.ior_variation, outer_ior).b(), + } * + refraction_factor; + } + + color get_refraction(const RefractionData& data) noexcept + { + const auto outer_ior = get_surrounding_ior(data.surface_point, data.state.surfaces, data.state.materials); + + const auto reflection_factor = fresnel(data.incoming_direction, data.normal, data.material_ior, outer_ior); + + return get_reflective_component(data, reflection_factor) + get_refractive_component(data, 1.0 - reflection_factor); + } +} // namespace Raychel diff --git a/src/Render/Renderer.cpp b/src/Render/Renderer.cpp new file mode 100644 index 0000000..8a49b56 --- /dev/null +++ b/src/Render/Renderer.cpp @@ -0,0 +1,234 @@ +/** +* \file Renderer.cpp +* \author Weckyy702 (weckyy702@gmail.com) +* \brief Implementation file for Renderer class +* \date 2022-04-11 +* +* MIT License +* Copyright (c) [2022] [Weckyy702 (weckyy702@gmail.com | https://github.com/Weckyy702)] +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +*/ + +#include "Raychel/Render/Renderer.h" +#include "Raychel/Core/Scene.h" +#include "Raychel/Core/ZigguratNormal.h" +#include "Raychel/Render/FatPixel.h" +#include "Raychel/Render/RenderUtils.h" + +#include "RaychelCore/ScopedTimer.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace Raychel { + + struct CacheComparator + { + constexpr bool operator()(const auto& lhs, const auto& rhs) const + { + if (lhs.first < rhs.first) { + return true; + } + return std::lexicographical_compare(lhs.second.begin(), lhs.second.end(), rhs.second.begin(), rhs.second.end()); + } + }; + + struct CacheResult + { + std::vector& data; + bool data_was_in_cache{false}; + }; + + static CacheResult get_cached_ray_data(const Camera& camera, const RenderOptions options) noexcept + { + using Key = std::pair; + + static std::map, CacheComparator> ray_cache{}; + + const Key cache_key{camera.zoom, options.output_size}; + + if (const auto it = ray_cache.find(cache_key); it != ray_cache.end()) { + Logger::debug("Found cached ray data for zoom=", camera.zoom, ", size=", options.output_size, '\n'); + return {it->second, true}; + } + + Logger::debug("Cache not populated. Generating rays for ", options.output_size, " plane\n"); + return {ray_cache[cache_key], false}; + } + + static void generate_rays_internal(std::vector& rays, const Camera& camera, const RenderOptions options) noexcept + { + using vec2 = basic_vec2; + constexpr vec3 right{1, 0, 0}; + constexpr vec3 up{0, 1, 0}; + constexpr vec3 forward{0, 0, 1}; + + const auto get_relative_coordinates = [&options](const vec2 pixel_coordinate) { + const auto [plane_x, plane_y] = options.output_size; + + const auto aspect_ratio = static_cast(plane_x) / static_cast(plane_y); + + const auto raw_relative_x = pixel_coordinate.x() / static_cast(plane_x) - 0.5; + const auto raw_relative_y = pixel_coordinate.y() / static_cast(plane_y) - 0.5; + if (aspect_ratio > 1.0) { + return vec2{raw_relative_x * aspect_ratio, raw_relative_y}; + } + return vec2{raw_relative_x, raw_relative_y / aspect_ratio}; + }; + + rays.reserve(options.output_size.y() * options.output_size.x()); + + for (std::size_t y{options.output_size.y()}; y != 0U; --y) { + for (std::size_t x{}; x != options.output_size.x(); ++x) { + const auto [relative_x, relative_y] = get_relative_coordinates(vec2{x, y}); + + // clang-format off + const auto direction = normalize( (right * relative_x) + + (up * relative_y) + + (forward * camera.zoom)); + // clang-format on + + rays.emplace_back(direction); + } + } + } + + static const std::vector& generate_rays(const Camera& camera, const RenderOptions& options) noexcept + { + const auto [rays, was_cached] = get_cached_ray_data(camera, options); + + if (was_cached) { + return rays; + } + + generate_rays_internal(rays, camera, options); + + return rays; + } + + [[nodiscard]] static vec3 get_direction_with_aa(const vec3& direction, const Size2D& output_size) noexcept + { + const vec3 jitter{ + uniform_random() / static_cast(output_size.x()), uniform_random() / static_cast(output_size.y())}; + + return normalize(direction + jitter); + } + + [[maybe_unused]] static void + write_framebuffer(const std::string& file_name, Size2D size, const std::vector& pixel_data) noexcept + { + std::ofstream output_image{file_name}; + if (!output_image) { + return; + } + + output_image << "P6\n" << size.x() << ' ' << size.y() << '\n' << "255\n"; + for (const auto& pixel : pixel_data) { + const auto pixel_rgb = convert_color(pixel.noisy_color); + if (!output_image.write(reinterpret_cast(&pixel_rgb), sizeof(pixel_rgb)).good()) { + break; + } + } + } + + [[nodiscard]] static std::vector + render_fat_pixels(const Scene& scene, const Camera& camera, const RenderOptions& options) noexcept + { + const auto& rays = generate_rays(camera, options); + std::vector fat_pixels{rays.size()}; + + ScopedTimer timer{"Render time"}; + + std::atomic_size_t pixels_rendered{}; + + std::jthread notifier{[&pixels_rendered, pixel_count = rays.size(), &fat_pixels, options] { + using namespace std::chrono_literals; + + [[maybe_unused]] std::chrono::high_resolution_clock::time_point last_check_point{}; + std::size_t pixels_so_far{}; + do { + const auto previous = pixels_so_far; + pixels_so_far = pixels_rendered.load(); + + const auto percentage = (pixels_so_far * 100) / pixel_count; //beware of overflow *spooky noises* + const auto pixels_per_second = (pixels_so_far - previous) * 1'000 / 30; + + Logger::info( + "Rendered ", + pixels_so_far, + "/", + pixel_count, + " pixels (", + percentage, + "%) ~", + pixels_per_second, + " pixels per second \r"); //big brain padding + +#if 1 + const auto now = std::chrono::high_resolution_clock::now(); + if (duration_cast(now-last_check_point).count() >= 1) { + last_check_point = now; + write_framebuffer("progress.ppm", options.output_size, fat_pixels); + } +#endif + std::this_thread::sleep_for(30ms); + } while (pixels_so_far != pixel_count); + Logger::log('\n'); + }}; + + const RenderState state{scene.objects(), scene.materials(), scene.background_function(), options}; + + std::transform(std::execution::par, rays.begin(), rays.end(), fat_pixels.begin(), [&](const vec3& ray_direction) { + const auto get_direction = [&] { + if (options.do_aa) { + return get_direction_with_aa(ray_direction, options.output_size) * camera.transform.rotation; + } + return ray_direction * camera.transform.rotation; + }; + + FatPixel::Histogram histogram; + color pixel_color{}; + + for (std::size_t i{}; i != options.samples_per_pixel; ++i) { + const auto sample_direction = get_direction(); + const auto sample = get_shaded_color(RenderData{camera.transform.offset, sample_direction, state, 0U}); + histogram.add_sample(sample); + pixel_color += (sample / options.samples_per_pixel); + } + + ++pixels_rendered; + + return FatPixel{pixel_color, histogram}; + }); + + return fat_pixels; + } + + FatFramebuffer render_scene(const Scene& scene, const Camera& camera, const RenderOptions& options) noexcept + { + return {options.output_size, render_fat_pixels(scene, camera, options)}; + } + +} //namespace Raychel diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 0000000..60a7b59 --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,23 @@ +if(NOT RAYCHEL_LOGGER_EXTERNAL) + find_package(RaychelLogger REQUIRED) +endif() + +if(NOT RAYCHEL_CORE_EXTERNAL) + find_package(RaychelCore REQUIRED) +endif() + +if(NOT RAYCHEL_MATH_EXTERNAL) + find_package(RaychelMath REQUIRED) +endif() + +file(GLOB RAYCHEL_TEST_SOURCES "*.test.cpp") + +add_executable(Raychel_test + ${RAYCHEL_TEST_SOURCES} +) + +target_compile_features(Raychel_test PUBLIC cxx_std_20) + +target_link_libraries(Raychel_test PUBLIC + Raychel +) diff --git a/test/Ziggurat.test.cpp b/test/Ziggurat.test.cpp new file mode 100644 index 0000000..e3a05da --- /dev/null +++ b/test/Ziggurat.test.cpp @@ -0,0 +1,29 @@ +#include "Raychel/Core/ZigguratNormal.h" + +#include +#include +#include +#include + +int zig_main() +{ + constexpr auto scale = 25.0; + constexpr std::size_t n{100'000'000}; + constexpr auto expected0 = static_cast(n * 0.04 / 100.0); + + std::map hist{}; + + for (std::size_t i{}; i != n; ++i) { + ++hist[static_cast(std::round(Raychel::ziggurat_normal() * scale))]; + } + + for (const auto& [value, count] : hist) { + const auto actual_value = static_cast(value) / scale; + if (actual_value >= 0) { + std::cout << ' '; + } + std::cout << std::fixed << std::setprecision(3) << actual_value << ": " << std::string(count / expected0, '*') << ">\n"; + } + + return 0; +} diff --git a/test/main.test.cpp b/test/main.test.cpp new file mode 100644 index 0000000..dbadac1 --- /dev/null +++ b/test/main.test.cpp @@ -0,0 +1,283 @@ +#include "Raychel/Core/Deserialize.h" +#include "Raychel/Core/Raymarch.h" +#include "Raychel/Core/SDFBooleans.h" +#include "Raychel/Core/SDFModifiers.h" +#include "Raychel/Core/SDFPrimitives.h" +#include "Raychel/Core/SDFTransforms.h" +#include "Raychel/Core/Scene.h" +#include "Raychel/Core/Types.h" +#include "Raychel/Render/RenderUtils.h" +#include "Raychel/Render/Renderer.h" + +#include "RaychelMath/vector.h" + +#include +#include +#include +#include +#include +#include +#include + +struct FlatMaterial +{ + Raychel::color surface_color{}; +}; + +struct ReflectiveMaterial +{ + Raychel::color reflectivity{}; +}; + +struct DiffuseMaterial +{ + Raychel::color surface_color{}; +}; + +struct TransparentMaterial +{ + Raychel::color transparency{}; + double ior{1.0}; + double ior_variation{.1}; +}; + +struct DebugMaterial +{}; + +template <> +struct Raychel::is_transparent_material : std::true_type +{}; + +Raychel::color get_surface_color(const FlatMaterial& material, const Raychel::ShadingData& /*unused*/) noexcept +{ + return material.surface_color; +} + +Raychel::color get_surface_color(const ReflectiveMaterial& material, const Raychel::ShadingData& data) noexcept +{ + return Raychel::get_shaded_color(Raychel::RenderData{ + .origin = data.position, + .direction = reflect(data.incoming_direction, data.normal), + .state = data.state, + .recursion_depth = data.recursion_depth}) * + material.reflectivity; +} + +Raychel::color get_surface_color(const DiffuseMaterial& material, const Raychel::ShadingData& data) noexcept +{ + return Raychel::get_diffuse_lighting(data) * material.surface_color; +} + +Raychel::color get_surface_color(const TransparentMaterial& material, const Raychel::ShadingData& data) noexcept +{ + return Raychel::get_refraction(Raychel::RefractionData{ + .surface_point = data.position, + .incoming_direction = data.incoming_direction, + .normal = data.normal, + .material_ior = material.ior, + .ior_variation = material.ior_variation, + .state = data.state, + .recursion_depth = data.recursion_depth}) * + material.transparency; +} + +Raychel::color get_surface_color(const DebugMaterial& /*unused*/, const Raychel::ShadingData& data) noexcept +{ + const auto [x, y, z] = data.normal; + return Raychel::color{std::abs(x), std::abs(y), std::abs(z)}; +} + +double get_material_ior(const TransparentMaterial& material) noexcept +{ + return material.ior; +} + +[[maybe_unused]] static void write_framebuffer(const std::string& file_name, const Raychel::Framebuffer& framebuffer) noexcept +{ + //Don't bother writing an empty framebuffer + if (framebuffer.pixel_data.empty()) { + return; + } + + const auto label = Logger::startTimer("Write time"); + std::ofstream output_image{file_name}; + if (!output_image) { + Logger::error("Unable to open output file '", file_name, "'\n"); + return; + } + + output_image << "P6\n" << framebuffer.size.x() << ' ' << framebuffer.size.y() << '\n' << "255\n"; + for (const auto& pixel : framebuffer.pixel_data) { + const auto pixel_rgb = convert_color(pixel); + if (!output_image.write(reinterpret_cast(&pixel_rgb), sizeof(pixel_rgb)).good()) { + Logger::error("Error while writing output file!\n"); + break; + } + } + Logger::logDuration(label); +} + +[[maybe_unused]] static Raychel::Framebuffer fat_pixels_to_regular(const Raychel::FatFramebuffer& input) noexcept +{ + std::vector output{}; + output.reserve(input.size.x() * input.size.y()); + for (const auto& pixel : input) { + output.emplace_back(pixel.noisy_color); + } + return {input.size, std::move(output)}; +} + +[[maybe_unused]] static void build_cornell_box(Raychel::Scene& scene) +{ + using namespace Raychel; + + constexpr auto room_size = 1.0; + constexpr auto box_size = room_size * 1.1; + constexpr auto slim = 0.1; + + //Floor + scene.add_object(Translate{Box{vec3{box_size, slim, box_size}}, vec3{0, -room_size, 0}}, DiffuseMaterial{color{1, 1, 1}}); + //Ceiling + scene.add_object(Translate{Box{vec3{box_size, slim, box_size}}, vec3{0, room_size, 0}}, FlatMaterial{color{1, 1, .9} * 2.5}); + + //Left wall + scene.add_object( + Translate{Box{vec3{slim, box_size, box_size}}, vec3{-room_size * 1.01, 0, 0}}, DiffuseMaterial{color{1, 0, 0}}); + //Right wall + scene.add_object(Translate{Box{vec3{slim, box_size, box_size}}, vec3{room_size, 0, 0}}, DiffuseMaterial{color{0, 1, 0}}); + //Back wall + scene.add_object(Translate{Box{vec3{box_size, box_size, slim}}, vec3{0, 0, room_size}}, DiffuseMaterial{color{1, 1, 1}}); + + //Back sphere + scene.add_object( + Translate{Sphere{0.5}, vec3{-room_size + slim + 0.5, -room_size + 1.1 * slim + 0.5, room_size - 2 * slim - 0.5}}, + ReflectiveMaterial{color_from_hex(0xFF5733) * 0.95}); + + //Front box + scene.add_object( + Translate{ + Rotate{Sphere{.25}, rotate_around(vec3{0, 1, 0}, 60 * deg_to_rad)}, + vec3{room_size - slim - .5625, -room_size + slim + .25, -room_size + .375}}, + TransparentMaterial{.transparency = color_from_hex(0xa8ccd7), .ior = 1.5}); +} + +bool do_serialize(std::ostream& os, const FlatMaterial& material) noexcept +{ + os << material.surface_color << '\n'; + return os.good(); +} + +bool do_serialize(std::ostream& os, const ReflectiveMaterial& material) noexcept +{ + os << material.reflectivity << '\n'; + return os.good(); +} + +bool do_serialize(std::ostream& os, const DiffuseMaterial& material) noexcept +{ + os << material.surface_color << '\n'; + return os.good(); +} + +std::optional do_deserialize(std::istream& is, Raychel::DeserializationTag) noexcept +{ + Raychel::color c{}; + if (!(is >> c)) + return std::nullopt; + + return DiffuseMaterial{c}; +} + +std::optional do_deserialize(std::istream& is, Raychel::DeserializationTag) noexcept +{ + Raychel::color c{}; + if (!(is >> c)) + return std::nullopt; + + return FlatMaterial{c}; +} + +std::optional do_deserialize(std::istream& is, Raychel::DeserializationTag) noexcept +{ + Raychel::color c{}; + if (!(is >> c)) + return std::nullopt; + + return ReflectiveMaterial{c}; +} + +template T_> +constexpr Raychel::basic_color lerp(const Raychel::basic_color& a, const Raychel::basic_color& b, T_ x) +{ + return (b * x) + (a * (1.0 - x)); +} + +int main() +{ + Logger::setMinimumLogLevel(Logger::LogLevel::debug); + + using namespace Raychel; + +#if 0 + Scene scene; + build_cornell_box(scene); + + std::ofstream out_stream{"cornell_box.txt"}; + + serialize_scene(scene, out_stream); +#endif +#if 0 + std::ifstream in_file{"cornell_box.txt"}; + + auto scene = deserialize_scene( + in_file, + object_deserializers, Rotate<>, Sphere, Box>(), + material_deserializers()); + +#endif + +#if 1 + Scene scene; + //scene.add_object(Translate{Sphere{.75}, vec3{0, -.25, 0}}, TransparentMaterial{color{1, .65, .45}, 1.5, .05}); + //scene.add_object(Translate{Plane{vec3{0, 1, 0}}, vec3{0, -1.01, 0}}, DiffuseMaterial{color{1}}); + //scene.add_object(Translate{Box{vec3{.1, 2, 2}}, vec3{-2, 0, 0}}, FlatMaterial{color{10}}); + + scene.add_object(Translate{Sphere{.5}, vec3{-3.5, 2.5, -1.5}}, FlatMaterial{(color_from_hex(0x2FE3E0)) * 10}); + scene.add_object(Translate{Sphere{.5}, vec3{2.5, 2.5, 1.5}}, FlatMaterial{color_from_hex(0xD01C1F) * 10}); + scene.add_object(Translate{Sphere{.05}, vec3{0, .8, 0}}, FlatMaterial{color{1, .75, .5625} * 5}); + + scene.add_object( + Difference{Translate{Sphere{.5}, vec3{0, .85, 0}}, Rounded{Box{vec3{1, 1, 1}}, 0.1}}, DiffuseMaterial{color{1, 1, 1}}); + scene.set_background_function([](const RenderData& data) { +/* if (data.direction.z() < -0.99) { + return color_from_hex(0xfdd835) * 15; + } + return lerp( + color_from_hex(0xFFC922), + color_from_hex(0x87CEEB), + std::pow(std::abs(data.direction.y()) + 0.2, 1.0 / 3.0)) * + 0.75;*/ + (void)data; + return color{}; + }); +#endif + + for (const auto& obj : scene.objects()) { + obj.unsafe_impl()->debug_log(); + } + + const auto rendered_image = render_scene( + scene, + Camera{ + .transform = {.offset = vec3{0, 2.5, -2.5}, .rotation = rotate_around(vec3{1, 0, 0}, quarter_pi)}, + .zoom = 1.0}, + RenderOptions{ + .output_size = Size2D{1920, 1080} / 2U, + .max_ray_steps = 4096, + .max_recursion_depth = 100, + .samples_per_pixel = 1U << 10U, + }); + + write_framebuffer("out.ppm", fat_pixels_to_regular(rendered_image)); + return 0; +}