diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 311a9992b..c147d4f77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -141,7 +141,7 @@ jobs: setenvs: export OSL_CMAKE_FLAGS="-DSANITIZE=address -DOSL_TEST_BIG_TIMEOUT=1200" CMAKE_BUILD_TYPE=Debug CTEST_TEST_TIMEOUT=1200 - CTEST_EXCLUSIONS="broken|noise-reg.regress|noise-gabor-reg.regress|example-.*deformer|spline-reg.regress.batched|osl-imageio" + CTEST_EXCLUSIONS="broken|noise-reg.regress|noise-gabor-reg.regress|example-.*deformer|example-tinyrender|spline-reg.regress.batched|osl-imageio" OSL_OPTIONS=llvm_debugging_symbols=1 TESTRENDER_AA=1 OSL_TESTSUITE_SKIP_DIFF=1 diff --git a/src/cmake/testing.cmake b/src/cmake/testing.cmake index 46fab0e0e..fd6cd1e56 100644 --- a/src/cmake/testing.cmake +++ b/src/cmake/testing.cmake @@ -368,6 +368,7 @@ macro (osl_add_all_tests) error-dupes error-malformed error-serialized example-deformer example-batched-deformer + example-tinyrender exit exponential filterwidth-reg for-reg format-reg fprintf diff --git a/testsuite/example-tinyrender/CMakeLists.txt b/testsuite/example-tinyrender/CMakeLists.txt new file mode 100644 index 000000000..71f056927 --- /dev/null +++ b/testsuite/example-tinyrender/CMakeLists.txt @@ -0,0 +1,60 @@ +# Copyright Contributors to the Open Shading Language project. +# SPDX-License-Identifier: BSD-3-Clause +# https://github.com/AcademySoftwareFoundation/OpenShadingLanguage + +cmake_minimum_required (VERSION 3.23) +project (tinyrender + LANGUAGES CXX) + +if (NOT CMAKE_BUILD_TYPE) + set (CMAKE_BUILD_TYPE "Release") +endif () + +message (STATUS "Building ${PROJECT_NAME}...") + +# Make the build area layout look like we expect +set (CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) +set (CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) +set (CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) + +set (CMAKE_CXX_STANDARD 17 CACHE STRING "C++ standard to prefer (17, 20, etc.)") +set (CMAKE_CXX_STANDARD_REQUIRED ON) +set (CMAKE_CXX_EXTENSIONS OFF) + + +# Make sure we have dependencies we need +find_package (OSL REQUIRED) + + +# Compile the shaders with the oslc that was installed along with OSL. +set (shader_src_dir "${CMAKE_CURRENT_SOURCE_DIR}/shaders") +set (shader_oso_dir "${CMAKE_BINARY_DIR}/shaders") +file (MAKE_DIRECTORY "${shader_oso_dir}") + +set (compiled_shaders) +foreach (shader matte metal glass plastic) + set (oso "${shader_oso_dir}/${shader}.oso") + add_custom_command ( + OUTPUT "${oso}" + COMMAND "$" + -I "${shader_src_dir}" -o "${oso}" "${shader_src_dir}/${shader}.osl" + MAIN_DEPENDENCY "${shader_src_dir}/${shader}.osl" + DEPENDS "${shader_src_dir}/rtiow.h" OSL::oslc + COMMENT "Compiling OSL shader ${shader}.osl" + VERBATIM) + list (APPEND compiled_shaders "${oso}") +endforeach () +add_custom_target (tinyrender_shaders ALL DEPENDS ${compiled_shaders}) + + +add_executable (tinyrender + tinyrender.cpp + osl/shading.cpp) +add_dependencies (tinyrender tinyrender_shaders) +target_include_directories (tinyrender + PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") +target_compile_definitions (tinyrender + PRIVATE TINYRENDER_SHADER_DIR="${shader_oso_dir}") +# OSL::oslexec brings in OpenImageIO and Imath. +target_link_libraries (tinyrender + PRIVATE OSL::oslexec) diff --git a/testsuite/example-tinyrender/camera.h b/testsuite/example-tinyrender/camera.h new file mode 100644 index 000000000..a68d770ae --- /dev/null +++ b/testsuite/example-tinyrender/camera.h @@ -0,0 +1,155 @@ +// Copyright Contributors to the Open Shading Language project. +// SPDX-License-Identifier: BSD-3-Clause +// https://github.com/AcademySoftwareFoundation/OpenShadingLanguage + +#pragma once + +#include "osl/material.h" +#include "rtweekend.h" + +#include + +#include +#include +#include + +class camera { +public: + double aspect_ratio = 1.0; + int image_width = 100; + int samples_per_pixel = 10; + int max_depth = 10; + + double vfov = 90; // Vertical field of view + point3 lookfrom = point3(0, 0, 0); + point3 lookat = point3(0, 0, -1); + vec3 vup = vec3(0, 1, 0); // Camera-relative "up" direction + + int height() const { return image_height; } + + // Linear color. The book's gamma correction is left out, since the + // output is an EXR and those hold linear values. + const std::vector& pixels() const { return frameBuffer; } + + void render(const hittable& world, OSL::ShadingSystem& shadingsys) + { + initialize(); + + OIIO::parallel_for_chunked( + 0, image_height, 0, + [this, &world, &shadingsys](int64_t jbegin, int64_t jend) { + // A ShadingContext can only be used by one thread at a time. + OSL::PerThreadInfo* thread_info + = shadingsys.create_thread_info(); + OSL::ShadingContext* ctx = shadingsys.get_context(thread_info); + + for (int j = static_cast(jbegin); j < jend; j++) { + // Reseed per row, so the image is the same for any number + // of threads. + seed_random_generator(static_cast(j)); + + for (int i = 0; i < image_width; i++) { + color pixel_color(0, 0, 0); + for (int sample = 0; sample < samples_per_pixel; + sample++) { + ray r = get_ray(i, j); + pixel_color += ray_color(r, max_depth, world, *ctx); + } + frameBuffer[static_cast(j) * image_width + + i] = pixel_samples_scale * pixel_color; + } + } + + shadingsys.release_context(ctx); + shadingsys.destroy_thread_info(thread_info); + }); + } + +private: + int image_height = 0; + double pixel_samples_scale = 0; + point3 center; + point3 pixel00_loc; + vec3 pixel_delta_u; + vec3 pixel_delta_v; + vec3 u, v, w; // Camera frame basis vectors + std::vector frameBuffer; + + void initialize() + { + image_height = static_cast(image_width / aspect_ratio); + image_height = (image_height < 1) ? 1 : image_height; + + frameBuffer.resize(static_cast(image_width) + * image_height); + pixel_samples_scale = 1.0 / samples_per_pixel; + + center = lookfrom; + + // Determine viewport dimensions. + double focal_length = (lookfrom - lookat).length(); + double theta = degrees_to_radians(vfov); + double h = std::tan(theta / 2); + double viewport_height = 2 * h * focal_length; + double viewport_width = viewport_height + * (static_cast(image_width) + / image_height); + + // Calculate the u, v, w unit basis vectors for the camera frame. + w = unit_vector(lookfrom - lookat); + u = unit_vector(cross(vup, w)); + v = cross(w, u); + + vec3 viewport_u = viewport_width * u; + vec3 viewport_v = viewport_height * -v; + + pixel_delta_u = viewport_u / image_width; + pixel_delta_v = viewport_v / image_height; + + point3 viewport_upper_left = center - (focal_length * w) + - viewport_u / 2 - viewport_v / 2; + pixel00_loc = viewport_upper_left + + 0.5 * (pixel_delta_u + pixel_delta_v); + } + + ray get_ray(int i, int j) const + { + // A ray from the camera center to a random point around pixel i, j. + vec3 offset = sample_square(); + point3 pixel_sample = pixel00_loc + ((i + offset.x()) * pixel_delta_u) + + ((j + offset.y()) * pixel_delta_v); + + return ray(center, pixel_sample - center); + } + + static vec3 sample_square() + { + // A random point in the [-.5,-.5]-[+.5,+.5] unit square. + double x = random_double() - 0.5; + double y = random_double() - 0.5; + return vec3(x, y, 0); + } + + color ray_color(const ray& r, int depth, const hittable& world, + OSL::ShadingContext& ctx) const + { + // Past the bounce limit, no more light is gathered. + if (depth <= 0) + return color(0, 0, 0); + + hit_record rec; + + if (world.hit(r, interval(0.001, infinity), rec)) { + ray scattered; + color attenuation; + if (rec.mat->scatter(r, rec, ctx, attenuation, scattered)) + return attenuation + * ray_color(scattered, depth - 1, world, ctx); + return color(0, 0, 0); + } + + vec3 unit_direction = unit_vector(r.direction()); + double a = 0.5 * (unit_direction.y() + 1.0); + return (1.0 - a) * color(1.0, 1.0, 1.0) + a * color(0.5, 0.7, 1.0); + } +}; diff --git a/testsuite/example-tinyrender/osl/material.h b/testsuite/example-tinyrender/osl/material.h new file mode 100644 index 000000000..c4f6cd1bf --- /dev/null +++ b/testsuite/example-tinyrender/osl/material.h @@ -0,0 +1,137 @@ +// Copyright Contributors to the Open Shading Language project. +// SPDX-License-Identifier: BSD-3-Clause +// https://github.com/AcademySoftwareFoundation/OpenShadingLanguage + +#pragma once + +#include "rtweekend.h" +#include "shading.h" + +#include + +#include +#include +#include + +// The book has one material class each for matte, metal and glass. Here a +// single class covers all three: the OSL shader it holds decides which. +class material { +public: + material(OSL::ShadingSystem& shadingsys, OSL::ShaderGroupRef group) + : shadingsys(shadingsys), group(std::move(group)) + { + } + + [[nodiscard]] bool scatter(const ray& r_in, const hit_record& rec, + OSL::ShadingContext& ctx, color& attenuation, + ray& scattered) const + { + vec3 I = unit_vector(r_in.direction()); + + OSL::ShaderGlobals sg; + globals_from_hit(sg, I, rec); + + // 0 is fine for the thread index: it is only read by renderers that + // collect shader messages per thread, and this one does not. + if (!shadingsys.execute(ctx, *group, /*thread_index=*/0, + /*shadeindex=*/0, sg, nullptr, nullptr)) + return false; + + if (sg.Ci == nullptr) + return false; + + vec3 wi; + if (!sample_closure(sg.Ci, I, wi, attenuation)) + return false; + + scattered = ray(rec.p, wi); + return true; + } + +private: + OSL::ShadingSystem& shadingsys; + OSL::ShaderGroupRef group; + + // The book's vec3 holds doubles, and OSL's Vec3 holds floats. + static OSL::Vec3 to_osl(const vec3& v) + { + return OSL::Vec3(static_cast(v.x()), static_cast(v.y()), + static_cast(v.z())); + } + + static void globals_from_hit(OSL::ShaderGlobals& sg, const vec3& I, + const hit_record& rec) + { + // Imath vectors start uninitialized, so `{}` would not zero this. + std::memset(static_cast(&sg), 0, sizeof(sg)); + + sg.P = to_osl(rec.p); + sg.I = to_osl(I); + + // rec.normal already faces the ray, which is what OSL wants. + sg.N = sg.Ng = to_osl(rec.normal); + sg.backfacing = rec.front_face ? 0 : 1; + + sg.u = static_cast(rec.u); + sg.v = static_cast(rec.v); + sg.dPdu = to_osl(rec.dpdu); + sg.dPdv = to_osl(rec.dpdv); + + // Only light shaders would read this, and there are none. + sg.surfacearea = 1; + } +}; + + + +// Each material is a shader group holding one surface shader. + +inline std::shared_ptr +make_matte(OSL::ShadingSystem& shadingsys, const OSL::Color3& albedo, bool& ok) +{ + OSL::ShaderGroupRef group = shadingsys.ShaderGroupBegin("matte"); + ok &= shadingsys.Parameter(*group, "Cs", OSL::TypeColor, &albedo); + ok &= shadingsys.Shader(*group, "surface", "matte", "layer"); + ok &= shadingsys.ShaderGroupEnd(*group); + return std::make_shared(shadingsys, group); +} + + + +inline std::shared_ptr +make_metal(OSL::ShadingSystem& shadingsys, const OSL::Color3& albedo, + float fuzz, bool& ok) +{ + OSL::ShaderGroupRef group = shadingsys.ShaderGroupBegin("metal"); + ok &= shadingsys.Parameter(*group, "Cs", OSL::TypeColor, &albedo); + ok &= shadingsys.Parameter(*group, "fuzz", OSL::TypeFloat, &fuzz); + ok &= shadingsys.Shader(*group, "surface", "metal", "layer"); + ok &= shadingsys.ShaderGroupEnd(*group); + return std::make_shared(shadingsys, group); +} + + + +inline std::shared_ptr +make_glass(OSL::ShadingSystem& shadingsys, float ior, bool& ok) +{ + OSL::ShaderGroupRef group = shadingsys.ShaderGroupBegin("glass"); + ok &= shadingsys.Parameter(*group, "ior", OSL::TypeFloat, &ior); + ok &= shadingsys.Shader(*group, "surface", "glass", "layer"); + ok &= shadingsys.ShaderGroupEnd(*group); + return std::make_shared(shadingsys, group); +} + + + +// An addition to the book's three materials, to test a sum of closures. +inline std::shared_ptr +make_plastic(OSL::ShadingSystem& shadingsys, const OSL::Color3& albedo, + bool& ok) +{ + OSL::ShaderGroupRef group = shadingsys.ShaderGroupBegin("plastic"); + ok &= shadingsys.Parameter(*group, "Cs", OSL::TypeColor, &albedo); + ok &= shadingsys.Shader(*group, "surface", "plastic", "layer"); + ok &= shadingsys.ShaderGroupEnd(*group); + return std::make_shared(shadingsys, group); +} diff --git a/testsuite/example-tinyrender/osl/shading.cpp b/testsuite/example-tinyrender/osl/shading.cpp new file mode 100644 index 000000000..3528fc81e --- /dev/null +++ b/testsuite/example-tinyrender/osl/shading.cpp @@ -0,0 +1,267 @@ +// Copyright Contributors to the Open Shading Language project. +// SPDX-License-Identifier: BSD-3-Clause +// https://github.com/AcademySoftwareFoundation/OpenShadingLanguage + +#include "shading.h" + +#include "rtweekend.h" + +#include +#include +#include +#include +#include + +using OSL::TypeDesc; +using OSL::TypeFloat; +using OSL::TypeVector; + +void +register_closures(OSL::ShadingSystem& shadingsys) +{ + // Describe the memory layout of each closure's parameters to OSL. + constexpr int MaxParams = 4; + + struct BuiltinClosures { + const char* name; + int id; + OSL::ClosureParam params[MaxParams]; + }; + + const BuiltinClosures builtins[] = { + { "diffuse", + DIFFUSE_ID, + { CLOSURE_VECTOR_PARAM(DiffuseParams, N), + CLOSURE_FINISH_PARAM(DiffuseParams) } }, + { "rtiow_metal", + RTIOW_METAL_ID, + { CLOSURE_VECTOR_PARAM(RtiowMetalParams, N), + CLOSURE_FLOAT_PARAM(RtiowMetalParams, fuzz), + CLOSURE_FINISH_PARAM(RtiowMetalParams) } }, + { "rtiow_dielectric", + RTIOW_DIELECTRIC_ID, + { CLOSURE_VECTOR_PARAM(RtiowDielectricParams, N), + CLOSURE_FLOAT_PARAM(RtiowDielectricParams, eta), + CLOSURE_FINISH_PARAM(RtiowDielectricParams) } }, + }; + + for (const BuiltinClosures& b : builtins) + shadingsys.register_closure(b.name, b.id, b.params, nullptr, nullptr); +} + + + +constexpr std::size_t max_lobes = 8; + +struct Lobe { + int id = 0; + const void* params = nullptr; + color weight; // closure weight accumulated down the tree + double albedo = 0; // rough estimate of the light this lobe carries +}; + +using LobeList = std::array; + +vec3 +from_osl(const OSL::Vec3& v) +{ + return vec3(v.x, v.y, v.z); +} + + + +double +average(const color& c) +{ + return (c.x() + c.y() + c.z()) / 3; +} + + + +// Schlick's approximation, as in the book's dielectric::reflectance(). +double +schlick_reflectance(double cosine, double eta) +{ + double r0 = (1 - eta) / (1 + eta); + r0 = r0 * r0; + return r0 + (1 - r0) * std::pow(1 - cosine, 5); +} + + + +// The book's lambertian::scatter(). +bool +sample_diffuse(const vec3& n, vec3& wi, color& value) +{ + // A unit vector added to the normal is a cosine-weighted hemisphere + // sample, and cosine weighting makes f * cos / pdf collapse to 1. + vec3 direction = n + random_unit_vector(); + + if (direction.near_zero()) + direction = n; + + wi = direction; + value = color(1, 1, 1); + return true; +} + + + +// The book's metal::scatter(). +bool +sample_rtiow_metal(const RtiowMetalParams* p, const vec3& I, vec3& wi, + color& value) +{ + vec3 n = from_osl(p->N); + double fuzz = std::fmin(static_cast(p->fuzz), 1.0); + + wi = unit_vector(reflect(I, n)) + fuzz * random_unit_vector(); + + // Fuzz can push the ray below the surface, where the book absorbs it. + if (dot(wi, n) <= 0) + return false; + + value = color(1, 1, 1); + return true; +} + + + +// The book's dielectric::scatter(). +bool +sample_rtiow_dielectric(const RtiowDielectricParams* p, const vec3& I, vec3& wi, + color& value) +{ + vec3 n = from_osl(p->N); + // incident over transmitted, already flipped by the shader + double eta = p->eta; + + double cos_theta = std::fmin(dot(-I, n), 1.0); + double sin_theta = std::sqrt(1 - cos_theta * cos_theta); + + // Past the critical angle the ray can only reflect. + bool cannot_refract = eta * sin_theta > 1.0; + + if (cannot_refract || schlick_reflectance(cos_theta, eta) > random_double()) + wi = reflect(I, n); + else + wi = refract(I, n, eta); + + value = color(1, 1, 1); + return true; +} + + + +void +flatten(const OSL::ClosureColor* c, const color& weight, LobeList& lobes, + std::size_t& count) +{ + if (c == nullptr) + return; + + if (count >= lobes.size()) { + static std::atomic warned { false }; + if (!warned.exchange(true)) + OSL::print(stderr, + "warning: a closure tree has more than {} lobes. " + "Ignoring the rest...\n", + max_lobes); + return; + } + + switch (c->id) { + case OSL::ClosureColor::MUL: { + const OSL::ClosureMul* m = c->as_mul(); + flatten(m->closure, + weight * color(m->weight.x, m->weight.y, m->weight.z), lobes, + count); + break; + } + + case OSL::ClosureColor::ADD: { + const OSL::ClosureAdd* a = c->as_add(); + flatten(a->closureA, weight, lobes, count); + flatten(a->closureB, weight, lobes, count); + break; + } + + default: { + const OSL::ClosureComponent* comp = c->as_comp(); + color w = weight * color(comp->w.x, comp->w.y, comp->w.z); + + lobes[count] = { comp->id, comp->data(), w }; + count++; + break; + } + } +} + + + +bool +sample_lobe(const Lobe& l, const vec3& I, vec3& wi, color& value) +{ + switch (l.id) { + case DIFFUSE_ID: + return sample_diffuse( + from_osl(static_cast(l.params)->N), wi, + value); + + case RTIOW_METAL_ID: + return sample_rtiow_metal(static_cast(l.params), + I, wi, value); + + case RTIOW_DIELECTRIC_ID: + return sample_rtiow_dielectric( + static_cast(l.params), I, wi, value); + + default: return false; + } +} + + + +bool +sample_closure(const OSL::ClosureColor* Ci, const vec3& I, vec3& wi, + color& weight) +{ + LobeList lobes; + std::size_t count = 0; + + flatten(Ci, color(1, 1, 1), lobes, count); + if (count == 0) + return false; + + double total = 0; + for (std::size_t i = 0; i < count; i++) { + lobes[i].albedo = std::fmax(0.0, average(lobes[i].weight)); + total += lobes[i].albedo; + } + + if (total <= 0) + return false; + + double selection_point = random_double() * total; + std::size_t chosen = count - 1; + double cumulative_albedo = 0; + + for (std::size_t i = 0; i < count; i++) { + cumulative_albedo += lobes[i].albedo; + if (selection_point < cumulative_albedo) { + chosen = i; + break; + } + } + + double probability = lobes[chosen].albedo / total; + if (probability <= 0) + return false; + + color value; + if (!sample_lobe(lobes[chosen], I, wi, value)) + return false; + + weight = lobes[chosen].weight * value / probability; + return true; +} diff --git a/testsuite/example-tinyrender/osl/shading.h b/testsuite/example-tinyrender/osl/shading.h new file mode 100644 index 000000000..1f1e00504 --- /dev/null +++ b/testsuite/example-tinyrender/osl/shading.h @@ -0,0 +1,43 @@ +// Copyright Contributors to the Open Shading Language project. +// SPDX-License-Identifier: BSD-3-Clause +// https://github.com/AcademySoftwareFoundation/OpenShadingLanguage + +#pragma once + +#include "rtweekend.h" + +#include +#include +#include + +enum ClosureIDs : int { + DIFFUSE_ID = 1, + RTIOW_METAL_ID, + RTIOW_DIELECTRIC_ID, +}; + +// OSL copies closure arguments into these structs, so registered parameters +// must match the shader declarations in order and type. +struct DiffuseParams { + OSL::Vec3 N; +}; + +struct RtiowMetalParams { + OSL::Vec3 N; + float fuzz; +}; + +struct RtiowDielectricParams { + OSL::Vec3 N; + float eta; +}; + +void +register_closures(OSL::ShadingSystem& shadingsys); + +// Samples one lobe of the shader's closure tree, chosen in proportion to its +// weight. `I` points toward the surface. On success, `wi` is the sampled +// direction and `weight` its throughput. +[[nodiscard]] bool +sample_closure(const OSL::ClosureColor* Ci, const vec3& I, vec3& wi, + color& weight); diff --git a/testsuite/example-tinyrender/ref/out.exr b/testsuite/example-tinyrender/ref/out.exr new file mode 100644 index 000000000..0f7c316da Binary files /dev/null and b/testsuite/example-tinyrender/ref/out.exr differ diff --git a/testsuite/example-tinyrender/rtweekend.h b/testsuite/example-tinyrender/rtweekend.h new file mode 100644 index 000000000..df6d2eac9 --- /dev/null +++ b/testsuite/example-tinyrender/rtweekend.h @@ -0,0 +1,391 @@ +// Copyright Contributors to the Open Shading Language project. +// SPDX-License-Identifier: BSD-3-Clause +// https://github.com/AcademySoftwareFoundation/OpenShadingLanguage + +// The code from "Ray Tracing in One Weekend" (https://raytracing.github.io) +// that knows nothing about OSL. + +#pragma once + +#include +#include +#include +#include +#include +#include + +////////////////////////////////// UTILITIES /////////////////////////////////// + +// Constants + +inline constexpr double infinity = std::numeric_limits::infinity(); +inline constexpr double pi = 3.1415926535897932385; + +// Utility Functions + +inline double +degrees_to_radians(double degrees) +{ + return degrees * pi / 180.0; +} + +// Each rendering thread gets an independent generator. +inline thread_local uint64_t random_state = 0; + +inline void +seed_random_generator(unsigned int seed) +{ + random_state = seed; +} + +// Returns a random real in [0,1). This is SplitMix64 rather than , +// whose distributions are not guaranteed to give the same numbers in every +// standard library. That way every platform renders with the same numbers. +inline double +random_double() +{ + uint64_t z = (random_state += 0x9e3779b97f4a7c15ull); + z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9ull; + z = (z ^ (z >> 27)) * 0x94d049bb133111ebull; + z ^= z >> 31; + // Keep the top 53 bits, which is all a double's mantissa can hold. + return static_cast(z >> 11) * 0x1.0p-53; +} + +// Returns a random real in [min,max). +inline double +random_double(double min, double max) +{ + return min + (max - min) * random_double(); +} + + + +///////////////////////////////////// VEC3 ///////////////////////////////////// + +class vec3 { +public: + double e[3]; + + vec3() : e { 0, 0, 0 } {} + vec3(double e0, double e1, double e2) : e { e0, e1, e2 } {} + + double x() const { return e[0]; } + double y() const { return e[1]; } + double z() const { return e[2]; } + + vec3 operator-() const { return vec3(-e[0], -e[1], -e[2]); } + double operator[](int i) const { return e[i]; } + double& operator[](int i) { return e[i]; } + + vec3& operator+=(const vec3& v) + { + e[0] += v.e[0]; + e[1] += v.e[1]; + e[2] += v.e[2]; + return *this; + } + + vec3& operator*=(double t) + { + e[0] *= t; + e[1] *= t; + e[2] *= t; + return *this; + } + + vec3& operator/=(double t) { return *this *= 1 / t; } + + double length() const { return std::sqrt(length_squared()); } + + double length_squared() const + { + return e[0] * e[0] + e[1] * e[1] + e[2] * e[2]; + } + + bool near_zero() const + { + constexpr double s = 1e-8; + return (std::fabs(e[0]) < s) && (std::fabs(e[1]) < s) + && (std::fabs(e[2]) < s); + } + + static vec3 random(double min, double max) + { + double x = random_double(min, max); + double y = random_double(min, max); + double z = random_double(min, max); + return vec3(x, y, z); + } +}; + +// Aliases of vec3, for clarity. +using point3 = vec3; +using color = vec3; + +// Vector Utility Functions + +inline vec3 +operator+(const vec3& u, const vec3& v) +{ + return vec3(u.e[0] + v.e[0], u.e[1] + v.e[1], u.e[2] + v.e[2]); +} + +inline vec3 +operator-(const vec3& u, const vec3& v) +{ + return vec3(u.e[0] - v.e[0], u.e[1] - v.e[1], u.e[2] - v.e[2]); +} + +inline vec3 +operator*(const vec3& u, const vec3& v) +{ + return vec3(u.e[0] * v.e[0], u.e[1] * v.e[1], u.e[2] * v.e[2]); +} + +inline vec3 +operator*(double t, const vec3& v) +{ + return vec3(t * v.e[0], t * v.e[1], t * v.e[2]); +} + +inline vec3 +operator*(const vec3& v, double t) +{ + return t * v; +} + +inline vec3 +operator/(const vec3& v, double t) +{ + return (1 / t) * v; +} + +inline double +dot(const vec3& u, const vec3& v) +{ + return u.e[0] * v.e[0] + u.e[1] * v.e[1] + u.e[2] * v.e[2]; +} + +inline vec3 +cross(const vec3& u, const vec3& v) +{ + return vec3(u.e[1] * v.e[2] - u.e[2] * v.e[1], + u.e[2] * v.e[0] - u.e[0] * v.e[2], + u.e[0] * v.e[1] - u.e[1] * v.e[0]); +} + +inline vec3 +unit_vector(const vec3& v) +{ + return v / v.length(); +} + +inline vec3 +random_unit_vector() +{ + while (true) { + vec3 p = vec3::random(-1, 1); + double lensq = p.length_squared(); + if (1e-160 < lensq && lensq <= 1) + return p / std::sqrt(lensq); + } +} + +inline vec3 +reflect(const vec3& v, const vec3& n) +{ + return v - 2 * dot(v, n) * n; +} + +inline vec3 +refract(const vec3& uv, const vec3& n, double etai_over_etat) +{ + double cos_theta = std::fmin(dot(-uv, n), 1.0); + vec3 r_out_perp = etai_over_etat * (uv + cos_theta * n); + vec3 r_out_parallel + = -std::sqrt(std::fabs(1.0 - r_out_perp.length_squared())) * n; + return r_out_perp + r_out_parallel; +} + + + +///////////////////////////////////// RAY ////////////////////////////////////// + +class ray { +public: + ray() = default; + + ray(const point3& origin, const vec3& direction) + : orig(origin), dir(direction) + { + } + + const point3& origin() const { return orig; } + const vec3& direction() const { return dir; } + + point3 at(double t) const { return orig + t * dir; } + +private: + point3 orig; + vec3 dir; +}; + + + +/////////////////////////////////// INTERVAL /////////////////////////////////// + +class interval { +public: + double min, max; + + // Default interval is empty + interval() : min(+infinity), max(-infinity) {} + + interval(double min, double max) : min(min), max(max) {} + + double size() const { return max - min; } + + bool contains(double x) const { return min <= x && x <= max; } + + bool surrounds(double x) const { return min < x && x < max; } +}; + + + +/////////////////////////////////// HITTABLE /////////////////////////////////// + +// Defined in osl/material.h, since it is the class that runs OSL shaders. +class material; + +class hit_record { +public: + point3 p; + vec3 normal; + std::shared_ptr mat; + double t = 0; + bool front_face = false; + + // Surface parameterization, passed on to the shader globals. + double u = 0; + double v = 0; + vec3 dpdu; + vec3 dpdv; + + void set_face_normal(const ray& r, const vec3& outward_normal) + { + // `outward_normal` is assumed to have unit length. + front_face = dot(r.direction(), outward_normal) < 0; + normal = front_face ? outward_normal : -outward_normal; + } +}; + +class hittable { +public: + virtual ~hittable() = default; + + [[nodiscard]] virtual bool hit(const ray& r, interval ray_t, + hit_record& rec) const = 0; +}; + + +//////////////////////////////// HITTABLE_LIST ///////////////////////////////// + +class hittable_list : public hittable { +public: + std::vector> objects; + + hittable_list() = default; + + void add(std::shared_ptr object) + { + objects.push_back(std::move(object)); + } + + bool hit(const ray& r, interval ray_t, hit_record& rec) const override + { + hit_record temp_rec; + bool hit_anything = false; + double closest_so_far = ray_t.max; + + for (const auto& object : objects) { + if (object->hit(r, interval(ray_t.min, closest_so_far), temp_rec)) { + hit_anything = true; + closest_so_far = temp_rec.t; + rec = temp_rec; + } + } + + return hit_anything; + } +}; + + + +//////////////////////////////////// SPHERE //////////////////////////////////// + +class sphere : public hittable { +public: + sphere(const point3& center, double radius, std::shared_ptr mat) + : center(center), radius(std::fmax(0, radius)), mat(std::move(mat)) + { + } + + bool hit(const ray& r, interval ray_t, hit_record& rec) const override + { + vec3 oc = center - r.origin(); + double a = r.direction().length_squared(); + double h = dot(r.direction(), oc); + double c = oc.length_squared() - radius * radius; + + double discriminant = h * h - a * c; + if (discriminant < 0) + return false; + + double sqrtd = std::sqrt(discriminant); + + // Find the nearest root that lies in the acceptable range. + double root = (h - sqrtd) / a; + if (!ray_t.surrounds(root)) { + root = (h + sqrtd) / a; + if (!ray_t.surrounds(root)) + return false; + } + + rec.t = root; + rec.p = r.at(rec.t); + vec3 outward_normal = (rec.p - center) / radius; + rec.set_face_normal(r, outward_normal); + rec.mat = mat; + set_sphere_uv(outward_normal, rec); + + return true; + } + +private: + // Spherical UVs and tangents at the unit outward normal `p`. + // u = phi / (2*pi), v = theta / pi. + void set_sphere_uv(const vec3& p, hit_record& rec) const + { + // Rounding can push this past 1 near a pole and make acos a NaN. + double cos_theta = std::fmin(std::fmax(-p.y(), -1.0), 1.0); + + double theta = std::acos(cos_theta); + double phi = std::atan2(-p.z(), p.x()) + pi; + + rec.u = phi / (2 * pi); + rec.v = theta / pi; + + double sin_theta = std::sin(theta); + double sin_phi = std::sin(phi), cos_phi = std::cos(phi); + + rec.dpdu = 2 * pi * radius + * vec3(sin_theta * sin_phi, 0, sin_theta * cos_phi); + rec.dpdv = pi * radius + * vec3(-cos_theta * cos_phi, sin_theta, cos_theta * sin_phi); + } + + point3 center; + double radius; + std::shared_ptr mat; +}; diff --git a/testsuite/example-tinyrender/run.py b/testsuite/example-tinyrender/run.py new file mode 100644 index 000000000..29be22836 --- /dev/null +++ b/testsuite/example-tinyrender/run.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python + +# Copyright Contributors to the Open Shading Language project. +# SPDX-License-Identifier: BSD-3-Clause +# https://github.com/AcademySoftwareFoundation/OpenShadingLanguage + + +outputs = [ "out.exr" ] +command += run_app("cmake -DCMAKE_BUILD_TYPE=Release data >> build.txt 2>&1", silent=True) +command += run_app("cmake --build . >> build.txt 2>&1", silent=True) +command += run_app("bin/tinyrender >> out.txt") diff --git a/testsuite/example-tinyrender/scene.h b/testsuite/example-tinyrender/scene.h new file mode 100644 index 000000000..ce8d664cb --- /dev/null +++ b/testsuite/example-tinyrender/scene.h @@ -0,0 +1,64 @@ +// Copyright Contributors to the Open Shading Language project. +// SPDX-License-Identifier: BSD-3-Clause +// https://github.com/AcademySoftwareFoundation/OpenShadingLanguage + +#pragma once + +#include "osl/material.h" +#include "camera.h" +#include "rtweekend.h" + +#include + +inline bool +setup_scene(OSL::ShadingSystem& shadingsys, hittable_list& world) +{ + using OSL::Color3; + using std::make_shared; + + bool ok = true; + + auto ground = make_matte(shadingsys, Color3(0.5f, 0.5f, 0.5f), ok); + auto blue = make_matte(shadingsys, Color3(0.1f, 0.2f, 0.5f), ok); + auto green = make_matte(shadingsys, Color3(0.2f, 0.6f, 0.2f), ok); + auto glass = make_glass(shadingsys, 1.5f, ok); + auto bubble = make_glass(shadingsys, 1.0f / 1.5f, ok); + auto gold = make_metal(shadingsys, Color3(0.8f, 0.6f, 0.2f), 0.3f, ok); + auto mirror = make_metal(shadingsys, Color3(0.8f, 0.8f, 0.8f), 0.0f, ok); + auto plastic = make_plastic(shadingsys, Color3(0.7f, 0.1f, 0.1f), ok); + + world.add(make_shared(point3(0.0, -100.5, -1.0), 100.0, ground)); + + world.add(make_shared(point3(-1.1, 0.0, -1.5), 0.5, blue)); + world.add(make_shared(point3(0.0, 0.0, -1.5), 0.5, glass)); + world.add(make_shared(point3(0.0, 0.0, -1.5), 0.4, bubble)); + world.add(make_shared(point3(1.1, 0.0, -1.5), 0.5, gold)); + + world.add(make_shared(point3(-0.9, -0.25, -0.4), 0.25, plastic)); + world.add(make_shared(point3(-0.3, -0.25, -0.4), 0.25, mirror)); + world.add(make_shared(point3(0.3, -0.25, -0.4), 0.25, glass)); + world.add(make_shared(point3(0.9, -0.25, -0.4), 0.25, green)); + + return ok; +} + + + +// Resolution and sample count are small to keep the test quick. +inline camera +setup_camera() +{ + camera cam; + + cam.aspect_ratio = 16.0 / 9.0; + cam.image_width = 160; + cam.samples_per_pixel = 64; + cam.max_depth = 10; + + cam.vfov = 30; + cam.lookfrom = point3(0, 1.6, 3.2); + cam.lookat = point3(0, -0.1, -1); + cam.vup = vec3(0, 1, 0); + + return cam; +} diff --git a/testsuite/example-tinyrender/shaders/glass.osl b/testsuite/example-tinyrender/shaders/glass.osl new file mode 100644 index 000000000..c9f9032a9 --- /dev/null +++ b/testsuite/example-tinyrender/shaders/glass.osl @@ -0,0 +1,22 @@ +// Copyright Contributors to the Open Shading Language project. +// SPDX-License-Identifier: BSD-3-Clause +// https://github.com/AcademySoftwareFoundation/OpenShadingLanguage + +#include "rtiow.h" + +surface +glass + [[ string help = "Dielectric material" ]] +( + color Cs = 1 + [[ string help = "Transmission color", + float min = 0, float max = 1 ]], + float ior = 1.5 + [[ string help = "Index of refraction", + float min = 1, float max = 3 ]] + ) +{ + float eta = backfacing() ? ior : 1.0 / ior; + + Ci = Cs * rtiow_dielectric(N, eta); +} diff --git a/testsuite/example-tinyrender/shaders/matte.osl b/testsuite/example-tinyrender/shaders/matte.osl new file mode 100644 index 000000000..e9799cebe --- /dev/null +++ b/testsuite/example-tinyrender/shaders/matte.osl @@ -0,0 +1,18 @@ +// Copyright Contributors to the Open Shading Language project. +// SPDX-License-Identifier: BSD-3-Clause +// https://github.com/AcademySoftwareFoundation/OpenShadingLanguage + +surface +matte + [[ string help = "Lambertian diffuse material" ]] +( + float Kd = 1 + [[ string help = "Diffuse scaling", + float min = 0, float max = 1 ]], + color Cs = 1 + [[ string help = "Base color", + float min = 0, float max = 1 ]] + ) +{ + Ci = Kd * Cs * diffuse (N); +} diff --git a/testsuite/example-tinyrender/shaders/metal.osl b/testsuite/example-tinyrender/shaders/metal.osl new file mode 100644 index 000000000..1f0d28fdf --- /dev/null +++ b/testsuite/example-tinyrender/shaders/metal.osl @@ -0,0 +1,20 @@ +// Copyright Contributors to the Open Shading Language project. +// SPDX-License-Identifier: BSD-3-Clause +// https://github.com/AcademySoftwareFoundation/OpenShadingLanguage + +#include "rtiow.h" + +surface +metal + [[ string help = "Reflective metal material" ]] +( + color Cs = 0.8 + [[ string help = "Reflection color", + float min = 0, float max = 1 ]], + float fuzz = 0 + [[ string help = "Blur of the reflection", + float min = 0, float max = 1 ]] + ) +{ + Ci = Cs * rtiow_metal(N, fuzz); +} diff --git a/testsuite/example-tinyrender/shaders/plastic.osl b/testsuite/example-tinyrender/shaders/plastic.osl new file mode 100644 index 000000000..a8720ce8a --- /dev/null +++ b/testsuite/example-tinyrender/shaders/plastic.osl @@ -0,0 +1,26 @@ +// Copyright Contributors to the Open Shading Language project. +// SPDX-License-Identifier: BSD-3-Clause +// https://github.com/AcademySoftwareFoundation/OpenShadingLanguage + +#include "rtiow.h" + +surface +plastic + [[ string help = "Diffuse base with a glossy reflection added on top" ]] +( + color Cs = 0.8 + [[ string help = "Base color", + float min = 0, float max = 1 ]], + float Kd = 0.8 + [[ string help = "Diffuse scaling", + float min = 0, float max = 1 ]], + float Ks = 0.2 + [[ string help = "Reflection scaling", + float min = 0, float max = 1 ]], + float fuzz = 0.1 + [[ string help = "Blur of the reflection", + float min = 0, float max = 1 ]] + ) +{ + Ci = Kd * Cs * diffuse(N) + Ks * rtiow_metal(N, fuzz); +} diff --git a/testsuite/example-tinyrender/shaders/rtiow.h b/testsuite/example-tinyrender/shaders/rtiow.h new file mode 100644 index 000000000..a469ec7e3 --- /dev/null +++ b/testsuite/example-tinyrender/shaders/rtiow.h @@ -0,0 +1,9 @@ +// Copyright Contributors to the Open Shading Language project. +// SPDX-License-Identifier: BSD-3-Clause +// https://github.com/AcademySoftwareFoundation/OpenShadingLanguage + +#pragma once + +// Closures +closure color rtiow_metal(normal N, float fuzz)[[int builtin = 1]]; +closure color rtiow_dielectric(normal N, float eta)[[int builtin = 1]]; diff --git a/testsuite/example-tinyrender/tinyrender.cpp b/testsuite/example-tinyrender/tinyrender.cpp new file mode 100644 index 000000000..7aed57dcf --- /dev/null +++ b/testsuite/example-tinyrender/tinyrender.cpp @@ -0,0 +1,100 @@ +// Copyright Contributors to the Open Shading Language project. +// SPDX-License-Identifier: BSD-3-Clause +// https://github.com/AcademySoftwareFoundation/OpenShadingLanguage + +// A minimal standalone renderer that shades with OSL, built against an +// installed OSL. The ray tracer is from "Ray Tracing in One Weekend" by +// Peter Shirley, Trevor David Black, and Steve Hollasch +// (https://raytracing.github.io), with its materials replaced by OSL shaders. + +#include "osl/material.h" +#include "osl/shading.h" +#include "camera.h" +#include "rtweekend.h" +#include "scene.h" + +#include +#include + +#include + +#include +#include +#include + +// The scene is world space only, so every transform is identity. +class TinyRendererServices : public OSL::RendererServices { +public: + bool get_matrix(OSL::ShaderGlobals*, OSL::Matrix44& result, + OSL::TransformationPtr, float) override + { + result.makeIdentity(); + return true; + } + + bool get_matrix(OSL::ShaderGlobals*, OSL::Matrix44& result, + OSL::TransformationPtr) override + { + result.makeIdentity(); + return true; + } +}; + + + +bool +write_image(const camera& cam, const std::string& filename) +{ + const int width = cam.image_width; + const int height = cam.height(); + + OIIO::ImageBuf buf(OIIO::ImageSpec(width, height, 3, OIIO::TypeFloat)); + for (int j = 0; j < height; j++) { + for (int i = 0; i < width; i++) { + const color& c + = cam.pixels()[static_cast(j) * width + i]; + const float rgb[3] = { static_cast(c.x()), + static_cast(c.y()), + static_cast(c.z()) }; + buf.setpixel(i, j, rgb); + } + } + + if (!buf.write(filename, OIIO::TypeHalf)) { + OSL::print(stderr, "could not write {}: {}\n", filename, + buf.geterror()); + return false; + } + return true; +} + + + +int +main() +{ + // As in RTIOW, the materials do the shading, so each one holds a + // reference to the shading system. The shading system is declared + // before the scene so that it outlives them. + TinyRendererServices services; + OSL::ShadingSystem shadingsys(&services); + shadingsys.attribute("searchpath:shader", TINYRENDER_SHADER_DIR); + register_closures(shadingsys); + + hittable_list world; + if (!setup_scene(shadingsys, world)) { + OSL::print(stderr, "could not build the scene's materials from {}\n", + TINYRENDER_SHADER_DIR); + return EXIT_FAILURE; + } + + camera cam = setup_camera(); + cam.render(world, shadingsys); + + if (!write_image(cam, "out.exr")) + return EXIT_FAILURE; + + OSL::print("Rendered {}x{} image with {} samples\n", cam.image_width, + cam.height(), cam.samples_per_pixel); + return EXIT_SUCCESS; +}