diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index 7004f18..dd3d9eb 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -5,14 +5,10 @@ function (generate_example example_name) add_executable(${example_name} "${example_name}_example.cc") target_link_libraries(${example_name} "${LIB_PREFIX}spica_renderer${LIB_SUFFIX}") configure_file(project.vcxproj.user.in "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${example_name}.vcxproj.user" @ONLY) - - endif() endfunction() -add_executable(simplept simplept_example.cc) -add_executable(simplebpt simplebpt_example.cc) - -target_link_libraries(simplept "${LIB_PREFIX}spica_renderer${LIB_SUFFIX}") -target_link_libraries(simplebpt "${LIB_PREFIX}spica_renderer${LIB_SUFFIX}") +generate_example(simplept) +generate_example(simplebpt) +generate_example(simplemlt) include_directories(${CMAKE_CURRENT_LIST_DIR}) diff --git a/example/simplemlt_example.cc b/example/simplemlt_example.cc new file mode 100644 index 0000000..b051733 --- /dev/null +++ b/example/simplemlt_example.cc @@ -0,0 +1,37 @@ +#include + +#include "../include/spica.h" +using namespace spica; + + +int main(int argc, char **argv) { + std::cout << "Metropolis light transport" << std::endl; + + Scene scene; + scene.addSphere(Sphere(5.0, Vector3(50.0, 75.0, 81.6), Color(12, 12, 12), Color(), REFLECTION_DIFFUSE), true); // Light + scene.addSphere(Sphere(1e5, Vector3(1e5 + 1, 40.8, 81.6), Color(), Color(0.75, 0.25, 0.25), REFLECTION_DIFFUSE)); // Left + scene.addSphere(Sphere(1e5, Vector3(-1e5 + 99, 40.8, 81.6), Color(), Color(0.25, 0.25, 0.75), REFLECTION_DIFFUSE)); // Right + scene.addSphere(Sphere(1e5, Vector3(50, 40.8, 1e5), Color(), Color(0.75, 0.75, 0.75), REFLECTION_DIFFUSE)); // Back + scene.addSphere(Sphere(1e5, Vector3(50, 40.8, -1e5 + 170), Color(), Color(), REFLECTION_DIFFUSE)); // Front + scene.addSphere(Sphere(1e5, Vector3(50, 1e5, 81.6), Color(), Color(0.75, 0.75, 0.75), REFLECTION_DIFFUSE)); // Floor + scene.addSphere(Sphere(1e5, Vector3(50, -1e5 + 81.6, 81.6), Color(), Color(0.75, 0.75, 0.75), REFLECTION_DIFFUSE)); // Ceil + scene.addSphere(Sphere(16.5, Vector3(27, 16.5, 47), Color(), Color(1, 1, 1)*.99, REFLECTION_SPECULAR)); // Mirror + scene.addSphere(Sphere(16.5, Vector3(73, 16.5, 78), Color(), Color(1, 1, 1)*.99, REFLECTION_REFRACTION)); // Glass + + const int width = 320; + const int height = 240; + const int mutation = 32 * width * height; + const int mlt_num = 32; + const int maxDepth = 5; + Random rng = Random::getRNG(); + + Ray camera(Vector3(50.0, 52.0, 295.6), Vector3(0.0, -0.042612, -1.0).normalize()); + Vector3 cx = Vector3(width * 0.5135 / height, 0.0, 0.0); + Vector3 cy = cx.cross(camera.direction()).normalize() * 0.5135; + Image image(width, height); + + MLTRenderer renderer; + renderer.render(scene, mlt_num, mutation, image, camera, cx, cy, width, height, maxDepth, rng); + + image.savePPM("simplemlt.ppm"); +} \ No newline at end of file diff --git a/include/spica.h b/include/spica.h index 68db9b6..89a0465 100644 --- a/include/spica.h +++ b/include/spica.h @@ -10,5 +10,6 @@ #include "../src/renderer/scene.h" #include "../src/renderer/camera.h" #include "../src/renderer/bpt_renderer.h" +#include "../src/renderer/mlt_renderer.h" #endif // SPICA_H_ diff --git a/src/renderer/CMakeLists.txt b/src/renderer/CMakeLists.txt index e2a43ce..4fc22ef 100644 --- a/src/renderer/CMakeLists.txt +++ b/src/renderer/CMakeLists.txt @@ -7,6 +7,7 @@ set(SOURCES ${CMAKE_CURRENT_LIST_DIR}/renderer.cc ${CMAKE_CURRENT_LIST_DIR}/renderer_base.cc ${CMAKE_CURRENT_LIST_DIR}/bpt_renderer.cc + ${CMAKE_CURRENT_LIST_DIR}/mlt_renderer.cc PARENT_SCOPE) set(HEADERS @@ -18,4 +19,5 @@ set(HEADERS ${CMAKE_CURRENT_LIST_DIR}/renderer_base.h ${CMAKE_CURRENT_LIST_DIR}/bpt_renderer.h ${CMAKE_CURRENT_LIST_DIR}/material.h + ${CMAKE_CURRENT_LIST_DIR}/mlt_renderer.h PARENT_SCOPE) diff --git a/src/renderer/mlt_renderer.cc b/src/renderer/mlt_renderer.cc new file mode 100644 index 0000000..b9308e7 --- /dev/null +++ b/src/renderer/mlt_renderer.cc @@ -0,0 +1,403 @@ +#define SPICA_MLT_RENDERER_EXPORT +#include "mlt_renderer.h" + +#include +#include +#include +#include + +namespace spica { + + namespace { + + struct PrimarySample { + int modify_time; + double value; + static const Random rnd; + + PrimarySample() { + modify_time = 0; + value = rnd.randReal(); + } + }; + + const Random PrimarySample::rnd = Random::getRNG(); + + struct KelemenMLT { + private: + static const int num_init_primary_samples = 128; + + inline double mutate(const double x) { + const double r = rng->randReal(); + const double s1 = 1.0 / 512.0; + const double s2 = 1.0 / 16.0; + const double dx = s1 / (s1 / s2 + abs(2.0 * r - 1.0)) - s1 / (s1 / s2 + 1.0); + if (r < 0.5) { + const double x1 = x + dx; + return (x1 < 1.0) ? x1 : x1 - 1.0; + } else { + const double x1 = x - dx; + return (x1 < 0.0) ? x1 + 1.0 : x1; + } + } + + public: + + int global_time; + int large_step; + int large_step_time; + int used_rand_coords; + const Random* rng; + + std::vector primary_samples; + std::stack primary_samples_stack; + + KelemenMLT(const Random& rng = Random::getRNG()) + : global_time(0) + , large_step(0) + , large_step_time(0) + , used_rand_coords(0) + , rng(&rng) + , primary_samples() + , primary_samples_stack() + { + primary_samples.resize(num_init_primary_samples); + } + + void initUsedRandCoords() { + used_rand_coords = 0; + } + + inline double nextSample() { + if (primary_samples.size() <= used_rand_coords) { + primary_samples.resize(primary_samples.size() * 1.5); + } + + if (primary_samples[used_rand_coords].modify_time < global_time) { + if (large_step > 0) { + primary_samples_stack.push(primary_samples[used_rand_coords]); + primary_samples[used_rand_coords].modify_time = global_time; + primary_samples[used_rand_coords].value = rng->randReal(); + } else { + if (primary_samples[used_rand_coords].modify_time < large_step_time) { + primary_samples[used_rand_coords].modify_time = large_step_time; + primary_samples[used_rand_coords].value = rng->randReal(); + } + + while (primary_samples[used_rand_coords].modify_time < global_time - 1) { + primary_samples[used_rand_coords].value = mutate(primary_samples[used_rand_coords].value); + primary_samples[used_rand_coords].modify_time++; + } + + primary_samples_stack.push(primary_samples[used_rand_coords]); + primary_samples[used_rand_coords].value = mutate(primary_samples[used_rand_coords].value); + primary_samples[used_rand_coords].modify_time = global_time; + } + } + used_rand_coords++; + return primary_samples[used_rand_coords - 1].value; + } + }; + + double luminance(const Color& color) { + return Vector3(0.2126, 0.7152, 0.0722).dot(color); + } + + Color direct_radiance_sample(const Scene& scene, const Vector3& v0, const Vector3& normal, const int id, KelemenMLT& mlt) { + const double r1 = 2.0 * PI * mlt.nextSample(); + const double r2 = 1.0 - 2.0 * mlt.nextSample(); + const Sphere* light_ptr = reinterpret_cast(scene.getObjectPtr(scene.lightId())); + const Vector3 light_pos = light_ptr->center() + (light_ptr->radius() * Vector3(sqrt(1.0 - r2 * r2) * cos(r1), sqrt(1.0 - r2 * r2) * sin(r1), r2)); + + // -- + const Vector3 light_normal = (light_pos - light_ptr->center()).normalize(); + const Vector3 v_to_l = light_pos - v0; + const Vector3 light_dir = v_to_l.normalize(); + const double dist2 = v_to_l.dot(v_to_l); + const double dot0 = normal.dot(light_dir); + const double dot1 = light_normal.dot(-1.0 * light_dir); + + if (dot0 >= 0.0 && dot1 >= 0.0) { + const double G = dot0 * dot1 / dist2; + Intersection intersection; + if (scene.intersect(Ray(v0, light_dir), intersection) && intersection.objectId() == scene.lightId()) { + const Primitive* obj_ptr = scene.getObjectPtr(id); + const double light_radius = light_ptr->radius(); + return obj_ptr->color().cwiseMultiply(light_ptr->emission()) * (1.0 / PI) * G * (4.0 * PI * light_radius * light_radius); + } + } + return Color(0.0, 0.0, 0.0); + } + + Color radiance(const Scene& scene, const Ray& ray, const int depth, const int maxDepth, KelemenMLT& mlt) { + Intersection intersection; + if (!scene.intersect(ray, intersection)) { + return Color(0.0, 0.0, 0.0); + } + + const Primitive* obj_ptr = scene.getObjectPtr(intersection.objectId()); + const HitPoint& hitpoint = intersection.hitPoint(); + const Vector3 orient_normal = hitpoint.normal().dot(ray.direction()) < 0.0 ? hitpoint.normal() : -hitpoint.normal(); + + const Color& obj_color = obj_ptr->color(); + double roulette_probability = std::max(obj_color.red(), std::max(obj_color.green(), obj_color.blue())); + + if (depth > maxDepth) { + if (mlt.nextSample() >= roulette_probability) { + return Color(0.0, 0.0, 0.0); + } + } else { + roulette_probability = 1.0; + } + + if (obj_ptr->reftype() == REFLECTION_DIFFUSE) { + if (intersection.objectId() != scene.lightId()) { + const int shadow_ray = 1; + Vector3 direct_light; + for (int i = 0; i < shadow_ray; i++) { + direct_light = direct_light + direct_radiance_sample(scene, hitpoint.position(), orient_normal, intersection.objectId(), mlt) / shadow_ray; + } + + Vector3 w, u, v; + w = orient_normal; + if (abs(w.x()) > EPS) { + u = Vector3(0.0, 1.0, 0.0).cross(w).normalize(); + } else { + u = Vector3(1.0, 0.0, 0.0).cross(w).normalize(); + } + v = w.cross(u); + + const double r1 = 2.0 * PI * mlt.nextSample(); + const double r2 = mlt.nextSample(); + const double r2s = sqrt(r2); + Vector3 next_dir = (u * cos(r1) * r2s + v * sin(r1) * r2s + w * sqrt(1.0 - r2)).normalize(); + + const Color next_bounce_color = radiance(scene, Ray(hitpoint.position(), next_dir), depth + 1, maxDepth, mlt); + return (direct_light + obj_color.cwiseMultiply(next_bounce_color)) / roulette_probability; + } else if (depth == 0) { + return obj_ptr->emission(); + } else { + return Color(0.0, 0.0, 0.0); + } + } + else if (obj_ptr->reftype() == REFLECTION_SPECULAR) { + Intersection light_intersect; + Ray reflection_ray = Ray(hitpoint.position(), Vector3::reflect(ray.direction(), hitpoint.normal())); + scene.intersect(reflection_ray, light_intersect); + Vector3 direct_light; + if (light_intersect.objectId() == scene.lightId()) { + direct_light = scene.getObjectPtr(scene.lightId())->emission(); + } + const Color next_bounce_color = radiance(scene, reflection_ray, depth + 1, maxDepth, mlt); + return (direct_light + obj_color.cwiseMultiply(next_bounce_color)) / roulette_probability; + } else if (obj_ptr->reftype() == REFLECTION_REFRACTION) { + Intersection light_intersect; + Ray reflection_ray = Ray(hitpoint.position(), Vector3::reflect(ray.direction(), hitpoint.normal())); + scene.intersect(reflection_ray, light_intersect); + Vector3 direct_light; + if (light_intersect.objectId() == scene.lightId()) { + direct_light = scene.getObjectPtr(scene.lightId())->emission(); + } + + bool is_incoming = hitpoint.normal().dot(orient_normal) > 0.0; + + // Snell + const double nc = 1.0; + const double nt = 1.5; + const double nnt = is_incoming ? nc / nt : nt / nc; + const double ddn = ray.direction().dot(orient_normal); + const double cos2t = 1.0 - nnt * nnt * (1.0 - ddn * ddn); + + if (cos2t < 0.0) { // Total reflection + const Color next_bounce_color = radiance(scene, reflection_ray, depth + 1, maxDepth, mlt); + return (direct_light + obj_color.cwiseMultiply(next_bounce_color)) / roulette_probability; + } + + Vector3 tdir = (ray.direction() * nnt - hitpoint.normal() * (is_incoming ? 1.0 : -1.0) * (ddn * nnt + sqrt(cos2t))).normalize(); + + // Schlick + const double a = nt - nc; + const double b = nt + nc; + const double R0 = (a * a) / (b * b); + const double c = 1.0 - (is_incoming ? -ddn : tdir.dot(hitpoint.normal())); + const double Re = R0 + (1.0 - R0) * pow(c, 5.0); + const double Tr = 1.0 - Re; + const double probability = 0.25 + 0.5 * Re; + + Ray refraction_ray = Ray(hitpoint.position(), tdir); + Intersection light_intersect_refract; + scene.intersect(reflection_ray, light_intersect_refract); + Vector3 direct_light_refraction; + if (light_intersect_refract.objectId() == scene.lightId()) { + direct_light_refraction = scene.getObjectPtr(scene.lightId())->emission(); + } + + if (depth > 2) { + if (mlt.nextSample() < probability) { + const Color next_bounce_color = radiance(scene, reflection_ray, depth + 1, maxDepth, mlt); + return obj_color.cwiseMultiply(direct_light + Re * next_bounce_color) / (probability * roulette_probability); + } else { + const Color next_bounce_color = radiance(scene, refraction_ray, depth + 1, maxDepth, mlt); + return obj_color.cwiseMultiply(direct_light_refraction + Tr * next_bounce_color) / ((1.0 - probability) * roulette_probability); + } + } else { + const Color next_bounce_color_reflect = direct_light + radiance(scene, reflection_ray, depth + 1, maxDepth, mlt); + const Color next_bounce_color_refract = direct_light_refraction + radiance(scene, refraction_ray, depth + 1, maxDepth, mlt); + const Color next_bounce_color = Re * next_bounce_color_reflect + Tr * next_bounce_color_refract; + return obj_color.cwiseMultiply(next_bounce_color) / roulette_probability; + } + } + return Color(0.0, 0.0, 0.0); + } + + struct PathSample { + int x, y; + Color F; + double weight; + PathSample(const int x_ = 0, const int y_ = 0, const Color& F_ = Color(), const double weight_ = 1.0) + : x(x_) + , y(y_) + , F(F_) + , weight(weight_) + { + } + }; + + PathSample generate_new_path(const Scene& scene, const Ray& camera, const Vector3& cx, const Vector3& cy, const int width, const int height, KelemenMLT& mlt, int x, int y, int maxDepth) { + double weight = 4.0; + if (x < 0) { + weight *= width; + x = mlt.nextSample() * width; + if (x == width) { + x = 0; + } + } + + if (y < 0) { + weight *= height; + y = mlt.nextSample() * height; + if (y == height) { + y = 0; + } + } + + int sx = mlt.nextSample() < 0.5 ? 0 : 1; + int sy = mlt.nextSample() < 0.5 ? 0 : 1; + + const double r1 = 2.0 * mlt.nextSample(); + const double r2 = 2.0 * mlt.nextSample(); + const double dx = r1 < 1.0 ? sqrt(r1) - 1.0 : 1.0 - sqrt(2.0 - r1); + const double dy = r2 < 1.0 ? sqrt(r2) - 1.0 : 1.0 - sqrt(2.0 - r2); + Vector3 dir = cx * (((sx + 0.5 + dx) / 2.0 + x) / width - 0.5) + cy * (((sy + 0.5 + dy) / 2.0 + y) / height - 0.5) + camera.direction(); + const Ray ray = Ray(camera.origin() + dir * 130.0, dir.normalize()); + + Color c = radiance(scene, ray, 0, maxDepth, mlt); + + return PathSample(x, y, c, weight); + } + + } // anonymous namespace + + MLTRenderer::MLTRenderer() + { + } + + MLTRenderer::~MLTRenderer() + { + } + + int MLTRenderer::render(const Scene& scene, const int mlt_num, const int mutation, Image& image, const Ray& camera, const Vector3& cx, const Vector3& cy, const int width, const int height, const int maxDepth, const Random& rng) { + for (int mi = 0; mi < mlt_num; mi++) { + Image tmp_image(width, height); + KelemenMLT mlt; + + int seed_path_max = width * height; + if (seed_path_max <= 0) { + seed_path_max = 1; + } + + std::vector seed_paths(seed_path_max); + double sumI = 0.0; + mlt.large_step = 1; + for (int i = 0; i < seed_path_max; i++) { + mlt.initUsedRandCoords(); + PathSample sample = generate_new_path(scene, camera, cx, cy, width, height, mlt, -1, -1, maxDepth); + mlt.global_time++; + while (!mlt.primary_samples_stack.empty()) { + mlt.primary_samples_stack.pop(); + } + + sumI += luminance(sample.F); + seed_paths[i] = sample; + } + + // Compute first path + const double rnd = rng.randReal() * sumI; + int selected_path = 0; + double accumulated_importance = 0.0; + for (int i = 0; i < seed_path_max; i++) { + accumulated_importance += luminance(seed_paths[i].F); + if (accumulated_importance >= rnd) { + selected_path = i; + break; + } + } + + // -- + const double b = sumI / seed_path_max; + const double p_large = 0.5; + const int M = mutation; + int accept = 0; + int reject = 0; + PathSample old_path = seed_paths[selected_path]; + int progress = 0; + for (int i = 0; i < M; i++) { + if ((i + 1) % (M / 10) == 0) { + progress += 10; + std::cout << progress << " % "; + std::cout << "Accept: " << accept << ", Reject: " << reject; + std::cout << ", Rate: " << (100.0 * accept / (accept + reject)) << " %" << std::endl; + } + + mlt.large_step = rng.randReal() < p_large ? 1 : 0; + mlt.initUsedRandCoords(); + PathSample new_path = generate_new_path(scene, camera, cx, cy, width, height, mlt, -1, -1, maxDepth); + + double a = std::min(1.0, luminance(new_path.F) / luminance(old_path.F)); + const double new_path_weight = (a + mlt.large_step) / (luminance(new_path.F) / b + p_large) / M; + const double old_path_weight = (1.0 - a) / (luminance(old_path.F) / b + p_large) / M; + + tmp_image.pixel(new_path.x, new_path.y) += new_path.weight * new_path_weight * new_path.F; + tmp_image.pixel(old_path.x, old_path.y) += old_path.weight * old_path_weight * old_path.F; + + if (rng.randReal() < a) { // Accept + accept++; + old_path = new_path; + if (mlt.large_step) { + mlt.large_step_time = mlt.global_time; + } + mlt.global_time++; + while (!mlt.primary_samples_stack.empty()) { + mlt.primary_samples_stack.pop(); + } + } else { // Reject + reject++; + int idx = mlt.used_rand_coords - 1; + while (!mlt.primary_samples_stack.empty()) { + mlt.primary_samples[idx--] = mlt.primary_samples_stack.top(); + mlt.primary_samples_stack.pop(); + } + } + } + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + image.pixel(x, height - y - 1) += tmp_image.pixel(x, y) / mlt_num; + } + } + } + + return 0; + } + +} // namespace spica diff --git a/src/renderer/mlt_renderer.h b/src/renderer/mlt_renderer.h new file mode 100644 index 0000000..c9f00ac --- /dev/null +++ b/src/renderer/mlt_renderer.h @@ -0,0 +1,30 @@ +#ifndef SPICA_MLT_RENDERER_H_ +#define SPICA_MLT_RENDERER_H_ + +#if defined(_WIN32) || defined(__WIN32__) + #ifdef SPICA_MLT_RENDERER_EXPORT + #define SPICA_MLT_RENDERER_DLL __declspec(dllexport) + #else + #define SPICA_MLT_RENDERER_DLL __declspec(dllimport) + #endif +#elif defined(linux) || defined(__linux) + #define SPICA_MLT_RENDERER_DLL +#endif + +#include "camera.h" +#include "scene.h" +#include "renderer_base.h" + +namespace spica { + + class SPICA_MLT_RENDERER_DLL MLTRenderer { + public: + MLTRenderer(); + ~MLTRenderer(); + + int render(const Scene& scene, const int mlt_num, const int mutation, Image& image, const Ray& camera, const Vector3& cx, const Vector3& cy, const int width, const int height, const int maxDepth, const Random& rng); + }; + +} // namespace spica + +#endif // SPICA_MLT_RENDERER_H_ diff --git a/src/utils/image.cc b/src/utils/image.cc index 2b63633..401a682 100644 --- a/src/utils/image.cc +++ b/src/utils/image.cc @@ -6,6 +6,8 @@ #include #include +#include "common.h" + namespace spica { Image::Image() @@ -18,8 +20,10 @@ namespace spica { Image::Image(int width, int height) : _width(width) , _height(height) - , _pixels(new Color[width * height]) + , _pixels(0) { + msg_assert(width >= 0 && height >= 0, "Image size must be positive"); + _pixels = new Color[_width * _height]; } Image::Image(const Image& image) @@ -47,11 +51,13 @@ namespace spica { } const Color& Image::operator()(int x, int y) const { + msg_assert(0 <= x && x < _width && 0 <= y && y < _height, "Pixel index out of bounds"); return _pixels[y * _width + x]; } Color& Image::pixel(int x, int y) { - return _pixels[y * _width + x]; + msg_assert(0 <= x && x < _width && 0 <= y && y < _height, "Pixel index out of bounds"); + return _pixels[y * _width + x]; } void Image::savePPM(const std::string& filename) const { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 86e519d..4b51a0e 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,43 +1,42 @@ # Enable GTest enable_testing() -# Link directory -link_directories(${CMAKE_LIBRARY_OUTPUT_DIRECTORY}) - -# Function to add GTest -if (NOT "$ENV{GTEST_ROOT}" STREQUAL "") - set(GTEST_LIB) - if (WIN32) - set(GTEST_LIB "gtest.lib") - set(GTEST_MAIN_LIB "gtest_main.lib") +option(BUILD_TEST "BUILD_TEST" ON) +set(GTEST_ROOT $ENV{GTEST_ROOT} CACHE STRING "GTEST_ROOT") + +if (${BUILD_TEST} STREQUAL "ON") + # Link directory + link_directories(${CMAKE_LIBRARY_OUTPUT_DIRECTORY}) + + if (NOT "${GTEST_ROOT}" STREQUAL "") + set(GTEST_LIB) + if (WIN32) + set(GTEST_LIB "gtest.lib") + set(GTEST_MAIN_LIB "gtest_main.lib") + else() + set(GTEST_LIB "libgtest.a") + set(GTEST_MAIN_LIB "libgtest_main.a") + endif() + set(GTEST_LIBRARY $ENV{GTEST_ROOT}/lib/${GTEST_LIB}) + set(GTEST_MAIN_LIBRARY $ENV{GTEST_ROOT}/lib/${GTEST_MAIN_LIB}) + set(GTEST_INCLUDE_DIRS $ENV{GTEST_ROOT}/include) else() - set(GTEST_LIB "libgtest.a") - set(GTEST_MAIN_LIB "libgtest_main.a") + set(GTEST_LIBRARY $ENV{GTEST_LIBRARY}) + set(GTEST_MAIN_LIBRARY $ENV{GTEST_MAIN_LIBRARY}) + set(GTEST_INCLUDE_DIRS $ENV{GTEST_INCLUDE_DIRS}) endif() - set(GTEST_LIBRARY $ENV{GTEST_ROOT}/lib/${GTEST_LIB}) - set(GTEST_MAIN_LIBRARY $ENV{GTEST_ROOT}/lib/${GTEST_MAIN_LIB}) - set(GTEST_INCLUDE_DIRS $ENV{GTEST_ROOT}/include) -else() - set(GTEST_LIBRARY $ENV{GTEST_LIBRARY}) - set(GTEST_MAIN_LIBRARY $ENV{GTEST_MAIN_LIBRARY}) - set(GTEST_INCLUDE_DIRS $ENV{GTEST_INCLUDE_DIRS}) -endif() - -function (add_gtest test_name test_source) - set(SOURCE_FILES ${test_source}) - add_executable(${test_name} ${SOURCE_FILES}) - target_link_libraries(${test_name} ${GTEST_LIBRARY}) - target_link_libraries(${test_name} ${GTEST_MAIN_LIBRARY}) - target_link_libraries(${test_name} "${LIB_PREFIX}spica_renderer${LIB_SUFFIX}") + set(TEST_NAME spica_tests) + set(SOURCE_FILES all_tests.cc test_geometry.cc test_vector.cc test_image.cc) - add_test(NAME ${test_name} COMMAND ${test_name}) -endfunction(add_gtest) + add_executable(${TEST_NAME} ${SOURCE_FILES}) + target_link_libraries(${TEST_NAME} ${GTEST_LIBRARY}) + target_link_libraries(${TEST_NAME} ${GTEST_MAIN_LIBRARY}) + target_link_libraries(${TEST_NAME} "${LIB_PREFIX}spica_renderer${LIB_SUFFIX}") -add_gtest(test_vector test_vector.cc) -add_gtest(test_geometry test_geometry.cc) + add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME}) + add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND} DEPENDS ${TEST_NAME}) -add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND} DEPENDS test_vector test_geometry) - -include_directories(${CMAKE_CURRENT_LIST_DIR}) -include_directories(${GTEST_INCLUDE_DIRS}) + include_directories(${CMAKE_CURRENT_LIST_DIR}) + include_directories(${GTEST_INCLUDE_DIRS}) +endif() diff --git a/test/all_tests.cc b/test/all_tests.cc new file mode 100644 index 0000000..8f68b40 --- /dev/null +++ b/test/all_tests.cc @@ -0,0 +1,6 @@ +#include "gtest/gtest.h" + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/test/test_geometry.cc b/test/test_geometry.cc index 060d9d1..f04ad90 100644 --- a/test/test_geometry.cc +++ b/test/test_geometry.cc @@ -59,7 +59,7 @@ TEST(PrimitiveTest, InstanceTest) { } // ------------------------------ -// Sphere class test +// Plane class test // ------------------------------ TEST(PlaneTest, InstanceTest) { Plane pl(3.0, Vector3(-1.0, 0.0, 0.0), Color(0.1, 0.2, 0.3), Color(0.25, 0.50, 0.75), REFLECTION_DIFFUSE); @@ -94,6 +94,12 @@ TEST(PlaneTest, InstanceTest) { // Sphere class test // ------------------------------ TEST(SphereTest, InstanceTest) { + Sphere sp0; + EXPECT_EQ(0.0, sp0.center().x()); + EXPECT_EQ(0.0, sp0.center().y()); + EXPECT_EQ(0.0, sp0.center().z()); + EXPECT_EQ(0.0, sp0.radius()); + Sphere sp(2.0, Vector3(0.0, 0.0, 0.0), Color(), Color(0.75, 0.75, 0.75), REFLECTION_DIFFUSE); EXPECT_EQ(0.0, sp.center().x()); @@ -110,6 +116,21 @@ TEST(SphereTest, InstanceTest) { EXPECT_EQ(REFLECTION_DIFFUSE, sp.reftype()); + // copy constructor + sp0 = sp; + EXPECT_EQ(0.0, sp0.center().x()); + EXPECT_EQ(0.0, sp0.center().y()); + EXPECT_EQ(0.0, sp0.center().z()); + + EXPECT_EQ(0.0, sp0.emission().x()); + EXPECT_EQ(0.0, sp0.emission().y()); + EXPECT_EQ(0.0, sp0.emission().z()); + + EXPECT_EQ(0.75, sp0.color().x()); + EXPECT_EQ(0.75, sp0.color().y()); + EXPECT_EQ(0.75, sp0.color().z()); + + // intersection HitPoint hitpoint; EXPECT_TRUE(sp.intersect(Ray(Vector3(10.0, 0.0, 0.0), Vector3(-1.0, 0.0, 0.0)), hitpoint)); EXPECT_EQ(2.0, hitpoint.position().x()); @@ -123,7 +144,3 @@ TEST(SphereTest, InstanceTest) { EXPECT_FALSE(sp.intersect(Ray(Vector3(10.0, 0.0, 0.0), Vector3(0.0, 1.0, 0.0)), hitpoint)); } -int main(int argc, char **argv) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/test/test_image.cc b/test/test_image.cc new file mode 100644 index 0000000..d9d0555 --- /dev/null +++ b/test/test_image.cc @@ -0,0 +1,45 @@ +#include "gtest/gtest.h" +#include "../include/spica.h" +using namespace spica; + +Random rng = Random::getRNG(); + +TEST(ImageTest, InstanceTest) { + const int width = 320; + const int height = 240; + + Image image; + EXPECT_EQ(0, image.width()); + EXPECT_EQ(0, image.height()); + ASSERT_DEATH(image(0, 0), ""); + ASSERT_DEATH(Image(-1, -1), ""); + + image = Image(width, height); + EXPECT_EQ(320, image.width()); + EXPECT_EQ(240, image.height()); + for (int y = 0; y < image.height(); y++) { + for (int x = 0; x < image.width(); x++) { + EXPECT_EQ(0.0, image(x, y).red()); + EXPECT_EQ(0.0, image(x, y).green()); + EXPECT_EQ(0.0, image(x, y).blue()); + } + } + + Image rand(160, 120); + for (int y = 0; y < rand.height(); y++) { + for (int x = 0; x < rand.width(); x++) { + rand.pixel(x, y) = Color(rng.randReal(), rng.randReal(), rng.randReal()); + } + } + + image = rand; + EXPECT_EQ(rand.width(), image.width()); + EXPECT_EQ(rand.height(), image.height()); + for (int y = 0; y < image.height(); y++) { + for (int x = 0; x < image.width(); x++) { + EXPECT_EQ(rand(x, y).red(), image(x, y).red()); + EXPECT_EQ(rand(x, y).green(), image(x, y).green()); + EXPECT_EQ(rand(x, y).blue(), image(x, y).blue()); + } + } +} diff --git a/test/test_vector.cc b/test/test_vector.cc index 9656cc4..3a77d48 100644 --- a/test/test_vector.cc +++ b/test/test_vector.cc @@ -74,8 +74,3 @@ TEST(Vector3Test, AlgebraTest) { EXPECT_EQ(u.y() / nrm, w.y()); EXPECT_EQ(u.z() / nrm, w.z()); } - -int main(int argc, char **argv) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -}