diff --git a/.clang-format b/.clang-format index b446b7ed5..dc95dc49e 100644 --- a/.clang-format +++ b/.clang-format @@ -1,13 +1,12 @@ BasedOnStyle: Google # Language: Cpp -Standard: Cpp11 +Standard: Cpp11 AlignAfterOpenBracket: DontAlign AlignConsecutiveAssignments: true AlignConsecutiveDeclarations: true -AllowShortCaseLabelsOnASingleLine: true AllowShortCaseLabelsOnASingleLine: true BreakConstructorInitializersBeforeComma: true diff --git a/apps/CMakeLists.txt b/apps/CMakeLists.txt index c466620da..23dca3fc9 100644 --- a/apps/CMakeLists.txt +++ b/apps/CMakeLists.txt @@ -1,7 +1,7 @@ function(add_yapp name) - add_executable(${name} ${name}.cpp) - set_target_properties(${name} PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED YES) - target_include_directories(${name} PRIVATE ${CMAKE_SOURCE_DIR}/libs) + add_executable(${name} ${name}.cpp) + set_target_properties(${name} PROPERTIES CXX_STANDARD 20 CXX_STANDARD_REQUIRED YES) + target_include_directories(${name} PRIVATE ${CMAKE_SOURCE_DIR}/libs) target_link_libraries(${name} PRIVATE yocto) endfunction() @@ -16,6 +16,5 @@ add_yapp(yconverts) add_yapp(ysamples) if(YOCTO_CUDA) -add_yapp(ycutrace) + add_yapp(ycutrace) endif() - diff --git a/apps/ycolorgrade.cpp b/apps/ycolorgrade.cpp index 2e5399aee..f500b449a 100644 --- a/apps/ycolorgrade.cpp +++ b/apps/ycolorgrade.cpp @@ -72,7 +72,7 @@ void run(const vector& args) { // switch between interactive and offline if (!interactive) { // apply color grade - image = colorgrade_image(image, params); + image = colorgrade_image(image, true, params); // save image save_image(outname, image); @@ -83,8 +83,8 @@ void run(const vector& args) { auto params = colorgrade_params{}; // display image - auto display = make_image(image.width, image.height, false); - colorgrade_image_mt(display, image, params); + auto display = image; + colorgrade_image(display, image, true, params); // opengl image auto glimage = glimage_state{}; @@ -123,7 +123,7 @@ void run(const vector& args) { "highlights color", params.highlights_color); end_gui_header(); if (edited) { - colorgrade_image_mt(display, image, params); + colorgrade_image(display, image, true, params); set_image(glimage, display); } } diff --git a/apps/yimalpha.cpp b/apps/yimalpha.cpp index 81f294a59..54be1b59f 100644 --- a/apps/yimalpha.cpp +++ b/apps/yimalpha.cpp @@ -60,25 +60,22 @@ void run(const vector& args) { auto alpha = load_image(alphaname); // check sizes - if (image.width != alpha.width || image.height != alpha.height) + if (image.size() != alpha.size()) throw io_error("different image sizes"); - // check types - if (image.linear != alpha.linear) throw io_error("different image types"); - // edit alpha - auto out = make_image(image.width, image.height, image.linear); - for (auto idx : range(image.pixels.size())) { - auto calpha = alpha.pixels[idx]; + auto out = image_t{image.size()}; + for (auto idx : range(image.size())) { + auto calpha = alpha[idx]; auto alpha_ = from_color ? mean(xyz(calpha)) : from_black ? (mean(xyz(calpha)) > 0.01 ? 1.0f : 0.0f) : calpha.w; if (to_color) { - out.pixels[idx] = {alpha_, alpha_, alpha_, alpha_}; + out[idx] = {alpha_, alpha_, alpha_, alpha_}; } else { - auto color = image.pixels[idx]; + auto color = image[idx]; color.w = alpha_; - out.pixels[idx] = color; + out[idx] = color; } } diff --git a/apps/yimdiff.cpp b/apps/yimdiff.cpp index 1b24b0ae8..262413c2b 100644 --- a/apps/yimdiff.cpp +++ b/apps/yimdiff.cpp @@ -57,11 +57,7 @@ void run(const vector& args) { auto image2 = load_image(filename2); // check sizes - if (image1.width != image2.width || image1.height != image2.height) - throw io_error("different image sizes"); - - // check types - if (image1.linear != image2.linear) throw io_error("different image types"); + if (image1.size() != image2.size()) throw io_error("different image sizes"); // compute diff auto diff = image_difference(image1, image2, true); @@ -71,7 +67,7 @@ void run(const vector& args) { // check diff if (signal) { - for (auto& c : diff.pixels) { + for (auto& c : diff) { if (max(xyz(c)) > threshold) { throw std::runtime_error{"image content differ"}; } diff --git a/apps/ytonemap.cpp b/apps/ytonemap.cpp index 495a0184b..b47da591f 100644 --- a/apps/ytonemap.cpp +++ b/apps/ytonemap.cpp @@ -59,17 +59,18 @@ void run(const vector& args) { parse_cli(cli, args); // load - auto image = load_image(imagename); + auto image = load_image(imagename, is_ldr_filename(imagename)); + auto linear = is_hdr_filename(imagename); // resize if needed if (width != 0 || height != 0) { - image = resize_image(image, width, height); + image = resize_image(image, {width, height}); } // switch between interactive and offline if (!interactive) { // tonemap if needed - if (image.linear && is_ldr_filename(outname)) { + if (linear && is_ldr_filename(outname)) { image = tonemap_image(image, exposure, filmic); } @@ -78,10 +79,11 @@ void run(const vector& args) { } else { #ifdef YOCTO_OPENGL // display image - auto display = make_image(image.width, image.height, false); + if (!linear) image = srgb_to_rgb(image); + auto display = image; float exposure = 0; bool filmic = false; - tonemap_image_mt(display, image, exposure, filmic); + tonemap_image(display, image, exposure, filmic); // opengl image auto glimage = glimage_state{}; @@ -100,7 +102,7 @@ void run(const vector& args) { }; callbacks.widgets = [&](const gui_input& input) { if (draw_tonemap_widgets(input, exposure, filmic)) { - tonemap_image_mt(display, image, exposure, filmic); + tonemap_image(display, image, exposure, filmic); set_image(glimage, display); } draw_image_widgets(input, image, display, glparams); diff --git a/apps/ytrace.cpp b/apps/ytrace.cpp index ed3802c59..920bf1a0e 100644 --- a/apps/ytrace.cpp +++ b/apps/ytrace.cpp @@ -108,7 +108,7 @@ void run(const vector& args) { // add environment if (!envname.empty()) { - add_environment(scene, envname); + add_environment(scene, "environment", envname); } // camera @@ -164,7 +164,7 @@ void run(const vector& args) { auto context = make_trace_context(params); // init image - auto image = make_image(state.width, state.height, true); + auto image = image_t{state.render.size()}; // opengl image auto glimage = glimage_state{}; @@ -190,8 +190,8 @@ void run(const vector& args) { // make sure we can start trace_cancel(context); state = make_trace_state(scene, params); - if (image.width != state.width || image.height != state.height) - image = make_image(state.width, state.height, true); + if (image.size() != state.render.size()) + image = image_t{state.render.size()}; // render preview trace_preview(image, context, state, scene, bvh, lights, params); diff --git a/libs/yocto/yocto_cutrace.cpp b/libs/yocto/yocto_cutrace.cpp index 5e22a904e..db1606e85 100644 --- a/libs/yocto/yocto_cutrace.cpp +++ b/libs/yocto/yocto_cutrace.cpp @@ -788,7 +788,7 @@ cutrace_bvh make_cutrace_bvh(cutrace_context& context, // compact auto compacted_size = download_buffer_value(compacted_size_buffer); sbvh.buffer = make_buffer( - context.cuda_stream, compacted_size, (byte*)nullptr); + context.cuda_stream, compacted_size, (byte*)nullptr); check_result(optixAccelCompact(context.optix_context, /*cuda_stream:*/ 0, sbvh.handle, sbvh.buffer.device_ptr(), sbvh.buffer.size_in_bytes(), &sbvh.handle)); @@ -861,7 +861,7 @@ cutrace_bvh make_cutrace_bvh(cutrace_context& context, // compact auto compacted_size = download_buffer_value(compacted_size_buffer); ibvh.buffer = make_buffer( - context.cuda_stream, compacted_size, (byte*)nullptr); + context.cuda_stream, compacted_size, (byte*)nullptr); check_result(optixAccelCompact(context.optix_context, /*cuda_stream:*/ 0, ibvh.handle, ibvh.buffer.device_ptr(), ibvh.buffer.size_in_bytes(), &ibvh.handle)); @@ -896,7 +896,7 @@ cutrace_state make_cutrace_state(cutrace_context& context, } state.samples = 0; state.image = make_buffer( - context.cuda_stream, state.width * state.height, (vec4f*)nullptr); + context.cuda_stream, state.width * state.height, (vec4f*)nullptr); state.albedo = make_buffer( context.cuda_stream, state.width * state.height, (vec3f*)nullptr); state.normal = make_buffer( diff --git a/libs/yocto/yocto_cutrace.h b/libs/yocto/yocto_cutrace.h index 70c93ba32..0f4196637 100644 --- a/libs/yocto/yocto_cutrace.h +++ b/libs/yocto/yocto_cutrace.h @@ -192,8 +192,8 @@ struct cuspan { CUdeviceptr device_ptr() const { return _data; } size_t size_in_bytes() const { return _size * sizeof(T); } void swap(cuspan& other) { - std::swap(_data, other._data); - std::swap(_size, other._size); + std::swap(_data, other._data); + std::swap(_size, other._size); } CUdeviceptr _data = 0; diff --git a/libs/yocto/yocto_diagram.cpp b/libs/yocto/yocto_diagram.cpp new file mode 100644 index 000000000..4317926dc --- /dev/null +++ b/libs/yocto/yocto_diagram.cpp @@ -0,0 +1,2325 @@ +// +// Implementation for Yocto/Diagram +// + +// +// LICENSE: +// +// Copyright (c) 2016 -- 2023 Fabio Pellacini +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +// ----------------------------------------------------------------------------- +// INCLUDES +// ----------------------------------------------------------------------------- + +#include "yocto_diagram.h" + +#include + +#include + +// ----------------------------------------------------------------------------- +// USING DIRECTIVES +// ----------------------------------------------------------------------------- +namespace yocto { + +// using directives +using std::array; +using std::pair; +using std::string; +using namespace std::string_literals; + +} // namespace yocto + +// ----------------------------------------------------------------------------- +// SHAPE HELPERS +// ----------------------------------------------------------------------------- +namespace yocto { + +// Merge shape elements +static void merge_dshape_inplace(shape_data& shape, const shape_data& merge) { + auto offset = (int)shape.positions.size(); + for (auto& p : merge.points) shape.points.push_back(p + offset); + for (auto& l : merge.lines) + shape.lines.push_back({l.x + offset, l.y + offset}); + for (auto& t : merge.triangles) + shape.triangles.push_back({t.x + offset, t.y + offset, t.z + offset}); + for (auto& q : merge.quads) + shape.quads.push_back( + {q.x + offset, q.y + offset, q.z + offset, q.w + offset}); + shape.positions.insert( + shape.positions.end(), merge.positions.begin(), merge.positions.end()); + shape.tangents.insert( + shape.tangents.end(), merge.tangents.begin(), merge.tangents.end()); + shape.texcoords.insert( + shape.texcoords.end(), merge.texcoords.begin(), merge.texcoords.end()); + shape.colors.insert( + shape.colors.end(), merge.colors.begin(), merge.colors.end()); + shape.radius.insert( + shape.radius.end(), merge.radius.begin(), merge.radius.end()); +} + +// Make a tesselated rectangle. Useful in other subdivisions. +static shape_data make_dquads( + const vec2i& steps, const vec2f& scale, const vec2f& uvscale) { + auto shape = shape_data{}; + + shape.positions.resize((steps.x + 1) * (steps.y + 1)); + shape.normals.resize((steps.x + 1) * (steps.y + 1)); + shape.texcoords.resize((steps.x + 1) * (steps.y + 1)); + for (auto j : range(steps.y + 1)) { + for (auto i : range(steps.x + 1)) { + auto uv = vec2f{i / (float)steps.x, j / (float)steps.y}; + shape.positions[j * (steps.x + 1) + i] = { + (2 * uv.x - 1) * scale.x, (2 * uv.y - 1) * scale.y, 0}; + shape.normals[j * (steps.x + 1) + i] = {0, 0, 1}; + shape.texcoords[j * (steps.x + 1) + i] = vec2f{uv.x, 1 - uv.y} * uvscale; + } + } + + shape.quads.resize(steps.x * steps.y); + for (auto j : range(steps.y)) { + for (auto i : range(steps.x)) { + shape.quads[j * steps.x + i] = {j * (steps.x + 1) + i, + j * (steps.x + 1) + i + 1, (j + 1) * (steps.x + 1) + i + 1, + (j + 1) * (steps.x + 1) + i}; + } + } + + return shape; +} + +// Make a rect +static shape_data make_drect( + const vec2i& steps, const vec2f& scale, const vec2f& uvscale = {1, 1}) { + return make_dquads(steps, scale, uvscale); +} + +// Make a box. +static shape_data make_dbox( + const vec3i& steps, const vec3f& scale, const vec3f& uvscale = {1, 1, 1}) { + auto shape = shape_data{}; + auto qshape = shape_data{}; + // + z + qshape = make_drect( + {steps.x, steps.y}, {scale.x, scale.y}, {uvscale.x, uvscale.y}); + for (auto& p : qshape.positions) p = {p.x, p.y, scale.z}; + for (auto& n : qshape.normals) n = {0, 0, 1}; + merge_dshape_inplace(shape, qshape); + // - z + qshape = make_drect( + {steps.x, steps.y}, {scale.x, scale.y}, {uvscale.x, uvscale.y}); + for (auto& p : qshape.positions) p = {-p.x, p.y, -scale.z}; + for (auto& n : qshape.normals) n = {0, 0, -1}; + merge_dshape_inplace(shape, qshape); + // + x + qshape = make_drect( + {steps.z, steps.y}, {scale.z, scale.y}, {uvscale.z, uvscale.y}); + for (auto& p : qshape.positions) p = {scale.x, p.y, -p.x}; + for (auto& n : qshape.normals) n = {1, 0, 0}; + merge_dshape_inplace(shape, qshape); + // - x + qshape = make_drect( + {steps.z, steps.y}, {scale.z, scale.y}, {uvscale.z, uvscale.y}); + for (auto& p : qshape.positions) p = {-scale.x, p.y, p.x}; + for (auto& n : qshape.normals) n = {-1, 0, 0}; + merge_dshape_inplace(shape, qshape); + // + y + qshape = make_drect( + {steps.x, steps.z}, {scale.x, scale.z}, {uvscale.x, uvscale.z}); + for (auto i : range(qshape.positions.size())) { + qshape.positions[i] = { + qshape.positions[i].x, scale.y, -qshape.positions[i].y}; + qshape.normals[i] = {0, 1, 0}; + } + merge_dshape_inplace(shape, qshape); + // - y + qshape = make_rect( + {steps.x, steps.z}, {scale.x, scale.z}, {uvscale.x, uvscale.z}); + for (auto i : range(qshape.positions.size())) { + qshape.positions[i] = { + qshape.positions[i].x, -scale.y, qshape.positions[i].y}; + qshape.normals[i] = {0, -1, 0}; + } + merge_dshape_inplace(shape, qshape); + return shape; +} + +// Make a sphere. +static shape_data make_dsphere(int steps, float scale, float uvscale = 1) { + auto shape = make_dbox({steps, steps, steps}, {scale, scale, scale}, + {uvscale, uvscale, uvscale}); + for (auto& p : shape.positions) p = normalize(p) * scale; + shape.normals = shape.positions; + for (auto& n : shape.normals) n = normalize(n); + return shape; +} + +// Make a sphere. +static shape_data make_duvsphere( + const vec2i& steps, float scale, const vec2f& uvscale = {1, 1}) { + auto shape = make_drect({steps.x, steps.y}, {1, 1}); + for (auto i : range(shape.positions.size())) { + auto uv = shape.texcoords[i]; + auto a = vec2f{2 * pif * uv.x, pif * (1 - uv.y)}; + shape.positions[i] = + vec3f{cos(a.x) * sin(a.y), sin(a.x) * sin(a.y), cos(a.y)} * scale; + shape.normals[i] = normalize(shape.positions[i]); + shape.texcoords[i] = uv * uvscale; + } + return shape; +} + +// Make a hemisphere. +static shape_data make_duvhemisphere( + const vec2i& steps, float scale, const vec2f& uvscale = {1, 1}) { + auto shape = make_drect({steps.x, steps.y}, {1, 1}); + for (auto i : range(shape.positions.size())) { + auto uv = shape.texcoords[i]; + auto a = vec2f{pif * uv.x, pif * (1 - uv.y)}; + shape.positions[i] = + vec3f{cos(a.x) * sin(a.y), sin(a.x) * sin(a.y), cos(a.y)} * scale; + shape.normals[i] = normalize(shape.positions[i]); + shape.texcoords[i] = uv * uvscale; + } + return shape; +} + +// Make a uv capsule +static shape_data make_duvcapsule( + const vec3i& steps, const vec2f& scale, const vec3f& uvscale = {1, 1, 1}) { + auto shape = shape_data{}; + auto qshape = shape_data{}; + // side + qshape = make_drect({steps.x, steps.y}, {1, 1}, {1, 1}); + for (auto i : range(qshape.positions.size())) { + auto uv = qshape.texcoords[i]; + auto phi = 2 * pif * uv.x; + qshape.positions[i] = { + cos(phi) * scale.x, sin(phi) * scale.x, (2 * uv.y - 1) * scale.y}; + qshape.normals[i] = {cos(phi), sin(phi), 0}; + qshape.texcoords[i] = uv * vec2f{uvscale.x, uvscale.y}; + } + for (auto& quad : qshape.quads) quad = {quad.x, quad.w, quad.z, quad.y}; + merge_dshape_inplace(shape, qshape); + // top + qshape = make_drect({steps.x, steps.z}, {1, 1}, {1, 1}); + for (auto i : range(qshape.positions.size())) { + auto uv = qshape.texcoords[i]; + auto phi = uv.x * 2 * pif, theta = (1 - uv.y) * pif / 2; + qshape.positions[i] = {cos(phi) * sin(theta) * scale.x, + sin(phi) * sin(theta) * scale.x, cos(theta) * scale.x + scale.y}; + qshape.normals[i] = { + cos(phi) * sin(theta), sin(phi) * sin(theta), cos(theta)}; + qshape.texcoords[i] = uv * vec2f{uvscale.x, uvscale.z}; + } + merge_dshape_inplace(shape, qshape); + // bottom + qshape = make_drect({steps.x, steps.z}, {1, 1}, {1, 1}); + for (auto i : range(qshape.positions.size())) { + auto uv = qshape.texcoords[i]; + auto phi = uv.x * 2 * pif, theta = (1 - uv.y) * pif / 2; + qshape.positions[i] = {cos(phi) * sin(theta) * scale.x, + sin(phi) * sin(theta) * scale.x, -cos(theta) * scale.x - scale.y}; + qshape.normals[i] = { + cos(phi) * sin(theta), sin(phi) * sin(theta), -cos(theta)}; + qshape.texcoords[i] = uv * vec2f{uvscale.x, uvscale.z}; + qshape.normals[i] = -qshape.normals[i]; + } + for (auto& qquad : qshape.quads) swap(qquad.x, qquad.z); + merge_dshape_inplace(shape, qshape); + return shape; +} + +// Make a uv cone +static shape_data make_duvcone( + const vec3i& steps, const vec2f& scale, const vec3f& uvscale = {1, 1, 1}) { + auto shape = shape_data{}; + auto qshape = shape_data{}; + // side + qshape = make_rect({steps.x, steps.y}, {1, 1}, {1, 1}); + for (auto i : range(qshape.positions.size())) { + auto uv = qshape.texcoords[i]; + auto phi = 2 * pif * uv.x; + auto normal2d = normalize(scale * vec2f{1, 2}); + qshape.positions[i] = {cos(phi) * (1 - uv.y) * scale.x, + sin(phi) * (1 - uv.y) * scale.x, scale.y * (2 * uv.y - 1)}; + qshape.normals[i] = { + cos(phi) * normal2d.y, sin(phi) * normal2d.y, normal2d.x}; + qshape.texcoords[i] = uv * vec2f{uvscale.x, uvscale.y}; + } + for (auto& quad : qshape.quads) quad = {quad.x, quad.w, quad.z, quad.y}; + merge_shape_inplace(shape, qshape); + // bottom + qshape = make_rect({steps.x, steps.z}, {1, 1}, {1, 1}); + for (auto i : range(qshape.positions.size())) { + auto uv = qshape.texcoords[i]; + auto phi = uv.x * 2 * pif; + qshape.positions[i] = { + uv.y * scale.x * cos(phi), uv.y * scale.x * sin(phi), -scale.y}; + qshape.normals[i] = {0, 0, -1}; + qshape.texcoords[i] = uv * vec2f{uvscale.x, uvscale.z}; + } + for (auto& qquad : qshape.quads) swap(qquad.x, qquad.z); + merge_shape_inplace(shape, qshape); + return shape; +} + +} // namespace yocto + +// ----------------------------------------------------------------------------- +// DIAGRAM CREATION +// ----------------------------------------------------------------------------- +namespace yocto { + +// Init diagram +diagram_data make_diagram() { + auto diagram = diagram_data{}; + diagram.scenes.reserve(16); + return diagram; +} + +// Shape transformations +shape_data scale_shape(const shape_data& shape, float size) { + auto tshape = shape; + for (auto& position : tshape.positions) position *= size; + return tshape; +} +shape_data transform_shape( + const shape_data& shape, const frame3f& frame, float size) { + return transform_shape(shape, frame * scaling_frame(vec3f{size, size, size})); +} + +// Shapes +shape_data load_dshape(const string& filename) { + auto loaded = load_shape(filename); + auto shape = shape_data{ + .points = loaded.points, + .lines = loaded.lines, + .triangles = loaded.triangles, + .quads = loaded.quads, + .positions = loaded.positions, + .texcoords = loaded.texcoords, + }; + return shape; +} + +// Helper +// TODO: remove +template +static frame3f params_frame(const Params& params) { + return params.frame; +} + +// Helper +template +struct zip_labels { + struct iterator { + iterator(size_t idx, const vector& labels, const vector& others) + : idx{idx}, labels{labels}, others{others} {} + + bool operator!=(const iterator& other) const { return idx != other.idx; } + void operator++() { idx++; } + pair operator*() const { + return {labels[idx], others[idx]}; + } + + private: + size_t idx; + const vector& labels; + const vector& others; + }; + + zip_labels(const vector& labels, const vector& others) + : labels{labels}, others{others} {} + iterator begin() { return {(size_t)0, labels, others}; } + iterator end() { return {min(labels.size(), others.size()), labels, others}; } + + private: + const vector& labels; + const vector& others; +}; + +// Automatically set offsets +static void update_offsets(diagram_data& diagram) { + auto total_size = vec2f{0, 0}; + for (auto& scene : diagram.scenes) total_size += scene.size + scene.margin; + auto offset = vec2f{-total_size.x / 2, 0}; + for (auto& scene : diagram.scenes) { + scene.offset.x = offset.x + scene.size.x / 2 + scene.margin.x / 2; + scene.offset.y = 0; + offset.x += scene.size.x + scene.margin.x; + } +} + +// Scene +diagram_scene& add_scene(diagram_data& diagram, const string& title, + const vec2f& size, const frame3f& frame, const vec2f& margin) { + return add_scenews(diagram, title, "", size, frame, margin); +} +diagram_scene& add_scene(diagram_data& diagram, const string& title, + const frame3f& frame, const vec2f& margin) { + return add_scenews(diagram, title, "", {2, 2}, frame, margin); +} +diagram_scene& add_scenews(diagram_data& diagram, const string& title, + const string& subtitle, const vec2f& size, const frame3f& frame, + const vec2f& margin) { + auto& scene = diagram.scenes.emplace_back(); + scene.size = size; + scene.margin = margin; + // scene.frame = dscaling({params.scale, params.scale, params.scale}) * + // dtranslation({-params.center.x, -params.center.y, 0}) * + // params.frame; + scene.frame = frame; + + if (!title.empty()) { + scene.objects.push_back({.labels = { + .labels = {title + "!!t"}, + .positions = {{0, scene.size.y / 2, 0}}, + }}); + } + + if (!subtitle.empty()) { + scene.objects.push_back({.labels = { + .labels = {subtitle + "!!b"}, + .positions = {{0, -scene.size.y / 2, 0}}, + }}); + } + + update_offsets(diagram); + + return scene; +} + +// Labels +diagram_labels dlabels(const vector& labels) { + return dlabels({}, labels); +} +diagram_labels dlabels( + const vector& positions, const vector& labels) { + return {.labels = labels, .positions = positions}; +} +diagram_labels dllabels(const vector& labels) { + return {.llabels = labels}; +} +diagram_labels dflabels(const vector& labels) { + return {.flabels = labels}; +} +diagram_labels dclabels(const vector& labels) { + return {.clabels = labels}; +} +diagram_labels dglabels(const vector& labels, + const vector& llabels, const vector& flabels, + const vector& clabels) { + return {.labels = labels, + .llabels = llabels, + .flabels = flabels, + .clabels = clabels, + .positions = {}}; +} +diagram_labels dglabels(const vector& positions, + const vector& labels, const vector& llabels, + const vector& flabels, const vector& clabels) { + return {.labels = labels, + .llabels = llabels, + .flabels = flabels, + .clabels = clabels, + .positions = positions}; +} + +// Add labels +void add_labels(diagram_scene& diagram, const diagram_labels& labels, + const diagram_style& style) { + add_labels(diagram, identity3x4f, labels, style); +} + +void add_labels(diagram_scene& diagram, const diagram_frame& frame, + const diagram_labels& labels, const diagram_style& style) { + diagram.objects.push_back({diagram.frame * frame.frame, {}, labels, style}); +} + +// Add shape +void add_shape(diagram_scene& diagram, const diagram_shape& shape, + const diagram_style& style) { + return add_shape(diagram, identity3x4f, shape, diagram_labels{}, style); +} +void add_shape(diagram_scene& diagram, const diagram_shape& shape, + const diagram_labels& labels, const diagram_style& style) { + return add_shape(diagram, identity3x4f, shape, labels, style); +} +void add_shape(diagram_scene& diagram, const diagram_frame& frame, + const diagram_shape& shape, const diagram_style& style) { + return add_shape(diagram, frame.frame, shape, diagram_labels{}, style); +} + +void add_shape(diagram_scene& diagram, const diagram_frame& frame, + const diagram_shape& shape_, const diagram_labels& labels_, + const diagram_style& style) { + // Copy back + auto shape = shape_; + + // TODO: wireframe + auto wireframe = true; + if (wireframe && shape.lines.empty() && !shape.triangles.empty()) { + shape.lines = get_edges(shape.triangles); + } + if (wireframe && shape.lines.empty() && !shape.quads.empty()) { + shape.lines = get_edges(shape.quads); + } + + auto labels = diagram_labels{}; + for (auto&& [label, position] : zip_labels(labels_.labels, shape.positions)) { + labels.labels.push_back(label); + labels.positions.push_back(position); + } + for (auto&& [label, line] : zip_labels(labels_.llabels, shape.lines)) { + labels.labels.push_back(label); + labels.positions.push_back( + (shape.positions[line.x] + shape.positions[line.y]) / 2); + } + for (auto&& [label, arrow] : zip_labels(labels_.llabels, shape.arrows)) { + labels.labels.push_back(label); + labels.positions.push_back( + (shape.positions[arrow.x] + shape.positions[arrow.y]) / 2); + } + for (auto&& [label, triangle] : + zip_labels(labels_.flabels, shape.triangles)) { + labels.labels.push_back(label); + labels.positions.push_back( + (shape.positions[triangle.x] + shape.positions[triangle.y] + + shape.positions[triangle.z]) / + 3); + } + for (auto&& [label, quad] : zip_labels(labels_.flabels, shape.quads)) { + labels.labels.push_back(label); + labels.positions.push_back( + (shape.positions[quad.x] + shape.positions[quad.y] + + shape.positions[quad.z] + shape.positions[quad.w]) / + 4); + } + if (!labels_.clabels.empty()) { + auto bbox = invalidb3f; + for (auto& position : shape.positions) bbox = merge(bbox, position); + auto center = (bbox.min + bbox.max) / 2; + for (auto&& label : labels_.clabels) { + labels.labels.push_back(label); + labels.positions.push_back(center); + } + } + + diagram.objects.push_back( + {diagram.frame * frame.frame, shape, labels, style}); +} + +// Add points +diagram_shape dpoints(const vector& positions) { + auto shape = diagram_shape{}; + shape.positions = positions; + for (auto idx : range((int)positions.size())) shape.points.push_back(idx); + return shape; +} +diagram_shape dpoints( + const vector& positions, const vector& points) { + auto shape = diagram_shape{}; + shape.positions = positions; + shape.points = points; + return shape; +} + +// Add lines +diagram_shape dlines(const vector& positions) { + auto lines = vector{}; + for (auto idx : range((int)positions.size() / 2)) + lines.push_back({idx * 2 + 0, idx * 2 + 1}); + return dlines(lines, positions); +} +diagram_shape dlines( + const vector& lines, const vector& positions) { + auto shape = diagram_shape{}; + shape.positions = positions; + shape.lines = lines; + return shape; +} +diagram_shape dclines(const vector& positions) { + auto lines = vector{}; + for (auto idx : range((int)positions.size() / 2)) + lines.push_back({idx * 2 + 0, idx * 2 + 1}); + return dclines(lines, positions); +} +diagram_shape dclines( + const vector& lines, const vector& positions) { + auto shape = diagram_shape{}; + shape.positions = positions; + shape.lines = lines; + shape.connect = 0.3f; + return shape; +} + +// Add arrows +diagram_shape darrows(const vector& positions) { + auto arrows = vector{}; + for (auto idx : range((int)positions.size() / 2)) + arrows.push_back({idx * 2 + 0, idx * 2 + 1}); + return darrows(arrows, positions); +} +diagram_shape darrows( + const vector& arrows, const vector& positions) { + auto shape = diagram_shape{}; + shape.positions = positions; + shape.arrows = arrows; + return shape; +} +diagram_shape dcarrows(const vector& positions) { + auto arrows = vector{}; + for (auto idx : range((int)positions.size() / 2)) + arrows.push_back({idx * 2 + 0, idx * 2 + 1}); + return dcarrows(arrows, positions); +} +diagram_shape dcarrows( + const vector& arrows, const vector& positions) { + auto shape = diagram_shape{}; + shape.positions = positions; + shape.arrows = arrows; + shape.connect = 0.3f; + return shape; +} + +// Add vectors +diagram_shape dvectors(const vector& vectors) { + auto shape = diagram_shape{}; + shape.positions = vectors; + shape.positions.push_back({0, 0, 0}); + for (auto idx : range((int)vectors.size())) + shape.arrows.push_back({(int)vectors.size(), idx}); + return shape; +} +diagram_shape dcvectors(const vector& vectors) { + auto shape = diagram_shape{}; + shape.positions = vectors; + shape.positions.push_back({0, 0, 0}); + for (auto idx : range((int)vectors.size())) + shape.arrows.push_back({(int)vectors.size(), idx}); + shape.connect = 0.3; + return shape; +} + +// Add axes +diagram_shape daxes(const frame3f& axes, const vec3f& aspect) { + auto shape = diagram_shape{}; + shape.positions = {axes.o, axes.o + aspect.x * axes.x, + axes.o + aspect.y * axes.y, axes.o + aspect.z * axes.z}; + shape.arrows = {{0, 1}, {0, 2}, {0, 3}}; + return shape; +} +diagram_shape daxes2(const frame3f& axes, const vec3f& aspect) { + auto shape = diagram_shape{}; + shape.positions = { + axes.o, axes.o + aspect.x * axes.x, axes.o + aspect.y * axes.y}; + shape.arrows = {{0, 1}, {0, 2}}; + return shape; +} + +// Add rays +diagram_shape drays(const vector& rays) { + auto positions = vector{}; + for (auto& ray : rays) { + positions.push_back(ray.o); + positions.push_back(ray.o + ray.d); + } + return darrows(positions); +} +diagram_shape drayscont(const vector& rays, float length) { + auto positions = vector{}; + for (auto& ray : rays) { + positions.push_back(ray.o + ray.d); + positions.push_back(ray.o + ray.d * length); + } + return dclines(positions); +} + +// Add quads +diagram_shape dquads( + const vector& positions, const vector& texcoords) { + auto quads = vector{}; + for (auto idx : range((int)positions.size() / 4)) + quads.push_back({idx * 4 + 0, idx * 4 + 1, idx * 4 + 2, idx * 4 + 3}); + return dquads(quads, positions, texcoords); +} +diagram_shape dquads(const vector& quads, const vector& positions, + const vector& texcoords) { + auto shape = diagram_shape{}; + shape.positions = positions; + shape.texcoords = texcoords; + if (texcoords.empty() && positions.size() == 4) { + shape.texcoords = dconstants::quad_texcoords; + } + + if (quads.empty()) { + for (auto idx : range((int)shape.positions.size() / 4)) + shape.quads.push_back( + {idx * 4 + 0, idx * 4 + 1, idx * 4 + 2, idx * 4 + 3}); + } else { + shape.quads = quads; + } + shape.lines = get_edges(shape.quads); + + return shape; +} + +// Add triangles +diagram_shape dtriangles( + const vector& positions, const vector& texcoords) { + auto triangles = vector{}; + for (auto idx : range((int)positions.size() / 3)) + triangles.push_back({idx * 3 + 0, idx * 3 + 1, idx * 3 + 2}); + return dtriangles(triangles, positions, {}); +} +diagram_shape dtriangles(const vector& triangles, + const vector& positions, const vector& texcoords) { + auto shape = diagram_shape{}; + shape.positions = positions; + shape.texcoords = texcoords; + + if (triangles.empty()) { + for (auto idx : range((int)positions.size() / 3)) + shape.triangles.push_back({idx * 3 + 0, idx * 3 + 1, idx * 3 + 2}); + } else { + shape.triangles = triangles; + } + shape.lines = get_edges(shape.triangles); + + return shape; +} + +// Add shape +diagram_shape dshape(const shape_data& shape_, bool wireframe) { + auto shape = diagram_shape{}; + shape.positions = shape_.positions; + shape.normals = shape_.normals; + shape.texcoords = shape_.texcoords; + + shape.points = shape_.points; + shape.lines = shape_.lines; + shape.triangles = shape_.triangles; + shape.quads = shape_.quads; + + if (wireframe && shape.lines.empty() && !shape.triangles.empty()) { + shape.lines = get_edges(shape.triangles); + } + if (wireframe && shape.lines.empty() && !shape.quads.empty()) { + shape.lines = get_edges(shape.quads); + } + + return shape; +} + +// Add polyline +diagram_shape dpolyline(const vector& positions) { + auto shape = diagram_shape{}; + shape.positions = positions; + + for (auto idx : range((int)positions.size() - 1)) + shape.lines.push_back({idx + 0, idx + 1}); + + return shape; +} + +// Add polygon +diagram_shape dpolygon(const vector& positions) { + auto bbox = invalidb3f; + for (auto& position : positions) bbox = merge(bbox, position); + auto center = (bbox.min + bbox.max) / 2; + + auto steps = (int)positions.size(); + auto positions_ = positions; + positions_.push_back(center); + + auto shape = diagram_shape{}; + shape.positions = positions_; + + for (auto idx : range(steps)) { + shape.triangles.push_back({idx, (idx + 1) % steps, steps}); + } + + for (auto idx : range(steps)) { + shape.lines.push_back({idx, (idx + 1) % steps}); + } + + return shape; +} + +// Add rect +diagram_shape drect(const vec2f& aspect) { + auto positions_ = dconstants::quad_positions; + auto scale = vec3f{aspect.x / aspect.y, 1, 1}; + for (auto& position : positions_) position *= scale; + auto shape = diagram_shape{}; + shape.positions = positions_; + shape.texcoords = dconstants::quad_texcoords; + + shape.quads = dconstants::quad_quads; + shape.lines = get_edges(shape.quads); + + return shape; +} + +// Add disk +diagram_shape ddisk(int steps) { + auto positions_ = vector{}; + for (auto idx : range(steps)) { + auto theta = 2 * pif * idx / (float)steps; + positions_.push_back({cos(theta), sin(theta), 0}); + } + positions_.push_back({0, 0, 0}); + auto shape = diagram_shape{}; + shape.positions = positions_; + + for (auto idx : range(steps)) { + shape.triangles.push_back({idx, (idx + 1) % steps, steps}); + } + + for (auto idx : range(steps)) { + shape.lines.push_back({idx, (idx + 1) % steps}); + } + + return shape; +} + +// Add half disk +diagram_shape dhalfdisk(int steps) { + auto positions_ = vector{}; + for (auto idx : range(steps + 1)) { + auto theta = pif * idx / (float)steps; + positions_.push_back({cos(theta), sin(theta), 0}); + } + positions_.push_back({0, 0, 0}); + auto shape = diagram_shape{}; + shape.positions = positions_; + + for (auto idx : range(steps)) { + shape.triangles.push_back({idx, idx, steps + 1}); + } + + for (auto idx : range(steps)) { + shape.lines.push_back({idx, idx + 1}); + } + shape.lines.push_back({0, steps + 1}); + shape.lines.push_back({steps, steps + 1}); + + return shape; +} + +// Add arc +diagram_shape darc( + const vec3f& from, const vec3f& to, const vec3f& center, int steps) { + // refence frame + auto x = normalize(from - center); + auto y = normalize(to - center); + auto angle = acos(dot(x, y)); + auto z = normalize(cross(x, y)); + y = normalize(cross(z, x)); + auto frame = frame3f{x, y, z, center}; + + auto positions_ = vector{}; + for (auto idx : range(steps + 1)) { + auto theta = angle * idx / (float)steps; + positions_.push_back(transform_point(frame, {cos(theta), sin(theta), 0})); + } + auto shape = diagram_shape{}; + shape.positions = positions_; + + for (auto idx : range(steps)) { + shape.lines.push_back({idx, idx + 1}); + } + + return shape; +} + +// Add qbezier +diagram_shape dqbeziers(const vector& positions, int steps) { + auto qbezier_point = [](vec3f p1, vec3f p2, vec3f p3, float u) { + return (1 - u) * (1 - u) * p1 + 2 * (1 - u) * u * p2 + u * u * p3; + }; + auto positions_ = vector(); + for (auto segment : range(positions.size() / 3)) { + for (auto idx : range(steps + 1)) { + positions_.push_back( + qbezier_point(positions[0 + segment * 3], positions[1 + segment * 3], + positions[2 + segment * 3], idx / (float)steps)); + } + } + + return dpolyline(positions_); +} + +// Add bezier +diagram_shape dbeziers(const vector& positions, int steps) { + auto bezier_point = [](vec3f p1, vec3f p2, vec3f p3, vec3f p4, float u) { + return (1 - u) * (1 - u) * (1 - u) * p1 + 3 * (1 - u) * (1 - u) * u * p2 + + 3 * (1 - u) * u * u * p3 + u * u * u * p4; + }; + auto positions_ = vector(); + for (auto segment : range(positions.size() / 4)) { + for (auto idx : range(steps + 1)) { + positions_.push_back(bezier_point(positions[0 + segment * 4], + positions[1 + segment * 4], positions[2 + segment * 4], + positions[3 + segment * 4], idx / (float)steps)); + } + } + + return dpolyline(positions_); +} + +// Add cube +diagram_shape dcube() { + auto shape = diagram_shape{}; + shape.positions = dconstants::cube_positions; + + shape.quads = dconstants::cube_quads; + + shape.lines = get_edges(shape.quads); + + return shape; +} + +// Sphere +diagram_shape dsphere(int steps, bool isolines) { + auto shape_ = make_dsphere(steps, 1); + auto shape = diagram_shape{}; + shape.positions = shape_.positions; + shape.normals = shape_.normals; + shape.texcoords = shape_.texcoords; + + shape.quads = shape_.quads; + if (isolines) { + using dconstants::xup3x4f; + using dconstants::yup3x4f; + for (auto frame : {identity3x4f, xup3x4f, yup3x4f}) { + auto offset = (int)shape.positions.size(); + for (auto idx = 0; idx < steps * 2; idx++) { + auto u = 2 * pif * idx / float(steps * 2); + shape.positions.push_back( + transform_point(frame, vec3f{cos(u), sin(u), 0})); + } + for (auto idx = 0; idx < steps * 2; idx++) { + shape.lines.push_back({offset + idx, offset + (idx + 1) % (steps * 2)}); + } + } + } else { + shape.lines = get_edges(shape.quads); + } + + return shape; +} +diagram_shape duvsphere(int steps, bool isolines) { + auto shape_ = make_duvsphere({steps * 2, steps}, 1); + auto shape = diagram_shape{}; + shape.positions = shape_.positions; + shape.normals = shape_.normals; + shape.texcoords = shape_.texcoords; + + shape.quads = shape_.quads; + if (isolines) { + using dconstants::xup3x4f; + using dconstants::yup3x4f; + for (auto frame : {identity3x4f, xup3x4f, yup3x4f}) { + auto offset = (int)shape.positions.size(); + for (auto idx = 0; idx < steps * 2; idx++) { + auto u = 2 * pif * idx / float(steps * 2); + shape.positions.push_back( + transform_point(frame, vec3f{cos(u), sin(u), 0})); + } + for (auto idx = 0; idx < steps * 2; idx++) { + shape.lines.push_back({offset + idx, offset + (idx + 1) % (steps * 2)}); + } + } + } else { + shape.lines = get_edges(shape.quads); + } + + return shape; +} + +// Add hemisphere +diagram_shape dhemisphere(int steps, bool isolines) { + auto shape_ = make_duvhemisphere({steps * 2, steps / 2}, 1); + auto shape = diagram_shape{}; + shape.positions = shape_.positions; + shape.normals = shape_.normals; + shape.texcoords = shape_.texcoords; + + shape.quads = shape_.quads; + if (isolines) { + using dconstants::xup3x4f; + using dconstants::yup3x4f; + auto offset = (int)shape.positions.size(); + for (auto idx = 0; idx < steps * 2; idx++) { + auto u = 2 * pif * idx / float(steps * 2); + shape.positions.push_back({cos(u), sin(u), 0}); + shape.lines.push_back({offset + idx, offset + (idx + 1) % (steps * 2)}); + } + for (auto frame : {yup3x4f, drotation({1, 0, 0}, 90) * xup3x4f}) { + auto offset = (int)shape.positions.size(); + for (auto idx = 0; idx < steps + 1; idx++) { + auto u = pif * idx / float(steps); + shape.positions.push_back(transform_point(frame, {cos(u), sin(u), 0})); + if (idx != steps) + shape.lines.push_back({offset + idx, offset + (idx + 1)}); + } + } + } else { + shape.lines = get_edges(shape.quads); + } + + return shape; +} + +// Add cone +diagram_shape dcone(int steps, bool isolines) { + auto shape_ = make_duvcone({steps, 1, 1}, {1, 1}); + auto shape = diagram_shape{}; + shape.positions = shape_.positions; + shape.normals = shape_.normals; + shape.texcoords = shape_.texcoords; + + shape.quads = shape_.quads; + if (isolines) { + auto offset = (int)shape.positions.size(); + for (auto idx = 0; idx < steps * 2; idx++) { + auto u = 2 * pif * idx / float(steps * 2); + shape.positions.push_back({cos(u), sin(u), 0}); + } + for (auto idx = 0; idx < steps * 2; idx++) { + shape.lines.push_back({offset + idx, offset + (idx + 1) % steps}); + } + } else { + shape.lines = get_edges(shape.quads); + } + + return shape; +} + +// Add bbox +diagram_shape dbbox(const bbox3f& bbox_, float epsilon) { + auto bbox = bbox3f{bbox_.min - epsilon, bbox_.max + epsilon}; + auto positions_ = dconstants::cube_positions; + for (auto& position : positions_) { + position.x = position.x < 0 ? bbox.min.x : bbox.max.x; + position.y = position.y < 0 ? bbox.min.y : bbox.max.y; + position.z = position.z < 0 ? bbox.min.z : bbox.max.z; + } + + auto shape = diagram_shape{}; + shape.positions = positions_; + + shape.quads = dconstants::cube_quads; + shape.lines = get_edges(shape.quads); + + return shape; +} + +// Add grid +diagram_shape ddotgrid(const vec2i& steps) { + auto ratio = (float)steps.x / (float)steps.y; + auto shape_ = make_drect({steps.x, steps.y}, {ratio, 1}); + auto positions_ = vector{}; + for (auto& quad : shape_.quads) { + positions_.push_back( + (shape_.positions[quad.x] + shape_.positions[quad.y] + + shape_.positions[quad.z] + shape_.positions[quad.w]) / + 4); + } + auto shape = diagram_shape{}; + shape.positions = positions_; + + for (auto idx : range((int)steps.x * (int)steps.y)) + shape.points.push_back(idx); + + return shape; +} + +// Add grid +diagram_shape dgrid(const vec2i& steps) { + auto ratio = (float)steps.x / (float)steps.y; + auto shape_ = make_drect({steps.x, steps.y}, {ratio, 1}); + auto shape = diagram_shape{}; + shape.positions = shape_.positions; + + shape.quads = shape_.quads; + shape.lines = get_edges(shape.quads); + + return shape; +} + +// Add grid +diagram_shape ddiskgrid(const vec2i& steps, int dsteps) { + auto shape = diagram_shape{}; + for (auto idxr : range(steps.x)) { + auto offset = (int)shape.positions.size(); + auto radius = (idxr + 1) / (float)steps.x; + for (auto idx : range(dsteps * 2)) { + auto u = 2 * pif * idx / float(dsteps * 2); + shape.positions.push_back({radius * cos(u), radius * sin(u), 0}); + shape.lines.push_back({offset + idx, offset + (idx + 1) % (dsteps * 2)}); + } + } + + for (auto idx : range(steps.y)) { + auto offset = (int)shape.positions.size(); + auto theta = 2 * pif * idx / (float)steps.y; + shape.positions.push_back({cos(theta), sin(theta), 0}); + shape.positions.push_back({cos(theta + pif), sin(theta + pif), 0}); + shape.lines.push_back({offset + 0, offset + 1}); + } + + return shape; +} +diagram_shape dudiskgrid(const vec2i& steps, int dsteps) { + auto shape = diagram_shape{}; + for (auto idxr : range(steps.x)) { + auto offset = (int)shape.positions.size(); + auto radius = sqrt((idxr + 1) / (float)steps.x); + for (auto idx : range(dsteps * 2)) { + auto u = 2 * pif * idx / float(dsteps * 2); + shape.positions.push_back({radius * cos(u), radius * sin(u), 0}); + shape.lines.push_back({offset + idx, offset + (idx + 1) % (dsteps * 2)}); + } + } + + for (auto idx : range(steps.y)) { + auto offset = (int)shape.positions.size(); + auto theta = 2 * pif * idx / (float)steps.y; + shape.positions.push_back({cos(theta), sin(theta), 0}); + shape.positions.push_back({cos(theta + pif), sin(theta + pif), 0}); + shape.lines.push_back({offset + 0, offset + 1}); + } + + return shape; +} + +// Add affine grid +diagram_shape daffinegrid( + const vec3f& axes_a, const vec3f& axes_b, const vec2i& steps) { + auto ratio = (float)steps.x / (float)steps.y; + auto shape_ = make_drect({steps.x, steps.y}, {ratio, 1}); + auto positions_ = shape_.positions; + for (auto& position : positions_) + position = position.x * axes_a + position.y * axes_b; + auto shape = diagram_shape{}; + shape.positions = positions_; + + shape.quads = shape_.quads; + shape.lines = get_edges(shape.quads); + + return shape; +} + +// Add image +diagram_shape dimagerect(const vec2i& size) { + auto ratio = (float)size.x / (float)size.y; + auto scale = vec3f{ratio, 1, 1}; + auto positions_ = dconstants::quad_positions; + for (auto& position : positions_) position *= scale; + auto shape = diagram_shape{}; + shape.positions = positions_; + shape.texcoords = dconstants::quad_texcoords; + + shape.quads = dconstants::quad_quads; + shape.lines = get_edges(shape.quads); + + return shape; +} +diagram_shape dimagerect(const image_t& img) { + return dimagerect(img.size()); +} + +// Add image grid +diagram_shape dimagegrid(const vec2i& size) { + auto ratio = (float)size.x / (float)size.y; + auto shape_ = make_dquads({size.x, size.y}, {ratio, 1}, {1, 1}); + auto shape = diagram_shape{}; + shape.positions = shape_.positions; + shape.texcoords = shape_.texcoords; + shape.quads = shape_.quads; + return shape; +} +diagram_shape dimagegrid(const image_t& img) { + return dimagegrid(img.size()); +} + +// Add image label +diagram_shape dimagelabel(const vec2i& size, float scale) { + auto ratio = (float)size.x / (float)size.y; + auto oscale = vec3f{3 * scale, 1, 1} * 0.1f; + auto ocenter = vec3f{ratio, -1, 0} + vec3f{-0.40, +0.175, +0.01}; + auto opositions = dconstants::quad_positions; + for (auto& oposition : opositions) oposition = oposition * oscale + ocenter; + return { + .positions = opositions, + .texcoords = dconstants::quad_texcoords, + .quads = dconstants::quad_quads, + }; +} +diagram_shape dimagelabel(const image_t& image, float scale) { + return dimagelabel(image.size(), scale); +} + +// Add random points +diagram_shape drandompoints(int num, bool stratified) { + return drandompoints(drandompoints_type::quad, num, stratified); +} +diagram_shape drandompoints(drandompoints_type type, int num, bool stratified) { + auto rng = make_rng(187291); + auto positions_ = vector{}; + auto ssteps = int(round(sqrt((float)num))); + for (auto idx : range(num)) { + auto uv = !stratified ? rand2f(rng) + : vec2f{(idx % ssteps + rand1f(rng)) / ssteps, + (idx / ssteps + rand1f(rng)) / ssteps}; + switch (type) { + case drandompoints_type::quad: { + positions_.push_back({2 * uv.x - 1, 2 * uv.y - 1, 0}); + } break; + case drandompoints_type::disk: { + positions_.push_back({sample_disk(uv).x, sample_disk(uv).y, 0}); + } break; + case drandompoints_type::disknu: { + auto uv_ = vec2f{uv.x, uv.y * uv.y}; + positions_.push_back({sample_disk(uv_).x, sample_disk(uv_).y, 0}); + } break; + case drandompoints_type::triangle: { + auto& triangle = dconstants::triangle_positions; + positions_.push_back(interpolate_triangle( + triangle[0], triangle[1], triangle[2], sample_triangle(uv))); + } break; + case drandompoints_type::hemi: { + positions_.push_back(sample_hemisphere(uv)); + } break; + case drandompoints_type::hemicos: { + positions_.push_back(sample_hemisphere_cos(uv)); + } break; + case drandompoints_type::hemicospower: { + positions_.push_back(sample_hemisphere_cospower(64, uv)); + } break; + case drandompoints_type::sphere: { + positions_.push_back(sample_sphere(uv)); + } break; + case drandompoints_type::linex: { + positions_.push_back({2 * uv.x - 1, 0, 0}); + } break; + case drandompoints_type::liney: { + positions_.push_back({0, 2 * uv.y - 1, 0}); + } break; + } + } + auto shape = diagram_shape{}; + shape.positions = positions_; + + for (auto idx : range(num)) shape.points.push_back(idx); + + return shape; +} + +// Add random points +diagram_shape drandompoints3(int num, bool stratified) { + return drandompoints3(drandompoints3_type::cube, num, stratified); +} +diagram_shape drandompoints3( + drandompoints3_type type, int num, bool stratified) { + auto rng = make_rng(187291); + auto positions_ = vector{}; + if (!stratified) { + for (auto idx : range(num)) { + auto uv = rand3f(rng); + switch (type) { + case drandompoints3_type::cube: { + positions_.push_back(2 * uv - 1); + } break; + case drandompoints3_type::linex: { + positions_.push_back({2 * uv.x - 1, 0, 0}); + } break; + case drandompoints3_type::liney: { + positions_.push_back({0, 2 * uv.y - 1, 0}); + } break; + case drandompoints3_type::linez: { + positions_.push_back({0, 0, 2 * uv.z - 1}); + } break; + } + } + } else { + auto ssteps = int(round(pow((float)num, 1 / 3.0f))); + for (auto k : range(ssteps)) { + for (auto j : range(ssteps)) { + for (auto i : range(ssteps)) { + auto idx = vec3i{i, j, k}; + auto uv = ((vec3f)idx + rand3f(rng)) / ssteps; + switch (type) { + case drandompoints3_type::cube: { + positions_.push_back(2 * uv - 1); + } break; + case drandompoints3_type::linex: { + positions_.push_back({2 * uv.x - 1, 0, 0}); + } break; + case drandompoints3_type::liney: { + positions_.push_back({0, 2 * uv.y - 1, 0}); + } break; + case drandompoints3_type::linez: { + positions_.push_back({0, 0, 2 * uv.z - 1}); + } break; + } + } + } + } + } + auto shape = diagram_shape{}; + shape.positions = positions_; + + for (auto idx : range(num)) shape.points.push_back(idx); + + return shape; +} + +// Add random lines +diagram_shape drandomlines(int num, bool stratified) { + return drandomlines(drandomlines_type::hemi, num, stratified); +} +diagram_shape drandomlines(drandomlines_type type, int num, bool stratified) { + auto rng = make_rng(187291); + auto ssteps = int(round(sqrt((float)num))); + auto positions_ = vector{}; + for (auto idx : range(num)) { + auto uv = !stratified ? rand2f(rng) + : vec2f{(idx % ssteps + rand1f(rng)) / ssteps, + (idx / ssteps + rand1f(rng)) / ssteps}; + switch (type) { + case drandomlines_type::hemi: + positions_.push_back({0, 0, 0}); + positions_.push_back(sample_hemisphere({0, 0, 1}, uv)); + break; + case drandomlines_type::hemicos: + positions_.push_back({0, 0, 0}); + positions_.push_back(sample_hemisphere_cos({0, 0, 1}, uv)); + break; + case drandomlines_type::hemicospower: + positions_.push_back({0, 0, 0}); + positions_.push_back(sample_hemisphere_cospower(64, {0, 0, 1}, uv)); + break; + case drandomlines_type::beam: + auto rdisk = sample_disk(uv); + positions_.push_back({rdisk.x, rdisk.y, 0}); + positions_.push_back({rdisk.x, rdisk.y, 1}); + break; + } + } + auto shape = diagram_shape{}; + shape.positions = positions_; + + for (auto idx : range(num)) { + shape.lines.push_back({idx * 2 + 0, idx * 2 + 1}); + } + + return shape; +} + +// Add plot +diagram_scene& add_plot(diagram_data& diagram, const string& title, + const vec2f& size, const frame3f& frame, const vec2f& margin) { + return add_scene(diagram, title, size, frame, margin); +} + +// Add plot axes +diagram_scene& add_plotaxes(diagram_scene& diagram, const bbox2f& bounds, + const vector>& xticks, + const vector>& yticks, const diagram_style& style, + const diagram_style& lstyle) { + // set frame + auto min = bounds.min, max = bounds.max; + auto center = (max + min) / 2, diagonal = (max - min); + auto pframe = dscaling({diagram.size.x / diagonal.x, + diagram.size.y / diagonal.y, 1.0f}) * + dtranslation({-center.x, -center.y, 0.0f}); + diagram.frame = diagram.frame * pframe; + + // add frame + add_shape(diagram, + dquads({{min.x, min.y, 0}, {max.x, min.y, 0}, {max.x, max.y, 0}, + {min.x, max.y, 0}}), + style); + + auto lpositions = vector{}; + for (auto& [x, _] : xticks) { + lpositions.push_back({x, min.y, 0}); + lpositions.push_back({x, max.y, 0}); + } + for (auto& [y, _] : yticks) { + lpositions.push_back({min.x, y, 0}); + lpositions.push_back({max.x, y, 0}); + } + add_shape(diagram, dlines(lpositions), lstyle); + + auto tpositions = vector{}; + auto tlabels = vector{}; + for (auto&& [x, label] : xticks) { + tpositions.push_back({x, min.y, 0}); + tlabels.push_back(label + "!!b"); + } + for (auto&& [y, label] : yticks) { + tpositions.push_back({min.x, y, 0}); + tlabels.push_back(label + "!!l"); + } + add_labels(diagram, {.positions = tpositions, .labels = tlabels}); + + return diagram; +} + +// Add plot shape +void add_plotshape(diagram_scene& diagram, const diagram_shape& shape, + const diagram_style& style) { + return add_plotshape(diagram, shape, {}, style); +} +void add_plotshape(diagram_scene& diagram, const diagram_shape& shape, + const diagram_labels& labels, const diagram_style& style) { + return add_shape(diagram, shape, labels, style); +} + +// Plot functions +diagram_shape dplotline(const vector& points) { + auto shape = diagram_shape{}; + for (auto& point : points) shape.positions.push_back({point.x, point.y, 0}); + for (auto idx : range((int)points.size() - 1)) + shape.lines.push_back({idx, idx + 1}); + return shape; +} +diagram_shape dplotpoints(const vector& points) { + auto shape = diagram_shape{}; + for (auto& point : points) shape.positions.push_back({point.x, point.y, 0}); + for (auto idx : range((int)points.size())) shape.points.push_back(idx); + return shape; +} +diagram_shape dplotarrows(const vector& points) { + auto shape = diagram_shape{}; + for (auto& point : points) shape.positions.push_back({point.x, point.y, 0}); + for (auto idx : range((int)points.size() - 1)) + shape.arrows.push_back({idx, idx + 1}); + return shape; +} +vector dplotfunc( + const function& func, const vec2f& range_, int samples) { + auto points = vector(samples); + for (auto idx : range(samples)) { + auto x = lerp(range_.x, range_.y, (float)idx / (float)(samples - 1)); + points[idx] = {x, func(x)}; + } + return points; +} +vector dplotpfunc(const function& func, int samples) { + auto points = vector(samples + 1); + for (auto idx : range(samples)) { + auto theta = lerp(0, 2 * pif, (float)idx / (float)(samples)); + auto radius = func(theta); + points[idx] = {radius * cos(theta), radius * sin(theta)}; + } + points[samples] = points[0]; + return points; +} +vector dplotcurve(const vector& curve, bool center) { + auto size = (int)curve.size(); + auto points = vector(size); + for (auto i : range(size)) + points[i] = {center ? ((i + 0.5f) / size) : (float(i) / size), curve[i]}; + return points; +} +vector dplotcurve(const vector& curve, const vec2f& range) { + auto size = (int)curve.size(); + auto points = vector(curve.size()); + for (auto i : yocto::range(size)) + points[i] = {lerp(range.x, range.y, i / (float)(size - 1)), curve[i]}; + return points; +} + +// Add plot +diagram_scene& add_plot3(diagram_data& diagram, const string& title, + const vec2f& size, const frame3f& frame, const vec2f& margin) { + return add_scene(diagram, title, size, frame, margin); +} + +// Add plot axes +diagram_scene& add_plotaxes3(diagram_scene& diagram, const bbox3f& bounds, + const vec3f& size, const vector>& xticks, + const vector>& yticks, + const vector>& zticks, const diagram_style& style, + const diagram_style& lstyle) { + // set frame + auto min = bounds.min, max = bounds.max; + auto center = (max + min) / 2, diagonal = (max - min); + auto pframe = dscaling(size / diagonal) * dtranslation(-center); + // diagram.frame = diagram.frame * pframe * dconstants::yup3x4f; + diagram.frame = diagram.frame * pframe; + + // add frame + add_shape(diagram, + dquads({ + // z quad + {min.x, min.y, min.z}, {max.x, min.y, min.z}, {max.x, max.y, min.z}, + {min.x, max.y, min.z}, // z quad + // x quad + {min.x, min.y, min.z}, {min.x, max.y, min.z}, {min.x, max.y, max.z}, + {min.x, min.y, max.z}, // x quad + // y quad + {min.x, min.y, min.z}, {max.x, min.y, min.z}, {max.x, min.y, max.z}, + {min.x, min.y, max.z} // y quad + }), + style); + +#if 0 + auto lpositions = vector{}; + for (auto& [x, _] : xticks) { + lpositions.push_back({x, min.y, 0}); + lpositions.push_back({x, max.y, 0}); + } + for (auto& [y, _] : yticks) { + lpositions.push_back({min.x, y, 0}); + lpositions.push_back({max.x, y, 0}); + } + add_shape(diagram, dlines(lpositions), lstyle); +#endif + + auto tpositions = vector{}; + auto tlabels = vector{}; + for (auto&& [x, label] : xticks) { + tpositions.push_back({x, min.y, min.z}); + tlabels.push_back(label + "!!l"); + } + for (auto&& [y, label] : yticks) { + tpositions.push_back({min.x, y, min.z}); + tlabels.push_back(label + "!!r"); + } + for (auto&& [z, label] : zticks) { + tpositions.push_back({min.x, min.y, z}); + tlabels.push_back(label + "!!r"); + } + add_labels(diagram, {.positions = tpositions, .labels = tlabels}); + + return diagram; +} + +// Plot surface +diagram_shape dplotsurface(const function& func, + const vec2f& xrange, const vec2f& yrange, const vec2i& steps, + bool isolines) { + auto shape = diagram_shape{}; + + // steps + auto stepsp1 = vec2i{steps.x + 1, steps.y + 1}; + + // vertices + shape.positions = vector(stepsp1.x * stepsp1.y); + shape.texcoords = vector(stepsp1.x * stepsp1.y); + for (auto j : range(stepsp1.y)) { + for (auto i : range(stepsp1.x)) { + auto uv = vec2f{i / (float)steps.x, j / (float)steps.y}; + auto x = lerp(xrange.x, xrange.y, uv.x); + auto y = lerp(yrange.x, yrange.y, uv.y); + auto z = func({x, y}); + shape.positions[j * stepsp1.x + i] = {x, y, z}; + shape.texcoords[j * stepsp1.x + i] = uv; + } + } + + // faces + shape.quads = vector(steps.x * steps.y); + for (auto j : range(steps.y)) { + for (auto i : range(steps.x)) { + shape.quads[j * steps.x + i] = {j * stepsp1.x + i, j * stepsp1.x + i + 1, + (j + 1) * stepsp1.x + i + 1, (j + 1) * stepsp1.x + i}; + } + } + + // no isolines + if (!isolines) return shape; + + // isolines + for (auto j : {0, steps.y / 2, steps.y}) { + for (auto i : range(steps.x)) { + shape.lines.push_back({j * stepsp1.x + i, j * stepsp1.x + i + 1}); + } + } + for (auto i : {0, steps.x / 2, steps.x}) { + for (auto j : range(steps.y)) { + shape.lines.push_back({j * stepsp1.x + i, (j + 1) * stepsp1.x + i}); + } + } + + // done + return shape; +} + +} // namespace yocto + +// ----------------------------------------------------------------------------- +// DIAGRAM RENDERING +// ----------------------------------------------------------------------------- +namespace yocto { + +// Make default camera +// 100 : 36 = 25 : x => x = 36 * 25 / 100 = 9 +// 100 : 36 = d : 10 => d = 100 / 36 * 10 = 27.77777 +// l : 36 = 25 : 10 => l = 25 * 36 / 10 = 90 +// l : 36 = 25 : 9 => l = 25 * 36 / 9 = 100 +static camera_data make_default_camera() { + return { + .frame = lookat_frame(vec3f{0, 0, 25}, vec3f{0, 0, 0}, vec3f{0, 1, 0}), + .lens = 0.1f, + // .aspect = 16.0f / 9.0f, + .aspect = 1, + .focus = 25, + }; +} + +// Render text +static texture_data make_text_texture(const string& text) { + // Rendered text location + static string text_location = "./textcache"; + + // Latex template + static string latex_template_header = + "\\documentclass[10pt]{article}\n" + "\\usepackage[cochineal]{newtx}\n" + "\\usepackage[T1]{fontenc}\n" + "\\usepackage{parskip}\n" + "\n" + "\\begin{document}\n" + "\\pagenumbering{gobble}\n\n"; + static string latex_template_footer = + "\n\n" + "\\end{document}\n\n"; + + // djb2 algorithm from http://www.cse.yorku.ca/~oz/hash.html + auto hash = [](const string& text) -> string { + auto str = text.c_str(); + auto hash = (unsigned long)5381; + auto c = (int)*str++; + while (c) { + hash = ((hash << 5) + hash) + c; // hash * 33 + c + c = (int)*str++; + } + return std::to_string(hash); + }; + + auto text_name = hash(text); + auto texture_name = text_location + "/" + text_name + ".png"; + auto latex_name = text_location + "/" + text_name + ".tex"; + if (!path_exists(texture_name)) { + // save latex + printf("missing text: %s\n", text.c_str()); + auto latex = latex_template_header + text + latex_template_footer; + save_text(latex_name, latex); + // render latex + auto command = "cd " + text_location + " && pdflatex " + text_name + ".tex"; + printf("%s\n", command.c_str()); + system(command.c_str()); + command = "cd " + text_location + " && pdfcrop " + text_name + ".pdf " + + text_name + "-cropped.pdf"; + printf("%s\n", command.c_str()); + system(command.c_str()); + command = "cd " + text_location + + " && gs -sDEVICE=pnggray -sBATCH -sOutputFile=" + text_name + + ".png" + " -dNOPAUSE -r1200 " + text_name + "-cropped.pdf"; + printf("%s\n", command.c_str()); + system(command.c_str()); + system(("rm " + text_location + "/*.aux").c_str()); + system(("rm " + text_location + "/*.fls").c_str()); + system(("rm " + text_location + "/*.log").c_str()); + system(("rm " + text_location + "/*.gz").c_str()); + system(("rm " + text_location + "/*.fdb_latexmk").c_str()); + } + + auto texture = load_texture(texture_name); + for (auto& c : texture.pixelsb) { + c = {255, 255, 255, (byte)(255 - c.x)}; + } + + return texture; +} + +// Text shape +static shape_data make_text_shape(const string& text, const vec3f& offset, + const texture_data& texture, float scale) { + auto size = max(texture.pixelsf.size(), texture.pixelsb.size()); + auto [width, height] = scale * (vec2f)size / 72.0f; + auto offset_ = vec3f{scale * 144 / 72.0f, scale * 144 / 72.0f, 1}; + auto z = 0.1f; // small offset to avoid z fighting + auto shape = shape_data{}; + shape.quads = {{0, 1, 2, 3}}; + shape.positions = {{-width, -height, z}, {+width, -height, z}, + {+width, +height, z}, {-width, +height, z}}; + for (auto& p : shape.positions) { + if (offset.x > 0) p.x += +width + offset_.x * offset.x; + if (offset.x < 0) p.x += -width + offset_.x * offset.x; + if (offset.y > 0) p.y += +height + offset_.y * offset.y; + if (offset.y < 0) p.y += -height + offset_.y * offset.y; + if (offset.z > 0) p.z += offset.z; + } + shape.texcoords = {{0, 1}, {1, 1}, {1, 0}, {0, 0}}; + return shape; +} + +// Text offset +static frame3f make_text_frame(const string& text, const texture_data& texture, + const vec3f& camera, const vec3f& position, float scale) { + return lookat_frame(position, camera, {0, 1, 0}, true); +} + +// Make text material +static material_data make_text_material(const vec4f& color, int textureid) { + return { + .type = material_type::matte, + .color = {color.x, color.y, color.z}, + .opacity = color.w, + .color_tex = textureid, + }; +} + +// split label +[[maybe_unused]] static pair split_label(const string& label) { + auto pos = label.find("!!"); + if (pos == string::npos) return {label, {0, 0, 0}}; + auto text = label.substr(0, pos); + auto offset = vec3f{0, 0, 0}; + auto scale = 1.0f; + for (auto c : label.substr(pos + 2)) { + auto reset_scale = true; + switch (c) { + case 'h': + scale = 0.5f; + reset_scale = false; + break; + case 'a': + scale = 0.001f; + reset_scale = false; + break; + case 'l': offset.x -= scale; break; + case 'r': offset.x += scale; break; + case 't': offset.y += scale; break; + case 'b': offset.y -= scale; break; + case 'n': offset.z += scale; break; + case 'f': offset.z -= scale; break; + } + if (reset_scale) scale = 1.0f; + } + return {text, offset}; +} + +// Make text labels +static vector make_text_labels(const vector& labels) { + if (labels.empty()) return {}; + auto texts = vector{}; + for (auto& label : labels) texts.push_back(split_label(label).first); + return texts; +} + +// Make text offsets +static vector make_text_offsets(const vector& labels) { + if (labels.empty()) return {}; + auto offsets = vector{}; + for (auto& label : labels) offsets.push_back(split_label(label).second); + return offsets; +} + +// Make surface +static shape_data make_triangles_shape(const vector& triangles, + const vector& positions_, const vector& texcoords, + const frame3f& frame) { + auto positions = positions_; + for (auto& position : positions) position = transform_point(frame, position); + + auto nshape = shape_data{}; + nshape.triangles = triangles; + nshape.positions = positions; + nshape.texcoords = texcoords; + return nshape; +} + +// Make surface +static shape_data make_quads_shape(const vector& quads, + const vector& positions_, const vector& texcoords, + const frame3f& frame) { + auto positions = positions_; + for (auto& position : positions) position = transform_point(frame, position); + + auto nshape = shape_data{}; + nshape.quads = quads; + nshape.positions = positions; + nshape.texcoords = texcoords; + return nshape; +} + +// Make fill material +static material_data make_fill_material( + const vec4f& fill, bool highlight, int textureid) { + return { + .type = highlight ? material_type::glossy : material_type::matte, + .color = {fill.x, fill.y, fill.z}, + .opacity = fill.w, + .color_tex = textureid, + }; +} + +// Make a shape of lines and points +static shape_data make_points_shape(const vector& points, + const vector& positions_, const frame3f& frame, float radius) { + auto make_sphere = [](vec3f position, float radius) { + auto frame = translation_frame(position) * + scaling_frame(vec3f{radius * 3, radius * 3, radius * 3}); + auto sphere = make_dsphere(8, 1); + for (auto& p : sphere.positions) p = transform_point(frame, p); + return sphere; + }; + + auto positions = positions_; + for (auto& position : positions) position = transform_point(frame, position); + + auto nshape = shape_data{}; + for (auto& point : points) { + merge_shape_inplace(nshape, make_sphere(positions[point], radius)); + } + return nshape; +} + +// Make a shape of lines +static shape_data make_lines_shape(const vector& lines, + const vector& positions_, const frame3f& frame, float radius, + float connect) { + auto make_capsule = [](vec3f position1, vec3f position2, float radius) { + auto axis = position2 - position1; + auto rotation_axis = normalize(normalize(axis) + vec3f{0, 0, 1}); + auto rotation_angle = pif; + auto frame = translation_frame((position1 + position2) / 2) * + rotation_frame(rotation_axis, rotation_angle); + auto capsule = make_duvcapsule({16, 8, 8}, {radius, length(axis) / 2}); + for (auto& p : capsule.positions) p = transform_point(frame, p); + return capsule; + }; + + auto positions = positions_; + for (auto& position : positions) position = transform_point(frame, position); + + auto nshape = shape_data{}; + for (auto& line : lines) { + if (connect <= 0) { + merge_shape_inplace( + nshape, make_capsule(positions[line.x], positions[line.y], radius)); + } else { + auto position1 = positions[line.x], position2 = positions[line.y]; + auto middle = (position1 + position2) / 2; + auto offset = max(0.0f, length(position2 - position1) - connect) * + normalize(position2 - position1) / 2; + merge_shape_inplace( + nshape, make_capsule(middle - offset, middle + offset, radius)); + } + } + return nshape; +} + +// Make a shape of arrowa +static shape_data make_arrows_shape(const vector& lines, + const vector& positions_, const frame3f& frame, float radius, + float connect) { + auto make_capsule = [](vec3f position1, vec3f position2, float radius) { + auto axis = position2 - position1; + auto rotation_axis = normalize(normalize(axis) + vec3f{0, 0, 1}); + auto rotation_angle = pif; + auto frame = translation_frame((position1 + position2) / 2) * + rotation_frame(rotation_axis, rotation_angle); + auto capsule = make_duvcapsule({16, 8, 8}, {radius, length(axis) / 2}); + for (auto& p : capsule.positions) p = transform_point(frame, p); + return capsule; + }; + auto make_arrow = [](vec3f position1, vec3f position2, float radius) { + auto nshape = shape_data{}; + auto axis = position2 - position1; + auto rotation_axis = normalize(normalize(axis) + vec3f{0, 0, 1}); + auto rotation_angle = pif; + auto frame = translation_frame((position1 + position2) / 2) * + rotation_frame(rotation_axis, rotation_angle); + auto capsule = make_duvcapsule({16, 8, 8}, {radius, length(axis) / 2}); + for (auto& p : capsule.positions) p = transform_point(frame, p); + merge_shape_inplace(nshape, capsule); + auto frame2 = translation_frame(position2) * + rotation_frame(rotation_axis, rotation_angle); + auto cone = make_duvcone({8, 8, 8}, {radius * 3, radius * 5}); + for (auto& p : cone.positions) p = transform_point(frame2, p); + merge_shape_inplace(nshape, cone); + return nshape; + }; + + auto positions = positions_; + for (auto& position : positions) position = transform_point(frame, position); + + auto nshape = shape_data{}; + for (auto& line : lines) { + if (connect <= 0) { + merge_shape_inplace( + nshape, make_arrow(positions[line.x], positions[line.y], radius)); + } else { + auto position1 = positions[line.x], position2 = positions[line.y]; + auto middle = (position1 + position2) / 2; + auto offset = max(0.0f, length(position2 - position1) - connect) * + normalize(position2 - position1) / 2; + merge_shape_inplace( + nshape, make_arrow(middle - offset, middle + offset, radius)); + } + } + return nshape; +} + +// Make stroke material +static material_data make_stroke_material( + const vec4f& stroke, int textureid = invalidid) { + return { + .type = material_type::matte, + .color = {stroke.x, stroke.y, stroke.z}, + .opacity = stroke.w, + .color_tex = textureid, + }; +} + +// Make an image texture +static texture_data make_image_texture( + const image_t& image, bool linear, bool nearest) { + if (linear) { + return texture_data{.pixelsf = image, .nearest = nearest}; + } else { + return texture_data{.pixelsb = float_to_byte(image), .nearest = nearest}; + } +} + +// Add an object to a scene +static void add_object(scene_data& scene, const diagram_object& object) { + // unpack + auto& [frame, shape, labels, style] = object; + + // triangles + if (!shape.triangles.empty() && style.fill.w > 0) { + scene.shapes.push_back(make_triangles_shape( + shape.triangles, shape.positions, shape.texcoords, frame.frame)); + if (style.texture.empty()) { + scene.materials.push_back( + make_fill_material(style.fill, style.highlight, invalidid)); + } else { + scene.textures.emplace_back( + make_image_texture(style.texture, style.linear, style.nearest)); + scene.materials.push_back(make_fill_material( + style.fill, style.highlight, (int)scene.textures.size() - 1)); + } + auto& ninstance = scene.instances.emplace_back(); + ninstance.shape = (int)scene.shapes.size() - 1; + ninstance.material = (int)scene.materials.size() - 1; + ninstance.frame = identity3x4f; + } + + // quads + if (!shape.quads.empty() && style.fill.w > 0) { + scene.shapes.push_back(make_quads_shape( + shape.quads, shape.positions, shape.texcoords, frame.frame)); + if (style.texture.empty()) { + scene.materials.push_back( + make_fill_material(style.fill, style.highlight, invalidid)); + } else { + scene.textures.emplace_back( + make_image_texture(style.texture, style.linear, style.nearest)); + scene.materials.push_back(make_fill_material( + style.fill, style.highlight, (int)scene.textures.size() - 1)); + } + auto& ninstance = scene.instances.emplace_back(); + ninstance.shape = (int)scene.shapes.size() - 1; + ninstance.material = (int)scene.materials.size() - 1; + ninstance.frame = identity3x4f; + } + + // points + if (!shape.points.empty() && style.stroke.w > 0) { + scene.shapes.push_back(make_points_shape( + shape.points, shape.positions, frame.frame, style.thickness)); + scene.materials.push_back(make_stroke_material(style.stroke)); + auto& ninstance = scene.instances.emplace_back(); + ninstance.shape = (int)scene.shapes.size() - 1; + ninstance.material = (int)scene.materials.size() - 1; + ninstance.frame = identity3x4f; + } + + // lines + if (!shape.lines.empty() && style.stroke.w > 0) { + scene.shapes.push_back(make_lines_shape(shape.lines, shape.positions, + frame.frame, style.thickness, max(shape.connect, style.connect))); + scene.materials.push_back(make_stroke_material(style.stroke)); + auto& ninstance = scene.instances.emplace_back(); + ninstance.shape = (int)scene.shapes.size() - 1; + ninstance.material = (int)scene.materials.size() - 1; + ninstance.frame = identity3x4f; + } + + // arrows + if (!shape.arrows.empty() && style.stroke.w > 0) { + scene.shapes.push_back(make_arrows_shape(shape.arrows, shape.positions, + frame.frame, style.thickness, max(shape.connect, style.connect))); + scene.materials.push_back(make_stroke_material(style.stroke)); + auto& ninstance = scene.instances.emplace_back(); + ninstance.shape = (int)scene.shapes.size() - 1; + ninstance.material = (int)scene.materials.size() - 1; + ninstance.frame = identity3x4f; + } + + // text + if ((!labels.labels.empty()) && style.text.w > 0) { + auto positions = labels.positions; + auto offsets = make_text_offsets(labels.labels); + auto texts = make_text_labels(labels.labels); + for (auto idx = 0; idx < (int)texts.size(); idx++) { + if (texts[idx].empty()) continue; + auto& texture = scene.textures.emplace_back( + make_text_texture(texts[idx])); + scene.materials.push_back( + make_text_material(style.text, (int)scene.textures.size() - 1)); + scene.shapes.push_back( + make_text_shape(texts[idx], offsets[idx], texture, style.textscale)); + auto& ninstance = scene.instances.emplace_back(); + ninstance.frame = make_text_frame(texts[idx], texture, + scene.cameras.front().frame.o, + transform_point(frame.frame, positions[idx]), style.textscale); + ninstance.material = (int)scene.materials.size() - 1; + ninstance.shape = (int)scene.shapes.size() - 1; + } + } +} + +// Convert diagram to scene +static scene_data convert_scene(const diagram_scene& diagram) { + auto scene = scene_data{}; + scene.cameras.push_back(make_default_camera()); + for (auto& object : diagram.objects) add_object(scene, object); + return scene; +} + +// crop image vertically +static image_t crop_image(const image_t& source) { + // find min and max + auto [width, height] = source.size(); + auto min = 0, max = height; + for (auto j : range(height)) { + auto empty = true; + for (auto i : range(width)) { + if (source[{i, j}] != vec4f{1, 1, 1, 1}) empty = false; + } + if (empty) { + min = j; + } else { + break; + } + } + for (auto j : range(height)) { + auto empty = true; + for (auto i : range(width)) { + if (source[{i, height - j - 1}] != vec4f{1, 1, 1, 1}) empty = false; + } + if (empty) { + max = height - j; + } else { + break; + } + } + + // no content + if (max < min) return source; + + // crop + auto cropped = image_t({width, max - min}); + for (auto j : range(min, max)) { + for (auto i : range(width)) { + cropped[{i, j - min}] = source[{i, j}]; + } + } + + // done + return cropped; +} + +// Image rendering +static image_t render_image(const scene_data& scene, int resolution, + int samples, bool noparallel = false); + +// Render a diagram to an image +image_t render_diagram(const diagram_data& diagram, int resolution, + int samples, bool boxes, bool crop) { + // final image + auto composite = image_t{{resolution, resolution}, {1, 1, 1, 1}}; + + // render scenes + for (auto& scene : diagram.scenes) { + // convert diagram to scene + auto yscene = convert_scene(scene); + + // image + auto render = render_image(yscene, resolution, samples, false); + + // helpers + auto draw_quad = [](image_t& composite, const vec2i& center, + const vec2i& size, const vec4f& color) { + auto extents = composite.size(); + for (auto i : range(-size.x / 2, +size.x / 2)) { + composite[clamp( + center + vec2i{i, -size.y / 2}, vec2i{0, 0}, extents - 1)] = color; + composite[clamp( + center + vec2i{i, +size.y / 2}, vec2i{0, 0}, extents - 1)] = color; + } + for (auto j : range(-size.y / 2, +size.y / 2)) { + composite[clamp( + center + vec2i{-size.x / 2, j}, vec2i{0, 0}, extents - 1)] = color; + composite[clamp( + center + vec2i{+size.x / 2, j}, vec2i{0, 0}, extents - 1)] = color; + } + }; + auto copy_quad = [](image_t& composite, const image_t& source, + const vec2i& icenter, const vec2i& scenter, + const vec2i& size) { + auto csize = composite.size(); + auto ssize = source.size(); + for (auto j : range(-size.y / 2, +size.y / 2)) { + for (auto i : range(-size.x / 2, +size.x / 2)) { + auto cij = vec2i{i, j} + icenter, sij = vec2i{i, j} + scenter; + if (cij.x < 0 || cij.y < 0 || cij.x >= csize.x || cij.y >= csize.y) + continue; + if (sij.x < 0 || sij.y < 0 || sij.x >= ssize.x || sij.y >= ssize.y) + continue; + composite[cij] = source[sij]; + } + } + }; + + // composite + auto size = (vec2i)(scene.size * resolution / 9.0f); + auto margin = (vec2i)(scene.margin * resolution / 9.0f); + auto icenter = render.size() / 2; + auto ccenter = composite.size() / 2 + + (vec2i)(scene.offset * resolution / 9.0f); + + copy_quad(composite, render, ccenter, icenter, size + margin); + if (boxes) { + draw_quad(composite, ccenter, size, {0, 1, 0, 1}); + draw_quad(composite, ccenter, size + margin, {0, 0, 1, 1}); + } + } + + // crop + if (crop) composite = crop_image(composite); + + // done + return composite; +} + +} // namespace yocto + +// ----------------------------------------------------------------------------- +// DIAGRAM RENDERER +// ----------------------------------------------------------------------------- +namespace yocto { + +// Simple parallel for used since our target platforms do not yet support +// parallel algorithms. `Func` takes the two integer indices. +template +inline void parallel_for_batch(vec2i num, Func&& func) { + auto futures = vector>{}; + auto nthreads = std::thread::hardware_concurrency(); + std::atomic next_idx(0); + std::atomic has_error(false); + for (auto thread_id = 0; thread_id < (int)nthreads; thread_id++) { + futures.emplace_back( + std::async(std::launch::async, [&func, &next_idx, &has_error, num]() { + try { + while (true) { + auto j = next_idx.fetch_add(1); + if (j >= num[1]) break; + if (has_error) break; + for (auto i = 0; i < num[0]; i++) func(vec2i{i, j}); + } + } catch (...) { + has_error = true; + throw; + } + })); + } + for (auto& f : futures) f.get(); +} + +// Convenience functions +[[maybe_unused]] static vec3f eval_position( + const scene_data& scene, const scene_intersection& intersection) { + return eval_position(scene, scene.instances[intersection.instance], + intersection.element, intersection.uv); +} +[[maybe_unused]] static vec3f eval_normal( + const scene_data& scene, const scene_intersection& intersection) { + return eval_normal(scene, scene.instances[intersection.instance], + intersection.element, intersection.uv); +} +[[maybe_unused]] static vec2f eval_texcoord( + const scene_data& scene, const scene_intersection& intersection) { + return eval_texcoord(scene, scene.instances[intersection.instance], + intersection.element, intersection.uv); +} +[[maybe_unused]] static material_point eval_material( + const scene_data& scene, const scene_intersection& intersection) { + return eval_material(scene, scene.instances[intersection.instance], + intersection.element, intersection.uv); +} + +// Generates a ray from a camera. +static ray3f eval_camera(const camera_data& camera, const vec2f& uv_) { + auto film = vec2f{camera.film, camera.film / camera.aspect}; + if (!camera.orthographic) { + // point on film + auto uv = vec2f{1 - uv_.x, uv_.y}; + auto q = vec3f{film.x * (uv.x - 0.5f), film.y * (uv.y - 0.5f), camera.lens}; + // point on the lens + auto e = vec3f{0, 0, 0}; + // ray direction through the lens center + auto d = normalize(e - q); + // done + return ray3f{ + transform_point(camera.frame, e), transform_direction(camera.frame, d)}; + } else { + // point on film + auto uv = vec2f{1 - uv_.x, uv_.y}; + auto scale = 1 / camera.lens; + auto q = vec3f{film.x * (uv.x - 0.5f) * scale, + film.y * (uv.y - 0.5f) * scale, camera.lens}; + // point on the lens + auto e = vec3f{-q.x, -q.y, 0}; + // correct ray direction to account for camera focusing + auto d = normalize(e - q); + // done + return ray3f{ + transform_point(camera.frame, e), transform_direction(camera.frame, d)}; + } +} + +// Diagram previewing. +static vec4f render_ray( + const scene_data& scene, const scene_bvh& bvh, const ray3f& ray_) { + // lights + auto light = normalize(vec3f{1, 1, 2}); + + // initialize + auto radiance = vec3f{0, 0, 0}; + auto weight = vec3f{1, 1, 1}; + auto ray = ray_; + + // trace path + for (auto bounce = 0; bounce < 16; bounce++) { + // intersect next point + auto intersection = intersect_scene_bvh(bvh, scene, ray); + if (!intersection.hit) { + radiance += weight * vec3f{1, 1, 1}; + break; + } + + // prepare shading point + auto outgoing = -ray.d; + auto position = eval_position(scene, intersection); + auto normal = eval_normal(scene, intersection); + auto material = eval_material(scene, intersection); + + // accumulate color + radiance += weight * material.color * material.opacity; + if (material.type == material_type::glossy) { + radiance += weight * pow(max(dot(normal, light), 0.0f), 10) * + material.opacity; + } + + // handle opacity + if (material.opacity >= 1) break; + weight *= 1 - material.opacity; + ray = {position + ray.d * 1e-2f, ray.d}; + } + + return {radiance.x, radiance.y, radiance.z, 1.0f}; +} + +// Progressively computes an image. +static image_t render_image( + const scene_data& scene, int resolution, int samples, bool noparallel) { + // Bvh + auto bvh = make_scene_bvh(scene, false, noparallel); + // Camera + auto& camera = scene.cameras[0]; + // Image + auto size = vec2i{resolution, (int)round(resolution / camera.aspect)}; + auto render = image_t{size, {0, 0, 0, 0}}; + // Start rendering + auto nsamples = (int)round(sqrt((float)samples)); + if (noparallel) { + for (auto ij : range(size)) { + for (auto sij : range(vec2i{nsamples, nsamples})) { + auto ray = eval_camera( + camera, ((vec2f)ij + ((vec2f)sij + 0.5f) / (float)nsamples) / + (vec2f)render.size()); + auto color = render_ray(scene, bvh, ray); + render[ij] += isfinite(color) ? color : vec4f{1, 1, 1, 1}; + } + render[ij] /= nsamples * nsamples; + } + } else { + parallel_for_batch(size, [&](vec2i ij) { + for (auto sij : range(vec2i{nsamples, nsamples})) { + auto ray = eval_camera( + camera, ((vec2f)ij + ((vec2f)sij + 0.5f) / (float)nsamples) / + (vec2f)render.size()); + auto color = render_ray(scene, bvh, ray); + render[ij] += isfinite(color) ? color : vec4f{1, 1, 1, 1}; + } + render[ij] /= nsamples * nsamples; + }); + } + + return render; +} + +// Helpers to clip lines +diagram_shape clip_lines(const diagram_shape& shape, const bbox3f& bbox) { + return clip_lines(identity3x4f, shape, bbox); +} + +diagram_shape clip_lines( + const frame3f& frame, const diagram_shape& shape, const bbox3f& bbox) { + auto inside = vector(shape.positions.size(), false); + for (auto index : range(shape.positions.size())) { + inside[index] = contains( + bbox, transform_point(frame, shape.positions[index])); + } + auto nshape = shape; + nshape.lines.clear(); + for (auto line : shape.lines) { + if (inside.at(line.x) && inside.at(line.y)) nshape.lines.push_back(line); + } + return nshape; +} + +} // namespace yocto diff --git a/libs/yocto/yocto_diagram.h b/libs/yocto/yocto_diagram.h new file mode 100644 index 000000000..a51bc4c10 --- /dev/null +++ b/libs/yocto/yocto_diagram.h @@ -0,0 +1,684 @@ +// +// # Yocto/Diagram: Diagram representation +// +// Yocto/Diagram defines diagram representations. +// Yocto/Diagram is implemented in `yocto_diagram.h` and `yocto_diagram.cpp`. +// + +// +// LICENSE: +// +// Copyright (c) 2016 -- 2023 Fabio Pellacini +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// + +#ifndef _YOCTO_DIAGRAM_H_ +#define _YOCTO_DIAGRAM_H_ + +// ----------------------------------------------------------------------------- +// INCLUDES +// ----------------------------------------------------------------------------- + +#include + +#include +#include +#include +#include + +// ----------------------------------------------------------------------------- +// USING DIRECTIVES +// ----------------------------------------------------------------------------- +namespace yocto { + +// using directives +using std::array; +using std::function; +using std::string; +using std::vector; + +} // namespace yocto + +// ----------------------------------------------------------------------------- +// DIAGRAM REPRESENTATION +// ----------------------------------------------------------------------------- +namespace yocto { + +// Diagram frame +struct diagram_frame { + frame3f frame = identity3x4f; + + diagram_frame() {} + diagram_frame(const diagram_frame&) = default; + diagram_frame(const frame3f& frame_) : frame{frame_} {} +}; + +// Diagram shape +struct diagram_shape { + // Element data + vector points = {}; + vector lines = {}; + vector arrows = {}; + vector triangles = {}; + vector quads = {}; + + // Vertex data + vector positions = {}; + vector normals = {}; + vector texcoords = {}; + vector colors = {}; + + // Options + float connect = 0; +}; + +// Diagram labels +struct diagram_labels { + // Labels data + vector labels = {}; + vector llabels = {}; + vector flabels = {}; + vector clabels = {}; + vector positions = {}; +}; + +// Diagram style +struct diagram_style { + // Style data + vec4f stroke = {0, 0, 0, 1}; + vec4f fill = {0.4, 1.0, 1.0, 1}; + vec4f text = {0, 0, 0, 1}; + image_t texture = {}; + bool highlight = false; + bool arrow = false; + bool nearest = false; + bool linear = false; + bool wireframe = true; + float thickness = 0.015f; + float textscale = 0.05f; + float connect = 0; +}; + +// Diagram object +struct diagram_object { + diagram_frame frame = {}; + diagram_shape shape = {}; + diagram_labels labels = {}; + diagram_style style = {}; +}; + +// Diagram scene +struct diagram_scene { + // scene rendering + vec2f size = {0, 0}; + vec2f offset = {0, 0}; + vec2f margin = {0.1, 0.3}; + + // scene transforms + frame3f frame = identity3x4f; + + // scene objects + vector objects = {}; +}; + +// Diagram data +struct diagram_data { + // Scene data + vector scenes = {}; +}; + +} // namespace yocto + +// ----------------------------------------------------------------------------- +// DIAGRAM INITIALIZATION AND RENDERING +// ----------------------------------------------------------------------------- +namespace yocto { + +// Init diagram +diagram_data make_diagram(); + +// Rendering a diagram +image_t render_diagram(const diagram_data& diagram, + int resolution = 1440, int samples = 64, bool boxes = false, + bool crop = true); + +} // namespace yocto + +// ----------------------------------------------------------------------------- +// DIAGRAM CREATION +// ----------------------------------------------------------------------------- +namespace yocto { + +// Useful constants +namespace dcolors { +inline const auto black = vec4f{0.0, 0.0, 0.0, 1.0}; +inline const auto gray = vec4f{0.5, 0.5, 0.5, 1.0}; +inline const auto white = vec4f{1.0, 1.0, 1.0, 1.0}; +inline const auto transparent = vec4f{1.0, 1.0, 1.0, 0.0}; +inline const auto stroke1 = vec4f{0.0, 0.7, 0.7, 1.0}; +inline const auto stroke2 = vec4f{0.0, 0.7, 0.0, 1.0}; +inline const auto stroke3 = vec4f{0.7, 0.3, 0.0, 1.0}; +inline const auto stroke4 = vec4f{0.7, 0.3, 0.7, 1.0}; +inline const auto stroke5 = vec4f{0.7, 0.7, 0.0, 1.0}; +inline const auto fill1 = vec4f{0.4, 1.0, 1.0, 1.0}; +inline const auto fill2 = vec4f{0.4, 1.0, 0.4, 1.0}; +inline const auto fill3 = vec4f{1.0, 0.6, 0.2, 1.0}; +inline const auto fill4 = vec4f{1.0, 0.6, 1.0, 1.0}; +inline const auto fill5 = vec4f{1.0, 1.0, 0.4, 1.0}; +inline const auto tfill1 = vec4f{0.0, 1.0, 1.0, 0.4}; +inline const auto tfill2 = vec4f{0.0, 1.0, 0.0, 0.4}; +inline const auto tfill3 = vec4f{1.0, 0.3, 0.0, 0.4}; +inline const auto tfill4 = vec4f{1.0, 0.3, 1.0, 0.4}; +inline const auto tfill5 = vec4f{1.0, 1.0, 0.0, 0.4}; +inline const auto etfill1 = vec4f{0.0, 1.0, 1.0, 0.1}; +inline const auto etfill2 = vec4f{0.0, 1.0, 0.0, 0.1}; +inline const auto etfill3 = vec4f{1.0, 0.3, 0.0, 0.1}; +inline const auto etfill4 = vec4f{1.0, 0.3, 1.0, 0.1}; +inline const auto etfill5 = vec4f{1.0, 1.0, 0.0, 0.1}; +}; // namespace dcolors + +// Thickness +namespace dthickness { +inline const auto default_ = 0.015f; +inline const auto regular = 0.0144f; +inline const auto medium = 0.0156f; +inline const auto thin = 0.010f; +inline const auto thick = 0.020f; +inline const auto ethin = 0.005f; +inline const auto extra_thin = 0.005f; +inline const auto extra_thick = 0.035f; +}; // namespace dthickness + +// Points and frames +namespace dconstants { +inline const auto xup3x4f = frame3f{{0, 0, 1}, {0, 1, 0}, {1, 0, 0}, {0, 0, 0}}; +inline const auto yup3x4f = frame3f{{1, 0, 0}, {0, 0, 1}, {0, 1, 0}, {0, 0, 0}}; +inline const auto zup3x4f = frame3f{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}, {0, 0, 0}}; +inline const auto quad_quads = vector{{0, 1, 2, 3}}; +inline const auto quad_positions = vector{ + {-1, -1, 0}, {+1, -1, 0}, {+1, +1, 0}, {-1, +1, 0}}; +inline const auto quad_texcoords = vector{ + {0, 1}, {1, 1}, {1, 0}, {0, 0}}; +inline const auto triangle_triangles = vector{{0, 1, 2}}; +inline const auto triangle_positions = vector{ + {-1, -1, 0}, {+1, -1, 0}, {0, +1, 0}}; +inline const auto triangle_texcoords = vector{{0, 0}, {1, 0}, {0.5, 1}}; +inline const auto cube_quads = vector{{0, 3, 2, 1}, {4, 5, 6, 7}, + {1, 2, 6, 5}, {0, 4, 7, 3}, {0, 1, 5, 4}, {2, 3, 7, 6}}; +inline const auto cube_positions = vector{{-1, -1, +1}, {+1, -1, +1}, + {+1, -1, -1}, {-1, -1, -1}, {-1, +1, +1}, {+1, +1, +1}, {+1, +1, -1}, + {-1, +1, -1}}; +inline const auto tetra_triangles = vector{ + {0, 2, 1}, {0, 1, 3}, {0, 3, 2}, {1, 3, 2}}; +inline const auto tetra_positions = vector{{sqrt(2.0f), -1, 0}, + {-sqrt(1.0f / 2.0f), -1, -sqrt(3.0f / 2.0f)}, + {-sqrt(1.0f / 2.0f), -1, +sqrt(3.0f / 2.0f)}, {0, +1, 0}}; +inline const auto fourtriangles_triangles = vector{ + {0, 1, 3}, {3, 1, 4}, {4, 1, 2}, {4, 2, 5}}; +inline const auto fourtriangles_positions = vector{{-2.5, -1, 0}, + {-0.5, -1, 0}, {+1.5, -1, 0}, {-1.5, +1, 0}, {+0.5, +1, 0}, {+2.5, +1, 0}}; +inline const auto fourtriangles2_triangles = vector{ + {0, 1, 2}, {2, 1, 3}, {2, 3, 4}, {4, 3, 5}}; +inline const auto fourtriangles2_positions = vector{ + {-2, -2, 0}, {0, -2, 0}, {-1, 0, 0}, {+1, 0, 0}, {0, +2, 0}, {+2, +2, 0}}; +inline const auto slabs_quads = vector{{0, 1, 2, 3}, {4, 5, 6, 7}, + {8, 9, 10, 11}, {12, 13, 14, 15}, {16, 17, 18, 19}, {20, 21, 22, 23}}; +inline const auto slabs_positions = vector{{-2, -2, +1}, {+2, -2, +1}, + {+2, +2, +1}, {-2, +2, +1}, {+2, -2, -1}, {-2, -2, -1}, {-2, +2, -1}, + {+2, +2, -1}, {+1, -2, +2}, {+1, -2, -2}, {+1, +2, -2}, {+1, +2, +2}, + {-1, -2, -2}, {-1, -2, +2}, {-1, +2, +2}, {-1, +2, -2}, {-2, +1, +2}, + {+2, +1, +2}, {+2, +1, -2}, {-2, +1, -2}, {+2, -1, +2}, {-2, -1, +2}, + {-2, -1, -2}, {+2, -1, -2}}; +inline const auto slabs_texcoords = vector{{0, 1}, {1, 1}, {1, 0}, + {0, 0}, {0, 1}, {1, 1}, {1, 0}, {0, 0}, {0, 1}, {1, 1}, {1, 0}, {0, 0}, + {0, 1}, {1, 1}, {1, 0}, {0, 0}, {0, 1}, {1, 1}, {1, 0}, {0, 0}, {0, 1}, + {1, 1}, {1, 0}, {0, 0}}; +inline const auto slab_quads = vector{{0, 1, 2, 3}, {4, 5, 6, 7}}; +inline const auto slab_positions = vector{{-1, -1, +1}, {+1, -1, +1}, + {+1, +1, +1}, {-1, +1, +1}, {+1, -1, -1}, {-1, -1, -1}, {-1, +1, -1}, + {+1, +1, -1}}; +inline const auto slab_texcoords = vector{ + {0, 1}, {1, 1}, {1, 0}, {0, 0}, {0, 1}, {1, 1}, {1, 0}, {0, 0}}; +inline const auto fvcube_quads = vector{{0, 1, 2, 3}, {4, 5, 6, 7}, + {1, 4, 7, 2}, {5, 0, 3, 6}, {3, 2, 7, 6}, {1, 0, 5, 4}}; +inline const auto fvcube_positions = vector{{-1, -1, +1}, {+1, -1, +1}, + {+1, +1, +1}, {-1, +1, +1}, {+1, -1, -1}, {-1, -1, -1}, {-1, +1, -1}, + {+1, +1, -1}}; +inline const auto fvcube_normals = vector{{0, 0, +1}, {0, 0, +1}, + {0, 0, +1}, {0, 0, +1}, {0, 0, -1}, {0, 0, -1}, {0, 0, -1}, {0, 0, -1}, + {+1, 0, 0}, {+1, 0, 0}, {+1, 0, 0}, {+1, 0, 0}, {-1, 0, 0}, {-1, 0, 0}, + {-1, 0, 0}, {-1, 0, 0}, {0, +1, 0}, {0, +1, 0}, {0, +1, 0}, {0, +1, 0}, + {0, -1, 0}, {0, -1, 0}, {0, -1, 0}, {0, -1, 0}}; +inline const auto fvcube_texcoords = vector{{0, 1}, {1, 1}, {1, 0}, + {0, 0}, {0, 1}, {1, 1}, {1, 0}, {0, 0}, {0, 1}, {1, 1}, {1, 0}, {0, 0}, + {0, 1}, {1, 1}, {1, 0}, {0, 0}, {0, 1}, {1, 1}, {1, 0}, {0, 0}, {0, 1}, + {1, 1}, {1, 0}, {0, 0}}; +inline const auto fvcube_quadspos = vector{{0, 1, 2, 3}, {4, 5, 6, 7}, + {1, 4, 7, 2}, {5, 0, 3, 6}, {3, 2, 7, 6}, {1, 0, 5, 4}}; +inline const auto fvcube_quadsnorm = vector{{0, 1, 2, 3}, {4, 5, 6, 7}, + {8, 9, 10, 11}, {12, 13, 14, 15}, {16, 17, 18, 19}, {20, 21, 22, 23}}; +inline const auto fvcube_quadstexcoord = vector{{0, 1, 2, 3}, + {4, 5, 6, 7}, {8, 9, 10, 11}, {12, 13, 14, 15}, {16, 17, 18, 19}, + {20, 21, 22, 23}}; +}; // namespace dconstants + +// Easy checking in lists +inline bool contains(const string& tag, const vector& tags) { + for (auto tag_ : tags) { + if (tag_ == tag) return true; + } + return false; +} + +// Frames +inline frame3f dtranslation(const vec3f& translation) { + return translation_frame(translation); +} +inline frame3f drotation(const vec3f& rotation) { + return rotation_frame(vec3f{1, 0, 0}, radians(rotation.x)) * + rotation_frame(vec3f{0, 1, 0}, radians(rotation.y)) * + rotation_frame(vec3f{0, 0, 1}, radians(rotation.z)); +} +inline frame3f drotation(const vec3f& axis, float angle) { + return rotation_frame(axis, radians(angle)); +} +inline frame3f dscaling(const vec3f& scaling) { return scaling_frame(scaling); } +inline frame3f dscaling(float scale) { + return scaling_frame(vec3f{scale, scale, scale}); +} +inline frame3f dscale(float scale) { + return scaling_frame(vec3f{scale, scale, scale}); +} +inline frame3f dframez(const vec3f& z, const vec3f& pos = {0, 0, 0}) { + return frame_fromz(pos, z); +} +inline frame3f dlookat(const vec3f& from, const vec3f& to = {0, 0, 0}, + const vec3f& up = {0, 1, 0}) { + return lookat_frame(from, to, up, true); +} +inline frame3f dtransform(const vec3f& translation, const vec3f& rotation) { + return dtranslation(translation) * drotation(rotation); +} +inline frame3f dgtransform( + const vec3f& translation, const vec3f& rotation, const vec3f& scaling) { + return dtranslation(translation) * drotation(rotation) * dscaling(scaling); +} +inline frame3f dreflectx() { + return frame3f{{-1, 0, 0}, {0, 1, 0}, {0, 0, 1}, {0, 0, 0}}; +} +inline frame3f dcentering(const vec2f& center) { + return dtranslation({-center.x, -center.y, 0}); +} + +inline vector transform_points( + const frame3f& frame, const vector& points) { + auto tpoints = vector{}; + tpoints.reserve(points.size()); + for (auto& point : points) tpoints.push_back(transform_point(frame, point)); + return tpoints; +} +inline vector transform_normals( + const frame3f& frame, const vector& normals) { + auto tnormals = vector{}; + tnormals.reserve(normals.size()); + for (auto& normal : normals) + tnormals.push_back(transform_normal(frame, normal, true)); + return tnormals; +} +inline diagram_shape transform_shape( + const frame3f& frame, const diagram_shape& shape) { + auto tshape = shape; + for (auto& position : tshape.positions) + position = transform_point(frame, position); + return tshape; +} + +// Styles +inline diagram_style dstroke(const vec4f& stroke = dcolors::black, + float thickness = dthickness::default_) { + return { + .stroke = stroke, .fill = dcolors::transparent, .thickness = thickness}; +} +inline diagram_style dfill(const vec4f& fill = dcolors::fill1) { + return {.stroke = dcolors::transparent, .fill = fill}; +} +inline diagram_style dfilled(const vec4f& fill = dcolors::fill1, + const vec4f& stroke = dcolors::black, + float thickness = dthickness::default_) { + return {.stroke = stroke, .fill = fill, .thickness = thickness}; +} +inline diagram_style dtextured(const image_t& texture, + const vec4f& stroke = dcolors::black, + float thickness = dthickness::default_) { + return {.stroke = stroke, + .fill = {1, 1, 1, 1}, + .thickness = thickness, + .texture = texture}; +} +inline diagram_style dtextured(const image_t& texture, bool interpolate, + const vec4f& stroke = dcolors::black, + float thickness = dthickness::default_) { + return {.stroke = stroke, + .fill = {1, 1, 1, 1}, + .thickness = thickness, + .texture = texture, + .nearest = !interpolate}; +} +inline diagram_style dimtextured(const image_t& texture, + const vec4f& stroke = dcolors::transparent, + float thickness = dthickness::default_) { + return { + .stroke = stroke, + .fill = {1, 1, 1, 1}, + .thickness = thickness, + .texture = texture, + .nearest = true, + }; +} +inline diagram_style dimtextured(const image_t& texture, + bool interpolate, const vec4f& stroke = dcolors::transparent, + float thickness = dthickness::default_) { + return {.stroke = stroke, + .fill = {1, 1, 1, 1}, + .thickness = thickness, + .texture = texture, + .nearest = !interpolate}; +} +inline diagram_style dtextcolor(const vec4f textcolor = dcolors::black, + const vec4f& fill = dcolors::fill1, const vec4f& stroke = dcolors::black, + float thickness = dthickness::default_) { + return {.stroke = stroke, + .fill = fill, + .thickness = thickness, + .text = textcolor}; +} + +} // namespace yocto + +// ----------------------------------------------------------------------------- +// DIAGRAM CREATION +// ----------------------------------------------------------------------------- +namespace yocto { + +// Add scene +diagram_scene& add_scene(diagram_data& diagram, const string& title, + const frame3f& frame = identity3x4f, const vec2f& margin = {0.2, 1.0}); +diagram_scene& add_scene(diagram_data& diagram, const string& title, + const vec2f& size, const frame3f& frame = identity3x4f, + const vec2f& margin = {0.2, 1.0}); +diagram_scene& add_scenews(diagram_data& diagram, const string& title, + const string& subtitle, const vec2f& size = {2, 2}, + const frame3f& frame = identity3x4f, const vec2f& margin = {0.2, 1.0}); + +// Labels +diagram_labels dlabels(const vector& labels); +diagram_labels dlabels( + const vector& positions, const vector& labels); +diagram_labels dllabels(const vector& labels); +diagram_labels dflabels(const vector& labels); +diagram_labels dclabels(const vector& labels); +diagram_labels dglabels(const vector& positions, + const vector& labels, const vector& llabels, + const vector& flabels, const vector& clabels); +diagram_labels dglabels(const vector& labels, + const vector& llabels, const vector& flabels, + const vector& clabels); + +// Add labels +void add_labels(diagram_scene& diagram, const diagram_labels& labels, + const diagram_style& style = dstroke()); +void add_labels(diagram_scene& diagram, const diagram_frame& frame, + const diagram_labels& labels, const diagram_style& style = dstroke()); + +// Add shapes +void add_shape(diagram_scene& diagram, const diagram_shape& shape, + const diagram_style& style = dfilled()); +void add_shape(diagram_scene& diagram, const diagram_shape& shape, + const diagram_labels& labels, const diagram_style& style = dfilled()); +void add_shape(diagram_scene& diagram, const diagram_frame& frame, + const diagram_shape& shape, const diagram_style& style = dfilled()); +void add_shape(diagram_scene& diagram, const diagram_frame& frame, + const diagram_shape& shape, const diagram_labels& labels, + const diagram_style& style = dfilled()); + +// Points +diagram_shape dpoints(const vector& positions = {{0, 0, 0}}); +diagram_shape dpoints( + const vector& positions, const vector& points); + +// Lines +diagram_shape dlines(const vector& positions); +diagram_shape dlines( + const vector& lines, const vector& positions); +diagram_shape dclines(const vector& positions); +diagram_shape dclines( + const vector& lines, const vector& positions); + +// Arrows +diagram_shape darrows(const vector& positions); +diagram_shape darrows( + const vector& arrows, const vector& positions); +diagram_shape dcarrows(const vector& positions); +diagram_shape dcarrows( + const vector& arrows, const vector& positions); + +// Vectors +diagram_shape dvectors(const vector& positions); +diagram_shape dcvectors(const vector& positions); + +// Axes +diagram_shape daxes( + const frame3f& axes = identity3x4f, const vec3f& aspect = {1, 1, 1}); +diagram_shape daxes2( + const frame3f& axes = identity3x4f, const vec3f& aspect = {1, 1, 1}); + +// Rays +diagram_shape drays(const vector& rays = {{{0, 0, 0}, {0, 0, 1}}}); +diagram_shape drayscont( + const vector& rays = {{{0, 0, 0}, {0, 0, 1}}}, float length = 5); + +// Quads +diagram_shape dquads(const vector& positions = {{-1, -1, 0}, {+1, -1, 0}, + {+1, +1, 0}, {-1, +1, 0}}, + const vector& texcoords = {}); +diagram_shape dquads(const vector& quads, const vector& positions, + const vector& texcoords = {}); + +// Triangles +diagram_shape dtriangles( + const vector& positions = {{-1, -1, 0}, {+1, -1, 0}, {0, +1, 0}}, + const vector& texcoords = {}); +diagram_shape dtriangles(const vector& triangles, + const vector& positions, const vector& texcoords = {}); + +// Shape +diagram_shape dshape(const shape_data& shape, bool wireframe = true); + +// Polyline +diagram_shape dpolyline(const vector& positions); + +// Polygon +diagram_shape dpolygon(const vector& positions); // TODO: remove + +// Rect +diagram_shape drect(const vec2f& aspect = {1, 1}); // TODO: remove + +// Disk +diagram_shape ddisk(int steps = 64); + +// Half disk +diagram_shape dhalfdisk(int steps = 32); + +// Arc +diagram_shape darc(const vec3f& from, const vec3f& to, + const vec3f& center = {0, 0, 0}, int steps = 16); + +// Qbezier +diagram_shape dqbeziers(const vector& positions, int steps = 32); + +// Bezier +diagram_shape dbeziers(const vector& positions, int steps = 32); + +// Cube +diagram_shape dcube(); + +// Sphere +diagram_shape dsphere(int steps = 32, bool isolines = true); +diagram_shape duvsphere(int steps = 32, bool isolines = true); + +// Bbox +diagram_shape dbbox( + const bbox3f& bbox = {{-1, -1, -1}, {+1, +1, +1}}, float epsilon = 0.01); + +// Hemisphere +diagram_shape dhemisphere(int steps = 32, bool isolines = true); + +// Cone +diagram_shape dcone(int steps = 32, bool isolines = true); + +// Dot grid +diagram_shape ddotgrid(const vec2i& steps = {4, 4}); + +// Grid +diagram_shape dgrid(const vec2i& steps = {4, 4}); + +// Disk grid +diagram_shape ddiskgrid(const vec2i& steps = {4, 4}, int dstep = 32); +diagram_shape dudiskgrid(const vec2i& steps = {4, 4}, int dstep = 32); + +// Affine grid +diagram_shape daffinegrid(const vec3f& axes_a = {1, 0, 0}, + const vec3f& axes_b = {0, 1, 0}, const vec2i& steps = {4, 4}); + +// Image +diagram_shape dimagerect(const vec2i& size); +diagram_shape dimagerect(const image_t& image); +diagram_shape dimagegrid(const vec2i& size); +diagram_shape dimagegrid(const image_t& image); +diagram_shape dimagelabel(const vec2i& size, float scale = 1); +diagram_shape dimagelabel(const image_t& image, float scale = 1); + +// Random points +enum struct drandompoints_type { + // clang-format off + quad, disk, disknu, triangle, hemi, hemicos, hemicospower, sphere, + linex, liney + // clang-format on +}; +diagram_shape drandompoints(int num = 16, bool stratified = true); +diagram_shape drandompoints( + drandompoints_type type, int num = 16, bool stratified = true); + +// Random points +enum struct drandompoints3_type { + // clang-format off + cube, linex, liney, linez + // clang-format on +}; +diagram_shape drandompoints3(int num = 16, bool stratified = true); +diagram_shape drandompoints3( + drandompoints3_type type, int num = 16, bool stratified = true); + +// Random lines +enum struct drandomlines_type { hemi, hemicos, hemicospower, beam }; +diagram_shape drandomlines(int num = 16, bool stratified = true); +diagram_shape drandomlines( + drandomlines_type type, int num = 16, bool stratified = true); + +// Add plot +diagram_scene& add_plot(diagram_data& diagram, const string& title, + const vec2f& size = {4, 2}, const frame3f& frame = identity3x4f, + const vec2f& margin = {0.8, 1.0}); + +// Add plot axes +diagram_scene& add_plotaxes(diagram_scene& diagram, + const bbox2f& bounds = {{0, 0}, {1, 1}}, + const vector>& xticks = {}, + const vector>& yticks = {}, + const diagram_style& style = dstroke(), + const diagram_style& lstyle = dstroke(dcolors::transparent)); + +// Add plot shape +void add_plotshape(diagram_scene& diagram, const diagram_shape& shape, + const diagram_style& style = dstroke(dcolors::black)); +void add_plotshape(diagram_scene& diagram, const diagram_shape& shape, + const diagram_labels& labels, + const diagram_style& style = dstroke(dcolors::black)); + +// Plot function +diagram_shape dplotline(const vector& points); +diagram_shape dplotpoints(const vector& points); +diagram_shape dplotarrows(const vector& points); +vector dplotfunc(const function& func, + const vec2f& range = {0, 1}, int samples = 100); +vector dplotpfunc(const function& func, int samples = 100); +vector dplotcurve(const vector& curve, bool center = true); +vector dplotcurve(const vector& curve, const vec2f& range); +inline diagram_shape dplotline(const function& func, + const vec2f& range = {0, 1}, int samples = 100) { + return dplotline(dplotfunc(func, range, samples)); +} +inline diagram_shape dplotpoints(const function& func, + const vec2f& range = {0, 1}, int samples = 100) { + return dplotpoints(dplotfunc(func, range, samples)); +} +inline diagram_shape dplotline(const vector& curve, bool center = true) { + return dplotline(dplotcurve(curve, center)); +} +inline diagram_shape dplotpoints( + const vector& curve, bool center = true) { + return dplotpoints(dplotcurve(curve, center)); +} + +// Add plot3 +diagram_scene& add_plot3(diagram_data& diagram, const vec2f& size = {4, 3}, + const frame3f& frame = drotation({25, 0, 0}) * drotation({0, 45, 0}) * + drotation({270, 0, 0}) * drotation({0, 0, 180}), + const vec2f& margin = {0.8, 1.0}); +diagram_scene& add_plot3(diagram_data& diagram, const string& title, + const vec2f& size = {4, 3}, + const frame3f& frame = drotation({25, 0, 0}) * drotation({0, 45, 0}) * + drotation({270, 0, 0}) * drotation({0, 0, 180}), + const vec2f& margin = {0.8, 1.0}); + +// Add plot axes +diagram_scene& add_plotaxes3(diagram_scene& diagram, + const bbox3f& bounds = {{0, 0, 0}, {1, 1, 1}}, + const vec3f& size = {1, 1, 1}, + const vector>& xticks = {}, + const vector>& yticks = {}, + const vector>& zticks = {}, + const diagram_style& style = dstroke(), + const diagram_style& lstyle = dstroke(dcolors::transparent)); + +// Plot surface +diagram_shape dplotsurface(const function& func, + const vec2f& xrange = {0, 1}, const vec2f& yrange = {0, 1}, + const vec2i& steps = {32, 32}, bool wireframe = true); + +// Helpers to clip lines +diagram_shape clip_lines(const diagram_shape& shape, const bbox3f& bbox); +diagram_shape clip_lines( + const frame3f& frame, const diagram_shape& shape, const bbox3f& bbox); + +} // namespace yocto + +#endif diff --git a/libs/yocto/yocto_gui.cpp b/libs/yocto/yocto_gui.cpp index 1b022cab4..64af00e3e 100644 --- a/libs/yocto/yocto_gui.cpp +++ b/libs/yocto/yocto_gui.cpp @@ -177,13 +177,12 @@ static void draw_scene(glscene_state& glscene, const scene_data& scene, // ----------------------------------------------------------------------------- namespace yocto { -void update_image_params( - const gui_input& input, const image_data& image, glimage_params& glparams) { +void update_image_params(const gui_input& input, const image_t& image, + glimage_params& glparams) { glparams.window = input.window; glparams.framebuffer = input.framebuffer; std::tie(glparams.center, glparams.scale) = camera_imview(glparams.center, - glparams.scale, {image.width, image.height}, glparams.window, - glparams.fit); + glparams.scale, image.size(), glparams.window, glparams.fit); } bool uiupdate_image_params(const gui_input& input, glimage_params& glparams) { @@ -235,21 +234,21 @@ bool draw_tonemap_widgets( return (bool)edited; } -bool draw_image_widgets(const gui_input& input, const image_data& image, - const image_data& display, glimage_params& glparams) { +bool draw_image_widgets(const gui_input& input, const image_t& image, + const image_t& display, glimage_params& glparams) { if (draw_gui_header("inspect")) { draw_gui_slider("zoom", glparams.scale, 0.1f, 10); draw_gui_checkbox("fit", glparams.fit); draw_gui_coloredit("background", glparams.background); - auto [i, j] = image_coords(input.cursor, glparams.center, glparams.scale, - vec2i{image.width, image.height}); - auto ij = vec2i{i, j}; + auto ij = image_coords( + input.cursor, glparams.center, glparams.scale, image.size()); draw_gui_dragger("mouse", ij); auto image_pixel = vec4f{0, 0, 0, 0}; auto display_pixel = vec4f{0, 0, 0, 0}; - if (i >= 0 && i < image.width && j >= 0 && j < image.height) { - image_pixel = image.pixels[j * image.width + i]; - display_pixel = image.pixels[j * image.width + i]; + if (ij.x >= 0 && ij.x < image.size().x && ij.y >= 0 && + ij.y < image.size().y) { + image_pixel = image[ij]; + display_pixel = image[ij]; } draw_gui_coloredit("image", image_pixel); draw_gui_coloredit("display", display_pixel); @@ -258,20 +257,20 @@ bool draw_image_widgets(const gui_input& input, const image_data& image, return false; } -bool draw_image_widgets( - const gui_input& input, const image_data& image, glimage_params& glparams) { +bool draw_image_widgets(const gui_input& input, const image_t& image, + glimage_params& glparams) { if (draw_gui_header("inspect")) { draw_gui_slider("zoom", glparams.scale, 0.1f, 10); draw_gui_checkbox("fit", glparams.fit); draw_gui_coloredit("background", glparams.background); - auto [i, j] = image_coords(input.cursor, glparams.center, glparams.scale, - vec2i{image.width, image.height}); - auto ij = vec2i{i, j}; + auto ij = image_coords( + input.cursor, glparams.center, glparams.scale, image.size()); draw_gui_dragger("mouse", ij); auto image_pixel = vec4f{0, 0, 0, 0}; auto display_pixel = vec4f{0, 0, 0, 0}; - if (i >= 0 && i < image.width && j >= 0 && j < image.height) { - image_pixel = image.pixels[j * image.width + i]; + if (ij.x >= 0 && ij.x < image.size().x && ij.y >= 0 && + ij.y < image.size().y) { + image_pixel = image[ij]; display_pixel = tonemap( image_pixel, glparams.exposure, glparams.filmic, glparams.srgb); } @@ -389,9 +388,12 @@ bool draw_scene_widgets(scene_data& scene, scene_selection& selection, if (draw_gui_header("textures")) { draw_gui_combobox("texture", selection.texture, scene.texture_names); auto& texture = scene.textures.at(selection.texture); - draw_gui_label("width", texture.width); - draw_gui_label("height", texture.height); - draw_gui_label("linear", texture.linear); + draw_gui_label( + "width", max(texture.pixelsb.size().x, texture.pixelsf.size().x)); + draw_gui_label( + "height", max(texture.pixelsb.size().y, texture.pixelsf.size().y)); + draw_gui_label("clamp", texture.clamp); + draw_gui_label("nearest", texture.nearest); draw_gui_label("byte", !texture.pixelsb.empty()); end_gui_header(); } @@ -417,13 +419,13 @@ bool draw_scene_widgets(scene_data& scene, scene_selection& selection, namespace yocto { // Open a window and show an image -void show_image_gui( - const string& title, const string& name, const image_data& image) { +void show_image_gui(const string& title, const string& name, + const image_t& image, bool linear) { // display image - auto display = make_image(image.width, image.height, false); + auto display = image; float exposure = 0; bool filmic = false; - tonemap_image_mt(display, image, exposure, filmic); + if (linear) tonemap_image(display, image, exposure, filmic); // opengl image auto glimage = glimage_state{}; @@ -447,7 +449,7 @@ void show_image_gui( callbacks.widgets = [&](const gui_input& input) { draw_gui_combobox("name", selected, names); if (draw_tonemap_widgets(input, exposure, filmic)) { - tonemap_image_mt(display, image, exposure, filmic); + if (linear) tonemap_image(display, image, exposure, filmic); set_image(glimage, display); } draw_image_widgets(input, image, display, glparams); @@ -462,14 +464,15 @@ void show_image_gui( // Open a window and show an image void show_image_gui(const string& title, const vector& names, - const vector& images) { + const vector>& images, const vector& linears) { // display image - auto displays = vector(images.size()); + auto displays = vector>(images.size()); auto exposures = vector(images.size(), 0); auto filmics = vector(images.size(), false); for (auto idx : range((int)images.size())) { - displays[idx] = make_image(images[idx].width, images[idx].height, false); - tonemap_image_mt(displays[idx], images[idx], exposures[idx], filmics[idx]); + displays[idx] = images[idx]; + if (linears[idx]) + tonemap_image(displays[idx], images[idx], exposures[idx], filmics[idx]); } // opengl image @@ -501,8 +504,9 @@ void show_image_gui(const string& title, const vector& names, auto filmic = (bool)filmics[selected]; // vector of bool ... if (draw_tonemap_widgets(input, exposures[selected], filmic)) { filmics[selected] = filmic; - tonemap_image_mt(displays[selected], images[selected], - exposures[selected], filmics[selected]); + if (linears[selected]) + tonemap_image(displays[selected], images[selected], exposures[selected], + filmics[selected]); set_image(glimages[selected], displays[selected]); } draw_image_widgets( @@ -517,14 +521,14 @@ void show_image_gui(const string& title, const vector& names, } // Open a window and show an image -void show_colorgrade_gui( - const string& title, const string& name, const image_data& image) { +void show_colorgrade_gui(const string& title, const string& name, + const image_t& image, bool linear) { // color grading parameters auto params = colorgrade_params{}; // display image - auto display = make_image(image.width, image.height, false); - colorgrade_image_mt(display, image, params); + auto display = image; + colorgrade_image(display, image, linear, params); // opengl image auto glimage = glimage_state{}; @@ -567,7 +571,7 @@ void show_colorgrade_gui( edited += draw_gui_coloredit("highlights color", params.highlights_color); end_gui_header(); if (edited) { - colorgrade_image_mt(display, image, params); + colorgrade_image(display, image, linear, params); set_image(glimage, display); } } @@ -603,7 +607,7 @@ void show_trace_gui(const string& title, const string& name, scene_data& scene, // init state auto state = make_trace_state(scene, params); - auto image = make_image(state.width, state.height, true); + auto image = image_t{state.render.size()}; // opengl image auto glimage = glimage_state{}; @@ -635,11 +639,9 @@ void show_trace_gui(const string& title, const string& name, scene_data& scene, auto pstate = make_trace_state(scene, pparams); trace_samples(pstate, scene, bvh, lights, pparams); auto preview = get_image(pstate); - for (auto idx = 0; idx < state.width * state.height; idx++) { - auto i = idx % image.width, j = idx / image.width; - auto pi = clamp(i / params.pratio, 0, preview.width - 1), - pj = clamp(j / params.pratio, 0, preview.height - 1); - image.pixels[idx] = preview.pixels[pj * preview.width + pi]; + for (auto idx : range(state.render.size())) { + auto pij = clamp(idx / params.pratio, {0, 0}, preview.size() - 1); + image[idx] = preview[pij]; } return true; }; @@ -649,8 +651,7 @@ void show_trace_gui(const string& title, const string& name, scene_data& scene, // make sure we can start trace_cancel(context); state = make_trace_state(scene, params); - if (image.width != state.width || image.height != state.height) - image = make_image(state.width, state.height, true); + if (image.size() != state.render.size()) image = state.render.size(); }; // start rendering batch @@ -1262,22 +1263,22 @@ void clear_image(glimage_state& glimage) { glimage = {}; } -void set_image(glimage_state& glimage, const image_data& image) { - if (!glimage.texture || glimage.width != image.width || - glimage.height != image.height) { +void set_image(glimage_state& glimage, const image_t& image) { + if (!glimage.texture || glimage.width != image.size().x || + glimage.height != image.size().y) { if (!glimage.texture) glGenTextures(1, &glimage.texture); glBindTexture(GL_TEXTURE_2D, glimage.texture); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, image.width, image.height, 0, - GL_RGBA, GL_FLOAT, image.pixels.data()); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, image.size().x, image.size().y, + 0, GL_RGBA, GL_FLOAT, image.data()); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); } else { glBindTexture(GL_TEXTURE_2D, glimage.texture); - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, image.width, image.height, GL_RGBA, - GL_FLOAT, image.pixels.data()); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, image.size().x, image.size().y, + GL_RGBA, GL_FLOAT, image.data()); } - glimage.width = image.width; - glimage.height = image.height; + glimage.width = image.size().x; + glimage.height = image.size().y; } // draw image @@ -1602,16 +1603,21 @@ void main() { // Create texture static void set_texture( glscene_texture& gltexture, const texture_data& texture) { - if (!gltexture.texture || gltexture.width != texture.width || - gltexture.height != texture.height) { + if (!gltexture.texture || + gltexture.width != + max(texture.pixelsb.size().x, texture.pixelsf.size().x) || + gltexture.height != + max(texture.pixelsb.size().y, texture.pixelsf.size().y)) { if (!gltexture.texture) glGenTextures(1, &gltexture.texture); glBindTexture(GL_TEXTURE_2D, gltexture.texture); if (!texture.pixelsb.empty()) { - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texture.width, texture.height, 0, - GL_RGBA, GL_UNSIGNED_BYTE, texture.pixelsb.data()); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texture.pixelsb.size().x, + texture.pixelsb.size().y, 0, GL_RGBA, GL_UNSIGNED_BYTE, + texture.pixelsb.data()); } else { - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texture.width, texture.height, 0, - GL_RGBA, GL_FLOAT, texture.pixelsf.data()); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texture.pixelsf.size().x, + texture.pixelsf.size().y, 0, GL_RGBA, GL_FLOAT, + texture.pixelsf.data()); } glGenerateMipmap(GL_TEXTURE_2D); glTexParameteri( @@ -1620,11 +1626,12 @@ static void set_texture( } else { glBindTexture(GL_TEXTURE_2D, gltexture.texture); if (!texture.pixelsb.empty()) { - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, texture.width, texture.height, - GL_RGBA, GL_UNSIGNED_BYTE, texture.pixelsb.data()); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, texture.pixelsb.size().x, + texture.pixelsb.size().y, GL_RGBA, GL_UNSIGNED_BYTE, + texture.pixelsb.data()); } else { - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, texture.width, texture.height, - GL_RGBA, GL_FLOAT, texture.pixelsf.data()); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, texture.pixelsf.size().x, + texture.pixelsf.size().y, GL_RGBA, GL_FLOAT, texture.pixelsf.data()); } glGenerateMipmap(GL_TEXTURE_2D); } diff --git a/libs/yocto/yocto_gui.h b/libs/yocto/yocto_gui.h index b7a0ef04a..cdd0ec527 100644 --- a/libs/yocto/yocto_gui.h +++ b/libs/yocto/yocto_gui.h @@ -65,15 +65,15 @@ namespace yocto { // Open a window and show an image void show_image_gui( - const string& title, const string& name, const image_data& image); + const string& title, const string& name, const image_t& image); // Open a window and show a set of images void show_image_gui(const string& title, const vector& names, - const vector& images); + const vector>& images); // Open a window and show an image for color grading void show_colorgrade_gui( - const string& title, const string& name, const image_data& image); + const string& title, const string& name, const image_t& image); // Open a window and show an scene via path tracing void show_trace_gui(const string& title, const string& name, scene_data& scene, @@ -166,7 +166,7 @@ bool init_image(glimage_state& glimage); void clear_image(glimage_state& glimage); // update image data -void set_image(glimage_state& glimage, const image_data& image); +void set_image(glimage_state& glimage, const image_t& image); // draw image void draw_image(glimage_state& image, const glimage_params& params); @@ -294,14 +294,14 @@ bool draw_tonemap_widgets( const gui_input& input, float& exposure, bool& filmic); // draw image inspector -bool draw_image_widgets(const gui_input& input, const image_data& image, - const image_data& display, glimage_params& glparams); -bool draw_image_widgets( - const gui_input& input, const image_data& image, glimage_params& glparams); +bool draw_image_widgets(const gui_input& input, const image_t& image, + const image_t& display, glimage_params& glparams); +bool draw_image_widgets(const gui_input& input, const image_t& image, + glimage_params& glparams); // update image params -void update_image_params( - const gui_input& input, const image_data& image, glimage_params& glparams); +void update_image_params(const gui_input& input, const image_t& image, + glimage_params& glparams); // update image params from mouse bool uiupdate_image_params(const gui_input& input, glimage_params& glparams); diff --git a/libs/yocto/yocto_image.cpp b/libs/yocto/yocto_image.cpp index 82788952a..b04f1946d 100644 --- a/libs/yocto/yocto_image.cpp +++ b/libs/yocto/yocto_image.cpp @@ -87,327 +87,166 @@ inline void parallel_for_batch(T num, T batch, Func&& func) { } // namespace yocto // ----------------------------------------------------------------------------- -// IMPLEMENTATION OF IMAGE DATA AND UTILITIES +// IMPLEMENTATION OF IMAGE OPERATIONS // ----------------------------------------------------------------------------- namespace yocto { -// image creation -image_data make_image(int width, int height, bool linear) { - return image_data{ - width, height, linear, vector(width * height, vec4f{0, 0, 0, 0})}; -} - -// equality -bool operator==(const image_data& a, const image_data& b) { - return a.width == b.width && a.height == b.height && a.linear == b.linear && - a.pixels == b.pixels; +// Conversion from/to floats. +image_t byte_to_float(const image_t& bt) { + auto fl = image_t{bt.size()}; + for (auto&& [fl_, bt_] : zip(fl, bt)) fl_ = byte_to_float(bt_); + return fl; } -bool operator!=(const image_data& a, const image_data& b) { - return a.width != b.width || a.height != b.height || a.linear != b.linear || - a.pixels != b.pixels; +image_t float_to_byte(const image_t& fl) { + auto bt = image_t{fl.size()}; + for (auto&& [fl_, bt_] : zip(fl, bt)) bt_ = float_to_byte(fl_); + return bt; } -// swap -void swap(image_data& a, image_data& b) { - std::swap(a.width, b.width); - std::swap(a.height, b.height); - std::swap(a.linear, b.linear); - std::swap(a.pixels, b.pixels); +// Conversion between linear and gamma-encoded images. +image_t srgb_to_rgb(const image_t& srgb) { + auto rgb = image_t{srgb.size()}; + for (auto&& [rgb_, srgb_] : zip(rgb, srgb)) rgb_ = srgb_to_rgb(srgb_); + return rgb; } - -// conversions -image_data convert_image(const image_data& image, bool linear) { - if (image.linear == linear) return image; - auto result = make_image(image.width, image.height, linear); - convert_image(result, image); - return result; +image_t rgb_to_srgb(const image_t& rgb) { + auto srgb = image_t{rgb.size()}; + for (auto&& [rgb_, srgb_] : zip(rgb, srgb)) srgb_ = rgb_to_srgb(rgb_); + return srgb; } -void convert_image(image_data& result, const image_data& image) { - if (image.width != result.width || image.height != result.height) - throw std::invalid_argument{"image have to be the same size"}; - if (image.linear == result.linear) { - result.pixels = image.pixels; - } else { - for (auto idx : range(image.pixels.size())) { - result.pixels[idx] = image.linear ? rgb_to_srgb(image.pixels[idx]) - : srgb_to_rgb(image.pixels[idx]); - } - } +image_t srgbb_to_rgb(const image_t& srgb) { + auto rgb = image_t{srgb.size()}; + for (auto&& [rgb_, srgb_] : zip(rgb, srgb)) + rgb_ = srgb_to_rgb(byte_to_float(srgb_)); + return rgb; } - -// Lookup pixel for evaluation -static vec4f lookup_image( - const image_data& image, int i, int j, bool as_linear) { - if (as_linear && !image.linear) { - return srgb_to_rgb(image.pixels[j * image.width + i]); - } else { - return image.pixels[j * image.width + i]; - } +image_t rgb_to_srgbb(const image_t& rgb) { + auto srgb = image_t{rgb.size()}; + for (auto&& [rgb_, srgb_] : zip(rgb, srgb)) + srgb_ = float_to_byte(rgb_to_srgb(rgb_)); + return srgb; } -// Evaluates an image at a point `uv`. -vec4f eval_image(const image_data& image, const vec2f& uv, bool as_linear, - bool no_interpolation, bool clamp_to_edge) { - if (image.width == 0 || image.height == 0) return {0, 0, 0, 0}; - - // get image width/height - auto size = vec2i{image.width, image.height}; - - // get coordinates normalized for tiling - auto s = 0.0f, t = 0.0f; - if (clamp_to_edge) { - s = clamp(uv.x, 0.0f, 1.0f) * size.x; - t = clamp(uv.y, 0.0f, 1.0f) * size.y; - } else { - s = fmod(uv.x, 1.0f) * size.x; - if (s < 0) s += size.x; - t = fmod(uv.y, 1.0f) * size.y; - if (t < 0) t += size.y; - } - - // get image coordinates and residuals - auto i = clamp((int)s, 0, size.x - 1), j = clamp((int)t, 0, size.y - 1); - auto ii = (i + 1) % size.x, jj = (j + 1) % size.y; - auto u = s - i, v = t - j; - - // handle interpolation - if (no_interpolation) { - return lookup_image(image, i, j, as_linear); - } else { - return lookup_image(image, i, j, as_linear) * (1 - u) * (1 - v) + - lookup_image(image, i, jj, as_linear) * (1 - u) * v + - lookup_image(image, ii, j, as_linear) * u * (1 - v) + - lookup_image(image, ii, jj, as_linear) * u * v; - } +// Apply exposure and filmic tone mapping +image_t tonemap_image( + const image_t& hdr, float exposure, bool filmic, bool srgb) { + auto ldr = image_t{hdr.size()}; + for (auto&& [ldr_, hdr_] : zip(ldr, hdr)) + ldr_ = tonemap(hdr_, exposure, filmic, srgb); + return ldr; +} +image_t tonemapb_image( + const image_t& hdr, float exposure, bool filmic, bool srgb) { + auto ldr = image_t{hdr.size()}; + for (auto&& [ldr_, hdr_] : zip(ldr, hdr)) + ldr_ = float_to_byte(tonemap(hdr_, exposure, filmic, srgb)); + return ldr; +} +void tonemap_image(image_t& ldr, const image_t& hdr, + float exposure, bool filmic, bool srgb) { + for (auto&& [ldr_, hdr_] : zip(ldr, hdr)) + ldr_ = tonemap(hdr_, exposure, filmic, srgb); } -// Apply tone mapping returning a float or byte image. -image_data tonemap_image(const image_data& image, float exposure, bool filmic) { - if (!image.linear) return image; - auto result = make_image(image.width, image.height, false); - for (auto idx = 0; idx < image.width * image.height; idx++) { - result.pixels[idx] = tonemap(image.pixels[idx], exposure, filmic, true); - } - return result; +// Apply exposure and filmic tone mapping +image_t colorgrade_image( + const image_t& img, bool linear, const colorgrade_params& params) { + auto graded = image_t{img.size()}; + for (auto&& [cor_, img_] : zip(graded, img)) + cor_ = colorgrade(img_, linear, params); + return graded; +} +void colorgrade_image(image_t& graded, const image_t& img, + bool linear, const colorgrade_params& params) { + for (auto&& [cor_, img_] : zip(graded, img)) + cor_ = colorgrade(img_, linear, params); } -// Apply tone mapping. If the input image is an ldr, does nothing. -void tonemap_image( - image_data& result, const image_data& image, float exposure, bool filmic) { - if (image.width != result.width || image.height != result.height) - throw std::invalid_argument{"image should be the same size"}; - if (result.linear) throw std::invalid_argument{"ldr expected"}; - if (image.linear) { - for (auto idx : range(image.pixels.size())) { - result.pixels[idx] = tonemap(image.pixels[idx], exposure, filmic); - } - } else { - auto scale = vec4f{ - pow(2.0f, exposure), pow(2.0f, exposure), pow(2.0f, exposure), 1}; - for (auto idx : range(image.pixels.size())) { - result.pixels[idx] = image.pixels[idx] * scale; - } - } -} -// Apply tone mapping using multithreading for speed. -void tonemap_image_mt( - image_data& result, const image_data& image, float exposure, bool filmic) { - if (image.width != result.width || image.height != result.height) - throw std::invalid_argument{"image should be the same size"}; - if (result.linear) throw std::invalid_argument{"ldr expected"}; - if (image.linear) { - parallel_for_batch((size_t)image.width * (size_t)image.height, - (size_t)image.width, [&result, &image, exposure, filmic](size_t idx) { - result.pixels[idx] = tonemap(image.pixels[idx], exposure, filmic); - }); - } else { - auto scale = vec4f{ - pow(2.0f, exposure), pow(2.0f, exposure), pow(2.0f, exposure), 1}; - parallel_for_batch((size_t)image.width * (size_t)image.height, - (size_t)image.width, [&result, &image, scale](size_t idx) { - result.pixels[idx] = image.pixels[idx] * scale; - }); - } +// compute white balance +vec3f compute_white_balance(const image_t& img) { + auto rgb = vec3f{0, 0, 0}; + for (auto& p : img) rgb += xyz(p); + if (rgb == vec3f{0, 0, 0}) return {0, 0, 0}; + return rgb / max(rgb); } -// Resize an image. -image_data resize_image( - const image_data& image, int res_width, int res_height) { - if (res_width == 0 && res_height == 0) { - throw std::invalid_argument{"bad image size in resize"}; +// image compositing +image_t composite_image( + const image_t& foreground, const image_t& background) { + auto composited = image_t{background.size()}; + for (auto ij : range(background.size())) { + composited[ij] = composite(foreground[ij], background[ij]); } - if (res_height == 0) { - res_height = (int)round( - res_width * (double)image.height / (double)image.width); - } else if (res_width == 0) { - res_width = (int)round( - res_height * (double)image.width / (double)image.height); - } - auto result = make_image(res_width, res_height, image.linear); - stbir_resize_float_generic((float*)image.pixels.data(), (int)image.width, - (int)image.height, (int)(sizeof(vec4f) * image.width), - (float*)result.pixels.data(), (int)result.width, (int)result.height, - (int)(sizeof(vec4f) * result.width), 4, 3, 0, STBIR_EDGE_CLAMP, - STBIR_FILTER_DEFAULT, STBIR_COLORSPACE_LINEAR, nullptr); - return result; + return composited; } -// Compute the difference between two images. -image_data image_difference( - const image_data& image1, const image_data& image2, bool display) { - // check sizes - if (image1.width != image2.width || image1.height != image2.height) { - throw std::invalid_argument{"image sizes are different"}; +// removes alpha +image_t remove_alpha(const image_t& img) { + auto res = image_t{img.size()}; + for (auto ij : range(img.size())) { + res[ij] = {img[ij].x, img[ij].y, img[ij].z, 1}; } - - // check types - if (image1.linear != image2.linear) { - throw std::invalid_argument{"image types are different"}; - } - - // compute diff - auto difference = make_image(image1.width, image1.height, image1.linear); - for (auto idx : range(difference.pixels.size())) { - auto diff = abs(image1.pixels[idx] - image2.pixels[idx]); - difference.pixels[idx] = display ? vec4f{max(diff), max(diff), max(diff), 1} - : diff; - } - return difference; + return res; } -void set_region(image_data& image, const image_data& region, int x, int y) { - for (auto j : range(region.height)) { - for (auto i : range(region.width)) { - image.pixels[(j + y) * image.width + (i + x)] = - region.pixels[j * region.width + i]; - } +// turns alpha into a gray scale image +image_t alpha_to_gray(const image_t& img) { + auto res = image_t{img.size()}; + for (auto ij : range(img.size())) { + auto a = img[ij].w; + res[ij] = {a, a, a, 1}; } + return res; } -void get_region(image_data& region, const image_data& image, int x, int y, - int width, int height) { - if (region.width != width || region.height != height) { - region = make_image(width, height, image.linear); - } - for (auto j : range(height)) { - for (auto i : range(width)) { - region.pixels[j * region.width + i] = - image.pixels[(j + y) * image.width + (i + x)]; +image_t image_difference( + const image_t& a, const image_t& b, bool display) { + if (a.size() != b.size()) + throw std::invalid_argument{"images have different sizes"}; + auto diff = image_t{a.size()}; + for (auto i : range(diff.size())) diff[i] = abs(a[i] - b[i]); + if (display) { + for (auto i : range(diff.size())) { + auto d = max(diff[i]); + diff[i] = {d, d, d, 1}; } } + return diff; } -// Composite two images together. -image_data composite_image( - const image_data& image_a, const image_data& image_b) { - if (image_a.width != image_b.width || image_a.height != image_b.height) - throw std::invalid_argument{"image should be the same size"}; - if (image_a.linear != image_b.linear) - throw std::invalid_argument{"image should be of the same type"}; - auto result = make_image(image_a.width, image_a.height, image_a.linear); - for (auto idx : range(result.pixels.size())) { - result.pixels[idx] = composite(image_a.pixels[idx], image_b.pixels[idx]); - } - return result; -} - -// Composite two images together. -void composite_image( - image_data& result, const image_data& image_a, const image_data& image_b) { - if (image_a.width != image_b.width || image_a.height != image_b.height) - throw std::invalid_argument{"image should be the same size"}; - if (image_a.linear != image_b.linear) - throw std::invalid_argument{"image should be of the same type"}; - if (image_a.width != result.width || image_a.height != result.height) - throw std::invalid_argument{"image should be the same size"}; - if (image_a.linear != result.linear) - throw std::invalid_argument{"image should be of the same type"}; - for (auto idx : range(result.pixels.size())) { - result.pixels[idx] = composite(image_a.pixels[idx], image_b.pixels[idx]); - } -} - -// Apply color grading from a linear or srgb color to an srgb color. -vec4f colorgradeb( - const vec4f& color, bool linear, const colorgrade_params& params) { - auto rgb = xyz(color); - auto alpha = color.w; - if (linear) { - if (params.exposure != 0) rgb *= exp2(params.exposure); - if (params.tint != vec3f{1, 1, 1}) rgb *= params.tint; - if (params.lincontrast != 0.5f) - rgb = lincontrast(rgb, params.lincontrast, 0.18f); - if (params.logcontrast != 0.5f) - rgb = logcontrast(rgb, params.logcontrast, 0.18f); - if (params.linsaturation != 0.5f) rgb = saturate(rgb, params.linsaturation); - if (params.filmic) rgb = tonemap_filmic(rgb); - if (params.srgb) rgb = rgb_to_srgb(rgb); - } - if (params.contrast != 0.5f) rgb = contrast(rgb, params.contrast); - if (params.saturation != 0.5f) rgb = saturate(rgb, params.saturation); - if (params.shadows != 0.5f || params.midtones != 0.5f || - params.highlights != 0.5f || params.shadows_color != vec3f{1, 1, 1} || - params.midtones_color != vec3f{1, 1, 1} || - params.highlights_color != vec3f{1, 1, 1}) { - auto lift = params.shadows_color; - auto gamma = params.midtones_color; - auto gain = params.highlights_color; - lift = lift - mean(lift) + params.shadows - (float)0.5; - gain = gain - mean(gain) + params.highlights + (float)0.5; - auto grey = gamma - mean(gamma) + params.midtones; - gamma = log(((float)0.5 - lift) / (gain - lift)) / log(grey); - // apply_image - auto lerp_value = clamp(pow(rgb, 1 / gamma), (float)0, (float)1); - rgb = gain * lerp_value + lift * (1 - lerp_value); +image_t resize_image(const image_t& img, vec2i resize) { + auto size = img.size(); + if (resize.x == 0 && resize.y == 0) { + throw std::invalid_argument{"bad image size in resize"}; } - return vec4f{rgb.x, rgb.y, rgb.z, alpha}; -} - -// Color grade an hsr or ldr image to an ldr image. -image_data colorgrade_image( - const image_data& image, const colorgrade_params& params) { - auto result = make_image(image.width, image.height, false); - for (auto idx : range(image.pixels.size())) { - result.pixels[idx] = colorgrade(image.pixels[idx], image.linear, params); + if (resize.y == 0) { + resize.y = (int)round(resize.x * (double)size.y / (double)size.x); + } else if (resize.x == 0) { + resize.x = (int)round(resize.y * (double)size.x / (double)size.y); } - return result; -} - -// Color grade an hsr or ldr image to an ldr image. -// Uses multithreading for speed. -void colorgrade_image(image_data& result, const image_data& image, - const colorgrade_params& params) { - if (image.width != result.width || image.height != result.height) - throw std::invalid_argument{"image should be the same size"}; - if (!!result.linear) throw std::invalid_argument{"non linear expected"}; - for (auto idx : range(image.pixels.size())) { - result.pixels[idx] = colorgrade(image.pixels[idx], image.linear, params); + auto res = image_t{resize}; + stbir_resize_float_generic((float*)img.data(), size.x, size.y, + sizeof(vec4f) * size.x, (float*)res.data(), resize.x, resize.y, + sizeof(vec4f) * resize.x, 4, 3, 0, STBIR_EDGE_CLAMP, STBIR_FILTER_DEFAULT, + STBIR_COLORSPACE_LINEAR, nullptr); + return res; +} +image_t resize_image(const image_t& img, vec2i resize) { + auto size = img.size(); + if (resize.x == 0 && resize.y == 0) { + throw std::invalid_argument{"bad image size in resize"}; } -} - -// Color grade an hsr or ldr image to an ldr image. -// Uses multithreading for speed. -void colorgrade_image_mt(image_data& result, const image_data& image, - const colorgrade_params& params) { - if (image.width != result.width || image.height != result.height) - throw std::invalid_argument{"image should be the same size"}; - if (!!result.linear) throw std::invalid_argument{"non linear expected"}; - parallel_for_batch((size_t)image.width * (size_t)image.height, - (size_t)image.width, [&result, &image, ¶ms](size_t idx) { - result.pixels[idx] = colorgrade( - image.pixels[idx], image.linear, params); - }); -} - -// determine white balance colors -vec4f compute_white_balance(const image_data& image) { - auto rgb = vec3f{0, 0, 0}; - for (auto idx = (size_t)0; image.pixels.size(); idx++) { - rgb += xyz(image.pixels[idx]); + if (resize.y == 0) { + resize.y = (int)round(resize.x * (double)size.y / (double)size.x); + } else if (resize.x == 0) { + resize.x = (int)round(resize.y * (double)size.x / (double)size.y); } - if (rgb == vec3f{0, 0, 0}) return {0, 0, 0, 1}; - rgb /= max(rgb); - return {rgb.x, rgb.y, rgb.z, 1}; + auto res = image_t{resize}; + stbir_resize_uint8_generic((byte*)img.data(), size.x, size.y, + sizeof(vec4b) * size.x, (byte*)res.data(), resize.x, resize.y, + sizeof(vec4b) * resize.x, 4, 3, 0, STBIR_EDGE_CLAMP, STBIR_FILTER_DEFAULT, + STBIR_COLORSPACE_LINEAR, nullptr); + return res; } } // namespace yocto @@ -419,54 +258,44 @@ namespace yocto { // Comvert a bump map to a normal map. void bump_to_normal( - image_data& normalmap, const image_data& bumpmap, float scale) { - auto width = bumpmap.width, height = bumpmap.height; - if (normalmap.width != bumpmap.width || normalmap.height != bumpmap.height) { - normalmap = make_image(width, height, bumpmap.linear); - } - auto dx = 1.0f / width, dy = 1.0f / height; - for (int j = 0; j < height; j++) { - for (int i = 0; i < width; i++) { - auto i1 = (i + 1) % width, j1 = (j + 1) % height; - auto p00 = bumpmap.pixels[j * bumpmap.width + i], - p10 = bumpmap.pixels[j * bumpmap.width + i1], - p01 = bumpmap.pixels[j1 * bumpmap.width + i]; - auto g00 = (p00.x + p00.y + p00.z) / 3; - auto g01 = (p01.x + p01.y + p01.z) / 3; - auto g10 = (p10.x + p10.y + p10.z) / 3; - auto normal = vec3f{ - scale * (g00 - g10) / dx, scale * (g00 - g01) / dy, 1.0f}; - normal.y = -normal.y; // make green pointing up, even if y axis - // points down - normal = normalize(normal) * 0.5f + vec3f{0.5f, 0.5f, 0.5f}; - set_pixel(normalmap, i, j, {normal.x, normal.y, normal.z, 1}); - } + image_t& normalmap, const image_t& bumpmap, float scale) { + if (normalmap.size() != bumpmap.size()) normalmap = {bumpmap.size()}; + auto dx = 1.0f / bumpmap.size().x, dy = 1.0f / bumpmap.size().y; + auto size = bumpmap.size(); + for (auto [i, j] : range(size)) { + auto i1 = (i + 1) % size.x, j1 = (j + 1) % size.y; + auto p00 = bumpmap[{i, j}], p10 = bumpmap[{i1, j}], p01 = bumpmap[{i, j1}]; + auto g00 = (p00.x + p00.y + p00.z) / 3; + auto g01 = (p01.x + p01.y + p01.z) / 3; + auto g10 = (p10.x + p10.y + p10.z) / 3; + auto normal = vec3f{ + scale * (g00 - g10) / dx, scale * (g00 - g01) / dy, 1.0f}; + normal.y = -normal.y; // make green pointing up, even if y axis + // points down + normal = normalize(normal) * 0.5f + vec3f{0.5f, 0.5f, 0.5f}; + normalmap[{i, j}] = {normal.x, normal.y, normal.z, 1}; } } -image_data bump_to_normal(const image_data& bumpmap, float scale) { - auto normalmap = make_image(bumpmap.width, bumpmap.height, bumpmap.linear); +image_t bump_to_normal(const image_t& bumpmap, float scale) { + auto normalmap = image_t{bumpmap.size()}; bump_to_normal(normalmap, bumpmap, scale); return normalmap; } template -static image_data make_proc_image( - int width, int height, bool linear, Shader&& shader) { - auto image = make_image(width, height, linear); - auto scale = 1.0f / max(width, height); - for (auto j : range(height)) { - for (auto i : range(width)) { - auto uv = vec2f{i * scale, j * scale}; - image.pixels[j * width + i] = shader(uv); - } +static image_t make_proc_image(vec2i size, Shader&& shader) { + auto image_ = image_t{size}; + auto scale = 1.0f / max(size); + for (auto ij : range(size)) { + image_[ij] = shader((vec2f)ij * scale); } - return image; + return image_; } // Make an image -image_data make_grid(int width, int height, float scale, const vec4f& color0, - const vec4f& color1) { - return make_proc_image(width, height, true, [=](vec2f uv) { +image_t make_grid( + vec2i size, float scale, const vec4f& color0, const vec4f& color1) { + return make_proc_image(size, [=](vec2f uv) { uv *= 4 * scale; uv -= vec2f{(float)(int)uv.x, (float)(int)uv.y}; auto thick = 0.01f / 2; @@ -478,9 +307,9 @@ image_data make_grid(int width, int height, float scale, const vec4f& color0, }); } -image_data make_checker(int width, int height, float scale, const vec4f& color0, - const vec4f& color1) { - return make_proc_image(width, height, true, [=](vec2f uv) { +image_t make_checker( + vec2i size, float scale, const vec4f& color0, const vec4f& color1) { + return make_proc_image(size, [=](vec2f uv) { uv *= 4 * scale; uv -= vec2f{(float)(int)uv.x, (float)(int)uv.y}; auto c = uv.x <= 0.5f != uv.y <= 0.5f; @@ -488,9 +317,9 @@ image_data make_checker(int width, int height, float scale, const vec4f& color0, }); } -image_data make_bumps(int width, int height, float scale, const vec4f& color0, - const vec4f& color1) { - return make_proc_image(width, height, true, [=](vec2f uv) { +image_t make_bumps( + vec2i size, float scale, const vec4f& color0, const vec4f& color1) { + return make_proc_image(size, [=](vec2f uv) { uv *= 4 * scale; uv -= vec2f{(float)(int)uv.x, (float)(int)uv.y}; auto thick = 0.125f; @@ -505,18 +334,18 @@ image_data make_bumps(int width, int height, float scale, const vec4f& color0, }); } -image_data make_ramp(int width, int height, float scale, const vec4f& color0, - const vec4f& color1) { - return make_proc_image(width, height, true, [=](vec2f uv) { +image_t make_ramp( + vec2i size, float scale, const vec4f& color0, const vec4f& color1) { + return make_proc_image(size, [=](vec2f uv) { uv *= scale; uv -= vec2f{(float)(int)uv.x, (float)(int)uv.y}; return lerp(color0, color1, uv.x); }); } -image_data make_gammaramp(int width, int height, float scale, - const vec4f& color0, const vec4f& color1) { - return make_proc_image(width, height, false, [=](vec2f uv) { +image_t make_gammaramp( + vec2i size, float scale, const vec4f& color0, const vec4f& color1) { + return make_proc_image(size, [=](vec2f uv) { uv *= scale; uv -= vec2f{(float)(int)uv.x, (float)(int)uv.y}; if (uv.y < 1 / 3.0f) { @@ -529,16 +358,16 @@ image_data make_gammaramp(int width, int height, float scale, }); } -image_data make_uvramp(int width, int height, float scale) { - return make_proc_image(width, height, true, [=](vec2f uv) { +image_t make_uvramp(vec2i size, float scale) { + return make_proc_image(size, [=](vec2f uv) { uv *= scale; uv -= vec2f{(float)(int)uv.x, (float)(int)uv.y}; return vec4f{uv.x, uv.y, 0, 1}; }); } -image_data make_uvgrid(int width, int height, float scale, bool colored) { - return make_proc_image(width, height, true, [=](vec2f uv) { +image_t make_uvgrid(vec2i size, float scale, bool colored) { + return make_proc_image(size, [=](vec2f uv) { uv *= scale; uv -= vec2f{(float)(int)uv.x, (float)(int)uv.y}; uv.y = 1 - uv.y; @@ -565,9 +394,9 @@ image_data make_uvgrid(int width, int height, float scale, bool colored) { }); } -image_data make_blackbodyramp( - int width, int height, float scale, float from, float to) { - return make_proc_image(width, height, true, [=](vec2f uv) { +image_t make_blackbodyramp( + vec2i size, float scale, float from, float to) { + return make_proc_image(size, [=](vec2f uv) { uv *= scale; uv -= vec2f{(float)(int)uv.x, (float)(int)uv.y}; auto rgb = blackbody_to_rgb(lerp(from, to, uv.x)); @@ -575,8 +404,8 @@ image_data make_blackbodyramp( }); } -image_data make_colormapramp(int width, int height, float scale) { - return make_proc_image(width, height, false, [=](vec2f uv) { +image_t make_colormapramp(vec2i size, float scale) { + return make_proc_image(size, [=](vec2f uv) { uv *= scale; uv -= vec2f{(float)(int)uv.x, (float)(int)uv.y}; auto rgb = vec3f{0, 0, 0}; @@ -593,9 +422,9 @@ image_data make_colormapramp(int width, int height, float scale) { }); } -image_data make_noisemap(int width, int height, float scale, - const vec4f& color0, const vec4f& color1) { - return make_proc_image(width, height, true, [=](vec2f uv) { +image_t make_noisemap( + vec2i size, float scale, const vec4f& color0, const vec4f& color1) { + return make_proc_image(size, [=](vec2f uv) { uv *= 8 * scale; auto v = perlin_noise(vec3f{uv.x, uv.y, 0}); v = clamp(v, 0.0f, 1.0f); @@ -603,9 +432,9 @@ image_data make_noisemap(int width, int height, float scale, }); } -image_data make_fbmmap(int width, int height, float scale, const vec4f& noise, +image_t make_fbmmap(vec2i size, float scale, const vec4f& noise, const vec4f& color0, const vec4f& color1) { - return make_proc_image(width, height, true, [=](vec2f uv) { + return make_proc_image(size, [=](vec2f uv) { uv *= 8 * scale; auto v = perlin_fbm({uv.x, uv.y, 0}, noise.x, noise.y, (int)noise.z); v = clamp(v, 0.0f, 1.0f); @@ -613,9 +442,9 @@ image_data make_fbmmap(int width, int height, float scale, const vec4f& noise, }); } -image_data make_turbulencemap(int width, int height, float scale, - const vec4f& noise, const vec4f& color0, const vec4f& color1) { - return make_proc_image(width, height, true, [=](vec2f uv) { +image_t make_turbulencemap(vec2i size, float scale, const vec4f& noise, + const vec4f& color0, const vec4f& color1) { + return make_proc_image(size, [=](vec2f uv) { uv *= 8 * scale; auto v = perlin_turbulence({uv.x, uv.y, 0}, noise.x, noise.y, (int)noise.z); v = clamp(v, 0.0f, 1.0f); @@ -623,9 +452,9 @@ image_data make_turbulencemap(int width, int height, float scale, }); } -image_data make_ridgemap(int width, int height, float scale, const vec4f& noise, +image_t make_ridgemap(vec2i size, float scale, const vec4f& noise, const vec4f& color0, const vec4f& color1) { - return make_proc_image(width, height, true, [=](vec2f uv) { + return make_proc_image(size, [=](vec2f uv) { uv *= 8 * scale; auto v = perlin_ridge( {uv.x, uv.y, 0}, noise.x, noise.y, (int)noise.z, noise.w); @@ -635,24 +464,22 @@ image_data make_ridgemap(int width, int height, float scale, const vec4f& noise, } // Add image border -image_data add_border( - const image_data& image, float width, const vec4f& color) { +image_t add_border( + const image_t& image, float width, const vec4f& color) { auto result = image; - auto scale = 1.0f / max(image.width, image.height); - for (auto j : range(image.height)) { - for (auto i : range(image.width)) { - auto uv = vec2f{i * scale, j * scale}; - if (uv.x < width || uv.y < width || uv.x > image.width * scale - width || - uv.y > image.height * scale - width) { - set_pixel(result, i, j, color); - } + auto scale = 1.0f / max(image.size()); + for (auto ij : range(image.size())) { + auto uv = (vec2f)ij * scale; + if (uv.x < width || uv.y < width || uv.x > image.size().x * scale - width || + uv.y > image.size().y * scale - width) { + result[ij] = color; } } return result; } // Implementation of sunsky modified heavily from pbrt -image_data make_sunsky(int width, int height, float theta_sun, float turbidity, +image_t make_sunsky(vec2i size, float theta_sun, float turbidity, bool has_sun, float sun_intensity, float sun_radius, const vec3f& ground_albedo) { auto zenith_xyY = vec3f{ @@ -732,7 +559,7 @@ image_data make_sunsky(int width, int height, float theta_sun, float turbidity, // the minimum 5 pixel diamater auto sun_angular_radius = 9.35e-03f / 2; // Wikipedia sun_angular_radius *= sun_radius; - sun_angular_radius = max(sun_angular_radius, 2 * pif / height); + sun_angular_radius = max(sun_angular_radius, 2 * pif / size.y); // sun direction auto sun_direction = vec3f{0, cos(theta_sun), sin(theta_sun)}; @@ -743,41 +570,41 @@ image_data make_sunsky(int width, int height, float theta_sun, float turbidity, }; // Make the sun sky image - auto img = make_image(width, height, true); - for (auto j = 0; j < height / 2; j++) { - auto theta = pif * ((j + 0.5f) / height); + auto img = image_t{size}; + for (auto j = 0; j < size.y / 2; j++) { + auto theta = pif * ((j + 0.5f) / size.y); theta = clamp(theta, 0.0f, pif / 2 - flt_eps); - for (int i = 0; i < width; i++) { - auto phi = 2 * pif * (float(i + 0.5f) / width); + for (int i = 0; i < size.x; i++) { + auto phi = 2 * pif * (float(i + 0.5f) / size.x); auto w = vec3f{cos(phi) * sin(theta), cos(theta), sin(phi) * sin(theta)}; auto gamma = acos(clamp(dot(w, sun_direction), -1.0f, 1.0f)); auto sky_col = sky(theta, gamma, theta_sun); auto sun_col = sun(theta, gamma); auto col = sky_col + sun_col; - img.pixels[j * width + i] = {col.x, col.y, col.z, 1}; + img[{i, j}] = {col.x, col.y, col.z, 1}; } } if (ground_albedo != vec3f{0, 0, 0}) { auto ground = vec3f{0, 0, 0}; - for (auto j = 0; j < height / 2; j++) { - auto theta = pif * ((j + 0.5f) / height); - for (int i = 0; i < width; i++) { - auto pxl = img.pixels[j * width + i]; + for (auto j = 0; j < size.y / 2; j++) { + auto theta = pif * ((j + 0.5f) / size.y); + for (int i = 0; i < size.x; i++) { + auto pxl = img[{i, j}]; auto le = vec3f{pxl.x, pxl.y, pxl.z}; - auto angle = sin(theta) * 4 * pif / (width * height); + auto angle = sin(theta) * 4 * pif / (size.x * size.y); ground += le * (ground_albedo / pif) * cos(theta) * angle; } } - for (auto j = height / 2; j < height; j++) { - for (int i = 0; i < width; i++) { - img.pixels[j * width + i] = {ground.x, ground.y, ground.z, 1}; + for (auto j = size.y / 2; j < size.y; j++) { + for (int i = 0; i < size.x; i++) { + img[{i, j}] = {ground.x, ground.y, ground.z, 1}; } } } else { - for (auto j = height / 2; j < height; j++) { - for (int i = 0; i < width; i++) { - img.pixels[j * width + i] = {0, 0, 0, 1}; + for (auto j = size.y / 2; j < size.y; j++) { + for (int i = 0; i < size.x; i++) { + img[{i, j}] = {0, 0, 0, 1}; } } } @@ -787,21 +614,21 @@ image_data make_sunsky(int width, int height, float theta_sun, float turbidity, } // Make an image of multiple lights. -image_data make_lights(int width, int height, const vec3f& le, int nlights, +image_t make_lights(vec2i size, const vec3f& le, int nlights, float langle, float lwidth, float lheight) { - auto img = make_image(width, height, true); - for (auto j = 0; j < height / 2; j++) { - auto theta = pif * ((j + 0.5f) / height); + auto img = image_t{size}; + for (auto j = 0; j < size.y / 2; j++) { + auto theta = pif * ((j + 0.5f) / size.y); theta = clamp(theta, 0.0f, pif / 2 - 0.00001f); if (fabs(theta - langle) > lheight / 2) continue; - for (int i = 0; i < width; i++) { - auto phi = 2 * pif * (float(i + 0.5f) / width); + for (int i = 0; i < size.x; i++) { + auto phi = 2 * pif * (float(i + 0.5f) / size.x); auto inlight = false; for (auto l : range(nlights)) { auto lphi = 2 * pif * (l + 0.5f) / nlights; inlight = inlight || fabs(phi - lphi) < lwidth / 2; } - img.pixels[j * width + i] = {le.x, le.y, le.z, 1}; + img[{i, j}] = {le.x, le.y, le.z, 1}; } } return img; @@ -1402,8 +1229,8 @@ void make_lights(vector& pixels, int width, int height, const vec3f& le, // ----------------------------------------------------------------------------- namespace yocto { -image filter_bilateral(const image& img, float spatial_sigma, - float range_sigma, const vector>& features, +image_t filter_bilateral(const image_t& img, float spatial_sigma, + float range_sigma, const vector>& features, const vector& features_sigma) { auto filtered = image{img.imsize(), vec4f{0,0,0,0}}; auto filter_width = (int)ceil(2.57f * spatial_sigma); @@ -1439,8 +1266,8 @@ image filter_bilateral(const image& img, float spatial_sigma, return filtered; } -image filter_bilateral( - const image& img, float spatial_sigma, float range_sigma) { +image_t filter_bilateral( + const image_t& img, float spatial_sigma, float range_sigma) { auto filtered = image{img.imsize(), vec4f{0,0,0,0}}; auto fwidth = (int)ceil(2.57f * spatial_sigma); auto sw = 1 / (2.0f * spatial_sigma * spatial_sigma); diff --git a/libs/yocto/yocto_image.h b/libs/yocto/yocto_image.h index 8f095879e..d18308b18 100644 --- a/libs/yocto/yocto_image.h +++ b/libs/yocto/yocto_image.h @@ -65,91 +65,161 @@ namespace yocto { // Image data as array of float or byte pixels. Images can be stored in linear // or non linear color space. -struct image_data { - // image data - int width = 0; - int height = 0; - bool linear = false; - vector pixels = {}; +template +struct image_t { + // iterators and references + using iterator = typename vector::iterator; + using const_iterator = typename vector::const_iterator; + using reference = typename vector::reference; + using const_reference = typename vector::const_reference; + + // image api similar to vector + image_t(); + image_t(const vec2i& size, const T& value = T{}); + image_t(const vec2i& size, const T* values); + + // size + bool empty() const; + vec2i size() const; + + // iterator access + iterator begin(); + iterator end(); + const_iterator begin() const; + const_iterator end() const; // pixel access - vec4f& operator[](vec2i ij); - const vec4f& operator[](vec2i ij) const; + T& operator[](vec2i ij); + const T& operator[](vec2i ij) const; + + // data access + T* data(); + const T* data() const; + vector& pixels(); + const vector& pixels() const; + + private: + vec2i _size = {0, 0}; + vector _data = {}; }; -// image creation -image_data make_image(int width, int height, bool linear); - // equality -bool operator==(const image_data& a, const image_data& b); -bool operator!=(const image_data& a, const image_data& b); - -// swap -void swap(image_data& a, image_data& b); +template +inline bool operator==(const image_t& a, const image_t& b); +template +inline bool operator!=(const image_t& a, const image_t& b); -// pixel access -inline vec4f get_pixel(const image_data& image, int i, int j); -inline void set_pixel(image_data& image, int i, int j, const vec4f& pixel); - -// conversions -image_data convert_image(const image_data& image, bool linear); -void convert_image(image_data& result, const image_data& image); +// Image data +template +inline float image_aspect(const image_t& image); // Evaluates an image at a point `uv`. -vec4f eval_image(const image_data& image, const vec2f& uv, - bool as_linear = false, bool no_interpolation = false, - bool clamp_to_edge = false); - -// Apply tone mapping returning a float or byte image. -image_data tonemap_image( - const image_data& image, float exposure, bool filmic = false); +template +inline T eval_image(const image_t& image, const vec2f& uv, + bool no_interpolation = false, bool clamp_to_edge = false); + +// Make an image from a function that goes from [0,1]x[0,1] to T. +template > +inline image_t make_image(const vec2i& size, Func&& func); +template > +inline image_t make_image(const vec2i& size, int samples, Func&& func); + +// Convolve an image with a kernel +template +inline image_t convolve_image( + const image_t& img, const image_t& kernel); +// Convolve an image with a separable kernel +template +inline image_t convolve_image( + const image_t& img, const vector& kernel); +// Gaussian kernel +inline vector make_gaussian_kernel1d(float sigma); +inline image_t make_gaussian_kernel2d(float sigma); +// Sharpening kernel +inline vector make_sharpening_kernel1d(float sigma, float scale = 1); +inline image_t make_sharpening_kernel2d(float sigma, float scale = 1); + +// Get/Set region +template +inline image_t get_region( + const image_t& source, const vec2i& offset, const vec2i& size); +template +inline void get_region(image_t& region, const image_t& source, + const vec2i& offset, const vec2i& size); +template +inline void set_region( + image_t& destination, const image_t& source, const vec2i& offset); + +// Convenience functions +template +inline T sum(const image_t& img); +template +inline T sum(const vector& img); + +// Image reconstruction +template +inline T reconstruct_image(const image_t& img, const vec2f& uv, + Func&& kernel, int kradius = 2, bool clamp_to_edge = false); + +// Reconstruction kernels +inline float box_filter(float x); +inline float hat_filter(float x); +inline float bspline_filter(float x); +inline float catmullrom_filter(float x); +inline float mitchell_filter(float x); -// Apply tone mapping. If the input image is an ldr, does nothing. -void tonemap_image(image_data& ldr, const image_data& image, float exposure, - bool filmic = false); -// Apply tone mapping using multithreading for speed. -void tonemap_image_mt(image_data& ldr, const image_data& image, float exposure, - bool filmic = false); - -// Resize an image. -image_data resize_image(const image_data& image, int width, int height); +} // namespace yocto -// set/get region -void set_region(image_data& image, const image_data& region, int x, int y); -void get_region(image_data& region, const image_data& image, int x, int y, - int width, int height); +// ----------------------------------------------------------------------------- +// IMAGE OPERATIONS +// ----------------------------------------------------------------------------- +namespace yocto { -// Compute the difference between two images. -image_data image_difference( - const image_data& image_a, const image_data& image_b, bool display_diff); +// Conversion from/to floats. +image_t byte_to_float(const image_t& bt); +image_t float_to_byte(const image_t& fl); -// Composite two images together. -image_data composite_image( - const image_data& image_a, const image_data& image_b); +// Conversion between linear and gamma-encoded images. +image_t srgb_to_rgb(const image_t& srgb); +image_t rgb_to_srgb(const image_t& rgb); +image_t srgbb_to_rgb(const image_t& srgb); +image_t rgb_to_srgbb(const image_t& rgb); -// Composite two images together. -void composite_image( - image_data& result, const image_data& image_a, const image_data& image_b); +// Apply exposure and filmic tone mapping +image_t tonemap_image(const image_t& hdr, float exposure = 0, + bool filmic = false, bool srgb = true); +image_t tonemapb_image(const image_t& hdr, float exposure = 0, + bool filmic = false, bool srgb = true); +// fast tone map for UI +void tonemap_image(image_t& ldr, const image_t& hdr, + float exposure = 0, bool filmic = false, bool srgb = true); + +// Apply exposure and filmic tone mapping +image_t colorgrade_image( + const image_t& img, bool linear, const colorgrade_params& params); +// compute white balance +vec3f compute_white_balance(const image_t& img); +// fast color grade for UI +void colorgrade_image(image_t& graded, const image_t& img, + bool linear, const colorgrade_params& params); -// Composite two images together. -void composite_image(image_data& result, const vector& images); +// image compositing +image_t composite_image( + const image_t& foreground, const image_t& background); -// Color grade an hsr or ldr image to an ldr image. -image_data colorgrade_image( - const image_data& image, const colorgrade_params& params); +// removes alpha +image_t remove_alpha(const image_t& img); -// Color grade an hsr or ldr image to an ldr image. -// Uses multithreading for speed. -void colorgrade_image(image_data& result, const image_data& image, - const colorgrade_params& params); +// turns alpha into a gray scale image +image_t alpha_to_gray(const image_t& img); -// Color grade an hsr or ldr image to an ldr image. -// Uses multithreading for speed. -void colorgrade_image_mt(image_data& result, const image_data& image, - const colorgrade_params& params); +// image difference +image_t image_difference( + const image_t& a, const image_t& b, bool display); -// determine white balance colors -vec4f compute_white_balance(const image_data& image); +// image resizing +image_t resize_image(const image_t& img, vec2i resize); +image_t resize_image(const image_t& img, vec2i resize); } // namespace yocto @@ -159,45 +229,44 @@ vec4f compute_white_balance(const image_data& image); namespace yocto { // Make a grid image. -image_data make_grid(int width, int height, float scale = 1, +image_t make_grid(vec2i size, float scale = 1, const vec4f& color0 = vec4f{0.2f, 0.2f, 0.2f, 1.0f}, const vec4f& color1 = vec4f{0.5f, 0.5f, 0.5f, 1.0f}); // Make a checker image. -image_data make_checker(int width, int height, float scale = 1, +image_t make_checker(vec2i size, float scale = 1, const vec4f& color0 = vec4f{0.2f, 0.2f, 0.2f, 1.0f}, const vec4f& color1 = vec4f{0.5f, 0.5f, 0.5f, 1.0f}); // Make a bump map. -image_data make_bumps(int width, int height, float scale = 1, +image_t make_bumps(vec2i size, float scale = 1, const vec4f& color0 = vec4f{0, 0, 0, 1}, const vec4f& color1 = vec4f{1, 1, 1, 1}); // Make a ramp -image_data make_ramp(int width, int height, float scale = 1, +image_t make_ramp(vec2i size, float scale = 1, const vec4f& color0 = vec4f{0, 0, 0, 1}, const vec4f& color1 = vec4f{1, 1, 1, 1}); // Make a gamma ramp. -image_data make_gammaramp(int width, int height, float scale = 1, +image_t make_gammaramp(vec2i size, float scale = 1, const vec4f& color0 = vec4f{0, 0, 0, 1}, const vec4f& color1 = vec4f{1, 1, 1, 1}); // Make a uv ramp -image_data make_uvramp(int width, int height, float scale = 1); +image_t make_uvramp(vec2i size, float scale = 1); // Make a uv grid -image_data make_uvgrid( - int width, int height, float scale = 1, bool colored = true); +image_t make_uvgrid(vec2i size, float scale = 1, bool colored = true); // Make blackbody ramp. -image_data make_blackbodyramp(int width, int height, float scale = 1, - float from = 1000, float to = 12000); +image_t make_blackbodyramp( + vec2i size, float scale = 1, float from = 1000, float to = 12000); // Make color map ramp. -image_data make_colormapramp(int width, int height, float scale = 1); +image_t make_colormapramp(vec2i size, float scale = 1); // Make a noise image. Noise parameters: lacunarity, gain, octaves, offset. -image_data make_noisemap(int width, int height, float scale = 1, +image_t make_noisemap(vec2i size, float scale = 1, const vec4f& color0 = {0, 0, 0, 1}, const vec4f& color1 = {1, 1, 1, 1}); -image_data make_fbmmap(int width, int height, float scale = 1, +image_t make_fbmmap(vec2i size, float scale = 1, const vec4f& noise = {2, 0.5, 8, 1}, const vec4f& color0 = {0, 0, 0, 1}, const vec4f& color1 = {1, 1, 1, 1}); -image_data make_turbulencemap(int width, int height, float scale = 1, +image_t make_turbulencemap(vec2i size, float scale = 1, const vec4f& noise = {2, 0.5, 8, 1}, const vec4f& color0 = {0, 0, 0, 1}, const vec4f& color1 = {1, 1, 1, 1}); -image_data make_ridgemap(int width, int height, float scale = 1, +image_t make_ridgemap(vec2i size, float scale = 1, const vec4f& noise = {2, 0.5, 8, 1}, const vec4f& color0 = {0, 0, 0, 1}, const vec4f& color1 = {1, 1, 1, 1}); @@ -206,20 +275,20 @@ image_data make_ridgemap(int width, int height, float scale = 1, // disabled with has_sun. The sun parameters can be slightly modified by // changing the sun intensity and temperature. Has a convention, a temperature // of 0 sets the eath sun defaults (ignoring intensity too). -image_data make_sunsky(int width, int height, float sun_angle, - float turbidity = 3, bool has_sun = false, float sun_intensity = 1, - float sun_radius = 1, const vec3f& ground_albedo = {0.2f, 0.2f, 0.2f}); +image_t make_sunsky(vec2i size, float sun_angle, float turbidity = 3, + bool has_sun = false, float sun_intensity = 1, float sun_radius = 1, + const vec3f& ground_albedo = {0.2f, 0.2f, 0.2f}); // Make an image of multiple lights. -image_data make_lights(int width, int height, const vec3f& le = {1, 1, 1}, +image_t make_lights(vec2i size, const vec3f& le = {1, 1, 1}, int nlights = 4, float langle = pif / 4, float lwidth = pif / 16, float lheight = pif / 16); // Comvert a bump map to a normal map. All linear color spaces. -image_data bump_to_normal(const image_data& image, float scale = 1); +image_t bump_to_normal(const image_t& image, float scale = 1); // Add a border to an image -image_data add_border( - const image_data& img, float width, const vec4f& color = {0, 0, 0, 1}); +image_t add_border( + const image_t& img, float width, const vec4f& color = {0, 0, 0, 1}); } // namespace yocto @@ -353,15 +422,6 @@ void add_border(vector& pixels, const vector& source, int width, } // namespace yocto -// ----------------------------------------------------------------------------- -// BACKWARDS COMPATIBILITY -// ----------------------------------------------------------------------------- -namespace yocto { - -using color_image = image_data; - -} // namespace yocto - // ----------------------------------------------------------------------------- // // @@ -375,20 +435,345 @@ using color_image = image_data; // ----------------------------------------------------------------------------- namespace yocto { -// pixel access -inline vec4f& image_data::operator[](vec2i ij) { - return pixels[ij.y * width + ij.x]; +// image api similar to vector +template +inline image_t::image_t() : _size{0, 0}, _data{} {} +template +inline image_t::image_t(const vec2i& size, const T& value) + : _size{size}, _data((size_t)size.x * (size_t)size.y, value) {} +template +inline image_t::image_t(const vec2i& size, const T* values) + : _size{size}, _data{values, values + size.x * size.y} {} + +// size +template +inline bool image_t::empty() const { + return _size == zero2i; } -inline const vec4f& image_data::operator[](vec2i ij) const { - return pixels[ij.y * width + ij.x]; +template +inline vec2i image_t::size() const { + return _size; +} + +// iterator access +template +inline typename image_t::iterator image_t::begin() { + return _data.begin(); +} +template +inline typename image_t::iterator image_t::end() { + return _data.end(); +} +template +inline typename image_t::const_iterator image_t::begin() const { + return _data.begin(); +} +template +inline typename image_t::const_iterator image_t::end() const { + return _data.end(); } // pixel access -inline vec4f get_pixel(const image_data& image, int i, int j) { - return image.pixels[j * image.width + i]; +template +inline T& image_t::operator[](vec2i ij) { + return _data[ij.y * _size.x + ij.x]; +} +template +inline const T& image_t::operator[](vec2i ij) const { + return _data[ij.y * _size.x + ij.x]; +} + +// data access +template +inline T* image_t::data() { + return _data.data(); +} +template +inline const T* image_t::data() const { + return _data.data(); +} +template +inline vector& image_t::pixels() { + return _data; +} +template +inline const vector& image_t::pixels() const { + return _data; +} + +// equality +template +inline bool operator==(const image_t& a, const image_t& b) { + return a.size() == b.size() && a.pixels() == b.pixels(); +} +template +inline bool operator!=(const image_t& a, const image_t& b) { + return a.size() != b.size() || a.pixels() != b.pixels(); +} + +// Image data +template +inline float image_aspect(const image_t& image) { + if (image.empty()) return 0; + return float{image.size().x} / float{image.size().y}; +} + +// Evaluates an image at a point `uv`. +template +inline T eval_image(const image_t& image, const vec2f& uv, + bool no_interpolation, bool clamp_to_edge) { + if (image.empty()) return T{}; + + // get image width/height + auto size = image.size(); + + // handle interpolation + if (no_interpolation) { + // get coordinates normalized for tiling + auto st = (clamp_to_edge ? clamp(uv, 0, 1) : mod(uv, 1)) * (vec2f)size; + + // lookup image + auto ij = clamp((vec2i)st, zero2i, size - 1); + return image[ij]; + } else { + // get coordinates normalized for tiling + auto st = (clamp_to_edge ? clamp(uv, 0, 1) : mod(uv, 1)) * (vec2f)size; + st -= 0.5f; // shift pixel centers to the pixel corners + + // get image coordinates + auto ij = clamp((vec2i)st, zero2i, size - 1); + auto i1j = clamp_to_edge ? min(ij + vec2i{1, 0}, size - 1) + : ((ij + vec2i{1, 0}) % size); + auto ij1 = clamp_to_edge ? min(ij + vec2i{0, 1}, size - 1) + : ((ij + vec2i{0, 1}) % size); + auto i1j1 = clamp_to_edge ? min(ij + vec2i{1, 1}, size - 1) + : ((ij + vec2i{1, 1}) % size); + + // interpolate + auto w = st - (vec2f)ij; + return image[ij] * (1 - w.x) * (1 - w.y) + image[ij1] * (1 - w.x) * w.y + + image[i1j] * w.x * (1 - w.y) + image[i1j1] * w.x * w.y; + } +} + +// Make an image from a function that goes from [0,1]x[0,1] to T. +template +inline image_t make_image(const vec2i& size, Func&& func) { + auto img = image_t(size); + for (auto ij : range(size)) { + img[ij] = func(((vec2f)ij + 0.5f) / (vec2f)size); + } + return img; +} + +// Make an image from a function that goes from [0,1]x[0,1] to T. +template +inline image_t make_image(const vec2i& size, int samples, Func&& func) { + if (samples <= 1) return make_image(size, std::forward(func)); + auto ns = (int)round(sqrt((float)samples)); + auto img = image_t(size, T{}); + for (auto ij : range(size)) { + for (auto sij : range({ns, ns})) { + img[ij] += func(((vec2f)ij + ((vec2f)sij + 0.5f) / ns) / (vec2f)size); + } + img[ij] /= ns * ns; + } + return img; +} + +// Evaluates a curve at a point `u`. +// This is just for the book images. +template +inline T eval_curve(const vector& curve, float u, + bool no_interpolation = false, bool clamp_to_edge = false) { + if (curve.empty()) return T{}; + + // get curve size + auto size = (int)curve.size(); + + // handle interpolation + if (no_interpolation) { + auto s = (clamp_to_edge ? clamp(u, 0.0f, 1.0f) : mod(u, 1.0f)) * size; + auto i = clamp((int)s, 0, size - 1); + return curve[i]; + } else { + auto s = (clamp_to_edge ? clamp(u, 0.0f, 1.0f) : mod(u, 1.0f)) * size; + s -= 0.5f; // shift pixel centers to the pixel corners + auto i = clamp((int)s, 0, size - 1); + auto i1 = clamp_to_edge ? min(i + 1, size - 1) : ((i + 1) % size); + auto w = s - i; + return curve[i] * (1 - w) + curve[i1] * w; + } +} + +// Make an image from a function that goes from [0,1] to T. +// This is just for the book images. +template > +inline vector make_curve(int size, Func&& func) { + auto curve = vector(size); + for (auto i : range(size)) { + curve[i] = func((i + 0.5f) / size); + } + return curve; +} + +// Convenience functions +template +inline T sum(const image_t& img) { + auto sum = T{}; + for (auto& value : img) sum += value; + return sum; +} +template +inline T sum(const vector& values) { + auto sum = T{}; + for (auto& value : values) sum += value; + return sum; +} + +// Convolve an image with a kernel +template +inline image_t convolve_image( + const image_t& img, const image_t& kernel) { + auto result = image_t(img.size()); + auto kcenter = kernel.size() / 2; + for (auto ij : range(img.size())) { + auto sum = T{}; + auto weight = 0.0f; + for (auto kij : range(kernel.size())) { + auto iijj = ij + kij - kcenter; + if (iijj.x < 0 || iijj.x >= img.size().x || iijj.y < 0 || + iijj.y >= img.size().y) + continue; + sum += img[iijj] * kernel[kij]; + weight += kernel[kij]; + } + result[ij] = max(sum / weight, T{}); + } + return result; +} +// Convolve an image with a separable kernel +template +inline image_t convolve_image( + const image_t& img, const vector& kernel); +// Gaussian kernel +inline vector make_gaussian_kernel1d(float sigma) { + auto width = (int)ceil(sigma * 3); + auto kernel = vector(width); + for (auto i : range(kernel.size())) + kernel[i] = exp( + -0.5f * (i - width / 2) * (i - width / 2) / (sigma * sigma)); + for (auto& value : kernel) value /= sum(kernel); + return kernel; +} +inline image_t make_gaussian_kernel2d(float sigma) { + auto kernel1d = make_gaussian_kernel1d(sigma); + auto kernel = image_t({(int)kernel1d.size(), (int)kernel1d.size()}); + for (auto ij : range(kernel.size())) + kernel[ij] = kernel1d[ij.x] * kernel1d[ij.y]; + for (auto& value : kernel) value /= sum(kernel); + return kernel; +} +// Sharpening kernel +inline vector make_sharpening_kernel1d(float sigma, float scale) { + auto kernel = make_gaussian_kernel1d(sigma); + for (auto& value : kernel) value = -scale * value; + kernel[kernel.size() / 2] += 1 + scale; + return kernel; +} +// Sharpening kernel +inline image_t make_sharpening_kernel2d(float sigma, float scale) { + auto kernel = make_gaussian_kernel2d(sigma); + for (auto& value : kernel) value = -scale * value; + kernel[kernel.size() / 2] += 1 + scale; + return kernel; +} + +// Get/Set region +template +inline image_t get_region( + const image_t& source, const vec2i& offset, const vec2i& size) { + auto region = image_t(size); + for (auto ij : range(region.size())) region[ij] = source[ij + offset]; + return region; +} +template +inline void get_region(image_t& region, const image_t& source, + const vec2i& offset, const vec2i& size) { + if (region.size() != size) region = image_t(size); + for (auto ij : range(region.size())) region[ij] = source[ij + offset]; +} +template +inline void set_region( + image_t& destination, const image_t& source, const vec2i& offset) { + for (auto ij : range(source.size())) destination[ij + offset] = source[ij]; +} + +inline float box_filter(float x) { return x < 0.5f && x >= -0.5f ? 1 : 0; } +inline float hat_filter(float x) { + auto xp = abs(x); + return xp < 1 ? 1 - xp : 0; +} +inline float bspline_filter(float x) { + auto xp = abs(x); + + if (xp < 1.0f) + return (4 + xp * xp * (3 * xp - 6)) / 6; + else if (xp < 2.0f) + return (8 + xp * (-12 + xp * (6 - xp))) / 6; + else + return 0; +} +inline float catmullrom_filter(float x) { + auto xp = abs(x); + + if (xp < 1.0f) + return 1 - xp * xp * (2.5f - 1.5f * xp); + else if (xp < 2.0f) + return 2 - xp * (4 + xp * (0.5f * xp - 2.5f)); + else + return 0; +} +inline float mitchell_filter(float x) { + auto xp = abs(x); + + if (xp < 1.0f) + return (16 + xp * xp * (21 * xp - 36)) / 18; + else if (xp < 2.0f) + return (32 + xp * (-60 + xp * (36 - 7 * xp))) / 18; + else + return 0; +} + +// Image reconstruction +template +inline T reconstruct_image(const image_t& img, const vec2f& uv, + Func&& kernel, int kradius, bool clamp_to_edge) { + auto x = uv * (vec2f)img.size(); + auto ij = (vec2i)(x + 0.5f); + auto size = img.size(); + auto sum = T{}; + for (auto j : range(ij.y - kradius, ij.y + kradius)) { + for (auto i : range(ij.x - kradius, ij.x + kradius)) { + auto xi = vec2f{(float)i, (float)j} + 0.5f; // shift centers + auto w = kernel(x - xi); + sum += img[clamp(vec2i{i, j}, vec2i{0, 0}, size - 1)] * w; + } + } + return sum; } -inline void set_pixel(image_data& image, int i, int j, const vec4f& pixel) { - image.pixels[j * image.width + i] = pixel; +template +inline T reconstruct_curve(const vector& curve, float u, Func&& kernel, + int kradius, bool clamp_to_edge) { + auto x = u * (float)curve.size(); + auto size = (int)curve.size(); + auto sum = 0.0f; + for (auto i : range(-2, size + 2)) { + auto xi = i + 0.5f; // shift centers + auto w = kernel(x - xi); + sum += curve[clamp(i, 0, size - 1)] * w; + } + return sum; } } // namespace yocto diff --git a/libs/yocto/yocto_matht.h b/libs/yocto/yocto_matht.h index df09d94a0..32f57f30c 100644 --- a/libs/yocto/yocto_matht.h +++ b/libs/yocto/yocto_matht.h @@ -2298,7 +2298,7 @@ inline pair, vec> camera_turntable(const vec& from_, auto theta = acos(z.y) + rotate.y; theta = clamp(theta, (T)0.001, (T)pi - (T)0.001); auto nz = vec{sin(theta) * cos(phi) * lz, cos(theta) * lz, - sin(theta) * sin(phi) * lz}; + sin(theta) * sin(phi) * lz}; from = to - nz; } @@ -2413,7 +2413,7 @@ inline void update_turntable(vec& from, vec& to, vec& up, auto theta = acos(z.y) + rotate.y; theta = clamp(theta, (T)0.001, (T)pi - (T)0.001); auto nz = vec{sin(theta) * cos(phi) * lz, cos(theta) * lz, - sin(theta) * sin(phi) * lz}; + sin(theta) * sin(phi) * lz}; from = to - nz; } diff --git a/libs/yocto/yocto_scene.cpp b/libs/yocto/yocto_scene.cpp index 4b0b4170e..0763ca692 100644 --- a/libs/yocto/yocto_scene.cpp +++ b/libs/yocto/yocto_scene.cpp @@ -109,82 +109,62 @@ namespace yocto { // pixel access vec4f lookup_texture( - const texture_data& texture, int i, int j, bool as_linear) { - auto color = vec4f{0, 0, 0, 0}; - if (!texture.pixelsf.empty()) { - color = texture.pixelsf[j * texture.width + i]; - } else { - color = byte_to_float(texture.pixelsb[j * texture.width + i]); - } - if (as_linear && !texture.linear) { - return srgb_to_rgb(color); - } else { - return color; - } + const texture_data& texture, vec2i ij, bool ldr_as_linear) { + if (!texture.pixelsf.empty()) return texture.pixelsf[ij]; + if (!texture.pixelsb.empty()) + return ldr_as_linear ? byte_to_float(texture.pixelsb[ij]) + : srgb_to_rgb(byte_to_float(texture.pixelsb[ij])); + return vec4f{0, 0, 0, 0}; } // Evaluates an image at a point `uv`. -vec4f eval_texture(const texture_data& texture, const vec2f& uv, bool as_linear, - bool no_interpolation, bool clamp_to_edge) { - if (texture.width == 0 || texture.height == 0) return {0, 0, 0, 0}; +vec4f eval_texture(const texture_data& texture, const vec2f& uv, + bool ldr_as_linear, bool no_interpolation, bool clamp_to_edge) { + if (texture.pixelsf.empty() && texture.pixelsb.empty()) return {0, 0, 0, 0}; // get texture width/height - auto size = vec2i{texture.width, texture.height}; + auto size = max(texture.pixelsf.size(), texture.pixelsb.size()); // get coordinates normalized for tiling - auto s = 0.0f, t = 0.0f; - if (clamp_to_edge) { - s = clamp(uv.x, 0.0f, 1.0f) * size.x; - t = clamp(uv.y, 0.0f, 1.0f) * size.y; - } else { - s = fmod(uv.x, 1.0f) * size.x; - if (s < 0) s += size.x; - t = fmod(uv.y, 1.0f) * size.y; - if (t < 0) t += size.y; - } - - // get image coordinates and residuals - auto i = clamp((int)s, 0, size.x - 1), j = clamp((int)t, 0, size.y - 1); - auto ii = (i + 1) % size.x, jj = (j + 1) % size.y; - auto u = s - i, v = t - j; + auto st = (clamp_to_edge ? clamp(uv, 0.0f, 1.0f) : mod(uv, 1.0f)) * + (vec2f)size; // handle interpolation if (no_interpolation) { - return lookup_texture(texture, i, j, as_linear); + auto ij = clamp((vec2i)st, {0, 0}, size - 1); + return lookup_texture(texture, ij, ldr_as_linear); } else { - return lookup_texture(texture, i, j, as_linear) * (1 - u) * (1 - v) + - lookup_texture(texture, i, jj, as_linear) * (1 - u) * v + - lookup_texture(texture, ii, j, as_linear) * u * (1 - v) + - lookup_texture(texture, ii, jj, as_linear) * u * v; + auto ij = clamp((vec2i)st, zero2i, size - 1); + auto i1j = (ij + vec2i{1, 0}) % size; + auto ij1 = (ij + vec2i{0, 1}) % size; + auto i1j1 = (ij + vec2i{1, 1}) % size; + auto w = st - (vec2f)ij; + return lookup_texture(texture, ij, ldr_as_linear) * (1 - w.x) * (1 - w.y) + + lookup_texture(texture, ij1, ldr_as_linear) * (1 - w.x) * w.y + + lookup_texture(texture, i1j, ldr_as_linear) * w.x * (1 - w.y) + + lookup_texture(texture, i1j1, ldr_as_linear) * w.x * w.y; } } vec4f eval_texture( - const texture_data& texture, const vec2f& uv, bool as_linear) { - return eval_texture(texture, uv, as_linear, texture.nearest, texture.clamp); + const texture_data& texture, const vec2f& uv, bool ldr_as_linear) { + return eval_texture( + texture, uv, ldr_as_linear, texture.nearest, texture.clamp); } // Helpers vec4f eval_texture( const scene_data& scene, int texture, const vec2f& uv, bool ldr_as_linear) { if (texture == invalidid) return {1, 1, 1, 1}; - return eval_texture(scene.textures[texture], uv, ldr_as_linear, - scene.textures[texture].nearest, scene.textures[texture].clamp); -} -vec4f eval_texture(const scene_data& scene, int texture, const vec2f& uv, - bool ldr_as_linear, bool no_interpolation, bool clamp_to_edge) { - if (texture == invalidid) return {1, 1, 1, 1}; - return eval_texture(scene.textures[texture], uv, ldr_as_linear, - no_interpolation, clamp_to_edge); + return eval_texture(scene.textures[texture], uv, ldr_as_linear); } // conversion from image -texture_data image_to_texture(const image_data& image) { - auto texture = texture_data{image.width, image.height, image.linear, {}, {}}; - if (image.linear) { - texture.pixelsf = image.pixels; +texture_data image_to_texture(const image_t& image, bool linear) { + auto texture = texture_data{}; + if (linear) { + texture.pixelsf = image; } else { - texture.pixelsb.resize(image.pixels.size()); - float_to_byte(texture.pixelsb, image.pixels); + texture.pixelsb = float_to_byte(image); } return texture; } @@ -204,18 +184,15 @@ material_point eval_material(const scene_data& scene, const material_data& material, const vec2f& texcoord, const vec4f& color_shp) { // evaluate textures - auto emission_tex = eval_texture( - scene, material.emission_tex, texcoord, true); - auto color_tex = eval_texture(scene, material.color_tex, texcoord, true); - auto roughness_tex = eval_texture( - scene, material.roughness_tex, texcoord, false); - auto scattering_tex = eval_texture( - scene, material.scattering_tex, texcoord, true); + auto emission_tex = eval_texture(scene, material.emission_tex, texcoord); + auto color_tex = eval_texture(scene, material.color_tex, texcoord); + auto roughness_tex = eval_texture(scene, material.roughness_tex, texcoord); + auto scattering_tex = eval_texture(scene, material.scattering_tex, texcoord); // material point auto point = material_point{}; point.type = material.type; - point.emission = material.emission * xyz(emission_tex) * xyz(color_shp); + point.emission = material.emission * xyz(emission_tex); point.color = material.color * xyz(color_tex) * xyz(color_shp); point.opacity = material.opacity * color_tex.w * color_shp.w; point.metallic = material.metallic * roughness_tex.z; @@ -453,7 +430,7 @@ vec3f eval_normalmap(const scene_data& scene, const instance_data& instance, if (material.normal_tex != invalidid && (!shape.triangles.empty() || !shape.quads.empty())) { auto& normal_tex = scene.textures[material.normal_tex]; - auto normalmap = -1 + 2 * xyz(eval_texture(normal_tex, texcoord, false)); + auto normalmap = -1 + 2 * xyz(eval_texture(normal_tex, texcoord, true)); auto [tu, tv] = eval_element_tangents(scene, instance, element); auto frame = frame3f{tu, tv, normal, {0, 0, 0}}; frame.x = orthonormalize(frame.x, frame.z); @@ -645,7 +622,7 @@ void add_camera(scene_data& scene) { void add_sky(scene_data& scene, float sun_angle) { scene.texture_names.emplace_back("sky"); auto& texture = scene.textures.emplace_back(); - texture = image_to_texture(make_sunsky(1024, 512, sun_angle)); + texture = image_to_texture(make_sunsky({1024, 512}, sun_angle), true); scene.environment_names.emplace_back("sky"); auto& environment = scene.environments.emplace_back(); environment.emission = {1, 1, 1}; @@ -729,6 +706,160 @@ bbox3f compute_bounds(const scene_data& scene) { return bbox; } +// Scene creation helpers +scene_data make_scene() { return scene_data{}; } + +// Scene creation helpers +int add_camera( + scene_data& scene, const string& name, const camera_data& camera) { + scene.camera_names.push_back(name); + scene.cameras.push_back(camera); + return (int)scene.cameras.size() - 1; +} +int add_texture( + scene_data& scene, const string& name, const texture_data& texture) { + scene.texture_names.push_back(name); + scene.textures.push_back(texture); + return (int)scene.textures.size() - 1; +} +int add_material( + scene_data& scene, const string& name, const material_data& material) { + scene.material_names.push_back(name); + scene.materials.push_back(material); + return (int)scene.materials.size() - 1; +} +int add_shape(scene_data& scene, const string& name, const shape_data& shape) { + scene.camera_names.push_back(name); + scene.shapes.push_back(shape); + return (int)scene.shapes.size() - 1; +} +int add_instance( + scene_data& scene, const string& name, const instance_data& instance) { + scene.instance_names.push_back(name); + scene.instances.push_back(instance); + return (int)scene.instances.size() - 1; +} +int add_environment(scene_data& scene, const string& name, + const environment_data& environment) { + scene.environment_names.push_back(name); + scene.environments.push_back(environment); + return (int)scene.environments.size() - 1; +} + +int add_camera(scene_data& scene, const string& name, const vec3f& from, + const vec3f& to, float lens, float aspect, float aperture, + float focus_offset) { + return add_camera(scene, name, + {.frame = lookat_frame(from, to, {0, 1, 0}), + .lens = lens, + .aspect = aspect, + .aperture = aperture, + .focus = length(from - to) + focus_offset}); +} + +int add_camera(scene_data& scene, const string& name, const frame3f& frame, + float lens, float aspect, float aperture, float focus) { + return add_camera(scene, name, + {.frame = frame, + .lens = lens, + .aspect = aspect, + .aperture = aperture, + .focus = focus}); +} + +// Scene creation helpers +int add_material(scene_data& scene, const string& name, const vec3f& emission, + material_type type, const vec3f& color, float roughness, int color_tex, + int roughness_tex, int normal_tex) { + return add_material(scene, name, + {.type = type, + .emission = emission, + .color = color, + .roughness = roughness, + .color_tex = color_tex, + .roughness_tex = roughness_tex, + .normal_tex = normal_tex}); +} + +// Scene creation helpers +int add_material(scene_data& scene, const string& name, material_type type, + const vec3f& color, float roughness, int color_tex, int roughness_tex, + int normal_tex) { + return add_material(scene, name, + {.type = type, + .color = color, + .roughness = roughness, + .color_tex = color_tex, + .roughness_tex = roughness_tex, + .normal_tex = normal_tex}); +} + +// Scene creation helpers +int add_material(scene_data& scene, const string& name, material_type type, + const vec3f& color, float roughness, const vec3f& scattering, + float scanisotropy, float trdepth, int color_tex, int roughness_tex, + int scattering_tex, int normal_tex) { + return add_material(scene, name, + {.type = type, + .color = color, + .roughness = roughness, + .scattering = scattering, + .scanisotropy = scanisotropy, + .trdepth = trdepth, + .color_tex = color_tex, + .roughness_tex = roughness_tex, + .scattering_tex = scattering_tex, + .normal_tex = normal_tex}); +} + +// Scene creation helpers +int add_instance(scene_data& scene, const string& name, const frame3f& frame, + int shape, int material) { + return add_instance( + scene, name, {.frame = frame, .shape = shape, .material = material}); +} + +// Scene creation helpers +int add_instance(scene_data& scene, const string& name, const frame3f& frame, + const shape_data& shape, const material_data& material) { + return add_instance(scene, name, + {.frame = frame, + .shape = add_shape(scene, name, shape), + .material = add_material(scene, name, material)}); +} + +// Scene creation helpers +int add_environment(scene_data& scene, const string& name, const frame3f& frame, + const vec3f& emission, int emission_tex) { + return add_environment(scene, name, + {.frame = frame, .emission = emission, .emission_tex = emission_tex}); + return (int)scene.environments.size() - 1; +} + +// Scene creation helpers +int add_texture(scene_data& scene, const string& name, + const image_t& texture, bool linear) { + if (texture.empty()) return invalidid; + if (linear) { + scene.textures.push_back({.pixelsf = texture}); + } else { + scene.textures.push_back({.pixelsb = float_to_byte(texture)}); + } + return (int)scene.textures.size() - 1; +} + +// Scene creation helpers +int add_texture(scene_data& scene, const string& name, + const image_t& texture, bool linear) { + if (texture.empty()) return invalidid; + if (linear) { + scene.textures.push_back({.pixelsf = srgbb_to_rgb(texture)}); + } else { + scene.textures.push_back({.pixelsb = texture}); + } + return (int)scene.textures.size() - 1; +} + } // namespace yocto // ----------------------------------------------------------------------------- @@ -824,6 +955,11 @@ size_t compute_memory(const scene_data& scene) { if (values.empty()) return 0; return values.size() * sizeof(values[0]); }; + auto image_memory = [](auto& values) -> size_t { + if (values.empty()) return 0; + return (size_t)values.size().x * (size_t)values.size().y * + sizeof(values[{0, 0}]); + }; auto memory = (size_t)0; memory += vector_memory(scene.cameras); @@ -858,8 +994,8 @@ size_t compute_memory(const scene_data& scene) { memory += vector_memory(subdiv.texcoords); } for (auto& texture : scene.textures) { - memory += vector_memory(texture.pixelsb); - memory += vector_memory(texture.pixelsf); + memory += image_memory(texture.pixelsb); + memory += image_memory(texture.pixelsf); } return memory; } @@ -870,6 +1006,11 @@ vector scene_stats(const scene_data& scene, bool verbose) { for (auto& value : values) sum += func(value); return sum; }; + auto accumulate2d = [](const auto& values, const auto& func) -> size_t { + auto sum = (size_t)0; + for (auto& value : values) sum += func(value); + return sum; + }; auto format = [](size_t num) { auto str = string{}; while (num > 0) { @@ -913,12 +1054,16 @@ vector scene_stats(const scene_data& scene, bool verbose) { stats.push_back("fvquads: " + format(accumulate(scene.subdivs, [](auto& subdiv) { return subdiv.quadspos.size(); }))); - stats.push_back("texels4b: " + - format(accumulate(scene.textures, - [](auto& texture) { return texture.pixelsb.size(); }))); - stats.push_back("texels4f: " + - format(accumulate(scene.textures, - [](auto& texture) { return texture.pixelsf.size(); }))); + stats.push_back( + "texels4b: " + format(accumulate(scene.textures, [](auto& texture) { + return (size_t)texture.pixelsb.size().x * + (size_t)texture.pixelsb.size().y; + }))); + stats.push_back( + "texels4f: " + format(accumulate(scene.textures, [](auto& texture) { + return (size_t)texture.pixelsf.size().x * + (size_t)texture.pixelsf.size().y; + }))); stats.push_back("center: " + format3(center(bbox))); stats.push_back("size: " + format3(size(bbox))); diff --git a/libs/yocto/yocto_scene.h b/libs/yocto/yocto_scene.h index d0df1ab80..10e6734c5 100644 --- a/libs/yocto/yocto_scene.h +++ b/libs/yocto/yocto_scene.h @@ -81,7 +81,7 @@ inline const int invalidid = -1; // To compute good apertures, one can use the F-stop number from photography // and set the aperture to focal length over f-stop. struct camera_data { - frame3f frame = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}, {0, 0, 0}}; + frame3f frame = identity3x4f; bool orthographic = false; float lens = 0.050f; float film = 0.036f; @@ -93,13 +93,10 @@ struct camera_data { // Texture data as array of float or byte pixels. Textures can be stored in // linear or non linear color space. struct texture_data { - int width = 0; - int height = 0; - bool linear = false; - bool nearest = false; - bool clamp = false; - vector pixelsf = {}; - vector pixelsb = {}; + image_t pixelsf = {}; + image_t pixelsb = {}; + bool nearest = false; + bool clamp = false; }; // Material type @@ -143,7 +140,7 @@ struct material_data { // Instance. struct instance_data { // instance data - frame3f frame = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}, {0, 0, 0}}; + frame3f frame = identity3x4f; int shape = invalidid; int material = invalidid; }; @@ -151,7 +148,7 @@ struct instance_data { // Environment map. struct environment_data { // environment data - frame3f frame = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}, {0, 0, 0}}; + frame3f frame = identity3x4f; vec3f emission = {0, 0, 0}; int emission_tex = invalidid; }; @@ -235,17 +232,13 @@ vec4f eval_texture( const texture_data& texture, const vec2f& uv, bool as_linear = false); vec4f eval_texture(const scene_data& scene, int texture, const vec2f& uv, bool as_linear = false); -vec4f eval_texture(const texture_data& texture, const vec2f& uv, bool as_linear, - bool no_interpolation, bool clamp_to_edge); -vec4f eval_texture(const scene_data& scene, int texture, const vec2f& uv, - bool as_linear, bool no_interpolation, bool clamp_to_edge); // pixel access vec4f lookup_texture( - const texture_data& texture, int i, int j, bool as_linear = false); + const texture_data& texture, vec2i ij, bool as_linear = false); // conversion from image -texture_data image_to_texture(const image_data& image); +texture_data image_to_texture(const image_t& image, bool linear); } // namespace yocto @@ -353,6 +346,53 @@ bool has_lights(const scene_data& scene); // create a scene from a shape scene_data make_shape_scene(const shape_data& shape, bool add_sky = false); +// Initialize scene +scene_data make_scene(); + +// Scene creation helpers +int add_camera( + scene_data& scene, const string& name, const camera_data& camera); +int add_texture( + scene_data& scene, const string& name, const texture_data& texture); +int add_material( + scene_data& scene, const string& name, const material_data& material); +int add_shape( + scene_data& scene, const string& name, const shape_data& material); +int add_instance( + scene_data& scene, const string& name, const instance_data& instance); +int add_environment( + scene_data& scene, const string& name, const environment_data& environment); + +// Scene creation helpers +int add_camera(scene_data& scene, const string& name, const vec3f& from, + const vec3f& to, float lens = 0.1f, float aspect = 16.0f / 9.0f, + float aperture = 0, float focus_offset = 0); +int add_camera(scene_data& scene, const string& name, const frame3f& frame, + float lens = 0.1f, float aspect = 16.0f / 9.0f, float aperture = 0, + float focus = 1); +int add_material(scene_data& scene, const string& name, const vec3f& emission, + material_type type, const vec3f& color, float roughness = 0, + int color_tex = invalidid, int roughness_tex = invalidid, + int normal_tex = invalidid); +int add_material(scene_data& scene, const string& name, material_type type, + const vec3f& color, float roughness = 0, int color_tex = invalidid, + int roughness_tex = invalidid, int normal_tex = invalidid); +int add_material(scene_data& scene, const string& name, material_type type, + const vec3f& color, float roughness, const vec3f& scattering, + float scanisotropy = 0, float trdepth = 0.01f, int color_tex = invalidid, + int roughness_tex = invalidid, int scattering_tex = invalidid, + int normal_tex = invalidid); +int add_instance(scene_data& scene, const string& name, const frame3f& frame, + int shape, int material); +int add_instance(scene_data& scene, const string& name, const frame3f& frame, + const shape_data& shape, const material_data& material); +int add_environment(scene_data& scene, const string& name, const frame3f& frame, + const vec3f& emission, int emission_tex = invalidid); +int add_texture(scene_data& scene, const string& name, + const image_t& texture, bool linear = true); +int add_texture(scene_data& scene, const string& name, + const image_t& texture, bool linear = false); + // Return scene statistics as list of strings. vector scene_stats(const scene_data& scene, bool verbose = false); // Return validation errors as list of strings. diff --git a/libs/yocto/yocto_sceneio.cpp b/libs/yocto/yocto_sceneio.cpp index 4598ccd6b..6545fba8b 100644 --- a/libs/yocto/yocto_sceneio.cpp +++ b/libs/yocto/yocto_sceneio.cpp @@ -76,49 +76,87 @@ namespace yocto { // Simple parallel for used since our target platforms do not yet support // parallel algorithms. `Func` takes the integer index. template -inline bool parallel_for(T num, string& error, Func&& func) { - auto futures = vector>{}; - auto nthreads = std::thread::hardware_concurrency(); - std::atomic next_idx(0); - std::atomic has_error(false); - std::mutex error_mutex; - for (auto thread_id = 0; thread_id < (int)nthreads; thread_id++) { - futures.emplace_back(std::async(std::launch::async, - [&func, &next_idx, &has_error, &error_mutex, &error, num]() { - auto this_error = string{}; - while (true) { - if (has_error) break; - auto idx = next_idx.fetch_add(1); - if (idx >= num) break; - if (!func(idx, this_error)) { +inline void parallel_for(T num, bool noparallel, Func&& func) { + if (noparallel) { + for (auto idx : range(num)) { + func(idx); + } + } else { + auto futures = vector>{}; + auto nthreads = std::thread::hardware_concurrency(); + std::atomic next_idx(0); + std::atomic has_error(false); + for (auto thread_id = 0; thread_id < (int)nthreads; thread_id++) { + futures.emplace_back( + std::async(std::launch::async, [&func, &next_idx, &has_error, num]() { + while (true) { + if (has_error) break; + auto idx = next_idx.fetch_add(1); + if (idx >= num) break; + try { + func(idx); + } catch (std::exception& error) { + has_error = true; + throw; + } + } + })); + } + for (auto& f : futures) f.get(); + } +} + +// Simple parallel for used since our target platforms do not yet support +// parallel algorithms. `Func` takes the integer index. +template +inline void parallel_zip(Sequence1&& sequence1, Sequence2&& sequence2, + bool noparallel, Func&& func) { + if (std::size(sequence1) != std::size(sequence2)) + throw std::out_of_range{"invalid sequence lengths"}; + if (noparallel) { + for (auto idx : range(sequence1.size())) { + func(std::forward(sequence1)[idx], + std::forward(sequence2)[idx]); + } + } else { + auto num = std::size(sequence1); + auto futures = vector>{}; + auto nthreads = std::thread::hardware_concurrency(); + std::atomic next_idx(0); + std::atomic has_error(false); + for (auto thread_id = 0; thread_id < (int)nthreads; thread_id++) { + futures.emplace_back(std::async(std::launch::async, + [&func, &next_idx, &has_error, num, &sequence1, &sequence2]() { + try { + while (true) { + auto idx = next_idx.fetch_add(1); + if (idx >= num) break; + if (has_error) break; + func(std::forward(sequence1)[idx], + std::forward(sequence2)[idx]); + } + } catch (...) { has_error = true; - auto _ = std::lock_guard{error_mutex}; - error = this_error; - break; + throw; } - } - })); + })); + } + for (auto& f : futures) f.get(); } - for (auto& f : futures) f.get(); - return !(bool)has_error; } // Simple parallel for used since our target platforms do not yet support // parallel algorithms. `Func` takes a reference to a `T`. template -inline bool parallel_foreach(vector& values, string& error, Func&& func) { - return parallel_for( - values.size(), error, [&func, &values](size_t idx, string& error) { - return func(values[idx], error); - }); +inline void parallel_foreach(vector& values, bool noparallel, Func&& func) { + return parallel_for(values.size(), noparallel, + [&func, &values](size_t idx) { return func(values[idx]); }); } template -inline bool parallel_foreach( - const vector& values, string& error, Func&& func) { - return parallel_for( - values.size(), error, [&func, &values](size_t idx, string& error) { - return func(values[idx], error); - }); +inline void parallel_foreach( + const vector& values, bool noparallel, Func&& func) { + return parallel_for(values.size(), noparallel, + [&func, &values](size_t idx) { return func(values[idx]); }); } } // namespace yocto @@ -198,67 +236,6 @@ static string path_join( } // namespace yocto -// ----------------------------------------------------------------------------- -// FILE WATCHER -// ----------------------------------------------------------------------------- -namespace yocto { - -// Initialize file watcher -watch_context make_watch_context(const vector& filenames, int delay) { - return {{0}, {}, filenames, vector(filenames.size(), 0), - (int64_t)delay, {false}}; -} - -// Start file watcher -void watch_start(watch_context& context) { - // stop - if (context.worker.valid()) context.worker.get(); - context.stop = false; - - // initialize file times - for (auto index : range(context.filenames.size())) { - auto time = std::filesystem::last_write_time(context.filenames[index]) - .time_since_epoch() - .count(); - context.filetimes[index] = (int64_t)time; - } - - // start watcher - context.worker = std::async(std::launch::async, [&]() { - // until done - while (!context.stop) { - // sleep - std::this_thread::sleep_for(std::chrono::milliseconds(context.delay)); - - // check times - auto changed = false; - for (auto index : range(context.filenames.size())) { - auto time = std::filesystem::last_write_time(context.filenames[index]) - .time_since_epoch() - .count(); - if ((int64_t)time != context.filetimes[index]) { - changed = true; - context.filetimes[index] = (int64_t)time; - } - } - - // update version - if (changed) context.version++; - } - }); -} - -// Stop file watcher -void watch_stop(watch_context& context) { - context.stop = true; - if (context.worker.valid()) context.worker.get(); -} - -// Got file versions -int get_version(const watch_context& context) { return context.version; } - -} // namespace yocto - // ----------------------------------------------------------------------------- // FILE IO // ----------------------------------------------------------------------------- @@ -287,115 +264,72 @@ FILE* fopen_utf8(const string& filename, const string& mode) { #endif } -// Load a text file +// Load a binary file string load_text(const string& filename) { - auto error = string{}; - auto str = string{}; - if (!load_text(filename, str, error)) throw io_error{error}; + auto str = string{}; + load_text(filename, str); return str; } -void load_text(const string& filename, string& text) { - auto error = string{}; - if (!load_text(filename, text, error)) throw io_error{error}; -} - -// Save a text file -void save_text(const string& filename, const string& text) { - auto error = string{}; - if (!save_text(filename, text, error)) throw io_error{error}; -} - -// Load a binary file -vector load_binary(const string& filename) { - auto error = string{}; - auto data = vector{}; - if (!load_binary(filename, data, error)) throw io_error{error}; - return data; -} -void load_binary(const string& filename, vector& data) { - auto error = string{}; - if (!load_binary(filename, data, error)) throw io_error{error}; -} - -// Save a binary file -void save_binary(const string& filename, const vector& data) { - auto error = string{}; - if (!save_binary(filename, data, error)) throw io_error{error}; -} // Load a text file -bool load_text(const string& filename, string& str, string& error) { +void load_text(const string& filename, string& str) { // https://stackoverflow.com/questions/174531/how-to-read-the-content-of-a-file-to-a-string-in-c auto fs = fopen_utf8(filename.c_str(), "rb"); - if (!fs) { - error = "cannot open " + filename; - return false; - } + if (fs == nullptr) throw io_error("cannot open " + filename); fseek(fs, 0, SEEK_END); auto length = ftell(fs); fseek(fs, 0, SEEK_SET); - str.resize(length); + str = string(length, '\0'); if (fread(str.data(), 1, length, fs) != length) { fclose(fs); - error = "cannot read " + filename; - return false; + throw io_error("cannot read " + filename); } fclose(fs); - return true; } // Save a text file -bool save_text(const string& filename, const string& str, string& error) { +void save_text(const string& filename, const string& str) { auto fs = fopen_utf8(filename.c_str(), "wt"); - if (!fs) { - error = "cannot create " + filename; - return false; - } + if (fs == nullptr) throw io_error("cannot create " + filename); if (fprintf(fs, "%s", str.c_str()) < 0) { fclose(fs); - error = "cannot write " + filename; - return false; + throw io_error("cannot write " + filename); } fclose(fs); - return true; } // Load a binary file -bool load_binary(const string& filename, vector& data, string& error) { +vector load_binary(const string& filename) { + auto data = vector{}; + load_binary(filename, data); + return data; +} + +// Load a binary file +void load_binary(const string& filename, vector& data) { // https://stackoverflow.com/questions/174531/how-to-read-the-content-of-a-file-to-a-string-in-c auto fs = fopen_utf8(filename.c_str(), "rb"); - if (!fs) { - error = "cannot open " + filename; - return false; - } + if (fs == nullptr) throw io_error("cannot open " + filename); fseek(fs, 0, SEEK_END); auto length = ftell(fs); fseek(fs, 0, SEEK_SET); - data.resize(length); + data = vector(length); if (fread(data.data(), 1, length, fs) != length) { fclose(fs); - error = "cannot read " + filename; - return false; + throw io_error("cannot read " + filename); } fclose(fs); - return true; } // Save a binary file -bool save_binary( - const string& filename, const vector& data, string& error) { +void save_binary(const string& filename, const vector& data) { auto fs = fopen_utf8(filename.c_str(), "wb"); - if (!fs) { - error = "cannot create " + filename; - return false; - } + if (fs == nullptr) throw io_error("cannot create " + filename); if (fwrite(data.data(), 1, data.size(), fs) != data.size()) { fclose(fs); - error = "cannot write " + filename; - return false; + throw io_error("cannot write " + filename); } fclose(fs); - return true; } } // namespace yocto @@ -409,38 +343,25 @@ namespace yocto { using json_value = nlohmann::ordered_json; // Load/save json -static bool load_json(const string& filename, json_value& json, string& error) { - auto text = string{}; - if (!load_text(filename, text, error)) return false; - try { - json = json_value::parse(text); - return true; - } catch (...) { - error = "cannot parse " + filename; - return false; - } -} -static bool save_json( - const string& filename, const json_value& json, string& error) { - return save_text(filename, json.dump(2), error); -} - -// Load/save json +[[maybe_unused]] static void load_json( + const string& filename, json_value& json); [[maybe_unused]] static json_value load_json(const string& filename) { - auto error = string{}; - auto json = json_value{}; - if (!load_json(filename, json, error)) throw io_error{error}; + auto json = json_value{}; + load_json(filename, json); return json; } [[maybe_unused]] static void load_json( const string& filename, json_value& json) { - auto error = string{}; - if (!load_json(filename, json, error)) throw io_error{error}; + auto text = load_text(filename); + try { + json = json_value::parse(text); + } catch (...) { + throw io_error("cannot parse " + filename); + } } [[maybe_unused]] static void save_json( const string& filename, const json_value& json) { - auto error = string{}; - if (!save_json(filename, json, error)) throw io_error{error}; + return save_text(filename, json.dump(2)); } // Json conversions @@ -531,495 +452,306 @@ bool is_ldr_filename(const string& filename) { ext == ".tga"; } -// Loads/saves an image. Chooses hdr or ldr based on file name. -bool load_image(const string& filename, image_data& image, string& error) { - auto read_error = [&]() { - error = "cannot read " + filename; - return false; - }; +// Loads a float image. +image_t load_image(const string& filename, bool srgb) { + auto image_ = image_t{}; + load_image(filename, image_, srgb); + return image_; +} - // conversion helpers - auto from_linear = [](const float* pixels, int width, int height) { - if (pixels == nullptr) return vector{}; - return vector{ - (vec4f*)pixels, (vec4f*)pixels + (size_t)width * (size_t)height}; - }; - auto from_srgb = [](const byte* pixels, int width, int height) { - if (pixels == nullptr) return vector{}; - auto pixelsf = vector((size_t)width * (size_t)height); - for (auto idx : range(pixelsf.size())) { - pixelsf[idx] = byte_to_float(((vec4b*)pixels)[idx]); - } - return pixelsf; - }; +// Loads a byte image. +image_t load_imageb(const string& filename, bool srgb) { + auto image_ = image_t{}; + load_image(filename, image_, srgb); + return image_; +} +// Loads a float image. +void load_image(const string& filename, image_t& image, bool srgb) { auto ext = path_extension(filename); if (ext == ".exr" || ext == ".EXR") { - auto buffer = vector{}; - if (!load_binary(filename, buffer, error)) return false; + auto buffer = load_binary(filename); + auto width = 0, height = 0; auto pixels = (float*)nullptr; - if (LoadEXRFromMemory(&pixels, &image.width, &image.height, buffer.data(), + if (LoadEXRFromMemory(&pixels, &width, &height, buffer.data(), buffer.size(), nullptr) != 0) - return read_error(); - image.linear = true; - image.pixels = from_linear(pixels, image.width, image.height); + throw io_error{"cannot read " + filename}; + image = {{width, height}, (vec4f*)pixels}; + if (srgb) image = rgb_to_srgb(image); free(pixels); - return true; } else if (ext == ".hdr" || ext == ".HDR") { - auto buffer = vector{}; - if (!load_binary(filename, buffer, error)) return false; - auto ncomp = 0; - auto pixels = stbi_loadf_from_memory(buffer.data(), (int)buffer.size(), - &image.width, &image.height, &ncomp, 4); - if (!pixels) return read_error(); - image.linear = true; - image.pixels = from_linear(pixels, image.width, image.height); - free(pixels); - return true; - } else if (ext == ".png" || ext == ".PNG") { - auto buffer = vector{}; - if (!load_binary(filename, buffer, error)) return false; - auto ncomp = 0; - auto pixels = stbi_load_from_memory(buffer.data(), (int)buffer.size(), - &image.width, &image.height, &ncomp, 4); - if (!pixels) return read_error(); - image.linear = false; - image.pixels = from_srgb(pixels, image.width, image.height); - free(pixels); - return true; - } else if (ext == ".jpg" || ext == ".JPG" || ext == ".jpeg" || - ext == ".JPEG") { - auto buffer = vector{}; - if (!load_binary(filename, buffer, error)) return false; - auto ncomp = 0; - auto pixels = stbi_load_from_memory(buffer.data(), (int)buffer.size(), - &image.width, &image.height, &ncomp, 4); - if (!pixels) return read_error(); - image.linear = false; - image.pixels = from_srgb(pixels, image.width, image.height); - free(pixels); - return true; - } else if (ext == ".tga" || ext == ".TGA") { - auto buffer = vector{}; - if (!load_binary(filename, buffer, error)) return false; - auto ncomp = 0; - auto pixels = stbi_load_from_memory(buffer.data(), (int)buffer.size(), - &image.width, &image.height, &ncomp, 4); - if (!pixels) return read_error(); - image.linear = false; - image.pixels = from_srgb(pixels, image.width, image.height); - free(pixels); - return true; - } else if (ext == ".bmp" || ext == ".BMP") { - auto buffer = vector{}; - if (!load_binary(filename, buffer, error)) return false; - auto ncomp = 0; - auto pixels = stbi_load_from_memory(buffer.data(), (int)buffer.size(), - &image.width, &image.height, &ncomp, 4); - if (!pixels) return read_error(); - image.linear = false; - image.pixels = from_srgb(pixels, image.width, image.height); + auto buffer = load_binary(filename); + auto width = 0, height = 0, ncomp = 0; + auto pixels = stbi_loadf_from_memory( + buffer.data(), (int)buffer.size(), &width, &height, &ncomp, 4); + if (pixels == nullptr) throw io_error{"cannot read " + filename}; + image = {{width, height}, (vec4f*)pixels}; + if (srgb) image = rgb_to_srgb(image); free(pixels); - return true; + } else if (ext == ".png" || ext == ".PNG" || ext == ".jpg" || ext == ".JPG" || + ext == ".jpeg" || ext == ".JPEG" || ext == ".tga" || + ext == ".TGA" || ext == ".bmp" || ext == ".BMP") { + auto imageb = load_imageb(filename); + image = srgb ? byte_to_float(imageb) : srgbb_to_rgb(imageb); } else if (ext == ".ypreset" || ext == ".YPRESET") { - // create preset - if (!make_image_preset(filename, image, error)) return false; - return true; + image = make_image_preset(filename); + if (is_srgb_preset(filename) != srgb) { + image = srgb ? rgb_to_srgb(image) : srgb_to_rgb(image); + } } else { - error = "unsupported format " + filename; - return false; + throw io_error{"unsupported format " + filename}; } } -// Saves an hdr image. -bool save_image( - const string& filename, const image_data& image, string& error) { - auto write_error = [&]() { - error = "cannot write " + filename; - return false; - }; - - // conversion helpers - auto to_linear = [](const image_data& image) { - if (image.linear) return image.pixels; - auto pixelsf = vector(image.pixels.size()); - srgb_to_rgb(pixelsf, image.pixels); - return pixelsf; - }; - auto to_srgb = [](const image_data& image) { - auto pixelsb = vector(image.pixels.size()); - if (image.linear) { - rgb_to_srgb(pixelsb, image.pixels); +// Loads a byte image. +void load_image(const string& filename, image_t& image, bool srgb) { + auto ext = path_extension(filename); + if (ext == ".exr" || ext == ".EXR" || ext == ".hdr" || ext == ".HDR") { + auto imagef = load_image(filename); + image = srgb ? rgb_to_srgbb(imagef) : float_to_byte(imagef); + } else if (ext == ".png" || ext == ".PNG" || ext == ".jpg" || ext == ".JPG" || + ext == ".jpeg" || ext == ".JPEG" || ext == ".tga" || + ext == ".TGA" || ext == ".bmp" || ext == ".BMP") { + auto buffer = load_binary(filename); + auto width = 0, height = 0, ncomp = 0; + auto pixels = stbi_load_from_memory( + buffer.data(), (int)buffer.size(), &width, &height, &ncomp, 4); + if (pixels == nullptr) throw io_error{"cannot read " + filename}; + image = {{width, height}, (vec4b*)pixels}; + if (!srgb) image = float_to_byte(srgbb_to_rgb(image)); + free(pixels); + } else if (ext == ".ypreset" || ext == ".YPRESET") { + auto imagef = load_image(filename); + if (is_srgb_preset(filename) != srgb) { + image = srgb ? rgb_to_srgbb(imagef) : float_to_byte(srgb_to_rgb(imagef)); } else { - float_to_byte(pixelsb, image.pixels); + image = float_to_byte(imagef); } - return pixelsb; - }; - - // write data - auto stbi_write_data = [](void* context, void* data, int size) { - auto& buffer = *(vector*)context; - buffer.insert(buffer.end(), (byte*)data, (byte*)data + size); - }; - - auto ext = path_extension(filename); - if (ext == ".hdr" || ext == ".HDR") { - auto buffer = vector{}; - if (!stbi_write_hdr_to_func(stbi_write_data, &buffer, (int)image.width, - (int)image.height, 4, (const float*)to_linear(image).data())) - return write_error(); - if (!save_binary(filename, buffer, error)) return false; - return true; - } else if (ext == ".exr" || ext == ".EXR") { - auto data = (byte*)nullptr; - auto size = (size_t)0; - if (SaveEXRToMemory((const float*)to_linear(image).data(), (int)image.width, - (int)image.height, 4, 1, &data, &size, nullptr) < 0) - return write_error(); - auto buffer = vector{data, data + size}; - free(data); - if (!save_binary(filename, buffer, error)) return false; - return true; - } else if (ext == ".png" || ext == ".PNG") { - auto buffer = vector{}; - if (!stbi_write_png_to_func(stbi_write_data, &buffer, (int)image.width, - (int)image.height, 4, (const byte*)to_srgb(image).data(), - (int)image.width * 4)) - return write_error(); - if (!save_binary(filename, buffer, error)) return false; - return true; - } else if (ext == ".jpg" || ext == ".JPG" || ext == ".jpeg" || - ext == ".JPEG") { - auto buffer = vector{}; - if (!stbi_write_jpg_to_func(stbi_write_data, &buffer, (int)image.width, - (int)image.height, 4, (const byte*)to_srgb(image).data(), 75)) - return write_error(); - if (!save_binary(filename, buffer, error)) return false; - return true; - } else if (ext == ".tga" || ext == ".TGA") { - auto buffer = vector{}; - if (!stbi_write_tga_to_func(stbi_write_data, &buffer, (int)image.width, - (int)image.height, 4, (const byte*)to_srgb(image).data())) - return write_error(); - if (!save_binary(filename, buffer, error)) return false; - return true; - } else if (ext == ".bmp" || ext == ".BMP") { - auto buffer = vector{}; - if (!stbi_write_bmp_to_func(stbi_write_data, &buffer, (int)image.width, - (int)image.height, 4, (const byte*)to_srgb(image).data())) - return write_error(); - if (!save_binary(filename, buffer, error)) return false; - return true; } else { - error = "unsupported format " + filename; - return false; + throw io_error{"unsupported format " + filename}; } } -image_data make_image_preset(const string& type_) { - auto type = path_basename(type_); - auto width = 1024, height = 1024; - if (type.find("sky") != type.npos) width = 2048; - if (type.find("images2") != type.npos) width = 2048; +bool is_srgb_preset(const string& type_) { + auto type = path_basename(type_); + return type.find("sky") == string::npos; +} +image_t make_image_preset(const string& type_) { + auto type = path_basename(type_); + auto extents = vec2i{1024, 1024}; + if (type.find("sky") != string::npos) extents = {2048, 1024}; + if (type.find("images2") != string::npos) extents = {2048, 1024}; if (type == "grid") { - return make_grid(width, height); + return make_grid(extents); } else if (type == "checker") { - return make_checker(width, height); + return make_checker(extents); } else if (type == "bumps") { - return make_bumps(width, height); + return make_bumps(extents); } else if (type == "uvramp") { - return make_uvramp(width, height); + return make_uvramp(extents); } else if (type == "gammaramp") { - return make_gammaramp(width, height); - } else if (type == "blackbodyramp") { - return make_blackbodyramp(width, height); + return make_gammaramp(extents); } else if (type == "uvgrid") { - return make_uvgrid(width, height); + return make_uvgrid(extents); } else if (type == "colormapramp") { - return make_colormapramp(width, height); + return make_colormapramp(extents); } else if (type == "sky") { - return make_sunsky(width, height, pif / 4, 3.0f, false, 1.0f, 1.0f, - vec3f{0.7f, 0.7f, 0.7f}); + return make_sunsky( + extents, pif / 4, 3.0f, false, 1.0f, 1.0f, vec3f{0.7f, 0.7f, 0.7f}); } else if (type == "sunsky") { - return make_sunsky(width, height, pif / 4, 3.0f, true, 1.0f, 1.0f, - vec3f{0.7f, 0.7f, 0.7f}); - } else if (type == "noise") { - return make_noisemap(width, height, 1); - } else if (type == "fbm") { - return make_fbmmap(width, height, 1); - } else if (type == "ridge") { - return make_ridgemap(width, height, 1); - } else if (type == "turbulence") { - return make_turbulencemap(width, height, 1); + return make_sunsky( + extents, pif / 4, 3.0f, true, 1.0f, 1.0f, vec3f{0.7f, 0.7f, 0.7f}); + } else if (type == "gnoisemap") { + return make_noisemap(extents, 1.0f); + } else if (type == "fnoisemap") { + return make_fbmmap(extents, 1.0f); + } else if (type == "rnoisemap") { + return make_ridgemap(extents, 1.0f); + } else if (type == "tnoisemap") { + return make_turbulencemap(extents, 1.0f); } else if (type == "bump-normal") { - return make_bumps(width, height); + return make_bumps(extents); // TODO(fabio): fix color space // img = srgb_to_rgb(bump_to_normal(img, 0.05f)); } else if (type == "images1") { auto sub_types = vector{"grid", "uvgrid", "checker", "gammaramp", "bumps", "bump-normal", "noise", "fbm", "blackbodyramp"}; - auto sub_images = vector(); + auto sub_images = vector>(); for (auto& sub_type : sub_types) sub_images.push_back(make_image_preset(sub_type)); auto montage_size = vec2i{0, 0}; for (auto& sub_image : sub_images) { - montage_size.x += sub_image.width; - montage_size.y = max(montage_size.y, sub_image.height); + montage_size = {montage_size.x + sub_image.size().x, + max(montage_size.y, sub_image.size().y)}; } - auto image = make_image( - montage_size.x, montage_size.y, sub_images[0].linear); - auto pos = 0; + auto composite = image_t(montage_size); + auto pos = 0; for (auto& sub_image : sub_images) { - set_region(image, sub_image, pos, 0); - pos += sub_image.width; + set_region(composite, sub_image, {pos, 0}); + pos += sub_image.size().x; } - return image; + return composite; } else if (type == "images2") { auto sub_types = vector{"sky", "sunsky"}; - auto sub_images = vector(); + auto sub_images = vector>(); for (auto& sub_type : sub_types) sub_images.push_back(make_image_preset(sub_type)); auto montage_size = vec2i{0, 0}; for (auto& sub_image : sub_images) { - montage_size.x += sub_image.width; - montage_size.y = max(montage_size.y, sub_image.height); + montage_size = {montage_size.x + sub_image.size().x, + max(montage_size.y, sub_image.size().y)}; } - auto image = make_image( - montage_size.x, montage_size.y, sub_images[0].linear); - auto pos = 0; + auto composite = image_t(montage_size); + auto pos = 0; for (auto& sub_image : sub_images) { - set_region(image, sub_image, pos, 0); - pos += sub_image.width; + set_region(composite, sub_image, {pos, 0}); + pos += sub_image.size().x; } - return image; + return composite; } else if (type == "test-floor") { - return add_border(make_grid(width, height), 0.0025f); + return add_border(make_grid(extents), 0.0025f); } else if (type == "test-grid") { - return make_grid(width, height); + return make_grid(extents); } else if (type == "test-checker") { - return make_checker(width, height); + return make_checker(extents); } else if (type == "test-bumps") { - return make_bumps(width, height); + return make_bumps(extents); } else if (type == "test-uvramp") { - return make_uvramp(width, height); + return make_uvramp(extents); } else if (type == "test-gammaramp") { - return make_gammaramp(width, height); - } else if (type == "test-blackbodyramp") { - return make_blackbodyramp(width, height); + return make_gammaramp(extents); } else if (type == "test-colormapramp") { - return make_colormapramp(width, height); + return make_colormapramp(extents); // TODO(fabio): fix color space // img = srgb_to_rgb(img); } else if (type == "test-uvgrid") { - return make_uvgrid(width, height); + return make_uvgrid(extents); } else if (type == "test-sky") { - return make_sunsky(width, height, pif / 4, 3.0f, false, 1.0f, 1.0f, - vec3f{0.7f, 0.7f, 0.7f}); + return make_sunsky( + extents, pif / 4, 3.0f, false, 1.0f, 1.0f, vec3f{0.7f, 0.7f, 0.7f}); } else if (type == "test-sunsky") { - return make_sunsky(width, height, pif / 4, 3.0f, true, 1.0f, 1.0f, - vec3f{0.7f, 0.7f, 0.7f}); - } else if (type == "test-noise") { - return make_noisemap(width, height); - } else if (type == "test-fbm") { - return make_noisemap(width, height); + return make_sunsky( + extents, pif / 4, 3.0f, true, 1.0f, 1.0f, vec3f{0.7f, 0.7f, 0.7f}); } else if (type == "test-bumps-normal") { - return bump_to_normal(make_bumps(width, height), 0.05f); + return bump_to_normal(make_bumps(extents), 0.05f); } else if (type == "test-bumps-displacement") { - return make_bumps(width, height); - // TODO(fabio): fix color space - // img = srgb_to_rgb(img); - } else if (type == "test-fbm-displacement") { - return make_fbmmap(width, height); + return make_bumps(extents); // TODO(fabio): fix color space // img = srgb_to_rgb(img); } else if (type == "test-checker-opacity") { - return make_checker(width, height, 1, {1, 1, 1, 1}, {0, 0, 0, 0}); + return make_checker(extents, 1.0f, vec4f{1, 1, 1, 1}, vec4f{0, 0, 0, 0}); } else if (type == "test-grid-opacity") { - return make_grid(width, height, 1, {1, 1, 1, 1}, {0, 0, 0, 0}); + return make_grid(extents, 1.0f, vec4f{1, 1, 1, 1}, vec4f{0, 0, 0, 0}); } else { - return {}; + throw io_error{"unknown preset " + type}; } } -// Loads/saves an image. Chooses hdr or ldr based on file name. -image_data load_image(const string& filename, string& error) { - auto image = image_data{}; - if (!load_image(filename, image, error)) return image_data{}; - return image; -} -image_data load_image(const string& filename) { - auto error = string{}; - auto image = image_data{}; - if (!load_image(filename, image, error)) throw io_error{error}; - return image; -} -void load_image(const string& filename, image_data& image) { - auto error = string{}; - if (!load_image(filename, image, error)) throw io_error{error}; -} -void save_image(const string& filename, const image_data& image) { - auto error = string{}; - if (!save_image(filename, image, error)) throw io_error{error}; -} - -bool make_image_preset( - const string& filename, image_data& image, string& error) { - image = make_image_preset(path_basename(filename)); - if (image.pixels.empty()) { - error = "unknown preset"; - return false; - } - return true; -} - -} // namespace yocto - -#if 0 - -// ----------------------------------------------------------------------------- -// IMPLEMENTATION FOR VOLUME IMAGE IO -// ----------------------------------------------------------------------------- -namespace yocto { - -// Volume load -static bool load_yvol(const string& filename, int& width, int& height, - int& depth, int& components, vector& voxels, string& error) { - // error helpers - auto open_error = [filename, &error]() { - error = "cannot open " + filename; - return false; - }; - auto parse_error = [filename, &error]() { - error = "cannot parse " + filename; - return false; - }; - auto read_error = [filename, &error]() { - error = "cannot read " + filename; - return false; - }; - - // Split a string - auto split_string = [](const string& str) -> vector { - auto ret = vector(); - if (str.empty()) return ret; - auto lpos = (size_t)0; - while (lpos != string::npos) { - auto pos = str.find_first_of(" \t\n\r", lpos); - if (pos != string::npos) { - if (pos > lpos) ret.push_back(str.substr(lpos, pos - lpos)); - lpos = pos + 1; - } else { - if (lpos < str.size()) ret.push_back(str.substr(lpos)); - lpos = pos; - } - } - return ret; - }; - - auto fs = fopen_utf8(filename.c_str(), "rb"); - auto fs_guard = unique_ptr(fs, &fclose); - if (!fs) return open_error(); - - // buffer - auto buffer = array{}; - auto toks = vector(); - - // read magic - if (!fgets(buffer.data(), (int)buffer.size(), fs)) return parse_error(); - toks = split_string(buffer.data()); - if (toks[0] != "YVOL") return parse_error(); - - // read width, height - if (!fgets(buffer.data(), (int)buffer.size(), fs)) return parse_error(); - toks = split_string(buffer.data()); - width = atoi(toks[0].c_str()); - height = atoi(toks[1].c_str()); - depth = atoi(toks[2].c_str()); - components = atoi(toks[3].c_str()); - - // read data - auto nvoxels = (size_t)width * (size_t)height * (size_t)depth; - auto nvalues = nvoxels * (size_t)components; - voxels = vector(nvalues); - if (!read_values(fs, voxels.data(), nvalues)) return read_error(); - - // done - return true; -} - -// save pfm -static bool save_yvol(const string& filename, int width, int height, int depth, - int components, const vector& voxels, string& error) { - // error helpers - auto open_error = [filename, &error]() { - error = "cannot create " + filename; - return false; - }; - auto write_error = [filename, &error]() { - error = "cannot read " + filename; - return false; +// Saves a float image. +void save_image( + const string& filename, const image_t& image, bool srgb) { + // write data + auto stbi_write_data = [](void* context, void* data, int size) { + auto& buffer = *(vector*)context; + buffer.insert(buffer.end(), (byte*)data, (byte*)data + size); }; - auto fs = fopen_utf8(filename.c_str(), "wb"); - auto fs_guard = unique_ptr(fs, &fclose); - if (!fs) return open_error(); + // grab data for low level apis + auto [width, height] = image.size(); + auto num_channels = 4; - if (!write_text(fs, "YVOL\n")) return write_error(); - if (!write_text(fs, std::to_string(width) + " " + std::to_string(height) + - " " + std::to_string(depth) + " " + - std::to_string(components) + "\n")) - return write_error(); - auto nvalues = (size_t)width * (size_t)height * (size_t)depth * - (size_t)components; - if (!write_values(fs, voxels.data(), nvalues)) return write_error(); - return true; + auto ext = path_extension(filename); + if (ext == ".hdr" || ext == ".HDR") { + auto& image_ = srgb ? srgb_to_rgb(image) : image; + auto buffer = vector{}; + if (!(bool)stbi_write_hdr_to_func(stbi_write_data, &buffer, width, height, + num_channels, (const float*)image_.data())) + throw io_error{"cannot write " + filename}; + return save_binary(filename, buffer); + } else if (ext == ".exr" || ext == ".EXR") { + auto& image_ = srgb ? srgb_to_rgb(image) : image; + auto data = (byte*)nullptr; + auto count = (size_t)0; + if (SaveEXRToMemory((const float*)image_.data(), width, height, + num_channels, 1, &data, &count, nullptr) < 0) + throw io_error{"cannot write " + filename}; + auto buffer = vector{data, data + count}; + free(data); + return save_binary(filename, buffer); + } else if (ext == ".png" || ext == ".PNG" || ext == ".jpg" || ext == ".JPG" || + ext == ".jpeg" || ext == ".JPEG" || ext == ".tga" || + ext == ".TGA" || ext == ".bmp" || ext == ".BMP") { + save_image(filename, srgb ? float_to_byte(image) : rgb_to_srgbb(image)); + } else { + throw io_error{"unsupported format " + filename}; + } } -// Loads volume data from binary format. -bool load_volume(const string& filename, volume& vol, string& error) { - auto read_error = [filename, &error]() { - error = "cannot read " + filename; - return false; +// Saves a byte image. +void save_image( + const string& filename, const image_t& image, bool srgb) { + // write data + auto stbi_write_data = [](void* context, void* data, int size) { + auto& buffer = *(vector*)context; + buffer.insert(buffer.end(), (byte*)data, (byte*)data + size); }; - auto width = 0, height = 0, depth = 0, ncomp = 0; - auto voxels = vector{}; - if (!load_yvol(filename, width, height, depth, ncomp, voxels, error)) - return false; - if (ncomp != 1) voxels = convert_components(voxels, ncomp, 1); - vol = volume{{width, height, depth}, (const float*)voxels.data()}; - return true; -} -// Saves volume data in binary format. -bool save_volume( - const string& filename, const volume& vol, string& error) { - return save_yvol(filename, vol.width(), vol.height(), vol.depth(), 1, - {vol.data(), vol.data() + vol.count()}, error); + // grab data for low level apis + auto [width, height] = image.size(); + auto num_channels = 4; + + auto ext = path_extension(filename); + if (ext == ".hdr" || ext == ".HDR" || ext == ".exr" || ext == ".EXR") { + return save_image( + filename, srgb ? srgbb_to_rgb(image) : byte_to_float(image)); + } else if (ext == ".png" || ext == ".PNG") { + auto& image_ = srgb ? image : rgb_to_srgbb(byte_to_float(image)); + auto buffer = vector{}; + if (!(bool)stbi_write_png_to_func(stbi_write_data, &buffer, width, height, + num_channels, (const byte*)image_.data(), width * 4)) + throw io_error{"cannot write " + filename}; + return save_binary(filename, buffer); + } else if (ext == ".jpg" || ext == ".JPG" || ext == ".jpeg" || + ext == ".JPEG") { + auto& image_ = srgb ? image : rgb_to_srgbb(byte_to_float(image)); + auto buffer = vector{}; + if (!(bool)stbi_write_jpg_to_func(stbi_write_data, &buffer, width, height, + num_channels, (const byte*)image_.data(), 75)) + throw io_error{"cannot write " + filename}; + return save_binary(filename, buffer); + } else if (ext == ".tga" || ext == ".TGA") { + auto& image_ = srgb ? image : rgb_to_srgbb(byte_to_float(image)); + auto buffer = vector{}; + if (!(bool)stbi_write_tga_to_func(stbi_write_data, &buffer, width, height, + num_channels, (const byte*)image_.data())) + throw io_error{"cannot write " + filename}; + return save_binary(filename, buffer); + } else if (ext == ".bmp" || ext == ".BMP") { + auto& image_ = srgb ? image : rgb_to_srgbb(byte_to_float(image)); + auto buffer = vector{}; + if (!(bool)stbi_write_bmp_to_func(stbi_write_data, &buffer, width, height, + num_channels, (const byte*)image_.data())) + throw io_error{"cannot write " + filename}; + return save_binary(filename, buffer); + } else { + throw io_error{"unsupported format " + filename}; + } } } // namespace yocto -#endif - // ----------------------------------------------------------------------------- // SHAPE IO // ----------------------------------------------------------------------------- namespace yocto { // Load mesh -bool load_shape(const string& filename, shape_data& shape, string& error, - bool flip_texcoord) { - auto shape_error = [&]() { - error = "empty shape " + filename; - return false; - }; - +void load_shape(const string& filename, shape_data& shape, bool flip_texcoord) { shape = {}; auto ext = path_extension(filename); if (ext == ".ply" || ext == ".PLY") { - auto ply = ply_model{}; - if (!load_ply(filename, ply, error)) return false; + auto ply = load_ply(filename); // TODO: remove when all as arrays get_positions(ply, (vector>&)shape.positions); get_normals(ply, (vector>&)shape.normals); @@ -1033,11 +765,9 @@ bool load_shape(const string& filename, shape_data& shape, string& error, get_points(ply, shape.points); if (shape.points.empty() && shape.lines.empty() && shape.triangles.empty() && shape.quads.empty()) - return shape_error(); - return true; + throw io_error{"empty shape " + filename}; } else if (ext == ".obj" || ext == ".OBJ") { - auto obj = obj_shape{}; - if (!load_obj(filename, obj, error, false)) return false; + auto obj = load_sobj(filename, false); auto materials = vector{}; // TODO: remove when all as arrays get_positions(obj, (vector>&)shape.positions); @@ -1050,35 +780,25 @@ bool load_shape(const string& filename, shape_data& shape, string& error, get_points(obj, shape.points, materials); if (shape.points.empty() && shape.lines.empty() && shape.triangles.empty() && shape.quads.empty()) - return shape_error(); - return true; + throw io_error{"empty shape " + filename}; } else if (ext == ".stl" || ext == ".STL") { - auto stl = stl_model{}; - if (!load_stl(filename, stl, error, true)) return false; - if (stl.shapes.size() != 1) return shape_error(); + auto stl = load_stl(filename, true); + if (stl.shapes.size() != 1) throw io_error{"empty shape " + filename}; auto fnormals = vector{}; if (!get_triangles(stl, 0, (vector>&)shape.triangles, (vector>&)shape.positions, (vector>&)fnormals)) - return shape_error(); - return true; + throw io_error{"empty shape " + filename}; } else if (ext == ".ypreset" || ext == ".YPRESET") { - if (!make_shape_preset(filename, shape, error)) return false; - return true; + shape = make_shape_preset(filename); } else { - error = "unsupported format " + filename; - return false; + throw io_error("unsupported format " + filename); } } // Save ply mesh -bool save_shape(const string& filename, const shape_data& shape, string& error, +void save_shape(const string& filename, const shape_data& shape, bool flip_texcoord, bool ascii) { - auto shape_error = [&]() { - error = "empty shape " + filename; - return false; - }; - auto ext = path_extension(filename); if (ext == ".ply" || ext == ".PLY") { auto ply = ply_model{}; @@ -1093,8 +813,7 @@ bool save_shape(const string& filename, const shape_data& shape, string& error, (const vector>&)shape.quads); add_lines(ply, (const vector>&)shape.lines); add_points(ply, shape.points); - if (!save_ply(filename, ply, error)) return false; - return true; + save_ply(filename, ply); } else if (ext == ".obj" || ext == ".OBJ") { auto obj = obj_shape{}; // TODO: remove when all as arrays @@ -1110,12 +829,11 @@ bool save_shape(const string& filename, const shape_data& shape, string& error, !shape.normals.empty(), !shape.texcoords.empty()); add_points( obj, shape.points, 0, !shape.normals.empty(), !shape.texcoords.empty()); - if (!save_obj(filename, obj, error)) return false; - return true; + save_obj(filename, obj); } else if (ext == ".stl" || ext == ".STL") { auto stl = stl_model{}; - if (!shape.lines.empty()) return shape_error(); - if (!shape.points.empty()) return shape_error(); + if (!shape.lines.empty()) throw io_error{"empty shape " + filename}; + if (!shape.points.empty()) throw io_error{"empty shape " + filename}; if (!shape.triangles.empty()) { add_triangles(stl, (const vector>&)shape.triangles, (const vector>&)shape.positions, {}); @@ -1124,10 +842,9 @@ bool save_shape(const string& filename, const shape_data& shape, string& error, add_triangles(stl, (const vector>&)triangles, (const vector>&)shape.positions, {}); } else { - return shape_error(); + throw io_error{"empty shape " + filename}; } - if (!save_stl(filename, stl, error)) return false; - return true; + save_stl(filename, stl); } else if (ext == ".cpp" || ext == ".CPP") { auto to_cpp = [](const string& name, const string& vname, const auto& values) -> string { @@ -1178,28 +895,20 @@ bool save_shape(const string& filename, const shape_data& shape, string& error, str += to_cpp(name, "lines", shape.lines); str += to_cpp(name, "triangles", shape.triangles); str += to_cpp(name, "quads", shape.quads); - if (!save_text(filename, str, error)) return false; - return true; + save_text(filename, str); } else { - error = "unsupported format " + filename; - return false; + throw io_error("unsupported format " + filename); } } // Load face-varying mesh -bool load_fvshape(const string& filename, fvshape_data& shape, string& error, - bool flip_texcoord) { - auto shape_error = [&]() { - error = "empty shape " + filename; - return false; - }; - +void load_fvshape( + const string& filename, fvshape_data& shape, bool flip_texcoord) { shape = {}; auto ext = path_extension(filename); if (ext == ".ply" || ext == ".PLY") { - auto ply = ply_model{}; - if (!load_ply(filename, ply, error)) return false; + auto ply = load_ply(filename); // TODO: remove when all as arrays get_positions(ply, (vector>&)shape.positions); get_normals(ply, (vector>&)shape.normals); @@ -1208,11 +917,9 @@ bool load_fvshape(const string& filename, fvshape_data& shape, string& error, get_quads(ply, (vector>&)shape.quadspos); if (!shape.normals.empty()) shape.quadsnorm = shape.quadspos; if (!shape.texcoords.empty()) shape.quadstexcoord = shape.quadspos; - if (shape.quadspos.empty()) return shape_error(); - return true; + if (shape.quadspos.empty()) throw io_error{"empty shape " + filename}; } else if (ext == ".obj" || ext == ".OBJ") { - auto obj = obj_shape{}; - if (!load_obj(filename, obj, error, true)) return false; + auto obj = load_sobj(filename, true); // TODO: remove when all as arrays auto materials = vector{}; get_positions(obj, (vector>&)shape.positions); @@ -1222,38 +929,28 @@ bool load_fvshape(const string& filename, fvshape_data& shape, string& error, get_fvquads(obj, (vector>&)shape.quadspos, (vector>&)shape.quadsnorm, (vector>&)shape.quadstexcoord, materials); - if (shape.quadspos.empty()) return shape_error(); - return true; + if (shape.quadspos.empty()) throw io_error{"empty shape " + filename}; } else if (ext == ".stl" || ext == ".STL") { - auto stl = stl_model{}; - if (!load_stl(filename, stl, error, true)) return false; - if (stl.shapes.empty()) return shape_error(); - if (stl.shapes.size() > 1) return shape_error(); + auto stl = load_stl(filename, true); + if (stl.shapes.empty()) throw io_error{"empty shape " + filename}; + if (stl.shapes.size() > 1) throw io_error{"empty shape " + filename}; auto fnormals = vector{}; auto triangles = vector{}; if (!get_triangles(stl, 0, (vector>&)triangles, (vector>&)shape.positions, (vector>&)fnormals)) - return shape_error(); + throw io_error{"empty shape " + filename}; shape.quadspos = triangles_to_quads(triangles); - return true; } else if (ext == ".ypreset" || ext == ".YPRESET") { - if (!make_fvshape_preset(filename, shape, error)) return false; - return true; + shape = make_fvshape_preset(filename); } else { - error = "unsupported format " + filename; - return false; + throw io_error("unsupported format " + filename); } } // Save ply mesh -bool save_fvshape(const string& filename, const fvshape_data& shape, - string& error, bool flip_texcoord, bool ascii) { - auto shape_error = [&]() { - error = "empty shape " + filename; - return false; - }; - +void save_fvshape(const string& filename, const fvshape_data& shape, + bool flip_texcoord, bool ascii) { auto ext = path_extension(filename); if (ext == ".ply" || ext == ".PLY") { auto ply = ply_model{}; @@ -1270,8 +967,7 @@ bool save_fvshape(const string& filename, const fvshape_data& shape, add_texcoords( ply, (const vector>&)split_texcoords, flip_texcoord); add_quads(ply, (const vector>&)split_quads); - if (!save_ply(filename, ply, error)) return false; - return true; + save_ply(filename, ply); } else if (ext == ".obj" || ext == ".OBJ") { auto obj = obj_shape{}; // TODO: remove when all as arrays @@ -1282,8 +978,7 @@ bool save_fvshape(const string& filename, const fvshape_data& shape, add_fvquads(obj, (const vector>&)shape.quadspos, (const vector>&)shape.quadsnorm, (const vector>&)shape.quadstexcoord, 0); - if (!save_obj(filename, obj, error)) return false; - return true; + save_obj(filename, obj); } else if (ext == ".stl" || ext == ".STL") { auto stl = stl_model{}; if (!shape.quadspos.empty()) { @@ -1298,10 +993,9 @@ bool save_fvshape(const string& filename, const fvshape_data& shape, add_triangles(stl, (const vector>&)triangles, (const vector>&)split_positions, {}); } else { - return shape_error(); + throw io_error{"empty shape " + filename}; } - if (!save_stl(filename, stl, error)) return false; - return true; + save_stl(filename, stl); } else if (ext == ".cpp" || ext == ".CPP") { auto to_cpp = [](const string& name, const string& vname, const auto& values) -> string { @@ -1348,11 +1042,9 @@ bool save_fvshape(const string& filename, const fvshape_data& shape, str += to_cpp(name, "quadspos", shape.quadspos); str += to_cpp(name, "quadsnorm", shape.quadsnorm); str += to_cpp(name, "quadstexcoord", shape.quadstexcoord); - if (!save_text(filename, str, error)) return false; - return true; + save_text(filename, str); } else { - error = "unsupported format " + filename; - return false; + throw io_error("unsupported format " + filename); } } @@ -1492,7 +1184,7 @@ shape_data make_shape_preset(const string& type) { shape.normals = {}; return shape; } else if (type == "test-geosphere-subdivided") { - auto shape = make_geosphere(3, 0.075f); + auto shape = make_geosphere(6, 0.075f); for (auto& p : shape.positions) p += {0, 0.075f, 0}; return shape; } else if (type == "test-hairball1") { @@ -1572,220 +1264,40 @@ shape_data make_shape_preset(const string& type) { } else if (type == "test-clothy") { return make_recty({64, 64}, {0.2f, 0.2f}); } else { - return {}; + throw io_error{"unknown preset " + type}; } } // Shape presets used for testing. fvshape_data make_fvshape_preset(const string& type) { - if (type == "default-quad") { - return shape_to_fvshape(make_rect()); - } else if (type == "default-quady") { - return shape_to_fvshape(make_recty()); - } else if (type == "default-cube") { - return shape_to_fvshape(make_box()); - } else if (type == "default-cube-rounded") { - return shape_to_fvshape(make_rounded_box()); - } else if (type == "default-sphere") { - return shape_to_fvshape(make_sphere()); - } else if (type == "default-matcube") { - return shape_to_fvshape(make_rounded_box()); - } else if (type == "default-matsphere") { - return shape_to_fvshape(make_uvspherey()); - } else if (type == "default-disk") { - return shape_to_fvshape(make_disk()); - } else if (type == "default-disk-bulged") { - return shape_to_fvshape(make_bulged_disk()); - } else if (type == "default-quad-bulged") { - return shape_to_fvshape(make_bulged_rect()); - } else if (type == "default-uvsphere") { - return shape_to_fvshape(make_uvsphere()); - } else if (type == "default-uvsphere-flipcap") { - return shape_to_fvshape(make_capped_uvsphere()); - } else if (type == "default-uvspherey") { - return shape_to_fvshape(make_uvspherey()); - } else if (type == "default-uvspherey-flipcap") { - return shape_to_fvshape(make_capped_uvspherey()); - } else if (type == "default-uvdisk") { - return shape_to_fvshape(make_uvdisk()); - } else if (type == "default-uvcylinder") { - return shape_to_fvshape(make_uvcylinder()); - } else if (type == "default-uvcylinder-rounded") { - return shape_to_fvshape(make_rounded_uvcylinder({32, 32, 32})); - } else if (type == "default-geosphere") { - return shape_to_fvshape(make_geosphere()); - } else if (type == "default-floor") { - return shape_to_fvshape(make_floor()); - } else if (type == "default-floor-bent") { - return shape_to_fvshape(make_bent_floor()); - } else if (type == "default-matball") { - return shape_to_fvshape(make_sphere()); - } else if (type == "default-hairball-interior") { - return shape_to_fvshape(make_sphere(pow2(5), 0.8f)); - } else if (type == "default-suzanne") { - return shape_to_fvshape(make_monkey()); - } else if (type == "default-cube-facevarying") { + auto test_xform = translation_frame(vec3f{0, 0.75f, 0}) * + scaling_frame(vec3f{0.75f, 0.75f, 0.75f}); + if (type == "fvcube") { return make_fvbox(); - } else if (type == "default-sphere-facevarying") { + } else if (type == "fvsphere") { return make_fvsphere(); - } else if (type == "default-quady-displaced") { - return shape_to_fvshape(make_recty({256, 256})); - } else if (type == "default-sphere-displaced") { - return shape_to_fvshape(make_sphere(128)); - } else if (type == "test-cube") { - auto shape = make_rounded_box( - {32, 32, 32}, {0.075f, 0.075f, 0.075f}, {1, 1, 1}, 0.3f * 0.075f); - for (auto& p : shape.positions) p += {0, 0.075f, 0}; - return shape_to_fvshape(shape); - } else if (type == "test-matsphere") { - auto shape = make_uvspherey({32, 32}, 0.075f, {2, 1}); - for (auto& p : shape.positions) p += {0, 0.075f, 0}; - return shape_to_fvshape(shape); - } else if (type == "test-uvsphere") { - auto shape = make_uvsphere({32, 32}, 0.075f); - for (auto& p : shape.positions) p += {0, 0.075f, 0}; - return shape_to_fvshape(shape); - } else if (type == "test-uvsphere-flipcap") { - auto shape = make_capped_uvsphere({32, 32}, 0.075f, {1, 1}, 0.3f * 0.075f); - for (auto& p : shape.positions) p += {0, 0.075f, 0}; - return shape_to_fvshape(shape); - } else if (type == "test-uvspherey") { - auto shape = make_uvspherey({32, 32}, 0.075f); - for (auto& p : shape.positions) p += {0, 0.075f, 0}; - return shape_to_fvshape(shape); - } else if (type == "test-uvspherey-flipcap") { - auto shape = make_capped_uvspherey({32, 32}, 0.075f, {1, 1}, 0.3f * 0.075f); - for (auto& p : shape.positions) p += {0, 0.075f, 0}; - return shape_to_fvshape(shape); - } else if (type == "test-sphere") { - auto shape = make_sphere(32, 0.075f, 1); - for (auto& p : shape.positions) p += {0, 0.075f, 0}; - return shape_to_fvshape(shape); - } else if (type == "test-sphere-displaced") { - auto shape = make_sphere(128, 0.075f, 1); - for (auto& p : shape.positions) p += {0, 0.075f, 0}; - return shape_to_fvshape(shape); - } else if (type == "test-matcube") { - auto shape = make_rounded_box( - {32, 32, 32}, {0.075f, 0.075f, 0.075f}, {1, 1, 1}, 0.3f * 0.075f); - for (auto& p : shape.positions) p += {0, 0.075f, 0}; - return shape_to_fvshape(shape); - } else if (type == "test-disk") { - auto shape = make_disk(32, 0.075f, 1); - for (auto& p : shape.positions) p += {0, 0.075f, 0}; - return shape_to_fvshape(shape); - } else if (type == "test-uvcylinder") { - auto shape = make_rounded_uvcylinder( - {32, 32, 32}, {0.075f, 0.075f}, {1, 1, 1}, 0.3f * 0.075f); - for (auto& p : shape.positions) p += {0, 0.075f, 0}; - return shape_to_fvshape(shape); - } else if (type == "test-floor") { - return shape_to_fvshape(make_floor({1, 1}, {2, 2}, {20, 20})); - } else if (type == "test-smallfloor") { - return shape_to_fvshape(make_floor({1, 1}, {0.5f, 0.5f}, {1, 1})); - } else if (type == "test-quad") { - return shape_to_fvshape(make_rect({1, 1}, {0.075f, 0.075f}, {1, 1})); - } else if (type == "test-quady") { - return shape_to_fvshape(make_recty({1, 1}, {0.075f, 0.075f}, {1, 1})); - } else if (type == "test-quad-displaced") { - return shape_to_fvshape(make_rect({256, 256}, {0.075f, 0.075f}, {1, 1})); - } else if (type == "test-quady-displaced") { - return shape_to_fvshape(make_recty({256, 256}, {0.075f, 0.075f}, {1, 1})); - } else if (type == "test-matball") { - auto shape = make_sphere(32, 0.075f); - for (auto& p : shape.positions) p += {0, 0.075f, 0}; - return shape_to_fvshape(shape); - } else if (type == "test-suzanne-subdiv") { - auto shape = make_monkey(0, 0.075f * 0.8f); - for (auto& p : shape.positions) p += {0, 0.075f, 0}; - return shape_to_fvshape(shape); - } else if (type == "test-cube-subdiv") { - auto fvshape = make_fvcube(0, 0.075f); - for (auto& p : fvshape.positions) p += {0, 0.075f, 0}; - return fvshape; - } else if (type == "test-arealight1") { - return shape_to_fvshape(make_rect({1, 1}, {0.2f, 0.2f})); - } else if (type == "test-arealight2") { - return shape_to_fvshape(make_rect({1, 1}, {0.2f, 0.2f})); - } else if (type == "test-largearealight1") { - return shape_to_fvshape(make_rect({1, 1}, {0.4f, 0.4f})); - } else if (type == "test-largearealight2") { - return shape_to_fvshape(make_rect({1, 1}, {0.4f, 0.4f})); - } else if (type == "test-cloth") { - return shape_to_fvshape(make_rect({64, 64}, {0.2f, 0.2f})); - } else if (type == "test-clothy") { - return shape_to_fvshape(make_recty({64, 64}, {0.2f, 0.2f})); + } else if (type == "test_facevarying_cube") { + return transform_fvshape(make_fvbox(), test_xform); + } else if (type == "test_facevarying_sphere") { + return transform_fvshape(make_fvsphere(), test_xform); } else { - return {}; + return shape_to_fvshape(make_shape_preset(type)); } } // Load mesh -shape_data load_shape( - const string& filename, string& error, bool flip_texcoord) { - auto shape = shape_data{}; - if (!load_shape(filename, shape, error, flip_texcoord)) return shape_data{}; - return shape; -} shape_data load_shape(const string& filename, bool flip_texcoord) { - auto error = string{}; auto shape = shape_data{}; - if (!load_shape(filename, shape, error, flip_texcoord)) throw io_error{error}; + load_shape(filename, shape, flip_texcoord); return shape; } -void load_shape(const string& filename, shape_data& shape, bool flip_texcoord) { - auto error = string{}; - if (!load_shape(filename, shape, error, flip_texcoord)) throw io_error{error}; -} -void save_shape(const string& filename, const shape_data& shape, - bool flip_texcoord, bool ascii) { - auto error = string{}; - if (!save_shape(filename, shape, error, flip_texcoord, ascii)) - throw io_error{error}; -} // Load mesh fvshape_data load_fvshape(const string& filename, bool flip_texcoord) { - auto error = string{}; auto shape = fvshape_data{}; - if (!load_fvshape(filename, shape, error, flip_texcoord)) - throw io_error{error}; + load_fvshape(filename, shape, flip_texcoord); return shape; } -void load_fvshape( - const string& filename, fvshape_data& fvshape, bool flip_texcoord) { - auto error = string{}; - if (!load_fvshape(filename, fvshape, error, flip_texcoord)) - throw io_error{error}; -} -void save_fvshape(const string& filename, const fvshape_data& fvshape, - bool flip_texcoord, bool ascii) { - auto error = string{}; - if (!save_fvshape(filename, fvshape, error, flip_texcoord, ascii)) - throw io_error{error}; -} - -// Shape presets used ofr testing. -bool make_shape_preset( - const string& filename, shape_data& shape, string& error) { - shape = make_shape_preset(path_basename(filename)); - if (shape.positions.empty()) { - error = "unknown preset"; - return false; - } - return true; -} - -// Shape presets used for testing. -bool make_fvshape_preset( - const string& filename, fvshape_data& fvshape, string& error) { - fvshape = make_fvshape_preset(path_basename(filename)); - if (fvshape.positions.empty()) { - error = "unknown preset"; - return false; - } - return true; -} } // namespace yocto @@ -1795,214 +1307,40 @@ bool make_fvshape_preset( namespace yocto { // Loads/saves an image. Chooses hdr or ldr based on file name. -bool load_texture( - const string& filename, texture_data& texture, string& error) { - auto read_error = [&]() { - error = "cannot raed " + filename; - return false; - }; - +void load_texture(const string& filename, texture_data& texture) { auto ext = path_extension(filename); - if (ext == ".exr" || ext == ".EXR") { - auto pixels = (float*)nullptr; - if (LoadEXR(&pixels, &texture.width, &texture.height, filename.c_str(), - nullptr) != 0) - return read_error(); - texture.linear = true; - texture.pixelsf = vector{ - (vec4f*)pixels, (vec4f*)pixels + texture.width * texture.height}; - free(pixels); - return true; - } else if (ext == ".hdr" || ext == ".HDR") { - auto buffer = vector{}; - if (!load_binary(filename, buffer, error)) return false; - auto ncomp = 0; - auto pixels = stbi_loadf_from_memory(buffer.data(), (int)buffer.size(), - &texture.width, &texture.height, &ncomp, 4); - if (!pixels) return read_error(); - texture.linear = true; - texture.pixelsf = vector{ - (vec4f*)pixels, (vec4f*)pixels + texture.width * texture.height}; - free(pixels); - return true; - } else if (ext == ".png" || ext == ".PNG") { - auto buffer = vector{}; - if (!load_binary(filename, buffer, error)) return false; - auto ncomp = 0; - auto pixels = stbi_load_from_memory(buffer.data(), (int)buffer.size(), - &texture.width, &texture.height, &ncomp, 4); - if (!pixels) return read_error(); - texture.linear = false; - texture.pixelsb = vector{ - (vec4b*)pixels, (vec4b*)pixels + texture.width * texture.height}; - free(pixels); - return true; - } else if (ext == ".jpg" || ext == ".JPG" || ext == ".jpeg" || - ext == ".JPEG") { - auto buffer = vector{}; - if (!load_binary(filename, buffer, error)) return false; - auto ncomp = 0; - auto pixels = stbi_load_from_memory(buffer.data(), (int)buffer.size(), - &texture.width, &texture.height, &ncomp, 4); - if (!pixels) return read_error(); - texture.linear = false; - texture.pixelsb = vector{ - (vec4b*)pixels, (vec4b*)pixels + texture.width * texture.height}; - free(pixels); - return true; - } else if (ext == ".tga" || ext == ".TGA") { - auto buffer = vector{}; - if (!load_binary(filename, buffer, error)) return false; - auto ncomp = 0; - auto pixels = stbi_load_from_memory(buffer.data(), (int)buffer.size(), - &texture.width, &texture.height, &ncomp, 4); - if (!pixels) return read_error(); - texture.linear = false; - texture.pixelsb = vector{ - (vec4b*)pixels, (vec4b*)pixels + texture.width * texture.height}; - free(pixels); - return true; - } else if (ext == ".bmp" || ext == ".BMP") { - auto buffer = vector{}; - if (!load_binary(filename, buffer, error)) return false; - auto ncomp = 0; - auto pixels = stbi_load_from_memory(buffer.data(), (int)buffer.size(), - &texture.width, &texture.height, &ncomp, 4); - if (!pixels) return read_error(); - texture.linear = false; - texture.pixelsb = vector{ - (vec4b*)pixels, (vec4b*)pixels + texture.width * texture.height}; - free(pixels); - return true; + if (ext == ".exr" || ext == ".EXR" || ext == ".hdr" || ext == ".HDR") { + texture.pixelsf = load_image(filename, false); + } else if (ext == ".png" || ext == ".PNG" || ext == ".jpg" || ext == ".JPG" || + ext == ".jpeg" || ext == ".JPEG" || ext == ".tga" || + ext == ".TGA" || ext == ".bmp" || ext == ".BMP") { + texture.pixelsb = load_imageb(filename, true); } else if (ext == ".ypreset" || ext == ".YPRESET") { - if (!make_texture_preset(filename, texture, error)) return false; - return true; + texture = make_texture_preset(filename); } else { - error = "unsupported format " + filename; - return false; + throw io_error("unsupported format " + filename); } } // Saves an hdr image. -bool save_texture( - const string& filename, const texture_data& texture, string& error) { - auto write_error = [&]() { - error = "cannot write " + filename; - return false; - }; - - // check for correct handling - if (!texture.pixelsf.empty() && is_ldr_filename(filename)) { - auto ntexture = texture_data{}; - ntexture.width = texture.width; - ntexture.height = texture.height; - ntexture.pixelsb.resize(texture.pixelsf.size()); - for (auto idx : range(texture.pixelsf.size())) { - ntexture.pixelsb[idx] = float_to_byte(rgb_to_srgb(texture.pixelsf[idx])); - } - return save_texture(filename, ntexture, error); - } - if (!texture.pixelsb.empty() && is_hdr_filename(filename)) { - auto ntexture = texture_data{}; - ntexture.width = texture.width; - ntexture.height = texture.height; - ntexture.pixelsf.resize(texture.pixelsb.size()); - for (auto idx : range(texture.pixelsb.size())) { - ntexture.pixelsf[idx] = srgb_to_rgb(byte_to_float(texture.pixelsb[idx])); - } - return save_texture(filename, ntexture, error); - } - - // write data - auto stbi_write_data = [](void* context, void* data, int size) { - auto& buffer = *(vector*)context; - buffer.insert(buffer.end(), (byte*)data, (byte*)data + size); - }; - - auto ext = path_extension(filename); - if (ext == ".hdr" || ext == ".HDR") { - auto buffer = vector{}; - if (!stbi_write_hdr_to_func(stbi_write_data, &buffer, (int)texture.width, - (int)texture.height, 4, (const float*)texture.pixelsf.data())) - return write_error(); - if (!save_binary(filename, buffer, error)) return false; - return true; - } else if (ext == ".exr" || ext == ".EXR") { - auto data = (byte*)nullptr; - auto size = (size_t)0; - if (SaveEXRToMemory((const float*)texture.pixelsf.data(), - (int)texture.width, (int)texture.height, 4, 1, &data, &size, - nullptr) < 0) - return write_error(); - auto buffer = vector{data, data + size}; - free(data); - if (!save_binary(filename, buffer, error)) return false; - return true; - } else if (ext == ".png" || ext == ".PNG") { - auto buffer = vector{}; - if (!stbi_write_png_to_func(stbi_write_data, &buffer, (int)texture.width, - (int)texture.height, 4, (const byte*)texture.pixelsb.data(), - (int)texture.width * 4)) - return write_error(); - if (!save_binary(filename, buffer, error)) return false; - return true; - } else if (ext == ".jpg" || ext == ".JPG" || ext == ".jpeg" || - ext == ".JPEG") { - auto buffer = vector{}; - if (!stbi_write_jpg_to_func(stbi_write_data, &buffer, (int)texture.width, - (int)texture.height, 4, (const byte*)texture.pixelsb.data(), 75)) - return write_error(); - if (!save_binary(filename, buffer, error)) return false; - return true; - } else if (ext == ".tga" || ext == ".TGA") { - auto buffer = vector{}; - if (!stbi_write_tga_to_func(stbi_write_data, &buffer, (int)texture.width, - (int)texture.height, 4, (const byte*)texture.pixelsb.data())) - return write_error(); - if (!save_binary(filename, buffer, error)) return false; - return true; - } else if (ext == ".bmp" || ext == ".BMP") { - auto buffer = vector{}; - if (!stbi_write_bmp_to_func(stbi_write_data, &buffer, (int)texture.width, - (int)texture.height, 4, (const byte*)texture.pixelsb.data())) - return write_error(); - if (!save_binary(filename, buffer, error)) return false; - return true; +void save_texture(const string& filename, const texture_data& texture) { + if (!texture.pixelsf.empty()) { + save_image(filename, texture.pixelsf); } else { - error = "unsupported format " + filename; - return false; + save_image(filename, texture.pixelsb); } } texture_data make_texture_preset(const string& type) { - return image_to_texture(make_image_preset(type)); + return image_to_texture(make_image_preset(type), !is_srgb_preset(type)); } // Loads/saves an image. Chooses hdr or ldr based on file name. texture_data load_texture(const string& filename) { - auto error = string{}; auto texture = texture_data{}; - if (!load_texture(filename, texture, error)) throw io_error{error}; + load_texture(filename, texture); return texture; } -void load_texture(const string& filename, texture_data& texture) { - auto error = string{}; - if (!load_texture(filename, texture, error)) throw io_error{error}; -} -void save_texture(const string& filename, const texture_data& texture) { - auto error = string{}; - if (!save_texture(filename, texture, error)) throw io_error{error}; -} - -bool make_texture_preset( - const string& filename, texture_data& texture, string& error) { - texture = make_texture_preset(path_basename(filename)); - if (texture.width == 0 || texture.height == 0) { - error = "unknown preset"; - return false; - } - return true; -} } // namespace yocto @@ -2192,10 +1530,6 @@ static void trim_memory(scene_data& scene) { subdiv.quadsnorm.shrink_to_fit(); subdiv.quadstexcoord.shrink_to_fit(); } - for (auto& texture : scene.textures) { - texture.pixelsf.shrink_to_fit(); - texture.pixelsb.shrink_to_fit(); - } scene.cameras.shrink_to_fit(); scene.shapes.shrink_to_fit(); scene.subdivs.shrink_to_fit(); @@ -2714,155 +2048,126 @@ bool make_scene_preset( namespace yocto { // Load/save a scene in the builtin JSON format. -static bool load_json_scene( - const string& filename, scene_data& scene, string& error, bool noparallel); -static bool save_json_scene(const string& filename, const scene_data& scene, - string& error, bool noparallel); +static void load_json_scene( + const string& filename, scene_data& scene, bool noparallel); +static void save_json_scene( + const string& filename, const scene_data& scene, bool noparallel); // Load/save a scene from/to OBJ. -static bool load_obj_scene( - const string& filename, scene_data& scene, string& error, bool noparallel); -static bool save_obj_scene(const string& filename, const scene_data& scene, - string& error, bool noparallel); - -// Load/save a scene from/to PLY. Loads/saves only one mesh with no other data. -static bool load_ply_scene( - const string& filename, scene_data& scene, string& error, bool noparallel); -static bool save_ply_scene(const string& filename, const scene_data& scene, - string& error, bool noparallel); - -// Load/save a scene from/to STL. Loads/saves only one mesh with no other data. -static bool load_stl_scene( - const string& filename, scene_data& scene, string& error, bool noparallel); -static bool save_stl_scene(const string& filename, const scene_data& scene, - string& error, bool noparallel); +static void load_obj_scene( + const string& filename, scene_data& scene, bool noparallel); +static void save_obj_scene( + const string& filename, const scene_data& scene, bool noparallel); + +// Load/save a scene from/to PLY. Loads/saves only one mesh with no other +// data. +static void load_ply_scene( + const string& filename, scene_data& scene, bool noparallel); +static void save_ply_scene( + const string& filename, const scene_data& scene, bool noparallel); + +// Load/save a scene from/to STL. Loads/saves only one mesh with no other +// data. +static void load_stl_scene( + const string& filename, scene_data& scene, bool noparallel); +static void save_stl_scene( + const string& filename, const scene_data& scene, bool noparallel); // Load/save a scene from/to glTF. -static bool load_gltf_scene( - const string& filename, scene_data& scene, string& error, bool noparallel); -static bool save_gltf_scene(const string& filename, const scene_data& scene, - string& error, bool noparallel); +static void load_gltf_scene( + const string& filename, scene_data& scene, bool noparallel); +static void save_gltf_scene( + const string& filename, const scene_data& scene, bool noparallel); // Load/save a scene from/to pbrt. This is not robust at all and only // works on scene that have been previously adapted since the two renderers // are too different to match. -static bool load_pbrt_scene( - const string& filename, scene_data& scene, string& error, bool noparallel); -static bool save_pbrt_scene(const string& filename, const scene_data& scene, - string& error, bool noparallel); +static void load_pbrt_scene( + const string& filename, scene_data& scene, bool noparallel); +static void save_pbrt_scene( + const string& filename, const scene_data& scene, bool noparallel); // Load/save a scene from/to mitsuba. This is not robust at all and only // works on scene that have been previously adapted since the two renderers // are too different to match. For now, only saving is allowed. -static bool load_mitsuba_scene( - const string& filename, scene_data& scene, string& error, bool noparallel); -static bool save_mitsuba_scene(const string& filename, const scene_data& scene, - string& error, bool noparallel); +static void load_mitsuba_scene( + const string& filename, scene_data& scene, bool noparallel); +static void save_mitsuba_scene( + const string& filename, const scene_data& scene, bool noparallel); // Load a scene -bool load_scene( - const string& filename, scene_data& scene, string& error, bool noparallel) { +void load_scene(const string& filename, scene_data& scene, bool noparallel) { auto ext = path_extension(filename); if (ext == ".json" || ext == ".JSON") { - return load_json_scene(filename, scene, error, noparallel); + return load_json_scene(filename, scene, noparallel); } else if (ext == ".obj" || ext == ".OBJ") { - return load_obj_scene(filename, scene, error, noparallel); + return load_obj_scene(filename, scene, noparallel); } else if (ext == ".gltf" || ext == ".GLTF") { - return load_gltf_scene(filename, scene, error, noparallel); + return load_gltf_scene(filename, scene, noparallel); } else if (ext == ".pbrt" || ext == ".PBRT") { - return load_pbrt_scene(filename, scene, error, noparallel); + return load_pbrt_scene(filename, scene, noparallel); } else if (ext == ".xml" || ext == ".XML") { - return load_mitsuba_scene(filename, scene, error, noparallel); + return load_mitsuba_scene(filename, scene, noparallel); } else if (ext == ".ply" || ext == ".PLY") { - return load_ply_scene(filename, scene, error, noparallel); + return load_ply_scene(filename, scene, noparallel); } else if (ext == ".stl" || ext == ".STL") { - return load_stl_scene(filename, scene, error, noparallel); + return load_stl_scene(filename, scene, noparallel); } else if (ext == ".ypreset" || ext == ".YPRESET") { - return make_scene_preset(filename, scene, error); + scene = make_scene_preset(filename); } else { - error = "unsupported format " + filename; - return false; + throw io_error("unsupported format " + filename); } } // Save a scene -bool save_scene(const string& filename, const scene_data& scene, string& error, - bool noparallel) { +void save_scene( + const string& filename, const scene_data& scene, bool noparallel) { auto ext = path_extension(filename); if (ext == ".json" || ext == ".JSON") { - return save_json_scene(filename, scene, error, noparallel); + return save_json_scene(filename, scene, noparallel); } else if (ext == ".obj" || ext == ".OBJ") { - return save_obj_scene(filename, scene, error, noparallel); + return save_obj_scene(filename, scene, noparallel); } else if (ext == ".gltf" || ext == ".GLTF") { - return save_gltf_scene(filename, scene, error, noparallel); + return save_gltf_scene(filename, scene, noparallel); } else if (ext == ".pbrt" || ext == ".PBRT") { - return save_pbrt_scene(filename, scene, error, noparallel); + return save_pbrt_scene(filename, scene, noparallel); } else if (ext == ".xml" || ext == ".XML") { - return save_mitsuba_scene(filename, scene, error, noparallel); + return save_mitsuba_scene(filename, scene, noparallel); } else if (ext == ".ply" || ext == ".PLY") { - return save_ply_scene(filename, scene, error, noparallel); + return save_ply_scene(filename, scene, noparallel); } else if (ext == ".stl" || ext == ".STL") { - return save_stl_scene(filename, scene, error, noparallel); + return save_stl_scene(filename, scene, noparallel); } else { - error = "unsupported format " + filename; - return false; + throw io_error("unsupported format " + filename); } } // Load/save a scene scene_data load_scene(const string& filename, bool noparallel) { - auto error = string{}; auto scene = scene_data{}; - if (!load_scene(filename, scene, error, noparallel)) throw io_error{error}; + load_scene(filename, scene, noparallel); return scene; } -void load_scene(const string& filename, scene_data& scene, bool noparallel) { - auto error = string{}; - if (!load_scene(filename, scene, error, noparallel)) throw io_error{error}; -} -void save_scene( - const string& filename, const scene_data& scene, bool noparallel) { - auto error = string{}; - if (!save_scene(filename, scene, error, noparallel)) throw io_error{error}; -} // Make missing scene directories -bool make_scene_directories( - const string& filename, const scene_data& scene, string& error) { +void make_scene_directories(const string& filename, const scene_data& scene) { // make a directory if needed - if (!make_directory(path_dirname(filename), error)) return false; + make_directory(path_dirname(filename)); if (!scene.shapes.empty()) - if (!make_directory(path_join(path_dirname(filename), "shapes"), error)) - return false; + make_directory(path_join(path_dirname(filename), "shapes")); if (!scene.textures.empty()) - if (!make_directory(path_join(path_dirname(filename), "textures"), error)) - return false; + make_directory(path_join(path_dirname(filename), "textures")); if (!scene.subdivs.empty()) - if (!make_directory(path_join(path_dirname(filename), "subdivs"), error)) - return false; - return true; + make_directory(path_join(path_dirname(filename), "subdivs")); } // Add environment -bool add_environment(scene_data& scene, const string& filename, string& error) { - auto texture = texture_data{}; - if (!load_texture(filename, texture, error)) return false; +void add_environment( + scene_data& scene, const string& name, const string& filename) { + auto texture = load_texture(filename); scene.textures.push_back(std::move(texture)); scene.environments.push_back({{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}, {0, 0, 0}}, {1, 1, 1}, (int)scene.textures.size() - 1}); - return true; -} - -// Make missing scene directories -void make_scene_directories(const string& filename, const scene_data& scene) { - auto error = string{}; - if (!make_scene_directories(filename, scene, error)) throw io_error{error}; -} - -// Add environment -void add_environment(scene_data& scene, const string& filename) { - auto error = string{}; - if (!add_environment(scene, filename, error)) throw io_error{error}; } } // namespace yocto @@ -2873,30 +2178,25 @@ void add_environment(scene_data& scene, const string& filename) { namespace yocto { // load instances -static bool load_instance( - const string& filename, vector& frames, string& error) { +static void load_instance(const string& filename, vector& frames) { auto ext = path_extension(filename); if (ext == ".ply" || ext == ".PLY") { - auto ply = ply_model{}; - if (!load_ply(filename, ply, error)) return false; + auto ply = load_ply(filename); // TODO: remove when all as arrays if (!get_values(ply, "instance", {"xx", "xy", "xz", "yx", "yy", "yz", "zx", "zy", "zz", "ox", "oy", "oz"}, (vector>&)frames)) { - error = "cannot parse " + filename; - return false; + throw io_error{"cannot parse " + filename}; } - return true; } else { - error = "unsupported format " + filename; - return false; + throw io_error("unsupported format " + filename); } } // save instances -[[maybe_unused]] static bool save_instance(const string& filename, - const vector& frames, string& error, bool ascii = false) { +[[maybe_unused]] static void save_instance( + const string& filename, const vector& frames, bool ascii = false) { auto ext = path_extension(filename); if (ext == ".ply" || ext == ".PLY") { auto ply = ply_model{}; @@ -2905,30 +2205,25 @@ static bool load_instance( {"xx", "xy", "xz", "yx", "yy", "yz", "zx", "zy", "zz", "ox", "oy", "oz"}, (const vector>&)frames); - if (!save_ply(filename, ply, error)) return false; - return true; + save_ply(filename, ply); } else { - error = "unsupported format " + filename; - return false; + throw io_error("unsupported format " + filename); } } // load subdiv -bool load_subdiv(const string& filename, subdiv_data& subdiv, string& error) { - auto lsubdiv = fvshape_data{}; - if (!load_fvshape(filename, lsubdiv, error, true)) return false; +void load_subdiv(const string& filename, subdiv_data& subdiv) { + auto lsubdiv = load_fvshape(filename, true); subdiv.quadspos = lsubdiv.quadspos; subdiv.quadsnorm = lsubdiv.quadsnorm; subdiv.quadstexcoord = lsubdiv.quadstexcoord; subdiv.positions = lsubdiv.positions; subdiv.normals = lsubdiv.normals; subdiv.texcoords = lsubdiv.texcoords; - return true; } // save subdiv -bool save_subdiv( - const string& filename, const subdiv_data& subdiv, string& error) { +void save_subdiv(const string& filename, const subdiv_data& subdiv) { auto ssubdiv = fvshape_data{}; ssubdiv.quadspos = subdiv.quadspos; ssubdiv.quadsnorm = subdiv.quadsnorm; @@ -2936,29 +2231,18 @@ bool save_subdiv( ssubdiv.positions = subdiv.positions; ssubdiv.normals = subdiv.normals; ssubdiv.texcoords = subdiv.texcoords; - if (!save_fvshape(filename, ssubdiv, error, true)) return false; - return true; + save_fvshape(filename, ssubdiv, true); } // load/save subdiv subdiv_data load_subdiv(const string& filename) { - auto error = string{}; auto subdiv = subdiv_data{}; - if (!load_subdiv(filename, subdiv, error)) throw io_error{error}; + load_subdiv(filename, subdiv); return subdiv; } -void load_subdiv(const string& filename, subdiv_data& subdiv) { - auto error = string{}; - if (!load_subdiv(filename, subdiv, error)) throw io_error{error}; -} -void save_subdiv(const string& filename, const subdiv_data& subdiv) { - auto error = string{}; - if (!save_subdiv(filename, subdiv, error)) throw io_error{error}; -} // save binary shape -static bool save_binshape( - const string& filename, const shape_data& shape, string& error) { +static void save_binshape(const string& filename, const shape_data& shape) { auto write_values = [](vector& buffer, const auto& values) { if (values.empty()) return; buffer.insert(buffer.end(), (byte*)values.data(), @@ -2977,8 +2261,7 @@ static bool save_binshape( write_values(buffer, shape.triangles); write_values(buffer, quads_to_triangles(shape.quads)); - if (!save_binary(filename, buffer, error)) return false; - return true; + save_binary(filename, buffer); } } // namespace yocto @@ -3024,25 +2307,8 @@ NLOHMANN_JSON_SERIALIZE_ENUM( }) // Load a scene in the builtin JSON format. -static bool load_json_scene_version40(const string& filename, - const json_value& json, scene_data& scene, string& error, bool noparallel) { - auto parse_error = [filename, &error](const string& patha, - const string& pathb = "", const string& pathc = "") { - auto path = patha; - if (!pathb.empty()) path += "/" + pathb; - if (!pathc.empty()) path += "/" + pathc; - error = "parse error " + filename + " at " + path; - return false; - }; - auto key_error = [filename, &error](const string& patha, - const string& pathb = "", const string& pathc = "") { - auto path = patha; - if (!pathb.empty()) path += "/" + pathb; - if (!pathc.empty()) path += "/" + pathc; - error = filename + "; unknow key at " + path; - return false; - }; - +static void load_json_scene_version40(const string& filename, + const json_value& json, scene_data& scene, bool noparallel) { // parse json value auto get_opt = [](const json_value& json, const string& key, auto& value) { value = json.value(key, value); @@ -3114,7 +2380,7 @@ static bool load_json_scene_version40(const string& filename, auto ply_instances = vector{}; auto ply_instances_names = vector{}; auto ply_instance_map = unordered_map{ - {"", invalidid}}; + {"", invalidid}}; auto instance_ply = unordered_map{}; auto get_ist = [&scene, &ply_instances, &ply_instances_names, &ply_instance_map, &instance_ply](const json_value& json, @@ -3247,16 +2513,11 @@ static bool load_json_scene_version40(const string& filename, } } } catch (...) { - error = "cannot parse " + filename; - return false; + throw io_error("cannot parse " + filename); } // dirname - auto dirname = path_dirname(filename); - auto dependent_error = [&filename, &error]() { - error = "cannot load " + filename + " since " + error; - return false; - }; + auto dirname = path_dirname(filename); // get filename from name auto find_path = [dirname](const string& name, const string& group, @@ -3269,69 +2530,34 @@ static bool load_json_scene_version40(const string& filename, }; // load resources - if (noparallel) { - auto error = string{}; + try { // load shapes - for (auto& shape : scene.shapes) { + parallel_foreach(scene.shapes, noparallel, [&](auto& shape) { auto path = find_path( get_shape_name(scene, shape), "shapes", {".ply", ".obj"}); - if (!load_shape(path_join(dirname, path), shape, error, true)) - return dependent_error(); - } + return load_shape(path_join(dirname, path), shape, true); + }); // load subdivs - for (auto& subdiv : scene.subdivs) { + parallel_foreach(scene.subdivs, noparallel, [&](auto& subdiv) { auto path = find_path( get_subdiv_name(scene, subdiv), "subdivs", {".ply", ".obj"}); - if (!load_subdiv(path_join(dirname, path), subdiv, error)) - return dependent_error(); - } + return load_subdiv(path_join(dirname, path), subdiv); + }); // load textures - for (auto& texture : scene.textures) { + parallel_foreach(scene.textures, noparallel, [&](auto& texture) { auto path = find_path(get_texture_name(scene, texture), "textures", {".hdr", ".exr", ".png", ".jpg"}); - if (!load_texture(path_join(dirname, path), texture, error)) - return dependent_error(); - } + return load_texture(path_join(dirname, path), texture); + }); // load instances - for (auto& ply_instance : ply_instances) { + parallel_foreach(ply_instances, noparallel, [&](auto& ply_instance) { auto path = find_path( get_ply_instance_name(scene, ply_instance), "instances", {".ply"}); - if (!load_instance(path_join(dirname, path), ply_instance.frames, error)) - return dependent_error(); - } - } else { - // load shapes - if (!parallel_foreach(scene.shapes, error, [&](auto& shape, string& error) { - auto path = find_path( - get_shape_name(scene, shape), "shapes", {".ply", ".obj"}); - return load_shape(path_join(dirname, path), shape, error, true); - })) - return dependent_error(); - // load subdivs - if (!parallel_foreach( - scene.subdivs, error, [&](auto& subdiv, string& error) { - auto path = find_path( - get_subdiv_name(scene, subdiv), "subdivs", {".ply", ".obj"}); - return load_subdiv(path_join(dirname, path), subdiv, error); - })) - return dependent_error(); - // load textures - if (!parallel_foreach( - scene.textures, error, [&](auto& texture, string& error) { - auto path = find_path(get_texture_name(scene, texture), - "textures", {".hdr", ".exr", ".png", ".jpg"}); - return load_texture(path_join(dirname, path), texture, error); - })) - return dependent_error(); - // load instances - if (!parallel_foreach( - ply_instances, error, [&](auto& ply_instance, string& error) { - auto path = find_path(get_ply_instance_name(scene, ply_instance), - "instances", {".ply"}); - return load_instance( - path_join(dirname, path), ply_instance.frames, error); - })) - return dependent_error(); + return load_instance(path_join(dirname, path), ply_instance.frames); + }); + } catch (std::exception& except) { + throw io_error( + "cannot load " + filename + " since " + string(except.what())); } // apply instances @@ -3369,17 +2595,14 @@ static bool load_json_scene_version40(const string& filename, add_missing_camera(scene); add_missing_radius(scene); trim_memory(scene); - - // done - return true; } // Load a scene in the builtin JSON format. -static bool load_json_scene_version41(const string& filename, json_value& json, - scene_data& scene, string& error, bool noparallel) { +static void load_json_scene_version41(const string& filename, json_value& json, + scene_data& scene, bool noparallel) { // check version if (!json.contains("asset") || !json.at("asset").contains("version")) - return load_json_scene_version40(filename, json, scene, error, noparallel); + return load_json_scene_version40(filename, json, scene, noparallel); // parse json value auto get_opt = [](const json_value& json, const string& key, auto& value) { @@ -3423,10 +2646,11 @@ static bool load_json_scene_version41(const string& filename, json_value& json, get_opt(element, "focus", camera.focus); get_opt(element, "aperture", camera.aperture); if (element.contains("lookat")) { - get_opt(element, "lookat", (mat3f&)camera.frame); - camera.focus = length(camera.frame.x - camera.frame.y); - camera.frame = lookat_frame( - camera.frame.x, camera.frame.y, camera.frame.z); + auto lookat = mat3f{}; + get_opt(element, "lookat", lookat); + auto from = lookat[0], to = lookat[1], up = lookat[2]; + camera.focus = length(from - to); + camera.frame = lookat_frame(from, to, up); } } } @@ -3522,9 +2746,10 @@ static bool load_json_scene_version41(const string& filename, json_value& json, get_ref(element, "shape", instance.shape, shape_map); get_ref(element, "material", instance.material, material_map); if (element.contains("lookat")) { - get_opt(element, "lookat", (mat3f&)instance.frame); - instance.frame = lookat_frame( - instance.frame.x, instance.frame.y, instance.frame.z, false); + auto lookat = mat3f{}; + get_opt(element, "lookat", lookat); + auto from = lookat[0], to = lookat[1], up = lookat[2]; + instance.frame = lookat_frame(from, to, up, false); } } } @@ -3539,23 +2764,19 @@ static bool load_json_scene_version41(const string& filename, json_value& json, get_opt(element, "emission", environment.emission); get_ref(element, "emission_tex", environment.emission_tex, texture_map); if (element.contains("lookat")) { - get_opt(element, "lookat", (mat3f&)environment.frame); - environment.frame = lookat_frame(environment.frame.x, - environment.frame.y, environment.frame.z, false); + auto lookat = mat3f{}; + get_opt(element, "lookat", lookat); + auto from = lookat[0], to = lookat[1], up = lookat[2]; + environment.frame = lookat_frame(from, to, up, false); } } } } catch (...) { - error = "cannot parse " + filename; - return false; + throw io_error("cannot parse " + filename); } // prepare data - auto dirname = path_dirname(filename); - auto dependent_error = [&filename, &error]() { - error = "cannot load " + filename + " since " + error; - return false; - }; + auto dirname = path_dirname(filename); // fix paths for (auto& datafile : shape_filenames) @@ -3566,69 +2787,45 @@ static bool load_json_scene_version41(const string& filename, json_value& json, datafile = path_join(dirname, "subdivs", datafile); // load resources - if (noparallel) { - auto error = string{}; - // load shapes - for (auto idx : range(scene.shapes.size())) { - if (!load_shape(shape_filenames[idx], scene.shapes[idx], error, true)) - return dependent_error(); - } - // load subdivs - for (auto idx : range(scene.subdivs.size())) { - if (!load_subdiv(subdiv_filenames[idx], scene.subdivs[idx], error)) - return dependent_error(); - } - // load textures - for (auto idx : range(scene.textures.size())) { - if (!load_texture(texture_filenames[idx], scene.textures[idx], error)) - return dependent_error(); - } - } else { + try { // load shapes - if (!parallel_for( - scene.shapes.size(), error, [&](size_t idx, string& error) { - return load_shape( - shape_filenames[idx], scene.shapes[idx], error, true); - })) - return dependent_error(); + parallel_zip(shape_filenames, scene.shapes, noparallel, + [&](auto&& filename, auto&& shape) { + return load_shape(filename, shape, true); + }); // load subdivs - if (!parallel_for( - scene.subdivs.size(), error, [&](size_t idx, string& error) { - return load_subdiv( - subdiv_filenames[idx], scene.subdivs[idx], error); - })) - return dependent_error(); + parallel_zip(subdiv_filenames, scene.subdivs, noparallel, + [&](auto&& filename, auto&& subdiv) { + return load_subdiv(filename, subdiv); + }); // load textures - if (!parallel_for( - scene.textures.size(), error, [&](size_t idx, string& error) { - return load_texture( - texture_filenames[idx], scene.textures[idx], error); - })) - return dependent_error(); + parallel_zip(texture_filenames, scene.textures, noparallel, + [&](auto&& filename, auto&& texture) { + return load_texture(filename, texture); + }); + } catch (std::exception& except) { + throw io_error( + "cannot load " + filename + " since " + string(except.what())); } // fix scene add_missing_camera(scene); add_missing_radius(scene); trim_memory(scene); - - // done - return false; } // Load a scene in the builtin JSON format. -static bool load_json_scene( - const string& filename, scene_data& scene, string& error, bool noparallel) { +static void load_json_scene( + const string& filename, scene_data& scene, bool noparallel) { // open file - auto json = json_value{}; - if (!load_json(filename, json, error)) return false; + auto json = load_json(filename); // check version if (!json.contains("asset") || !json.at("asset").contains("version")) - return load_json_scene_version40(filename, json, scene, error, noparallel); + return load_json_scene_version40(filename, json, scene, noparallel); if (json.contains("asset") && json.at("asset").contains("version") && json.at("asset").at("version") == "4.1") - return load_json_scene_version41(filename, json, scene, error, noparallel); + return load_json_scene_version41(filename, json, scene, noparallel); // parse json value auto get_opt = [](const json_value& json, const string& key, auto& value) { @@ -3640,12 +2837,6 @@ static bool load_json_scene( auto texture_filenames = vector{}; auto subdiv_filenames = vector{}; - // errors - auto parse_error = [&filename, &error]() { - error = "cannot parse " + filename; - return false; - }; - // parsing values try { if (json.contains("asset")) { @@ -3653,7 +2844,8 @@ static bool load_json_scene( get_opt(element, "copyright", scene.copyright); auto version = string{}; get_opt(element, "version", version); - if (version != "4.2" && version != "5.0") return parse_error(); + if (version != "4.2" && version != "5.0") + throw io_error("unsupported format version " + filename); } if (json.contains("cameras")) { auto& group = json.at("cameras"); @@ -3689,9 +2881,6 @@ static bool load_json_scene( auto& uri = texture_filenames.emplace_back(); get_opt(element, "name", name); get_opt(element, "uri", uri); - get_opt(element, "linear", texture.linear); - get_opt(element, "nearest", texture.nearest); - get_opt(element, "clamp", texture.clamp); } } if (json.contains("materials")) { @@ -3788,72 +2977,43 @@ static bool load_json_scene( } } } catch (...) { - return parse_error(); + throw io_error("cannot parse " + filename); } // prepare data - auto dirname = path_dirname(filename); - auto dependent_error = [&filename, &error]() { - error = "cannot load " + filename + " since " + error; - return false; - }; + auto dirname = path_dirname(filename); // load resources - if (noparallel) { - // load shapes - for (auto idx : range(scene.shapes.size())) { - if (!load_shape(path_join(dirname, shape_filenames[idx]), - scene.shapes[idx], error, true)) - return dependent_error(); - } - // load subdivs - for (auto idx : range(scene.subdivs.size())) { - if (!load_subdiv(path_join(dirname, subdiv_filenames[idx]), - scene.subdivs[idx], error)) - return dependent_error(); - } - // load textures - for (auto idx : range(scene.textures.size())) { - if (!load_texture(path_join(dirname, texture_filenames[idx]), - scene.textures[idx], error)) - return dependent_error(); - } - } else { + try { // load shapes - if (!parallel_for( - scene.shapes.size(), error, [&](size_t idx, string& error) { - return load_shape(path_join(dirname, shape_filenames[idx]), - scene.shapes[idx], error, true); - })) - return dependent_error(); + parallel_zip(shape_filenames, scene.shapes, noparallel, + [&](auto&& filename, auto&& shape) { + return load_shape(path_join(dirname, filename), shape, true); + }); // load subdivs - if (!parallel_for( - scene.subdivs.size(), error, [&](size_t idx, string& error) { - return load_subdiv(path_join(dirname, subdiv_filenames[idx]), - scene.subdivs[idx], error); - })) - return dependent_error(); + parallel_zip(subdiv_filenames, scene.subdivs, noparallel, + [&](auto&& filename, auto&& subdiv) { + return load_subdiv(path_join(dirname, filename), subdiv); + }); // load textures - if (!parallel_for( - scene.textures.size(), error, [&](size_t idx, string& error) { - return load_texture(path_join(dirname, texture_filenames[idx]), - scene.textures[idx], error); - })) - return dependent_error(); + parallel_zip(texture_filenames, scene.textures, noparallel, + [&](auto&& filename, auto&& texture) { + return load_texture(path_join(dirname, filename), texture); + }); + } catch (std::exception& except) { + throw io_error( + "cannot load " + filename + " since " + string(except.what())); } // fix scene add_missing_camera(scene); add_missing_radius(scene); trim_memory(scene); - - // done - return true; } // Save a scene in the builtin JSON format. -static bool save_json_scene(const string& filename, const scene_data& scene, - string& error, bool noparallel) { +static void save_json_scene( + const string& filename, const scene_data& scene, bool noparallel) { // helpers to handel old code paths auto add_object = [](json_value& json, const string& name) -> json_value& { auto& item = json[name]; @@ -3945,16 +3105,12 @@ static bool save_json_scene(const string& filename, const scene_data& scene, } if (!scene.textures.empty()) { - auto default_ = texture_data{}; - auto& group = add_array(json, "textures"); + auto& group = add_array(json, "textures"); reserve_values(group, scene.textures.size()); for (auto&& [idx, texture] : enumerate(scene.textures)) { auto& element = append_object(group); set_val(element, "name", get_name(scene.texture_names, idx), ""); set_val(element, "uri", texture_filenames[idx], ""s); - set_val(element, "linear", texture.linear, default_.linear); - set_val(element, "nearest", texture.nearest, default_.nearest); - set_val(element, "clamp", texture.clamp, default_.clamp); } } @@ -4046,60 +3202,32 @@ static bool save_json_scene(const string& filename, const scene_data& scene, } // save json - if (!save_json(filename, json, error)) return false; + save_json(filename, json); // prepare data - auto dirname = path_dirname(filename); - auto dependent_error = [&filename, &error]() { - error = "cannot save " + filename + " since " + error; - return false; - }; + auto dirname = path_dirname(filename); // dirname - if (noparallel) { - // save shapes - for (auto idx : range(scene.shapes.size())) { - if (!save_shape(path_join(dirname, shape_filenames[idx]), - scene.shapes[idx], error, true)) - return dependent_error(); - } - // save subdiv - for (auto idx : range(scene.subdivs.size())) { - if (!save_subdiv(path_join(dirname, subdiv_filenames[idx]), - scene.subdivs[idx], error)) - return dependent_error(); - } - // save textures - for (auto idx : range(scene.textures.size())) { - if (!save_texture(path_join(dirname, texture_filenames[idx]), - scene.textures[idx], error)) - return dependent_error(); - } - } else { + try { // save shapes - if (!parallel_for(scene.shapes.size(), error, [&](auto idx, string& error) { - return save_shape(path_join(dirname, shape_filenames[idx]), - scene.shapes[idx], error, true); - })) - return dependent_error(); + parallel_zip(shape_filenames, scene.shapes, noparallel, + [&](auto&& filename, auto&& shape) { + return save_shape(path_join(dirname, filename), shape, true); + }); // save subdivs - if (!parallel_for( - scene.subdivs.size(), error, [&](auto idx, string& error) { - return save_subdiv(path_join(dirname, subdiv_filenames[idx]), - scene.subdivs[idx], error); - })) - return dependent_error(); + parallel_zip(subdiv_filenames, scene.subdivs, noparallel, + [&](auto&& filename, auto&& subdiv) { + return save_subdiv(path_join(dirname, filename), subdiv); + }); // save textures - if (!parallel_for( - scene.textures.size(), error, [&](auto idx, string& error) { - return save_texture(path_join(dirname, texture_filenames[idx]), - scene.textures[idx], error); - })) - return dependent_error(); + parallel_zip(texture_filenames, scene.textures, noparallel, + [&](auto&& filename, auto&& texture) { + return save_texture(path_join(dirname, filename), texture); + }); + } catch (std::exception& except) { + throw io_error( + "cannot save " + filename + " since " + string(except.what())); } - - // done - return true; } } // namespace yocto @@ -4110,11 +3238,10 @@ static bool save_json_scene(const string& filename, const scene_data& scene, namespace yocto { // Loads an OBJ -static bool load_obj_scene( - const string& filename, scene_data& scene, string& error, bool noparallel) { +static void load_obj_scene( + const string& filename, scene_data& scene, bool noparallel) { // load obj - auto obj = obj_model{}; - if (!load_obj(filename, obj, error, false, true)) return false; + auto obj = load_obj(filename, false, true); // convert cameras scene.cameras.reserve(obj.cameras.size()); @@ -4214,39 +3341,26 @@ static bool load_obj_scene( scene.instance_names = make_names(scene.instances, {}, "instance"); // dirname - auto dirname = path_dirname(filename); - auto dependent_error = [&filename, &error]() { - error = "cannot load " + filename + " since " + error; - return false; - }; + auto dirname = path_dirname(filename); - if (noparallel) { - // load textures - for (auto& texture : scene.textures) { - auto& path = texture_paths[&texture - &scene.textures.front()]; - if (!load_texture(path_join(dirname, path), texture, error)) - return dependent_error(); - } - } else { + try { // load textures - if (!parallel_foreach( - scene.textures, error, [&](auto& texture, string& error) { - auto& path = texture_paths[&texture - &scene.textures.front()]; - return load_texture(path_join(dirname, path), texture, error); - })) - return dependent_error(); + parallel_zip(texture_paths, scene.textures, noparallel, + [&](auto&& path, auto&& texture) { + return load_texture(path_join(dirname, path), texture); + }); + } catch (std::exception& except) { + throw io_error( + "cannot load " + filename + " since " + string(except.what())); } // fix scene add_missing_camera(scene); add_missing_radius(scene); - - // done - return true; } -static bool save_obj_scene(const string& filename, const scene_data& scene, - string& error, bool noparallel) { +static void save_obj_scene( + const string& filename, const scene_data& scene, bool noparallel) { // build obj auto obj = obj_model{}; @@ -4324,36 +3438,22 @@ static bool save_obj_scene(const string& filename, const scene_data& scene, } // save obj - if (!save_obj(filename, obj, error)) return false; + save_obj(filename, obj); // dirname - auto dirname = path_dirname(filename); - auto dependent_error = [&filename, &error]() { - error = "cannot save " + filename + " since " + error; - return false; - }; + auto dirname = path_dirname(filename); - if (noparallel) { + try { // save textures - for (auto& texture : scene.textures) { + parallel_foreach(scene.textures, noparallel, [&](auto& texture) { auto path = "textures/" + get_texture_name(scene, texture) + (!texture.pixelsf.empty() ? ".hdr"s : ".png"s); - if (!save_texture(path_join(dirname, path), texture, error)) - return dependent_error(); - } - } else { - // save textures - if (!parallel_foreach( - scene.textures, error, [&](auto& texture, string& error) { - auto path = "textures/" + get_texture_name(scene, texture) + - (!texture.pixelsf.empty() ? ".hdr"s : ".png"s); - return save_texture(path_join(dirname, path), texture, error); - })) - return dependent_error(); + return save_texture(path_join(dirname, path), texture); + }); + } catch (std::exception& except) { + throw io_error( + "cannot save " + filename + " since " + string(except.what())); } - - // done - return true; } } // namespace yocto @@ -4363,11 +3463,10 @@ static bool save_obj_scene(const string& filename, const scene_data& scene, // ----------------------------------------------------------------------------- namespace yocto { -static bool load_ply_scene( - const string& filename, scene_data& scene, string& error, bool noparallel) { +static void load_ply_scene( + const string& filename, scene_data& scene, bool noparallel) { // load ply mesh and make instance - auto shape = shape_data{}; - if (!load_shape(filename, shape, error, true)) return false; + auto shape = load_shape(filename, true); scene.shapes.push_back(shape); scene.instances.push_back({{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}, {0, 0, 0}}, (int)scene.shapes.size() - 1, -1}); @@ -4377,16 +3476,13 @@ static bool load_ply_scene( add_missing_camera(scene); add_missing_radius(scene); add_missing_lights(scene); - - // done - return true; } -static bool save_ply_scene(const string& filename, const scene_data& scene, - string& error, bool noparallel) { +static void save_ply_scene( + const string& filename, const scene_data& scene, bool noparallel) { // save shape if (scene.shapes.empty()) throw std::invalid_argument{"empty shape"}; - return save_shape(filename, scene.shapes.front(), error, false); + return save_shape(filename, scene.shapes.front(), false); } } // namespace yocto @@ -4396,11 +3492,10 @@ static bool save_ply_scene(const string& filename, const scene_data& scene, // ----------------------------------------------------------------------------- namespace yocto { -static bool load_stl_scene( - const string& filename, scene_data& scene, string& error, bool noparallel) { +static void load_stl_scene( + const string& filename, scene_data& scene, bool noparallel) { // load ply mesh and make instance - auto shape = shape_data{}; - if (!load_shape(filename, shape, error, true)) return false; + auto shape = load_shape(filename, true); scene.instances.push_back({{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}, {0, 0, 0}}, (int)scene.shapes.size() - 1, -1}); @@ -4409,16 +3504,13 @@ static bool load_stl_scene( add_missing_camera(scene); add_missing_radius(scene); add_missing_lights(scene); - - // done - return true; } -static bool save_stl_scene(const string& filename, const scene_data& scene, - string& error, bool noparallel) { +static void save_stl_scene( + const string& filename, const scene_data& scene, bool noparallel) { // save shape if (scene.shapes.empty()) throw std::invalid_argument{"empty shape"}; - return save_shape(filename, scene.shapes.front(), error, false); + return save_shape(filename, scene.shapes.front(), false); } } // namespace yocto @@ -4429,11 +3521,10 @@ static bool save_stl_scene(const string& filename, const scene_data& scene, namespace yocto { // Load a scene -static bool load_gltf_scene( - const string& filename, scene_data& scene, string& error, bool noparallel) { +static void load_gltf_scene( + const string& filename, scene_data& scene, bool noparallel) { // load gltf data - auto data = vector{}; - if (!load_binary(filename, data, error)) return false; + auto data = load_binary(filename); // parse glTF auto options = cgltf_options{}; @@ -4442,8 +3533,7 @@ static bool load_gltf_scene( auto cgltf_result = cgltf_parse( &options, data.data(), data.size(), &cgltf_ptr); if (cgltf_result != cgltf_result_success) { - error = "cannot parse " + filename; - return false; + throw io_error{"cannot parse " + filename}; } // load buffers @@ -4452,19 +3542,14 @@ static bool load_gltf_scene( auto buffer_result = cgltf_load_buffers( &options, cgltf_ptr, dirname_.c_str()); if (buffer_result != cgltf_result_success) { - error = "cannot load " + filename + " since cannot load buffers"; cgltf_free(cgltf_ptr); - return false; + throw io_error{"cannot load " + filename + " since cannot load buffers"}; } // setup parsing auto& cgltf = *cgltf_ptr; auto cgltf_guard = std::unique_ptr( cgltf_ptr, cgltf_free); - auto unsupported_error = [&filename, &error](const string& message) { - error = "cannot load " + filename + " for sunsupported " + message; - return false; - }; // convert cameras auto cameras = vector{}; @@ -4491,7 +3576,8 @@ static bool load_gltf_scene( } camera.focus = 1; } else { - return unsupported_error("camera type"); + throw io_error{ + "cannot load " + filename + " for unsupported camera type"}; } } @@ -4576,27 +3662,36 @@ static bool load_gltf_scene( for (auto idx : range(gprimitive.attributes_count)) { auto& gattribute = gprimitive.attributes[idx]; auto& gaccessor = *gattribute.data; - if (gaccessor.is_sparse) return unsupported_error("sparse accessor"); + if (gaccessor.is_sparse) + throw io_error{ + "cannot load " + filename + " for unsupported sparse accessor"}; auto gname = string{gattribute.name}; auto count = gaccessor.count; auto components = cgltf_num_components(gaccessor.type); auto dcomponents = components; auto data = (float*)nullptr; if (gname == "POSITION") { - if (components != 3) return unsupported_error("position components"); + if (components != 3) + throw io_error{"cannot load " + filename + + " for unsupported position components"}; shape.positions.resize(count); data = (float*)shape.positions.data(); } else if (gname == "NORMAL") { - if (components != 3) return unsupported_error("normal components"); + if (components != 3) + throw io_error{"cannot load " + filename + + " for unsupported normal components"}; shape.normals.resize(count); data = (float*)shape.normals.data(); } else if (gname == "TEXCOORD" || gname == "TEXCOORD_0") { - if (components != 2) return unsupported_error("texcoord components"); + if (components != 2) + throw io_error{"cannot load " + filename + + " for unsupported texture components"}; shape.texcoords.resize(count); data = (float*)shape.texcoords.data(); } else if (gname == "COLOR" || gname == "COLOR_0") { if (components != 3 && components != 4) - return unsupported_error("color components"); + throw io_error{"cannot load " + filename + + " for unsupported color components"}; shape.colors.resize(count); data = (float*)shape.colors.data(); if (components == 3) { @@ -4604,11 +3699,15 @@ static bool load_gltf_scene( for (auto& c : shape.colors) c.w = 1; } } else if (gname == "TANGENT") { - if (components != 4) return unsupported_error("tangent components"); + if (components != 4) + throw io_error{"cannot load " + filename + + " for unsupported tangent components"}; shape.tangents.resize(count); data = (float*)shape.tangents.data(); } else if (gname == "RADIUS") { - if (components != 1) return unsupported_error("radius components"); + if (components != 1) + throw io_error{"cannot load " + filename + + " for unsupported radius components"}; shape.radius.resize(count); data = (float*)shape.radius.data(); } else { @@ -4619,7 +3718,8 @@ static bool load_gltf_scene( for (auto idx : range(count)) { if (!cgltf_accessor_read_float( &gaccessor, idx, &data[idx * dcomponents], components)) - return unsupported_error("accessor float conversion"); + throw io_error{"cannot load " + filename + + " for unsupported accessor conversion"}; } // fixes if (gname == "TANGENT") { @@ -4654,19 +3754,23 @@ static bool load_gltf_scene( for (auto i = 1; i < (int)shape.positions.size(); i++) shape.lines[i - 1] = {i - 1, i}; } else if (gprimitive.type == cgltf_primitive_type_points) { - return unsupported_error("point primitive"); + throw io_error{ + "cannot load " + filename + " for unsupported point primitive"}; } else { - return unsupported_error("primitive type"); + throw io_error{ + "cannot load " + filename + " for unsupported primitive type"}; } } else { auto& gaccessor = *gprimitive.indices; if (gaccessor.type != cgltf_type_scalar) - return unsupported_error("non-scalar indices"); + throw io_error{"cannot load " + filename + + " for unsupported non-scalar indices"}; auto indices = vector(gaccessor.count); for (auto idx : range(gaccessor.count)) { if (!cgltf_accessor_read_uint( &gaccessor, idx, (cgltf_uint*)&indices[idx], 1)) - return unsupported_error("accessor uint conversion"); + throw io_error{ + "cannot load " + filename + " for unsupported accessor type"}; } if (gprimitive.type == cgltf_primitive_type_triangles) { shape.triangles.resize(indices.size() / 3); @@ -4703,9 +3807,11 @@ static bool load_gltf_scene( shape.lines[i] = {indices[i + 0], indices[i + 1]}; } } else if (gprimitive.type == cgltf_primitive_type_points) { - return unsupported_error("points primitive"); + throw io_error{ + "cannot load " + filename + " for unsupported points indices"}; } else { - return unsupported_error("primitive type"); + throw io_error{ + "cannot load " + filename + " for unsupported primitive type"}; } } } @@ -4718,7 +3824,7 @@ static bool load_gltf_scene( auto& camera = scene.cameras.emplace_back(); camera = cameras.at(gnode.camera - cgltf.cameras); auto xform = mat4f{ - {1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}, {0, 0, 0, 1}}; + {1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}, {0, 0, 0, 1}}; cgltf_node_transform_world(&gnode, &xform.x.x); camera.frame = mat_to_frame(xform); } @@ -4727,7 +3833,7 @@ static bool load_gltf_scene( auto& instance = scene.instances.emplace_back(); instance = primitive; auto xform = mat4f{ - {1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}, {0, 0, 0, 1}}; + {1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}, {0, 0, 0, 1}}; cgltf_node_transform_world(&gnode, &xform.x.x); instance.frame = mat_to_frame(xform); } @@ -4735,27 +3841,17 @@ static bool load_gltf_scene( } // dirname - auto dirname = path_dirname(filename); - auto dependent_error = [&filename, &error]() { - error = "cannot save " + filename + " since " + error; - return false; - }; + auto dirname = path_dirname(filename); - if (noparallel) { - // load texture - for (auto& texture : scene.textures) { - auto& path = texture_paths[&texture - &scene.textures.front()]; - if (!load_texture(path_join(dirname, path), texture, error)) - return dependent_error(); - } - } else { + try { // load textures - if (!parallel_foreach( - scene.textures, error, [&](auto& texture, string& error) { - auto& path = texture_paths[&texture - &scene.textures.front()]; - return load_texture(path_join(dirname, path), texture, error); - })) - return dependent_error(); + parallel_foreach(scene.textures, noparallel, [&](auto& texture) { + auto& path = texture_paths[&texture - &scene.textures.front()]; + return load_texture(path_join(dirname, path), texture); + }); + } catch (std::exception& except) { + throw io_error( + "cannot load " + filename + " since " + string(except.what())); } // fix scene @@ -4763,14 +3859,11 @@ static bool load_gltf_scene( add_missing_camera(scene); add_missing_radius(scene); add_missing_lights(scene); - - // done - return true; } // Load a scene -static bool save_gltf_scene(const string& filename, const scene_data& scene, - string& error, bool noparallel) { +static void save_gltf_scene( + const string& filename, const scene_data& scene, bool noparallel) { // prepare data auto cgltf_ptr = (cgltf_data*)malloc(sizeof(cgltf_data)); memset(cgltf_ptr, 0, sizeof(cgltf_data)); @@ -4941,7 +4034,7 @@ static bool save_gltf_scene(const string& filename, const scene_data& scene, auto& gbuffer = cgltf.buffers[idx]; shape_accessor_start[idx] = (int)cgltf.accessors_count; gbuffer.uri = copy_string( - "shapes/" + get_shape_name(scene, shape) + ".bin"); + "shapes/" + get_shape_name(scene, shape) + ".bin"); add_vertex(cgltf, gbuffer, shape.positions.size(), cgltf_type_vec3, (const float*)shape.positions.data()); add_vertex(cgltf, gbuffer, shape.normals.size(), cgltf_type_vec3, @@ -5075,9 +4168,8 @@ static bool save_gltf_scene(const string& filename, const scene_data& scene, cgltf.memory.free = [](void*, void* ptr) { free(ptr); }; auto result = cgltf_write_file(&options, filename.c_str(), &cgltf); if (result != cgltf_result_success) { - error = "cannot save " + filename; cgltf_free(&cgltf); - return false; + throw io_error{"cannot save " + filename}; } // cleanup @@ -5090,44 +4182,23 @@ static bool save_gltf_scene(const string& filename, const scene_data& scene, }; // dirname - auto dirname = path_dirname(filename); - auto dependent_error = [&filename, &error]() { - error = "cannot save " + filename + " since " + error; - return false; - }; + auto dirname = path_dirname(filename); - if (noparallel) { + try { // save shapes - for (auto& shape : scene.shapes) { + parallel_foreach(scene.shapes, noparallel, [&](auto& shape) { auto path = "shapes/" + get_shape_name(scene, shape) + ".bin"; - if (!save_binshape(path_join(dirname, path), shape, error)) - return dependent_error(); - } + return save_binshape(path_join(dirname, path), shape); + }); // save textures - for (auto& texture : scene.textures) { + parallel_foreach(scene.textures, noparallel, [&](auto& texture) { auto path = "textures/" + get_texture_name(scene, texture) + ".png"; - if (!save_texture(path_join(dirname, path), texture, error)) - return dependent_error(); - } - } else { - // save shapes - if (!parallel_foreach(scene.shapes, error, [&](auto& shape, string& error) { - auto path = "shapes/" + get_shape_name(scene, shape) + ".bin"; - return save_binshape(path_join(dirname, path), shape, error); - })) - return dependent_error(); - // save textures - if (!parallel_foreach( - scene.textures, error, [&](auto& texture, string& error) { - auto path = "textures/" + get_texture_name(scene, texture) + - ".png"; - return save_texture(path_join(dirname, path), texture, error); - })) - return dependent_error(); + return save_texture(path_join(dirname, path), texture); + }); + } catch (std::exception& except) { + throw io_error( + "cannot save " + filename + " since " + string(except.what())); } - - // done - return true; } } // namespace yocto @@ -5138,11 +4209,10 @@ static bool save_gltf_scene(const string& filename, const scene_data& scene, namespace yocto { // load pbrt scenes -static bool load_pbrt_scene( - const string& filename, scene_data& scene, string& error, bool noparallel) { +static void load_pbrt_scene( + const string& filename, scene_data& scene, bool noparallel) { // load pbrt - auto pbrt = pbrt_model{}; - if (!load_pbrt(filename, pbrt, error)) return false; + auto pbrt = load_pbrt(filename); // convert cameras for (auto& pcamera : pbrt.cameras) { @@ -5235,54 +4305,33 @@ static bool load_pbrt_scene( } // dirname - auto dirname = path_dirname(filename); - auto dependent_error = [&filename, &error]() { - error = "cannot load " + filename + " since " + error; - return false; - }; + auto dirname = path_dirname(filename); - if (noparallel) { - // load shape - for (auto& shape : scene.shapes) { - auto& path = shapes_paths[&shape - &scene.shapes.front()]; - if (path.empty()) continue; - if (!load_shape(path_join(dirname, path), shape, error, true)) - return dependent_error(); - } - // load texture - for (auto& texture : scene.textures) { - auto& path = texture_paths[&texture - &scene.textures.front()]; - if (!load_texture(path_join(dirname, path), texture, error)) - return dependent_error(); - } - } else { + try { // load shapes - if (!parallel_foreach(scene.shapes, error, [&](auto& shape, string& error) { - auto& path = shapes_paths[&shape - &scene.shapes.front()]; - if (path.empty()) return true; - return load_shape(path_join(dirname, path), shape, error, true); - })) - return dependent_error(); + parallel_foreach(scene.shapes, noparallel, [&](auto& shape) { + auto& path = shapes_paths[&shape - &scene.shapes.front()]; + if (path.empty()) return; + load_shape(path_join(dirname, path), shape, true); + }); // load textures - if (!parallel_foreach( - scene.textures, error, [&](auto& texture, string& error) { - auto& path = texture_paths[&texture - &scene.textures.front()]; - return load_texture(path_join(dirname, path), texture, error); - })) - return dependent_error(); + parallel_foreach(scene.textures, noparallel, [&](auto& texture) { + auto& path = texture_paths[&texture - &scene.textures.front()]; + return load_texture(path_join(dirname, path), texture); + }); + } catch (std::exception& except) { + throw io_error( + "cannot load " + filename + " since " + string(except.what())); } // fix scene add_missing_camera(scene); add_missing_radius(scene); - - // done - return true; } // Save a pbrt scene -static bool save_pbrt_scene(const string& filename, const scene_data& scene, - string& error, bool noparallel) { +static void save_pbrt_scene( + const string& filename, const scene_data& scene, bool noparallel) { // save pbrt auto pbrt = pbrt_model{}; @@ -5342,48 +4391,27 @@ static bool save_pbrt_scene(const string& filename, const scene_data& scene, } // save pbrt - if (!save_pbrt(filename, pbrt, error)) return false; + save_pbrt(filename, pbrt); // dirname - auto dirname = path_dirname(filename); - auto dependent_error = [&filename, &error]() { - error = "cannot save " + filename + " since " + error; - return false; - }; + auto dirname = path_dirname(filename); - if (noparallel) { - // save textures - for (auto& shape : scene.shapes) { - auto path = "shapes/" + get_shape_name(scene, shape) + ".ply"; - if (!save_shape(path_join(dirname, path), shape, error, true)) - return dependent_error(); - } - // save shapes - for (auto& texture : scene.textures) { - auto path = "textures/" + get_texture_name(scene, texture) + - (!texture.pixelsf.empty() ? ".hdr" : ".png"); - if (!save_texture(path_join(dirname, path), texture, error)) - return dependent_error(); - } - } else { + try { // save shapes - if (!parallel_foreach(scene.shapes, error, [&](auto& shape, string& error) { - auto path = "shapes/" + get_shape_name(scene, shape) + ".ply"; - return save_shape(path_join(dirname, path), shape, error, true); - })) - return dependent_error(); + parallel_foreach(scene.shapes, noparallel, [&](auto& shape) { + auto path = "shapes/" + get_shape_name(scene, shape) + ".ply"; + return save_shape(path_join(dirname, path), shape, true); + }); // save textures - if (!parallel_foreach( - scene.textures, error, [&](auto& texture, string& error) { - auto path = "textures/" + get_texture_name(scene, texture) + - (!texture.pixelsf.empty() ? ".hdr"s : ".png"s); - return save_texture(path_join(dirname, path), texture, error); - })) - return dependent_error(); + parallel_foreach(scene.textures, noparallel, [&](auto& texture) { + auto path = "textures/" + get_texture_name(scene, texture) + + (!texture.pixelsf.empty() ? ".hdr"s : ".png"s); + return save_texture(path_join(dirname, path), texture); + }); + } catch (std::exception& except) { + throw io_error( + "cannot save " + filename + " since " + string(except.what())); } - - // done - return true; } } // namespace yocto @@ -5394,12 +4422,11 @@ static bool save_pbrt_scene(const string& filename, const scene_data& scene, namespace yocto { // load mitsuba scenes -static bool load_mitsuba_scene( - const string& filename, scene_data& scene, string& error, bool noparallel) { +static void load_mitsuba_scene( + const string& filename, scene_data& scene, bool noparallel) { // not implemented - error = "cannot load " + filename + - " since format is not supported for reading"; - return false; + throw io_error( + "cannot load " + filename + " since format is not supported for reading"); } // To xml helpers @@ -5506,8 +4533,8 @@ static void xml_property(string& xml, const string& indent, const string& name, } // Save a mitsuba scene -static bool save_mitsuba_scene(const string& filename, const scene_data& scene, - string& error, bool noparallel) { +static void save_mitsuba_scene( + const string& filename, const scene_data& scene, bool noparallel) { // write xml directly auto xml = string{}; @@ -5717,14 +4744,10 @@ static bool save_mitsuba_scene(const string& filename, const scene_data& scene, xml_end(xml, indent, "scene"); // save xml - if (!save_text(filename, xml, error)) return false; + save_text(filename, xml); // dirname - auto dirname = path_dirname(filename); - auto dependent_error = [&filename, &error]() { - error = "cannot save " + filename + " since " + error; - return false; - }; + auto dirname = path_dirname(filename); auto triangulate = [](const shape_data& shape) -> shape_data { if (shape.quads.empty()) return shape; @@ -5734,41 +4757,22 @@ static bool save_mitsuba_scene(const string& filename, const scene_data& scene, return tshape; }; - if (noparallel) { + try { // save shapes - for (auto& shape : scene.shapes) { + parallel_foreach(scene.shapes, noparallel, [&](auto& shape) { auto path = "shapes/" + get_shape_name(scene, shape) + ".ply"; - if (!save_shape( - path_join(dirname, path), triangulate(shape), error, true)) - return dependent_error(); - } + return save_shape(path_join(dirname, path), triangulate(shape), true); + }); // save textures - for (auto& texture : scene.textures) { + parallel_foreach(scene.textures, noparallel, [&](auto& texture) { auto path = "textures/" + get_texture_name(scene, texture) + - (!texture.pixelsf.empty() ? ".hdr" : ".png"); - if (!save_texture(path_join(dirname, path), texture, error)) - return dependent_error(); - } - } else { - // save shapes - if (!parallel_foreach(scene.shapes, error, [&](auto& shape, string& error) { - auto path = "shapes/" + get_shape_name(scene, shape) + ".ply"; - return save_shape( - path_join(dirname, path), triangulate(shape), error, true); - })) - return dependent_error(); - // save textures - if (!parallel_foreach( - scene.textures, error, [&](auto& texture, string& error) { - auto path = "textures/" + get_texture_name(scene, texture) + - (!texture.pixelsf.empty() ? ".hdr"s : ".png"s); - return save_texture(path_join(dirname, path), texture, error); - })) - return dependent_error(); + (!texture.pixelsf.empty() ? ".hdr"s : ".png"s); + return save_texture(path_join(dirname, path), texture); + }); + } catch (std::exception& except) { + throw io_error( + "cannot save " + filename + " since " + string(except.what())); } - - // done - return true; } } // namespace yocto @@ -5829,9 +4833,6 @@ static void to_json(json_value& json, const trace_params& value) { json["embreebvh"] = value.embreebvh; json["highqualitybvh"] = value.highqualitybvh; json["noparallel"] = value.noparallel; - json["pratio"] = value.pratio; - json["denoise"] = value.denoise; - json["batch"] = value.batch; } static void from_json(const json_value& json, trace_params& value) { value.camera = json.value("camera", value.camera); @@ -5848,9 +4849,6 @@ static void from_json(const json_value& json, trace_params& value) { value.embreebvh = json.value("embreebvh", value.embreebvh); value.highqualitybvh = json.value("highqualitybvh", value.highqualitybvh); value.noparallel = json.value("noparallel", value.noparallel); - value.pratio = json.value("pratio", value.pratio); - value.denoise = json.value("denoise", value.denoise); - value.batch = json.value("batch", value.batch); } // Conversion to/from json @@ -5890,115 +4888,72 @@ static void from_json(const json_value& json, colorgrade_params& value) { "highlights_color", value.highlights_color); } -// Load/Save/Update trace params template -static bool load_params(const string& filename, Params& params, string& error) { +static Params load_params(const string& filename) { + auto params = Params{}; + load_params(filename, params); + return params; +} +template +static void load_params(const string& filename, Params& params) { // json - auto json = json_value{}; - if (!load_json(filename, json, error)) return false; + auto json = load_json(filename); // conversion try { params = {}; from_json(json, params); - return true; } catch (...) { - error = "error parsing params"; - return false; + throw io_error{"error parsing params"}; } } template -static bool update_params( - const string& filename, Params& params, string& error) { +static void update_params(const string& filename, Params& params) { // json - auto json = json_value{}; - if (!load_json(filename, json, error)) return false; + auto json = load_json(filename); // conversion try { from_json(json, params); - return true; } catch (...) { - error = "error parsing params"; - return false; + throw io_error{"error parsing params"}; } } template -static bool save_params( - const string& filename, const Params& params, string& error) { +static void save_params(const string& filename, const Params& params) { auto json = json_value{}; to_json(json, params); - return save_json(filename, json, error); + return save_json(filename, json); } // Load/Save/Update trace params trace_params load_trace_params(const string& filename) { - auto params = trace_params{}; - auto error = string{}; - if (!load_trace_params(filename, params, error)) throw io_error{error}; - return params; + return load_params(filename); } void load_trace_params(const string& filename, trace_params& params) { - auto error = string{}; - if (!load_trace_params(filename, params, error)) throw io_error{error}; + return load_params(filename, params); } void update_trace_params(const string& filename, trace_params& params) { - auto error = string{}; - if (!update_trace_params(filename, params, error)) throw io_error{error}; + return update_params(filename, params); } void save_trace_params(const string& filename, const trace_params& params) { - auto error = string{}; - if (!save_trace_params(filename, params, error)) throw io_error{error}; + return save_params(filename, params); } // Load/Save/Update color grade params colorgrade_params load_colorgrade_params(const string& filename) { - auto params = colorgrade_params{}; - auto error = string{}; - if (!load_colorgrade_params(filename, params, error)) throw io_error{error}; - return params; + return load_params(filename); } void load_colorgrade_params(const string& filename, colorgrade_params& params) { - auto error = string{}; - if (!load_colorgrade_params(filename, params, error)) throw io_error{error}; + return load_params(filename, params); } void update_colorgrade_params( const string& filename, colorgrade_params& params) { - auto error = string{}; - if (!update_colorgrade_params(filename, params, error)) throw io_error{error}; + return update_params(filename, params); } void save_colorgrade_params( const string& filename, const colorgrade_params& params) { - auto error = string{}; - if (!save_colorgrade_params(filename, params, error)) throw io_error{error}; -} - -// Load/Save/Update trace params -bool load_trace_params( - const string& filename, trace_params& params, string& error) { - return load_params(filename, params, error); -} -bool update_trace_params( - const string& filename, trace_params& params, string& error) { - return update_params(filename, params, error); -} -bool save_trace_params( - const string& filename, const trace_params& params, string& error) { - return save_params(filename, params, error); -} - -// Load/Save/Update color grade params -bool load_colorgrade_params( - const string& filename, colorgrade_params& params, string& error) { - return load_params(filename, params, error); -} -bool update_colorgrade_params( - const string& filename, colorgrade_params& params, string& error) { - return update_params(filename, params, error); -} -bool save_colorgrade_params( - const string& filename, const colorgrade_params& params, string& error) { - return save_params(filename, params, error); + return save_params(filename, params); } } // namespace yocto @@ -6295,8 +5250,8 @@ bool save_volume( // ----------------------------------------------------------------------------- namespace yocto { -image filter_bilateral(const image& img, float spatial_sigma, - float range_sigma, const vector>& features, +image_t filter_bilateral(const image_t& img, float spatial_sigma, + float range_sigma, const vector>& features, const vector& features_sigma) { auto filtered = image{img.imsize(), vec4f{0,0,0,0}}; auto filter_width = (int)ceil(2.57f * spatial_sigma); @@ -6332,8 +5287,8 @@ image filter_bilateral(const image& img, float spatial_sigma, return filtered; } -image filter_bilateral( - const image& img, float spatial_sigma, float range_sigma) { +image_t filter_bilateral( + const image_t& img, float spatial_sigma, float range_sigma) { auto filtered = image{img.imsize(), vec4f{0,0,0,0}}; auto fwidth = (int)ceil(2.57f * spatial_sigma); auto sw = 1 / (2.0f * spatial_sigma * spatial_sigma); diff --git a/libs/yocto/yocto_sceneio.h b/libs/yocto/yocto_sceneio.h index c12414df6..1fc325f33 100644 --- a/libs/yocto/yocto_sceneio.h +++ b/libs/yocto/yocto_sceneio.h @@ -39,6 +39,7 @@ // INCLUDES // ----------------------------------------------------------------------------- +#include #include #include #include @@ -79,20 +80,24 @@ bool is_hdr_filename(const string& filename); bool is_ldr_filename(const string& filename); // Loads/saves a 4 channels float/byte image in linear/srgb color space. -bool load_image(const string& filename, image_data& img, string& error); -bool save_image(const string& filename, const image_data& img, string& error); - -// Loads/saves a 4 channels float/byte image in linear/srgb color space. -image_data load_image(const string& filename); -void load_image(const string& filename, image_data& image); -void save_image(const string& filename, const image_data& image); - -// Make presets. Supported mostly in IO. -image_data make_image_preset(const string& type); +image_t load_image(const string& filename, bool srgb = false); +void load_image( + const string& filename, image_t& image, bool srgb = false); +void save_image( + const string& filename, const image_t& image, bool srgb = false); + +// Loads/saves a byte image. +image_t load_imageb(const string& filename, bool srgb = true); +void load_image( + const string& filename, image_t& image, bool srgb = true); +void save_image( + const string& filename, const image_t& image, bool srgb = true); // Make presets. Supported mostly in IO. -bool make_image_preset( - const string& filename, image_data& image, string& error); +bool is_srgb_preset(const string& type_); +image_t make_image_preset(const string& type); +bool make_image_preset( + const string& filename, image_t& image, string& error); } // namespace yocto @@ -195,7 +200,10 @@ bool make_scene_directories( const string& filename, const scene_data& scene, string& error); // Add environment -bool add_environment(scene_data& scene, const string& filename, string& error); +bool add_environment(scene_data& scene, const string& name, + const string& filename, string& error); +void add_environment( + scene_data& scene, const string& name, const string& filename); // Load/save a scene in the supported formats. scene_data load_scene(const string& filename, bool noparallel = false); @@ -205,7 +213,8 @@ void save_scene( const string& filename, const scene_data& scene, bool noparallel = false); // Add environment -void add_environment(scene_data& scene, const string& filename); +void add_environment( + scene_data& scene, const string& name, const string& filename); // Make missing scene directories void make_scene_directories(const string& filename, const scene_data& scene); @@ -314,31 +323,4 @@ bool make_directory(const string& path, string& error); } // namespace yocto -// ----------------------------------------------------------------------------- -// FILE WATCHER -// ----------------------------------------------------------------------------- -namespace yocto { - -// File watcher -struct watch_context { - std::atomic version = 0; - std::future worker = {}; - vector filenames = {}; - vector filetimes = {}; - int64_t delay = 500; - std::atomic stop = false; -}; - -// Initialize file watcher -watch_context make_watch_context( - const vector& filenames, int delay = 500); -// Start file watcher -void watch_start(watch_context& context); -// Stop file watcher -void watch_stop(watch_context& context); -// Get file version -int get_version(const watch_context& context); - -} // namespace yocto - #endif diff --git a/libs/yocto/yocto_trace.cpp b/libs/yocto/yocto_trace.cpp index 67942867a..07f862942 100644 --- a/libs/yocto/yocto_trace.cpp +++ b/libs/yocto/yocto_trace.cpp @@ -77,6 +77,33 @@ inline void parallel_for(T num1, T num2, Func&& func) { for (auto& f : futures) f.get(); } +// Simple parallel for used since our target platforms do not yet support +// parallel algorithms. `Func` takes the two integer indices. +template +inline void parallel_for_batch(vec2i num, Func&& func) { + auto futures = vector>{}; + auto nthreads = std::thread::hardware_concurrency(); + std::atomic next_idx(0); + std::atomic has_error(false); + for (auto thread_id = 0; thread_id < (int)nthreads; thread_id++) { + futures.emplace_back( + std::async(std::launch::async, [&func, &next_idx, &has_error, num]() { + try { + while (true) { + auto j = next_idx.fetch_add(1); + if (j >= num[1]) break; + if (has_error) break; + for (auto i = 0; i < num[0]; i++) func(vec2i{i, j}); + } + } catch (...) { + has_error = true; + throw; + } + })); + } + for (auto& f : futures) f.get(); +} + } // namespace yocto // ----------------------------------------------------------------------------- @@ -372,10 +399,11 @@ static vec3f sample_lights(const scene_data& scene, const trace_lights& lights, } else if (light.environment != invalidid) { auto& environment = scene.environments[light.environment]; if (environment.emission_tex != invalidid) { - auto& emission_tex = scene.textures[environment.emission_tex]; - auto idx = sample_discrete(light.elements_cdf, rel); - auto uv = vec2f{((idx % emission_tex.width) + 0.5f) / emission_tex.width, - ((idx / emission_tex.width) + 0.5f) / emission_tex.height}; + auto& texture = scene.textures[environment.emission_tex]; + auto idx = sample_discrete(light.elements_cdf, rel); + auto size = max(texture.pixelsf.size(), texture.pixelsb.size()); + auto uv = vec2f{ + ((idx % size.x) + 0.5f) / size.x, ((idx / size.x) + 0.5f) / size.y}; return transform_direction(environment.frame, {cos(uv.x * 2 * pif) * sin(uv.y * pif), cos(uv.y * pif), sin(uv.x * 2 * pif) * sin(uv.y * pif)}); @@ -422,16 +450,14 @@ static float sample_lights_pdf(const scene_data& scene, const trace_bvh& bvh, auto texcoord = vec2f{atan2(wl.z, wl.x) / (2 * pif), acos(clamp(wl.y, -1.0f, 1.0f)) / pif}; if (texcoord.x < 0) texcoord.x += 1; - auto i = clamp( - (int)(texcoord.x * emission_tex.width), 0, emission_tex.width - 1); - auto j = clamp((int)(texcoord.y * emission_tex.height), 0, - emission_tex.height - 1); + auto size = max( + emission_tex.pixelsf.size(), emission_tex.pixelsb.size()); + auto ij = clamp((vec2i)(texcoord * (vec2f)size), zero2i, size - 1); auto prob = sample_discrete_pdf( - light.elements_cdf, j * emission_tex.width + i) / + light.elements_cdf, ij.y * size.x + ij.x) / light.elements_cdf.back(); - auto angle = (2 * pif / emission_tex.width) * - (pif / emission_tex.height) * - sin(pif * (j + 0.5f) / emission_tex.height); + auto angle = (2 * pif / size.x) * (pif / size.y) * + sin(pif * (ij.y + 0.5f) / size.y); pdf += prob / angle; } else { pdf += 1 / (4 * pif); @@ -479,7 +505,7 @@ static trace_result trace_path(const scene_data& scene, const trace_bvh& bvh, if (!volume_stack.empty()) { auto& vsdf = volume_stack.back(); auto distance = sample_transmittance( - vsdf.density, intersection.distance, rand1f(rng), rand1f(rng)); + vsdf.density, intersection.distance, rand1f(rng), rand1f(rng)); weight *= eval_transmittance(vsdf.density, distance) / sample_transmittance_pdf( vsdf.density, distance, intersection.distance); @@ -626,7 +652,7 @@ static trace_result trace_pathdirect(const scene_data& scene, if (!volume_stack.empty()) { auto& vsdf = volume_stack.back(); auto distance = sample_transmittance( - vsdf.density, intersection.distance, rand1f(rng), rand1f(rng)); + vsdf.density, intersection.distance, rand1f(rng), rand1f(rng)); weight *= eval_transmittance(vsdf.density, distance) / sample_transmittance_pdf( vsdf.density, distance, intersection.distance); @@ -805,7 +831,7 @@ static trace_result trace_pathmis(const scene_data& scene, const trace_bvh& bvh, if (!volume_stack.empty()) { auto& vsdf = volume_stack.back(); auto distance = sample_transmittance( - vsdf.density, intersection.distance, rand1f(rng), rand1f(rng)); + vsdf.density, intersection.distance, rand1f(rng), rand1f(rng)); weight *= eval_transmittance(vsdf.density, distance) / sample_transmittance_pdf( vsdf.density, distance, intersection.distance); @@ -1028,25 +1054,26 @@ static trace_result trace_pathtest(const scene_data& scene, return {radiance, hit, hit_albedo, hit_normal}; } -// Recursive path tracing. -static trace_result trace_naive(const scene_data& scene, const trace_bvh& bvh, - const trace_lights& lights, const ray3f& ray_, rng_state& rng, - const trace_params& params) { +// Recursive path tracing by sampling lights. +static trace_result trace_lightsampling(const scene_data& scene, + const trace_bvh& bvh, const trace_lights& lights, const ray3f& ray_, + rng_state& rng, const trace_params& params) { // initialize - auto radiance = vec3f{0, 0, 0}; - auto weight = vec3f{1, 1, 1}; - auto ray = ray_; - auto hit = false; - auto hit_albedo = vec3f{0, 0, 0}; - auto hit_normal = vec3f{0, 0, 0}; - auto opbounce = 0; + auto radiance = vec3f{0, 0, 0}; + auto weight = vec3f{1, 1, 1}; + auto ray = ray_; + auto hit = false; + auto hit_albedo = vec3f{0, 0, 0}; + auto hit_normal = vec3f{0, 0, 0}; + auto next_emission = true; + auto opbounce = 0; // trace path for (auto bounce = 0; bounce < params.bounces; bounce++) { // intersect next point auto intersection = intersect_scene(bvh, scene, ray); if (!intersection.hit) { - if (bounce > 0 || !params.envhidden) + if ((bounce > 0 || !params.envhidden) && next_emission) radiance += weight * eval_environment(scene, ray.d); break; } @@ -1073,7 +1100,33 @@ static trace_result trace_naive(const scene_data& scene, const trace_bvh& bvh, } // accumulate emission - radiance += weight * eval_emission(material, normal, outgoing); + if (next_emission) + radiance += weight * eval_emission(material, normal, outgoing); + + // direct + if (!is_delta(material)) { + auto incoming = sample_lights( + scene, lights, position, rand1f(rng), rand1f(rng), rand2f(rng)); + auto pdf = sample_lights_pdf(scene, bvh, lights, position, incoming); + auto bsdfcos = eval_bsdfcos(material, normal, outgoing, incoming); + if (bsdfcos != vec3f{0, 0, 0} && pdf > 0) { + auto intersection = intersect_scene(bvh, scene, {position, incoming}); + auto emission = + !intersection.hit + ? eval_environment(scene, incoming) + : eval_emission(eval_material(scene, + scene.instances[intersection.instance], + intersection.element, intersection.uv), + eval_shading_normal(scene, + scene.instances[intersection.instance], + intersection.element, intersection.uv, -incoming), + -incoming); + radiance += weight * bsdfcos * emission / pdf; + } + next_emission = false; + } else { + next_emission = true; + } // next direction auto incoming = vec3f{0, 0, 0}; @@ -1107,10 +1160,10 @@ static trace_result trace_naive(const scene_data& scene, const trace_bvh& bvh, return {radiance, hit, hit_albedo, hit_normal}; } -// Eyelight for quick previewing. -static trace_result trace_eyelight(const scene_data& scene, - const trace_bvh& bvh, const trace_lights& lights, const ray3f& ray_, - rng_state& rng, const trace_params& params) { +// Recursive path tracing. +static trace_result trace_naive(const scene_data& scene, const trace_bvh& bvh, + const trace_lights& lights, const ray3f& ray_, rng_state& rng, + const trace_params& params) { // initialize auto radiance = vec3f{0, 0, 0}; auto weight = vec3f{1, 1, 1}; @@ -1121,7 +1174,7 @@ static trace_result trace_eyelight(const scene_data& scene, auto opbounce = 0; // trace path - for (auto bounce = 0; bounce < max(params.bounces, 4); bounce++) { + for (auto bounce = 0; bounce < params.bounces; bounce++) { // intersect next point auto intersection = intersect_scene(bvh, scene, ray); if (!intersection.hit) { @@ -1152,21 +1205,33 @@ static trace_result trace_eyelight(const scene_data& scene, } // accumulate emission - auto incoming = outgoing; radiance += weight * eval_emission(material, normal, outgoing); - // brdf * light - radiance += weight * pif * - eval_bsdfcos(material, normal, outgoing, incoming); + // next direction + auto incoming = vec3f{0, 0, 0}; + if (!is_delta(material)) { + incoming = sample_bsdfcos( + material, normal, outgoing, rand1f(rng), rand2f(rng)); + if (incoming == vec3f{0, 0, 0}) break; + weight *= eval_bsdfcos(material, normal, outgoing, incoming) / + sample_bsdfcos_pdf(material, normal, outgoing, incoming); + } else { + incoming = sample_delta(material, normal, outgoing, rand1f(rng)); + if (incoming == vec3f{0, 0, 0}) break; + weight *= eval_delta(material, normal, outgoing, incoming) / + sample_delta_pdf(material, normal, outgoing, incoming); + } - // continue path - if (!is_delta(material)) break; - incoming = sample_delta(material, normal, outgoing, rand1f(rng)); - if (incoming == vec3f{0, 0, 0}) break; - weight *= eval_delta(material, normal, outgoing, incoming) / - sample_delta_pdf(material, normal, outgoing, incoming); + // check weight if (weight == vec3f{0, 0, 0} || !isfinite(weight)) break; + // russian roulette + if (bounce > 3) { + auto rr_prob = min((float)0.99, max(weight)); + if (rand1f(rng) >= rr_prob) break; + weight *= 1 / rr_prob; + } + // setup next iteration ray = {position, incoming}; } @@ -1174,10 +1239,10 @@ static trace_result trace_eyelight(const scene_data& scene, return {radiance, hit, hit_albedo, hit_normal}; } -// Diagram previewing. -static trace_result trace_diagram(const scene_data& scene, const trace_bvh& bvh, - const trace_lights& lights, const ray3f& ray_, rng_state& rng, - const trace_params& params) { +// Eyelight for quick previewing. +static trace_result trace_eyelight(const scene_data& scene, + const trace_bvh& bvh, const trace_lights& lights, const ray3f& ray_, + rng_state& rng, const trace_params& params) { // initialize auto radiance = vec3f{0, 0, 0}; auto weight = vec3f{1, 1, 1}; @@ -1192,10 +1257,8 @@ static trace_result trace_diagram(const scene_data& scene, const trace_bvh& bvh, // intersect next point auto intersection = intersect_scene(bvh, scene, ray); if (!intersection.hit) { - radiance += weight * vec3f{1, 1, 1}; - hit = true; - // if (bounce > 0 || !params.envhidden) - // radiance += weight * eval_environment(scene, ray.d); + if (bounce > 0 || !params.envhidden) + radiance += weight * eval_environment(scene, ray.d); break; } @@ -1428,9 +1491,9 @@ static sampler_func get_trace_sampler_func(const trace_params& params) { case trace_sampler_type::pathdirect: return trace_pathdirect; case trace_sampler_type::pathmis: return trace_pathmis; case trace_sampler_type::pathtest: return trace_pathtest; + case trace_sampler_type::lightsampling: return trace_lightsampling; case trace_sampler_type::naive: return trace_naive; case trace_sampler_type::eyelight: return trace_eyelight; - case trace_sampler_type::diagram: return trace_diagram; case trace_sampler_type::furnace: return trace_furnace; case trace_sampler_type::falsecolor: return trace_falsecolor; default: { @@ -1459,62 +1522,59 @@ bool is_sampler_lit(const trace_params& params) { // Trace a block of samples void trace_sample(trace_state& state, const scene_data& scene, - const trace_bvh& bvh, const trace_lights& lights, int i, int j, int sample, + const trace_bvh& bvh, const trace_lights& lights, vec2i ij, int sample, const trace_params& params) { auto& camera = scene.cameras[params.camera]; auto sampler = get_trace_sampler_func(params); - auto idx = state.width * j + i; - auto ray = sample_camera(camera, {i, j}, {state.width, state.height}, - rand2f(state.rngs[idx]), rand2f(state.rngs[idx]), params.tentfilter); + auto ray = sample_camera(camera, ij, state.render.size(), + rand2f(state.rngs[ij]), rand2f(state.rngs[ij]), params.tentfilter); auto [radiance, hit, albedo, normal] = sampler( - scene, bvh, lights, ray, state.rngs[idx], params); + scene, bvh, lights, ray, state.rngs[ij], params); if (!isfinite(radiance)) radiance = {0, 0, 0}; if (max(radiance) > params.clamp) radiance = radiance * (params.clamp / max(radiance)); auto weight = 1.0f / (sample + 1); if (hit) { - state.image[idx] = lerp( - state.image[idx], {radiance.x, radiance.y, radiance.z, 1}, weight); - state.albedo[idx] = lerp(state.albedo[idx], albedo, weight); - state.normal[idx] = lerp(state.normal[idx], normal, weight); - state.hits[idx] += 1; + state.render[ij] = lerp( + state.render[ij], {radiance.x, radiance.y, radiance.z, 1}, weight); + state.albedo[ij] = lerp(state.albedo[ij], albedo, weight); + state.normal[ij] = lerp(state.normal[ij], normal, weight); + state.hits[ij] += 1; } else if (!params.envhidden && !scene.environments.empty()) { - state.image[idx] = lerp( - state.image[idx], {radiance.x, radiance.y, radiance.z, 1}, weight); - state.albedo[idx] = lerp(state.albedo[idx], {1, 1, 1}, weight); - state.normal[idx] = lerp(state.normal[idx], -ray.d, weight); - state.hits[idx] += 1; + state.render[ij] = lerp( + state.render[ij], {radiance.x, radiance.y, radiance.z, 1}, weight); + state.albedo[ij] = lerp(state.albedo[ij], {1, 1, 1}, weight); + state.normal[ij] = lerp(state.normal[ij], -ray.d, weight); + state.hits[ij] += 1; } else { - state.image[idx] = lerp(state.image[idx], {0, 0, 0, 0}, weight); - state.albedo[idx] = lerp(state.albedo[idx], {0, 0, 0}, weight); - state.normal[idx] = lerp(state.normal[idx], -ray.d, weight); + state.render[ij] = lerp(state.render[ij], {0, 0, 0, 0}, weight); + state.albedo[ij] = lerp(state.albedo[ij], {0, 0, 0}, weight); + state.normal[ij] = lerp(state.normal[ij], -ray.d, weight); } } // Init a sequence of random number generators. trace_state make_trace_state( const scene_data& scene, const trace_params& params) { - auto& camera = scene.cameras[params.camera]; - auto state = trace_state{}; - if (camera.aspect >= 1) { - state.width = params.resolution; - state.height = (int)round(params.resolution / camera.aspect); - } else { - state.height = params.resolution; - state.width = (int)round(params.resolution * camera.aspect); - } - state.samples = 0; - state.image.assign(state.width * state.height, {0, 0, 0, 0}); - state.albedo.assign(state.width * state.height, {0, 0, 0}); - state.normal.assign(state.width * state.height, {0, 0, 0}); - state.hits.assign(state.width * state.height, 0); - state.rngs.assign(state.width * state.height, {}); - auto rng_ = make_rng(1301081); + auto& camera = scene.cameras[params.camera]; + auto state = trace_state{}; + auto resolution = (camera.aspect >= 1) + ? vec2i{params.resolution, + (int)round(params.resolution / camera.aspect)} + : vec2i{(int)round(params.resolution * camera.aspect), + params.resolution}; + state.samples = 0; + state.render = image_t{resolution}; + state.albedo = image_t{resolution}; + state.normal = image_t{resolution}; + state.hits = image_t{resolution}; + state.rngs = image_t{resolution}; + auto rng_ = make_rng(1301081); for (auto& rng : state.rngs) { rng = make_rng(params.seed, rand1i(rng_, 1 << 31) / 2 + 1); } if (params.denoise) { - state.denoised.assign(state.width * state.height, {0, 0, 0, 0}); + state.denoised = image_t{resolution}; } return state; } @@ -1565,11 +1625,12 @@ trace_lights make_trace_lights( light.environment = (int)handle; if (environment.emission_tex != invalidid) { auto& texture = scene.textures[environment.emission_tex]; - light.elements_cdf = vector(texture.width * texture.height); + auto size = max(texture.pixelsf.size(), texture.pixelsb.size()); + light.elements_cdf = vector(size.x * size.y); for (auto idx : range(light.elements_cdf.size())) { - auto ij = vec2i{(int)idx % texture.width, (int)idx / texture.width}; - auto th = (ij.y + 0.5f) * pif / texture.height; - auto value = lookup_texture(texture, ij.x, ij.y); + auto ij = vec2i{(int)idx % size.x, (int)idx / size.x}; + auto th = (ij.y + 0.5f) * pif / size.y; + auto value = lookup_texture(texture, ij); light.elements_cdf[idx] = max(value) * sin(th); if (idx != 0) light.elements_cdf[idx] += light.elements_cdf[idx - 1]; } @@ -1580,8 +1641,26 @@ trace_lights make_trace_lights( return lights; } +// Convenience helper +image_t trace_image(const scene_data& scene, trace_sampler_type type, + int resolution, int samples, int bounces) { + auto params = trace_params{}; + params.sampler = type; + params.resolution = resolution; + params.samples = samples; + params.bounces = bounces; + auto bvh = make_trace_bvh(scene, params); + auto lights = make_trace_lights(scene, params); + auto state = make_trace_state(scene, params); + for (auto sample = 0; sample < params.samples; sample++) { + trace_samples(state, scene, bvh, lights, params); + } + return get_image(state); +} + // Progressively computes an image. -image_data trace_image(const scene_data& scene, const trace_params& params) { +image_t trace_image( + const scene_data& scene, const trace_params& params) { auto bvh = make_trace_bvh(scene, params); auto lights = make_trace_lights(scene, params); auto state = make_trace_state(scene, params); @@ -1597,24 +1676,21 @@ void trace_samples(trace_state& state, const scene_data& scene, const trace_params& params) { if (state.samples >= params.samples) return; if (params.noparallel) { - for (auto j : range(state.height)) { - for (auto i : range(state.width)) { - for (auto sample : range(state.samples, state.samples + params.batch)) { - trace_sample(state, scene, bvh, lights, i, j, sample, params); - } + for (auto ij : range(state.render.size())) { + for (auto sample : range(state.samples, state.samples + params.batch)) { + trace_sample(state, scene, bvh, lights, ij, sample, params); } } } else { - parallel_for(state.width, state.height, [&](int i, int j) { + parallel_for_batch(state.render.size(), [&](vec2i ij) { for (auto sample : range(state.samples, state.samples + params.batch)) { - trace_sample(state, scene, bvh, lights, i, j, sample, params); + trace_sample(state, scene, bvh, lights, ij, sample, params); } }); } state.samples += params.batch; if (params.denoise && !state.denoised.empty()) { - denoise_image(state.denoised, state.width, state.height, state.image, - state.albedo, state.normal); + denoise_image(state.denoised, state.render, state.albedo, state.normal); } } @@ -1632,17 +1708,16 @@ void trace_start(trace_context& context, trace_state& state, context.done = false; context.worker = std::async(std::launch::async, [&]() { if (context.stop) return; - parallel_for(state.width, state.height, [&](int i, int j) { + parallel_for_batch(state.render.size(), [&](vec2i ij) { for (auto sample : range(state.samples, state.samples + params.batch)) { if (context.stop) return; - trace_sample(state, scene, bvh, lights, i, j, sample, params); + trace_sample(state, scene, bvh, lights, ij, sample, params); } }); state.samples += params.batch; if (context.stop) return; if (params.denoise && !state.denoised.empty()) { - denoise_image(state.denoised, state.width, state.height, state.image, - state.albedo, state.normal); + denoise_image(state.denoised, state.render, state.albedo, state.normal); } context.done = true; }); @@ -1657,7 +1732,7 @@ void trace_cancel(trace_context& context) { // Async done bool trace_done(const trace_context& context) { return context.done; } -void trace_preview(color_image& image, trace_context& context, +void trace_preview(image_t& image, trace_context& context, trace_state& state, const scene_data& scene, const trace_bvh& bvh, const trace_lights& lights, const trace_params& params) { // preview @@ -1667,66 +1742,50 @@ void trace_preview(color_image& image, trace_context& context, auto pstate = make_trace_state(scene, pparams); trace_samples(pstate, scene, bvh, lights, pparams); auto preview = get_image(pstate); - for (auto idx = 0; idx < state.width * state.height; idx++) { - auto i = idx % image.width, j = idx / image.width; - auto pi = clamp(i / params.pratio, 0, preview.width - 1), - pj = clamp(j / params.pratio, 0, preview.height - 1); - image.pixels[idx] = preview.pixels[pj * preview.width + pi]; + for (auto ij : range(state.size())) { + auto pij = clamp(ij / params.pratio, vec2i{0, 0}, preview.size() - 1); + image[ij] = preview[pij]; } }; // Check image type -static void check_image( - const image_data& image, int width, int height, bool linear) { - if (image.width != width || image.height != height) - throw std::invalid_argument{"image should have the same size"}; - if (image.linear != linear) - throw std::invalid_argument{ - linear ? "expected linear image" : "expected srgb image"}; -} template -static void check_image(const vector& image, int width, int height) { - if (image.size() != (size_t)width * (size_t)height) +static void check_image(const image_t& image, vec2i size) { + if (image.size() != size) throw std::invalid_argument{"image should have the same size"}; } // Get resulting render, denoised if requested -image_data get_image(const trace_state& state) { - auto image = make_image(state.width, state.height, true); - get_image(image, state); - return image; +image_t get_image(const trace_state& state) { + auto render = image_t{state.render.size()}; + get_image(render, state); + return render; } -void get_image(image_data& image, const trace_state& state) { - image.width = state.width; - image.height = state.height; - image.linear = true; +void get_image(image_t& render, const trace_state& state) { + check_image(render, state.render.size()); if (state.denoised.empty()) { - image.pixels = state.image; + render = state.render; } else { - image.pixels = state.denoised; + render = state.denoised; } } // Get resulting render -image_data get_rendered_image(const trace_state& state) { - auto image = make_image(state.width, state.height, true); - get_rendered_image(image, state); - return image; +image_t get_rendered_image(const trace_state& state) { + return state.render; } -void get_rendered_image(image_data& image, const trace_state& state) { - check_image(image, state.width, state.height, true); - for (auto idx = 0; idx < state.width * state.height; idx++) { - image.pixels[idx] = state.image[idx]; - } +void get_rendered_image(image_t& image, const trace_state& state) { + check_image(image, state.render.size()); + image = state.render; } // Get denoised render -image_data get_denoised_image(const trace_state& state) { - auto image = make_image(state.width, state.height, true); +image_t get_denoised_image(const trace_state& state) { + auto image = state.render; get_denoised_image(image, state); return image; } -void get_denoised_image(image_data& image, const trace_state& state) { +void get_denoised_image(image_t& image, const trace_state& state) { #if YOCTO_DENOISE // Create an Intel Open Image Denoise device oidn::DeviceRef device = oidn::newDevice(); @@ -1734,25 +1793,20 @@ void get_denoised_image(image_data& image, const trace_state& state) { // get image get_rendered_image(image, state); - - // get albedo and normal - auto albedo = vector(image.pixels.size()), - normal = vector(image.pixels.size()); - for (auto idx = 0; idx < state.width * state.height; idx++) { - albedo[idx] = state.albedo[idx]; - normal[idx] = state.normal[idx]; - } + auto& albedo = state.albedo; + auto& normal = state.normal; // Create a denoising filter oidn::FilterRef filter = device.newFilter("RT"); // ray tracing filter - filter.setImage("color", (void*)image.pixels.data(), oidn::Format::Float3, - state.width, state.height, 0, sizeof(vec4f), sizeof(vec4f) * state.width); + filter.setImage("color", (void*)image.data(), oidn::Format::Float3, + state.size().x, state.size().y, 0, sizeof(vec4f), + sizeof(vec4f) * state.size().x); filter.setImage("albedo", (void*)albedo.data(), oidn::Format::Float3, - state.width, state.height); + state.size().x, state.size().y); filter.setImage("normal", (void*)normal.data(), oidn::Format::Float3, - state.width, state.height); - filter.setImage("output", image.pixels.data(), oidn::Format::Float3, - state.width, state.height, 0, sizeof(vec4f), sizeof(vec4f) * state.width); + state.size().x, state.size().y); + filter.setImage("output", image.data(), oidn::Format::Float3, state.size().x, + state.size().y, 0, sizeof(vec4f), sizeof(vec4f) * state.size().x); filter.set("inputScale", 1.0f); // set scale as fixed filter.set("hdr", true); // image is HDR filter.commit(); @@ -1765,83 +1819,31 @@ void get_denoised_image(image_data& image, const trace_state& state) { } // Get denoising buffers -image_data get_albedo_image(const trace_state& state) { - auto albedo = make_image(state.width, state.height, true); - get_albedo_image(albedo, state); - return albedo; +image_t get_albedo_image(const trace_state& state) { + return state.albedo; } -void get_albedo_image(image_data& albedo, const trace_state& state) { - check_image(albedo, state.width, state.height, true); - for (auto idx = 0; idx < state.width * state.height; idx++) { - albedo.pixels[idx] = { - state.albedo[idx].x, state.albedo[idx].y, state.albedo[idx].z, 1.0f}; - } +void get_albedo_image(image_t& albedo, const trace_state& state) { + albedo = state.albedo; } -image_data get_normal_image(const trace_state& state) { - auto normal = make_image(state.width, state.height, true); - get_normal_image(normal, state); - return normal; +image_t get_normal_image(const trace_state& state) { + return state.normal; } -void get_normal_image(image_data& normal, const trace_state& state) { - check_image(normal, state.width, state.height, true); - for (auto idx = 0; idx < state.width * state.height; idx++) { - normal.pixels[idx] = { - state.normal[idx].x, state.normal[idx].y, state.normal[idx].z, 1.0f}; - } +void get_normal_image(image_t& normal, const trace_state& state) { + normal = state.normal; } // Denoise image -image_data denoise_image(const image_data& render, const image_data& albedo, - const image_data& normal) { - auto denoised = make_image(render.width, render.height, render.linear); +image_t denoise_image(const image_t& render, + const image_t& albedo, const image_t& normal) { + auto denoised = render; denoise_image(denoised, render, albedo, normal); return denoised; } -void denoise_image(image_data& denoised, const image_data& render, - const image_data& albedo, const image_data& normal) { - check_image(denoised, render.width, render.height, render.linear); - check_image(albedo, render.width, render.height, albedo.linear); - check_image(normal, render.width, render.height, normal.linear); -#if YOCTO_DENOISE - // Create an Intel Open Image Denoise device - oidn::DeviceRef device = oidn::newDevice(); - device.commit(); - - // set image - denoised = render; - - // Create a denoising filter - oidn::FilterRef filter = device.newFilter("RT"); // ray tracing filter - filter.setImage("color", (void*)render.pixels.data(), oidn::Format::Float3, - render.width, render.height, 0, sizeof(vec4f), - sizeof(vec4f) * render.width); - filter.setImage("albedo", (void*)albedo.pixels.data(), oidn::Format::Float3, - albedo.width, albedo.height, 0, sizeof(vec4f), - sizeof(vec4f) * albedo.width); - filter.setImage("normal", (void*)normal.pixels.data(), oidn::Format::Float3, - normal.width, normal.height, 0, sizeof(vec4f), - sizeof(vec4f) * normal.width); - filter.setImage("output", denoised.pixels.data(), oidn::Format::Float3, - denoised.width, denoised.height, 0, sizeof(vec4f), - sizeof(vec4f) * denoised.width); - filter.set("inputScale", 1.0f); // set scale as fixed - filter.set("hdr", true); // image is HDR - filter.commit(); - - // Filter the image - filter.execute(); -#else - denoised = render; -#endif -} - -void denoise_image(vector& denoised, int width, int height, - const vector& render, const vector& albedo, - const vector& normal) { - check_image(denoised, width, height); - check_image(render, width, height); - check_image(albedo, width, height); - check_image(normal, width, height); +void denoise_image(image_t& denoised, const image_t& render, + const image_t& albedo, const image_t& normal) { + check_image(denoised, render.size()); + check_image(albedo, render.size()); + check_image(normal, render.size()); #if YOCTO_DENOISE // Create an Intel Open Image Denoise device oidn::DeviceRef device = oidn::newDevice(); @@ -1852,14 +1854,18 @@ void denoise_image(vector& denoised, int width, int height, // Create a denoising filter oidn::FilterRef filter = device.newFilter("RT"); // ray tracing filter - filter.setImage("color", (void*)render.data(), oidn::Format::Float3, width, - height, 0, sizeof(vec4f), sizeof(vec4f) * width); - filter.setImage("albedo", (void*)albedo.data(), oidn::Format::Float3, width, - height, 0, sizeof(vec3f), sizeof(vec3f) * width); - filter.setImage("normal", (void*)normal.data(), oidn::Format::Float3, width, - height, 0, sizeof(vec3f), sizeof(vec3f) * width); - filter.setImage("output", denoised.data(), oidn::Format::Float3, width, - height, 0, sizeof(vec4f), sizeof(vec4f) * width); + filter.setImage("color", (void*)render.data(), oidn::Format::Float3, + render.size().x, render.size().y, 0, sizeof(vec4f), + sizeof(vec4f) * render.size().x); + filter.setImage("albedo", (void*)albedo.data(), oidn::Format::Float3, + albedo.size().x, albedo.size().y, 0, sizeof(vec3f), + sizeof(vec4f) * albedo.size().x); + filter.setImage("normal", (void*)normal.data(), oidn::Format::Float3, + normal.size().x, normal.size().y, 0, sizeof(vec3f), + sizeof(vec4f) * normal.size().x); + filter.setImage("output", denoised.data(), oidn::Format::Float3, + denoised.size().x, denoised.size().y, 0, sizeof(vec4f), + sizeof(vec4f) * denoised.size().x); filter.set("inputScale", 1.0f); // set scale as fixed filter.set("hdr", true); // image is HDR filter.commit(); diff --git a/libs/yocto/yocto_trace.h b/libs/yocto/yocto_trace.h index db972fa35..74c06135a 100644 --- a/libs/yocto/yocto_trace.h +++ b/libs/yocto/yocto_trace.h @@ -69,15 +69,15 @@ namespace yocto { // Type of tracing algorithm enum struct trace_sampler_type { - path, // path tracing - pathdirect, // path tracing with direct - pathmis, // path tracing with mis - pathtest, // path tracing test - naive, // naive path tracing - eyelight, // eyelight rendering - diagram, // diagram rendering - furnace, // furnace test - falsecolor, // false color rendering + path, // path tracing + pathdirect, // path tracing with direct + pathmis, // path tracing with mis + pathtest, // path tracing test + lightsampling, // path tracing with light sampling + naive, // naive path tracing + eyelight, // eyelight rendering + furnace, // furnace test + falsecolor, // false color rendering }; // Type of false color visualization enum struct trace_falsecolor_type { @@ -99,7 +99,7 @@ struct trace_params { trace_falsecolor_type falsecolor = trace_falsecolor_type::color; int samples = 512; int bounces = 8; - float clamp = 10; + float clamp = 100; bool nocaustics = false; bool envhidden = false; bool tentfilter = false; @@ -113,7 +113,19 @@ struct trace_params { }; // Progressively computes an image. -image_data trace_image(const scene_data& scene, const trace_params& params); +image_t trace_image(const scene_data& scene, const trace_params& params); + +// Convenience helper +image_t trace_image(const scene_data& scene, + trace_sampler_type type = trace_sampler_type::path, int resolution = 1440, + int samples = 256, int bounces = 8); + +// Forward compatibility +inline image_t pathtrace_image(const scene_data& scene, + trace_sampler_type type = trace_sampler_type::path, int resolution = 1440, + int samples = 256, int bounces = 8) { + return trace_image(scene, type, resolution, samples, bounces); +} } // namespace yocto @@ -145,15 +157,15 @@ bool is_sampler_lit(const trace_params& params); // Trace state struct trace_state { - int width = 0; - int height = 0; - int samples = 0; - vector image = {}; - vector albedo = {}; - vector normal = {}; - vector hits = {}; - vector rngs = {}; - vector denoised = {}; + image_t render = {}; + image_t albedo = {}; + image_t normal = {}; + image_t hits = {}; + image_t rngs = {}; + image_t denoised = {}; + int samples = 0; + + vec2i size() const { return render.size(); } }; // Initialize state. @@ -176,27 +188,27 @@ void trace_sample(trace_state& state, const scene_data& scene, const trace_params& params); // Get resulting render, denoised if requested -image_data get_image(const trace_state& state); -void get_image(image_data& image, const trace_state& state); +image_t get_image(const trace_state& state); +void get_image(image_t& image, const trace_state& state); // Get internal images from state -image_data get_rendered_image(const trace_state& state); -void get_rendered_image(image_data& image, const trace_state& state); -image_data get_denoised_image(const trace_state& state); -void get_denoised_image(image_data& image, const trace_state& state); -image_data get_albedo_image(const trace_state& state); -void get_albedo_image(image_data& image, const trace_state& state); -image_data get_normal_image(const trace_state& state); -void get_normal_image(image_data& image, const trace_state& state); +image_t get_rendered_image(const trace_state& state); +void get_rendered_image(image_t& image, const trace_state& state); +image_t get_denoised_image(const trace_state& state); +void get_denoised_image(image_t& image, const trace_state& state); +image_t get_albedo_image(const trace_state& state); +void get_albedo_image(image_t& image, const trace_state& state); +image_t get_normal_image(const trace_state& state); +void get_normal_image(image_t& image, const trace_state& state); // Denoise image -image_data denoise_image(const image_data& render, const image_data& albedo, - const image_data& normal); -void denoise_image(image_data& image, const image_data& render, - const image_data& albedo, const image_data& normal); -void denoise_image(vector& denoised, int width, int height, - const vector& render, const vector& albedo, - const vector& normal); +image_t denoise_image(const image_t& render, + const image_t& albedo, const image_t& normal); +void denoise_image(image_t& image, const image_t& render, + const image_t& albedo, const image_t& normal); +void denoise_image(vector& denoised, int width, int height, + const vector& render, const vector& albedo, + const vector& normal); // Async implementation struct trace_context { @@ -220,7 +232,7 @@ void trace_cancel(trace_context& context); void trace_done(trace_context& context); // Async preview -void trace_preview(color_image& image, trace_context& context, +void trace_preview(image_t& image, trace_context& context, trace_state& state, const scene_data& scene, const trace_bvh& bvh, const trace_lights& lights, const trace_params& params); @@ -233,7 +245,8 @@ namespace yocto { // trace sampler names inline const auto trace_sampler_names = vector{"path", "pathdirect", - "pathmis", "pathtest", "naive", "eyelight", "furnace", "falsecolor"}; + "pathmis", "pathtest", "lightsampling", "naive", "eyelight", "furnace", + "falsecolor"}; // false color names inline const auto trace_falsecolor_names = vector{"position", "normal", @@ -247,9 +260,9 @@ inline const auto trace_sampler_labels = {trace_sampler_type::pathdirect, "pathdirect"}, {trace_sampler_type::pathmis, "pathmis"}, {trace_sampler_type::pathtest, "pathtest"}, + {trace_sampler_type::lightsampling, "lightsampling"}, {trace_sampler_type::naive, "naive"}, {trace_sampler_type::eyelight, "eyelight"}, - {trace_sampler_type::diagram, "diagram"}, {trace_sampler_type::furnace, "furnace"}, {trace_sampler_type::falsecolor, "falsecolor"}}; @@ -277,63 +290,4 @@ inline const auto trace_falsecolor_labels = } // namespace yocto -// ----------------------------------------------------------------------------- -// BACKWARD COMPATIBILITY -// ----------------------------------------------------------------------------- -namespace yocto { - -// Initialize state. -[[deprecated]] inline trace_state make_state( - const scene_data& scene, const trace_params& params) { - return make_trace_state(scene, params); -} - -// Initialize lights. -[[deprecated]] inline trace_lights make_lights( - const scene_data& scene, const trace_params& params) { - return make_trace_lights(scene, params); -} - -// Build the bvh acceleration structure. -[[deprecated]] inline trace_bvh make_bvh( - const scene_data& scene, const trace_params& params) { - return make_trace_bvh(scene, params); -} - -// Get resulting render -[[deprecated]] inline image_data get_render(const trace_state& state) { - return get_rendered_image(state); -} -[[deprecated]] inline void get_render( - image_data& image, const trace_state& state) { - return get_rendered_image(image, state); -} - -// Get denoised result -[[deprecated]] inline image_data get_denoised(const trace_state& state) { - return get_denoised_image(state); -} -[[deprecated]] inline void get_denoised( - image_data& image, const trace_state& state) { - return get_denoised_image(image, state); -} - -// Get denoising buffers -[[deprecated]] inline image_data get_albedo(const trace_state& state) { - return get_albedo_image(state); -} -[[deprecated]] inline void get_albedo( - image_data& image, const trace_state& state) { - return get_albedo_image(image, state); -} -[[deprecated]] inline image_data get_normal(const trace_state& state) { - return get_normal_image(state); -} -[[deprecated]] inline void get_normal( - image_data& image, const trace_state& state) { - return get_normal_image(image, state); -} - -} // namespace yocto - #endif