├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── LICENSE.txt ├── README.md ├── docs ├── basics.jpg ├── constraint_type.jpg ├── constraint_type_2.jpg ├── header.gif ├── intermediate_steps.jpg ├── manifold_exploration.jpg ├── newton_parameters.jpg ├── raytracing.jpg ├── sample_paths.jpg ├── sample_paths_offset.jpg ├── sms_vs_mnee.jpg ├── specular_manifold_sampling.jpg ├── tangent_frame.jpg └── tangent_step.jpg ├── ext └── tinyformat │ ├── README.rst │ └── tinyformat.h ├── python ├── bezier_shapes.py ├── draw.py ├── knob.py ├── misc.py ├── mode.py ├── modes │ ├── manifold_exploration.py │ ├── raytracing.py │ └── specular_manifold_sampling.py ├── path.py ├── scene.py ├── scenes.py └── viewer.py └── src ├── bbox.h ├── ddistr.h ├── global.h ├── interaction.cpp ├── interaction.h ├── main.cpp ├── python ├── interaction.cpp ├── python.cpp ├── python.h ├── ray.cpp ├── scene.cpp └── shape.cpp ├── ray.h ├── scene.cpp ├── scene.h ├── shape.cpp ├── shape.h └── shapes ├── bezier_curve.h ├── circle.h ├── concave_segment.h ├── convex_segment.h └── linear_segment.h /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | .DS_Store 3 | .ipynb_checkpoints/ 4 | TODO.md 5 | *.ipynb 6 | *__pycache__* 7 | *.svg 8 | /header-video -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "ext/enoki"] 2 | path = ext/enoki 3 | url = https://github.com/mitsuba-renderer/enoki.git 4 | [submodule "ext/--recursive"] 5 | path = ext/--recursive 6 | url = https://github.com/mitsuba-renderer/nanogui.git 7 | [submodule "ext/nanogui"] 8 | path = ext/nanogui 9 | url = https://github.com/mitsuba-renderer/nanogui.git 10 | [submodule "ext/pybind11"] 11 | path = ext/pybind11 12 | url = https://github.com/pybind/pybind11.git 13 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 2.8.3) 2 | project(manifold-visualizer) 3 | 4 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 5 | message(STATUS "Setting build type to 'Release' as none was specified.") 6 | set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose the type of build." FORCE) 7 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo") 8 | endif() 9 | string(TOUPPER "${CMAKE_BUILD_TYPE}" U_CMAKE_BUILD_TYPE) 10 | 11 | if(NOT IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/ext/enoki") 12 | message(FATAL_ERROR "Dependency repositories (enoki, pybind11, etc.) are missing! " 13 | "You probably did not clone the project with --recursive. It is possible to recover by calling \"git submodule update --init --recursive\"") 14 | endif() 15 | 16 | # Enable folders for projects in Visual Studio 17 | if (CMAKE_GENERATOR MATCHES "Visual Studio") 18 | set_property(GLOBAL PROPERTY USE_FOLDERS ON) 19 | endif() 20 | 21 | # Sanitize build environment for static build with C++11 22 | if (MSVC) 23 | add_definitions (/D "_CRT_SECURE_NO_WARNINGS") 24 | 25 | # Parallel build on MSVC (all targets) 26 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP") 27 | endif() 28 | 29 | # Enable C++17 mode on GCC / Clang 30 | if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") 31 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17") 32 | endif() 33 | 34 | include(CheckCXXCompilerFlag) 35 | include(CheckCXXSourceRuns) 36 | 37 | macro(CHECK_CXX_COMPILER_AND_LINKER_FLAGS _RESULT _CXX_FLAGS _LINKER_FLAGS) 38 | set(CMAKE_REQUIRED_FLAGS ${_CXX_FLAGS}) 39 | set(CMAKE_REQUIRED_LIBRARIES ${_LINKER_FLAGS}) 40 | set(CMAKE_REQUIRED_QUIET TRUE) 41 | check_cxx_source_runs("#include \nint main(int argc, char **argv) { std::cout << \"test\"; return 0; }" ${_RESULT}) 42 | set(CMAKE_REQUIRED_FLAGS "") 43 | set(CMAKE_REQUIRED_LIBRARIES "") 44 | endmacro() 45 | 46 | # Prefer libc++ in conjunction with Clang 47 | if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") 48 | if (CMAKE_CXX_FLAGS MATCHES "-stdlib=libc\\+\\+") 49 | message(STATUS "Using libc++.") 50 | else() 51 | CHECK_CXX_COMPILER_AND_LINKER_FLAGS(HAS_LIBCPP "-stdlib=libc++" "-stdlib=libc++") 52 | if (HAS_LIBCPP) 53 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++") 54 | set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -stdlib=libc++") 55 | set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -stdlib=libc++") 56 | message(STATUS "Using libc++.") 57 | else() 58 | message(STATUS "NOT using libc++.") 59 | endif() 60 | endif() 61 | endif() 62 | 63 | # Add enoki 64 | add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/ext/enoki) 65 | enoki_set_compile_flags() 66 | enoki_set_native_flags() 67 | 68 | # Add nanogui 69 | set(NANOGUI_BUILD_EXAMPLES ON CACHE BOOL " " FORCE) 70 | set(NANOGUI_BUILD_PYTHON ON CACHE BOOL " " FORCE) 71 | set(NANOGUI_INSTALL OFF CACHE BOOL " " FORCE) 72 | set(NANOGUI_PYBIND11_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ext/pybind11") 73 | add_subdirectory(ext/nanogui) 74 | set_property(TARGET glfw glfw_objects nanogui PROPERTY FOLDER "dependencies") 75 | 76 | # Compiler warnings 77 | if(MSVC) 78 | if(CMAKE_CXX_FLAGS MATCHES "/W[0-4]") 79 | string(REGEX REPLACE "/W[0-4]" "/W4" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") 80 | else() 81 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4") 82 | endif() 83 | elseif (CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID MATCHES "GNU") 84 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wno-unused-parameter") 85 | if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") 86 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-gnu-anonymous-struct -Wno-c99-extensions -Wno-nested-anon-types") 87 | endif() 88 | endif() 89 | 90 | include_directories( 91 | ${CMAKE_CURRENT_SOURCE_DIR}/ext/enoki/include 92 | ${CMAKE_CURRENT_SOURCE_DIR}/ext/tinyformat 93 | ${CMAKE_CURRENT_SOURCE_DIR}/ext/nanogui/ext/nanovg/src 94 | ${CMAKE_CURRENT_SOURCE_DIR}/src 95 | ) 96 | 97 | # add_executable(manifold-visualizer 98 | # ${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp 99 | # ) 100 | 101 | # Python bindings 102 | set(PYBIND11_CPP_STANDARD "-std=c++17") 103 | add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/ext/pybind11) 104 | pybind11_add_module(manifolds 105 | ${CMAKE_CURRENT_SOURCE_DIR}/src/python/python.cpp 106 | ${CMAKE_CURRENT_SOURCE_DIR}/src/python/ray.cpp 107 | ${CMAKE_CURRENT_SOURCE_DIR}/src/python/interaction.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/interaction.cpp 108 | ${CMAKE_CURRENT_SOURCE_DIR}/src/python/shape.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/shape.cpp 109 | ${CMAKE_CURRENT_SOURCE_DIR}/src/python/scene.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/scene.cpp 110 | ) 111 | target_link_libraries(manifolds PRIVATE nanogui ${NANOGUI_EXTRA_LIBS}) -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Tizian Zeltner 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 2D Specular Manifold Visualizer 2 | 3 | ![Header](https://github.com/tizian/manifold-visualizer/raw/master/docs/header.gif) 4 | 5 | ## Introduction 6 | 7 | This is a helper tool written while working on the 2020 SIGGRAPH paper ["Specular Manifold Sampling for Rendering High-Frequency Caustics and Glints"](http://rgl.epfl.ch/publications/Zeltner2020Specular) by [Tizian Zeltner](https://tizianzeltner.com), [Iliyan Georgiev](https://www.iliyan.com), and [Wenzel Jakob](http://rgl.epfl.ch/people/wjakob). Its main use is visualize some of the ideas behind the *specular manifold constraint framework* in a simplified 2D setting, e.g. to prototype new ideas or to generate animations. 8 | 9 | It also includes visualizations for previous work 10 | * ["Manifold Exploration"](http://rgl.epfl.ch/publications/Jakob2012Manifold), Jakob and Marschner 2012 11 | * ["Manifold Next Event Estimation"](https://jo.dreggn.org/home/2015_mnee.pdf), Hanika et al. 2015 12 | 13 | The implementation is written in a combination of C++ (for ray tracing against primitives such as Bezier curves), and Python (for the main algorithms and the GUI). 14 | 15 | ## Usage 16 | 17 | ### Basics 18 | 19 | basics 20 | 21 | * The first two parts of the GUI allow you to choose between different test scenes (left), and the current *mode* (right). 22 | * There is also a button to reload the current scene (keyboard shortcut: R). 23 | * The selected scene can be moved with Shift+Left mouse click+drag and zoomed in/out with Scroll wheel 24 | * Please have a look at `python/scenes.py` to define more scenes. 25 | * You can also switch between the available three modes using the number keys 1, 2, and 3. 26 | 27 | ### Raytracing 28 | 29 | raytracing 30 | 31 | In the *Raytracing* mode, there is one movable point for the beginning of the light path. Its position can be moved along the surface with Left mouse click+drag and the ray direction can be rotated with Alt+Left mouse click+drag. 32 | 33 | **Options:** 34 | 35 | - Toggle normals and tangents for ray intersections on specular surfaces. 36 | 37 | 38 | 39 | ### Manifold exploration 40 | 41 | manifold_exploration 42 | 43 | In the *Manifold exploration* mode, there is a movable point that corresponds to the end of the light path. Moving it (Left mouse click+drag) will attempt to perform a *manifold walk* that moves the complete light path while **a)** keeping the start point fixed, and **b)** keeping all specular contraints along the path fulfilled. 44 | Switch to the *Raytracing* mode in order to move the start of the path instead. 45 | 46 | **Options**: 47 | 48 | - Toggle normals and tangents for ray intersections on specular surfaces. 49 | 50 | 51 | - Switch between the original specular constraints (left) based on projected half-vectors (Jakob and Marschner 2012) or the new constraints (right) based on angle differences (Zeltner et al. 2020). 52 | 53 | 54 | - Instead of doing full *manifold walks*, just move the specular vertices up to a first-order approximation (left toggle) and visualize the updated light path after projecting it back onto the specular manifold (right toggle). This is useful to visualize the first iteration of the underlying Newton solver. 55 | 56 | 57 | - Set the max. number of iterations (N) and the threshold value (eps) to determine the success of the underlying Newton solver. 58 | 59 | 60 | ### Specular manifold sampling (SMS) and manifold next event estimation (MNEE) 61 | 62 | specular_manifold_sampling 63 | 64 | In the *Specular manifold sampling* mode, there are three movable (Left mouse click+drag) points: the two endpoints of the path, and a position on a specular surface. The algorithm will try to refine the *initial path* derived from their configuration into a valid light path using a Newton solver. 65 | 66 | **Options**: 67 | 68 | - Toggle between *Manifold next event estimation* (left toggle) or *Specular manifold sampling* (right toggle). 69 | - Set the number of specular bounces (N) that are required to sample a full path. This will usually be set appropriately based on the selected scene. 70 | 71 | 72 | - Switch between the original specular constraints (left) based on projected half-vectors (Jakob and Marschner 2012) or the new constraints (right) based on angle differences (Zeltner et al. 2020). 73 | - Toggle if the constraints should be visualized ("show"), and if the alternative set of constraints should be shown for the case of angle differences. 74 | 75 | 76 | - Set the max. number of iterations (N) and the threshold value (eps) to determine the success of the underlying Newton solver. 77 | 78 | 79 | - Toggle if the paths at intermediate steps of the Newton solver should be shown. They will use a color scheme from start/red -> end/green. 80 | - Set a scale factor for all Newton solver steps. 81 | 82 | 83 | - "Go" will sample a set of "N" random points on the specular surface that will be converted to initial paths that are given to the Newton solver. 84 | - "Show seeds" will toggle whether the initial paths should also be shown alongside the converged paths. 85 | 86 | 87 | - "Go" will sample a set of "N" *offset microfacet normals* with a given variance (value in parentheses) for the current path that will then be used as the surface normals for the specular constraints. (See *offset manifold walks* in Jakob and Marschner 2012 and *two-stage sampling* in Zeltner et al. 2020. 88 | 89 | 90 | 91 | ## Building 92 | 93 | Clone the repository *recursively* with all dependencies and use CMake to generate project files for your favourite IDE or build system. Unix example using make: 94 | 95 | ``` 96 | git clone https://github.com/tizian/manifold-visualizer --recursive 97 | cd manifold-visualizer 98 | mkdir build 99 | cd build 100 | cmake .. 101 | make 102 | ``` 103 | 104 | This will compile the C++ components of this project into a Python libray also build the [nanogui](https://github.com/mitsuba-renderer/nanogui) dependency at the same time. 105 | 106 | ## Running 107 | 108 | Simply run the Python file in `/python/viewer.py`, but make sure both the `manifolds` and `nanogui` Python libraries are somewhere on the `PYTHONPATH`, e.g.: 109 | 110 | ``` 111 | export PYTHONPATH=/build:/build/ext/nanogui:$PYTHONPATH 112 | python /python/viewer.py 113 | ``` 114 | 115 | ## Third party code 116 | 117 | This project depends on the following libraries: 118 | 119 | * [enoki](https://github.com/mitsuba-renderer/enoki) 120 | * [nanogui](https://github.com/mitsuba-renderer/nanogui) 121 | * [pybind11](https://github.com/pybind/pybind11) 122 | * [tinyformat](https://github.com/c42f/tinyformat) 123 | -------------------------------------------------------------------------------- /docs/basics.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizian/manifold-visualizer/9e2f4e8f225882ced2cfc106beeb77f5daf4d2fd/docs/basics.jpg -------------------------------------------------------------------------------- /docs/constraint_type.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizian/manifold-visualizer/9e2f4e8f225882ced2cfc106beeb77f5daf4d2fd/docs/constraint_type.jpg -------------------------------------------------------------------------------- /docs/constraint_type_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizian/manifold-visualizer/9e2f4e8f225882ced2cfc106beeb77f5daf4d2fd/docs/constraint_type_2.jpg -------------------------------------------------------------------------------- /docs/header.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizian/manifold-visualizer/9e2f4e8f225882ced2cfc106beeb77f5daf4d2fd/docs/header.gif -------------------------------------------------------------------------------- /docs/intermediate_steps.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizian/manifold-visualizer/9e2f4e8f225882ced2cfc106beeb77f5daf4d2fd/docs/intermediate_steps.jpg -------------------------------------------------------------------------------- /docs/manifold_exploration.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizian/manifold-visualizer/9e2f4e8f225882ced2cfc106beeb77f5daf4d2fd/docs/manifold_exploration.jpg -------------------------------------------------------------------------------- /docs/newton_parameters.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizian/manifold-visualizer/9e2f4e8f225882ced2cfc106beeb77f5daf4d2fd/docs/newton_parameters.jpg -------------------------------------------------------------------------------- /docs/raytracing.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizian/manifold-visualizer/9e2f4e8f225882ced2cfc106beeb77f5daf4d2fd/docs/raytracing.jpg -------------------------------------------------------------------------------- /docs/sample_paths.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizian/manifold-visualizer/9e2f4e8f225882ced2cfc106beeb77f5daf4d2fd/docs/sample_paths.jpg -------------------------------------------------------------------------------- /docs/sample_paths_offset.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizian/manifold-visualizer/9e2f4e8f225882ced2cfc106beeb77f5daf4d2fd/docs/sample_paths_offset.jpg -------------------------------------------------------------------------------- /docs/sms_vs_mnee.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizian/manifold-visualizer/9e2f4e8f225882ced2cfc106beeb77f5daf4d2fd/docs/sms_vs_mnee.jpg -------------------------------------------------------------------------------- /docs/specular_manifold_sampling.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizian/manifold-visualizer/9e2f4e8f225882ced2cfc106beeb77f5daf4d2fd/docs/specular_manifold_sampling.jpg -------------------------------------------------------------------------------- /docs/tangent_frame.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizian/manifold-visualizer/9e2f4e8f225882ced2cfc106beeb77f5daf4d2fd/docs/tangent_frame.jpg -------------------------------------------------------------------------------- /docs/tangent_step.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizian/manifold-visualizer/9e2f4e8f225882ced2cfc106beeb77f5daf4d2fd/docs/tangent_step.jpg -------------------------------------------------------------------------------- /ext/tinyformat/README.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | tinyformat.h 3 | ============ 4 | ---------------------------------------- 5 | A minimal type safe printf() replacement 6 | ---------------------------------------- 7 | 8 | **tinyformat.h** is a type safe printf replacement library in a single C++ 9 | header file. If you've ever wanted ``printf("%s", s)`` to just work regardless 10 | of the type of ``s``, tinyformat might be for you. Design goals include: 11 | 12 | * Type safety and extensibility for user defined types. 13 | * C99 ``printf()`` compatibility, to the extent possible using ``std::ostream`` 14 | * Simplicity and minimalism. A single header file to include and distribute 15 | with your projects. 16 | * Augment rather than replace the standard stream formatting mechanism 17 | * C++98 support, with optional C++11 niceties 18 | 19 | 20 | Example usage 21 | ------------- 22 | 23 | To print a date to ``std::cout``:: 24 | 25 | std::string weekday = "Wednesday"; 26 | const char* month = "July"; 27 | size_t day = 27; 28 | long hour = 14; 29 | int min = 44; 30 | 31 | tfm::printf("%s, %s %d, %.2d:%.2d\n", weekday, month, day, hour, min); 32 | 33 | The strange types here emphasize the type safety of the interface, for example 34 | it is possible to print a ``std::string`` using the ``"%s"`` conversion, and a 35 | ``size_t`` using the ``"%d"`` conversion. A similar result could be achieved 36 | using either of the ``tfm::format()`` functions. One prints on a user provided 37 | stream:: 38 | 39 | tfm::format(std::cerr, "%s, %s %d, %.2d:%.2d\n", 40 | weekday, month, day, hour, min); 41 | 42 | The other returns a ``std::string``:: 43 | 44 | std::string date = tfm::format("%s, %s %d, %.2d:%.2d\n", 45 | weekday, month, day, hour, min); 46 | std::cout << date; 47 | 48 | 49 | It is safe to use tinyformat inside a template function. For any type which 50 | has the usual stream insertion ``operator<<`` defined, the following will work 51 | as desired:: 52 | 53 | template 54 | void myPrint(const T& value) 55 | { 56 | tfm::printf("My value is '%s'\n", value); 57 | } 58 | 59 | (The above is a compile error for types ``T`` without a stream insertion 60 | operator.) 61 | 62 | 63 | Function reference 64 | ------------------ 65 | 66 | All user facing functions are defined in the namespace ``tinyformat``. A 67 | namespace alias ``tfm`` is provided to encourage brevity, but can easily be 68 | disabled if desired. 69 | 70 | Three main interface functions are available: an iostreams-based ``format()``, 71 | a string-based ``format()`` and a ``printf()`` replacement. These functions 72 | can be thought of as C++ replacements for C's ``fprintf()``, ``sprintf()`` and 73 | ``printf()`` functions respectively. All the interface functions can take an 74 | unlimited number of input arguments if compiled with C++11 variadic templates 75 | support. In C++98 mode, the number of arguments must be limited to some fixed 76 | upper bound which is currently 16 as of version 1.3 [#]_. 77 | 78 | The ``format()`` function which takes a stream as the first argument is the 79 | main part of the tinyformat interface. ``stream`` is the output stream, 80 | ``formatString`` is a format string in C99 ``printf()`` format, and the values 81 | to be formatted have arbitrary types:: 82 | 83 | template 84 | void format(std::ostream& stream, const char* formatString, 85 | const Args&... args); 86 | 87 | The second version of ``format()`` is a convenience function which returns a 88 | ``std::string`` rather than printing onto a stream. This function simply 89 | calls the main version of ``format()`` using a ``std::ostringstream``, and 90 | returns the resulting string:: 91 | 92 | template 93 | std::string format(const char* formatString, const Args&... args); 94 | 95 | Finally, ``printf()`` and ``printfln()`` are convenience functions which call 96 | ``format()`` with ``std::cout`` as the first argument; both have the same 97 | signature:: 98 | 99 | template 100 | void printf(const char* formatString, const Args&... args); 101 | 102 | ``printfln()`` is the same as ``printf()`` but appends an additional newline 103 | for convenience - a concession to the author's tendency to forget the newline 104 | when using the library for simple logging. 105 | 106 | .. [#] Generating the code to support more arguments is quite easy using the 107 | in-source code generator based on the excellent code generation script 108 | ``cog.py`` (http://nedbatchelder.com/code/cog): Set the ``maxParams`` 109 | parameter in the code generator and rerun cog using 110 | ``cog.py -r tinyformat.h``. Alternatively, it should be quite easy to simply 111 | add extra versions of the associated macros by hand. 112 | 113 | Format strings and type safety 114 | ------------------------------ 115 | 116 | Tinyformat parses C99 format strings to guide the formatting process --- please 117 | refer to any standard C99 printf documentation for format string syntax. In 118 | contrast to printf, tinyformat does not use the format string to decide on 119 | the type to be formatted so this does not compromise the type safety: *you may 120 | use any format specifier with any C++ type*. The author suggests standardising 121 | on the ``%s`` conversion unless formatting numeric types. 122 | 123 | Let's look at what happens when you execute the function call:: 124 | 125 | tfm::format(outStream, "%+6.4f", yourType); 126 | 127 | First, the library parses the format string, and uses it to modify the state of 128 | ``outStream``: 129 | 130 | 1. The ``outStream`` formatting flags are cleared and the width, precision and 131 | fill reset to the default. 132 | 2. The flag ``'+'`` means to prefix positive numbers with a ``'+'``; tinyformat 133 | executes ``outStream.setf(std::ios::showpos)`` 134 | 3. The number 6 gives the field width; execute ``outStream.width(6)``. 135 | 4. The number 4 gives the precision; execute ``outStream.precision(4)``. 136 | 5. The conversion specification character ``'f'`` means that floats should be 137 | formatted with a fixed number of digits; this corresponds to executing 138 | ``outStream.setf(std::ios::fixed, std::ios::floatfield);`` 139 | 140 | After all these steps, tinyformat executes:: 141 | 142 | outStream << yourType; 143 | 144 | and finally restores the stream flags, precision and fill. 145 | 146 | What happens if ``yourType`` isn't actually a floating point type? In this 147 | case the flags set above are probably irrelevant and will be ignored by the 148 | underlying ``std::ostream`` implementation. The field width of six may cause 149 | some padding in the output of ``yourType``, but that's about it. 150 | 151 | 152 | Special cases for "%p", "%c" and "%s" 153 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 154 | 155 | Tinyformat normally uses ``operator<<`` to convert types to strings. However, 156 | the "%p" and "%c" conversions require special rules for robustness. Consider:: 157 | 158 | uint8_t* pixels = get_pixels(/* ... */); 159 | tfm::printf("%p", pixels); 160 | 161 | Clearly the intention here is to print a representation of the *pointer* to 162 | ``pixels``, but since ``uint8_t`` is a character type the compiler would 163 | attempt to print it as a C string if we blindly fed it into ``operator<<``. To 164 | counter this kind of madness, tinyformat tries to static_cast any type fed to 165 | the "%p" conversion into a ``const void*`` before printing. If this can't be 166 | done at compile time the library falls back to using ``operator<<`` as usual. 167 | 168 | The "%c" conversion has a similar problem: it signifies that the given integral 169 | type should be converted into a ``char`` before printing. The solution is 170 | identical: attempt to convert the provided type into a char using 171 | ``static_cast`` if possible, and if not fall back to using ``operator<<``. 172 | 173 | The "%s" conversion sets the boolalpha flag on the formatting stream. This 174 | means that a ``bool`` variable printed with "%s" will come out as ``true`` or 175 | ``false`` rather than the ``1`` or ``0`` that you would otherwise get. 176 | 177 | 178 | Incompatibilities with C99 printf 179 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 180 | 181 | Not all features of printf can be simulated simply using standard iostreams. 182 | Here's a list of known incompatibilities: 183 | 184 | * The C99 ``"%a"`` and ``"%A"`` hexadecimal floating point conversions are not 185 | supported since the iostreams don't have the necessary flags. Using either 186 | of these flags will result in a call to ``TINYFORMAT_ERROR``. 187 | * The precision for integer conversions cannot be supported by the iostreams 188 | state independently of the field width. (Note: **this is only a 189 | problem for certain obscure integer conversions**; float conversions like 190 | ``%6.4f`` work correctly.) In tinyformat the field width takes precedence, 191 | so the 4 in ``%6.4d`` will be ignored. However, if the field width is not 192 | specified, the width used internally is set equal to the precision and padded 193 | with zeros on the left. That is, a conversion like ``%.4d`` effectively 194 | becomes ``%04d`` internally. This isn't correct for every case (eg, negative 195 | numbers end up with one less digit than desired) but it's about the closest 196 | simple solution within the iostream model. 197 | * The ``"%n"`` query specifier isn't supported to keep things simple and will 198 | result in a call to ``TINYFORMAT_ERROR``. 199 | * The ``"%ls"`` conversion is not supported, and attempting to format a 200 | ``wchar_t`` array will cause a compile time error to minimise unexpected 201 | surprises. If you know the encoding of your wchar_t strings, you could write 202 | your own ``std::ostream`` insertion operator for them, and disable the 203 | compile time check by defining the macro ``TINYFORMAT_ALLOW_WCHAR_STRINGS``. 204 | If you want to print the *address* of a wide character with the ``"%p"`` 205 | conversion, you should cast it to a ``void*`` before passing it to one of the 206 | formatting functions. 207 | 208 | 209 | Error handling 210 | -------------- 211 | 212 | By default, tinyformat calls ``assert()`` if it encounters an error in the 213 | format string or number of arguments. This behaviour can be changed (for 214 | example, to throw an exception) by defining the ``TINYFORMAT_ERROR`` macro 215 | before including tinyformat.h, or editing the config section of the header. 216 | 217 | 218 | Formatting user defined types 219 | ----------------------------- 220 | 221 | User defined types with a stream insertion operator will be formatted using 222 | ``operator<<(std::ostream&, T)`` by default. The ``"%s"`` format specifier is 223 | suggested for user defined types, unless the type is inherently numeric. 224 | 225 | For further customization, the user can override the ``formatValue()`` 226 | function to specify formatting independently of the stream insertion operator. 227 | If you override this function, the library will have already parsed the format 228 | specification and set the stream flags accordingly - see the source for details. 229 | 230 | 231 | Wrapping tfm::format() inside a user defined format function 232 | ------------------------------------------------------------ 233 | 234 | Suppose you wanted to define your own function which wraps ``tfm::format``. 235 | For example, consider an error function taking an error code, which in C++11 236 | might be written simply as:: 237 | 238 | template 239 | void error(int code, const char* fmt, const Args&... args) 240 | { 241 | std::cerr << "error (code " << code << ")"; 242 | tfm::format(std::cerr, fmt, args...); 243 | } 244 | 245 | Simulating this functionality in C++98 is pretty painful since it requires 246 | writing out a version of ``error()`` for each desired number of arguments. To 247 | make this bearable tinyformat comes with a set of macros which are used 248 | internally to generate the API, but which may also be used in user code. 249 | 250 | The three macros ``TINYFORMAT_ARGTYPES(n)``, ``TINYFORMAT_VARARGS(n)`` and 251 | ``TINYFORMAT_PASSARGS(n)`` will generate a list of ``n`` argument types, 252 | type/name pairs and argument names respectively when called with an integer 253 | ``n`` between 1 and 16. We can use these to define a macro which generates the 254 | desired user defined function with ``n`` arguments. This should be followed by 255 | a call to ``TINYFORMAT_FOREACH_ARGNUM`` to generate the set of functions for 256 | all supported ``n``:: 257 | 258 | #define MAKE_ERROR_FUNC(n) \ 259 | template \ 260 | void error(int code, const char* fmt, TINYFORMAT_VARARGS(n)) \ 261 | { \ 262 | std::cerr << "error (code " << code << ")"; \ 263 | tfm::format(std::cerr, fmt, TINYFORMAT_PASSARGS(n)); \ 264 | } 265 | TINYFORMAT_FOREACH_ARGNUM(MAKE_ERROR_FUNC) 266 | 267 | Sometimes it's useful to be able to pass a list of format arguments through to 268 | a non-template function. The ``FormatList`` class is provided as a way to do 269 | this by storing the argument list in a type-opaque way. For example:: 270 | 271 | template 272 | void error(int code, const char* fmt, const Args&... args) 273 | { 274 | tfm::FormatListRef formatList = tfm::makeFormatList(args...); 275 | errorImpl(code, fmt, formatList); 276 | } 277 | 278 | What's interesting here is that ``errorImpl()`` is a non-template function so 279 | it could be separately compiled if desired. The ``FormatList`` instance can be 280 | used via a call to the ``vformat()`` function (the name chosen for semantic 281 | similarity to ``vprintf()``):: 282 | 283 | void errorImpl(int code, const char* fmt, tfm::FormatListRef formatList) 284 | { 285 | std::cerr << "error (code " << code << ")"; 286 | tfm::vformat(std::cout, fmt, formatList); 287 | } 288 | 289 | The construction of a ``FormatList`` instance is very lightweight - it defers 290 | all formatting and simply stores a couple of function pointers and a value 291 | pointer per argument. Since most of the actual work is done inside 292 | ``vformat()``, any logic which causes an early exit of ``errorImpl()`` - 293 | filtering of verbose log messages based on error code for example - could be a 294 | useful optimization for programs using tinyformat. (A faster option would be 295 | to write any early bailout code inside ``error()``, though this must be done in 296 | the header.) 297 | 298 | 299 | Benchmarks 300 | ---------- 301 | 302 | Compile time and code bloat 303 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 304 | 305 | The script ``bloat_test.sh`` included in the repository tests whether 306 | tinyformat succeeds in avoiding compile time and code bloat for nontrivial 307 | projects. The idea is to include ``tinyformat.h`` into 100 translation units 308 | and use ``printf()`` five times in each to simulate a medium sized project. 309 | The resulting executable size and compile time (g++-4.8.2, linux ubuntu 14.04) 310 | is shown in the following tables, which can be regenerated using ``make 311 | bloat_test``: 312 | 313 | **Non-optimized build** 314 | 315 | ====================== ================== ========================== 316 | test name compiler wall time executable size (stripped) 317 | ====================== ================== ========================== 318 | libc printf 1.8s 48K (36K) 319 | std::ostream 10.7s 96K (76K) 320 | tinyformat, no inlines 18.9s 140K (104K) 321 | tinyformat 21.1s 220K (180K) 322 | tinyformat, c++0x mode 20.7s 220K (176K) 323 | boost::format 70.1s 844K (736K) 324 | ====================== ================== ========================== 325 | 326 | **Optimized build (-O3 -DNDEBUG)** 327 | 328 | ====================== ================== ========================== 329 | test name compiler wall time executable size (stripped) 330 | ====================== ================== ========================== 331 | libc printf 2.3s 40K (28K) 332 | std::ostream 11.8s 104K (80K) 333 | tinyformat, no inlines 23.0s 128K (104K) 334 | tinyformat 32.9s 128K (104K) 335 | tinyformat, c++0x mode 34.0s 128K (104K) 336 | boost::format 147.9s 644K (600K) 337 | ====================== ================== ========================== 338 | 339 | For large projects it's arguably worthwhile to do separate compilation of the 340 | non-templated parts of tinyformat, as shown in the rows labelled *tinyformat, 341 | no inlines*. These were generated by putting the implementation of ``vformat`` 342 | (``detail::formatImpl()`` etc) it into a separate file, tinyformat.cpp. Note 343 | that the results above can vary considerably with different compilers. For 344 | example, the ``-fipa-cp-clone`` optimization pass in g++-4.6 resulted in 345 | excessively large binaries. On the other hand, the g++-4.8 results are quite 346 | similar to using clang++-3.4. 347 | 348 | 349 | Speed tests 350 | ~~~~~~~~~~~ 351 | 352 | The following speed tests results were generated by building 353 | ``tinyformat_speed_test.cpp`` on an Intel core i7-2600K running Linux Ubuntu 354 | 14.04 with g++-4.8.2 using ``-O3 -DNDEBUG``. In the test, the format string 355 | ``"%0.10f:%04d:%+g:%s:%p:%c:%%\n"`` is filled 2000000 times with output sent to 356 | ``/dev/null``; for further details see the source and Makefile. 357 | 358 | ============== ======== 359 | test name run time 360 | ============== ======== 361 | libc printf 1.20s 362 | std::ostream 1.82s 363 | tinyformat 2.08s 364 | boost::format 9.04s 365 | ============== ======== 366 | 367 | It's likely that tinyformat has an advantage over boost.format because it tries 368 | reasonably hard to avoid formatting into temporary strings, preferring instead 369 | to send the results directly to the stream buffer. Tinyformat cannot 370 | be faster than the iostreams because it uses them internally, but it comes 371 | acceptably close. 372 | 373 | 374 | Rationale 375 | --------- 376 | 377 | Or, why did I reinvent this particularly well studied wheel? 378 | 379 | Nearly every program needs text formatting in some form but in many cases such 380 | formatting is *incidental* to the main purpose of the program. In these cases, 381 | you really want a library which is simple to use but as lightweight as 382 | possible. 383 | 384 | The ultimate in lightweight dependencies are the solutions provided by the C++ 385 | and C libraries. However, both the C++ iostreams and C's printf() have 386 | well known usability problems: iostreams are hopelessly verbose for complicated 387 | formatting and printf() lacks extensibility and type safety. For example:: 388 | 389 | // Verbose; hard to read, hard to type: 390 | std::cout << std::setprecision(2) << std::fixed << 1.23456 << "\n"; 391 | // The alternative using a format string is much easier on the eyes 392 | tfm::printf("%.2f\n", 1.23456); 393 | 394 | // Type mismatch between "%s" and int: will cause a segfault at runtime! 395 | printf("%s", 1); 396 | // The following is perfectly fine, and will result in "1" being printed. 397 | tfm::printf("%s", 1); 398 | 399 | On the other hand, there are plenty of excellent and complete libraries which 400 | solve the formatting problem in great generality (boost.format and fastformat 401 | come to mind, but there are many others). Unfortunately these kind of 402 | libraries tend to be rather heavy dependencies, far too heavy for projects 403 | which need to do only a little formatting. Problems include 404 | 405 | 1. Having many large source files. This makes a heavy dependency unsuitable to 406 | bundle within other projects for convenience. 407 | 2. Slow build times for every file using any sort of formatting (this is very 408 | noticeable with g++ and boost/format.hpp. I'm not sure about the various 409 | other alternatives.) 410 | 3. Code bloat due to instantiating many templates 411 | 412 | Tinyformat tries to solve these problems while providing formatting which is 413 | sufficiently general and fast for incidental day to day uses. 414 | 415 | 416 | License 417 | ------- 418 | 419 | For minimum license-related fuss, tinyformat.h is distributed under the boost 420 | software license, version 1.0. (Summary: you must keep the license text on 421 | all source copies, but don't have to mention tinyformat when distributing 422 | binaries.) 423 | 424 | 425 | Author and acknowledgements 426 | --------------------------- 427 | 428 | Tinyformat was written by Chris Foster, with contributions from various people 429 | as recorded in the git repository. 430 | The implementation owes a lot to ``boost::format`` for showing that it's fairly 431 | easy to use stream based formatting to simulate most of the ``printf()`` 432 | syntax. Douglas Gregor's introduction to variadic templates --- see 433 | http://www.generic-programming.org/~dgregor/cpp/variadic-templates.html --- was 434 | also helpful, especially since it solves exactly the ``printf()`` problem for 435 | the case of trivial format strings. 436 | 437 | Bugs 438 | ---- 439 | 440 | Here's a list of known bugs which are probably cumbersome to fix: 441 | 442 | * Field padding won't work correctly with complicated user defined types. For 443 | general types, the only way to do this correctly seems to be format to a 444 | temporary string stream, check the length, and finally send to the output 445 | stream with padding if necessary. Doing this for all types would be 446 | quite inelegant because it implies extra allocations to make the temporary 447 | stream. A workaround is to add logic to ``operator<<()`` for composite user 448 | defined types so they are aware of the stream field width. 449 | -------------------------------------------------------------------------------- /python/draw.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from nanogui import nanovg as nvg 3 | from misc import * 4 | import copy 5 | 6 | def draw_coord_system(ctx): 7 | ctx.StrokeColor(nvg.RGB(0, 0, 0)) 8 | ctx.StrokeWidth(0.005) 9 | 10 | for i in range(-100, 101): 11 | ctx.BeginPath() 12 | ctx.MoveTo(-1000.0, float(i)) 13 | ctx.LineTo(+1000.0, float(i)) 14 | ctx.Stroke() 15 | 16 | for i in range(-100, 101): 17 | ctx.BeginPath() 18 | ctx.MoveTo(float(i), -1000.0) 19 | ctx.LineTo(float(i), +1000.0) 20 | ctx.Stroke() 21 | 22 | ctx.StrokeWidth(0.01) 23 | 24 | ctx.BeginPath() 25 | ctx.MoveTo(-1000.0, 0.0) 26 | ctx.LineTo(+1000.0, 0.0) 27 | ctx.Stroke() 28 | 29 | ctx.BeginPath() 30 | ctx.MoveTo(0.0, -1000.0) 31 | ctx.LineTo(0.0, +1000.0) 32 | ctx.Stroke() 33 | 34 | def draw_path_lines(ctx, path, modifier='', scale=1): 35 | if modifier == 'tangent' or modifier == 'seed': 36 | ctx.StrokeColor(nvg.RGB(255, 0, 0)) 37 | ctx.StrokeWidth(0.004*scale) 38 | elif modifier == 'manifold': 39 | ctx.StrokeColor(nvg.RGB(114, 104, 130)) 40 | ctx.StrokeWidth(0.008*scale) 41 | elif modifier == '': 42 | ctx.StrokeColor(nvg.RGB(80, 80, 80)) 43 | ctx.StrokeWidth(0.008*scale) 44 | for i in range(len(path) - 1): 45 | v0 = path[i].p 46 | v1 = path[i+1].p 47 | 48 | ctx.BeginPath() 49 | ctx.MoveTo(v0[0], v0[1]) 50 | ctx.LineTo(v1[0], v1[1]) 51 | ctx.Stroke() 52 | 53 | def draw_intermediate_path_lines(ctx, path, color, scale=1): 54 | ctx.StrokeColor(color) 55 | ctx.StrokeWidth(0.004*scale) 56 | for i in range(len(path) - 1): 57 | v0 = path[i].p 58 | v1 = path[i+1].p 59 | 60 | ctx.BeginPath() 61 | ctx.MoveTo(v0[0], v0[1]) 62 | ctx.LineTo(v1[0], v1[1]) 63 | ctx.Stroke() 64 | 65 | def draw_dotted_path_lines(ctx, path, scale=1.0, spacing=0.05): 66 | color = nvg.RGB(80, 80, 80) 67 | for i in range(len(path) - 1): 68 | v0 = path[i].p 69 | v1 = path[i+1].p 70 | draw_dotted_line(ctx, v0, v1, color, scale=scale, spacing=spacing) 71 | 72 | def draw_path_vertices(ctx, path, modifier='', scale=1): 73 | if modifier == 'seed': 74 | ctx.FillColor(nvg.RGB(255, 0, 0)) 75 | elif modifier == 'manifold': 76 | ctx.FillColor(nvg.RGB(114, 104, 130)) 77 | else: 78 | ctx.FillColor(nvg.RGB(80, 80, 80)) 79 | 80 | ctx.StrokeColor(nvg.RGB(255, 255, 255)) 81 | ctx.StrokeWidth(0.005*scale) 82 | for i in range(0, len(path)): 83 | vtx = path[i] 84 | ctx.BeginPath() 85 | ctx.Circle(vtx.p[0], vtx.p[1], 0.015*scale) 86 | ctx.Fill() 87 | if modifier != 'seed': 88 | ctx.Stroke() 89 | 90 | def draw_points(ctx, positions, color, scale=1): 91 | ctx.Save() 92 | ctx.FillColor(color) 93 | for p in positions: 94 | ctx.BeginPath() 95 | ctx.Circle(p[0], p[1], 0.008*scale) 96 | ctx.Fill() 97 | ctx.Restore() 98 | 99 | def draw_vertices(ctx, positions, color, scale=1): 100 | ctx.Save() 101 | ctx.FillColor(color) 102 | 103 | ctx.StrokeColor(nvg.RGB(255, 255, 255)) 104 | ctx.StrokeWidth(0.005*scale) 105 | for p in positions: 106 | ctx.BeginPath() 107 | ctx.Circle(p[0], p[1], 0.015*scale) 108 | ctx.Fill() 109 | ctx.Stroke() 110 | ctx.Restore() 111 | 112 | def draw_line(ctx, a, b, color, scale=1.0, endcap_a=False, endcap_b=False): 113 | ctx.Save() 114 | ctx.StrokeWidth(0.01*scale) 115 | ctx.StrokeColor(color) 116 | 117 | ctx.BeginPath() 118 | ctx.MoveTo(a[0], a[1]) 119 | ctx.LineTo(b[0], b[1]) 120 | ctx.Stroke() 121 | 122 | ctx.StrokeWidth(0.005*scale) 123 | v = normalize(b - a) 124 | v_p = np.array([-v[1], v[0]]) 125 | 126 | if endcap_a: 127 | ctx.BeginPath() 128 | a1 = a + 0.02*v_p 129 | a2 = a - 0.02*v_p 130 | ctx.MoveTo(a1[0], a1[1]) 131 | ctx.LineTo(a2[0], a2[1]) 132 | ctx.Stroke() 133 | 134 | if endcap_b: 135 | ctx.BeginPath() 136 | b1 = b + 0.02*v_p 137 | b2 = b - 0.02*v_p 138 | ctx.MoveTo(b1[0], b1[1]) 139 | ctx.LineTo(b2[0], b2[1]) 140 | ctx.Stroke() 141 | 142 | ctx.Restore() 143 | 144 | def draw_dotted_line(ctx, a, b, color, scale=1.0, spacing=0.05): 145 | ctx.Save() 146 | ctx.FillColor(color) 147 | 148 | ctx.BeginPath() 149 | v = b - a 150 | dist = norm(v) 151 | v /= dist 152 | k = 0 153 | while True: 154 | offset = (k+0.5)*spacing*v 155 | if norm(offset) > dist: 156 | break 157 | c = a + offset 158 | ctx.Circle(c[0], c[1], 0.005*scale) 159 | k += 1 160 | ctx.Fill() 161 | ctx.Restore() 162 | 163 | def draw_angle(ctx, p, r, phi_0, phi_1, color, scale=1.0, flip=False): 164 | o = nvg.NVGwinding.CCW if flip else nvg.NVGwinding.CW 165 | ctx.Save() 166 | ctx.StrokeWidth(0.01*scale) 167 | ctx.StrokeColor(color) 168 | ctx.BeginPath() 169 | ctx.Arc(p[0], p[1], r, phi_0, phi_1, o) 170 | ctx.Stroke() 171 | ctx.Restore() 172 | 173 | def draw_arrow_helper(ctx, p0, v, scale=1.0, length=0.12, head_scale=1.0): 174 | theta = np.arctan2(v[1], v[0]) 175 | p1 = p0 + v * length * scale 176 | 177 | ctx.Save() 178 | ctx.StrokeWidth(0.01*scale) 179 | 180 | ctx.BeginPath() 181 | ctx.MoveTo(p0[0], p0[1]) 182 | ctx.LineTo(p1[0], p1[1]) 183 | ctx.Stroke() 184 | 185 | ctx.Restore() 186 | 187 | v_p = np.array([-v[1], v[0]]) 188 | p_a = p1 - 0.02*head_scale*scale*v_p - 0.01*head_scale*scale*v 189 | p_b = p1 + 0.02*head_scale*scale*v_p - 0.01*head_scale*scale*v 190 | 191 | p_tip = p1 + v * 0.03*head_scale * scale 192 | 193 | ctx.BeginPath() 194 | ctx.MoveTo(p_tip[0], p_tip[1]) 195 | ctx.LineTo(p_a[0], p_a[1]) 196 | ctx.ArcTo(p1[0], p1[1], p_b[0], p_b[1], scale*0.028) 197 | ctx.LineTo(p_b[0], p_b[1]) 198 | ctx.LineTo(p_tip[0], p_tip[1]) 199 | ctx.Fill() 200 | 201 | def draw_arrow(ctx, p, v, color, scale=1.0, length=1.0, head_scale=1.0): 202 | ctx.Save() 203 | ctx.FillColor(color) 204 | ctx.StrokeColor(color) 205 | draw_arrow_helper(ctx, p, v, scale, length, head_scale) 206 | ctx.Restore() 207 | 208 | def draw_path_normals(ctx, path, scale=1.0): 209 | ctx.Save() 210 | ctx.StrokeWidth(0.01*scale) 211 | ctx.FillColor(nvg.RGB(255, 0, 0)) 212 | ctx.StrokeColor(nvg.RGB(255, 0, 0)) 213 | for i in range(1, len(path)-1): 214 | vtx = path[i] 215 | draw_arrow_helper(ctx, vtx.p, vtx.n, scale) 216 | ctx.Restore() 217 | 218 | def draw_path_tangents(ctx, path, scale=1.0): 219 | ctx.Save() 220 | ctx.FillColor(nvg.RGB(0, 0, 255)) 221 | ctx.StrokeColor(nvg.RGB(0, 0, 255)) 222 | ctx.StrokeWidth(0.01*scale) 223 | for i in range(1, len(path)-1): 224 | vtx = path[i] 225 | draw_arrow_helper(ctx, vtx.p, vtx.s, scale) 226 | ctx.Restore() 227 | 228 | def draw_path_origin(ctx, path, last_picked_vtx_idx): 229 | if last_picked_vtx_idx == None: 230 | return 231 | 232 | ctx.FillColor(nvg.RGB(255, 255, 255)) 233 | ctx.StrokeColor(nvg.RGB(255, 255, 255)) 234 | ctx.StrokeWidth(0.01) 235 | 236 | i0 = -1 if last_picked_vtx_idx == 0 else 0 237 | i1 = -2 if last_picked_vtx_idx == 0 else 1 238 | p0 = copy.copy(path[i0].p) 239 | p1 = copy.copy(path[i1].p) 240 | wi = p1 - p0 241 | theta = np.arctan2(wi[1], wi[0]) 242 | wi = np.array([np.cos(theta), np.sin(theta)]) 243 | p1 = p0 + wi * 0.08 244 | 245 | ctx.BeginPath() 246 | ctx.MoveTo(p0[0], p0[1]) 247 | ctx.LineTo(p1[0], p1[1]) 248 | ctx.Stroke() 249 | 250 | wi2 = np.array([np.cos(theta+0.3), np.sin(theta+0.3)]) 251 | wi3 = np.array([np.cos(theta-0.3), np.sin(theta-0.3)]) 252 | p2 = p0 + wi2 * 0.08 253 | p3 = p0 + wi3 * 0.08 254 | p4 = p1 + wi * 0.03 255 | 256 | ctx.BeginPath() 257 | ctx.MoveTo(p4[0], p4[1]) 258 | ctx.LineTo(p2[0], p2[1]) 259 | ctx.LineTo(p3[0], p3[1]) 260 | ctx.LineTo(p4[0], p4[1]) 261 | ctx.Fill() 262 | -------------------------------------------------------------------------------- /python/knob.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import nanogui.nanovg as nvg 3 | 4 | class DraggableKnob: 5 | def __init__(self, p=[0,0]): 6 | self.p = np.array(p) 7 | 8 | self.hover = False 9 | self.drag_possible = False 10 | 11 | self.directional = False 12 | self.angle = 0 13 | 14 | self.active = True 15 | 16 | def update(self, input): 17 | if not self.active: 18 | return 19 | 20 | d = input.scale*0.03 21 | dist_current2 = (input.mouse_p[0] - self.p[0])**2 + (input.mouse_p[1] - self.p[1])**2 22 | dist_last2 = (input.mouse_p_click[0] - self.p[0])**2 + (input.mouse_p_click[1] - self.p[1])**2 23 | self.hover = False 24 | self.drag_possible = False 25 | if (dist_current2 < d**2): 26 | self.hover = True 27 | if not dist_last2 > d**2: 28 | self.drag_possible = True 29 | 30 | def draw(self, ctx, scale=1.0): 31 | if not self.active: 32 | return 33 | 34 | if self.directional: 35 | if self.hover: 36 | ctx.StrokeColor(nvg.RGB(255, 0, 0)) 37 | ctx.FillColor(nvg.RGB(255, 0, 0)) 38 | else: 39 | ctx.StrokeColor(nvg.RGB(255, 255, 255)) 40 | ctx.FillColor(nvg.RGB(255, 255, 255)) 41 | p0 = self.p 42 | theta = np.radians(self.angle) 43 | wo = np.array([np.cos(theta), np.sin(theta)]) 44 | p1 = p0 + wo * 0.1*scale 45 | 46 | ctx.StrokeWidth(0.012*scale) 47 | ctx.BeginPath() 48 | ctx.MoveTo(p0[0], p0[1]) 49 | ctx.LineTo(p1[0], p1[1]) 50 | ctx.Stroke() 51 | 52 | wo2 = np.array([np.cos(theta+0.3), np.sin(theta+0.3)]) 53 | wo3 = np.array([np.cos(theta-0.3), np.sin(theta-0.3)]) 54 | 55 | p2 = p0 + wo2 * 0.1*scale 56 | p3 = p0 + wo3 * 0.1*scale 57 | p4 = p1 + wo * 0.05*scale 58 | 59 | ctx.BeginPath() 60 | ctx.MoveTo(p4[0], p4[1]) 61 | ctx.LineTo(p2[0], p2[1]) 62 | ctx.LineTo(p3[0], p3[1]) 63 | ctx.LineTo(p4[0], p4[1]) 64 | ctx.Fill() 65 | 66 | if self.hover: 67 | ctx.FillColor(nvg.RGB(255, 0, 0)) 68 | else: 69 | ctx.FillColor(nvg.RGB(255, 255, 255)) 70 | 71 | 72 | ctx.StrokeColor(nvg.RGB(0, 0, 0)) 73 | 74 | ctx.StrokeWidth(0.01*scale) 75 | 76 | ctx.BeginPath() 77 | ctx.Circle(self.p[0], self.p[1], 0.022*scale) 78 | ctx.Fill() 79 | ctx.Stroke() 80 | -------------------------------------------------------------------------------- /python/misc.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import numpy as np 3 | np.set_printoptions(suppress=True) 4 | from enum import Enum, IntEnum 5 | 6 | Epsilon = 1e-4 7 | 8 | class ConstraintType(IntEnum): 9 | HalfVector = 0, 10 | AngleDifference = 1 11 | 12 | class StrategyType(IntEnum): 13 | MNEE = 0, 14 | SMS = 1 15 | 16 | class ModeType(IntEnum): 17 | Raytracing = 0, 18 | ManifoldExploration = 1, 19 | SpecularManifoldSampling = 2 20 | 21 | def lerp(t, a, b): 22 | return a + (b - a)*t 23 | 24 | def normalize(v): 25 | n = np.sqrt(v[0]**2 + v[1]**2) 26 | return v/n 27 | 28 | def norm(v): 29 | return np.sqrt(v[0]**2 + v[1]**2) 30 | 31 | def cross(u, v): 32 | return u[0]*v[1] - u[1]*v[0] 33 | 34 | def dot(u, v): 35 | return u @ v 36 | 37 | def reflect(w, n): 38 | return True, 2*(w @ n)*n - w 39 | 40 | def d_reflect(w, dw_du, n, dn_du): 41 | dot_w_n = dot(w, n) 42 | dot_dwdu_n = dot(dw_du, n) 43 | dot_w_dndu = dot(w, dn_du) 44 | return 2*((dot_dwdu_n + dot_w_dndu)*n + dot_w_n*dn_du) - dw_du 45 | 46 | def refract(w, n_, eta_): 47 | n = copy.copy(n_) 48 | eta = copy.copy(eta_) 49 | 50 | if dot(w, n) < 0: 51 | eta = 1.0 / eta 52 | n = -n 53 | f = 1.0 / eta 54 | 55 | dot_w_n = dot(w, n) 56 | root_term = 1.0 - f*f * (1 - dot_w_n*dot_w_n) 57 | if root_term < 0: 58 | return False, np.array([0,0]) # TIR 59 | 60 | wt = -f*(w - dot_w_n*n) - n*np.sqrt(root_term) 61 | return True, wt 62 | 63 | def d_refract(w, dw_du, n_, dn_du_, eta_): 64 | n = copy.copy(n_) 65 | dn_du = copy.copy(dn_du_) 66 | eta = copy.copy(eta_) 67 | 68 | if dot(w, n) < 0: 69 | eta = 1.0 / eta 70 | n = -n 71 | dn_du = -dn_du 72 | f = 1.0 / eta 73 | 74 | dot_w_n = dot(w, n) 75 | dot_dwdu_n = dot(dw_du, n) 76 | dot_w_dndu = dot(w, dn_du) 77 | root = np.sqrt(1 - f*f*(1 - dot_w_n*dot_w_n)) 78 | 79 | a_u = -f*(dw_du - ((dot_dwdu_n + dot_w_dndu)*n + dot_w_n*dn_du)) 80 | b1_u = dn_du * root 81 | b2_u = n * 1/(2*root) * (-f*f*(-2*dot_w_n*(dot_dwdu_n + dot_w_dndu))) 82 | b_u = -(b1_u + b2_u) 83 | return a_u + b_u 84 | 85 | def angle(w): 86 | phi = np.arctan2(w[1], w[0]) 87 | if phi < 0: 88 | phi += 2*np.pi 89 | return phi 90 | 91 | def d_angle(w, dw_du): 92 | yx = w[1] / w[0] 93 | d_atan = 1/(1 + yx*yx) 94 | d_phi = d_atan * (w[0]*dw_du[1] - w[1]*dw_du[0]) / (w[0]*w[0]) 95 | return d_phi -------------------------------------------------------------------------------- /python/mode.py: -------------------------------------------------------------------------------- 1 | from misc import * 2 | 3 | class Mode: 4 | def __init__(self, viewer): 5 | self.viewer = viewer 6 | self.scene = viewer.scenes[viewer.scene_idx] 7 | 8 | def enter(self, last): 9 | pass 10 | 11 | def exit(self, next): 12 | pass 13 | 14 | def scene_reset(self): 15 | pass 16 | 17 | def scene_changed(self): 18 | pass 19 | 20 | def update(self, input, scene): 21 | pass 22 | 23 | def draw(self, ctx, scene): 24 | pass 25 | 26 | def layout(self, window): 27 | return [], [] 28 | 29 | def keyboard_event(self, key, scancode, action, modifiers): 30 | return False -------------------------------------------------------------------------------- /python/modes/manifold_exploration.py: -------------------------------------------------------------------------------- 1 | import copy 2 | from misc import * 3 | from path import * 4 | from draw import * 5 | from mode import Mode 6 | from knob import DraggableKnob 7 | from nanogui import * 8 | 9 | class ManifoldExplorationMode(Mode): 10 | def __init__(self, viewer): 11 | super().__init__(viewer) 12 | 13 | self.path = None 14 | self.tangent_path = None 15 | 16 | self.dragging_start = False 17 | self.dragging_end = False 18 | self.knob_start = DraggableKnob() 19 | self.knob_start.active = False 20 | self.knob_end = DraggableKnob() 21 | self.knob_end.active = False 22 | 23 | self.path_needs_update = True 24 | self.constraint_type = ConstraintType.HalfVector 25 | 26 | self.positions = [] 27 | 28 | def enter(self, last): 29 | super().enter(last) 30 | self.path_needs_update = True 31 | 32 | def scene_reset(self): 33 | super().scene_reset() 34 | self.path_needs_update = True 35 | 36 | def scene_changed(self): 37 | super().scene_changed() 38 | self.path_needs_update = True 39 | 40 | def update(self, input, scene): 41 | super().update(input, scene) 42 | 43 | self.tangent_path = None 44 | 45 | # Sample a new path from start pos & ang 46 | if self.path_needs_update: 47 | self.path = scene.sample_path() 48 | self.positions = self.path.copy_positions() 49 | self.path_needs_update = False 50 | start_vtx = self.path[0] 51 | end_vtx = self.path[-1] 52 | 53 | self.path.compute_tangent_derivatives(self.constraint_type) 54 | 55 | # Update start knob 56 | self.knob_start.p = copy.copy(start_vtx.p) 57 | self.knob_start.angle = scene.start_angle_current 58 | self.knob_start.update(input) 59 | 60 | directional = input.alt 61 | self.knob_start.directional = directional 62 | 63 | if not directional: 64 | start_shape = start_vtx.shape 65 | p = copy.copy(self.knob_start.p) 66 | if input.click and (self.knob_start.drag_possible or self.dragging_start): 67 | self.dragging_start = True 68 | p += input.mouse_dp 69 | self.path_needs_update = True 70 | else: 71 | self.dragging_start = False 72 | 73 | u_proj = start_shape.project(p) 74 | scene.start_u_current = u_proj 75 | 76 | if directional: 77 | if input.click and (self.knob_start.drag_possible or self.dragging_start): 78 | self.dragging_start = True 79 | delta = input.mouse_p - input.mouse_p_click 80 | angle = np.arctan2(delta[1], delta[0]) 81 | scene.start_angle_current = np.degrees(angle) 82 | self.path_needs_update = True 83 | else: 84 | self.dragging_start = False 85 | 86 | valid_path = self.path.has_specular_segment() 87 | 88 | old_position = copy.copy(self.positions[-1]) 89 | 90 | # Update end knob 91 | self.knob_end.active = valid_path 92 | if valid_path: 93 | self.knob_end.update(input) 94 | if input.click and (self.knob_end.drag_possible or self.dragging_end): 95 | self.dragging_end = True 96 | 97 | # Adjust endpoint based on mouse movement 98 | dp = input.mouse_dp 99 | self.positions[-1] += dp 100 | else: 101 | self.dragging_end = False 102 | 103 | if self.dragging_end: 104 | end_shape = self.path[-1].shape 105 | t_proj = end_shape.project(self.positions[-1]) 106 | self.positions[-1] = end_shape.sample_position(t_proj).p 107 | 108 | new_position = copy.copy(self.positions[-1]) 109 | 110 | if self.tangents_btn.pushed(): 111 | ## TANGENT MODE 112 | 113 | # Convert spatial offset of endpoint into tangential offset (along u) 114 | du = self.path[-1].s @ (self.positions[-1] - old_position) 115 | 116 | # Update path vertices (to first order) based on endpoint movement 117 | for idx in range(1, len(self.positions)-1): 118 | self.positions[idx] -= 1.0 * self.path[idx].dp_du * self.path[idx].dC_duk * du 119 | 120 | self.tangent_path = scene.reproject_path_me(self.positions) 121 | else: 122 | ## FULL MANIFOLD WALK MODE 123 | new_position = copy.copy(self.positions[-1]) 124 | 125 | # Try to walk from start to end position 126 | dx = new_position - old_position 127 | current_path = self.path.copy() 128 | 129 | if norm(dx) > 0: 130 | i = 0 131 | beta = 1.0 132 | N = self.max_steps() 133 | threshold = self.eps_threshold() 134 | success = False 135 | while True: 136 | # Give up after too many iterations 137 | if i >= N: 138 | break 139 | 140 | # Compute tangents and constraints 141 | current_path.compute_tangent_derivatives(self.constraint_type) 142 | if current_path.singular: 143 | break 144 | 145 | # Convert spatial offset of endpoint into tangential offset (along u) 146 | du = current_path[-1].s @ dx 147 | 148 | # And move first specular vertex in chain according to it 149 | offset_positions = current_path.copy_positions() 150 | offset_positions[1] -= beta * current_path[1].dp_du * current_path[1].dC_duk * du 151 | 152 | # Ray trace to re-project onto specular manifold 153 | proposed_path = scene.reproject_path_me(offset_positions) 154 | if not current_path.same_submanifold(proposed_path): 155 | beta = 0.5 * beta 156 | i += 1 157 | continue 158 | 159 | # Check for forward progress 160 | if proposed_path: 161 | delta_old = norm(new_position - current_path[-1].p) 162 | delta_new = norm(new_position - proposed_path[-1].p) 163 | if delta_new < delta_old: 164 | beta = min(1.0, 2*beta) 165 | current_path = proposed_path 166 | else: 167 | beta = 0.5 * beta 168 | else: 169 | beta = 0.5 * beta 170 | 171 | i += 1 172 | 173 | # Check for success 174 | dx = new_position - current_path[-1].p 175 | if norm(dx) < threshold: 176 | success = True 177 | break 178 | 179 | if success: 180 | self.path = current_path 181 | else: 182 | self.positions[-1] = old_position 183 | 184 | # Update start angle s.t. we can move start point without "jump" in visualization 185 | new_start_dir = normalize(self.path[1].p - self.path[0].p) 186 | new_start_angle = np.arctan2(new_start_dir[1], new_start_dir[0]) 187 | scene.start_angle_current = np.degrees(new_start_angle) 188 | 189 | self.knob_end.p = self.positions[-1] 190 | 191 | def draw(self, ctx, scene): 192 | super().draw(ctx, scene) 193 | s = scene.scale 194 | 195 | draw_path_lines(ctx, self.path, '', s) 196 | scene.draw(ctx) 197 | if self.display_tangents_btn.pushed(): 198 | draw_path_tangents(ctx, self.path, s) 199 | if self.display_normals_btn.pushed(): 200 | draw_path_normals(ctx, self.path, scale=s) 201 | 202 | if self.tangent_path and self.tangents_btn.pushed() and self.tangents_path_btn.pushed(): 203 | draw_path_lines(ctx, self.tangent_path, modifier='tangent', scale=s) 204 | 205 | draw_path_vertices(ctx, self.path, '', s) 206 | if self.tangents_btn.pushed() and self.path.has_specular_segment(): 207 | draw_vertices(ctx, self.positions[1:-1], nvg.RGB(255, 0, 0), 1.3*s) 208 | 209 | self.knob_start.draw(ctx) 210 | self.knob_end.draw(ctx) 211 | 212 | def layout(self, window): 213 | frame_tools = Widget(window) 214 | frame_tools.set_layout(BoxLayout(Orientation.Horizontal, 215 | Alignment.Middle, 0, 3)) 216 | Label(frame_tools, "Display local frame: ") 217 | self.display_normals_btn = Button(frame_tools, "", icons.FA_ARROW_UP) 218 | self.display_normals_btn.set_pushed(True) 219 | self.display_normals_btn.set_flags(Button.Flags.ToggleButton) 220 | self.display_tangents_btn = Button(frame_tools, "", icons.FA_ARROW_RIGHT) 221 | self.display_tangents_btn.set_flags(Button.Flags.ToggleButton) 222 | 223 | constraint_tools = Widget(window) 224 | constraint_tools.set_layout(BoxLayout(Orientation.Horizontal, 225 | Alignment.Middle, 0, 3)) 226 | Label(constraint_tools, "Constraint type: ") 227 | 228 | self.constraint_hv_btn = Button(constraint_tools, "", icons.FA_RULER_COMBINED) 229 | self.constraint_hv_btn.set_flags(Button.Flags.RadioButton) 230 | self.constraint_hv_btn.set_pushed(self.constraint_type == ConstraintType.HalfVector) 231 | def constraint_hv_cb(state): 232 | if state: 233 | self.constraint_type = ConstraintType.HalfVector 234 | self.constraint_hv_btn.set_change_callback(constraint_hv_cb) 235 | 236 | self.constraint_dir_btn = Button(constraint_tools, "", icons.FA_DRAFTING_COMPASS) 237 | self.constraint_dir_btn.set_flags(Button.Flags.RadioButton) 238 | self.constraint_dir_btn.set_pushed(self.constraint_type == ConstraintType.AngleDifference) 239 | def constraint_dir_cb(state): 240 | if state: 241 | self.constraint_type = ConstraintType.AngleDifference 242 | self.constraint_dir_btn.set_change_callback(constraint_dir_cb) 243 | 244 | tangent_tools = Widget(window) 245 | tangent_tools.set_layout(BoxLayout(Orientation.Horizontal, 246 | Alignment.Middle, 0, 3)) 247 | Label(tangent_tools, "Toggle tangent steps: ") 248 | self.tangents_btn = Button(tangent_tools, "", icons.FA_RULER) 249 | self.tangents_btn.set_pushed(False) 250 | self.tangents_btn.set_flags(Button.Flags.ToggleButton) 251 | def tangents_mode_cb(state): 252 | self.positions = self.path.copy_positions() 253 | if not self.tangents_btn.pushed(): 254 | self.tangents_path_btn.set_pushed(False) 255 | self.tangents_btn.set_change_callback(tangents_mode_cb) 256 | 257 | self.tangents_path_btn = Button(tangent_tools, "", icons.FA_SLASH) 258 | self.tangents_path_btn.set_pushed(False) 259 | self.tangents_path_btn.set_flags(Button.Flags.ToggleButton) 260 | 261 | self.debug_btn = Button(window, "Debug print", icons.FA_SPIDER) 262 | def debug_cb(): 263 | self.path.print_derivative_debug(1, self.constraint_type) 264 | self.debug_btn.set_callback(debug_cb) 265 | 266 | steps_eps_tools = Widget(window) 267 | steps_eps_tools.set_layout(BoxLayout(Orientation.Horizontal, 268 | Alignment.Middle, 0, 3)) 269 | 270 | Label(steps_eps_tools, "N") 271 | self.max_steps_sl = Slider(steps_eps_tools) 272 | max_steps_tb = TextBox(steps_eps_tools) 273 | max_steps_tb.set_value("20") 274 | max_steps_tb.set_font_size(20) 275 | max_steps_tb.set_alignment(TextBox.Alignment.Right) 276 | 277 | self.max_steps_sl.set_value(0.3878) 278 | def max_steps_cb(value): 279 | max_steps_tb.set_value("%i" % (1 + int(49*value))) 280 | self.max_steps_sl.set_callback(max_steps_cb) 281 | 282 | 283 | Label(steps_eps_tools, "eps") 284 | self.eps_sl = Slider(steps_eps_tools) 285 | eps_tb = TextBox(steps_eps_tools) 286 | eps_tb.set_value("1.0E-03") 287 | eps_tb.set_font_size(20) 288 | eps_tb.set_alignment(TextBox.Alignment.Right) 289 | 290 | self.eps_sl.set_value(0.285) 291 | def eps_cb(value): 292 | eps_tb.set_value("%.1E" % 10.0**(-(1 + value*7))) 293 | self.eps_sl.set_callback(eps_cb) 294 | 295 | 296 | return [frame_tools, constraint_tools, tangent_tools, self.debug_btn, steps_eps_tools], [] 297 | 298 | def max_steps(self): 299 | value = self.max_steps_sl.value() 300 | return 1 + int(49*value) 301 | 302 | def eps_threshold(self): 303 | value = self.eps_sl.value() 304 | return 10.0**(-(1 + value*7)) -------------------------------------------------------------------------------- /python/modes/raytracing.py: -------------------------------------------------------------------------------- 1 | import copy 2 | from misc import * 3 | from path import * 4 | from draw import * 5 | from mode import Mode 6 | from knob import DraggableKnob 7 | from nanogui import * 8 | 9 | class RaytracingMode(Mode): 10 | def __init__(self, viewer): 11 | super().__init__(viewer) 12 | 13 | self.path = [] 14 | 15 | self.dragging_start = False 16 | self.dragging_end = False 17 | self.knob_start = DraggableKnob() 18 | 19 | def update(self, input, scene): 20 | super().update(input, scene) 21 | 22 | # Sample a new path from start pos & ang 23 | self.path = scene.sample_path() 24 | start_vtx = self.path[0] 25 | 26 | # Update start knob 27 | self.knob_start.p = copy.copy(start_vtx.p) 28 | self.knob_start.angle = scene.start_angle_current 29 | self.knob_start.update(input) 30 | 31 | directional = input.alt 32 | self.knob_start.directional = directional 33 | 34 | if not directional: 35 | start_shape = start_vtx.shape 36 | p = copy.copy(self.knob_start.p) 37 | if input.click and (self.knob_start.drag_possible or self.dragging_start): 38 | self.dragging_start = True 39 | p += input.mouse_dp 40 | else: 41 | self.dragging_start = False 42 | 43 | u_proj = start_shape.project(p) 44 | scene.start_u_current = u_proj 45 | 46 | if directional: 47 | if input.click and (self.knob_start.drag_possible or self.dragging_start): 48 | self.dragging_start = True 49 | delta = input.mouse_p - input.mouse_p_click 50 | angle = np.arctan2(delta[1], delta[0]) 51 | scene.start_angle_current = np.degrees(angle) 52 | else: 53 | self.dragging_start = False 54 | 55 | def draw(self, ctx, scene): 56 | super().draw(ctx, scene) 57 | s = scene.scale 58 | 59 | draw_path_lines(ctx, self.path, '', s) 60 | scene.draw(ctx) 61 | if self.display_tangents_btn.pushed(): 62 | draw_path_tangents(ctx, self.path, s) 63 | if self.display_normals_btn.pushed(): 64 | draw_path_normals(ctx, self.path, scale=s) 65 | draw_path_vertices(ctx, self.path, '', s) 66 | 67 | self.knob_start.draw(ctx) 68 | 69 | def layout(self, window): 70 | toggle_tools = Widget(window) 71 | toggle_tools.set_layout(BoxLayout(Orientation.Horizontal, 72 | Alignment.Middle, 0, 3)) 73 | Label(toggle_tools, "Display local frame: ") 74 | self.display_normals_btn = Button(toggle_tools, "", icons.FA_ARROW_UP) 75 | self.display_normals_btn.set_pushed(True) 76 | self.display_normals_btn.set_flags(Button.Flags.ToggleButton) 77 | self.display_tangents_btn = Button(toggle_tools, "", icons.FA_ARROW_RIGHT) 78 | self.display_tangents_btn.set_flags(Button.Flags.ToggleButton) 79 | 80 | return [toggle_tools], [] -------------------------------------------------------------------------------- /python/modes/specular_manifold_sampling.py: -------------------------------------------------------------------------------- 1 | import copy 2 | from misc import * 3 | from path import * 4 | from draw import * 5 | from mode import Mode 6 | from knob import DraggableKnob 7 | from nanogui import * 8 | 9 | class SpecularManifoldSamplingMode(Mode): 10 | def __init__(self, viewer): 11 | super().__init__(viewer) 12 | 13 | self.seed_path = None 14 | self.solution_path = None 15 | self.intermediate_paths = None 16 | 17 | self.sms_mode = False 18 | self.seed_paths = [] 19 | self.solution_paths = [] 20 | self.rough_mode = False 21 | 22 | self.constraint_type = ConstraintType.HalfVector 23 | self.strategy_type = StrategyType.SMS 24 | 25 | self.dragging_start = False 26 | self.dragging_end = False 27 | self.dragging_spec = False 28 | self.knob_start = DraggableKnob() 29 | self.knob_end = DraggableKnob() 30 | self.knob_spec = DraggableKnob() 31 | 32 | self.n_bounces_box = None 33 | 34 | self.animating = False 35 | self.time = 0.0 36 | self.scene = None 37 | 38 | def enter(self, last): 39 | super().enter(last) 40 | if self.n_bounces_box: 41 | self.n_bounces_box.set_value(self.scene.n_bounces_default) 42 | 43 | def scene_changed(self): 44 | scene = self.viewer.scenes[self.viewer.scene_idx] 45 | self.n_bounces_box.set_value(scene.n_bounces_default) 46 | 47 | def update(self, input, scene): 48 | super().update(input, scene) 49 | self.scene = scene 50 | 51 | if self.animating: 52 | self.time += 0.01 53 | time_p = 0.5 + 0.5*np.sin(self.time) 54 | 55 | scene.spec_u_current = lerp(time_p, 0.1, 0.9) 56 | 57 | # Set positions and update for all three knobs 58 | p_start = scene.sample_start_position(scene.start_u_current).p 59 | p_end = scene.sample_end_position(scene.end_u_current).p 60 | p_spec = scene.sample_spec_position(scene.spec_u_current).p 61 | self.knob_start.p = copy.copy(p_start) 62 | self.knob_end.p = copy.copy(p_end) 63 | self.knob_spec.p = copy.copy(p_spec) 64 | 65 | self.knob_start.update(input) 66 | if input.click and (self.knob_start.drag_possible or self.dragging_start): 67 | self.dragging_start = True 68 | p_start += input.mouse_dp 69 | else: 70 | self.dragging_start = False 71 | u_proj = scene.start_shape().project(p_start) 72 | scene.start_u_current = u_proj 73 | 74 | self.knob_end.update(input) 75 | if input.click and (self.knob_end.drag_possible or self.dragging_end): 76 | self.dragging_end = True 77 | p_end += input.mouse_dp 78 | else: 79 | self.dragging_end = False 80 | u_proj = scene.end_shape().project(p_end) 81 | scene.end_u_current = u_proj 82 | 83 | self.knob_spec.active = self.strategy_type == StrategyType.SMS and not self.sms_mode and not self.rough_mode 84 | self.knob_spec.update(input) 85 | if input.click and (self.knob_spec.drag_possible or self.dragging_spec): 86 | self.dragging_spec = True 87 | p_spec += input.mouse_dp 88 | else: 89 | self.dragging_spec = False 90 | u_proj = scene.first_specular_shape().project(p_spec) 91 | scene.spec_u_current = u_proj 92 | 93 | if not self.sms_mode and not self.rough_mode: 94 | if self.strategy_type == StrategyType.MNEE: 95 | self.seed_path = scene.sample_mnee_seed_path() 96 | else: 97 | self.seed_path = scene.sample_seed_path(self.n_bounces_box.value()) 98 | 99 | if self.seed_path.has_specular_segment(): 100 | self.solution_path, self.intermediate_paths = self.newton_solver(scene, self.seed_path) 101 | 102 | def newton_solver(self, scene, seed_path): 103 | current_path = seed_path.copy() 104 | intermediate_paths = [current_path] 105 | 106 | i = 0 107 | beta = 1.0 108 | N = self.max_steps() 109 | threshold = self.eps_threshold() 110 | success = False 111 | while True: 112 | # Give up after too many iterations 113 | if i >= N: 114 | break 115 | 116 | # Compute tangents and constraints 117 | current_path.compute_tangent_derivatives(self.constraint_type) 118 | if current_path.singular: 119 | break 120 | 121 | # Check for success 122 | converged = True 123 | for vtx in current_path: 124 | if vtx.shape.type == Shape.Type.Reflection or vtx.shape.type == Shape.Type.Refraction: 125 | if abs(vtx.C) > threshold: 126 | converged = False 127 | break 128 | if converged: 129 | success = True 130 | break 131 | 132 | proposed_offsets = current_path.copy_positions() 133 | for k, vtx in enumerate(current_path): 134 | if vtx.shape.type == Shape.Type.Reflection or vtx.shape.type == Shape.Type.Refraction: 135 | proposed_offsets[k] -= self.step_size_scale()*beta * vtx.dp_du * vtx.dX 136 | 137 | # Ray trace to re-project onto specular manifold 138 | proposed_path = scene.reproject_path_sms(proposed_offsets, current_path, self.n_bounces_box.value()) 139 | if not current_path.same_submanifold(proposed_path): 140 | beta = 0.5 * beta 141 | else: 142 | beta = min(1.0, 2*beta) 143 | current_path = proposed_path 144 | intermediate_paths.append(current_path) 145 | 146 | i = i + 1 147 | 148 | if success: 149 | p_last = current_path[-1].p 150 | p_spec = current_path[-2].p 151 | d = p_spec - p_last 152 | d_norm = norm(d) 153 | d /= d_norm 154 | ray = Ray2f(p_last, d, 1e-4, d_norm) 155 | it = scene.ray_intersect(ray) 156 | if it.is_valid(): 157 | success = False 158 | 159 | if success: 160 | solution_path = current_path 161 | else: 162 | solution_path = None 163 | return solution_path, intermediate_paths 164 | 165 | def draw(self, ctx, scene): 166 | super().draw(ctx, scene) 167 | s = scene.scale 168 | 169 | show_seed_paths = self.show_seed_paths_chb.checked() 170 | show_intermediate_steps = self.show_intermediate_steps_chb.checked() 171 | 172 | if self.sms_mode: 173 | if show_seed_paths: 174 | for seed_path in self.seed_paths: 175 | draw_dotted_path_lines(ctx, seed_path, 0.6*s, spacing=0.02) 176 | for solution in self.solution_paths: 177 | draw_path_lines(ctx, solution, '', s) 178 | elif self.rough_mode: 179 | draw_dotted_path_lines(ctx, self.seed_path, 0.6*s, spacing=0.02) 180 | for solution in self.solution_paths: 181 | draw_intermediate_path_lines(ctx, solution, nvg.RGB(80, 80, 80), s) 182 | if self.solution_path: 183 | draw_intermediate_path_lines(ctx, self.solution_path, nvg.RGB(255, 0, 0), s) 184 | elif not show_intermediate_steps: 185 | draw_dotted_path_lines(ctx, self.seed_path, s, spacing=0.02) 186 | if self.solution_path: 187 | draw_path_lines(ctx, self.solution_path, '', s) 188 | else: 189 | N = len(self.intermediate_paths) 190 | g = np.linspace(0, 255, N) 191 | r = np.linspace(255, 0, N) 192 | for k, path in enumerate(self.intermediate_paths): 193 | draw_intermediate_path_lines(ctx, path, nvg.RGB(int(r[k]), int(g[k]), 0), s) 194 | 195 | scene.draw(ctx) 196 | 197 | if self.sms_mode or self.rough_mode: 198 | pass 199 | elif self.show_constraint_chb.checked(): 200 | for k, vtx in enumerate(self.seed_path): 201 | if vtx.shape.type == Shape.Type.Reflection or vtx.shape.type == Shape.Type.Refraction: 202 | vtx_prev = self.seed_path[k-1] 203 | vtx_next = self.seed_path[k+1] 204 | wo = normalize(vtx_next.p - vtx.p) 205 | wi = normalize(vtx_prev.p - vtx.p) 206 | 207 | eta = self.seed_path[k].shape.eta 208 | if dot(wi, self.seed_path[k].n) < 0.0: 209 | eta = 1.0/eta 210 | 211 | if self.constraint_type == ConstraintType.HalfVector: 212 | draw_arrow(ctx, vtx.p, vtx.n, nvg.RGB(255, 0, 0), scale=s, length=0.25) 213 | 214 | h = normalize(wi + eta * wo) 215 | if eta != 1.0: 216 | h *= -1 217 | draw_arrow(ctx, vtx.p, wi, nvg.RGB(0, 0, 0), scale=s, length=0.25) 218 | draw_arrow(ctx, vtx.p, wo, nvg.RGB(0, 0, 0), scale=s, length=0.25) 219 | draw_arrow(ctx, vtx.p, h, nvg.RGB(0, 128, 0), scale=s, length=0.25) 220 | 221 | constraint = dot(vtx.s, h) 222 | draw_line(ctx, vtx.p, vtx.p+0.25*constraint*vtx.s, nvg.RGB(120, 80, 250), scale=1.2*s, endcap_b=True) 223 | elif self.constraint_type == ConstraintType.AngleDifference: 224 | if self.flip_constraint_chb.checked(): 225 | wi, wo = wo, wi 226 | 227 | m = vtx.s * vtx.n_offset[0] + vtx.n * vtx.n_offset[1] 228 | if eta == 1.0: 229 | wio = reflect(wi, m) 230 | else: 231 | wio = refract(wi, m, eta) 232 | if wio[0]: 233 | wio = wio[1] 234 | phi_wo = np.arctan2(wo[1], wo[0]) 235 | phi_wio = np.arctan2(wio[1], wio[0]) 236 | draw_arrow(ctx, vtx.p, vtx.n, nvg.RGB(255, 0, 0), scale=s, length=0.25) 237 | draw_angle(ctx, vtx.p, 0.2, phi_wo, phi_wio, nvg.RGB(120, 80, 250), scale=1.2*s, flip=(phi_wio - phi_wo < 0)) 238 | draw_arrow(ctx, vtx.p, wi, nvg.RGB(0, 0, 0), scale=s, length=0.25) 239 | draw_arrow(ctx, vtx.p, wo, nvg.RGB(0, 0, 0), scale=s, length=0.25) 240 | draw_arrow(ctx, vtx.p, wio, nvg.RGB(0, 128, 0), scale=s, length=0.25) 241 | elif not show_intermediate_steps: 242 | draw_path_normals(ctx, self.seed_path, scale=s) 243 | 244 | if self.rough_mode: 245 | pass 246 | elif self.sms_mode: 247 | for solution in self.solution_paths: 248 | draw_path_vertices(ctx, solution, '', s) 249 | elif not show_intermediate_steps: 250 | draw_path_vertices(ctx, self.seed_path, '', s) 251 | if self.solution_path: 252 | draw_path_vertices(ctx, self.solution_path, '', s) 253 | 254 | self.knob_start.draw(ctx) 255 | self.knob_end.draw(ctx) 256 | self.knob_spec.draw(ctx) 257 | 258 | def layout(self, window): 259 | strategy_tools = Widget(window) 260 | strategy_tools.set_layout(BoxLayout(Orientation.Horizontal, 261 | Alignment.Middle, 0, 3)) 262 | Label(strategy_tools, "MNEE vs. SMS:") 263 | 264 | self.strategy_mnee_btn = Button(strategy_tools, "", icons.FA_CIRCLE) 265 | self.strategy_mnee_btn.set_flags(Button.Flags.RadioButton) 266 | self.strategy_mnee_btn.set_pushed(self.strategy_type == StrategyType.MNEE) 267 | def strategy_mnee_cb(state): 268 | if state: 269 | self.strategy_type = StrategyType.MNEE 270 | self.strategy_mnee_btn.set_change_callback(strategy_mnee_cb) 271 | 272 | self.strategy_sms_btn = Button(strategy_tools, "", icons.FA_CERTIFICATE) 273 | self.strategy_sms_btn.set_flags(Button.Flags.RadioButton) 274 | self.strategy_sms_btn.set_pushed(self.strategy_type == StrategyType.SMS) 275 | def strategy_sms_cb(state): 276 | if state: 277 | self.strategy_type = StrategyType.SMS 278 | self.strategy_sms_btn.set_change_callback(strategy_sms_cb) 279 | 280 | Label(strategy_tools, " N=") 281 | self.n_bounces_box = IntBox(strategy_tools) 282 | self.n_bounces_box.set_fixed_size((50, 20)) 283 | scene = self.viewer.scenes[self.viewer.scene_idx] 284 | self.n_bounces_box.set_value(scene.n_bounces_default) 285 | self.n_bounces_box.set_default_value("1") 286 | self.n_bounces_box.set_font_size(20) 287 | self.n_bounces_box.set_spinnable(True) 288 | self.n_bounces_box.set_min_value(1) 289 | self.n_bounces_box.set_value_increment(1) 290 | 291 | 292 | constraint_tools = Widget(window) 293 | constraint_tools.set_layout(BoxLayout(Orientation.Horizontal, 294 | Alignment.Middle, 0, 3)) 295 | Label(constraint_tools, "Constraint type: ") 296 | 297 | self.constraint_hv_btn = Button(constraint_tools, "", icons.FA_RULER_COMBINED) 298 | self.constraint_hv_btn.set_flags(Button.Flags.RadioButton) 299 | self.constraint_hv_btn.set_pushed(self.constraint_type == ConstraintType.HalfVector) 300 | def constraint_hv_cb(state): 301 | if state: 302 | self.constraint_type = ConstraintType.HalfVector 303 | self.constraint_hv_btn.set_change_callback(constraint_hv_cb) 304 | 305 | self.constraint_dir_btn = Button(constraint_tools, "", icons.FA_DRAFTING_COMPASS) 306 | self.constraint_dir_btn.set_flags(Button.Flags.RadioButton) 307 | self.constraint_dir_btn.set_pushed(self.constraint_type == ConstraintType.AngleDifference) 308 | def constraint_dir_cb(state): 309 | if state: 310 | self.constraint_type = ConstraintType.AngleDifference 311 | self.constraint_dir_btn.set_change_callback(constraint_dir_cb) 312 | 313 | self.show_constraint_chb = CheckBox(constraint_tools, "Show") 314 | self.show_constraint_chb.set_checked(True) 315 | self.flip_constraint_chb = CheckBox(constraint_tools, "Flip") 316 | self.flip_constraint_chb.set_checked(False) 317 | 318 | steps_eps_tools = Widget(window) 319 | steps_eps_tools.set_layout(BoxLayout(Orientation.Horizontal, 320 | Alignment.Middle, 0, 3)) 321 | 322 | Label(steps_eps_tools, "N") 323 | self.max_steps_sl = Slider(steps_eps_tools) 324 | max_steps_tb = TextBox(steps_eps_tools) 325 | max_steps_tb.set_value("20") 326 | max_steps_tb.set_font_size(20) 327 | max_steps_tb.set_alignment(TextBox.Alignment.Right) 328 | self.max_steps_sl.set_value(0.3878) 329 | def max_steps_cb(value): 330 | max_steps_tb.set_value("%i" % (1 + int(49*value))) 331 | self.max_steps_sl.set_callback(max_steps_cb) 332 | 333 | Label(steps_eps_tools, "eps") 334 | self.eps_sl = Slider(steps_eps_tools) 335 | eps_tb = TextBox(steps_eps_tools) 336 | eps_tb.set_value("1.0E-03") 337 | eps_tb.set_font_size(20) 338 | eps_tb.set_alignment(TextBox.Alignment.Right) 339 | self.eps_sl.set_value(0.285) 340 | def eps_cb(value): 341 | eps_tb.set_value("%.1E" % 10.0**(-(1 + value*7))) 342 | self.eps_sl.set_callback(eps_cb) 343 | 344 | intermediate_tools = Widget(window) 345 | intermediate_tools.set_layout(BoxLayout(Orientation.Horizontal, 346 | Alignment.Middle, 0, 3)) 347 | Label(intermediate_tools, "Show intermediate steps:") 348 | self.show_intermediate_steps_chb = CheckBox(intermediate_tools, "") 349 | self.show_intermediate_steps_chb.set_checked(False) 350 | Label(intermediate_tools, "Step size:") 351 | self.step_size_sl = Slider(intermediate_tools) 352 | self.step_size_sl.set_value(1.0) 353 | 354 | sms_tools = Widget(window) 355 | sms_tools.set_layout(BoxLayout(Orientation.Horizontal, 356 | Alignment.Middle, 0, 2)) 357 | Label(sms_tools, "Sample") 358 | self.n_sms_paths_box = IntBox(sms_tools) 359 | self.n_sms_paths_box.set_fixed_size((50, 20)) 360 | self.n_sms_paths_box.set_value(10) 361 | self.n_sms_paths_box.set_default_value("10") 362 | self.n_sms_paths_box.set_font_size(20) 363 | self.n_sms_paths_box.set_spinnable(True) 364 | self.n_sms_paths_box.set_min_value(1) 365 | self.n_sms_paths_box.set_value_increment(1) 366 | Label(sms_tools, " paths") 367 | self.sms_btn = Button(sms_tools, "Go", icons.FA_ROCKET) 368 | self.sms_btn.set_background_color(Color(0, 1.0, 0, 0.1)) 369 | def sms_cb(): 370 | self.sms_mode = not self.sms_mode 371 | if self.sms_mode: 372 | self.sms_btn.set_background_color(Color(1.0, 0, 0, 0.1)) 373 | self.rough_btn.set_enabled(False) 374 | else: 375 | self.sms_btn.set_background_color(Color(0, 1.0, 0, 0.1)) 376 | self.rough_btn.set_enabled(True) 377 | 378 | if self.sms_mode: 379 | spec_u_current = self.scene.spec_u_current 380 | self.seed_paths = [] 381 | self.solution_paths = [] 382 | N = self.n_sms_paths_box.value() 383 | for k in range(N): 384 | self.scene.spec_u_current = np.random.uniform() 385 | seed_path = self.scene.sample_seed_path(self.n_bounces_box.value()) 386 | self.seed_paths.append(seed_path.copy()) 387 | 388 | if seed_path.has_specular_segment(): 389 | solution_path, _ = self.newton_solver(self.scene, seed_path) 390 | if solution_path: 391 | self.solution_paths.append(solution_path.copy()) 392 | 393 | self.scene.spec_u_current = spec_u_current 394 | self.sms_btn.set_callback(sms_cb) 395 | 396 | Label(sms_tools, " Show seeds:") 397 | self.show_seed_paths_chb = CheckBox(sms_tools, "") 398 | self.show_seed_paths_chb.set_checked(True) 399 | 400 | rough_tools = Widget(window) 401 | rough_tools.set_layout(BoxLayout(Orientation.Horizontal, 402 | Alignment.Middle, 0, 2)) 403 | Label(rough_tools, "Sample") 404 | self.n_normals_box = IntBox(rough_tools) 405 | self.n_normals_box.set_fixed_size((50, 20)) 406 | self.n_normals_box.set_value(10) 407 | self.n_normals_box.set_default_value("10") 408 | self.n_normals_box.set_font_size(20) 409 | self.n_normals_box.set_spinnable(True) 410 | self.n_normals_box.set_min_value(1) 411 | self.n_normals_box.set_value_increment(1) 412 | Label(rough_tools, " normals ( ") 413 | self.roughness_box = FloatBox(rough_tools) 414 | self.roughness_box.set_editable(True) 415 | self.roughness_box.set_fixed_size((60, 20)) 416 | self.roughness_box.set_value(0.1) 417 | self.roughness_box.set_default_value("0.1") 418 | self.roughness_box.set_font_size(20) 419 | self.roughness_box.set_format("[0-9]*\\.?[0-9]+") 420 | Label(rough_tools, " )") 421 | self.rough_btn = Button(rough_tools, "Go", icons.FA_ROCKET) 422 | self.rough_btn.set_background_color(Color(0, 1.0, 0, 0.1)) 423 | def rough_cb(): 424 | self.rough_mode = not self.rough_mode 425 | if self.rough_mode: 426 | self.rough_btn.set_background_color(Color(1.0, 0, 0, 0.1)) 427 | self.sms_btn.set_enabled(False) 428 | else: 429 | self.rough_btn.set_background_color(Color(0, 1.0, 0, 0.1)) 430 | self.sms_btn.set_enabled(True) 431 | 432 | if self.rough_mode: 433 | self.solution_paths = [] 434 | N = self.n_normals_box.value() 435 | for k in range(N): 436 | seed_path = self.seed_path.copy() 437 | for k, vtx in enumerate(seed_path): 438 | if vtx.shape.type == Shape.Type.Reflection or vtx.shape.type == Shape.Type.Refraction: 439 | alpha = self.roughness_box.value() 440 | sigma2 = 0.5*alpha*alpha 441 | slope = np.random.normal(0, np.sqrt(sigma2)) 442 | vtx.n_offset = normalize(np.array([-slope, 1])) 443 | if seed_path.has_specular_segment(): 444 | solution_path, _ = self.newton_solver(self.scene, seed_path) 445 | if solution_path: 446 | self.solution_paths.append(solution_path.copy()) 447 | self.rough_btn.set_callback(rough_cb) 448 | 449 | return [strategy_tools, constraint_tools, steps_eps_tools, sms_tools, rough_tools, intermediate_tools], [] 450 | 451 | def keyboard_event(self, key, scancode, action, modifiers): 452 | super().keyboard_event(key, scancode, action, modifiers) 453 | 454 | if key == glfw.KEY_RIGHT and action == glfw.PRESS: 455 | self.animating = not self.animating 456 | return True 457 | 458 | def max_steps(self): 459 | value = self.max_steps_sl.value() 460 | return 1 + int(49*value) 461 | 462 | def eps_threshold(self): 463 | value = self.eps_sl.value() 464 | return 10.0**(-(1 + value*7)) 465 | 466 | def step_size_scale(self): 467 | return self.step_size_sl.value() -------------------------------------------------------------------------------- /python/path.py: -------------------------------------------------------------------------------- 1 | from misc import * 2 | from manifolds import * 3 | 4 | class Path(): 5 | def __init__(self): 6 | self.vertices = [] 7 | self.n_total = 0 8 | self.n_specular = 0 9 | self.singular = False 10 | 11 | def __getitem__(self, i): 12 | return self.vertices[i] 13 | 14 | def __setitem__(self, key, val): 15 | self.vertices[key] = val 16 | 17 | def __len__(self): 18 | return len(self.vertices) 19 | 20 | def append(self, vtx): 21 | vtx.C = 0 22 | vtx.dC_du_prev = 0 23 | vtx.dC_du_cur = 0 24 | vtx.dC_du_next = 0 25 | vtx.dM_dz = 0 26 | vtx.dM_de = 0 27 | self.vertices.append(vtx) 28 | if vtx.shape.type == Shape.Type.Reflection or vtx.shape.type == Shape.Type.Refraction: 29 | self.n_specular += 1 30 | self.n_total += 1 31 | 32 | def copy(self): 33 | path = Path() 34 | 35 | for i in range(len(self)): 36 | vtx = self[i].copy() 37 | path.append(vtx) 38 | return path 39 | 40 | def copy_positions(self): 41 | positions = [] 42 | for i in range(len(self)): 43 | positions.append(copy.copy(self[i].p)) 44 | return positions 45 | 46 | def __repr__(self): 47 | ret = "Path vertices: %d (%d specular)\n\n" % (self.n_total, self.n_specular) 48 | for i in range(len(self)): 49 | vtx = self.vertices[i] 50 | 51 | p = np.array2string(vtx.p, formatter={'float_kind':lambda x: "%.2f" % x}) 52 | n = np.array2string(vtx.n, formatter={'float_kind':lambda x: "%.2f" % x}) 53 | dp_du = np.array2string(vtx.dp_du, formatter={'float_kind':lambda x: "%.2f" % x}) 54 | dn_du = np.array2string(vtx.dn_du, formatter={'float_kind':lambda x: "%.2f" % x}) 55 | bsdf = vtx.shape.type 56 | bsdf_str = "S" if bsdf == Shape.Type.Reflection or bsdf == Shape.Type.Refraction else "D" 57 | bsdf_str += ", " + vtx.shape.name 58 | ret += "%d (%s):\n\tp: %s, dp_du: %s\n\tn: %s, dn_du: %s\n" % (i, bsdf_str, p, dp_du, n, dn_du) 59 | 60 | return ret 61 | 62 | def has_specular_segment(self): 63 | K = len(self) 64 | if K < 3: 65 | return False 66 | 67 | test = True 68 | for k, vtx in enumerate(self.vertices): 69 | if k == 0 or k == K-1: 70 | test &= (vtx.shape.type == Shape.Type.Diffuse or 71 | vtx.shape.type == Shape.Type.Emitter) 72 | else: 73 | test &= (vtx.shape.type == Shape.Type.Reflection or 74 | vtx.shape.type == Shape.Type.Refraction) 75 | return test 76 | 77 | def grad_constraints_halfvector(self, k): 78 | C = np.inf 79 | dC = [0, 0, 0] 80 | 81 | x_prev = self.vertices[k-1].p 82 | x_cur = self.vertices[k].p 83 | x_next = self.vertices[k+1].p 84 | 85 | wo = x_next - x_cur 86 | wi = x_prev - x_cur 87 | ilo = norm(wo) 88 | ili = norm(wi) 89 | if ilo == 0.0 or ili == 0.0: 90 | return False, C, dC 91 | ilo = 1.0 / ilo 92 | ili = 1.0 / ili 93 | wo *= ilo 94 | wi *= ili 95 | 96 | eta = self.vertices[k].eta 97 | if dot(wi, self.vertices[k].n) < 0.0: 98 | eta = 1.0/eta 99 | h = wi + eta * wo 100 | if eta != 1.0: 101 | h *= -1 102 | ilh = 1.0 / norm(h) 103 | h *= ilh 104 | 105 | # For debugging only (see "print_derivative_debug") 106 | self.vertices[k].h = h 107 | 108 | ilo *= eta * ilh 109 | ili *= ilh 110 | 111 | # Derivative of specular constraint w.r.t. u_{i-1} 112 | dh_du = ili * (self.vertices[k-1].dp_du - wi*dot(wi, self.vertices[k-1].dp_du)) 113 | dh_du -= h*dot(dh_du, h) 114 | if eta != 1.0: 115 | dh_du *= -1 116 | dC_du_prev = dot(self.vertices[k].s, dh_du) 117 | self.vertices[k].dh_du_prev = dh_du 118 | 119 | # Derivative of specular constraint w.r.t. u_{i} 120 | dh_du = -self.vertices[k].dp_du * (ili + ilo) + wi * (dot(wi, self.vertices[k].dp_du) * ili) \ 121 | + wo * (dot(wo, self.vertices[k].dp_du) * ilo) 122 | dh_du -= h*dot(dh_du, h) 123 | if eta != 1.0: 124 | dh_du *= -1 125 | dC_du_cur = dot(self.vertices[k].ds_du, h) + dot(self.vertices[k].s, dh_du) 126 | self.vertices[k].dh_du_cur = dh_du 127 | 128 | # Derivative of specular constraint w.r.t. u_{i+1} 129 | dh_du = ilo * (self.vertices[k+1].dp_du - wo*dot(wo, self.vertices[k+1].dp_du)) 130 | dh_du -= h*dot(dh_du, h) 131 | if eta != 1.0: 132 | dh_du *= -1 133 | dC_du_next = dot(self.vertices[k].s, dh_du) 134 | self.vertices[k].dh_du_next = dh_du 135 | 136 | # Evaluation of specular constraint 137 | H = dot(self.vertices[k].s, h) 138 | m = self.vertices[k].s * self.vertices[k].n_offset[0] + self.vertices[k].n * self.vertices[k].n_offset[1] 139 | N = dot(self.vertices[k].s, m) 140 | C = H - N 141 | 142 | dC = [dC_du_prev, dC_du_cur, dC_du_next] 143 | return True, C, dC 144 | 145 | def grad_constraints_anglediff(self, k): 146 | C = np.inf 147 | dC = [0, 0, 0] 148 | 149 | x_prev = self.vertices[k-1].p 150 | x_cur = self.vertices[k].p 151 | x_next = self.vertices[k+1].p 152 | 153 | wo = x_next - x_cur 154 | wi = x_prev - x_cur 155 | ilo = norm(wo) 156 | ili = norm(wi) 157 | if ilo == 0.0 or ili == 0.0: 158 | return False, C, dC 159 | ilo = 1.0 / ilo 160 | ili = 1.0 / ili 161 | wo *= ilo 162 | wi *= ili 163 | 164 | dwo_du_cur = -ilo * (self.vertices[k].dp_du - wo*dot(wo, self.vertices[k].dp_du)) 165 | dwi_du_cur = -ili * (self.vertices[k].dp_du - wi*dot(wi, self.vertices[k].dp_du)) 166 | 167 | # For debugging only (see "print_derivative_debug") 168 | self.vertices[k].wo = wo 169 | self.vertices[k].wi = wi 170 | self.vertices[k].dwo_du_prev = np.array([0, 0]) 171 | self.vertices[k].dwo_du_cur = dwo_du_cur 172 | self.vertices[k].dwo_du_next = np.array([0, 0]) 173 | self.vertices[k].dwi_du_prev = np.array([0, 0]) 174 | self.vertices[k].dwi_du_cur = dwi_du_cur 175 | self.vertices[k].dwi_du_next = np.array([0, 0]) 176 | self.vertices[k].wio = np.array([0, 0]) 177 | self.vertices[k].woi = np.array([0, 0]) 178 | self.vertices[k].dwio_du_prev = np.array([0, 0]) 179 | self.vertices[k].dwio_du_cur = np.array([0, 0]) 180 | self.vertices[k].dwio_du_next = np.array([0, 0]) 181 | self.vertices[k].dwoi_du_prev = np.array([0, 0]) 182 | self.vertices[k].dwoi_du_cur = np.array([0, 0]) 183 | self.vertices[k].dwoi_du_next = np.array([0, 0]) 184 | 185 | self.vertices[k].po = 0 186 | self.vertices[k].pio = 0 187 | self.vertices[k].pi = 0 188 | self.vertices[k].poi = 0 189 | self.vertices[k].dpo_du_prev= 0 190 | self.vertices[k].dpio_du_prev = 0 191 | self.vertices[k].dpi_du_prev = 0 192 | self.vertices[k].dpoi_du_prev = 0 193 | self.vertices[k].dpo_du_cur = 0 194 | self.vertices[k].dpio_du_cur = 0 195 | self.vertices[k].dpi_du_cur = 0 196 | self.vertices[k].dpoi_du_cur = 0 197 | self.vertices[k].dpo_du_next = 0 198 | self.vertices[k].dpio_du_next = 0 199 | self.vertices[k].dpi_du_next = 0 200 | self.vertices[k].dpoi_du_next = 0 201 | 202 | def transform(w, n, eta): 203 | if eta == 1.0: 204 | return reflect(w, n) 205 | else: 206 | return refract(w, n, eta) 207 | 208 | def d_transform(w, dw_du, n, dn_du, eta): 209 | if eta == 1.0: 210 | return d_reflect(w, dw_du, n, dn_du) 211 | else: 212 | return d_refract(w, dw_du, n, dn_du, eta) 213 | 214 | # Handle offset normals. These lines are no-ops in the n_offset=[0, 1] case 215 | n = self.vertices[k].s * self.vertices[k].n_offset[0] + \ 216 | self.vertices[k].n * self.vertices[k].n_offset[1] 217 | dn_du = self.vertices[k].ds_du * self.vertices[k].n_offset[0] + \ 218 | self.vertices[k].dn_du * self.vertices[k].n_offset[1] 219 | 220 | dC_du_prev = 0 221 | dC_du_cur = 0 222 | dC_du_next = 0 223 | 224 | # Set up constraint function and its derivatives 225 | valid_refr_i, wio = transform(wi, n, self.vertices[k].eta) 226 | if valid_refr_i: 227 | po = angle(wo) 228 | pio = angle(wio) 229 | C = po - dpi 230 | # Take care of periodicity 231 | if C < -np.pi: 232 | C += 2*np.pi 233 | elif C > np.pi: 234 | C -= 2*np.pi 235 | 236 | # Derivative of specular constraint w.r.t. u_{i-1} 237 | dwi_du_prev = ili * (self.vertices[k-1].dp_du - wi*dot(wi, self.vertices[k-1].dp_du)) 238 | # dwo_du_prev = ilo * (self.vertices[k-1].dp_du - wo*dot(wo, self.vertices[k-1].dp_du)) # = 0 239 | dwio_du_prev = d_transform(wi, dwi_du_prev, n, np.array([0,0]), self.vertices[k].eta) # Possible optimization: specific implementation here that already knows some of these are 0. 240 | # dpo_du = d_angle(wo, dwo_du_prev) # = 0 241 | dpio_du = d_angle(wio, dwio_du_prev) 242 | 243 | dC_du_prev = -dpio_du 244 | self.vertices[k].dpio_du_prev = dpio_du 245 | 246 | # Derivative of specular constraint w.r.t. u_{i} 247 | dwio_du_cur = d_transform(wi, dwi_du_cur, n, dn_du, self.vertices[k].eta) 248 | dpo_du = d_angle(wo, dwo_du_cur) 249 | dpio_du = d_angle(wio, dwio_du_cur) 250 | 251 | dC_du_cur = dpo_du - dpio_du 252 | self.vertices[k].dpo_du_cur = dpo_du 253 | self.vertices[k].dpio_du_cur = dpio_du 254 | 255 | # Derivative of specular constraint w.r.t. u_{i+1} 256 | # dwi_du_next = ili * (selv.vertices[k+1].dp_du - wi*dot(wi, self.vertices[k+1].dp_du)) # = 0 257 | dwo_du_next = ilo * (self.vertices[k+1].dp_du - wo*dot(wo, self.vertices[k+1].dp_du)) 258 | # dwio_du_next = d_transform(wi, dwi_du_next, n, np.array([0,0]), self.vertices[k].eta) # = 0 259 | dpo_du = d_angle(wo, dwo_du_next) 260 | # dpio_du = d_angle(wio, dwio_du_next) # = 0 261 | 262 | dC_du_next = dpo_du 263 | self.vertices[k].dpo_du_next = dpo_du 264 | 265 | self.vertices[k].wio = wio 266 | self.vertices[k].dwi_du_prev = dwi_du_prev 267 | self.vertices[k].dwio_du_prev = dwio_du_prev 268 | self.vertices[k].dwio_du_cur = dwio_du_cur 269 | self.vertices[k].dwo_du_next = dwo_du_next 270 | 271 | self.vertices[k].po = po 272 | self.vertices[k].pio = pio 273 | 274 | valid_refr_o, woi = transform(wo, n, self.vertices[k].eta) 275 | if valid_refr_o and not valid_refr_i: 276 | pi = angle(wi) 277 | poi = angle(woi) 278 | C = pi - poi 279 | 280 | # Derivative of specular constraint w.r.t. u_{i-1} 281 | dwi_du_prev = ili * (self.vertices[k-1].dp_du - wi*dot(wi, self.vertices[k-1].dp_du)) 282 | # dwo_du_prev = ilo * (self.vertices[k-1].dp_du - wo*dot(wo, self.vertices[k-1].dp_du)) # = 0 283 | # dwoi_du_prev = d_transform(wo, dwo_du_prev, n, np.array([0,0]), self.vertices[k].eta) # = 0 284 | dpi_du = d_angle(wi, dwi_du_prev) 285 | # dpoi_du = d_angle(woi, dwoi_du_prev) # = 0 286 | 287 | dC_du_prev = dpi_du 288 | self.vertices[k].dpi_du_prev = dpi_du 289 | 290 | # Derivative of specular constraint w.r.t. u_{i} 291 | dwoi_du_cur = d_transform(wo, dwo_du_cur, n, dn_du, self.vertices[k].eta) 292 | dpi_du = d_angle(wi, dwi_du_cur) 293 | dpoi_du = d_angle(woi, dwoi_du_cur) 294 | 295 | dC_du_cur = dpi_du - dpoi_du 296 | self.vertices[k].dpi_du_cur = dpi_du 297 | self.vertices[k].dpoi_du_cur = dpoi_du 298 | 299 | # Derivative of specular constraint w.r.t. u_{i+1} 300 | # dwi_du_next = ili * (self.vertices[k+1].dp_du - wi*dot(wi, self.vertices[k+1].dp_du)) # = 0 301 | dwo_du_next = ilo * (self.vertices[k+1].dp_du - wo*dot(wo, self.vertices[k+1].dp_du)) 302 | dwoi_du_next = d_transform(wo, dwo_du_next, n, np.array([0,0]), self.vertices[k].eta); # Possible optimization: specific implementation here that already knows some of these are 0. 303 | 304 | # dpi_du = d_angle(wi, dwi_du_next) # = 0 305 | dpoi_du = d_angle(woi, dwoi_du_next) 306 | 307 | dC_du_next = -dpoi_du 308 | self.vertices[k].dpoi_du_next = dpoi_du 309 | 310 | self.vertices[k].woi = woi 311 | self.vertices[k].dwi_du_prev = dwi_du_prev 312 | self.vertices[k].dwoi_du_cur = dwoi_du_cur 313 | self.vertices[k].dwo_du_next = dwo_du_next 314 | self.vertices[k].dwoi_du_next = dwoi_du_next 315 | 316 | dC = [dC_du_prev, dC_du_cur, dC_du_next] 317 | return (valid_refr_i or valid_refr_o), C, dC 318 | 319 | def compute_tangent_derivatives(self, constraint_type): 320 | self.gradC = None 321 | if not self.has_specular_segment(): 322 | return 323 | 324 | gradC = np.zeros((self.n_specular, self.n_total)) 325 | vC = np.zeros(self.n_specular) 326 | 327 | for i in range(self.n_specular): 328 | idx = i+1 329 | if constraint_type == ConstraintType.HalfVector: 330 | success, C, dC = self.grad_constraints_halfvector(idx) 331 | else: 332 | success, C, dC = self.grad_constraints_anglediff(idx) 333 | 334 | self.vertices[idx].C = C 335 | du_prev, du_cur, du_next = dC 336 | self.vertices[idx].dC_du_prev = du_prev 337 | self.vertices[idx].dC_du_cur = du_cur 338 | self.vertices[idx].dC_du_next = du_next 339 | 340 | if not success: 341 | self.singular = True 342 | return 343 | 344 | gradC[i, idx-1] = du_prev 345 | gradC[i, idx] = du_cur 346 | gradC[i, idx+1] = du_next 347 | vC[i] = C 348 | 349 | A = gradC[:,1:-1] 350 | B1 = gradC[:,0] 351 | Bk = gradC[:,-1] 352 | 353 | self.singular = not np.abs(np.linalg.det(A)) > 0 354 | if self.singular: 355 | return 356 | 357 | self.gradC = gradC 358 | 359 | Ainv = np.linalg.inv(A) 360 | T1 = -Ainv @ B1 361 | Tk = -Ainv @ Bk 362 | Tx = Ainv @ vC 363 | 364 | for i in range(self.n_specular): 365 | idx = i+1 366 | self.vertices[idx].dC_du1 = T1[i] 367 | self.vertices[idx].dC_duk = Tk[i] 368 | self.vertices[idx].dX = Tx[i] 369 | 370 | def same_submanifold(self, other): 371 | if len(self) != len(other): 372 | return False 373 | for i in range(len(self)): 374 | if self.vertices[i].shape.id != other.vertices[i].shape.id: 375 | return False 376 | return True 377 | 378 | def print_derivative_debug(self, c_idx, c_type, eps=1e-5): 379 | path = self.copy() 380 | path_du_prev = self.copy() 381 | path_du_cur = self.copy() 382 | path_du_next = self.copy() 383 | 384 | path.compute_tangent_derivatives(c_type) 385 | 386 | print("-----") 387 | 388 | path_du_prev[c_idx-1] = path_du_prev[c_idx-1].shape.sample_position(path_du_prev[c_idx-1].u + eps) 389 | path_du_prev[c_idx-1].eta = path_du_prev[c_idx-1].shape.eta 390 | du = path_du_prev[c_idx-1].u - path[c_idx-1].u 391 | path_du_prev.compute_tangent_derivatives(c_type) 392 | 393 | print("d_prev:") 394 | if c_type == ConstraintType.HalfVector: 395 | dh_du_fd = (path_du_prev[c_idx].h - path[c_idx].h) / du 396 | print("dh_du: ", path[c_idx].dh_du_prev, " vs. ", dh_du_fd) 397 | dC_du_fd = (path_du_prev[c_idx].C - path[c_idx].C) / du 398 | print("dC_du: ", path[c_idx].dC_du_prev, " vs. ", dC_du_fd) 399 | print("") 400 | 401 | path_du_cur[c_idx] = path_du_cur[c_idx].shape.sample_position(path_du_cur[c_idx].u + eps) 402 | path_du_cur[c_idx].eta = path_du_cur[c_idx].shape.eta 403 | du = path_du_cur[c_idx].u - path[c_idx].u 404 | path_du_cur.compute_tangent_derivatives(c_type) 405 | 406 | print("d_cur:") 407 | dp_du_fd = (path_du_cur[c_idx].p - path[c_idx].p) / du 408 | print("dp_du: ", path[c_idx].dp_du, " vs. ", dp_du_fd) 409 | dn_du_fd = (path_du_cur[c_idx].n - path[c_idx].n) / du 410 | print("dn_du: ", path[c_idx].dn_du, " vs. ", dn_du_fd) 411 | ds_du_fd = (path_du_cur[c_idx].s - path[c_idx].s) / du 412 | print("ds_du: ", path[c_idx].ds_du, " vs. ", ds_du_fd) 413 | if c_type == ConstraintType.HalfVector: 414 | dh_du_fd = (path_du_cur[c_idx].h - path[c_idx].h) / du 415 | print("dh_du: ", path[c_idx].dh_du_cur, " vs. ", dh_du_fd) 416 | else: 417 | dwo_du_fd = (path_du_cur[c_idx].wo - path[c_idx].wo) / du 418 | print("dwo_du: ", path[c_idx].dwo_du_cur, " vs. ", dwo_du_fd) 419 | dwi_du_fd = (path_du_cur[c_idx].wi - path[c_idx].wi) / du 420 | print("dwi_du: ", path[c_idx].dwi_du_cur, " vs. ", dwi_du_fd) 421 | dwio_du_fd = (path_du_cur[c_idx].wio - path[c_idx].wio) / du 422 | print("dwio_du: ", path[c_idx].dwio_du_cur, " vs. ", dwio_du_fd) 423 | dwoi_du_fd = (path_du_cur[c_idx].woi - path[c_idx].woi) / du 424 | print("dwoi_du: ", path[c_idx].dwoi_du_cur, " vs. ", dwoi_du_fd) 425 | 426 | dpo_du_fd = (path_du_cur[c_idx].po - path[c_idx].po) / du 427 | print("dpo_du: ", path[c_idx].dpo_du_cur, " vs. ", dpo_du_fd) 428 | dpi_du_fd = (path_du_cur[c_idx].pi - path[c_idx].pi) / du 429 | print("dpi_du: ", path[c_idx].dpi_du_cur, " vs. ", dpi_du_fd) 430 | dpio_du_fd = (path_du_cur[c_idx].pio - path[c_idx].pio) / du 431 | print("dpio_du: ", path[c_idx].dpio_du_cur, " vs. ", dpio_du_fd) 432 | dpoi_du_fd = (path_du_cur[c_idx].poi - path[c_idx].poi) / du 433 | print("dpoi_du: ", path[c_idx].dpoi_du_cur, " vs. ", dpoi_du_fd) 434 | dC_du_fd = (path_du_cur[c_idx].C - path[c_idx].C) / du 435 | print("dC_du: ", path[c_idx].dC_du_cur, " vs. ", dC_du_fd) 436 | print("") 437 | 438 | path_du_next[c_idx+1] = path_du_next[c_idx+1].shape.sample_position(path_du_next[c_idx+1].u + eps) 439 | path_du_next[c_idx+1].eta = path_du_next[c_idx+1].shape.eta 440 | du = path_du_next[c_idx+1].u - path[c_idx+1].u 441 | path_du_next.compute_tangent_derivatives(c_type) 442 | 443 | print("d_next:") 444 | if c_type == ConstraintType.HalfVector: 445 | dh_du_fd = (path_du_next[c_idx].h - path[c_idx].h) / du 446 | print("dh_du: ", path[c_idx].dh_du_next, " vs. ", dh_du_fd) 447 | dC_du_fd = (path_du_next[c_idx].C - path[c_idx].C) / du 448 | print("dC_du: ", path[c_idx].dC_du_next, " vs. ", dC_du_fd) 449 | print("") 450 | 451 | print("-----") 452 | -------------------------------------------------------------------------------- /python/scene.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import manifolds 3 | from manifolds import Scene as CppScene 4 | from manifolds import Ray2f, Shape 5 | from misc import * 6 | from path import Path 7 | from draw import * 8 | 9 | class Scene: 10 | def __init__(self, shapes): 11 | self.shapes = shapes 12 | self.offset = [0, 0] 13 | self.zoom = 1.0 14 | self.scale = 1.0 15 | 16 | self.start_u_default = 0 17 | self.start_u_current = 0 18 | self.start_angle_default = 0 19 | self.start_angle_current = 0 20 | self.end_u_default = 0 21 | self.end_u_current = 0 22 | self.spec_u_default = 0 23 | self.spec_u_current = 0 24 | self.n_bounces_default = 1 25 | 26 | self.cpp_scene = CppScene() 27 | 28 | shape_id = 0 29 | for s in shapes: 30 | s.id = shape_id 31 | self.cpp_scene.add_shape(s) 32 | shape_id += 1 33 | 34 | def set_start(self, start_u, start_angle, end_u=0.5, spec_u=0.5): 35 | self.start_u_default = start_u 36 | self.start_angle_default = start_angle 37 | self.end_u_default = end_u 38 | self.spec_u_default = spec_u 39 | 40 | self.start_u_current = start_u 41 | self.start_angle_current = start_angle 42 | self.end_u_current = end_u 43 | self.spec_u_current = spec_u 44 | 45 | def start_shape(self): 46 | return self.cpp_scene.start_shape() 47 | 48 | def end_shape(self): 49 | return self.cpp_scene.end_shape() 50 | 51 | def first_specular_shape(self): 52 | return self.cpp_scene.first_specular_shape() 53 | 54 | def draw(self, ctx): 55 | for shape in self.shapes: 56 | if shape.type == Shape.Type.Emitter: 57 | for t in np.linspace(0, 1, 10): 58 | it = shape.sample_position(t) 59 | draw_arrow(ctx, it.p, it.n, nvg.RGB(255, 255, 180), scale=0.5, length=0.03) 60 | 61 | self.cpp_scene.draw(ctx) 62 | 63 | def ray_intersect(self, ray): 64 | # Trace ray against C++ representation 65 | it = self.cpp_scene.ray_intersect(ray) 66 | 67 | if it.is_valid(): 68 | # We now need to track the relative IOR change at this interaction 69 | wi = -ray.d 70 | it.eta = it.shape.eta 71 | it.n_offset = np.array([0, 1]) 72 | 73 | return it 74 | 75 | def sample_start_position(self, u): 76 | it = self.cpp_scene.start_shape().sample_position(u) 77 | it.eta = it.shape.eta 78 | it.n_offset = np.array([0, 1]) 79 | return it 80 | 81 | def sample_end_position(self, u): 82 | it = self.cpp_scene.end_shape().sample_position(u) 83 | it.eta = it.shape.eta 84 | it.n_offset = np.array([0, 1]) 85 | return it 86 | 87 | def sample_spec_position(self, u): 88 | it = self.cpp_scene.first_specular_shape().sample_position(u) 89 | it.eta = it.shape.eta 90 | it.n_offset = np.array([0, 1]) 91 | return it 92 | 93 | def sample_path(self): 94 | it = self.sample_start_position(self.start_u_current) 95 | 96 | path = Path() 97 | path.append(it) 98 | 99 | theta = np.radians(self.start_angle_current) 100 | wo = [np.cos(theta), np.sin(theta)] 101 | if wo @ it.n < 0.0: 102 | return path 103 | 104 | while True: 105 | ray = Ray2f(it.p, wo) 106 | 107 | it = self.ray_intersect(ray) 108 | wi = -ray.d 109 | 110 | path.append(it) 111 | 112 | m = it.s * it.n_offset[0] + it.n * it.n_offset[1] 113 | if it.shape.type == Shape.Type.Reflection: 114 | if wi @ it.n < 0: 115 | break 116 | wo = reflect(wi, m) 117 | elif it.shape.type == Shape.Type.Refraction: 118 | wo = refract(wi, m, it.shape.eta) 119 | else: 120 | break 121 | 122 | if not wo[0]: 123 | break 124 | wo = wo[1] 125 | 126 | return path 127 | 128 | def sample_seed_path(self, n_spec_bounces=1): 129 | path = Path() 130 | 131 | it1 = self.sample_start_position(self.start_u_current) 132 | it2 = self.sample_spec_position(self.spec_u_current) 133 | wo = normalize(it2.p - it1.p) 134 | if wo @ it1.n < 0.0: 135 | return path 136 | 137 | ray = Ray2f(it1.p, wo) 138 | it = self.ray_intersect(ray) 139 | if not it.is_valid() or (it.shape != it2.shape): 140 | return path 141 | it2 = it 142 | 143 | path.append(it1) 144 | path.append(it) 145 | 146 | while True: 147 | wi = -wo 148 | if len(path) - 1 >= n_spec_bounces: 149 | break 150 | 151 | m = it.s * it.n_offset[0] + it.n * it.n_offset[1] 152 | if it.shape.type == Shape.Type.Reflection: 153 | if wi @ it.n < 0: 154 | break 155 | wo = reflect(wi, m) 156 | elif it.shape.type == Shape.Type.Refraction: 157 | wo = refract(wi, m, it.shape.eta) 158 | else: 159 | print("Should not happen!!") 160 | break 161 | 162 | if not wo[0]: 163 | break 164 | wo = wo[1] 165 | 166 | ray = Ray2f(it.p, wo) 167 | it = self.ray_intersect(ray) 168 | if not (it.shape.type == Shape.Type.Reflection or it.shape.type == Shape.Type.Refraction): 169 | break 170 | 171 | path.append(it) 172 | 173 | it3 = self.sample_end_position(self.end_u_current) 174 | path.append(it3) 175 | 176 | if len(path) != n_spec_bounces + 2: 177 | return Path() 178 | 179 | return path 180 | 181 | def sample_mnee_seed_path(self): 182 | path = Path() 183 | 184 | it1 = self.sample_start_position(self.start_u_current) 185 | it3 = self.sample_end_position(self.end_u_current) 186 | 187 | wo = normalize(it3.p - it1.p) 188 | if wo @ it1.n < 0.0: 189 | return path 190 | path.append(it1) 191 | 192 | it = it1 193 | while True: 194 | ray = Ray2f(it.p, wo) 195 | 196 | it = self.ray_intersect(ray) 197 | 198 | if it.shape.type == Shape.Type.Reflection: 199 | break 200 | elif it.shape.type == Shape.Type.Refraction: 201 | pass 202 | else: 203 | break 204 | 205 | path.append(it) 206 | 207 | it3 = self.sample_end_position(self.end_u_current) 208 | path.append(it3) 209 | 210 | return path 211 | 212 | def reproject_path_me(self, offset_vertices): 213 | path = Path() 214 | 215 | p0 = offset_vertices[0] 216 | t0 = self.cpp_scene.start_shape().project(p0) 217 | it = self.cpp_scene.start_shape().sample_position(t0) 218 | path.append(it) 219 | 220 | p1 = offset_vertices[1] 221 | wo = normalize(p1 - p0) 222 | 223 | while True: 224 | ray = Ray2f(it.p, wo) 225 | 226 | it = self.ray_intersect(ray) 227 | wi = -ray.d 228 | 229 | path.append(it) 230 | 231 | m = it.s * it.n_offset[0] + it.n * it.n_offset[1] 232 | if it.shape.type == Shape.Type.Reflection: 233 | if wi @ it.n < 0: 234 | break 235 | wo = reflect(wi, m) 236 | elif it.shape.type == Shape.Type.Refraction: 237 | wo = refract(wi, m, it.shape.eta) 238 | else: 239 | break 240 | 241 | if not wo[0]: 242 | break 243 | wo = wo[1] 244 | 245 | return path 246 | 247 | def reproject_path_sms(self, offset_vertices, previous_path, n_spec_bounces=1): 248 | path = Path() 249 | 250 | p1 = offset_vertices[0] 251 | t1 = self.cpp_scene.start_shape().project(p1) 252 | it1 = self.cpp_scene.start_shape().sample_position(t1) 253 | 254 | p2 = offset_vertices[1] 255 | wo = normalize(p2 - p1) 256 | if wo @ it1.n < 0.0: 257 | return path 258 | 259 | ray = Ray2f(p1, wo) 260 | it2 = self.ray_intersect(ray) 261 | if it2.shape.id != self.cpp_scene.first_specular_shape().id: 262 | return path 263 | it2.n_offset = previous_path.vertices[1].n_offset 264 | 265 | path.append(it1) 266 | path.append(it2) 267 | 268 | it = it2 269 | while True: 270 | wi = -wo 271 | if len(path) - 1 >= n_spec_bounces: 272 | break 273 | 274 | m = it.s * it.n_offset[0] + it.n * it.n_offset[1] 275 | if it.shape.type == Shape.Type.Reflection: 276 | if wi @ it.n < 0: 277 | break 278 | wo = reflect(wi, m) 279 | elif it.shape.type == Shape.Type.Refraction: 280 | wo = refract(wi, m, it.shape.eta) 281 | else: 282 | print("Should not happen!!") 283 | break 284 | 285 | if not wo[0]: 286 | break 287 | wo = wo[1] 288 | 289 | ray = Ray2f(it.p, wo) 290 | it = self.ray_intersect(ray) 291 | if not (it.shape.type == Shape.Type.Reflection or it.shape.type == Shape.Type.Refraction): 292 | break 293 | if len(path) > len(previous_path): 294 | break 295 | 296 | it.n_offset = previous_path.vertices[len(path)].n_offset 297 | path.append(it) 298 | 299 | it3 = self.sample_end_position(self.end_u_current) 300 | path.append(it3) 301 | 302 | if len(path) != n_spec_bounces + 2: 303 | return Path() 304 | 305 | return path -------------------------------------------------------------------------------- /python/scenes.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scene import Scene 3 | from manifolds import BezierCurve, Circle, ConcaveSegment, ConvexSegment, LinearSegment, Shape 4 | from nanogui import nanovg as nvg 5 | from bezier_shapes import * 6 | 7 | def create_scenes(): 8 | scenes = [] 9 | 10 | 11 | # SIMPLE REFLECTION 12 | 13 | s1 = LinearSegment([0.7, 1.5], [-0.7, 1.5]) 14 | s1.type = Shape.Type.Diffuse 15 | s1.end = True 16 | s1.start = True 17 | 18 | s3 = ConvexSegment([-0.6, 0.5], [0.6, 0.5], 2.0, False) 19 | s3.type = Shape.Type.Reflection 20 | s3.first_specular = True 21 | 22 | 23 | bounds = Circle([0, 0], 100.0) 24 | bounds.type = Shape.Type.Null 25 | bounds.visible = False 26 | 27 | scene = Scene([s1, s3, bounds]) 28 | scene.name = "Simple reflection" 29 | scene.set_start(0.1, -121.65789, 0.9, 0.5) 30 | scene.offset = [0, -0.75] 31 | scene.zoom = 0.93 32 | scenes.append(scene) 33 | 34 | 35 | # SIMPLE REFRACTION 36 | 37 | s1 = LinearSegment([-0.7, 0], [0.7, 0]) 38 | s1.type = Shape.Type.Diffuse 39 | s1.end = True 40 | 41 | s2 = LinearSegment([0.7, 1.5], [-0.7, 1.5]) 42 | s2.type = Shape.Type.Diffuse 43 | s2.start = True 44 | 45 | s3 = ConvexSegment([-0.6, 0.5], [0.6, 0.5], 2.0, False) 46 | s3.type = Shape.Type.Refraction 47 | s3.eta = 1.5 48 | s3.first_specular = True 49 | s3.gradient_start = 0.4 50 | 51 | bounds = Circle([0, 0], 100.0) 52 | bounds.type = Shape.Type.Null 53 | bounds.visible = False 54 | 55 | scene = Scene([s2, s3, s1, bounds]) 56 | scene.name = "Simple refraction" 57 | scene.set_start(0.1, -121.429, 0.3429, 0.5) 58 | scene.offset = [0, -0.75] 59 | scene.zoom = 0.93 60 | scenes.append(scene) 61 | 62 | 63 | # CONCAVE REFLECTOR 64 | 65 | s1 = LinearSegment([-1, 0], [-0.1, 0]) 66 | s1.type = Shape.Type.Diffuse 67 | s1.start = True 68 | 69 | s2 = LinearSegment([0.1, 0], [1, 0]) 70 | s2.type = Shape.Type.Diffuse 71 | s2.end = True 72 | 73 | s3 = ConcaveSegment([0.45, 1], [-0.45, 1], 2.0) 74 | s3.type = Shape.Type.Reflection 75 | s3.first_specular = True 76 | 77 | bounds = Circle([0, 0], 100.0) 78 | bounds.type = Shape.Type.Null 79 | bounds.visible = False 80 | 81 | scene = Scene([s1, s2, s3, bounds]) 82 | scene.name = "Concave reflector" 83 | scene.set_start(0.47, 60.54, end_u=0.53, spec_u=-13.34) 84 | scene.offset = [0, -0.75] 85 | scene.zoom = 0.93 86 | scenes.append(scene) 87 | 88 | 89 | # REFLECTIVE SPHERE 90 | 91 | s1 = LinearSegment([-1, 0], [-0.1, 0]) 92 | s1.type = Shape.Type.Diffuse 93 | s1.start = True 94 | 95 | s2 = LinearSegment([0.1, 0], [1, 0]) 96 | s2.type = Shape.Type.Diffuse 97 | s2.end = True 98 | 99 | s3 = Circle([0.0, 1.5], 0.7) 100 | s3.type = Shape.Type.Reflection 101 | s3.first_specular = True 102 | 103 | bounds = Circle([0, 0], 100.0) 104 | bounds.type = Shape.Type.Null 105 | bounds.visible = False 106 | 107 | scene = Scene([s1, s2, s3, bounds]) 108 | scene.name = "Reflective sphere" 109 | scene.set_start(0.43, 52.36, spec_u=0.75) 110 | scene.offset = [0.13, -0.8] 111 | scene.zoom = 0.75 112 | scenes.append(scene) 113 | 114 | 115 | # MNEE / REFRACTIVE SPHERE 116 | 117 | s1 = LinearSegment([-1.0, 0], [1.0, 0]) 118 | s1.type = Shape.Type.Diffuse 119 | s1.end = True 120 | 121 | s2 = Circle([0.0, 0.0], 1.0) 122 | s2 = ConvexSegment([-0.9, 0], [0.9, 0], 0.9, True) 123 | s2.type = Shape.Type.Refraction 124 | s2.eta = 2.0 125 | s2.first_specular = True 126 | 127 | s3 = LinearSegment([1.15, 1.5], [0.95, 1.8]) 128 | s3.type = Shape.Type.Diffuse 129 | s3.start = True 130 | s3.gradient_start = 0.08 131 | 132 | bounds = Circle([0, 0], 100.0) 133 | bounds.type = Shape.Type.Null 134 | bounds.visible = False 135 | 136 | scene = Scene([s2, s1, s3, bounds]) 137 | scene.name = "Refractive sphere / MNEE" 138 | scene.set_start(0.48, -143.97, 0.5, 0.5) 139 | scene.offset = [0, -1] 140 | scene.zoom = 0.65 141 | scenes.append(scene) 142 | 143 | 144 | # TWO BOUNCE SPHERE 145 | 146 | s1 = LinearSegment([-0.5, 0], [0.5, 0]) 147 | s1.type = Shape.Type.Diffuse 148 | s1.start = True 149 | 150 | s2 = Circle([0.0, 1.0], 0.3) 151 | s2.type = Shape.Type.Refraction 152 | s2.eta = 1.5 153 | s2.first_specular = True 154 | 155 | s3 = LinearSegment([0.5, 1.8], [-0.5, 1.8]) 156 | s3.type = Shape.Type.Diffuse 157 | s3.end = True 158 | 159 | bounds = Circle([0, 0], 100.0) 160 | bounds.type = Shape.Type.Null 161 | bounds.visible = False 162 | 163 | scene = Scene([s1, s2, s3, bounds]) 164 | scene.name = "Two-bounce sphere" 165 | scene.set_start(0.675, 87.7394, 0.7552, 0.5) 166 | scene.offset = [0, -1] 167 | scene.zoom = 0.75 168 | scene.n_bounces_default = 2 169 | scenes.append(scene) 170 | 171 | 172 | # WAVY REFLECTION 173 | 174 | s1 = LinearSegment([-0.7, 0], [0.7, 0]) 175 | s1.type = Shape.Type.Diffuse 176 | 177 | s2 = LinearSegment([0.7, 1.5], [-0.7, 1.5]) 178 | s2.type = Shape.Type.Diffuse 179 | s2.start = True 180 | s2.end = True 181 | 182 | plane_x = pts_wavy_plane[0] 183 | plane_y = pts_wavy_plane[1] 184 | plane_x *= 0.7; 185 | plane_y *= 0.7; 186 | plane_y += 0.08; 187 | 188 | s3 = BezierCurve(list(plane_x.flatten()), list(plane_y.flatten())) 189 | s3.type = Shape.Type.Reflection 190 | s3.first_specular = True 191 | 192 | bounds = Circle([0, 0], 100.0) 193 | bounds.type = Shape.Type.Null 194 | bounds.visible = False 195 | 196 | scene = Scene([s2, s3, s1, bounds]) 197 | scene.name = "Wavy reflection" 198 | scene.set_start(0.83, -76.76, 0.2, 0.5) 199 | scene.offset = [0, -0.75] 200 | scene.zoom = 0.93 201 | scenes.append(scene) 202 | 203 | 204 | # WAVY REFRACTION / POOL 205 | 206 | s1 = LinearSegment([-0.7, 0], [0.7, 0]) 207 | s1.type = Shape.Type.Diffuse 208 | s1.end = True 209 | 210 | s2 = LinearSegment([0.7, 1.5], [-0.7, 1.5]) 211 | s2.type = Shape.Type.Diffuse 212 | s2.start = True 213 | 214 | scale = 0.6 215 | offset = 0.2 216 | pool_x = pts_pool[0] 217 | pool_y = pts_pool[1] 218 | pool_x *= scale; pool_y *= scale 219 | pool_y += offset 220 | 221 | s3 = BezierCurve(list(pool_x.flatten()), list(pool_y.flatten())) 222 | s3.type = Shape.Type.Refraction 223 | s3.eta = 1.33 224 | s3.first_specular = True 225 | 226 | s4 = LinearSegment([-0.6, 0.5], [-0.55, 0.5]) 227 | s4.type = Shape.Type.Diffuse 228 | s4.gradient_start = 9.31 229 | s4.gradient_width = 0.08 230 | s4.height = 10.0 231 | 232 | s5 = LinearSegment([0.55, 0.5], [0.6, 0.5]) 233 | s5.type = Shape.Type.Diffuse 234 | s5.gradient_start = 9.31 235 | s5.gradient_width = 0.08 236 | s5.height = 10.0 237 | 238 | bounds = Circle([0, 0], 100.0) 239 | bounds.type = Shape.Type.Null 240 | bounds.visible = False 241 | 242 | scene = Scene([s3, s1, s2, s4, s5, bounds]) 243 | scene.name = "Wavy refraction / pool" 244 | scene.set_start(0.1, -121.429, 0.3849, 0.5) 245 | scene.offset = [0, -0.75] 246 | scene.zoom = 0.93 247 | scenes.append(scene) 248 | 249 | 250 | # SPHERE (MANY BOUNCES) 251 | 252 | s1 = LinearSegment([-0.8, 0], [0.8, 0]) 253 | s1.type = Shape.Type.Diffuse 254 | s1.start = True 255 | s1.end = True 256 | 257 | s2 = Circle([0.0, 1.0], 0.5) 258 | s2.type = Shape.Type.Refraction 259 | s2.eta = 1.5 260 | s2.first_specular = True 261 | 262 | s3 = LinearSegment([0.8, 2], [-0.8, 2]) 263 | s3.type = Shape.Type.Reflection 264 | 265 | bounds = Circle([0, 0], 100.0) 266 | bounds.type = Shape.Type.Null 267 | bounds.visible = False 268 | 269 | scene = Scene([s1, s2, s3, bounds]) 270 | scene.name = "Sphere (many bounces)" 271 | scene.set_start(0.41, 100.61, spec_u=0.6956925392150879) 272 | scene.offset = [0, -1] 273 | scene.zoom = 0.75 274 | scene.n_bounces_default = 5 275 | scenes.append(scene) 276 | 277 | 278 | # BUNNY 279 | 280 | s1 = LinearSegment([-0.8, 0], [0.8, 0]) 281 | s1.type = Shape.Type.Diffuse 282 | s1.start = True 283 | s1.end = True 284 | 285 | bunny_x = pts_bunny[0]; bunny_x *= 0.6 286 | bunny_y = pts_bunny[1]; bunny_y *= 0.6; bunny_y += 1.0 287 | s2 = BezierCurve(list(bunny_x.flatten()), list(bunny_y.flatten())) 288 | s2.type = Shape.Type.Refraction 289 | s2.eta = 1.5 290 | s2.first_specular = True 291 | 292 | s3 = LinearSegment([0.8, 2], [-0.8, 2]) 293 | s3.type = Shape.Type.Reflection 294 | 295 | bounds = Circle([0, 0], 100.0) 296 | bounds.type = Shape.Type.Null 297 | bounds.visible = False 298 | 299 | scene = Scene([s1, s2, s3, bounds]) 300 | scene.name = "Bunny" 301 | scene.set_start(0.644, 85.17827, end_u=0.39, spec_u=0.4038328230381012) 302 | scene.offset = [0, -1] 303 | scene.zoom = 0.75 304 | scene.n_bounces_default = 7 305 | scenes.append(scene) 306 | 307 | 308 | # DRAGON 309 | 310 | s1 = LinearSegment([-0.7, 0], [0.7, 0]) 311 | s1.type = Shape.Type.Diffuse 312 | 313 | s2 = LinearSegment([0.7, 1.5], [-0.7, 1.5]) 314 | s2.type = Shape.Type.Diffuse 315 | s2.start = True 316 | s2.end = True 317 | 318 | dragon_x = pts_dragon[0] 319 | dragon_y = pts_dragon[1] 320 | dragon_hole_x = pts_dragon_hole[0] 321 | dragon_hole_y = pts_dragon_hole[1] 322 | dragon_x *= 0.7; dragon_hole_x *= 0.7 323 | dragon_y *= 0.7; dragon_hole_y *= 0.7 324 | dragon_y += 0.48; dragon_hole_y += 0.48 325 | 326 | 327 | s3 = BezierCurve(list(dragon_x.flatten()), list(dragon_y.flatten())) 328 | s3.type = Shape.Type.Reflection 329 | s3.first_specular = True 330 | s3.name = "dragon" 331 | 332 | s_hole = BezierCurve(list(dragon_hole_x.flatten()), list(dragon_hole_y.flatten())) 333 | s_hole.type = Shape.Type.Reflection 334 | # s_hole.flip() 335 | s_hole.hole = True 336 | s_hole.parent = "dragon" 337 | 338 | bounds = Circle([0, 0], 100.0) 339 | bounds.type = Shape.Type.Null 340 | bounds.visible = False 341 | 342 | scene = Scene([s2, s3, s1, s_hole, bounds]) 343 | scene.name = "Dragon" 344 | scene.set_start(0.104, -135.0, 0.832, 0.5) 345 | scene.offset = [0, -0.75] 346 | scene.zoom = 0.93 347 | scenes.append(scene) 348 | 349 | 350 | # SIGGRAPH LOGO 351 | 352 | s1 = LinearSegment([-0.7, 0], [0.7, 0]) 353 | s1.type = Shape.Type.Diffuse 354 | s1.end = True 355 | 356 | s2 = LinearSegment([0.7, 1.5], [-0.7, 1.5]) 357 | s2.type = Shape.Type.Diffuse 358 | s2.start = True 359 | 360 | siggraph_1_x = pts_siggraph_1[0] 361 | siggraph_1_y = pts_siggraph_1[1] 362 | siggraph_2_x = pts_siggraph_2[0] 363 | siggraph_2_y = pts_siggraph_2[1] 364 | siggraph_3_x = pts_siggraph_3[0] 365 | siggraph_3_y = pts_siggraph_3[1] 366 | siggraph_4_x = pts_siggraph_4[0] 367 | siggraph_4_y = pts_siggraph_4[1] 368 | mitsuba_x = pts_mitsuba[0] 369 | mitsuba_y = pts_mitsuba[1] 370 | 371 | scale = 0.65 372 | offset = 0.7 373 | siggraph_1_x *= scale; siggraph_2_x *= scale; siggraph_3_x *= scale; siggraph_4_x *= scale; 374 | siggraph_1_y *= scale; siggraph_2_y *= scale; siggraph_3_y *= scale; siggraph_4_y *= scale; 375 | siggraph_1_y += offset; siggraph_2_y += offset; siggraph_3_y += offset; siggraph_4_y += offset; 376 | mitsuba_x *= scale; mitsuba_y *= scale 377 | mitsuba_y += offset 378 | 379 | s3 = BezierCurve(list(siggraph_1_x.flatten()), list(siggraph_1_y.flatten())) 380 | s3.type = Shape.Type.Refraction 381 | s3.eta = 1.5 382 | s3.first_specular = True 383 | 384 | s4 = BezierCurve(list(siggraph_2_x.flatten()), list(siggraph_2_y.flatten())) 385 | s4.type = Shape.Type.Refraction 386 | s4.eta = 1.5 387 | 388 | s5 = BezierCurve(list(siggraph_3_x.flatten()), list(siggraph_3_y.flatten())) 389 | s5.type = Shape.Type.Refraction 390 | s5.eta = 1.5 391 | 392 | s6 = BezierCurve(list(siggraph_4_x.flatten()), list(siggraph_4_y.flatten())) 393 | s6.type = Shape.Type.Refraction 394 | s6.eta = 1.5 395 | 396 | s7 = BezierCurve(list(mitsuba_x.flatten()), list(mitsuba_y.flatten())) 397 | s7.type = Shape.Type.Refraction 398 | s7.eta = 1.5 399 | 400 | bounds = Circle([0, 0], 100.0) 401 | bounds.type = Shape.Type.Null 402 | bounds.visible = False 403 | 404 | scene = Scene([s1, s2, s3, s4, s5, bounds]) 405 | scene.name = "SIGGRAPH logo" 406 | scene.set_start(0.38, -85.24, 0.57, 0.89) 407 | scene.offset = [0, -0.75] 408 | scene.zoom = 0.93 409 | scene.n_bounces_default = 6 410 | scenes.append(scene) 411 | 412 | 413 | # MITSUBA LOGO 414 | 415 | s1 = LinearSegment([-0.7, 0], [0.7, 0]) 416 | s1.type = Shape.Type.Diffuse 417 | 418 | s2 = LinearSegment([0.7, 1.5], [-0.7, 1.5]) 419 | s2.type = Shape.Type.Diffuse 420 | s2.start = True 421 | s2.end = True 422 | 423 | mitsuba_x = pts_mitsuba[0] 424 | mitsuba_y = pts_mitsuba[1] 425 | mitsuba_x *= 0.7 426 | mitsuba_y *= 0.7 427 | 428 | s3 = BezierCurve(list(mitsuba_x.flatten()), list(mitsuba_y.flatten())) 429 | s3.type = Shape.Type.Reflection 430 | s3.first_specular = True 431 | 432 | bounds = Circle([0, 0], 100.0) 433 | bounds.type = Shape.Type.Null 434 | bounds.visible = False 435 | 436 | scene = Scene([s2, s3, s1, s_hole, bounds]) 437 | scene.name = "Mitsuba logo" 438 | scene.set_start(0.347, -109.09, 0.832, 0.5) 439 | scene.offset = [0, -0.75] 440 | scene.zoom = 0.93 441 | scenes.append(scene) 442 | 443 | 444 | # TEAPOT 445 | 446 | s1 = LinearSegment([-0.7, 0], [0.7, 0]) 447 | s1.type = Shape.Type.Diffuse 448 | s1.end = True 449 | 450 | s2 = LinearSegment([0.7, 1.5], [-0.7, 1.5]) 451 | s2.type = Shape.Type.Diffuse 452 | s2.start = True 453 | 454 | teapot_x = pts_teapot[0] 455 | teapot_y = pts_teapot[1] 456 | teapot_hole_x = pts_teapot_hole[0] 457 | teapot_hole_y = pts_teapot_hole[1] 458 | teapot_x *= 0.8; teapot_hole_x *= 0.8 459 | teapot_y *= 0.8; teapot_hole_y *= 0.8 460 | teapot_y += 0.6; teapot_hole_y += 0.6 461 | 462 | s3 = BezierCurve(list(teapot_x.flatten()), list(teapot_y.flatten())) 463 | s3.type = Shape.Type.Refraction 464 | s3.first_specular = True 465 | s3.eta = 1.5 466 | s3.name = "teapot" 467 | 468 | s_hole = BezierCurve(list(teapot_hole_x.flatten()), list(teapot_hole_y.flatten())) 469 | s_hole.type = Shape.Type.Refraction 470 | # s_hole.flip() 471 | s_hole.hole = True 472 | s_hole.eta = 1.5 473 | s_hole.parent = "teapot" 474 | 475 | bounds = Circle([0, 0], 100.0) 476 | bounds.type = Shape.Type.Null 477 | bounds.visible = False 478 | 479 | scene = Scene([s2, s3, s1, s_hole, bounds]) 480 | scene.name = "Teapot" 481 | scene.set_start(0.716, -69.54, end_u=0.55298, spec_u=0.22485) 482 | scene.offset = [0, -0.75] 483 | scene.zoom = 0.93 484 | scene.n_bounces_default = 2 485 | scenes.append(scene) 486 | 487 | 488 | return scenes 489 | -------------------------------------------------------------------------------- /python/viewer.py: -------------------------------------------------------------------------------- 1 | import gc 2 | import numpy as np 3 | 4 | import nanogui 5 | import nanogui.nanovg as nvg 6 | from nanogui import * 7 | 8 | import manifolds 9 | from misc import * 10 | from modes.raytracing import * 11 | from modes.manifold_exploration import * 12 | from modes.specular_manifold_sampling import * 13 | from scenes import create_scenes 14 | 15 | class Input: 16 | def __init__(self, screen): 17 | self.click = False 18 | 19 | self.alt = False 20 | self.shift = False 21 | 22 | self.mouse_p = np.array([0.0, 0.0]) 23 | self.mouse_dp = np.array([0.0, 0.0]) 24 | self.mouse_p_click = np.array([0.0, 0.0]) 25 | 26 | self.screen = screen 27 | 28 | class ManifoldViewer(Screen): 29 | def __init__(self): 30 | super(ManifoldViewer, self).__init__((1024, 900), "Manifold 2D Viewer") 31 | self.set_background(Color(200, 200, 200, 255)) 32 | 33 | # Input & view 34 | self.zoom = 1.0 35 | self.offset = [0, 0] 36 | self.input = Input(self) 37 | self.input.scale = 1.0 38 | 39 | # Scenes 40 | self.scenes = create_scenes() 41 | self.scene_idx = 0 42 | scene = self.scenes[self.scene_idx] 43 | self.offset = scene.offset 44 | self.zoom = scene.zoom 45 | 46 | # Modes 47 | self.modes = {} 48 | mode_dicts = [ 49 | (ModeType.Raytracing, RaytracingMode), 50 | (ModeType.ManifoldExploration, ManifoldExplorationMode), 51 | (ModeType.SpecularManifoldSampling, SpecularManifoldSamplingMode) 52 | ] 53 | for mode, constructor in mode_dicts: 54 | self.modes[mode] = constructor(self) 55 | self.modes[mode].mode = mode 56 | self.mode = ModeType.Raytracing 57 | 58 | # Interface 59 | self.window = Window(self, "Manifolds") 60 | self.window.set_position((15, 15)) 61 | self.window.set_layout(GroupLayout()) 62 | 63 | ## Scene 64 | Label(self.window, "Scene selection", "sans-bold") 65 | scene_tools = Widget(self.window) 66 | scene_tools.set_layout(BoxLayout(Orientation.Horizontal, Alignment.Middle, 0, 3)) 67 | ### Scene reset 68 | scene_reset = Button(scene_tools, "", icons.FA_REDO_ALT) 69 | def scene_reset_cb(): 70 | scene = self.scenes[self.scene_idx] 71 | scene.start_u_current = scene.start_u_default 72 | scene.start_angle_current = scene.start_angle_default 73 | scene.end_u_current = scene.end_u_default 74 | scene.spec_u_current = scene.spec_u_default 75 | self.modes[self.mode].scene_reset() 76 | scene_reset.set_callback(scene_reset_cb) 77 | scene_reset.set_tooltip("Reset scene") 78 | self.scene_reset_cb = scene_reset_cb 79 | ### Scene selection 80 | scene_selection = ComboBox(scene_tools, [s.name for s in self.scenes]) 81 | scene_selection.set_selected_index(self.scene_idx) 82 | scene_selection.set_fixed_width(260) 83 | def scene_selection_cb(idx): 84 | self.scene_idx = idx 85 | scene = self.scenes[idx] 86 | self.offset = scene.offset 87 | self.zoom = scene.zoom 88 | self.modes[self.mode].scene_changed() 89 | scene_selection.set_callback(scene_selection_cb) 90 | self.scene_selection_cb = scene_selection_cb 91 | 92 | self.modes[self.mode].enter(0) 93 | 94 | # Options 95 | Label(self.window, "Options", "sans-bold") 96 | mode_selection = ComboBox(self.window, [ 97 | "Raytracing", 98 | "Manifold Exploration", 99 | "Specular Manifold Sampling" 100 | ]) 101 | mode_selection.set_selected_index(self.mode) 102 | def mode_selection_cb(idx): 103 | last_mode = self.mode 104 | self.modes[last_mode].exit(self.modes[idx]) 105 | self.mode = ModeType(idx) 106 | self.modes[self.mode].enter(self.modes[last_mode]) 107 | for g in self.mode_guis: 108 | self.window.remove_child(g) 109 | for w in self.mode_windows: 110 | self.window.remove_child(w) 111 | self.mode_guis, self.mode_windows = self.modes[self.mode].layout(self.window) 112 | self.window.set_fixed_width(350) 113 | self.perform_layout() 114 | mode_selection.set_callback(mode_selection_cb) 115 | self.mode_selection_cb = mode_selection_cb 116 | self.mode_selection = mode_selection 117 | 118 | self.window.set_fixed_width(350) 119 | self.perform_layout() 120 | 121 | self.mode_guis = [] 122 | self.mode_windows = [] 123 | mode_selection_cb(self.mode) 124 | 125 | 126 | def keyboard_event(self, key, scancode, action, modifiers): 127 | self.input.shift = False 128 | self.input.alt = False 129 | if action == glfw.PRESS: 130 | if key == glfw.KEY_LEFT_SHIFT or key == glfw.KEY_RIGHT_SHIFT: 131 | self.input.shift = True 132 | elif key == glfw.KEY_LEFT_ALT or key == glfw.KEY_RIGHT_ALT: 133 | self.input.alt = True 134 | 135 | if super(ManifoldViewer, self).keyboard_event(key, scancode, action, modifiers): 136 | return True 137 | if key == glfw.KEY_ESCAPE and action == glfw.PRESS: 138 | self.set_visible(False) 139 | return True 140 | 141 | if key == glfw.KEY_SPACE and action == glfw.PRESS: 142 | print("offset: ", self.offset) 143 | print("zoom: ", self.zoom) 144 | print("start_u: ", self.scenes[self.scene_idx].start_u_current) 145 | print("start_angle: ", self.scenes[self.scene_idx].start_angle_current) 146 | print("end_u: ", self.scenes[self.scene_idx].end_u_current) 147 | print("spec_u: ", self.scenes[self.scene_idx].spec_u_current) 148 | return True 149 | 150 | if key == glfw.KEY_1 and action == glfw.PRESS: 151 | self.mode_selection_cb(ModeType.Raytracing) 152 | self.mode_selection.set_selected_index(ModeType.Raytracing) 153 | return True 154 | elif key == glfw.KEY_2 and action == glfw.PRESS: 155 | self.mode_selection_cb(ModeType.ManifoldExploration) 156 | self.mode_selection.set_selected_index(ModeType.ManifoldExploration) 157 | return True 158 | elif key == glfw.KEY_3 and action == glfw.PRESS: 159 | self.mode_selection_cb(ModeType.SpecularManifoldSampling) 160 | self.mode_selection.set_selected_index(ModeType.SpecularManifoldSampling) 161 | return True 162 | 163 | if key == glfw.KEY_R and action == glfw.PRESS: 164 | self.scene_reset_cb() 165 | return True 166 | 167 | if key == glfw.KEY_TAB and action == glfw.PRESS: 168 | self.window.set_visible(not self.window.visible()) 169 | 170 | self.modes[self.mode].keyboard_event(key, scancode, action, modifiers) 171 | 172 | return False 173 | 174 | def mouse_motion_event(self, p, rel, button, modifiers): 175 | if super(ManifoldViewer, self).mouse_motion_event(p, rel, button, modifiers): 176 | return True 177 | 178 | if button == glfw.MOUSE_BUTTON_2 and self.input.shift: 179 | self.offset += (0.002/self.zoom) * np.array([rel[0], -rel[1]]) 180 | 181 | def mouse_button_event(self, p, button, down, modifiers): 182 | click = button == 0 and down 183 | if click: 184 | self.input.mouse_p_click = self.input.mouse_p 185 | 186 | self.input.click = click 187 | 188 | if super(ManifoldViewer, self).mouse_button_event(p, button, down, modifiers): 189 | return True 190 | 191 | def scroll_event(self, p, rel): 192 | if not super(ManifoldViewer, self).scroll_event(p, rel) and self.input.shift: 193 | v = rel[1] if rel[1] != 0 else rel[0] 194 | self.zoom += v * 0.1 195 | self.zoom = min(2.0, max(0.01, self.zoom)) 196 | 197 | return True 198 | 199 | def draw(self, ctx): 200 | if self.mode == ModeType.Raytracing: 201 | self.set_caption("Manifold 2D Visualization - Raytracing") 202 | elif self.mode == ModeType.ManifoldExploration: 203 | self.set_caption("Manifold 2D Visualization - Manifold Exploration") 204 | elif self.mode == ModeType.SpecularManifoldSampling: 205 | self.set_caption("Manifold 2D Visualization - Specular Manifold Sampling") 206 | else: 207 | self.set_caption("Manifold 2D Visualization") 208 | 209 | # Setup view transform 210 | size = self.size() 211 | aspect = size[1] / size[0] 212 | 213 | ctx.Scale(size[0], size[0]) 214 | ctx.Translate(+0.5, +0.5*aspect) 215 | ctx.Scale(0.5, -0.5) 216 | ctx.Scale(self.zoom, self.zoom) 217 | ctx.Translate(self.offset[0], self.offset[1]) 218 | mvp = nvgCurrentTransform(ctx) 219 | mvp = np.array([ 220 | [mvp[0], mvp[2], mvp[4]], 221 | [mvp[1], mvp[3], mvp[5]], 222 | [0, 0, 1], 223 | ]) 224 | self.mvp = np.linalg.inv(mvp) 225 | 226 | mp = self.mouse_pos() 227 | mp = self.mvp @ np.array([mp[0], mp[1], 1]) 228 | new_mp = np.array([mp[0], mp[1]]) 229 | self.input.mouse_dp = new_mp - self.input.mouse_p 230 | self.input.mouse_p = new_mp 231 | 232 | scene = self.scenes[self.scene_idx] 233 | self.modes[self.mode].update(self.input, scene) 234 | self.modes[self.mode].draw(ctx, scene) 235 | 236 | self.input.mouse_dp = np.array([0.0, 0.0]) 237 | ctx.ResetTransform() 238 | super(ManifoldViewer, self).draw(ctx) 239 | 240 | if __name__ == "__main__": 241 | nanogui.init() 242 | app = ManifoldViewer() 243 | app.draw_all() 244 | app.set_visible(True) 245 | nanogui.mainloop(refresh=10) 246 | del app 247 | gc.collect() 248 | nanogui.shutdown() -------------------------------------------------------------------------------- /src/bbox.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | struct BoundingBox2f { 7 | Point2f min, max; 8 | 9 | BoundingBox2f() { reset(); } 10 | 11 | BoundingBox2f(const Point2f &min, const Point2f &max) 12 | : min(min), max(max) {} 13 | 14 | bool valid() const { 15 | return all(max >= min); 16 | } 17 | 18 | bool collapsed() const { 19 | return any(eq(min, max)); 20 | } 21 | 22 | void reset() { 23 | min = Infinity; 24 | max = -Infinity; 25 | } 26 | 27 | void expand(const Point2f &p) { 28 | min = enoki::min(min, p); 29 | max = enoki::max(max, p); 30 | } 31 | 32 | void expand(const BoundingBox2f &bbox) { 33 | min = enoki::min(min, bbox.min); 34 | max = enoki::max(max, bbox.max); 35 | } 36 | 37 | std::tuple ray_intersect(const Ray2f &ray) { 38 | bool active = all(neq(ray.d, zero()) || ((ray.o > min) || (ray.o < max))); 39 | 40 | Vector2f t1 = (min - ray.o) * rcp(ray.d), 41 | t2 = (max - ray.o) * rcp(ray.d); 42 | 43 | Vector2f t1p = enoki::min(t1, t2), 44 | t2p = enoki::max(t1, t2); 45 | 46 | float mint = hmax(t1p), 47 | maxt = hmin(t2p); 48 | 49 | active = active && (maxt >= mint); 50 | 51 | return std::make_tuple(active, mint, maxt); 52 | } 53 | 54 | std::string to_string() const { 55 | std::ostringstream oss; 56 | oss << "BoundingBox2f"; 57 | if (!valid()) { 58 | oss << "[invalid]"; 59 | } else { 60 | oss << "[" << std::endl 61 | << " min = " << min << "," << std::endl 62 | << " max = " << max << std::endl 63 | << "]"; 64 | } 65 | return oss.str(); 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /src/ddistr.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | template 6 | int find_interval(T size, const Predicate &pred) { 7 | T first = 0, len = size; 8 | while (len > 0) { 9 | T half = len >> 1, middle = first + half; 10 | // Bisect range based on value of pred at middle 11 | if (pred(middle)) { 12 | first = middle + 1; 13 | len -= half + 1; 14 | } else { 15 | len = half; 16 | } 17 | } 18 | return clamp(first - 1, T(0), size - 2); 19 | } 20 | 21 | class DiscreteDistribution { 22 | public: 23 | DiscreteDistribution() {} 24 | 25 | DiscreteDistribution(const float *f, int n) : m_func(f, f + n), m_cdf(n + 1) { 26 | m_cdf[0] = 0.f; 27 | for (int i = 1; i < n + 1; ++i) { 28 | m_cdf[i] = m_cdf[i - 1] + m_func[i - 1]; 29 | } 30 | 31 | m_normalization = 1.f / m_cdf[n]; 32 | 33 | for (int i = 1; i < n + 1; ++i) { 34 | m_cdf[i] *= m_normalization; 35 | } 36 | } 37 | 38 | inline float operator[](int i) const { 39 | return m_cdf[i+1] - m_cdf[i]; 40 | } 41 | 42 | inline float cdf(int i) const { 43 | return m_cdf[i]; 44 | } 45 | 46 | inline int sample(float u) const { 47 | return find_interval(int(m_cdf.size()), [&](int index) { return m_cdf[index] <= u; }); 48 | } 49 | 50 | inline int sample(float u, float &pdf) const { 51 | int offset = sample(u); 52 | pdf = m_func[offset] * m_normalization; 53 | return offset; 54 | } 55 | 56 | inline int sample_reuse(float &u) const { 57 | int offset = sample(u); 58 | u = (u - m_cdf[offset]) / (m_cdf[offset + 1] - m_cdf[offset]); 59 | return offset; 60 | } 61 | 62 | inline int sample_reuse(float &u, float &pdf) { 63 | int offset = sample(u, pdf); 64 | u = (u - m_cdf[offset]) / (m_cdf[offset + 1] - m_cdf[offset]); 65 | return offset; 66 | } 67 | 68 | inline float normalization() const { return m_normalization; } 69 | 70 | private: 71 | std::vector m_func, m_cdf; 72 | float m_normalization; 73 | }; -------------------------------------------------------------------------------- /src/global.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | using namespace enoki; 11 | 12 | #define VERSION "0.0.1" 13 | 14 | constexpr float Pi = 3.14159265358979323846f; 15 | constexpr float InvTwoPi = 0.15915494309189533577f; 16 | constexpr float Epsilon = 1e-3f; 17 | constexpr float Infinity = std::numeric_limits::infinity(); 18 | constexpr float MaxFloat = std::numeric_limits::max(); 19 | 20 | #include 21 | 22 | static const NVGcolor COLOR_REFLECTION = nvgRGB(240, 200, 91); 23 | static const NVGcolor COLOR_REFRACTION = nvgRGBA(128, 200, 255, 128); 24 | static const NVGcolor COLOR_EMITTER = nvgRGB(255, 255, 180); 25 | static const NVGcolor COLOR_DIFFUSE = nvgRGB(120, 120, 120); 26 | static const NVGcolor COLOR_TRANSPARENT = nvgRGBA(0, 0, 0, 0); 27 | 28 | #define TERM_COLOR_RED "\x1B[31m" 29 | #define TERM_COLOR_YELLOW "\x1B[33m" 30 | #define TERM_COLOR_WHITE "\x1B[37m" 31 | 32 | #define LOG(str, ...) std::cout << tfm::format(str "\n", ##__VA_ARGS__) 33 | #define PRINT(str, ...) std::cout << tfm::format(str "\n", ##__VA_ARGS__) 34 | #define INFO(str, ...) std::cout << tfm::format("%s(%d): " str "\n", __FILE__, __LINE__, ##__VA_ARGS__) 35 | #define WARN(str, ...) std::cout << tfm::format(TERM_COLOR_YELLOW "%s(%d): " str "\n" TERM_COLOR_WHITE, __FILE__, __LINE__, ##__VA_ARGS__) 36 | #define ERROR(str, ...) throw std::runtime_error(tfm::format(TERM_COLOR_RED "\nError - %s(%d): " str "\n" TERM_COLOR_WHITE, __FILE__, __LINE__, ##__VA_ARGS__)) 37 | 38 | #define VARLOG(x) (std::cout << #x << ": " << (x) << std::endl) 39 | #define VARLOG2(x, y) (std::cout << #x << ": " << (x) << ", " << #y << ": " << (y) << std::endl) 40 | #define VARLOG3(x, y, z) (std::cout << #x << ": " << (x) << ", " << #y << ": " << (y) << ", " << #z << ": " << (z) << std::endl) 41 | #define VARLOG4(x, y, z, w) (std::cout << #x << ": " << (x) << ", " << #y << ": " << (y) << ", " << #z << ": " << (z) << ", " << #w << ": " << (w) << std::endl) 42 | 43 | extern "C" { 44 | /* Dummy handle type -- will never be instantiated */ 45 | typedef struct NVGcontext { int unused; } NVGcontext; 46 | }; 47 | 48 | using Vector2f = Array; 49 | using Vector2i = Array; 50 | 51 | using Point2f = Array; 52 | 53 | using Matrix2f = Matrix; 54 | 55 | inline float rad_to_deg(float rad) { 56 | return rad * (180 / Pi); 57 | } 58 | 59 | inline float deg_to_rad(float deg) { 60 | return deg * (Pi / 180); 61 | } 62 | 63 | inline float cross(const Vector2f &v0, const Vector2f &v1) { 64 | return v0[0]*v1[1] - v0[1]*v1[0]; 65 | } 66 | 67 | template 68 | inline T lerp(float t, T a, T b) { 69 | return a + t * (b - a); 70 | } 71 | 72 | inline std::tuple solve_quadratic(float a, float b, float c) { 73 | /* Is this perhaps a linear equation? */ 74 | bool linear_case = eq(a, 0.f); 75 | 76 | /* If so, we require b > 0 */ 77 | bool active = !linear_case || (b > 0.f); 78 | 79 | /* Initialize solution with that of linear equation */ 80 | float x0, x1; 81 | x0 = x1 = -c / b; 82 | 83 | /* Check if the quadratic equation is solvable */ 84 | float discrim = fmsub(b, b, 4.f * a * c); 85 | active &= linear_case || (discrim >= 0); 86 | 87 | if (active) { 88 | float sqrt_discrim = sqrt(discrim); 89 | 90 | /* Numerically stable version of (-b (+/-) sqrt_discrim) / (2 * a) 91 | * 92 | * Based on the observation that one solution is always 93 | * accurate while the other is not. Finds the solution of 94 | * greater magnitude which does not suffer from loss of 95 | * precision and then uses the identity x1 * x2 = c / a 96 | */ 97 | float temp = -0.5f * (b + copysign(sqrt_discrim, b)); 98 | 99 | float x0p = temp / a, 100 | x1p = c / temp; 101 | 102 | /* Order the results so that x0 < x1 */ 103 | float x0m = min(x0p, x1p), 104 | x1m = max(x0p, x1p); 105 | 106 | x0 = select(linear_case, x0, x0m); 107 | x1 = select(linear_case, x0, x1m); 108 | } 109 | 110 | return std::make_tuple(active, x0, x1); 111 | } -------------------------------------------------------------------------------- /src/interaction.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | std::string Interaction::to_string() const { 5 | std::ostringstream oss; 6 | oss << "Interaction[" << std::endl 7 | << " rayt = " << rayt << "," << std::endl 8 | << " p = " << p << "," << std::endl 9 | << " s = " << s << "," << std::endl 10 | << " n = " << n << "," << std::endl 11 | << " dp_du = " << dp_du << "," << std::endl 12 | << " dn_du = " << dn_du << "," << std::endl 13 | << " u = " << u << "," << std::endl 14 | << " eta = " << eta << "," << std::endl 15 | << " shape = " << (shape == nullptr ? "null" : shape->to_string()) << "," << std::endl 16 | << "]"; 17 | return oss.str(); 18 | } 19 | -------------------------------------------------------------------------------- /src/interaction.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | class Shape; 6 | 7 | struct Interaction { 8 | // Distance of the interaction along ray 9 | float rayt = Infinity; 10 | 11 | // Position of the interaction 12 | Point2f p; 13 | 14 | // Normal 15 | Vector2f n; 16 | 17 | // Position partial derivative w.r.t. the local parameterization 18 | Vector2f dp_du; 19 | 20 | // Normal partial derivative w.r.t. the local parameterization 21 | Vector2f dn_du; 22 | 23 | // Tangent 24 | Vector2f s; 25 | 26 | // Tangent derivative 27 | Vector2f ds_du; 28 | 29 | // Offset normal (for rough variations) 30 | Vector2f n_offset; 31 | 32 | // Hit position in local parameterization 33 | float u; 34 | 35 | // Relative index of refraction at the interaction 36 | float eta; 37 | 38 | // Associated shape 39 | const Shape *shape = nullptr; 40 | 41 | // String representation 42 | std::string to_string() const; 43 | 44 | bool is_valid() const { return rayt < Infinity; } 45 | }; 46 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | int main(int argc, char **argv) { 5 | return 0; 6 | } -------------------------------------------------------------------------------- /src/python/interaction.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | 6 | PYTHON_EXPORT(Interaction) { 7 | py::class_(m, "Interaction", "2D Interaction record", py::dynamic_attr()) 8 | .def(py::init<>()) 9 | .def_readwrite("rayt", &Interaction::rayt) 10 | .def_readwrite("p", &Interaction::p) 11 | .def_readwrite("n", &Interaction::n) 12 | .def_readwrite("dp_du", &Interaction::dp_du) 13 | .def_readwrite("dn_du", &Interaction::dn_du) 14 | .def_readwrite("s", &Interaction::s) 15 | .def_readwrite("ds_du", &Interaction::ds_du) 16 | .def_readwrite("n_offset", &Interaction::n_offset) 17 | .def_readwrite("u", &Interaction::u) 18 | .def_readwrite("eta", &Interaction::eta) 19 | .def_readonly("shape", &Interaction::shape) 20 | .def("is_valid", &Interaction::is_valid) 21 | .def("__repr__", &Interaction::to_string) 22 | .def("copy", [](const Interaction &in) { 23 | Interaction new_in; 24 | new_in.rayt = in.rayt; 25 | new_in.p = in.p; 26 | new_in.n = in.n; 27 | new_in.dp_du = in.dp_du; 28 | new_in.dn_du = in.dn_du; 29 | new_in.s = in.s; 30 | new_in.ds_du = in.ds_du; 31 | new_in.n_offset = in.n_offset; 32 | new_in.u = in.u; 33 | new_in.eta = in.eta; 34 | new_in.shape = in.shape; 35 | return new_in; 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /src/python/python.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | PYTHON_DECLARE(Ray2f); 4 | PYTHON_DECLARE(Interaction); 5 | PYTHON_DECLARE(Shape); 6 | PYTHON_DECLARE(Scene); 7 | 8 | PYBIND11_MODULE(manifolds, m) { 9 | m.doc() = "manifold viewer python library"; 10 | 11 | PYTHON_IMPORT(Ray2f); 12 | PYTHON_IMPORT(Interaction); 13 | PYTHON_IMPORT(Shape); 14 | PYTHON_IMPORT(Scene); 15 | } 16 | -------------------------------------------------------------------------------- /src/python/python.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace py = pybind11; 11 | using namespace py::literals; 12 | 13 | #include 14 | using namespace enoki; 15 | 16 | #define PYTHON_EXPORT(name) \ 17 | void python_export_##name(py::module &m) 18 | #define PYTHON_DECLARE(name) \ 19 | extern void python_export_##name(py::module &) 20 | #define PYTHON_IMPORT(name) \ 21 | python_export_##name(m) 22 | -------------------------------------------------------------------------------- /src/python/ray.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | PYTHON_EXPORT(Ray2f) { 6 | py::class_(m, "Ray2f", "2D Ray", py::dynamic_attr()) 7 | .def(py::init()) 8 | .def(py::init()) 9 | .def_readwrite("o", &Ray2f::o) 10 | .def_readwrite("d", &Ray2f::d) 11 | .def_readwrite("mint", &Ray2f::mint) 12 | .def_readwrite("maxt", &Ray2f::maxt) 13 | .def("__repr__", &Ray2f::to_string); 14 | } 15 | -------------------------------------------------------------------------------- /src/python/scene.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | 6 | PYTHON_EXPORT(Scene) { 7 | py::class_(m, "Scene", "Scene", py::dynamic_attr()) 8 | .def(py::init<>()) 9 | .def("add_shape", &Scene::add_shape) 10 | .def("ray_intersect", &Scene::ray_intersect) 11 | .def("draw", &Scene::draw) 12 | .def("shape", &Scene::shape) 13 | .def("start_shape", &Scene::start_shape) 14 | .def("end_shape", &Scene::end_shape) 15 | .def("first_specular_shape", &Scene::first_specular_shape); 16 | 17 | // This function is not exposed by the nanogui Python API --- so do it here instead. 18 | m.def("nvgCurrentTransform", 19 | [](NVGcontext *ctx) { 20 | float v[6]; 21 | nvgCurrentTransform(ctx, v); 22 | return std::make_tuple(v[0], v[1], v[2], v[3], v[4], v[5]); 23 | } 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/python/shape.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | PYTHON_EXPORT(Shape) { 13 | auto shape = py::class_>(m, "Shape", py::dynamic_attr()) 14 | .def_readwrite("name", &Shape::name) 15 | .def_readwrite("id", &Shape::id) 16 | .def_readwrite("type", &Shape::type) 17 | .def_readwrite("eta", &Shape::eta) 18 | .def_readwrite("hole", &Shape::hole) 19 | .def_readwrite("visible", &Shape::visible) 20 | .def_readwrite("parent", &Shape::parent) 21 | .def_readwrite("start", &Shape::start) 22 | .def_readwrite("end", &Shape::end) 23 | .def_readwrite("first_specular", &Shape::first_specular) 24 | 25 | .def("flip", &Shape::flip) 26 | .def("add_hole", &Shape::add_hole, 27 | "hole"_a) 28 | .def("sample_position", &Shape::sample_position, 29 | "sample"_a) 30 | .def("project", &Shape::project, 31 | "p"_a) 32 | .def("draw", &Shape::draw, 33 | "ctx"_a, "hole"_a=false); 34 | 35 | py::enum_(shape, "Type", "Interaction Type", py::arithmetic()) 36 | .value("Null", Shape::Type::Null) 37 | .value("Diffuse", Shape::Type::Diffuse) 38 | .value("Emitter", Shape::Type::Emitter) 39 | .value("Reflection", Shape::Type::Reflection) 40 | .value("Refraction", Shape::Type::Refraction); 41 | 42 | py::class_>(m, "Circle") 43 | .def(py::init()); 44 | 45 | py::class_>(m, "ConcaveSegment") 46 | .def(py::init()) 47 | .def_readwrite("gradient_start", &ConcaveSegment::gradient_start) 48 | .def_readwrite("gradient_width", &ConcaveSegment::gradient_width); 49 | 50 | py::class_>(m, "ConvexSegment") 51 | .def(py::init(), 52 | "a"_a, "b"_a, "radius"_a, "alt"_a=false) 53 | .def_readwrite("gradient_start", &ConvexSegment::gradient_start) 54 | .def_readwrite("gradient_width", &ConvexSegment::gradient_width); 55 | 56 | py::class_>(m, "LinearSegment") 57 | .def(py::init()) 58 | .def_readwrite("gradient_start", &LinearSegment::gradient_start) 59 | .def_readwrite("gradient_width", &LinearSegment::gradient_width) 60 | .def_readwrite("height", &LinearSegment::height); 61 | 62 | py::class_>(m, "BezierCurve") 63 | .def(py::init &, const std::vector &>()); 64 | } 65 | -------------------------------------------------------------------------------- /src/ray.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | struct Ray2f { 6 | Point2f o; 7 | Vector2f d; 8 | float mint = Epsilon; 9 | float maxt = Infinity; 10 | 11 | Ray2f(const Point2f &o, const Vector2f &d) 12 | : o(o), d(d) {} 13 | 14 | Ray2f(const Point2f &o, const Vector2f &d, float mint, float maxt) 15 | : o(o), d(d), mint(mint), maxt(maxt) {} 16 | 17 | Point2f operator() (float t) const { return fmadd(d, t, o); } 18 | 19 | std::string to_string() const { 20 | std::ostringstream oss; 21 | oss << "Ray2f[" << std::endl 22 | << " o = " << o << "," << std::endl 23 | << " d = " << d << "," << std::endl 24 | << " mint = " << mint << "," << std::endl 25 | << " maxt = " << maxt << "," << std::endl 26 | << "]"; 27 | return oss.str(); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /src/scene.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | Scene::Scene() {} 5 | 6 | Scene::~Scene() {} 7 | 8 | void Scene::add_shape(std::shared_ptr shape) { 9 | m_shapes.push_back(shape); 10 | if (shape->hole) { 11 | for (size_t k = 0; k < m_draw_shapes.size(); ++k) { 12 | if (m_draw_shapes[k]->name == shape->parent) { 13 | m_draw_shapes[k]->add_hole(shape); 14 | } 15 | } 16 | } else if (shape->visible) { 17 | m_draw_shapes.push_back(shape); 18 | } 19 | 20 | if (shape->start) { 21 | if (m_start_shape) { 22 | WARN("Scene: can only specify one start shape per scene!"); 23 | } 24 | m_start_shape = shape; 25 | } 26 | 27 | if (shape->end) { 28 | if (m_end_shape) { 29 | WARN("Scene: can only specify one end shape per scene!"); 30 | } 31 | m_end_shape = shape; 32 | } 33 | 34 | if (shape->first_specular) { 35 | if (m_first_specular_shape) { 36 | WARN("Scene: can only specify one shape as the \"first specular shape\" per scene!"); 37 | } 38 | m_first_specular_shape = shape; 39 | } 40 | } 41 | 42 | Interaction Scene::ray_intersect(const Ray2f &ray_) const { 43 | bool found_hit = false; 44 | size_t idx = -1; 45 | float spline_t = -1.f; 46 | size_t spline_idx = -1; 47 | Ray2f ray(ray_); 48 | 49 | for (uint32_t k = 0; k < m_shapes.size(); ++k) { 50 | auto [hit_bbox, unused_0, unused_1] = m_shapes[k]->bbox.ray_intersect(ray); 51 | if (hit_bbox) { 52 | auto [hit, t, st, sidx] = m_shapes[k]->ray_intersect(ray); 53 | if (hit && t > ray.mint && t < ray.maxt) { 54 | found_hit = true; 55 | idx = k; 56 | spline_t = st; 57 | spline_idx = sidx; 58 | ray.maxt = t; 59 | } 60 | } 61 | } 62 | 63 | if (found_hit) { 64 | Interaction it = m_shapes[idx]->fill_interaction(ray, spline_t, spline_idx); 65 | it.rayt = ray.maxt; 66 | return it; 67 | } 68 | return Interaction(); 69 | } 70 | 71 | void Scene::draw(NVGcontext *ctx) const { 72 | for (size_t k = 0; k < m_draw_shapes.size(); ++k) { 73 | m_draw_shapes[k]->draw(ctx); 74 | } 75 | } 76 | 77 | std::shared_ptr Scene::shape(size_t k) { 78 | return m_shapes[k]; 79 | } 80 | 81 | std::shared_ptr Scene::start_shape() { 82 | return m_start_shape; 83 | } 84 | 85 | std::shared_ptr Scene::end_shape() { 86 | return m_end_shape; 87 | } 88 | 89 | std::shared_ptr Scene::first_specular_shape() { 90 | return m_first_specular_shape; 91 | } -------------------------------------------------------------------------------- /src/scene.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | class Scene { 8 | public: 9 | Scene(); 10 | virtual ~Scene(); 11 | 12 | void add_shape(std::shared_ptr shape); 13 | 14 | Interaction ray_intersect(const Ray2f &ray_) const; 15 | 16 | void draw(NVGcontext *ctx) const; 17 | 18 | std::shared_ptr shape(size_t k); 19 | 20 | std::shared_ptr start_shape(); 21 | std::shared_ptr end_shape(); 22 | std::shared_ptr first_specular_shape(); 23 | 24 | protected: 25 | std::shared_ptr m_start_shape, m_end_shape, m_first_specular_shape; 26 | std::vector> m_shapes; 27 | std::vector> m_draw_shapes; 28 | }; 29 | -------------------------------------------------------------------------------- /src/shape.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | Shape::~Shape() {} 6 | 7 | Interaction Shape::sample_position(float sample) const { 8 | ERROR("Shape::sample_position(): Not implemented!"); 9 | } 10 | 11 | float Shape::project(const Point2f &p) const { 12 | ERROR("Shape::project(): Not implemented!"); 13 | } 14 | 15 | std::tuple Shape::ray_intersect(const Ray2f &ray) const { 16 | ERROR("Shape::ray_intersect(): Not implemented!"); 17 | } 18 | 19 | Interaction Shape::fill_interaction(const Ray2f &ray, float spline_t, size_t spline_idx) const { 20 | ERROR("Shape::fill_interaction(): Not implemented!"); 21 | } 22 | 23 | void Shape::draw(NVGcontext *ctx, bool hole) const { 24 | nvgStrokeWidth(ctx, 0.007f); 25 | nvgStrokeColor(ctx, nvgRGB(0, 0, 0)); 26 | 27 | if (type == Type::Reflection) { 28 | nvgFillColor(ctx, COLOR_REFLECTION); 29 | } else if (type == Type::Refraction) { 30 | nvgFillColor(ctx, COLOR_REFRACTION); 31 | } else if (type == Type::Emitter) { 32 | nvgFillColor(ctx, COLOR_EMITTER); 33 | } else { 34 | nvgFillColor(ctx, COLOR_DIFFUSE); 35 | } 36 | } 37 | 38 | std::string Shape::to_string() const { 39 | return ""; 40 | } -------------------------------------------------------------------------------- /src/shape.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | struct NVGcontext; 9 | 10 | class Shape { 11 | public: 12 | virtual ~Shape(); 13 | 14 | // Sample surface interaction from local parameterization 15 | virtual Interaction sample_position(float sample) const; 16 | 17 | // Give local parameterization of closest point on the shape 18 | virtual float project(const Point2f &p) const; 19 | 20 | // Intersect shape with ray 21 | virtual std::tuple ray_intersect(const Ray2f &ray) const; 22 | 23 | // Fill hit information after successful ray intersect 24 | virtual Interaction fill_interaction(const Ray2f &ray, float spline_t, size_t spline_idx) const; 25 | 26 | // Flip orientation and normals 27 | virtual void flip() {} 28 | 29 | // Draw this shape in the window 30 | virtual void draw(NVGcontext *ctx, bool hole=false) const; 31 | 32 | // String representation 33 | virtual std::string to_string() const; 34 | 35 | public: 36 | std::string name; 37 | int id; 38 | 39 | BoundingBox2f bbox; 40 | 41 | enum Type { 42 | Null, 43 | Diffuse, 44 | Emitter, 45 | Refraction, 46 | Reflection 47 | }; 48 | 49 | Type type; 50 | float eta = 1.f; 51 | 52 | bool start = false; 53 | bool first_specular = false; 54 | bool end = false; 55 | 56 | bool hole = false; 57 | bool visible = true; 58 | std::string parent; 59 | void add_hole(std::shared_ptr hole) { 60 | m_holes.push_back(hole); 61 | } 62 | 63 | protected: 64 | std::vector> m_holes; 65 | }; 66 | -------------------------------------------------------------------------------- /src/shapes/bezier_curve.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #define SPLINE_DISCRETIZATION 15 8 | 9 | struct BezierSpline { 10 | Point2f p[4]; // Control points for a cubic bezier spline 11 | 12 | BoundingBox2f bbox() const { 13 | BoundingBox2f result; 14 | for (int i=0; i<4; ++i) 15 | result.expand(p[i]); 16 | result.min -= Vector2f(Epsilon); 17 | result.min += Vector2f(Epsilon); 18 | return result; 19 | } 20 | 21 | Point2f eval(float t) const { 22 | float tmp = 1.f - t, 23 | tmp2 = tmp * tmp, 24 | tmp3 = tmp * tmp2, 25 | t2 = t * t, 26 | t3 = t * t2; 27 | // Cubic Bezier curve (explicit form) 28 | return tmp3 * p[0] + 3.f*tmp2*t * p[1] + 3.f*tmp*t2 * p[2] + t3 * p[3]; 29 | } 30 | 31 | Vector2f eval_tangent(float t) const { 32 | float tmp = 1.f - t, 33 | tmp2 = tmp * tmp, 34 | t2 = t * t; 35 | // First derivative of cubic Bezier curve 36 | return 3.f*tmp2 * (p[1] - p[0]) + 6.f*tmp*t * (p[2] - p[1]) + 3.f*t2 * (p[3] - p[2]); 37 | } 38 | 39 | Vector2f eval_curvature(float t) const { 40 | float tmp = 1.f - t; 41 | return 6.f*tmp * (p[2] - 2.f*p[1] + p[0]) + 6.f*t*(p[3] - 2.f*p[2] + p[1]); 42 | } 43 | 44 | void flip() { 45 | std::reverse(&p[0], &p[4]); 46 | } 47 | 48 | float length() const { 49 | const int n_steps = SPLINE_DISCRETIZATION; 50 | float length = 0; 51 | for (int i = 0; i < n_steps; ++i) 52 | length += norm(eval_tangent(float(i) / (n_steps-1))); 53 | return length / n_steps; 54 | } 55 | 56 | Interaction fill_interaction(float u) const { 57 | Vector2f P = eval(u); 58 | Vector2f T = eval_tangent(u); 59 | Vector2f N(-T[1], T[0]); 60 | float scale = 1.0f / std::sqrt(dot(N, N)); 61 | N *= scale; 62 | 63 | // Second derivative of cubic Bezier curve 64 | Vector2f dN = eval_curvature(u); 65 | dN *= scale; 66 | dN = Vector2f(-dN[1], dN[0]); 67 | 68 | Interaction it; 69 | it.p = P; 70 | it.n = N; 71 | it.dp_du = T; 72 | it.dn_du = dN; 73 | it.u = u; 74 | it.s = Vector2f(-it.n[1], it.n[0]); 75 | it.ds_du = Vector2f(-it.dn_du[1], it.dn_du[0]); 76 | return it; 77 | } 78 | 79 | Interaction sample_position(float sample) const { 80 | return fill_interaction(sample); 81 | } 82 | 83 | std::tuple ray_intersect(const Ray2f &ray) const { 84 | const int n_steps = SPLINE_DISCRETIZATION; 85 | bool success = false; 86 | float t = Infinity; 87 | float spline_t; 88 | 89 | for (int i = 0; i < n_steps; ++i) { 90 | float t0 = float(i) / n_steps, 91 | t1 = float(i+1) / n_steps; 92 | Point2f p0 = eval(t0), p1 = eval(t1); 93 | Vector2f d(p1 - p0); 94 | float len = norm(d); 95 | if (len == 0.f) 96 | continue; 97 | d /= len; 98 | 99 | Vector2f n(-d.y(), d.x()); 100 | 101 | float dp = dot(n, ray.d); 102 | if (dp == 0) 103 | continue; 104 | 105 | float tp = dot(n, p0 - ray.o) / dp; 106 | float proj = dot(ray(tp)-p0, d) / len; 107 | 108 | if (tp >= ray.mint && tp <= ray.maxt && tp < t && proj >= 0 && proj <= 1) { 109 | spline_t = t0*(1-proj) + t1*proj; 110 | success = true; 111 | t = tp; 112 | } 113 | } 114 | 115 | if (success) { 116 | for (int i = 0; i < 3; ++i) { 117 | // Do a few 2D Newton iterations to converge on the root 118 | Point2f p_spline = eval(spline_t); 119 | Vector2f dp_spline = eval_tangent(spline_t); 120 | Point2f p_ray = ray(t); 121 | Vector2f dp_ray = ray.d; 122 | 123 | Matrix2f Jf(dp_spline.x(), -dp_ray.x(), 124 | dp_spline.y(), -dp_ray.y()); 125 | Vector2f b(p_spline.x() - p_ray.x(), 126 | p_spline.y() - p_ray.y()); 127 | 128 | Vector2f x = inverse(Jf) * b; 129 | 130 | spline_t -= x[0]; 131 | t -= x[1]; 132 | } 133 | if (t < ray.mint || t > ray.maxt || spline_t < 0 || spline_t > 1) 134 | return { false, t, spline_t }; 135 | } 136 | 137 | return { success, t, spline_t }; 138 | } 139 | 140 | std::tuple project(const Point2f &p) const { 141 | const int n_steps = SPLINE_DISCRETIZATION; 142 | float d2 = Infinity; 143 | float t; 144 | 145 | for (int i = 0; i < n_steps; ++i) { 146 | float t0 = float(i) / (n_steps - 1); 147 | Point2f p0 = eval(t0); 148 | 149 | float d20 = squared_norm(p0 - p); 150 | if (d20 < d2) { 151 | d2 = d20; 152 | t = t0; 153 | } 154 | } 155 | 156 | for (int i = 0; i < 3; ++i) { 157 | // Do a few 1D Newton iterations to converge on minimum (i.e. root of first derivative) 158 | Point2f p_spline = eval(t); 159 | Vector2f dp_spline = eval_tangent(t); 160 | Vector2f d2p_spline = eval_curvature(t); 161 | 162 | float df = 2.f*(dot(p_spline, dp_spline) - dot(p, dp_spline)); 163 | float d2f = 2.f*(dot(p_spline, d2p_spline) + dot(dp_spline, dp_spline) - dot(p, d2p_spline)); 164 | 165 | float dt = rcp(d2f) * df; 166 | t -= dt; 167 | } 168 | t = max(0.f, min(1.f, t)); 169 | return { eval(t), t }; 170 | } 171 | }; 172 | 173 | 174 | class BezierCurve : public Shape { 175 | public: 176 | BezierCurve(const std::vector &pts_x, 177 | const std::vector &pts_y) 178 | : Shape() { 179 | name = "BezierCurve"; 180 | 181 | if (pts_x.size() != pts_y.size()) { 182 | WARN("BezierCurve: Number of control points in x and y should be the same."); 183 | } 184 | if (pts_x.size() % 4 != 0) { 185 | WARN("BezierCurve: Number of control points should be multiple of 4."); 186 | } 187 | 188 | // Fill in control points 189 | size_t n_splines = pts_x.size() / 4; 190 | for (size_t i = 0; i < n_splines; ++i) { 191 | BezierSpline spline; 192 | for (size_t k = 0; k < 4; ++k) { 193 | spline.p[k] = Point2f(pts_x[4*i + k], pts_y[4*i + k]); 194 | } 195 | m_splines.push_back(spline); 196 | } 197 | 198 | // Precompute lengths for sampling 199 | std::vector lengths; 200 | for (size_t i = 0; i < n_splines; ++i) { 201 | lengths.push_back(m_splines[i].length()); 202 | } 203 | m_length_map = DiscreteDistribution(lengths.data(), lengths.size()); 204 | 205 | // Precompute bounding box 206 | bbox = BoundingBox2f(); 207 | for (size_t i = 0; i < m_splines.size(); i++) { 208 | bbox.expand(m_splines[i].bbox()); 209 | } 210 | } 211 | 212 | Interaction sample_position(float sample) const override { 213 | int spline_idx = m_length_map.sample_reuse(sample); 214 | const BezierSpline &spline = m_splines[spline_idx]; 215 | Interaction it = spline.sample_position(sample); 216 | it.u = sample; 217 | it.shape = this; 218 | return it; 219 | } 220 | 221 | float project(const Point2f &p) const override { 222 | float d2 = Infinity; 223 | float t; 224 | size_t idx; 225 | 226 | for (size_t i = 0; i < m_splines.size(); ++i) { 227 | auto [p0, t0] = m_splines[i].project(p); 228 | float d20 = squared_norm(p0 - p); 229 | if (d20 < d2) { 230 | d2 = d20; 231 | t = t0; 232 | idx = i; 233 | } 234 | } 235 | 236 | float t0 = m_length_map.cdf(idx), 237 | t1 = m_length_map.cdf(idx + 1); 238 | return t0*(1.f - t) + t1*t; 239 | } 240 | 241 | std::tuple ray_intersect(const Ray2f &ray_) const override { 242 | bool found_hit = false; 243 | 244 | size_t idx = -1; 245 | float spline_t = -1.f; 246 | Ray2f ray(ray_); 247 | 248 | for (uint32_t k = 0; k < m_splines.size(); ++k) { 249 | auto [hit_bbox, unused_0, unused_1] = m_splines[k].bbox().ray_intersect(ray); 250 | if (hit_bbox) { 251 | auto [hit, t, st] = m_splines[k].ray_intersect(ray); 252 | if (hit && t > ray.mint && t < ray.maxt) { 253 | found_hit = true; 254 | idx = k; 255 | spline_t = st; 256 | ray.maxt = t; 257 | } 258 | } 259 | } 260 | 261 | return { found_hit, ray.maxt, spline_t, idx }; 262 | } 263 | 264 | Interaction fill_interaction(const Ray2f &ray, float spline_t, size_t spline_idx) const override { 265 | Interaction it = m_splines[spline_idx].fill_interaction(spline_t); 266 | it.shape = this; 267 | return it; 268 | } 269 | 270 | void flip() override { 271 | std::reverse(m_splines.begin(), m_splines.end()); 272 | for (size_t i = 0; i < m_splines.size(); ++i) { 273 | m_splines[i].flip(); 274 | } 275 | } 276 | 277 | void draw(NVGcontext *ctx, bool hole=false) const override { 278 | if (m_splines.size() == 0) return; 279 | 280 | if (hole) { 281 | nvgMoveTo(ctx, m_splines[0].p[0].x(), m_splines[0].p[0].y()); 282 | nvgPathWinding(ctx, NVG_HOLE); 283 | for (size_t i = 0; i < m_splines.size(); ++i) { 284 | const BezierSpline &s = m_splines[i]; 285 | nvgBezierTo(ctx, s.p[1].x(), s.p[1].y(), 286 | s.p[2].x(), s.p[2].y(), 287 | s.p[3].x(), s.p[3].y()); 288 | nvgPathWinding(ctx, NVG_HOLE); 289 | } 290 | } else { 291 | nvgSave(ctx); 292 | Shape::draw(ctx); 293 | 294 | nvgBeginPath(ctx); 295 | nvgMoveTo(ctx, m_splines[0].p[0].x(), m_splines[0].p[0].y()); 296 | for (size_t i = 0; i < m_splines.size(); ++i) { 297 | const BezierSpline &s = m_splines[i]; 298 | nvgBezierTo(ctx, s.p[1].x(), s.p[1].y(), 299 | s.p[2].x(), s.p[2].y(), 300 | s.p[3].x(), s.p[3].y()); 301 | } 302 | 303 | for (size_t i=0; idraw(ctx, true); 305 | } 306 | 307 | nvgFill(ctx); 308 | nvgStroke(ctx); 309 | 310 | nvgRestore(ctx); 311 | } 312 | } 313 | 314 | std::string to_string() const override { 315 | std::ostringstream oss; 316 | oss << "BezierCurve[]" << std::endl; 317 | return oss.str(); 318 | } 319 | 320 | protected: 321 | std::vector m_splines; 322 | DiscreteDistribution m_length_map; 323 | }; 324 | -------------------------------------------------------------------------------- /src/shapes/circle.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | class Circle : public Shape { 7 | public: 8 | Circle(const Point2f ¢er, float radius) 9 | : Shape(), m_center(center), m_radius(radius) { 10 | name = "Circle"; 11 | 12 | bbox = BoundingBox2f(); 13 | bbox.min = m_center - Vector2f(m_radius); 14 | bbox.max = m_center + Vector2f(m_radius); 15 | } 16 | 17 | Interaction sample_position(float sample) const override { 18 | float phi = 2.f*Pi*sample; 19 | auto [sp, cp] = sincos(phi); 20 | Point2f p_local(cp, sp); 21 | 22 | Interaction in; 23 | in.rayt = 0; 24 | in.p = m_center + m_radius*p_local; 25 | in.n = p_local; 26 | in.dp_du = 2.f*Pi*Vector2f(-p_local[1], p_local[0]); 27 | float inv_radius = (m_flipped ? -1.f : 1.f) * rcp(m_radius); 28 | in.dn_du = in.dp_du*inv_radius; 29 | if (m_flipped) { 30 | in.n *= -1.f; 31 | } 32 | in.u = sample; 33 | in.s = Vector2f(-in.n[1], in.n[0]); 34 | in.ds_du = Vector2f(-in.dn_du[1], in.dn_du[0]); 35 | in.shape = this; 36 | return in; 37 | } 38 | 39 | float project(const Point2f &p) const override { 40 | Vector2f d = normalize(p - m_center); 41 | float phi = atan2(d.y(), d.x()); 42 | if (phi < 0.f) phi += 2.f*Pi; 43 | return phi*InvTwoPi; 44 | } 45 | 46 | std::tuple ray_intersect(const Ray2f &ray) const override { 47 | float mint = ray.mint, 48 | maxt = ray.maxt; 49 | 50 | Vector2f o = Vector2f(ray.o) - m_center; 51 | Vector2f d(ray.d); 52 | 53 | float A = squared_norm(d), 54 | B = 2.0f * dot(o, d), 55 | C = squared_norm(o) - m_radius*m_radius; 56 | 57 | auto [solution_found, near_t, far_t] = solve_quadratic(A, B, C); 58 | 59 | bool out_bounds = !((near_t <= maxt) && (far_t >= mint)), 60 | in_bounds = (near_t < mint) && (far_t > maxt); 61 | 62 | bool valid_intersection = solution_found && (!out_bounds) && (!in_bounds); 63 | float t = near_t < mint ? far_t : near_t; 64 | 65 | return { valid_intersection, t, -1.f, 0 }; 66 | } 67 | 68 | Interaction fill_interaction(const Ray2f &ray, float spline_t, size_t spline_idx) const override { 69 | Interaction in; 70 | in.rayt = ray.maxt; 71 | in.p = ray(in.rayt); 72 | in.p = m_center + normalize(in.p - m_center) * m_radius; 73 | in.n = normalize(in.p - m_center); 74 | Point2f p_local = in.p - m_center; 75 | in.dp_du = 2.f*Pi*Vector2f(-p_local[1], p_local[0]); 76 | if (m_flipped) { 77 | in.n = -in.n; 78 | } 79 | float inv_radius = (m_flipped ? -1.f : 1.f) * rcp(m_radius); 80 | in.dn_du = in.dp_du * inv_radius; 81 | in.u = project(in.p); 82 | in.s = Vector2f(-in.n[1], in.n[0]); 83 | in.ds_du = Vector2f(-in.dn_du[1], in.dn_du[0]); 84 | in.shape = this; 85 | return in; 86 | } 87 | 88 | void flip() override { 89 | m_flipped = !m_flipped; 90 | } 91 | 92 | void draw(NVGcontext *ctx, bool hole=false) const override { 93 | if (hole) { 94 | nvgCircle(ctx, m_center[0], m_center[1], m_radius); 95 | nvgPathWinding(ctx, NVG_HOLE); 96 | } else { 97 | Shape::draw(ctx); 98 | 99 | nvgBeginPath(ctx); 100 | nvgCircle(ctx, m_center[0], m_center[1], m_radius); 101 | 102 | for (size_t i=0; idraw(ctx, true); 104 | } 105 | 106 | nvgFill(ctx); 107 | nvgStroke(ctx); 108 | } 109 | } 110 | 111 | std::string to_string() const override { 112 | std::ostringstream oss; 113 | oss << "Circle[" << std::endl 114 | << " radius = " << m_radius << "," << std::endl 115 | << " center = " << m_center << std::endl 116 | << "]"; 117 | return oss.str(); 118 | } 119 | 120 | protected: 121 | Vector2f m_center; 122 | float m_radius; 123 | bool m_flipped = false; 124 | }; 125 | -------------------------------------------------------------------------------- /src/shapes/concave_segment.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | class ConcaveSegment : public Shape { 7 | public: 8 | ConcaveSegment(const Point2f &a, const Point2f &b, float arc_radius) 9 | : Shape(), m_a(a), m_b(b), m_arc_radius(arc_radius) { 10 | name = "ConcaveSegment"; 11 | 12 | Vector2f dir = m_b - m_a; 13 | float length = norm(dir); 14 | Vector2f n(-dir[1], dir[0]); 15 | n *= rcp(length); 16 | float phi = atan2(n.y(), n.x()); 17 | if (phi < 0.f) phi += 2.f*Pi; 18 | 19 | float d_phi = asin(0.5f*length*rcp(m_arc_radius)); 20 | m_phi_0 = phi - d_phi + Pi; 21 | m_phi_1 = phi + d_phi - Pi + 2.f*Pi; 22 | 23 | float offset = safe_sqrt(sqr(m_arc_radius) - sqr(0.5f*length)); 24 | m_center = 0.5f*(m_a + m_b) + n*offset; 25 | 26 | size_t K = 30; 27 | bbox = BoundingBox2f(); 28 | for (size_t k = 0; k < K; ++k) { 29 | float t = k * rcp(float(K-1)); 30 | Interaction it = sample_position(t); 31 | bbox.expand(it.p); 32 | } 33 | } 34 | 35 | Interaction sample_position(float sample) const override { 36 | float phi = m_phi_0 + (m_phi_1 - m_phi_0)*sample; 37 | auto [sp, cp] = sincos(phi); 38 | Point2f p_local(cp, sp); 39 | 40 | Interaction in; 41 | in.rayt = 0; 42 | in.p = m_center + m_arc_radius*p_local; 43 | in.n = -p_local; 44 | in.dp_du = (m_phi_1 - m_phi_0)*Vector2f(-p_local[1], p_local[0]); 45 | float inv_radius = -rcp(m_arc_radius); 46 | in.dn_du = in.dp_du*inv_radius; 47 | in.u = sample; 48 | in.s = Vector2f(-in.n[1], in.n[0]); 49 | in.ds_du = Vector2f(-in.dn_du[1], in.dn_du[0]); 50 | in.shape = this; 51 | return in; 52 | } 53 | 54 | std::tuple angle_test(const Point2f &p) const { 55 | // Compute extent of valid angles 56 | Vector2f dir = m_b - m_a; 57 | float length = norm(dir); 58 | float d_phi = asin(0.5f*length*rcp(m_arc_radius)); 59 | 60 | // Project to circle 61 | Vector2f d = normalize(p - m_center); 62 | float phi = atan2(d.y(), d.x()); 63 | if (phi < 0.f) phi += 2.f*Pi; 64 | 65 | // Rotate everything s.t. it alignes with the normal of [A, B] 66 | Vector2f n(-dir[1], dir[0]); 67 | n *= rcp(length); 68 | float phi_n = atan2(-n.y(), -n.x()); 69 | if (phi_n < 0.f) phi_n += 2.f*Pi; 70 | float phi_p = phi - phi_n; 71 | 72 | // Make sure, phi_p is in [-Pi, +Pi) 73 | phi_p = fmod(phi_p + Pi, 2.f*Pi); 74 | if (phi_p < 0.f) phi_p += 2.f*Pi; 75 | phi_p -= Pi; 76 | 77 | return std::make_tuple(phi, phi_p, d_phi); 78 | } 79 | 80 | float project(const Point2f &p) const override { 81 | auto [phi, phi_p, d_phi] = angle_test(p); 82 | if (phi_p > d_phi) return 1.f; 83 | if (phi_p < -d_phi) return 0.f; 84 | return (phi - m_phi_0) / (m_phi_1 - m_phi_0); 85 | } 86 | 87 | std::tuple ray_intersect(const Ray2f &ray) const override { 88 | float mint = ray.mint, 89 | maxt = ray.maxt; 90 | 91 | Vector2f o = Vector2f(ray.o) - m_center; 92 | Vector2f d(ray.d); 93 | 94 | float A = squared_norm(d), 95 | B = 2.0f * dot(o, d), 96 | C = squared_norm(o) - m_arc_radius*m_arc_radius; 97 | 98 | auto [solution_found, near_t, far_t] = solve_quadratic(A, B, C); 99 | 100 | bool out_bounds = !((near_t <= maxt) && (far_t >= mint)), 101 | in_bounds = (near_t < mint) && (far_t > maxt); 102 | 103 | bool valid_intersection = solution_found && (!out_bounds) && (!in_bounds); 104 | if (!valid_intersection) { 105 | return { false, Infinity, -1.f, 0 }; 106 | } 107 | 108 | if (near_t >= mint) { 109 | // Test near hit 110 | Point2f p = ray(near_t); 111 | p = m_center + normalize(p - m_center) * m_arc_radius; 112 | auto [unused, phi_p, d_phi] = angle_test(p); 113 | if (abs(phi_p) < d_phi) { 114 | return { true, near_t, -1.f, 0 }; 115 | } 116 | } 117 | // Test far hit 118 | Point2f p = ray(far_t); 119 | p = m_center + normalize(p - m_center) * m_arc_radius; 120 | auto [unused, phi_p, d_phi] = angle_test(p); 121 | if (abs(phi_p) < d_phi) { 122 | return { true, far_t, -1.f, 0 }; 123 | } 124 | 125 | return { false, Infinity, -1.f, 0 }; 126 | } 127 | 128 | Interaction fill_interaction(const Ray2f &ray, float spline_t, size_t spline_idx) const override { 129 | Interaction in; 130 | in.rayt = ray.maxt; 131 | in.p = ray(in.rayt); 132 | in.p = m_center + normalize(in.p - m_center) * m_arc_radius; 133 | in.n = -normalize(in.p - m_center); 134 | Point2f p_local = in.p - m_center; 135 | in.dp_du = (m_phi_1 - m_phi_0)*Vector2f(-p_local[1], p_local[0]); 136 | in.dn_du = -in.dp_du * rcp(m_arc_radius); 137 | in.u = project(in.p); 138 | in.s = Vector2f(-in.n[1], in.n[0]); 139 | in.ds_du = Vector2f(-in.dn_du[1], in.dn_du[0]); 140 | in.shape = this; 141 | return in; 142 | } 143 | 144 | void draw(NVGcontext *ctx, bool hole=false) const override { 145 | if (hole) return; 146 | 147 | Shape::draw(ctx); 148 | nvgSave(ctx); 149 | 150 | NVGcolor fill_color; 151 | if (type == Type::Reflection) { 152 | fill_color = COLOR_REFLECTION; 153 | } else if (type == Type::Refraction) { 154 | fill_color = COLOR_REFRACTION; 155 | } else if (type == Type::Emitter) { 156 | fill_color = COLOR_EMITTER; 157 | } else { 158 | fill_color = COLOR_DIFFUSE; 159 | } 160 | 161 | // The rounded rectangles and gradients behave inconsistently 162 | // when elements get too small in nanovg, so we work in a scaled coord. 163 | // system here. 164 | float nvg_scale = 1e2f, 165 | inv_nvg_scale = rcp(nvg_scale); 166 | nvgScale(ctx, inv_nvg_scale, inv_nvg_scale); 167 | 168 | auto nvgLinearGradientS = [&](NVGcontext *ctx, float sx, float sy, float ex, float ey, 169 | NVGcolor icolor, NVGcolor ecolor) { 170 | return nvgLinearGradient(ctx, nvg_scale*sx, nvg_scale*sy, nvg_scale*ex, nvg_scale*ey, 171 | icolor, ecolor); 172 | }; 173 | auto nvgMoveToS = [&](NVGcontext *ctx, float x, float y) { 174 | nvgMoveTo(ctx, nvg_scale*x, nvg_scale*y); 175 | }; 176 | auto nvgLineToS = [&](NVGcontext *ctx, float x, float y) { 177 | nvgLineTo(ctx, nvg_scale*x, nvg_scale*y); 178 | }; 179 | auto nvgArcS = [&](NVGcontext *ctx, float cx, float cy, float r, float a0, float a1, int dir) { 180 | nvgArc(ctx, nvg_scale*cx, nvg_scale*cy, nvg_scale*r, a0, a1, dir); 181 | }; 182 | 183 | // Another limitation of nanovg is that rectangles are always axis-aligned. 184 | // We need to do some more coordinate transforms to generalize. 185 | float phi = atan2(m_b[1] - m_a[1], m_b[0] - m_a[0]); 186 | if (phi < 0.f) phi += 2.f*Pi; 187 | auto [sp, cp] = sincos(phi); 188 | float length = norm(m_b - m_a); 189 | 190 | Matrix2f R = Matrix2f(cp, -sp, sp, cp), 191 | Ri = transpose(R), 192 | Si = Matrix2f(rcp(length)); 193 | 194 | Point2f ap = Si * Ri * m_a, 195 | bp = Si * Ri * m_b; 196 | float arc_radius = rcp(length) * m_arc_radius; 197 | 198 | nvgStrokeWidth(ctx, nvg_scale*0.007f*rcp(length)); 199 | nvgRotate(ctx, phi); 200 | nvgScale(ctx, length, length); 201 | 202 | // Set up gradients 203 | Point2f p0 = 0.5f*(ap + bp), 204 | p1 = p0 - Vector2f(0.f, gradient_start), 205 | p2 = p1 - Vector2f(0.f, gradient_width); 206 | auto fill_grad = nvgLinearGradientS(ctx, p1[0], p1[1], p2[0], p2[1], 207 | fill_color, COLOR_TRANSPARENT); 208 | auto stroke_grad = nvgLinearGradientS(ctx, p1[0], p1[1], p2[0], p2[1], 209 | nvgRGBA(0, 0, 0, 255), COLOR_TRANSPARENT); 210 | nvgFillPaint(ctx, fill_grad); 211 | nvgStrokePaint(ctx, stroke_grad); 212 | 213 | nvgBeginPath(ctx); 214 | 215 | // Main arc 216 | float offset = safe_sqrt(sqr(arc_radius) - 0.25f); 217 | Point2f c_main = Point2f(ap[0] + 0.5f, bp[1] + offset); 218 | float alpha = asin(0.5f*rcp(arc_radius)); 219 | 220 | // Smaller arcs left and right to transition smoothly to vertical 221 | Vector2f n_left = normalize(ap - c_main), 222 | n_right = normalize(bp - c_main); 223 | Point2f c_left = ap + corner_radius*n_left, 224 | c_right = bp + corner_radius*n_right; 225 | float beta_left = atan2(-n_left.y(), -n_left.x()), 226 | beta_right = atan2(-n_right.y(), -n_right.x()); 227 | if (beta_left < 0.f) beta_left += 2.f*Pi; 228 | if (beta_right < 0.f) beta_right += 2.f*Pi; 229 | 230 | // Rest of shape 231 | Point2f d_left = c_left - Vector2f(corner_radius, 0.f), 232 | e_left = d_left - Vector2f(0.f, 1.f), 233 | d_right = c_right + Vector2f(corner_radius, 0.f), 234 | e_right = d_right - Vector2f(0.f, 1.f); 235 | 236 | nvgMoveToS(ctx, d_right[0], d_right[1]); 237 | nvgLineToS(ctx, e_right[0], e_right[1]); 238 | nvgLineToS(ctx, e_left[0], e_left[1]); 239 | nvgLineToS(ctx, d_left[0], d_left[1]); 240 | 241 | if (abs(beta_left - Pi) > Epsilon) 242 | nvgArcS(ctx, c_left[0], c_left[1], corner_radius, Pi, beta_left, NVG_CCW); 243 | nvgArcS(ctx, c_main[0], c_main[1], arc_radius, 1.5f*Pi - alpha, 1.5f*Pi + alpha, NVG_CW); 244 | if (abs(beta_right) > Epsilon) 245 | nvgArcS(ctx, c_right[0], c_right[1], corner_radius, beta_right, 0.f, NVG_CCW); 246 | 247 | nvgFill(ctx); 248 | nvgStroke(ctx); 249 | 250 | nvgRestore(ctx); 251 | } 252 | 253 | std::string to_string() const override { 254 | std::ostringstream oss; 255 | oss << "ConcaveSegment[" << std::endl 256 | << " a = " << m_a << "," << std::endl 257 | << " b = " << m_b << std::endl 258 | << "]"; 259 | return oss.str(); 260 | } 261 | 262 | protected: 263 | Point2f m_a, m_b; 264 | float m_arc_radius; 265 | Point2f m_center; 266 | float m_phi_0, m_phi_1; 267 | 268 | public: 269 | float gradient_start = 0.08f, 270 | gradient_width = 0.03f; 271 | float corner_radius = 0.02f; 272 | }; 273 | -------------------------------------------------------------------------------- /src/shapes/convex_segment.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | class ConvexSegment : public Shape { 7 | public: 8 | ConvexSegment(const Point2f &a, const Point2f &b, float arc_radius, bool alt) 9 | : Shape(), m_a(a), m_b(b), m_arc_radius(arc_radius), m_alt(alt) { 10 | name = "ConvexSegment"; 11 | 12 | Vector2f dir = m_b - m_a; 13 | float length = norm(dir); 14 | Vector2f n(-dir[1], dir[0]); 15 | n *= rcp(length); 16 | float phi = atan2(n.y(), n.x()); 17 | if (phi < 0.f) phi += 2.f*Pi; 18 | 19 | float d_phi = asin(0.5f*length*rcp(m_arc_radius)); 20 | m_phi_0 = phi + d_phi; 21 | m_phi_1 = phi - d_phi; 22 | float sign = 1.f; 23 | if (alt) { 24 | sign = -1.f; 25 | m_phi_0 -= Pi; 26 | m_phi_1 += Pi; 27 | } 28 | 29 | float offset = safe_sqrt(sqr(m_arc_radius) - sqr(0.5f*length)); 30 | m_center = 0.5f*(m_a + m_b) - sign*n*offset; 31 | 32 | size_t K = 30; 33 | bbox = BoundingBox2f(); 34 | for (size_t k = 0; k < K; ++k) { 35 | float t = k * rcp(float(K-1)); 36 | Interaction it = sample_position(t); 37 | bbox.expand(it.p); 38 | } 39 | } 40 | 41 | Interaction sample_position(float sample) const override { 42 | float phi = m_phi_0 + (m_phi_1 - m_phi_0)*sample; 43 | auto [sp, cp] = sincos(phi); 44 | Point2f p_local(cp, sp); 45 | 46 | Interaction in; 47 | in.rayt = 0; 48 | in.p = m_center + m_arc_radius*p_local; 49 | in.n = p_local; 50 | in.dp_du = (m_phi_1 - m_phi_0)*Vector2f(-p_local[1], p_local[0]); 51 | float inv_radius = rcp(m_arc_radius); 52 | in.dn_du = in.dp_du*inv_radius; 53 | in.u = sample; 54 | in.s = Vector2f(-in.n[1], in.n[0]); 55 | in.ds_du = Vector2f(-in.dn_du[1], in.dn_du[0]); 56 | in.shape = this; 57 | return in; 58 | } 59 | 60 | std::tuple angle_test(const Point2f &p) const { 61 | // Compute extent of valid angles 62 | Vector2f dir = m_b - m_a; 63 | float length = norm(dir); 64 | float d_phi = asin(0.5f*length*rcp(m_arc_radius)); 65 | if (m_alt) { 66 | d_phi = Pi - d_phi; 67 | } 68 | 69 | // Project to circle 70 | Vector2f d = normalize(p - m_center); 71 | float phi = atan2(d.y(), d.x()); 72 | if (phi < 0.f) phi += 2.f*Pi; 73 | 74 | // Rotate everything s.t. it alignes with the normal of [A, B] 75 | Vector2f n(-dir[1], dir[0]); 76 | n *= rcp(length); 77 | float phi_n = atan2(n.y(), n.x()); 78 | if (phi_n < 0.f) phi_n += 2.f*Pi; 79 | float phi_p = phi - phi_n; 80 | 81 | // Make sure, phi_p is in [-Pi, +Pi) 82 | phi_p = fmod(phi_p + Pi, 2.f*Pi); 83 | if (phi_p < 0.f) phi_p += 2.f*Pi; 84 | phi_p -= Pi; 85 | 86 | return std::make_tuple(phi, phi_p, d_phi); 87 | } 88 | 89 | float project(const Point2f &p) const override { 90 | auto [phi, phi_p, d_phi] = angle_test(p); 91 | if (phi_p > d_phi) return m_alt ? 1.f : 0.f; 92 | if (phi_p < -d_phi) return m_alt ? 0.f : 1.f; 93 | return (phi - m_phi_0) / (m_phi_1 - m_phi_0); 94 | } 95 | 96 | std::tuple ray_intersect(const Ray2f &ray) const override { 97 | float mint = ray.mint, 98 | maxt = ray.maxt; 99 | 100 | Vector2f o = Vector2f(ray.o) - m_center; 101 | Vector2f d(ray.d); 102 | 103 | float A = squared_norm(d), 104 | B = 2.0f * dot(o, d), 105 | C = squared_norm(o) - m_arc_radius*m_arc_radius; 106 | 107 | auto [solution_found, near_t, far_t] = solve_quadratic(A, B, C); 108 | 109 | bool out_bounds = !((near_t <= maxt) && (far_t >= mint)), 110 | in_bounds = (near_t < mint) && (far_t > maxt); 111 | 112 | bool valid_intersection = solution_found && (!out_bounds) && (!in_bounds); 113 | if (!valid_intersection) { 114 | return { false, Infinity, -1.f, 0 }; 115 | } 116 | 117 | if (near_t >= mint) { 118 | // Test near hit 119 | Point2f p = ray(near_t); 120 | p = m_center + normalize(p - m_center) * m_arc_radius; 121 | auto [unused, phi_p, d_phi] = angle_test(p); 122 | if (abs(phi_p) < d_phi) { 123 | return { true, near_t, -1.f, 0 }; 124 | } 125 | } 126 | // Test far hit 127 | Point2f p = ray(far_t); 128 | p = m_center + normalize(p - m_center) * m_arc_radius; 129 | auto [unused, phi_p, d_phi] = angle_test(p); 130 | if (abs(phi_p) < d_phi) { 131 | return { true, far_t, -1.f, 0 }; 132 | } 133 | 134 | return { false, Infinity, -1.f, 0 }; 135 | } 136 | 137 | Interaction fill_interaction(const Ray2f &ray, float spline_t, size_t spline_idx) const override { 138 | Interaction in; 139 | in.rayt = ray.maxt; 140 | in.p = ray(in.rayt); 141 | in.p = m_center + normalize(in.p - m_center) * m_arc_radius; 142 | in.n = normalize(in.p - m_center); 143 | Point2f p_local = in.p - m_center; 144 | in.dp_du = (m_phi_1 - m_phi_0)*Vector2f(-p_local[1], p_local[0]); 145 | in.dn_du = in.dp_du * rcp(m_arc_radius); 146 | in.u = project(in.p); 147 | in.s = Vector2f(-in.n[1], in.n[0]); 148 | in.ds_du = Vector2f(-in.dn_du[1], in.dn_du[0]); 149 | in.shape = this; 150 | return in; 151 | } 152 | 153 | void draw(NVGcontext *ctx, bool hole=false) const override { 154 | if (hole) return; 155 | 156 | Shape::draw(ctx); 157 | nvgSave(ctx); 158 | 159 | NVGcolor fill_color; 160 | if (type == Type::Reflection) { 161 | fill_color = COLOR_REFLECTION; 162 | } else if (type == Type::Refraction) { 163 | fill_color = COLOR_REFRACTION; 164 | } else if (type == Type::Emitter) { 165 | fill_color = COLOR_EMITTER; 166 | } else { 167 | fill_color = COLOR_DIFFUSE; 168 | } 169 | 170 | // The rounded rectangles and gradients behave inconsistently 171 | // when elements get too small in nanovg, so we work in a scaled coord. 172 | // system here. 173 | float nvg_scale = 1e2f, 174 | inv_nvg_scale = rcp(nvg_scale); 175 | nvgScale(ctx, inv_nvg_scale, inv_nvg_scale); 176 | 177 | auto nvgLinearGradientS = [&](NVGcontext *ctx, float sx, float sy, float ex, float ey, 178 | NVGcolor icolor, NVGcolor ecolor) { 179 | return nvgLinearGradient(ctx, nvg_scale*sx, nvg_scale*sy, nvg_scale*ex, nvg_scale*ey, 180 | icolor, ecolor); 181 | }; 182 | auto nvgMoveToS = [&](NVGcontext *ctx, float x, float y) { 183 | nvgMoveTo(ctx, nvg_scale*x, nvg_scale*y); 184 | }; 185 | auto nvgLineToS = [&](NVGcontext *ctx, float x, float y) { 186 | nvgLineTo(ctx, nvg_scale*x, nvg_scale*y); 187 | }; 188 | auto nvgArcS = [&](NVGcontext *ctx, float cx, float cy, float r, float a0, float a1, int dir) { 189 | nvgArc(ctx, nvg_scale*cx, nvg_scale*cy, nvg_scale*r, a0, a1, dir); 190 | }; 191 | 192 | // Another limitation of nanovg is that rectangles are always axis-aligned. 193 | // We need to do some more coordinate transforms to generalize. 194 | float phi = atan2(m_b[1] - m_a[1], m_b[0] - m_a[0]); 195 | if (phi < 0.f) phi += 2.f*Pi; 196 | auto [sp, cp] = sincos(phi); 197 | float length = norm(m_b - m_a); 198 | 199 | Matrix2f R = Matrix2f(cp, -sp, sp, cp), 200 | Ri = transpose(R), 201 | Si = Matrix2f(rcp(length)); 202 | 203 | Point2f ap = Si * Ri * m_a, 204 | bp = Si * Ri * m_b; 205 | float arc_radius = rcp(length) * m_arc_radius; 206 | 207 | nvgStrokeWidth(ctx, nvg_scale*0.007f*rcp(length)); 208 | nvgRotate(ctx, phi); 209 | nvgScale(ctx, length, length); 210 | 211 | // Set up gradients 212 | Point2f p0 = 0.5f*(ap + bp), 213 | p1 = p0 - Vector2f(0.f, gradient_start), 214 | p2 = p1 - Vector2f(0.f, gradient_width); 215 | auto fill_grad = nvgLinearGradientS(ctx, p1[0], p1[1], p2[0], p2[1], 216 | fill_color, COLOR_TRANSPARENT); 217 | auto stroke_grad = nvgLinearGradientS(ctx, p1[0], p1[1], p2[0], p2[1], 218 | nvgRGBA(0, 0, 0, 255), COLOR_TRANSPARENT); 219 | nvgFillPaint(ctx, fill_grad); 220 | nvgStrokePaint(ctx, stroke_grad); 221 | 222 | nvgBeginPath(ctx); 223 | 224 | // Main arc 225 | float offset = safe_sqrt(sqr(arc_radius) - 0.25f); 226 | float sign = m_alt ? -1.f : 1.f; 227 | Point2f c_main = Point2f(ap[0] + 0.5f, bp[1] - sign*offset); 228 | float alpha = asin(0.5f*rcp(arc_radius)); 229 | 230 | // Smaller arcs left and right to transition smoothly to vertical 231 | Vector2f n_left = normalize(ap - c_main), 232 | n_right = normalize(bp - c_main); 233 | Point2f c_left = ap - corner_radius*n_left, 234 | c_right = bp - corner_radius*n_right; 235 | float beta_left = atan2(n_left.y(), n_left.x()), 236 | beta_right = atan2(n_right.y(), n_right.x()); 237 | if (beta_left < 0.f) beta_left += 2.f*Pi; 238 | if (beta_right < 0.f) beta_right += 2.f*Pi; 239 | 240 | if (abs(beta_right) > Epsilon && !m_alt) 241 | nvgArcS(ctx, c_right[0], c_right[1], corner_radius, 0.f, beta_right, NVG_CW); 242 | 243 | if (m_alt) { 244 | nvgArcS(ctx, c_main[0], c_main[1], arc_radius, 1.5f*Pi - alpha, 1.5f*Pi + alpha, NVG_CCW); 245 | } else { 246 | nvgArcS(ctx, c_main[0], c_main[1], arc_radius, 0.5f*Pi - alpha, 0.5f*Pi + alpha, NVG_CW); 247 | } 248 | 249 | if (abs(beta_left - Pi) > Epsilon && !m_alt) 250 | nvgArcS(ctx, c_left[0], c_left[1], corner_radius, beta_left, Pi, NVG_CW); 251 | 252 | // Rest of shape 253 | Point2f d_left = c_left - Vector2f(corner_radius, 0.f), 254 | e_left = d_left - Vector2f(0.f, 1.f), 255 | d_right = c_right + Vector2f(corner_radius, 0.f), 256 | e_right = d_right - Vector2f(0.f, 1.f); 257 | 258 | nvgMoveToS(ctx, d_left[0], d_left[1]); 259 | nvgLineToS(ctx, e_left[0], e_left[1]); 260 | nvgLineToS(ctx, e_right[0], e_right[1]); 261 | nvgLineToS(ctx, d_right[0], d_right[1]); 262 | 263 | nvgFill(ctx); 264 | nvgStroke(ctx); 265 | 266 | nvgRestore(ctx); 267 | } 268 | 269 | std::string to_string() const override { 270 | std::ostringstream oss; 271 | oss << "ConvexSegment[" << std::endl 272 | << " a = " << m_a << "," << std::endl 273 | << " b = " << m_b << std::endl 274 | << "]"; 275 | return oss.str(); 276 | } 277 | 278 | protected: 279 | Point2f m_a, m_b; 280 | float m_arc_radius; 281 | Point2f m_center; 282 | float m_phi_0, m_phi_1; 283 | bool m_alt; 284 | 285 | public: 286 | float gradient_start = 0.01f, 287 | gradient_width = 0.03f; 288 | float corner_radius = 0.02f; 289 | }; 290 | -------------------------------------------------------------------------------- /src/shapes/linear_segment.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | class LinearSegment : public Shape { 7 | public: 8 | LinearSegment(const Point2f &a, const Point2f &b) 9 | : Shape(), m_a(a), m_b(b) { 10 | name = "LinearSegment"; 11 | 12 | bbox = BoundingBox2f(); 13 | bbox.expand(m_a); 14 | bbox.expand(m_b); 15 | } 16 | 17 | Interaction sample_position(float sample) const override { 18 | Interaction in; 19 | in.rayt = 0; 20 | in.p = m_a + (m_b - m_a)*sample; 21 | Vector2f dir = normalize(m_b - m_a); 22 | in.n = Vector2f(-dir[1], dir[0]); 23 | in.dp_du = m_b - m_a; 24 | in.dn_du = Vector2f(0, 0); 25 | in.u = sample; 26 | in.s = Vector2f(-in.n[1], in.n[0]); 27 | in.ds_du = Vector2f(-in.dn_du[1], in.dn_du[0]); 28 | in.shape = this; 29 | return in; 30 | } 31 | 32 | float project(const Point2f &p) const override { 33 | Vector2f v = m_b - m_a; 34 | float scale = rcp(dot(v, v)); 35 | 36 | float t = dot((p - m_a), v)*scale; 37 | return min(1.f, max(0.f, t)); 38 | } 39 | 40 | std::tuple ray_intersect(const Ray2f &ray) const override { 41 | float mint = ray.mint, 42 | maxt = ray.maxt; 43 | 44 | Vector2f v1 = ray.o - m_a, 45 | v2 = m_b - m_a; 46 | Vector2f v3; 47 | if (cross(v1, v2) > 0) { 48 | v3 = Vector2f(ray.d[1], -ray.d[0]); 49 | } else { 50 | v3 = Vector2f(-ray.d[1], ray.d[0]); 51 | } 52 | 53 | float denom = dot(v2, v3); 54 | if (denom == 0) 55 | return { false, Infinity, -1.f, 0 }; 56 | 57 | float t1 = abs(cross(v2, v1)) / denom, 58 | t2 = dot(v1, v3) / denom; 59 | 60 | if (t1 < mint || t1 > maxt || t2 < Epsilon || t2 > (1.f + Epsilon)) 61 | return { false, Infinity, -1.f, 0 }; 62 | return { true, t1, -1.f, 0 }; 63 | } 64 | 65 | Interaction fill_interaction(const Ray2f &ray, float spline_t, size_t spline_idx) const override { 66 | Interaction in; 67 | in.rayt = ray.maxt; 68 | in.p = ray(in.rayt); 69 | Vector2f dir = normalize(m_b - m_a); 70 | in.n = Vector2f(-dir[1], dir[0]); 71 | in.dp_du = m_b - m_a; 72 | in.dn_du = Vector2f(0, 0); 73 | in.u = project(in.p); 74 | in.s = Vector2f(-in.n[1], in.n[0]); 75 | in.ds_du = Vector2f(-in.dn_du[1], in.dn_du[0]); 76 | in.shape = this; 77 | return in; 78 | } 79 | 80 | void flip() override { 81 | std::swap(m_a, m_b); 82 | } 83 | 84 | void draw(NVGcontext *ctx, bool hole=false) const override { 85 | if (hole) return; 86 | 87 | Shape::draw(ctx); 88 | nvgSave(ctx); 89 | 90 | NVGcolor fill_color; 91 | if (type == Type::Reflection) { 92 | fill_color = COLOR_REFLECTION; 93 | } else if (type == Type::Refraction) { 94 | fill_color = COLOR_REFRACTION; 95 | } else if (type == Type::Emitter) { 96 | fill_color = COLOR_EMITTER; 97 | } else { 98 | fill_color = COLOR_DIFFUSE; 99 | } 100 | 101 | // The rounded rectangles and gradients behave inconsistently 102 | // when elements get too small in nanovg, so we work in a scaled coord. 103 | // system here. 104 | float nvg_scale = 1e2f, 105 | inv_nvg_scale = rcp(nvg_scale); 106 | nvgScale(ctx, inv_nvg_scale, inv_nvg_scale); 107 | 108 | auto nvgRectS = [&](NVGcontext *ctx, float x, float y, float w, float h, float r) { 109 | nvgRoundedRectVarying(ctx, nvg_scale*x, nvg_scale*y, nvg_scale*w, nvg_scale*h, 110 | nvg_scale*r, nvg_scale*r, 0.f, 0.f); 111 | }; 112 | 113 | auto nvgLinearGradientS = [&](NVGcontext *ctx, float sx, float sy, float ex, float ey, 114 | NVGcolor icolor, NVGcolor ecolor) { 115 | return nvgLinearGradient(ctx, nvg_scale*sx, nvg_scale*sy, nvg_scale*ex, nvg_scale*ey, 116 | icolor, ecolor); 117 | }; 118 | 119 | // Another limitation of nanovg is that rectangles are always axis-aligned. 120 | // We need to do some more coordinate transforms to generalize. 121 | float phi = atan2(m_b[1] - m_a[1], m_b[0] - m_a[0]); 122 | auto [sp, cp] = sincos(phi); 123 | float length = (1.f + 2.f*corner_radius)*norm(m_b - m_a); 124 | 125 | Matrix2f R = Matrix2f(cp, -sp, sp, cp), 126 | Ri = transpose(R), 127 | Si = Matrix2f(rcp(length)); 128 | 129 | Point2f ap = Si * Ri * m_a, 130 | bp = Si * Ri * m_b; 131 | ap -= Vector2f(corner_radius, 0.f); 132 | bp -= Vector2f(corner_radius, 0.f); 133 | 134 | nvgStrokeWidth(ctx, nvg_scale*0.007f*rcp(length)); 135 | nvgRotate(ctx, phi); 136 | nvgScale(ctx, length, length); 137 | 138 | // Set up gradients 139 | Point2f p0 = 0.5f*(ap + bp), 140 | p1 = p0 - Vector2f(0.f, gradient_start), 141 | p2 = p1 - Vector2f(0.f, gradient_width); 142 | auto fill_grad = nvgLinearGradientS(ctx, p1[0], p1[1], p2[0], p2[1], 143 | fill_color, COLOR_TRANSPARENT); 144 | auto stroke_grad = nvgLinearGradientS(ctx, p1[0], p1[1], p2[0], p2[1], 145 | nvgRGBA(0, 0, 0, 255), COLOR_TRANSPARENT); 146 | nvgFillPaint(ctx, fill_grad); 147 | nvgStrokePaint(ctx, stroke_grad); 148 | 149 | nvgBeginPath(ctx); 150 | 151 | nvgRectS(ctx, ap[0], ap[1], 1.f, -height, corner_radius); 152 | nvgFill(ctx); 153 | nvgStroke(ctx); 154 | 155 | nvgRestore(ctx); 156 | } 157 | 158 | std::string to_string() const override { 159 | std::ostringstream oss; 160 | oss << "LinearSegment[" << std::endl 161 | << " a = " << m_a << "," << std::endl 162 | << " b = " << m_b << std::endl 163 | << "]"; 164 | return oss.str(); 165 | } 166 | 167 | protected: 168 | Point2f m_a, m_b; 169 | 170 | public: 171 | float height = 1.f; 172 | float gradient_start = 0.04f, 173 | gradient_width = 0.03f; 174 | float corner_radius = 0.04f; 175 | }; 176 | --------------------------------------------------------------------------------