From 234a2a8584ea982f834355358a851a77362e0047 Mon Sep 17 00:00:00 2001 From: Jinnie Kim Date: Sun, 4 Oct 2026 15:39:05 -0700 Subject: [PATCH] test: add example-tinyrender Add a minimal standalone renderer that is built against the installed OSL, as requested in #2175. testrender and testshade are built as part of OSL, so they cannot notice a broken install. This test is a separate CMake project that uses find_package(OSL), compiles its shaders with the installed oslc, links OSL::oslexec, renders a small image, and compares it against a reference. The renderer is the path tracer from "Ray Tracing in One Weekend" with its materials replaced by OSL shaders. Random numbers are reseeded per scanline so the image is the same for any number of threads. Assisted-by: Claude Code / Opus-5.5 Signed-off-by: Jinnie Kim --- .github/workflows/ci.yml | 2 +- src/cmake/testing.cmake | 1 + testsuite/example-tinyrender/CMakeLists.txt | 60 +++ testsuite/example-tinyrender/camera.h | 155 +++++++ testsuite/example-tinyrender/osl/material.h | 137 ++++++ testsuite/example-tinyrender/osl/shading.cpp | 267 ++++++++++++ testsuite/example-tinyrender/osl/shading.h | 43 ++ testsuite/example-tinyrender/ref/out.exr | Bin 0 -> 46057 bytes testsuite/example-tinyrender/rtweekend.h | 391 ++++++++++++++++++ testsuite/example-tinyrender/run.py | 11 + testsuite/example-tinyrender/scene.h | 64 +++ .../example-tinyrender/shaders/glass.osl | 22 + .../example-tinyrender/shaders/matte.osl | 18 + .../example-tinyrender/shaders/metal.osl | 20 + .../example-tinyrender/shaders/plastic.osl | 26 ++ testsuite/example-tinyrender/shaders/rtiow.h | 9 + testsuite/example-tinyrender/tinyrender.cpp | 100 +++++ 17 files changed, 1325 insertions(+), 1 deletion(-) create mode 100644 testsuite/example-tinyrender/CMakeLists.txt create mode 100644 testsuite/example-tinyrender/camera.h create mode 100644 testsuite/example-tinyrender/osl/material.h create mode 100644 testsuite/example-tinyrender/osl/shading.cpp create mode 100644 testsuite/example-tinyrender/osl/shading.h create mode 100644 testsuite/example-tinyrender/ref/out.exr create mode 100644 testsuite/example-tinyrender/rtweekend.h create mode 100644 testsuite/example-tinyrender/run.py create mode 100644 testsuite/example-tinyrender/scene.h create mode 100644 testsuite/example-tinyrender/shaders/glass.osl create mode 100644 testsuite/example-tinyrender/shaders/matte.osl create mode 100644 testsuite/example-tinyrender/shaders/metal.osl create mode 100644 testsuite/example-tinyrender/shaders/plastic.osl create mode 100644 testsuite/example-tinyrender/shaders/rtiow.h create mode 100644 testsuite/example-tinyrender/tinyrender.cpp 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 0000000000000000000000000000000000000000..0f7c316da3c7383eecf1024311a7f56632320cb5 GIT binary patch literal 46057 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 B4eS5_ literal 0 HcmV?d00001 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; +}