diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..95bd0d2 --- /dev/null +++ b/.clang-format @@ -0,0 +1,13 @@ +BasedOnStyle: Google +IndentWidth: 4 +AccessModifierOffset: -4 +ColumnLimit: 100 +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +DerivePointerAlignment: true +KeepEmptyLinesAtTheStartOfBlocks: true +SeparateDefinitionBlocks: Always +EmptyLineBeforeAccessModifier: LogicalBlock +SortIncludes: false +InsertNewlineAtEOF : true diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..1f59684 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,41 @@ +--- +Checks: '-*,cppcoreguidelines-avoid-goto,cppcoreguidelines-pro-type-const-cast, google-readability-casting, modernize-replace-random-shuffle, readability-braces-around-statements, readability-container-size-empty, readability-redundant-control-flow, readability-redundant-string-init, modernize-use-nullptr, readability-identifier-naming, google-build-using-namespace' +HeaderFilterRegex: '\.h$' +WarningsAsErrors: '*' +CheckOptions: + - key: readability-identifier-naming.NamespaceCase + value: lower_case + - key: readability-identifier-naming.ClassCase + value: CamelCase + - key: readability-identifier-naming.TypedefCase + value: CamelCase + - key: readability-identifier-naming.TypedefIgnoredRegexp + value: (allocator_type|size_type|key_type|value_type|mapped_type|difference_type|pointer|const_pointer|reference|const_reference|iterator_category|const_iterator|iterator|reverse_iterator|const_reverse_iterator|key_compare) + - key: readability-identifier-naming.TypeAliasCase + value: CamelCase + - key: readability-identifier-naming.PrivateMemberSuffix + value: '_' + - key: readability-identifier-naming.StructCase + value: CamelCase + - key: readability-identifier-naming.FunctionCase + value: CamelCase + - key: readability-identifier-naming.VariableCase + value: lower_case + - key: readability-identifier-naming.PrivateMemberCase + value: lower_case + - key: readability-identifier-naming.ParameterCase + value: lower_case + - key: readability-identifier-naming.GlobalConstantPrefix + value: k + - key: readability-identifier-naming.GlobalConstantCase + value: CamelCase + - key: readability-identifier-naming.StaticConstantPrefix + value: k + - key: readability-identifier-naming.StaticConstantCase + value: CamelCase + - key: readability-identifier-naming.ConstexprVariableCase + value: CamelCase + - key: readability-identifier-naming.ConstexprVariablePrefix + value: k + - key: google-runtime-int.TypeSuffix + value: _t diff --git a/.gitignore b/.gitignore index 259148f..27f53a4 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,12 @@ *.exe *.out *.app + +# Build files +/build/ +/build_debug/ +compile_commands.json +/.cache/ + +# 3D Models +/data/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..60fdb6a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "submodules/stb"] + path = submodules/stb + url = https://github.com/nothings/stb.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..7667d4d --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.5) +project(3d-renderer) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED True) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) + +add_subdirectory(src) diff --git a/README.md b/README.md new file mode 100644 index 0000000..c828f60 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# 3D Renderer +Курсовой проект по имплементации 3D рендерера с нуля. +## Скачивание +```shell +git clone --recurse-submodules git@github.com:rualss/3d-renderer.git +git checkout dev +git submodule update --init --recursive +``` +## Сборка и запуск +Перед сборкой нужно установить зависимости SFML. Все остальные библиотеки подтянутся сами +```shell +sudo apt update && sudo apt install \ + libxrandr-dev \ + libxcursor-dev \ + libxi-dev \ + libudev-dev \ + libflac-dev \ + libvorbis-dev \ + libgl1-mesa-dev \ + libegl1-mesa-dev \ + libdrm-dev \ + libgbm-dev +``` +Далее в корне репозитория нужно выполнить +```shell +mkdir build +cd build +cmake -DCMAKE_BUILD_TYPE=Release .. +cmake --build . --parallel <количество потоков у процессора> +./renderer +``` +## Управление ++ **W** - переместить камеру вперёд ++ **A** - переместить камеру влево ++ **S** - переместить камеру назад ++ **D** - переместить камеру вправо ++ **Up** - повернуть камеру вверх ++ **Down** - повернуть камеру вниз ++ **Right** - повернуть камеру вправо ++ **Left** - повернуть камеру влево ++ **Q** - наклонить камеру влево ++ **E** - наклонить камеру вправо + +## Пример работы +![floppa](pictures/floppa.png) diff --git a/pictures/floppa.png b/pictures/floppa.png new file mode 100644 index 0000000..8b312ab Binary files /dev/null and b/pictures/floppa.png differ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..3496f52 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,23 @@ +project(3d-renderer) + +include(sources.cmake) + +add_subdirectory(app) + +include(FetchContent) +FetchContent_Declare( + assimp + GIT_REPOSITORY https://github.com/assimp/assimp.git + GIT_TAG master +) +FetchContent_MakeAvailable(assimp) + +FetchContent_Declare( + glm + GIT_REPOSITORY https://github.com/g-truc/glm.git + GIT_TAG master +) +FetchContent_MakeAvailable(glm) + +target_include_directories(3d_pipeline_lib PRIVATE ${CMAKE_SOURCE_DIR}/submodules/stb) +target_link_libraries(3d_pipeline_lib PUBLIC glm::glm assimp::assimp) diff --git a/src/alias.h b/src/alias.h new file mode 100644 index 0000000..91cd826 --- /dev/null +++ b/src/alias.h @@ -0,0 +1,11 @@ +#pragma once + +#include +namespace renderer { + +enum class Axis { X, Y, Z }; + +enum Height : int32_t; +enum Width : int32_t; + +} // namespace renderer diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt new file mode 100644 index 0000000..cd9d7e9 --- /dev/null +++ b/src/app/CMakeLists.txt @@ -0,0 +1,20 @@ +project(3d-renderer) + +include(FetchContent) +FetchContent_Declare(SFML + GIT_REPOSITORY https://github.com/SFML/SFML.git + GIT_TAG 3.0.0 + GIT_SHALLOW ON + EXCLUDE_FROM_ALL + SYSTEM) +FetchContent_MakeAvailable(SFML) + +add_executable( + renderer + main.cpp + application.cpp + timer.cpp +) + +target_include_directories(renderer PUBLIC ${CMAKE_SOURCE_DIR}/src) +target_link_libraries(renderer PRIVATE 3d_pipeline_lib SFML::Graphics) diff --git a/src/app/application.cpp b/src/app/application.cpp new file mode 100644 index 0000000..8e00bc8 --- /dev/null +++ b/src/app/application.cpp @@ -0,0 +1,116 @@ +#include "application.h" +#include +#include +#include "camera.h" +#include "color.h" +#include "light.h" +#include "linalg.h" +#include "material.h" +#include "model_loader.h" +#include "object_3d.h" +#include "picture.h" +#include "polygon.h" +#include "world.h" +#include +#include +#include + +namespace renderer { + +namespace { + +World ExampleScene() { + ModelLoader loader; + loader.Open("../data/floppa2.fbx"); + Object3D floppa = loader.GetObject(); + floppa.ApplyMatrix(glm::scale(Mat4(1.), {0.01, 0.01, 0.01})); + World world; + world.AddObject(floppa); + world.AddLight(DirectionalLight{}); + return world; +} + +} // namespace + +Application::Application() + : world_(ExampleScene()), + window_(sf::VideoMode({static_cast(picture_.GetWidth()), + static_cast(picture_.GetHeight())}), + kDefaultName) { +} + +void Application::Run() { + while (window_.isOpen()) { + window_.clear(); + HandleEvents(); + HandleKeyboard(); + RenderFrame(); + } +} + +void Application::HandleEvents() { + while (const std::optional event = window_.pollEvent()) { + if (event->is()) { + window_.close(); + } + } +} + +void Application::HandleKeyboard() { + Time elapsed = timer_.Elapsed(); + CoordType move_distance = movement_speed_ * elapsed.ToSeconds(); + CoordType rotate_angle = rotation_speed_ * elapsed.ToSeconds(); + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::W)) { + camera_.Move(camera_.GetForwardDirection() * move_distance); + } + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::A)) { + camera_.Move(-camera_.GetRightDirecton() * move_distance); + } + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::S)) { + camera_.Move(-camera_.GetForwardDirection() * move_distance); + } + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::D)) { + camera_.Move(camera_.GetRightDirecton() * move_distance); + } + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Space)) { + camera_.Move(camera_.GetUpDirection() * move_distance); + } + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Z)) { + camera_.Move(-camera_.GetUpDirection() * move_distance); + } + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Up)) { + camera_.Rotate(Axis::X, rotate_angle); + } + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Down)) { + camera_.Rotate(Axis::X, -rotate_angle); + } + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Left)) { + camera_.Rotate(Axis::Y, rotate_angle); + } + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Right)) { + camera_.Rotate(Axis::Y, -rotate_angle); + } + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::E)) { + camera_.Rotate(Axis::Z, rotate_angle); + } + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Q)) { + camera_.Rotate(Axis::Z, -rotate_angle); + } +} + +void Application::RenderFrame() { + renderer_.Render(world_, camera_, &picture_); + pixels_.clear(); + for (size_t x = 0; x < picture_.GetWidth(); ++x) { + for (size_t y = 0; y < picture_.GetHeight(); ++y) { + DiscreteColor color = picture_(x, y); + if (color != kBlackDiscrete) { + pixels_.emplace_back(sf::Vector2f(x, y), sf::Color(color.x, color.y, color.z)); + } + } + } + window_.draw(pixels_.data(), pixels_.size(), sf::PrimitiveType::Points); + window_.display(); +} + +} // namespace renderer diff --git a/src/app/application.h b/src/app/application.h new file mode 100644 index 0000000..59fda3d --- /dev/null +++ b/src/app/application.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include "camera.h" +#include "linalg.h" +#include "picture.h" +#include "renderer.h" +#include "world.h" +#include "timer.h" + +namespace renderer { + +class Application { + using Window = sf::RenderWindow; + using Index = uint32_t; // SFML default size type, there is no sf::size_type + +public: + Application(); + void Run(); + +private: + void HandleEvents(); + void HandleKeyboard(); + void RenderFrame(); + + static constexpr CoordType kDefaultMovementSpeed = 50.; + static constexpr CoordType kDefaultRotationSpeed = 80.; + static constexpr std::string kDefaultName = "3D renderer"; + + Renderer renderer_; + Camera camera_; + World world_; + Picture picture_; + Window window_; + CoordType movement_speed_ = kDefaultMovementSpeed; + CoordType rotation_speed_ = kDefaultRotationSpeed; + std::vector pixels_; + Timer timer_; +}; + +} // namespace renderer diff --git a/src/app/main.cpp b/src/app/main.cpp new file mode 100644 index 0000000..ff0de22 --- /dev/null +++ b/src/app/main.cpp @@ -0,0 +1,16 @@ +#include +#include + +#include "application.h" + +using namespace renderer; + +int main() { + try { + Application app; + app.Run(); + } catch (std::exception& e) { + std::cout << e.what() << '\n'; + } catch (...) { + } +} diff --git a/src/app/timer.cpp b/src/app/timer.cpp new file mode 100644 index 0000000..d6e97cb --- /dev/null +++ b/src/app/timer.cpp @@ -0,0 +1,40 @@ +#include "timer.h" +#include + +namespace renderer { + +using namespace std::chrono; + +namespace { + +static constexpr Time::TimeUnit kSecondsInMicrosecond = 1 / 1000000.; +static constexpr Time::TimeUnit kMillisecondsInMicrosecond = 1 / 1000.; + +} // namespace + +Time::Time(Duration duration) : duration_(duration) { +} + +Time::TimeUnit Time::ToSeconds() { + return duration_cast(duration_).count() * kSecondsInMicrosecond; +} + +Time::TimeUnit Time::ToMilliseconds() { + return duration_cast(duration_).count() * kMillisecondsInMicrosecond; +} + +Time::TimeUnit Time::ToMicroseconds() { + return duration_cast(duration_).count(); +} + +Timer::Timer() : latest_time_(Clock::now()) { +} + +Time Timer::Elapsed() { + TimePoint current_time = Clock::now(); + Time elapsed = (current_time - latest_time_); + latest_time_ = current_time; + return elapsed; +} + +} // namespace renderer diff --git a/src/app/timer.h b/src/app/timer.h new file mode 100644 index 0000000..9842b26 --- /dev/null +++ b/src/app/timer.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +namespace renderer { + +class Time { +public: + using TimeUnit = double; + using Duration = std::chrono::high_resolution_clock::duration; + + Time(Duration duration); + TimeUnit ToSeconds(); + TimeUnit ToMilliseconds(); + TimeUnit ToMicroseconds(); + +private: + Duration duration_; +}; + +class Timer { +public: + using Clock = std::chrono::high_resolution_clock; + using TimePoint = Clock::time_point; + + Timer(); + Time Elapsed(); + +private: + TimePoint latest_time_; +}; + +} // namespace renderer diff --git a/src/camera.cpp b/src/camera.cpp new file mode 100644 index 0000000..bdaaa83 --- /dev/null +++ b/src/camera.cpp @@ -0,0 +1,77 @@ +#include "camera.h" +#include +#include +#include +#include +#include +#include +#include "linalg.h" + +namespace renderer { + +Camera::Camera(const Vec3& focal_point, CoordType near_dist, CoordType far_dist, CoordType fov_y) + : focal_point_(focal_point), near_dist_(near_dist), far_dist_(far_dist), fov_y_(fov_y) { +} + +const Vec3& Camera::GetFocalPoint() const { + return focal_point_; +} + +CoordType Camera::GetFOV() const { + return fov_y_; +} + +CoordType Camera::GetNearDist() const { + return near_dist_; +} + +CoordType Camera::GetFarDist() const { + return far_dist_; +} + +void Camera::Move(const Vec3& shift) { + position_ += shift; +} + +void Camera::Rotate(Axis axis, CoordType angle) { + switch (axis) { + case Axis::X: + rotation_ = glm::rotate(Mat4(rotation_), glm::radians(angle), Vec3{1, 0, 0}); + break; + case Axis::Y: + rotation_ = glm::rotate(Mat4(rotation_), glm::radians(angle), Vec3{0, 1, 0}); + break; + case Axis::Z: + rotation_ = glm::rotate(Mat4(rotation_), glm::radians(angle), Vec3{0, 0, -1}); + break; + default: + assert(false); + break; + } +} + +Mat4 Camera::MakeWorldToCameraMatrix() const { + return glm::transpose(rotation_) * glm::translate(Mat4(1.), -position_); +} + +Vec3 Camera::GetRightDirecton() const { + return glm::normalize(rotation_[0]); +} + +Vec3 Camera::GetUpDirection() const { + return glm::normalize(rotation_[1]); +} + +Vec3 Camera::GetForwardDirection() const { + return -glm::normalize(rotation_[2]); +} + +const Mat4& Camera::GetRotationMatrix() const { + return rotation_; +} + +const Vec3& Camera::GetPosition() const { + return position_; +} + +} // namespace renderer diff --git a/src/camera.h b/src/camera.h new file mode 100644 index 0000000..d5cce90 --- /dev/null +++ b/src/camera.h @@ -0,0 +1,42 @@ +#pragma once + +#include "linalg.h" +#include "alias.h" + +namespace renderer { + +class Camera { +public: + Camera() = default; + Camera(const Vec3& focal_point, CoordType near_dist, CoordType far_dist, CoordType fov_y); + + const Vec3& GetFocalPoint() const; + CoordType GetFOV() const; + CoordType GetNearDist() const; + CoordType GetFarDist() const; + void Move(const Vec3& shift); + void Rotate(Axis axis, CoordType angle); + Mat4 MakeWorldToCameraMatrix() const; + Vec3 GetRightDirecton() const; + Vec3 GetUpDirection() const; + Vec3 GetForwardDirection() const; + const Mat4& GetRotationMatrix() const; + const Vec3& GetPosition() const; + +private: + static constexpr Vec3 kDefaultFocalPoint = {0, 0, 0}; + static constexpr CoordType kDefaultNearDist = 0.1; + static constexpr CoordType kDefaultFarDist = 500.0; + static constexpr CoordType kDefaultFOV = 45.0; + static constexpr Vec3 kDefaultPosition = Vec3{0, 0, 0}; + static constexpr Mat3 kDefaultRotation = Mat4{1.}; + + Vec3 focal_point_ = kDefaultFocalPoint; + CoordType near_dist_ = kDefaultNearDist; + CoordType far_dist_ = kDefaultFarDist; + CoordType fov_y_ = kDefaultFOV; + Vec3 position_ = kDefaultPosition; + Mat4 rotation_ = kDefaultRotation; +}; + +} // namespace renderer diff --git a/src/color.cpp b/src/color.cpp new file mode 100644 index 0000000..28668ac --- /dev/null +++ b/src/color.cpp @@ -0,0 +1,21 @@ +#include "color.h" +#include +#include "glm/common.hpp" + +namespace renderer { + +DiscreteColor ColorToDiscrete(const Color& color) { + DiscreteColor discrete_color; + discrete_color = glm::round(color * static_cast(kDiscreteColorMax)); + discrete_color = glm::clamp(discrete_color, 0, kDiscreteColorMax); + return discrete_color; +} + +Color DiscreteColorToColor(const DiscreteColor& discrete_color) { + Color color{discrete_color}; + color /= 255.; + color = glm::clamp(color, 0.0f, 1.0f); + return color; +} + +} // namespace renderer diff --git a/src/color.h b/src/color.h new file mode 100644 index 0000000..bc0eb36 --- /dev/null +++ b/src/color.h @@ -0,0 +1,28 @@ +#pragma once + +#include "linalg.h" + +namespace renderer { + +using ColorValue = float; +using Color = glm::vec3; +using DiscreteColor = glm::vec<3, int32_t>; + +constexpr static Index kDiscreteColorMax = 255; + +constexpr static DiscreteColor kBlackDiscrete = {0, 0, 0}; +constexpr static DiscreteColor kWhiteDiscrete = {255, 255, 255}; +constexpr static DiscreteColor kRedDiscrete = {255, 0, 0}; +constexpr static DiscreteColor kGreenDiscrete = {0, 255, 0}; +constexpr static DiscreteColor kBlueDiscrete = {0, 0, 255}; + +constexpr static Color kBlack = {0, 0, 0}; +constexpr static Color kWhite = {1, 1, 1}; +constexpr static Color kRed = {1, 0, 0}; +constexpr static Color kGreen = {0, 1, 0}; +constexpr static Color kBlue = {0, 0, 1}; + +DiscreteColor ColorToDiscrete(const Color& color); +Color DiscreteColorToColor(const DiscreteColor& discrete_color); + +} // namespace renderer diff --git a/src/geometry.cpp b/src/geometry.cpp new file mode 100644 index 0000000..bc54688 --- /dev/null +++ b/src/geometry.cpp @@ -0,0 +1,23 @@ +#include "geometry.h" +#include "linalg.h" + +namespace renderer { + +BarycentricCoordinateSystem::BarycentricCoordinateSystem(const Polygon& polygon) + : a_(polygon.vertices[0]), + v0_(polygon.vertices[1] - polygon.vertices[0]), + v1_(polygon.vertices[2] - polygon.vertices[0]), + inv_denominator_(1. / (v0_.x * v1_.y - v1_.x * v0_.y)) { +} + +Vec3 BarycentricCoordinateSystem::GetBarycentricCoordinates(const Vec2& p) { + Vec3 barycentric; + CoordType v2x = p.x - a_.x; + CoordType v2y = p.y - a_.y; + barycentric.y = (v2x * v1_.y - v1_.x * v2y) * inv_denominator_; + barycentric.z = (v0_.x * v2y - v2x * v0_.y) * inv_denominator_; + barycentric.x = 1.0 - barycentric.y - barycentric.z; + return barycentric; +} + +} // namespace renderer diff --git a/src/geometry.h b/src/geometry.h new file mode 100644 index 0000000..ea5380e --- /dev/null +++ b/src/geometry.h @@ -0,0 +1,20 @@ +#pragma once + +#include "linalg.h" +#include "polygon.h" + +namespace renderer { + +class BarycentricCoordinateSystem { +public: + BarycentricCoordinateSystem(const Polygon& polygon); + Vec3 GetBarycentricCoordinates(const Vec2& p); + +private: + Vec2 a_; + Vec2 v0_; + Vec2 v1_; + CoordType inv_denominator_; +}; + +} // namespace renderer diff --git a/src/light.h b/src/light.h new file mode 100644 index 0000000..5235265 --- /dev/null +++ b/src/light.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include "linalg.h" +#include "color.h" + +namespace renderer { + +struct AmbientLight { + Color color = {0.2, 0.2, 0.2}; +}; + +struct DirectionalLight { + Color color = kWhite; + Vec3 direction = {0, 0, -1}; +}; + +struct PointLight { + Color color = kWhite; + CoordType constant_attenuation = 1; + CoordType linear_attenuation = 0.022; + CoordType quadratic_attenuation = 0.0019; + Vec3 position = {0, 0, 0}; +}; + +using Light = std::variant; + +} // namespace renderer diff --git a/src/linalg.h b/src/linalg.h new file mode 100644 index 0000000..3bc09aa --- /dev/null +++ b/src/linalg.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include +#include + +namespace renderer { + +using Vec2 = glm::dvec2; +using Vec3 = glm::dvec3; +using Vec4 = glm::dvec4; +using Mat3 = glm::dmat3; +using Mat4 = glm::dmat4; +using CoordType = double; +using Index = int32_t; + +const static CoordType kEps = 1e-6; + +} // namespace renderer diff --git a/src/material.h b/src/material.h new file mode 100644 index 0000000..53c151b --- /dev/null +++ b/src/material.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include "linalg.h" +#include "texture.h" +#include "color.h" + +namespace renderer { + +struct Material { + Color ambient = kWhite; + Color diffuse = kWhite; + Color specular = kBlack; + CoordType shininess = 32; + + bool two_sided = false; + std::optional ambient_texture = std::nullopt; + std::optional diffuse_texture = std::nullopt; + std::optional specular_texture = std::nullopt; +}; + +} // namespace renderer diff --git a/src/mesh.cpp b/src/mesh.cpp new file mode 100644 index 0000000..452fb7a --- /dev/null +++ b/src/mesh.cpp @@ -0,0 +1,39 @@ +#include "mesh.h" +#include "linalg.h" +#include "material.h" +#include "polygon.h" +#include + +namespace renderer { + +const std::vector& Mesh::GetPolygons() const { + return polygons_; +} + +void Mesh::AddPolygon(const Polygon& polygon) { + polygons_.push_back(polygon); +} + +void Mesh::AddPolygon(Polygon&& polygon) { + polygons_.push_back(std::move(polygon)); +} + +void Mesh::ApplyMatrix(const Mat4& mat) { + for (Polygon& polygon : polygons_) { + TransformPolygon(mat, polygon); + } +} + +void Mesh::SetMaterial(const Material& material) { + material_ = material; +} + +void Mesh::SetMaterial(Material&& material) { + material_ = std::move(material); +} + +const Material& Mesh::GetMaterial() const { + return material_; +} + +} // namespace renderer diff --git a/src/mesh.h b/src/mesh.h new file mode 100644 index 0000000..ec8fa7f --- /dev/null +++ b/src/mesh.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include "polygon.h" +#include "material.h" + +namespace renderer { +class Mesh { +public: + Mesh() = default; + + template + requires std::convertible_to, Polygon> + Mesh(InputIt first, InputIt last) : polygons_(first, last) { + } + + const std::vector& GetPolygons() const; + void AddPolygon(const Polygon& polygon); + void AddPolygon(Polygon&& polygon); + void ApplyMatrix(const Mat4& mat); + void SetMaterial(const Material& material); + void SetMaterial(Material&& material); + const Material& GetMaterial() const; + +private: + Material material_; + std::vector polygons_; +}; +} // namespace renderer diff --git a/src/model_loader.cpp b/src/model_loader.cpp new file mode 100644 index 0000000..4d234dd --- /dev/null +++ b/src/model_loader.cpp @@ -0,0 +1,165 @@ +#include "model_loader.h" +#include "assimp/material.h" +#include "assimp/scene.h" +#include "linalg.h" +#include "object_3d.h" +#include "polygon.h" +#include "texture_loader.h" + +#include +#include +#include +#include +#include + +namespace renderer { + +void ModelLoader::Open(std::filesystem::path path) { + const aiScene* model = importer_.ReadFile( + path.c_str(), aiProcess_Triangulate | aiProcess_GenNormals | aiProcess_FlipUVs); + if (IsInvalid(model)) { + loaded_object_ = Object3D{}; + } + std::vector materials = ParseMaterials(model, path); + std::vector meshes = ParseMeshes(model, materials); + loaded_object_ = Object3D{meshes.begin(), meshes.end()}; +} + +Object3D ModelLoader::GetObject() { + return loaded_object_; +} + +bool ModelLoader::IsInvalid(const aiScene* model) { + return (model == nullptr) || (model->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || + (model->mRootNode == nullptr); +} + +std::vector ModelLoader::ParseMaterials(const aiScene* model, Path path) { + std::vector materials; + materials.reserve(model->mNumMaterials); + for (Index i = 0; i < model->mNumMaterials; ++i) { + const aiMaterial* assimp_material = model->mMaterials[i]; + materials.push_back(std::move(ParseMaterial(model->mMaterials[i], path))); + } + return materials; +} + +Material ModelLoader::ParseMaterial(const aiMaterial* assimp_material, Path path) { + assert(assimp_material && "Material must not be nullptr"); + Material material; + aiString name; + if (assimp_material->Get(AI_MATKEY_NAME, name) == AI_SUCCESS) { + if (strcmp(name.C_Str(), "DefaultMaterial") == 0) { + return material; + } + } + aiColor3D color{material.ambient.r, material.ambient.g, material.ambient.b}; + assimp_material->Get(AI_MATKEY_COLOR_AMBIENT, color); + material.ambient = {color.r, color.g, color.b}; + + color = {material.diffuse.r, material.diffuse.g, material.diffuse.b}; + assimp_material->Get(AI_MATKEY_COLOR_DIFFUSE, color); + material.diffuse = {color.r, color.g, color.b}; + + color = {material.specular.r, material.specular.g, material.specular.b}; + assimp_material->Get(AI_MATKEY_COLOR_SPECULAR, color); + material.specular = {color.r, color.g, color.b}; + + assimp_material->Get(AI_MATKEY_SHININESS, material.shininess); + + int two_sided = 0; + assimp_material->Get(AI_MATKEY_TWOSIDED, two_sided); + material.two_sided = (two_sided != 0); + + SetTextures(assimp_material, &material, path); + return material; +} + +void ModelLoader::SetTextures(const aiMaterial* assimp_material, Material* material, Path path) { + SetTexture(assimp_material, material, aiTextureType_AMBIENT, path); + SetTexture(assimp_material, material, aiTextureType_DIFFUSE, path); + SetTexture(assimp_material, material, aiTextureType_SPECULAR, path); + if (material->diffuse_texture.has_value() && !material->ambient_texture.has_value()) { + material->ambient_texture = material->diffuse_texture; + } +} + +void ModelLoader::SetTexture(const aiMaterial* assimp_material, Material* material, + aiTextureType type, Path path) { + if (assimp_material->GetTextureCount(type) > 0) { + std::filesystem::path parent_path = path.parent_path(); + aiString path; + assimp_material->GetTexture(type, 0, &path); + std::filesystem::path path_to_texture = parent_path; + path_to_texture /= std::filesystem::path(path.C_Str()); + switch (type) { + case aiTextureType_AMBIENT: + material->ambient_texture = texture_loader_.LoadTexture(path_to_texture); + break; + case aiTextureType_DIFFUSE: + material->diffuse_texture = texture_loader_.LoadTexture(path_to_texture); + break; + case aiTextureType_SPECULAR: + material->specular_texture = texture_loader_.LoadTexture(path_to_texture); + break; + default: + assert(false); + break; + } + } +} + +std::vector ModelLoader::ParseMeshes(const aiScene* model, + const std::vector& materials) { + std::vector meshes; + meshes.reserve(model->mNumMeshes); + for (Index i = 0; i < model->mNumMeshes; ++i) { + meshes.push_back(std::move(ParseMesh(model->mMeshes[i], materials))); + } + return meshes; +} + +Mesh ModelLoader::ParseMesh(const aiMesh* assimp_mesh, const std::vector& materials) { + assert(assimp_mesh && "Mesh must not be nullptr"); + Mesh mesh; + mesh.SetMaterial(materials[assimp_mesh->mMaterialIndex]); + std::vector vertices; + std::vector normals; + std::vector texture_coordinates; + vertices.reserve(assimp_mesh->mNumVertices); + normals.reserve(assimp_mesh->mNumVertices); + bool has_texture = false; + if (assimp_mesh->mTextureCoords[0]) { + texture_coordinates.reserve(assimp_mesh->mNumVertices); + has_texture = true; + } + for (Index i = 0; i < assimp_mesh->mNumVertices; ++i) { + vertices.emplace_back(assimp_mesh->mVertices[i].x, assimp_mesh->mVertices[i].y, + assimp_mesh->mVertices[i].z); + normals.emplace_back(assimp_mesh->mNormals[i].x, assimp_mesh->mNormals[i].y, + assimp_mesh->mNormals[i].z); + if (has_texture) { + texture_coordinates.emplace_back(assimp_mesh->mTextureCoords[0][i].x, + assimp_mesh->mTextureCoords[0][i].y); + } + } + for (Index i = 0; i < assimp_mesh->mNumFaces; ++i) { + const aiFace assimp_face = assimp_mesh->mFaces[i]; + assert(assimp_face.mNumIndices == 3 && "Face must be a triangle"); + Polygon polygon; + if (has_texture) { + polygon.texture_vertices.emplace(); + } + for (int i = 0; i < 3; ++i) { + polygon.vertices[i] = vertices[assimp_face.mIndices[i]]; + polygon.normals[i] = normals[assimp_face.mIndices[i]]; + if (has_texture) { + polygon.texture_vertices.value()[i] = texture_coordinates[assimp_face.mIndices[i]]; + } + } + mesh.AddPolygon(std::move(polygon)); + } + return mesh; +} + +} // namespace renderer diff --git a/src/model_loader.h b/src/model_loader.h new file mode 100644 index 0000000..a46b42c --- /dev/null +++ b/src/model_loader.h @@ -0,0 +1,38 @@ +#pragma once + +#include "assimp/Importer.hpp" +#include "assimp/material.h" +#include "material.h" +#include "mesh.h" +#include "object_3d.h" +#include "texture_loader.h" + +#include +#include +#include + +namespace renderer { + +class ModelLoader { +public: + using Path = std::filesystem::path; + + void Open(Path path); + Object3D GetObject(); + +private: + bool IsInvalid(const aiScene* model); + std::vector ParseMaterials(const aiScene* model, Path path); + Material ParseMaterial(const aiMaterial* assimp_material, Path path); + void SetTextures(const aiMaterial* assimp_material, Material* material, Path path); + void SetTexture(const aiMaterial* assimp_material, Material* material, aiTextureType type, + Path path); + std::vector ParseMeshes(const aiScene* model, const std::vector& materials); + Mesh ParseMesh(const aiMesh* assimp_mesh, const std::vector& materials); + + Assimp::Importer importer_; + TextureLoader texture_loader_; + Object3D loaded_object_; +}; + +} // namespace renderer diff --git a/src/object_3d.cpp b/src/object_3d.cpp new file mode 100644 index 0000000..849f14b --- /dev/null +++ b/src/object_3d.cpp @@ -0,0 +1,44 @@ +#include "object_3d.h" + +#include +#include "model_loader.h" +#include +#include + +namespace renderer { + +Mesh& Object3D::operator[](Index i) { + return meshes_[i]; +} + +const Mesh& Object3D::operator[](Index i) const { + return meshes_[i]; +} + +void Object3D::AddMesh(const Mesh& mesh) { + meshes_.push_back(mesh); +} + +void Object3D::AddMesh(Mesh&& mesh) { + meshes_.push_back(std::move(mesh)); +} + +void Object3D::ApplyMatrix(const Mat4& mat) { + for (Mesh& mesh : meshes_) { + mesh.ApplyMatrix(mat); + } +} + +const Vec3& Object3D::GetLocalOrigin() const { + return local_origin_; +} + +void Object3D::SetLocalOrigin(const Vec3& new_origin) { + local_origin_ = new_origin; +} + +const std::vector& Object3D::GetMeshes() const { + return meshes_; +} + +} // namespace renderer diff --git a/src/object_3d.h b/src/object_3d.h new file mode 100644 index 0000000..f9de77a --- /dev/null +++ b/src/object_3d.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include +#include "mesh.h" + +namespace renderer { + +class Object3D { +public: + Object3D() = default; + + template + Object3D(InputIt first, InputIt last) : meshes_(first, last) { + } + + Mesh& operator[](Index i); + const Mesh& operator[](Index i) const; + void AddMesh(const Mesh& polygon); + void AddMesh(Mesh&& polygon); + void ApplyMatrix(const Mat4& mat); + const Vec3& GetLocalOrigin() const; + void SetLocalOrigin(const Vec3& new_origin); + const std::vector& GetMeshes() const; + +private: + static constexpr Vec3 kDefaultLocalOrigin = {0, 0, 0}; + + std::vector meshes_; + Vec3 local_origin_ = kDefaultLocalOrigin; +}; + +} // namespace renderer diff --git a/src/picture.cpp b/src/picture.cpp new file mode 100644 index 0000000..2a1dfbd --- /dev/null +++ b/src/picture.cpp @@ -0,0 +1,58 @@ +#include "picture.h" +#include "color.h" +#include "linalg.h" +#include +#include +#include + +namespace renderer { + +Picture::Picture() : pixels_(kDefaultPixelsSize, kDefaultColor) { +} + +Picture::Picture(Height height, Width width) : width_(width) { + assert(height > 0 && "Height must be positive"); + assert(width > 0 && "Width must be positive"); + pixels_.resize(static_cast(width) * height, kDefaultColor); +} + +Picture::Picture(Width width, std::vector&& pixels) + : width_(width), pixels_(std::move(pixels)) { + assert(pixels.size() % width == 0 && "Width must divide number of pixels"); +} + +DiscreteColor& Picture::operator()(Index x, Index y) { + assert(x >= 0 && x < width_ && "x coordinates out of bounds"); + assert(y >= 0 && y < GetHeight() && "y coordinates out of bounds"); + return pixels_[width_ * y + x]; +} + +const DiscreteColor& Picture::operator()(Index x, Index y) const { + assert(x >= 0 && x < width_ && "x coordinates out of bounds"); + assert(y >= 0 && y < GetHeight() && "y coordinates out of bounds"); + return pixels_[width_ * y + x]; +} + +const std::vector& Picture::GetPixels() const { + return pixels_; +} + +Index Picture::GetHeight() const { + return pixels_.size() / width_; +} + +Index Picture::GetWidth() const { + return width_; +} + +void Picture::SetDefaultColor() { + std::fill(std::execution::par, pixels_.begin(), pixels_.end(), kDefaultColor); +} + +const DiscreteColor& Picture::SampleColor(Vec2 coords) const { + assert(0 <= coords.x <= 1 && 0 <= coords.y <= 1 && "Coords must be in [0, 1]"); + return (*this)(static_cast(std::round(coords.x * (GetWidth() - 1))), + static_cast(std::round(coords.y * (GetHeight() - 1)))); +} + +} // namespace renderer diff --git a/src/picture.h b/src/picture.h new file mode 100644 index 0000000..85d5b96 --- /dev/null +++ b/src/picture.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include "linalg.h" +#include "alias.h" +#include "color.h" + +namespace renderer { + +class Picture { +public: + Picture(); + Picture(Height height, Width width); + Picture(Width width, std::vector&& pixels); + + DiscreteColor& operator()(Index x, Index y); + const DiscreteColor& operator()(Index x, Index y) const; + const std::vector& GetPixels() const; + Index GetHeight() const; + Index GetWidth() const; + void SetDefaultColor(); + const DiscreteColor& SampleColor(Vec2 coords) const; + +private: + static constexpr Width kDefaultWidth = Width{1280}; + static constexpr Index kDefaultPixelsSize = 1280 * 720; + static constexpr DiscreteColor kDefaultColor = kBlack; + + Index width_ = kDefaultWidth; + std::vector pixels_; // X axis directed right Y axis directed downwards +}; + +} // namespace renderer diff --git a/src/polygon.cpp b/src/polygon.cpp new file mode 100644 index 0000000..4e67e26 --- /dev/null +++ b/src/polygon.cpp @@ -0,0 +1,38 @@ +#include "polygon.h" +#include "glm/geometric.hpp" + +namespace renderer { + +void TransformPolygon(const Mat4& mat, Polygon& polygon) { + for (int i = 0; i < Polygon::kVertexCount; ++i) { + Vec4 tmp_vertex(polygon.vertices[i], 1.); + polygon.vertices[i] = Vec3(mat * tmp_vertex); + } +} + +void TransformNormals(const Mat4& mat, Polygon& polygon) { + for (int i = 0; i < Polygon::kVertexCount; ++i) { + Vec4 tmp_normal(polygon.normals[i], 1.); + polygon.normals[i] = glm::normalize(Vec3(mat * tmp_normal)); + } +} + +Vec3 GetNonUnitNormal(const Polygon& polygon) { + return glm::cross(polygon.vertices[2] - polygon.vertices[0], + polygon.vertices[1] - polygon.vertices[0]); +} + +void ProjectiveTransformVector(const Mat4& transformation_matrix, Vec3& vector) { + Vec4 homogeneous(vector, 1.0); + homogeneous = transformation_matrix * homogeneous; + assert(std::abs(homogeneous.w) > kEps && "TransformVector: Point went to infinity"); + vector = Vec3(homogeneous / homogeneous.w); +} + +void ProjectiveTransformPolygon(const Mat4& transformation_matrix, Polygon& polygon) { + for (size_t i = 0; i < Polygon::kVertexCount; ++i) { + ProjectiveTransformVector(transformation_matrix, polygon.vertices[i]); + } +} + +} // namespace renderer diff --git a/src/polygon.h b/src/polygon.h new file mode 100644 index 0000000..66c474a --- /dev/null +++ b/src/polygon.h @@ -0,0 +1,24 @@ +#pragma once + +#include "linalg.h" +#include +#include + +namespace renderer { + +struct Polygon { +public: + static constexpr Index kVertexCount = 3; + + std::array vertices; + std::array normals; + std::optional> texture_vertices = std::nullopt; +}; + +void TransformPolygon(const Mat4& mat, Polygon& polygon); +void TransformNormals(const Mat4& mat, Polygon& polygon); +Vec3 GetNonUnitNormal(const Polygon& polygon); +void ProjectiveTransformVector(const Mat4& transformation_matrix, Vec3& vector); +void ProjectiveTransformPolygon(const Mat4& transformation_matrix, Polygon& polygon); + +} // namespace renderer diff --git a/src/renderer.cpp b/src/renderer.cpp new file mode 100644 index 0000000..2f5a726 --- /dev/null +++ b/src/renderer.cpp @@ -0,0 +1,386 @@ +#include "renderer.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include "camera.h" +#include "color.h" +#include "glm/geometric.hpp" +#include "light.h" +#include "linalg.h" +#include "geometry.h" +#include "polygon.h" + +namespace renderer { + +namespace { + +CoordType GetAspectRatio(Height height, Width width) { + return static_cast(width) / static_cast(height); +} + +void TransformPolygonToScreenSpace(Polygon& polygon, Height height, Width width) { + CoordType real_height = static_cast(height); + CoordType real_width = static_cast(width); + + for (Index i = 0; i < Polygon::kVertexCount; ++i) { + polygon.vertices[i].x = real_width * ((polygon.vertices[i].x + 1.) / 2.); + polygon.vertices[i].y = real_height * ((1. - polygon.vertices[i].y) / 2.); + } +} + +bool IsInsidePolygon(const Vec3& barycentric_coordinates) { + return barycentric_coordinates.x <= 1.0 && barycentric_coordinates.x >= 0.0 && + barycentric_coordinates.y <= 1.0 && barycentric_coordinates.y >= 0.0 && + barycentric_coordinates.z <= 1.0 && barycentric_coordinates.z >= 0.0; +} + +Index RoundDown(CoordType coordinate) { + return static_cast(std::max(0.0, std::floor(coordinate))); +} + +Index RoundUp(CoordType coordinate) { + return static_cast(std::ceil(coordinate)); +} + +CoordType CalculateZ(const Vec3& barycentric, const Polygon& polygon) { + return barycentric[0] * polygon.vertices[0].z + barycentric[1] * polygon.vertices[1].z + + barycentric[2] * polygon.vertices[2].z; +} + +bool IsVisible(const Polygon& polygon) { + Vec3 look_dir = polygon.vertices[0]; + return glm::dot(look_dir, GetNonUnitNormal(polygon)) > 0; +} + +struct PolygonVertex { + Vec3 vertex; + Vec3 normal; + Vec2 texture_coordinates; +}; + +PolygonVertex PolygonSidePlaneIntersection(const Vec3& normal, CoordType offset, + const PolygonVertex& point1, + const PolygonVertex& point2) { + Vec3 line_direction = point2.vertex - point1.vertex; + CoordType t = -(glm::dot(normal, point1.vertex) + offset) / glm::dot(normal, line_direction); + return {.vertex = point1.vertex + line_direction * t, + .normal = point1.normal + (point2.normal - point1.normal) * t, + .texture_coordinates = point1.texture_coordinates + + (point2.texture_coordinates - point1.texture_coordinates) * t}; +} + +bool IsPointOnCorrectSideOfPlane(Vec3 normal, CoordType offset, const Vec3& point) { + assert(std::abs(glm::length(normal) - 1) <= kEps && + "IsPointOnCorrectSideOfPlane: normal vector length must be 1"); + CoordType a = glm::dot(normal, point) + offset; + return glm::dot(normal, point) + offset > 0; +} + +PolygonVertex MakePolygonVertex(const Polygon& polygon, Index i) { + PolygonVertex polygon_vertex = {.vertex = polygon.vertices[i], + .normal = polygon.normals[i], + .texture_coordinates = {-1, -1}}; + if (polygon.texture_vertices) { + polygon_vertex.texture_coordinates = polygon.texture_vertices.value()[i]; + } + return polygon_vertex; +} + +Polygon MakePolygonFromVertices(const PolygonVertex& a, const PolygonVertex& b, + const PolygonVertex& c) { + Polygon polygon; + polygon.vertices = {a.vertex, b.vertex, c.vertex}; + polygon.normals = {a.normal, b.normal, c.normal}; + polygon.texture_vertices = std::nullopt; + + polygon.texture_vertices = std::nullopt; + if (a.texture_coordinates.x >= 0) { + polygon.texture_vertices = {a.texture_coordinates, b.texture_coordinates, + c.texture_coordinates}; + } + return polygon; +} + +std::vector ClipPolygon(Vec3 plane_normal, CoordType plane_offset, + const Polygon& polygon) { + std::vector inside_indices; + std::vector outside_indices; + for (Index i = 0; i < Polygon::kVertexCount; ++i) { + if (IsPointOnCorrectSideOfPlane(plane_normal, plane_offset, polygon.vertices[i])) { + inside_indices.push_back(i); + } else { + outside_indices.push_back(i); + } + } + if (inside_indices.size() == 0) { + return {}; + } + if (inside_indices.size() == 1) { + if (inside_indices[0] == 1) { + std::swap(outside_indices[0], outside_indices[1]); + } + PolygonVertex inside = MakePolygonVertex(polygon, inside_indices[0]); + PolygonVertex outside0 = MakePolygonVertex(polygon, outside_indices[0]); + PolygonVertex outside1 = MakePolygonVertex(polygon, outside_indices[1]); + + PolygonVertex intersection1 = + PolygonSidePlaneIntersection(plane_normal, plane_offset, inside, outside0); + + PolygonVertex intersection2 = + PolygonSidePlaneIntersection(plane_normal, plane_offset, inside, outside1); + return {MakePolygonFromVertices(inside, intersection1, intersection2)}; + } + if (inside_indices.size() == 2) { + if (outside_indices[0] == 1) { + std::swap(inside_indices[0], inside_indices[1]); + } + PolygonVertex outside = MakePolygonVertex(polygon, outside_indices[0]); + PolygonVertex inside0 = MakePolygonVertex(polygon, inside_indices[0]); + PolygonVertex inside1 = MakePolygonVertex(polygon, inside_indices[1]); + + PolygonVertex intersection1 = + PolygonSidePlaneIntersection(plane_normal, plane_offset, outside, inside0); + PolygonVertex intersection2 = + PolygonSidePlaneIntersection(plane_normal, plane_offset, outside, inside1); + return {MakePolygonFromVertices(inside0, intersection2, intersection1), + MakePolygonFromVertices(inside0, inside1, intersection2)}; + } + return {polygon}; +} + +void NormalizePlane(Vec4& plane) { + plane /= glm::length(Vec3(plane)); +} + +std::array GetClippingPlanes(const Mat4& projection_matrix) { + std::array frustum_planes; + frustum_planes[0] = glm::row(projection_matrix, 3) + glm::row(projection_matrix, 2); + frustum_planes[1] = glm::row(projection_matrix, 3) - glm::row(projection_matrix, 2); + NormalizePlane(frustum_planes[0]); + NormalizePlane(frustum_planes[1]); + return frustum_planes; +} + +void ClipPolygons(const Mat4& projection_matrix, std::vector& polygons_to_clip) { + auto frustum_planes = GetClippingPlanes(projection_matrix); + for (Index i = 0; i < frustum_planes.size(); ++i) { + std::vector clipped; + for (Polygon& polygon : polygons_to_clip) { + for (Polygon& clipped_polygon : + ClipPolygon(Vec3(frustum_planes[i]), frustum_planes[i].w, polygon)) { + clipped.emplace_back(std::move(clipped_polygon)); + } + } + polygons_to_clip = std::move(clipped); + } +} + +Color CalculateLightColor(const Light& light, const Vec3& position, const Vec3& normal, + const Material& material, Vec2 texture_coordinates) { + Color ambient_color = material.ambient; + if (material.ambient_texture) { + ambient_color = DiscreteColorToColor( + material.ambient_texture.value()->SampleColor(texture_coordinates)); + } + Color diffuse_color = material.diffuse; + if (material.diffuse_texture) { + diffuse_color = DiscreteColorToColor( + material.diffuse_texture.value()->SampleColor(texture_coordinates)); + } + Color specular_color = material.specular; + if (material.specular_texture) { + specular_color = DiscreteColorToColor( + material.specular_texture.value()->SampleColor(texture_coordinates)); + } + if (std::holds_alternative(light)) { + const AmbientLight& current_light = std::get(light); + return current_light.color * ambient_color; + } + if (std::holds_alternative(light)) { + const DirectionalLight& current_light = std::get(light); + Vec3 light_direction = glm::normalize(-current_light.direction); + Vec3 view_direction = glm::normalize(-position); + Vec3 halfway = glm::normalize(view_direction + light_direction); + + ColorValue diff = std::max(glm::dot(light_direction, normal), 0.); + ColorValue spec = std::pow(std::max(glm::dot(halfway, normal), 0.), material.shininess); + Color diffuse = diffuse_color * diff; + Color specular = specular_color * spec; + return (diffuse + specular) * current_light.color; + } + if (std::holds_alternative(light)) { + const PointLight& current_light = std::get(light); + Vec3 light_direction = (current_light.position - position); + ColorValue distance_to_surface = glm::length(light_direction); + light_direction = glm::normalize(light_direction); + Vec3 view_direction = glm::normalize(-position); + Vec3 halfway = glm::normalize(view_direction + light_direction); + ColorValue attenuation = 1.0f / (current_light.constant_attenuation + + current_light.linear_attenuation * distance_to_surface + + current_light.quadratic_attenuation * distance_to_surface * + distance_to_surface); + + ColorValue diff = std::max(glm::dot(light_direction, normal), 0.); + ColorValue spec = std::pow(std::max(glm::dot(halfway, normal), 0.), material.shininess); + Color diffuse = diffuse_color * diff; + Color specular = specular_color * spec; + return (diffuse + specular) * current_light.color * attenuation; + } + return kBlack; +} + +DiscreteColor CalculateColor(const Vec3& barycentric, const Polygon& polygon, + const Polygon& original_polygon, const Material& material, + const std::vector& lights) { + Vec3 inv_w = {-original_polygon.vertices[0].z, -original_polygon.vertices[1].z, + -original_polygon.vertices[2].z}; + inv_w = 1. / inv_w; + CoordType inv_denominator = 1. / glm::dot(barycentric, inv_w); + Vec3 weights = barycentric * inv_w; + Vec3 normal = (weights[0] * polygon.normals[0] + weights[1] * polygon.normals[1] + + weights[2] * polygon.normals[2]) * + inv_denominator; + normal = glm::normalize(normal); + Vec3 position = + (weights[0] * original_polygon.vertices[0] + weights[1] * original_polygon.vertices[1] + + weights[2] * original_polygon.vertices[2]) * + inv_denominator; + Vec2 texture_coords = {-1, -1}; + if (polygon.texture_vertices) { + const auto& texture_vertices = polygon.texture_vertices.value(); + texture_coords = (weights[0] * texture_vertices[0] + weights[1] * texture_vertices[1] + + weights[2] * texture_vertices[2]) * + inv_denominator; + } + Color light_color = kBlack; + for (const Light& light : lights) { + light_color += CalculateLightColor(light, position, normal, material, texture_coords); + } + return ColorToDiscrete(light_color); +} + +Light GetTransformedLight(const Camera& camera, const Light& light) { + if (std::holds_alternative(light)) { + return light; + } + if (std::holds_alternative(light)) { + DirectionalLight current_light = std::get(light); + Vec4 dir(current_light.direction, 0); + dir = glm::transpose(camera.GetRotationMatrix()) * dir; + current_light.direction = Vec3(dir); + return current_light; + } + if (std::holds_alternative(light)) { + PointLight current_light = std::get(light); + Vec4 pos(current_light.position, 1.); + pos = camera.MakeWorldToCameraMatrix() * pos; + current_light.position = Vec3(pos); + return current_light; + } + assert(false); + return AmbientLight{}; +} + +} // namespace + +void Renderer::Render(const World& world, const Camera& camera, Picture* picture) { + Index height = picture->GetHeight(); + Index width = picture->GetWidth(); + assert(height > 0 && "Height must be positive"); + assert(width > 0 && "Width must be positive"); + picture->SetDefaultColor(); + z_buffer_.SetDefaultValue(*picture); + CoordType aspect_ratio = GetAspectRatio(Height{height}, Width{width}); + Mat4 projection_matrix = + glm::perspective(camera.GetFOV(), GetAspectRatio(Height{height}, Width{width}), + camera.GetNearDist(), camera.GetFarDist()); + + for (const Object3D& object : world.GetObjects()) { + RenderObject(object, camera, world.GetLights(), projection_matrix, picture); + } +} + +void Renderer::RenderObject(const Object3D& object, const Camera& camera, + const std::vector& lights, const Mat4& projection_matrix, + Picture* picture) { + Mat4 transform_to_camera = + camera.MakeWorldToCameraMatrix() * glm::translate(Mat4(1.), object.GetLocalOrigin()); + for (const Mesh& mesh : object.GetMeshes()) { + RenderMesh(mesh, camera, lights, projection_matrix, transform_to_camera, picture); + } +} + +void Renderer::RenderMesh(const Mesh& mesh, const Camera& camera, const std::vector& lights, + const Mat4& projection_matrix, const Mat4& transform_to_camera, + Picture* picture) { + Height height = Height{picture->GetHeight()}; + Width width = Width{picture->GetWidth()}; + std::vector polygons; + for (const Polygon& polygon : mesh.GetPolygons()) { + Polygon translated_polygon(polygon); + TransformPolygon(transform_to_camera, translated_polygon); + if (mesh.GetMaterial().two_sided || IsVisible(translated_polygon)) { + TransformNormals(glm::transpose(camera.GetRotationMatrix()), translated_polygon); + polygons.emplace_back(std::move(translated_polygon)); + } + } + ClipPolygons(projection_matrix, polygons); + std::vector transformed_lights; + transformed_lights.reserve(lights.size()); + for (const Light& light : lights) { + transformed_lights.push_back(GetTransformedLight(camera, light)); + } + std::vector transformed_polygons = polygons; + for (Polygon& transformed_polygon : transformed_polygons) { + ProjectiveTransformPolygon(projection_matrix, transformed_polygon); + TransformPolygonToScreenSpace(transformed_polygon, height, width); + } + for (Index i = 0; i < transformed_polygons.size(); ++i) { + DrawPolygon(picture, transformed_polygons[i], polygons[i], mesh.GetMaterial(), + transformed_lights); + } +} + +void Renderer::DrawPolygon(Picture* picture, const Polygon& polygon, + const Polygon& original_polygon, const Material& material, + const std::vector& lights) { + Index min_x = picture->GetWidth() + picture->GetHeight() + 1; + Index min_y = picture->GetWidth() + picture->GetHeight() + 1; + Index max_x = -1; + Index max_y = -1; + for (int i = 0; i < Polygon::kVertexCount; ++i) { + min_x = std::min(RoundDown(polygon.vertices[i].x), min_x); + min_y = std::min(RoundDown(polygon.vertices[i].y), min_y); + max_x = std::max(RoundUp(polygon.vertices[i].x), max_x); + max_y = std::max(RoundUp(polygon.vertices[i].y), max_y); + } + min_x = std::max(0, min_x); + min_y = std::max(0, min_y); + max_x = std::min((picture->GetWidth() - 1), max_x); + max_y = std::min((picture->GetHeight() - 1), max_y); + BarycentricCoordinateSystem barycentric_system(polygon); + for (Index x = min_x; x <= max_x; ++x) { + for (Index y = min_y; y <= max_y; ++y) { + Vec2 point_to_check = {static_cast(x) + 0.5, + static_cast(y) + 0.5}; + Vec3 barycentric = barycentric_system.GetBarycentricCoordinates(point_to_check); + if (!IsInsidePolygon(barycentric)) { + continue; + } + CoordType current_z = CalculateZ(barycentric, polygon); + if (z_buffer_(x, y) <= current_z) { + continue; + } + z_buffer_(x, y) = current_z; + (*picture)(x, y) = + CalculateColor(barycentric, polygon, original_polygon, material, lights); + } + } +} + +} // namespace renderer diff --git a/src/renderer.h b/src/renderer.h new file mode 100644 index 0000000..df2d07c --- /dev/null +++ b/src/renderer.h @@ -0,0 +1,29 @@ +#pragma once + +#include "linalg.h" +#include "picture.h" +#include "world.h" +#include "camera.h" +#include "light.h" +#include "z_buffer.h" + +namespace renderer { + +class Renderer { +public: + void Render(const World& world, const Camera& camera, Picture* picture); + +private: + void DrawPolygon(Picture* picture, const Polygon& polygon, const Polygon& original_polygon, + const Material& material, const std::vector& lights); + void RenderObject(const Object3D& object, const Camera& camera, + const std::vector& lights, const Mat4& projection_matrix, + Picture* picture); + void RenderMesh(const Mesh& mesh, const Camera& camera, const std::vector& lights, + const Mat4& projection_matrix, const Mat4& transform_to_camera, + Picture* picture); + + ZBuffer z_buffer_; +}; + +} // namespace renderer diff --git a/src/sources.cmake b/src/sources.cmake new file mode 100644 index 0000000..4ea811f --- /dev/null +++ b/src/sources.cmake @@ -0,0 +1,15 @@ +add_library(3d_pipeline_lib + camera.cpp + picture.cpp + renderer.cpp + polygon.cpp + world.cpp + mesh.cpp + geometry.cpp + texture.cpp + texture_loader.cpp + model_loader.cpp + object_3d.cpp + z_buffer.cpp + color.cpp +) diff --git a/src/texture.cpp b/src/texture.cpp new file mode 100644 index 0000000..12775fa --- /dev/null +++ b/src/texture.cpp @@ -0,0 +1,44 @@ +#include "texture.h" +#include +#include +#include +#include "alias.h" +#include "color.h" +#include "linalg.h" +#include "picture.h" + +namespace renderer { + +Texture::Texture() { + std::vector pixel(1, kWhite); + impl_ = std::make_shared(Width{1}, std::move(pixel)); +} + +Texture::Texture(const Picture& picture) : impl_(std::make_shared(picture)) { +} + +Texture::Texture(Picture&& picture) : impl_(std::make_shared(std::move(picture))) { +} + +Texture Texture::From(const Picture& picture) { + return Texture(picture); +} + +Texture Texture::From(Picture&& picture) { + return Texture(std::move(picture)); +} + +const Picture& Texture::operator*() const { + return *impl_; +} + +const Picture* Texture::operator->() const { + return impl_.get(); +} + +const DiscreteColor& Texture::SampleColor(Vec2 coords) const { + assert(0 <= coords.x <= 1 && 0 <= coords.y <= 1 && "Texture coordinates must be in [0, 1]"); + return impl_->SampleColor(coords); +} + +} // namespace renderer diff --git a/src/texture.h b/src/texture.h new file mode 100644 index 0000000..1c3aee4 --- /dev/null +++ b/src/texture.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include "linalg.h" +#include "picture.h" +#include "color.h" + +namespace renderer { + +class Texture { +public: + Texture(); + Texture(const Picture& picture); + Texture(Picture&& picture); + + static Texture From(const Picture& picture); + Texture From(Picture&& picture); + const Picture& operator*() const; + const Picture* operator->() const; + + const DiscreteColor& SampleColor(Vec2 coords) const; + +private: + std::shared_ptr impl_; +}; + +} // namespace renderer diff --git a/src/texture_loader.cpp b/src/texture_loader.cpp new file mode 100644 index 0000000..f4469a0 --- /dev/null +++ b/src/texture_loader.cpp @@ -0,0 +1,71 @@ +#include "texture_loader.h" +#include +#include +#include +#include "linalg.h" +#include "picture.h" +#include "texture.h" +#define STB_IMAGE_IMPLEMENTATION +#include + +namespace renderer { + +namespace { + +class STBLoader { +public: + explicit STBLoader(std::filesystem::path path) { + data_ = stbi_load(path.c_str(), &width_, &height_, &channel_count_, 3); + } + + ~STBLoader() { + stbi_image_free(data_); + } + + Index GetSize() { + return height_ * width_; + } + + const unsigned char *GetData() { + return data_; + } + + Index GetWidth() { + return width_; + } + +private: + Index width_; + Index height_; + Index channel_count_; + unsigned char *data_; +}; + +} // namespace + +Texture TextureLoader::LoadTexture(std::filesystem::path path) { + if (HasTexture(path)) { + return loaded_textures_[path]; + } + STBLoader loader(path); + if (loader.GetData() == nullptr) { + return Texture{}; + } + std::vector pixels; + pixels.reserve(loader.GetSize()); + for (Index i = 0; i < loader.GetSize(); ++i) { + Index data_index = i * 3; + pixels.emplace_back(loader.GetData()[data_index], loader.GetData()[data_index + 1], + loader.GetData()[data_index + 2]); + } + Picture loaded_picture{Width{loader.GetWidth()}, std::move(pixels)}; + Texture texture{std::move(loaded_picture)}; + loaded_textures_[path] = texture; + return texture; +} + +bool TextureLoader::HasTexture(std::filesystem::path path) const { + return loaded_textures_.contains(path); +} + +} // namespace renderer diff --git a/src/texture_loader.h b/src/texture_loader.h new file mode 100644 index 0000000..aee083f --- /dev/null +++ b/src/texture_loader.h @@ -0,0 +1,19 @@ +#pragma once + +#include "texture.h" +#include + +namespace renderer { + +// я не справился написать pimpl :( + +class TextureLoader { +public: + Texture LoadTexture(std::filesystem::path path); + bool HasTexture(std::filesystem::path path) const; + +private: + std::unordered_map loaded_textures_; +}; + +} // namespace renderer diff --git a/src/world.cpp b/src/world.cpp new file mode 100644 index 0000000..921e72a --- /dev/null +++ b/src/world.cpp @@ -0,0 +1,35 @@ +#include "world.h" + +namespace renderer { + +World::World(std::vector&& objects) : objects_(std::move(objects)) { +} + +World::World(const std::vector& objects) : objects_(objects) { +} + +const std::vector& World::GetObjects() const { + return objects_; +} + +void World::AddObject(const Object3D& object) { + objects_.push_back(object); +} + +void World::AddObject(Object3D&& object) { + objects_.push_back(std::move(object)); +} + +void World::AddLight(const Light& light) { + lights_.push_back(light); +} + +void World::AddLight(Light&& light) { + lights_.push_back(std::move(light)); +} + +const std::vector& World::GetLights() const { + return lights_; +} + +} // namespace renderer diff --git a/src/world.h b/src/world.h new file mode 100644 index 0000000..b0942d6 --- /dev/null +++ b/src/world.h @@ -0,0 +1,28 @@ +#pragma once + +#include "light.h" +#include "object_3d.h" + +#include + +namespace renderer { + +class World { +public: + World() = default; + World(std::vector&& objects); + World(const std::vector& objects); + + const std::vector& GetObjects() const; + void AddObject(const Object3D& object); + void AddObject(Object3D&& object); + void AddLight(const Light& light); + void AddLight(Light&& light); + const std::vector& GetLights() const; + +private: + std::vector objects_; + std::vector lights_; +}; + +} // namespace renderer diff --git a/src/z_buffer.cpp b/src/z_buffer.cpp new file mode 100644 index 0000000..18027dd --- /dev/null +++ b/src/z_buffer.cpp @@ -0,0 +1,31 @@ +#include "z_buffer.h" +#include +#include +#include +#include "linalg.h" + +namespace renderer { + +void ZBuffer::SetDefaultValue(const Picture& picture) { + if (z_buffer_.size() < picture.GetPixels().size()) { + z_buffer_.resize(picture.GetPixels().size()); + } + width_ = picture.GetWidth(); + std::fill(std::execution::par, z_buffer_.begin(), z_buffer_.end(), 2.); +} + +CoordType ZBuffer::operator()(Index x, Index y) const { + Index index_in_buffer = y * width_ + x; + assert(0 <= index_in_buffer && index_in_buffer < z_buffer_.size() && + "Position must be in buffer"); + return z_buffer_[index_in_buffer]; +} + +CoordType& ZBuffer::operator()(Index x, Index y) { + Index index_in_buffer = y * width_ + x; + assert(0 <= index_in_buffer && index_in_buffer < z_buffer_.size() && + "Position must be in buffer"); + return z_buffer_[index_in_buffer]; +} + +} // namespace renderer diff --git a/src/z_buffer.h b/src/z_buffer.h new file mode 100644 index 0000000..e8219c8 --- /dev/null +++ b/src/z_buffer.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include "linalg.h" +#include "picture.h" + +namespace renderer { + +class ZBuffer { +public: + ZBuffer() = default; + void SetDefaultValue(const Picture& picture); + CoordType operator()(Index x, Index y) const; + CoordType& operator()(Index x, Index y); + +private: + Index width_ = 0; + std::vector z_buffer_; +}; + +} // namespace renderer diff --git a/submodules/stb b/submodules/stb new file mode 160000 index 0000000..f056911 --- /dev/null +++ b/submodules/stb @@ -0,0 +1 @@ +Subproject commit f0569113c93ad095470c54bf34a17b36646bbbb5