Repository: cessen/psychopath Branch: master Commit: c5965ec8746c Files: 115 Total size: 882.7 KB Directory structure: gitextract_b8728v5v/ ├── .github/ │ └── pull_request_template.md ├── .gitignore ├── Cargo.toml ├── LICENSE.md ├── README.md ├── example_scenes/ │ ├── cornell_box.psy │ └── cube.psy ├── licenses/ │ ├── Apache-2.0.txt │ ├── GPL-2.0.txt │ ├── GPL-3.0.txt │ └── MIT.txt ├── psychoblend/ │ ├── LICENSE.md │ ├── __init__.py │ ├── assembly.py │ ├── psy_export.py │ ├── render.py │ ├── ui.py │ ├── util.py │ └── world.py ├── src/ │ ├── accel/ │ │ ├── bvh.rs │ │ ├── bvh4.rs │ │ ├── bvh_base.rs │ │ ├── light_array.rs │ │ ├── light_tree.rs │ │ ├── mod.rs │ │ └── objects_split.rs │ ├── algorithm.rs │ ├── bbox.rs │ ├── bbox4.rs │ ├── boundable.rs │ ├── camera.rs │ ├── color.rs │ ├── fp_utils.rs │ ├── hash.rs │ ├── hilbert.rs │ ├── image.rs │ ├── lerp.rs │ ├── light/ │ │ ├── distant_disk_light.rs │ │ ├── mod.rs │ │ ├── rectangle_light.rs │ │ └── sphere_light.rs │ ├── main.rs │ ├── math.rs │ ├── mis.rs │ ├── parse/ │ │ ├── basics.rs │ │ ├── data_tree.rs │ │ ├── mod.rs │ │ ├── psy.rs │ │ ├── psy_assembly.rs │ │ ├── psy_light.rs │ │ ├── psy_mesh_surface.rs │ │ └── psy_surface_shader.rs │ ├── ray.rs │ ├── renderer.rs │ ├── sampling/ │ │ ├── mod.rs │ │ └── monte_carlo.rs │ ├── scene/ │ │ ├── assembly.rs │ │ ├── mod.rs │ │ └── world.rs │ ├── shading/ │ │ ├── mod.rs │ │ └── surface_closure.rs │ ├── surface/ │ │ ├── bilinear_patch.rs │ │ ├── micropoly_batch.rs │ │ ├── mod.rs │ │ ├── triangle.rs │ │ └── triangle_mesh.rs │ ├── timer.rs │ ├── tracer.rs │ └── transform_stack.rs └── sub_crates/ ├── bvh_order/ │ ├── Cargo.toml │ ├── LICENSE.md │ ├── build.rs │ └── src/ │ └── lib.rs ├── color/ │ ├── Cargo.toml │ ├── LICENSE.md │ ├── build.rs │ └── src/ │ └── lib.rs ├── compact/ │ ├── Cargo.toml │ ├── LICENSE.md │ ├── benches/ │ │ └── bench.rs │ ├── src/ │ │ ├── fluv/ │ │ │ ├── fluv32.rs │ │ │ └── mod.rs │ │ ├── lib.rs │ │ ├── shared_exp/ │ │ │ ├── mod.rs │ │ │ ├── signed48.rs │ │ │ ├── unsigned32.rs │ │ │ └── unsigned40.rs │ │ └── unit_vec/ │ │ ├── mod.rs │ │ └── oct32.rs │ └── tests/ │ └── proptest_tests.rs ├── halton/ │ ├── Cargo.toml │ ├── LICENSE.md │ ├── build.rs │ └── src/ │ └── lib.rs ├── math3d/ │ ├── Cargo.toml │ ├── LICENSE.md │ └── src/ │ ├── lib.rs │ ├── normal.rs │ ├── point.rs │ ├── transform.rs │ └── vector.rs └── spectral_upsampling/ ├── Cargo.toml ├── LICENSE.md ├── build.rs ├── jakob_tables/ │ ├── LICENSE.txt │ ├── aces2065_1.coeff │ ├── rec2020.coeff │ └── srgb.coeff └── src/ ├── jakob.rs ├── lib.rs ├── meng/ │ ├── generate_meng_spectra_tables.py │ ├── meng_spectra_tables.rs │ ├── xyz_5nm_360_830.csv │ └── xyz_5nm_380_780.csv └── meng.rs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/pull_request_template.md ================================================ ## Sorry, but... I'm sorry you put the time and effort into creating this pull request, but I am not currently accepting contributions to Psychopath, as explained in the project's readme file. You are more than welcome to fork Psychopath and play with it, or even develop it independently into something of your own. But this repo is, at least for now, a one-man project. ================================================ FILE: .gitignore ================================================ target *.rs.bk clippy.toml # Python Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] .zedstate .vscode test_renders perf.data* ================================================ FILE: Cargo.toml ================================================ [workspace] members = [ "sub_crates/bvh_order", "sub_crates/color", "sub_crates/compact", "sub_crates/halton", "sub_crates/math3d", "sub_crates/spectral_upsampling", ] [package] name = "psychopath" version = "0.1.0" authors = ["Nathan Vegdahl "] edition = "2018" license = "GPL v3" [profile.release] debug = true [dependencies] # Crates.io dependencies base64 = "0.9" clap = "2.30" copy_in_place = "0.2.0" crossbeam = "0.3" half = "1.0" lazy_static = "1.0" nom = "5" num_cpus = "1.8" openexr = "0.7" kioku = "0.3" sobol_burley = "0.3" png_encode_mini = "0.1.2" rustc-serialize = "0.3" scoped_threadpool = "0.1" time = "0.1" glam = "0.15" fastapprox = "0.3" # Local crate dependencies [dependencies.bvh_order] path = "sub_crates/bvh_order" [dependencies.color] path = "sub_crates/color" [dependencies.compact] path = "sub_crates/compact" [dependencies.halton] path = "sub_crates/halton" [dependencies.math3d] path = "sub_crates/math3d" [dependencies.spectral_upsampling] path = "sub_crates/spectral_upsampling" ================================================ FILE: LICENSE.md ================================================ ## Psychopath With the exception of files under `psychoblend/` and `sub_crates/`, this project is licensed under the GPLv3 as follows: Copyright (c) 2020 Nathan Vegdahl This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ## Psychoblend For the license of the files under `psychoblend/`, see `psychoblend/LICENSE.md`. ## Sub-crates For the license of the files under `sub_crates/`, see the license files in each of its respective subdirectories. ================================================ FILE: README.md ================================================ # Overview Psychopath is a path tracing 3d renderer. You can read about its development at [psychopath.io](http://psychopath.io). This project is mostly just for me to have fun, learn, and play with ideas in 3d rendering. I do have vague hopes that it will eventually be useful for real things, but that's not a hard goal. Unlike many for-fun 3d rendering projects, Psychopath is being designed with production rendering in mind. I think that architecting a renderer to efficiently handle very large data sets, complex shading, motion blur, color management, etc. presents a much richer and more challenging problem space to explore than just writing a basic path tracer. ## Building Psychopath is written in [Rust](https://www.rust-lang.org), and is pretty straightforward to build except for its OpenEXR dependency. If you have OpenEXR 2.2 installed on your system such that pkg-config can find it, then as long as you have Rust (including Cargo) and a C++ compiler installed, you should be able to build Psychopath with this command at the repository root: ``` cargo build --release ``` However, if you are on an OS that doesn't have pkg-config (e.g. OSX, Windows), or you prefer to do a custom build of OpenEXR, then you will need to download and build OpenEXR yourself and specify the necessary environment variables as documented in the [OpenEXR-rs readme](https://github.com/cessen/openexr-rs/blob/master/README.md). Once those environment variables are set, then you should be able to build using the same simple cargo command above. # PsychoBlend Included in the repository is an add-on for [Blender](http://www.blender.org) called "PsychoBlend" that lets you use Psychopath for rendering in Blender. However, most Blender features are not yet supported because Psychopath itself doesn't support them yet. ## Features Supported - Polygon meshes. - Point, area, and sun lamps (exported as sphere, rectangle, and distant disc lights, respectively) - Simple materials assigned per-object. - Focal blur / DoF - Camera, transform, and deformation motion blur - Exports dupligroups with full hierarchical instancing - Limited auto-detection of instanced meshes # License See LICENSE.md for details. But the gist is: * The overall project is licensed under GPLv3. * PsychoBlend is licensed under GPLv2, for compatibility with Blender. * Most crates under the `sub_crates` directory are dual-licensed under MIT and Apache 2.0 (but with some exceptions--see each crate for its respective licenses). The intent of this scheme is to keep Psychopath itself copyleft, while allowing smaller reusable components to be licensed more liberally. # Contributing This is a personal, experimental, for-fun project, and I am specifically not looking for contributions of any kind. All PRs will be rejected without review. However, feel free to fork this into an entirely new project, or examine the code for ideas for a project of your own. ================================================ FILE: example_scenes/cornell_box.psy ================================================ Scene $Scene_fr1 { Output { Path ["test_renders/cornell_box.png"] } RenderSettings { Resolution [512 512] SamplesPerPixel [16] Seed [1] } Camera { Fov [39.449188] FocalDistance [10.620000] ApertureRadius [0.000000] Transform [1.000000 -0.000000 0.000000 0.000000 -0.000000 0.000000 1.000000 0.000000 0.000000 1.000000 -0.000000 0.000000 -2.779998 -8.000000 2.730010 1.000000] } World { BackgroundShader { Type [Color] Color [rec709, 0.000000 0.000000 0.000000] } } Shaders { SurfaceShader $Green { Type [Lambert] Color [rec709, 0.117000 0.412500 0.115000] } SurfaceShader $Red { Type [Lambert] Color [rec709, 0.611000 0.055500 0.062000] } SurfaceShader $White { Type [Lambert] Color [rec709, 0.729500 0.735500 0.729000] } } Objects { RectangleLight $__Area { Color [rec709, 84.300003 53.800003 18.500000] Dimensions [1.350000 1.100000] } MeshSurface $__Plane.010_ { SurfaceShaderBind [$White] Vertices [-2.649998 2.959996 3.299997 -4.229996 2.469997 3.299997 -3.139998 4.559995 3.299997 -4.719996 4.059995 3.299997 -4.719996 4.059996 0.000000 -3.139998 4.559995 0.000000 -4.229996 2.469997 0.000000 -2.649998 2.959997 0.000000 ] FaceVertCounts [4 4 4 4 4 ] FaceVertIndices [0 1 3 2 1 0 7 6 3 1 6 4 2 3 4 5 0 2 5 7 ] } MeshSurface $__Plane.008_ { SurfaceShaderBind [$White] Vertices [-1.299999 0.649999 1.649998 -0.820000 2.249998 1.649999 -2.899997 1.139998 1.649999 -2.399998 2.719997 1.649999 -1.299999 0.649999 0.000000 -0.820000 2.249998 0.000000 -2.899997 1.139998 0.000000 -2.399998 2.719997 0.000000 ] FaceVertCounts [4 4 4 4 4 ] FaceVertIndices [0 2 3 1 3 2 6 7 1 3 7 5 0 1 5 4 2 0 4 6 ] } MeshSurface $__Plane.006_ { SurfaceShaderBind [$Red] Vertices [-5.495996 5.591994 0.000000 -5.527995 -0.000001 -0.000000 -5.559996 5.591993 5.487995 -5.559995 -0.000001 5.487995 ] FaceVertCounts [4 ] FaceVertIndices [0 1 3 2 ] } MeshSurface $__Plane.004_ { SurfaceShaderBind [$Green] Vertices [-0.000001 5.591995 0.000000 0.000000 0.000000 0.000000 -0.000001 5.591994 5.487995 0.000000 -0.000000 5.487995 ] FaceVertCounts [4 ] FaceVertIndices [1 0 2 3 ] } MeshSurface $__Plane.002_ { SurfaceShaderBind [$White] Vertices [-5.495996 5.591994 0.000000 -0.000001 5.591995 0.000000 -5.559996 5.591993 5.487995 -0.000001 5.591994 5.487995 ] FaceVertCounts [4 ] FaceVertIndices [0 1 3 2 ] } MeshSurface $__Plane.001_ { SurfaceShaderBind [$White] Vertices [-5.559996 5.591993 5.487995 -0.000001 5.591994 5.487995 -5.559995 -0.000001 5.487995 0.000000 -0.000000 5.487995 -3.429997 3.319996 5.487995 -2.129998 3.319996 5.487995 -3.429997 2.269997 5.487995 -2.129998 2.269997 5.487995 ] FaceVertCounts [4 4 4 4 ] FaceVertIndices [1 5 4 0 0 4 6 2 2 6 7 3 7 5 1 3 ] } MeshSurface $__Plane_ { SurfaceShaderBind [$White] Vertices [-5.495996 5.591994 0.000000 -0.000001 5.591995 0.000000 -5.527995 -0.000001 -0.000000 0.000000 0.000000 0.000000 ] FaceVertCounts [4 ] FaceVertIndices [0 1 3 2 ] } } Assembly { Instance { Data [$__Area] Transform [1.000000 -0.000000 0.000000 -0.000000 -0.000000 1.000000 -0.000000 0.000000 0.000000 -0.000000 1.000000 -0.000000 2.779475 -2.794788 -5.498045 1.000000] } Instance { Data [$__Plane.010_] Transform [1.000000 -0.000000 0.000000 -0.000000 -0.000000 1.000000 -0.000000 0.000000 0.000000 -0.000000 1.000000 -0.000000 -0.000000 0.000000 -0.000000 1.000000] } Instance { Data [$__Plane.008_] Transform [1.000000 -0.000000 0.000000 -0.000000 -0.000000 1.000000 -0.000000 0.000000 0.000000 -0.000000 1.000000 -0.000000 -0.000000 0.000000 -0.000000 1.000000] } Instance { Data [$__Plane.006_] Transform [1.000000 -0.000000 0.000000 -0.000000 -0.000000 1.000000 -0.000000 0.000000 0.000000 -0.000000 1.000000 -0.000000 -0.000000 0.000000 -0.000000 1.000000] } Instance { Data [$__Plane.004_] Transform [1.000000 -0.000000 0.000000 -0.000000 -0.000000 1.000000 -0.000000 0.000000 0.000000 -0.000000 1.000000 -0.000000 -0.000000 0.000000 -0.000000 1.000000] } Instance { Data [$__Plane.002_] Transform [1.000000 -0.000000 0.000000 -0.000000 -0.000000 1.000000 -0.000000 0.000000 0.000000 -0.000000 1.000000 -0.000000 -0.000000 0.000000 -0.000000 1.000000] } Instance { Data [$__Plane.001_] Transform [1.000000 -0.000000 0.000000 -0.000000 -0.000000 1.000000 -0.000000 0.000000 0.000000 -0.000000 1.000000 -0.000000 -0.000000 0.000000 -0.000000 1.000000] } Instance { Data [$__Plane_] Transform [1.000000 -0.000000 0.000000 -0.000000 -0.000000 1.000000 -0.000000 0.000000 0.000000 -0.000000 1.000000 -0.000000 -0.000000 0.000000 -0.000000 1.000000] } } } ================================================ FILE: example_scenes/cube.psy ================================================ Scene $Scene_fr1 { Output { Path ["test_renders/cube.png"] } RenderSettings { Resolution [960 540] SamplesPerPixel [16] Seed [1] } Camera { Fov [49.134342] FocalDistance [9.559999] ApertureRadius [0.250000] Transform [0.685881 0.727634 -0.010817 0.000000 -0.317370 0.312469 0.895343 0.000000 -0.654862 0.610666 -0.445245 0.000000 7.481132 -6.507640 5.343665 1.000000] } World { BackgroundShader { Type [Color] Color [rec709, 0.050876 0.050876 0.050876] } } Shaders { SurfaceShader $Material { Type [Lambert] Color [rec709, 0.800000 0.800000 0.800000] } } Objects { MeshSurface $__Plane_ { SurfaceShaderBind [$Material] Vertices [-1.000000 -1.000000 0.000000 1.000000 -1.000000 0.000000 -1.000000 1.000000 0.000000 1.000000 1.000000 0.000000] FaceVertCounts [4 ] FaceVertIndices [0 1 3 2 ] } MeshSurface $__Cube_ { SurfaceShaderBind [$Material] Vertices [1.000000 1.000000 -1.000000 1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 1.000000 -1.000000 1.000000 0.999999 1.000000 0.999999 -1.000001 1.000000 -1.000000 -1.000000 1.000000 -1.000000 1.000000 1.000000 ] FaceVertCounts [4 4 4 4 4 4 ] FaceVertIndices [0 1 2 3 4 7 6 5 0 4 5 1 1 5 6 2 2 6 7 3 4 0 3 7 ] } SphereLight $__Lamp { Color [rec709, 50.000000 50.000000 50.000000] Radius [0.100000] } } Assembly { Instance { Data [$__Plane_] Transform [0.078868 -0.000000 0.000000 -0.000000 -0.000000 0.078868 -0.000000 0.000000 0.000000 -0.000000 0.078868 -0.000000 -0.000000 0.000000 -0.000000 1.000000] } Instance { Data [$__Cube_] Transform [1.000000 -0.000000 0.000000 -0.000000 -0.000000 1.000000 -0.000000 0.000000 0.000000 -0.000000 1.000000 -0.000000 -0.000000 0.000000 -1.000000 1.000000] } Instance { Data [$__Lamp] Transform [0.019856 -0.060763 0.000000 -0.000000 0.015191 0.079422 -0.000000 0.000000 0.000000 -0.000000 1.000000 -0.000000 -0.026851 -0.125233 -4.432303 1.000000] } } } ================================================ FILE: licenses/Apache-2.0.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: licenses/GPL-2.0.txt ================================================ GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. ================================================ FILE: licenses/GPL-3.0.txt ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: licenses/MIT.txt ================================================ Copyright (c) 2020 Nathan Vegdahl Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: psychoblend/LICENSE.md ================================================ Copyright (c) 2020 Nathan Vegdahl This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. ================================================ FILE: psychoblend/__init__.py ================================================ bl_info = { "name": "PsychoBlend", "version": (0, 1), "author": "Nathan Vegdahl", "blender": (2, 70, 0), "description": "Psychopath renderer integration", "location": "", "wiki_url": "https://github.com/cessen/psychopath/wiki", "tracker_url": "https://github.com/cessen/psychopath/issues", "category": "Render"} if "bpy" in locals(): import imp imp.reload(ui) imp.reload(psy_export) imp.reload(render) else: from . import ui, psy_export, render import bpy from bpy.types import (AddonPreferences, PropertyGroup, Operator, ) from bpy.props import (StringProperty, BoolProperty, IntProperty, FloatProperty, FloatVectorProperty, EnumProperty, PointerProperty, ) # Custom Scene settings class RenderPsychopathSettingsScene(PropertyGroup): spp = IntProperty( name="Samples Per Pixel", description="Total number of samples to take per pixel", min=1, max=65536, default=16 ) max_samples_per_bucket = IntProperty( name="Max Samples Per Bucket", description="How many samples to simultaneously calculate per thread; indirectly determines bucket size", min=1, max=2**28, soft_max=2**16, default=4096 ) dicing_rate = FloatProperty( name="Dicing Rate", description="The target microgeometry width in pixels", min=0.0001, max=100.0, soft_min=0.125, soft_max=1.0, default=0.25 ) motion_blur_segments = IntProperty( name="Motion Segments", description="The number of segments to use in motion blur. Zero means no motion blur. Will be rounded down to the nearest power of two.", min=0, max=256, default=0 ) shutter_start = FloatProperty( name="Shutter Open", description="The time during the frame that the shutter opens, for motion blur", min=-1.0, max=1.0, soft_min=0.0, soft_max=1.0, default=0.0 ) shutter_end = FloatProperty( name="Shutter Close", description="The time during the frame that the shutter closes, for motion blur", min=-1.0, max=1.0, soft_min=0.0, soft_max=1.0, default=0.5 ) export_path = StringProperty( name="Export Path", description="The path to where the .psy files should be exported when rendering. If left blank, /tmp or the equivalent is used.", subtype='FILE_PATH' ) # Custom Camera properties class PsychopathCamera(bpy.types.PropertyGroup): aperture_radius = FloatProperty( name="Aperture Radius", description="Size of the camera's aperture, for DoF", min=0.0, max=10000.0, soft_min=0.0, soft_max=2.0, default=0.0 ) # Psychopath material class PsychopathLight(bpy.types.PropertyGroup): color_type = EnumProperty( name="Color Type", description="", items=[ ('Rec709', 'Rec709', ""), ('Blackbody', 'Blackbody', ""), ('ColorTemperature', 'ColorTemperature', "Same as Blackbody, except with brightness kept more even."), ], default="Rec709" ) color_blackbody_temp = FloatProperty( name="Temperature", description="Blackbody temperature in kelvin", min=0.0, soft_min=800.0, soft_max=6500.0, default=1200.0 ) # Custom Mesh properties class PsychopathMesh(bpy.types.PropertyGroup): is_subdivision_surface = BoolProperty( name="Is Subdivision Surface", description="Whether this is a sibdivision surface or just a normal mesh", default=False ) # Psychopath material class PsychopathMaterial(bpy.types.PropertyGroup): surface_shader_type = EnumProperty( name="Surface Shader Type", description="", items=[('Emit', 'Emit', ""), ('Lambert', 'Lambert', ""), ('GGX', 'GGX', "")], default="Lambert" ) color_type = EnumProperty( name="Color Type", description="", items=[ ('Rec709', 'Rec709', ""), ('Blackbody', 'Blackbody', ""), ('ColorTemperature', 'ColorTemperature', "Same as Blackbody, except with brightness kept more even."), ], default="Rec709" ) color = FloatVectorProperty( name="Color", description="", subtype='COLOR', min=0.0, soft_min=0.0, soft_max = 1.0, default=[0.8,0.8,0.8] ) color_blackbody_temp = FloatProperty( name="Temperature", description="Blackbody temperature in kelvin", min=0.0, soft_min=800.0, soft_max=6500.0, default=1200.0 ) roughness = FloatProperty( name="Roughness", description="", min=-1.0, max=1.0, soft_min=0.0, soft_max=1.0, default=0.1 ) tail_shape = FloatProperty( name="Tail Shape", description="", min=0.0, max=8.0, soft_min=1.0, soft_max=3.0, default=2.0 ) fresnel = FloatProperty( name="Fresnel", description="", min=0.0, max=1.0, soft_min=0.0, soft_max=1.0, default=0.9 ) # Addon Preferences class PsychopathPreferences(AddonPreferences): bl_idname = __name__ filepath_psychopath = StringProperty( name="Psychopath Location", description="Path to renderer executable", subtype='DIR_PATH', ) def draw(self, context): layout = self.layout layout.prop(self, "filepath_psychopath") ##### REGISTER ##### def register(): bpy.utils.register_class(PsychopathPreferences) bpy.utils.register_class(RenderPsychopathSettingsScene) bpy.utils.register_class(PsychopathCamera) bpy.utils.register_class(PsychopathLight) bpy.utils.register_class(PsychopathMesh) bpy.utils.register_class(PsychopathMaterial) bpy.types.Scene.psychopath = PointerProperty(type=RenderPsychopathSettingsScene) bpy.types.Camera.psychopath = PointerProperty(type=PsychopathCamera) bpy.types.Lamp.psychopath = PointerProperty(type=PsychopathLight) bpy.types.Mesh.psychopath = PointerProperty(type=PsychopathMesh) bpy.types.Material.psychopath = PointerProperty(type=PsychopathMaterial) render.register() ui.register() def unregister(): bpy.utils.unregister_class(PsychopathPreferences) bpy.utils.unregister_class(RenderPsychopathSettingsScene) bpy.utils.unregister_class(PsychopathCamera) bpy.utils.unregister_class(PsychopathLight) bpy.utils.unregister_class(PsychopathMesh) bpy.utils.unregister_class(PsychopathMaterial) del bpy.types.Scene.psychopath del bpy.types.Camera.psychopath del bpy.types.Lamp.psychopath del bpy.types.Mesh.psychopath del bpy.types.Material.psychopath render.unregister() ui.unregister() ================================================ FILE: psychoblend/assembly.py ================================================ import bpy from .util import escape_name, mat2str, needs_def_mb, needs_xform_mb, ExportCancelled class Assembly: def __init__(self, render_engine, objects, visible_layers, group_prefix="", translation_offset=(0,0,0)): self.name = group_prefix self.translation_offset = translation_offset self.render_engine = render_engine self.materials = [] self.objects = [] self.instances = [] self.material_names = set() self.mesh_names = set() self.assembly_names = set() # Collect all the objects, materials, instances, etc. for ob in objects: # Check if render is cancelled if render_engine.test_break(): raise ExportCancelled() # Check if the object is visible for rendering vis_layer = False for i in range(len(ob.layers)): vis_layer = vis_layer or (ob.layers[i] and visible_layers[i]) if ob.hide_render or not vis_layer: continue # Store object data name = None if ob.type == 'EMPTY': if ob.dupli_type == 'GROUP': name = group_prefix + "__" + escape_name(ob.dupli_group.name) if name not in self.assembly_names: self.assembly_names.add(name) self.objects += [Assembly(self.render_engine, ob.dupli_group.objects, ob.dupli_group.layers, name, ob.dupli_group.dupli_offset*-1)] elif ob.type == 'MESH': name = self.get_mesh(ob, group_prefix) elif ob.type == 'LAMP' and ob.data.type == 'POINT': name = self.get_sphere_lamp(ob, group_prefix) elif ob.type == 'LAMP' and ob.data.type == 'AREA': name = self.get_rect_lamp(ob, group_prefix) # Store instance if name != None: self.instances += [Instance(render_engine, ob, name)] def export(self, render_engine, w): if self.name == "": w.write("Assembly {\n") else: w.write("Assembly $%s {\n" % self.name) w.indent() for mat in self.materials: # Check if render is cancelled if render_engine.test_break(): raise ExportCancelled() mat.export(render_engine, w) for ob in self.objects: # Check if render is cancelled if render_engine.test_break(): raise ExportCancelled() ob.export(render_engine, w) for inst in self.instances: # Check if render is cancelled if render_engine.test_break(): raise ExportCancelled() inst.export(render_engine, w) w.unindent() w.write("}\n") #---------------- def take_sample(self, render_engine, scene, time): for mat in self.materials: # Check if render is cancelled if render_engine.test_break(): raise ExportCancelled() mat.take_sample(render_engine, scene, time) for ob in self.objects: # Check if render is cancelled if render_engine.test_break(): raise ExportCancelled() ob.take_sample(render_engine, scene, time) for inst in self.instances: # Check if render is cancelled if render_engine.test_break(): raise ExportCancelled() inst.take_sample(render_engine, time, self.translation_offset) def cleanup(self): for mat in self.materials: mat.cleanup() for ob in self.objects: ob.cleanup() def get_mesh(self, ob, group_prefix): # Figure out if we need to export or not and figure out what name to # export with. has_modifiers = len(ob.modifiers) > 0 deform_mb = needs_def_mb(ob) if has_modifiers or deform_mb: mesh_name = group_prefix + escape_name("__" + ob.name + "__" + ob.data.name + "_") else: mesh_name = group_prefix + escape_name("__" + ob.data.name + "_") has_faces = len(ob.data.polygons) > 0 should_export_mesh = has_faces and (mesh_name not in self.mesh_names) # Get mesh if should_export_mesh: self.mesh_names.add(mesh_name) self.objects += [Mesh(self.render_engine, ob, mesh_name)] # Get materials for ms in ob.material_slots: if ms != None: if ms.material.name not in self.material_names: self.material_names.add(ms.material.name) self.materials += [Material(self.render_engine, ms.material)] return mesh_name else: return None def get_sphere_lamp(self, ob, group_prefix): name = group_prefix + "__" + escape_name(ob.name) self.objects += [SphereLamp(self.render_engine, ob, name)] return name def get_rect_lamp(self, ob, group_prefix): name = group_prefix + "__" + escape_name(ob.name) self.objects += [RectLamp(self.render_engine, ob, name)] return name #========================================================================= class Mesh: """ Holds data for a mesh to be exported. """ def __init__(self, render_engine, ob, name): self.ob = ob self.name = name self.needs_mb = needs_def_mb(self.ob) self.time_meshes = [] def take_sample(self, render_engine, scene, time): if len(self.time_meshes) == 0 or self.needs_mb: render_engine.update_stats("", "Psychopath: Collecting '{}' at time {}".format(self.ob.name, time)) self.time_meshes += [self.ob.to_mesh(scene, True, 'RENDER')] def cleanup(self): for mesh in self.time_meshes: bpy.data.meshes.remove(mesh) def export(self, render_engine, w): render_engine.update_stats("", "Psychopath: Exporting %s" % self.ob.name) if self.ob.data.psychopath.is_subdivision_surface == False: # Exporting normal mesh w.write("MeshSurface $%s {\n" % self.name) w.indent() else: # Exporting subdivision surface cage w.write("SubdivisionSurface $%s {\n" % self.name) w.indent() # Write vertices and (if it's smooth shaded) normals for ti in range(len(self.time_meshes)): w.write("Vertices [") w.write(" ".join([("%f" % i) for vert in self.time_meshes[ti].vertices for i in vert.co]), False) w.write("]\n", False) if self.time_meshes[0].polygons[0].use_smooth and self.ob.data.psychopath.is_subdivision_surface == False: w.write("Normals [") w.write(" ".join([("%f" % i) for vert in self.time_meshes[ti].vertices for i in vert.normal]), False) w.write("]\n", False) # Write face vertex counts w.write("FaceVertCounts [") w.write(" ".join([("%d" % len(p.vertices)) for p in self.time_meshes[0].polygons]), False) w.write("]\n", False) # Write face vertex indices w.write("FaceVertIndices [") w.write(" ".join([("%d"%v) for p in self.time_meshes[0].polygons for v in p.vertices]), False) w.write("]\n", False) # MeshSurface/SubdivisionSurface section end w.unindent() w.write("}\n") class SphereLamp: """ Holds data for a sphere light to be exported. """ def __init__(self, render_engine, ob, name): self.ob = ob self.name = name self.time_col = [] self.time_rad = [] def take_sample(self, render_engine, scene, time): render_engine.update_stats("", "Psychopath: Collecting '{}' at time {}".format(self.ob.name, time)) if self.ob.data.psychopath.color_type == 'Rec709': self.time_col += [('Rec709', self.ob.data.color * self.ob.data.energy)] elif self.ob.data.psychopath.color_type == 'Blackbody': self.time_col += [('Blackbody', self.ob.data.psychopath.color_blackbody_temp, self.ob.data.energy)] elif self.ob.data.psychopath.color_type == 'ColorTemperature': self.time_col += [('ColorTemperature', self.ob.data.psychopath.color_blackbody_temp, self.ob.data.energy)] self.time_rad += [self.ob.data.shadow_soft_size] def cleanup(self): pass def export(self, render_engine, w): render_engine.update_stats("", "Psychopath: Exporting %s" % self.ob.name) w.write("SphereLight $%s {\n" % self.name) w.indent() for col in self.time_col: if col[0] == 'Rec709': w.write("Color [rec709, %f %f %f]\n" % (col[1][0], col[1][1], col[1][2])) elif col[0] == 'Blackbody': w.write("Color [blackbody, %f %f]\n" % (col[1], col[2])) elif col[0] == 'ColorTemperature': w.write("Color [color_temperature, %f %f]\n" % (col[1], col[2])) for rad in self.time_rad: w.write("Radius [%f]\n" % rad) w.unindent() w.write("}\n") class RectLamp: """ Holds data for a rectangular light to be exported. """ def __init__(self, render_engine, ob, name): self.ob = ob self.name = name self.time_col = [] self.time_dim = [] def take_sample(self, render_engine, scene, time): render_engine.update_stats("", "Psychopath: Collecting '{}' at time {}".format(self.ob.name, time)) if self.ob.data.psychopath.color_type == 'Rec709': self.time_col += [('Rec709', self.ob.data.color * self.ob.data.energy)] elif self.ob.data.psychopath.color_type == 'Blackbody': self.time_col += [('Blackbody', self.ob.data.psychopath.color_blackbody_temp, self.ob.data.energy)] elif self.ob.data.psychopath.color_type == 'ColorTemperature': self.time_col += [('ColorTemperature', self.ob.data.psychopath.color_blackbody_temp, self.ob.data.energy)] if self.ob.data.shape == 'RECTANGLE': self.time_dim += [(self.ob.data.size, self.ob.data.size_y)] else: self.time_dim += [(self.ob.data.size, self.ob.data.size)] def cleanup(self): pass def export(self, render_engine, w): render_engine.update_stats("", "Psychopath: Exporting %s" % self.ob.name) w.write("RectangleLight $%s {\n" % self.name) w.indent() for col in self.time_col: if col[0] == 'Rec709': w.write("Color [rec709, %f %f %f]\n" % (col[1][0], col[1][1], col[1][2])) elif col[0] == 'Blackbody': w.write("Color [blackbody, %f %f]\n" % (col[1], col[2])) elif col[0] == 'ColorTemperature': w.write("Color [color_temperature, %f %f]\n" % (col[1], col[2])) for dim in self.time_dim: w.write("Dimensions [%f %f]\n" % dim) w.unindent() w.write("}\n") class Instance: def __init__(self, render_engine, ob, data_name): self.ob = ob self.data_name = data_name self.needs_mb = needs_xform_mb(self.ob) self.time_xforms = [] def take_sample(self, render_engine, time, translation_offset): if len(self.time_xforms) == 0 or self.needs_mb: render_engine.update_stats("", "Psychopath: Collecting '{}' xforms at time {}".format(self.ob.name, time)) mat = self.ob.matrix_world.copy() mat[0][3] += translation_offset[0] mat[1][3] += translation_offset[1] mat[2][3] += translation_offset[2] self.time_xforms += [mat] def export(self, render_engine, w): render_engine.update_stats("", "Psychopath: Exporting %s" % self.ob.name) w.write("Instance {\n") w.indent() w.write("Data [$%s]\n" % self.data_name) for mat in self.time_xforms: w.write("Transform [%s]\n" % mat2str(mat.inverted())) for ms in self.ob.material_slots: if ms != None: w.write("SurfaceShaderBind [$%s]\n" % escape_name(ms.material.name)) break w.unindent() w.write("}\n") class Material: def __init__(self, render_engine, material): self.mat = material def take_sample(self, render_engine, time, translation_offset): # TODO: motion blur of material settings pass def export(self, render_engine, w): render_engine.update_stats("", "Psychopath: Exporting %s" % self.mat.name) w.write("SurfaceShader $%s {\n" % escape_name(self.mat.name)) w.indent() if self.mat.psychopath.surface_shader_type == 'Emit': w.write("Type [Emit]\n") if self.mat.psychopath.color_type == 'Rec709': col = self.mat.psychopath.color w.write("Color [rec709, %f %f %f]\n" % ( col[0], col[1], col[2], )) elif self.mat.psychopath.color_type == 'Blackbody': w.write("Color [blackbody, %f %f]\n" % ( self.mat.psychopath.color_blackbody_temp, 1.0, )) elif self.mat.psychopath.color_type == 'ColorTemperature': w.write("Color [color_temperature, %f %f]\n" % ( self.mat.psychopath.color_blackbody_temp, 1.0, )) elif self.mat.psychopath.surface_shader_type == 'Lambert': w.write("Type [Lambert]\n") if self.mat.psychopath.color_type == 'Rec709': col = self.mat.psychopath.color w.write("Color [rec709, %f %f %f]\n" % ( col[0], col[1], col[2], )) elif self.mat.psychopath.color_type == 'Blackbody': w.write("Color [blackbody, %f %f]\n" % ( self.mat.psychopath.color_blackbody_temp, 1.0, )) elif self.mat.psychopath.color_type == 'ColorTemperature': w.write("Color [color_temperature, %f %f]\n" % ( self.mat.psychopath.color_blackbody_temp, 1.0, )) elif self.mat.psychopath.surface_shader_type == 'GGX': w.write("Type [GGX]\n") if self.mat.psychopath.color_type == 'Rec709': col = self.mat.psychopath.color w.write("Color [rec709, %f %f %f]\n" % ( col[0], col[1], col[2], )) elif self.mat.psychopath.color_type == 'Blackbody': w.write("Color [blackbody, %f %f]\n" % ( self.mat.psychopath.color_blackbody_temp, 1.0, )) elif self.mat.psychopath.color_type == 'ColorTemperature': w.write("Color [color_temperature, %f %f]\n" % ( self.mat.psychopath.color_blackbody_temp, 1.0, )) w.write("Roughness [%f]\n" % self.mat.psychopath.roughness) w.write("Fresnel [%f]\n" % self.mat.psychopath.fresnel) else: raise "Unsupported surface shader type '%s'" % self.mat.psychopath.surface_shader_type w.unindent() w.write("}\n") def cleanup(self): pass ================================================ FILE: psychoblend/psy_export.py ================================================ import bpy from math import log from .assembly import Assembly from .util import escape_name, mat2str, ExportCancelled from .world import World class IndentedWriter: def __init__(self, file_handle): self.f = file_handle self.indent_level = 0 self.indent_size = 4 def indent(self): self.indent_level += self.indent_size def unindent(self): self.indent_level -= self.indent_size if self.indent_level < 0: self.indent_level = 0 def write(self, text, do_indent=True): if do_indent: self.f.write(bytes(' '*self.indent_level + text, "utf-8")) else: self.f.write(bytes(text, "utf-8")) class PsychoExporter: def __init__(self, f, render_engine, scene): self.w = IndentedWriter(f) self.render_engine = render_engine self.scene = scene self.mesh_names = {} self.group_names = {} # Motion blur segments are rounded down to a power of two if scene.psychopath.motion_blur_segments > 0: self.time_samples = (2**int(log(scene.psychopath.motion_blur_segments, 2))) + 1 else: self.time_samples = 1 # pre-calculate useful values for exporting motion blur self.shutter_start = scene.psychopath.shutter_start self.shutter_diff = (scene.psychopath.shutter_end - scene.psychopath.shutter_start) / max(1, (self.time_samples-1)) self.fr = scene.frame_current def set_frame(self, frame, fraction): if fraction >= 0: self.scene.frame_set(frame, fraction) else: self.scene.frame_set(frame-1, 1.0+fraction) def export_psy(self): try: self._export_psy() except ExportCancelled: # Cleanup self.scene.frame_set(self.fr) return False else: # Cleanup self.scene.frame_set(self.fr) return True def _export_psy(self): # Info self.w.write("# Exported from Blender 2.7x\n") # Scene begin self.w.write("\n\nScene $%s_fr%d {\n" % (escape_name(self.scene.name), self.fr)) self.w.indent() ####################### # Output section begin self.w.write("Output {\n") self.w.indent() self.w.write('Path [""]\n') # Output section end self.w.unindent() self.w.write("}\n") ############################### # RenderSettings section begin self.w.write("RenderSettings {\n") self.w.indent() res_x = int(self.scene.render.resolution_x * (self.scene.render.resolution_percentage / 100)) res_y = int(self.scene.render.resolution_y * (self.scene.render.resolution_percentage / 100)) self.w.write('Resolution [%d %d]\n' % (res_x, res_y)) self.w.write("SamplesPerPixel [%d]\n" % self.scene.psychopath.spp) self.w.write("DicingRate [%f]\n" % self.scene.psychopath.dicing_rate) self.w.write('Seed [%d]\n' % self.fr) # RenderSettings section end self.w.unindent() self.w.write("}\n") ############################### # Export world and object data world = None root_assembly = None try: # Prep for data collection world = World(self.render_engine, self.scene, self.scene.layers, float(res_x) / float(res_y)) root_assembly = Assembly(self.render_engine, self.scene.objects, self.scene.layers) # Collect data for each time sample for i in range(self.time_samples): time = self.fr + self.shutter_start + (self.shutter_diff*i) self.set_frame(self.fr, self.shutter_start + (self.shutter_diff*i)) world.take_sample(self.render_engine, self.scene, time) root_assembly.take_sample(self.render_engine, self.scene, time) # Export collected data world.export(self.render_engine, self.w) root_assembly.export(self.render_engine, self.w) finally: if world != None: world.cleanup() if root_assembly != None: root_assembly.cleanup() # Scene end self.w.unindent() self.w.write("}\n") ================================================ FILE: psychoblend/render.py ================================================ import bpy import time import os import subprocess import base64 import struct from . import psy_export class PsychopathRender(bpy.types.RenderEngine): bl_idname = 'PSYCHOPATH_RENDER' bl_label = "Psychopath" DELAY = 1.0 @staticmethod def _locate_binary(): addon_prefs = bpy.context.user_preferences.addons[__package__].preferences # Use the system preference if its set. psy_binary = addon_prefs.filepath_psychopath if psy_binary: if os.path.exists(psy_binary): return psy_binary else: print("User Preference to psychopath %r NOT FOUND, checking $PATH" % psy_binary) # search the path all os's psy_binary_default = "psychopath" os_path_ls = os.getenv("PATH").split(':') + [""] for dir_name in os_path_ls: psy_binary = os.path.join(dir_name, psy_binary_default) if os.path.exists(psy_binary): return psy_binary return "" def _start_psychopath(self, scene, psy_filepath, use_stdin, crop): psy_binary = PsychopathRender._locate_binary() if not psy_binary: print("Psychopath: could not execute psychopath, possibly Psychopath isn't installed") return False # Figure out command line options args = [] if crop != None: args += ["--crop", str(crop[0]), str(self.size_y - crop[3]), str(crop[2] - 1), str(self.size_y - crop[1] - 1)] if use_stdin: args += ["--spb", str(scene.psychopath.max_samples_per_bucket), "--serialized_output", "--use_stdin"] else: args += ["--spb", str(scene.psychopath.max_samples_per_bucket), "--serialized_output", "-i", psy_filepath] # Start Rendering! try: self._process = subprocess.Popen([psy_binary] + args, bufsize=1, stdin=subprocess.PIPE, stdout=subprocess.PIPE) except OSError: # TODO, report api print("Psychopath: could not execute '%s'" % psy_binary) import traceback traceback.print_exc() print ("***-DONE-***") return False return True def _draw_bucket(self, crop, bucket_info, pixels_encoded): if crop != None: x = bucket_info[0] - crop[0] y = self.size_y - bucket_info[3] - crop[1] else: x = bucket_info[0] y = self.size_y - bucket_info[3] width = bucket_info[2] - bucket_info[0] height = bucket_info[3] - bucket_info[1] # Decode pixel data pixels = [p for p in struct.iter_unpack("ffff", base64.b64decode(pixels_encoded))] pixels_flipped = [] for i in range(height): n = height - i - 1 pixels_flipped += pixels[n*width:(n+1)*width] # Write pixel data to render image result = self.begin_result(x, y, width, height) lay = result.layers[0].passes["Combined"] lay.rect = pixels_flipped self.end_result(result) def render(self, scene): self._process = None try: self._render(scene) except: if self._process != None: self._process.terminate() raise def _render(self, scene): # has to be called to update the frame on exporting animations scene.frame_set(scene.frame_current) export_path = scene.psychopath.export_path.strip() use_stdin = False r = scene.render # compute resolution self.size_x = int(r.resolution_x * r.resolution_percentage / 100) self.size_y = int(r.resolution_y * r.resolution_percentage / 100) # Calculate border cropping, if any. if scene.render.use_border == True: minx = r.resolution_x * scene.render.border_min_x * (r.resolution_percentage / 100) miny = r.resolution_y * scene.render.border_min_y * (r.resolution_percentage / 100) maxx = r.resolution_x * scene.render.border_max_x * (r.resolution_percentage / 100) maxy = r.resolution_y * scene.render.border_max_y * (r.resolution_percentage / 100) crop = (int(minx), int(miny), int(maxx), int(maxy)) else: crop = None # Are we using an output file or standard in/out? if export_path != "": export_path += "_%d.psy" % scene.frame_current else: # We'll write directly to Psychopath's stdin use_stdin = True if use_stdin: # Start rendering if not self._start_psychopath(scene, export_path, use_stdin, crop): self.update_stats("", "Psychopath: Not found") return self.update_stats("", "Psychopath: Collecting...") # Export to Psychopath's stdin if not psy_export.PsychoExporter(self._process.stdin, self, scene).export_psy(): # Render cancelled in the middle of exporting, # so just return. self._process.terminate() return self._process.stdin.write(bytes("__PSY_EOF__", "utf-8")) self._process.stdin.flush() else: # Export to file self.update_stats("", "Psychopath: Exporting data from Blender") with open(export_path, 'w+b') as f: if not psy_export.PsychoExporter(f, self, scene).export_psy(): # Render cancelled in the middle of exporting, # so just return. return # Start rendering self.update_stats("", "Psychopath: Rendering from %s" % export_path) if not self._start_psychopath(scene, export_path, use_stdin, crop): self.update_stats("", "Psychopath: Not found") return self.update_stats("", "Psychopath: Building") # If we can, make the render process's stdout non-blocking. The # benefit of this is that canceling the render won't block waiting # for the next piece of input. try: import fcntl fd = self._process.stdout.fileno() fl = fcntl.fcntl(fd, fcntl.F_GETFL) fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) except: print("NOTE: Can't make Psychopath's stdout non-blocking, so canceling renders may take a moment to respond.") # Process output from rendering process reached_first_bucket = False output = b"" render_process_finished = False all_output_consumed = False while not (render_process_finished and all_output_consumed): if self._process.poll() != None: render_process_finished = True # Check for render cancel if self.test_break(): self._process.terminate() break # Get render output from stdin tmp = self._process.stdout.read1(2**16) if len(tmp) == 0: time.sleep(0.0001) # Don't spin on the CPU if render_process_finished: all_output_consumed = True continue output += tmp outputs = output.split(b'DIV\n') # Skip render process output until we hit the first bucket. # (The stuff before it is just informational printouts.) if not reached_first_bucket: if len(outputs) > 1: reached_first_bucket = True outputs = outputs[1:] else: continue self.update_stats("", "Psychopath: Rendering") # Clear output buffer, since it's all in 'outputs' now. output = b"" # Process buckets for bucket in outputs: if len(bucket) == 0: continue if bucket[-11:] == b'BUCKET_END\n': # Parse bucket text contents = bucket.split(b'\n') percentage = contents[0] bucket_info = [int(i) for i in contents[1].split(b' ')] pixels = contents[2] # Draw the bucket self._draw_bucket(crop, bucket_info, pixels) # Update render progress bar try: progress = float(percentage[:-1]) except ValueError: pass finally: self.update_progress(progress/100) else: output += bucket def register(): bpy.utils.register_class(PsychopathRender) def unregister(): bpy.utils.unregister_class(PsychopathRender) ================================================ FILE: psychoblend/ui.py ================================================ import bpy # Use some of the existing buttons. from bl_ui import properties_render properties_render.RENDER_PT_render.COMPAT_ENGINES.add('PSYCHOPATH_RENDER') properties_render.RENDER_PT_dimensions.COMPAT_ENGINES.add('PSYCHOPATH_RENDER') properties_render.RENDER_PT_output.COMPAT_ENGINES.add('PSYCHOPATH_RENDER') del properties_render from bl_ui import properties_data_camera properties_data_camera.DATA_PT_lens.COMPAT_ENGINES.add('PSYCHOPATH_RENDER') properties_data_camera.DATA_PT_camera.COMPAT_ENGINES.add('PSYCHOPATH_RENDER') properties_data_camera.DATA_PT_camera_display.COMPAT_ENGINES.add('PSYCHOPATH_RENDER') properties_data_camera.DATA_PT_custom_props_camera.COMPAT_ENGINES.add('PSYCHOPATH_RENDER') del properties_data_camera class PsychopathPanel(): COMPAT_ENGINES = {'PSYCHOPATH_RENDER'} @classmethod def poll(cls, context): rd = context.scene.render return (rd.use_game_engine is False) and (rd.engine in cls.COMPAT_ENGINES) class RENDER_PT_psychopath_render_settings(PsychopathPanel, bpy.types.Panel): bl_label = "Render Settings" bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' bl_context = "render" def draw(self, context): scene = context.scene layout = self.layout col = layout.column() col.label(text="Sampling") col.prop(scene.psychopath, "spp") col.label(text="Dicing") col.prop(scene.psychopath, "dicing_rate") col.label(text="Motion Blur") col.prop(scene.psychopath, "motion_blur_segments") col.prop(scene.psychopath, "shutter_start") col.prop(scene.psychopath, "shutter_end") col.label(text="Performance") col.prop(scene.psychopath, "max_samples_per_bucket") class RENDER_PT_psychopath_export_settings(PsychopathPanel, bpy.types.Panel): bl_label = "Export Settings" bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' bl_context = "render" def draw(self, context): scene = context.scene layout = self.layout col = layout.column() col.prop(scene.psychopath, "export_path") class WORLD_PT_psychopath_background(PsychopathPanel, bpy.types.Panel): bl_label = "Background" bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' bl_context = "world" @classmethod def poll(cls, context): return context.world and PsychopathPanel.poll(context) def draw(self, context): layout = self.layout world = context.world layout.prop(world, "horizon_color", text="Color") class DATA_PT_psychopath_camera_dof(PsychopathPanel, bpy.types.Panel): bl_label = "Depth of Field" bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' bl_context = "data" @classmethod def poll(cls, context): engine = context.scene.render.engine return context.camera and PsychopathPanel.poll(context) def draw(self, context): ob = context.active_object layout = self.layout col = layout.column() col.prop(ob.data, "dof_object") col.prop(ob.data, "dof_distance") col.prop(ob.data.psychopath, "aperture_radius") class DATA_PT_psychopath_lamp(PsychopathPanel, bpy.types.Panel): bl_label = "Lamp" bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' bl_context = "data" @classmethod def poll(cls, context): engine = context.scene.render.engine return context.lamp and PsychopathPanel.poll(context) def draw(self, context): ob = context.active_object layout = self.layout col = layout.column() row = col.row() row.prop(ob.data, "type", expand=True) if ob.data.type != 'HEMI' and ob.data.type != 'AREA': col.prop(ob.data, "shadow_soft_size") col.prop(ob.data.psychopath, "color_type") if ob.data.psychopath.color_type == 'Rec709': col.prop(ob.data, "color") elif ob.data.psychopath.color_type == 'Blackbody' or ob.data.psychopath.color_type == 'ColorTemperature': col.prop(ob.data.psychopath, "color_blackbody_temp") col.prop(ob.data, "energy") class DATA_PT_psychopath_area_lamp(PsychopathPanel, bpy.types.Panel): bl_label = "Area Shape" bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' bl_context = "data" @classmethod def poll(cls, context): lamp = context.lamp engine = context.scene.render.engine return (lamp and lamp.type == 'AREA') and (engine in cls.COMPAT_ENGINES) def draw(self, context): layout = self.layout lamp = context.lamp col = layout.column() col.row().prop(lamp, "shape", expand=True) sub = col.row(align=True) if lamp.shape == 'SQUARE': sub.prop(lamp, "size") elif lamp.shape == 'RECTANGLE': sub.prop(lamp, "size", text="Size X") sub.prop(lamp, "size_y", text="Size Y") class DATA_PT_psychopath_mesh(PsychopathPanel, bpy.types.Panel): bl_label = "Psychopath Mesh Properties" bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' bl_context = "data" @classmethod def poll(cls, context): engine = context.scene.render.engine return context.mesh and (engine in cls.COMPAT_ENGINES) def draw(self, context): layout = self.layout mesh = context.mesh layout.row().prop(mesh.psychopath, "is_subdivision_surface") class MATERIAL_PT_psychopath_context_material(PsychopathPanel, bpy.types.Panel): bl_label = "" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "material" bl_options = {'HIDE_HEADER'} @classmethod def poll(cls, context): return (context.material or context.object) and PsychopathPanel.poll(context) def draw(self, context): layout = self.layout mat = context.material ob = context.object slot = context.material_slot space = context.space_data if ob: row = layout.row() row.template_list("MATERIAL_UL_matslots", "", ob, "material_slots", ob, "active_material_index", rows=1) col = row.column(align=True) col.operator("object.material_slot_add", icon='ZOOMIN', text="") col.operator("object.material_slot_remove", icon='ZOOMOUT', text="") col.menu("MATERIAL_MT_specials", icon='DOWNARROW_HLT', text="") if ob.mode == 'EDIT': row = layout.row(align=True) row.operator("object.material_slot_assign", text="Assign") row.operator("object.material_slot_select", text="Select") row.operator("object.material_slot_deselect", text="Deselect") split = layout.split(percentage=0.65) if ob: split.template_ID(ob, "active_material", new="material.new") row = split.row() if slot: row.prop(slot, "link", text="") else: row.label() elif mat: split.template_ID(space, "pin_id") split.separator() class MATERIAL_PT_psychopath_surface(PsychopathPanel, bpy.types.Panel): bl_label = "Surface" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "material" @classmethod def poll(cls, context): return context.material and PsychopathPanel.poll(context) def draw(self, context): layout = self.layout col = layout.column() mat = context.material col.prop(mat.psychopath, "surface_shader_type") col.prop(mat.psychopath, "color_type") if mat.psychopath.color_type == 'Rec709': col.prop(mat.psychopath, "color") elif mat.psychopath.color_type == 'Blackbody' or mat.psychopath.color_type == 'ColorTemperature': col.prop(mat.psychopath, "color_blackbody_temp") if mat.psychopath.surface_shader_type == 'GTR': layout.prop(mat.psychopath, "roughness") layout.prop(mat.psychopath, "tail_shape") layout.prop(mat.psychopath, "fresnel") if mat.psychopath.surface_shader_type == 'GGX': layout.prop(mat.psychopath, "roughness") layout.prop(mat.psychopath, "fresnel") def register(): bpy.utils.register_class(RENDER_PT_psychopath_render_settings) bpy.utils.register_class(RENDER_PT_psychopath_export_settings) bpy.utils.register_class(WORLD_PT_psychopath_background) bpy.utils.register_class(DATA_PT_psychopath_camera_dof) bpy.utils.register_class(DATA_PT_psychopath_mesh) bpy.utils.register_class(DATA_PT_psychopath_lamp) bpy.utils.register_class(DATA_PT_psychopath_area_lamp) bpy.utils.register_class(MATERIAL_PT_psychopath_context_material) bpy.utils.register_class(MATERIAL_PT_psychopath_surface) def unregister(): bpy.utils.unregister_class(RENDER_PT_psychopath_render_settings) bpy.utils.unregister_class(RENDER_PT_psychopath_export_settings) bpy.utils.unregister_class(WORLD_PT_psychopath_background) bpy.utils.unregister_class(DATA_PT_psychopath_camera_dof) bpy.utils.register_class(DATA_PT_psychopath_mesh) bpy.utils.unregister_class(DATA_PT_psychopath_lamp) bpy.utils.unregister_class(DATA_PT_psychopath_area_lamp) bpy.utils.unregister_class(MATERIAL_PT_psychopath_context_material) bpy.utils.unregister_class(MATERIAL_PT_psychopath_surface) ================================================ FILE: psychoblend/util.py ================================================ class ExportCancelled(Exception): """ Indicates that the render was cancelled in the middle of exporting the scene file. """ pass def mat2str(m): """ Converts a matrix into a single-line string of values. """ s = "" for j in range(4): for i in range(4): s += (" %f" % m[i][j]) return s[1:] def needs_def_mb(ob): """ Determines if the given object needs to be exported with deformation motion blur or not. """ anim = ob.animation_data no_anim_data = anim == None or (anim.action == None and len(anim.nla_tracks) == 0 and len(anim.drivers) == 0) for mod in ob.modifiers: if mod.type == 'SUBSURF': pass elif mod.type == 'MULTIRES': pass elif mod.type == 'MIRROR': if mod.mirror_object == None: pass else: return True elif mod.type == 'BEVEL' and no_anim_data: pass elif mod.type == 'EDGE_SPLIT' and no_anim_data: pass elif mod.type == 'SOLIDIFY' and no_anim_data: pass elif mod.type == 'MASK' and no_anim_data: pass elif mod.type == 'REMESH' and no_anim_data: pass elif mod.type == 'TRIANGULATE' and no_anim_data: pass elif mod.type == 'WIREFRAME' and no_anim_data: pass else: return True if ob.type == 'MESH': if ob.data.shape_keys == None: pass else: return True return False def escape_name(name): name = name.replace("\\", "\\\\") name = name.replace(" ", "\\ ") name = name.replace("$", "\\$") name = name.replace("[", "\\[") name = name.replace("]", "\\]") name = name.replace("{", "\\{") name = name.replace("}", "\\}") return name def needs_xform_mb(ob): """ Determines if the given object needs to be exported with transformation motion blur or not. """ if ob.animation_data != None: return True if len(ob.constraints) > 0: return True if ob.parent != None: return needs_xform_mb(ob.parent) return False ================================================ FILE: psychoblend/world.py ================================================ import bpy from math import degrees, tan, atan from mathutils import Vector, Matrix from .util import escape_name, mat2str, ExportCancelled class World: def __init__(self, render_engine, scene, visible_layers, aspect_ratio): self.background_shader = BackgroundShader(render_engine, scene.world) self.camera = Camera(render_engine, scene.camera, aspect_ratio) self.lights = [] # Collect infinite-extent light sources. # TODO: also get sun lamps inside group instances. for ob in scene.objects: if ob.type == 'LAMP' and ob.data.type == 'SUN': name = escape_name(ob.name) self.lights += [DistantDiskLamp(ob, name)] def take_sample(self, render_engine, scene, time): self.camera.take_sample(render_engine, scene, time) for light in self.lights: # Check if render is cancelled if render_engine.test_break(): raise ExportCancelled() light.take_sample(render_engine, scene, time) def export(self, render_engine, w): self.camera.export(render_engine, w) w.write("World {\n") w.indent() self.background_shader.export(render_engine, w) for light in self.lights: light.export(render_engine, w) w.unindent() w.write("}\n") def cleanup(self): # For future use. This is run by the calling code when finished, # even if export did not succeed. pass #================================================================ class Camera: def __init__(self, render_engine, ob, aspect_ratio): self.ob = ob self.aspect_ratio = aspect_ratio self.fovs = [] self.aperture_radii = [] self.focal_distances = [] self.xforms = [] def take_sample(self, render_engine, scene, time): render_engine.update_stats("", "Psychopath: Collecting '{}' at time {}".format(self.ob.name, time)) # Fov if self.aspect_ratio >= 1.0: self.fovs += [degrees(self.ob.data.angle)] else: self.fovs += [degrees(2.0 * atan(tan(self.ob.data.angle * 0.5) * self.aspect_ratio))] # Aperture radius self.aperture_radii += [self.ob.data.psychopath.aperture_radius] # Dof distance if self.ob.data.dof_object == None: self.focal_distances += [self.ob.data.dof_distance] else: # TODO: implement DoF object tracking here self.focal_distances += [0.0] print("WARNING: DoF object tracking not yet implemented.") # Transform mat = self.ob.matrix_world.copy() matz = Matrix() matz[2][2] = -1 self.xforms += [mat * matz] def export(self, render_engine, w): render_engine.update_stats("", "Psychopath: Exporting %s" % self.ob.name) w.write("Camera {\n") w.indent() for fov in self.fovs: w.write("Fov [%f]\n" % fov) for rad in self.aperture_radii: w.write("ApertureRadius [%f]\n" % rad) for dist in self.focal_distances: w.write("FocalDistance [%f]\n" % dist) for mat in self.xforms: w.write("Transform [%s]\n" % mat2str(mat)) w.unindent() w.write("}\n") class BackgroundShader: def __init__(self, render_engine, world): self.world = world if self.world != None: self.color = (world.horizon_color[0], world.horizon_color[1], world.horizon_color[2]) def export(self, render_engine, w): if self.world != None: w.write("BackgroundShader {\n") w.indent(); w.write("Type [Color]\n") w.write("Color [rec709, %f %f %f]\n" % self.color) w.unindent() w.write("}\n") class DistantDiskLamp: def __init__(self, ob, name): self.ob = ob self.name = name self.time_col = [] self.time_dir = [] self.time_rad = [] def take_sample(self, render_engine, scene, time): render_engine.update_stats("", "Psychopath: Collecting '{}' at time {}".format(self.ob.name, time)) self.time_dir += [tuple(self.ob.matrix_world.to_3x3() * Vector((0, 0, -1)))] if self.ob.data.psychopath.color_type == 'Rec709': self.time_col += [('Rec709', self.ob.data.color * self.ob.data.energy)] elif self.ob.data.psychopath.color_type == 'Blackbody': self.time_col += [('Blackbody', self.ob.data.psychopath.color_blackbody_temp, self.ob.data.energy)] elif self.ob.data.psychopath.color_type == 'ColorTemperature': self.time_col += [('ColorTemperature', self.ob.data.psychopath.color_blackbody_temp, self.ob.data.energy)] self.time_rad += [self.ob.data.shadow_soft_size] def export(self, render_engine, w): render_engine.update_stats("", "Psychopath: Exporting %s" % self.ob.name) w.write("DistantDiskLight $%s {\n" % self.name) w.indent() for direc in self.time_dir: w.write("Direction [%f %f %f]\n" % (direc[0], direc[1], direc[2])) for col in self.time_col: if col[0] == 'Rec709': w.write("Color [rec709, %f %f %f]\n" % (col[1][0], col[1][1], col[1][2])) elif col[0] == 'Blackbody': w.write("Color [blackbody, %f %f]\n" % (col[1], col[2])) elif col[0] == 'ColorTemperature': w.write("Color [color_temperature, %f %f]\n" % (col[1], col[2])) for rad in self.time_rad: w.write("Radius [%f]\n" % rad) w.unindent() w.write("}\n") ================================================ FILE: src/accel/bvh.rs ================================================ #![allow(dead_code)] use mem_arena::MemArena; use crate::{ algorithm::partition, bbox::BBox, boundable::Boundable, lerp::lerp_slice, ray::AccelRay, timer::Timer, }; use super::{ bvh_base::{BVHBase, BVHBaseNode, BVH_MAX_DEPTH}, ACCEL_NODE_RAY_TESTS, ACCEL_TRAV_TIME, }; #[derive(Copy, Clone, Debug)] pub struct BVH<'a> { root: Option<&'a BVHNode<'a>>, depth: usize, } #[derive(Copy, Clone, Debug)] pub enum BVHNode<'a> { Internal { bounds_len: u16, split_axis: u8, bounds_start: &'a BBox, children: (&'a BVHNode<'a>, &'a BVHNode<'a>), }, Leaf { bounds_start: &'a BBox, bounds_len: u16, object_range: (usize, usize), }, } impl<'a> BVH<'a> { pub fn from_objects<'b, T, F>( arena: &'a MemArena, objects: &mut [T], objects_per_leaf: usize, bounder: F, ) -> BVH<'a> where F: 'b + Fn(&T) -> &'b [BBox], { if objects.is_empty() { BVH { root: None, depth: 0, } } else { let base = BVHBase::from_objects(objects, objects_per_leaf, bounder); BVH { root: Some(BVH::construct_from_base( arena, &base, base.root_node_index(), )), depth: base.depth, } } } pub fn tree_depth(&self) -> usize { self.depth } pub fn traverse(&self, rays: &mut [AccelRay], objects: &[T], mut obj_ray_test: F) where F: FnMut(&T, &mut [AccelRay]), { if self.root.is_none() { return; } let mut timer = Timer::new(); let mut trav_time: f64 = 0.0; let mut node_tests: u64 = 0; let ray_sign = [ rays[0].dir_inv.x() >= 0.0, rays[0].dir_inv.y() >= 0.0, rays[0].dir_inv.z() >= 0.0, ]; // +2 of max depth for root and last child let mut node_stack = [self.root.unwrap(); BVH_MAX_DEPTH + 2]; let mut ray_i_stack = [rays.len(); BVH_MAX_DEPTH + 2]; let mut stack_ptr = 1; while stack_ptr > 0 { node_tests += ray_i_stack[stack_ptr] as u64; match *node_stack[stack_ptr] { BVHNode::Internal { children, bounds_start, bounds_len, split_axis, } => { let bounds = unsafe { std::slice::from_raw_parts(bounds_start, bounds_len as usize) }; let part = partition(&mut rays[..ray_i_stack[stack_ptr]], |r| { (!r.is_done()) && lerp_slice(bounds, r.time).intersect_accel_ray(r) }); if part > 0 { ray_i_stack[stack_ptr] = part; ray_i_stack[stack_ptr + 1] = part; if ray_sign[split_axis as usize] { node_stack[stack_ptr] = children.1; node_stack[stack_ptr + 1] = children.0; } else { node_stack[stack_ptr] = children.0; node_stack[stack_ptr + 1] = children.1; } stack_ptr += 1; } else { stack_ptr -= 1; } } BVHNode::Leaf { object_range, bounds_start, bounds_len, } => { let bounds = unsafe { std::slice::from_raw_parts(bounds_start, bounds_len as usize) }; let part = partition(&mut rays[..ray_i_stack[stack_ptr]], |r| { (!r.is_done()) && lerp_slice(bounds, r.time).intersect_accel_ray(r) }); trav_time += timer.tick() as f64; if part > 0 { for obj in &objects[object_range.0..object_range.1] { obj_ray_test(obj, &mut rays[..part]); } } timer.tick(); stack_ptr -= 1; } } } trav_time += timer.tick() as f64; ACCEL_TRAV_TIME.with(|att| { let v = att.get(); att.set(v + trav_time); }); ACCEL_NODE_RAY_TESTS.with(|anv| { let v = anv.get(); anv.set(v + node_tests); }); } #[allow(clippy::mut_from_ref)] fn construct_from_base( arena: &'a MemArena, base: &BVHBase, node_index: usize, ) -> &'a mut BVHNode<'a> { match base.nodes[node_index] { BVHBaseNode::Internal { bounds_range, children_indices, split_axis, } => { let node = unsafe { arena.alloc_uninitialized_with_alignment::(32) }; let bounds = arena .copy_slice_with_alignment(&base.bounds[bounds_range.0..bounds_range.1], 32); let child1 = BVH::construct_from_base(arena, base, children_indices.0); let child2 = BVH::construct_from_base(arena, base, children_indices.1); *node = BVHNode::Internal { bounds_len: bounds.len() as u16, split_axis: split_axis, bounds_start: &bounds[0], children: (child1, child2), }; node } BVHBaseNode::Leaf { bounds_range, object_range, } => { let node = unsafe { arena.alloc_uninitialized::() }; let bounds = arena.copy_slice(&base.bounds[bounds_range.0..bounds_range.1]); *node = BVHNode::Leaf { bounds_start: &bounds[0], bounds_len: bounds.len() as u16, object_range: object_range, }; node } } } } lazy_static! { static ref DEGENERATE_BOUNDS: [BBox; 1] = [BBox::new()]; } impl<'a> Boundable for BVH<'a> { fn bounds(&self) -> &[BBox] { match self.root { None => &DEGENERATE_BOUNDS[..], Some(root) => match *root { BVHNode::Internal { bounds_start, bounds_len, .. } | BVHNode::Leaf { bounds_start, bounds_len, .. } => unsafe { std::slice::from_raw_parts(bounds_start, bounds_len as usize) }, }, } } } ================================================ FILE: src/accel/bvh4.rs ================================================ //! This BVH4 implementation is based on the ideas from the paper //! "Efficient Ray Tracing Kernels for Modern CPU Architectures" //! by Fuetterling et al. #![allow(dead_code)] use std::mem::{transmute, MaybeUninit}; use glam::BVec4A; use kioku::Arena; use crate::{ bbox::BBox, bbox4::BBox4, boundable::Boundable, lerp::lerp_slice, math::Vector, ray::{RayBatch, RayStack}, }; use super::{ bvh_base::{BVHBase, BVHBaseNode, BVH_MAX_DEPTH}, ACCEL_NODE_RAY_TESTS, }; use bvh_order::{calc_traversal_code, SplitAxes, TRAVERSAL_TABLE}; pub fn ray_code(dir: Vector) -> usize { let ray_sign_is_neg = [dir.x() < 0.0, dir.y() < 0.0, dir.z() < 0.0]; ray_sign_is_neg[0] as usize + ((ray_sign_is_neg[1] as usize) << 1) + ((ray_sign_is_neg[2] as usize) << 2) } #[derive(Copy, Clone, Debug)] pub struct BVH4<'a> { root: Option<&'a BVH4Node<'a>>, depth: usize, node_count: usize, _bounds: Option<&'a [BBox]>, } #[derive(Copy, Clone, Debug)] pub enum BVH4Node<'a> { Internal { bounds: &'a [BBox4], children: &'a [BVH4Node<'a>], traversal_code: u8, }, Leaf { object_range: (usize, usize), }, } impl<'a> BVH4<'a> { pub fn from_objects<'b, T, F>( arena: &'a Arena, objects: &mut [T], objects_per_leaf: usize, bounder: F, ) -> BVH4<'a> where F: 'b + Fn(&T) -> &'b [BBox], { if objects.is_empty() { BVH4 { root: None, depth: 0, node_count: 0, _bounds: None, } } else { let base = BVHBase::from_objects(objects, objects_per_leaf, bounder); let fill_node = arena.alloc_align_uninit::(32); let node_count = BVH4::construct_from_base( arena, &base, &base.nodes[base.root_node_index()], fill_node, ); BVH4 { root: Some(unsafe { transmute(fill_node) }), depth: (base.depth / 2) + 1, node_count: node_count, _bounds: { let range = base.nodes[base.root_node_index()].bounds_range(); Some(arena.copy_slice(&base.bounds[range.0..range.1])) }, } } } pub fn tree_depth(&self) -> usize { self.depth } pub fn traverse(&self, rays: &mut RayBatch, ray_stack: &mut RayStack, mut obj_ray_test: F) where F: FnMut(std::ops::Range, &mut RayBatch, &mut RayStack), { if self.root.is_none() { return; } let mut node_tests: u64 = 0; let traversal_table = &TRAVERSAL_TABLE[ray_code(rays.dir_inv_local(ray_stack.next_task_ray_idx(0)))]; // +2 of max depth for root and last child let mut node_stack = [self.root.unwrap(); (BVH_MAX_DEPTH * 3) + 2]; let mut stack_ptr = 1; while stack_ptr > 0 { match *node_stack[stack_ptr] { BVH4Node::Internal { bounds, children, traversal_code, } => { node_tests += ray_stack.ray_count_in_next_task() as u64; let mut all_hits = BVec4A::default(); // Ray testing ray_stack.pop_do_next_task_and_push_rays(children.len(), |ray_idx| { if rays.is_done(ray_idx) { BVec4A::default() } else { let hits = if bounds.len() == 1 { bounds[0].intersect_ray( rays.orig_local(ray_idx), rays.dir_inv_local(ray_idx), rays.max_t(ray_idx), ) } else { lerp_slice(bounds, rays.time(ray_idx)).intersect_ray( rays.orig_local(ray_idx), rays.dir_inv_local(ray_idx), rays.max_t(ray_idx), ) }; all_hits |= hits; hits } }); // If there were any intersections, create tasks. if all_hits.any() { let order_code = traversal_table[traversal_code as usize]; let mut lane_count = 0; let mut i = children.len() as u8; while i > 0 { i -= 1; let child_i = ((order_code >> (i * 2)) & 3) as usize; if ray_stack.push_lane_to_task(child_i) { node_stack[stack_ptr + lane_count] = &children[child_i]; lane_count += 1; } } stack_ptr += lane_count - 1; } else { stack_ptr -= 1; } } BVH4Node::Leaf { object_range } => { // Do the ray tests. obj_ray_test(object_range.0..object_range.1, rays, ray_stack); stack_ptr -= 1; } } } ACCEL_NODE_RAY_TESTS.with(|anv| { let v = anv.get(); anv.set(v + node_tests); }); } fn construct_from_base( arena: &'a Arena, base: &BVHBase, node: &BVHBaseNode, fill_node: &mut MaybeUninit>, ) -> usize { let mut node_count = 0; match *node { // Create internal node BVHBaseNode::Internal { children_indices, split_axis, .. } => { let child_l = &base.nodes[children_indices.0]; let child_r = &base.nodes[children_indices.1]; // Prepare convenient access to the stuff we need. let child_count: usize; let children; // [Optional, Optional, Optional, Optional] let split_info: SplitAxes; match *child_l { BVHBaseNode::Internal { children_indices: i_l, split_axis: s_l, .. } => { match *child_r { BVHBaseNode::Internal { children_indices: i_r, split_axis: s_r, .. } => { // Four nodes child_count = 4; children = [ Some(&base.nodes[i_l.0]), Some(&base.nodes[i_l.1]), Some(&base.nodes[i_r.0]), Some(&base.nodes[i_r.1]), ]; split_info = SplitAxes::Full((split_axis, s_l, s_r)); } BVHBaseNode::Leaf { .. } => { // Three nodes with left split child_count = 3; children = [ Some(&base.nodes[i_l.0]), Some(&base.nodes[i_l.1]), Some(child_r), None, ]; split_info = SplitAxes::Left((split_axis, s_l)); } } } BVHBaseNode::Leaf { .. } => { match *child_r { BVHBaseNode::Internal { children_indices: i_r, split_axis: s_r, .. } => { // Three nodes with right split child_count = 3; children = [ Some(child_l), Some(&base.nodes[i_r.0]), Some(&base.nodes[i_r.1]), None, ]; split_info = SplitAxes::Right((split_axis, s_r)); } BVHBaseNode::Leaf { .. } => { // Two nodes child_count = 2; children = [Some(child_l), Some(child_r), None, None]; split_info = SplitAxes::TopOnly(split_axis); } } } } node_count += child_count; // Construct bounds let bounds = { let bounds_len = children .iter() .map(|c| { if let Some(n) = *c { let len = n.bounds_range().1 - n.bounds_range().0; debug_assert!(len >= 1); len } else { 0 } }) .max() .unwrap(); debug_assert!(bounds_len >= 1); let bounds = arena.alloc_array_align_uninit(bounds_len, 32); if bounds_len < 2 { let b1 = children[0].map_or(BBox::new(), |c| base.bounds[c.bounds_range().0]); let b2 = children[1].map_or(BBox::new(), |c| base.bounds[c.bounds_range().0]); let b3 = children[2].map_or(BBox::new(), |c| base.bounds[c.bounds_range().0]); let b4 = children[3].map_or(BBox::new(), |c| base.bounds[c.bounds_range().0]); unsafe { *bounds[0].as_mut_ptr() = BBox4::from_bboxes(b1, b2, b3, b4); } } else { for (i, b) in bounds.iter_mut().enumerate() { let time = i as f32 / (bounds_len - 1) as f32; let b1 = children[0].map_or(BBox::new(), |c| { let (x, y) = c.bounds_range(); lerp_slice(&base.bounds[x..y], time) }); let b2 = children[1].map_or(BBox::new(), |c| { let (x, y) = c.bounds_range(); lerp_slice(&base.bounds[x..y], time) }); let b3 = children[2].map_or(BBox::new(), |c| { let (x, y) = c.bounds_range(); lerp_slice(&base.bounds[x..y], time) }); let b4 = children[3].map_or(BBox::new(), |c| { let (x, y) = c.bounds_range(); lerp_slice(&base.bounds[x..y], time) }); unsafe { *b.as_mut_ptr() = BBox4::from_bboxes(b1, b2, b3, b4); } } } bounds }; // Construct child nodes let child_nodes = arena.alloc_array_align_uninit::(child_count, 32); for (i, c) in children[0..child_count].iter().enumerate() { node_count += BVH4::construct_from_base(arena, base, c.unwrap(), &mut child_nodes[i]); } // Build this node unsafe { *fill_node.as_mut_ptr() = BVH4Node::Internal { bounds: transmute(bounds), children: transmute(child_nodes), traversal_code: calc_traversal_code(split_info), }; } } // Create internal node BVHBaseNode::Leaf { object_range, .. } => { unsafe { *fill_node.as_mut_ptr() = BVH4Node::Leaf { object_range: object_range, }; } node_count += 1; } } return node_count; } } impl<'a> Boundable for BVH4<'a> { fn bounds<'b>(&'b self) -> &'b [BBox] { self._bounds.unwrap_or(&[]) } } ================================================ FILE: src/accel/bvh_base.rs ================================================ #![allow(dead_code)] use crate::{algorithm::merge_slices_append, bbox::BBox, lerp::lerp_slice, math::log2_64}; use super::objects_split::{median_split, sah_split}; pub const BVH_MAX_DEPTH: usize = 42; // Amount bigger the union of all time samples can be // and still use the union rather than preserve the // individual time samples. const USE_UNION_FACTOR: f32 = 1.4; /// An intermediary structure for creating a BVH. #[derive(Debug)] pub struct BVHBase { pub nodes: Vec, pub bounds: Vec, pub depth: usize, bounds_cache: Vec, } #[derive(Copy, Clone, Debug)] pub enum BVHBaseNode { Internal { bounds_range: (usize, usize), children_indices: (usize, usize), split_axis: u8, }, Leaf { bounds_range: (usize, usize), object_range: (usize, usize), }, } impl BVHBaseNode { pub fn bounds_range(&self) -> (usize, usize) { match *self { BVHBaseNode::Internal { bounds_range, .. } | BVHBaseNode::Leaf { bounds_range, .. } => { bounds_range } } } } impl BVHBase { fn new() -> BVHBase { BVHBase { nodes: Vec::new(), bounds: Vec::new(), depth: 0, bounds_cache: Vec::new(), } } pub fn from_objects<'b, T, F>(objects: &mut [T], objects_per_leaf: usize, bounder: F) -> BVHBase where F: 'b + Fn(&T) -> &'b [BBox], { let mut bvh = BVHBase::new(); bvh.recursive_build(0, 0, objects_per_leaf, objects, &bounder); bvh } pub fn root_node_index(&self) -> usize { 0 } fn acc_bounds<'a, T, F>(&mut self, objects: &mut [T], bounder: &F) where F: 'a + Fn(&T) -> &'a [BBox], { // TODO: do all of this without the temporary cache let max_len = objects.iter().map(|obj| bounder(obj).len()).max().unwrap(); self.bounds_cache.clear(); self.bounds_cache.resize(max_len, BBox::new()); for obj in objects.iter() { let bounds = bounder(obj); debug_assert!(!bounds.is_empty()); if bounds.len() == max_len { for i in 0..bounds.len() { self.bounds_cache[i] |= bounds[i]; } } else { let s = (max_len - 1) as f32; for (i, bbc) in self.bounds_cache.iter_mut().enumerate() { *bbc |= lerp_slice(bounds, i as f32 / s); } } } } fn recursive_build<'a, T, F>( &mut self, offset: usize, depth: usize, objects_per_leaf: usize, objects: &mut [T], bounder: &F, ) -> (usize, (usize, usize)) where F: 'a + Fn(&T) -> &'a [BBox], { let me = self.nodes.len(); if objects.is_empty() { return (0, (0, 0)); } else if objects.len() <= objects_per_leaf { // Leaf node let bi = self.bounds.len(); // Get bounds { // We make sure that it's worth having multiple time samples, and if not // we reduce to the union of the time samples. self.acc_bounds(objects, bounder); let union_bounds = self .bounds_cache .iter() .fold(BBox::new(), |b1, b2| (b1 | *b2)); let average_area = self .bounds_cache .iter() .fold(0.0, |area, bb| area + bb.surface_area()) / self.bounds_cache.len() as f32; if union_bounds.surface_area() <= (average_area * USE_UNION_FACTOR) { self.bounds.push(union_bounds); } else { self.bounds.extend(&self.bounds_cache); } } // Create node self.nodes.push(BVHBaseNode::Leaf { bounds_range: (bi, self.bounds.len()), object_range: (offset, offset + objects.len()), }); if self.depth < depth { self.depth = depth; } return (me, (bi, self.bounds.len())); } else { // Not a leaf node self.nodes.push(BVHBaseNode::Internal { bounds_range: (0, 0), children_indices: (0, 0), split_axis: 0, }); // Partition objects. // If we're too near the max depth, we do balanced building to // avoid exceeding max depth. // Otherwise we do SAH splitting to build better trees. let (split_index, split_axis) = if (log2_64(objects.len() as u64) as usize) < (BVH_MAX_DEPTH - depth) { // SAH splitting, when we have room to play sah_split(objects, &bounder) } else { // Balanced splitting, when we don't have room to play median_split(objects, &bounder) }; // Create child nodes let (c1_index, c1_bounds) = self.recursive_build( offset, depth + 1, objects_per_leaf, &mut objects[..split_index], bounder, ); let (c2_index, c2_bounds) = self.recursive_build( offset + split_index, depth + 1, objects_per_leaf, &mut objects[split_index..], bounder, ); // Determine bounds // TODO: do merging without the temporary vec. let bi = self.bounds.len(); { let mut merged = Vec::new(); merge_slices_append( &self.bounds[c1_bounds.0..c1_bounds.1], &self.bounds[c2_bounds.0..c2_bounds.1], &mut merged, |b1, b2| *b1 | *b2, ); // We make sure that it's worth having multiple time samples, and if not // we reduce to the union of the time samples. let union_bounds = merged.iter().fold(BBox::new(), |b1, b2| (b1 | *b2)); let average_area = merged.iter().fold(0.0, |area, bb| area + bb.surface_area()) / merged.len() as f32; if union_bounds.surface_area() <= (average_area * USE_UNION_FACTOR) { self.bounds.push(union_bounds); } else { self.bounds.extend(merged.drain(0..)); } } // Set node self.nodes[me] = BVHBaseNode::Internal { bounds_range: (bi, self.bounds.len()), children_indices: (c1_index, c2_index), split_axis: split_axis as u8, }; return (me, (bi, self.bounds.len())); } } } ================================================ FILE: src/accel/light_array.rs ================================================ use kioku::Arena; use crate::{ bbox::BBox, math::{Normal, Point, Vector}, shading::surface_closure::SurfaceClosure, }; use super::LightAccel; #[derive(Debug, Copy, Clone)] pub struct LightArray<'a> { indices: &'a [usize], aprx_energy: f32, } impl<'a> LightArray<'a> { #[allow(dead_code)] pub fn from_objects<'b, T, F>( arena: &'a Arena, objects: &mut [T], info_getter: F, ) -> LightArray<'a> where F: 'b + Fn(&T) -> (&'b [BBox], f32), { let mut indices = Vec::new(); let mut aprx_energy = 0.0; for (i, thing) in objects.iter().enumerate() { let (_, power) = info_getter(thing); if power > 0.0 { indices.push(i); aprx_energy += power; } } LightArray { indices: arena.copy_slice(&indices), aprx_energy: aprx_energy, } } } impl<'a> LightAccel for LightArray<'a> { fn select( &self, inc: Vector, pos: Point, nor: Normal, nor_g: Normal, sc: &SurfaceClosure, time: f32, n: f32, ) -> Option<(usize, f32, f32)> { let _ = (inc, pos, nor, nor_g, sc, time); // Not using these, silence warnings assert!(n >= 0.0 && n <= 1.0); if self.indices.is_empty() { return None; } let n2 = n * self.indices.len() as f32; let i = if n == 1.0 { *self.indices.last().unwrap() } else { self.indices[n2 as usize] }; let whittled_n = n2 - i as f32; let pdf = 1.0 / self.indices.len() as f32; Some((i, pdf, whittled_n)) } fn approximate_energy(&self) -> f32 { self.aprx_energy } } ================================================ FILE: src/accel/light_tree.rs ================================================ use std::mem::{transmute, MaybeUninit}; use kioku::Arena; use crate::{ algorithm::merge_slices_append, bbox::BBox, lerp::lerp_slice, math::{Normal, Point, Vector}, shading::surface_closure::SurfaceClosure, }; use super::{objects_split::sah_split, LightAccel}; const ARITY_LOG2: usize = 3; // Determines how much to collapse the binary tree, // implicitly defining the light tree's arity. 1 = no collapsing, leave as binary // tree. const ARITY: usize = 1 << ARITY_LOG2; // Arity of the final tree #[derive(Copy, Clone, Debug)] pub struct LightTree<'a> { root: Option<&'a Node<'a>>, depth: usize, } #[derive(Copy, Clone, Debug)] enum Node<'a> { Inner { children: &'a [Node<'a>], bounds: &'a [BBox], energy: f32, }, Leaf { light_index: usize, bounds: &'a [BBox], energy: f32, }, } impl<'a> Node<'a> { fn bounds(&self) -> &'a [BBox] { match *self { Node::Inner { bounds, .. } | Node::Leaf { bounds, .. } => bounds, } } fn energy(&self) -> f32 { match *self { Node::Inner { energy, .. } | Node::Leaf { energy, .. } => energy, } } fn light_index(&self) -> usize { match *self { Node::Inner { .. } => panic!(), Node::Leaf { light_index, .. } => light_index, } } } impl<'a> LightTree<'a> { pub fn from_objects<'b, T, F>( arena: &'a Arena, objects: &mut [T], info_getter: F, ) -> LightTree<'a> where F: 'b + Fn(&T) -> (&'b [BBox], f32), { if objects.is_empty() { LightTree { root: None, depth: 0, } } else { let mut builder = LightTreeBuilder::new(); builder.recursive_build(0, 0, objects, &info_getter); let root = arena.alloc_uninit::(); LightTree::construct_from_builder(arena, &builder, builder.root_node_index(), root); LightTree { root: Some(unsafe { transmute(root) }), depth: builder.depth, } } } fn construct_from_builder( arena: &'a Arena, base: &LightTreeBuilder, node_index: usize, node_mem: &mut MaybeUninit>, ) { if base.nodes[node_index].is_leaf { // Leaf let bounds_range = base.nodes[node_index].bounds_range; let bounds = arena.copy_slice(&base.bounds[bounds_range.0..bounds_range.1]); unsafe { *node_mem.as_mut_ptr() = Node::Leaf { light_index: base.nodes[node_index].child_index, bounds: bounds, energy: base.nodes[node_index].energy, }; } } else { // Inner let bounds_range = base.nodes[node_index].bounds_range; let bounds = arena.copy_slice(&base.bounds[bounds_range.0..bounds_range.1]); let child_count = base.node_child_count(node_index); let children = arena.alloc_array_uninit::(child_count); for i in 0..child_count { LightTree::construct_from_builder( arena, base, base.node_nth_child_index(node_index, i), &mut children[i], ); } unsafe { *node_mem.as_mut_ptr() = Node::Inner { children: transmute(children), bounds: bounds, energy: base.nodes[node_index].energy, }; } } } } impl<'a> LightAccel for LightTree<'a> { fn select( &self, inc: Vector, pos: Point, nor: Normal, nor_g: Normal, sc: &SurfaceClosure, time: f32, n: f32, ) -> Option<(usize, f32, f32)> { // Calculates the selection probability for a node let node_prob = |node_ref: &Node| { let bbox = lerp_slice(node_ref.bounds(), time); let d = bbox.center() - pos; let r2 = bbox.diagonal2() * 0.25; let inv_surface_area = 1.0 / r2; // Get the approximate amount of light contribution from the // composite light source. let approx_contrib = sc.estimate_eval_over_sphere_light(inc, d, r2, nor, nor_g); node_ref.energy() * inv_surface_area * approx_contrib }; // Traverse down the tree, keeping track of the relative probabilities let mut node = self.root?; let mut tot_prob = 1.0; let mut n = n; while let Node::Inner { children, .. } = *node { // Calculate the relative probabilities of the children let ps = { let mut ps = [0.0; ARITY]; let mut total = 0.0; for (i, child) in children.iter().enumerate() { let p = node_prob(child); ps[i] = p; total += p; } if total <= 0.0 { let p = 1.0 / children.len() as f32; for prob in &mut ps[..] { *prob = p; } } else { for prob in &mut ps[..] { *prob /= total; } } ps }; // Pick child and update probabilities let mut base = 0.0; for (i, &p) in ps.iter().enumerate() { if (n <= base + p) || (i == children.len() - 1) { tot_prob *= p; node = &children[i]; n = (n - base) / p; break; } else { base += p; } } } // Found our light! Some((node.light_index(), tot_prob, n)) } fn approximate_energy(&self) -> f32 { if let Some(node) = self.root { node.energy() } else { 0.0 } } } struct LightTreeBuilder { nodes: Vec, bounds: Vec, depth: usize, } #[derive(Copy, Clone, Debug)] struct BuilderNode { is_leaf: bool, bounds_range: (usize, usize), energy: f32, child_index: usize, } impl LightTreeBuilder { fn new() -> LightTreeBuilder { LightTreeBuilder { nodes: Vec::new(), bounds: Vec::new(), depth: 0, } } pub fn root_node_index(&self) -> usize { 0 } // Returns the number of children of this node, assuming a collapse // number of `ARITY_LOG2`. pub fn node_child_count(&self, node_index: usize) -> usize { self.node_child_count_recurse(ARITY_LOG2, node_index) } // Returns the index of the nth child, assuming a collapse // number of `ARITY_LOG2`. pub fn node_nth_child_index(&self, node_index: usize, child_n: usize) -> usize { self.node_nth_child_index_recurse(ARITY_LOG2, node_index, child_n) .0 } // Returns the number of children of this node, assuming a collapse // number of `level_collapse`. pub fn node_child_count_recurse(&self, level_collapse: usize, node_index: usize) -> usize { if level_collapse > 0 { if self.nodes[node_index].is_leaf { 1 } else { let a = self.node_child_count_recurse(level_collapse - 1, node_index + 1); let b = self.node_child_count_recurse( level_collapse - 1, self.nodes[node_index].child_index, ); a + b } } else { 1 } } // Returns the index of the nth child, assuming a collapse // number of `level_collapse`. pub fn node_nth_child_index_recurse( &self, level_collapse: usize, node_index: usize, child_n: usize, ) -> (usize, usize) { if level_collapse > 0 && !self.nodes[node_index].is_leaf { let (index, rem) = self.node_nth_child_index_recurse(level_collapse - 1, node_index + 1, child_n); if rem == 0 { return (index, 0); } return self.node_nth_child_index_recurse( level_collapse - 1, self.nodes[node_index].child_index, rem - 1, ); } else { return (node_index, child_n); } } fn recursive_build<'a, T, F>( &mut self, offset: usize, depth: usize, objects: &mut [T], info_getter: &F, ) -> (usize, (usize, usize)) where F: 'a + Fn(&T) -> (&'a [BBox], f32), { let me_index = self.nodes.len(); if objects.is_empty() { return (0, (0, 0)); } else if objects.len() == 1 { // Leaf node let bi = self.bounds.len(); let (obj_bounds, energy) = info_getter(&objects[0]); self.bounds.extend(obj_bounds); self.nodes.push(BuilderNode { is_leaf: true, bounds_range: (bi, self.bounds.len()), energy: energy, child_index: offset, }); if self.depth < depth { self.depth = depth; } return (me_index, (bi, self.bounds.len())); } else { // Not a leaf node self.nodes.push(BuilderNode { is_leaf: false, bounds_range: (0, 0), energy: 0.0, child_index: 0, }); // Partition objects. let (split_index, _) = sah_split(objects, &|obj_ref| info_getter(obj_ref).0); // Create child nodes let (_, c1_bounds) = self.recursive_build(offset, depth + 1, &mut objects[..split_index], info_getter); let (c2_index, c2_bounds) = self.recursive_build( offset + split_index, depth + 1, &mut objects[split_index..], info_getter, ); // Determine bounds // TODO: do merging without the temporary vec. let bi = self.bounds.len(); let mut merged = Vec::new(); merge_slices_append( &self.bounds[c1_bounds.0..c1_bounds.1], &self.bounds[c2_bounds.0..c2_bounds.1], &mut merged, |b1, b2| *b1 | *b2, ); self.bounds.extend(merged.drain(0..)); // Set node let energy = self.nodes[me_index + 1].energy + self.nodes[c2_index].energy; self.nodes[me_index] = BuilderNode { is_leaf: false, bounds_range: (bi, self.bounds.len()), energy: energy, child_index: c2_index, }; return (me_index, (bi, self.bounds.len())); } } } ================================================ FILE: src/accel/mod.rs ================================================ // mod bvh; mod bvh4; mod bvh_base; mod light_array; mod light_tree; mod objects_split; use std::cell::Cell; use crate::{ math::{Normal, Point, Vector}, shading::surface_closure::SurfaceClosure, }; pub use self::{ // bvh::{BVHNode, BVH}, bvh4::{ray_code, BVH4Node, BVH4}, light_array::LightArray, light_tree::LightTree, }; // Track BVH traversal time thread_local! { pub static ACCEL_NODE_RAY_TESTS: Cell = Cell::new(0); } pub trait LightAccel { /// Returns (index_of_light, selection_pdf, whittled_n) fn select( &self, inc: Vector, pos: Point, nor: Normal, nor_g: Normal, sc: &SurfaceClosure, time: f32, n: f32, ) -> Option<(usize, f32, f32)>; fn approximate_energy(&self) -> f32; } ================================================ FILE: src/accel/objects_split.rs ================================================ #![allow(dead_code)] use std::cmp::Ordering; use crate::{ algorithm::{partition, quick_select}, bbox::BBox, lerp::lerp_slice, math::{dot, Vector}, sampling::uniform_sample_hemisphere, }; const SAH_BIN_COUNT: usize = 13; // Prime numbers work best, for some reason const SPLIT_PLANE_COUNT: usize = 5; /// Takes a slice of boundable objects and partitions them based on the Surface /// Area Heuristic, but using arbitrarily oriented planes. /// /// Returns the index of the partition boundary and the axis that it split on /// (0 = x, 1 = y, 2 = z). pub fn free_sah_split<'a, T, F>(seed: u32, objects: &mut [T], bounder: &F) -> (usize, usize) where F: Fn(&T) -> &'a [BBox], { // Generate the planes for splitting let planes = { let mut planes = [Vector::new(0.0, 0.0, 0.0); SPLIT_PLANE_COUNT]; let offset = seed * SPLIT_PLANE_COUNT as u32; for i in 0..SPLIT_PLANE_COUNT { let u = halton::sample(0, offset + i as u32); let v = halton::sample(1, offset + i as u32); planes[i] = uniform_sample_hemisphere(u, v).normalized(); } planes }; // Get the extents of the objects with respect to the split planes let extents = { let mut extents = [(std::f32::INFINITY, std::f32::NEG_INFINITY); SPLIT_PLANE_COUNT]; for obj in &objects[..] { let centroid = lerp_slice(bounder(obj), 0.5).center().into_vector(); for i in 0..SPLIT_PLANE_COUNT { let dist = dot(centroid, planes[i]); extents[i].0 = extents[i].0.min(dist); extents[i].1 = extents[i].1.max(dist); } } extents }; // Pre-calc SAH div distances let sah_divs = { let mut sah_divs = [[0.0f32; SAH_BIN_COUNT - 1]; SPLIT_PLANE_COUNT]; for pi in 0..SPLIT_PLANE_COUNT { let extent = extents[pi].1 - extents[pi].0; for div in 0..(SAH_BIN_COUNT - 1) { let part = extent * ((div + 1) as f32 / SAH_BIN_COUNT as f32); sah_divs[pi][div] = extents[pi].0 + part; } } sah_divs }; // Build SAH bins let sah_bins = { let mut sah_bins = [[(BBox::new(), BBox::new(), 0, 0); SAH_BIN_COUNT - 1]; SPLIT_PLANE_COUNT]; for obj in objects.iter() { let tb = lerp_slice(bounder(obj), 0.5); let centroid = tb.center().into_vector(); for pi in 0..SPLIT_PLANE_COUNT { for div in 0..(SAH_BIN_COUNT - 1) { let dist = dot(centroid, planes[pi]); if dist <= sah_divs[pi][div] { sah_bins[pi][div].0 |= tb; sah_bins[pi][div].2 += 1; } else { sah_bins[pi][div].1 |= tb; sah_bins[pi][div].3 += 1; } } } } sah_bins }; // Find best split axis and div point let (split_plane_i, div_n) = { let mut split_plane_i = 0; let mut div_n = 0.0; let mut smallest_cost = std::f32::INFINITY; for pi in 0..SPLIT_PLANE_COUNT { for div in 0..(SAH_BIN_COUNT - 1) { let left_cost = sah_bins[pi][div].0.surface_area() * sah_bins[pi][div].2 as f32; let right_cost = sah_bins[pi][div].1.surface_area() * sah_bins[pi][div].3 as f32; let tot_cost = left_cost + right_cost; if tot_cost < smallest_cost { split_plane_i = pi; div_n = sah_divs[pi][div]; smallest_cost = tot_cost; } } } (split_plane_i, div_n) }; // Calculate the approximate axis-aligned split, along with flipping the split plane as // appropriate. let (plane, approx_axis, div) = { // Find axis with largest value let mut largest_axis = 0; let mut n = 0.0; for d in 0..3 { let m = planes[split_plane_i].get_n(d).abs(); if n < m { largest_axis = d; n = m; } } // If it's negative, flip if planes[split_plane_i].get_n(largest_axis).is_sign_positive() { (planes[split_plane_i], largest_axis, div_n) } else { (planes[split_plane_i] * -1.0, largest_axis, div_n * -1.0) } }; // Partition let mut split_i = partition(&mut objects[..], |obj| { let centroid = lerp_slice(bounder(obj), 0.5).center().into_vector(); let dist = dot(centroid, plane); dist < div }); if split_i < 1 { split_i = 1; } else if split_i >= objects.len() { split_i = objects.len() - 1; } (split_i, approx_axis) } /// Takes a slice of boundable objects and partitions them based on the Surface /// Area Heuristic. /// /// Returns the index of the partition boundary and the axis that it split on /// (0 = x, 1 = y, 2 = z). pub fn sah_split<'a, T, F>(objects: &mut [T], bounder: &F) -> (usize, usize) where F: Fn(&T) -> &'a [BBox], { // Get combined object centroid extents let bounds = { let mut bb = BBox::new(); for obj in &objects[..] { bb |= lerp_slice(bounder(obj), 0.5).center(); } bb }; // Pre-calc SAH div points let sah_divs = { let mut sah_divs = [[0.0f32; SAH_BIN_COUNT - 1]; 3]; for d in 0..sah_divs.len() { let extent = bounds.max.get_n(d) - bounds.min.get_n(d); for div in 0..(SAH_BIN_COUNT - 1) { let part = extent * ((div + 1) as f32 / SAH_BIN_COUNT as f32); sah_divs[d][div] = bounds.min.get_n(d) + part; } } sah_divs }; // Build SAH bins let sah_bins = { let mut sah_bins = [[(BBox::new(), BBox::new(), 0, 0); SAH_BIN_COUNT - 1]; 3]; for obj in objects.iter() { let tb = lerp_slice(bounder(obj), 0.5); let centroid = (tb.min.into_vector() + tb.max.into_vector()) * 0.5; for d in 0..3 { for div in 0..(SAH_BIN_COUNT - 1) { if centroid.get_n(d) <= sah_divs[d][div] { sah_bins[d][div].0 |= tb; sah_bins[d][div].2 += 1; } else { sah_bins[d][div].1 |= tb; sah_bins[d][div].3 += 1; } } } } sah_bins }; // Find best split axis and div point let (split_axis, div) = { let mut dim = 0; let mut div_n = 0.0; let mut smallest_cost = std::f32::INFINITY; for d in 0..3 { for div in 0..(SAH_BIN_COUNT - 1) { let left_cost = sah_bins[d][div].0.surface_area() * sah_bins[d][div].2 as f32; let right_cost = sah_bins[d][div].1.surface_area() * sah_bins[d][div].3 as f32; let left_diag = sah_bins[d][div].0.diagonal(); let right_diag = sah_bins[d][div].1.diagonal(); let tot_cost = (left_cost * left_diag) + (right_cost * right_diag); if tot_cost < smallest_cost { dim = d; div_n = sah_divs[d][div]; smallest_cost = tot_cost; } } } (dim, div_n) }; // Partition let mut split_i = partition(&mut objects[..], |obj| { let tb = lerp_slice(bounder(obj), 0.5); let centroid = (tb.min.get_n(split_axis) + tb.max.get_n(split_axis)) * 0.5; centroid < div }); if split_i < 1 { split_i = 1; } else if split_i >= objects.len() { split_i = objects.len() - 1; } (split_i, split_axis) } /// Takes a slice of boundable objects and partitions them based on the bounds mean heuristic. /// /// Returns the index of the partition boundary and the axis that it split on /// (0 = x, 1 = y, 2 = z). pub fn bounds_mean_split<'a, T, F>(objects: &mut [T], bounder: &F) -> (usize, usize) where F: Fn(&T) -> &'a [BBox], { // Get combined object bounds let bounds = { let mut bb = BBox::new(); for obj in &objects[..] { bb |= lerp_slice(bounder(obj), 0.5); } bb }; let split_axis = { let mut axis = 0; let mut largest = std::f32::NEG_INFINITY; for i in 0..3 { let extent = bounds.max.get_n(i) - bounds.min.get_n(i); if extent > largest { largest = extent; axis = i; } } axis }; let div = (bounds.min.get_n(split_axis) + bounds.max.get_n(split_axis)) * 0.5; // Partition let mut split_i = partition(&mut objects[..], |obj| { let tb = lerp_slice(bounder(obj), 0.5); let centroid = (tb.min.get_n(split_axis) + tb.max.get_n(split_axis)) * 0.5; centroid < div }); if split_i < 1 { split_i = 1; } else if split_i >= objects.len() { split_i = objects.len() - 1; } (split_i, split_axis) } /// Takes a slice of boundable objects and partitions them based on the median heuristic. /// /// Returns the index of the partition boundary and the axis that it split on /// (0 = x, 1 = y, 2 = z). pub fn median_split<'a, T, F>(objects: &mut [T], bounder: &F) -> (usize, usize) where F: Fn(&T) -> &'a [BBox], { // Get combined object bounds let bounds = { let mut bb = BBox::new(); for obj in &objects[..] { bb |= lerp_slice(bounder(obj), 0.5); } bb }; let split_axis = { let mut axis = 0; let mut largest = std::f32::NEG_INFINITY; for i in 0..3 { let extent = bounds.max.get_n(i) - bounds.min.get_n(i); if extent > largest { largest = extent; axis = i; } } axis }; let place = { let place = objects.len() / 2; if place > 0 { place } else { 1 } }; quick_select(objects, place, |a, b| { let tb_a = lerp_slice(bounder(a), 0.5); let tb_b = lerp_slice(bounder(b), 0.5); let centroid_a = (tb_a.min.get_n(split_axis) + tb_a.max.get_n(split_axis)) * 0.5; let centroid_b = (tb_b.min.get_n(split_axis) + tb_b.max.get_n(split_axis)) * 0.5; if centroid_a < centroid_b { Ordering::Less } else if centroid_a == centroid_b { Ordering::Equal } else { Ordering::Greater } }); (place, split_axis) } ================================================ FILE: src/algorithm.rs ================================================ #![allow(dead_code)] use std::{ cmp::{self, Ordering}, mem::MaybeUninit, }; use crate::{ hash::hash_u64, lerp::{lerp_slice, Lerp}, }; /// Selects an item from a slice based on a weighting function and a /// number (n) between 0.0 and 1.0. Returns the index of the selected /// item and the probability that it would have been selected with a /// random n. pub fn weighted_choice(slc: &[T], n: f32, weight: F) -> (usize, f32) where F: Fn(&T) -> f32, { assert!(!slc.is_empty()); let total_weight = slc.iter().fold(0.0, |sum, v| sum + weight(v)); let n = n * total_weight; let mut x = 0.0; for (i, v) in slc.iter().enumerate() { let w = weight(v); x += w; if x > n || i == slc.len() { return (i, w / total_weight); } } unreachable!() } /// Partitions a slice in-place with the given unary predicate, returning /// the index of the first element for which the predicate evaluates /// false. /// /// The predicate is executed precisely once on every element in /// the slice, and is allowed to modify the elements. pub fn partition(slc: &mut [T], mut pred: F) -> usize where F: FnMut(&mut T) -> bool, { // This version uses raw pointers and pointer arithmetic to squeeze more // performance out of the code. unsafe { let mut a = slc.as_mut_ptr(); let mut b = a.add(slc.len()); let start = a as usize; loop { loop { if a == b { return ((a as usize) - start) / std::mem::size_of::(); } if !pred(&mut *a) { break; } a = a.offset(1); } loop { b = b.offset(-1); if a == b { return ((a as usize) - start) / std::mem::size_of::(); } if pred(&mut *b) { break; } } std::ptr::swap(a, b); a = a.offset(1); } } } /// Partitions a slice in-place with the given unary predicate, returning /// the index of the first element for which the predicate evaluates /// false. /// /// The predicate is executed precisely once on every element in /// the slice, and is allowed to modify the elements. /// /// The only difference between this and plain partition above, is that /// the predicate function is passed a bool representing which side /// of the array we're currently on: left or right. False means left, /// True means right. pub fn partition_with_side(slc: &mut [T], mut pred: F) -> usize where F: FnMut(&mut T, bool) -> bool, { // This version uses raw pointers and pointer arithmetic to squeeze more // performance out of the code. unsafe { let mut a = slc.as_mut_ptr(); let mut b = a.add(slc.len()); let start = a as usize; loop { loop { if a == b { return ((a as usize) - start) / std::mem::size_of::(); } if !pred(&mut *a, false) { break; } a = a.offset(1); } loop { b = b.offset(-1); if a == b { return ((a as usize) - start) / std::mem::size_of::(); } if pred(&mut *b, true) { break; } } std::ptr::swap(a, b); a = a.offset(1); } } } /// Partitions two slices in-place in concert based on the given unary /// predicate, returning the index of the first element for which the /// predicate evaluates false. /// /// Because this runs on two slices at once, they must both be the same /// length. /// /// The predicate takes a usize (which will receive the index of the elments /// being tested), a mutable reference to an element of the first slice's type, /// and a mutable reference to an element of the last slice's type. /// /// The predicate is executed precisely once on every element in /// the slices, and is allowed to modify the elements. pub fn partition_pair(slc1: &mut [A], slc2: &mut [B], mut pred: F) -> usize where F: FnMut(usize, &mut A, &mut B) -> bool, { assert_eq!(slc1.len(), slc2.len()); // This version uses raw pointers and pointer arithmetic to squeeze more // performance out of the code. unsafe { let mut a1 = slc1.as_mut_ptr(); let mut a2 = slc2.as_mut_ptr(); let mut b1 = a1.add(slc1.len()); let mut b2 = a2.add(slc2.len()); let start = a1 as usize; loop { loop { if a1 == b1 { return ((a1 as usize) - start) / std::mem::size_of::(); } if !pred( ((a1 as usize) - start) / std::mem::size_of::(), &mut *a1, &mut *a2, ) { break; } a1 = a1.offset(1); a2 = a2.offset(1); } loop { b1 = b1.offset(-1); b2 = b2.offset(-1); if a1 == b1 { return ((a1 as usize) - start) / std::mem::size_of::(); } if pred( ((b1 as usize) - start) / std::mem::size_of::(), &mut *b1, &mut *b2, ) { break; } } std::ptr::swap(a1, b1); std::ptr::swap(a2, b2); a1 = a1.offset(1); a2 = a2.offset(1); } } } /// Partitions the slice of items to place the nth-ordered item in the nth place, /// and the items less than it before and the items more than it after. pub fn quick_select(slc: &mut [T], n: usize, mut order: F) where F: FnMut(&T, &T) -> Ordering, { let mut left = 0; let mut right = slc.len(); let mut seed = n as u64; loop { let i = left + (hash_u64(right as u64, seed) as usize % (right - left)); slc.swap(i, right - 1); let ii = left + { let (val, list) = (&mut slc[left..right]).split_last_mut().unwrap(); partition(list, |n| order(n, val) == Ordering::Less) }; slc.swap(ii, right - 1); if ii == n { return; } else if ii > n { right = ii; } else { left = ii + 1; } seed += 1; } } /// Merges two slices of things, appending the result to `vec_out` pub fn merge_slices_append( slice1: &[T], slice2: &[T], vec_out: &mut Vec, merge: F, ) where F: Fn(&T, &T) -> T, { // Transform the bounding boxes if slice1.is_empty() || slice2.is_empty() { return; } else if slice1.len() == slice2.len() { for (xf1, xf2) in Iterator::zip(slice1.iter(), slice2.iter()) { vec_out.push(merge(xf1, xf2)); } } else if slice1.len() > slice2.len() { let s = (slice1.len() - 1) as f32; for (i, xf1) in slice1.iter().enumerate() { let xf2 = lerp_slice(slice2, i as f32 / s); vec_out.push(merge(xf1, &xf2)); } } else if slice1.len() < slice2.len() { let s = (slice2.len() - 1) as f32; for (i, xf2) in slice2.iter().enumerate() { let xf1 = lerp_slice(slice1, i as f32 / s); vec_out.push(merge(&xf1, xf2)); } } } /// Merges two slices of things, storing the result in `slice_out`. /// Panics if `slice_out` is not the right size. pub fn merge_slices_to( slice1: &[T], slice2: &[T], slice_out: &mut [MaybeUninit], merge: F, ) where F: Fn(&T, &T) -> T, { assert_eq!(slice_out.len(), cmp::max(slice1.len(), slice2.len())); // Transform the bounding boxes if slice1.is_empty() || slice2.is_empty() { return; } else if slice1.len() == slice2.len() { for (xfo, (xf1, xf2)) in Iterator::zip( slice_out.iter_mut(), Iterator::zip(slice1.iter(), slice2.iter()), ) { unsafe { *xfo.as_mut_ptr() = merge(xf1, xf2); } } } else if slice1.len() > slice2.len() { let s = (slice1.len() - 1) as f32; for (i, (xfo, xf1)) in Iterator::zip(slice_out.iter_mut(), slice1.iter()).enumerate() { let xf2 = lerp_slice(slice2, i as f32 / s); unsafe { *xfo.as_mut_ptr() = merge(xf1, &xf2); } } } else if slice1.len() < slice2.len() { let s = (slice2.len() - 1) as f32; for (i, (xfo, xf2)) in Iterator::zip(slice_out.iter_mut(), slice2.iter()).enumerate() { let xf1 = lerp_slice(slice1, i as f32 / s); unsafe { *xfo.as_mut_ptr() = merge(&xf1, xf2); } } } } #[cfg(test)] mod tests { use super::*; use std::cmp::Ordering; fn quick_select_ints(list: &mut [i32], i: usize) { quick_select(list, i, |a, b| { if a < b { Ordering::Less } else if a == b { Ordering::Equal } else { Ordering::Greater } }); } #[test] fn quick_select_1() { let mut list = [8, 9, 7, 4, 6, 1, 0, 5, 3, 2]; quick_select_ints(&mut list, 5); assert_eq!(list[5], 5); } #[test] fn quick_select_2() { let mut list = [8, 9, 7, 4, 6, 1, 0, 5, 3, 2]; quick_select_ints(&mut list, 3); assert_eq!(list[3], 3); } #[test] fn quick_select_3() { let mut list = [8, 9, 7, 4, 6, 1, 0, 5, 3, 2]; quick_select_ints(&mut list, 0); assert_eq!(list[0], 0); } #[test] fn quick_select_4() { let mut list = [8, 9, 7, 4, 6, 1, 0, 5, 3, 2]; quick_select_ints(&mut list, 9); assert_eq!(list[9], 9); } } ================================================ FILE: src/bbox.rs ================================================ #![allow(dead_code)] use std::{ iter::Iterator, ops::{BitOr, BitOrAssign}, }; use crate::{ lerp::{lerp, lerp_slice, Lerp}, math::{Point, Transform, Vector}, }; const BBOX_MAXT_ADJUST: f32 = 1.000_000_24; /// A 3D axis-aligned bounding box. #[derive(Debug, Copy, Clone)] pub struct BBox { pub min: Point, pub max: Point, } impl BBox { /// Creates a degenerate BBox with +infinity min and -infinity max. pub fn new() -> BBox { BBox { min: Point::new(std::f32::INFINITY, std::f32::INFINITY, std::f32::INFINITY), max: Point::new( std::f32::NEG_INFINITY, std::f32::NEG_INFINITY, std::f32::NEG_INFINITY, ), } } /// Creates a BBox with min as the minimum extent and max as the maximum /// extent. pub fn from_points(min: Point, max: Point) -> BBox { BBox { min: min, max: max } } // Returns whether the given ray intersects with the bbox. pub fn intersect_ray(&self, orig: Point, dir_inv: Vector, max_t: f32) -> bool { // Calculate slab intersections let t1 = (self.min.co - orig.co) * dir_inv.co; let t2 = (self.max.co - orig.co) * dir_inv.co; // Find the far and near intersection let far_t = t1.max(t2).extend(std::f32::INFINITY); let near_t = t1.min(t2).extend(0.0); let far_hit_t = (far_t.min_element() * BBOX_MAXT_ADJUST).min(max_t); let near_hit_t = near_t.max_element(); // Did we hit? near_hit_t <= far_hit_t } // Creates a new BBox transformed into a different space. pub fn transformed(&self, xform: Transform) -> BBox { // BBox corners let vs = [ Point::new(self.min.x(), self.min.y(), self.min.z()), Point::new(self.min.x(), self.min.y(), self.max.z()), Point::new(self.min.x(), self.max.y(), self.min.z()), Point::new(self.min.x(), self.max.y(), self.max.z()), Point::new(self.max.x(), self.min.y(), self.min.z()), Point::new(self.max.x(), self.min.y(), self.max.z()), Point::new(self.max.x(), self.max.y(), self.min.z()), Point::new(self.max.x(), self.max.y(), self.max.z()), ]; // Transform BBox corners and make new bbox let mut b = BBox::new(); for v in &vs { let v = *v * xform; b.min = v.min(b.min); b.max = v.max(b.max); } b } pub fn surface_area(&self) -> f32 { let d = self.max - self.min; ((d.x() * d.y()) + (d.y() * d.z()) + (d.z() * d.x())) * 2.0 } pub fn center(&self) -> Point { self.min.lerp(self.max, 0.5) } pub fn diagonal(&self) -> f32 { (self.max - self.min).length() } pub fn diagonal2(&self) -> f32 { (self.max - self.min).length2() } } /// Union of two `BBox`es. impl BitOr for BBox { type Output = BBox; fn bitor(self, rhs: BBox) -> BBox { BBox::from_points( Point { co: self.min.co.min(rhs.min.co), }, Point { co: self.max.co.max(rhs.max.co), }, ) } } impl BitOrAssign for BBox { fn bitor_assign(&mut self, rhs: BBox) { *self = *self | rhs; } } /// Expand `BBox` by a point. impl BitOr for BBox { type Output = BBox; fn bitor(self, rhs: Point) -> BBox { BBox::from_points( Point { co: self.min.co.min(rhs.co), }, Point { co: self.max.co.max(rhs.co), }, ) } } impl BitOrAssign for BBox { fn bitor_assign(&mut self, rhs: Point) { *self = *self | rhs; } } impl Lerp for BBox { fn lerp(self, other: BBox, alpha: f32) -> BBox { BBox { min: lerp(self.min, other.min, alpha), max: lerp(self.max, other.max, alpha), } } } pub fn transform_bbox_slice_from(bbs_in: &[BBox], xforms: &[Transform], bbs_out: &mut Vec) { bbs_out.clear(); // Transform the bounding boxes if xforms.is_empty() { bbs_out.extend_from_slice(bbs_in); } else if bbs_in.len() == xforms.len() { for (bb, xf) in Iterator::zip(bbs_in.iter(), xforms.iter()) { bbs_out.push(bb.transformed(xf.inverse())); } } else if bbs_in.len() > xforms.len() { let s = (bbs_in.len() - 1) as f32; for (i, bb) in bbs_in.iter().enumerate() { bbs_out.push(bb.transformed(lerp_slice(xforms, i as f32 / s).inverse())); } } else if bbs_in.len() < xforms.len() { let s = (xforms.len() - 1) as f32; for (i, xf) in xforms.iter().enumerate() { bbs_out.push(lerp_slice(bbs_in, i as f32 / s).transformed(xf.inverse())); } } } ================================================ FILE: src/bbox4.rs ================================================ #![allow(dead_code)] use std; use std::ops::{BitOr, BitOrAssign}; use crate::{ bbox::BBox, lerp::{lerp, Lerp}, math::{Point, Vector}, }; use glam::{BVec4A, Vec4}; const BBOX_MAXT_ADJUST: f32 = 1.000_000_24; /// A SIMD set of 4 3D axis-aligned bounding boxes. #[derive(Debug, Copy, Clone)] pub struct BBox4 { pub x: (Vec4, Vec4), // (min, max) pub y: (Vec4, Vec4), // (min, max) pub z: (Vec4, Vec4), // (min, max) } impl BBox4 { /// Creates a degenerate BBox with +infinity min and -infinity max. pub fn new() -> BBox4 { BBox4 { x: ( Vec4::splat(std::f32::INFINITY), Vec4::splat(std::f32::NEG_INFINITY), ), y: ( Vec4::splat(std::f32::INFINITY), Vec4::splat(std::f32::NEG_INFINITY), ), z: ( Vec4::splat(std::f32::INFINITY), Vec4::splat(std::f32::NEG_INFINITY), ), } } /// Creates a BBox with min as the minimum extent and max as the maximum /// extent. pub fn from_bboxes(b1: BBox, b2: BBox, b3: BBox, b4: BBox) -> BBox4 { BBox4 { x: ( Vec4::new(b1.min.x(), b2.min.x(), b3.min.x(), b4.min.x()), Vec4::new(b1.max.x(), b2.max.x(), b3.max.x(), b4.max.x()), ), y: ( Vec4::new(b1.min.y(), b2.min.y(), b3.min.y(), b4.min.y()), Vec4::new(b1.max.y(), b2.max.y(), b3.max.y(), b4.max.y()), ), z: ( Vec4::new(b1.min.z(), b2.min.z(), b3.min.z(), b4.min.z()), Vec4::new(b1.max.z(), b2.max.z(), b3.max.z(), b4.max.z()), ), } } // Returns whether the given ray intersects with the bboxes. pub fn intersect_ray(&self, orig: Point, dir_inv: Vector, max_t: f32) -> BVec4A { // Get the ray data into SIMD format. let ro_x = Vec4::splat(orig.co[0]); let ro_y = Vec4::splat(orig.co[1]); let ro_z = Vec4::splat(orig.co[2]); let rdi_x = Vec4::splat(dir_inv.co[0]); let rdi_y = Vec4::splat(dir_inv.co[1]); let rdi_z = Vec4::splat(dir_inv.co[2]); let max_t = Vec4::splat(max_t); // Slab tests let t1_x = (self.x.0 - ro_x) * rdi_x; let t1_y = (self.y.0 - ro_y) * rdi_y; let t1_z = (self.z.0 - ro_z) * rdi_z; let t2_x = (self.x.1 - ro_x) * rdi_x; let t2_y = (self.y.1 - ro_y) * rdi_y; let t2_z = (self.z.1 - ro_z) * rdi_z; // Get the far and near t hits for each axis. let t_far_x = t1_x.max(t2_x); let t_far_y = t1_y.max(t2_y); let t_far_z = t1_z.max(t2_z); let t_near_x = t1_x.min(t2_x); let t_near_y = t1_y.min(t2_y); let t_near_z = t1_z.min(t2_z); // Calculate over-all far t hit. let far_t = (t_far_x.min(t_far_y.min(t_far_z)) * Vec4::splat(BBOX_MAXT_ADJUST)).min(max_t); // Calculate over-all near t hit. let near_t = t_near_x.max(t_near_y).max(t_near_z.max(Vec4::splat(0.0))); // Hit results near_t.cmplt(far_t) } } /// Union of two BBoxes. impl BitOr for BBox4 { type Output = BBox4; fn bitor(self, rhs: BBox4) -> BBox4 { BBox4 { x: (self.x.0.min(rhs.x.0), self.x.1.max(rhs.x.1)), y: (self.y.0.min(rhs.y.0), self.y.1.max(rhs.y.1)), z: (self.z.0.min(rhs.z.0), self.z.1.max(rhs.z.1)), } } } impl BitOrAssign for BBox4 { fn bitor_assign(&mut self, rhs: BBox4) { *self = *self | rhs; } } impl Lerp for BBox4 { fn lerp(self, other: BBox4, alpha: f32) -> BBox4 { BBox4 { x: ( lerp(self.x.0, other.x.0, alpha), lerp(self.x.1, other.x.1, alpha), ), y: ( lerp(self.y.0, other.y.0, alpha), lerp(self.y.1, other.y.1, alpha), ), z: ( lerp(self.z.0, other.z.0, alpha), lerp(self.z.1, other.z.1, alpha), ), } } } ================================================ FILE: src/boundable.rs ================================================ #![allow(dead_code)] use crate::bbox::BBox; pub trait Boundable { fn bounds(&self) -> &[BBox]; } ================================================ FILE: src/camera.rs ================================================ #![allow(dead_code)] use kioku::Arena; use crate::{ lerp::lerp_slice, math::{Point, Transform, Vector}, ray::Ray, sampling::square_to_circle, }; #[derive(Copy, Clone, Debug)] pub struct Camera<'a> { transforms: &'a [Transform], fovs: &'a [f32], tfovs: &'a [f32], aperture_radii: &'a [f32], focus_distances: &'a [f32], } impl<'a> Camera<'a> { pub fn new( arena: &'a Arena, transforms: &[Transform], fovs: &[f32], mut aperture_radii: &[f32], mut focus_distances: &[f32], ) -> Camera<'a> { assert!(!transforms.is_empty(), "Camera has no transform(s)!"); assert!(!fovs.is_empty(), "Camera has no fov(s)!"); // Aperture needs focus distance and vice-versa. if aperture_radii.is_empty() || focus_distances.is_empty() { aperture_radii = &[0.0]; focus_distances = &[1.0]; if aperture_radii.is_empty() && !focus_distances.is_empty() { println!( "WARNING: camera has aperture radius but no focus distance. Disabling \ focal blur." ); } else if !aperture_radii.is_empty() && focus_distances.is_empty() { println!( "WARNING: camera has focus distance but no aperture radius. Disabling \ focal blur." ); } } // Can't have focus distance of zero. if focus_distances.iter().any(|d| *d == 0.0) { if aperture_radii.iter().any(|a| *a > 0.0) { println!("WARNING: camera focal distance is zero or less. Disabling focal blur."); } aperture_radii = &[0.0]; focus_distances = &[1.0]; } // Convert angle fov into linear fov. let tfovs: Vec = fovs .iter() .map(|n| (n / 2.0).sin() / (n / 2.0).cos()) .collect(); Camera { transforms: arena.copy_slice(&transforms), fovs: arena.copy_slice(&fovs), tfovs: arena.copy_slice(&tfovs), aperture_radii: arena.copy_slice(&aperture_radii), focus_distances: arena.copy_slice(&focus_distances), } } pub fn generate_ray(&self, x: f32, y: f32, time: f32, wavelength: f32, u: f32, v: f32) -> Ray { // Get time-interpolated camera settings let transform = lerp_slice(self.transforms, time); let tfov = lerp_slice(self.tfovs, time); let aperture_radius = lerp_slice(self.aperture_radii, time); let focus_distance = lerp_slice(self.focus_distances, time); // Ray origin let orig = { let (u, v) = square_to_circle((u * 2.0) - 1.0, (v * 2.0) - 1.0); Point::new(aperture_radius * u, aperture_radius * v, 0.0) }; // Ray direction let dir = Vector::new( (x * tfov) - (orig.x() / focus_distance), (y * tfov) - (orig.y() / focus_distance), 1.0, ) .normalized(); Ray { orig: orig * transform, dir: dir * transform, time: time, wavelength: wavelength, max_t: std::f32::INFINITY, } } } ================================================ FILE: src/color.rs ================================================ use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign}; pub use color::{ rec709_e_to_xyz, rec709_to_xyz, xyz_to_aces_ap0, xyz_to_aces_ap0_e, xyz_to_rec709, xyz_to_rec709_e, }; use compact::fluv::fluv32; use glam::Vec4; use half::f16; use spectral_upsampling::meng::{spectrum_xyz_to_p_4, EQUAL_ENERGY_REFLECTANCE}; use crate::{lerp::Lerp, math::fast_exp}; // Minimum and maximum wavelength of light we care about, in nanometers const WL_MIN: f32 = 380.0; const WL_MAX: f32 = 700.0; const WL_RANGE: f32 = WL_MAX - WL_MIN; const WL_RANGE_Q: f32 = WL_RANGE / 4.0; pub fn map_0_1_to_wavelength(n: f32) -> f32 { n * WL_RANGE + WL_MIN } #[inline(always)] fn nth_wavelength(hero_wavelength: f32, n: usize) -> f32 { let wl = hero_wavelength + (WL_RANGE_Q * n as f32); if wl > WL_MAX { wl - WL_RANGE } else { wl } } /// Returns all wavelengths of a hero wavelength set as a Vec4 #[inline(always)] fn wavelengths(hero_wavelength: f32) -> Vec4 { Vec4::new( nth_wavelength(hero_wavelength, 0), nth_wavelength(hero_wavelength, 1), nth_wavelength(hero_wavelength, 2), nth_wavelength(hero_wavelength, 3), ) } //---------------------------------------------------------------- #[derive(Debug, Copy, Clone)] pub enum Color { XYZ(f32, f32, f32), Blackbody { temperature: f32, // In kelvin factor: f32, // Brightness multiplier }, // Same as Blackbody except with the spectrum's energy roughly // normalized. Temperature { temperature: f32, // In kelvin factor: f32, // Brightness multiplier }, } impl Color { #[inline(always)] pub fn new_xyz(xyz: (f32, f32, f32)) -> Self { Color::XYZ(xyz.0, xyz.1, xyz.2) } #[inline(always)] pub fn new_blackbody(temp: f32, fac: f32) -> Self { Color::Blackbody { temperature: temp, factor: fac, } } #[inline(always)] pub fn new_temperature(temp: f32, fac: f32) -> Self { Color::Temperature { temperature: temp, factor: fac, } } pub fn to_spectral_sample(self, hero_wavelength: f32) -> SpectralSample { let wls = wavelengths(hero_wavelength); match self { Color::XYZ(x, y, z) => SpectralSample { e: xyz_to_spectrum_4((x, y, z), wls), hero_wavelength: hero_wavelength, }, Color::Blackbody { temperature, factor, } => { SpectralSample::from_parts( // TODO: make this SIMD Vec4::new( plancks_law(temperature, wls[0]) * factor, plancks_law(temperature, wls[1]) * factor, plancks_law(temperature, wls[2]) * factor, plancks_law(temperature, wls[3]) * factor, ), hero_wavelength, ) } Color::Temperature { temperature, factor, } => { SpectralSample::from_parts( // TODO: make this SIMD Vec4::new( plancks_law_normalized(temperature, wls[0]) * factor, plancks_law_normalized(temperature, wls[1]) * factor, plancks_law_normalized(temperature, wls[2]) * factor, plancks_law_normalized(temperature, wls[3]) * factor, ), hero_wavelength, ) } } } /// Calculates an approximate total spectral energy of the color. /// /// Note: this really is very _approximate_. pub fn approximate_energy(self) -> f32 { // TODO: better approximation for Blackbody and Temperature. match self { Color::XYZ(_, y, _) => y, Color::Blackbody { temperature, factor, } => { let t2 = temperature * temperature; t2 * t2 * factor } Color::Temperature { factor, .. } => factor, } } /// Returns the post-compression size of this color. pub fn compressed_size(&self) -> usize { match self { Color::XYZ(_, _, _) => 5, Color::Blackbody { .. } => 5, Color::Temperature { .. } => 5, } } /// Writes the compressed form of this color to `out_data`. /// /// `out_data` must be at least `compressed_size()` bytes long, otherwise /// this method will panic. /// /// Returns the number of bytes written. pub fn write_compressed(&self, out_data: &mut [u8]) -> usize { match *self { Color::XYZ(x, y, z) => { out_data[0] = 0; // Discriminant (&mut out_data[1..5]).copy_from_slice(&fluv32::encode((x, y, z)).to_ne_bytes()[..]); } Color::Blackbody { temperature, factor, } => { out_data[0] = 1; // Discriminant let tmp = (temperature.min(std::u16::MAX as f32) as u16).to_le_bytes(); let fac = f16::from_f32(factor).to_bits().to_le_bytes(); out_data[1] = tmp[0]; out_data[2] = tmp[1]; out_data[3] = fac[0]; out_data[4] = fac[1]; } Color::Temperature { temperature, factor, } => { out_data[0] = 2; // Discriminant let tmp = (temperature.min(std::u16::MAX as f32) as u16).to_le_bytes(); let fac = f16::from_f32(factor).to_bits().to_le_bytes(); out_data[1] = tmp[0]; out_data[2] = tmp[1]; out_data[3] = fac[0]; out_data[4] = fac[1]; } } self.compressed_size() } /// Constructs a Color from compressed color data, and also returns the /// number of bytes consumed from `in_data`. pub fn from_compressed(in_data: &[u8]) -> (Color, usize) { match in_data[0] { 0 => { // XYZ let mut bytes = [0u8; 4]; (&mut bytes[..]).copy_from_slice(&in_data[1..5]); let (x, y, z) = fluv32::decode(u32::from_ne_bytes(bytes)); (Color::XYZ(x, y, z), 5) } 1 => { // Blackbody let mut tmp = [0u8; 2]; let mut fac = [0u8; 2]; tmp[0] = in_data[1]; tmp[1] = in_data[2]; fac[0] = in_data[3]; fac[1] = in_data[4]; let tmp = u16::from_le_bytes(tmp); let fac = f16::from_bits(u16::from_le_bytes(fac)); ( Color::Blackbody { temperature: tmp as f32, factor: fac.into(), }, 5, ) } 2 => { // Temperature let mut tmp = [0u8; 2]; let mut fac = [0u8; 2]; tmp[0] = in_data[1]; tmp[1] = in_data[2]; fac[0] = in_data[3]; fac[1] = in_data[4]; let tmp = u16::from_le_bytes(tmp); let fac = f16::from_bits(u16::from_le_bytes(fac)); ( Color::Temperature { temperature: tmp as f32, factor: fac.into(), }, 5, ) } _ => unreachable!(), } } } impl Mul for Color { type Output = Self; fn mul(self, rhs: f32) -> Self { match self { Color::XYZ(x, y, z) => Color::XYZ(x * rhs, y * rhs, z * rhs), Color::Blackbody { temperature, factor, } => Color::Blackbody { temperature: temperature, factor: factor * rhs, }, Color::Temperature { temperature, factor, } => Color::Temperature { temperature: temperature, factor: factor * rhs, }, } } } impl MulAssign for Color { fn mul_assign(&mut self, rhs: f32) { *self = *self * rhs; } } impl Lerp for Color { /// Note that this isn't a proper lerp in spectral space. However, /// for our purposes that should be fine: all we care about is that /// the interpolation is smooth and "reasonable". /// /// If at some point it turns out this causes artifacts, then we /// also have bigger problems: texture filtering in the shading /// pipeline will have the same issues, which will be even harder /// to address. However, I strongly suspect this will not be an issue. /// (Famous last words!) fn lerp(self, other: Self, alpha: f32) -> Self { let inv_alpha = 1.0 - alpha; match (self, other) { (Color::XYZ(x1, y1, z1), Color::XYZ(x2, y2, z2)) => Color::XYZ( (x1 * inv_alpha) + (x2 * alpha), (y1 * inv_alpha) + (y2 * alpha), (z1 * inv_alpha) + (z2 * alpha), ), ( Color::Blackbody { temperature: tmp1, factor: fac1, }, Color::Blackbody { temperature: tmp2, factor: fac2, }, ) => Color::Blackbody { temperature: (tmp1 * inv_alpha) + (tmp2 * alpha), factor: (fac1 * inv_alpha) + (fac2 * alpha), }, ( Color::Temperature { temperature: tmp1, factor: fac1, }, Color::Temperature { temperature: tmp2, factor: fac2, }, ) => Color::Temperature { temperature: (tmp1 * inv_alpha) + (tmp2 * alpha), factor: (fac1 * inv_alpha) + (fac2 * alpha), }, _ => panic!("Cannot lerp colors with different representations."), } } } fn plancks_law(temperature: f32, wavelength: f32) -> f32 { const C: f32 = 299_792_458.0; // Speed of light const H: f32 = 6.626_070_15e-34; // Planck constant const KB: f32 = 1.380_648_52e-23; // Boltzmann constant // At 400 kelvin and below, the spectrum is black anyway, // but the equations become numerically unstable somewhere // around 100 kelvin. So just return zero energy below 200. // (Technically there is still a tiny amount of energy that // we're losing this way, but it's incredibly tiny, with tons // of zeros after the decimal point--way less energy than would // ever, ever, ever even have the slightest chance of showing // impacting a render.) if temperature < 200.0 { return 0.0; } // Convert the wavelength from nanometers to meters for // the equations below. let wavelength = wavelength * 1.0e-9; // // As written at https://en.wikipedia.org/wiki/Planck's_law, here for // // reference and clarity: // let a = (2.0 * H * C * C) / (wavelength * wavelength * wavelength * wavelength * wavelength); // let b = 1.0 / (((H * C) / (wavelength * KB * temperature)).exp() - 1.0); // let energy = a * b; // Optimized version of the commented code above: const TMP1: f32 = (2.0f64 * H as f64 * C as f64 * C as f64) as f32; const TMP2: f32 = (H as f64 * C as f64 / KB as f64) as f32; let wl5 = { let wl2 = wavelength * wavelength; wl2 * wl2 * wavelength }; let tmp3 = wl5 * (fast_exp(TMP2 / (wavelength * temperature)) - 1.0); let energy = TMP1 / tmp3; // Convert energy to appropriate units and return. (energy * 1.0e-6).max(0.0) } /// Same as above, except normalized to keep roughly equal spectral /// energy across temperatures. This makes it easier to use for /// choosing colors without making brightness explode. fn plancks_law_normalized(temperature: f32, wavelength: f32) -> f32 { let t2 = temperature * temperature; plancks_law(temperature, wavelength) * 4.0e7 / (t2 * t2) } //---------------------------------------------------------------- #[derive(Copy, Clone, Debug)] pub struct SpectralSample { pub e: Vec4, hero_wavelength: f32, } impl SpectralSample { pub fn new(wavelength: f32) -> SpectralSample { debug_assert!(wavelength >= WL_MIN && wavelength <= WL_MAX); SpectralSample { e: Vec4::splat(0.0), hero_wavelength: wavelength, } } #[allow(dead_code)] pub fn from_value(value: f32, wavelength: f32) -> SpectralSample { debug_assert!(wavelength >= WL_MIN && wavelength <= WL_MAX); SpectralSample { e: Vec4::splat(value), hero_wavelength: wavelength, } } pub fn from_parts(e: Vec4, wavelength: f32) -> SpectralSample { debug_assert!(wavelength >= WL_MIN && wavelength <= WL_MAX); SpectralSample { e: e, hero_wavelength: wavelength, } } /// Returns the nth wavelength fn wl_n(&self, n: usize) -> f32 { let wl = self.hero_wavelength + (WL_RANGE_Q * n as f32); if wl > WL_MAX { wl - WL_RANGE } else { wl } } } impl Add for SpectralSample { type Output = SpectralSample; fn add(self, rhs: SpectralSample) -> Self::Output { assert_eq!(self.hero_wavelength, rhs.hero_wavelength); SpectralSample { e: self.e + rhs.e, hero_wavelength: self.hero_wavelength, } } } impl AddAssign for SpectralSample { fn add_assign(&mut self, rhs: SpectralSample) { assert_eq!(self.hero_wavelength, rhs.hero_wavelength); self.e = self.e + rhs.e; } } impl Mul for SpectralSample { type Output = SpectralSample; fn mul(self, rhs: SpectralSample) -> Self::Output { assert_eq!(self.hero_wavelength, rhs.hero_wavelength); SpectralSample { e: self.e * rhs.e, hero_wavelength: self.hero_wavelength, } } } impl MulAssign for SpectralSample { fn mul_assign(&mut self, rhs: SpectralSample) { assert_eq!(self.hero_wavelength, rhs.hero_wavelength); self.e = self.e * rhs.e; } } impl Mul for SpectralSample { type Output = SpectralSample; fn mul(self, rhs: f32) -> Self::Output { SpectralSample { e: self.e * rhs, hero_wavelength: self.hero_wavelength, } } } impl MulAssign for SpectralSample { fn mul_assign(&mut self, rhs: f32) { self.e = self.e * rhs; } } impl Div for SpectralSample { type Output = SpectralSample; fn div(self, rhs: f32) -> Self::Output { SpectralSample { e: self.e / rhs, hero_wavelength: self.hero_wavelength, } } } impl DivAssign for SpectralSample { fn div_assign(&mut self, rhs: f32) { self.e = self.e / rhs; } } //---------------------------------------------------------------- #[derive(Copy, Clone, Debug)] pub struct XYZ { pub x: f32, pub y: f32, pub z: f32, } impl XYZ { pub fn new(x: f32, y: f32, z: f32) -> XYZ { XYZ { x: x, y: y, z: z } } pub fn from_wavelength(wavelength: f32, intensity: f32) -> XYZ { XYZ { x: x_1931(wavelength) * intensity, y: y_1931(wavelength) * intensity, z: z_1931(wavelength) * intensity, } } pub fn from_spectral_sample(ss: &SpectralSample) -> XYZ { let xyz0 = XYZ::from_wavelength(ss.wl_n(0), ss.e[0]); let xyz1 = XYZ::from_wavelength(ss.wl_n(1), ss.e[1]); let xyz2 = XYZ::from_wavelength(ss.wl_n(2), ss.e[2]); let xyz3 = XYZ::from_wavelength(ss.wl_n(3), ss.e[3]); (xyz0 + xyz1 + xyz2 + xyz3) * 0.75 } pub fn to_tuple(&self) -> (f32, f32, f32) { (self.x, self.y, self.z) } } impl Lerp for XYZ { fn lerp(self, other: XYZ, alpha: f32) -> XYZ { (self * (1.0 - alpha)) + (other * alpha) } } impl Add for XYZ { type Output = XYZ; fn add(self, rhs: XYZ) -> Self::Output { XYZ { x: self.x + rhs.x, y: self.y + rhs.y, z: self.z + rhs.z, } } } impl AddAssign for XYZ { fn add_assign(&mut self, rhs: XYZ) { self.x += rhs.x; self.y += rhs.y; self.z += rhs.z; } } impl Mul for XYZ { type Output = XYZ; fn mul(self, rhs: f32) -> Self::Output { XYZ { x: self.x * rhs, y: self.y * rhs, z: self.z * rhs, } } } impl MulAssign for XYZ { fn mul_assign(&mut self, rhs: f32) { self.x *= rhs; self.y *= rhs; self.z *= rhs; } } impl Div for XYZ { type Output = XYZ; fn div(self, rhs: f32) -> Self::Output { XYZ { x: self.x / rhs, y: self.y / rhs, z: self.z / rhs, } } } impl DivAssign for XYZ { fn div_assign(&mut self, rhs: f32) { self.x /= rhs; self.y /= rhs; self.z /= rhs; } } //---------------------------------------------------------------- /// Samples an CIE 1931 XYZ color at a particular set of wavelengths, according to /// the method in the paper "Physically Meaningful Rendering using Tristimulus /// Colours" by Meng et al. #[inline(always)] fn xyz_to_spectrum_4(xyz: (f32, f32, f32), wavelengths: Vec4) -> Vec4 { spectrum_xyz_to_p_4(wavelengths, xyz) * Vec4::splat(1.0 / EQUAL_ENERGY_REFLECTANCE) // aces_to_spectrum_p4(wavelengths, xyz_to_aces_ap0_e(xyz)) } /// Close analytic approximations of the CIE 1931 XYZ color curves. /// From the paper "Simple Analytic Approximations to the CIE XYZ Color Matching /// Functions" by Wyman et al. pub fn x_1931(wavelength: f32) -> f32 { let t1 = (wavelength - 442.0) * (if wavelength < 442.0 { 0.0624 } else { 0.0374 }); let t2 = (wavelength - 599.8) * (if wavelength < 599.8 { 0.0264 } else { 0.0323 }); let t3 = (wavelength - 501.1) * (if wavelength < 501.1 { 0.0490 } else { 0.0382 }); (0.362 * fast_exp(-0.5 * t1 * t1)) + (1.056 * fast_exp(-0.5 * t2 * t2)) - (0.065 * fast_exp(-0.5 * t3 * t3)) } pub fn y_1931(wavelength: f32) -> f32 { let t1 = (wavelength - 568.8) * (if wavelength < 568.8 { 0.0213 } else { 0.0247 }); let t2 = (wavelength - 530.9) * (if wavelength < 530.9 { 0.0613 } else { 0.0322 }); (0.821 * fast_exp(-0.5 * t1 * t1)) + (0.286 * fast_exp(-0.5 * t2 * t2)) } pub fn z_1931(wavelength: f32) -> f32 { let t1 = (wavelength - 437.0) * (if wavelength < 437.0 { 0.0845 } else { 0.0278 }); let t2 = (wavelength - 459.0) * (if wavelength < 459.0 { 0.0385 } else { 0.0725 }); (1.217 * fast_exp(-0.5 * t1 * t1)) + (0.681 * fast_exp(-0.5 * t2 * t2)) } ================================================ FILE: src/fp_utils.rs ================================================ //! Utilities for handling floating point precision issues //! //! This is based on the work in section 3.9 of "Physically Based Rendering: //! From Theory to Implementation" 3rd edition by Pharr et al. use crate::math::{dot, Normal, Point, Vector}; #[inline(always)] pub fn fp_gamma(n: u32) -> f32 { use std::f32::EPSILON; let e = EPSILON * 0.5; (e * n as f32) / (1.0 - (e * n as f32)) } pub fn increment_ulp(v: f32) -> f32 { if v.is_finite() { if v > 0.0 { f32::from_bits(v.to_bits() + 1) } else if v < -0.0 { f32::from_bits(v.to_bits() - 1) } else { f32::from_bits(0x00_00_00_01) } } else { // Infinity or NaN. v } } pub fn decrement_ulp(v: f32) -> f32 { if v.is_finite() { if v > 0.0 { f32::from_bits(v.to_bits() - 1) } else if v < -0.0 { f32::from_bits(v.to_bits() + 1) } else { f32::from_bits(0x80_00_00_01) } } else { // Infinity or NaN. v } } pub fn robust_ray_origin(pos: Point, pos_err: f32, nor: Normal, ray_dir: Vector) -> Point { // Get surface normal pointing in the same // direction as ray_dir. let nor = { let nor = nor.into_vector(); if dot(nor, ray_dir) >= 0.0 { nor } else { -nor } }; // Calculate offset point let d = dot(nor.abs(), Vector::new(pos_err, pos_err, pos_err)); let offset = nor * d; let p = pos + offset; // Calculate ulp offsets let x = if nor.x() >= 0.0 { increment_ulp(p.x()) } else { decrement_ulp(p.x()) }; let y = if nor.y() >= 0.0 { increment_ulp(p.y()) } else { decrement_ulp(p.y()) }; let z = if nor.z() >= 0.0 { increment_ulp(p.z()) } else { decrement_ulp(p.z()) }; Point::new(x, y, z) } #[cfg(test)] mod tests { use super::*; #[test] fn inc_ulp() { assert!(increment_ulp(1.0) > 1.0); assert!(increment_ulp(-1.0) > -1.0); } #[test] fn dec_ulp() { assert!(decrement_ulp(1.0) < 1.0); assert!(decrement_ulp(-1.0) < -1.0); } #[test] fn inc_ulp_zero() { assert!(increment_ulp(0.0) > 0.0); assert!(increment_ulp(0.0) > -0.0); assert!(increment_ulp(-0.0) > 0.0); assert!(increment_ulp(-0.0) > -0.0); } #[test] fn dec_ulp_zero() { assert!(decrement_ulp(0.0) < 0.0); assert!(decrement_ulp(0.0) < -0.0); assert!(decrement_ulp(-0.0) < 0.0); assert!(decrement_ulp(-0.0) < -0.0); } #[test] fn inc_dec_ulp() { assert_eq!(decrement_ulp(increment_ulp(1.0)), 1.0); assert_eq!(decrement_ulp(increment_ulp(-1.0)), -1.0); assert_eq!(decrement_ulp(increment_ulp(1.2)), 1.2); assert_eq!(decrement_ulp(increment_ulp(-1.2)), -1.2); } #[test] fn dec_inc_ulp() { assert_eq!(increment_ulp(decrement_ulp(1.0)), 1.0); assert_eq!(increment_ulp(decrement_ulp(-1.0)), -1.0); assert_eq!(increment_ulp(decrement_ulp(1.2)), 1.2); assert_eq!(increment_ulp(decrement_ulp(-1.2)), -1.2); } } ================================================ FILE: src/hash.rs ================================================ pub fn hash_u32(n: u32, seed: u32) -> u32 { let mut hash = n; for _ in 0..3 { hash = hash.wrapping_mul(0x736caf6f); hash ^= hash.wrapping_shr(16); hash ^= seed; } hash } pub fn hash_u64(n: u64, seed: u64) -> u64 { let mut hash = n; for _ in 0..4 { hash = hash.wrapping_mul(32_416_190_071 * 314_604_959); hash ^= hash.wrapping_shr(32); hash ^= seed; } hash } /// Returns a random float in [0, 1] based on 'n' and a seed. /// Generally use n for getting a bunch of different random /// numbers, and use seed to vary between runs. pub fn hash_u32_to_f32(n: u32, seed: u32) -> f32 { const INV_MAX: f32 = 1.0 / std::u32::MAX as f32; hash_u32(n, seed) as f32 * INV_MAX } ================================================ FILE: src/hilbert.rs ================================================ #![allow(dead_code)] const N: u32 = 1 << 16; // Utility function used by the functions below. fn hil_rot(n: u32, rx: u32, ry: u32, x: &mut u32, y: &mut u32) { use std::mem; if ry == 0 { if rx == 1 { *x = (n - 1).wrapping_sub(*x); *y = (n - 1).wrapping_sub(*y); } mem::swap(x, y); } } /// Convert (x,y) to hilbert curve index. /// /// x: The x coordinate. Must be a positive integer no greater than 2^16-1. /// y: The y coordinate. Must be a positive integer no greater than 2^16-1. /// /// Returns the hilbert curve index corresponding to the (x,y) coordinates given. pub fn xy2d(x: u32, y: u32) -> u32 { assert!(x < N); assert!(y < N); let (mut x, mut y) = (x, y); let mut d = 0; let mut s = N >> 1; while s > 0 { let rx = if (x & s) > 0 { 1 } else { 0 }; let ry = if (y & s) > 0 { 1 } else { 0 }; d += s * s * ((3 * rx) ^ ry); hil_rot(s, rx, ry, &mut x, &mut y); s >>= 1 } d } /// Convert hilbert curve index to (x,y). /// /// d: The hilbert curve index. /// /// Returns the (x, y) coords at the given index. pub fn d2xy(d: u32) -> (u32, u32) { let (mut x, mut y) = (0, 0); let mut s = 1; let mut t = d; while s < N { let rx = 1 & (t >> 1); let ry = 1 & (t ^ rx); hil_rot(s, rx, ry, &mut x, &mut y); x += s * rx; y += s * ry; t >>= 2; s <<= 1; } (x, y) } #[cfg(test)] mod tests { use super::*; #[test] fn reversible() { let d = 54; let (x, y) = d2xy(d); let d2 = xy2d(x, y); assert_eq!(d, d2); } } ================================================ FILE: src/image.rs ================================================ #![allow(dead_code)] use std::{ cell::{RefCell, UnsafeCell}, cmp, fs::File, io, io::Write, marker::PhantomData, path::Path, sync::Mutex, }; use half::f16; use crate::color::{xyz_to_rec709_e, XYZ}; #[derive(Debug)] #[allow(clippy::type_complexity)] pub struct Image { data: UnsafeCell>, res: (usize, usize), checked_out_blocks: Mutex>>, // (min, max) } unsafe impl Sync for Image {} impl Image { pub fn new(width: usize, height: usize) -> Image { Image { data: UnsafeCell::new(vec![XYZ::new(0.0, 0.0, 0.0); width * height]), res: (width, height), checked_out_blocks: Mutex::new(RefCell::new(Vec::new())), } } pub fn width(&self) -> usize { self.res.0 } pub fn height(&self) -> usize { self.res.1 } pub fn get(&mut self, x: usize, y: usize) -> XYZ { assert!(x < self.res.0); assert!(y < self.res.1); let data: &Vec = unsafe { &*self.data.get() }; data[self.res.0 * y + x] } pub fn set(&mut self, x: usize, y: usize, value: XYZ) { assert!(x < self.res.0); assert!(y < self.res.1); let data: &mut Vec = unsafe { &mut *self.data.get() }; data[self.res.0 * y + x] = value; } pub fn get_bucket<'a>(&'a self, min: (u32, u32), max: (u32, u32)) -> Bucket<'a> { let tmp = self.checked_out_blocks.lock().unwrap(); let mut bucket_list = tmp.borrow_mut(); // Make sure this won't overlap with any already checked out buckets for bucket in bucket_list.iter() { // Calculate the intersection between the buckets let inter_min = (cmp::max(min.0, (bucket.0).0), cmp::max(min.1, (bucket.0).1)); let inter_max = (cmp::min(max.0, (bucket.1).0), cmp::min(max.1, (bucket.1).1)); // If it's not degenerate and not zero-sized, there's overlap, so // panic. if inter_min.0 < inter_max.0 && inter_min.1 < inter_max.1 { panic!("Attempted to check out a bucket with pixels that are already checked out."); } } // Clip bucket to image let max = ( cmp::min(max.0, self.res.0 as u32), cmp::min(max.1, self.res.1 as u32), ); // Push bucket onto list bucket_list.push((min, max)); Bucket { min: min, max: max, // This cast to `*mut` is okay, because we have already dynamically // ensured earlier in this function that the same memory locations // aren't aliased. img: self as *const Image as *mut Image, _phantom: PhantomData, } } pub fn write_ascii_ppm(&mut self, path: &Path) -> io::Result<()> { // Open file. let mut f = io::BufWriter::new(File::create(path)?); // Write header write!(f, "P3\n{} {}\n255\n", self.res.0, self.res.1)?; // Write pixels for y in 0..self.res.1 { for x in 0..self.res.0 { let (r, g, b) = quantize_tri_255(xyz_to_srgbe(self.get(x, y).to_tuple())); write!(f, "{} {} {} ", r, g, b)?; } write!(f, "\n")?; } // Done Ok(()) } pub fn write_binary_ppm(&mut self, path: &Path) -> io::Result<()> { // Open file. let mut f = io::BufWriter::new(File::create(path)?); // Write header write!(f, "P6\n{} {}\n255\n", self.res.0, self.res.1)?; // Write pixels for y in 0..self.res.1 { for x in 0..self.res.0 { let (r, g, b) = quantize_tri_255(xyz_to_srgbe(self.get(x, y).to_tuple())); let d = [r, g, b]; f.write_all(&d)?; } } // Done Ok(()) } pub fn write_png(&mut self, path: &Path) -> io::Result<()> { let mut image = Vec::new(); // Convert pixels let res_x = self.res.0; let res_y = self.res.1; for y in 0..res_y { for x in 0..res_x { let (r, g, b) = quantize_tri_255(xyz_to_srgbe(self.get(x, res_y - 1 - y).to_tuple())); image.push(r); image.push(g); image.push(b); image.push(255); } } // Write file png_encode_mini::write_rgba_from_u8( &mut File::create(path)?, &image, self.res.0 as u32, self.res.1 as u32, )?; // Done Ok(()) } pub fn write_exr(&mut self, path: &Path) { let mut image = Vec::new(); // Convert pixels for y in 0..self.res.1 { for x in 0..self.res.0 { let (r, g, b) = xyz_to_rec709_e(self.get(x, y).to_tuple()); image.push((f16::from_f32(r), f16::from_f32(g), f16::from_f32(b))); } } let mut file = io::BufWriter::new(File::create(path).unwrap()); let mut wr = openexr::ScanlineOutputFile::new( &mut file, openexr::Header::new() .set_resolution(self.res.0 as u32, self.res.1 as u32) .add_channel("R", openexr::PixelType::HALF) .add_channel("G", openexr::PixelType::HALF) .add_channel("B", openexr::PixelType::HALF) .set_compression(openexr::header::Compression::PIZ_COMPRESSION), ) .unwrap(); wr.write_pixels( openexr::FrameBuffer::new(self.res.0 as u32, self.res.1 as u32) .insert_channels(&["R", "G", "B"], &image), ) .unwrap(); } } #[derive(Debug)] pub struct Bucket<'a> { min: (u32, u32), max: (u32, u32), img: *mut Image, _phantom: PhantomData<&'a Image>, } impl<'a> Bucket<'a> { pub fn get(&mut self, x: u32, y: u32) -> XYZ { assert!(x >= self.min.0 && x < self.max.0); assert!(y >= self.min.1 && y < self.max.1); let img: &mut Image = unsafe { &mut *self.img }; let data: &Vec = unsafe { &mut *img.data.get() }; data[img.res.0 * y as usize + x as usize] } pub fn set(&mut self, x: u32, y: u32, value: XYZ) { assert!(x >= self.min.0 && x < self.max.0); assert!(y >= self.min.1 && y < self.max.1); let img: &mut Image = unsafe { &mut *self.img }; let data: &mut Vec = unsafe { &mut *img.data.get() }; data[img.res.0 * y as usize + x as usize] = value; } /// Returns the bucket's contents encoded in base64. /// /// `color_convert` lets you do a colorspace conversion before base64 /// encoding if desired. /// /// The data is laid out as four-floats-per-pixel in scanline order before /// encoding to base64. The fourth channel is alpha, and is set to 1.0 for /// all pixels. pub fn rgba_base64(&mut self, color_convert: F) -> String where F: Fn((f32, f32, f32)) -> (f32, f32, f32), { use std::slice; let mut data = Vec::with_capacity( (4 * (self.max.0 - self.min.0) * (self.max.1 - self.min.1)) as usize, ); for y in self.min.1..self.max.1 { for x in self.min.0..self.max.0 { let color = color_convert(self.get(x, y).to_tuple()); data.push(color.0); data.push(color.1); data.push(color.2); data.push(1.0); } } let data_u8 = unsafe { slice::from_raw_parts(&data[0] as *const f32 as *const u8, data.len() * 4) }; base64::encode(data_u8) } } impl<'a> Drop for Bucket<'a> { fn drop(&mut self) { let img: &mut Image = unsafe { &mut *self.img }; let tmp = img.checked_out_blocks.lock().unwrap(); let mut bucket_list = tmp.borrow_mut(); // Find matching bucket and remove it let i = bucket_list.iter().position(|bucket| { (bucket.0).0 == self.min.0 && (bucket.0).1 == self.min.1 && (bucket.1).0 == self.max.0 && (bucket.1).1 == self.max.1 }); bucket_list.swap_remove(i.unwrap()); } } fn srgb_gamma(n: f32) -> f32 { if n < 0.003_130_8 { n * 12.92 } else { (1.055 * n.powf(1.0 / 2.4)) - 0.055 } } fn srgb_inv_gamma(n: f32) -> f32 { if n < 0.04045 { n / 12.92 } else { ((n + 0.055) / 1.055).powf(2.4) } } fn xyz_to_srgbe(xyz: (f32, f32, f32)) -> (f32, f32, f32) { let rgb = xyz_to_rec709_e(xyz); (srgb_gamma(rgb.0), srgb_gamma(rgb.1), srgb_gamma(rgb.2)) } fn quantize_tri_255(tri: (f32, f32, f32)) -> (u8, u8, u8) { fn quantize(n: f32) -> u8 { let n = 1.0f32.min(0.0f32.max(n)) * 255.0; n as u8 } (quantize(tri.0), quantize(tri.1), quantize(tri.2)) } ================================================ FILE: src/lerp.rs ================================================ #![allow(dead_code)] use math3d::{Normal, Point, Transform, Vector}; /// Trait for allowing a type to be linearly interpolated. pub trait Lerp: Copy { fn lerp(self, other: Self, alpha: f32) -> Self; } /// Interpolates between two instances of a Lerp types. pub fn lerp(a: T, b: T, alpha: f32) -> T { debug_assert!(alpha >= 0.0); debug_assert!(alpha <= 1.0); a.lerp(b, alpha) } /// Interpolates a slice of data as if each adjecent pair of elements /// represent a linear segment. pub fn lerp_slice(s: &[T], alpha: f32) -> T { debug_assert!(!s.is_empty()); debug_assert!(alpha >= 0.0); debug_assert!(alpha <= 1.0); if s.len() == 1 || alpha == 1.0 { *s.last().unwrap() } else { let tmp = alpha * ((s.len() - 1) as f32); let i1 = tmp as usize; let i2 = i1 + 1; let alpha2 = tmp - (i1 as f32); lerp(s[i1], s[i2], alpha2) } } pub fn lerp_slice_with(s: &[T], alpha: f32, f: F) -> T where T: Copy, F: Fn(T, T, f32) -> T, { debug_assert!(!s.is_empty()); debug_assert!(alpha >= 0.0); debug_assert!(alpha <= 1.0); if s.len() == 1 || alpha == 1.0 { *s.last().unwrap() } else { let tmp = alpha * ((s.len() - 1) as f32); let i1 = tmp as usize; let i2 = i1 + 1; let alpha2 = tmp - (i1 as f32); f(s[i1], s[i2], alpha2) } } impl Lerp for f32 { fn lerp(self, other: f32, alpha: f32) -> f32 { (self * (1.0 - alpha)) + (other * alpha) } } impl Lerp for f64 { fn lerp(self, other: f64, alpha: f32) -> f64 { (self * (1.0 - alpha as f64)) + (other * alpha as f64) } } impl Lerp for (T, T) { fn lerp(self, other: (T, T), alpha: f32) -> (T, T) { (self.0.lerp(other.0, alpha), self.1.lerp(other.1, alpha)) } } impl Lerp for [T; 2] { fn lerp(self, other: Self, alpha: f32) -> Self { [self[0].lerp(other[0], alpha), self[1].lerp(other[1], alpha)] } } impl Lerp for [T; 3] { fn lerp(self, other: Self, alpha: f32) -> Self { [ self[0].lerp(other[0], alpha), self[1].lerp(other[1], alpha), self[2].lerp(other[2], alpha), ] } } impl Lerp for [T; 4] { fn lerp(self, other: Self, alpha: f32) -> Self { [ self[0].lerp(other[0], alpha), self[1].lerp(other[1], alpha), self[2].lerp(other[2], alpha), self[3].lerp(other[3], alpha), ] } } impl Lerp for glam::Vec4 { fn lerp(self, other: glam::Vec4, alpha: f32) -> glam::Vec4 { (self * (1.0 - alpha)) + (other * alpha) } } impl Lerp for Transform { fn lerp(self, other: Transform, alpha: f32) -> Transform { (self * (1.0 - alpha)) + (other * alpha) } } impl Lerp for Normal { fn lerp(self, other: Normal, alpha: f32) -> Normal { (self * (1.0 - alpha)) + (other * alpha) } } impl Lerp for Point { fn lerp(self, other: Point, alpha: f32) -> Point { let s = self; let o = other; Point { co: (s.co * (1.0 - alpha)) + (o.co * alpha), } } } impl Lerp for Vector { fn lerp(self, other: Vector, alpha: f32) -> Vector { (self * (1.0 - alpha)) + (other * alpha) } } #[cfg(test)] mod tests { use super::*; #[test] fn lerp1() { let a = 1.0f32; let b = 2.0f32; let alpha = 0.0f32; assert_eq!(1.0, lerp(a, b, alpha)); } #[test] fn lerp2() { let a = 1.0f32; let b = 2.0f32; let alpha = 1.0f32; assert_eq!(2.0, lerp(a, b, alpha)); } #[test] fn lerp3() { let a = 1.0f32; let b = 2.0f32; let alpha = 0.5f32; assert_eq!(1.5, lerp(a, b, alpha)); } #[test] fn lerp_slice1() { let s = [0.0f32, 1.0, 2.0, 3.0, 4.0]; let alpha = 0.0f32; assert_eq!(0.0, lerp_slice(&s[..], alpha)); } #[test] fn lerp_slice2() { let s = [0.0f32, 1.0, 2.0, 3.0, 4.0]; let alpha = 1.0f32; assert_eq!(4.0, lerp_slice(&s[..], alpha)); } #[test] fn lerp_slice3() { let s = [0.0f32, 1.0, 2.0, 3.0, 4.0]; let alpha = 0.5f32; assert_eq!(2.0, lerp_slice(&s[..], alpha)); } #[test] fn lerp_slice4() { let s = [0.0f32, 1.0, 2.0, 3.0, 4.0]; let alpha = 0.25f32; assert_eq!(1.0, lerp_slice(&s[..], alpha)); } #[test] fn lerp_slice5() { let s = [0.0f32, 1.0, 2.0, 3.0, 4.0]; let alpha = 0.75f32; assert_eq!(3.0, lerp_slice(&s[..], alpha)); } #[test] fn lerp_slice6() { let s = [0.0f32, 1.0, 2.0, 3.0, 4.0]; let alpha = 0.625f32; assert_eq!(2.5, lerp_slice(&s[..], alpha)); } #[test] fn lerp_matrix() { let a = Transform::new_from_values( 0.0, 2.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, ); let b = Transform::new_from_values( -1.0, 1.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, ); let c1 = Transform::new_from_values( -0.25, 1.75, 2.25, 3.25, 4.25, 5.25, 6.25, 7.25, 8.25, 9.25, 10.25, 11.25, ); let c2 = Transform::new_from_values( -0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5, 10.5, 11.5, ); let c3 = Transform::new_from_values( -0.75, 1.25, 2.75, 3.75, 4.75, 5.75, 6.75, 7.75, 8.75, 9.75, 10.75, 11.75, ); assert_eq!(a.lerp(b, 0.0), a); assert_eq!(a.lerp(b, 0.25), c1); assert_eq!(a.lerp(b, 0.5), c2); assert_eq!(a.lerp(b, 0.75), c3); assert_eq!(a.lerp(b, 1.0), b); } #[test] fn lerp_point_1() { let p1 = Point::new(1.0, 2.0, 1.0); let p2 = Point::new(-2.0, 1.0, -1.0); let p3 = Point::new(1.0, 2.0, 1.0); assert_eq!(p3, p1.lerp(p2, 0.0)); } #[test] fn lerp_point_2() { let p1 = Point::new(1.0, 2.0, 1.0); let p2 = Point::new(-2.0, 1.0, -1.0); let p3 = Point::new(-2.0, 1.0, -1.0); assert_eq!(p3, p1.lerp(p2, 1.0)); } #[test] fn lerp_point_3() { let p1 = Point::new(1.0, 2.0, 1.0); let p2 = Point::new(-2.0, 1.0, -1.0); let p3 = Point::new(-0.5, 1.5, 0.0); assert_eq!(p3, p1.lerp(p2, 0.5)); } #[test] fn lerp_normal_1() { let n1 = Normal::new(1.0, 2.0, 1.0); let n2 = Normal::new(-2.0, 1.0, -1.0); let n3 = Normal::new(1.0, 2.0, 1.0); assert_eq!(n3, n1.lerp(n2, 0.0)); } #[test] fn lerp_normal_2() { let n1 = Normal::new(1.0, 2.0, 1.0); let n2 = Normal::new(-2.0, 1.0, -1.0); let n3 = Normal::new(-2.0, 1.0, -1.0); assert_eq!(n3, n1.lerp(n2, 1.0)); } #[test] fn lerp_normal_3() { let n1 = Normal::new(1.0, 2.0, 1.0); let n2 = Normal::new(-2.0, 1.0, -1.0); let n3 = Normal::new(-0.5, 1.5, 0.0); assert_eq!(n3, n1.lerp(n2, 0.5)); } #[test] fn lerp_vector_1() { let v1 = Vector::new(1.0, 2.0, 1.0); let v2 = Vector::new(-2.0, 1.0, -1.0); let v3 = Vector::new(1.0, 2.0, 1.0); assert_eq!(v3, v1.lerp(v2, 0.0)); } #[test] fn lerp_vector_2() { let v1 = Vector::new(1.0, 2.0, 1.0); let v2 = Vector::new(-2.0, 1.0, -1.0); let v3 = Vector::new(-2.0, 1.0, -1.0); assert_eq!(v3, v1.lerp(v2, 1.0)); } #[test] fn lerp_vector_3() { let v1 = Vector::new(1.0, 2.0, 1.0); let v2 = Vector::new(-2.0, 1.0, -1.0); let v3 = Vector::new(-0.5, 1.5, 0.0); assert_eq!(v3, v1.lerp(v2, 0.5)); } } ================================================ FILE: src/light/distant_disk_light.rs ================================================ use std::f64::consts::PI as PI_64; use kioku::Arena; use crate::{ color::{Color, SpectralSample}, lerp::lerp_slice, math::{coordinate_system_from_vector, Vector}, sampling::{uniform_sample_cone, uniform_sample_cone_pdf}, }; use super::WorldLightSource; // TODO: handle case where radius = 0.0. #[derive(Copy, Clone, Debug)] pub struct DistantDiskLight<'a> { radii: &'a [f32], directions: &'a [Vector], colors: &'a [Color], } impl<'a> DistantDiskLight<'a> { pub fn new( arena: &'a Arena, radii: &[f32], directions: &[Vector], colors: &[Color], ) -> DistantDiskLight<'a> { DistantDiskLight { radii: arena.copy_slice(&radii), directions: arena.copy_slice(&directions), colors: arena.copy_slice(&colors), } } // fn sample_pdf(&self, sample_dir: Vector, wavelength: f32, time: f32) -> f32 { // // We're not using these, silence warnings // let _ = (sample_dir, wavelength); // let radius: f64 = lerp_slice(self.radii, time) as f64; // uniform_sample_cone_pdf(radius.cos()) as f32 // } // fn outgoing(&self, dir: Vector, wavelength: f32, time: f32) -> SpectralSample { // // We're not using this, silence warning // let _ = dir; // let radius = lerp_slice(self.radii, time) as f64; // let col = lerp_slice(self.colors, time); // let solid_angle = 2.0 * PI_64 * (1.0 - radius.cos()); // (col / solid_angle as f32).to_spectral_sample(wavelength) // } } impl<'a> WorldLightSource for DistantDiskLight<'a> { fn sample_from_point( &self, u: f32, v: f32, wavelength: f32, time: f32, ) -> (SpectralSample, Vector, f32) { // Calculate time interpolated values let radius: f64 = lerp_slice(self.radii, time) as f64; let direction = lerp_slice(self.directions, time); let col = lerp_slice(self.colors, time); let solid_angle_inv = 1.0 / (2.0 * PI_64 * (1.0 - radius.cos())); // Create a coordinate system from the vector pointing at the center of // of the light. let (z, x, y) = coordinate_system_from_vector(-direction.normalized()); // Sample the cone subtended by the light. let cos_theta_max: f64 = radius.cos(); let sample = uniform_sample_cone(u, v, cos_theta_max).normalized(); // Calculate the final values and return everything. let spectral_sample = col.to_spectral_sample(wavelength) * solid_angle_inv as f32; let shadow_vec = (x * sample.x()) + (y * sample.y()) + (z * sample.z()); let pdf = uniform_sample_cone_pdf(cos_theta_max); (spectral_sample, shadow_vec, pdf as f32) } fn is_delta(&self) -> bool { false } fn approximate_energy(&self) -> f32 { self.colors .iter() .fold(0.0, |a, &b| a + b.approximate_energy()) / self.colors.len() as f32 } } ================================================ FILE: src/light/mod.rs ================================================ mod distant_disk_light; mod rectangle_light; mod sphere_light; use std::fmt::Debug; use crate::{ color::SpectralSample, math::{Normal, Point, Transform, Vector}, surface::Surface, }; pub use self::{ distant_disk_light::DistantDiskLight, rectangle_light::RectangleLight, sphere_light::SphereLight, }; /// A finite light source that can be bounded in space. pub trait SurfaceLight: Surface { /// Samples the surface given a point to be illuminated. /// /// - `space`: The world-to-object space transform of the light. /// - `arr`: The point to be illuminated (in world space). /// - `u`: Random parameter U. /// - `v`: Random parameter V. /// - `wavelength`: The wavelength of light to sample at. /// - `time`: The time to sample at. /// /// Returns: /// - The light arriving at the point arr. /// - A tuple with the sample point on the light, the surface normal at /// that point, and the point's error magnitude. These are used /// elsewhere to create a robust shadow ray. /// - The pdf of the sample. fn sample_from_point( &self, space: &Transform, arr: Point, u: f32, v: f32, wavelength: f32, time: f32, ) -> (SpectralSample, (Point, Normal, f32), f32); /// Returns whether the light has a delta distribution. /// /// If a light has no chance of a ray hitting it through random process /// then it is a delta light source. For example, point light sources, /// lights that only emit in a single direction, etc. fn is_delta(&self) -> bool; /// Returns an approximation of the total energy emitted by the surface. /// /// Note: this does not need to be exact, but it does need to be non-zero /// for any surface that does emit light. This is used for importance /// sampling. fn approximate_energy(&self) -> f32; } /// An infinite light source that cannot be bounded in space. E.g. /// a sun light source. pub trait WorldLightSource: Debug + Sync { /// Samples the light source for a given point to be illuminated. /// /// - u: Random parameter U. /// - v: Random parameter V. /// - wavelength: The wavelength of light to sample at. /// - time: The time to sample at. /// /// Returns: The light arriving from the shadow-testing direction, the /// vector to use for shadow testing, and the pdf of the sample. fn sample_from_point( &self, u: f32, v: f32, wavelength: f32, time: f32, ) -> (SpectralSample, Vector, f32); /// Returns whether the light has a delta distribution. /// /// If a light has no chance of a ray hitting it through random process /// then it is a delta light source. For example, point light sources, /// lights that only emit in a single direction, etc. fn is_delta(&self) -> bool; /// Returns an approximation of the total energy emitted by the light /// source. /// /// Note: this does not need to be exact, but it does need to be non-zero /// for any light that emits any light. This is used for importance /// sampling. fn approximate_energy(&self) -> f32; } ================================================ FILE: src/light/rectangle_light.rs ================================================ use kioku::Arena; use crate::{ bbox::BBox, boundable::Boundable, color::{Color, SpectralSample}, lerp::lerp_slice, math::{cross, dot, Normal, Point, Transform, Vector}, ray::{RayBatch, RayStack}, sampling::{ spherical_triangle_solid_angle, triangle_surface_area, uniform_sample_spherical_triangle, uniform_sample_triangle, }, shading::surface_closure::SurfaceClosure, shading::SurfaceShader, surface::{triangle, Surface, SurfaceIntersection, SurfaceIntersectionData}, }; use super::SurfaceLight; const SIMPLE_SAMPLING_THRESHOLD: f32 = 0.01; #[derive(Copy, Clone, Debug)] pub struct RectangleLight<'a> { dimensions: &'a [(f32, f32)], colors: &'a [Color], bounds_: &'a [BBox], } impl<'a> RectangleLight<'a> { pub fn new<'b>( arena: &'b Arena, dimensions: &[(f32, f32)], colors: &[Color], ) -> RectangleLight<'b> { let bbs: Vec<_> = dimensions .iter() .map(|d| BBox { min: Point::new(d.0 * -0.5, d.1 * -0.5, 0.0), max: Point::new(d.0 * 0.5, d.1 * 0.5, 0.0), }) .collect(); RectangleLight { dimensions: arena.copy_slice(&dimensions), colors: arena.copy_slice(&colors), bounds_: arena.copy_slice(&bbs), } } // TODO: this is only used from within `intersect_rays`, and could be done // more efficiently by inlining it there. fn sample_pdf( &self, space: &Transform, arr: Point, sample_dir: Vector, hit_point: Point, wavelength: f32, time: f32, ) -> f32 { // We're not using these, silence warnings let _ = wavelength; let dim = lerp_slice(self.dimensions, time); // Get the four corners of the rectangle, transformed into world space let space_inv = space.inverse(); let p1 = Point::new(dim.0 * 0.5, dim.1 * 0.5, 0.0) * space_inv; let p2 = Point::new(dim.0 * -0.5, dim.1 * 0.5, 0.0) * space_inv; let p3 = Point::new(dim.0 * -0.5, dim.1 * -0.5, 0.0) * space_inv; let p4 = Point::new(dim.0 * 0.5, dim.1 * -0.5, 0.0) * space_inv; // Get the four corners of the rectangle, projected on to the unit // sphere centered around arr. let sp1 = (p1 - arr).normalized(); let sp2 = (p2 - arr).normalized(); let sp3 = (p3 - arr).normalized(); let sp4 = (p4 - arr).normalized(); // Get the solid angles of the rectangle split into two triangles let area_1 = spherical_triangle_solid_angle(sp2, sp1, sp3); let area_2 = spherical_triangle_solid_angle(sp4, sp1, sp3); // World-space surface normal let normal = Normal::new(0.0, 0.0, 1.0) * space_inv; // PDF if (area_1 + area_2) < SIMPLE_SAMPLING_THRESHOLD { let area = triangle_surface_area(p2, p1, p3) + triangle_surface_area(p4, p1, p3); (hit_point - arr).length2() / dot(sample_dir.normalized(), normal.into_vector().normalized()).abs() / area } else { 1.0 / (area_1 + area_2) } } // fn outgoing( // &self, // space: &Transform, // dir: Vector, // u: f32, // v: f32, // wavelength: f32, // time: f32, // ) -> SpectralSample { // // We're not using these, silence warnings // let _ = (space, dir, u, v); // let dim = lerp_slice(self.dimensions, time); // let col = lerp_slice(self.colors, time); // // TODO: Is this right? Do we need to get the surface area post-transform? // let surface_area_inv: f64 = 1.0 / (dim.0 as f64 * dim.1 as f64); // (col * surface_area_inv as f32 * 0.5).to_spectral_sample(wavelength) // } } impl<'a> SurfaceLight for RectangleLight<'a> { fn sample_from_point( &self, space: &Transform, arr: Point, u: f32, v: f32, wavelength: f32, time: f32, ) -> (SpectralSample, (Point, Normal, f32), f32) { // Calculate time interpolated values let dim = lerp_slice(self.dimensions, time); let col = lerp_slice(self.colors, time); let surface_area: f64 = dim.0 as f64 * dim.1 as f64; let surface_area_inv: f64 = 1.0 / surface_area; // Get the four corners of the rectangle, transformed into world space let space_inv = space.inverse(); let p1 = Point::new(dim.0 * 0.5, dim.1 * 0.5, 0.0) * space_inv; let p2 = Point::new(dim.0 * -0.5, dim.1 * 0.5, 0.0) * space_inv; let p3 = Point::new(dim.0 * -0.5, dim.1 * -0.5, 0.0) * space_inv; let p4 = Point::new(dim.0 * 0.5, dim.1 * -0.5, 0.0) * space_inv; // Get the four corners of the rectangle relative to arr. let lp1 = p1 - arr; let lp2 = p2 - arr; let lp3 = p3 - arr; let lp4 = p4 - arr; // Four corners projected on to the unit sphere. let sp1 = lp1.normalized(); let sp2 = lp2.normalized(); let sp3 = lp3.normalized(); let sp4 = lp4.normalized(); // Get the solid angles of the rectangle split into two triangles let area_1 = spherical_triangle_solid_angle(sp2, sp1, sp3); let area_2 = spherical_triangle_solid_angle(sp4, sp1, sp3); // Calculate world-space surface normal let normal = Normal::new(0.0, 0.0, 1.0) * space_inv; if (area_1 + area_2) < SIMPLE_SAMPLING_THRESHOLD { // Simple sampling for more distant lights let surface_area_1 = triangle_surface_area(p2, p1, p3); let surface_area_2 = triangle_surface_area(p4, p1, p3); let sample_point = { // Select which triangle to sample let threshhold = surface_area_1 / (surface_area_1 + surface_area_2); if u < threshhold { uniform_sample_triangle( p2.into_vector(), p1.into_vector(), p3.into_vector(), v, u / threshhold, ) } else { uniform_sample_triangle( p4.into_vector(), p1.into_vector(), p3.into_vector(), v, (u - threshhold) / (1.0 - threshhold), ) } } .into_point(); let shadow_vec = sample_point - arr; let spectral_sample = (col).to_spectral_sample(wavelength) * surface_area_inv as f32 * 0.5; let pdf = (sample_point - arr).length2() / dot(shadow_vec.normalized(), normal.into_vector().normalized()).abs() / (surface_area_1 + surface_area_2); let point_err = 0.0001; // TODO: this is a hack, do properly. (spectral_sample, (sample_point, normal, point_err), pdf) } else { // Sophisticated sampling for close lights. // Normalize the solid angles for selection purposes let prob_1 = if area_1.is_infinite() { 1.0 } else if area_2.is_infinite() { 0.0 } else { area_1 / (area_1 + area_2) }; let prob_2 = 1.0 - prob_1; // Select one of the triangles and sample it let shadow_vec = if u < prob_1 { uniform_sample_spherical_triangle(sp2, sp1, sp3, v, u / prob_1) } else { uniform_sample_spherical_triangle(sp4, sp1, sp3, v, 1.0 - ((u - prob_1) / prob_2)) }; // Project shadow_vec back onto the light's surface let arr_local = arr * *space; let shadow_vec_local = shadow_vec * *space; let shadow_vec_local = shadow_vec_local * (-arr_local.z() / shadow_vec_local.z()); let mut sample_point_local = arr_local + shadow_vec_local; { let x = sample_point_local.x().max(dim.0 * -0.5).min(dim.0 * 0.5); let y = sample_point_local.y().max(dim.1 * -0.5).min(dim.1 * 0.5); sample_point_local.set_x(x); sample_point_local.set_y(y); sample_point_local.set_z(0.0); } let sample_point = sample_point_local * space_inv; let point_err = 0.0001; // TODO: this is a hack, do properly. // Calculate pdf and light energy let pdf = 1.0 / (area_1 + area_2); // PDF of the ray direction being sampled let spectral_sample = col.to_spectral_sample(wavelength) * surface_area_inv as f32 * 0.5; ( spectral_sample, (sample_point, normal, point_err), pdf as f32, ) } } fn is_delta(&self) -> bool { false } fn approximate_energy(&self) -> f32 { self.colors .iter() .fold(0.0, |a, &b| a + b.approximate_energy()) / self.colors.len() as f32 } } impl<'a> Surface for RectangleLight<'a> { fn intersect_rays( &self, rays: &mut RayBatch, ray_stack: &mut RayStack, isects: &mut [SurfaceIntersection], shader: &dyn SurfaceShader, space: &[Transform], ) { let _ = shader; // Silence 'unused' warning ray_stack.pop_do_next_task(|ray_idx| { let time = rays.time(ray_idx); let orig = rays.orig(ray_idx); let dir = rays.dir(ray_idx); let max_t = rays.max_t(ray_idx); // Calculate time interpolated values let dim = lerp_slice(self.dimensions, time); let xform = lerp_slice(space, time); let space_inv = xform.inverse(); // Get the four corners of the rectangle, transformed into world space let p1 = Point::new(dim.0 * 0.5, dim.1 * 0.5, 0.0) * space_inv; let p2 = Point::new(dim.0 * -0.5, dim.1 * 0.5, 0.0) * space_inv; let p3 = Point::new(dim.0 * -0.5, dim.1 * -0.5, 0.0) * space_inv; let p4 = Point::new(dim.0 * 0.5, dim.1 * -0.5, 0.0) * space_inv; // Test against two triangles that make up the light let ray_pre = triangle::RayTriPrecompute::new(dir); for tri in &[(p1, p2, p3), (p3, p4, p1)] { if let Some((t, b0, b1, b2)) = triangle::intersect_ray(orig, ray_pre, max_t, *tri) { if t < max_t { if rays.is_occlusion(ray_idx) { isects[ray_idx] = SurfaceIntersection::Occlude; rays.mark_done(ray_idx); } else { let (pos, pos_err) = triangle::surface_point(*tri, (b0, b1, b2)); let normal = cross(tri.0 - tri.1, tri.0 - tri.2).into_normal(); let intersection_data = SurfaceIntersectionData { incoming: dir, t: t, pos: pos, pos_err: pos_err, nor: normal, nor_g: normal, local_space: xform, sample_pdf: self.sample_pdf( &xform, orig, dir, pos, rays.wavelength(ray_idx), time, ), }; let closure = { let inv_surface_area = (1.0 / (dim.0 as f64 * dim.1 as f64)) as f32; let color = lerp_slice(self.colors, time) * inv_surface_area; SurfaceClosure::Emit(color) }; // Fill in intersection isects[ray_idx] = SurfaceIntersection::Hit { intersection_data: intersection_data, closure: closure, }; // Set ray's max t rays.set_max_t(ray_idx, t); } break; } } } }); } } impl<'a> Boundable for RectangleLight<'a> { fn bounds(&self) -> &[BBox] { self.bounds_ } } ================================================ FILE: src/light/sphere_light.rs ================================================ use std::f64::consts::PI as PI_64; use kioku::Arena; use crate::{ bbox::BBox, boundable::Boundable, color::{Color, SpectralSample}, lerp::lerp_slice, math::{coordinate_system_from_vector, dot, Normal, Point, Transform, Vector}, ray::{RayBatch, RayStack}, sampling::{uniform_sample_cone, uniform_sample_cone_pdf, uniform_sample_sphere}, shading::surface_closure::SurfaceClosure, shading::SurfaceShader, surface::{Surface, SurfaceIntersection, SurfaceIntersectionData}, }; use super::SurfaceLight; // TODO: use proper error bounds for sample generation to avoid self-shadowing // instead of these fudge factors. const SAMPLE_POINT_FUDGE: f32 = 0.001; // TODO: handle case where radius = 0.0. #[derive(Copy, Clone, Debug)] pub struct SphereLight<'a> { radii: &'a [f32], colors: &'a [Color], bounds_: &'a [BBox], } impl<'a> SphereLight<'a> { pub fn new<'b>(arena: &'b Arena, radii: &[f32], colors: &[Color]) -> SphereLight<'b> { let bbs: Vec<_> = radii .iter() .map(|r| BBox { min: Point::new(-*r, -*r, -*r), max: Point::new(*r, *r, *r), }) .collect(); SphereLight { radii: arena.copy_slice(&radii), colors: arena.copy_slice(&colors), bounds_: arena.copy_slice(&bbs), } } // TODO: this is only used from within `intersect_rays`, and could be done // more efficiently by inlining it there. fn sample_pdf( &self, space: &Transform, arr: Point, sample_dir: Vector, sample_u: f32, sample_v: f32, wavelength: f32, time: f32, ) -> f32 { // We're not using these, silence warnings let _ = (sample_dir, sample_u, sample_v, wavelength); let arr = arr * *space; let pos = Point::new(0.0, 0.0, 0.0); let radius: f64 = lerp_slice(self.radii, time) as f64; let d2: f64 = (pos - arr).length2() as f64; // Distance from center of sphere squared let d: f64 = d2.sqrt(); // Distance from center of sphere if d > radius { // Calculate the portion of the sphere visible from the point let sin_theta_max2: f64 = ((radius * radius) / d2).min(1.0); let cos_theta_max2: f64 = 1.0 - sin_theta_max2; let cos_theta_max: f64 = cos_theta_max2.sqrt(); uniform_sample_cone_pdf(cos_theta_max) as f32 } else { (1.0 / (4.0 * PI_64)) as f32 } } } impl<'a> SurfaceLight for SphereLight<'a> { fn sample_from_point( &self, space: &Transform, arr: Point, u: f32, v: f32, wavelength: f32, time: f32, ) -> (SpectralSample, (Point, Normal, f32), f32) { // TODO: track fp error due to transforms let arr = arr * *space; let pos = Point::new(0.0, 0.0, 0.0); // Precalculate local->world space transform matrix let inv_space = space.inverse(); // Calculate time interpolated values let radius: f64 = lerp_slice(self.radii, time) as f64; let col = lerp_slice(self.colors, time); let surface_area_inv: f64 = 1.0 / (4.0 * PI_64 * radius * radius); // Create a coordinate system from the vector between the // point and the center of the light let z = pos - arr; let d2: f64 = z.length2() as f64; // Distance from center of sphere squared let d = d2.sqrt(); // Distance from center of sphere let (z, x, y) = coordinate_system_from_vector(z); let (x, y, z) = (x.normalized(), y.normalized(), z.normalized()); // Pre-calculate sample point error magnitude. // TODO: do this properly. This is a total hack. let sample_point_err = { let v = Vector::new(radius as f32, radius as f32, radius as f32); let v2 = v * inv_space; v2.length() * SAMPLE_POINT_FUDGE }; // If we're outside the sphere, sample the surface based on // the angle it subtends from the point being lit. if d > radius { // Calculate the portion of the sphere visible from the point let sin_theta_max2: f64 = ((radius * radius) / d2).min(1.0); let cos_theta_max2: f64 = 1.0 - sin_theta_max2; let sin_theta_max: f64 = sin_theta_max2.sqrt(); let cos_theta_max: f64 = cos_theta_max2.sqrt(); // Sample the cone subtended by the sphere and calculate // useful data from that. let sample = uniform_sample_cone(u, v, cos_theta_max).normalized(); let cos_theta: f64 = sample.z() as f64; let cos_theta2: f64 = cos_theta * cos_theta; let sin_theta2: f64 = (1.0 - cos_theta2).max(0.0); let sin_theta: f64 = sin_theta2.sqrt(); // Convert to a point on the sphere. // The technique for this is from "Akalin" on ompf2.com: // http://ompf2.com/viewtopic.php?f=3&t=1914#p4414 let dd = 1.0 - (d2 * sin_theta * sin_theta / (radius * radius)); let cos_a = if dd <= 0.0 { sin_theta_max } else { ((d / radius) * sin_theta2) + (cos_theta * dd.sqrt()) }; let sin_a = ((1.0 - (cos_a * cos_a)).max(0.0)).sqrt(); let phi = v as f64 * 2.0 * PI_64; let sample = Vector::new( (phi.cos() * sin_a * radius) as f32, (phi.sin() * sin_a * radius) as f32, (d - (cos_a * radius)) as f32, ); // Calculate the final values and return everything. let (sample_point, normal) = { let sample_vec = (x * sample.x()) + (y * sample.y()) + (z * sample.z()); let normal = (arr + sample_vec).into_vector().normalized(); let point = normal * radius as f32; ( point.into_point() * inv_space, normal.into_normal() * inv_space, ) }; let pdf = uniform_sample_cone_pdf(cos_theta_max); let spectral_sample = col.to_spectral_sample(wavelength) * surface_area_inv as f32; return ( spectral_sample, (sample_point, normal, sample_point_err), pdf as f32, ); } else { // If we're inside the sphere, there's light from every direction. let (sample_point, normal) = { let sample_vec = uniform_sample_sphere(u, v); let normal = (arr + sample_vec).into_vector().normalized(); let point = normal * radius as f32; ( point.into_point() * inv_space, normal.into_normal() * inv_space, ) }; let pdf = 1.0 / (4.0 * PI_64); let spectral_sample = col.to_spectral_sample(wavelength) * surface_area_inv as f32; return ( spectral_sample, (sample_point, normal, sample_point_err), pdf as f32, ); } } fn is_delta(&self) -> bool { false } fn approximate_energy(&self) -> f32 { self.colors .iter() .fold(0.0, |a, &b| a + b.approximate_energy()) / self.colors.len() as f32 } } impl<'a> Surface for SphereLight<'a> { fn intersect_rays( &self, rays: &mut RayBatch, ray_stack: &mut RayStack, isects: &mut [SurfaceIntersection], shader: &dyn SurfaceShader, space: &[Transform], ) { let _ = shader; // Silence 'unused' warning ray_stack.pop_do_next_task(|ray_idx| { let time = rays.time(ray_idx); // Get the transform space let xform = lerp_slice(space, time); // Get the radius of the sphere at the ray's time let radius = lerp_slice(self.radii, time); // Radius of the sphere // Get the ray origin and direction in local space let orig = rays.orig_local(ray_idx).into_vector(); let dir = rays.dir(ray_idx) * xform; // Code adapted to Rust from https://github.com/Tecla/Rayito // Ray-sphere intersection can result in either zero, one or two points // of intersection. It turns into a quadratic equation, so we just find // the solution using the quadratic formula. Note that there is a // slightly more stable form of it when computing it on a computer, and // we use that method to keep everything accurate. // Calculate quadratic coeffs let a = dir.length2(); let b = 2.0 * dot(dir, orig); let c = orig.length2() - (radius * radius); let discriminant = (b * b) - (4.0 * a * c); if discriminant < 0.0 { // Discriminant less than zero? No solution => no intersection. return; } let discriminant = discriminant.sqrt(); // Compute a more stable form of our param t (t0 = q/a, t1 = c/q) // q = -0.5 * (b - sqrt(b * b - 4.0 * a * c)) if b < 0, or // q = -0.5 * (b + sqrt(b * b - 4.0 * a * c)) if b >= 0 let q = if b < 0.0 { -0.5 * (b - discriminant) } else { -0.5 * (b + discriminant) }; // Get our final parametric values let mut t0 = q / a; let mut t1 = if q != 0.0 { c / q } else { rays.max_t(ray_idx) }; // Swap them so they are ordered right if t0 > t1 { use std::mem::swap; swap(&mut t0, &mut t1); } // Check our intersection for validity against this ray's extents if t0 > rays.max_t(ray_idx) || t1 <= 0.0 { // Didn't hit because sphere is entirely outside of ray's extents return; } let t = if t0 > 0.0 { t0 } else if t1 <= rays.max_t(ray_idx) { t1 } else { // Didn't hit because ray is entirely within the sphere, and // therefore doesn't hit its surface. return; }; // We hit the sphere, so calculate intersection info. if rays.is_occlusion(ray_idx) { isects[ray_idx] = SurfaceIntersection::Occlude; rays.mark_done(ray_idx); } else { let inv_xform = xform.inverse(); // Position is calculated from the local-space ray and t, and then // re-projected onto the surface of the sphere. let t_pos = orig + (dir * t); let unit_pos = t_pos.normalized(); let pos = (unit_pos * radius * inv_xform).into_point(); // TODO: proper error bounds. let pos_err = 0.001; let normal = unit_pos.into_normal() * inv_xform; let intersection_data = SurfaceIntersectionData { incoming: rays.dir(ray_idx), t: t, pos: pos, pos_err: pos_err, nor: normal, nor_g: normal, local_space: xform, sample_pdf: self.sample_pdf( &xform, rays.orig(ray_idx), rays.dir(ray_idx), 0.0, 0.0, rays.wavelength(ray_idx), time, ), }; let closure = { let inv_surface_area = (1.0 / (4.0 * PI_64 * radius as f64 * radius as f64)) as f32; let color = lerp_slice(self.colors, time) * inv_surface_area; SurfaceClosure::Emit(color) }; // Fill in intersection isects[ray_idx] = SurfaceIntersection::Hit { intersection_data: intersection_data, closure: closure, }; // Set ray's max t rays.set_max_t(ray_idx, t); } }); } } impl<'a> Boundable for SphereLight<'a> { fn bounds(&self) -> &[BBox] { self.bounds_ } } ================================================ FILE: src/main.rs ================================================ #![allow(clippy::float_cmp)] #![allow(clippy::inline_always)] #![allow(clippy::many_single_char_names)] #![allow(clippy::needless_lifetimes)] #![allow(clippy::needless_return)] #![allow(clippy::or_fun_call)] #![allow(clippy::too_many_arguments)] #![allow(clippy::redundant_field_names)] #![allow(clippy::enum_variant_names)] #![allow(clippy::cast_lossless)] #![allow(clippy::needless_range_loop)] #![allow(clippy::excessive_precision)] #![allow(clippy::transmute_ptr_to_ptr)] extern crate lazy_static; mod accel; mod algorithm; mod bbox; mod bbox4; mod boundable; mod camera; mod color; mod fp_utils; mod hash; mod hilbert; mod image; mod lerp; mod light; mod math; mod mis; mod parse; mod ray; mod renderer; mod sampling; mod scene; mod shading; mod surface; mod timer; mod tracer; mod transform_stack; use std::{fs::File, io, io::Read, mem, path::Path, str::FromStr}; use clap::{App, Arg}; use nom::bytes::complete::take_until; use kioku::Arena; use crate::{ accel::BVH4Node, bbox::BBox, parse::{parse_scene, DataTree}, renderer::LightPath, surface::SurfaceIntersection, timer::Timer, }; const VERSION: &str = env!("CARGO_PKG_VERSION"); #[allow(clippy::cognitive_complexity)] fn main() { let mut t = Timer::new(); // Parse command line arguments. let args = App::new("Psychopath") .version(VERSION) .about("A slightly psychotic path tracer") .arg( Arg::with_name("input") .short("i") .long("input") .value_name("FILE") .help("Input .psy file") .takes_value(true) .required_unless_one(&["dev", "use_stdin"]), ) .arg( Arg::with_name("spp") .short("s") .long("spp") .value_name("N") .help("Number of samples per pixel") .takes_value(true) .validator(|s| { usize::from_str(&s) .and(Ok(())) .or(Err("must be an integer".to_string())) }), ) .arg( Arg::with_name("max_bucket_samples") .short("b") .long("spb") .value_name("N") .help("Target number of samples per bucket (determines bucket size)") .takes_value(true) .validator(|s| { usize::from_str(&s) .and(Ok(())) .or(Err("must be an integer".to_string())) }), ) .arg( Arg::with_name("crop") .long("crop") .value_name("X1 Y1 X2 Y2") .help( "Only render the image between pixel coordinates (X1, Y1) \ and (X2, Y2). Coordinates are zero-indexed and inclusive.", ) .takes_value(true) .number_of_values(4) .validator(|s| { usize::from_str(&s) .and(Ok(())) .or(Err("must be four integers".to_string())) }), ) .arg( Arg::with_name("threads") .short("t") .long("threads") .value_name("N") .help( "Number of threads to render with. Defaults to the number of logical \ cores on the system.", ) .takes_value(true) .validator(|s| { usize::from_str(&s) .and(Ok(())) .or(Err("must be an integer".to_string())) }), ) .arg( Arg::with_name("stats") .long("stats") .help("Print additional statistics about rendering"), ) .arg( Arg::with_name("dev") .long("dev") .help("Show useful dev/debug info."), ) .arg( Arg::with_name("serialized_output") .long("serialized_output") .help("Serialize and send render output to standard output.") .hidden(true), ) .arg( Arg::with_name("use_stdin") .long("use_stdin") .help("Take scene file in from stdin instead of a file path.") .hidden(true), ) .get_matches(); // Print some misc useful dev info. if args.is_present("dev") { println!( "SurfaceIntersection size: {} bytes", mem::size_of::() ); println!("LightPath size: {} bytes", mem::size_of::()); println!("BBox size: {} bytes", mem::size_of::()); // println!("BVHNode size: {} bytes", mem::size_of::()); println!("BVH4Node size: {} bytes", mem::size_of::()); return; } let crop = args.values_of("crop").map(|mut vals| { let coords = ( u32::from_str(vals.next().unwrap()).unwrap(), u32::from_str(vals.next().unwrap()).unwrap(), u32::from_str(vals.next().unwrap()).unwrap(), u32::from_str(vals.next().unwrap()).unwrap(), ); if coords.0 > coords.2 { panic!("Argument '--crop': X1 must be less than or equal to X2"); } if coords.1 > coords.3 { panic!("Argument '--crop': Y1 must be less than or equal to Y2"); } coords }); // Parse data tree of scene file if !args.is_present("serialized_output") { println!("Parsing scene file...",); } t.tick(); let psy_contents = if args.is_present("use_stdin") { // Read from stdin let mut input = Vec::new(); let tmp = std::io::stdin(); let mut stdin = tmp.lock(); let mut buf = vec![0u8; 4096]; loop { let count = stdin .read(&mut buf) .expect("Unexpected end of scene input."); let start = if input.len() < 11 { 0 } else { input.len() - 11 }; let end = input.len() + count; input.extend(&buf[..count]); let mut done = false; let mut trunc_len = 0; if let nom::IResult::Ok((remaining, _)) = take_until::<&str, &[u8], ()>("__PSY_EOF__")(&input[start..end]) { done = true; trunc_len = input.len() - remaining.len(); } if done { input.truncate(trunc_len); break; } } String::from_utf8(input).unwrap() } else { // Read from file let mut input = String::new(); let fp = args.value_of("input").unwrap(); let mut f = io::BufReader::new(File::open(fp).unwrap()); let _ = f.read_to_string(&mut input); input }; let dt = DataTree::from_str(&psy_contents).unwrap(); if !args.is_present("serialized_output") { println!("\tParsed scene file in {:.3}s", t.tick()); } // Iterate through scenes and render them if let DataTree::Internal { ref children, .. } = dt { for child in children { t.tick(); if child.type_name() == "Scene" { if !args.is_present("serialized_output") { println!("Building scene..."); } let arena = Arena::new().with_block_size((1 << 20) * 4); let mut r = parse_scene(&arena, child).unwrap_or_else(|e| { e.print(&psy_contents); panic!("Parse error."); }); if let Some(spp) = args.value_of("spp") { if !args.is_present("serialized_output") { println!("\tOverriding scene spp: {}", spp); } r.spp = usize::from_str(spp).unwrap(); } let max_samples_per_bucket = if let Some(max_samples_per_bucket) = args.value_of("max_bucket_samples") { u32::from_str(max_samples_per_bucket).unwrap() } else { 4096 }; let thread_count = if let Some(threads) = args.value_of("threads") { u32::from_str(threads).unwrap() } else { num_cpus::get() as u32 }; if !args.is_present("serialized_output") { println!("\tBuilt scene in {:.3}s", t.tick()); } if !args.is_present("serialized_output") { println!("Rendering scene with {} threads...", thread_count); } let (mut image, rstats) = r.render( max_samples_per_bucket, crop, thread_count, args.is_present("serialized_output"), ); // Print render stats if !args.is_present("serialized_output") { let rtime = t.tick(); let ntime = rtime as f64 / rstats.total_time; println!("\tRendered scene in {:.3}s", rtime); println!( "\t\tTrace: {:.3}s", ntime * rstats.trace_time ); println!("\t\t\tRays traced: {}", rstats.ray_count); println!( "\t\t\tRays/sec: {}", (rstats.ray_count as f64 / (ntime * rstats.trace_time) as f64) as u64 ); println!("\t\t\tRay/node tests: {}", rstats.accel_node_visits); println!( "\t\tInitial ray generation: {:.3}s", ntime * rstats.initial_ray_generation_time ); println!( "\t\tRay generation: {:.3}s", ntime * rstats.ray_generation_time ); println!( "\t\tSample writing: {:.3}s", ntime * rstats.sample_writing_time ); } // Write to disk if !args.is_present("serialized_output") { println!("Writing image to disk into '{}'...", r.output_file); if r.output_file.ends_with(".png") { image .write_png(Path::new(&r.output_file)) .expect("Failed to write png..."); } else if r.output_file.ends_with(".exr") { image.write_exr(Path::new(&r.output_file)); } else { panic!("Unknown output file extension."); } println!("\tWrote image in {:.3}s", t.tick()); } // Print memory stats if stats are wanted. if args.is_present("stats") { // let arena_stats = arena.stats(); // let mib_occupied = arena_stats.0 as f64 / 1_048_576.0; // let mib_allocated = arena_stats.1 as f64 / 1_048_576.0; // println!("MemArena stats:"); // if mib_occupied >= 1.0 { // println!("\tOccupied: {:.1} MiB", mib_occupied); // } else { // println!("\tOccupied: {:.4} MiB", mib_occupied); // } // if mib_allocated >= 1.0 { // println!("\tUsed: {:.1} MiB", mib_allocated); // } else { // println!("\tUsed: {:.4} MiB", mib_allocated); // } // println!("\tTotal blocks: {}", arena_stats.2); } } } } // End with blank line println!(); } ================================================ FILE: src/math.rs ================================================ #![allow(dead_code)] use std::f32; pub use math3d::{cross, dot, CrossProduct, DotProduct, Normal, Point, Transform, Vector}; /// Gets the log base 2 of the given integer pub fn log2_64(n: u64) -> u64 { // This works by finding the largest non-zero binary digit in the // number. Its bit position is then the log2 of the integer. if n == 0 { 0 } else { (63 - n.leading_zeros()) as u64 } } /// Creates a coordinate system from a single vector. /// /// The input vector, v, becomes the first vector of the /// returned tuple, with the other two vectors in the returned /// tuple defining the orthoganal axes. /// /// Algorithm taken from "Building an Orthonormal Basis, Revisited" by Duff et al. pub fn coordinate_system_from_vector(v: Vector) -> (Vector, Vector, Vector) { let sign = v.z().signum(); let a = -1.0 / (sign + v.z()); let b = v.x() * v.y() * a; let v2 = Vector::new(1.0 + sign * v.x() * v.x() * a, sign * b, -sign * v.x()); let v3 = Vector::new(b, sign + v.y() * v.y() * a, -v.y()); (v, v2, v3) } /// Simple mapping of a vector that exists in a z-up space to /// the space of another vector who's direction is considered /// z-up for the purpose. /// Obviously this doesn't care about the direction _around_ /// the z-up, although it will be sufficiently consistent for /// isotropic sampling purposes. /// /// from: The vector we're transforming. /// toz: The vector whose space we are transforming "from" into. /// /// Returns the transformed vector. pub fn zup_to_vec(from: Vector, toz: Vector) -> Vector { let (toz, tox, toy) = coordinate_system_from_vector(toz.normalized()); // Use simple linear algebra to convert the "from" // vector to a space composed of tox, toy, and toz // as the x, y, and z axes. (tox * from.x()) + (toy * from.y()) + (toz * from.z()) } /// A highly accurate approximation of the probit function. /// /// From Peter John Acklam, sourced from here: /// https://web.archive.org/web/20151030215612/http://home.online.no/~pjacklam/notes/invnorm/ /// /// Regarding the approximation error, he says, "The absolute value of the /// relative error is less than 1.15 × 10−9 in the entire region." /// /// Given that this implementation outputs 32-bit floating point values, /// and 32-bit floating point has significantly less precision than that, /// this approximation can essentially be considered exact. pub fn probit(n: f32, width: f32) -> f32 { let n = n as f64; // Coefficients of the rational approximations. const A1: f64 = -3.969683028665376e+01; const A2: f64 = 2.209460984245205e+02; const A3: f64 = -2.759285104469687e+02; const A4: f64 = 1.383577518672690e+02; const A5: f64 = -3.066479806614716e+01; const A6: f64 = 2.506628277459239e+00; const B1: f64 = -5.447609879822406e+01; const B2: f64 = 1.615858368580409e+02; const B3: f64 = -1.556989798598866e+02; const B4: f64 = 6.680131188771972e+01; const B5: f64 = -1.328068155288572e+01; const C1: f64 = -7.784894002430293e-03; const C2: f64 = -3.223964580411365e-01; const C3: f64 = -2.400758277161838e+00; const C4: f64 = -2.549732539343734e+00; const C5: f64 = 4.374664141464968e+00; const C6: f64 = 2.938163982698783e+00; const D1: f64 = 7.784695709041462e-03; const D2: f64 = 3.224671290700398e-01; const D3: f64 = 2.445134137142996e+00; const D4: f64 = 3.754408661907416e+00; // Transition points between the rational functions. const N_LOW: f64 = 0.02425; const N_HIGH: f64 = 1.0 - N_LOW; let x = match n { // Lower region. n if 0.0 < n && n < N_LOW => { let q = (-2.0 * n.ln()).sqrt(); (((((C1 * q + C2) * q + C3) * q + C4) * q + C5) * q + C6) / ((((D1 * q + D2) * q + D3) * q + D4) * q + 1.0) } // Central region. n if n <= N_HIGH => { let q = n - 0.5; let r = q * q; (((((A1 * r + A2) * r + A3) * r + A4) * r + A5) * r + A6) * q / (((((B1 * r + B2) * r + B3) * r + B4) * r + B5) * r + 1.0) } // Upper region. n if n < 1.0 => { let q = (-2.0 * (1.0 - n).ln()).sqrt(); -(((((C1 * q + C2) * q + C3) * q + C4) * q + C5) * q + C6) / ((((D1 * q + D2) * q + D3) * q + D4) * q + 1.0) } // Exactly 1 or 0. Should be extremely rare. n if n == 0.0 => -std::f64::INFINITY, n if n == 1.0 => std::f64::INFINITY, // Outside of valid input range. _ => std::f64::NAN, }; x as f32 * width } pub fn fast_ln(x: f32) -> f32 { fastapprox::fast::ln(x) } pub fn fast_pow2(p: f32) -> f32 { fastapprox::fast::pow2(p) } pub fn fast_log2(x: f32) -> f32 { fastapprox::fast::log2(x) } pub fn fast_exp(p: f32) -> f32 { fastapprox::fast::exp(p) } pub fn fast_pow(x: f32, p: f32) -> f32 { fastapprox::fast::pow(x, p) } pub fn faster_pow2(p: f32) -> f32 { fastapprox::faster::pow2(p) } pub fn faster_exp(p: f32) -> f32 { fastapprox::faster::exp(p) } //---------------------------------------------------------------- #[cfg(test)] mod tests { use super::*; #[test] fn log2_64_test() { assert_eq!(0, log2_64(0)); for i in 0..64 { assert_eq!(i, log2_64(1 << i)); } for i in 8..64 { assert_eq!(i, log2_64((1 << i) + 227)); } for i in 16..64 { assert_eq!(i, log2_64((1 << i) + 56369)); } for i in 32..64 { assert_eq!(i, log2_64((1 << i) + 2514124923)); } } } ================================================ FILE: src/mis.rs ================================================ #![allow(dead_code)] pub fn balance_heuristic(a: f32, b: f32) -> f32 { if a.is_infinite() { a } else { let mis_fac = a / (a + b); a / mis_fac } } pub fn power_heuristic(a: f32, b: f32) -> f32 { if a.is_infinite() { a } else { let a2 = a * a; let b2 = b * b; let mis_fac = a2 / (a2 + b2); a / mis_fac } } ================================================ FILE: src/parse/basics.rs ================================================ //! Some basic nom parsers #![allow(dead_code)] use std::str::{self, FromStr}; use nom::{ character::complete::{digit1, multispace0, one_of}, combinator::{map_res, opt, recognize}, number::complete::float, sequence::{delimited, tuple}, IResult, }; // ======================================================== pub fn ws_f32(input: &str) -> IResult<&str, f32, ()> { delimited(multispace0, float, multispace0)(input) } pub fn ws_u32(input: &str) -> IResult<&str, u32, ()> { map_res(delimited(multispace0, digit1, multispace0), u32::from_str)(input) } pub fn ws_usize(input: &str) -> IResult<&str, usize, ()> { map_res(delimited(multispace0, digit1, multispace0), usize::from_str)(input) } pub fn ws_i32(input: &str) -> IResult<&str, i32, ()> { map_res( delimited( multispace0, recognize(tuple((opt(one_of("-")), digit1))), multispace0, ), i32::from_str, )(input) } // ======================================================== #[cfg(test)] mod test { use super::*; use nom::{combinator::all_consuming, sequence::tuple}; #[test] fn ws_u32_1() { assert_eq!(ws_u32("42"), Ok((&""[..], 42))); assert_eq!(ws_u32(" 42"), Ok((&""[..], 42))); assert_eq!(ws_u32("42 "), Ok((&""[..], 42))); assert_eq!(ws_u32(" 42"), Ok((&""[..], 42))); assert_eq!(ws_u32(" 42 53"), Ok((&"53"[..], 42))); } #[test] fn ws_usize_1() { assert_eq!(ws_usize("42"), Ok((&""[..], 42))); assert_eq!(ws_usize(" 42"), Ok((&""[..], 42))); assert_eq!(ws_usize("42 "), Ok((&""[..], 42))); assert_eq!(ws_usize(" 42"), Ok((&""[..], 42))); assert_eq!(ws_usize(" 42 53"), Ok((&"53"[..], 42))); } #[test] fn ws_i32_1() { assert_eq!(ws_i32("42"), Ok((&""[..], 42))); assert_eq!(ws_i32(" 42"), Ok((&""[..], 42))); assert_eq!(ws_i32("42 "), Ok((&""[..], 42))); assert_eq!(ws_i32(" 42"), Ok((&""[..], 42))); assert_eq!(ws_i32(" 42 53"), Ok((&"53"[..], 42))); } #[test] fn ws_i32_2() { assert_eq!(ws_i32("-42"), Ok((&""[..], -42))); assert_eq!(ws_i32(" -42"), Ok((&""[..], -42))); assert_eq!(ws_i32("-42 "), Ok((&""[..], -42))); assert_eq!(ws_i32(" -42"), Ok((&""[..], -42))); assert_eq!(ws_i32(" -42 53"), Ok((&"53"[..], -42))); assert_eq!(ws_i32("--42").is_err(), true); } #[test] fn ws_f32_1() { assert_eq!(ws_f32("42"), Ok((&""[..], 42.0))); assert_eq!(ws_f32(" 42"), Ok((&""[..], 42.0))); assert_eq!(ws_f32("42 "), Ok((&""[..], 42.0))); assert_eq!(ws_f32(" 42"), Ok((&""[..], 42.0))); assert_eq!(ws_f32(" 42 53"), Ok((&"53"[..], 42.0))); } #[test] fn ws_f32_2() { assert_eq!(ws_f32("42.5"), Ok((&""[..], 42.5))); assert_eq!(ws_f32(" 42.5"), Ok((&""[..], 42.5))); assert_eq!(ws_f32("42.5 "), Ok((&""[..], 42.5))); assert_eq!(ws_f32(" 42.5"), Ok((&""[..], 42.5))); assert_eq!(ws_f32(" 42.5 53"), Ok((&"53"[..], 42.5))); } #[test] fn ws_f32_3() { assert_eq!(ws_f32("-42.5"), Ok((&""[..], -42.5))); assert_eq!(ws_f32(" -42.5"), Ok((&""[..], -42.5))); assert_eq!(ws_f32("-42.5 "), Ok((&""[..], -42.5))); assert_eq!(ws_f32(" -42.5"), Ok((&""[..], -42.5))); assert_eq!(ws_f32(" -42.5 53"), Ok((&"53"[..], -42.5))); } #[test] fn ws_f32_4() { assert_eq!(ws_f32("a1.0").is_err(), true); assert_eq!(all_consuming(ws_f32)("0abc").is_err(), true); assert_eq!(tuple((ws_f32, ws_f32))("0.abc 1.2").is_err(), true); } } ================================================ FILE: src/parse/data_tree.rs ================================================ #![allow(dead_code)] use std::{iter::Iterator, result::Result, slice}; #[derive(Debug, Eq, PartialEq)] pub enum DataTree<'a> { Internal { type_name: &'a str, ident: Option<&'a str>, children: Vec>, byte_offset: usize, }, Leaf { type_name: &'a str, contents: &'a str, byte_offset: usize, }, } impl<'a> DataTree<'a> { pub fn from_str(source_text: &'a str) -> Result, ParseError> { let mut items = Vec::new(); let mut remaining_text = (0, source_text); while let Some((item, text)) = parse_node(remaining_text)? { remaining_text = text; items.push(item); } remaining_text = skip_ws_and_comments(remaining_text); if remaining_text.1.is_empty() { return Ok(DataTree::Internal { type_name: "ROOT", ident: None, children: items, byte_offset: 0, }); } else { // If the whole text wasn't parsed, something went wrong. return Err(ParseError::Other((0, "Failed to parse the entire string."))); } } pub fn type_name(&'a self) -> &'a str { match *self { DataTree::Internal { type_name, .. } | DataTree::Leaf { type_name, .. } => type_name, } } pub fn byte_offset(&'a self) -> usize { match *self { DataTree::Internal { byte_offset, .. } | DataTree::Leaf { byte_offset, .. } => { byte_offset } } } pub fn is_internal(&self) -> bool { match *self { DataTree::Internal { .. } => true, DataTree::Leaf { .. } => false, } } pub fn is_leaf(&self) -> bool { match *self { DataTree::Internal { .. } => false, DataTree::Leaf { .. } => true, } } pub fn leaf_contents(&'a self) -> Option<&'a str> { match *self { DataTree::Internal { .. } => None, DataTree::Leaf { contents, .. } => Some(contents), } } pub fn iter_children(&'a self) -> slice::Iter<'a, DataTree<'a>> { if let DataTree::Internal { ref children, .. } = *self { children.iter() } else { [].iter() } } pub fn iter_children_with_type(&'a self, type_name: &'static str) -> DataTreeFilterIter<'a> { if let DataTree::Internal { ref children, .. } = *self { DataTreeFilterIter { type_name: type_name, iter: children.iter(), } } else { DataTreeFilterIter { type_name: type_name, iter: [].iter(), } } } pub fn iter_internal_children_with_type( &'a self, type_name: &'static str, ) -> DataTreeFilterInternalIter<'a> { if let DataTree::Internal { ref children, .. } = *self { DataTreeFilterInternalIter { type_name: type_name, iter: children.iter(), } } else { DataTreeFilterInternalIter { type_name: type_name, iter: [].iter(), } } } pub fn iter_leaf_children_with_type( &'a self, type_name: &'static str, ) -> DataTreeFilterLeafIter<'a> { if let DataTree::Internal { ref children, .. } = *self { DataTreeFilterLeafIter { type_name: type_name, iter: children.iter(), } } else { DataTreeFilterLeafIter { type_name: type_name, iter: [].iter(), } } } // For unit tests fn internal_data_or_panic(&'a self) -> (&'a str, Option<&'a str>, &'a Vec>) { if let DataTree::Internal { type_name, ident, ref children, .. } = *self { (type_name, ident, children) } else { panic!("Expected DataTree::Internal, found DataTree::Leaf") } } fn leaf_data_or_panic(&'a self) -> (&'a str, &'a str) { if let DataTree::Leaf { type_name, contents, .. } = *self { (type_name, contents) } else { panic!("Expected DataTree::Leaf, found DataTree::Internal") } } } /// An iterator over the children of a `DataTree` node that filters out the /// children not matching a specified type name. pub struct DataTreeFilterIter<'a> { type_name: &'a str, iter: slice::Iter<'a, DataTree<'a>>, } impl<'a> Iterator for DataTreeFilterIter<'a> { type Item = &'a DataTree<'a>; fn next(&mut self) -> Option<&'a DataTree<'a>> { loop { if let Some(dt) = self.iter.next() { if dt.type_name() == self.type_name { return Some(dt); } else { continue; } } else { return None; } } } } /// An iterator over the children of a `DataTree` node that filters out the /// children that aren't internal nodes and that don't match a specified /// type name. pub struct DataTreeFilterInternalIter<'a> { type_name: &'a str, iter: slice::Iter<'a, DataTree<'a>>, } impl<'a> Iterator for DataTreeFilterInternalIter<'a> { type Item = (&'a str, Option<&'a str>, &'a Vec>, usize); fn next(&mut self) -> Option<(&'a str, Option<&'a str>, &'a Vec>, usize)> { loop { match self.iter.next() { Some(&DataTree::Internal { type_name, ident, ref children, byte_offset, }) => { if type_name == self.type_name { return Some((type_name, ident, children, byte_offset)); } else { continue; } } Some(&DataTree::Leaf { .. }) => { continue; } None => { return None; } } } } } /// An iterator over the children of a `DataTree` node that filters out the /// children that aren't internal nodes and that don't match a specified /// type name. pub struct DataTreeFilterLeafIter<'a> { type_name: &'a str, iter: slice::Iter<'a, DataTree<'a>>, } impl<'a> Iterator for DataTreeFilterLeafIter<'a> { type Item = (&'a str, &'a str, usize); fn next(&mut self) -> Option<(&'a str, &'a str, usize)> { loop { match self.iter.next() { Some(&DataTree::Internal { .. }) => { continue; } Some(&DataTree::Leaf { type_name, contents, byte_offset, }) => { if type_name == self.type_name { return Some((type_name, contents, byte_offset)); } else { continue; } } None => { return None; } } } } } #[derive(Copy, Clone, Eq, PartialEq, Debug)] pub enum ParseError { MissingOpener(usize), MissingOpenInternal(usize), MissingCloseInternal(usize), MissingOpenLeaf(usize), MissingCloseLeaf(usize), MissingTypeName(usize), UnexpectedIdent(usize), UnknownToken(usize), Other((usize, &'static str)), } // ================================================================ #[derive(Debug, PartialEq, Eq)] enum Token<'a> { OpenInner, CloseInner, OpenLeaf, CloseLeaf, TypeName(&'a str), Ident(&'a str), End, Unknown, } type ParseResult<'a> = Result, (usize, &'a str))>, ParseError>; fn parse_node<'a>(source_text: (usize, &'a str)) -> ParseResult<'a> { let (token, text1) = next_token(source_text); if let Token::TypeName(type_name) = token { match next_token(text1) { // Internal with name (Token::Ident(n), text2) => { if let (Token::OpenInner, text3) = next_token(text2) { let mut children = Vec::new(); let mut text_remaining = text3; while let Some((node, text4)) = parse_node(text_remaining)? { text_remaining = text4; children.push(node); } if let (Token::CloseInner, text4) = next_token(text_remaining) { return Ok(Some(( DataTree::Internal { type_name: type_name, ident: Some(n), children: children, byte_offset: text1.0, }, text4, ))); } else { return Err(ParseError::MissingCloseInternal(text_remaining.0)); } } else { return Err(ParseError::MissingOpenInternal(text2.0)); } } // Internal without name (Token::OpenInner, text2) => { let mut children = Vec::new(); let mut text_remaining = text2; while let Some((node, text3)) = parse_node(text_remaining)? { text_remaining = text3; children.push(node); } if let (Token::CloseInner, text3) = next_token(text_remaining) { return Ok(Some(( DataTree::Internal { type_name: type_name, ident: None, children: children, byte_offset: text1.0, }, text3, ))); } else { return Err(ParseError::MissingCloseInternal(text_remaining.0)); } } // Leaf (Token::OpenLeaf, text2) => { let (contents, text3) = parse_leaf_content(text2); if let (Token::CloseLeaf, text4) = next_token(text3) { return Ok(Some(( DataTree::Leaf { type_name: type_name, contents: contents, byte_offset: text1.0, }, text4, ))); } else { return Err(ParseError::MissingCloseLeaf(text3.0)); } } // Other _ => { return Err(ParseError::MissingOpener(text1.0)); } } } else { return Ok(None); } } fn parse_leaf_content(source_text: (usize, &str)) -> (&str, (usize, &str)) { let mut si = 1; let mut escaped = false; let mut reached_end = true; for (i, c) in source_text.1.char_indices() { si = i; if escaped { escaped = false; } else if c == '\\' { escaped = true; } else if c == ']' { reached_end = false; break; } } if reached_end { si = source_text.1.len(); } return ( &source_text.1[0..si], (source_text.0 + si, &source_text.1[si..]), ); } fn next_token<'a>(source_text: (usize, &'a str)) -> (Token<'a>, (usize, &'a str)) { let text1 = skip_ws_and_comments(source_text); if let Some(c) = text1.1.chars().nth(0) { let text2 = (text1.0 + c.len_utf8(), &text1.1[c.len_utf8()..]); match c { '{' => { return (Token::OpenInner, text2); } '}' => { return (Token::CloseInner, text2); } '[' => { return (Token::OpenLeaf, text2); } ']' => { return (Token::CloseLeaf, text2); } '$' => { // Parse name let mut si = 1; let mut escaped = false; let mut reached_end = true; for (i, c) in text1.1.char_indices().skip(1) { si = i; if escaped { escaped = false; } else if c == '\\' { escaped = true; } else if !is_ident_char(c) { reached_end = false; break; } } if reached_end { si = text1.1.len(); } return ( Token::Ident(&text1.1[0..si]), (text1.0 + si, &text1.1[si..]), ); } _ => { if is_ident_char(c) { // Parse type let mut si = 0; let mut reached_end = true; for (i, c) in text1.1.char_indices() { si = i; if !is_ident_char(c) { reached_end = false; break; } } if reached_end { si = text1.1.len(); } return ( Token::TypeName(&text1.1[0..si]), (text1.0 + si, &text1.1[si..]), ); } } } } else { return (Token::End, text1); } return (Token::Unknown, text1); } fn is_ws(c: char) -> bool { match c { '\n' | '\r' | '\t' | ' ' => true, _ => false, } } fn is_nl(c: char) -> bool { match c { '\n' | '\r' => true, _ => false, } } fn is_reserved_char(c: char) -> bool { match c { '{' | '}' | '[' | ']' | '$' | '#' | '\\' => true, _ => false, } } fn is_ident_char(c: char) -> bool { // Anything that isn't whitespace or a reserved character !is_ws(c) && !is_reserved_char(c) } fn skip_ws(text: &str) -> &str { let mut si = 0; let mut reached_end = true; for (i, c) in text.char_indices() { si = i; if !is_ws(c) { reached_end = false; break; } } if reached_end { si = text.len(); } return &text[si..]; } fn skip_comment(text: &str) -> &str { let mut si = 0; if Some('#') == text.chars().nth(0) { let mut reached_end = true; for (i, c) in text.char_indices() { si = i; if is_nl(c) { reached_end = false; break; } } if reached_end { si = text.len(); } } return &text[si..]; } fn skip_ws_and_comments(text: (usize, &str)) -> (usize, &str) { let mut remaining_text = text.1; loop { let tmp = skip_comment(skip_ws(remaining_text)); if tmp.len() == remaining_text.len() { break; } else { remaining_text = tmp; } } let offset = text.0 + text.1.len() - remaining_text.len(); return (offset, remaining_text); } // ================================================================ #[cfg(test)] mod tests { use super::*; use super::{next_token, Token}; #[test] fn tokenize_1() { let input = (0, "Thing"); assert_eq!(next_token(input), (Token::TypeName("Thing"), (5, ""))); } #[test] fn tokenize_2() { let input = (0, " \n# gdfgdf gfdg dggdf\\sg dfgsd \n Thing"); assert_eq!(next_token(input), (Token::TypeName("Thing"), (41, ""))); } #[test] fn tokenize_3() { let input1 = (0, " Thing { }"); let (token1, input2) = next_token(input1); let (token2, input3) = next_token(input2); let (token3, input4) = next_token(input3); assert_eq!((token1, input2.1), (Token::TypeName("Thing"), " { }")); assert_eq!((token2, input3.1), (Token::OpenInner, " }")); assert_eq!((token3, input4.1), (Token::CloseInner, "")); } #[test] fn tokenize_4() { let input = (0, " $hi_there "); assert_eq!(next_token(input), (Token::Ident("$hi_there"), (10, " "))); } #[test] fn tokenize_5() { let input = (0, " $hi\\ t\\#he\\[re "); assert_eq!( next_token(input), (Token::Ident("$hi\\ t\\#he\\[re"), (15, " "),) ); } #[test] fn tokenize_6() { let input1 = (0, " $hi the[re"); let (token1, input2) = next_token(input1); let (token2, input3) = next_token(input2); let (token3, input4) = next_token(input3); let (token4, input5) = next_token(input4); let (token5, input6) = next_token(input5); assert_eq!((token1, input2), (Token::Ident("$hi"), (4, " the[re"))); assert_eq!((token2, input3), (Token::TypeName("the"), (8, "[re"))); assert_eq!((token3, input4), (Token::OpenLeaf, (9, "re"))); assert_eq!((token4, input5), (Token::TypeName("re"), (11, ""))); assert_eq!((token5, input6), (Token::End, (11, ""))); } #[test] fn tokenize_7() { let input1 = (0, "Thing $yar { # A comment\n\tThing2 []\n}"); let (token1, input2) = next_token(input1); let (token2, input3) = next_token(input2); let (token3, input4) = next_token(input3); let (token4, input5) = next_token(input4); let (token5, input6) = next_token(input5); let (token6, input7) = next_token(input6); let (token7, input8) = next_token(input7); let (token8, input9) = next_token(input8); assert_eq!( (token1, input2), ( Token::TypeName("Thing"), (5, " $yar { # A comment\n\tThing2 []\n}",) ) ); assert_eq!( (token2, input3), ( Token::Ident("$yar"), (10, " { # A comment\n\tThing2 []\n}",) ) ); assert_eq!( (token3, input4), (Token::OpenInner, (12, " # A comment\n\tThing2 []\n}",)) ); assert_eq!( (token4, input5), (Token::TypeName("Thing2"), (32, " []\n}")) ); assert_eq!((token5, input6), (Token::OpenLeaf, (34, "]\n}"))); assert_eq!((token6, input7), (Token::CloseLeaf, (35, "\n}"))); assert_eq!((token7, input8), (Token::CloseInner, (37, ""))); assert_eq!((token8, input9), (Token::End, (37, ""))); } #[test] fn parse_1() { let input = r#" Thing {} "#; let dt = DataTree::from_str(input).unwrap(); // Root let (t, i, c) = dt.internal_data_or_panic(); assert_eq!(t, "ROOT"); assert_eq!(i, None); assert_eq!(c.len(), 1); // First (and only) child let (t, i, c) = c[0].internal_data_or_panic(); assert_eq!(t, "Thing"); assert_eq!(i, None); assert_eq!(c.len(), 0); } #[test] fn iter_1() { let dt = DataTree::from_str( r#" A {} B {} A [] A {} B {} "#, ) .unwrap(); let i = dt.iter_children_with_type("A"); assert_eq!(i.count(), 3); } #[test] fn iter_2() { let dt = DataTree::from_str( r#" A {} B {} A [] A {} B {} "#, ) .unwrap(); let i = dt.iter_internal_children_with_type("A"); assert_eq!(i.count(), 2); } #[test] fn iter_3() { let dt = DataTree::from_str( r#" A [] B {} A {} A [] B {} "#, ) .unwrap(); let i = dt.iter_leaf_children_with_type("A"); assert_eq!(i.count(), 2); } } ================================================ FILE: src/parse/mod.rs ================================================ pub mod basics; mod data_tree; mod psy; mod psy_assembly; mod psy_light; mod psy_mesh_surface; mod psy_surface_shader; pub use self::{data_tree::DataTree, psy::parse_scene}; ================================================ FILE: src/parse/psy.rs ================================================ #![allow(dead_code)] use std::{f32, result::Result}; use nom::{combinator::all_consuming, sequence::tuple, IResult}; use kioku::Arena; use crate::{ camera::Camera, color::{rec709_e_to_xyz, Color}, light::WorldLightSource, math::Transform, renderer::Renderer, scene::Scene, scene::World, }; use super::{ basics::{ws_f32, ws_u32}, psy_assembly::parse_assembly, psy_light::parse_distant_disk_light, DataTree, }; #[derive(Debug)] pub enum PsyParseError { // The first usize for all errors is their byte offset // into the psy content where they occured. UnknownError(usize), UnknownVariant(usize, &'static str), // Error message ExpectedInternalNode(usize, &'static str), // Error message ExpectedLeafNode(usize, &'static str), // Error message MissingNode(usize, &'static str), // Error message IncorrectLeafData(usize, &'static str), // Error message WrongNodeCount(usize, &'static str, usize), // Error message, sections found InstancedMissingData(usize, &'static str, String), // Error message, data name } impl PsyParseError { pub fn print(&self, psy_content: &str) { match *self { PsyParseError::UnknownError(offset) => { let line = line_count_to_byte_offset(psy_content, offset); println!( "Line {}: Unknown parse error. If you get this message, please report \ it to the developers so they can improve the error messages.", line ); } PsyParseError::UnknownVariant(offset, error) => { let line = line_count_to_byte_offset(psy_content, offset); println!("Line {}: {}", line, error); } PsyParseError::ExpectedInternalNode(offset, error) => { let line = line_count_to_byte_offset(psy_content, offset); println!("Line {}: {}", line, error); } PsyParseError::ExpectedLeafNode(offset, error) => { let line = line_count_to_byte_offset(psy_content, offset); println!("Line {}: {}", line, error); } PsyParseError::MissingNode(offset, error) => { let line = line_count_to_byte_offset(psy_content, offset); println!("Line {}: {}", line, error); } PsyParseError::IncorrectLeafData(offset, error) => { let line = line_count_to_byte_offset(psy_content, offset); println!("Line {}: {}", line, error); } PsyParseError::WrongNodeCount(offset, error, count) => { let line = line_count_to_byte_offset(psy_content, offset); println!("Line {}: {} Found: {}", line, error, count); } PsyParseError::InstancedMissingData(offset, error, ref data_name) => { let line = line_count_to_byte_offset(psy_content, offset); println!("Line {}: {} Data name: '{}'", line, error, data_name); } } } } fn line_count_to_byte_offset(text: &str, offset: usize) -> usize { text[..offset].matches('\n').count() + 1 } /// Takes in a `DataTree` representing a Scene node and returns pub fn parse_scene<'a>( arena: &'a Arena, tree: &'a DataTree, ) -> Result, PsyParseError> { // Verify we have the right number of each section if tree.iter_children_with_type("Output").count() != 1 { let count = tree.iter_children_with_type("Output").count(); return Err(PsyParseError::WrongNodeCount( tree.byte_offset(), "Scene should have precisely one Output \ section.", count, )); } if tree.iter_children_with_type("RenderSettings").count() != 1 { let count = tree.iter_children_with_type("RenderSettings").count(); return Err(PsyParseError::WrongNodeCount( tree.byte_offset(), "Scene should have precisely one \ RenderSettings section.", count, )); } if tree.iter_children_with_type("Camera").count() != 1 { let count = tree.iter_children_with_type("Camera").count(); return Err(PsyParseError::WrongNodeCount( tree.byte_offset(), "Scene should have precisely one Camera \ section.", count, )); } if tree.iter_children_with_type("World").count() != 1 { let count = tree.iter_children_with_type("World").count(); return Err(PsyParseError::WrongNodeCount( tree.byte_offset(), "Scene should have precisely one World section.", count, )); } if tree.iter_children_with_type("Assembly").count() != 1 { let count = tree.iter_children_with_type("Assembly").count(); return Err(PsyParseError::WrongNodeCount( tree.byte_offset(), "Scene should have precisely one Root Assembly \ section.", count, )); } // Parse output info let output_info = parse_output_info(tree.iter_children_with_type("Output").nth(0).unwrap())?; // Parse render settings let render_settings = parse_render_settings( tree.iter_children_with_type("RenderSettings") .nth(0) .unwrap(), )?; // Parse camera let camera = parse_camera( arena, tree.iter_children_with_type("Camera").nth(0).unwrap(), )?; // Parse world let world = parse_world(arena, tree.iter_children_with_type("World").nth(0).unwrap())?; // Parse root scene assembly let assembly = parse_assembly( arena, tree.iter_children_with_type("Assembly").nth(0).unwrap(), )?; // Put scene together let scene_name = if let DataTree::Internal { ident, .. } = *tree { if let Some(name) = ident { Some(name.to_string()) } else { None } } else { None }; let scene = Scene { name: scene_name, camera: camera, world: world, root: assembly, }; // Put renderer together let renderer = Renderer { output_file: output_info.clone(), resolution: ( (render_settings.0).0 as usize, (render_settings.0).1 as usize, ), spp: render_settings.1 as usize, seed: render_settings.2, scene: scene, }; return Ok(renderer); } fn parse_output_info(tree: &DataTree) -> Result { if let DataTree::Internal { ref children, .. } = *tree { let mut found_path = false; let mut path = String::new(); for child in children { match *child { DataTree::Leaf { type_name, contents, byte_offset, } if type_name == "Path" => { // Trim and validate let tc = contents.trim(); if tc.chars().count() < 2 { return Err(PsyParseError::IncorrectLeafData( byte_offset, "File path format is \ incorrect.", )); } if tc.chars().nth(0).unwrap() != '"' || !tc.ends_with('"') { return Err(PsyParseError::IncorrectLeafData( byte_offset, "File paths must be \ surrounded by quotes.", )); } let len = tc.len(); let tc = &tc[1..len - 1]; // Parse // TODO: proper string escaping found_path = true; path = tc.to_string(); } _ => {} } } if found_path { return Ok(path); } else { return Err(PsyParseError::MissingNode( tree.byte_offset(), "Output section must contain a Path.", )); } } else { return Err(PsyParseError::ExpectedInternalNode( tree.byte_offset(), "Output section should be an internal \ node, containing at least a Path.", )); }; } fn parse_render_settings(tree: &DataTree) -> Result<((u32, u32), u32, u32), PsyParseError> { if let DataTree::Internal { ref children, .. } = *tree { let mut found_res = false; let mut found_spp = false; let mut res = (0, 0); let mut spp = 0; let mut seed = 0; for child in children { match *child { // Resolution DataTree::Leaf { type_name, contents, byte_offset, } if type_name == "Resolution" => { if let IResult::Ok((_, (w, h))) = all_consuming(tuple((ws_u32, ws_u32)))(contents) { found_res = true; res = (w, h); } else { // Found Resolution, but its contents is not in the right format return Err(PsyParseError::IncorrectLeafData( byte_offset, "Resolution should be specified with two \ integers in the form '[width height]'.", )); } } // SamplesPerPixel DataTree::Leaf { type_name, contents, byte_offset, } if type_name == "SamplesPerPixel" => { if let IResult::Ok((_, n)) = all_consuming(ws_u32)(contents) { found_spp = true; spp = n; } else { // Found SamplesPerPixel, but its contents is not in the right format return Err(PsyParseError::IncorrectLeafData( byte_offset, "SamplesPerPixel should be \ an integer specified in \ the form '[samples]'.", )); } } // Seed DataTree::Leaf { type_name, contents, byte_offset, } if type_name == "Seed" => { if let IResult::Ok((_, n)) = all_consuming(ws_u32)(contents) { seed = n; } else { // Found Seed, but its contents is not in the right format return Err(PsyParseError::IncorrectLeafData( byte_offset, "Seed should be an integer \ specified in the form \ '[samples]'.", )); } } _ => {} } } if found_res && found_spp { return Ok((res, spp, seed)); } else { return Err(PsyParseError::MissingNode( tree.byte_offset(), "RenderSettings must have both Resolution and \ SamplesPerPixel specified.", )); } } else { return Err(PsyParseError::ExpectedInternalNode( tree.byte_offset(), "RenderSettings section should be an \ internal node, containing at least \ Resolution and SamplesPerPixel.", )); }; } fn parse_camera<'a>(arena: &'a Arena, tree: &'a DataTree) -> Result, PsyParseError> { if let DataTree::Internal { ref children, .. } = *tree { let mut mats = Vec::new(); let mut fovs = Vec::new(); let mut focus_distances = Vec::new(); let mut aperture_radii = Vec::new(); // Parse for child in children.iter() { match *child { // Fov DataTree::Leaf { type_name, contents, byte_offset, } if type_name == "Fov" => { if let IResult::Ok((_, fov)) = all_consuming(ws_f32)(contents) { fovs.push(fov * (f32::consts::PI / 180.0)); } else { // Found Fov, but its contents is not in the right format return Err(PsyParseError::IncorrectLeafData( byte_offset, "Fov should be a decimal \ number specified in the \ form '[fov]'.", )); } } // FocalDistance DataTree::Leaf { type_name, contents, byte_offset, } if type_name == "FocalDistance" => { if let IResult::Ok((_, fd)) = all_consuming(ws_f32)(contents) { focus_distances.push(fd); } else { // Found FocalDistance, but its contents is not in the right format return Err(PsyParseError::IncorrectLeafData( byte_offset, "FocalDistance should be a \ decimal number specified \ in the form '[fov]'.", )); } } // ApertureRadius DataTree::Leaf { type_name, contents, byte_offset, } if type_name == "ApertureRadius" => { if let IResult::Ok((_, ar)) = all_consuming(ws_f32)(contents) { aperture_radii.push(ar); } else { // Found ApertureRadius, but its contents is not in the right format return Err(PsyParseError::IncorrectLeafData( byte_offset, "ApertureRadius should be a \ decimal number specified \ in the form '[fov]'.", )); } } // Transform DataTree::Leaf { type_name, contents, byte_offset, } if type_name == "Transform" => { if let Ok(mat) = parse_matrix(contents) { mats.push(mat); } else { // Found Transform, but its contents is not in the right format return Err(make_transform_format_error(byte_offset)); } } _ => {} } } return Ok(Camera::new( arena, &mats, &fovs, &aperture_radii, &focus_distances, )); } else { return Err(PsyParseError::ExpectedInternalNode( tree.byte_offset(), "Camera section should be an internal \ node, containing at least Fov and \ Transform.", )); } } fn parse_world<'a>(arena: &'a Arena, tree: &'a DataTree) -> Result, PsyParseError> { if tree.is_internal() { let background_color; let mut lights: Vec<&dyn WorldLightSource> = Vec::new(); // Parse background shader let bgs = { if tree.iter_children_with_type("BackgroundShader").count() != 1 { return Err(PsyParseError::WrongNodeCount( tree.byte_offset(), "World should have precisely one BackgroundShader section.", tree.iter_children_with_type("BackgroundShader").count(), )); } tree.iter_children_with_type("BackgroundShader") .nth(0) .unwrap() }; let bgs_type = { if bgs.iter_children_with_type("Type").count() != 1 { return Err(PsyParseError::WrongNodeCount( bgs.byte_offset(), "BackgroundShader should have \ precisely one Type specified.", bgs.iter_children_with_type("Type").count(), )); } if let DataTree::Leaf { contents, .. } = *bgs.iter_children_with_type("Type").nth(0).unwrap() { contents.trim() } else { return Err(PsyParseError::ExpectedLeafNode( bgs.byte_offset(), "BackgroundShader's Type should be a \ leaf node.", )); } }; match bgs_type { "Color" => { if let Some(&DataTree::Leaf { contents, byte_offset, .. }) = bgs.iter_children_with_type("Color").nth(0) { if let Ok(color) = parse_color(contents) { background_color = color; } else { return Err(PsyParseError::IncorrectLeafData( byte_offset, "Color should be specified \ with three decimal numbers \ in the form '[R G B]'.", )); } } else { return Err(PsyParseError::MissingNode( bgs.byte_offset(), "BackgroundShader's Type is Color, \ but no Color is specified.", )); } } _ => { return Err(PsyParseError::UnknownVariant( bgs.byte_offset(), "The specified BackgroundShader Type \ isn't a recognized type.", )) } } // Parse light sources for child in tree.iter_children() { match *child { DataTree::Internal { type_name, .. } if type_name == "DistantDiskLight" => { lights.push(arena.alloc(parse_distant_disk_light(arena, child)?)); } _ => {} } } // Build and return the world return Ok(World { background_color: background_color, lights: arena.copy_slice(&lights), }); } else { return Err(PsyParseError::ExpectedInternalNode( tree.byte_offset(), "World section should be an internal \ node, containing at least a \ BackgroundShader.", )); } } pub fn parse_matrix(contents: &str) -> Result { if let IResult::Ok((leftover, ns)) = all_consuming(tuple(( ws_f32, ws_f32, ws_f32, ws_f32, ws_f32, ws_f32, ws_f32, ws_f32, ws_f32, ws_f32, ws_f32, ws_f32, ws_f32, ws_f32, ws_f32, ws_f32, )))(contents) { if leftover.is_empty() { return Ok(Transform::new_from_values( // We throw away the last row, since it's not necessarily affine. // TODO: is there a more correct way to handle this? ns.0, ns.4, ns.8, ns.12, ns.1, ns.5, ns.9, ns.13, ns.2, ns.6, ns.10, ns.14, )); } } return Err(PsyParseError::UnknownError(0)); } pub fn make_transform_format_error(byte_offset: usize) -> PsyParseError { PsyParseError::IncorrectLeafData( byte_offset, "Transform should be sixteen integers specified in \ the form '[# # # # # # # # # # # # # # # #]'.", ) } pub fn parse_color(contents: &str) -> Result { let items: Vec<_> = contents.split(',').map(|s| s.trim()).collect(); if items.len() != 2 { return Err(PsyParseError::UnknownError(0)); } match items[0] { "rec709" => { if let IResult::Ok((_, color)) = tuple((ws_f32, ws_f32, ws_f32))(items[1]) { return Ok(Color::new_xyz(rec709_e_to_xyz(color))); } else { return Err(PsyParseError::UnknownError(0)); } } "blackbody" => { if let IResult::Ok((_, (temperature, factor))) = tuple((ws_f32, ws_f32))(items[1]) { return Ok(Color::new_blackbody(temperature, factor)); } else { return Err(PsyParseError::UnknownError(0)); } } "color_temperature" => { if let IResult::Ok((_, (temperature, factor))) = tuple((ws_f32, ws_f32))(items[1]) { return Ok(Color::new_temperature(temperature, factor)); } else { return Err(PsyParseError::UnknownError(0)); } } _ => return Err(PsyParseError::UnknownError(0)), } } ================================================ FILE: src/parse/psy_assembly.rs ================================================ #![allow(dead_code)] use std::result::Result; use kioku::Arena; use crate::scene::{Assembly, AssemblyBuilder, Object}; use super::{ psy::{parse_matrix, PsyParseError}, psy_light::{parse_rectangle_light, parse_sphere_light}, psy_mesh_surface::parse_mesh_surface, psy_surface_shader::parse_surface_shader, DataTree, }; pub fn parse_assembly<'a>( arena: &'a Arena, tree: &'a DataTree, ) -> Result, PsyParseError> { let mut builder = AssemblyBuilder::new(arena); if tree.is_internal() { for child in tree.iter_children() { match child.type_name() { // Sub-Assembly "Assembly" => { if let DataTree::Internal { ident: Some(ident), .. } = *child { builder.add_assembly(ident, parse_assembly(arena, child)?); } else { return Err(PsyParseError::UnknownError(child.byte_offset())); } } // Instance "Instance" => { // Pre-conditions if !child.is_internal() { return Err(PsyParseError::UnknownError(child.byte_offset())); } // Get data name let name = { if child.iter_leaf_children_with_type("Data").count() != 1 { return Err(PsyParseError::UnknownError(child.byte_offset())); } child.iter_leaf_children_with_type("Data").nth(0).unwrap().1 }; // Get surface shader binding, if any. let surface_shader_name = if child .iter_leaf_children_with_type("SurfaceShaderBind") .count() > 0 { Some( child .iter_leaf_children_with_type("SurfaceShaderBind") .nth(0) .unwrap() .1, ) } else { None }; // Get xforms let mut xforms = Vec::new(); for (_, contents, _) in child.iter_leaf_children_with_type("Transform") { xforms.push(parse_matrix(contents)?); } // Add instance if builder.name_exists(name) { builder.add_instance(name, surface_shader_name, Some(&xforms)); } else { return Err(PsyParseError::InstancedMissingData( child.iter_leaf_children_with_type("Data").nth(0).unwrap().2, "Attempted to add \ instance for data with \ a name that doesn't \ exist.", name.to_string(), )); } } // SurfaceShader "SurfaceShader" => { if let DataTree::Internal { ident: Some(ident), .. } = *child { builder.add_surface_shader(ident, parse_surface_shader(arena, child)?); } else { // TODO: error condition of some kind, because no ident panic!( "SurfaceShader encountered that was a leaf, but SurfaceShaders cannot \ be a leaf: {}", child.byte_offset() ); } } // MeshSurface "MeshSurface" => { if let DataTree::Internal { ident: Some(ident), .. } = *child { builder.add_object( ident, Object::Surface(arena.alloc(parse_mesh_surface(arena, child)?)), ); } else { // TODO: error condition of some kind, because no ident panic!( "MeshSurface encountered that was a leaf, but MeshSurfaces cannot \ be a leaf: {}", child.byte_offset() ); } } // Sphere Light "SphereLight" => { if let DataTree::Internal { ident: Some(ident), .. } = *child { builder.add_object( ident, Object::SurfaceLight(arena.alloc(parse_sphere_light(arena, child)?)), ); } else { // No ident return Err(PsyParseError::UnknownError(child.byte_offset())); } } // Rectangle Light "RectangleLight" => { if let DataTree::Internal { ident: Some(ident), .. } = *child { builder.add_object( ident, Object::SurfaceLight(arena.alloc(parse_rectangle_light(arena, child)?)), ); } else { // No ident return Err(PsyParseError::UnknownError(child.byte_offset())); } } _ => { // TODO: some kind of error, because not a known type name } // // Bilinear Patch // "BilinearPatch" => { // assembly->add_object(child.name, parse_bilinear_patch(child)); // } // // // Bicubic Patch // else if (child.type == "BicubicPatch") { // assembly->add_object(child.name, parse_bicubic_patch(child)); // } // // // Subdivision surface // else if (child.type == "SubdivisionSurface") { // assembly->add_object(child.name, parse_subdivision_surface(child)); // } // // // Sphere // else if (child.type == "Sphere") { // assembly->add_object(child.name, parse_sphere(child)); // } } } } else { return Err(PsyParseError::UnknownError(tree.byte_offset())); } return Ok(builder.build()); } ================================================ FILE: src/parse/psy_light.rs ================================================ #![allow(dead_code)] use std::result::Result; use nom::{combinator::all_consuming, sequence::tuple, IResult}; use kioku::Arena; use crate::{ light::{DistantDiskLight, RectangleLight, SphereLight}, math::Vector, }; use super::{ basics::ws_f32, psy::{parse_color, PsyParseError}, DataTree, }; pub fn parse_distant_disk_light<'a>( arena: &'a Arena, tree: &'a DataTree, ) -> Result, PsyParseError> { if let DataTree::Internal { ref children, .. } = *tree { let mut radii = Vec::new(); let mut directions = Vec::new(); let mut colors = Vec::new(); // Parse for child in children.iter() { match *child { // Radius DataTree::Leaf { type_name, contents, byte_offset, } if type_name == "Radius" => { if let IResult::Ok((_, radius)) = all_consuming(ws_f32)(contents) { radii.push(radius); } else { // Found radius, but its contents is not in the right format return Err(PsyParseError::UnknownError(byte_offset)); } } // Direction DataTree::Leaf { type_name, contents, byte_offset, } if type_name == "Direction" => { if let IResult::Ok((_, direction)) = all_consuming(tuple((ws_f32, ws_f32, ws_f32)))(contents) { directions.push(Vector::new(direction.0, direction.1, direction.2)); } else { // Found direction, but its contents is not in the right format return Err(PsyParseError::UnknownError(byte_offset)); } } // Color DataTree::Leaf { type_name, contents, byte_offset, } if type_name == "Color" => { if let Ok(color) = parse_color(contents) { colors.push(color); } else { // Found color, but its contents is not in the right format return Err(PsyParseError::UnknownError(byte_offset)); } } _ => {} } } return Ok(DistantDiskLight::new(arena, &radii, &directions, &colors)); } else { return Err(PsyParseError::UnknownError(tree.byte_offset())); } } pub fn parse_sphere_light<'a>( arena: &'a Arena, tree: &'a DataTree, ) -> Result, PsyParseError> { if let DataTree::Internal { ref children, .. } = *tree { let mut radii = Vec::new(); let mut colors = Vec::new(); // Parse for child in children.iter() { match *child { // Radius DataTree::Leaf { type_name, contents, byte_offset, } if type_name == "Radius" => { if let IResult::Ok((_, radius)) = all_consuming(ws_f32)(contents) { radii.push(radius); } else { // Found radius, but its contents is not in the right format return Err(PsyParseError::UnknownError(byte_offset)); } } // Color DataTree::Leaf { type_name, contents, byte_offset, } if type_name == "Color" => { if let Ok(color) = parse_color(contents) { colors.push(color); } else { // Found color, but its contents is not in the right format return Err(PsyParseError::UnknownError(byte_offset)); } } _ => {} } } return Ok(SphereLight::new(arena, &radii, &colors)); } else { return Err(PsyParseError::UnknownError(tree.byte_offset())); } } pub fn parse_rectangle_light<'a>( arena: &'a Arena, tree: &'a DataTree, ) -> Result, PsyParseError> { if let DataTree::Internal { ref children, .. } = *tree { let mut dimensions = Vec::new(); let mut colors = Vec::new(); // Parse for child in children.iter() { match *child { // Dimensions DataTree::Leaf { type_name, contents, byte_offset, } if type_name == "Dimensions" => { if let IResult::Ok((_, radius)) = all_consuming(tuple((ws_f32, ws_f32)))(contents) { dimensions.push(radius); } else { // Found dimensions, but its contents is not in the right format return Err(PsyParseError::UnknownError(byte_offset)); } } // Color DataTree::Leaf { type_name, contents, byte_offset, } if type_name == "Color" => { if let Ok(color) = parse_color(contents) { colors.push(color); } else { // Found color, but its contents is not in the right format return Err(PsyParseError::UnknownError(byte_offset)); } } _ => {} } } return Ok(RectangleLight::new(arena, &dimensions, &colors)); } else { return Err(PsyParseError::UnknownError(tree.byte_offset())); } } ================================================ FILE: src/parse/psy_mesh_surface.rs ================================================ #![allow(dead_code)] use std::result::Result; use nom::{sequence::tuple, IResult}; use kioku::Arena; use crate::{ math::{Normal, Point}, surface::triangle_mesh::TriangleMesh, }; use super::{ basics::{ws_f32, ws_usize}, psy::PsyParseError, DataTree, }; // pub struct TriangleMesh { // time_samples: usize, // geo: Vec<(Point, Point, Point)>, // indices: Vec, // accel: BVH, // } pub fn parse_mesh_surface<'a>( arena: &'a Arena, tree: &'a DataTree, ) -> Result, PsyParseError> { let mut verts = Vec::new(); // Vec of vecs, one for each time sample let mut normals = Vec::new(); // Vec of vecs, on for each time sample let mut face_vert_counts = Vec::new(); let mut face_vert_indices = Vec::new(); // TODO: make sure there are the right number of various children, // and other validation. // Get verts for (_, mut text, _) in tree.iter_leaf_children_with_type("Vertices") { // Collect verts for this time sample let mut tverts = Vec::new(); while let IResult::Ok((remaining, vert)) = tuple((ws_f32, ws_f32, ws_f32))(text) { text = remaining; tverts.push(Point::new(vert.0, vert.1, vert.2)); } verts.push(tverts); } // Make sure all time samples have same vert count let vert_count = verts[0].len(); for vs in &verts { assert_eq!(vert_count, vs.len()); } // Get normals, if they exist for (_, mut text, _) in tree.iter_leaf_children_with_type("Normals") { // Collect normals for this time sample let mut tnormals = Vec::new(); while let IResult::Ok((remaining, nor)) = tuple((ws_f32, ws_f32, ws_f32))(text) { text = remaining; tnormals.push(Normal::new(nor.0, nor.1, nor.2).normalized()); } normals.push(tnormals); } // Make sure normal's time samples and vert count match the vertices if !normals.is_empty() { assert_eq!(normals.len(), verts.len()); for ns in &normals { assert_eq!(vert_count, ns.len()); } } // Get face vert counts if let Some((_, mut text, _)) = tree.iter_leaf_children_with_type("FaceVertCounts").nth(0) { while let IResult::Ok((remaining, count)) = ws_usize(text) { text = remaining; face_vert_counts.push(count); } } // Get face vert indices if let Some((_, mut text, _)) = tree.iter_leaf_children_with_type("FaceVertIndices").nth(0) { while let IResult::Ok((remaining, index)) = ws_usize(text) { text = remaining; face_vert_indices.push(index); } } // Build triangle mesh let mut tri_vert_indices = Vec::new(); let mut ii = 0; for fvc in &face_vert_counts { if *fvc >= 3 { // Store the polygon, split up into triangles if >3 verts let v1 = ii; for vi in 0..(fvc - 2) { tri_vert_indices.push(( face_vert_indices[v1], face_vert_indices[v1 + vi + 1], face_vert_indices[v1 + vi + 2], )); } } else { // TODO: proper error panic!("Cannot handle polygons with less than three vertices."); } ii += *fvc; } Ok(TriangleMesh::from_verts_and_indices( arena, &verts, &if normals.is_empty() { None } else { Some(normals) }, &tri_vert_indices, )) } ================================================ FILE: src/parse/psy_surface_shader.rs ================================================ #![allow(dead_code)] use std::result::Result; use nom::{combinator::all_consuming, IResult}; use kioku::Arena; use crate::shading::{SimpleSurfaceShader, SurfaceShader}; use super::{ basics::ws_f32, psy::{parse_color, PsyParseError}, DataTree, }; // pub struct TriangleMesh { // time_samples: usize, // geo: Vec<(Point, Point, Point)>, // indices: Vec, // accel: BVH, // } pub fn parse_surface_shader<'a>( arena: &'a Arena, tree: &'a DataTree, ) -> Result<&'a dyn SurfaceShader, PsyParseError> { let type_name = if let Some((_, text, _)) = tree.iter_leaf_children_with_type("Type").nth(0) { text.trim() } else { return Err(PsyParseError::MissingNode( tree.byte_offset(), "Expected a Type field in SurfaceShader.", )); }; let shader = match type_name { "Lambert" => { let color = if let Some((_, contents, byte_offset)) = tree.iter_leaf_children_with_type("Color").nth(0) { if let Ok(color) = parse_color(contents) { color } else { // Found color, but its contents is not in the right format return Err(PsyParseError::UnknownError(byte_offset)); } } else { return Err(PsyParseError::MissingNode( tree.byte_offset(), "Expected a Color field in Lambert SurfaceShader.", )); }; arena.alloc(SimpleSurfaceShader::Lambert { color: color }) } "GGX" => { // Color let color = if let Some((_, contents, byte_offset)) = tree.iter_leaf_children_with_type("Color").nth(0) { if let Ok(color) = parse_color(contents) { color } else { // Found color, but its contents is not in the right format return Err(PsyParseError::UnknownError(byte_offset)); } } else { return Err(PsyParseError::MissingNode( tree.byte_offset(), "Expected a Color field in GTR SurfaceShader.", )); }; // Roughness let roughness = if let Some((_, contents, byte_offset)) = tree.iter_leaf_children_with_type("Roughness").nth(0) { if let IResult::Ok((_, roughness)) = all_consuming(ws_f32)(contents) { roughness } else { return Err(PsyParseError::UnknownError(byte_offset)); } } else { return Err(PsyParseError::MissingNode( tree.byte_offset(), "Expected a Roughness field in GTR SurfaceShader.", )); }; // Fresnel let fresnel = if let Some((_, contents, byte_offset)) = tree.iter_leaf_children_with_type("Fresnel").nth(0) { if let IResult::Ok((_, fresnel)) = all_consuming(ws_f32)(contents) { fresnel } else { return Err(PsyParseError::UnknownError(byte_offset)); } } else { return Err(PsyParseError::MissingNode( tree.byte_offset(), "Expected a Fresnel field in GTR SurfaceShader.", )); }; arena.alloc(SimpleSurfaceShader::GGX { color: color, roughness: roughness, fresnel: fresnel, }) } "Emit" => { let color = if let Some((_, contents, byte_offset)) = tree.iter_leaf_children_with_type("Color").nth(0) { if let Ok(color) = parse_color(contents) { color } else { // Found color, but its contents is not in the right format return Err(PsyParseError::UnknownError(byte_offset)); } } else { return Err(PsyParseError::MissingNode( tree.byte_offset(), "Expected a Color field in Emit SurfaceShader.", )); }; arena.alloc(SimpleSurfaceShader::Emit { color: color }) } _ => unimplemented!(), }; Ok(shader) } ================================================ FILE: src/ray.rs ================================================ #![allow(dead_code)] use glam::BVec4A; use crate::math::{Point, Transform, Vector}; type RayIndexType = u16; type FlagType = u8; const OCCLUSION_FLAG: FlagType = 1; const DONE_FLAG: FlagType = 1 << 1; /// This is never used directly in ray tracing--it's only used as a convenience /// for filling the RayBatch structure. #[derive(Debug, Copy, Clone)] pub struct Ray { pub orig: Point, pub dir: Vector, pub time: f32, pub wavelength: f32, pub max_t: f32, } /// The hot (frequently accessed) parts of ray data. #[derive(Debug, Copy, Clone)] struct RayHot { orig_local: Point, // Local-space ray origin dir_inv_local: Vector, // Local-space 1.0/ray direction max_t: f32, time: f32, flags: FlagType, } /// The cold (infrequently accessed) parts of ray data. #[derive(Debug, Copy, Clone)] struct RayCold { orig: Point, // World-space ray origin dir: Vector, // World-space ray direction wavelength: f32, } /// A batch of rays, separated into hot and cold parts. #[derive(Debug)] pub struct RayBatch { hot: Vec, cold: Vec, } impl RayBatch { /// Creates a new empty ray batch. pub fn new() -> RayBatch { RayBatch { hot: Vec::new(), cold: Vec::new(), } } /// Creates a new empty ray batch, with pre-allocated capacity for /// `n` rays. pub fn with_capacity(n: usize) -> RayBatch { RayBatch { hot: Vec::with_capacity(n), cold: Vec::with_capacity(n), } } pub fn push(&mut self, ray: Ray, is_occlusion: bool) { self.hot.push(RayHot { orig_local: ray.orig, // Bogus, to place-hold. dir_inv_local: ray.dir, // Bogus, to place-hold. max_t: ray.max_t, time: ray.time, flags: if is_occlusion { OCCLUSION_FLAG } else { 0 }, }); self.cold.push(RayCold { orig: ray.orig, dir: ray.dir, wavelength: ray.wavelength, }); } pub fn swap(&mut self, a: usize, b: usize) { self.hot.swap(a, b); self.cold.swap(a, b); } pub fn set_from_ray(&mut self, ray: &Ray, is_occlusion: bool, idx: usize) { self.hot[idx].orig_local = ray.orig; self.hot[idx].dir_inv_local = Vector { co: ray.dir.co.recip(), }; self.hot[idx].max_t = ray.max_t; self.hot[idx].time = ray.time; self.hot[idx].flags = if is_occlusion { OCCLUSION_FLAG } else { 0 }; self.cold[idx].orig = ray.orig; self.cold[idx].dir = ray.dir; self.cold[idx].wavelength = ray.wavelength; } pub fn truncate(&mut self, len: usize) { self.hot.truncate(len); self.cold.truncate(len); } /// Clear all rays, settings the size of the batch back to zero. /// /// Capacity is maintained. pub fn clear(&mut self) { self.hot.clear(); self.cold.clear(); } pub fn len(&self) -> usize { self.hot.len() } /// Updates the accel data of the given ray (at index `idx`) with the /// given world-to-local-space transform matrix. /// /// This should be called when entering (and exiting) traversal of a /// new transform space. pub fn update_local(&mut self, idx: usize, xform: &Transform) { self.hot[idx].orig_local = self.cold[idx].orig * *xform; self.hot[idx].dir_inv_local = Vector { co: (self.cold[idx].dir * *xform).co.recip(), }; } //========================================================== // Data access #[inline(always)] pub fn orig(&self, idx: usize) -> Point { self.cold[idx].orig } #[inline(always)] pub fn dir(&self, idx: usize) -> Vector { self.cold[idx].dir } #[inline(always)] pub fn orig_local(&self, idx: usize) -> Point { self.hot[idx].orig_local } #[inline(always)] pub fn dir_inv_local(&self, idx: usize) -> Vector { self.hot[idx].dir_inv_local } #[inline(always)] pub fn time(&self, idx: usize) -> f32 { self.hot[idx].time } #[inline(always)] pub fn max_t(&self, idx: usize) -> f32 { self.hot[idx].max_t } #[inline(always)] pub fn set_max_t(&mut self, idx: usize, new_max_t: f32) { self.hot[idx].max_t = new_max_t; } #[inline(always)] pub fn wavelength(&self, idx: usize) -> f32 { self.cold[idx].wavelength } /// Returns whether the given ray (at index `idx`) is an occlusion ray. #[inline(always)] pub fn is_occlusion(&self, idx: usize) -> bool { (self.hot[idx].flags & OCCLUSION_FLAG) != 0 } /// Returns whether the given ray (at index `idx`) has finished traversal. #[inline(always)] pub fn is_done(&self, idx: usize) -> bool { (self.hot[idx].flags & DONE_FLAG) != 0 } /// Marks the given ray (at index `idx`) as an occlusion ray. #[inline(always)] pub fn mark_occlusion(&mut self, idx: usize) { self.hot[idx].flags |= OCCLUSION_FLAG } /// Marks the given ray (at index `idx`) as having finished traversal. #[inline(always)] pub fn mark_done(&mut self, idx: usize) { self.hot[idx].flags |= DONE_FLAG } } /// A structure used for tracking traversal of a ray batch through a scene. #[derive(Debug)] pub struct RayStack { lanes: Vec, tasks: Vec, } impl RayStack { pub fn new() -> RayStack { RayStack { lanes: Vec::new(), tasks: Vec::new(), } } /// Returns whether the stack is empty of tasks or not. pub fn is_empty(&self) -> bool { self.tasks.is_empty() } /// Makes sure there are at least `count` lanes. pub fn ensure_lane_count(&mut self, count: usize) { while self.lanes.len() < count { self.lanes.push(Lane { idxs: Vec::new(), end_len: 0, }) } } pub fn ray_count_in_next_task(&self) -> usize { let task = self.tasks.last().unwrap(); let end = self.lanes[task.lane].end_len; end - task.start_idx } pub fn next_task_ray_idx(&self, i: usize) -> usize { let task = self.tasks.last().unwrap(); let i = i + task.start_idx; debug_assert!(i < self.lanes[task.lane].end_len); self.lanes[task.lane].idxs[i] as usize } /// Clears the lanes and tasks of the RayStack. /// /// Note: this is (importantly) different than calling clear individually /// on the `lanes` and `tasks` members. Specifically, we don't want to /// clear `lanes` itself, as that would also free all the memory of the /// individual lanes. Instead, we want to iterate over the individual /// lanes and clear them, but leave `lanes` itself untouched. pub fn clear(&mut self) { for lane in self.lanes.iter_mut() { lane.idxs.clear(); lane.end_len = 0; } self.tasks.clear(); } /// Pushes the given ray index onto the end of the specified lane. pub fn push_ray_index(&mut self, ray_idx: usize, lane: usize) { assert!(self.lanes.len() > lane); self.lanes[lane].idxs.push(ray_idx as RayIndexType); } /// Pushes any excess indices on the given lane to a new task on the /// task stack. /// /// Returns whether a task was pushed or not. No task will be pushed /// if there are no excess indices on the end of the lane. pub fn push_lane_to_task(&mut self, lane_idx: usize) -> bool { if self.lanes[lane_idx].end_len < self.lanes[lane_idx].idxs.len() { self.tasks.push(RayTask { lane: lane_idx, start_idx: self.lanes[lane_idx].end_len, }); self.lanes[lane_idx].end_len = self.lanes[lane_idx].idxs.len(); true } else { false } } /// Takes the given list of lane indices, and pushes any excess indices on /// the end of each into a new task, in the order provided. pub fn push_lanes_to_tasks(&mut self, lane_idxs: &[usize]) { for &l in lane_idxs { self.push_lane_to_task(l); } } pub fn duplicate_next_task(&mut self) { let task = self.tasks.last().unwrap(); let l = task.lane; let start = task.start_idx; let end = self.lanes[l].end_len; // Extend the indices vector self.lanes[l].idxs.reserve(end - start); let old_len = self.lanes[l].idxs.len(); let new_len = old_len + end - start; unsafe { self.lanes[l].idxs.set_len(new_len); } // Copy elements copy_in_place::copy_in_place(&mut self.lanes[l].idxs, start..end, end); // Push the new task onto the stack self.tasks.push(RayTask { lane: l, start_idx: end, }); self.lanes[l].end_len = self.lanes[l].idxs.len(); } // Pops the next task off the stack. pub fn pop_task(&mut self) { let task = self.tasks.pop().unwrap(); self.lanes[task.lane].end_len = task.start_idx; self.lanes[task.lane].idxs.truncate(task.start_idx); } // Executes a task without popping it from the task stack. pub fn do_next_task(&mut self, mut handle_ray: F) where F: FnMut(usize), { let task = self.tasks.last().unwrap(); let task_range = (task.start_idx, self.lanes[task.lane].end_len); // Execute task. for i in task_range.0..task_range.1 { let ray_idx = self.lanes[task.lane].idxs[i]; handle_ray(ray_idx as usize); } } /// Pops the next task off the stack, and executes the provided closure for /// each ray index in the task. #[inline(always)] pub fn pop_do_next_task(&mut self, handle_ray: F) where F: FnMut(usize), { self.do_next_task(handle_ray); self.pop_task(); } /// Pops the next task off the stack, executes the provided closure for /// each ray index in the task, and pushes the ray indices back onto the /// indicated lanes. pub fn pop_do_next_task_and_push_rays(&mut self, output_lane_count: usize, mut handle_ray: F) where F: FnMut(usize) -> BVec4A, { // Pop the task and do necessary bookkeeping. let task = self.tasks.pop().unwrap(); let task_range = (task.start_idx, self.lanes[task.lane].end_len); self.lanes[task.lane].end_len = task.start_idx; // SAFETY: this is probably evil, and depends on behavior of Vec that // are not actually promised. But we're essentially truncating the lane // to the start of our task range, but will continue to access it's // elements beyond that range via `get_unchecked()` below. Because the // memory is not freed nor altered, this is safe. However, again, the // Vec apis don't promise this behavior. So: // // TODO: build a slightly different lane abstraction to get this same // efficiency without depending on implicit Vec behavior. unsafe { self.lanes[task.lane].idxs.set_len(task.start_idx); } // Execute task. for i in task_range.0..task_range.1 { let ray_idx = *unsafe { self.lanes[task.lane].idxs.get_unchecked(i) }; let push_mask = handle_ray(ray_idx as usize).bitmask(); for l in 0..output_lane_count { if (push_mask & (1 << l)) != 0 { self.lanes[l as usize].idxs.push(ray_idx); } } } } } /// A lane within a RayStack. #[derive(Debug)] struct Lane { idxs: Vec, end_len: usize, } /// A task within a RayStack. // // Specifies the lane that the relevant ray pointers are in, and the // starting index within that lane. The relevant pointers are always // `&[start_idx..]` within the given lane. #[derive(Debug)] struct RayTask { lane: usize, start_idx: usize, } ================================================ FILE: src/renderer.rs ================================================ use std::{ cell::Cell, cmp, cmp::min, io::{self, Write}, sync::{Mutex, RwLock}, }; use crossbeam::sync::MsQueue; use scoped_threadpool::Pool; use glam::Vec4; use crate::{ accel::ACCEL_NODE_RAY_TESTS, color::{map_0_1_to_wavelength, SpectralSample, XYZ}, fp_utils::robust_ray_origin, hash::hash_u32, hilbert, image::Image, math::probit, mis::power_heuristic, ray::{Ray, RayBatch}, scene::{Scene, SceneLightSample}, surface, timer::Timer, tracer::Tracer, transform_stack::TransformStack, }; #[derive(Debug)] pub struct Renderer<'a> { pub output_file: String, pub resolution: (usize, usize), pub spp: usize, pub seed: u32, pub scene: Scene<'a>, } #[derive(Debug, Copy, Clone)] pub struct RenderStats { pub trace_time: f64, pub accel_node_visits: u64, pub ray_count: u64, pub initial_ray_generation_time: f64, pub ray_generation_time: f64, pub sample_writing_time: f64, pub total_time: f64, } impl RenderStats { fn new() -> RenderStats { RenderStats { trace_time: 0.0, accel_node_visits: 0, ray_count: 0, initial_ray_generation_time: 0.0, ray_generation_time: 0.0, sample_writing_time: 0.0, total_time: 0.0, } } fn collect(&mut self, other: RenderStats) { self.trace_time += other.trace_time; self.accel_node_visits += other.accel_node_visits; self.ray_count += other.ray_count; self.initial_ray_generation_time += other.initial_ray_generation_time; self.ray_generation_time += other.ray_generation_time; self.sample_writing_time += other.sample_writing_time; self.total_time += other.total_time; } } impl<'a> Renderer<'a> { pub fn render( &self, max_samples_per_bucket: u32, crop: Option<(u32, u32, u32, u32)>, thread_count: u32, do_blender_output: bool, ) -> (Image, RenderStats) { let mut tpool = Pool::new(thread_count); let image = Image::new(self.resolution.0, self.resolution.1); let (img_width, img_height) = (image.width(), image.height()); let all_jobs_queued = RwLock::new(false); let collective_stats = RwLock::new(RenderStats::new()); // Set up job queue let job_queue = MsQueue::new(); // For printing render progress let pixels_rendered = Mutex::new(Cell::new(0)); // Calculate dimensions and coordinates of what we're rendering. This // accounts for cropping. let (width, height, start_x, start_y) = if let Some((x1, y1, x2, y2)) = crop { let x1 = min(x1 as usize, img_width - 1); let y1 = min(y1 as usize, img_height - 1); let x2 = min(x2 as usize, img_width - 1); let y2 = min(y2 as usize, img_height - 1); (x2 - x1 + 1, y2 - y1 + 1, x1, y1) } else { (img_width, img_height, 0, 0) }; // Render tpool.scoped(|scope| { // Spawn worker tasks for _ in 0..thread_count { let jq = &job_queue; let ajq = &all_jobs_queued; let img = ℑ let pixrenref = &pixels_rendered; let cstats = &collective_stats; scope.execute(move || { self.render_job( jq, ajq, img, width * height, pixrenref, cstats, do_blender_output, ) }); } // Print initial 0.00% progress print!("0.00%"); let _ = io::stdout().flush(); // Determine bucket size based on the per-thread maximum number of samples to // calculate at a time. let (bucket_w, bucket_h) = { let target_pixels_per_bucket = max_samples_per_bucket as f64 / self.spp as f64; let target_bucket_dim = if target_pixels_per_bucket.sqrt() < 1.0 { 1usize } else { target_pixels_per_bucket.sqrt() as usize }; (target_bucket_dim, target_bucket_dim) }; // Populate job queue let bucket_n = { let bucket_count_x = ((width / bucket_w) + 1) as u32; let bucket_count_y = ((height / bucket_h) + 1) as u32; let larger = cmp::max(bucket_count_x, bucket_count_y); let pow2 = larger.next_power_of_two(); pow2 * pow2 }; for hilbert_d in 0..bucket_n { let (bx, by) = hilbert::d2xy(hilbert_d); let x = bx as usize * bucket_w; let y = by as usize * bucket_h; let w = if width >= x { min(bucket_w, width - x) } else { bucket_w }; let h = if height >= y { min(bucket_h, height - y) } else { bucket_h }; if x < width && y < height && w > 0 && h > 0 { job_queue.push(BucketJob { x: (start_x + x) as u32, y: (start_y + y) as u32, w: w as u32, h: h as u32, }); } } // Mark done queuing jobs *all_jobs_queued.write().unwrap() = true; }); // Clear percentage progress print print!("\r \r",); // Return the rendered image and stats return (image, *collective_stats.read().unwrap()); } /// Waits for buckets in the job queue to render and renders them when available. fn render_job( &self, job_queue: &MsQueue, all_jobs_queued: &RwLock, image: &Image, total_pixels: usize, pixels_rendered: &Mutex>, collected_stats: &RwLock, do_blender_output: bool, ) { let mut stats = RenderStats::new(); let mut timer = Timer::new(); let mut total_timer = Timer::new(); let mut paths = Vec::new(); let mut rays = RayBatch::new(); let mut tracer = Tracer::from_assembly(&self.scene.root); let mut xform_stack = TransformStack::new(); // Pre-calculate some useful values related to the image plane let cmpx = 1.0 / self.resolution.0 as f32; let cmpy = 1.0 / self.resolution.1 as f32; let min_x = -1.0; let max_x = 1.0; let min_y = -(self.resolution.1 as f32 / self.resolution.0 as f32); let max_y = self.resolution.1 as f32 / self.resolution.0 as f32; let x_extent = max_x - min_x; let y_extent = max_y - min_y; // Render 'render_loop: loop { paths.clear(); rays.clear(); // Get bucket, or exit if no more jobs left let bucket: BucketJob; loop { if let Some(b) = job_queue.try_pop() { bucket = b; break; } else if *all_jobs_queued.read().unwrap() { break 'render_loop; } } timer.tick(); // Generate light paths and initial rays for y in bucket.y..(bucket.y + bucket.h) { for x in bucket.x..(bucket.x + bucket.w) { for si in 0..self.spp { // Raw sample numbers. let (d0, d1, d2, d3) = get_sample_4d(si as u32, 0, (x, y), self.seed); let (d4, _, _, _) = get_sample_4d(si as u32, 1, (x, y), self.seed); // Calculate image plane x and y coordinates let (img_x, img_y) = { let filter_x = probit(d3, 2.0 / 6.0) + 0.5; let filter_y = probit(d4, 2.0 / 6.0) + 0.5; let samp_x = (filter_x + x as f32) * cmpx; let samp_y = (filter_y + y as f32) * cmpy; ((samp_x - 0.5) * x_extent, (0.5 - samp_y) * y_extent) }; // Create the light path and initial ray for this sample let (path, ray) = LightPath::new( &self.scene, self.seed, (x, y), (img_x, img_y), (d1, d2), d0, map_0_1_to_wavelength(golden_ratio_sample( si as u32, hash_u32((x << 16) ^ y, self.seed), )), si as u32, ); paths.push(path); rays.push(ray, false); } } } stats.initial_ray_generation_time += timer.tick() as f64; // Trace the paths! let mut pi = paths.len(); while pi > 0 { // Test rays against scene let isects = tracer.trace(&mut rays); stats.trace_time += timer.tick() as f64; // Determine next rays to shoot based on result let mut new_end = 0; for i in 0..pi { if paths[i].next(&mut xform_stack, &self.scene, &isects[i], &mut rays, i) { paths.swap(new_end, i); rays.swap(new_end, i); new_end += 1; } } rays.truncate(new_end); pi = new_end; stats.ray_generation_time += timer.tick() as f64; } { // Calculate color based on ray hits and save to image let min = (bucket.x, bucket.y); let max = (bucket.x + bucket.w, bucket.y + bucket.h); let mut img_bucket = image.get_bucket(min, max); for path in &paths { let path_col = SpectralSample::from_parts(path.color, path.wavelength); let mut col = img_bucket.get(path.pixel_co.0, path.pixel_co.1); col += XYZ::from_spectral_sample(&path_col) / self.spp as f32; img_bucket.set(path.pixel_co.0, path.pixel_co.1, col); } stats.sample_writing_time += timer.tick() as f64; // Pre-calculate base64 encoding if needed let base64_enc = if do_blender_output { use crate::color::xyz_to_rec709_e; Some(img_bucket.rgba_base64(xyz_to_rec709_e)) } else { None }; // Print render progress, and image data if doing blender output let guard = pixels_rendered.lock().unwrap(); let mut pr = (*guard).get(); let percentage_old = pr as f64 / total_pixels as f64 * 100.0; pr += bucket.w as usize * bucket.h as usize; (*guard).set(pr); let percentage_new = pr as f64 / total_pixels as f64 * 100.0; let old_string = format!("{:.2}%", percentage_old); let new_string = format!("{:.2}%", percentage_new); if let Some(bucket_data) = base64_enc { // If doing Blender output println!("DIV"); println!("{}", new_string); println!("{} {} {} {}", min.0, min.1, max.0, max.1); println!("{}", bucket_data); println!("BUCKET_END"); println!("DIV"); } else { // If doing console output if new_string != old_string { print!("\r{}", new_string); } } let _ = io::stdout().flush(); } } stats.total_time += total_timer.tick() as f64; stats.ray_count = tracer.rays_traced(); ACCEL_NODE_RAY_TESTS.with(|anv| { stats.accel_node_visits = anv.get(); anv.set(0); }); // Collect stats collected_stats.write().unwrap().collect(stats); } } #[derive(Debug)] enum LightPathEvent { CameraRay, BounceRay, ShadowRay, } #[derive(Debug)] pub struct LightPath { event: LightPathEvent, bounce_count: u32, sampling_seed: u32, pixel_co: (u32, u32), sample_number: u32, // Which sample in the LDS sequence this is. dim_offset: u32, time: f32, wavelength: f32, next_bounce_ray: Option, next_attenuation_fac: Vec4, closure_sample_pdf: f32, light_attenuation: Vec4, pending_color_addition: Vec4, color: Vec4, } #[allow(clippy::new_ret_no_self)] impl LightPath { fn new( scene: &Scene, sampling_seed: u32, pixel_co: (u32, u32), image_plane_co: (f32, f32), lens_uv: (f32, f32), time: f32, wavelength: f32, sample_number: u32, ) -> (LightPath, Ray) { ( LightPath { event: LightPathEvent::CameraRay, bounce_count: 0, sampling_seed: sampling_seed ^ 0x40d4682b, pixel_co: pixel_co, sample_number: sample_number, dim_offset: 0, time: time, wavelength: wavelength, next_bounce_ray: None, next_attenuation_fac: Vec4::splat(1.0), closure_sample_pdf: 1.0, light_attenuation: Vec4::splat(1.0), pending_color_addition: Vec4::splat(0.0), color: Vec4::splat(0.0), }, scene.camera.generate_ray( image_plane_co.0, image_plane_co.1, time, wavelength, lens_uv.0, lens_uv.1, ), ) } fn next_lds_sequence(&mut self) { self.dim_offset = 0; self.sampling_seed += 1; } fn next_lds_samp(&mut self) -> (f32, f32, f32, f32) { let dimension = self.dim_offset; self.dim_offset += 1; get_sample_4d( self.sample_number, dimension, self.pixel_co, self.sampling_seed, ) } fn next( &mut self, xform_stack: &mut TransformStack, scene: &Scene, isect: &surface::SurfaceIntersection, rays: &mut RayBatch, ray_idx: usize, ) -> bool { match self.event { //-------------------------------------------------------------------- // Result of Camera or bounce ray, prepare next bounce and light rays LightPathEvent::CameraRay | LightPathEvent::BounceRay => { if let surface::SurfaceIntersection::Hit { intersection_data: ref idata, ref closure, } = *isect { // Hit something! Do the stuff // If it's an emission closure, handle specially: // - Collect light from the emission. // - Terminate the path. use crate::shading::surface_closure::SurfaceClosure; if let SurfaceClosure::Emit(color) = *closure { let color = color.to_spectral_sample(self.wavelength).e; if let LightPathEvent::CameraRay = self.event { self.color += color; } else { let mis_pdf = power_heuristic(self.closure_sample_pdf, idata.sample_pdf); self.color += color * self.light_attenuation / mis_pdf; }; return false; } // Roll the previous closure pdf into the attenauation self.light_attenuation /= self.closure_sample_pdf; // Prepare light ray self.next_lds_sequence(); let (light_n, d2, d3, d4) = self.next_lds_samp(); let light_uvw = (d2, d3, d4); xform_stack.clear(); let light_info = scene.sample_lights( xform_stack, light_n, light_uvw, self.wavelength, self.time, isect, ); let found_light = if light_info.is_none() || light_info.pdf() <= 0.0 || light_info.selection_pdf() <= 0.0 { false } else { let light_pdf = light_info.pdf(); let light_sel_pdf = light_info.selection_pdf(); // Calculate the shadow ray and surface closure stuff let (attenuation, closure_pdf, shadow_ray) = match light_info { SceneLightSample::None => unreachable!(), // Distant light SceneLightSample::Distant { direction, .. } => { let (attenuation, closure_pdf) = closure.evaluate( rays.dir(ray_idx), direction, idata.nor, idata.nor_g, self.wavelength, ); let shadow_ray = { // Calculate the shadow ray for testing if the light is // in shadow or not. let offset_pos = robust_ray_origin( idata.pos, idata.pos_err, idata.nor_g.normalized(), direction, ); Ray { orig: offset_pos, dir: direction, time: self.time, wavelength: self.wavelength, max_t: std::f32::INFINITY, } }; (attenuation, closure_pdf, shadow_ray) } // Surface light SceneLightSample::Surface { sample_geo, .. } => { let dir = sample_geo.0 - idata.pos; let (attenuation, closure_pdf) = closure.evaluate( rays.dir(ray_idx), dir, idata.nor, idata.nor_g, self.wavelength, ); let shadow_ray = { // Calculate the shadow ray for testing if the light is // in shadow or not. let offset_pos = robust_ray_origin( idata.pos, idata.pos_err, idata.nor_g.normalized(), dir, ); let offset_end = robust_ray_origin( sample_geo.0, sample_geo.2, sample_geo.1.normalized(), -dir, ); Ray { orig: offset_pos, dir: offset_end - offset_pos, time: self.time, wavelength: self.wavelength, max_t: 1.0, } }; (attenuation, closure_pdf, shadow_ray) } }; // If there's any possible contribution, set up for a // light ray. if attenuation.e.max_element() <= 0.0 { false } else { // Calculate and store the light that will be contributed // to the film plane if the light is not in shadow. let light_mis_pdf = power_heuristic(light_pdf, closure_pdf); self.pending_color_addition = light_info.color().e * attenuation.e * self.light_attenuation / (light_mis_pdf * light_sel_pdf); rays.set_from_ray(&shadow_ray, true, ray_idx); true } }; // Prepare bounce ray let do_bounce = if self.bounce_count < 2 { self.bounce_count += 1; // Sample closure let (dir, filter, pdf) = { self.next_lds_sequence(); let (u, v, _, _) = self.next_lds_samp(); closure.sample( idata.incoming, idata.nor, idata.nor_g, (u, v), self.wavelength, ) }; // Check if pdf is zero, to avoid NaN's. if (pdf > 0.0) && (filter.e.max_element() > 0.0) { // Account for the additional light attenuation from // this bounce self.next_attenuation_fac = filter.e; self.closure_sample_pdf = pdf; // Calculate the ray for this bounce let offset_pos = robust_ray_origin( idata.pos, idata.pos_err, idata.nor_g.normalized(), dir, ); self.next_bounce_ray = Some(Ray { orig: offset_pos, dir: dir, time: self.time, wavelength: self.wavelength, max_t: std::f32::INFINITY, }); true } else { false } } else { self.next_bounce_ray = None; false }; // Book keeping for next event if found_light { self.event = LightPathEvent::ShadowRay; return true; } else if do_bounce { rays.set_from_ray(&self.next_bounce_ray.unwrap(), false, ray_idx); self.event = LightPathEvent::BounceRay; self.light_attenuation *= self.next_attenuation_fac; return true; } else { return false; } } else { // Didn't hit anything, so background color self.color += scene .world .background_color .to_spectral_sample(self.wavelength) .e * self.light_attenuation / self.closure_sample_pdf; return false; } } //-------------------------------------------------------------------- // Result of shadow ray from sampling a light LightPathEvent::ShadowRay => { // If the light was not in shadow, add it's light to the film // plane. if let surface::SurfaceIntersection::Miss = *isect { self.color += self.pending_color_addition; } // Set up for the next bounce, if any if let Some(ref nbr) = self.next_bounce_ray { rays.set_from_ray(nbr, false, ray_idx); self.light_attenuation *= self.next_attenuation_fac; self.event = LightPathEvent::BounceRay; return true; } else { return false; } } } } } /// Gets a sample, using LDS samples for lower dimensions, /// and switching to random samples at higher dimensions where /// LDS samples aren't available. #[inline(always)] fn get_sample_4d( i: u32, dimension_set: u32, pixel_co: (u32, u32), seed: u32, ) -> (f32, f32, f32, f32) { // A unique seed for every pixel coordinate up to a resolution of // 65536 x 65536. Also incorperating the seed. let seed = pixel_co.0 ^ (pixel_co.1 << 16) ^ seed.wrapping_mul(0x736caf6f); match dimension_set { ds if ds < sobol_burley::NUM_DIMENSION_SETS_4D as u32 => { // Sobol sampling. let n4 = sobol_burley::sample_4d(i, ds, seed); (n4[0], n4[1], n4[2], n4[3]) } ds => { // Random sampling. use crate::hash::hash_u32_to_f32; ( hash_u32_to_f32((ds * 4 + 0) ^ (i << 16), seed), hash_u32_to_f32((ds * 4 + 1) ^ (i << 16), seed), hash_u32_to_f32((ds * 4 + 2) ^ (i << 16), seed), hash_u32_to_f32((ds * 4 + 3) ^ (i << 16), seed), ) } } } /// Golden ratio sampler. fn golden_ratio_sample(i: u32, scramble: u32) -> f32 { // NOTE: use this for the wavelength dimension, because // due to the nature of hero wavelength sampling this ends up // being crazily more efficient than pretty much any other sampler, // and reduces variance by a huge amount. let n = i .wrapping_add(hash_u32(scramble, 0)) .wrapping_mul(2654435769); n as f32 * (1.0 / (1u64 << 32) as f32) } #[derive(Debug)] struct BucketJob { x: u32, y: u32, w: u32, h: u32, } ================================================ FILE: src/sampling/mod.rs ================================================ mod monte_carlo; pub use self::monte_carlo::{ cosine_sample_hemisphere, spherical_triangle_solid_angle, square_to_circle, triangle_surface_area, uniform_sample_cone, uniform_sample_cone_pdf, uniform_sample_hemisphere, uniform_sample_sphere, uniform_sample_spherical_triangle, uniform_sample_triangle, }; ================================================ FILE: src/sampling/monte_carlo.rs ================================================ #![allow(dead_code)] use std::{f32::consts::FRAC_PI_4 as QPI_32, f32::consts::PI as PI_32, f64::consts::PI as PI_64}; use crate::math::{cross, dot, Point, Vector}; /// Maps the unit square to the unit circle. /// NOTE: x and y should be distributed within [-1, 1], /// not [0, 1]. pub fn square_to_circle(x: f32, y: f32) -> (f32, f32) { debug_assert!(x >= -1.0 && x <= 1.0 && y >= -1.0 && y <= 1.0); if x == 0.0 && y == 0.0 { return (0.0, 0.0); } let (radius, angle) = if x > y.abs() { // Quadrant 1 (x, QPI_32 * (y / x)) } else if y > x.abs() { // Quadrant 2 (y, QPI_32 * (2.0 - (x / y))) } else if x < -(y.abs()) { // Quadrant 3 (-x, QPI_32 * (4.0 + (y / x))) } else { // Quadrant 4 (-y, QPI_32 * (6.0 - (x / y))) }; (radius * angle.cos(), radius * angle.sin()) } pub fn cosine_sample_hemisphere(u: f32, v: f32) -> Vector { let (u, v) = square_to_circle((u * 2.0) - 1.0, (v * 2.0) - 1.0); let z = (1.0 - ((u * u) + (v * v))).max(0.0).sqrt(); Vector::new(u, v, z) } pub fn uniform_sample_hemisphere(u: f32, v: f32) -> Vector { let z = u; let r = (1.0 - (z * z)).max(0.0).sqrt(); let phi = 2.0 * PI_32 * v; let x = r * phi.cos(); let y = r * phi.sin(); Vector::new(x, y, z) } pub fn uniform_sample_sphere(u: f32, v: f32) -> Vector { let z = 1.0 - (2.0 * u); let r = (1.0 - (z * z)).max(0.0).sqrt(); let phi = 2.0 * PI_32 * v; let x = r * phi.cos(); let y = r * phi.sin(); Vector::new(x, y, z) } /// Samples a solid angle defined by a cone originating from (0,0,0) /// and pointing down the positive z-axis. /// /// `u`, `v`: sampling variables, should each be in the interval [0,1] /// `cos_theta_max`: cosine of the max angle from the z-axis, defining /// the outer extent of the cone. pub fn uniform_sample_cone(u: f32, v: f32, cos_theta_max: f64) -> Vector { let cos_theta = (1.0 - u as f64) + (u as f64 * cos_theta_max); let sin_theta = (1.0 - (cos_theta * cos_theta)).sqrt(); let phi = v as f64 * 2.0 * PI_64; Vector::new( (phi.cos() * sin_theta) as f32, (phi.sin() * sin_theta) as f32, cos_theta as f32, ) } pub fn uniform_sample_cone_pdf(cos_theta_max: f64) -> f64 { // 1.0 / solid angle 1.0 / (2.0 * PI_64 * (1.0 - cos_theta_max)) } /// Generates a uniform sample on a triangle given two uniform random /// variables i and j in [0, 1]. pub fn uniform_sample_triangle(va: Vector, vb: Vector, vc: Vector, i: f32, j: f32) -> Vector { let isqrt = i.sqrt(); let a = 1.0 - isqrt; let b = isqrt * (1.0 - j); let c = j * isqrt; (va * a) + (vb * b) + (vc * c) } /// Calculates the surface area of a triangle. pub fn triangle_surface_area(p0: Point, p1: Point, p2: Point) -> f32 { 0.5 * cross(p1 - p0, p2 - p0).length() } /// Calculates the projected solid angle of a spherical triangle. /// /// A, B, and C are the points of the triangle on a unit sphere. pub fn spherical_triangle_solid_angle(va: Vector, vb: Vector, vc: Vector) -> f32 { // Calculate sines and cosines of the spherical triangle's edge lengths let cos_a: f64 = dot(vb, vc).max(-1.0).min(1.0) as f64; let cos_b: f64 = dot(vc, va).max(-1.0).min(1.0) as f64; let cos_c: f64 = dot(va, vb).max(-1.0).min(1.0) as f64; let sin_a: f64 = (1.0 - (cos_a * cos_a)).sqrt(); let sin_b: f64 = (1.0 - (cos_b * cos_b)).sqrt(); let sin_c: f64 = (1.0 - (cos_c * cos_c)).sqrt(); // If two of the vertices are coincident, area is zero. // Return early to avoid a divide by zero below. if cos_a == 1.0 || cos_b == 1.0 || cos_c == 1.0 { return 0.0; } // Calculate the cosine of the angles at the vertices let cos_va = ((cos_a - (cos_b * cos_c)) / (sin_b * sin_c)) .max(-1.0) .min(1.0); let cos_vb = ((cos_b - (cos_c * cos_a)) / (sin_c * sin_a)) .max(-1.0) .min(1.0); let cos_vc = ((cos_c - (cos_a * cos_b)) / (sin_a * sin_b)) .max(-1.0) .min(1.0); // Calculate the angles themselves, in radians let ang_va = cos_va.acos(); let ang_vb = cos_vb.acos(); let ang_vc = cos_vc.acos(); // Calculate and return the solid angle of the triangle (ang_va + ang_vb + ang_vc - PI_64) as f32 } /// Generates a uniform sample on a spherical triangle given two uniform /// random variables i and j in [0, 1]. pub fn uniform_sample_spherical_triangle( va: Vector, vb: Vector, vc: Vector, i: f32, j: f32, ) -> Vector { // Calculate sines and cosines of the spherical triangle's edge lengths let cos_a: f64 = dot(vb, vc).max(-1.0).min(1.0) as f64; let cos_b: f64 = dot(vc, va).max(-1.0).min(1.0) as f64; let cos_c: f64 = dot(va, vb).max(-1.0).min(1.0) as f64; let sin_a: f64 = (1.0 - (cos_a * cos_a)).sqrt(); let sin_b: f64 = (1.0 - (cos_b * cos_b)).sqrt(); let sin_c: f64 = (1.0 - (cos_c * cos_c)).sqrt(); // If two of the vertices are coincident, area is zero. // Return early to avoid a divide by zero below. if cos_a == 1.0 || cos_b == 1.0 || cos_c == 1.0 { // TODO: do something more intelligent here, in the case that it's // an infinitely thin line. return va; } // Calculate the cosine of the angles at the vertices let cos_va = ((cos_a - (cos_b * cos_c)) / (sin_b * sin_c)) .max(-1.0) .min(1.0); let cos_vb = ((cos_b - (cos_c * cos_a)) / (sin_c * sin_a)) .max(-1.0) .min(1.0); let cos_vc = ((cos_c - (cos_a * cos_b)) / (sin_a * sin_b)) .max(-1.0) .min(1.0); // Calculate sine for A let sin_va = (1.0 - (cos_va * cos_va)).sqrt(); // Calculate the angles themselves, in radians let ang_va = cos_va.acos(); let ang_vb = cos_vb.acos(); let ang_vc = cos_vc.acos(); // Calculate the area of the spherical triangle let area = ang_va + ang_vb + ang_vc - PI_64; // The rest of this is from the paper "Stratified Sampling of Spherical // Triangles" by James Arvo. let area_2 = area * i as f64; let s = (area_2 - ang_va).sin(); let t = (area_2 - ang_va).cos(); let u = t - cos_va; let v = s + (sin_va * cos_c); let q_top = (((v * t) - (u * s)) * cos_va) - v; let q_bottom = ((v * s) + (u * t)) * sin_va; let q = q_top / q_bottom; let vc_2 = (va * q as f32) + ((vc - (va * dot(vc, va))).normalized() * (1.0 - (q * q)).sqrt() as f32); let z = 1.0 - (j * (1.0 - dot(vc_2, vb))); (vb * z) + ((vc_2 - (vb * dot(vc_2, vb))).normalized() * (1.0 - (z * z)).sqrt()) } ================================================ FILE: src/scene/assembly.rs ================================================ use std::collections::HashMap; use kioku::Arena; use crate::{ accel::BVH4, accel::{LightAccel, LightTree}, bbox::{transform_bbox_slice_from, BBox}, boundable::Boundable, color::SpectralSample, lerp::lerp_slice, light::SurfaceLight, math::{Normal, Point, Transform}, shading::SurfaceShader, surface::{Surface, SurfaceIntersection}, transform_stack::TransformStack, }; #[derive(Copy, Clone, Debug)] pub struct Assembly<'a> { // Instance list pub instances: &'a [Instance], pub light_instances: &'a [Instance], pub xforms: &'a [Transform], // Surface shader list pub surface_shaders: &'a [&'a dyn SurfaceShader], // Object list pub objects: &'a [Object<'a>], // Assembly list pub assemblies: &'a [Assembly<'a>], // Object accel pub object_accel: BVH4<'a>, // Light accel pub light_accel: LightTree<'a>, } // TODO: actually fix this clippy warning, rather than `allow`ing it. #[allow(clippy::type_complexity)] impl<'a> Assembly<'a> { // Returns (light_color, (sample_point, normal, point_err), pdf, selection_pdf) pub fn sample_lights( &self, xform_stack: &mut TransformStack, n: f32, uvw: (f32, f32, f32), wavelength: f32, time: f32, intr: &SurfaceIntersection, ) -> Option<(SpectralSample, (Point, Normal, f32), f32, f32)> { if let SurfaceIntersection::Hit { intersection_data: idata, closure, } = *intr { let sel_xform = if !xform_stack.top().is_empty() { lerp_slice(xform_stack.top(), time) } else { Transform::new() }; if let Some((light_i, sel_pdf, whittled_n)) = self.light_accel.select( idata.incoming * sel_xform, idata.pos * sel_xform, idata.nor * sel_xform, idata.nor_g * sel_xform, &closure, time, n, ) { let inst = self.light_instances[light_i]; match inst.instance_type { InstanceType::Object => { match self.objects[inst.data_index] { Object::SurfaceLight(light) => { // Get the world-to-object space transform of the light let xform = if let Some((a, b)) = inst.transform_indices { let pxforms = xform_stack.top(); let xform = lerp_slice(&self.xforms[a..b], time); if !pxforms.is_empty() { lerp_slice(pxforms, time) * xform } else { xform } } else { let pxforms = xform_stack.top(); if !pxforms.is_empty() { lerp_slice(pxforms, time) } else { Transform::new() } }; // Sample the light let (color, sample_geo, pdf) = light.sample_from_point( &xform, idata.pos, uvw.0, uvw.1, wavelength, time, ); return Some((color, sample_geo, pdf, sel_pdf)); } _ => unimplemented!(), } } InstanceType::Assembly => { // Push the world-to-object space transforms of the assembly onto // the transform stack. if let Some((a, b)) = inst.transform_indices { xform_stack.push(&self.xforms[a..b]); } // Sample sub-assembly lights let sample = self.assemblies[inst.data_index].sample_lights( xform_stack, whittled_n, uvw, wavelength, time, intr, ); // Pop the assembly's transforms off the transform stack. if inst.transform_indices.is_some() { xform_stack.pop(); } // Return sample return sample.map(|(ss, v, pdf, spdf)| (ss, v, pdf, spdf * sel_pdf)); } } } else { None } } else { None } } } impl<'a> Boundable for Assembly<'a> { fn bounds(&self) -> &[BBox] { self.object_accel.bounds() } } #[derive(Debug)] pub struct AssemblyBuilder<'a> { arena: &'a Arena, // Instance list instances: Vec, xforms: Vec, // Shader list surface_shaders: Vec<&'a dyn SurfaceShader>, surface_shader_map: HashMap, // map Name -> Index // Object list objects: Vec>, object_map: HashMap, // map Name -> Index // Assembly list assemblies: Vec>, assembly_map: HashMap, // map Name -> Index } impl<'a> AssemblyBuilder<'a> { pub fn new(arena: &'a Arena) -> AssemblyBuilder<'a> { AssemblyBuilder { arena: arena, instances: Vec::new(), xforms: Vec::new(), surface_shaders: Vec::new(), surface_shader_map: HashMap::new(), objects: Vec::new(), object_map: HashMap::new(), assemblies: Vec::new(), assembly_map: HashMap::new(), } } pub fn add_surface_shader(&mut self, name: &str, shader: &'a dyn SurfaceShader) { // Make sure the name hasn't already been used. if self.surface_shader_map.contains_key(name) { panic!("Attempted to add surface shader to assembly with a name that already exists."); } // Add shader self.surface_shader_map .insert(name.to_string(), self.surface_shaders.len()); self.surface_shaders.push(shader); } pub fn add_object(&mut self, name: &str, obj: Object<'a>) { // Make sure the name hasn't already been used. if self.name_exists(name) { panic!("Attempted to add object to assembly with a name that already exists."); } // Add object self.object_map.insert(name.to_string(), self.objects.len()); self.objects.push(obj); } pub fn add_assembly(&mut self, name: &str, asmb: Assembly<'a>) { // Make sure the name hasn't already been used. if self.name_exists(name) { panic!( "Attempted to add assembly to another assembly with a name that already \ exists." ); } // Add assembly self.assembly_map .insert(name.to_string(), self.assemblies.len()); self.assemblies.push(asmb); } pub fn add_instance( &mut self, name: &str, surface_shader_name: Option<&str>, xforms: Option<&[Transform]>, ) { // Make sure name exists if !self.name_exists(name) { panic!("Attempted to add instance with a name that doesn't exist."); } // Map zero-length transforms to None let xforms = if let Some(xf) = xforms { if !xf.is_empty() { Some(xf) } else { None } } else { None }; // Create instance let instance = if self.object_map.contains_key(name) { Instance { instance_type: InstanceType::Object, data_index: self.object_map[name], surface_shader_index: surface_shader_name.map(|name| { *self .surface_shader_map .get(name) .unwrap_or_else(|| panic!("Unknown surface shader '{}'.", name)) }), id: self.instances.len(), transform_indices: xforms .map(|xf| (self.xforms.len(), self.xforms.len() + xf.len())), } } else { Instance { instance_type: InstanceType::Assembly, data_index: self.assembly_map[name], surface_shader_index: surface_shader_name.map(|name| { *self .surface_shader_map .get(name) .unwrap_or_else(|| panic!("Unknown surface shader '{}'.", name)) }), id: self.instances.len(), transform_indices: xforms .map(|xf| (self.xforms.len(), self.xforms.len() + xf.len())), } }; self.instances.push(instance); // Store transforms if let Some(xf) = xforms { self.xforms.extend(xf); } } pub fn name_exists(&self, name: &str) -> bool { self.object_map.contains_key(name) || self.assembly_map.contains_key(name) } pub fn build(mut self) -> Assembly<'a> { // Calculate instance bounds, used for building object accel and light accel. let (bis, bbs) = self.instance_bounds(); // Build object accel let object_accel = BVH4::from_objects(self.arena, &mut self.instances[..], 1, |inst| { &bbs[bis[inst.id]..bis[inst.id + 1]] }); // Get list of instances that are for light sources or assemblies that contain light // sources. let mut light_instances: Vec<_> = self .instances .iter() .filter(|inst| match inst.instance_type { InstanceType::Object => { if let Object::SurfaceLight(_) = self.objects[inst.data_index] { true } else { false } } InstanceType::Assembly => { self.assemblies[inst.data_index] .light_accel .approximate_energy() > 0.0 } }) .cloned() .collect(); // Build light accel let light_accel = LightTree::from_objects(self.arena, &mut light_instances[..], |inst| { let bounds = &bbs[bis[inst.id]..bis[inst.id + 1]]; let energy = match inst.instance_type { InstanceType::Object => { if let Object::SurfaceLight(light) = self.objects[inst.data_index] { light.approximate_energy() } else { 0.0 } } InstanceType::Assembly => self.assemblies[inst.data_index] .light_accel .approximate_energy(), }; (bounds, energy) }); Assembly { instances: self.arena.copy_slice(&self.instances), light_instances: self.arena.copy_slice(&light_instances), xforms: self.arena.copy_slice(&self.xforms), surface_shaders: self.arena.copy_slice(&self.surface_shaders), objects: self.arena.copy_slice(&self.objects), assemblies: self.arena.copy_slice(&self.assemblies), object_accel: object_accel, light_accel: light_accel, } } /// Returns a pair of vectors with the bounds of all instances. /// This is used for building the assembly's BVH4. fn instance_bounds(&self) -> (Vec, Vec) { let mut indices = vec![0]; let mut bounds = Vec::new(); for inst in &self.instances { let mut bbs = Vec::new(); let mut bbs2 = Vec::new(); // Get bounding boxes match inst.instance_type { InstanceType::Object => { // Push bounds onto bbs let obj = &self.objects[inst.data_index]; match *obj { Object::Surface(s) => bbs.extend(s.bounds()), Object::SurfaceLight(l) => bbs.extend(l.bounds()), } } InstanceType::Assembly => { // Push bounds onto bbs let asmb = &self.assemblies[inst.data_index]; bbs.extend(asmb.bounds()); } } // Transform the bounding boxes, if necessary if let Some((xstart, xend)) = inst.transform_indices { let xf = &self.xforms[xstart..xend]; transform_bbox_slice_from(&bbs, xf, &mut bbs2); } else { bbs2.clear(); bbs2.extend(bbs); } // Push transformed bounds onto vec bounds.extend(bbs2); indices.push(bounds.len()); } (indices, bounds) } } #[derive(Copy, Clone, Debug)] pub enum Object<'a> { Surface(&'a dyn Surface), SurfaceLight(&'a dyn SurfaceLight), } #[derive(Debug, Copy, Clone)] pub struct Instance { pub instance_type: InstanceType, pub data_index: usize, pub surface_shader_index: Option, pub id: usize, pub transform_indices: Option<(usize, usize)>, } #[derive(Debug, Copy, Clone)] pub enum InstanceType { Object, Assembly, } ================================================ FILE: src/scene/mod.rs ================================================ mod assembly; mod world; use crate::{ accel::LightAccel, algorithm::weighted_choice, camera::Camera, color::SpectralSample, math::{Normal, Point, Vector}, surface::SurfaceIntersection, transform_stack::TransformStack, }; pub use self::{ assembly::{Assembly, AssemblyBuilder, InstanceType, Object}, world::World, }; #[derive(Debug)] pub struct Scene<'a> { pub name: Option, pub camera: Camera<'a>, pub world: World<'a>, pub root: Assembly<'a>, } impl<'a> Scene<'a> { pub fn sample_lights( &self, xform_stack: &mut TransformStack, n: f32, uvw: (f32, f32, f32), wavelength: f32, time: f32, intr: &SurfaceIntersection, ) -> SceneLightSample { // TODO: this just selects between world lights and local lights // with a 50/50 chance. We should do something more sophisticated // than this, accounting for the estimated impact of the lights // on the point being lit. // Calculate relative probabilities of traversing into world lights // or local lights. let wl_energy = if self .world .lights .iter() .fold(0.0, |energy, light| energy + light.approximate_energy()) <= 0.0 { 0.0 } else { 1.0 }; let ll_energy = if self.root.light_accel.approximate_energy() <= 0.0 { 0.0 } else { 1.0 }; let tot_energy = wl_energy + ll_energy; // Decide either world or local lights, and select and sample a light. if tot_energy <= 0.0 { return SceneLightSample::None; } else { let wl_prob = wl_energy / tot_energy; if n < wl_prob { // World lights let n = n / wl_prob; let (i, p) = weighted_choice(self.world.lights, n, |l| l.approximate_energy()); let (ss, sv, pdf) = self.world.lights[i].sample_from_point(uvw.0, uvw.1, wavelength, time); return SceneLightSample::Distant { color: ss, direction: sv, pdf: pdf, selection_pdf: p * wl_prob, }; } else { // Local lights let n = (n - wl_prob) / (1.0 - wl_prob); if let Some((ss, sgeo, pdf, spdf)) = self.root .sample_lights(xform_stack, n, uvw, wavelength, time, intr) { return SceneLightSample::Surface { color: ss, sample_geo: sgeo, pdf: pdf, selection_pdf: spdf * (1.0 - wl_prob), }; } else { return SceneLightSample::None; } } } } } #[derive(Debug, Copy, Clone)] pub enum SceneLightSample { None, Distant { color: SpectralSample, direction: Vector, pdf: f32, selection_pdf: f32, }, Surface { color: SpectralSample, sample_geo: (Point, Normal, f32), pdf: f32, selection_pdf: f32, }, } impl SceneLightSample { pub fn is_none(&self) -> bool { if let SceneLightSample::None = *self { true } else { false } } pub fn color(&self) -> SpectralSample { match *self { SceneLightSample::None => panic!(), SceneLightSample::Distant { color, .. } => color, SceneLightSample::Surface { color, .. } => color, } } pub fn pdf(&self) -> f32 { match *self { SceneLightSample::None => panic!(), SceneLightSample::Distant { pdf, .. } => pdf, SceneLightSample::Surface { pdf, .. } => pdf, } } pub fn selection_pdf(&self) -> f32 { match *self { SceneLightSample::None => panic!(), SceneLightSample::Distant { selection_pdf, .. } => selection_pdf, SceneLightSample::Surface { selection_pdf, .. } => selection_pdf, } } } ================================================ FILE: src/scene/world.rs ================================================ use crate::{color::Color, light::WorldLightSource}; #[derive(Debug)] pub struct World<'a> { pub background_color: Color, pub lights: &'a [&'a dyn WorldLightSource], } ================================================ FILE: src/shading/mod.rs ================================================ pub mod surface_closure; use std::fmt::Debug; use crate::{color::Color, surface::SurfaceIntersectionData}; pub use self::surface_closure::SurfaceClosure; /// Trait for surface shaders. pub trait SurfaceShader: Debug + Sync { /// Takes the result of a surface intersection and returns the surface /// closure to be evaluated at that intersection point. fn shade(&self, data: &SurfaceIntersectionData, time: f32) -> SurfaceClosure; } /// Clearly we must eat this brownie before the world ends, lest it /// go uneaten before the world ends. But to do so we must trek /// far--much like in Lord of the Rings--to fetch the golden fork with /// which to eat the brownie. Only this fork can be used to eat this /// brownie, for any who try to eat it with a normal fork shall /// perish immediately and without honor. But guarding the fork are /// three large donuts, which must all be eaten in sixty seconds or /// less to continue on. It's called the donut challenge. But these /// are no ordinary donuts. To call them large is actually doing /// them a great injustice, for they are each the size of a small /// building. #[derive(Debug, Copy, Clone)] pub enum SimpleSurfaceShader { Emit { color: Color, }, Lambert { color: Color, }, GGX { color: Color, roughness: f32, fresnel: f32, }, } impl SurfaceShader for SimpleSurfaceShader { fn shade(&self, data: &SurfaceIntersectionData, time: f32) -> SurfaceClosure { let _ = (data, time); // Silence "unused" compiler warning match *self { SimpleSurfaceShader::Emit { color } => SurfaceClosure::Emit(color), SimpleSurfaceShader::Lambert { color } => SurfaceClosure::Lambert(color), SimpleSurfaceShader::GGX { color, roughness, fresnel, } => SurfaceClosure::GGX { color: color, roughness: roughness, fresnel: fresnel, }, } } } ================================================ FILE: src/shading/surface_closure.rs ================================================ #![allow(dead_code)] use std::f32::consts::PI as PI_32; use glam::Vec4; use crate::{ color::{Color, SpectralSample}, lerp::{lerp, Lerp}, math::{dot, zup_to_vec, Normal, Vector}, sampling::cosine_sample_hemisphere, }; const INV_PI: f32 = 1.0 / PI_32; const H_PI: f32 = PI_32 / 2.0; /// A surface closure, specifying a BSDF for a point on a surface. #[derive(Debug, Copy, Clone)] pub enum SurfaceClosure { // Normal surface closures. Lambert(Color), GGX { color: Color, roughness: f32, fresnel: f32, // [0.0, 1.0] determines how much fresnel reflection comes into play }, // Special closures that need special handling by the renderer. Emit(Color), } use self::SurfaceClosure::*; /// Note when implementing new BSDFs: both the the color filter and pdf returned from /// `sample()` and `evaluate()` should be identical for the same parameters and outgoing /// light direction. impl SurfaceClosure { /// Returns whether the closure has a delta distribution or not. pub fn is_delta(&self) -> bool { match *self { Lambert(_) => false, GGX { roughness, .. } => roughness == 0.0, Emit(_) => false, } } /// Given an incoming ray and sample values, generates an outgoing ray and /// color filter. /// /// inc: Incoming light direction. /// nor: The shading surface normal at the surface point. /// nor_g: The geometric surface normal at the surface point. /// uv: The sampling values. /// wavelength: Hero wavelength to generate the color filter for. /// /// Returns a tuple with the generated outgoing light direction, color filter, and pdf. pub fn sample( &self, inc: Vector, nor: Normal, nor_g: Normal, uv: (f32, f32), wavelength: f32, ) -> (Vector, SpectralSample, f32) { match *self { Lambert(color) => lambert_closure::sample(color, inc, nor, nor_g, uv, wavelength), GGX { color, roughness, fresnel, } => ggx_closure::sample(color, roughness, fresnel, inc, nor, nor_g, uv, wavelength), Emit(color) => emit_closure::sample(color, inc, nor, nor_g, uv, wavelength), } } /// Evaluates the closure for the given incoming and outgoing rays. /// /// inc: The incoming light direction. /// out: The outgoing light direction. /// nor: The shading surface normal at the surface point. /// nor_g: The geometric surface normal at the surface point. /// wavelength: Hero wavelength to generate the color filter for. /// /// Returns the resulting filter color and pdf of if this had been generated /// by `sample()`. pub fn evaluate( &self, inc: Vector, out: Vector, nor: Normal, nor_g: Normal, wavelength: f32, ) -> (SpectralSample, f32) { match *self { Lambert(color) => lambert_closure::evaluate(color, inc, out, nor, nor_g, wavelength), GGX { color, roughness, fresnel, } => ggx_closure::evaluate(color, roughness, fresnel, inc, out, nor, nor_g, wavelength), Emit(color) => emit_closure::evaluate(color, inc, out, nor, nor_g, wavelength), } } /// Returns an estimate of the sum total energy that evaluate() would return /// when integrated over a spherical light source with a center at relative /// position 'to_light_center' and squared radius 'light_radius_squared'. /// This is used for importance sampling, so does not need to be exact, /// but it does need to be non-zero anywhere that an exact solution would /// be non-zero. pub fn estimate_eval_over_sphere_light( &self, inc: Vector, to_light_center: Vector, light_radius_squared: f32, nor: Normal, nor_g: Normal, ) -> f32 { match *self { Lambert(color) => lambert_closure::estimate_eval_over_sphere_light( color, inc, to_light_center, light_radius_squared, nor, nor_g, ), GGX { color, roughness, fresnel, } => ggx_closure::estimate_eval_over_sphere_light( color, roughness, fresnel, inc, to_light_center, light_radius_squared, nor, nor_g, ), Emit(color) => emit_closure::estimate_eval_over_sphere_light( color, inc, to_light_center, light_radius_squared, nor, nor_g, ), } } /// Returns the post-compression size of this closure. pub fn compressed_size(&self) -> usize { 1 + match *self { Lambert(color) => color.compressed_size(), GGX { color, .. } => { 2 // Roughness + 2 // Fresnel + color.compressed_size() // Color } Emit(color) => color.compressed_size(), } } /// Writes the compressed form of this closure to `out_data`. /// /// `out_data` must be at least `compressed_size()` bytes long, otherwise /// this method will panic. /// /// Returns the number of bytes written. pub fn write_compressed(&self, out_data: &mut [u8]) -> usize { match *self { Lambert(color) => { out_data[0] = 0; // Discriminant color.write_compressed(&mut out_data[1..]); } GGX { color, roughness, fresnel, } => { out_data[0] = 1; // Discriminant // Roughness and fresnel (we write these first because they are // constant-size, whereas the color is variable-size, so this // makes things a little easier). let rgh = ((roughness.max(0.0).min(1.0) * std::u16::MAX as f32) as u16).to_le_bytes(); let frs = ((fresnel.max(0.0).min(1.0) * std::u16::MAX as f32) as u16).to_le_bytes(); out_data[1] = rgh[0]; out_data[2] = rgh[1]; out_data[3] = frs[0]; out_data[4] = frs[1]; // Color color.write_compressed(&mut out_data[5..]); // Color } Emit(color) => { out_data[0] = 2; // Discriminant color.write_compressed(&mut out_data[1..]); } } self.compressed_size() } /// Constructs a SurfaceClosure from compressed closure data, and also /// returns the number of bytes consumed from `in_data`. pub fn from_compressed(in_data: &[u8]) -> (SurfaceClosure, usize) { match in_data[0] { 0 => { // Lambert let (col, size) = Color::from_compressed(&in_data[1..]); (SurfaceClosure::Lambert(col), 1 + size) } 1 => { // GGX let mut rgh = [0u8; 2]; let mut frs = [0u8; 2]; rgh[0] = in_data[1]; rgh[1] = in_data[2]; frs[0] = in_data[3]; frs[1] = in_data[4]; let rgh = u16::from_le_bytes(rgh) as f32 * (1.0 / std::u16::MAX as f32); let frs = u16::from_le_bytes(frs) as f32 * (1.0 / std::u16::MAX as f32); let (col, size) = Color::from_compressed(&in_data[5..]); ( SurfaceClosure::GGX { color: col, roughness: rgh, fresnel: frs, }, 5 + size, ) } 2 => { // Emit let (col, size) = Color::from_compressed(&in_data[1..]); (SurfaceClosure::Emit(col), 1 + size) } _ => unreachable!(), } } } impl Lerp for SurfaceClosure { fn lerp(self, other: SurfaceClosure, alpha: f32) -> SurfaceClosure { match (self, other) { (Lambert(col1), Lambert(col2)) => Lambert(lerp(col1, col2, alpha)), ( GGX { color: col1, roughness: rgh1, fresnel: frs1, }, GGX { color: col2, roughness: rgh2, fresnel: frs2, }, ) => GGX { color: lerp(col1, col2, alpha), roughness: lerp(rgh1, rgh2, alpha), fresnel: lerp(frs1, frs2, alpha), }, (Emit(col1), Emit(col2)) => Emit(lerp(col1, col2, alpha)), _ => panic!("Cannot lerp between different surface closure types."), } } } /// Lambert closure code. mod lambert_closure { use super::*; pub fn sample( color: Color, inc: Vector, nor: Normal, nor_g: Normal, uv: (f32, f32), wavelength: f32, ) -> (Vector, SpectralSample, f32) { let (nn, flipped_nor_g) = if dot(nor_g.into_vector(), inc) <= 0.0 { (nor.normalized().into_vector(), nor_g.into_vector()) } else { (-nor.normalized().into_vector(), -nor_g.into_vector()) }; // Generate a random ray direction in the hemisphere // of the shading surface normal. let dir = cosine_sample_hemisphere(uv.0, uv.1); let pdf = dir.z() * INV_PI; let out = zup_to_vec(dir, nn); // Make sure it's not on the wrong side of the geometric normal. if dot(flipped_nor_g, out) >= 0.0 { (out, color.to_spectral_sample(wavelength) * pdf, pdf) } else { (out, SpectralSample::new(0.0), 0.0) } } pub fn evaluate( color: Color, inc: Vector, out: Vector, nor: Normal, nor_g: Normal, wavelength: f32, ) -> (SpectralSample, f32) { let (nn, flipped_nor_g) = if dot(nor_g.into_vector(), inc) <= 0.0 { (nor.normalized().into_vector(), nor_g.into_vector()) } else { (-nor.normalized().into_vector(), -nor_g.into_vector()) }; if dot(flipped_nor_g, out) >= 0.0 { let fac = dot(nn, out.normalized()).max(0.0) * INV_PI; (color.to_spectral_sample(wavelength) * fac, fac) } else { (SpectralSample::new(0.0), 0.0) } } pub fn estimate_eval_over_sphere_light( _color: Color, inc: Vector, to_light_center: Vector, light_radius_squared: f32, nor: Normal, nor_g: Normal, ) -> f32 { let _ = nor_g; // Not using this, silence warning // Analytically calculates lambert shading from a uniform light source // subtending a circular solid angle. // Only works for solid angle subtending equal to or less than a hemisphere. // // Formula taken from "Area Light Sources for Real-Time Graphics" // by John M. Snyder fn sphere_lambert(nlcos: f32, rcos: f32) -> f32 { assert!(nlcos >= -1.0 && nlcos <= 1.0); assert!(rcos >= 0.0 && rcos <= 1.0); let nlsin: f32 = (1.0 - (nlcos * nlcos)).sqrt(); let rsin2: f32 = 1.0 - (rcos * rcos); let rsin: f32 = rsin2.sqrt(); let ysin: f32 = rcos / nlsin; let ycos2: f32 = 1.0 - (ysin * ysin); let ycos: f32 = ycos2.sqrt(); let g: f32 = (-2.0 * nlsin * rcos * ycos) + H_PI - ysin.asin() + (ysin * ycos); let h: f32 = nlcos * ((ycos * (rsin2 - ycos2).sqrt()) + (rsin2 * (ycos / rsin).asin())); let nl: f32 = nlcos.acos(); let r: f32 = rcos.acos(); if nl < (H_PI - r) { nlcos * rsin2 } else if nl < H_PI { (nlcos * rsin2) + g - h } else if nl < (H_PI + r) { (g + h) * INV_PI } else { 0.0 } } let dist2 = to_light_center.length2(); if dist2 <= light_radius_squared { return (light_radius_squared / dist2).min(4.0); } else { let sin_theta_max2 = (light_radius_squared / dist2).min(1.0); let cos_theta_max = (1.0 - sin_theta_max2).sqrt(); let v = to_light_center.normalized(); let nn = if dot(nor_g.into_vector(), inc) <= 0.0 { nor.normalized() } else { -nor.normalized() } .into_vector(); let cos_nv = dot(nn, v).max(-1.0).min(1.0); // Alt implementation from the SPI paper. // Worse sampling, but here for reference. // { // let nl_ang = cos_nv.acos(); // let rad_ang = cos_theta_max.acos(); // let min_ang = (nl_ang - rad_ang).max(0.0); // let lamb = min_ang.cos().max(0.0); // return lamb / dist2; // } return sphere_lambert(cos_nv, cos_theta_max); } } } mod ggx_closure { use super::*; // Makes sure values are in a valid range pub fn validate(roughness: f32, fresnel: f32) { debug_assert!(fresnel >= 0.0 && fresnel <= 1.0); debug_assert!(roughness >= 0.0 && roughness <= 1.0); } pub fn sample( col: Color, roughness: f32, fresnel: f32, inc: Vector, nor: Normal, nor_g: Normal, uv: (f32, f32), wavelength: f32, ) -> (Vector, SpectralSample, f32) { // Get normalized surface normal let (nn, flipped_nor_g) = if dot(nor_g.into_vector(), inc) <= 0.0 { (nor.normalized().into_vector(), nor_g.into_vector()) } else { (-nor.normalized().into_vector(), -nor_g.into_vector()) }; // Generate a random ray direction in the hemisphere // of the surface. let theta_cos = half_theta_sample(uv.0, roughness); let theta_sin = (1.0 - (theta_cos * theta_cos)).sqrt(); let angle = uv.1 * PI_32 * 2.0; let mut half_dir = Vector::new(angle.cos() * theta_sin, angle.sin() * theta_sin, theta_cos); half_dir = zup_to_vec(half_dir, nn).normalized(); let out = inc - (half_dir * 2.0 * dot(inc, half_dir)); // Make sure it's not on the wrong side of the geometric normal. if dot(flipped_nor_g, out) >= 0.0 { let (filter, pdf) = evaluate(col, roughness, fresnel, inc, out, nor, nor_g, wavelength); (out, filter, pdf) } else { (out, SpectralSample::new(0.0), 0.0) } } pub fn evaluate( col: Color, roughness: f32, fresnel: f32, inc: Vector, out: Vector, nor: Normal, nor_g: Normal, wavelength: f32, ) -> (SpectralSample, f32) { // Calculate needed vectors, normalized let aa = -inc.normalized(); // Vector pointing to where "in" came from let bb = out.normalized(); // Out let hh = (aa + bb).normalized(); // Half-way between aa and bb // Surface normal let (nn, flipped_nor_g) = if dot(nor_g.into_vector(), inc) <= 0.0 { (nor.normalized().into_vector(), nor_g.into_vector()) } else { (-nor.normalized().into_vector(), -nor_g.into_vector()) }; // Make sure everything's on the correct side of the surface if dot(nn, aa) < 0.0 || dot(nn, bb) < 0.0 || dot(flipped_nor_g, bb) < 0.0 { return (SpectralSample::new(0.0), 0.0); } // Calculate needed dot products let na = dot(nn, aa).clamp(-1.0, 1.0); let nb = dot(nn, bb).clamp(-1.0, 1.0); let ha = dot(hh, aa).clamp(-1.0, 1.0); let hb = dot(hh, bb).clamp(-1.0, 1.0); let nh = dot(nn, hh).clamp(-1.0, 1.0); // Calculate F - Fresnel let col_f = { let spectrum_sample = col.to_spectral_sample(wavelength); let rev_fresnel = 1.0 - fresnel; let c0 = lerp( schlick_fresnel_from_fac(spectrum_sample.e[0], hb), spectrum_sample.e[0], rev_fresnel, ); let c1 = lerp( schlick_fresnel_from_fac(spectrum_sample.e[1], hb), spectrum_sample.e[1], rev_fresnel, ); let c2 = lerp( schlick_fresnel_from_fac(spectrum_sample.e[2], hb), spectrum_sample.e[2], rev_fresnel, ); let c3 = lerp( schlick_fresnel_from_fac(spectrum_sample.e[3], hb), spectrum_sample.e[3], rev_fresnel, ); SpectralSample::from_parts(Vec4::new(c0, c1, c2, c3), wavelength) }; // Calculate everything else if roughness == 0.0 { // If sharp mirror, just return col * fresnel factor return (col_f, 0.0); } else { // Calculate D - Distribution let dist = ggx_d(nh, roughness) / na; // Calculate G1 and G2- Geometric microfacet shadowing let g1 = ggx_g(ha, na, roughness); let g2 = ggx_g(hb, nb, roughness); // Final result (col_f * (dist * g1 * g2) * INV_PI, dist * INV_PI) } } pub fn estimate_eval_over_sphere_light( _col: Color, roughness: f32, _fresnel: f32, inc: Vector, to_light_center: Vector, light_radius_squared: f32, nor: Normal, nor_g: Normal, ) -> f32 { // TODO: all of the stuff in this function is horribly hacky. // Find a proper way to approximate the light contribution from a // solid angle. let _ = nor_g; // Not using this, silence warning let dist2 = to_light_center.length2(); let sin_theta_max2 = (light_radius_squared / dist2).min(1.0); let cos_theta_max = (1.0 - sin_theta_max2).sqrt(); assert!(cos_theta_max >= -1.0); assert!(cos_theta_max <= 1.0); // Surface normal let nn = if dot(nor.into_vector(), inc) < 0.0 { nor.normalized() } else { -nor.normalized() // If back-facing, flip normal } .into_vector(); let aa = -inc.normalized(); // Vector pointing to where "in" came from let bb = to_light_center.normalized(); // Out // Brute-force method //let mut fac = 0.0; //const N: usize = 256; //for i in 0..N { // let uu = Halton::sample(0, i); // let vv = Halton::sample(1, i); // let mut samp = uniform_sample_cone(uu, vv, cos_theta_max); // samp = zup_to_vec(samp, bb).normalized(); // if dot(nn, samp) > 0.0 { // let hh = (aa+samp).normalized(); // fac += ggx_d(dot(nn, hh), roughness); // } //} //fac /= N * N; // Approximate method let theta = cos_theta_max.acos(); let hh = (aa + bb).normalized(); let nh = dot(nn, hh).clamp(-1.0, 1.0); let fac = ggx_d(nh, (1.0f32).min(roughness.sqrt() + (2.0 * theta / PI_32))); fac * (1.0f32).min(1.0 - cos_theta_max) * INV_PI } //---------------------------------------------------- // Returns the cosine of the half-angle that should be sampled, given // a random variable in [0,1] fn half_theta_sample(u: f32, rough: f32) -> f32 { let rough2 = rough * rough; // Calculate top half of equation let top = 1.0 - u; // Calculate bottom half of equation let bottom = 1.0 + ((rough2 - 1.0) * u); (top / bottom).sqrt() } /// The GGX microfacet distribution function. /// /// nh: cosine of the angle between the surface normal and the microfacet normal. fn ggx_d(nh: f32, rough: f32) -> f32 { if nh <= 0.0 { return 0.0; } let rough2 = rough * rough; let tmp = 1.0 + ((rough2 - 1.0) * (nh * nh)); rough2 / (PI_32 * tmp * tmp) } /// The GGX Smith shadow-masking function. /// /// vh: cosine of the angle between the view vector and the microfacet normal. /// vn: cosine of the angle between the view vector and surface normal. fn ggx_g(vh: f32, vn: f32, rough: f32) -> f32 { if (vh * vn) <= 0.0 { 0.0 } else { 2.0 / (1.0 + (1.0 + rough * rough * (1.0 - vn * vn) / (vn * vn)).sqrt()) } } } /// Emit closure code. /// /// NOTE: this needs to be handled specially by the integrator! It does not /// behave like a standard closure! mod emit_closure { use super::*; pub fn sample( color: Color, inc: Vector, nor: Normal, nor_g: Normal, uv: (f32, f32), wavelength: f32, ) -> (Vector, SpectralSample, f32) { let _ = (inc, nor, nor_g, uv); // Not using these, silence warning ( Vector::new(0.0, 0.0, 0.0), color.to_spectral_sample(wavelength), 1.0, ) } pub fn evaluate( color: Color, inc: Vector, out: Vector, nor: Normal, nor_g: Normal, wavelength: f32, ) -> (SpectralSample, f32) { let _ = (inc, out, nor, nor_g); // Not using these, silence warning (color.to_spectral_sample(wavelength), 1.0) } pub fn estimate_eval_over_sphere_light( _color: Color, _inc: Vector, _to_light_center: Vector, _light_radius_squared: f32, _nor: Normal, _nor_g: Normal, ) -> f32 { // TODO: what to do here? unimplemented!() } } //============================================================================= /// Utility function that calculates the fresnel reflection factor of a given /// incoming ray against a surface with the given normal-reflectance factor. /// /// `frensel_fac`: The ratio of light reflected back if the ray were to /// hit the surface head-on (perpendicular to the surface). /// `c`: The cosine of the angle between the incoming light and the /// surface's normal. Probably calculated e.g. with a normalized /// dot product. #[allow(dead_code)] fn dielectric_fresnel_from_fac(fresnel_fac: f32, c: f32) -> f32 { let tmp1 = fresnel_fac.sqrt() - 1.0; // Protect against divide by zero. if tmp1.abs() < 0.000_001 { return 1.0; } // Find the ior ratio let tmp2 = (-2.0 / tmp1) - 1.0; let ior_ratio = tmp2 * tmp2; // Calculate fresnel factor dielectric_fresnel(ior_ratio, c) } /// Schlick's approximation version of `dielectric_fresnel_from_fac()` above. #[allow(dead_code)] fn schlick_fresnel_from_fac(frensel_fac: f32, c: f32) -> f32 { let c1 = 1.0 - c; let c2 = c1 * c1; frensel_fac + ((1.0 - frensel_fac) * c1 * c2 * c2) } /// Utility function that calculates the fresnel reflection factor of a given /// incoming ray against a surface with the given ior outside/inside ratio. /// /// `ior_ratio`: The ratio of the outside material ior (probably 1.0 for air) /// over the inside ior. /// `c`: The cosine of the angle between the incoming light and the /// surface's normal. Probably calculated e.g. with a normalized /// dot product. #[allow(dead_code)] fn dielectric_fresnel(ior_ratio: f32, c: f32) -> f32 { let g = (ior_ratio - 1.0 + (c * c)).sqrt(); let f1 = g - c; let f2 = g + c; let f3 = (f1 * f1) / (f2 * f2); let f4 = (c * f2) - 1.0; let f5 = (c * f1) + 1.0; let f6 = 1.0 + ((f4 * f4) / (f5 * f5)); 0.5 * f3 * f6 } /// Schlick's approximation of the fresnel reflection factor. /// /// Same interface as `dielectric_fresnel()`, above. #[allow(dead_code)] fn schlick_fresnel(ior_ratio: f32, c: f32) -> f32 { let f1 = (1.0 - ior_ratio) / (1.0 + ior_ratio); let f2 = f1 * f1; let c1 = 1.0 - c; let c2 = c1 * c1; f2 + ((1.0 - f2) * c1 * c2 * c2) } ================================================ FILE: src/surface/bilinear_patch.rs ================================================ use super::{point_order, PointOrder, Splitable, MAX_EDGE_DICE}; use crate::{ lerp::{lerp, lerp_slice}, math::Point, }; #[derive(Debug, Copy, Clone)] pub struct BilinearPatch<'a> { // The control points are stored in clockwise order, like this: // u -----> // v 0 1 // | 3 2 // \/ control_points: &'a [[Point; 4]], // Indicates if any of the edges *must* be split, for example if there // are adjacent patches that were split for non-dicing reasons. // // Matching the ascii graph above, the edges are: // // 0 // ------- // 3 | | 1 // ------- // 2 must_split: [bool; 4], } fn bilerp_point(patch: [Point; 4], uv: (f32, f32)) -> Point { let a = lerp(patch[0], patch[1], uv.0); let b = lerp(patch[3], patch[2], uv.0); lerp(a, b, uv.1) } #[derive(Debug, Copy, Clone)] pub struct BilinearSubPatch<'a> { original: &'a BilinearPatch<'a>, clip: [(f32, f32); 4], must_split: [bool; 4], } impl<'a> Splitable for BilinearSubPatch<'a> { fn split(&self, metric: F) -> Option<(Self, Self)> where F: Fn(Point, Point) -> f32, { // Get the points of the sub-patch at time 0.5. let patch = lerp_slice(self.original.control_points, 0.5); let points = [ bilerp_point(patch, self.clip[0]), bilerp_point(patch, self.clip[1]), bilerp_point(patch, self.clip[2]), bilerp_point(patch, self.clip[3]), ]; // Calculate edge metrics. let edge_metric = [ metric(points[0], points[1]), metric(points[1], points[2]), metric(points[2], points[3]), metric(points[3], points[0]), ]; // Find an edge to split, if any. let split_edge_index = { // Find the "longest" edge in terms of the metric. let (edge_i, m) = edge_metric .iter() .enumerate() .max_by(|a, b| { if a.1 > b.1 { std::cmp::Ordering::Greater } else { std::cmp::Ordering::Less } }) .unwrap(); // Return an edge to split, if a split is needed. if *m > MAX_EDGE_DICE as f32 { // Split needed because of over-long edge. Some(edge_i) } else { // Return the the first edge with "must_split" set, if any. // Otherwise returns `None`. self.must_split .iter() .enumerate() .find(|a| *a.1) .map(|a| a.0) } }; // Do the split if needed if let Some(i) = split_edge_index { let edge_1 = (i, (i + 1) % 4); let edge_2 = ((i + 2) % 4, (i + 3) % 4); let new_must_split = { let mut new_must_split = self.must_split; new_must_split[edge_1.0] = false; new_must_split[edge_2.0] = false; new_must_split }; let midpoint_1 = lerp(self.clip[edge_1.0], self.clip[edge_1.1], 0.5); let midpoint_2 = { let alpha = if self.must_split[edge_2.0] || edge_metric[edge_2.0] > MAX_EDGE_DICE as f32 { 0.5 } else { let edge_2_dice_rate = edge_metric[edge_2.0].ceil(); (edge_2_dice_rate * 0.5).floor() / edge_2_dice_rate }; match point_order(points[edge_2.0], points[edge_2.1]) { PointOrder::AsIs => lerp(self.clip[edge_2.0], self.clip[edge_2.1], alpha), PointOrder::Flip => lerp(self.clip[edge_2.1], self.clip[edge_2.0], alpha), } }; // Build the new sub-patches let mut patch_1 = BilinearSubPatch { original: self.original, clip: self.clip, must_split: new_must_split, }; let mut patch_2 = patch_1; patch_1.clip[edge_1.1] = midpoint_1; patch_1.clip[edge_2.0] = midpoint_2; patch_2.clip[edge_1.0] = midpoint_1; patch_2.clip[edge_2.1] = midpoint_2; Some((patch_1, patch_2)) } else { // No split None } } } ================================================ FILE: src/surface/micropoly_batch.rs ================================================ #![allow(dead_code)] use std::collections::HashMap; use kioku::Arena; use crate::{ accel::BVH4, bbox::BBox, boundable::Boundable, lerp::lerp_slice, math::{cross, dot, Normal, Point, Transform}, ray::{RayBatch, RayStack}, shading::SurfaceClosure, }; use super::{triangle, SurfaceIntersection, SurfaceIntersectionData}; const MAX_LEAF_TRIANGLE_COUNT: usize = 3; /// This is the core surface primitive for rendering: all surfaces are /// ultimately processed into pre-shaded micropolygon batches for rendering. /// /// It is essentially a triangle soup that shares the same surface shader. /// The parameters of that shader can vary over the triangles, but its /// structure cannot. #[derive(Copy, Clone, Debug)] pub struct MicropolyBatch<'a> { // Vertices and associated normals. Time samples for the same vertex are // laid out next to each other in a contiguous slice. time_sample_count: usize, vertices: &'a [Point], normals: &'a [Normal], // Per-vertex shading data. compressed_vertex_closure_size: usize, // Size in bites of a single compressed closure vertex_closure_time_sample_count: usize, compressed_vertex_closures: &'a [u8], // Packed compressed closures // Micro-triangle indices. Each element of the tuple specifies the index // of a vertex, which indexes into all of the arrays above. indices: &'a [(u32, u32, u32)], // Acceleration structure for fast ray intersection testing. accel: BVH4<'a>, } impl<'a> MicropolyBatch<'a> { pub fn from_verts_and_indices<'b>( arena: &'b Arena, verts: &[Vec], vert_normals: &[Vec], tri_indices: &[(usize, usize, usize)], ) -> MicropolyBatch<'b> { let vert_count = verts[0].len(); let time_sample_count = verts.len(); // Copy verts over to a contiguous area of memory, reorganizing them // so that each vertices' time samples are contiguous in memory. let vertices = { let vertices = arena.alloc_array_uninit(vert_count * time_sample_count); for vi in 0..vert_count { for ti in 0..time_sample_count { unsafe { *vertices[(vi * time_sample_count) + ti].as_mut_ptr() = verts[ti][vi]; } } } unsafe { std::mem::transmute(vertices) } }; // Copy vertex normals, if any, organizing them the same as vertices // above. let normals = { let normals = arena.alloc_array_uninit(vert_count * time_sample_count); for vi in 0..vert_count { for ti in 0..time_sample_count { unsafe { *normals[(vi * time_sample_count) + ti].as_mut_ptr() = vert_normals[ti][vi]; } } } unsafe { std::mem::transmute(&normals[..]) } }; // Copy triangle vertex indices over, appending the triangle index itself to the tuple let indices: &mut [(u32, u32, u32)] = { let indices = arena.alloc_array_uninit(tri_indices.len()); for (i, tri_i) in tri_indices.iter().enumerate() { unsafe { *indices[i].as_mut_ptr() = (tri_i.0 as u32, tri_i.2 as u32, tri_i.1 as u32); } } unsafe { std::mem::transmute(indices) } }; // Create bounds array for use during BVH construction let (bounds, bounds_map) = { let mut bounds = Vec::with_capacity(indices.len() * time_sample_count); let mut bounds_map = HashMap::new(); for tri in tri_indices { let start = bounds.len(); for ti in 0..time_sample_count { let p0 = verts[ti][tri.0]; let p1 = verts[ti][tri.1]; let p2 = verts[ti][tri.2]; let minimum = p0.min(p1.min(p2)); let maximum = p0.max(p1.max(p2)); bounds.push(BBox::from_points(minimum, maximum)); } let end = bounds.len(); bounds_map.insert((tri.0 as u32, tri.1 as u32, tri.2 as u32), (start, end)); } (bounds, bounds_map) }; // Build BVH let accel = BVH4::from_objects(arena, &mut indices[..], MAX_LEAF_TRIANGLE_COUNT, |tri| { let (start, end) = bounds_map[tri]; &bounds[start..end] }); MicropolyBatch { time_sample_count: time_sample_count, vertices: vertices, normals: normals, compressed_vertex_closure_size: 0, vertex_closure_time_sample_count: 1, compressed_vertex_closures: &[], indices: indices, accel: accel, } } } impl<'a> Boundable for MicropolyBatch<'a> { fn bounds(&self) -> &[BBox] { self.accel.bounds() } } impl<'a> MicropolyBatch<'a> { fn intersect_rays( &self, rays: &mut RayBatch, ray_stack: &mut RayStack, isects: &mut [SurfaceIntersection], space: &[Transform], ) { // Precalculate transform for non-motion blur cases let static_mat_space = if space.len() == 1 { lerp_slice(space, 0.0).inverse() } else { Transform::new() }; self.accel .traverse(rays, ray_stack, |idx_range, rays, ray_stack| { let tri_count = idx_range.end - idx_range.start; // Build the triangle cache if we can! let is_cached = ray_stack.ray_count_in_next_task() >= tri_count && self.time_sample_count == 1 && space.len() <= 1; let mut tri_cache = [std::mem::MaybeUninit::uninit(); MAX_LEAF_TRIANGLE_COUNT]; if is_cached { for tri_idx in idx_range.clone() { let i = tri_idx - idx_range.start; let tri_indices = self.indices[tri_idx]; // For static triangles with static transforms, cache them. unsafe { *tri_cache[i].as_mut_ptr() = ( self.vertices[tri_indices.0 as usize], self.vertices[tri_indices.1 as usize], self.vertices[tri_indices.2 as usize], ); if !space.is_empty() { (*tri_cache[i].as_mut_ptr()).0 = (*tri_cache[i].as_mut_ptr()).0 * static_mat_space; (*tri_cache[i].as_mut_ptr()).1 = (*tri_cache[i].as_mut_ptr()).1 * static_mat_space; (*tri_cache[i].as_mut_ptr()).2 = (*tri_cache[i].as_mut_ptr()).2 * static_mat_space; } } } } // Test each ray against the triangles. ray_stack.do_next_task(|ray_idx| { let ray_idx = ray_idx as usize; if rays.is_done(ray_idx) { return; } let ray_time = rays.time(ray_idx); // Calculate the ray space, if necessary. let mat_space = if space.len() > 1 { // Per-ray transform, for motion blur lerp_slice(space, ray_time).inverse() } else { static_mat_space }; // Iterate through the triangles and test the ray against them. let mut non_shadow_hit = false; let mut hit_tri = std::mem::MaybeUninit::uninit(); let mut hit_tri_indices = std::mem::MaybeUninit::uninit(); let mut hit_tri_data = std::mem::MaybeUninit::uninit(); let ray_pre = triangle::RayTriPrecompute::new(rays.dir(ray_idx)); for tri_idx in idx_range.clone() { let tri_indices = self.indices[tri_idx]; // Get triangle if necessary let tri = if is_cached { let i = tri_idx - idx_range.start; unsafe { tri_cache[i].assume_init() } } else { let mut tri = if self.time_sample_count == 1 { // No deformation motion blur, so fast-path it. ( self.vertices[tri_indices.0 as usize], self.vertices[tri_indices.1 as usize], self.vertices[tri_indices.2 as usize], ) } else { // Deformation motion blur, need to interpolate. let p0_slice = &self.vertices[(tri_indices.0 as usize * self.time_sample_count) ..((tri_indices.0 as usize + 1) * self.time_sample_count)]; let p1_slice = &self.vertices[(tri_indices.1 as usize * self.time_sample_count) ..((tri_indices.1 as usize + 1) * self.time_sample_count)]; let p2_slice = &self.vertices[(tri_indices.2 as usize * self.time_sample_count) ..((tri_indices.2 as usize + 1) * self.time_sample_count)]; let p0 = lerp_slice(p0_slice, ray_time); let p1 = lerp_slice(p1_slice, ray_time); let p2 = lerp_slice(p2_slice, ray_time); (p0, p1, p2) }; if !space.is_empty() { tri.0 = tri.0 * mat_space; tri.1 = tri.1 * mat_space; tri.2 = tri.2 * mat_space; } tri }; // Test ray against triangle if let Some((t, b0, b1, b2)) = triangle::intersect_ray( rays.orig(ray_idx), ray_pre, rays.max_t(ray_idx), tri, ) { if rays.is_occlusion(ray_idx) { isects[ray_idx] = SurfaceIntersection::Occlude; rays.mark_done(ray_idx); break; } else { non_shadow_hit = true; rays.set_max_t(ray_idx, t); unsafe { *hit_tri.as_mut_ptr() = tri; *hit_tri_indices.as_mut_ptr() = tri_indices; *hit_tri_data.as_mut_ptr() = (t, b0, b1, b2); } } } } // Calculate intersection data if necessary. if non_shadow_hit { let hit_tri = unsafe { hit_tri.assume_init() }; let hit_tri_indices = unsafe { hit_tri_indices.assume_init() }; let (t, b0, b1, b2) = unsafe { hit_tri_data.assume_init() }; // Calculate intersection point and error magnitudes let (pos, pos_err) = triangle::surface_point(hit_tri, (b0, b1, b2)); // Calculate geometric surface normal let geo_normal = cross(hit_tri.0 - hit_tri.1, hit_tri.0 - hit_tri.2).into_normal(); // Calculate interpolated surface normal let shading_normal = { let n0_slice = &self.normals[(hit_tri_indices.0 as usize * self.time_sample_count) ..((hit_tri_indices.0 as usize + 1) * self.time_sample_count)]; let n1_slice = &self.normals[(hit_tri_indices.1 as usize * self.time_sample_count) ..((hit_tri_indices.1 as usize + 1) * self.time_sample_count)]; let n2_slice = &self.normals[(hit_tri_indices.2 as usize * self.time_sample_count) ..((hit_tri_indices.2 as usize + 1) * self.time_sample_count)]; let n0 = lerp_slice(n0_slice, ray_time).normalized(); let n1 = lerp_slice(n1_slice, ray_time).normalized(); let n2 = lerp_slice(n2_slice, ray_time).normalized(); let s_nor = ((n0 * b0) + (n1 * b1) + (n2 * b2)) * mat_space; if dot(s_nor, geo_normal) >= 0.0 { s_nor } else { -s_nor } }; // Calculate interpolated surface closure. // TODO: actually interpolate. let closure = { let start_byte = hit_tri_indices.0 as usize * self.compressed_vertex_closure_size * self.vertex_closure_time_sample_count; let end_byte = start_byte + self.compressed_vertex_closure_size; let (closure, _) = SurfaceClosure::from_compressed( &self.compressed_vertex_closures[start_byte..end_byte], ); closure }; let intersection_data = SurfaceIntersectionData { incoming: rays.dir(ray_idx), t: t, pos: pos, pos_err: pos_err, nor: shading_normal, nor_g: geo_normal, local_space: mat_space, sample_pdf: 0.0, }; // Fill in intersection data isects[ray_idx] = SurfaceIntersection::Hit { intersection_data: intersection_data, closure: closure, }; } }); ray_stack.pop_task(); }); } } ================================================ FILE: src/surface/mod.rs ================================================ #![allow(dead_code)] // pub mod micropoly_batch; pub mod bilinear_patch; pub mod micropoly_batch; pub mod triangle; pub mod triangle_mesh; use std::fmt::Debug; use crate::{ boundable::Boundable, math::{Normal, Point, Transform, Vector}, ray::{RayBatch, RayStack}, shading::surface_closure::SurfaceClosure, shading::SurfaceShader, }; const MAX_EDGE_DICE: u32 = 128; pub trait Surface: Boundable + Debug + Sync { fn intersect_rays( &self, rays: &mut RayBatch, ray_stack: &mut RayStack, isects: &mut [SurfaceIntersection], shader: &dyn SurfaceShader, space: &[Transform], ); } pub trait Splitable: Copy { /// Splits the surface into two pieces if necessary. fn split(&self, metric: F) -> Option<(Self, Self)> where F: Fn(Point, Point) -> f32; } #[derive(Debug, Copy, Clone)] pub enum PointOrder { AsIs, Flip, } pub fn point_order(p1: Point, p2: Point) -> PointOrder { let max_diff = { let v = p2 - p1; let v_abs = v.abs(); let mut diff = v.x(); let mut diff_abs = v_abs.x(); if v_abs.y() > diff_abs { diff = v.y(); diff_abs = v_abs.y(); } if v_abs.z() > diff_abs { diff = v.z(); } diff }; if max_diff <= 0.0 { PointOrder::AsIs } else { PointOrder::Flip } } #[derive(Debug, Copy, Clone)] #[allow(clippy::large_enum_variant)] pub enum SurfaceIntersection { Miss, Occlude, Hit { intersection_data: SurfaceIntersectionData, closure: SurfaceClosure, }, } #[derive(Debug, Copy, Clone)] pub struct SurfaceIntersectionData { pub incoming: Vector, // Direction of the incoming ray pub pos: Point, // Position of the intersection pub pos_err: f32, // Error magnitude of the intersection position. Imagine // a cube centered around `pos` with dimensions of `2 * pos_err`. pub nor: Normal, // Shading normal pub nor_g: Normal, // True geometric normal pub local_space: Transform, // Matrix from global space to local space pub t: f32, // Ray t-value at the intersection point pub sample_pdf: f32, // The PDF of getting this point by explicitly sampling the surface } ================================================ FILE: src/surface/triangle.rs ================================================ #![allow(dead_code)] use crate::{ fp_utils::fp_gamma, math::{Point, Vector}, }; #[derive(Debug, Copy, Clone)] pub struct RayTriPrecompute { i: (usize, usize, usize), s: (f32, f32, f32), } impl RayTriPrecompute { pub fn new(ray_dir: Vector) -> RayTriPrecompute { // Calculate the permuted dimension indices for the new ray space. let (xi, yi, zi) = { let xabs = ray_dir.x().abs(); let yabs = ray_dir.y().abs(); let zabs = ray_dir.z().abs(); if xabs > yabs && xabs > zabs { (1, 2, 0) } else if yabs > zabs { (2, 0, 1) } else { (0, 1, 2) } }; let dir_x = ray_dir.get_n(xi); let dir_y = ray_dir.get_n(yi); let dir_z = ray_dir.get_n(zi); // Calculate shear constants. let sx = dir_x / dir_z; let sy = dir_y / dir_z; let sz = 1.0 / dir_z; RayTriPrecompute { i: (xi, yi, zi), s: (sx, sy, sz), } } } /// Intersects `ray` with `tri`, returning `Some((t, b0, b1, b2))`, or `None` /// if no intersection. /// /// Returned values: /// /// * `t` is the ray t at the hit point. /// * `b0`, `b1`, and `b2` are the barycentric coordinates of the triangle at /// the hit point. /// /// Uses the ray-triangle test from the paper "Watertight Ray/Triangle /// Intersection" by Woop et al. pub fn intersect_ray( ray_orig: Point, ray_pre: RayTriPrecompute, ray_max_t: f32, tri: (Point, Point, Point), ) -> Option<(f32, f32, f32, f32)> { // Calculate vertices in ray space. let p0 = tri.0 - ray_orig; let p1 = tri.1 - ray_orig; let p2 = tri.2 - ray_orig; let p0x = p0.get_n(ray_pre.i.0) - (ray_pre.s.0 * p0.get_n(ray_pre.i.2)); let p0y = p0.get_n(ray_pre.i.1) - (ray_pre.s.1 * p0.get_n(ray_pre.i.2)); let p1x = p1.get_n(ray_pre.i.0) - (ray_pre.s.0 * p1.get_n(ray_pre.i.2)); let p1y = p1.get_n(ray_pre.i.1) - (ray_pre.s.1 * p1.get_n(ray_pre.i.2)); let p2x = p2.get_n(ray_pre.i.0) - (ray_pre.s.0 * p2.get_n(ray_pre.i.2)); let p2y = p2.get_n(ray_pre.i.1) - (ray_pre.s.1 * p2.get_n(ray_pre.i.2)); // Calculate scaled barycentric coordinates. let mut e0 = (p1x * p2y) - (p1y * p2x); let mut e1 = (p2x * p0y) - (p2y * p0x); let mut e2 = (p0x * p1y) - (p0y * p1x); // Fallback to test against edges using double precision. if e0 == 0.0 || e1 == 0.0 || e2 == 0.0 { e0 = ((p1x as f64 * p2y as f64) - (p1y as f64 * p2x as f64)) as f32; e1 = ((p2x as f64 * p0y as f64) - (p2y as f64 * p0x as f64)) as f32; e2 = ((p0x as f64 * p1y as f64) - (p0y as f64 * p1x as f64)) as f32; } // Check if the ray hit the triangle. if (e0 < 0.0 || e1 < 0.0 || e2 < 0.0) && (e0 > 0.0 || e1 > 0.0 || e2 > 0.0) { return None; } // Determinant let det = e0 + e1 + e2; if det == 0.0 { return None; } // Calculate t of hitpoint. let p0z = ray_pre.s.2 * p0.get_n(ray_pre.i.2); let p1z = ray_pre.s.2 * p1.get_n(ray_pre.i.2); let p2z = ray_pre.s.2 * p2.get_n(ray_pre.i.2); let t_scaled = (e0 * p0z) + (e1 * p1z) + (e2 * p2z); // Check if the hitpoint t is within ray min/max t. if (det > 0.0 && (t_scaled <= 0.0 || t_scaled > (ray_max_t * det))) || (det < 0.0 && (t_scaled >= 0.0 || t_scaled < (ray_max_t * det))) { return None; } // Calculate t and the hitpoint barycentric coordinates. let inv_det = 1.0 / det; let b0 = e0 * inv_det; let b1 = e1 * inv_det; let b2 = e2 * inv_det; let t = t_scaled * inv_det; // Check error bounds on t for very close hit points. // The technique used here is from "Physically Based Rendering: From Theory // to Implementation" third edition by Pharr et al. { // Calculate delta z let max_zt = max_abs_3(p0z, p1z, p2z); let dz = fp_gamma(3) * max_zt; // Calculate delta x and y let max_xt = max_abs_3(p0x, p1x, p2x); let max_yt = max_abs_3(p0y, p1y, p2y); let dx = fp_gamma(5) * (max_xt + max_zt); let dy = fp_gamma(5) * (max_yt + max_zt); // Calculate delta e let de = 2.0 * ((fp_gamma(2) * max_xt * max_yt) + (dy * max_xt + dx * max_yt)); // Calculate delta t let max_e = max_abs_3(e0, e1, e2); let dt = 3.0 * ((fp_gamma(3) * max_e * max_zt) + (de * max_zt + dz * max_e)) * inv_det.abs(); // Finally, do the check if t <= dt { return None; } } // Return t and barycentric coordinates Some((t, b0, b1, b2)) } /// Calculates a point on a triangle's surface at the given barycentric /// coordinates. /// /// Returns the point and the error magnitude of the point. pub fn surface_point(tri: (Point, Point, Point), bary: (f32, f32, f32)) -> (Point, f32) { let pos = ((tri.0.into_vector() * bary.0) + (tri.1.into_vector() * bary.1) + (tri.2.into_vector() * bary.2)) .into_point(); let pos_err = (((tri.0.into_vector().abs() * bary.0) + (tri.1.into_vector().abs() * bary.1) + (tri.2.into_vector().abs() * bary.2)) * fp_gamma(7)) .co .max_element(); (pos, pos_err) } fn max_abs_3(a: f32, b: f32, c: f32) -> f32 { let a = a.abs(); let b = b.abs(); let c = c.abs(); if a > b && a > c { a } else if b > c { b } else { c } } ================================================ FILE: src/surface/triangle_mesh.rs ================================================ #![allow(dead_code)] use kioku::Arena; use crate::{ accel::BVH4, bbox::BBox, boundable::Boundable, lerp::lerp_slice, math::{cross, dot, Normal, Point, Transform}, ray::{RayBatch, RayStack}, shading::SurfaceShader, }; use super::{triangle, Surface, SurfaceIntersection, SurfaceIntersectionData}; const MAX_LEAF_TRIANGLE_COUNT: usize = 3; #[derive(Copy, Clone, Debug)] pub struct TriangleMesh<'a> { time_sample_count: usize, vertices: &'a [Point], // Vertices, with the time samples for each vertex stored contiguously normals: Option<&'a [Normal]>, // Vertex normals, organized the same as `vertices` indices: &'a [(u32, u32, u32, u32)], // (v0_idx, v1_idx, v2_idx, original_tri_idx) accel: BVH4<'a>, } impl<'a> TriangleMesh<'a> { pub fn from_verts_and_indices<'b>( arena: &'b Arena, verts: &[Vec], vert_normals: &Option>>, tri_indices: &[(usize, usize, usize)], ) -> TriangleMesh<'b> { let vert_count = verts[0].len(); let time_sample_count = verts.len(); // Copy verts over to a contiguous area of memory, reorganizing them // so that each vertices' time samples are contiguous in memory. let vertices = { let vertices = arena.alloc_array_uninit(vert_count * time_sample_count); for vi in 0..vert_count { for ti in 0..time_sample_count { unsafe { *vertices[(vi * time_sample_count) + ti].as_mut_ptr() = verts[ti][vi]; } } } unsafe { std::mem::transmute(vertices) } }; // Copy vertex normals, if any, organizing them the same as vertices // above. let normals = match vert_normals { Some(ref vnors) => { let normals = arena.alloc_array_uninit(vert_count * time_sample_count); for vi in 0..vert_count { for ti in 0..time_sample_count { unsafe { *normals[(vi * time_sample_count) + ti].as_mut_ptr() = vnors[ti][vi]; } } } unsafe { Some(std::mem::transmute(&normals[..])) } } None => None, }; // Copy triangle vertex indices over, appending the triangle index itself to the tuple let indices: &mut [(u32, u32, u32, u32)] = { let indices = arena.alloc_array_uninit(tri_indices.len()); for (i, tri_i) in tri_indices.iter().enumerate() { unsafe { *indices[i].as_mut_ptr() = (tri_i.0 as u32, tri_i.2 as u32, tri_i.1 as u32, i as u32); } } unsafe { std::mem::transmute(indices) } }; // Create bounds array for use during BVH construction let bounds = { let mut bounds = Vec::with_capacity(indices.len() * time_sample_count); for tri in tri_indices { for ti in 0..time_sample_count { let p0 = verts[ti][tri.0]; let p1 = verts[ti][tri.1]; let p2 = verts[ti][tri.2]; let minimum = p0.min(p1.min(p2)); let maximum = p0.max(p1.max(p2)); bounds.push(BBox::from_points(minimum, maximum)); } } bounds }; // Build BVH let accel = BVH4::from_objects(arena, &mut indices[..], MAX_LEAF_TRIANGLE_COUNT, |tri| { &bounds [(tri.3 as usize * time_sample_count)..((tri.3 as usize + 1) * time_sample_count)] }); TriangleMesh { time_sample_count: time_sample_count, vertices: vertices, normals: normals, indices: indices, accel: accel, } } } impl<'a> Boundable for TriangleMesh<'a> { fn bounds(&self) -> &[BBox] { self.accel.bounds() } } impl<'a> Surface for TriangleMesh<'a> { fn intersect_rays( &self, rays: &mut RayBatch, ray_stack: &mut RayStack, isects: &mut [SurfaceIntersection], shader: &dyn SurfaceShader, space: &[Transform], ) { // Precalculate transform for non-motion blur cases let static_mat_space = if space.len() == 1 { lerp_slice(space, 0.0).inverse() } else { Transform::new() }; self.accel .traverse(rays, ray_stack, |idx_range, rays, ray_stack| { let tri_count = idx_range.end - idx_range.start; // Build the triangle cache if we can! let is_cached = ray_stack.ray_count_in_next_task() >= tri_count && self.time_sample_count == 1 && space.len() <= 1; let mut tri_cache = [std::mem::MaybeUninit::uninit(); MAX_LEAF_TRIANGLE_COUNT]; if is_cached { for tri_idx in idx_range.clone() { let i = tri_idx - idx_range.start; let tri_indices = self.indices[tri_idx]; // For static triangles with static transforms, cache them. unsafe { *tri_cache[i].as_mut_ptr() = ( self.vertices[tri_indices.0 as usize], self.vertices[tri_indices.1 as usize], self.vertices[tri_indices.2 as usize], ); if !space.is_empty() { (*tri_cache[i].as_mut_ptr()).0 = (*tri_cache[i].as_mut_ptr()).0 * static_mat_space; (*tri_cache[i].as_mut_ptr()).1 = (*tri_cache[i].as_mut_ptr()).1 * static_mat_space; (*tri_cache[i].as_mut_ptr()).2 = (*tri_cache[i].as_mut_ptr()).2 * static_mat_space; } } } } // Test each ray against the triangles. ray_stack.do_next_task(|ray_idx| { let ray_idx = ray_idx as usize; if rays.is_done(ray_idx) { return; } let ray_time = rays.time(ray_idx); // Calculate the ray space, if necessary. let mat_space = if space.len() > 1 { // Per-ray transform, for motion blur lerp_slice(space, ray_time).inverse() } else { static_mat_space }; // Iterate through the triangles and test the ray against them. let mut non_shadow_hit = false; let mut hit_tri = std::mem::MaybeUninit::uninit(); let mut hit_tri_indices = std::mem::MaybeUninit::uninit(); let mut hit_tri_data = std::mem::MaybeUninit::uninit(); let ray_pre = triangle::RayTriPrecompute::new(rays.dir(ray_idx)); for tri_idx in idx_range.clone() { let tri_indices = self.indices[tri_idx]; // Get triangle if necessary let tri = if is_cached { let i = tri_idx - idx_range.start; unsafe { tri_cache[i].assume_init() } } else { let mut tri = if self.time_sample_count == 1 { // No deformation motion blur, so fast-path it. ( self.vertices[tri_indices.0 as usize], self.vertices[tri_indices.1 as usize], self.vertices[tri_indices.2 as usize], ) } else { // Deformation motion blur, need to interpolate. let p0_slice = &self.vertices[(tri_indices.0 as usize * self.time_sample_count) ..((tri_indices.0 as usize + 1) * self.time_sample_count)]; let p1_slice = &self.vertices[(tri_indices.1 as usize * self.time_sample_count) ..((tri_indices.1 as usize + 1) * self.time_sample_count)]; let p2_slice = &self.vertices[(tri_indices.2 as usize * self.time_sample_count) ..((tri_indices.2 as usize + 1) * self.time_sample_count)]; let p0 = lerp_slice(p0_slice, ray_time); let p1 = lerp_slice(p1_slice, ray_time); let p2 = lerp_slice(p2_slice, ray_time); (p0, p1, p2) }; if !space.is_empty() { tri.0 = tri.0 * mat_space; tri.1 = tri.1 * mat_space; tri.2 = tri.2 * mat_space; } tri }; // Test ray against triangle if let Some((t, b0, b1, b2)) = triangle::intersect_ray( rays.orig(ray_idx), ray_pre, rays.max_t(ray_idx), tri, ) { if rays.is_occlusion(ray_idx) { isects[ray_idx] = SurfaceIntersection::Occlude; rays.mark_done(ray_idx); break; } else { non_shadow_hit = true; rays.set_max_t(ray_idx, t); unsafe { *hit_tri.as_mut_ptr() = tri; *hit_tri_indices.as_mut_ptr() = tri_indices; *hit_tri_data.as_mut_ptr() = (t, b0, b1, b2); } } } } // Calculate intersection data if necessary. if non_shadow_hit { let hit_tri = unsafe { hit_tri.assume_init() }; let (t, b0, b1, b2) = unsafe { hit_tri_data.assume_init() }; // Calculate intersection point and error magnitudes let (pos, pos_err) = triangle::surface_point(hit_tri, (b0, b1, b2)); // Calculate geometric surface normal let geo_normal = cross(hit_tri.0 - hit_tri.1, hit_tri.0 - hit_tri.2).into_normal(); // Calculate interpolated surface normal, if any let shading_normal = if let Some(normals) = self.normals { let hit_tri_indices = unsafe { hit_tri_indices.assume_init() }; let n0_slice = &normals[(hit_tri_indices.0 as usize * self.time_sample_count) ..((hit_tri_indices.0 as usize + 1) * self.time_sample_count)]; let n1_slice = &normals[(hit_tri_indices.1 as usize * self.time_sample_count) ..((hit_tri_indices.1 as usize + 1) * self.time_sample_count)]; let n2_slice = &normals[(hit_tri_indices.2 as usize * self.time_sample_count) ..((hit_tri_indices.2 as usize + 1) * self.time_sample_count)]; let n0 = lerp_slice(n0_slice, ray_time).normalized(); let n1 = lerp_slice(n1_slice, ray_time).normalized(); let n2 = lerp_slice(n2_slice, ray_time).normalized(); let s_nor = ((n0 * b0) + (n1 * b1) + (n2 * b2)) * mat_space; if dot(s_nor, geo_normal) >= 0.0 { s_nor } else { -s_nor } } else { geo_normal }; let intersection_data = SurfaceIntersectionData { incoming: rays.dir(ray_idx), t: t, pos: pos, pos_err: pos_err, nor: shading_normal, nor_g: geo_normal, local_space: mat_space, sample_pdf: 0.0, }; // Fill in intersection data isects[ray_idx] = SurfaceIntersection::Hit { intersection_data: intersection_data, closure: shader.shade(&intersection_data, ray_time), }; } }); ray_stack.pop_task(); }); } } ================================================ FILE: src/timer.rs ================================================ #![allow(dead_code)] use std::{thread, time::Duration}; #[derive(Copy, Clone)] pub struct Timer { last_time: u64, } impl Timer { pub fn new() -> Timer { Timer { last_time: time::precise_time_ns(), } } /// Marks a new tick time and returns the time elapsed in seconds since /// the last call to tick(). pub fn tick(&mut self) -> f32 { let n = time::precise_time_ns(); let dt = n - self.last_time; self.last_time = n; dt as f32 / 1_000_000_000.0 } /// Returns the time elapsed in seconds since the last call to tick(). pub fn elapsed(self) -> f32 { let dt = time::precise_time_ns() - self.last_time; dt as f32 / 1_000_000_000.0 } /// Sleeps the current thread until n seconds after the last tick. pub fn sleep_until(self, n: f32) { let dt = time::precise_time_ns() - self.last_time; let target_dt = ((n as f64) * 1_000_000_000.0) as u64; if dt < target_dt { let delay = target_dt - dt; let seconds = delay / 1_000_000_000; let nanoseconds = delay % 1_000_000_000; thread::sleep(Duration::new(seconds, nanoseconds as u32)); } } } ================================================ FILE: src/tracer.rs ================================================ use std::iter; use crate::{ accel::ray_code, color::{rec709_to_xyz, Color}, lerp::lerp_slice, math::Transform, ray::{RayBatch, RayStack}, scene::{Assembly, InstanceType, Object}, shading::{SimpleSurfaceShader, SurfaceShader}, surface::SurfaceIntersection, transform_stack::TransformStack, }; pub struct Tracer<'a> { ray_trace_count: u64, ray_stack: RayStack, inner: TracerInner<'a>, } impl<'a> Tracer<'a> { pub fn from_assembly(assembly: &'a Assembly) -> Tracer<'a> { Tracer { ray_trace_count: 0, ray_stack: RayStack::new(), inner: TracerInner { root: assembly, xform_stack: TransformStack::new(), isects: Vec::new(), }, } } pub fn trace<'b>(&'b mut self, rays: &mut RayBatch) -> &'b [SurfaceIntersection] { self.ray_trace_count += rays.len() as u64; self.inner.trace(rays, &mut self.ray_stack) } pub fn rays_traced(&self) -> u64 { self.ray_trace_count } } struct TracerInner<'a> { root: &'a Assembly<'a>, xform_stack: TransformStack, isects: Vec, } impl<'a> TracerInner<'a> { fn trace<'b>( &'b mut self, rays: &mut RayBatch, ray_stack: &mut RayStack, ) -> &'b [SurfaceIntersection] { ray_stack.clear(); // Ready the isects self.isects.clear(); self.isects.reserve(rays.len()); self.isects .extend(iter::repeat(SurfaceIntersection::Miss).take(rays.len())); // Prep the accel part of the rays. { let ident = Transform::new(); for i in 0..rays.len() { rays.update_local(i, &ident); } } // Divide the rays into 8 different lanes by direction. ray_stack.ensure_lane_count(8); for i in 0..rays.len() { ray_stack.push_ray_index(i, ray_code(rays.dir(i))); } ray_stack.push_lanes_to_tasks(&[0, 1, 2, 3, 4, 5, 6, 7]); // Trace each of the 8 lanes separately. while !ray_stack.is_empty() { self.trace_assembly(self.root, rays, ray_stack); } &self.isects } fn trace_assembly<'b>( &'b mut self, assembly: &Assembly, rays: &mut RayBatch, ray_stack: &mut RayStack, ) { assembly .object_accel .traverse(rays, ray_stack, |idx_range, rays, ray_stack| { let inst = &assembly.instances[idx_range.start]; // Transform rays if needed if let Some((xstart, xend)) = inst.transform_indices { // Push transforms to stack self.xform_stack.push(&assembly.xforms[xstart..xend]); // Do transforms // TODO: re-divide rays based on direction (maybe?). let xforms = self.xform_stack.top(); ray_stack.do_next_task(|ray_idx| { let t = rays.time(ray_idx); rays.update_local(ray_idx, &lerp_slice(xforms, t)); }); ray_stack.duplicate_next_task(); } // Trace rays match inst.instance_type { InstanceType::Object => { self.trace_object( &assembly.objects[inst.data_index], inst.surface_shader_index .map(|i| assembly.surface_shaders[i]), rays, ray_stack, ); } InstanceType::Assembly => { self.trace_assembly(&assembly.assemblies[inst.data_index], rays, ray_stack); } } // Un-transform rays if needed if inst.transform_indices.is_some() { // Pop transforms off stack self.xform_stack.pop(); // Undo transforms let xforms = self.xform_stack.top(); if !xforms.is_empty() { ray_stack.pop_do_next_task(|ray_idx| { let t = rays.time(ray_idx); rays.update_local(ray_idx, &lerp_slice(xforms, t)); }); } else { let ident = Transform::new(); ray_stack.pop_do_next_task(|ray_idx| { rays.update_local(ray_idx, &ident); }); } } }); } fn trace_object<'b>( &'b mut self, obj: &Object, surface_shader: Option<&dyn SurfaceShader>, rays: &mut RayBatch, ray_stack: &mut RayStack, ) { match *obj { Object::Surface(surface) => { let unassigned_shader = SimpleSurfaceShader::Emit { color: Color::new_xyz(rec709_to_xyz((1.0, 0.0, 1.0))), }; let shader = surface_shader.unwrap_or(&unassigned_shader); surface.intersect_rays( rays, ray_stack, &mut self.isects, shader, self.xform_stack.top(), ); } Object::SurfaceLight(surface) => { // Lights don't use shaders let bogus_shader = SimpleSurfaceShader::Emit { color: Color::new_xyz(rec709_to_xyz((1.0, 0.0, 1.0))), }; surface.intersect_rays( rays, ray_stack, &mut self.isects, &bogus_shader, self.xform_stack.top(), ); } } } } ================================================ FILE: src/transform_stack.rs ================================================ use std::{ cmp, mem::{transmute, MaybeUninit}, }; use crate::{algorithm::merge_slices_to, math::Transform}; pub struct TransformStack { stack: Vec>, stack_indices: Vec, } impl TransformStack { pub fn new() -> TransformStack { let mut ts = TransformStack { stack: Vec::new(), stack_indices: Vec::new(), }; ts.stack_indices.push(0); ts.stack_indices.push(0); ts } pub fn clear(&mut self) { self.stack.clear(); self.stack_indices.clear(); self.stack_indices.push(0); self.stack_indices.push(0); } pub fn push(&mut self, xforms: &[Transform]) { assert!(!xforms.is_empty()); if self.stack.is_empty() { let xforms: &[MaybeUninit] = unsafe { transmute(xforms) }; self.stack.extend(xforms); } else { let sil = self.stack_indices.len(); let i1 = self.stack_indices[sil - 2]; let i2 = self.stack_indices[sil - 1]; // Reserve stack space for the new transforms. // Note this leaves exposed uninitialized memory. The subsequent call to // merge_slices_to() fills that memory in. { let maxlen = cmp::max(xforms.len(), i2 - i1); self.stack.reserve(maxlen); let l = self.stack.len(); unsafe { self.stack.set_len(l + maxlen) }; } let (xfs1, xfs2) = self.stack.split_at_mut(i2); merge_slices_to( unsafe { transmute(&xfs1[i1..i2]) }, xforms, xfs2, |xf1, xf2| *xf1 * *xf2, ); } self.stack_indices.push(self.stack.len()); } pub fn pop(&mut self) { assert!(self.stack_indices.len() > 2); let sl = self.stack.len(); let sil = self.stack_indices.len(); let i1 = self.stack_indices[sil - 2]; let i2 = self.stack_indices[sil - 1]; self.stack.truncate(sl - (i2 - i1)); self.stack_indices.pop(); } pub fn top(&self) -> &[Transform] { let sil = self.stack_indices.len(); let i1 = self.stack_indices[sil - 2]; let i2 = self.stack_indices[sil - 1]; unsafe { transmute(&self.stack[i1..i2]) } } } ================================================ FILE: sub_crates/bvh_order/Cargo.toml ================================================ [package] name = "bvh_order" version = "0.1.0" authors = ["Nathan Vegdahl "] edition = "2018" license = "MIT, Apache 2.0" build = "build.rs" [lib] name = "bvh_order" path = "src/lib.rs" ================================================ FILE: sub_crates/bvh_order/LICENSE.md ================================================ Copyright (c) 2020 Nathan Vegdahl This project is licensed under either of * MIT license (licenses/MIT.txt or http://opensource.org/licenses/MIT) * Apache License, Version 2.0, (licenses/Apache-2.0.txt or http://www.apache.org/licenses/LICENSE-2.0) at your option. ================================================ FILE: sub_crates/bvh_order/build.rs ================================================ // Generate table for traversal order of quad BVHs. use std::{env, fs::File, io::Write, path::Path}; fn main() { // Build the traversal table. let mut traversal_table = [ Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new(), ]; for raydir in 0..8 { let ray = [raydir & 1, (raydir >> 1) & 1, (raydir >> 2) & 1]; for s2 in 0..3 { for s1 in 0..3 { for s0 in 0..3 { let mut perm = [0, 1, 2, 3]; if ray[s1] == 1 { perm.swap(0, 1); } if ray[s2] == 1 { perm.swap(2, 3); } if ray[s0] == 1 { perm.swap(0, 2); perm.swap(1, 3); } traversal_table[raydir] .push(perm[0] + (perm[1] << 2) + (perm[2] << 4) + (perm[3] << 6)); } } } for s1 in 0..3 { for s0 in 0..3 { let mut perm = [0, 1, 2]; if ray[s1] == 1 { perm.swap(0, 1); } if ray[s0] == 1 { perm.swap(0, 1); perm.swap(0, 2); } traversal_table[raydir].push(perm[0] + (perm[1] << 2) + (perm[2] << 4)); } } for s1 in 0..3 { for s0 in 0..3 { let mut perm = [0, 1, 2]; if ray[s1] == 1 { perm.swap(1, 2); } if ray[s0] == 1 { perm.swap(0, 2); perm.swap(0, 1); } traversal_table[raydir].push(perm[0] + (perm[1] << 2) + (perm[2] << 4)); } } for s0 in 0..3 { let mut perm = [0, 1]; if ray[s0] == 1 { perm.swap(0, 1); } traversal_table[raydir].push(perm[0] + (perm[1] << 2)); } } // Write traversal table to Rust file let out_dir = env::var("OUT_DIR").unwrap(); let dest_path = Path::new(&out_dir).join("table_inc.rs"); let mut f = File::create(&dest_path).unwrap(); f.write_all("pub static TRAVERSAL_TABLE: [[u8; 48]; 8] = [".as_bytes()) .unwrap(); for sub_table in traversal_table.iter() { f.write_all("\n [".as_bytes()).unwrap(); for (i, n) in sub_table.iter().enumerate() { if i == 27 || i == 36 || i == 45 { f.write_all("\n ".as_bytes()).unwrap(); } f.write_all(format!("{}", n).as_bytes()).unwrap(); if i != 47 { f.write_all(", ".as_bytes()).unwrap(); } } f.write_all("],".as_bytes()).unwrap(); } f.write_all("\n];".as_bytes()).unwrap(); } ================================================ FILE: sub_crates/bvh_order/src/lib.rs ================================================ #![allow(dead_code)] // Include TRAVERSAL_TABLE generated by the build.rs script include!(concat!(env!("OUT_DIR"), "/table_inc.rs")); /// Represents the split axes of the BVH2 node(s) that a BVH4 node was created /// from. /// /// * `Full` is four nodes from three splits: top, left, and right. /// * `Left` is three nodes from two splits: top and left. /// * `Right` is three nodes from two splits: top and right. /// * `TopOnly` is two nodes from one split (in other words, the BVH4 node is /// identical to the single BVH2 node that it was created from). /// /// The left node of a split is the node whose coordinate on the top split-axis /// is lower. For example, if the top split is on the x axis, then `left.x <= right.x`. /// /// The values representing each axis are x = 0, y = 1, and z = 2. #[derive(Debug, Copy, Clone)] pub enum SplitAxes { Full((u8, u8, u8)), // top, left, right Left((u8, u8)), // top, left Right((u8, u8)), // top, right TopOnly(u8), // top } /// Calculates the traversal code for a BVH4 node based on the splits and /// topology of the BVH2 node(s) it was created from. #[inline(always)] pub fn calc_traversal_code(split: SplitAxes) -> u8 { match split { SplitAxes::Full((top, left, right)) => top + (left * 3) + (right * 9), SplitAxes::Left((top, left)) => top + (left * 3) + 27, SplitAxes::Right((top, right)) => top + (right * 3) + (27 + 9), SplitAxes::TopOnly(top) => top + (27 + 9 + 9), } } ================================================ FILE: sub_crates/color/Cargo.toml ================================================ [package] name = "color" version = "0.1.0" authors = ["Nathan Vegdahl "] edition = "2018" license = "MIT, Apache 2.0" build = "build.rs" [lib] name = "color" path = "src/lib.rs" ================================================ FILE: sub_crates/color/LICENSE.md ================================================ Copyright (c) 2020 Nathan Vegdahl This project is licensed under either of * MIT license (licenses/MIT.txt or http://opensource.org/licenses/MIT) * Apache License, Version 2.0, (licenses/Apache-2.0.txt or http://www.apache.org/licenses/LICENSE-2.0) at your option. ================================================ FILE: sub_crates/color/build.rs ================================================ use std::{env, fs::File, io::Write, path::Path}; #[derive(Copy, Clone)] struct Chromaticities { r: (f64, f64), g: (f64, f64), b: (f64, f64), w: (f64, f64), } fn main() { let out_dir = env::var("OUT_DIR").unwrap(); // Rec709 { let chroma = Chromaticities { r: (0.640, 0.330), g: (0.300, 0.600), b: (0.150, 0.060), w: (0.3127, 0.3290), }; let dest_path = Path::new(&out_dir).join("rec709_inc.rs"); let mut f = File::create(&dest_path).unwrap(); write_conversion_functions("rec709", chroma, &mut f); } // Rec2020 { let chroma = Chromaticities { r: (0.708, 0.292), g: (0.170, 0.797), b: (0.131, 0.046), w: (0.3127, 0.3290), }; let dest_path = Path::new(&out_dir).join("rec2020_inc.rs"); let mut f = File::create(&dest_path).unwrap(); write_conversion_functions("rec2020", chroma, &mut f); } // ACES AP0 { let chroma = Chromaticities { r: (0.73470, 0.26530), g: (0.00000, 1.00000), b: (0.00010, -0.07700), w: (0.32168, 0.33767), }; let dest_path = Path::new(&out_dir).join("aces_ap0_inc.rs"); let mut f = File::create(&dest_path).unwrap(); write_conversion_functions("aces_ap0", chroma, &mut f); } // ACES AP1 { let chroma = Chromaticities { r: (0.713, 0.293), g: (0.165, 0.830), b: (0.128, 0.044), w: (0.32168, 0.33767), }; let dest_path = Path::new(&out_dir).join("aces_ap1_inc.rs"); let mut f = File::create(&dest_path).unwrap(); write_conversion_functions("aces_ap1", chroma, &mut f); } } /// Generates conversion functions for the given rgb to xyz transform matrix. fn write_conversion_functions(space_name: &str, chroma: Chromaticities, f: &mut File) { let to_xyz = rgb_to_xyz(chroma, 1.0); f.write_all( format!( r#" #[inline] pub fn {}_to_xyz(rgb: (f32, f32, f32)) -> (f32, f32, f32) {{ ( (rgb.0 * {:.10}) + (rgb.1 * {:.10}) + (rgb.2 * {:.10}), (rgb.0 * {:.10}) + (rgb.1 * {:.10}) + (rgb.2 * {:.10}), (rgb.0 * {:.10}) + (rgb.1 * {:.10}) + (rgb.2 * {:.10}), ) }} "#, space_name, to_xyz[0][0], to_xyz[0][1], to_xyz[0][2], to_xyz[1][0], to_xyz[1][1], to_xyz[1][2], to_xyz[2][0], to_xyz[2][1], to_xyz[2][2] ) .as_bytes(), ) .unwrap(); let inv = inverse(to_xyz); f.write_all( format!( r#" #[inline] pub fn xyz_to_{}(xyz: (f32, f32, f32)) -> (f32, f32, f32) {{ ( (xyz.0 * {:.10}) + (xyz.1 * {:.10}) + (xyz.2 * {:.10}), (xyz.0 * {:.10}) + (xyz.1 * {:.10}) + (xyz.2 * {:.10}), (xyz.0 * {:.10}) + (xyz.1 * {:.10}) + (xyz.2 * {:.10}), ) }} "#, space_name, inv[0][0], inv[0][1], inv[0][2], inv[1][0], inv[1][1], inv[1][2], inv[2][0], inv[2][1], inv[2][2] ) .as_bytes(), ) .unwrap(); let e_chroma = { let mut e_chroma = chroma; e_chroma.w = (1.0 / 3.0, 1.0 / 3.0); e_chroma }; let e_to_xyz = rgb_to_xyz(e_chroma, 1.0); f.write_all( format!( r#" #[inline] pub fn {}_e_to_xyz(rgb: (f32, f32, f32)) -> (f32, f32, f32) {{ ( (rgb.0 * {:.10}) + (rgb.1 * {:.10}) + (rgb.2 * {:.10}), (rgb.0 * {:.10}) + (rgb.1 * {:.10}) + (rgb.2 * {:.10}), (rgb.0 * {:.10}) + (rgb.1 * {:.10}) + (rgb.2 * {:.10}), ) }} "#, space_name, e_to_xyz[0][0], e_to_xyz[0][1], e_to_xyz[0][2], e_to_xyz[1][0], e_to_xyz[1][1], e_to_xyz[1][2], e_to_xyz[2][0], e_to_xyz[2][1], e_to_xyz[2][2] ) .as_bytes(), ) .unwrap(); let inv_e = inverse(e_to_xyz); f.write_all( format!( r#" #[inline] pub fn xyz_to_{}_e(xyz: (f32, f32, f32)) -> (f32, f32, f32) {{ ( (xyz.0 * {:.10}) + (xyz.1 * {:.10}) + (xyz.2 * {:.10}), (xyz.0 * {:.10}) + (xyz.1 * {:.10}) + (xyz.2 * {:.10}), (xyz.0 * {:.10}) + (xyz.1 * {:.10}) + (xyz.2 * {:.10}), ) }} "#, space_name, inv_e[0][0], inv_e[0][1], inv_e[0][2], inv_e[1][0], inv_e[1][1], inv_e[1][2], inv_e[2][0], inv_e[2][1], inv_e[2][2] ) .as_bytes(), ) .unwrap(); } /// Port of the RGBtoXYZ function from the ACES CTL reference implementation. /// See lib/IlmCtlMath/CtlColorSpace.cpp in the CTL reference implementation. /// /// This takes the chromaticities of an RGB colorspace and generates a /// transform matrix from that space to XYZ. /// /// * `chroma` is the chromaticities. /// * `y` is the XYZ "Y" value that should map to RGB (1,1,1) fn rgb_to_xyz(chroma: Chromaticities, y: f64) -> [[f64; 3]; 3] { // X and Z values of RGB value (1, 1, 1), or "white" let x = chroma.w.0 * y / chroma.w.1; let z = (1.0 - chroma.w.0 - chroma.w.1) * y / chroma.w.1; // Scale factors for matrix rows let d = chroma.r.0 * (chroma.b.1 - chroma.g.1) + chroma.b.0 * (chroma.g.1 - chroma.r.1) + chroma.g.0 * (chroma.r.1 - chroma.b.1); let sr = (x * (chroma.b.1 - chroma.g.1) - chroma.g.0 * (y * (chroma.b.1 - 1.0) + chroma.b.1 * (x + z)) + chroma.b.0 * (y * (chroma.g.1 - 1.0) + chroma.g.1 * (x + z))) / d; let sg = (x * (chroma.r.1 - chroma.b.1) + chroma.r.0 * (y * (chroma.b.1 - 1.0) + chroma.b.1 * (x + z)) - chroma.b.0 * (y * (chroma.r.1 - 1.0) + chroma.r.1 * (x + z))) / d; let sb = (x * (chroma.g.1 - chroma.r.1) - chroma.r.0 * (y * (chroma.g.1 - 1.0) + chroma.g.1 * (x + z)) + chroma.g.0 * (y * (chroma.r.1 - 1.0) + chroma.r.1 * (x + z))) / d; // Assemble the matrix let mut mat = [[0.0; 3]; 3]; mat[0][0] = sr * chroma.r.0; mat[0][1] = sg * chroma.g.0; mat[0][2] = sb * chroma.b.0; mat[1][0] = sr * chroma.r.1; mat[1][1] = sg * chroma.g.1; mat[1][2] = sb * chroma.b.1; mat[2][0] = sr * (1.0 - chroma.r.0 - chroma.r.1); mat[2][1] = sg * (1.0 - chroma.g.0 - chroma.g.1); mat[2][2] = sb * (1.0 - chroma.b.0 - chroma.b.1); mat } /// Calculates the inverse of the given 3x3 matrix. /// /// Ported to Rust from `gjInverse()` in IlmBase's Imath/ImathMatrix.h fn inverse(m: [[f64; 3]; 3]) -> [[f64; 3]; 3] { let mut s = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]; let mut t = m; // Forward elimination for i in 0..2 { let mut pivot = i; let mut pivotsize = t[i][i]; if pivotsize < 0.0 { pivotsize = -pivotsize; } for j in (i + 1)..3 { let mut tmp = t[j][i]; if tmp < 0.0 { tmp = -tmp; } if tmp > pivotsize { pivot = j; pivotsize = tmp; } } if pivotsize == 0.0 { panic!("Cannot invert singular matrix."); } if pivot != i { for j in 0..3 { let mut tmp = t[i][j]; t[i][j] = t[pivot][j]; t[pivot][j] = tmp; tmp = s[i][j]; s[i][j] = s[pivot][j]; s[pivot][j] = tmp; } } for j in (i + 1)..3 { let f = t[j][i] / t[i][i]; for k in 0..3 { t[j][k] -= f * t[i][k]; s[j][k] -= f * s[i][k]; } } } // Backward substitution for i in (0..3).rev() { let f = t[i][i]; if t[i][i] == 0.0 { panic!("Cannot invert singular matrix."); } for j in 0..3 { t[i][j] /= f; s[i][j] /= f; } for j in 0..i { let f = t[j][i]; for k in 0..3 { t[j][k] -= f * t[i][k]; s[j][k] -= f * s[i][k]; } } } s } ================================================ FILE: sub_crates/color/src/lib.rs ================================================ #![allow(clippy::excessive_precision)] #![allow(clippy::unreadable_literal)] #[allow(non_camel_case_types)] #[derive(Copy, Clone)] pub enum Space { XYZ, ACES_AP0, ACES_AP1, Rec709, Rec2020, } // Generated conversion functions between XYZ and various RGB colorspaces include!(concat!(env!("OUT_DIR"), "/rec709_inc.rs")); include!(concat!(env!("OUT_DIR"), "/rec2020_inc.rs")); include!(concat!(env!("OUT_DIR"), "/aces_ap0_inc.rs")); include!(concat!(env!("OUT_DIR"), "/aces_ap1_inc.rs")); ================================================ FILE: sub_crates/compact/Cargo.toml ================================================ [package] name = "compact" version = "0.1.0" authors = ["Nathan Vegdahl "] edition = "2018" license = "MIT, Apache 2.0" [lib] name = "compact" path = "src/lib.rs" [dev-dependencies] proptest = "0.8" bencher = "0.1.5" rand = "0.6" [[bench]] name = "bench" harness = false ================================================ FILE: sub_crates/compact/LICENSE.md ================================================ Copyright (c) 2020 Nathan Vegdahl This project is licensed under either of * MIT license (licenses/MIT.txt or http://opensource.org/licenses/MIT) * Apache License, Version 2.0, (licenses/Apache-2.0.txt or http://www.apache.org/licenses/LICENSE-2.0) at your option. ================================================ FILE: sub_crates/compact/benches/bench.rs ================================================ use bencher::{benchmark_group, benchmark_main, black_box, Bencher}; use compact::{ fluv::fluv32, shared_exp::{signed48, unsigned32, unsigned40}, unit_vec::oct32, }; use rand::{rngs::SmallRng, FromEntropy, Rng}; //---- fn unsigned32_encode_1000_values(bench: &mut Bencher) { let mut rng = SmallRng::from_entropy(); bench.iter(|| { let x = rng.gen::(); let y = rng.gen::(); let z = rng.gen::(); for _ in 0..1000 { black_box(unsigned32::encode(black_box((x, y, z)))); } }); } fn unsigned32_decode_1000_values(bench: &mut Bencher) { let mut rng = SmallRng::from_entropy(); bench.iter(|| { let v = rng.gen::(); for _ in 0..1000 { black_box(unsigned32::decode(black_box(v))); } }); } fn unsigned40_encode_1000_values(bench: &mut Bencher) { let mut rng = SmallRng::from_entropy(); bench.iter(|| { let x = rng.gen::(); let y = rng.gen::(); let z = rng.gen::(); for _ in 0..1000 { black_box(unsigned40::encode(black_box((x, y, z)))); } }); } fn unsigned40_decode_1000_values(bench: &mut Bencher) { let mut rng = SmallRng::from_entropy(); bench.iter(|| { let v = [ rng.gen::(), rng.gen::(), rng.gen::(), rng.gen::(), rng.gen::(), ]; for _ in 0..1000 { black_box(unsigned40::decode(black_box(v))); } }); } fn signed48_encode_1000_values(bench: &mut Bencher) { let mut rng = SmallRng::from_entropy(); bench.iter(|| { let x = rng.gen::() - 0.5; let y = rng.gen::() - 0.5; let z = rng.gen::() - 0.5; for _ in 0..1000 { black_box(signed48::encode(black_box((x, y, z)))); } }); } fn signed48_decode_1000_values(bench: &mut Bencher) { let mut rng = SmallRng::from_entropy(); bench.iter(|| { let v = [ rng.gen::(), rng.gen::(), rng.gen::(), rng.gen::(), rng.gen::(), rng.gen::(), ]; for _ in 0..1000 { black_box(signed48::decode(black_box(v))); } }); } fn fluv32_encode_1000_values(bench: &mut Bencher) { let mut rng = SmallRng::from_entropy(); bench.iter(|| { let x = rng.gen::(); let y = rng.gen::(); let z = rng.gen::(); for _ in 0..1000 { black_box(fluv32::encode(black_box((x, y, z)))); } }); } fn fluv32_decode_1000_values(bench: &mut Bencher) { let mut rng = SmallRng::from_entropy(); bench.iter(|| { let v = rng.gen::(); for _ in 0..1000 { black_box(fluv32::decode(black_box(v))); } }); } fn fluv32_decode_yuv_1000_values(bench: &mut Bencher) { let mut rng = SmallRng::from_entropy(); bench.iter(|| { let v = rng.gen::(); for _ in 0..1000 { black_box(fluv32::decode_yuv(black_box(v))); } }); } fn oct32_encode_1000_values(bench: &mut Bencher) { let mut rng = SmallRng::from_entropy(); bench.iter(|| { let x = rng.gen::() - 0.5; let y = rng.gen::() - 0.5; let z = rng.gen::() - 0.5; for _ in 0..1000 { black_box(oct32::encode(black_box((x, y, z)))); } }); } fn oct32_encode_precise_1000_values(bench: &mut Bencher) { let mut rng = SmallRng::from_entropy(); bench.iter(|| { let x = rng.gen::() - 0.5; let y = rng.gen::() - 0.5; let z = rng.gen::() - 0.5; for _ in 0..1000 { black_box(oct32::encode_precise(black_box((x, y, z)))); } }); } fn oct32_decode_1000_values(bench: &mut Bencher) { let mut rng = SmallRng::from_entropy(); bench.iter(|| { let v = rng.gen::(); for _ in 0..1000 { black_box(oct32::decode(black_box(v))); } }); } //---- benchmark_group!( benches, unsigned32_encode_1000_values, unsigned32_decode_1000_values, unsigned40_encode_1000_values, unsigned40_decode_1000_values, signed48_encode_1000_values, signed48_decode_1000_values, fluv32_encode_1000_values, fluv32_decode_1000_values, fluv32_decode_yuv_1000_values, oct32_encode_1000_values, oct32_encode_precise_1000_values, oct32_decode_1000_values, ); benchmark_main!(benches); ================================================ FILE: sub_crates/compact/src/fluv/fluv32.rs ================================================ //! Encoding/decoding for the 32-bit FLuv32 color format. //! //! This encoding is based on, but is slightly different than, the 32-bit //! LogLuv format from the paper "Overcoming Gamut and Dynamic Range //! Limitations in Digital Images" by Greg Ward: //! //! * It uses the same uv chroma storage approach, but with *very* slightly //! tweaked scales to allow perfect representation of E. //! * It uses a floating point rather than log encoding to store luminance, //! mainly for the sake of faster decoding. //! * It omits the sign bit of LogLuv, foregoing negative luminance //! capabilities. //! //! Aside from that, this format has the same chroma precision, very slightly //! improved luminance precision, and the same 127-stops of dynamic range as //! LogLuv. //! //! Like the LogLuv format, this is an absolute rather than relative color //! encoding, and as such takes CIE XYZ triplets as input. It is *not* //! designed to take arbitrary floating point triplets, and will perform poorly //! if e.g. passed RGB values. //! //! The bit layout is (from most to least significant bit): //! //! * 7 bits: luminance exponent (bias 63) //! * 9 bits: luminance mantissa (implied leading 1, for 10 bits precision) //! * 8 bits: u' //! * 8 bits: v' //! //! ## Luminance details //! //! Like typical floating point, the luminance mantissa is treated as having an //! implicit leading 1, giving it an extra bit of precision. //! //! The luminance exponent is stored in 7 bits with a bias of 63. The smallest //! exponent indicates a value of zero, and a valid encoding should also set //! the mantissa to zero in that case (denormal numbers are not supported). //! The largest exponent is given no special treatment (no infinities, no NaN). //! //! All together, this gives Fluv32 a worst-case precision that's slightly //! better than Logluv, and a luminance range of roughly `10^-19` to `10^19`, //! essentially the same as Logluv. //! //! Quoting Greg Ward about luminance ranges: //! //! > The sun is about `10^8 cd/m^2`, and the underside of a rock on a moonless //! > night is probably around `10^-6` or so [...] //! //! So Fluv32's luminance range is *massively* larger than needed for any //! day-to-day phenomena. The only things that exceed it are stellar events //! such as supernovae, images of which are unliklely to be used with physical //! units in most practical graphics applications. #![allow(clippy::cast_lossless)] const EXP_BIAS: i32 = 63; const BIAS_OFFSET: u32 = 127 - EXP_BIAS as u32; /// The scale factor of the quantized U component. pub const U_SCALE: f32 = 817.0 / 2.0; /// The scale factor of the quantized V component. pub const V_SCALE: f32 = 1235.0 / 3.0; /// Largest representable Y component. pub const Y_MAX: f32 = ((1u128 << (128 - EXP_BIAS)) - (1u128 << (128 - EXP_BIAS - 10))) as f32; /// Smallest representable non-zero Y component. pub const Y_MIN: f32 = 1.0 / (1u128 << (EXP_BIAS - 1)) as f32; /// Difference between 1.0 and the next largest representable Y value. pub const Y_EPSILON: f32 = 1.0 / 512.0; /// Encodes from CIE XYZ to 32-bit FloatLuv. #[inline] pub fn encode(xyz: (f32, f32, f32)) -> u32 { debug_assert!( xyz.0 >= 0.0 && xyz.1 >= 0.0 && xyz.2 >= 0.0 && !xyz.0.is_nan() && !xyz.1.is_nan() && !xyz.2.is_nan(), "trifloat::fluv32::encode(): encoding to fluv32 only \ works correctly for positive, non-NaN numbers, but the numbers passed \ were: ({}, {}, {})", xyz.0, xyz.1, xyz.2 ); // Calculates the 16-bit encoding of the UV values for the given XYZ input. #[inline(always)] fn encode_uv(xyz: (f32, f32, f32)) -> u32 { let s = xyz.0 + (15.0 * xyz.1) + (3.0 * xyz.2); // The `+ 0.5` is for rounding, and is not part of the normal equation. // The minimum value of 1.0 for v is to avoid a possible divide by zero // when decoding. A value less than 1.0 is outside the real colors, // so we don't need to store it anyway. let u = (((4.0 * U_SCALE) * xyz.0 / s) + 0.5).max(0.0).min(255.0); let v = (((9.0 * V_SCALE) * xyz.1 / s) + 0.5).max(1.0).min(255.0); ((u as u32) << 8) | (v as u32) } let y_bits = xyz.1.to_bits() & 0x7fffffff; if y_bits < ((BIAS_OFFSET + 1) << 23) { // Special case: black. encode_uv((1.0, 1.0, 1.0)) } else if y_bits >= ((BIAS_OFFSET + 128) << 23) { if xyz.1.is_infinite() { // Special case: infinity. In this case, we don't have any // reasonable basis for calculating chroma, so just return // the brightest white. 0xffff0000 | encode_uv((1.0, 1.0, 1.0)) } else { // Special case: non-infinite, but brighter luma than can be // represented. Return the correct chroma, and the brightest luma. 0xffff0000 | encode_uv(xyz) } } else { // Common case. (((y_bits - (BIAS_OFFSET << 23)) << 2) & 0xffff0000) | encode_uv(xyz) } } /// Decodes from 32-bit FloatLuv to CIE XYZ. #[inline] pub fn decode(fluv32: u32) -> (f32, f32, f32) { // Unpack values. let (y, u, v) = decode_yuv(fluv32); let u = u as f32; let v = v as f32; // Calculate x and z. // This is re-worked from the original equations, to allow a bunch of stuff // to cancel out and avoid operations. It makes the underlying equations a // bit non-obvious. // We also roll the U/V_SCALE application into the final x and z formulas, // since some of that cancels out as well, and all of it can be avoided at // runtime that way. const VU_RATIO: f32 = V_SCALE / U_SCALE; let tmp = y / v; let x = tmp * ((2.25 * VU_RATIO) * u); // y * (9u / 4v) let z = tmp * ((3.0 * V_SCALE) - ((0.75 * VU_RATIO) * u) - (5.0 * v)); // y * ((12 - 3u - 20v) / 4v) (x, y, z.max(0.0)) } /// Decodes from 32-bit FloatLuv to Yuv. /// /// The Y component is the luminance, and is simply the Y from CIE XYZ. /// /// The u and v components are the CIE LUV u' and v' chromaticity coordinates, /// but returned as `u8`s, and scaled by `U_SCALE` and `V_SCALE` respectively /// to fit the range 0-255. #[inline] pub fn decode_yuv(fluv32: u32) -> (f32, u8, u8) { let y = f32::from_bits(((fluv32 & 0xffff0000) >> 2) + (BIAS_OFFSET << 23)); let u = (fluv32 >> 8) as u8; let v = fluv32 as u8; if fluv32 <= 0xffff { (0.0, u, v) } else { (y, u, v) } } #[cfg(test)] mod tests { use super::*; fn round_trip(floats: (f32, f32, f32)) -> (f32, f32, f32) { decode(encode(floats)) } #[test] fn all_zeros() { let fs = (0.0f32, 0.0f32, 0.0f32); let tri = encode(fs); let fs2 = decode(tri); assert_eq!(0x000056c3, tri); assert_eq!(fs, fs2); } #[test] fn all_ones() { let fs = (1.0f32, 1.0f32, 1.0f32); let tri = encode(fs); let fs2 = decode(tri); assert!((fs.0 - fs2.0).abs() < 0.0000001); assert_eq!(fs.1, fs2.1); assert!((fs.2 - fs2.2).abs() < 0.0000001); assert_eq!(0x7e0056c3, tri); } #[test] fn powers_of_two() { let mut n = 0.25; for _ in 0..20 { let a = (n as f32, n as f32, n as f32); let b = round_trip(a); let rd0 = 2.0 * (a.0 - b.0).abs() / (a.0 + b.0); let rd2 = 2.0 * (a.2 - b.2).abs() / (a.2 + b.2); assert_eq!(a.1, b.1); assert!(rd0 < 0.01); assert!(rd2 < 0.01); n *= 2.0; } } #[test] fn accuracy_01() { let mut n = 1.0; for _ in 0..512 { let a = (n as f32, n as f32, n as f32); let b = round_trip(a); let rd0 = 2.0 * (a.0 - b.0).abs() / (a.0 + b.0); let rd2 = 2.0 * (a.2 - b.2).abs() / (a.2 + b.2); assert_eq!(a.1, b.1); assert!(rd0 < 0.01); assert!(rd2 < 0.01); n += 1.0 / 512.0; } } #[test] #[should_panic] fn accuracy_02() { let mut n = 1.0; for _ in 0..1024 { let a = (n as f32, n as f32, n as f32); let b = round_trip(a); assert_eq!(a.1, b.1); n += 1.0 / 1024.0; } } #[test] fn integers() { for n in 1..=512 { let a = (n as f32, n as f32, n as f32); let b = round_trip(a); let rd0 = 2.0 * (a.0 - b.0).abs() / (a.0 + b.0); let rd2 = 2.0 * (a.2 - b.2).abs() / (a.2 + b.2); assert_eq!(a.1, b.1); assert!(rd0 < 0.01); assert!(rd2 < 0.01); } } #[test] fn precision_floor() { let fs = (2049.0f32, 2049.0f32, 2049.0f32); assert_eq!(2048.0, round_trip(fs).1); } #[test] fn decode_yuv_01() { let fs = (1.0, 1.0, 1.0); let a = encode(fs); assert_eq!((1.0, 0x56, 0xc3), decode_yuv(a)); } #[test] fn saturate_y() { let fs = (1.0e+28, 1.0e+28, 1.0e+28); assert_eq!(Y_MAX, round_trip(fs).1); assert_eq!(Y_MAX, decode(0xFFFFFFFF).1); } #[test] fn inf_saturate() { use std::f32::INFINITY; let fs = (INFINITY, INFINITY, INFINITY); assert_eq!(Y_MAX, round_trip(fs).1); assert_eq!(0xffff56c3, encode(fs)); } #[test] fn smallest_value_and_underflow() { let fs1 = (Y_MIN, Y_MIN, Y_MIN); let fs2 = (Y_MIN * 0.99, Y_MIN * 0.99, Y_MIN * 0.99); dbg!(Y_MIN); assert_eq!(fs1.1, round_trip(fs1).1); assert_eq!(0.0, round_trip(fs2).1); assert_eq!(0x000056c3, encode(fs2)); } #[test] fn negative_z_impossible() { for y in 0..1024 { let fs = (1.0, 1.0 + (y as f32 / 4096.0), 0.0); let fs2 = round_trip(fs); assert!(fs2.2 >= 0.0); } } #[test] #[should_panic] fn nans_01() { encode((std::f32::NAN, 0.0, 0.0)); } #[test] #[should_panic] fn nans_02() { encode((0.0, std::f32::NAN, 0.0)); } #[test] #[should_panic] fn nans_03() { encode((0.0, 0.0, std::f32::NAN)); } #[test] #[should_panic] fn negative_01() { encode((-1.0, 0.0, 0.0)); } #[test] #[should_panic] fn negative_02() { encode((0.0, -1.0, 0.0)); } #[test] #[should_panic] fn negative_03() { encode((0.0, 0.0, -1.0)); } #[test] fn negative_04() { encode((-0.0, -0.0, -0.0)); } } ================================================ FILE: sub_crates/compact/src/fluv/mod.rs ================================================ //! Fluv, a set of formats for compactly storing HDR XYZ colors. //! //! At the moment, only a 32-bit variant of the fluv format is provided. pub mod fluv32; ================================================ FILE: sub_crates/compact/src/lib.rs ================================================ //! Functions for storing various kinds of data compactly, using domain //! knowledge of how that data is used. //! //! This includes functions for compactly storing e.g. colors and unit vectors. pub mod fluv; pub mod shared_exp; pub mod unit_vec; ================================================ FILE: sub_crates/compact/src/shared_exp/mod.rs ================================================ //! Shared-exponent float triplet formats. pub mod signed48; pub mod unsigned32; pub mod unsigned40; //=========================================================================== // Shared functions used by the other modules in this crate. /// Calculates 2.0^exp using IEEE bit fiddling. /// /// Only works for integer exponents in the range [-126, 127] /// due to IEEE 32-bit float limits. #[inline(always)] fn fiddle_exp2(exp: i32) -> f32 { use std::f32; f32::from_bits(((exp + 127) as u32) << 23) } /// Calculates a floor(log2(n)) using IEEE bit fiddling. /// /// Because of IEEE floating point format, infinity and NaN /// floating point values return 128, and subnormal numbers always /// return -127. These particular behaviors are not, of course, /// mathemetically correct, but are actually desireable for the /// calculations in this library. #[inline(always)] fn fiddle_log2(n: f32) -> i32 { use std::f32; ((f32::to_bits(n) >> 23) & 0b1111_1111) as i32 - 127 } ================================================ FILE: sub_crates/compact/src/shared_exp/signed48.rs ================================================ //! Encoding/decoding for signed 48-bit trifloat numbers. //! //! The encoding uses 13 bits of mantissa and 1 sign bit per number, and 6 //! bits for the shared exponent. The bit layout is: [sign 1, mantissa 1, //! sign 2, mantissa 2, sign 3, mantissa 3, exponent]. The exponent is stored //! as an unsigned integer with a bias of 26. //! //! The largest representable number is just under `2^38`, and the smallest //! representable positive number is `2^-38`. //! //! Since the exponent is shared between all three values, the precision //! of all three values depends on the largest (in magnitude) of the three. //! All integers in the range `[-8192, 8192]` can be represented exactly in the //! largest value. #![allow(clippy::cast_lossless)] use super::{fiddle_exp2, fiddle_log2}; /// Largest representable number. pub const MAX: f32 = ((1u128 << (64 - EXP_BIAS)) - (1 << (64 - EXP_BIAS - 13))) as f32; /// Smallest representable number. /// /// Note this is not the smallest _magnitude_ number. This is a negative /// number of large magnitude. pub const MIN: f32 = -MAX; /// Smallest representable positive number. /// /// This is the number with the smallest possible magnitude (aside from zero). pub const MIN_POSITIVE: f32 = 1.0 / (1u128 << (EXP_BIAS + 12)) as f32; /// Difference between 1.0 and the next largest representable number. pub const EPSILON: f32 = 1.0 / 4096.0; const EXP_BIAS: i32 = 26; /// Encodes three floating point values into a signed 48-bit trifloat. /// /// Input floats that are larger than `MAX` or smaller than `MIN` will saturate /// to `MAX` and `MIN` respectively, including +/- infinity. Values are /// converted to trifloat precision by rounding towards zero. /// /// Warning: NaN's are _not_ supported by the trifloat format. There are /// debug-only assertions in place to catch such values in the input floats. #[inline] pub fn encode(floats: (f32, f32, f32)) -> [u8; 6] { u64_to_bytes(encode_64(floats)) } /// Decodes a signed 48-bit trifloat into three full floating point numbers. /// /// This operation is lossless and cannot fail. #[inline] pub fn decode(trifloat: [u8; 6]) -> (f32, f32, f32) { decode_64(bytes_to_u64(trifloat)) } // Workhorse encode function, which operates on u64. #[inline(always)] fn encode_64(floats: (f32, f32, f32)) -> u64 { debug_assert!( !floats.0.is_nan() && !floats.1.is_nan() && !floats.2.is_nan(), "trifloat::signed48::encode(): encoding to signed tri-floats only \ works correctly for non-NaN numbers, but the numbers passed were: \ ({}, {}, {})", floats.0, floats.1, floats.2 ); let floats_abs = (floats.0.abs(), floats.1.abs(), floats.2.abs()); let largest_abs = floats_abs.0.max(floats_abs.1.max(floats_abs.2)); if largest_abs < MIN_POSITIVE { 0 } else { let e = fiddle_log2(largest_abs).max(-EXP_BIAS).min(63 - EXP_BIAS); let inv_multiplier = fiddle_exp2(-e + 12); let x_sign = (floats.0.to_bits() >> 31) as u64; let x = (floats_abs.0 * inv_multiplier).min(8191.0) as u64; let y_sign = (floats.1.to_bits() >> 31) as u64; let y = (floats_abs.1 * inv_multiplier).min(8191.0) as u64; let z_sign = (floats.2.to_bits() >> 31) as u64; let z = (floats_abs.2 * inv_multiplier).min(8191.0) as u64; (x_sign << 47) | (x << 34) | (y_sign << 33) | (y << 20) | (z_sign << 19) | (z << 6) | (e + EXP_BIAS) as u64 } } // Workhorse decode function, which operates on u64. #[inline(always)] fn decode_64(trifloat: u64) -> (f32, f32, f32) { // Unpack values. let x = (trifloat >> 34) & 0b111_11111_11111; let y = (trifloat >> 20) & 0b111_11111_11111; let z = (trifloat >> 6) & 0b111_11111_11111; let x_sign = ((trifloat >> 16) & 0x8000_0000) as u32; let y_sign = ((trifloat >> 2) & 0x8000_0000) as u32; let z_sign = ((trifloat << 12) & 0x8000_0000) as u32; let e = trifloat & 0b111_111; let multiplier = fiddle_exp2(e as i32 - EXP_BIAS - 12); ( f32::from_bits((x as f32 * multiplier).to_bits() | x_sign), f32::from_bits((y as f32 * multiplier).to_bits() | y_sign), f32::from_bits((z as f32 * multiplier).to_bits() | z_sign), ) } #[inline(always)] fn u64_to_bytes(n: u64) -> [u8; 6] { let a = n.to_ne_bytes(); let mut b = [0u8; 6]; if cfg!(target_endian = "big") { (&mut b[..]).copy_from_slice(&a[2..8]); } else { (&mut b[..]).copy_from_slice(&a[0..6]); } b } #[inline(always)] fn bytes_to_u64(a: [u8; 6]) -> u64 { let mut b = [0u8; 8]; if cfg!(target_endian = "big") { (&mut b[2..8]).copy_from_slice(&a[..]); } else { (&mut b[0..6]).copy_from_slice(&a[..]); } u64::from_ne_bytes(b) } #[cfg(test)] mod tests { use super::*; fn round_trip(floats: (f32, f32, f32)) -> (f32, f32, f32) { decode(encode(floats)) } #[test] fn all_zeros() { let fs = (0.0f32, 0.0f32, 0.0f32); let tri = encode_64(fs); let fs2 = decode_64(tri); assert_eq!(tri, 0); assert_eq!(fs, fs2); } #[test] fn powers_of_two() { let fs = (8.0f32, 128.0f32, 0.5f32); assert_eq!(fs, round_trip(fs)); } #[test] fn signs() { let fs1 = (1.0f32, 1.0f32, 1.0f32); let fs2 = (1.0f32, 1.0f32, -1.0f32); let fs3 = (1.0f32, -1.0f32, 1.0f32); let fs4 = (1.0f32, -1.0f32, -1.0f32); let fs5 = (-1.0f32, 1.0f32, 1.0f32); let fs6 = (-1.0f32, 1.0f32, -1.0f32); let fs7 = (-1.0f32, -1.0f32, 1.0f32); let fs8 = (-1.0f32, -1.0f32, -1.0f32); assert_eq!(fs1, round_trip(fs1)); assert_eq!(fs2, round_trip(fs2)); assert_eq!(fs3, round_trip(fs3)); assert_eq!(fs4, round_trip(fs4)); assert_eq!(fs5, round_trip(fs5)); assert_eq!(fs6, round_trip(fs6)); assert_eq!(fs7, round_trip(fs7)); assert_eq!(fs8, round_trip(fs8)); } #[test] fn accuracy() { let mut n = 1.0; for _ in 0..256 { let (x, _, _) = round_trip((n, 0.0, 0.0)); assert_eq!(n, x); n += 1.0 / 256.0; } } #[test] fn integers() { for n in -8192i32..=8192i32 { let (x, _, _) = round_trip((n as f32, 0.0, 0.0)); assert_eq!(n as f32, x); } } #[test] fn precision_floor() { let fs = (7.0f32, 8193.0f32, -1.0f32); let fsn = (-7.0f32, -8193.0f32, 1.0f32); assert_eq!((6.0, 8192.0, -0.0), round_trip(fs)); assert_eq!((-6.0, -8192.0, 0.0), round_trip(fsn)); } #[test] fn saturate() { let fs = ( 99_999_999_999_999.0, 99_999_999_999_999.0, 99_999_999_999_999.0, ); let fsn = ( -99_999_999_999_999.0, -99_999_999_999_999.0, -99_999_999_999_999.0, ); assert_eq!((MAX, MAX, MAX), round_trip(fs)); assert_eq!((MIN, MIN, MIN), round_trip(fsn)); assert_eq!((MAX, MAX, MAX), decode_64(0x7FFD_FFF7_FFFF)); assert_eq!((MIN, MIN, MIN), decode_64(0xFFFF_FFFF_FFFF)); } #[test] fn inf_saturate() { use std::f32::INFINITY; let fs = (INFINITY, 0.0, 0.0); let fsn = (-INFINITY, 0.0, 0.0); assert_eq!((MAX, 0.0, 0.0), round_trip(fs)); assert_eq!((MIN, 0.0, 0.0), round_trip(fsn)); assert_eq!(0x7FFC0000003F, encode_64(fs)); assert_eq!(0xFFFC0000003F, encode_64(fsn)); } #[test] fn partial_saturate() { let fs = (99_999_999_999_999.0, 4294967296.0, -17179869184.0); let fsn = (-99_999_999_999_999.0, 4294967296.0, -17179869184.0); assert_eq!((MAX, 4294967296.0, -17179869184.0), round_trip(fs)); assert_eq!((MIN, 4294967296.0, -17179869184.0), round_trip(fsn)); } #[test] fn smallest_value() { let fs = (MIN_POSITIVE * 1.5, MIN_POSITIVE, MIN_POSITIVE * 0.50); let fsn = (-MIN_POSITIVE * 1.5, -MIN_POSITIVE, -MIN_POSITIVE * 0.50); assert_eq!((MIN_POSITIVE, -MIN_POSITIVE, 0.0), decode_64(0x600100000)); assert_eq!((MIN_POSITIVE, MIN_POSITIVE, 0.0), round_trip(fs)); assert_eq!((-MIN_POSITIVE, -MIN_POSITIVE, -0.0), round_trip(fsn)); } #[test] fn underflow() { let fs = (MIN_POSITIVE * 0.5, -MIN_POSITIVE * 0.5, MIN_POSITIVE); assert_eq!(0x200000040, encode_64(fs)); assert_eq!((0.0, -0.0, MIN_POSITIVE), round_trip(fs)); } #[test] fn garbage_upper_bits_decode() { let fs1 = (4.0, -623.53, 12.3); let fs2 = (-63456254.2, 5235423.53, 54353.3); let fs3 = (-0.000000634, 0.00000000005, 0.00000000892); let n1 = encode_64(fs1); let n2 = encode_64(fs2); let n3 = encode_64(fs3); assert_eq!(decode_64(n1), decode_64(n1 | 0xffff_0000_0000_0000)); assert_eq!(decode_64(n2), decode_64(n2 | 0xffff_0000_0000_0000)); assert_eq!(decode_64(n3), decode_64(n3 | 0xffff_0000_0000_0000)); } #[test] #[should_panic] fn nans_01() { encode((std::f32::NAN, 1.0, -1.0)); } #[test] #[should_panic] fn nans_02() { encode((1.0, std::f32::NAN, -1.0)); } #[test] #[should_panic] fn nans_03() { encode((1.0, -1.0, std::f32::NAN)); } } ================================================ FILE: sub_crates/compact/src/shared_exp/unsigned32.rs ================================================ //! Encoding/decoding for unsigned 32-bit trifloat numbers. //! //! The encoding uses 9 bits of mantissa per number, and 5 bits for the shared //! exponent. The bit layout is [mantissa 1, mantissa 2, mantissa 3, exponent]. //! The exponent is stored as an unsigned integer with a bias of 11. //! //! The largest representable number is `2^21 - 4096`, and the smallest //! representable non-zero number is `2^-19`. //! //! Since the exponent is shared between the three values, the precision //! of all three values depends on the largest of the three. All integers //! up to 512 can be represented exactly in the largest value. use super::{fiddle_exp2, fiddle_log2}; /// Largest representable number. pub const MAX: f32 = ((1u64 << (32 - EXP_BIAS)) - (1 << (32 - EXP_BIAS - 9))) as f32; /// Smallest representable non-zero number. pub const MIN: f32 = 1.0 / (1 << (EXP_BIAS + 8)) as f32; /// Difference between 1.0 and the next largest representable number. pub const EPSILON: f32 = 1.0 / 256.0; const EXP_BIAS: i32 = 11; /// Encodes three floating point values into an unsigned 32-bit trifloat. /// /// Input floats larger than `MAX` will saturate to `MAX`, including infinity. /// Values are converted to trifloat precision by rounding down. /// /// Warning: negative values and NaN's are _not_ supported by the trifloat /// format. There are debug-only assertions in place to catch such /// values in the input floats. #[inline] pub fn encode(floats: (f32, f32, f32)) -> u32 { debug_assert!( floats.0 >= 0.0 && floats.1 >= 0.0 && floats.2 >= 0.0 && !floats.0.is_nan() && !floats.1.is_nan() && !floats.2.is_nan(), "trifloat::unsigned32::encode(): encoding to unsigned tri-floats only \ works correctly for positive, non-NaN numbers, but the numbers passed \ were: ({}, {}, {})", floats.0, floats.1, floats.2 ); let largest = floats.0.max(floats.1.max(floats.2)); if largest < MIN { return 0; } else { let e = fiddle_log2(largest).max(-EXP_BIAS).min(31 - EXP_BIAS); let inv_multiplier = fiddle_exp2(-e + 8); let x = (floats.0 * inv_multiplier).min(511.0) as u32; let y = (floats.1 * inv_multiplier).min(511.0) as u32; let z = (floats.2 * inv_multiplier).min(511.0) as u32; (x << (9 + 9 + 5)) | (y << (9 + 5)) | (z << 5) | (e + EXP_BIAS) as u32 } } /// Decodes an unsigned 32-bit trifloat into three full floating point numbers. /// /// This operation is lossless and cannot fail. #[inline] pub fn decode(trifloat: u32) -> (f32, f32, f32) { // Unpack values. let x = trifloat >> (9 + 9 + 5); let y = (trifloat >> (9 + 5)) & 0b1_1111_1111; let z = (trifloat >> 5) & 0b1_1111_1111; let e = trifloat & 0b1_1111; let multiplier = fiddle_exp2(e as i32 - EXP_BIAS - 8); ( x as f32 * multiplier, y as f32 * multiplier, z as f32 * multiplier, ) } #[cfg(test)] mod tests { use super::*; fn round_trip(floats: (f32, f32, f32)) -> (f32, f32, f32) { decode(encode(floats)) } #[test] fn all_zeros() { let fs = (0.0f32, 0.0f32, 0.0f32); let tri = encode(fs); let fs2 = decode(tri); assert_eq!(tri, 0u32); assert_eq!(fs, fs2); } #[test] fn powers_of_two() { let fs = (8.0f32, 128.0f32, 0.5f32); assert_eq!(fs, round_trip(fs)); } #[test] fn accuracy_01() { let mut n = 1.0; for _ in 0..256 { let (x, _, _) = round_trip((n, 0.0, 0.0)); assert_eq!(n, x); n += 1.0 / 256.0; } } #[test] #[should_panic] fn accuracy_02() { let mut n = 1.0; for _ in 0..512 { let (x, _, _) = round_trip((n, 0.0, 0.0)); assert_eq!(n, x); n += 1.0 / 512.0; } } #[test] fn integers() { for n in 0..=512 { let (x, _, _) = round_trip((n as f32, 0.0, 0.0)); assert_eq!(n as f32, x); } } #[test] fn precision_floor() { let fs = (7.0f32, 513.0f32, 1.0f32); assert_eq!((6.0, 512.0, 0.0), round_trip(fs)); } #[test] fn saturate() { let fs = (9999999999.0, 9999999999.0, 9999999999.0); assert_eq!((MAX, MAX, MAX), round_trip(fs)); assert_eq!((MAX, MAX, MAX), decode(0xFFFFFFFF)); } #[test] fn inf_saturate() { use std::f32::INFINITY; let fs = (INFINITY, 0.0, 0.0); assert_eq!((MAX, 0.0, 0.0), round_trip(fs)); assert_eq!(0xFF80001F, encode(fs)); } #[test] fn partial_saturate() { let fs = (9999999999.0, 4096.0, 262144.0); assert_eq!((MAX, 4096.0, 262144.0), round_trip(fs)); } #[test] fn smallest_value() { let fs = (MIN * 1.5, MIN, MIN * 0.5); assert_eq!((MIN, MIN, 0.0), round_trip(fs)); assert_eq!((MIN, MIN, 0.0), decode(0x00_80_40_00)); } #[test] fn underflow() { let fs = (MIN * 0.99, 0.0, 0.0); assert_eq!(0, encode(fs)); assert_eq!((0.0, 0.0, 0.0), round_trip(fs)); } #[test] #[should_panic] fn nans_01() { encode((std::f32::NAN, 0.0, 0.0)); } #[test] #[should_panic] fn nans_02() { encode((0.0, std::f32::NAN, 0.0)); } #[test] #[should_panic] fn nans_03() { encode((0.0, 0.0, std::f32::NAN)); } #[test] #[should_panic] fn negative_01() { encode((-1.0, 0.0, 0.0)); } #[test] #[should_panic] fn negative_02() { encode((0.0, -1.0, 0.0)); } #[test] #[should_panic] fn negative_03() { encode((0.0, 0.0, -1.0)); } #[test] fn negative_04() { encode((-0.0, -0.0, -0.0)); } } ================================================ FILE: sub_crates/compact/src/shared_exp/unsigned40.rs ================================================ //! Encoding/decoding for unsigned 40-bit trifloat numbers. //! //! The encoding uses 11 bits of mantissa per number, and 7 bits for the shared //! exponent. The bit layout is [mantissa 1, mantissa 2, mantissa 3, exponent]. //! The exponent is stored as an unsigned integer with a bias of 32. //! //! The largest representable number is just under `2^96`, and the smallest //! representable non-zero number is `2^-42`. //! //! Since the exponent is shared between the three values, the precision //! of all three values depends on the largest of the three. All integers //! up to 2048 can be represented exactly in the largest value. use super::{fiddle_exp2, fiddle_log2}; /// Largest representable number. pub const MAX: f32 = ((1u128 << (128 - EXP_BIAS)) - (1 << (128 - EXP_BIAS - 11))) as f32; /// Smallest representable non-zero number. pub const MIN: f32 = 1.0 / (1u128 << (EXP_BIAS + 10)) as f32; /// Difference between 1.0 and the next largest representable number. pub const EPSILON: f32 = 1.0 / 1024.0; const EXP_BIAS: i32 = 32; /// Encodes three floating point values into an unsigned 40-bit trifloat. /// /// Input floats larger than `MAX` will saturate to `MAX`, including infinity. /// Values are converted to trifloat precision by rounding down. /// /// Warning: negative values and NaN's are _not_ supported by the trifloat /// format. There are debug-only assertions in place to catch such /// values in the input floats. #[inline] pub fn encode(floats: (f32, f32, f32)) -> [u8; 5] { u64_to_bytes(encode_64(floats)) } /// Decodes an unsigned 40-bit trifloat into three full floating point numbers. /// /// This operation is lossless and cannot fail. #[inline] pub fn decode(trifloat: [u8; 5]) -> (f32, f32, f32) { decode_64(bytes_to_u64(trifloat)) } // Workhorse encode function, which operates on u64. #[inline(always)] fn encode_64(floats: (f32, f32, f32)) -> u64 { debug_assert!( floats.0 >= 0.0 && floats.1 >= 0.0 && floats.2 >= 0.0 && !floats.0.is_nan() && !floats.1.is_nan() && !floats.2.is_nan(), "trifloat::unsigned32::encode(): encoding to unsigned tri-floats only \ works correctly for positive, non-NaN numbers, but the numbers passed \ were: ({}, {}, {})", floats.0, floats.1, floats.2 ); let largest = floats.0.max(floats.1.max(floats.2)); if largest < MIN { return 0; } else { let e = fiddle_log2(largest).max(-EXP_BIAS).min(127 - EXP_BIAS); let inv_multiplier = fiddle_exp2(-e + 10); let x = (floats.0 * inv_multiplier).min(2047.0) as u64; let y = (floats.1 * inv_multiplier).min(2047.0) as u64; let z = (floats.2 * inv_multiplier).min(2047.0) as u64; (x << (11 + 11 + 7)) | (y << (11 + 7)) | (z << 7) | (e + EXP_BIAS) as u64 } } // Workhorse decode function, which operates on u64. #[inline(always)] fn decode_64(trifloat: u64) -> (f32, f32, f32) { // Unpack values. let x = trifloat >> (11 + 11 + 7); let y = (trifloat >> (11 + 7)) & 0b111_1111_1111; let z = (trifloat >> 7) & 0b111_1111_1111; let e = trifloat & 0b111_1111; let multiplier = fiddle_exp2(e as i32 - EXP_BIAS - 10); ( x as f32 * multiplier, y as f32 * multiplier, z as f32 * multiplier, ) } #[inline(always)] fn u64_to_bytes(n: u64) -> [u8; 5] { let a = n.to_ne_bytes(); let mut b = [0u8; 5]; if cfg!(target_endian = "big") { (&mut b[..]).copy_from_slice(&a[3..8]); } else { (&mut b[..]).copy_from_slice(&a[0..5]); } b } #[inline(always)] fn bytes_to_u64(a: [u8; 5]) -> u64 { let mut b = [0u8; 8]; if cfg!(target_endian = "big") { (&mut b[3..8]).copy_from_slice(&a[..]); } else { (&mut b[0..5]).copy_from_slice(&a[..]); } u64::from_ne_bytes(b) } #[cfg(test)] mod tests { use super::*; fn round_trip(floats: (f32, f32, f32)) -> (f32, f32, f32) { decode(encode(floats)) } #[test] fn all_zeros() { let fs = (0.0f32, 0.0f32, 0.0f32); let tri = encode_64(fs); let fs2 = decode_64(tri); assert_eq!(tri, 0u64); assert_eq!(fs, fs2); } #[test] fn powers_of_two() { let fs = (8.0f32, 128.0f32, 0.5f32); assert_eq!(fs, round_trip(fs)); } #[test] fn accuracy_01() { let mut n = 1.0; for _ in 0..1024 { let (x, _, _) = round_trip((n, 0.0, 0.0)); assert_eq!(n, x); n += 1.0 / 1024.0; } } #[test] #[should_panic] fn accuracy_02() { let mut n = 1.0; for _ in 0..2048 { let (x, _, _) = round_trip((n, 0.0, 0.0)); assert_eq!(n, x); n += 1.0 / 2048.0; } } #[test] fn integers() { for n in 0..=2048 { let (x, _, _) = round_trip((n as f32, 0.0, 0.0)); assert_eq!(n as f32, x); } } #[test] fn precision_floor() { let fs = (7.0f32, 2049.0f32, 1.0f32); assert_eq!((6.0, 2048.0, 0.0), round_trip(fs)); } #[test] fn saturate() { let fs = (1.0e+30, 1.0e+30, 1.0e+30); assert_eq!((MAX, MAX, MAX), round_trip(fs)); assert_eq!((MAX, MAX, MAX), decode_64(0xff_ffff_ffff)); } #[test] fn inf_saturate() { use std::f32::INFINITY; let fs = (INFINITY, 0.0, 0.0); assert_eq!((MAX, 0.0, 0.0), round_trip(fs)); assert_eq!(0xffe000007f, encode_64(fs)); } #[test] fn partial_saturate() { let fs = ( 1.0e+30, (1u128 << (128 - EXP_BIAS - 11)) as f32, (1u128 << (128 - EXP_BIAS - 12)) as f32, ); assert_eq!( (MAX, (1u128 << (128 - EXP_BIAS - 11)) as f32, 0.0), round_trip(fs) ); } #[test] fn smallest_value() { let fs = (MIN * 1.5, MIN, MIN * 0.5); assert_eq!((MIN, MIN, 0.0), round_trip(fs)); assert_eq!((MIN, MIN, 0.0), decode_64(0x20_04_00_00)); } #[test] fn underflow() { let fs = (MIN * 0.99, 0.0, 0.0); assert_eq!(0, encode_64(fs)); assert_eq!((0.0, 0.0, 0.0), round_trip(fs)); } #[test] #[should_panic] fn nans_01() { encode((std::f32::NAN, 0.0, 0.0)); } #[test] #[should_panic] fn nans_02() { encode((0.0, std::f32::NAN, 0.0)); } #[test] #[should_panic] fn nans_03() { encode((0.0, 0.0, std::f32::NAN)); } #[test] #[should_panic] fn negative_01() { encode((-1.0, 0.0, 0.0)); } #[test] #[should_panic] fn negative_02() { encode((0.0, -1.0, 0.0)); } #[test] #[should_panic] fn negative_03() { encode((0.0, 0.0, -1.0)); } #[test] fn negative_04() { encode((-0.0, -0.0, -0.0)); } } ================================================ FILE: sub_crates/compact/src/unit_vec/mod.rs ================================================ //! 3d unit vector formats. pub mod oct32; ================================================ FILE: sub_crates/compact/src/unit_vec/oct32.rs ================================================ //! Encoding/decoding for a 32-bit representation of unit 3d vectors. //! //! Follows the Oct32 encoding specified in the paper "A Survey //! of Efficient Representations for Independent Unit Vectors" by //! Cigolle et al. const STEP_SIZE: f32 = 1.0 / STEPS; const STEPS: f32 = ((1 << (16 - 1)) - 1) as f32; /// Encodes a vector of three floats to the oct32 format. /// /// The input vector does not need to be normalized--only the direction /// matters to the encoding process, not the length. #[inline] pub fn encode(vec: (f32, f32, f32)) -> u32 { let (u, v) = vec3_to_oct(vec); ((to_snorm_16(u) as u32) << 16) | to_snorm_16(v) as u32 } /// Encodes a vector of three floats to the oct32 format. /// /// This is the same as `encode()` except that it is slower and encodes /// with slightly better precision. pub fn encode_precise(vec: (f32, f32, f32)) -> u32 { #[inline(always)] fn dot_norm(a: (f32, f32, f32), b: (f32, f32, f32)) -> f64 { let l = ((a.0 as f64 * a.0 as f64) + (a.1 as f64 * a.1 as f64) + (a.2 as f64 * a.2 as f64)) .sqrt(); ((a.0 as f64 * b.0 as f64) + (a.1 as f64 * b.1 as f64) + (a.2 as f64 * b.2 as f64)) / l } // Calculate the initial floored version. let s = { let mut s = vec3_to_oct(vec); // Remap to the square. s.0 = (s.0.max(-1.0).min(1.0) * STEPS).floor() * STEP_SIZE; s.1 = (s.1.max(-1.0).min(1.0) * STEPS).floor() * STEP_SIZE; s }; // Test all combinations of floor and ceil and keep the best. // Note that at +/- 1, this will exit the square, but that // will be a worse encoding and never win. let mut best_rep = s; let mut max_dot = 0.0; for &(i, j) in &[ (0.0, 0.0), (0.0, STEP_SIZE), (STEP_SIZE, 0.0), (STEP_SIZE, STEP_SIZE), ] { let candidate = (s.0 + i, s.1 + j); let oct = oct_to_vec3(candidate); let dot = dot_norm(oct, vec); if dot > max_dot { best_rep = candidate; max_dot = dot; } } ((to_snorm_16(best_rep.0) as u32) << 16) | to_snorm_16(best_rep.1) as u32 } /// Decodes from an oct32 to a vector of three floats. /// /// The returned vector will not generally be normalized. Code that /// needs a normalized vector should normalize the returned vector. #[inline] pub fn decode(n: u32) -> (f32, f32, f32) { oct_to_vec3((from_snorm_16((n >> 16) as u16), from_snorm_16(n as u16))) } #[inline(always)] fn vec3_to_oct(vec: (f32, f32, f32)) -> (f32, f32) { let l1_norm = vec.0.abs() + vec.1.abs() + vec.2.abs(); let u = vec.0 / l1_norm; let v = vec.1 / l1_norm; if vec.2 > 0.0 { (u, v) } else { ((1.0 - v.abs()) * sign(vec.0), (1.0 - u.abs()) * sign(vec.1)) } } #[inline(always)] fn oct_to_vec3(oct: (f32, f32)) -> (f32, f32, f32) { let vec2 = 1.0 - (oct.0.abs() + oct.1.abs()); if vec2 < 0.0 { ( (1.0 - oct.1.abs()) * sign(oct.0), (1.0 - oct.0.abs()) * sign(oct.1), vec2, ) } else { (oct.0, oct.1, vec2) } } #[inline(always)] fn to_snorm_16(n: f32) -> u16 { (n * STEPS).round() as i16 as u16 } #[inline(always)] fn from_snorm_16(n: u16) -> f32 { f32::from(n as i16) * STEP_SIZE } #[inline(always)] fn sign(n: f32) -> f32 { if n < 0.0 { -1.0 } else { 1.0 } } #[cfg(test)] mod tests { use super::*; #[test] fn axis_directions() { let px = (1.0, 0.0, 0.0); let px_oct = encode(px); let px_octp = encode_precise(px); let nx = (-1.0, 0.0, 0.0); let nx_oct = encode(nx); let nx_octp = encode_precise(nx); let py = (0.0, 1.0, 0.0); let py_oct = encode(py); let py_octp = encode_precise(py); let ny = (0.0, -1.0, 0.0); let ny_oct = encode(ny); let ny_octp = encode_precise(ny); let pz = (0.0, 0.0, 1.0); let pz_oct = encode(pz); let pz_octp = encode_precise(pz); let nz = (0.0, 0.0, -1.0); let nz_oct = encode(nz); let nz_octp = encode_precise(nz); assert_eq!(px, decode(px_oct)); assert_eq!(nx, decode(nx_oct)); assert_eq!(py, decode(py_oct)); assert_eq!(ny, decode(ny_oct)); assert_eq!(pz, decode(pz_oct)); assert_eq!(nz, decode(nz_oct)); assert_eq!(px, decode(px_octp)); assert_eq!(nx, decode(nx_octp)); assert_eq!(py, decode(py_octp)); assert_eq!(ny, decode(ny_octp)); assert_eq!(pz, decode(pz_octp)); assert_eq!(nz, decode(nz_octp)); } } ================================================ FILE: sub_crates/compact/tests/proptest_tests.rs ================================================ #[macro_use] extern crate proptest; use compact::unit_vec::oct32::{decode, encode, encode_precise}; use proptest::test_runner::Config; /// Calculates the cosine of the angle between the two vectors, /// and checks to see if it's greater than the passed cos. fn cos_gt(a: (f32, f32, f32), b: (f32, f32, f32), cos: f64) -> bool { fn normalize(v: (f32, f32, f32)) -> (f64, f64, f64) { let norm = ((v.0 as f64 * v.0 as f64) + (v.1 as f64 * v.1 as f64) + (v.2 as f64 * v.2 as f64)) .sqrt(); (v.0 as f64 / norm, v.1 as f64 / norm, v.2 as f64 / norm) } let a = normalize(a); let b = normalize(b); let cos2 = (a.0 * b.0) + (a.1 * b.1) + (a.2 * b.2); let r = cos2 > cos as f64; if !r { println!("cos: {}, left: {:?}, right: {:?}", cos2, a, b); } r } /// Checks if the difference between the two vectors on all axes is /// less than delta. Both vectors are L1-normalized first. fn l1_delta_lt(a: (f32, f32, f32), b: (f32, f32, f32), delta: f32) -> bool { fn l1_normalize(v: (f32, f32, f32)) -> (f32, f32, f32) { let l1_norm = v.0.abs() + v.1.abs() + v.2.abs(); (v.0 / l1_norm, v.1 / l1_norm, v.2 / l1_norm) } let a = l1_normalize(a); let b = l1_normalize(b); let rx = (a.0 - b.0).abs() < delta; let ry = (a.1 - b.1).abs() < delta; let rz = (a.2 - b.2).abs() < delta; let r = rx && ry && rz; if !r { println!("left: {:?}, right: {:?}", a, b); } r } proptest! { #![proptest_config(Config::with_cases(4096))] #[test] fn oct32_pt_roundtrip_angle_precision(v in (-1.0f32..1.0, -1.0f32..1.0, -1.0f32..1.0)) { let oct = encode(v); let octp = encode_precise(v); // Check if the angle between the original and the roundtrip // is less than 0.004 degrees assert!(cos_gt(v, decode(oct), 0.9999999976)); // Check if the angle between the original and the roundtrip // is less than 0.003 degrees assert!(cos_gt(v, decode(octp), 0.9999999986)); } #[test] fn oct32_pt_roundtrip_component_precision(v in (-1.0f32..1.0, -1.0f32..1.0, -1.0f32..1.0)) { let oct = encode(v); let octp = encode_precise(v); assert!(l1_delta_lt(v, decode(oct), 0.00005)); assert!(l1_delta_lt(v, decode(octp), 0.00003)); } } ================================================ FILE: sub_crates/halton/Cargo.toml ================================================ [package] name = "halton" version = "0.1.0" authors = ["Nathan Vegdahl "] edition = "2018" license = "MIT" build = "build.rs" [lib] name = "halton" path = "src/lib.rs" ================================================ FILE: sub_crates/halton/LICENSE.md ================================================ The code in this project is adapted from code written by Leonhard Gruenschloss: Copyright (c) 2012 Leonhard Gruenschloss (leonhard@gruenschloss.org) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: sub_crates/halton/build.rs ================================================ // Copyright (c) 2012 Leonhard Gruenschloss (leonhard@gruenschloss.org) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies // of the Software, and to permit persons to whom the Software is furnished to do // so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // // Adapted from Python to Rust and to generate Rust instead of C by Nathan Vegdahl // Generate Rust code for evaluating Halton points with Faure-permutations for different bases. use std::{env, fs::File, io::Write, path::Path}; /// How many components to generate. const NUM_DIMENSIONS: usize = 128; fn main() { let out_dir = env::var("OUT_DIR").unwrap(); let dest_path = Path::new(&out_dir).join("halton.rs"); let mut f = File::create(&dest_path).unwrap(); // Init prime number array. let primes = { let mut primes = Vec::new(); let mut candidate = 1; for _ in 0..NUM_DIMENSIONS { loop { candidate += 1; if is_prime(candidate) { primes.push(candidate); break; } } } primes }; // Init Faure permutations. let faure = { let mut faure: Vec> = Vec::new(); for b in 0..(primes.last().unwrap() + 1) { let perm = get_faure_permutation(&faure, b); faure.push(perm); } faure }; // Write the beginning bits of the file f.write_all( format!( r#" // Copyright (c) 2012 Leonhard Gruenschloss (leonhard@gruenschloss.org) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies // of the Software, and to permit persons to whom the Software is furnished to do // so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // This file is automatically generated. // Compute points of the Halton sequence with with Faure-permutations for different bases. pub const MAX_DIMENSION: u32 = {}; "#, NUM_DIMENSIONS ) .as_bytes(), ) .unwrap(); // Write the sampling function f.write_all( format!( r#" pub fn sample(index: u32, dimension: u32) -> f32 {{ let mut index = index; match dimension {{"# ) .as_bytes(), ) .unwrap(); // Write the special-cased first dimension f.write_all( format!( r#" // Special case: radical inverse in base 2, with direct bit reversal. 0 => {{ index = (index << 16) | (index >> 16); index = ((index & 0x00ff00ff) << 8) | ((index & 0xff00ff00) >> 8); index = ((index & 0x0f0f0f0f) << 4) | ((index & 0xf0f0f0f0) >> 4); index = ((index & 0x33333333) << 2) | ((index & 0xcccccccc) >> 2); index = ((index & 0x55555555) << 1) | ((index & 0xaaaaaaaa) >> 1); return (index as f32) * (1.0 / ((1u64 << 32) as f32)); }}"#, ) .as_bytes(), ) .unwrap(); // The rest of the dimensions. for i in 1..NUM_DIMENSIONS { let base = primes[i]; // Based on the permutation table size, we process multiple digits at once. let mut digits = 1; let mut pow_base = base; while pow_base * base <= 500 { // Maximum permutation table size. pow_base *= base; digits += 1; } let mut max_power = pow_base; let mut powers = Vec::new(); while (max_power * pow_base) < (1 << 32) { // 32-bit unsigned precision powers.push(max_power); max_power *= pow_base; } // Build the permutation table. let perm = (0..pow_base) .map(|j| invert(&faure, base, j, digits)) .collect::>(); let perm_string = { let mut perm_string = String::new(); for i in perm.iter() { let s = format!("{}, ", i); perm_string.push_str(&s); } perm_string }; let mut power = max_power / pow_base; f.write_all( format!( r#" {} => {{ static PERM{}: [u16; {}] = [{}];"#, i, base, perm.len(), perm_string ) .as_bytes(), ) .unwrap(); f.write_all( format!( r#" return unsafe {{( *PERM{}.get_unchecked((index % {}) as usize) as u32 * {}"#, base, pow_base, power ) .as_bytes(), ) .unwrap(); // Advance to next set of digits. let mut div = 1; while power / pow_base > 1 { div *= pow_base; power /= pow_base; f.write_all( format!( r#" + *PERM{}.get_unchecked(((index / {}) % {}) as usize) as u32 * {}"#, base, div, pow_base, power ) .as_bytes(), ) .unwrap(); } f.write_all( format!( r#" + *PERM{}.get_unchecked(((index / {}) % {}) as usize) as u32 )}} as f32 * (0.999999940395355224609375f32 / ({}u32 as f32)); // Results in [0,1). }} "#, base, div * pow_base, pow_base, max_power ) .as_bytes(), ) .unwrap(); } f.write_all( format!( r#" _ => panic!("Halton sampling: exceeded max dimensions."), }} }} "# ) .as_bytes(), ) .unwrap(); } /// Check primality. Not optimized, since it's not performance-critical. fn is_prime(p: usize) -> bool { for i in 2..p { if (p % i) == 0 { return false; } } return true; } /// Computes the Faure digit permutation for 0, ..., b - 1. fn get_faure_permutation(faure: &Vec>, b: usize) -> Vec { if b < 2 { return vec![0]; } else if b == 2 { return vec![0, 1]; } else if (b & 1) != 0 { // odd let c = (b - 1) / 2; return (0..b) .map(|i| { if i == c { return c; } let f: usize = faure[b - 1][i - ((i > c) as usize)]; f + ((f >= c) as usize) }) .collect(); } else { // even let c = b / 2; return (0..b) .map(|i| { if i < c { 2 * faure[c][i] } else { 2 * faure[c][i - c] + 1 } }) .collect(); } } /// Compute the radical inverse with Faure permutations. fn invert(faure: &Vec>, base: usize, mut index: usize, digits: usize) -> usize { let mut result = 0; for _ in 0..digits { let remainder = index % base; index = index / base; result = result * base + faure[base][remainder]; } return result; } ================================================ FILE: sub_crates/halton/src/lib.rs ================================================ #![allow(dead_code)] #![allow(unused_parens)] #![allow(clippy::cast_lossless)] #![allow(clippy::excessive_precision)] #![allow(clippy::unreadable_literal)] #![allow(clippy::needless_return)] // Include the file generated by the build.rs script include!(concat!(env!("OUT_DIR"), "/halton.rs")); ================================================ FILE: sub_crates/math3d/Cargo.toml ================================================ [package] name = "math3d" version = "0.1.0" authors = ["Nathan Vegdahl "] edition = "2018" license = "MIT, Apache 2.0" [lib] name = "math3d" path = "src/lib.rs" # Local crate dependencies [dependencies] glam = "0.15" approx = "0.4" ================================================ FILE: sub_crates/math3d/LICENSE.md ================================================ Copyright (c) 2020 Nathan Vegdahl This project is licensed under either of * MIT license (licenses/MIT.txt or http://opensource.org/licenses/MIT) * Apache License, Version 2.0, (licenses/Apache-2.0.txt or http://www.apache.org/licenses/LICENSE-2.0) at your option. ================================================ FILE: sub_crates/math3d/src/lib.rs ================================================ #![allow(dead_code)] mod normal; mod point; mod transform; mod vector; pub use self::{normal::Normal, point::Point, transform::Transform, vector::Vector}; /// Trait for calculating dot products. pub trait DotProduct { fn dot(self, other: Self) -> f32; } #[inline] pub fn dot(a: T, b: T) -> f32 { a.dot(b) } /// Trait for calculating cross products. pub trait CrossProduct { fn cross(self, other: Self) -> Self; } #[inline] pub fn cross(a: T, b: T) -> T { a.cross(b) } ================================================ FILE: sub_crates/math3d/src/normal.rs ================================================ #![allow(dead_code)] use std::{ cmp::PartialEq, ops::{Add, Div, Mul, Neg, Sub}, }; use glam::Vec3A; use super::{CrossProduct, DotProduct, Transform, Vector}; /// A surface normal in 3d homogeneous space. #[derive(Debug, Copy, Clone)] pub struct Normal { pub co: Vec3A, } impl Normal { #[inline(always)] pub fn new(x: f32, y: f32, z: f32) -> Normal { Normal { co: Vec3A::new(x, y, z), } } #[inline(always)] pub fn length(&self) -> f32 { self.co.length() } #[inline(always)] pub fn length2(&self) -> f32 { self.co.length_squared() } #[inline(always)] pub fn normalized(&self) -> Normal { Normal { co: self.co.normalize(), } } #[inline(always)] pub fn into_vector(self) -> Vector { Vector { co: self.co } } #[inline(always)] pub fn get_n(&self, n: usize) -> f32 { match n { 0 => self.x(), 1 => self.y(), 2 => self.z(), _ => panic!("Attempt to access dimension beyond z."), } } #[inline(always)] pub fn x(&self) -> f32 { self.co[0] } #[inline(always)] pub fn y(&self) -> f32 { self.co[1] } #[inline(always)] pub fn z(&self) -> f32 { self.co[2] } #[inline(always)] pub fn set_x(&mut self, x: f32) { self.co[0] = x; } #[inline(always)] pub fn set_y(&mut self, y: f32) { self.co[1] = y; } #[inline(always)] pub fn set_z(&mut self, z: f32) { self.co[2] = z; } } impl PartialEq for Normal { #[inline(always)] fn eq(&self, other: &Normal) -> bool { self.co == other.co } } impl Add for Normal { type Output = Normal; #[inline(always)] fn add(self, other: Normal) -> Normal { Normal { co: self.co + other.co, } } } impl Sub for Normal { type Output = Normal; #[inline(always)] fn sub(self, other: Normal) -> Normal { Normal { co: self.co - other.co, } } } impl Mul for Normal { type Output = Normal; #[inline(always)] fn mul(self, other: f32) -> Normal { Normal { co: self.co * other, } } } impl Mul for Normal { type Output = Normal; #[inline] fn mul(self, other: Transform) -> Normal { Normal { co: other.0.matrix3.inverse().transpose().mul_vec3a(self.co), } } } impl Div for Normal { type Output = Normal; #[inline(always)] fn div(self, other: f32) -> Normal { Normal { co: self.co / other, } } } impl Neg for Normal { type Output = Normal; #[inline(always)] fn neg(self) -> Normal { Normal { co: self.co * -1.0 } } } impl DotProduct for Normal { #[inline(always)] fn dot(self, other: Normal) -> f32 { self.co.dot(other.co) } } impl CrossProduct for Normal { #[inline] fn cross(self, other: Normal) -> Normal { Normal { co: self.co.cross(other.co), } } } #[cfg(test)] mod tests { use super::super::{CrossProduct, DotProduct, Transform}; use super::*; use approx::assert_ulps_eq; #[test] fn add() { let v1 = Normal::new(1.0, 2.0, 3.0); let v2 = Normal::new(1.5, 4.5, 2.5); let v3 = Normal::new(2.5, 6.5, 5.5); assert_eq!(v3, v1 + v2); } #[test] fn sub() { let v1 = Normal::new(1.0, 2.0, 3.0); let v2 = Normal::new(1.5, 4.5, 2.5); let v3 = Normal::new(-0.5, -2.5, 0.5); assert_eq!(v3, v1 - v2); } #[test] fn mul_scalar() { let v1 = Normal::new(1.0, 2.0, 3.0); let v2 = 2.0; let v3 = Normal::new(2.0, 4.0, 6.0); assert_eq!(v3, v1 * v2); } #[test] fn mul_matrix_1() { let n = Normal::new(1.0, 2.5, 4.0); let m = Transform::new_from_values( 1.0, 2.0, 2.0, 1.5, 3.0, 6.0, 7.0, 8.0, 9.0, 2.0, 11.0, 12.0, ); let nm = n * m; let nm2 = Normal::new(-4.0625, 1.78125, -0.03125); for i in 0..3 { assert_ulps_eq!(nm.co[i], nm2.co[i], max_ulps = 4); } } #[test] fn div() { let v1 = Normal::new(1.0, 2.0, 3.0); let v2 = 2.0; let v3 = Normal::new(0.5, 1.0, 1.5); assert_eq!(v3, v1 / v2); } #[test] fn length() { let n = Normal::new(1.0, 2.0, 3.0); assert!((n.length() - 3.7416573867739413).abs() < 0.000001); } #[test] fn length2() { let n = Normal::new(1.0, 2.0, 3.0); assert_eq!(n.length2(), 14.0); } #[test] fn normalized() { let n1 = Normal::new(1.0, 2.0, 3.0); let n2 = Normal::new(0.2672612419124244, 0.5345224838248488, 0.8017837257372732); let n3 = n1.normalized(); assert!((n3.x() - n2.x()).abs() < 0.000001); assert!((n3.y() - n2.y()).abs() < 0.000001); assert!((n3.z() - n2.z()).abs() < 0.000001); } #[test] fn dot_test() { let v1 = Normal::new(1.0, 2.0, 3.0); let v2 = Normal::new(1.5, 4.5, 2.5); let v3 = 18.0f32; assert_eq!(v3, v1.dot(v2)); } #[test] fn cross_test() { let v1 = Normal::new(1.0, 0.0, 0.0); let v2 = Normal::new(0.0, 1.0, 0.0); let v3 = Normal::new(0.0, 0.0, 1.0); assert_eq!(v3, v1.cross(v2)); } } ================================================ FILE: sub_crates/math3d/src/point.rs ================================================ #![allow(dead_code)] use std::{ cmp::PartialEq, ops::{Add, Mul, Sub}, }; use glam::Vec3A; use super::{Transform, Vector}; /// A position in 3d homogeneous space. #[derive(Debug, Copy, Clone)] pub struct Point { pub co: Vec3A, } impl Point { #[inline(always)] pub fn new(x: f32, y: f32, z: f32) -> Point { Point { co: Vec3A::new(x, y, z), } } #[inline(always)] pub fn min(&self, other: Point) -> Point { let n1 = self; let n2 = other; Point { co: n1.co.min(n2.co), } } #[inline(always)] pub fn max(&self, other: Point) -> Point { let n1 = self; let n2 = other; Point { co: n1.co.max(n2.co), } } #[inline(always)] pub fn into_vector(self) -> Vector { Vector { co: self.co } } #[inline(always)] pub fn get_n(&self, n: usize) -> f32 { match n { 0 => self.x(), 1 => self.y(), 2 => self.z(), _ => panic!("Attempt to access dimension beyond z."), } } #[inline(always)] pub fn x(&self) -> f32 { self.co[0] } #[inline(always)] pub fn y(&self) -> f32 { self.co[1] } #[inline(always)] pub fn z(&self) -> f32 { self.co[2] } #[inline(always)] pub fn set_x(&mut self, x: f32) { self.co[0] = x; } #[inline(always)] pub fn set_y(&mut self, y: f32) { self.co[1] = y; } #[inline(always)] pub fn set_z(&mut self, z: f32) { self.co[2] = z; } } impl PartialEq for Point { #[inline(always)] fn eq(&self, other: &Point) -> bool { self.co == other.co } } impl Add for Point { type Output = Point; #[inline(always)] fn add(self, other: Vector) -> Point { Point { co: self.co + other.co, } } } impl Sub for Point { type Output = Vector; #[inline(always)] fn sub(self, other: Point) -> Vector { Vector { co: self.co - other.co, } } } impl Sub for Point { type Output = Point; #[inline(always)] fn sub(self, other: Vector) -> Point { Point { co: self.co - other.co, } } } impl Mul for Point { type Output = Point; #[inline] fn mul(self, other: Transform) -> Point { Point { co: other.0.transform_point3a(self.co), } } } #[cfg(test)] mod tests { use super::super::{Transform, Vector}; use super::*; #[test] fn add() { let p1 = Point::new(1.0, 2.0, 3.0); let v1 = Vector::new(1.5, 4.5, 2.5); let p2 = Point::new(2.5, 6.5, 5.5); assert_eq!(p2, p1 + v1); } #[test] fn sub() { let p1 = Point::new(1.0, 2.0, 3.0); let p2 = Point::new(1.5, 4.5, 2.5); let v1 = Vector::new(-0.5, -2.5, 0.5); assert_eq!(v1, p1 - p2); } #[test] fn mul_matrix_1() { let p = Point::new(1.0, 2.5, 4.0); let m = Transform::new_from_values( 1.0, 2.0, 2.0, 1.5, 3.0, 6.0, 7.0, 8.0, 9.0, 2.0, 11.0, 12.0, ); let pm = Point::new(15.5, 54.0, 70.0); assert_eq!(p * m, pm); } #[test] fn mul_matrix_2() { let p = Point::new(1.0, 2.5, 4.0); let m = Transform::new_from_values( 1.0, 2.0, 2.0, 1.5, 3.0, 6.0, 7.0, 8.0, 9.0, 2.0, 11.0, 12.0, ); let pm = Point::new(15.5, 54.0, 70.0); assert_eq!(p * m, pm); } #[test] fn mul_matrix_3() { // Make sure matrix multiplication composes the way one would expect let p = Point::new(1.0, 2.5, 4.0); let m1 = Transform::new_from_values( 1.0, 2.0, 2.0, 1.5, 3.0, 6.0, 7.0, 8.0, 9.0, 2.0, 11.0, 12.0, ); let m2 = Transform::new_from_values(4.0, 1.0, 2.0, 3.5, 3.0, 6.0, 5.0, 2.0, 2.0, 2.0, 4.0, 12.0); println!("{:?}", m1 * m2); let pmm1 = p * (m1 * m2); let pmm2 = (p * m1) * m2; assert!((pmm1 - pmm2).length2() <= 0.00001); // Assert pmm1 and pmm2 are roughly equal } } ================================================ FILE: sub_crates/math3d/src/transform.rs ================================================ #![allow(dead_code)] use std::ops::{Add, Mul}; use approx::relative_eq; use glam::{Affine3A, Mat3, Mat4, Vec3}; use super::Point; /// A 4x3 affine transform matrix, used for transforms. #[derive(Debug, Copy, Clone, PartialEq)] pub struct Transform(pub Affine3A); impl Transform { /// Creates a new identity matrix #[inline] pub fn new() -> Transform { Transform(Affine3A::IDENTITY) } /// Creates a new matrix with the specified values: /// a b c d /// e f g h /// i j k l /// m n o p #[inline] #[allow(clippy::many_single_char_names)] #[allow(clippy::too_many_arguments)] pub fn new_from_values( a: f32, b: f32, c: f32, d: f32, e: f32, f: f32, g: f32, h: f32, i: f32, j: f32, k: f32, l: f32, ) -> Transform { Transform(Affine3A::from_mat3_translation( Mat3::from_cols(Vec3::new(a, e, i), Vec3::new(b, f, j), Vec3::new(c, g, k)), Vec3::new(d, h, l), )) } #[inline] pub fn from_location(loc: Point) -> Transform { Transform(Affine3A::from_translation(loc.co.into())) } /// Returns whether the matrices are approximately equal to each other. /// Each corresponding element in the matrices cannot have a relative /// error exceeding epsilon. #[inline] pub fn aprx_eq(&self, other: Transform, epsilon: f32) -> bool { let mut eq = true; for c in 0..3 { for r in 0..3 { let a = self.0.matrix3.col(c)[r]; let b = other.0.matrix3.col(c)[r]; eq &= relative_eq!(a, b, epsilon = epsilon); } } for i in 0..3 { let a = self.0.translation[i]; let b = other.0.translation[i]; eq &= relative_eq!(a, b, epsilon = epsilon); } eq } /// Returns the inverse of the Matrix #[inline] pub fn inverse(&self) -> Transform { Transform(self.0.inverse()) } } impl Default for Transform { fn default() -> Self { Self::new() } } /// Multiply two matrices together impl Mul for Transform { type Output = Self; #[inline] fn mul(self, other: Self) -> Self { Self(other.0 * self.0) } } /// Multiply a matrix by a f32 impl Mul for Transform { type Output = Self; #[inline] fn mul(self, other: f32) -> Self { Self(Affine3A::from_mat4(Mat4::from(self.0) * other)) } } /// Add two matrices together impl Add for Transform { type Output = Self; #[inline] fn add(self, other: Self) -> Self { Self(Affine3A::from_mat4( Mat4::from(self.0) + Mat4::from(other.0), )) } } #[cfg(test)] mod tests { use super::*; #[test] fn equality_test() { let a = Transform::new(); let b = Transform::new(); let c = Transform::new_from_values(1.1, 0.0, 0.0, 0.0, 0.0, 1.1, 0.0, 0.0, 0.0, 0.0, 1.1, 0.0); assert_eq!(a, b); assert!(a != c); } #[test] fn approximate_equality_test() { let a = Transform::new(); let b = Transform::new_from_values( 1.000001, 0.0, 0.0, 0.0, 0.0, 1.000001, 0.0, 0.0, 0.0, 0.0, 1.000001, 0.0, ); let c = Transform::new_from_values( 1.000003, 0.0, 0.0, 0.0, 0.0, 1.000003, 0.0, 0.0, 0.0, 0.0, 1.000003, 0.0, ); let d = Transform::new_from_values( -1.000001, 0.0, 0.0, 0.0, 0.0, -1.000001, 0.0, 0.0, 0.0, 0.0, -1.000001, 0.0, ); assert!(a.aprx_eq(b, 0.000001)); assert!(!a.aprx_eq(c, 0.000001)); assert!(!a.aprx_eq(d, 0.000001)); } #[test] fn multiply_test() { let a = Transform::new_from_values( 1.0, 2.0, 2.0, 1.5, 3.0, 6.0, 7.0, 8.0, 9.0, 2.0, 11.0, 12.0, ); let b = Transform::new_from_values( 1.0, 5.0, 9.0, 13.0, 2.0, 6.0, 10.0, 14.0, 3.0, 7.0, 11.0, 15.0, ); let c = Transform::new_from_values( 97.0, 50.0, 136.0, 162.5, 110.0, 60.0, 156.0, 185.0, 123.0, 70.0, 176.0, 207.5, ); assert_eq!(a * b, c); } #[test] fn inverse_test() { let a = Transform::new_from_values( 1.0, 0.33, 0.0, -2.0, 0.0, 1.0, 0.0, 0.0, 2.1, 0.7, 1.3, 0.0, ); let b = a.inverse(); let c = Transform::new(); assert!((dbg!(a * b)).aprx_eq(dbg!(c), 0.0000001)); } } ================================================ FILE: sub_crates/math3d/src/vector.rs ================================================ #![allow(dead_code)] use std::{ cmp::PartialEq, ops::{Add, Div, Mul, Neg, Sub}, }; use glam::Vec3A; use super::{CrossProduct, DotProduct, Normal, Point, Transform}; /// A direction vector in 3d homogeneous space. #[derive(Debug, Copy, Clone)] pub struct Vector { pub co: Vec3A, } impl Vector { #[inline(always)] pub fn new(x: f32, y: f32, z: f32) -> Vector { Vector { co: Vec3A::new(x, y, z), } } #[inline(always)] pub fn length(&self) -> f32 { self.co.length() } #[inline(always)] pub fn length2(&self) -> f32 { self.co.length_squared() } #[inline(always)] pub fn normalized(&self) -> Vector { Vector { co: self.co.normalize(), } } #[inline(always)] pub fn abs(&self) -> Vector { Vector { co: self.co * self.co.signum(), } } #[inline(always)] pub fn into_point(self) -> Point { Point { co: self.co } } #[inline(always)] pub fn into_normal(self) -> Normal { Normal { co: self.co } } #[inline(always)] pub fn get_n(&self, n: usize) -> f32 { match n { 0 => self.x(), 1 => self.y(), 2 => self.z(), _ => panic!("Attempt to access dimension beyond z."), } } #[inline(always)] pub fn x(&self) -> f32 { self.co[0] } #[inline(always)] pub fn y(&self) -> f32 { self.co[1] } #[inline(always)] pub fn z(&self) -> f32 { self.co[2] } #[inline(always)] pub fn set_x(&mut self, x: f32) { self.co[0] = x; } #[inline(always)] pub fn set_y(&mut self, y: f32) { self.co[1] = y; } #[inline(always)] pub fn set_z(&mut self, z: f32) { self.co[2] = z; } } impl PartialEq for Vector { #[inline(always)] fn eq(&self, other: &Vector) -> bool { self.co == other.co } } impl Add for Vector { type Output = Vector; #[inline(always)] fn add(self, other: Vector) -> Vector { Vector { co: self.co + other.co, } } } impl Sub for Vector { type Output = Vector; #[inline(always)] fn sub(self, other: Vector) -> Vector { Vector { co: self.co - other.co, } } } impl Mul for Vector { type Output = Vector; #[inline(always)] fn mul(self, other: f32) -> Vector { Vector { co: self.co * other, } } } impl Mul for Vector { type Output = Vector; #[inline] fn mul(self, other: Transform) -> Vector { Vector { co: other.0.transform_vector3a(self.co), } } } impl Div for Vector { type Output = Vector; #[inline(always)] fn div(self, other: f32) -> Vector { Vector { co: self.co / other, } } } impl Neg for Vector { type Output = Vector; #[inline(always)] fn neg(self) -> Vector { Vector { co: self.co * -1.0 } } } impl DotProduct for Vector { #[inline(always)] fn dot(self, other: Vector) -> f32 { self.co.dot(other.co) } } impl CrossProduct for Vector { #[inline] fn cross(self, other: Vector) -> Vector { Vector { co: self.co.cross(other.co), } } } #[cfg(test)] mod tests { use super::super::{CrossProduct, DotProduct, Transform}; use super::*; #[test] fn add() { let v1 = Vector::new(1.0, 2.0, 3.0); let v2 = Vector::new(1.5, 4.5, 2.5); let v3 = Vector::new(2.5, 6.5, 5.5); assert_eq!(v3, v1 + v2); } #[test] fn sub() { let v1 = Vector::new(1.0, 2.0, 3.0); let v2 = Vector::new(1.5, 4.5, 2.5); let v3 = Vector::new(-0.5, -2.5, 0.5); assert_eq!(v3, v1 - v2); } #[test] fn mul_scalar() { let v1 = Vector::new(1.0, 2.0, 3.0); let v2 = 2.0; let v3 = Vector::new(2.0, 4.0, 6.0); assert_eq!(v3, v1 * v2); } #[test] fn mul_matrix_1() { let v = Vector::new(1.0, 2.5, 4.0); let m = Transform::new_from_values( 1.0, 2.0, 2.0, 1.5, 3.0, 6.0, 7.0, 8.0, 9.0, 2.0, 11.0, 12.0, ); assert_eq!(v * m, Vector::new(14.0, 46.0, 58.0)); } #[test] fn mul_matrix_2() { let v = Vector::new(1.0, 2.5, 4.0); let m = Transform::new_from_values( 1.0, 2.0, 2.0, 1.5, 3.0, 6.0, 7.0, 8.0, 9.0, 2.0, 11.0, 12.0, ); assert_eq!(v * m, Vector::new(14.0, 46.0, 58.0)); } #[test] fn div() { let v1 = Vector::new(1.0, 2.0, 3.0); let v2 = 2.0; let v3 = Vector::new(0.5, 1.0, 1.5); assert_eq!(v3, v1 / v2); } #[test] fn length() { let v = Vector::new(1.0, 2.0, 3.0); assert!((v.length() - 3.7416573867739413).abs() < 0.000001); } #[test] fn length2() { let v = Vector::new(1.0, 2.0, 3.0); assert_eq!(v.length2(), 14.0); } #[test] fn normalized() { let v1 = Vector::new(1.0, 2.0, 3.0); let v2 = Vector::new(0.2672612419124244, 0.5345224838248488, 0.8017837257372732); let v3 = v1.normalized(); assert!((v3.x() - v2.x()).abs() < 0.000001); assert!((v3.y() - v2.y()).abs() < 0.000001); assert!((v3.z() - v2.z()).abs() < 0.000001); } #[test] fn dot_test() { let v1 = Vector::new(1.0, 2.0, 3.0); let v2 = Vector::new(1.5, 4.5, 2.5); let v3 = 18.0f32; assert_eq!(v3, v1.dot(v2)); } #[test] fn cross_test() { let v1 = Vector::new(1.0, 0.0, 0.0); let v2 = Vector::new(0.0, 1.0, 0.0); let v3 = Vector::new(0.0, 0.0, 1.0); assert_eq!(v3, v1.cross(v2)); } } ================================================ FILE: sub_crates/spectral_upsampling/Cargo.toml ================================================ [package] name = "spectral_upsampling" version = "0.1.0" authors = ["Nathan Vegdahl "] edition = "2018" license = "MIT, Apache 2.0" [lib] name = "spectral_upsampling" path = "src/lib.rs" [dependencies] glam = "0.15" ================================================ FILE: sub_crates/spectral_upsampling/LICENSE.md ================================================ ## Adapted code and data files This crate includes code adapted from the supplemental material of the paper ["Physically Meaningful Rendering using Tristimulus Colours" by Meng et al.](https://cg.ivd.kit.edu/spectrum.php) Specifically the code in `src/meng/`. That code has no explicit license, but I contacted one of the authors and confirmed that it is intended to be used freely. Take that for what you will! This crate also includes data files from the supplemental material of the paper ["A Low-Dimensional Function Space for Efficient Spectral Upsampling" by Jakob et al.](https://rgl.epfl.ch/publications/Jakob2019Spectral). Specifically, the files under `jakob_tables/`. Please see LICENSE.txt in that directory for their license. ## Remaining code and files Copyright (c) 2020 Nathan Vegdahl This project is licensed under either of * MIT license (licenses/MIT.txt or http://opensource.org/licenses/MIT) * Apache License, Version 2.0, (licenses/Apache-2.0.txt or http://www.apache.org/licenses/LICENSE-2.0) at your option. ================================================ FILE: sub_crates/spectral_upsampling/build.rs ================================================ // Get Jakob tables into a native rust format. use std::{ env, fs::File, io::{self, Read, Write}, path::Path, }; /// How many polynomial coefficients? const RGB2SPEC_N_COEFFS: usize = 3; /// Table resolution. const TABLE_RES: usize = 64; // For the small table, what is the middle value used? const MID_VALUE: f32 = 0.5; fn main() { // Write tables to Rust file let out_dir = env::var("OUT_DIR").unwrap(); let dest_path = Path::new(&out_dir).join("jakob_table_inc.rs"); let mut f = File::create(&dest_path).unwrap(); // Rec.709 let rec709_table = rgb2spec_load_small("jakob_tables/srgb.coeff"); f.write_all(format!("\nconst REC709_TABLE_RES: usize = {};", TABLE_RES).as_bytes()) .unwrap(); f.write_all(format!("\nconst REC709_TABLE_MID_VALUE: f32 = {};", MID_VALUE).as_bytes()) .unwrap(); f.write_all("\n#[allow(clippy::unreadable_literal, clippy::approx_constant)]".as_bytes()) .unwrap(); f.write_all("\npub static REC709_TABLE: &[[(f32, f32, f32); 2]; 64 * 64 * 3] = &[".as_bytes()) .unwrap(); for item in &rec709_table { f.write_all( format!( "\n [({}, {}, {}), ({}, {}, {})],", item[0].0, item[0].1, item[0].2, item[1].0, item[1].1, item[1].2 ) .as_bytes(), ) .unwrap(); } f.write_all("\n];".as_bytes()).unwrap(); // Rec.2020 let rec2020_table = rgb2spec_load_small("jakob_tables/rec2020.coeff"); f.write_all(format!("\nconst REC2020_TABLE_RES: usize = {};", TABLE_RES).as_bytes()) .unwrap(); f.write_all(format!("\nconst REC2020_TABLE_MID_VALUE: f32 = {};", MID_VALUE).as_bytes()) .unwrap(); f.write_all("\n#[allow(clippy::unreadable_literal, clippy::approx_constant)]".as_bytes()) .unwrap(); f.write_all("\npub static REC2020_TABLE: &[[(f32, f32, f32); 2]; 64 * 64 * 3] = &[".as_bytes()) .unwrap(); for item in &rec2020_table { f.write_all( format!( "\n [({}, {}, {}), ({}, {}, {})],", item[0].0, item[0].1, item[0].2, item[1].0, item[1].1, item[1].2 ) .as_bytes(), ) .unwrap(); } f.write_all("\n];".as_bytes()).unwrap(); // sRGB / ACES let aces_table = rgb2spec_load_small("jakob_tables/aces2065_1.coeff"); f.write_all(format!("\nconst ACES_TABLE_RES: usize = {};", TABLE_RES).as_bytes()) .unwrap(); f.write_all(format!("\nconst ACES_TABLE_MID_VALUE: f32 = {};", MID_VALUE).as_bytes()) .unwrap(); f.write_all("\n#[allow(clippy::unreadable_literal, clippy::approx_constant)]".as_bytes()) .unwrap(); f.write_all("\npub static ACES_TABLE: &[[(f32, f32, f32); 2]; 64 * 64 * 3] = &[".as_bytes()) .unwrap(); for item in &aces_table { f.write_all( format!( "\n [({}, {}, {}), ({}, {}, {})],", item[0].0, item[0].1, item[0].2, item[1].0, item[1].1, item[1].2 ) .as_bytes(), ) .unwrap(); } f.write_all("\n];".as_bytes()).unwrap(); } /// Underlying representation pub struct RGB2Spec { res: usize, scale: Vec, data: Vec<[f32; RGB2SPEC_N_COEFFS]>, } pub fn rgb2spec_load(filepath: &str) -> RGB2Spec { let file_contents = { let mut file_contents = Vec::new(); let mut f = io::BufReader::new(File::open(filepath).unwrap()); f.read_to_end(&mut file_contents).unwrap(); file_contents }; // Check the header let header = &file_contents[0..4]; if header != "SPEC".as_bytes() { panic!("Not a spectral table."); } // Get resolution of the table let res = u32::from_le_bytes([ file_contents[4], file_contents[5], file_contents[6], file_contents[7], ]) as usize; // Calculate sizes let size_scale = res; let size_data = res * res * res * RGB2SPEC_N_COEFFS; // Load the table scale data let mut scale = Vec::with_capacity(size_scale); for i in 0..size_scale { let ii = i * 4 + 8; let n = f32::from_bits(u32::from_le_bytes([ file_contents[ii], file_contents[ii + 1], file_contents[ii + 2], file_contents[ii + 3], ])); scale.push(n); } // Load the table coefficient data let mut data = Vec::with_capacity(size_data); for i in 0..size_data { let ii = i * 4 * RGB2SPEC_N_COEFFS + 8 + (size_scale * 4); let n1 = f32::from_bits(u32::from_le_bytes([ file_contents[ii], file_contents[ii + 1], file_contents[ii + 2], file_contents[ii + 3], ])); let n2 = f32::from_bits(u32::from_le_bytes([ file_contents[ii + 4], file_contents[ii + 5], file_contents[ii + 6], file_contents[ii + 7], ])); let n3 = f32::from_bits(u32::from_le_bytes([ file_contents[ii + 8], file_contents[ii + 9], file_contents[ii + 10], file_contents[ii + 11], ])); data.push([n1, n2, n3]); } RGB2Spec { res: res, scale: scale, data: data, } } pub fn rgb2spec_load_small(filepath: &str) -> Vec<[(f32, f32, f32); 2]> { let big_table = rgb2spec_load(filepath); assert!(big_table.res == TABLE_RES); // Calculate z offsets and such for the mid value. let dz: usize = 1 * big_table.res * big_table.res; let z05_i = rgb2spec_find_interval(&big_table.scale, MID_VALUE); let z05_1: f32 = (MID_VALUE - big_table.scale[z05_i]) / (big_table.scale[z05_i + 1] - big_table.scale[z05_i]); let z05_0: f32 = 1.0 - z05_1; // Fill in table. let mut table = vec![[(0.0, 0.0, 0.0); 2]; TABLE_RES * TABLE_RES * 3]; for i in 0..3 { let offset = i * big_table.res * big_table.res * big_table.res; for j in 0..(big_table.res * big_table.res) { let one_coef = big_table.data[offset + ((TABLE_RES - 1) * dz) + j]; let mid_coef_0 = big_table.data[offset + (z05_i * dz) + j]; let mid_coef_1 = big_table.data[offset + ((z05_i + 1) * dz) + j]; let mid_coef = [ (mid_coef_0[0] * z05_0) + (mid_coef_1[0] * z05_1), (mid_coef_0[1] * z05_0) + (mid_coef_1[1] * z05_1), (mid_coef_0[2] * z05_0) + (mid_coef_1[2] * z05_1), ]; table[(i * big_table.res * big_table.res) + j] = [ (mid_coef[0], mid_coef[1], mid_coef[2]), (one_coef[0], one_coef[1], one_coef[2]), ]; } } table } fn rgb2spec_find_interval(values: &[f32], x: f32) -> usize { let last_interval = values.len() - 2; let mut left = 0; let mut size = last_interval; while size > 0 { let half = size >> 1; let middle = left + half + 1; if values[middle] < x { left = middle; size -= half + 1; } else { size = half; } } if left < last_interval { left } else { last_interval } } ================================================ FILE: sub_crates/spectral_upsampling/jakob_tables/LICENSE.txt ================================================ Copyright (c) 2020 Wenzel Jakob , All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: sub_crates/spectral_upsampling/src/jakob.rs ================================================ /// This file implements a lighter alternative version of the Jakob /// 2019 spectral upsampling method. Instead of using the entire 3D /// looking table, we use two 2d slices of the table and interpolate /// between the evaluated spectral values calculated from those tables. /// /// The provides similar color matching as full Jakob, at the expense of /// somewhat lower quality spectrums, and the inability to precalculate /// the coefficents for even more efficient evaluation later on. use glam::Vec4; /// How many polynomial coefficients? const RGB2SPEC_N_COEFFS: usize = 3; // Include tables generated by the build.rs script include!(concat!(env!("OUT_DIR"), "/jakob_table_inc.rs")); #[inline] pub fn rec709_to_spectrum_p4(lambdas: Vec4, rgb: (f32, f32, f32)) -> Vec4 { small_rgb_to_spectrum_p4( REC709_TABLE, REC709_TABLE_RES, REC709_TABLE_MID_VALUE, lambdas, rgb, ) } #[inline] pub fn rec2020_to_spectrum_p4(lambdas: Vec4, rgb: (f32, f32, f32)) -> Vec4 { small_rgb_to_spectrum_p4( REC2020_TABLE, REC2020_TABLE_RES, REC2020_TABLE_MID_VALUE, lambdas, rgb, ) } #[inline] pub fn aces_to_spectrum_p4(lambdas: Vec4, rgb: (f32, f32, f32)) -> Vec4 { small_rgb_to_spectrum_p4( ACES_TABLE, ACES_TABLE_RES, ACES_TABLE_MID_VALUE, lambdas, rgb, ) } //=============================================================== // Core functions, specialized above for specific color spaces. #[inline(always)] #[allow(clippy::many_single_char_names)] fn small_rgb_to_spectrum_p4( table: &[[(f32, f32, f32); 2]], table_res: usize, table_mid_value: f32, lambdas: Vec4, rgb: (f32, f32, f32), ) -> Vec4 { // Determine largest RGB component, and calculate the other two // components scaled for lookups. let (i, max_val, x, y) = if rgb.0 > rgb.1 && rgb.0 > rgb.2 { (0, rgb.0, rgb.1, rgb.2) } else if rgb.1 > rgb.2 { (1, rgb.1, rgb.2, rgb.0) } else { (2, rgb.2, rgb.0, rgb.1) }; if max_val == 0.0 { // If max_val is zero, just return zero. This avoids NaN's from // divide by zero. This is also correct, since it's black. return Vec4::splat(0.0); } let x = x * 63.0 / max_val; let y = y * 63.0 / max_val; // Calculate lookup coordinates. let xi = (x as usize).min(table_res - 2); let yi = (y as usize).min(table_res - 2); let offset = (table_res * table_res * i) + (yi * table_res) + xi; let dx = 1; let dy = table_res; // Look up values from table. let a0 = table[offset]; let a1 = table[offset + dx]; let a2 = table[offset + dy]; let a3 = table[offset + dy + dx]; // Convert to SIMD format for faster interpolation. let a0 = [ Vec4::new(a0[0].0, a0[0].1, a0[0].2, 0.0), Vec4::new(a0[1].0, a0[1].1, a0[1].2, 0.0), ]; let a1 = [ Vec4::new(a1[0].0, a1[0].1, a1[0].2, 0.0), Vec4::new(a1[1].0, a1[1].1, a1[1].2, 0.0), ]; let a2 = [ Vec4::new(a2[0].0, a2[0].1, a2[0].2, 0.0), Vec4::new(a2[1].0, a2[1].1, a2[1].2, 0.0), ]; let a3 = [ Vec4::new(a3[0].0, a3[0].1, a3[0].2, 0.0), Vec4::new(a3[1].0, a3[1].1, a3[1].2, 0.0), ]; // Do interpolation. let x1: f32 = x - xi as f32; let x0: f32 = 1.0 - x1 as f32; let y1: f32 = y - yi as f32; let y0: f32 = 1.0 - y1 as f32; let b0 = [(a0[0] * x0) + (a1[0] * x1), (a0[1] * x0) + (a1[1] * x1)]; let b1 = [(a2[0] * x0) + (a3[0] * x1), (a2[1] * x0) + (a3[1] * x1)]; let c = [(b0[0] * y0) + (b1[0] * y1), (b0[1] * y0) + (b1[1] * y1)]; // Evaluate the spectral function and return the result. if max_val <= table_mid_value { rgb2spec_eval_4([c[0][0], c[0][1], c[0][2]], lambdas) * (1.0 / table_mid_value) * max_val } else if max_val < 1.0 { let n = (max_val - table_mid_value) / (1.0 - table_mid_value); let s0 = rgb2spec_eval_4([c[0][0], c[0][1], c[0][2]], lambdas); let s1 = rgb2spec_eval_4([c[1][0], c[1][1], c[1][2]], lambdas); (s0 * (1.0 - n)) + (s1 * n) } else { rgb2spec_eval_4([c[1][0], c[1][1], c[1][2]], lambdas) * max_val } } //============================================================ // Coefficient -> eval functions #[inline(always)] fn rgb2spec_fma_4(a: Vec4, b: Vec4, c: Vec4) -> Vec4 { (a * b) + c } fn rgb2spec_eval_4(coeff: [f32; RGB2SPEC_N_COEFFS], lambda: Vec4) -> Vec4 { let co0 = Vec4::splat(coeff[0]); let co1 = Vec4::splat(coeff[1]); let co2 = Vec4::splat(coeff[2]); let x = rgb2spec_fma_4(rgb2spec_fma_4(co0, lambda, co1), lambda, co2); let y = { // TODO: replace this with a SIMD sqrt op. let (x, y, z, w) = rgb2spec_fma_4(x, x, Vec4::splat(1.0)).into(); Vec4::new(x.sqrt(), y.sqrt(), z.sqrt(), w.sqrt()).recip() }; rgb2spec_fma_4(Vec4::splat(0.5) * x, y, Vec4::splat(0.5)) } ================================================ FILE: sub_crates/spectral_upsampling/src/lib.rs ================================================ // Since this is basicallt translated from C, silence a bunch of // clippy warnings that stem from the C code. #![allow(clippy::needless_return)] #![allow(clippy::useless_let_if_seq)] #![allow(clippy::cognitive_complexity)] pub mod jakob; pub mod meng; ================================================ FILE: sub_crates/spectral_upsampling/src/meng/generate_meng_spectra_tables.py ================================================ #!/usr/bin/env python3 # This file is originally from the supplemental material of the paper # "Physically Meaningful Rendering using Tristimulus Colours" by Meng et al. # It has been adapted by Nathan Vegdahl to generate Rust instead of C. # Only the data tables are generated, and should be put in spectra_tables.rs # The executable code lives in lib.rs. import numpy as np import scipy import math import time import os import sys try: import colour.plotting as clr import colour.recovery as rec import colour have_colour_package = True except: print("Install colour-science using 'sudo pip install colour-science' to get xy grid plots.") print("See http://www.colour-science.org for more information.") have_colour_package = False # Looking at the code, it looks like "Path" is used unconditionally, so # matplotlib is actually just required. Import unconditionally. # --Nathan V #try: #print("Install matplotlib to get plots.") import matplotlib.pyplot as plt from matplotlib.path import Path have_matplotlib = True #except: # have_matplotlib = False # ------------------------------------------------------------------------------ # Color matching functions. # Note: The load function assumes a CSV input, where each row # has wavelength, x, y, z (in that order). # For our paper, we used the truncated set of CMF from 380nm to 780nm, CIE 1931 # standard colorimetric observer, as recommended in CIE Technical Report # Colorimetry, 2004 (ISBN 3901906339). The CMF values can be found n Table T.4. # of the technical report. # # The same values can be obtained by downloading # the CIE 1931 2-deg. XYZ CMFS here: http://www.cvrl.org/cmfs.htm. # In the table, use values for wavelengths in [380, 780] and round to 6 # decimal places. # ------------------------------------------------------------------------------ class Cmf: cmf = [] @classmethod def load(cls, filename): cls.cmf = np.loadtxt(filename, delimiter=',') assert(cls.cmf.shape[1] == 4) @classmethod def num_bins(cls): return cls.cmf.shape[0] @classmethod def bin_size(cls): return cls.cmf[1,0]-cls.cmf[0,0] @classmethod def wavelength(cls): return cls.cmf[:,0] @classmethod def x_bar(cls): return cls.cmf[:,1] @classmethod def y_bar(cls): return cls.cmf[:,2] @classmethod def z_bar(cls): return cls.cmf[:,3] @classmethod def xyz_from_spectrum(cls, spectrum): '''As CIE instructs, we integrate using simple summation.''' assert(cls.cmf.shape[0] == len(spectrum)) d_lambda = cls.wavelength()[1]-cls.wavelength()[0] xyz = [0, 0, 0] for x_bar, y_bar, z_bar, s in zip(cls.x_bar(), cls.y_bar(), cls.z_bar(), spectrum): xyz[0] += x_bar * s xyz[1] += y_bar * s xyz[2] += z_bar * s return [v * d_lambda for v in xyz] @classmethod def xyz_ee_white(cls): ee_white = [1] * cls.cmf.shape[0] return cls.xyz_from_spectrum(ee_white) # ------------------------------------------------------------------------------ # Transform between color spaces. # ------------------------------------------------------------------------------ class Transform: # -------------------------------------------------------------------------- # Homogenize/dehomogenize vectors. # -------------------------------------------------------------------------- @staticmethod def hom(v2): assert(len(v2) >= 2) return np.matrix([[v2[0]], [v2[1]], [1]]) @staticmethod def dehom(v3): assert((v3.shape[0] == 3 and v3.shape[1] == 1) or (v3.shape[0] == 1 and v3.shape[1] == 3)) v = v3.flatten().tolist()[0] return [v[0]/v[2], v[1]/v[2]] # ------------------------------------------------------------------------------ # Convert from xyy to xyz and back. # ------------------------------------------------------------------------------ @staticmethod def xyz_from_xyy(xyy): return (xyy[0] * xyy[2]/xyy[1], xyy[2], (1-xyy[0]-xyy[1]) * xyy[2]/xyy[1]) @staticmethod def xyy_from_xyz(xyz): s = sum(xyz) return (xyz[0] / s, xyz[1] / s, xyz[1]) # ------------------------------------------------------------------------------ # Convert from srgb to xyz and back. # ------------------------------------------------------------------------------ def xyz_from_srgb(srgb): # This matrix is computed by transforming the sRGB primaries into xyz. # Primaries are # red: xy = 0.64, Y = 0.2126 # green: xy = 0.30, Y = 0.7152 # blue: xy = 0.15, Y = 0.0722, # where the luminance values are taken from HDTV Recommendation BT.709 # http://www.itu.int/rec/R-REC-BT.709/en M = np.matrix([[ 0.41231515, 0.3576, 0.1805 ] [ 0.2126 , 0.7152, 0.0722 ] [ 0.01932727, 0.1192, 0.95063333]]) return np.dot(M, srgb) def srgb_from_xyz(xyz): # This is the inverse of the above matrix. M = np.matrix([[ 3.24156456, -1.53766524, -0.49870224], [-0.96920119, 1.87588535, 0.04155324], [ 0.05562416, -0.20395525, 1.05685902]]) return np.dot(M, xyz) # EE-white adapted sRGB (Smits uses this). def xyz_from_ergb(ergb): M = np.matrix([ [0.496859, 0.339094, 0.164047], [0.256193, 0.678188, 0.065619], [0.023290, 0.113031, 0.863978] ]) return np.dot(M, xyz) # ------------------------------------------------------------------------------ # Convert from xy to xy* and back. # ------------------------------------------------------------------------------ mat_xystar_to_xy = None mat_xy_to_xystar = None @classmethod def init_xystar(cls): '''xy* is a color space where the line between blue and red is horizontal. Also, equal-energy white is the origin. xy* depends only on the color matching functions used.''' num_bins = len(Cmf.wavelength()) # Pure blue. s = [0] * num_bins s[0] = 1 xy0 = cls.xyy_from_xyz(Cmf.xyz_from_spectrum(s)) # Pure red. s = [0] * num_bins s[-1] = 1 xy1 = cls.xyy_from_xyz(Cmf.xyz_from_spectrum(s)) d = np.array(xy1[:2])-np.array(xy0[:2]) d /= math.sqrt(np.vdot(d, d)) # Translation to make ee-white (in xy) the origin. T = np.matrix([[ 1, 0, -1/3], [ 0, 1, -1/3], [ 0, 0, 1]]) # Rotation to make purple line horizontal. R = np.matrix([[ d[0], d[1], 0], [-d[1], d[0], 0], [ 0, 0, 1]]) cls.mat_xy_to_xystar = np.dot(R, T) cls.mat_xystar_to_xy = cls.mat_xy_to_xystar.getI() @classmethod def xystar_from_xy(cls, xy): return cls.dehom(np.dot(cls.mat_xy_to_xystar, cls.hom(xy))) @classmethod def xy_from_xystar(cls, xystar): return cls.dehom(np.dot(cls.mat_xystar_to_xy, cls.hom(xystar))) # ------------------------------------------------------------------------------ # Convert from xy to uv and back. # ------------------------------------------------------------------------------ mat_uv_to_xystar = None mat_xystar_to_uv = None mat_uv_to_xy = None mat_xy_to_uv = None @classmethod def init_uv(cls, xystar_bbox, grid_res): '''uv is derived from xy* by transforming grid points to integer coordinates. uv depends on xy* and the grid used.''' # Translate xystar bounding box min to origin. T = np.matrix([[1, 0, -xystar_bbox[0][0]], [0, 1, -xystar_bbox[0][1]], [0, 0, 1]]) # Scale so that one grid cell has unit size. w = xystar_bbox[1][0]-xystar_bbox[0][0] h = xystar_bbox[1][1]-xystar_bbox[0][1] S = np.matrix([[grid_res[0] / w, 0, 0], [0, grid_res[1] / h, 0], [0, 0, 1]]) cls.mat_xystar_to_uv = np.dot(S, T) cls.mat_uv_to_xystar = cls.mat_xystar_to_uv.getI() cls.mat_xy_to_uv = np.dot(cls.mat_xystar_to_uv, cls.mat_xy_to_xystar) cls.mat_uv_to_xy = cls.mat_xy_to_uv.getI() @classmethod def uv_from_xy(cls, xy): return cls.dehom(np.dot(cls.mat_xy_to_uv, cls.hom(xy))) @classmethod def xy_from_uv(cls, uv): return cls.dehom(np.dot(cls.mat_uv_to_xy, cls.hom(uv))) @classmethod def uv_from_xystar(cls, xystar): return cls.dehom(np.dot(cls.mat_xystar_to_uv, cls.hom(xystar))) @classmethod def xystar_from_uv(cls, uv): return cls.dehom(np.dot(cls.mat_uv_to_xystar, cls.hom(uv))) # ------------------------------------------------------------------------------ # Compute functor for all elements of data using a process pool, and call # finished with (i, result) afterwards. # ------------------------------------------------------------------------------ def multiprocess_progress(data, functor, finished, data_size, early_clip=None): from multiprocessing import Process, current_process, Queue num_procs = os.cpu_count()-1 def worker(wnum, input_queue, output_queue): os.sched_setaffinity(0, [wnum]) while True: try: idx, value = input_queue.get(block=False) if value == 'STOP': break output_queue.put((idx, functor(value))) except: pass os.sched_yield() task_queue = Queue(2*num_procs) done_queue = Queue(2*num_procs) # Launch workers. print('Running {} workers ...'.format(num_procs)) processes = [] for i in range(num_procs): processes.append(Process(target = worker, args = (i, task_queue, done_queue), name = 'worker {}'.format(i), daemon = True)) processes[-1].start() # Push input data, and check for output data. num_sent = 0 num_done = 0 num_clipped = 0 iterator = iter(data) perc = 0 def print_progress(msg=None): msg_str = '' if msg is not None: msg_str = '['+msg+']' print('\033[2K\r{} sent, {} done, {} clipped, {} total ({} %) {}'.format(num_sent, num_done, num_clipped, data_size, perc, msg_str), end='') while num_done < data_size: print_progress('sending work') while num_sent < data_size and not task_queue.full(): nextval = next(iterator) clipped = False if early_clip is not None: clipped, clip_result = early_clip(num_sent, nextval) if clipped: finished(num_sent, clip_result) num_clipped += 1 num_done += 1 if not clipped: task_queue.put((num_sent, nextval)) num_sent += 1 os.sched_yield() while True: try: i, result = done_queue.get(block=False) finished(i, result) num_done += 1 perc = int(num_done / data_size * 100) print_progress('collecting results') except: break; time.sleep(0) print_progress() time.sleep(0) # Terminate workers. for i in range(num_procs): task_queue.put((-1, 'STOP')) for p in processes: p.join() print('\n ... done') # ------------------------------------------------------------------------------ # Given a color in XYZ, determine a smooth spectrum that corresponds to that # color. # ------------------------------------------------------------------------------ def find_spectrum(xyz): from scipy.optimize import minimize # As an objective, we use a similar roughness term as Smits did. def objective(S): roughness = 0 for i in range(len(S)-1): roughness += (S[i]-S[i+1])**2 # Note: We found much better convergence with the square term! # roughness = math.sqrt(roughness) return roughness num_bins = Cmf.num_bins() x0 = [1] * num_bins # Constraint: Match XYZ values. cnstr = { 'type': 'eq', 'fun': lambda s: (np.array(Cmf.xyz_from_spectrum(s))-xyz) } # We want positive spectra. bnds = [(0, 1000)] * num_bins res = minimize(objective, x0, method='SLSQP', constraints=cnstr, bounds=bnds, options={"maxiter": 2000, "ftol": 1e-10}) if not res.success: err_message = 'Error for xyz={} after {} iterations: {}'.format(xyz, res.nit, res.message) return ([0] * num_bins, True, err_message) else: # The result may contain some very tiny negative values due # to numerical issues. Clamp those to 0. return ([max(x, 0) for x in res.x], False, "") # ------------------------------------------------------------------------------ # Get the boundary of the horseshoe as a path in xy*. # ------------------------------------------------------------------------------ def horseshoe_path(): verts = [] codes = [] d_lambda = Cmf.wavelength()[1]-Cmf.wavelength()[0] for x, y, z in zip(Cmf.x_bar(), Cmf.y_bar(), Cmf.z_bar()): xyz = [x*d_lambda, y*d_lambda, z*d_lambda] xyY = Transform.xyy_from_xyz(xyz) xystar = Transform.xystar_from_xy(xyY[:2]) verts.append(xystar) codes.append(Path.LINETO) codes[0] = Path.MOVETO codes.append(Path.CLOSEPOLY) vx = [x for (x, y) in verts] vy = [y for (x, y) in verts] bbox = [ (min(vx), min(vy)), (max(vx), max(vy)) ] verts.append((0,0)) return (Path(verts, codes), bbox) # ------------------------------------------------------------------------------ # Grid data structures. # ------------------------------------------------------------------------------ class DataPoint: def __init__(self): self.xystar = (0, 0) self.uv = (0, 0) self.Y = 0 self.spectrum = [0] self.M = 0 self.inside = False self.equal_energy_white = False self.broken = False def update_uv(self): self.uv = Transform.uv_from_xystar(self.xystar) class GridCell: def __init__(self): self.indices = [] self.triangles = [] self.inside = True # binary search to find intersection def find_intersection(p0, p1, i0, i1, clip_path): delta = p1-p0 if np.linalg.norm(delta) < 0.0001: # Points are very close, terminate. # Move new intersection slightly into the gamut. delta *= 0.998 if i0: return p1 - delta else: return p0 + delta p01 = 0.5 * (p0 + p1) i01 = clip_path.contains_point(p01) if i0 != i01: return find_intersection(p0, p01, i0, i01, clip_path) elif i1 != i01: return find_intersection(p01, p1, i01, i1, clip_path) else: print ("something wrong here") return p01 def clip_edge(d0, d1, clip_path): from operator import xor if not xor(d0.inside, d1.inside): return (False, None) p0 = np.array(d0.xystar) p1 = np.array(d1.xystar) p = find_intersection(p0, p1, d0.inside, d1.inside, clip_path) data_point = DataPoint() data_point.xystar = p data_point.inside = True return (True, data_point) def generate_xystar_grid(scale): print("Generating clip path ...") clip_path, bbox = horseshoe_path() # We know that xy(1/3, 1/3) = xystar(0, 0) must be a grid point. # subdivide the rectangle between that and the purest red regularly with res. # Note: This can be freely chosen, but we found 6,4 to be a reasonable value. res = (6, 4) white_xystar = [0, 0] step_x = abs(white_xystar[0]-bbox[1][0]) / res[0] step_y = abs(white_xystar[1]-bbox[0][1]) / res[1] # Find bbox top left corner so that the whole diagram is contained. add_x = int(math.ceil(abs(white_xystar[0]-bbox[0][0]) / step_x)) add_y = int(math.ceil(abs(bbox[1][1]-white_xystar[1]) / step_y)) # The index of white - we will set this spectrum to equal energy white. white_idx = (add_x, res[1]) grid_res = (res[0] + add_x, res[1] + add_y) bbox = [ # min (white_xystar[0]- step_x * add_x, bbox[0][1]), # max (bbox[1][0], white_xystar[1] + step_y * add_y) ] grid = [GridCell() for i in range(grid_res[0] * grid_res[1])] data_points = [] # Generate grid points. print(" Generating grid points in xy* ...") for (x,y) in [(x,y) for y in range(grid_res[1]+1) for x in range(grid_res[0]+1)]: data_point = DataPoint() data_point.xystar = (bbox[0][0] + step_x * x, bbox[0][1] + step_y * y) if (x, y) == white_idx: # Numerically, we want the white point to be at xy = (1/3, 1/3). delta = np.array(data_point.xystar) - np.array(white_xystar) assert(np.dot(delta, delta) < 1e-7) data_point.equal_energy_white = True # Clip on horseshoe. if clip_path.contains_point(data_point.xystar) \ or (x > 0 and y == 0): # Special case for purple line. data_point.inside = True new_idx = len(data_points) data_points.append(data_point) # Add new index to this all four adjacent cells. for (cx, cy) in [(x-dx, y-dy) for dy in range(2) for dx in range(2)]: if cx >= 0 and cx < grid_res[0] and cy >= 0 and cy < grid_res[1]: cell = grid[cy * grid_res[0] + cx] cell.indices.append(new_idx) cell.inside = cell.inside and data_point.inside # Clip grid cells against horseshoe. print(" Clipping cells to xy gamut ...") for (x, y) in [(x, y) for x in range(grid_res[0]) for y in range(grid_res[1])]: cell = grid[y * grid_res[0] + x] # No need to clip cells that are completely inside. if cell.inside: continue # We clip the two outgoing edges of each point: # # d2 # . # d0 . d1 # Note: We assume here that data_points was generated as a regular # grid in row major order. d0 = data_points[(y+0)*(grid_res[0]+1)+(x+0)] d1 = data_points[(y+0)*(grid_res[0]+1)+(x+1)] d2 = data_points[(y+1)*(grid_res[0]+1)+(x+0)] (clipped_h, p_h) = clip_edge(d0, d1, clip_path) if clipped_h: new_idx = len(data_points) data_points.append(p_h) cell.indices.append(new_idx) if y > 0: grid[(y-1) * grid_res[0] + x].indices.append(new_idx) (clipped_v, p_v) = clip_edge(d0, d2, clip_path) if clipped_v: new_idx = len(data_points) data_points.append(p_v) cell.indices.append(new_idx) if x > 0: grid[y * grid_res[0] + x - 1].indices.append(new_idx) # Compact grid points (throw away points that are not inside). print(" Compacting grid ...") new_data_points = [] new_indices = [] prefix = 0 for data_point in data_points: if data_point.inside: new_indices.append(prefix) new_data_points.append(data_point) prefix += 1 else: new_indices.append(-1) data_points = new_data_points for gridcell in grid: new_cell_indices = [] for index in range(len(gridcell.indices)): old_index = gridcell.indices[index] if new_indices[old_index] >= 0: new_cell_indices.append(new_indices[old_index]) gridcell.indices = new_cell_indices[:] # Scale points down towards white point to avoid singular spectra. for data_point in data_points: data_point.xystar = [v * scale for v in data_point.xystar] bbox[0] = [v * scale for v in bbox[0]] bbox[1] = [v * scale for v in bbox[1]] return data_points, grid, grid_res, bbox # Plot the grid. def plot_grid(filename, data_points, grid, bbox_xystar, xystar=True): if not have_matplotlib or not have_colour_package: return print("Plotting the grid ...") plt.figure() # Draw a nice chromaticity diagram. clr.CIE_1931_chromaticity_diagram_plot(standalone=False) clr.canvas(figure_size=(7,7)) # Show the sRGB gamut. color_space = clr.get_RGB_colourspace('sRGB') x = color_space.primaries[:,0].tolist() y = color_space.primaries[:,1].tolist() plt.fill(x, y, color='black', label='sRGB', fill=False) # Draw crosses into all internal grid cells. # for gridcell in grid: # if len(gridcell.indices) > 0 and gridcell.inside: # if xystar: # pointx = sum([data_points[i].xystar[0] for i in gridcell.indices]) # pointy = sum([data_points[i].xystar[1] for i in gridcell.indices]) # pointx /= len(gridcell.indices) # pointy /= len(gridcell.indices) # (pointx, pointy) = Transform.xy_from_xystar((pointx, pointy)) # plt.plot(pointx, pointy, "x", color="black") # else: # pointx = sum([data_points[i].uv[0] for i in gridcell.indices]) # pointy = sum([data_points[i].uv[1] for i in gridcell.indices]) # pointx /= len(gridcell.indices) # pointy /= len(gridcell.indices) # (pointx, pointy) = Transform.xy_from_uv((pointx, pointy)) # plt.plot(pointx, pointy, "x", color="black") # Draw data points. for i, data_point in enumerate(data_points): if xystar: p = Transform.xy_from_xystar(data_point.xystar) else: p = Transform.xy_from_uv(data_point.uv) if data_point.equal_energy_white: plt.plot(p[0], p[1], "o", color="white", ms=4) elif data_point.broken: plt.plot(p[0], p[1], "o", color="red", ms=4) else: plt.plot(p[0], p[1], "o", color="green", ms=4) # Show grid point indices, for debugging. # plt.text(p[0]+0.01, p[1]-0.01, '{}'.format(i)) bp0 = Transform.xy_from_xystar([bbox_xystar[0][0], bbox_xystar[0][1]]) bp1 = Transform.xy_from_xystar([bbox_xystar[0][0], bbox_xystar[1][1]]) bp2 = Transform.xy_from_xystar([bbox_xystar[1][0], bbox_xystar[1][1]]) bp3 = Transform.xy_from_xystar([bbox_xystar[1][0], bbox_xystar[0][1]]) plt.plot([bp0[0], bp1[0], bp2[0], bp3[0], bp0[0]], [bp0[1], bp1[1], bp2[1], bp3[1], bp0[1]], label="Grid Bounding Box") plt.xlabel('$x$') plt.ylabel('$y$') plt.legend() plt.savefig(filename) # ------------------------------------------------------------------------------ # Compute spectra for all data points. # ------------------------------------------------------------------------------ def compute_spectrum(data_point): xy = Transform.xy_from_uv(data_point.uv) # Set luminance to y. This means that X+Y+Z = 1, # since y = Y / (X+Y+Z) = y / (X+Y+Z). xyY = [xy[0], xy[1], xy[1]] xyz = Transform.xyz_from_xyy(xyY) spectrum = [] broken = False if data_point.equal_energy_white: # Since we want X=Y=Z=1/3 (so that X+Y+Z=1), the equal-energy white # spectrum we want is 1/(3 int(x)) for x color matching function. spectrum = [1 / (3 * Cmf.xyz_ee_white()[0])] * Cmf.num_bins() else: spectrum, broken, message = find_spectrum(xyz) if broken: print("Couldn't find a spectrum for uv=({uv[0]},{uv[1]})".format(uv=data_point.uv)) print(message) xyz = Cmf.xyz_from_spectrum(spectrum) sum = xyz[0] + xyz[1] + xyz[2] if sum > 1.01 or sum < 0.99: print('Invalid brightness {} for uv=({uv[0]},{uv[1]})'.format(sum, uv=data_point.uv)) return (spectrum, broken) # ------------------------------------------------------------------------------ def compute_spectra(data_points): print('Computing spectra ...') def finished(i, result): data_points[i].spectrum = result[0] data_points[i].broken = result[1] multiprocess_progress(data_points, compute_spectrum, finished, len(data_points)) # ------------------------------------------------------------------------------ # Plot some of our fitted spectra. # Plot to multiple output files, since there are so many spectra. # ------------------------------------------------------------------------------ def plot_spectra(data_points): if not have_matplotlib or not have_colour_package: return print('Plotting spectra ...') plots_per_file = 15 #plt.figure(figsize=(12, 16)) cur_page = -1 ax_shape = (17, 4) axes = None for i, data_point in enumerate(data_points): page_size =(ax_shape[0]*ax_shape[1]) page = i // page_size if page > cur_page: if cur_page >= 0: plt.savefig('spectra_{}.svg'.format(cur_page)) fig, axes = plt.subplots(ax_shape[0], ax_shape[1], figsize=(14, 18)) cur_page = page j = i % page_size row = j % axes.shape[0] col = j // axes.shape[0] print(row, col) if row >= axes.shape[0] or col >= axes.shape[1]: print('cannot plot spectrum', i) continue ax = axes[row,col] xy = Transform.xy_from_uv(data_point.uv) # take a copy, we're going to normalise it s = data_point.spectrum[:] max_val = 0 for j in range(len(s)): if s[j] > max_val: max_val = s[j]; if max_val > 0: for j in range(len(s)): s[j] = s[j]/max_val ax.plot(Cmf.wavelength(), s, color='black', lw=2) ax.set_ylim(-0.01, 1.1) ax.set_yticklabels([]) ax.set_xticklabels([]) perc = int((i+1) / len(data_points) * 100) print(' {} / {} ({} %) \r'.format((i+1), len(data_points), perc), end='') plt.savefig('spectra_{}.svg'.format(cur_page)) print('\n... done') # ------------------------------------------------------------------------------ # Write spectral data # ------------------------------------------------------------------------------ def write_output(data_points, grid, grid_res, filename): print('Write output ...') with open(filename, 'w') as f: lambda_min = Cmf.wavelength()[0] lambda_max = Cmf.wavelength()[-1] num_spec_samples = Cmf.num_bins() spec_bin_size = Cmf.bin_size() f.write('// This file is auto-generated by generate_spectra_tables.py\n') f.write('#![allow(dead_code)]\n') f.write('#![cfg_attr(rustfmt, rustfmt_skip)]\n') f.write('#![allow(clippy::unreadable_literal)]\n') f.write('#![allow(clippy::excessive_precision)]\n') f.write('\n') f.write('/// This is 1 over the integral over either CMF.\n') f.write('/// Spectra can be mapped so that xyz=(1,1,1) is converted to constant 1 by\n') f.write('/// dividing by this value. This is important for valid reflectances.\n') f.write('pub const EQUAL_ENERGY_REFLECTANCE: f32 = {};'.format(1/max(Cmf.xyz_ee_white()))); f.write('\n\n') f.write('// Basic info on the spectrum grid.\n') f.write('pub(crate) const SPECTRUM_GRID_WIDTH: i32 = {};\n'.format(grid_res[0])) f.write('pub(crate) const SPECTRUM_GRID_HEIGHT: i32 = {};\n'.format(grid_res[1])) f.write('\n') f.write('// The spectra here have these properties.\n') f.write('pub const SPECTRUM_SAMPLE_MIN: f32 = {};\n'.format(lambda_min)) f.write('pub const SPECTRUM_SAMPLE_MAX: f32 = {};\n'.format(lambda_max)) f.write('pub(crate) const SPECTRUM_BIN_SIZE: f32 = {};\n'.format(spec_bin_size)) f.write('pub(crate) const SPECTRUM_NUM_SAMPLES: i32 = {};\n'.format(num_spec_samples)) f.write('\n') # Conversion routines xy->xystar and xy->uv and back. f.write('// xy* color space.\n') f.write('pub(crate) const SPECTRUM_MAT_XY_TO_XYSTAR: [f32; 6] = [\n') f.write(' {m[0]}, {m[1]}, {m[2]},\n {m[3]}, {m[4]}, {m[5]}\n' .format(m=Transform.mat_xy_to_xystar[:2,:].flatten().tolist()[0])) f.write('];\n') f.write('pub(crate) const SPECTRUM_MAT_XYSTAR_TO_XY: [f32; 6] = [\n') f.write(' {m[0]}, {m[1]}, {m[2]},\n {m[3]}, {m[4]}, {m[5]}\n' .format(m=Transform.mat_xystar_to_xy[:2,:].flatten().tolist()[0])) f.write('];\n') f.write('// uv color space.\n') f.write('pub(crate) const SPECTRUM_MAT_XY_TO_UV: [f32; 6] = [\n') f.write(' {m[0]}, {m[1]}, {m[2]},\n {m[3]}, {m[4]}, {m[5]}\n' .format(m=Transform.mat_xy_to_uv[:2,:].flatten().tolist()[0])) f.write('];\n') f.write('pub(crate) const SPECTRUM_MAT_UV_TO_XY: [f32; 6] = [\n') f.write(' {m[0]}, {m[1]}, {m[2]},\n {m[3]}, {m[4]}, {m[5]}\n' .format(m=Transform.mat_uv_to_xy[:2,:].flatten().tolist()[0])) f.write('];\n') f.write('// Grid cells. Laid out in row-major format.\n') f.write('// num_points = 0 for cells without data points.\n') f.write('#[derive(Copy, Clone)]\n') f.write('pub(crate) struct SpectrumGridCell {\n') f.write(' pub inside: bool,\n') f.write(' pub num_points: i32,\n') max_num_idx = 0 for c in grid: if len(c.indices) > max_num_idx: max_num_idx = len(c.indices) f.write(' pub idx: [i32; {}],\n'.format(max_num_idx)) f.write('}\n\n') # Count grid cells grid_cell_count = 0 for (x, y) in [(x,y) for y in range(grid_res[1]) for x in range(grid_res[0])]: grid_cell_count += 1 # Write grid cells f.write('pub(crate) const SPECTRUM_GRID: [SpectrumGridCell; {}] = [\n'.format(grid_cell_count)) cell_strings = [] for (x, y) in [(x,y) for y in range(grid_res[1]) for x in range(grid_res[0])]: cell = grid[y * grid_res[0] + x] # pad cell indices with -1. padded_indices = cell.indices[:] + [-1] * (max_num_idx-len(cell.indices)) num_inside = len(cell.indices) if num_inside > 0: idx_str = ', '.join(map(str, padded_indices)) if cell.inside and num_inside == 4: cell_strings.append(' SpectrumGridCell {{ inside: true, num_points: {}, idx: [{}] }}'.format(num_inside, idx_str)) else: cell_strings.append(' SpectrumGridCell {{ inside: false, num_points: {}, idx: [{}] }}'.format(num_inside, idx_str)) else: cell_strings.append(' SpectrumGridCell {{ inside: false, num_points: 0, idx: [{}] }}'.format(', '.join(['-1'] * max_num_idx))) f.write(',\n'.join(cell_strings)) f.write('\n];\n\n') f.write('// Grid data points.\n') f.write('#[derive(Copy, Clone)]\n') f.write('pub(crate) struct SpectrumDataPoint {\n') f.write(' pub xystar: (f32, f32),\n') f.write(' pub uv: (f32, f32),\n') f.write(' pub spectrum: [f32; {}], // X+Y+Z = 1\n'.format(num_spec_samples)) f.write('}\n\n') data_point_strings = [] data_point_count = 0 for p in data_points: data_point_count += 1 spec_str = ', '.join(["{:f}".format(v) for v in list(p.spectrum)]) data_point_strings.append( " SpectrumDataPoint {{\n" " xystar: ({p.xystar[0]}, {p.xystar[1]}),\n" " uv: ({p.uv[0]}, {p.uv[1]}),\n" " spectrum: [{spec}],\n" " }}".format(p=p, spec=spec_str) ) f.write('pub(crate) const SPECTRUM_DATA_POINTS: [SpectrumDataPoint; {}] = [\n'.format(data_point_count)) f.write(',\n'.join(data_point_strings)) f.write('\n];\n\n') f.write('// Color matching functions.\n') f.write('pub(crate) const CMF_WAVELENGTH: [f32; {}] = [\n'.format(len(Cmf.wavelength()))) f.write(' {}\n'.format(', '.join(str(v) for v in Cmf.wavelength()))) f.write('];\n') f.write('pub(crate) const CMF_X: [f32; {}] = [\n'.format(len(Cmf.x_bar()))) f.write(' {}\n'.format(', '.join(str(v) for v in Cmf.x_bar()))) f.write('];\n') f.write('pub(crate) const CMF_Y: [f32; {}] = [\n'.format(len(Cmf.y_bar()))) f.write(' {}\n'.format(', '.join(str(v) for v in Cmf.y_bar()))) f.write('];\n') f.write('pub(crate) const CMF_Z: [f32; {}] = [\n'.format(len(Cmf.z_bar()))) f.write(' {}\n'.format(', '.join(str(v) for v in Cmf.z_bar()))) f.write('];\n\n') print(' ... done') # ------------------------------------------------------------------------------ # We need to triangulate along the spectral locus, since our regular grid # cannot properly capture this edge. # ------------------------------------------------------------------------------ def create_triangle_fans(grid): print("generating triangle fans...") for cell in grid: num_points = len(cell.indices) # skip trivial inner cells (full quad interpolation)\n", if len(cell.indices) == 4 and cell.inside: # these could be sorted here, too. but turns out we always get them in scanline order # so we will know exactly how to treat them in the exported c code. continue # triangulate hard cases (irregular quads + n-gons, 5-gons in practice) if num_points > 0: # used for delaunay or plotting:\n", points = np.array([data_points[cell.indices[i]].xystar for i in range(num_points)]) centroid = (sum(points[:,0])/num_points, sum(points[:,1])/num_points) dp = DataPoint() dp.xystar = centroid dp.update_uv() index = len(data_points) data_points.append(dp) # create triangle fan: pts = [(points[i], i, cell.indices[i], math.atan2((points[i]-centroid)[1], (points[i]-centroid)[0])) for i in range(num_points)] pts = sorted(pts, key=lambda pt: pt[3]) # print('sorted {}'.format([pts[i][2] for i in range(num_points)])) cell.indices = [index] + [pts[i][2] for i in range(num_points)] # print('indices: {}'.format(cell.indices)) num_points = num_points + 1; # do that again with the new sort order: # points = np.array([data_points[cell.indices[i]].xystar for i in range(num_points)]) # now try triangle fan again with right pivot cell.triangles = [[0, i+1, i+2] for i in range(len(cell.indices)-2)] # ------------------------------------------------------------------------------ # Compute a high-resolution reflectance map. This map contains, for all # possible values of (xy), the largest value Y for which the corresponding # spectrum is still a valid reflectance. # ------------------------------------------------------------------------------ def compute_max_brightness(point): x = point[0] y = point[1] try: xyz = Transform.xyz_from_xyy((x, y, y)) # x+y+z = 1 spec, broken, msg = find_spectrum(xyz) if broken: print('{},{}: {}'.format(x, y, msg)) return -1 return 1.0/(106.8 * max(spec)) except: print('Exception - this is fatal.') raise def compute_reflectance_map(res): width = res height = res num_pixels = width * height buffer = [0, 0, 0.1] * num_pixels def store_buffer(): with open('reflectance_map.pfm', 'wb') as file: import struct header = 'PF\n{w} {h}\n{le}\n'.format( w = width, h = height, le = -1 if sys.byteorder == 'little' else 1) s = struct.pack('f' * len(buffer), *buffer) file.write(bytes(header, encoding='utf-8')) file.write(s) file.close() def coordinates(): for y in range(height): for x in range(width): yield (x / width, y / height) def store_pixel(i, v): global last_time_stored if v == 0: pass elif v < 0: buffer[3*i] = -v buffer[3*i+1] = 0 buffer[3*i+2] = 0 else: buffer[3*i] = v buffer[3*i+1] = v buffer[3*i+2] = v now = time.time() if (now-last_time_stored) > 60: store_buffer() last_time_stored = time.time() def early_clip(idx, v): global clip_path if clip_path.contains_point(Transform.xystar_from_xy(v)): return (False, 0) return (True, 0) multiprocess_progress(coordinates(), compute_max_brightness, store_pixel, width*height, early_clip) store_buffer() if __name__ == "__main__": # Parse command line options. import argparse parser = argparse.ArgumentParser(description='Generate spectrum_grid.h') parser.add_argument('-s', '--scale', metavar='SCALE', type=float, default=0.97, dest='scale', help='Scale grid points toward the EE white point using this factor. Defaults to 0.99.') parser.add_argument('-p', '--plot_spectra', default=False, action='store_const', const=True, dest='plot', help='Plot all spectra in a set of png files. Instructive, but takes quite a while.') parser.add_argument('-r', '--reflectance_map', metavar='RES', type=int, default=0, dest='reflectance_map', help='Generate a high-resolution reflectance map instead of the grid header.') parser.add_argument('cmf', metavar='CMF', type=str, help='The cmf file to be used.') args = parser.parse_args() # Init xystar. Cmf.load(args.cmf) Transform.init_xystar() last_time_stored = 0 clip_path,_ = horseshoe_path() # plot spectral locus # for i in range(0,Cmf.num_bins()): # print('{} {} {}'.format(Cmf.wavelength()[i], # Cmf.x_bar()[i]/(Cmf.x_bar()[i]+Cmf.y_bar()[i]+Cmf.z_bar()[i]), # Cmf.y_bar()[i]/(Cmf.x_bar()[i]+Cmf.y_bar()[i]+Cmf.z_bar()[i]))) if args.reflectance_map > 0: compute_reflectance_map(args.reflectance_map) else: # Generate the grid. data_points, grid, grid_res, xystar_bbox = generate_xystar_grid(args.scale) # Init uv. Transform.init_uv(xystar_bbox, grid_res) for dp in data_points: dp.update_uv() create_triangle_fans(grid) # plot_grid('grid.pdf', data_points, grid, xystar_bbox, False) # Compute spectra and store in spectrum_data.h compute_spectra(data_points) write_output(data_points, grid, grid_res, #'spectra_{}_{}.rs'.format(os.path.splitext(args.cmf)[0], args.scale)) 'meng_spectra_tables.rs') # Finally, plot all spectra. if args.plot: plot_spectra(data_points) ================================================ FILE: sub_crates/spectral_upsampling/src/meng/meng_spectra_tables.rs ================================================ // This file is auto-generated by generate_spectra_tables.py #![allow(dead_code)] #![cfg_attr(rustfmt, rustfmt_skip)] #![allow(clippy::unreadable_literal)] #![allow(clippy::excessive_precision)] /// This is 1 over the integral over either CMF. /// Spectra can be mapped so that xyz=(1,1,1) is converted to constant 1 by /// dividing by this value. This is important for valid reflectances. pub const EQUAL_ENERGY_REFLECTANCE: f32 = 0.009355121400914532; // Basic info on the spectrum grid. pub(crate) const SPECTRUM_GRID_WIDTH: i32 = 12; pub(crate) const SPECTRUM_GRID_HEIGHT: i32 = 14; // The spectra here have these properties. pub const SPECTRUM_SAMPLE_MIN: f32 = 360.0; pub const SPECTRUM_SAMPLE_MAX: f32 = 830.0; pub(crate) const SPECTRUM_BIN_SIZE: f32 = 5.0; pub(crate) const SPECTRUM_NUM_SAMPLES: i32 = 95; // xy* color space. pub(crate) const SPECTRUM_MAT_XY_TO_XYSTAR: [f32; 6] = [ 0.9067484787957371, 0.4216719058718719, -0.44280679488920294, -0.4216719058718719, 0.9067484787957371, -0.1616921909746217 ]; pub(crate) const SPECTRUM_MAT_XYSTAR_TO_XY: [f32; 6] = [ 0.9067484787957371, -0.4216719058718719, 0.33333333333333326, 0.4216719058718719, 0.9067484787957371, 0.3333333333333333 ]; // uv color space. pub(crate) const SPECTRUM_MAT_XY_TO_UV: [f32; 6] = [ 16.730260708356887, 7.7801960340706, -2.170152247475828, -7.530081094743006, 16.192422314095225, 1.1125529268825942 ]; pub(crate) const SPECTRUM_MAT_UV_TO_XY: [f32; 6] = [ 0.0491440520940413, -0.02361291916573777, 0.13292069743203658, 0.022853819546830627, 0.05077639329371236, -0.0068951571224999215 ]; // Grid cells. Laid out in row-major format. // num_points = 0 for cells without data points. #[derive(Copy, Clone)] pub(crate) struct SpectrumGridCell { pub inside: bool, pub num_points: i32, pub idx: [i32; 6], } pub(crate) const SPECTRUM_GRID: [SpectrumGridCell; 168] = [ SpectrumGridCell { inside: false, num_points: 5, idx: [148, 110, 0, 12, 111, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [0, 1, 12, 13, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [1, 2, 13, 14, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [2, 3, 14, 15, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [3, 4, 15, 16, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [4, 5, 16, 17, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [5, 6, 17, 18, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [6, 7, 18, 19, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [7, 8, 19, 20, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [8, 9, 20, 21, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [9, 10, 21, 22, -1, -1] }, SpectrumGridCell { inside: false, num_points: 5, idx: [149, 10, 11, 145, 22, -1] }, SpectrumGridCell { inside: false, num_points: 5, idx: [150, 111, 12, 23, 112, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [12, 13, 23, 24, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [13, 14, 24, 25, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [14, 15, 25, 26, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [15, 16, 26, 27, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [16, 17, 27, 28, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [17, 18, 28, 29, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [18, 19, 29, 30, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [19, 20, 30, 31, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [20, 21, 31, 32, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [21, 22, 32, 33, -1, -1] }, SpectrumGridCell { inside: false, num_points: 5, idx: [151, 22, 145, 146, 33, -1] }, SpectrumGridCell { inside: false, num_points: 5, idx: [152, 112, 23, 34, 113, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [23, 24, 34, 35, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [24, 25, 35, 36, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [25, 26, 36, 37, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [26, 27, 37, 38, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [27, 28, 38, 39, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [28, 29, 39, 40, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [29, 30, 40, 41, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [30, 31, 41, 42, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [31, 32, 42, 43, -1, -1] }, SpectrumGridCell { inside: false, num_points: 6, idx: [153, 32, 33, 147, 141, 43] }, SpectrumGridCell { inside: false, num_points: 4, idx: [154, 33, 146, 147, -1, -1] }, SpectrumGridCell { inside: false, num_points: 5, idx: [155, 113, 34, 44, 114, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [34, 35, 44, 45, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [35, 36, 45, 46, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [36, 37, 46, 47, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [37, 38, 47, 48, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [38, 39, 48, 49, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [39, 40, 49, 50, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [40, 41, 50, 51, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [41, 42, 51, 52, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [42, 43, 52, 53, -1, -1] }, SpectrumGridCell { inside: false, num_points: 5, idx: [156, 43, 141, 142, 53, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 5, idx: [157, 114, 44, 54, 115, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [44, 45, 54, 55, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [45, 46, 55, 56, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [46, 47, 56, 57, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [47, 48, 57, 58, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [48, 49, 58, 59, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [49, 50, 59, 60, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [50, 51, 60, 61, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [51, 52, 61, 62, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [52, 53, 62, 63, -1, -1] }, SpectrumGridCell { inside: false, num_points: 5, idx: [158, 53, 142, 143, 63, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 4, idx: [159, 115, 54, 116, -1, -1] }, SpectrumGridCell { inside: false, num_points: 6, idx: [160, 116, 54, 55, 64, 117] }, SpectrumGridCell { inside: true, num_points: 4, idx: [55, 56, 64, 65, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [56, 57, 65, 66, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [57, 58, 66, 67, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [58, 59, 67, 68, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [59, 60, 68, 69, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [60, 61, 69, 70, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [61, 62, 70, 71, -1, -1] }, SpectrumGridCell { inside: false, num_points: 6, idx: [161, 62, 63, 144, 138, 71] }, SpectrumGridCell { inside: false, num_points: 4, idx: [162, 63, 143, 144, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 5, idx: [163, 117, 64, 72, 118, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [64, 65, 72, 73, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [65, 66, 73, 74, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [66, 67, 74, 75, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [67, 68, 75, 76, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [68, 69, 76, 77, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [69, 70, 77, 78, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [70, 71, 78, 79, -1, -1] }, SpectrumGridCell { inside: false, num_points: 5, idx: [164, 71, 138, 139, 79, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 5, idx: [165, 118, 72, 80, 119, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [72, 73, 80, 81, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [73, 74, 81, 82, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [74, 75, 82, 83, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [75, 76, 83, 84, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [76, 77, 84, 85, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [77, 78, 85, 86, -1, -1] }, SpectrumGridCell { inside: false, num_points: 6, idx: [166, 78, 79, 140, 134, 86] }, SpectrumGridCell { inside: false, num_points: 4, idx: [167, 79, 139, 140, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 4, idx: [168, 119, 80, 120, -1, -1] }, SpectrumGridCell { inside: false, num_points: 6, idx: [169, 80, 81, 87, 121, 120] }, SpectrumGridCell { inside: true, num_points: 4, idx: [81, 82, 87, 88, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [82, 83, 88, 89, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [83, 84, 89, 90, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [84, 85, 90, 91, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [85, 86, 91, 92, -1, -1] }, SpectrumGridCell { inside: false, num_points: 5, idx: [170, 86, 134, 135, 92, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 5, idx: [171, 121, 87, 93, 122, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [87, 88, 93, 94, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [88, 89, 94, 95, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [89, 90, 95, 96, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [90, 91, 96, 97, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [91, 92, 97, 98, -1, -1] }, SpectrumGridCell { inside: false, num_points: 5, idx: [172, 92, 135, 136, 98, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 5, idx: [173, 122, 93, 99, 123, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [93, 94, 99, 100, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [94, 95, 100, 101, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [95, 96, 101, 102, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [96, 97, 102, 103, -1, -1] }, SpectrumGridCell { inside: false, num_points: 6, idx: [174, 97, 98, 137, 131, 103] }, SpectrumGridCell { inside: false, num_points: 4, idx: [175, 98, 136, 137, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 4, idx: [176, 123, 99, 124, -1, -1] }, SpectrumGridCell { inside: false, num_points: 6, idx: [177, 124, 99, 100, 104, 125] }, SpectrumGridCell { inside: true, num_points: 4, idx: [100, 101, 104, 105, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [101, 102, 105, 106, -1, -1] }, SpectrumGridCell { inside: true, num_points: 4, idx: [102, 103, 106, 107, -1, -1] }, SpectrumGridCell { inside: false, num_points: 5, idx: [178, 103, 131, 132, 107, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 4, idx: [179, 125, 104, 126, -1, -1] }, SpectrumGridCell { inside: false, num_points: 6, idx: [180, 104, 105, 108, 127, 126] }, SpectrumGridCell { inside: true, num_points: 4, idx: [105, 106, 108, 109, -1, -1] }, SpectrumGridCell { inside: false, num_points: 6, idx: [181, 106, 107, 133, 129, 109] }, SpectrumGridCell { inside: false, num_points: 4, idx: [182, 107, 132, 133, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 4, idx: [183, 127, 108, 128, -1, -1] }, SpectrumGridCell { inside: false, num_points: 5, idx: [184, 108, 109, 130, 128, -1] }, SpectrumGridCell { inside: false, num_points: 4, idx: [185, 109, 129, 130, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] }, SpectrumGridCell { inside: false, num_points: 0, idx: [-1, -1, -1, -1, -1, -1] } ]; // Grid data points. #[derive(Copy, Clone)] pub(crate) struct SpectrumDataPoint { pub xystar: (f32, f32), pub uv: (f32, f32), pub spectrum: [f32; 95], // X+Y+Z = 1 } pub(crate) const SPECTRUM_DATA_POINTS: [SpectrumDataPoint; 186] = [ SpectrumDataPoint { xystar: (-0.27099054061447164, -0.22399328802249138), uv: (0.9999999999999991, 0.0), spectrum: [0.023575, 0.023575, 0.023574, 0.023571, 0.023565, 0.023554, 0.023534, 0.023498, 0.023433, 0.023312, 0.023102, 0.022727, 0.022062, 0.020919, 0.019066, 0.016400, 0.013015, 0.009187, 0.005332, 0.002029, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000067, 0.000577, 0.001244, 0.001906, 0.002492, 0.002971, 0.003344, 0.003630, 0.003844, 0.004001, 0.004113, 0.004195, 0.004253, 0.004292, 0.004320, 0.004341, 0.004355, 0.004364, 0.004372, 0.004377, 0.004380, 0.004382, 0.004384, 0.004384, 0.004385, 0.004386, 0.004386, 0.004386, 0.004386, 0.004386, 0.004386, 0.004387, 0.004386, 0.004386, 0.004386, 0.004386, 0.004386, 0.004385, 0.004385, 0.004385], }, SpectrumDataPoint { xystar: (-0.21679243249157729, -0.22399328802249138), uv: (2.0, 0.0), spectrum: [0.023280, 0.023278, 0.023276, 0.023274, 0.023269, 0.023257, 0.023234, 0.023192, 0.023117, 0.022980, 0.022742, 0.022316, 0.021562, 0.020267, 0.018190, 0.015247, 0.011585, 0.007577, 0.003781, 0.000946, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000849, 0.002453, 0.004291, 0.006055, 0.007590, 0.008848, 0.009834, 0.010583, 0.011142, 0.011555, 0.011858, 0.012073, 0.012225, 0.012334, 0.012411, 0.012465, 0.012504, 0.012530, 0.012548, 0.012561, 0.012570, 0.012578, 0.012582, 0.012584, 0.012584, 0.012585, 0.012587, 0.012588, 0.012587, 0.012585, 0.012584, 0.012585, 0.012586, 0.012586, 0.012586, 0.012587, 0.012587, 0.012588, 0.012589, 0.012590, 0.012589], }, SpectrumDataPoint { xystar: (-0.16259432436868296, -0.22399328802249138), uv: (3.0, 0.0), spectrum: [0.023405, 0.023403, 0.023400, 0.023396, 0.023388, 0.023374, 0.023346, 0.023297, 0.023208, 0.023044, 0.022762, 0.022258, 0.021368, 0.019846, 0.017428, 0.014056, 0.009975, 0.005715, 0.002048, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000048, 0.002031, 0.004876, 0.007897, 0.010699, 0.013091, 0.015029, 0.016537, 0.017676, 0.018522, 0.019145, 0.019599, 0.019922, 0.020152, 0.020314, 0.020429, 0.020510, 0.020567, 0.020608, 0.020637, 0.020657, 0.020671, 0.020680, 0.020685, 0.020690, 0.020694, 0.020697, 0.020700, 0.020701, 0.020701, 0.020702, 0.020702, 0.020702, 0.020703, 0.020702, 0.020702, 0.020701, 0.020702, 0.020702, 0.020703, 0.020704, 0.020704], }, SpectrumDataPoint { xystar: (-0.10839621624578864, -0.22399328802249138), uv: (4.0, 0.0), spectrum: [0.023957, 0.023957, 0.023954, 0.023950, 0.023940, 0.023922, 0.023888, 0.023825, 0.023711, 0.023506, 0.023153, 0.022523, 0.021414, 0.019529, 0.016578, 0.012563, 0.007906, 0.003431, 0.000288, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.002783, 0.006879, 0.011267, 0.015355, 0.018852, 0.021687, 0.023894, 0.025564, 0.026807, 0.027722, 0.028387, 0.028862, 0.029198, 0.029436, 0.029606, 0.029726, 0.029811, 0.029872, 0.029914, 0.029943, 0.029963, 0.029977, 0.029987, 0.029994, 0.029999, 0.030002, 0.030004, 0.030004, 0.030005, 0.030007, 0.030008, 0.030009, 0.030009, 0.030009, 0.030010, 0.030010, 0.030010, 0.030010, 0.030009, 0.030009, 0.030009], }, SpectrumDataPoint { xystar: (-0.05419810812289431, -0.22399328802249138), uv: (5.0, 0.0), spectrum: [0.024728, 0.024726, 0.024723, 0.024716, 0.024702, 0.024677, 0.024633, 0.024552, 0.024405, 0.024135, 0.023669, 0.022845, 0.021408, 0.018992, 0.015295, 0.010469, 0.005302, 0.001183, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.002776, 0.008050, 0.014104, 0.019921, 0.024986, 0.029137, 0.032391, 0.034861, 0.036705, 0.038067, 0.039062, 0.039773, 0.040276, 0.040634, 0.040888, 0.041069, 0.041197, 0.041286, 0.041348, 0.041392, 0.041422, 0.041442, 0.041457, 0.041467, 0.041474, 0.041480, 0.041483, 0.041485, 0.041487, 0.041489, 0.041491, 0.041491, 0.041491, 0.041491, 0.041491, 0.041491, 0.041491, 0.041490, 0.041490, 0.041490, 0.041490], }, SpectrumDataPoint { xystar: (0.0, -0.22399328802249138), uv: (6.0, 0.0), spectrum: [0.025078, 0.025076, 0.025072, 0.025063, 0.025045, 0.025013, 0.024956, 0.024853, 0.024667, 0.024327, 0.023739, 0.022706, 0.020912, 0.017934, 0.013498, 0.008006, 0.002777, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.002130, 0.008537, 0.016516, 0.024438, 0.031454, 0.037262, 0.041845, 0.045339, 0.047956, 0.049893, 0.051309, 0.052324, 0.053043, 0.053554, 0.053918, 0.054177, 0.054359, 0.054487, 0.054577, 0.054640, 0.054684, 0.054714, 0.054736, 0.054750, 0.054761, 0.054768, 0.054773, 0.054777, 0.054779, 0.054780, 0.054781, 0.054782, 0.054783, 0.054783, 0.054783, 0.054784, 0.054784, 0.054784, 0.054784, 0.054783, 0.054784], }, SpectrumDataPoint { xystar: (0.05419810812289436, -0.22399328802249138), uv: (7.000000000000001, 0.0), spectrum: [0.024931, 0.024929, 0.024924, 0.024914, 0.024893, 0.024854, 0.024784, 0.024657, 0.024430, 0.024014, 0.023300, 0.022048, 0.019887, 0.016344, 0.011221, 0.005254, 0.000451, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000935, 0.008435, 0.018559, 0.028909, 0.038207, 0.045969, 0.052127, 0.056839, 0.060376, 0.062999, 0.064919, 0.066295, 0.067272, 0.067967, 0.068462, 0.068813, 0.069061, 0.069235, 0.069358, 0.069444, 0.069503, 0.069544, 0.069573, 0.069593, 0.069607, 0.069617, 0.069624, 0.069629, 0.069633, 0.069635, 0.069637, 0.069638, 0.069639, 0.069639, 0.069639, 0.069639, 0.069639, 0.069639, 0.069639, 0.069639, 0.069639], }, SpectrumDataPoint { xystar: (0.10839621624578867, -0.22399328802249138), uv: (8.0, 0.0), spectrum: [0.023903, 0.023900, 0.023894, 0.023881, 0.023856, 0.023808, 0.023723, 0.023570, 0.023296, 0.022795, 0.021938, 0.020448, 0.017902, 0.013818, 0.008231, 0.002550, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.007273, 0.019410, 0.032632, 0.044862, 0.055243, 0.063561, 0.069970, 0.074804, 0.078402, 0.081043, 0.082941, 0.084288, 0.085247, 0.085931, 0.086416, 0.086758, 0.087000, 0.087169, 0.087288, 0.087370, 0.087428, 0.087467, 0.087495, 0.087514, 0.087528, 0.087538, 0.087545, 0.087550, 0.087553, 0.087555, 0.087555, 0.087556, 0.087556, 0.087556, 0.087556, 0.087557, 0.087557, 0.087557, 0.087556, 0.087557], }, SpectrumDataPoint { xystar: (0.162594324368683, -0.22399328802249138), uv: (9.0, 0.0), spectrum: [0.022866, 0.022863, 0.022855, 0.022838, 0.022807, 0.022748, 0.022645, 0.022458, 0.022124, 0.021516, 0.020478, 0.018688, 0.015669, 0.010949, 0.004930, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.005327, 0.019065, 0.035406, 0.051084, 0.064651, 0.075650, 0.084187, 0.090661, 0.095500, 0.099063, 0.101628, 0.103452, 0.104751, 0.105676, 0.106332, 0.106796, 0.107123, 0.107352, 0.107513, 0.107625, 0.107703, 0.107757, 0.107795, 0.107821, 0.107840, 0.107853, 0.107862, 0.107869, 0.107873, 0.107876, 0.107878, 0.107880, 0.107881, 0.107882, 0.107882, 0.107883, 0.107883, 0.107883, 0.107883, 0.107883], }, SpectrumDataPoint { xystar: (0.21679243249157734, -0.22399328802249138), uv: (10.0, 0.0), spectrum: [0.019140, 0.019137, 0.019128, 0.019110, 0.019076, 0.019012, 0.018899, 0.018696, 0.018337, 0.017684, 0.016577, 0.014692, 0.011577, 0.006932, 0.001819, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.002556, 0.017879, 0.037710, 0.057330, 0.074579, 0.088691, 0.099708, 0.108097, 0.114386, 0.119028, 0.122375, 0.124758, 0.126455, 0.127665, 0.128523, 0.129129, 0.129556, 0.129857, 0.130066, 0.130213, 0.130314, 0.130384, 0.130434, 0.130469, 0.130493, 0.130511, 0.130523, 0.130532, 0.130538, 0.130542, 0.130545, 0.130547, 0.130548, 0.130548, 0.130549, 0.130549, 0.130549, 0.130549, 0.130550, 0.130550], }, SpectrumDataPoint { xystar: (0.27099054061447164, -0.22399328802249138), uv: (11.0, 0.0), spectrum: [0.013761, 0.013757, 0.013748, 0.013730, 0.013696, 0.013633, 0.013521, 0.013320, 0.012968, 0.012331, 0.011262, 0.009486, 0.006657, 0.002812, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.016185, 0.039428, 0.063213, 0.084465, 0.102012, 0.115790, 0.126325, 0.134246, 0.140107, 0.144339, 0.147353, 0.149500, 0.151032, 0.152117, 0.152886, 0.153427, 0.153806, 0.154071, 0.154256, 0.154384, 0.154473, 0.154536, 0.154581, 0.154614, 0.154636, 0.154651, 0.154662, 0.154669, 0.154674, 0.154678, 0.154681, 0.154683, 0.154684, 0.154685, 0.154684, 0.154685, 0.154685, 0.154686, 0.154687], }, SpectrumDataPoint { xystar: (0.3251886487373659, -0.22399328802249138), uv: (12.0, 0.0), spectrum: [0.003893, 0.003891, 0.003884, 0.003870, 0.003846, 0.003800, 0.003722, 0.003585, 0.003356, 0.002951, 0.002309, 0.001377, 0.000247, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.012060, 0.037710, 0.066528, 0.093367, 0.116030, 0.134066, 0.147987, 0.158525, 0.166362, 0.172040, 0.176091, 0.178980, 0.181040, 0.182501, 0.183534, 0.184262, 0.184772, 0.185130, 0.185380, 0.185554, 0.185675, 0.185759, 0.185819, 0.185861, 0.185890, 0.185911, 0.185926, 0.185936, 0.185943, 0.185948, 0.185951, 0.185953, 0.185955, 0.185956, 0.185957, 0.185958, 0.185958, 0.185958, 0.185958], }, SpectrumDataPoint { xystar: (-0.27099054061447164, -0.16799496601686853), uv: (0.9999999999999991, 1.0), spectrum: [0.009112, 0.009112, 0.009112, 0.009112, 0.009112, 0.009111, 0.009111, 0.009109, 0.009107, 0.009103, 0.009095, 0.009081, 0.009054, 0.009003, 0.008917, 0.008781, 0.008589, 0.008337, 0.008025, 0.007656, 0.007231, 0.006756, 0.006238, 0.005686, 0.005112, 0.004527, 0.003941, 0.003362, 0.002797, 0.002260, 0.001759, 0.001304, 0.000905, 0.000572, 0.000311, 0.000128, 0.000024, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000023, 0.000068, 0.000126, 0.000195, 0.000266, 0.000333, 0.000393, 0.000448, 0.000496, 0.000536, 0.000570, 0.000596, 0.000617, 0.000632, 0.000643, 0.000649, 0.000651, 0.000653, 0.000654, 0.000654, 0.000652, 0.000651, 0.000650, 0.000649, 0.000647, 0.000647, 0.000646, 0.000646, 0.000646, 0.000647, 0.000647, 0.000647, 0.000648, 0.000648, 0.000649, 0.000649, 0.000650, 0.000650, 0.000651, 0.000651, 0.000651, 0.000651, 0.000651, 0.000651, 0.000650, 0.000649, 0.000648, 0.000648, 0.000647], }, SpectrumDataPoint { xystar: (-0.21679243249157729, -0.16799496601686853), uv: (2.0, 1.0), spectrum: [0.008348, 0.008348, 0.008348, 0.008348, 0.008348, 0.008347, 0.008347, 0.008345, 0.008344, 0.008339, 0.008330, 0.008315, 0.008287, 0.008239, 0.008156, 0.008026, 0.007842, 0.007601, 0.007304, 0.006952, 0.006548, 0.006098, 0.005606, 0.005085, 0.004543, 0.003994, 0.003446, 0.002908, 0.002388, 0.001897, 0.001443, 0.001038, 0.000689, 0.000406, 0.000194, 0.000059, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000003, 0.000057, 0.000155, 0.000290, 0.000449, 0.000624, 0.000807, 0.000989, 0.001163, 0.001323, 0.001466, 0.001591, 0.001696, 0.001783, 0.001854, 0.001910, 0.001954, 0.001987, 0.002011, 0.002029, 0.002042, 0.002051, 0.002057, 0.002062, 0.002065, 0.002066, 0.002067, 0.002067, 0.002068, 0.002068, 0.002068, 0.002068, 0.002068, 0.002068, 0.002068, 0.002067, 0.002068, 0.002068, 0.002068, 0.002068, 0.002068, 0.002068, 0.002068, 0.002068, 0.002068, 0.002068, 0.002067, 0.002067, 0.002066, 0.002066, 0.002066, 0.002066, 0.002066, 0.002066], }, SpectrumDataPoint { xystar: (-0.16259432436868296, -0.16799496601686853), uv: (3.0, 1.0), spectrum: [0.007628, 0.007627, 0.007627, 0.007627, 0.007627, 0.007626, 0.007624, 0.007622, 0.007620, 0.007615, 0.007607, 0.007593, 0.007568, 0.007521, 0.007440, 0.007313, 0.007133, 0.006897, 0.006605, 0.006258, 0.005862, 0.005419, 0.004939, 0.004431, 0.003908, 0.003379, 0.002855, 0.002345, 0.001860, 0.001408, 0.001000, 0.000648, 0.000365, 0.000159, 0.000037, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000035, 0.000136, 0.000294, 0.000499, 0.000739, 0.001004, 0.001280, 0.001557, 0.001826, 0.002078, 0.002309, 0.002513, 0.002688, 0.002837, 0.002960, 0.003059, 0.003137, 0.003198, 0.003244, 0.003279, 0.003304, 0.003322, 0.003334, 0.003343, 0.003349, 0.003353, 0.003356, 0.003358, 0.003359, 0.003359, 0.003359, 0.003360, 0.003359, 0.003359, 0.003359, 0.003359, 0.003359, 0.003359, 0.003360, 0.003361, 0.003361, 0.003362, 0.003362, 0.003363, 0.003363, 0.003363, 0.003363, 0.003362, 0.003361, 0.003361, 0.003360, 0.003360, 0.003359, 0.003359, 0.003358], }, SpectrumDataPoint { xystar: (-0.10839621624578864, -0.16799496601686853), uv: (4.0, 1.0), spectrum: [0.006988, 0.006988, 0.006987, 0.006987, 0.006986, 0.006984, 0.006984, 0.006982, 0.006981, 0.006975, 0.006966, 0.006948, 0.006918, 0.006864, 0.006777, 0.006643, 0.006455, 0.006208, 0.005906, 0.005552, 0.005149, 0.004706, 0.004227, 0.003722, 0.003208, 0.002695, 0.002195, 0.001715, 0.001271, 0.000871, 0.000532, 0.000265, 0.000083, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000086, 0.000256, 0.000496, 0.000797, 0.001141, 0.001512, 0.001895, 0.002276, 0.002644, 0.002988, 0.003300, 0.003572, 0.003806, 0.004003, 0.004166, 0.004298, 0.004402, 0.004483, 0.004545, 0.004591, 0.004626, 0.004651, 0.004669, 0.004682, 0.004691, 0.004698, 0.004704, 0.004707, 0.004709, 0.004710, 0.004710, 0.004709, 0.004707, 0.004707, 0.004706, 0.004705, 0.004705, 0.004704, 0.004703, 0.004704, 0.004704, 0.004704, 0.004705, 0.004705, 0.004705, 0.004705, 0.004704, 0.004704, 0.004704, 0.004704, 0.004705, 0.004705, 0.004704, 0.004704, 0.004704], }, SpectrumDataPoint { xystar: (-0.05419810812289431, -0.16799496601686853), uv: (5.0, 1.0), spectrum: [0.006347, 0.006347, 0.006347, 0.006349, 0.006348, 0.006347, 0.006346, 0.006345, 0.006343, 0.006339, 0.006330, 0.006311, 0.006279, 0.006221, 0.006128, 0.005984, 0.005786, 0.005528, 0.005213, 0.004846, 0.004430, 0.003973, 0.003486, 0.002983, 0.002477, 0.001983, 0.001510, 0.001074, 0.000689, 0.000372, 0.000140, 0.000009, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000088, 0.000291, 0.000592, 0.000976, 0.001424, 0.001913, 0.002423, 0.002933, 0.003424, 0.003886, 0.004307, 0.004681, 0.005001, 0.005271, 0.005492, 0.005671, 0.005812, 0.005919, 0.006002, 0.006063, 0.006110, 0.006144, 0.006169, 0.006185, 0.006197, 0.006208, 0.006215, 0.006219, 0.006221, 0.006222, 0.006222, 0.006223, 0.006224, 0.006223, 0.006222, 0.006222, 0.006221, 0.006219, 0.006219, 0.006219, 0.006219, 0.006219, 0.006219, 0.006219, 0.006218, 0.006219, 0.006220, 0.006221, 0.006220, 0.006220, 0.006221, 0.006222, 0.006223, 0.006223, 0.006223], }, SpectrumDataPoint { xystar: (0.0, -0.16799496601686853), uv: (6.0, 1.0), spectrum: [0.005678, 0.005678, 0.005678, 0.005677, 0.005677, 0.005676, 0.005675, 0.005673, 0.005670, 0.005665, 0.005657, 0.005639, 0.005608, 0.005552, 0.005456, 0.005307, 0.005100, 0.004833, 0.004505, 0.004126, 0.003700, 0.003237, 0.002750, 0.002255, 0.001769, 0.001308, 0.000889, 0.000529, 0.000245, 0.000060, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000012, 0.000199, 0.000537, 0.001000, 0.001560, 0.002186, 0.002850, 0.003522, 0.004177, 0.004796, 0.005361, 0.005864, 0.006297, 0.006663, 0.006964, 0.007207, 0.007399, 0.007548, 0.007660, 0.007744, 0.007805, 0.007849, 0.007881, 0.007904, 0.007921, 0.007932, 0.007940, 0.007945, 0.007949, 0.007951, 0.007952, 0.007953, 0.007953, 0.007953, 0.007954, 0.007954, 0.007955, 0.007956, 0.007956, 0.007956, 0.007957, 0.007956, 0.007956, 0.007956, 0.007954, 0.007953, 0.007952, 0.007952, 0.007952, 0.007952, 0.007952, 0.007952, 0.007952, 0.007953, 0.007953], }, SpectrumDataPoint { xystar: (0.05419810812289436, -0.16799496601686853), uv: (7.000000000000001, 1.0), spectrum: [0.005026, 0.005026, 0.005026, 0.005026, 0.005025, 0.005024, 0.005023, 0.005021, 0.005017, 0.005011, 0.005000, 0.004981, 0.004946, 0.004884, 0.004780, 0.004620, 0.004399, 0.004115, 0.003773, 0.003378, 0.002943, 0.002478, 0.001999, 0.001526, 0.001079, 0.000681, 0.000353, 0.000118, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000078, 0.000389, 0.000894, 0.001553, 0.002323, 0.003160, 0.004024, 0.004877, 0.005688, 0.006434, 0.007100, 0.007677, 0.008166, 0.008568, 0.008894, 0.009153, 0.009353, 0.009505, 0.009619, 0.009702, 0.009762, 0.009805, 0.009835, 0.009857, 0.009872, 0.009882, 0.009890, 0.009895, 0.009898, 0.009900, 0.009902, 0.009903, 0.009904, 0.009904, 0.009905, 0.009905, 0.009906, 0.009906, 0.009906, 0.009906, 0.009906, 0.009906, 0.009905, 0.009905, 0.009905, 0.009905, 0.009905, 0.009905, 0.009905, 0.009905, 0.009905, 0.009905, 0.009905, 0.009905], }, SpectrumDataPoint { xystar: (0.10839621624578867, -0.16799496601686853), uv: (8.0, 1.0), spectrum: [0.004322, 0.004322, 0.004322, 0.004322, 0.004321, 0.004319, 0.004318, 0.004317, 0.004313, 0.004307, 0.004297, 0.004276, 0.004239, 0.004173, 0.004062, 0.003892, 0.003658, 0.003360, 0.003006, 0.002604, 0.002168, 0.001716, 0.001266, 0.000845, 0.000480, 0.000198, 0.000027, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000191, 0.000679, 0.001407, 0.002314, 0.003337, 0.004418, 0.005501, 0.006544, 0.007513, 0.008383, 0.009143, 0.009786, 0.010319, 0.010750, 0.011093, 0.011358, 0.011560, 0.011711, 0.011822, 0.011902, 0.011960, 0.012001, 0.012031, 0.012052, 0.012067, 0.012076, 0.012083, 0.012088, 0.012091, 0.012093, 0.012095, 0.012096, 0.012097, 0.012097, 0.012098, 0.012099, 0.012099, 0.012099, 0.012099, 0.012098, 0.012098, 0.012098, 0.012097, 0.012096, 0.012095, 0.012094, 0.012094, 0.012094, 0.012094, 0.012094, 0.012095, 0.012095, 0.012096], }, SpectrumDataPoint { xystar: (0.162594324368683, -0.16799496601686853), uv: (9.0, 1.0), spectrum: [0.003529, 0.003529, 0.003529, 0.003529, 0.003528, 0.003527, 0.003526, 0.003524, 0.003520, 0.003512, 0.003499, 0.003477, 0.003438, 0.003368, 0.003253, 0.003079, 0.002842, 0.002544, 0.002194, 0.001806, 0.001399, 0.000995, 0.000622, 0.000306, 0.000087, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000385, 0.001133, 0.002157, 0.003372, 0.004692, 0.006043, 0.007359, 0.008592, 0.009707, 0.010684, 0.011515, 0.012204, 0.012764, 0.013209, 0.013555, 0.013819, 0.014017, 0.014163, 0.014269, 0.014344, 0.014398, 0.014437, 0.014465, 0.014483, 0.014496, 0.014505, 0.014511, 0.014515, 0.014518, 0.014520, 0.014521, 0.014521, 0.014522, 0.014523, 0.014524, 0.014523, 0.014523, 0.014523, 0.014524, 0.014524, 0.014524, 0.014524, 0.014524, 0.014524, 0.014524, 0.014524, 0.014524, 0.014523, 0.014523, 0.014523, 0.014523, 0.014523], }, SpectrumDataPoint { xystar: (0.21679243249157734, -0.16799496601686853), uv: (10.0, 1.0), spectrum: [0.002618, 0.002618, 0.002617, 0.002618, 0.002618, 0.002618, 0.002617, 0.002614, 0.002609, 0.002601, 0.002589, 0.002567, 0.002527, 0.002457, 0.002341, 0.002168, 0.001939, 0.001655, 0.001333, 0.000992, 0.000655, 0.000354, 0.000123, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000107, 0.000762, 0.001841, 0.003227, 0.004797, 0.006445, 0.008081, 0.009630, 0.011042, 0.012286, 0.013350, 0.014237, 0.014960, 0.015536, 0.015984, 0.016326, 0.016582, 0.016770, 0.016908, 0.017007, 0.017078, 0.017129, 0.017165, 0.017189, 0.017206, 0.017219, 0.017227, 0.017233, 0.017238, 0.017239, 0.017241, 0.017243, 0.017243, 0.017243, 0.017244, 0.017244, 0.017244, 0.017244, 0.017244, 0.017243, 0.017242, 0.017242, 0.017242, 0.017241, 0.017242, 0.017242, 0.017242, 0.017241, 0.017242, 0.017242, 0.017242, 0.017243], }, SpectrumDataPoint { xystar: (0.27099054061447164, -0.16799496601686853), uv: (11.0, 1.0), spectrum: [0.001462, 0.001462, 0.001462, 0.001461, 0.001461, 0.001460, 0.001459, 0.001456, 0.001452, 0.001446, 0.001434, 0.001412, 0.001375, 0.001314, 0.001214, 0.001070, 0.000885, 0.000671, 0.000445, 0.000235, 0.000074, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000382, 0.001410, 0.002911, 0.004721, 0.006685, 0.008678, 0.010596, 0.012363, 0.013934, 0.015285, 0.016416, 0.017342, 0.018080, 0.018656, 0.019095, 0.019425, 0.019668, 0.019844, 0.019971, 0.020062, 0.020128, 0.020173, 0.020205, 0.020228, 0.020243, 0.020255, 0.020263, 0.020269, 0.020273, 0.020276, 0.020277, 0.020278, 0.020279, 0.020279, 0.020279, 0.020280, 0.020280, 0.020279, 0.020279, 0.020279, 0.020280, 0.020280, 0.020281, 0.020281, 0.020282, 0.020282, 0.020282, 0.020282, 0.020281, 0.020281, 0.020281], }, SpectrumDataPoint { xystar: (-0.27099054061447164, -0.11199664401124569), uv: (0.9999999999999991, 1.9999999999999998), spectrum: [0.007120, 0.007120, 0.007121, 0.007121, 0.007121, 0.007122, 0.007122, 0.007121, 0.007123, 0.007125, 0.007127, 0.007131, 0.007140, 0.007155, 0.007176, 0.007204, 0.007238, 0.007274, 0.007306, 0.007324, 0.007315, 0.007274, 0.007186, 0.007042, 0.006837, 0.006572, 0.006243, 0.005850, 0.005391, 0.004869, 0.004287, 0.003647, 0.002966, 0.002273, 0.001599, 0.000985, 0.000478, 0.000131, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000], }, SpectrumDataPoint { xystar: (-0.21679243249157729, -0.11199664401124569), uv: (2.0, 1.9999999999999998), spectrum: [0.007476, 0.007476, 0.007475, 0.007476, 0.007475, 0.007475, 0.007473, 0.007471, 0.007469, 0.007467, 0.007464, 0.007456, 0.007441, 0.007412, 0.007361, 0.007280, 0.007164, 0.007010, 0.006820, 0.006593, 0.006330, 0.006034, 0.005711, 0.005364, 0.005002, 0.004629, 0.004251, 0.003874, 0.003501, 0.003137, 0.002785, 0.002451, 0.002138, 0.001849, 0.001588, 0.001356, 0.001155, 0.000984, 0.000843, 0.000729, 0.000644, 0.000583, 0.000546, 0.000527, 0.000525, 0.000539, 0.000562, 0.000594, 0.000628, 0.000664, 0.000699, 0.000733, 0.000763, 0.000788, 0.000811, 0.000828, 0.000842, 0.000852, 0.000859, 0.000865, 0.000871, 0.000874, 0.000876, 0.000878, 0.000880, 0.000881, 0.000881, 0.000881, 0.000880, 0.000880, 0.000880, 0.000879, 0.000878, 0.000878, 0.000879, 0.000880, 0.000879, 0.000878, 0.000879, 0.000880, 0.000881, 0.000881, 0.000880, 0.000880, 0.000882, 0.000883, 0.000882, 0.000882, 0.000881, 0.000882, 0.000881, 0.000880, 0.000879, 0.000878, 0.000878], }, SpectrumDataPoint { xystar: (-0.16259432436868296, -0.11199664401124569), uv: (3.0, 1.9999999999999998), spectrum: [0.006771, 0.006771, 0.006770, 0.006769, 0.006769, 0.006768, 0.006767, 0.006766, 0.006765, 0.006763, 0.006759, 0.006750, 0.006734, 0.006703, 0.006651, 0.006568, 0.006453, 0.006302, 0.006116, 0.005894, 0.005639, 0.005354, 0.005044, 0.004715, 0.004373, 0.004022, 0.003670, 0.003321, 0.002978, 0.002648, 0.002332, 0.002036, 0.001764, 0.001524, 0.001316, 0.001145, 0.001010, 0.000910, 0.000847, 0.000817, 0.000819, 0.000849, 0.000903, 0.000976, 0.001064, 0.001163, 0.001267, 0.001372, 0.001473, 0.001568, 0.001656, 0.001733, 0.001799, 0.001855, 0.001900, 0.001937, 0.001965, 0.001989, 0.002007, 0.002021, 0.002032, 0.002041, 0.002047, 0.002050, 0.002053, 0.002055, 0.002057, 0.002058, 0.002058, 0.002059, 0.002058, 0.002058, 0.002057, 0.002057, 0.002056, 0.002055, 0.002054, 0.002054, 0.002054, 0.002054, 0.002054, 0.002054, 0.002054, 0.002053, 0.002052, 0.002051, 0.002049, 0.002048, 0.002047, 0.002047, 0.002047, 0.002048, 0.002048, 0.002049, 0.002048], }, SpectrumDataPoint { xystar: (-0.10839621624578864, -0.11199664401124569), uv: (4.0, 1.9999999999999998), spectrum: [0.006020, 0.006020, 0.006020, 0.006020, 0.006020, 0.006020, 0.006020, 0.006018, 0.006016, 0.006014, 0.006011, 0.006002, 0.005988, 0.005960, 0.005913, 0.005839, 0.005732, 0.005590, 0.005414, 0.005206, 0.004967, 0.004699, 0.004405, 0.004093, 0.003766, 0.003434, 0.003100, 0.002771, 0.002452, 0.002148, 0.001865, 0.001606, 0.001380, 0.001190, 0.001042, 0.000936, 0.000872, 0.000849, 0.000864, 0.000916, 0.001002, 0.001118, 0.001259, 0.001419, 0.001591, 0.001772, 0.001955, 0.002135, 0.002307, 0.002464, 0.002608, 0.002734, 0.002844, 0.002934, 0.003007, 0.003067, 0.003114, 0.003151, 0.003178, 0.003197, 0.003210, 0.003219, 0.003224, 0.003228, 0.003229, 0.003230, 0.003230, 0.003231, 0.003232, 0.003233, 0.003234, 0.003234, 0.003235, 0.003236, 0.003236, 0.003237, 0.003238, 0.003239, 0.003239, 0.003239, 0.003239, 0.003238, 0.003238, 0.003238, 0.003237, 0.003236, 0.003236, 0.003235, 0.003235, 0.003234, 0.003234, 0.003233, 0.003233, 0.003232, 0.003232], }, SpectrumDataPoint { xystar: (-0.05419810812289431, -0.11199664401124569), uv: (5.0, 1.9999999999999998), spectrum: [0.005330, 0.005330, 0.005330, 0.005330, 0.005329, 0.005330, 0.005330, 0.005330, 0.005329, 0.005325, 0.005319, 0.005309, 0.005293, 0.005262, 0.005212, 0.005135, 0.005026, 0.004886, 0.004714, 0.004511, 0.004278, 0.004016, 0.003732, 0.003432, 0.003121, 0.002808, 0.002495, 0.002191, 0.001901, 0.001629, 0.001384, 0.001170, 0.000995, 0.000861, 0.000773, 0.000732, 0.000739, 0.000791, 0.000886, 0.001021, 0.001192, 0.001396, 0.001625, 0.001872, 0.002131, 0.002394, 0.002655, 0.002908, 0.003145, 0.003364, 0.003557, 0.003727, 0.003873, 0.003994, 0.004093, 0.004172, 0.004234, 0.004283, 0.004319, 0.004346, 0.004364, 0.004378, 0.004387, 0.004394, 0.004398, 0.004401, 0.004404, 0.004405, 0.004406, 0.004407, 0.004408, 0.004408, 0.004407, 0.004407, 0.004407, 0.004407, 0.004407, 0.004406, 0.004407, 0.004406, 0.004405, 0.004406, 0.004406, 0.004405, 0.004405, 0.004405, 0.004405, 0.004405, 0.004406, 0.004406, 0.004406, 0.004406, 0.004405, 0.004405, 0.004405], }, SpectrumDataPoint { xystar: (0.0, -0.11199664401124569), uv: (6.0, 1.9999999999999998), spectrum: [0.004609, 0.004609, 0.004609, 0.004609, 0.004608, 0.004607, 0.004607, 0.004606, 0.004605, 0.004603, 0.004599, 0.004591, 0.004575, 0.004546, 0.004498, 0.004425, 0.004321, 0.004186, 0.004018, 0.003818, 0.003592, 0.003339, 0.003066, 0.002780, 0.002486, 0.002190, 0.001901, 0.001623, 0.001362, 0.001123, 0.000915, 0.000743, 0.000614, 0.000531, 0.000501, 0.000525, 0.000601, 0.000729, 0.000904, 0.001122, 0.001380, 0.001670, 0.001987, 0.002322, 0.002666, 0.003012, 0.003351, 0.003678, 0.003982, 0.004259, 0.004508, 0.004723, 0.004908, 0.005061, 0.005186, 0.005286, 0.005367, 0.005427, 0.005472, 0.005506, 0.005530, 0.005547, 0.005558, 0.005567, 0.005574, 0.005579, 0.005582, 0.005585, 0.005586, 0.005586, 0.005586, 0.005586, 0.005586, 0.005586, 0.005586, 0.005586, 0.005586, 0.005586, 0.005587, 0.005587, 0.005586, 0.005586, 0.005586, 0.005585, 0.005585, 0.005584, 0.005585, 0.005586, 0.005586, 0.005586, 0.005586, 0.005585, 0.005585, 0.005585, 0.005585], }, SpectrumDataPoint { xystar: (0.05419810812289436, -0.11199664401124569), uv: (7.000000000000001, 1.9999999999999998), spectrum: [0.003895, 0.003896, 0.003896, 0.003896, 0.003896, 0.003897, 0.003896, 0.003895, 0.003894, 0.003892, 0.003887, 0.003877, 0.003860, 0.003830, 0.003783, 0.003710, 0.003608, 0.003476, 0.003314, 0.003123, 0.002906, 0.002667, 0.002408, 0.002137, 0.001860, 0.001585, 0.001318, 0.001064, 0.000829, 0.000622, 0.000448, 0.000316, 0.000232, 0.000203, 0.000231, 0.000317, 0.000462, 0.000662, 0.000916, 0.001218, 0.001561, 0.001940, 0.002346, 0.002770, 0.003203, 0.003633, 0.004051, 0.004451, 0.004822, 0.005161, 0.005462, 0.005724, 0.005946, 0.006131, 0.006280, 0.006400, 0.006493, 0.006565, 0.006619, 0.006658, 0.006687, 0.006708, 0.006723, 0.006734, 0.006742, 0.006748, 0.006751, 0.006754, 0.006756, 0.006757, 0.006758, 0.006758, 0.006758, 0.006758, 0.006758, 0.006757, 0.006757, 0.006756, 0.006755, 0.006755, 0.006755, 0.006754, 0.006754, 0.006755, 0.006755, 0.006754, 0.006754, 0.006754, 0.006754, 0.006754, 0.006754, 0.006754, 0.006754, 0.006755, 0.006755], }, SpectrumDataPoint { xystar: (0.10839621624578867, -0.11199664401124569), uv: (8.0, 1.9999999999999998), spectrum: [0.003179, 0.003179, 0.003180, 0.003180, 0.003180, 0.003180, 0.003179, 0.003178, 0.003176, 0.003172, 0.003166, 0.003156, 0.003139, 0.003109, 0.003061, 0.002990, 0.002891, 0.002762, 0.002605, 0.002422, 0.002215, 0.001988, 0.001743, 0.001490, 0.001237, 0.000987, 0.000750, 0.000531, 0.000339, 0.000181, 0.000066, 0.000002, 0.000000, 0.000000, 0.000030, 0.000135, 0.000310, 0.000556, 0.000867, 0.001237, 0.001660, 0.002124, 0.002623, 0.003143, 0.003674, 0.004202, 0.004716, 0.005207, 0.005663, 0.006078, 0.006447, 0.006766, 0.007037, 0.007261, 0.007445, 0.007591, 0.007706, 0.007795, 0.007863, 0.007914, 0.007952, 0.007979, 0.007999, 0.008013, 0.008023, 0.008029, 0.008032, 0.008035, 0.008037, 0.008038, 0.008039, 0.008039, 0.008039, 0.008038, 0.008037, 0.008036, 0.008035, 0.008034, 0.008034, 0.008034, 0.008034, 0.008034, 0.008035, 0.008036, 0.008036, 0.008035, 0.008035, 0.008034, 0.008034, 0.008033, 0.008033, 0.008033, 0.008033, 0.008034, 0.008034], }, SpectrumDataPoint { xystar: (0.162594324368683, -0.11199664401124569), uv: (9.0, 1.9999999999999998), spectrum: [0.002410, 0.002410, 0.002410, 0.002410, 0.002409, 0.002409, 0.002408, 0.002407, 0.002406, 0.002404, 0.002399, 0.002391, 0.002376, 0.002348, 0.002304, 0.002236, 0.002141, 0.002019, 0.001870, 0.001698, 0.001506, 0.001299, 0.001085, 0.000869, 0.000660, 0.000465, 0.000294, 0.000154, 0.000053, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000013, 0.000130, 0.000349, 0.000663, 0.001064, 0.001543, 0.002089, 0.002687, 0.003322, 0.003980, 0.004642, 0.005293, 0.005918, 0.006501, 0.007035, 0.007510, 0.007925, 0.008278, 0.008572, 0.008811, 0.009002, 0.009151, 0.009266, 0.009354, 0.009420, 0.009468, 0.009502, 0.009526, 0.009543, 0.009556, 0.009564, 0.009569, 0.009573, 0.009576, 0.009577, 0.009579, 0.009580, 0.009580, 0.009581, 0.009581, 0.009581, 0.009581, 0.009581, 0.009581, 0.009581, 0.009581, 0.009581, 0.009580, 0.009580, 0.009579, 0.009579, 0.009579, 0.009578, 0.009577, 0.009577, 0.009577, 0.009578, 0.009579, 0.009580, 0.009581], }, SpectrumDataPoint { xystar: (0.21679243249157734, -0.11199664401124569), uv: (10.0, 1.9999999999999998), spectrum: [0.001571, 0.001571, 0.001571, 0.001572, 0.001572, 0.001571, 0.001571, 0.001571, 0.001570, 0.001567, 0.001563, 0.001555, 0.001541, 0.001518, 0.001478, 0.001417, 0.001334, 0.001229, 0.001102, 0.000958, 0.000801, 0.000639, 0.000477, 0.000324, 0.000191, 0.000086, 0.000019, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000130, 0.000400, 0.000800, 0.001318, 0.001937, 0.002637, 0.003399, 0.004200, 0.005016, 0.005824, 0.006605, 0.007338, 0.008012, 0.008617, 0.009145, 0.009595, 0.009971, 0.010278, 0.010524, 0.010718, 0.010868, 0.010981, 0.011064, 0.011125, 0.011169, 0.011201, 0.011224, 0.011240, 0.011251, 0.011259, 0.011265, 0.011269, 0.011272, 0.011274, 0.011275, 0.011276, 0.011275, 0.011276, 0.011275, 0.011275, 0.011274, 0.011274, 0.011274, 0.011273, 0.011274, 0.011274, 0.011274, 0.011274, 0.011274, 0.011274, 0.011274, 0.011274, 0.011274, 0.011274, 0.011273, 0.011273, 0.011273, 0.011273], }, SpectrumDataPoint { xystar: (0.27099054061447164, -0.11199664401124569), uv: (11.0, 1.9999999999999998), spectrum: [0.000577, 0.000577, 0.000578, 0.000580, 0.000582, 0.000583, 0.000584, 0.000583, 0.000581, 0.000578, 0.000574, 0.000568, 0.000558, 0.000542, 0.000514, 0.000474, 0.000422, 0.000356, 0.000284, 0.000206, 0.000130, 0.000066, 0.000018, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000152, 0.000495, 0.001009, 0.001672, 0.002460, 0.003344, 0.004295, 0.005282, 0.006273, 0.007241, 0.008158, 0.009005, 0.009766, 0.010434, 0.011005, 0.011483, 0.011874, 0.012188, 0.012435, 0.012624, 0.012766, 0.012871, 0.012948, 0.013004, 0.013044, 0.013074, 0.013096, 0.013112, 0.013123, 0.013131, 0.013136, 0.013141, 0.013143, 0.013142, 0.013141, 0.013140, 0.013139, 0.013140, 0.013139, 0.013141, 0.013141, 0.013141, 0.013142, 0.013141, 0.013142, 0.013142, 0.013142, 0.013142, 0.013142, 0.013141, 0.013142, 0.013143, 0.013143, 0.013144, 0.013143, 0.013143, 0.013143], }, SpectrumDataPoint { xystar: (-0.27099054061447164, -0.05599832200562286), uv: (0.9999999999999991, 2.999999999999999), spectrum: [0.000957, 0.000958, 0.000957, 0.000958, 0.000959, 0.000961, 0.000967, 0.000977, 0.000994, 0.001028, 0.001083, 0.001179, 0.001346, 0.001632, 0.002099, 0.002790, 0.003714, 0.004851, 0.006154, 0.007554, 0.008965, 0.010288, 0.011420, 0.012276, 0.012792, 0.012927, 0.012661, 0.011995, 0.010942, 0.009517, 0.007762, 0.005768, 0.003708, 0.001834, 0.000469, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000001, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000001, 0.000001, 0.000001, 0.000001, 0.000000], }, SpectrumDataPoint { xystar: (-0.21679243249157729, -0.05599832200562286), uv: (2.0, 2.999999999999999), spectrum: [0.006672, 0.006673, 0.006672, 0.006673, 0.006672, 0.006672, 0.006671, 0.006671, 0.006670, 0.006669, 0.006667, 0.006663, 0.006655, 0.006640, 0.006614, 0.006575, 0.006517, 0.006441, 0.006347, 0.006233, 0.006100, 0.005949, 0.005781, 0.005597, 0.005399, 0.005190, 0.004970, 0.004743, 0.004509, 0.004269, 0.004021, 0.003768, 0.003509, 0.003246, 0.002982, 0.002716, 0.002451, 0.002187, 0.001926, 0.001672, 0.001427, 0.001194, 0.000975, 0.000771, 0.000588, 0.000425, 0.000288, 0.000177, 0.000092, 0.000033, 0.000002, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000002, 0.000001, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000001, 0.000001, 0.000001, 0.000001], }, SpectrumDataPoint { xystar: (-0.16259432436868296, -0.05599832200562286), uv: (3.0, 2.999999999999999), spectrum: [0.006014, 0.006014, 0.006015, 0.006015, 0.006014, 0.006014, 0.006014, 0.006013, 0.006012, 0.006011, 0.006009, 0.006005, 0.005998, 0.005983, 0.005956, 0.005913, 0.005852, 0.005771, 0.005670, 0.005548, 0.005407, 0.005248, 0.005073, 0.004886, 0.004688, 0.004484, 0.004276, 0.004064, 0.003851, 0.003637, 0.003426, 0.003217, 0.003013, 0.002814, 0.002624, 0.002443, 0.002271, 0.002110, 0.001960, 0.001822, 0.001694, 0.001578, 0.001472, 0.001377, 0.001292, 0.001215, 0.001148, 0.001090, 0.001039, 0.000996, 0.000959, 0.000928, 0.000902, 0.000882, 0.000867, 0.000855, 0.000845, 0.000838, 0.000833, 0.000829, 0.000827, 0.000825, 0.000824, 0.000822, 0.000822, 0.000821, 0.000821, 0.000820, 0.000819, 0.000818, 0.000818, 0.000817, 0.000816, 0.000816, 0.000816, 0.000816, 0.000815, 0.000815, 0.000815, 0.000814, 0.000814, 0.000814, 0.000814, 0.000814, 0.000814, 0.000814, 0.000814, 0.000814, 0.000813, 0.000813, 0.000813, 0.000812, 0.000812, 0.000812, 0.000812], }, SpectrumDataPoint { xystar: (-0.10839621624578864, -0.05599832200562286), uv: (4.0, 2.999999999999999), spectrum: [0.005300, 0.005300, 0.005299, 0.005299, 0.005299, 0.005299, 0.005299, 0.005299, 0.005298, 0.005296, 0.005292, 0.005288, 0.005279, 0.005265, 0.005239, 0.005198, 0.005141, 0.005067, 0.004972, 0.004858, 0.004726, 0.004578, 0.004414, 0.004240, 0.004057, 0.003869, 0.003680, 0.003490, 0.003304, 0.003122, 0.002947, 0.002781, 0.002625, 0.002481, 0.002351, 0.002237, 0.002138, 0.002055, 0.001985, 0.001929, 0.001887, 0.001856, 0.001837, 0.001826, 0.001825, 0.001830, 0.001840, 0.001854, 0.001872, 0.001889, 0.001906, 0.001924, 0.001939, 0.001951, 0.001962, 0.001971, 0.001979, 0.001984, 0.001988, 0.001991, 0.001994, 0.001995, 0.001996, 0.001998, 0.001998, 0.001998, 0.001999, 0.001998, 0.001998, 0.001998, 0.001998, 0.001998, 0.001998, 0.001997, 0.001996, 0.001996, 0.001995, 0.001995, 0.001995, 0.001995, 0.001995, 0.001995, 0.001995, 0.001995, 0.001995, 0.001996, 0.001996, 0.001996, 0.001996, 0.001995, 0.001995, 0.001995, 0.001994, 0.001995, 0.001995], }, SpectrumDataPoint { xystar: (-0.05419810812289431, -0.05599832200562286), uv: (5.0, 2.999999999999999), spectrum: [0.004560, 0.004561, 0.004561, 0.004561, 0.004561, 0.004560, 0.004562, 0.004562, 0.004561, 0.004560, 0.004557, 0.004553, 0.004544, 0.004530, 0.004507, 0.004470, 0.004418, 0.004350, 0.004264, 0.004163, 0.004046, 0.003914, 0.003769, 0.003614, 0.003454, 0.003288, 0.003121, 0.002955, 0.002794, 0.002643, 0.002502, 0.002372, 0.002255, 0.002158, 0.002082, 0.002026, 0.001993, 0.001981, 0.001986, 0.002012, 0.002054, 0.002112, 0.002183, 0.002263, 0.002351, 0.002443, 0.002536, 0.002628, 0.002714, 0.002795, 0.002867, 0.002930, 0.002986, 0.003032, 0.003070, 0.003101, 0.003123, 0.003141, 0.003154, 0.003164, 0.003171, 0.003175, 0.003177, 0.003178, 0.003177, 0.003177, 0.003177, 0.003178, 0.003178, 0.003179, 0.003179, 0.003179, 0.003179, 0.003180, 0.003180, 0.003181, 0.003181, 0.003180, 0.003181, 0.003180, 0.003180, 0.003179, 0.003178, 0.003178, 0.003178, 0.003178, 0.003179, 0.003179, 0.003178, 0.003178, 0.003178, 0.003178, 0.003178, 0.003178, 0.003178], }, SpectrumDataPoint { xystar: (0.0, -0.05599832200562286), uv: (6.0, 2.999999999999999), spectrum: [0.003867, 0.003867, 0.003867, 0.003867, 0.003867, 0.003868, 0.003868, 0.003868, 0.003867, 0.003865, 0.003862, 0.003857, 0.003849, 0.003833, 0.003809, 0.003771, 0.003719, 0.003651, 0.003568, 0.003469, 0.003354, 0.003228, 0.003092, 0.002949, 0.002802, 0.002654, 0.002510, 0.002372, 0.002241, 0.002121, 0.002017, 0.001930, 0.001866, 0.001825, 0.001810, 0.001822, 0.001859, 0.001923, 0.002011, 0.002121, 0.002251, 0.002396, 0.002553, 0.002721, 0.002894, 0.003067, 0.003237, 0.003400, 0.003552, 0.003691, 0.003814, 0.003921, 0.004012, 0.004089, 0.004152, 0.004202, 0.004240, 0.004269, 0.004292, 0.004309, 0.004321, 0.004330, 0.004336, 0.004340, 0.004343, 0.004345, 0.004346, 0.004347, 0.004348, 0.004348, 0.004348, 0.004348, 0.004348, 0.004348, 0.004347, 0.004347, 0.004346, 0.004346, 0.004346, 0.004345, 0.004345, 0.004345, 0.004345, 0.004345, 0.004344, 0.004344, 0.004344, 0.004344, 0.004344, 0.004344, 0.004344, 0.004344, 0.004343, 0.004343, 0.004343], }, SpectrumDataPoint { xystar: (0.05419810812289436, -0.05599832200562286), uv: (7.000000000000001, 2.999999999999999), spectrum: [0.003146, 0.003146, 0.003147, 0.003147, 0.003147, 0.003148, 0.003148, 0.003148, 0.003149, 0.003150, 0.003147, 0.003144, 0.003137, 0.003124, 0.003101, 0.003065, 0.003016, 0.002951, 0.002870, 0.002774, 0.002667, 0.002549, 0.002423, 0.002292, 0.002160, 0.002031, 0.001910, 0.001799, 0.001700, 0.001616, 0.001550, 0.001506, 0.001490, 0.001502, 0.001547, 0.001622, 0.001726, 0.001863, 0.002027, 0.002218, 0.002432, 0.002664, 0.002911, 0.003166, 0.003424, 0.003681, 0.003930, 0.004168, 0.004388, 0.004589, 0.004767, 0.004921, 0.005053, 0.005162, 0.005250, 0.005320, 0.005374, 0.005415, 0.005445, 0.005467, 0.005484, 0.005496, 0.005505, 0.005512, 0.005517, 0.005520, 0.005524, 0.005525, 0.005526, 0.005527, 0.005527, 0.005528, 0.005528, 0.005527, 0.005527, 0.005527, 0.005527, 0.005526, 0.005525, 0.005525, 0.005523, 0.005523, 0.005522, 0.005522, 0.005523, 0.005522, 0.005522, 0.005522, 0.005522, 0.005522, 0.005523, 0.005522, 0.005522, 0.005522, 0.005522], }, SpectrumDataPoint { xystar: (0.10839621624578867, -0.05599832200562286), uv: (8.0, 2.999999999999999), spectrum: [0.002426, 0.002428, 0.002429, 0.002430, 0.002427, 0.002430, 0.002432, 0.002431, 0.002427, 0.002426, 0.002424, 0.002418, 0.002410, 0.002396, 0.002373, 0.002341, 0.002299, 0.002239, 0.002164, 0.002078, 0.001986, 0.001884, 0.001772, 0.001661, 0.001549, 0.001443, 0.001343, 0.001252, 0.001176, 0.001117, 0.001083, 0.001073, 0.001099, 0.001163, 0.001260, 0.001401, 0.001578, 0.001792, 0.002042, 0.002319, 0.002621, 0.002943, 0.003279, 0.003622, 0.003966, 0.004306, 0.004633, 0.004943, 0.005230, 0.005487, 0.005716, 0.005917, 0.006084, 0.006225, 0.006341, 0.006430, 0.006499, 0.006555, 0.006594, 0.006624, 0.006649, 0.006661, 0.006668, 0.006677, 0.006681, 0.006683, 0.006686, 0.006688, 0.006687, 0.006690, 0.006694, 0.006695, 0.006696, 0.006695, 0.006693, 0.006692, 0.006691, 0.006691, 0.006691, 0.006689, 0.006688, 0.006690, 0.006691, 0.006692, 0.006694, 0.006692, 0.006692, 0.006694, 0.006693, 0.006692, 0.006692, 0.006692, 0.006691, 0.006692, 0.006692], }, SpectrumDataPoint { xystar: (0.162594324368683, -0.05599832200562286), uv: (9.0, 2.999999999999999), spectrum: [0.001712, 0.001712, 0.001712, 0.001712, 0.001712, 0.001711, 0.001711, 0.001711, 0.001711, 0.001711, 0.001710, 0.001707, 0.001701, 0.001691, 0.001670, 0.001636, 0.001591, 0.001533, 0.001465, 0.001386, 0.001296, 0.001202, 0.001103, 0.001004, 0.000910, 0.000824, 0.000746, 0.000683, 0.000637, 0.000614, 0.000617, 0.000654, 0.000727, 0.000842, 0.001000, 0.001201, 0.001446, 0.001732, 0.002055, 0.002412, 0.002798, 0.003207, 0.003630, 0.004063, 0.004495, 0.004919, 0.005327, 0.005712, 0.006068, 0.006390, 0.006674, 0.006920, 0.007128, 0.007300, 0.007438, 0.007548, 0.007634, 0.007698, 0.007746, 0.007782, 0.007806, 0.007824, 0.007837, 0.007846, 0.007852, 0.007857, 0.007860, 0.007862, 0.007864, 0.007865, 0.007866, 0.007867, 0.007868, 0.007868, 0.007869, 0.007869, 0.007869, 0.007870, 0.007870, 0.007870, 0.007870, 0.007869, 0.007869, 0.007869, 0.007868, 0.007868, 0.007868, 0.007867, 0.007867, 0.007868, 0.007868, 0.007868, 0.007868, 0.007869, 0.007869], }, SpectrumDataPoint { xystar: (0.21679243249157734, -0.05599832200562286), uv: (10.0, 2.999999999999999), spectrum: [0.001022, 0.001024, 0.001025, 0.001025, 0.001027, 0.001024, 0.001024, 0.001023, 0.001021, 0.001013, 0.001012, 0.001003, 0.000991, 0.000972, 0.000948, 0.000920, 0.000877, 0.000822, 0.000756, 0.000687, 0.000610, 0.000531, 0.000449, 0.000363, 0.000286, 0.000219, 0.000164, 0.000127, 0.000108, 0.000116, 0.000155, 0.000230, 0.000351, 0.000517, 0.000732, 0.000997, 0.001312, 0.001672, 0.002072, 0.002511, 0.002983, 0.003478, 0.003990, 0.004506, 0.005021, 0.005522, 0.006006, 0.006466, 0.006887, 0.007275, 0.007609, 0.007907, 0.008161, 0.008372, 0.008548, 0.008686, 0.008800, 0.008885, 0.008953, 0.009000, 0.009038, 0.009066, 0.009087, 0.009099, 0.009106, 0.009110, 0.009116, 0.009119, 0.009119, 0.009119, 0.009119, 0.009119, 0.009115, 0.009115, 0.009112, 0.009112, 0.009111, 0.009111, 0.009110, 0.009108, 0.009109, 0.009108, 0.009106, 0.009107, 0.009106, 0.009105, 0.009105, 0.009103, 0.009103, 0.009102, 0.009102, 0.009102, 0.009102, 0.009101, 0.009102], }, SpectrumDataPoint { xystar: (-0.27099054061447164, 0.0), uv: (0.9999999999999991, 3.9999999999999996), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000071, 0.001598, 0.004288, 0.007769, 0.011604, 0.015331, 0.018511, 0.020803, 0.021982, 0.021926, 0.020638, 0.018184, 0.014704, 0.010461, 0.005968, 0.002079, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.21679243249157729, 0.0), uv: (2.0, 3.9999999999999996), spectrum: [0.004842, 0.004842, 0.004843, 0.004843, 0.004843, 0.004844, 0.004845, 0.004846, 0.004849, 0.004852, 0.004859, 0.004872, 0.004893, 0.004933, 0.005000, 0.005100, 0.005236, 0.005408, 0.005613, 0.005842, 0.006084, 0.006328, 0.006566, 0.006782, 0.006964, 0.007102, 0.007186, 0.007210, 0.007163, 0.007035, 0.006815, 0.006496, 0.006070, 0.005541, 0.004920, 0.004228, 0.003491, 0.002736, 0.001995, 0.001306, 0.000715, 0.000268, 0.000013, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000000, 0.000001, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.16259432436868296, 0.0), uv: (3.0, 3.9999999999999996), spectrum: [0.005225, 0.005225, 0.005225, 0.005225, 0.005226, 0.005227, 0.005226, 0.005227, 0.005227, 0.005228, 0.005227, 0.005228, 0.005227, 0.005228, 0.005226, 0.005224, 0.005220, 0.005213, 0.005204, 0.005192, 0.005177, 0.005157, 0.005132, 0.005099, 0.005060, 0.005012, 0.004957, 0.004893, 0.004816, 0.004725, 0.004620, 0.004496, 0.004351, 0.004188, 0.004001, 0.003794, 0.003567, 0.003321, 0.003060, 0.002789, 0.002510, 0.002226, 0.001942, 0.001663, 0.001394, 0.001140, 0.000904, 0.000690, 0.000503, 0.000347, 0.000219, 0.000123, 0.000055, 0.000015, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000002, 0.000001, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000002], }, SpectrumDataPoint { xystar: (-0.10839621624578864, 0.0), uv: (4.0, 3.9999999999999996), spectrum: [0.004583, 0.004582, 0.004582, 0.004582, 0.004582, 0.004582, 0.004582, 0.004582, 0.004582, 0.004581, 0.004578, 0.004574, 0.004571, 0.004565, 0.004558, 0.004550, 0.004541, 0.004530, 0.004517, 0.004500, 0.004481, 0.004460, 0.004436, 0.004407, 0.004372, 0.004331, 0.004286, 0.004235, 0.004178, 0.004115, 0.004043, 0.003963, 0.003873, 0.003770, 0.003657, 0.003534, 0.003397, 0.003251, 0.003094, 0.002929, 0.002759, 0.002584, 0.002408, 0.002232, 0.002059, 0.001892, 0.001732, 0.001582, 0.001445, 0.001321, 0.001212, 0.001117, 0.001038, 0.000973, 0.000920, 0.000877, 0.000844, 0.000818, 0.000798, 0.000783, 0.000773, 0.000765, 0.000760, 0.000757, 0.000755, 0.000754, 0.000752, 0.000751, 0.000750, 0.000749, 0.000748, 0.000747, 0.000746, 0.000745, 0.000745, 0.000744, 0.000743, 0.000742, 0.000741, 0.000740, 0.000739, 0.000739, 0.000739, 0.000739, 0.000739, 0.000741, 0.000741, 0.000741, 0.000742, 0.000742, 0.000741, 0.000741, 0.000740, 0.000740, 0.000740], }, SpectrumDataPoint { xystar: (-0.05419810812289431, 0.0), uv: (5.0, 3.9999999999999996), spectrum: [0.003833, 0.003833, 0.003833, 0.003833, 0.003834, 0.003834, 0.003835, 0.003835, 0.003835, 0.003835, 0.003835, 0.003835, 0.003835, 0.003834, 0.003832, 0.003829, 0.003825, 0.003821, 0.003816, 0.003810, 0.003803, 0.003794, 0.003783, 0.003769, 0.003752, 0.003734, 0.003713, 0.003689, 0.003661, 0.003628, 0.003592, 0.003551, 0.003504, 0.003451, 0.003391, 0.003326, 0.003255, 0.003180, 0.003100, 0.003017, 0.002931, 0.002843, 0.002756, 0.002669, 0.002584, 0.002502, 0.002424, 0.002351, 0.002284, 0.002225, 0.002171, 0.002125, 0.002086, 0.002053, 0.002026, 0.002004, 0.001987, 0.001973, 0.001962, 0.001955, 0.001949, 0.001945, 0.001942, 0.001940, 0.001938, 0.001936, 0.001935, 0.001934, 0.001933, 0.001932, 0.001932, 0.001931, 0.001931, 0.001930, 0.001930, 0.001929, 0.001929, 0.001928, 0.001928, 0.001927, 0.001926, 0.001926, 0.001926, 0.001926, 0.001926, 0.001925, 0.001925, 0.001925, 0.001926, 0.001926, 0.001926, 0.001926, 0.001926, 0.001926, 0.001926], }, SpectrumDataPoint { xystar: (0.0, 0.0), uv: (6.0, 3.9999999999999996), spectrum: [0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119, 0.003119], }, SpectrumDataPoint { xystar: (0.05419810812289436, 0.0), uv: (7.000000000000001, 3.9999999999999996), spectrum: [0.002415, 0.002415, 0.002415, 0.002415, 0.002414, 0.002413, 0.002413, 0.002413, 0.002413, 0.002413, 0.002413, 0.002414, 0.002413, 0.002411, 0.002410, 0.002408, 0.002410, 0.002411, 0.002414, 0.002420, 0.002428, 0.002438, 0.002451, 0.002468, 0.002487, 0.002508, 0.002531, 0.002558, 0.002587, 0.002620, 0.002657, 0.002696, 0.002740, 0.002790, 0.002847, 0.002910, 0.002979, 0.003053, 0.003133, 0.003217, 0.003303, 0.003392, 0.003480, 0.003569, 0.003656, 0.003739, 0.003818, 0.003891, 0.003958, 0.004020, 0.004073, 0.004119, 0.004157, 0.004189, 0.004213, 0.004232, 0.004247, 0.004259, 0.004267, 0.004273, 0.004277, 0.004279, 0.004281, 0.004282, 0.004282, 0.004283, 0.004283, 0.004283, 0.004284, 0.004284, 0.004284, 0.004284, 0.004285, 0.004284, 0.004284, 0.004284, 0.004283, 0.004283, 0.004282, 0.004282, 0.004281, 0.004280, 0.004280, 0.004279, 0.004279, 0.004279, 0.004278, 0.004278, 0.004278, 0.004278, 0.004277, 0.004277, 0.004277, 0.004277, 0.004277], }, SpectrumDataPoint { xystar: (0.10839621624578867, 0.0), uv: (8.0, 3.9999999999999996), spectrum: [0.001684, 0.001684, 0.001683, 0.001684, 0.001684, 0.001684, 0.001685, 0.001685, 0.001684, 0.001683, 0.001684, 0.001684, 0.001685, 0.001686, 0.001688, 0.001693, 0.001699, 0.001708, 0.001719, 0.001732, 0.001749, 0.001769, 0.001794, 0.001823, 0.001857, 0.001896, 0.001941, 0.001990, 0.002046, 0.002109, 0.002181, 0.002263, 0.002354, 0.002459, 0.002575, 0.002704, 0.002844, 0.002994, 0.003152, 0.003319, 0.003491, 0.003666, 0.003843, 0.004018, 0.004190, 0.004356, 0.004514, 0.004662, 0.004797, 0.004919, 0.005025, 0.005115, 0.005191, 0.005254, 0.005304, 0.005345, 0.005376, 0.005399, 0.005418, 0.005432, 0.005443, 0.005451, 0.005456, 0.005460, 0.005463, 0.005464, 0.005465, 0.005465, 0.005465, 0.005466, 0.005465, 0.005465, 0.005464, 0.005464, 0.005464, 0.005463, 0.005462, 0.005462, 0.005462, 0.005461, 0.005461, 0.005461, 0.005461, 0.005461, 0.005460, 0.005459, 0.005459, 0.005458, 0.005458, 0.005457, 0.005457, 0.005457, 0.005457, 0.005457, 0.005457], }, SpectrumDataPoint { xystar: (0.162594324368683, 0.0), uv: (9.0, 3.9999999999999996), spectrum: [0.000960, 0.000959, 0.000960, 0.000960, 0.000960, 0.000961, 0.000961, 0.000961, 0.000961, 0.000959, 0.000959, 0.000960, 0.000962, 0.000965, 0.000970, 0.000978, 0.000989, 0.001002, 0.001021, 0.001043, 0.001069, 0.001099, 0.001135, 0.001177, 0.001227, 0.001284, 0.001350, 0.001425, 0.001509, 0.001603, 0.001712, 0.001835, 0.001975, 0.002129, 0.002303, 0.002494, 0.002703, 0.002927, 0.003166, 0.003415, 0.003675, 0.003940, 0.004206, 0.004471, 0.004730, 0.004979, 0.005215, 0.005435, 0.005636, 0.005815, 0.005973, 0.006110, 0.006224, 0.006320, 0.006397, 0.006458, 0.006505, 0.006542, 0.006571, 0.006591, 0.006606, 0.006616, 0.006624, 0.006630, 0.006634, 0.006636, 0.006638, 0.006639, 0.006639, 0.006640, 0.006640, 0.006640, 0.006639, 0.006639, 0.006639, 0.006638, 0.006637, 0.006637, 0.006637, 0.006637, 0.006637, 0.006637, 0.006637, 0.006637, 0.006636, 0.006635, 0.006636, 0.006635, 0.006635, 0.006634, 0.006634, 0.006634, 0.006635, 0.006635, 0.006636], }, SpectrumDataPoint { xystar: (0.21679243249157734, 0.0), uv: (10.0, 3.9999999999999996), spectrum: [0.000240, 0.000240, 0.000241, 0.000241, 0.000242, 0.000244, 0.000246, 0.000248, 0.000249, 0.000250, 0.000249, 0.000251, 0.000254, 0.000256, 0.000259, 0.000265, 0.000278, 0.000296, 0.000317, 0.000345, 0.000380, 0.000422, 0.000472, 0.000531, 0.000599, 0.000676, 0.000763, 0.000862, 0.000973, 0.001099, 0.001241, 0.001404, 0.001589, 0.001798, 0.002031, 0.002287, 0.002566, 0.002866, 0.003184, 0.003517, 0.003862, 0.004212, 0.004566, 0.004918, 0.005263, 0.005595, 0.005911, 0.006205, 0.006475, 0.006717, 0.006929, 0.007112, 0.007265, 0.007391, 0.007493, 0.007574, 0.007635, 0.007681, 0.007714, 0.007737, 0.007753, 0.007763, 0.007771, 0.007777, 0.007782, 0.007786, 0.007791, 0.007795, 0.007796, 0.007798, 0.007800, 0.007801, 0.007800, 0.007799, 0.007797, 0.007796, 0.007796, 0.007797, 0.007797, 0.007798, 0.007798, 0.007798, 0.007798, 0.007798, 0.007799, 0.007800, 0.007800, 0.007801, 0.007802, 0.007802, 0.007802, 0.007803, 0.007803, 0.007803, 0.007804], }, SpectrumDataPoint { xystar: (-0.27099054061447164, 0.05599832200562286), uv: (0.9999999999999991, 5.0), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.001414, 0.010027, 0.021396, 0.031903, 0.038994, 0.041104, 0.037951, 0.030090, 0.018983, 0.007336, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.21679243249157729, 0.05599832200562286), uv: (2.0, 5.0), spectrum: [0.001605, 0.001606, 0.001606, 0.001607, 0.001608, 0.001609, 0.001613, 0.001620, 0.001628, 0.001642, 0.001666, 0.001712, 0.001799, 0.001953, 0.002210, 0.002600, 0.003134, 0.003811, 0.004615, 0.005517, 0.006485, 0.007476, 0.008442, 0.009335, 0.010115, 0.010748, 0.011205, 0.011458, 0.011481, 0.011258, 0.010760, 0.009968, 0.008898, 0.007585, 0.006092, 0.004523, 0.002996, 0.001636, 0.000583, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000002, 0.000000, 0.000000, 0.000001, 0.000000, 0.000002, 0.000002, 0.000002, 0.000003, 0.000002, 0.000004, 0.000003], }, SpectrumDataPoint { xystar: (-0.16259432436868296, 0.05599832200562286), uv: (3.0, 5.0), spectrum: [0.003983, 0.003984, 0.003984, 0.003985, 0.003986, 0.003987, 0.003987, 0.003988, 0.003989, 0.003994, 0.004001, 0.004010, 0.004027, 0.004058, 0.004111, 0.004191, 0.004301, 0.004443, 0.004613, 0.004807, 0.005022, 0.005251, 0.005489, 0.005724, 0.005948, 0.006154, 0.006332, 0.006478, 0.006582, 0.006637, 0.006630, 0.006552, 0.006395, 0.006157, 0.005836, 0.005438, 0.004971, 0.004446, 0.003877, 0.003279, 0.002672, 0.002074, 0.001508, 0.000999, 0.000572, 0.000249, 0.000052, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000002, 0.000002, 0.000002], }, SpectrumDataPoint { xystar: (-0.10839621624578864, 0.05599832200562286), uv: (4.0, 5.0), spectrum: [0.003745, 0.003746, 0.003746, 0.003746, 0.003746, 0.003746, 0.003747, 0.003747, 0.003748, 0.003750, 0.003754, 0.003759, 0.003769, 0.003784, 0.003811, 0.003850, 0.003904, 0.003974, 0.004058, 0.004154, 0.004263, 0.004380, 0.004504, 0.004630, 0.004754, 0.004872, 0.004981, 0.005077, 0.005157, 0.005216, 0.005250, 0.005252, 0.005219, 0.005147, 0.005033, 0.004880, 0.004685, 0.004454, 0.004186, 0.003890, 0.003568, 0.003227, 0.002874, 0.002515, 0.002159, 0.001814, 0.001486, 0.001184, 0.000912, 0.000674, 0.000473, 0.000311, 0.000185, 0.000095, 0.000037, 0.000007, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.05419810812289431, 0.05599832200562286), uv: (5.0, 5.0), spectrum: [0.003086, 0.003086, 0.003086, 0.003086, 0.003085, 0.003085, 0.003085, 0.003086, 0.003087, 0.003088, 0.003091, 0.003096, 0.003103, 0.003118, 0.003141, 0.003176, 0.003226, 0.003290, 0.003368, 0.003461, 0.003566, 0.003682, 0.003807, 0.003937, 0.004068, 0.004197, 0.004321, 0.004437, 0.004541, 0.004630, 0.004699, 0.004743, 0.004760, 0.004749, 0.004702, 0.004625, 0.004515, 0.004374, 0.004206, 0.004012, 0.003797, 0.003565, 0.003320, 0.003066, 0.002809, 0.002555, 0.002308, 0.002074, 0.001856, 0.001657, 0.001480, 0.001325, 0.001194, 0.001085, 0.000993, 0.000920, 0.000863, 0.000819, 0.000785, 0.000760, 0.000741, 0.000728, 0.000718, 0.000710, 0.000704, 0.000700, 0.000696, 0.000694, 0.000693, 0.000690, 0.000689, 0.000688, 0.000688, 0.000688, 0.000688, 0.000688, 0.000687, 0.000686, 0.000686, 0.000686, 0.000686, 0.000686, 0.000686, 0.000686, 0.000685, 0.000685, 0.000685, 0.000685, 0.000684, 0.000684, 0.000684, 0.000684, 0.000684, 0.000684, 0.000684], }, SpectrumDataPoint { xystar: (0.0, 0.05599832200562286), uv: (6.0, 5.0), spectrum: [0.002356, 0.002354, 0.002355, 0.002355, 0.002355, 0.002354, 0.002354, 0.002354, 0.002356, 0.002358, 0.002363, 0.002370, 0.002382, 0.002399, 0.002428, 0.002469, 0.002521, 0.002589, 0.002675, 0.002773, 0.002883, 0.003006, 0.003142, 0.003285, 0.003431, 0.003577, 0.003723, 0.003865, 0.003996, 0.004116, 0.004221, 0.004307, 0.004371, 0.004412, 0.004428, 0.004418, 0.004380, 0.004316, 0.004230, 0.004121, 0.003991, 0.003846, 0.003687, 0.003520, 0.003348, 0.003173, 0.003003, 0.002838, 0.002687, 0.002548, 0.002424, 0.002316, 0.002222, 0.002148, 0.002085, 0.002036, 0.001997, 0.001966, 0.001945, 0.001928, 0.001914, 0.001904, 0.001896, 0.001890, 0.001888, 0.001884, 0.001882, 0.001880, 0.001879, 0.001877, 0.001878, 0.001878, 0.001878, 0.001878, 0.001878, 0.001878, 0.001878, 0.001879, 0.001878, 0.001879, 0.001879, 0.001879, 0.001880, 0.001879, 0.001878, 0.001878, 0.001877, 0.001877, 0.001876, 0.001876, 0.001876, 0.001876, 0.001876, 0.001877, 0.001877], }, SpectrumDataPoint { xystar: (0.05419810812289436, 0.05599832200562286), uv: (7.000000000000001, 5.0), spectrum: [0.001660, 0.001659, 0.001659, 0.001659, 0.001659, 0.001659, 0.001659, 0.001658, 0.001658, 0.001659, 0.001660, 0.001665, 0.001672, 0.001686, 0.001712, 0.001751, 0.001806, 0.001880, 0.001969, 0.002075, 0.002196, 0.002334, 0.002483, 0.002642, 0.002808, 0.002975, 0.003142, 0.003305, 0.003463, 0.003613, 0.003753, 0.003879, 0.003988, 0.004081, 0.004155, 0.004209, 0.004242, 0.004255, 0.004247, 0.004219, 0.004175, 0.004117, 0.004045, 0.003965, 0.003878, 0.003788, 0.003698, 0.003610, 0.003527, 0.003448, 0.003379, 0.003316, 0.003262, 0.003217, 0.003180, 0.003150, 0.003127, 0.003110, 0.003098, 0.003088, 0.003081, 0.003076, 0.003071, 0.003067, 0.003064, 0.003061, 0.003059, 0.003057, 0.003056, 0.003054, 0.003054, 0.003053, 0.003052, 0.003052, 0.003051, 0.003050, 0.003051, 0.003050, 0.003051, 0.003051, 0.003051, 0.003053, 0.003054, 0.003054, 0.003054, 0.003054, 0.003054, 0.003053, 0.003054, 0.003053, 0.003054, 0.003054, 0.003054, 0.003054, 0.003054], }, SpectrumDataPoint { xystar: (0.10839621624578867, 0.05599832200562286), uv: (8.0, 5.0), spectrum: [0.000930, 0.000931, 0.000931, 0.000931, 0.000932, 0.000933, 0.000933, 0.000934, 0.000934, 0.000936, 0.000938, 0.000942, 0.000950, 0.000965, 0.000993, 0.001035, 0.001095, 0.001173, 0.001270, 0.001385, 0.001517, 0.001665, 0.001826, 0.001999, 0.002178, 0.002362, 0.002549, 0.002736, 0.002922, 0.003103, 0.003279, 0.003447, 0.003604, 0.003751, 0.003883, 0.004000, 0.004102, 0.004189, 0.004261, 0.004319, 0.004363, 0.004393, 0.004412, 0.004421, 0.004420, 0.004413, 0.004400, 0.004383, 0.004365, 0.004346, 0.004326, 0.004308, 0.004293, 0.004279, 0.004269, 0.004261, 0.004254, 0.004249, 0.004245, 0.004243, 0.004241, 0.004240, 0.004238, 0.004237, 0.004236, 0.004235, 0.004235, 0.004234, 0.004234, 0.004234, 0.004234, 0.004234, 0.004233, 0.004232, 0.004231, 0.004230, 0.004229, 0.004228, 0.004228, 0.004227, 0.004227, 0.004227, 0.004227, 0.004228, 0.004228, 0.004228, 0.004228, 0.004228, 0.004228, 0.004227, 0.004226, 0.004226, 0.004226, 0.004226, 0.004226], }, SpectrumDataPoint { xystar: (0.162594324368683, 0.05599832200562286), uv: (9.0, 5.0), spectrum: [0.000215, 0.000214, 0.000214, 0.000215, 0.000215, 0.000215, 0.000215, 0.000215, 0.000214, 0.000214, 0.000216, 0.000221, 0.000231, 0.000249, 0.000279, 0.000325, 0.000388, 0.000471, 0.000572, 0.000694, 0.000833, 0.000989, 0.001161, 0.001346, 0.001542, 0.001746, 0.001956, 0.002170, 0.002386, 0.002602, 0.002815, 0.003026, 0.003230, 0.003428, 0.003618, 0.003798, 0.003968, 0.004127, 0.004276, 0.004414, 0.004541, 0.004659, 0.004767, 0.004864, 0.004950, 0.005026, 0.005093, 0.005152, 0.005203, 0.005244, 0.005280, 0.005309, 0.005332, 0.005351, 0.005367, 0.005380, 0.005389, 0.005398, 0.005405, 0.005411, 0.005415, 0.005418, 0.005420, 0.005421, 0.005423, 0.005423, 0.005423, 0.005422, 0.005422, 0.005421, 0.005420, 0.005419, 0.005419, 0.005418, 0.005417, 0.005416, 0.005416, 0.005415, 0.005414, 0.005415, 0.005415, 0.005414, 0.005414, 0.005413, 0.005413, 0.005413, 0.005412, 0.005412, 0.005412, 0.005411, 0.005411, 0.005410, 0.005410, 0.005410, 0.005410], }, SpectrumDataPoint { xystar: (0.21679243249157734, 0.05599832200562286), uv: (10.0, 5.0), spectrum: [0.000002, 0.000002, 0.000002, 0.000002, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000002, 0.000097, 0.000268, 0.000494, 0.000763, 0.001062, 0.001380, 0.001714, 0.002056, 0.002400, 0.002740, 0.003071, 0.003389, 0.003693, 0.003981, 0.004252, 0.004504, 0.004737, 0.004950, 0.005142, 0.005316, 0.005471, 0.005609, 0.005729, 0.005834, 0.005924, 0.006000, 0.006065, 0.006118, 0.006161, 0.006197, 0.006225, 0.006247, 0.006264, 0.006276, 0.006286, 0.006294, 0.006299, 0.006303, 0.006305, 0.006306, 0.006307, 0.006307, 0.006308, 0.006308, 0.006308, 0.006308, 0.006308, 0.006308, 0.006308, 0.006308, 0.006308, 0.006307, 0.006307, 0.006306, 0.006306, 0.006306, 0.006306, 0.006306, 0.006306, 0.006305, 0.006305, 0.006304, 0.006304, 0.006304, 0.006304, 0.006303, 0.006303, 0.006303, 0.006304, 0.006304, 0.006304, 0.006304], }, SpectrumDataPoint { xystar: (-0.21679243249157729, 0.11199664401124566), uv: (2.0, 5.999999999999999), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000376, 0.001245, 0.002562, 0.004250, 0.006211, 0.008324, 0.010458, 0.012472, 0.014244, 0.015672, 0.016680, 0.017213, 0.017218, 0.016651, 0.015470, 0.013683, 0.011364, 0.008668, 0.005845, 0.003215, 0.001131, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000001, 0.000000, 0.000001, 0.000001, 0.000001, 0.000002, 0.000001], }, SpectrumDataPoint { xystar: (-0.16259432436868296, 0.11199664401124566), uv: (3.0, 5.999999999999999), spectrum: [0.002087, 0.002087, 0.002087, 0.002087, 0.002087, 0.002089, 0.002090, 0.002093, 0.002098, 0.002107, 0.002122, 0.002150, 0.002199, 0.002288, 0.002437, 0.002662, 0.002974, 0.003375, 0.003856, 0.004407, 0.005015, 0.005662, 0.006322, 0.006976, 0.007600, 0.008169, 0.008662, 0.009068, 0.009367, 0.009536, 0.009553, 0.009399, 0.009060, 0.008533, 0.007829, 0.006976, 0.006000, 0.004942, 0.003850, 0.002781, 0.001790, 0.000948, 0.000328, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000001, 0.000002, 0.000001, 0.000001, 0.000002, 0.000002, 0.000001, 0.000001, 0.000000, 0.000000, 0.000001], }, SpectrumDataPoint { xystar: (-0.10839621624578864, 0.11199664401124566), uv: (4.0, 5.999999999999999), spectrum: [0.002692, 0.002692, 0.002692, 0.002692, 0.002693, 0.002695, 0.002697, 0.002698, 0.002700, 0.002701, 0.002707, 0.002719, 0.002742, 0.002780, 0.002848, 0.002952, 0.003093, 0.003280, 0.003507, 0.003772, 0.004070, 0.004394, 0.004739, 0.005093, 0.005443, 0.005781, 0.006100, 0.006391, 0.006644, 0.006845, 0.006987, 0.007058, 0.007048, 0.006947, 0.006754, 0.006474, 0.006110, 0.005671, 0.005161, 0.004598, 0.004000, 0.003378, 0.002751, 0.002144, 0.001578, 0.001069, 0.000641, 0.000310, 0.000095, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000002, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000002, 0.000002, 0.000000, 0.000000, 0.000001, 0.000001, 0.000002, 0.000003], }, SpectrumDataPoint { xystar: (-0.05419810812289431, 0.11199664401124566), uv: (5.0, 5.999999999999999), spectrum: [0.002321, 0.002322, 0.002321, 0.002321, 0.002322, 0.002324, 0.002325, 0.002325, 0.002325, 0.002326, 0.002329, 0.002336, 0.002350, 0.002376, 0.002422, 0.002494, 0.002597, 0.002733, 0.002903, 0.003104, 0.003333, 0.003586, 0.003858, 0.004142, 0.004428, 0.004712, 0.004987, 0.005247, 0.005486, 0.005697, 0.005872, 0.006004, 0.006085, 0.006108, 0.006071, 0.005972, 0.005812, 0.005594, 0.005322, 0.004999, 0.004632, 0.004231, 0.003808, 0.003368, 0.002924, 0.002486, 0.002065, 0.001670, 0.001312, 0.000993, 0.000719, 0.000492, 0.000311, 0.000177, 0.000083, 0.000027, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000001, 0.000001, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (0.0, 0.11199664401124566), uv: (6.0, 5.999999999999999), spectrum: [0.001610, 0.001610, 0.001610, 0.001610, 0.001610, 0.001610, 0.001611, 0.001612, 0.001614, 0.001617, 0.001624, 0.001635, 0.001653, 0.001685, 0.001736, 0.001813, 0.001919, 0.002057, 0.002227, 0.002425, 0.002651, 0.002900, 0.003170, 0.003453, 0.003744, 0.004036, 0.004325, 0.004603, 0.004866, 0.005106, 0.005316, 0.005490, 0.005621, 0.005705, 0.005738, 0.005717, 0.005642, 0.005516, 0.005342, 0.005125, 0.004868, 0.004576, 0.004257, 0.003920, 0.003573, 0.003223, 0.002883, 0.002557, 0.002252, 0.001973, 0.001727, 0.001512, 0.001329, 0.001177, 0.001053, 0.000955, 0.000876, 0.000815, 0.000769, 0.000735, 0.000710, 0.000692, 0.000678, 0.000668, 0.000660, 0.000654, 0.000650, 0.000647, 0.000645, 0.000643, 0.000642, 0.000640, 0.000639, 0.000639, 0.000639, 0.000638, 0.000638, 0.000638, 0.000638, 0.000638, 0.000637, 0.000636, 0.000636, 0.000635, 0.000634, 0.000634, 0.000633, 0.000633, 0.000632, 0.000632, 0.000631, 0.000631, 0.000631, 0.000631, 0.000631], }, SpectrumDataPoint { xystar: (0.05419810812289436, 0.11199664401124566), uv: (7.000000000000001, 5.999999999999999), spectrum: [0.000878, 0.000877, 0.000876, 0.000875, 0.000874, 0.000873, 0.000873, 0.000874, 0.000877, 0.000883, 0.000890, 0.000902, 0.000922, 0.000955, 0.001008, 0.001087, 0.001198, 0.001342, 0.001521, 0.001730, 0.001971, 0.002237, 0.002523, 0.002826, 0.003136, 0.003449, 0.003759, 0.004059, 0.004346, 0.004614, 0.004859, 0.005073, 0.005249, 0.005383, 0.005470, 0.005510, 0.005503, 0.005449, 0.005351, 0.005213, 0.005038, 0.004833, 0.004603, 0.004356, 0.004098, 0.003837, 0.003578, 0.003328, 0.003093, 0.002877, 0.002686, 0.002518, 0.002375, 0.002257, 0.002159, 0.002081, 0.002019, 0.001972, 0.001934, 0.001905, 0.001883, 0.001865, 0.001852, 0.001843, 0.001836, 0.001831, 0.001828, 0.001825, 0.001823, 0.001822, 0.001821, 0.001820, 0.001819, 0.001819, 0.001820, 0.001821, 0.001822, 0.001824, 0.001824, 0.001825, 0.001825, 0.001824, 0.001824, 0.001823, 0.001823, 0.001824, 0.001825, 0.001826, 0.001827, 0.001827, 0.001828, 0.001828, 0.001828, 0.001828, 0.001828], }, SpectrumDataPoint { xystar: (0.10839621624578867, 0.11199664401124566), uv: (8.0, 5.999999999999999), spectrum: [0.000179, 0.000179, 0.000179, 0.000178, 0.000179, 0.000180, 0.000180, 0.000180, 0.000182, 0.000185, 0.000191, 0.000201, 0.000219, 0.000249, 0.000302, 0.000382, 0.000495, 0.000644, 0.000823, 0.001036, 0.001281, 0.001555, 0.001852, 0.002166, 0.002491, 0.002823, 0.003155, 0.003482, 0.003800, 0.004101, 0.004382, 0.004636, 0.004858, 0.005044, 0.005190, 0.005295, 0.005360, 0.005384, 0.005370, 0.005319, 0.005235, 0.005121, 0.004981, 0.004822, 0.004648, 0.004466, 0.004282, 0.004102, 0.003931, 0.003773, 0.003631, 0.003507, 0.003400, 0.003312, 0.003238, 0.003179, 0.003133, 0.003096, 0.003067, 0.003045, 0.003030, 0.003018, 0.003010, 0.003004, 0.003000, 0.002996, 0.002993, 0.002992, 0.002990, 0.002988, 0.002987, 0.002986, 0.002986, 0.002985, 0.002986, 0.002985, 0.002985, 0.002985, 0.002985, 0.002985, 0.002985, 0.002984, 0.002984, 0.002984, 0.002984, 0.002984, 0.002985, 0.002985, 0.002984, 0.002984, 0.002984, 0.002984, 0.002985, 0.002985, 0.002985], }, SpectrumDataPoint { xystar: (0.162594324368683, 0.11199664401124566), uv: (9.0, 5.999999999999999), spectrum: [0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000052, 0.000168, 0.000346, 0.000577, 0.000862, 0.001185, 0.001538, 0.001913, 0.002298, 0.002686, 0.003071, 0.003447, 0.003805, 0.004142, 0.004450, 0.004722, 0.004957, 0.005149, 0.005300, 0.005409, 0.005482, 0.005514, 0.005511, 0.005477, 0.005414, 0.005330, 0.005227, 0.005113, 0.004993, 0.004871, 0.004750, 0.004636, 0.004532, 0.004439, 0.004359, 0.004292, 0.004236, 0.004190, 0.004153, 0.004126, 0.004106, 0.004089, 0.004076, 0.004068, 0.004062, 0.004058, 0.004054, 0.004051, 0.004049, 0.004048, 0.004047, 0.004046, 0.004045, 0.004045, 0.004045, 0.004044, 0.004044, 0.004044, 0.004044, 0.004044, 0.004044, 0.004043, 0.004043, 0.004043, 0.004043, 0.004043, 0.004043, 0.004043, 0.004042, 0.004042, 0.004042, 0.004042, 0.004042, 0.004042, 0.004042, 0.004041, 0.004042], }, SpectrumDataPoint { xystar: (-0.21679243249157729, 0.16799496601686853), uv: (2.0, 6.999999999999999), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000219, 0.002207, 0.005534, 0.009696, 0.014123, 0.018315, 0.021870, 0.024468, 0.025906, 0.026029, 0.024730, 0.021964, 0.017858, 0.012795, 0.007451, 0.002788, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000002, 0.000001, 0.000002, 0.000001, 0.000001, 0.000000, 0.000000, 0.000001, 0.000002], }, SpectrumDataPoint { xystar: (-0.16259432436868296, 0.16799496601686853), uv: (3.0, 6.999999999999999), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000065, 0.000263, 0.000629, 0.001181, 0.001925, 0.002844, 0.003912, 0.005094, 0.006351, 0.007633, 0.008886, 0.010062, 0.011113, 0.012001, 0.012696, 0.013162, 0.013362, 0.013258, 0.012817, 0.012028, 0.010910, 0.009506, 0.007889, 0.006154, 0.004406, 0.002769, 0.001384, 0.000405, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.10839621624578864, 0.16799496601686853), uv: (4.0, 6.999999999999999), spectrum: [0.001212, 0.001213, 0.001213, 0.001214, 0.001213, 0.001214, 0.001215, 0.001218, 0.001224, 0.001233, 0.001248, 0.001274, 0.001321, 0.001403, 0.001541, 0.001744, 0.002034, 0.002407, 0.002856, 0.003381, 0.003960, 0.004592, 0.005255, 0.005929, 0.006594, 0.007229, 0.007815, 0.008337, 0.008783, 0.009129, 0.009355, 0.009441, 0.009366, 0.009127, 0.008722, 0.008159, 0.007458, 0.006641, 0.005729, 0.004761, 0.003770, 0.002808, 0.001916, 0.001135, 0.000525, 0.000130, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000000, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000002, 0.000001, 0.000001], }, SpectrumDataPoint { xystar: (-0.05419810812289431, 0.16799496601686853), uv: (5.0, 6.999999999999999), spectrum: [0.001278, 0.001277, 0.001277, 0.001277, 0.001278, 0.001279, 0.001279, 0.001281, 0.001285, 0.001291, 0.001300, 0.001318, 0.001350, 0.001405, 0.001496, 0.001634, 0.001826, 0.002076, 0.002382, 0.002740, 0.003144, 0.003588, 0.004061, 0.004552, 0.005049, 0.005540, 0.006011, 0.006454, 0.006857, 0.007207, 0.007493, 0.007699, 0.007813, 0.007825, 0.007730, 0.007530, 0.007226, 0.006826, 0.006341, 0.005782, 0.005164, 0.004504, 0.003821, 0.003139, 0.002477, 0.001857, 0.001301, 0.000825, 0.000449, 0.000184, 0.000033, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000002, 0.000002, 0.000001, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (0.0, 0.16799496601686853), uv: (6.0, 6.999999999999999), spectrum: [0.000820, 0.000820, 0.000820, 0.000820, 0.000820, 0.000820, 0.000820, 0.000822, 0.000826, 0.000831, 0.000841, 0.000855, 0.000881, 0.000928, 0.001006, 0.001124, 0.001288, 0.001501, 0.001763, 0.002070, 0.002421, 0.002809, 0.003228, 0.003666, 0.004114, 0.004563, 0.005004, 0.005429, 0.005826, 0.006190, 0.006507, 0.006767, 0.006960, 0.007078, 0.007115, 0.007067, 0.006938, 0.006729, 0.006447, 0.006096, 0.005686, 0.005227, 0.004730, 0.004210, 0.003678, 0.003149, 0.002636, 0.002152, 0.001709, 0.001313, 0.000971, 0.000685, 0.000456, 0.000279, 0.000152, 0.000067, 0.000018, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000002, 0.000002, 0.000002, 0.000002, 0.000003, 0.000002, 0.000003, 0.000003, 0.000002, 0.000002, 0.000002, 0.000002, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001], }, SpectrumDataPoint { xystar: (0.05419810812289436, 0.16799496601686853), uv: (7.000000000000001, 6.999999999999999), spectrum: [0.000159, 0.000160, 0.000161, 0.000163, 0.000164, 0.000165, 0.000168, 0.000170, 0.000172, 0.000174, 0.000180, 0.000192, 0.000215, 0.000256, 0.000328, 0.000441, 0.000600, 0.000810, 0.001070, 0.001376, 0.001727, 0.002117, 0.002539, 0.002984, 0.003443, 0.003904, 0.004359, 0.004799, 0.005218, 0.005607, 0.005957, 0.006256, 0.006497, 0.006672, 0.006775, 0.006805, 0.006761, 0.006646, 0.006463, 0.006218, 0.005918, 0.005569, 0.005182, 0.004766, 0.004333, 0.003894, 0.003460, 0.003042, 0.002652, 0.002295, 0.001977, 0.001701, 0.001467, 0.001274, 0.001118, 0.000994, 0.000899, 0.000826, 0.000771, 0.000732, 0.000705, 0.000685, 0.000672, 0.000664, 0.000659, 0.000656, 0.000654, 0.000652, 0.000650, 0.000647, 0.000644, 0.000643, 0.000642, 0.000640, 0.000637, 0.000634, 0.000633, 0.000632, 0.000629, 0.000627, 0.000626, 0.000624, 0.000622, 0.000620, 0.000617, 0.000616, 0.000614, 0.000612, 0.000609, 0.000608, 0.000607, 0.000605, 0.000605, 0.000603, 0.000603], }, SpectrumDataPoint { xystar: (0.10839621624578867, 0.16799496601686853), uv: (8.0, 6.999999999999999), spectrum: [0.000002, 0.000002, 0.000002, 0.000001, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000024, 0.000118, 0.000284, 0.000519, 0.000821, 0.001184, 0.001600, 0.002057, 0.002541, 0.003040, 0.003543, 0.004039, 0.004522, 0.004979, 0.005400, 0.005776, 0.006094, 0.006349, 0.006533, 0.006646, 0.006686, 0.006655, 0.006557, 0.006396, 0.006177, 0.005911, 0.005603, 0.005266, 0.004908, 0.004540, 0.004173, 0.003818, 0.003482, 0.003173, 0.002896, 0.002653, 0.002445, 0.002271, 0.002128, 0.002014, 0.001923, 0.001853, 0.001800, 0.001759, 0.001729, 0.001708, 0.001693, 0.001681, 0.001673, 0.001668, 0.001663, 0.001660, 0.001657, 0.001656, 0.001654, 0.001653, 0.001652, 0.001652, 0.001651, 0.001650, 0.001650, 0.001650, 0.001649, 0.001649, 0.001649, 0.001648, 0.001648, 0.001648, 0.001648, 0.001647, 0.001647, 0.001647, 0.001647, 0.001647, 0.001647, 0.001647, 0.001647, 0.001647, 0.001647], }, SpectrumDataPoint { xystar: (0.162594324368683, 0.16799496601686853), uv: (9.0, 6.999999999999999), spectrum: [0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000161, 0.000512, 0.001001, 0.001585, 0.002230, 0.002905, 0.003587, 0.004252, 0.004879, 0.005447, 0.005940, 0.006348, 0.006664, 0.006886, 0.007013, 0.007048, 0.006996, 0.006863, 0.006659, 0.006393, 0.006079, 0.005728, 0.005354, 0.004972, 0.004594, 0.004231, 0.003895, 0.003590, 0.003322, 0.003092, 0.002899, 0.002742, 0.002617, 0.002517, 0.002439, 0.002379, 0.002334, 0.002301, 0.002277, 0.002260, 0.002247, 0.002237, 0.002231, 0.002227, 0.002224, 0.002222, 0.002221, 0.002219, 0.002218, 0.002216, 0.002215, 0.002214, 0.002214, 0.002214, 0.002214, 0.002213, 0.002213, 0.002213, 0.002213, 0.002213, 0.002212, 0.002212, 0.002212, 0.002212, 0.002213, 0.002213, 0.002213, 0.002214, 0.002214, 0.002214, 0.002214, 0.002215], }, SpectrumDataPoint { xystar: (-0.21679243249157729, 0.22399328802249138), uv: (2.0, 7.999999999999999), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.001663, 0.008895, 0.018646, 0.028404, 0.036236, 0.040926, 0.041705, 0.038211, 0.030549, 0.019783, 0.008306, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.16259432436868296, 0.22399328802249138), uv: (3.0, 7.999999999999999), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000620, 0.001807, 0.003472, 0.005503, 0.007763, 0.010094, 0.012348, 0.014397, 0.016134, 0.017473, 0.018333, 0.018638, 0.018310, 0.017304, 0.015627, 0.013359, 0.010661, 0.007758, 0.004916, 0.002440, 0.000674, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.10839621624578864, 0.22399328802249138), uv: (4.0, 7.999999999999999), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000092, 0.000326, 0.000716, 0.001264, 0.001969, 0.002808, 0.003762, 0.004814, 0.005914, 0.007033, 0.008130, 0.009166, 0.010113, 0.010936, 0.011610, 0.012100, 0.012371, 0.012388, 0.012135, 0.011595, 0.010787, 0.009742, 0.008499, 0.007113, 0.005650, 0.004182, 0.002795, 0.001581, 0.000640, 0.000078, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000], }, SpectrumDataPoint { xystar: (-0.05419810812289431, 0.22399328802249138), uv: (5.0, 7.999999999999999), spectrum: [0.000011, 0.000011, 0.000011, 0.000011, 0.000012, 0.000013, 0.000015, 0.000018, 0.000023, 0.000032, 0.000048, 0.000076, 0.000125, 0.000213, 0.000359, 0.000581, 0.000890, 0.001291, 0.001779, 0.002348, 0.002989, 0.003689, 0.004435, 0.005202, 0.005971, 0.006722, 0.007437, 0.008098, 0.008686, 0.009184, 0.009569, 0.009819, 0.009916, 0.009845, 0.009605, 0.009201, 0.008645, 0.007949, 0.007137, 0.006235, 0.005273, 0.004285, 0.003309, 0.002387, 0.001561, 0.000872, 0.000360, 0.000059, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000], }, SpectrumDataPoint { xystar: (0.0, 0.22399328802249138), uv: (6.0, 7.999999999999999), spectrum: [0.000000, 0.000002, 0.000000, 0.000002, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000016, 0.000064, 0.000159, 0.000318, 0.000546, 0.000851, 0.001228, 0.001674, 0.002190, 0.002757, 0.003370, 0.004011, 0.004665, 0.005316, 0.005950, 0.006556, 0.007119, 0.007621, 0.008052, 0.008390, 0.008624, 0.008739, 0.008733, 0.008600, 0.008347, 0.007976, 0.007502, 0.006933, 0.006286, 0.005581, 0.004833, 0.004074, 0.003320, 0.002599, 0.001931, 0.001339, 0.000845, 0.000459, 0.000189, 0.000037, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000001, 0.000001, 0.000000, 0.000001, 0.000000, 0.000002, 0.000000, 0.000001, 0.000002, 0.000000, 0.000002, 0.000001, 0.000000, 0.000001, 0.000001, 0.000001, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (0.05419810812289436, 0.22399328802249138), uv: (7.000000000000001, 7.999999999999999), spectrum: [0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000079, 0.000245, 0.000501, 0.000843, 0.001264, 0.001760, 0.002317, 0.002918, 0.003549, 0.004191, 0.004831, 0.005454, 0.006048, 0.006598, 0.007090, 0.007510, 0.007841, 0.008073, 0.008200, 0.008218, 0.008127, 0.007934, 0.007642, 0.007260, 0.006800, 0.006273, 0.005692, 0.005078, 0.004445, 0.003811, 0.003193, 0.002606, 0.002067, 0.001585, 0.001166, 0.000818, 0.000538, 0.000324, 0.000170, 0.000071, 0.000016, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000002, 0.000001, 0.000001, 0.000001, 0.000001, 0.000002, 0.000001, 0.000001, 0.000002, 0.000001, 0.000002, 0.000002, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001], }, SpectrumDataPoint { xystar: (0.10839621624578867, 0.22399328802249138), uv: (8.0, 7.999999999999999), spectrum: [0.000002, 0.000002, 0.000002, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000129, 0.000421, 0.000855, 0.001404, 0.002036, 0.002724, 0.003441, 0.004166, 0.004880, 0.005562, 0.006197, 0.006764, 0.007247, 0.007631, 0.007909, 0.008076, 0.008130, 0.008073, 0.007912, 0.007653, 0.007303, 0.006878, 0.006388, 0.005847, 0.005274, 0.004686, 0.004101, 0.003532, 0.002996, 0.002503, 0.002059, 0.001672, 0.001342, 0.001066, 0.000841, 0.000660, 0.000518, 0.000409, 0.000325, 0.000263, 0.000217, 0.000184, 0.000161, 0.000145, 0.000133, 0.000124, 0.000118, 0.000113, 0.000110, 0.000108, 0.000106, 0.000104, 0.000104, 0.000103, 0.000102, 0.000102, 0.000102, 0.000102, 0.000102, 0.000102, 0.000101, 0.000100, 0.000099, 0.000099, 0.000099, 0.000099, 0.000099, 0.000098, 0.000099, 0.000098, 0.000098, 0.000098, 0.000098, 0.000098, 0.000098], }, SpectrumDataPoint { xystar: (-0.16259432436868296, 0.27999161002811424), uv: (3.0, 8.999999999999998), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000190, 0.002115, 0.005345, 0.009341, 0.013603, 0.017694, 0.021257, 0.024023, 0.025768, 0.026299, 0.025451, 0.023170, 0.019569, 0.014974, 0.009937, 0.005187, 0.001564, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000002, 0.000002, 0.000001, 0.000001, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000003, 0.000003, 0.000003], }, SpectrumDataPoint { xystar: (-0.10839621624578864, 0.27999161002811424), uv: (4.0, 8.999999999999998), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000403, 0.001222, 0.002407, 0.003898, 0.005611, 0.007442, 0.009292, 0.011069, 0.012696, 0.014107, 0.015237, 0.016023, 0.016395, 0.016298, 0.015698, 0.014601, 0.013057, 0.011152, 0.009001, 0.006737, 0.004517, 0.002524, 0.000959, 0.000038, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.05419810812289431, 0.27999161002811424), uv: (5.0, 8.999999999999998), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000040, 0.000290, 0.000749, 0.001404, 0.002233, 0.003211, 0.004304, 0.005468, 0.006658, 0.007830, 0.008948, 0.009977, 0.010885, 0.011635, 0.012188, 0.012510, 0.012570, 0.012351, 0.011858, 0.011105, 0.010118, 0.008939, 0.007617, 0.006206, 0.004771, 0.003386, 0.002132, 0.001092, 0.000355, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000], }, SpectrumDataPoint { xystar: (0.0, 0.27999161002811424), uv: (6.0, 8.999999999999998), spectrum: [0.000001, 0.000000, 0.000001, 0.000001, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000126, 0.000417, 0.000868, 0.001464, 0.002193, 0.003028, 0.003939, 0.004892, 0.005855, 0.006801, 0.007706, 0.008544, 0.009289, 0.009915, 0.010392, 0.010698, 0.010810, 0.010727, 0.010448, 0.009980, 0.009340, 0.008548, 0.007633, 0.006625, 0.005558, 0.004474, 0.003416, 0.002430, 0.001558, 0.000844, 0.000327, 0.000038, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000002, 0.000002, 0.000003, 0.000004, 0.000004, 0.000004, 0.000005, 0.000006, 0.000006, 0.000006, 0.000006, 0.000006, 0.000006, 0.000006], }, SpectrumDataPoint { xystar: (0.05419810812289436, 0.27999161002811424), uv: (7.000000000000001, 8.999999999999998), spectrum: [0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000125, 0.000447, 0.000950, 0.001610, 0.002391, 0.003254, 0.004165, 0.005093, 0.006008, 0.006887, 0.007704, 0.008433, 0.009048, 0.009525, 0.009844, 0.009996, 0.009980, 0.009795, 0.009450, 0.008957, 0.008332, 0.007596, 0.006772, 0.005887, 0.004970, 0.004056, 0.003174, 0.002356, 0.001629, 0.001019, 0.000543, 0.000213, 0.000033, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001], }, SpectrumDataPoint { xystar: (0.10839621624578867, 0.27999161002811424), uv: (8.0, 8.999999999999998), spectrum: [0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000139, 0.000650, 0.001426, 0.002377, 0.003433, 0.004542, 0.005650, 0.006710, 0.007679, 0.008516, 0.009187, 0.009675, 0.009969, 0.010064, 0.009969, 0.009693, 0.009245, 0.008648, 0.007923, 0.007105, 0.006218, 0.005292, 0.004367, 0.003476, 0.002649, 0.001905, 0.001272, 0.000762, 0.000384, 0.000134, 0.000010, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000000, 0.000001], }, SpectrumDataPoint { xystar: (-0.16259432436868296, 0.3359899320337371), uv: (3.0, 10.0), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.003207, 0.010021, 0.018352, 0.026499, 0.033223, 0.037591, 0.038902, 0.036687, 0.030958, 0.022441, 0.012719, 0.004211, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001], }, SpectrumDataPoint { xystar: (-0.10839621624578864, 0.3359899320337371), uv: (4.0, 10.0), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000188, 0.001488, 0.003670, 0.006439, 0.009503, 0.012602, 0.015516, 0.018066, 0.020093, 0.021449, 0.021991, 0.021616, 0.020282, 0.018049, 0.015084, 0.011641, 0.008032, 0.004621, 0.001825, 0.000112, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000], }, SpectrumDataPoint { xystar: (-0.05419810812289431, 0.3359899320337371), uv: (5.0, 10.0), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000127, 0.000760, 0.001840, 0.003281, 0.004972, 0.006801, 0.008664, 0.010471, 0.012144, 0.013612, 0.014804, 0.015645, 0.016072, 0.016035, 0.015515, 0.014533, 0.013141, 0.011410, 0.009435, 0.007329, 0.005223, 0.003267, 0.001624, 0.000473, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (0.0, 0.3359899320337371), uv: (6.0, 10.0), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000340, 0.001033, 0.002025, 0.003239, 0.004602, 0.006037, 0.007478, 0.008871, 0.010167, 0.011315, 0.012262, 0.012959, 0.013364, 0.013449, 0.013208, 0.012654, 0.011809, 0.010710, 0.009401, 0.007942, 0.006402, 0.004851, 0.003374, 0.002061, 0.001003, 0.000286, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000001, 0.000001], }, SpectrumDataPoint { xystar: (0.05419810812289436, 0.3359899320337371), uv: (7.000000000000001, 10.0), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000362, 0.001116, 0.002160, 0.003399, 0.004749, 0.006137, 0.007504, 0.008795, 0.009954, 0.010929, 0.011673, 0.012150, 0.012345, 0.012257, 0.011892, 0.011270, 0.010417, 0.009370, 0.008171, 0.006871, 0.005527, 0.004200, 0.002953, 0.001853, 0.000959, 0.000324, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (0.10839621624578867, 0.3359899320337371), uv: (8.0, 10.0), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000781, 0.002423, 0.004576, 0.006912, 0.009148, 0.011074, 0.012569, 0.013567, 0.014036, 0.013966, 0.013384, 0.012350, 0.010939, 0.009246, 0.007389, 0.005493, 0.003691, 0.002113, 0.000897, 0.000161, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000001, 0.000001, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.16259432436868296, 0.39198825403935994), uv: (3.0, 10.999999999999998), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000774, 0.020476, 0.044616, 0.062955, 0.068902, 0.059372, 0.036659, 0.010641, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.10839621624578864, 0.39198825403935994), uv: (4.0, 10.999999999999998), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.002385, 0.006713, 0.012032, 0.017536, 0.022603, 0.026720, 0.029451, 0.030420, 0.029389, 0.026340, 0.021555, 0.015636, 0.009438, 0.003969, 0.000388, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.05419810812289431, 0.39198825403935994), uv: (5.0, 10.999999999999998), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000985, 0.002940, 0.005535, 0.008470, 0.011484, 0.014360, 0.016916, 0.018981, 0.020393, 0.021014, 0.020758, 0.019611, 0.017654, 0.015041, 0.011974, 0.008702, 0.005513, 0.002746, 0.000771, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (0.0, 0.39198825403935994), uv: (6.0, 10.999999999999998), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000283, 0.001378, 0.003057, 0.005104, 0.007334, 0.009591, 0.011744, 0.013674, 0.015264, 0.016407, 0.017017, 0.017045, 0.016494, 0.015405, 0.013845, 0.011904, 0.009699, 0.007369, 0.005073, 0.002989, 0.001312, 0.000246, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001], }, SpectrumDataPoint { xystar: (0.05419810812289436, 0.39198825403935994), uv: (7.000000000000001, 10.999999999999998), spectrum: [0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000867, 0.002473, 0.004533, 0.006818, 0.009136, 0.011318, 0.013202, 0.014652, 0.015575, 0.015933, 0.015725, 0.014981, 0.013753, 0.012120, 0.010181, 0.008060, 0.005895, 0.003839, 0.002058, 0.000722, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002, 0.000002], }, SpectrumDataPoint { xystar: (-0.10839621624578864, 0.4479865760449827), uv: (4.0, 11.999999999999998), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.003473, 0.012890, 0.024467, 0.035265, 0.043015, 0.045962, 0.043133, 0.034720, 0.022428, 0.009488, 0.000315, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000001, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.05419810812289431, 0.4479865760449827), uv: (5.0, 11.999999999999998), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.001267, 0.004901, 0.009826, 0.015193, 0.020312, 0.024607, 0.027567, 0.028789, 0.028045, 0.025373, 0.021104, 0.015791, 0.010131, 0.004951, 0.001213, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (0.0, 0.4479865760449827), uv: (6.0, 11.999999999999998), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.001557, 0.004498, 0.008198, 0.012149, 0.015929, 0.019160, 0.021509, 0.022720, 0.022675, 0.021409, 0.019070, 0.015884, 0.012148, 0.008230, 0.004562, 0.001637, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (0.05419810812289436, 0.4479865760449827), uv: (7.000000000000001, 11.999999999999998), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.002626, 0.007837, 0.013870, 0.019250, 0.023008, 0.024733, 0.024362, 0.022048, 0.018145, 0.013206, 0.007968, 0.003318, 0.000286, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.05419810812289431, 0.5039848980506056), uv: (5.0, 13.0), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.007297, 0.018940, 0.031009, 0.040349, 0.044554, 0.042370, 0.034184, 0.022108, 0.009515, 0.000531, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (0.0, 0.5039848980506056), uv: (6.0, 13.0), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.003623, 0.012173, 0.022215, 0.030785, 0.035662, 0.035813, 0.031493, 0.023829, 0.014490, 0.005653, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.2709906464701516, -0.22399328802249138), uv: (0.9999980468749987, 0.0), spectrum: [0.023575, 0.023575, 0.023573, 0.023571, 0.023565, 0.023554, 0.023534, 0.023498, 0.023433, 0.023312, 0.023102, 0.022727, 0.022062, 0.020919, 0.019066, 0.016400, 0.013015, 0.009187, 0.005332, 0.002029, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000067, 0.000577, 0.001244, 0.001906, 0.002492, 0.002971, 0.003344, 0.003630, 0.003844, 0.004001, 0.004113, 0.004195, 0.004253, 0.004292, 0.004320, 0.004341, 0.004355, 0.004365, 0.004372, 0.004377, 0.004380, 0.004382, 0.004384, 0.004384, 0.004385, 0.004386, 0.004386, 0.004386, 0.004387, 0.004386, 0.004386, 0.004387, 0.004387, 0.004386, 0.004386, 0.004386, 0.004386, 0.004385, 0.004385, 0.004385], }, SpectrumDataPoint { xystar: (-0.2952315971735554, -0.16799496601686853), uv: (0.5527324218750005, 1.0), spectrum: [0.002755, 0.002755, 0.002756, 0.002757, 0.002759, 0.002763, 0.002770, 0.002784, 0.002809, 0.002852, 0.002925, 0.003051, 0.003269, 0.003636, 0.004222, 0.005062, 0.006140, 0.007403, 0.008747, 0.010037, 0.011103, 0.011761, 0.011858, 0.011318, 0.010148, 0.008438, 0.006340, 0.004093, 0.002002, 0.000458, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.29681943237246833, -0.11199664401124569), uv: (0.5234355468750005, 1.9999999999999998), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000644, 0.003307, 0.007382, 0.012106, 0.016609, 0.020063, 0.021845, 0.021638, 0.019453, 0.015563, 0.010596, 0.005450, 0.001342, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001], }, SpectrumDataPoint { xystar: (-0.29242642165547594, -0.05599832200562286), uv: (0.6044902343749996, 2.999999999999999), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.004514, 0.012223, 0.020649, 0.027526, 0.031288, 0.031190, 0.027174, 0.020110, 0.011515, 0.003670, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.28533409110033164, 0.0), uv: (0.7353496093749987, 3.9999999999999996), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.002017, 0.013202, 0.026877, 0.037910, 0.043095, 0.040934, 0.032211, 0.019299, 0.006340, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.2743780282278324, 0.05599832200562286), uv: (0.9374980468749996, 5.0), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.004900, 0.019327, 0.035095, 0.046403, 0.049670, 0.044252, 0.031492, 0.014845, 0.000659, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.27099054061447164, 0.07355259286543629), uv: (0.9999999999999991, 5.3134785156249995), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.001600, 0.016345, 0.034217, 0.047985, 0.053062, 0.048335, 0.035048, 0.016928, 0.001058, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.2617282744764928, 0.11199664401124566), uv: (1.1708964843750005, 5.999999999999999), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.009734, 0.028769, 0.046634, 0.056239, 0.054914, 0.042978, 0.023819, 0.004833, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001], }, SpectrumDataPoint { xystar: (-0.24738482984631277, 0.16799496601686853), uv: (1.4355449218750005, 6.999999999999999), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000277, 0.019442, 0.042522, 0.058819, 0.063242, 0.054463, 0.034926, 0.011788, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.23113598297743726, 0.22399328802249138), uv: (1.7353496093749996, 7.999999999999999), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.006807, 0.031020, 0.054932, 0.068424, 0.066501, 0.049084, 0.021876, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.21679243249157729, 0.2681795733517758), uv: (2.0, 8.789064453124999), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.021923, 0.049140, 0.068549, 0.072642, 0.059125, 0.031591, 0.003161, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.2129288060299024, 0.27999161002811424), uv: (2.0712871093750005, 8.999999999999998), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.017621, 0.045984, 0.068510, 0.075341, 0.062821, 0.034150, 0.003487, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.1925515876438533, 0.3359899320337371), uv: (2.447263671875, 10.0), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.001791, 0.031646, 0.063609, 0.081019, 0.075572, 0.047743, 0.011608, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.16899869885997837, 0.39198825403935994), uv: (2.881833984374999, 10.999999999999998), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.009856, 0.045649, 0.077287, 0.086458, 0.066290, 0.026512, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.16259432436868296, 0.40730040458449507), uv: (3.0, 11.273439453124999), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.003203, 0.040349, 0.076519, 0.089963, 0.071555, 0.030200, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.13999424255983572, 0.4479865760449827), uv: (3.4169902343749987, 11.999999999999998), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.020613, 0.058369, 0.084682, 0.082562, 0.050245, 0.007521, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.10839621624578864, 0.49922733748630377), uv: (4.0, 12.915041015625), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.023420, 0.071224, 0.096062, 0.075741, 0.024085, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.10331524946494723, 0.5039848980506056), uv: (4.093748046875, 13.0), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.021532, 0.065142, 0.090615, 0.077320, 0.032759, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.05419810812289431, 0.542483853801194), uv: (5.0, 13.687501953124999), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.014494, 0.055320, 0.081441, 0.071778, 0.033095, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (0.02397641735926503, 0.5039848980506056), uv: (6.442384765625, 13.0), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.005204, 0.020626, 0.035192, 0.042422, 0.040706, 0.031487, 0.018026, 0.005302, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (0.0, 0.5294686188037934), uv: (6.0, 13.455080078124999), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.010722, 0.035142, 0.053289, 0.055233, 0.041528, 0.019537, 0.000631, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (0.09077135139353519, 0.39198825403935994), uv: (7.674806640625, 10.999999999999998), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000604, 0.004426, 0.009453, 0.014267, 0.018085, 0.020476, 0.021221, 0.020297, 0.017879, 0.014312, 0.010088, 0.005816, 0.002196, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000001, 0.000001], }, SpectrumDataPoint { xystar: (0.06181982293335631, 0.4479865760449827), uv: (7.140626953125001, 11.999999999999998), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.001255, 0.008284, 0.016733, 0.023695, 0.027786, 0.028524, 0.025969, 0.020688, 0.013747, 0.006665, 0.001339, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000002], }, SpectrumDataPoint { xystar: (0.05419810812289436, 0.46050974766210345), uv: (7.000000000000001, 12.223634765624999), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.001972, 0.010154, 0.019515, 0.026750, 0.030390, 0.030062, 0.026050, 0.019258, 0.011200, 0.003936, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (0.16000096606613853, 0.22399328802249138), uv: (8.952150390625002, 7.999999999999999), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000381, 0.001385, 0.002759, 0.004279, 0.005778, 0.007149, 0.008320, 0.009241, 0.009883, 0.010233, 0.010293, 0.010076, 0.009613, 0.008941, 0.008102, 0.007142, 0.006113, 0.005065, 0.004044, 0.003092, 0.002242, 0.001519, 0.000937, 0.000500, 0.000205, 0.000043, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001], }, SpectrumDataPoint { xystar: (0.13845933520088657, 0.27999161002811424), uv: (8.554689453125, 8.999999999999998), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.001012, 0.002836, 0.005008, 0.007185, 0.009158, 0.010791, 0.011986, 0.012686, 0.012879, 0.012580, 0.011833, 0.010710, 0.009297, 0.007691, 0.006001, 0.004346, 0.002835, 0.001568, 0.000630, 0.000090, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (0.1157532918564318, 0.3359899320337371), uv: (8.135744140625, 10.0), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000632, 0.003065, 0.006282, 0.009544, 0.012415, 0.014622, 0.015992, 0.016441, 0.015975, 0.014677, 0.012696, 0.010232, 0.007530, 0.004863, 0.002525, 0.000809, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000001], }, SpectrumDataPoint { xystar: (0.10839621624578867, 0.35316140186421524), uv: (8.0, 10.306642578125), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000571, 0.003318, 0.006986, 0.010670, 0.013840, 0.016178, 0.017488, 0.017686, 0.016806, 0.014979, 0.012419, 0.009407, 0.006277, 0.003401, 0.001175, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (0.2017081039575845, 0.11199664401124566), uv: (9.721681640625, 5.999999999999999), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000121, 0.000506, 0.001076, 0.001767, 0.002517, 0.003271, 0.003991, 0.004651, 0.005234, 0.005724, 0.006115, 0.006403, 0.006593, 0.006686, 0.006690, 0.006615, 0.006473, 0.006278, 0.006045, 0.005787, 0.005517, 0.005248, 0.004990, 0.004753, 0.004542, 0.004357, 0.004200, 0.004070, 0.003966, 0.003883, 0.003819, 0.003770, 0.003733, 0.003706, 0.003687, 0.003671, 0.003661, 0.003654, 0.003648, 0.003643, 0.003640, 0.003637, 0.003636, 0.003635, 0.003634, 0.003633, 0.003633, 0.003632, 0.003632, 0.003632, 0.003632, 0.003632, 0.003631, 0.003631, 0.003631, 0.003630, 0.003630, 0.003630, 0.003630, 0.003630, 0.003629, 0.003629, 0.003629, 0.003629, 0.003629, 0.003629, 0.003629, 0.003629], }, SpectrumDataPoint { xystar: (0.18101331853175281, 0.16799496601686853), uv: (9.339845703125, 6.999999999999999), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000152, 0.000746, 0.001631, 0.002685, 0.003791, 0.004856, 0.005829, 0.006671, 0.007357, 0.007869, 0.008199, 0.008348, 0.008324, 0.008146, 0.007828, 0.007396, 0.006874, 0.006289, 0.005673, 0.005048, 0.004438, 0.003864, 0.003339, 0.002873, 0.002472, 0.002136, 0.001856, 0.001631, 0.001454, 0.001317, 0.001213, 0.001133, 0.001074, 0.001032, 0.001003, 0.000983, 0.000966, 0.000952, 0.000946, 0.000941, 0.000937, 0.000934, 0.000931, 0.000928, 0.000928, 0.000926, 0.000925, 0.000926, 0.000925, 0.000925, 0.000924, 0.000924, 0.000924, 0.000924, 0.000924, 0.000924, 0.000923, 0.000923, 0.000924, 0.000923, 0.000924, 0.000924, 0.000923, 0.000923, 0.000923, 0.000923, 0.000923], }, SpectrumDataPoint { xystar: (0.162594324368683, 0.21715766472751205), uv: (9.0, 7.877931640624999), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000430, 0.001426, 0.002756, 0.004211, 0.005644, 0.006955, 0.008079, 0.008968, 0.009595, 0.009950, 0.010031, 0.009854, 0.009445, 0.008834, 0.008061, 0.007171, 0.006210, 0.005222, 0.004251, 0.003335, 0.002507, 0.001787, 0.001190, 0.000723, 0.000380, 0.000153, 0.000032, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000000], }, SpectrumDataPoint { xystar: (0.26342196535533324, -0.05599832200562286), uv: (10.860353515625, 2.999999999999999), spectrum: [0.000196, 0.000197, 0.000197, 0.000197, 0.000198, 0.000199, 0.000200, 0.000200, 0.000199, 0.000200, 0.000200, 0.000199, 0.000196, 0.000190, 0.000180, 0.000166, 0.000147, 0.000123, 0.000096, 0.000068, 0.000041, 0.000019, 0.000004, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000030, 0.000115, 0.000261, 0.000472, 0.000748, 0.001087, 0.001486, 0.001941, 0.002446, 0.002993, 0.003573, 0.004177, 0.004793, 0.005409, 0.006013, 0.006594, 0.007142, 0.007649, 0.008107, 0.008513, 0.008864, 0.009162, 0.009408, 0.009609, 0.009769, 0.009895, 0.009990, 0.010062, 0.010116, 0.010155, 0.010183, 0.010203, 0.010218, 0.010228, 0.010236, 0.010241, 0.010244, 0.010247, 0.010248, 0.010249, 0.010249, 0.010248, 0.010249, 0.010249, 0.010249, 0.010249, 0.010248, 0.010249, 0.010248, 0.010248, 0.010248, 0.010248, 0.010248, 0.010248, 0.010248, 0.010248, 0.010248, 0.010248, 0.010248, 0.010248, 0.010248, 0.010248, 0.010247, 0.010247], }, SpectrumDataPoint { xystar: (0.24288596344939292, 0.0), uv: (10.481447265625, 3.9999999999999996), spectrum: [0.000000, 0.000001, 0.000000, 0.000001, 0.000002, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000018, 0.000056, 0.000116, 0.000195, 0.000285, 0.000387, 0.000513, 0.000652, 0.000808, 0.000985, 0.001176, 0.001396, 0.001642, 0.001910, 0.002206, 0.002525, 0.002863, 0.003221, 0.003595, 0.003977, 0.004370, 0.004765, 0.005153, 0.005536, 0.005903, 0.006250, 0.006572, 0.006867, 0.007133, 0.007368, 0.007568, 0.007736, 0.007876, 0.007990, 0.008079, 0.008151, 0.008206, 0.008245, 0.008276, 0.008297, 0.008312, 0.008325, 0.008333, 0.008338, 0.008341, 0.008346, 0.008346, 0.008346, 0.008348, 0.008348, 0.008349, 0.008350, 0.008350, 0.008350, 0.008350, 0.008349, 0.008348, 0.008348, 0.008348, 0.008349, 0.008349, 0.008349, 0.008348, 0.008347, 0.008348, 0.008349, 0.008349, 0.008348, 0.008348, 0.008347, 0.008348, 0.008348, 0.008348, 0.008348], }, SpectrumDataPoint { xystar: (0.22224410586352497, 0.05599832200562286), uv: (10.100587890625, 5.0), spectrum: [0.000005, 0.000005, 0.000004, 0.000003, 0.000002, 0.000002, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000036, 0.000207, 0.000479, 0.000820, 0.001210, 0.001632, 0.002069, 0.002507, 0.002934, 0.003344, 0.003730, 0.004088, 0.004414, 0.004707, 0.004968, 0.005196, 0.005392, 0.005556, 0.005693, 0.005805, 0.005896, 0.005967, 0.006021, 0.006062, 0.006092, 0.006113, 0.006128, 0.006139, 0.006146, 0.006150, 0.006151, 0.006152, 0.006154, 0.006155, 0.006156, 0.006156, 0.006156, 0.006156, 0.006155, 0.006155, 0.006154, 0.006153, 0.006152, 0.006153, 0.006152, 0.006152, 0.006151, 0.006151, 0.006151, 0.006151, 0.006151, 0.006151, 0.006151, 0.006151, 0.006151, 0.006150, 0.006149, 0.006149, 0.006148, 0.006149, 0.006149, 0.006149, 0.006149, 0.006149, 0.006149, 0.006149, 0.006148, 0.006148, 0.006147, 0.006148], }, SpectrumDataPoint { xystar: (0.21679243249157734, 0.07092767152142272), uv: (10.0, 5.266603515625), spectrum: [0.000002, 0.000002, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000117, 0.000384, 0.000759, 0.001208, 0.001704, 0.002222, 0.002739, 0.003237, 0.003705, 0.004134, 0.004520, 0.004858, 0.005147, 0.005385, 0.005575, 0.005719, 0.005821, 0.005886, 0.005919, 0.005925, 0.005910, 0.005882, 0.005844, 0.005801, 0.005756, 0.005714, 0.005676, 0.005642, 0.005613, 0.005589, 0.005570, 0.005554, 0.005542, 0.005533, 0.005526, 0.005520, 0.005517, 0.005514, 0.005512, 0.005510, 0.005509, 0.005508, 0.005507, 0.005507, 0.005506, 0.005506, 0.005505, 0.005505, 0.005504, 0.005504, 0.005503, 0.005503, 0.005503, 0.005503, 0.005503, 0.005502, 0.005502, 0.005502, 0.005502, 0.005502, 0.005501, 0.005501, 0.005501, 0.005501, 0.005501, 0.005501, 0.005501, 0.005501, 0.005501], }, SpectrumDataPoint { xystar: (0.3045998248471417, -0.16799496601686853), uv: (11.620119140625, 1.0), spectrum: [0.000432, 0.000432, 0.000432, 0.000433, 0.000433, 0.000433, 0.000434, 0.000433, 0.000431, 0.000427, 0.000420, 0.000407, 0.000384, 0.000346, 0.000289, 0.000212, 0.000126, 0.000047, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000137, 0.001087, 0.002640, 0.004596, 0.006770, 0.009006, 0.011176, 0.013187, 0.014983, 0.016531, 0.017831, 0.018895, 0.019746, 0.020411, 0.020918, 0.021298, 0.021578, 0.021782, 0.021929, 0.022035, 0.022112, 0.022165, 0.022203, 0.022229, 0.022248, 0.022260, 0.022269, 0.022275, 0.022279, 0.022282, 0.022283, 0.022285, 0.022286, 0.022286, 0.022287, 0.022288, 0.022288, 0.022288, 0.022288, 0.022288, 0.022288, 0.022288, 0.022288, 0.022288, 0.022288, 0.022288, 0.022288, 0.022287, 0.022287, 0.022287, 0.022288], }, SpectrumDataPoint { xystar: (0.28401089510123756, -0.11199664401124569), uv: (11.240236328125002, 1.9999999999999998), spectrum: [0.000269, 0.000269, 0.000270, 0.000270, 0.000271, 0.000272, 0.000274, 0.000275, 0.000277, 0.000277, 0.000276, 0.000271, 0.000263, 0.000249, 0.000228, 0.000199, 0.000162, 0.000120, 0.000074, 0.000035, 0.000008, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000106, 0.000420, 0.000922, 0.001589, 0.002395, 0.003309, 0.004298, 0.005328, 0.006368, 0.007384, 0.008349, 0.009242, 0.010046, 0.010753, 0.011357, 0.011864, 0.012279, 0.012613, 0.012874, 0.013077, 0.013230, 0.013342, 0.013425, 0.013485, 0.013529, 0.013560, 0.013582, 0.013598, 0.013609, 0.013617, 0.013623, 0.013626, 0.013629, 0.013630, 0.013630, 0.013630, 0.013630, 0.013631, 0.013630, 0.013631, 0.013631, 0.013631, 0.013631, 0.013631, 0.013632, 0.013631, 0.013632, 0.013631, 0.013631, 0.013631, 0.013631, 0.013631, 0.013631, 0.013631, 0.013631, 0.013630, 0.013630], }, SpectrumDataPoint { xystar: (0.27099054061447164, -0.07661478235667343), uv: (11.0, 2.6318378906249995), spectrum: [0.000217, 0.000218, 0.000219, 0.000219, 0.000219, 0.000220, 0.000222, 0.000223, 0.000223, 0.000223, 0.000223, 0.000221, 0.000217, 0.000209, 0.000197, 0.000179, 0.000156, 0.000127, 0.000095, 0.000064, 0.000036, 0.000012, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000061, 0.000223, 0.000482, 0.000835, 0.001277, 0.001797, 0.002389, 0.003040, 0.003734, 0.004457, 0.005193, 0.005926, 0.006639, 0.007316, 0.007945, 0.008519, 0.009029, 0.009471, 0.009848, 0.010160, 0.010415, 0.010620, 0.010781, 0.010903, 0.010995, 0.011063, 0.011113, 0.011149, 0.011175, 0.011194, 0.011208, 0.011217, 0.011224, 0.011228, 0.011231, 0.011233, 0.011234, 0.011235, 0.011235, 0.011235, 0.011235, 0.011235, 0.011235, 0.011235, 0.011236, 0.011236, 0.011236, 0.011236, 0.011236, 0.011235, 0.011235, 0.011235, 0.011235, 0.011235, 0.011235, 0.011235, 0.011234, 0.011235, 0.011234, 0.011235, 0.011235], }, SpectrumDataPoint { xystar: (-0.27705083121816254, -0.19599412701967994), uv: (0.8881826171874998, 0.5000000000000004), spectrum: [0.010364, 0.010365, 0.010364, 0.010364, 0.010364, 0.010363, 0.010362, 0.010360, 0.010355, 0.010347, 0.010332, 0.010304, 0.010254, 0.010166, 0.010018, 0.009790, 0.009470, 0.009054, 0.008545, 0.007947, 0.007269, 0.006521, 0.005719, 0.004886, 0.004046, 0.003221, 0.002435, 0.001712, 0.001075, 0.000554, 0.000183, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000010, 0.000072, 0.000164, 0.000270, 0.000377, 0.000477, 0.000566, 0.000644, 0.000708, 0.000758, 0.000798, 0.000828, 0.000851, 0.000867, 0.000879, 0.000887, 0.000893, 0.000895, 0.000898, 0.000900, 0.000902, 0.000903, 0.000904, 0.000904, 0.000904, 0.000904, 0.000904, 0.000904, 0.000903, 0.000903, 0.000903, 0.000903, 0.000904, 0.000904, 0.000903, 0.000903, 0.000903, 0.000903, 0.000902, 0.000903, 0.000903, 0.000903, 0.000903, 0.000902, 0.000903, 0.000903, 0.000903], }, SpectrumDataPoint { xystar: (0.29294238870336275, -0.19599412701967994), uv: (11.405029785156252, 0.5000000000000004), spectrum: [0.001850, 0.001850, 0.001849, 0.001849, 0.001849, 0.001847, 0.001843, 0.001837, 0.001826, 0.001806, 0.001773, 0.001712, 0.001606, 0.001429, 0.001158, 0.000804, 0.000422, 0.000109, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000753, 0.003311, 0.006908, 0.010950, 0.015008, 0.018799, 0.022162, 0.025033, 0.027397, 0.029286, 0.030753, 0.031867, 0.032696, 0.033302, 0.033742, 0.034060, 0.034288, 0.034449, 0.034562, 0.034642, 0.034699, 0.034740, 0.034768, 0.034788, 0.034801, 0.034811, 0.034817, 0.034822, 0.034825, 0.034827, 0.034829, 0.034830, 0.034830, 0.034831, 0.034831, 0.034831, 0.034831, 0.034831, 0.034831, 0.034831, 0.034831, 0.034831, 0.034831, 0.034831, 0.034831, 0.034831, 0.034831], }, SpectrumDataPoint { xystar: (-0.2835080276937417, -0.13999580501405712), uv: (0.7690419921874998, 1.5), spectrum: [0.006873, 0.006873, 0.006873, 0.006873, 0.006874, 0.006875, 0.006877, 0.006880, 0.006882, 0.006887, 0.006895, 0.006915, 0.006949, 0.007001, 0.007082, 0.007199, 0.007349, 0.007518, 0.007697, 0.007860, 0.007985, 0.008034, 0.007989, 0.007829, 0.007538, 0.007119, 0.006569, 0.005900, 0.005122, 0.004258, 0.003321, 0.002359, 0.001441, 0.000665, 0.000138, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000002, 0.000001, 0.000002, 0.000001, 0.000000, 0.000003, 0.000001, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (0.28264795029433065, -0.13999580501405712), uv: (11.2150888671875, 1.5), spectrum: [0.000658, 0.000658, 0.000658, 0.000658, 0.000658, 0.000658, 0.000658, 0.000657, 0.000656, 0.000653, 0.000647, 0.000640, 0.000625, 0.000597, 0.000554, 0.000493, 0.000413, 0.000320, 0.000219, 0.000123, 0.000046, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000219, 0.000791, 0.001656, 0.002753, 0.004015, 0.005375, 0.006768, 0.008138, 0.009438, 0.010631, 0.011696, 0.012617, 0.013393, 0.014035, 0.014554, 0.014963, 0.015280, 0.015522, 0.015702, 0.015834, 0.015929, 0.015997, 0.016047, 0.016082, 0.016107, 0.016124, 0.016136, 0.016145, 0.016151, 0.016155, 0.016157, 0.016159, 0.016160, 0.016160, 0.016161, 0.016161, 0.016162, 0.016162, 0.016162, 0.016162, 0.016162, 0.016162, 0.016161, 0.016162, 0.016162, 0.016162, 0.016162, 0.016162, 0.016161, 0.016161, 0.016161, 0.016161, 0.016161, 0.016161], }, SpectrumDataPoint { xystar: (-0.2828067338142219, -0.08399748300843428), uv: (0.7819814453124998, 2.4999999999999996), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000034, 0.000173, 0.000488, 0.001070, 0.001979, 0.003225, 0.004773, 0.006546, 0.008433, 0.010292, 0.011964, 0.013297, 0.014166, 0.014492, 0.014239, 0.013408, 0.012042, 0.010207, 0.007997, 0.005554, 0.003125, 0.001099, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (0.24779758231348623, -0.08252094287808212), uv: (10.572070703125, 2.5263675781249995), spectrum: [0.000739, 0.000739, 0.000739, 0.000738, 0.000737, 0.000737, 0.000737, 0.000737, 0.000738, 0.000737, 0.000734, 0.000730, 0.000723, 0.000710, 0.000688, 0.000653, 0.000606, 0.000548, 0.000479, 0.000406, 0.000327, 0.000245, 0.000167, 0.000099, 0.000044, 0.000009, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000058, 0.000211, 0.000457, 0.000794, 0.001214, 0.001712, 0.002279, 0.002901, 0.003568, 0.004263, 0.004972, 0.005677, 0.006364, 0.007017, 0.007625, 0.008177, 0.008666, 0.009090, 0.009450, 0.009750, 0.009994, 0.010190, 0.010345, 0.010463, 0.010553, 0.010620, 0.010669, 0.010704, 0.010729, 0.010746, 0.010757, 0.010765, 0.010770, 0.010773, 0.010776, 0.010778, 0.010779, 0.010780, 0.010781, 0.010781, 0.010782, 0.010782, 0.010782, 0.010781, 0.010781, 0.010780, 0.010780, 0.010779, 0.010778, 0.010778, 0.010777, 0.010777, 0.010777, 0.010777, 0.010777, 0.010776, 0.010776, 0.010774, 0.010773, 0.010773, 0.010772], }, SpectrumDataPoint { xystar: (0.27533065877672697, -0.10020269012638827), uv: (11.080078776041669, 2.210612630208333), spectrum: [0.000362, 0.000362, 0.000362, 0.000362, 0.000361, 0.000360, 0.000359, 0.000358, 0.000356, 0.000353, 0.000350, 0.000346, 0.000340, 0.000328, 0.000310, 0.000284, 0.000248, 0.000206, 0.000158, 0.000107, 0.000060, 0.000022, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000155, 0.000462, 0.000913, 0.001492, 0.002182, 0.002961, 0.003806, 0.004695, 0.005600, 0.006498, 0.007365, 0.008180, 0.008928, 0.009598, 0.010184, 0.010683, 0.011100, 0.011440, 0.011712, 0.011927, 0.012093, 0.012219, 0.012312, 0.012380, 0.012430, 0.012465, 0.012490, 0.012508, 0.012520, 0.012529, 0.012534, 0.012538, 0.012541, 0.012543, 0.012544, 0.012545, 0.012546, 0.012547, 0.012547, 0.012547, 0.012547, 0.012547, 0.012547, 0.012547, 0.012547, 0.012546, 0.012546, 0.012546, 0.012545, 0.012545, 0.012545, 0.012545, 0.012545, 0.012545, 0.012544, 0.012544, 0.012544, 0.012544], }, SpectrumDataPoint { xystar: (-0.2799353984961877, -0.02799916100281143), uv: (0.8349599609374989, 3.4999999999999996), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.001439, 0.004357, 0.008262, 0.012568, 0.016679, 0.020035, 0.022223, 0.023000, 0.022257, 0.020070, 0.016618, 0.012202, 0.007311, 0.002797, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000], }, SpectrumDataPoint { xystar: (0.23497319844697023, -0.02799916100281143), uv: (10.3354501953125, 3.4999999999999996), spectrum: [0.000379, 0.000379, 0.000379, 0.000379, 0.000380, 0.000381, 0.000381, 0.000381, 0.000380, 0.000379, 0.000377, 0.000375, 0.000373, 0.000369, 0.000362, 0.000352, 0.000339, 0.000324, 0.000306, 0.000287, 0.000268, 0.000252, 0.000240, 0.000232, 0.000231, 0.000242, 0.000266, 0.000304, 0.000361, 0.000437, 0.000539, 0.000673, 0.000841, 0.001047, 0.001290, 0.001573, 0.001892, 0.002247, 0.002633, 0.003046, 0.003482, 0.003934, 0.004396, 0.004861, 0.005321, 0.005770, 0.006198, 0.006600, 0.006971, 0.007304, 0.007598, 0.007852, 0.008065, 0.008241, 0.008383, 0.008496, 0.008583, 0.008649, 0.008698, 0.008735, 0.008762, 0.008781, 0.008795, 0.008806, 0.008814, 0.008820, 0.008823, 0.008826, 0.008828, 0.008829, 0.008829, 0.008829, 0.008829, 0.008828, 0.008828, 0.008827, 0.008827, 0.008827, 0.008827, 0.008827, 0.008827, 0.008826, 0.008826, 0.008826, 0.008826, 0.008826, 0.008826, 0.008827, 0.008827, 0.008828, 0.008828, 0.008829, 0.008829, 0.008830, 0.008830], }, SpectrumDataPoint { xystar: (-0.27542330013927685, 0.02799916100281143), uv: (0.9182119140624989, 4.5), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000232, 0.005811, 0.014294, 0.023164, 0.030417, 0.034696, 0.035224, 0.031977, 0.025412, 0.016514, 0.007021, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (0.22467873357401813, 0.02799916100281143), uv: (10.1455087890625, 4.5), spectrum: [0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000005, 0.000042, 0.000109, 0.000206, 0.000331, 0.000476, 0.000641, 0.000822, 0.001012, 0.001217, 0.001434, 0.001657, 0.001892, 0.002139, 0.002397, 0.002664, 0.002939, 0.003220, 0.003507, 0.003797, 0.004088, 0.004380, 0.004667, 0.004949, 0.005224, 0.005485, 0.005731, 0.005964, 0.006177, 0.006367, 0.006538, 0.006687, 0.006813, 0.006919, 0.007007, 0.007076, 0.007131, 0.007174, 0.007208, 0.007230, 0.007247, 0.007262, 0.007272, 0.007278, 0.007282, 0.007286, 0.007288, 0.007289, 0.007290, 0.007291, 0.007291, 0.007292, 0.007291, 0.007291, 0.007292, 0.007292, 0.007292, 0.007293, 0.007293, 0.007295, 0.007296, 0.007296, 0.007296, 0.007295, 0.007295, 0.007295, 0.007296, 0.007296, 0.007295, 0.007295, 0.007294, 0.007294, 0.007294, 0.007293, 0.007293, 0.007294], }, SpectrumDataPoint { xystar: (-0.2721197031522586, 0.06184974562556067), uv: (0.9791660156249993, 5.104492838541667), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.005774, 0.019422, 0.033963, 0.044428, 0.047728, 0.043253, 0.032054, 0.016954, 0.003193, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.24745884413771813, 0.08190850497983466), uv: (1.4341792968749996, 5.462695703124999), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000077, 0.001801, 0.004814, 0.008664, 0.012863, 0.016896, 0.020328, 0.022837, 0.024195, 0.024286, 0.023057, 0.020511, 0.016733, 0.012028, 0.007001, 0.002584, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (0.19209632353562106, 0.08138352071103196), uv: (9.544336328125002, 5.453320703125), spectrum: [0.000004, 0.000004, 0.000002, 0.000002, 0.000002, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000009, 0.000083, 0.000221, 0.000415, 0.000658, 0.000937, 0.001241, 0.001564, 0.001897, 0.002233, 0.002573, 0.002906, 0.003230, 0.003540, 0.003830, 0.004099, 0.004342, 0.004559, 0.004750, 0.004914, 0.005052, 0.005166, 0.005257, 0.005327, 0.005378, 0.005412, 0.005431, 0.005439, 0.005438, 0.005432, 0.005423, 0.005410, 0.005398, 0.005385, 0.005375, 0.005366, 0.005360, 0.005357, 0.005354, 0.005354, 0.005354, 0.005352, 0.005354, 0.005354, 0.005355, 0.005355, 0.005354, 0.005353, 0.005350, 0.005349, 0.005348, 0.005346, 0.005346, 0.005345, 0.005344, 0.005344, 0.005344, 0.005343, 0.005343, 0.005343, 0.005344, 0.005344, 0.005344, 0.005344, 0.005343, 0.005342, 0.005342, 0.005341, 0.005339, 0.005339, 0.005338, 0.005337, 0.005336, 0.005337, 0.005337, 0.005337], }, SpectrumDataPoint { xystar: (0.21860965694889323, 0.060974771844222814), uv: (10.033529296875003, 5.088867838541667), spectrum: [0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000013, 0.000157, 0.000402, 0.000712, 0.001070, 0.001464, 0.001876, 0.002296, 0.002711, 0.003113, 0.003495, 0.003852, 0.004182, 0.004483, 0.004753, 0.004991, 0.005199, 0.005376, 0.005526, 0.005649, 0.005748, 0.005827, 0.005888, 0.005935, 0.005970, 0.005995, 0.006012, 0.006024, 0.006032, 0.006037, 0.006040, 0.006042, 0.006043, 0.006043, 0.006043, 0.006043, 0.006043, 0.006043, 0.006042, 0.006041, 0.006041, 0.006040, 0.006040, 0.006040, 0.006040, 0.006039, 0.006039, 0.006039, 0.006039, 0.006038, 0.006038, 0.006037, 0.006037, 0.006037, 0.006036, 0.006036, 0.006036, 0.006036, 0.006035, 0.006035, 0.006035, 0.006035, 0.006035, 0.006034, 0.006034, 0.006034, 0.006034, 0.006034, 0.006034, 0.006034, 0.006034], }, SpectrumDataPoint { xystar: (-0.23567449232649001, 0.1399958050140571), uv: (1.6516103515625007, 6.499999999999998), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000797, 0.004422, 0.009859, 0.015976, 0.021790, 0.026531, 0.029634, 0.030784, 0.029806, 0.026660, 0.021483, 0.014816, 0.007770, 0.002072, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (0.17697751780667584, 0.1399958050140571), uv: (9.2653818359375, 6.499999999999998), spectrum: [0.000002, 0.000002, 0.000002, 0.000002, 0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000111, 0.000395, 0.000807, 0.001310, 0.001869, 0.002460, 0.003063, 0.003657, 0.004226, 0.004748, 0.005211, 0.005607, 0.005930, 0.006175, 0.006343, 0.006436, 0.006456, 0.006411, 0.006305, 0.006148, 0.005947, 0.005715, 0.005463, 0.005200, 0.004937, 0.004684, 0.004446, 0.004229, 0.004038, 0.003872, 0.003733, 0.003618, 0.003524, 0.003450, 0.003392, 0.003346, 0.003312, 0.003287, 0.003268, 0.003255, 0.003245, 0.003238, 0.003233, 0.003231, 0.003229, 0.003227, 0.003227, 0.003225, 0.003225, 0.003224, 0.003223, 0.003222, 0.003222, 0.003222, 0.003222, 0.003221, 0.003221, 0.003221, 0.003221, 0.003221, 0.003221, 0.003221, 0.003221, 0.003221, 0.003221, 0.003221, 0.003222, 0.003222, 0.003222, 0.003223, 0.003223, 0.003223], }, SpectrumDataPoint { xystar: (-0.22802641945172614, 0.19599412701967997), uv: (1.7927236328124998, 7.499999999999999), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.002041, 0.010218, 0.020912, 0.031206, 0.038913, 0.042767, 0.042066, 0.036661, 0.027065, 0.015069, 0.004227, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (0.14039640945901638, 0.2002268345612464), uv: (8.590430078125001, 7.5755863281249995), spectrum: [0.000001, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000143, 0.000491, 0.000993, 0.001601, 0.002283, 0.003006, 0.003742, 0.004473, 0.005169, 0.005807, 0.006372, 0.006849, 0.007223, 0.007492, 0.007649, 0.007696, 0.007640, 0.007490, 0.007245, 0.006919, 0.006527, 0.006082, 0.005603, 0.005104, 0.004605, 0.004110, 0.003639, 0.003207, 0.002818, 0.002479, 0.002188, 0.001941, 0.001740, 0.001579, 0.001451, 0.001355, 0.001281, 0.001226, 0.001184, 0.001156, 0.001135, 0.001119, 0.001108, 0.001101, 0.001096, 0.001089, 0.001086, 0.001083, 0.001081, 0.001081, 0.001080, 0.001078, 0.001079, 0.001077, 0.001076, 0.001077, 0.001076, 0.001076, 0.001075, 0.001075, 0.001075, 0.001073, 0.001072, 0.001073, 0.001073, 0.001072, 0.001073, 0.001071, 0.001071, 0.001071, 0.001071, 0.001071, 0.001071], }, SpectrumDataPoint { xystar: (0.16873398908970627, 0.1843825322537497), uv: (9.113281901041667, 7.292643880208333), spectrum: [0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000206, 0.000724, 0.001454, 0.002319, 0.003255, 0.004201, 0.005103, 0.005918, 0.006620, 0.007190, 0.007619, 0.007901, 0.008036, 0.008031, 0.007895, 0.007640, 0.007282, 0.006841, 0.006337, 0.005793, 0.005230, 0.004669, 0.004128, 0.003622, 0.003163, 0.002758, 0.002410, 0.002118, 0.001878, 0.001684, 0.001531, 0.001412, 0.001321, 0.001252, 0.001202, 0.001165, 0.001138, 0.001119, 0.001105, 0.001094, 0.001087, 0.001084, 0.001081, 0.001078, 0.001076, 0.001075, 0.001073, 0.001072, 0.001071, 0.001071, 0.001070, 0.001070, 0.001070, 0.001070, 0.001069, 0.001069, 0.001069, 0.001069, 0.001069, 0.001069, 0.001069, 0.001069, 0.001070, 0.001070, 0.001070, 0.001070, 0.001069, 0.001070, 0.001069], }, SpectrumDataPoint { xystar: (-0.22157361598686395, 0.23872204979891953), uv: (1.9117832031249993, 8.263021484374999), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.012982, 0.030813, 0.046441, 0.055671, 0.056186, 0.047435, 0.030933, 0.011783, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.19434046395008459, 0.2552298738905974), uv: (2.414257421875, 8.557812890625), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.003610, 0.009609, 0.016594, 0.023384, 0.029045, 0.032940, 0.034601, 0.033707, 0.030102, 0.024014, 0.016229, 0.008185, 0.001926, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (0.12881318343965062, 0.2519924490253028), uv: (8.3767099609375, 8.5), spectrum: [0.000001, 0.000001, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000223, 0.000771, 0.001548, 0.002474, 0.003482, 0.004523, 0.005548, 0.006511, 0.007370, 0.008094, 0.008660, 0.009056, 0.009280, 0.009329, 0.009211, 0.008936, 0.008519, 0.007977, 0.007332, 0.006610, 0.005837, 0.005042, 0.004250, 0.003487, 0.002776, 0.002133, 0.001574, 0.001103, 0.000725, 0.000436, 0.000227, 0.000092, 0.000019, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000002, 0.000001, 0.000001, 0.000002, 0.000002, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001], }, SpectrumDataPoint { xystar: (-0.1826672606027804, 0.30799077103092565), uv: (2.6296376953125, 9.499999999999998), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.002962, 0.011153, 0.021421, 0.031233, 0.038841, 0.042985, 0.042828, 0.037973, 0.028876, 0.017218, 0.006112, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (0.11775126488722393, 0.30799077103092565), uv: (8.1726083984375, 9.499999999999998), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000475, 0.001595, 0.003128, 0.004882, 0.006678, 0.008357, 0.009801, 0.010941, 0.011735, 0.012161, 0.012216, 0.011913, 0.011282, 0.010366, 0.009216, 0.007898, 0.006484, 0.005049, 0.003670, 0.002426, 0.001386, 0.000607, 0.000135, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000001, 0.000001, 0.000000, 0.000001, 0.000001, 0.000000, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001, 0.000001], }, SpectrumDataPoint { xystar: (-0.17168473381029942, 0.36398909303654853), uv: (2.8322744140624994, 10.5), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.009996, 0.028024, 0.045931, 0.057966, 0.060467, 0.051817, 0.033707, 0.012510, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (0.08319200002618024, 0.3618235548020819), uv: (7.534961328125, 10.461328515625), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000791, 0.002248, 0.004114, 0.006181, 0.008270, 0.010216, 0.011870, 0.013125, 0.013927, 0.014253, 0.014106, 0.013510, 0.012508, 0.011164, 0.009558, 0.007783, 0.005948, 0.004167, 0.002563, 0.001259, 0.000370, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (0.1108485747826697, 0.34171375531056314), uv: (8.045248046875, 10.102214192708333), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000363, 0.002276, 0.005030, 0.008008, 0.010757, 0.013016, 0.014633, 0.015515, 0.015626, 0.014996, 0.013707, 0.011880, 0.009672, 0.007274, 0.004894, 0.002759, 0.001090, 0.000105, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000], }, SpectrumDataPoint { xystar: (-0.16472911586578143, 0.39709230422107167), uv: (2.9606113281249997, 11.091146484374999), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.013110, 0.043196, 0.069841, 0.079880, 0.067008, 0.034892, 0.001225, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001], }, SpectrumDataPoint { xystar: (-0.13639506475775579, 0.41745001295063605), uv: (3.4833980468749997, 11.454687890624998), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.005873, 0.018390, 0.032485, 0.044330, 0.051106, 0.050858, 0.043004, 0.028959, 0.012614, 0.000245, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (0.06524684764317006, 0.4199874150421713), uv: (7.203858398437501, 11.499999999999998), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.001350, 0.004395, 0.008317, 0.012364, 0.015896, 0.018472, 0.019892, 0.020103, 0.019145, 0.017144, 0.014311, 0.010938, 0.007382, 0.004059, 0.001434, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000001, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000000, 0.000001, 0.000002, 0.000000, 0.000001], }, SpectrumDataPoint { xystar: (-0.11892889168380434, 0.4650668298587564), uv: (3.8056634114583328, 12.305013671874999), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.002157, 0.021387, 0.044337, 0.061319, 0.065705, 0.054910, 0.032189, 0.008070, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.08570077964046263, 0.480634057135496), uv: (4.418749609375, 12.583008203124997), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.005126, 0.016493, 0.029500, 0.040588, 0.046974, 0.046832, 0.039777, 0.027361, 0.013107, 0.002029, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001], }, SpectrumDataPoint { xystar: (0.02647452672101075, 0.47289053917065604), uv: (6.488476953125001, 12.444726953124999), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.003506, 0.009139, 0.015455, 0.021171, 0.025254, 0.027108, 0.026619, 0.024002, 0.019671, 0.014239, 0.008506, 0.003444, 0.000186, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (0.056738679726381684, 0.45216096658402294), uv: (7.046875651041668, 12.074544921874997), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.004328, 0.011605, 0.018940, 0.024427, 0.027217, 0.027102, 0.024261, 0.019237, 0.012936, 0.006584, 0.001689, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.07057048857024528, 0.5168178833008018), uv: (4.697916015625, 13.229167317708333), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000195, 0.017901, 0.040081, 0.056786, 0.061148, 0.050847, 0.029751, 0.007922, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (-0.027099054061447154, 0.5199805671765496), uv: (5.5, 13.285645507812498), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.005899, 0.019086, 0.033309, 0.043457, 0.046146, 0.040597, 0.028764, 0.014526, 0.002900, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000], }, SpectrumDataPoint { xystar: (0.00799213911975501, 0.5124794716350015), uv: (6.147461588541667, 13.151693359374999), spectrum: [0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.003433, 0.016112, 0.030274, 0.039955, 0.042108, 0.036770, 0.026016, 0.013147, 0.002607, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000001, 0.000000, 0.000000, 0.000002, 0.000000], } ]; // Color matching functions. pub(crate) const CMF_WAVELENGTH: [f32; 95] = [ 360.0, 365.0, 370.0, 375.0, 380.0, 385.0, 390.0, 395.0, 400.0, 405.0, 410.0, 415.0, 420.0, 425.0, 430.0, 435.0, 440.0, 445.0, 450.0, 455.0, 460.0, 465.0, 470.0, 475.0, 480.0, 485.0, 490.0, 495.0, 500.0, 505.0, 510.0, 515.0, 520.0, 525.0, 530.0, 535.0, 540.0, 545.0, 550.0, 555.0, 560.0, 565.0, 570.0, 575.0, 580.0, 585.0, 590.0, 595.0, 600.0, 605.0, 610.0, 615.0, 620.0, 625.0, 630.0, 635.0, 640.0, 645.0, 650.0, 655.0, 660.0, 665.0, 670.0, 675.0, 680.0, 685.0, 690.0, 695.0, 700.0, 705.0, 710.0, 715.0, 720.0, 725.0, 730.0, 735.0, 740.0, 745.0, 750.0, 755.0, 760.0, 765.0, 770.0, 775.0, 780.0, 785.0, 790.0, 795.0, 800.0, 805.0, 810.0, 815.0, 820.0, 825.0, 830.0 ]; pub(crate) const CMF_X: [f32; 95] = [ 0.0001299, 0.0002321, 0.0004149, 0.0007416, 0.001368, 0.002236, 0.004243, 0.00765, 0.01431, 0.02319, 0.04351, 0.07763, 0.13438, 0.21477, 0.2839, 0.3285, 0.34828, 0.34806, 0.3362, 0.3187, 0.2908, 0.2511, 0.19536, 0.1421, 0.09564, 0.05795001, 0.03201, 0.0147, 0.0049, 0.0024, 0.0093, 0.0291, 0.06327, 0.1096, 0.1655, 0.2257499, 0.2904, 0.3597, 0.4334499, 0.5120501, 0.5945, 0.6784, 0.7621, 0.8425, 0.9163, 0.9786, 1.0263, 1.0567, 1.0622, 1.0456, 1.0026, 0.9384, 0.8544499, 0.7514, 0.6424, 0.5419, 0.4479, 0.3608, 0.2835, 0.2187, 0.1649, 0.1212, 0.0874, 0.0636, 0.04677, 0.0329, 0.0227, 0.01584, 0.01135916, 0.008110916, 0.005790346, 0.004109457, 0.002899327, 0.00204919, 0.001439971, 0.0009999493, 0.0006900786, 0.0004760213, 0.0003323011, 0.0002348261, 0.0001661505, 0.000117413, 8.307527e-05, 5.870652e-05, 4.150994e-05, 2.935326e-05, 2.067383e-05, 1.455977e-05, 1.025398e-05, 7.221456e-06, 5.085868e-06, 3.581652e-06, 2.522525e-06, 1.776509e-06, 1.251141e-06 ]; pub(crate) const CMF_Y: [f32; 95] = [ 3.917e-06, 6.965e-06, 1.239e-05, 2.202e-05, 3.9e-05, 6.4e-05, 0.00012, 0.000217, 0.000396, 0.00064, 0.00121, 0.00218, 0.004, 0.0073, 0.0116, 0.01684, 0.023, 0.0298, 0.038, 0.048, 0.06, 0.0739, 0.09098, 0.1126, 0.13902, 0.1693, 0.20802, 0.2586, 0.323, 0.4073, 0.503, 0.6082, 0.71, 0.7932, 0.862, 0.9148501, 0.954, 0.9803, 0.9949501, 1.0, 0.995, 0.9786, 0.952, 0.9154, 0.87, 0.8163, 0.757, 0.6949, 0.631, 0.5668, 0.503, 0.4412, 0.381, 0.321, 0.265, 0.217, 0.175, 0.1382, 0.107, 0.0816, 0.061, 0.04458, 0.032, 0.0232, 0.017, 0.01192, 0.00821, 0.005723, 0.004102, 0.002929, 0.002091, 0.001484, 0.001047, 0.00074, 0.00052, 0.0003611, 0.0002492, 0.0001719, 0.00012, 8.48e-05, 6e-05, 4.24e-05, 3e-05, 2.12e-05, 1.499e-05, 1.06e-05, 7.4657e-06, 5.2578e-06, 3.7029e-06, 2.6078e-06, 1.8366e-06, 1.2934e-06, 9.1093e-07, 6.4153e-07, 4.5181e-07 ]; pub(crate) const CMF_Z: [f32; 95] = [ 0.0006061, 0.001086, 0.001946, 0.003486, 0.006450001, 0.01054999, 0.02005001, 0.03621, 0.06785001, 0.1102, 0.2074, 0.3713, 0.6456, 1.0390501, 1.3856, 1.62296, 1.74706, 1.7826, 1.77211, 1.7441, 1.6692, 1.5281, 1.28764, 1.0419, 0.8129501, 0.6162, 0.46518, 0.3533, 0.272, 0.2123, 0.1582, 0.1117, 0.07824999, 0.05725001, 0.04216, 0.02984, 0.0203, 0.0134, 0.008749999, 0.005749999, 0.0039, 0.002749999, 0.0021, 0.0018, 0.001650001, 0.0014, 0.0011, 0.001, 0.0008, 0.0006, 0.00034, 0.00024, 0.00019, 0.0001, 4.999999e-05, 3e-05, 2e-05, 1e-05, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 ]; ================================================ FILE: sub_crates/spectral_upsampling/src/meng/xyz_5nm_360_830.csv ================================================ 360,0.000129900000,0.000003917000,0.000606100000 365,0.000232100000,0.000006965000,0.001086000000 370,0.000414900000,0.000012390000,0.001946000000 375,0.000741600000,0.000022020000,0.003486000000 380,0.001368000000,0.000039000000,0.006450001000 385,0.002236000000,0.000064000000,0.010549990000 390,0.004243000000,0.000120000000,0.020050010000 395,0.007650000000,0.000217000000,0.036210000000 400,0.014310000000,0.000396000000,0.067850010000 405,0.023190000000,0.000640000000,0.110200000000 410,0.043510000000,0.001210000000,0.207400000000 415,0.077630000000,0.002180000000,0.371300000000 420,0.134380000000,0.004000000000,0.645600000000 425,0.214770000000,0.007300000000,1.039050100000 430,0.283900000000,0.011600000000,1.385600000000 435,0.328500000000,0.016840000000,1.622960000000 440,0.348280000000,0.023000000000,1.747060000000 445,0.348060000000,0.029800000000,1.782600000000 450,0.336200000000,0.038000000000,1.772110000000 455,0.318700000000,0.048000000000,1.744100000000 460,0.290800000000,0.060000000000,1.669200000000 465,0.251100000000,0.073900000000,1.528100000000 470,0.195360000000,0.090980000000,1.287640000000 475,0.142100000000,0.112600000000,1.041900000000 480,0.095640000000,0.139020000000,0.812950100000 485,0.057950010000,0.169300000000,0.616200000000 490,0.032010000000,0.208020000000,0.465180000000 495,0.014700000000,0.258600000000,0.353300000000 500,0.004900000000,0.323000000000,0.272000000000 505,0.002400000000,0.407300000000,0.212300000000 510,0.009300000000,0.503000000000,0.158200000000 515,0.029100000000,0.608200000000,0.111700000000 520,0.063270000000,0.710000000000,0.078249990000 525,0.109600000000,0.793200000000,0.057250010000 530,0.165500000000,0.862000000000,0.042160000000 535,0.225749900000,0.914850100000,0.029840000000 540,0.290400000000,0.954000000000,0.020300000000 545,0.359700000000,0.980300000000,0.013400000000 550,0.433449900000,0.994950100000,0.008749999000 555,0.512050100000,1.000000000000,0.005749999000 560,0.594500000000,0.995000000000,0.003900000000 565,0.678400000000,0.978600000000,0.002749999000 570,0.762100000000,0.952000000000,0.002100000000 575,0.842500000000,0.915400000000,0.001800000000 580,0.916300000000,0.870000000000,0.001650001000 585,0.978600000000,0.816300000000,0.001400000000 590,1.026300000000,0.757000000000,0.001100000000 595,1.056700000000,0.694900000000,0.001000000000 600,1.062200000000,0.631000000000,0.000800000000 605,1.045600000000,0.566800000000,0.000600000000 610,1.002600000000,0.503000000000,0.000340000000 615,0.938400000000,0.441200000000,0.000240000000 620,0.854449900000,0.381000000000,0.000190000000 625,0.751400000000,0.321000000000,0.000100000000 630,0.642400000000,0.265000000000,0.000049999990 635,0.541900000000,0.217000000000,0.000030000000 640,0.447900000000,0.175000000000,0.000020000000 645,0.360800000000,0.138200000000,0.000010000000 650,0.283500000000,0.107000000000,0.000000000000 655,0.218700000000,0.081600000000,0.000000000000 660,0.164900000000,0.061000000000,0.000000000000 665,0.121200000000,0.044580000000,0.000000000000 670,0.087400000000,0.032000000000,0.000000000000 675,0.063600000000,0.023200000000,0.000000000000 680,0.046770000000,0.017000000000,0.000000000000 685,0.032900000000,0.011920000000,0.000000000000 690,0.022700000000,0.008210000000,0.000000000000 695,0.015840000000,0.005723000000,0.000000000000 700,0.011359160000,0.004102000000,0.000000000000 705,0.008110916000,0.002929000000,0.000000000000 710,0.005790346000,0.002091000000,0.000000000000 715,0.004109457000,0.001484000000,0.000000000000 720,0.002899327000,0.001047000000,0.000000000000 725,0.002049190000,0.000740000000,0.000000000000 730,0.001439971000,0.000520000000,0.000000000000 735,0.000999949300,0.000361100000,0.000000000000 740,0.000690078600,0.000249200000,0.000000000000 745,0.000476021300,0.000171900000,0.000000000000 750,0.000332301100,0.000120000000,0.000000000000 755,0.000234826100,0.000084800000,0.000000000000 760,0.000166150500,0.000060000000,0.000000000000 765,0.000117413000,0.000042400000,0.000000000000 770,0.000083075270,0.000030000000,0.000000000000 775,0.000058706520,0.000021200000,0.000000000000 780,0.000041509940,0.000014990000,0.000000000000 785,0.000029353260,0.000010600000,0.000000000000 790,0.000020673830,0.000007465700,0.000000000000 795,0.000014559770,0.000005257800,0.000000000000 800,0.000010253980,0.000003702900,0.000000000000 805,0.000007221456,0.000002607800,0.000000000000 810,0.000005085868,0.000001836600,0.000000000000 815,0.000003581652,0.000001293400,0.000000000000 820,0.000002522525,0.000000910930,0.000000000000 825,0.000001776509,0.000000641530,0.000000000000 830,0.000001251141,0.000000451810,0.000000000000 ================================================ FILE: sub_crates/spectral_upsampling/src/meng/xyz_5nm_380_780.csv ================================================ 380,0.001368,0.000039,0.006450 385,0.002236,0.000064,0.010550 390,0.004243,0.000120,0.020050 395,0.007650,0.000217,0.036210 400,0.014310,0.000396,0.067850 405,0.023190,0.000640,0.110200 410,0.043510,0.001210,0.207400 415,0.077630,0.002180,0.371300 420,0.134380,0.004000,0.645600 425,0.214770,0.007300,1.039050 430,0.283900,0.011600,1.385600 435,0.328500,0.016840,1.622960 440,0.348280,0.023000,1.747060 445,0.348060,0.029800,1.782600 450,0.336200,0.038000,1.772110 455,0.318700,0.048000,1.744100 460,0.290800,0.060000,1.669200 465,0.251100,0.073900,1.528100 470,0.195360,0.090980,1.287640 475,0.142100,0.112600,1.041900 480,0.095640,0.139020,0.812950 485,0.057950,0.169300,0.616200 490,0.032010,0.208020,0.465180 495,0.014700,0.258600,0.353300 500,0.004900,0.323000,0.272000 505,0.002400,0.407300,0.212300 510,0.009300,0.503000,0.158200 515,0.029100,0.608200,0.111700 520,0.063270,0.710000,0.078250 525,0.109600,0.793200,0.057250 530,0.165500,0.862000,0.042160 535,0.225750,0.914850,0.029840 540,0.290400,0.954000,0.020300 545,0.359700,0.980300,0.013400 550,0.433450,0.994950,0.008750 555,0.512050,1.000000,0.005750 560,0.594500,0.995000,0.003900 565,0.678400,0.978600,0.002750 570,0.762100,0.952000,0.002100 575,0.842500,0.915400,0.001800 580,0.916300,0.870000,0.001650 585,0.978600,0.816300,0.001400 590,1.026300,0.757000,0.001100 595,1.056700,0.694900,0.001000 600,1.062200,0.631000,0.000800 605,1.045600,0.566800,0.000600 610,1.002600,0.503000,0.000340 615,0.938400,0.441200,0.000240 620,0.854450,0.381000,0.000190 625,0.751400,0.321000,0.000100 630,0.642400,0.265000,0.000050 635,0.541900,0.217000,0.000030 640,0.447900,0.175000,0.000020 645,0.360800,0.138200,0.000010 650,0.283500,0.107000,0.000000 655,0.218700,0.081600,0.000000 660,0.164900,0.061000,0.000000 665,0.121200,0.044580,0.000000 670,0.087400,0.032000,0.000000 675,0.063600,0.023200,0.000000 680,0.046770,0.017000,0.000000 685,0.032900,0.011920,0.000000 690,0.022700,0.008210,0.000000 695,0.015840,0.005723,0.000000 700,0.011359,0.004102,0.000000 705,0.008111,0.002929,0.000000 710,0.005790,0.002091,0.000000 715,0.004109,0.001484,0.000000 720,0.002899,0.001047,0.000000 725,0.002049,0.000740,0.000000 730,0.001440,0.000520,0.000000 735,0.001000,0.000361,0.000000 740,0.000690,0.000249,0.000000 745,0.000476,0.000172,0.000000 750,0.000332,0.000120,0.000000 755,0.000235,0.000085,0.000000 760,0.000166,0.000060,0.000000 765,0.000117,0.000042,0.000000 770,0.000083,0.000030,0.000000 775,0.000059,0.000021,0.000000 780,0.000042,0.000015,0.000000 ================================================ FILE: sub_crates/spectral_upsampling/src/meng.rs ================================================ // Since this is basicallt translated from C, silence a bunch of // clippy warnings that stem from the C code. #![allow(clippy::needless_return)] #![allow(clippy::useless_let_if_seq)] #![allow(clippy::cognitive_complexity)] use std::f32; use glam::Vec4; mod meng_spectra_tables; pub use self::meng_spectra_tables::{ EQUAL_ENERGY_REFLECTANCE, SPECTRUM_SAMPLE_MAX, SPECTRUM_SAMPLE_MIN, }; use self::meng_spectra_tables::{ SPECTRUM_DATA_POINTS, // CMF_X, // CMF_Y, // CMF_Z, // SPECTRUM_MAT_UV_TO_XY, SPECTRUM_GRID, SPECTRUM_GRID_HEIGHT, SPECTRUM_GRID_WIDTH, // SPECTRUM_MAT_XY_TO_XYSTAR, // SPECTRUM_MAT_XYSTAR_TO_XY, SPECTRUM_MAT_XY_TO_UV, // SPECTRUM_BIN_SIZE, SPECTRUM_NUM_SAMPLES, }; /// Evaluate the spectrum for xyz at the given wavelength. #[inline] pub fn spectrum_xyz_to_p(lambda: f32, xyz: (f32, f32, f32)) -> f32 { assert!(lambda >= SPECTRUM_SAMPLE_MIN); assert!(lambda <= SPECTRUM_SAMPLE_MAX); let inv_norm = xyz.0 + xyz.1 + xyz.2; let norm = { let norm = 1.0 / inv_norm; if norm < f32::MAX { norm } else { return 0.0; } }; let xyy = (xyz.0 * norm, xyz.1 * norm, xyz.1); // Rotate to align with grid let uv = spectrum_xy_to_uv((xyy.0, xyy.1)); if uv.0 < 0.0 || uv.0 >= SPECTRUM_GRID_WIDTH as f32 || uv.1 < 0.0 || uv.1 >= SPECTRUM_GRID_HEIGHT as f32 { return 0.0; } let uvi = (uv.0 as i32, uv.1 as i32); debug_assert!(uvi.0 < SPECTRUM_GRID_WIDTH); debug_assert!(uvi.1 < SPECTRUM_GRID_HEIGHT); let cell_idx: i32 = uvi.0 + SPECTRUM_GRID_WIDTH * uvi.1; debug_assert!(cell_idx >= 0); let cell = &SPECTRUM_GRID[cell_idx as usize]; let inside: bool = cell.inside; let idx = &cell.idx; let num: i32 = cell.num_points; // If the cell has no points, nothing we can do, so return 0.0 if num == 0 { return 0.0; } // Normalize lambda to spectrum table index range. let sb: f32 = (lambda - SPECTRUM_SAMPLE_MIN) / (SPECTRUM_SAMPLE_MAX - SPECTRUM_SAMPLE_MIN) * (SPECTRUM_NUM_SAMPLES as f32 - 1.0); debug_assert!(sb >= 0.0); debug_assert!(sb <= SPECTRUM_NUM_SAMPLES as f32); // Get the spectral values for the vertices of the grid cell. let mut p = [0.0f32; 6]; let sb0: i32 = sb as i32; let sb1: i32 = if (sb + 1.0) < SPECTRUM_NUM_SAMPLES as f32 { sb as i32 + 1 } else { SPECTRUM_NUM_SAMPLES - 1 }; assert!(sb0 < SPECTRUM_NUM_SAMPLES); let sbf: f32 = sb as f32 - sb0 as f32; for i in 0..(num as usize) { debug_assert!(idx[i] >= 0); let spectrum = &SPECTRUM_DATA_POINTS[idx[i] as usize].spectrum; p[i] = spectrum[sb0 as usize] * (1.0 - sbf) + spectrum[sb1 as usize] * sbf; } // Linearly interpolated the spectral power of the cell vertices. let mut interpolated_p: f32 = 0.0; if inside { // Fast path for normal inner quads: let uv2 = (uv.0 - uvi.0 as f32, uv.1 - uvi.1 as f32); assert!(uv2.0 >= 0.0 && uv2.0 <= 1.0); assert!(uv2.1 >= 0.0 && uv2.1 <= 1.0); // The layout of the vertices in the quad is: // 2 3 // 0 1 interpolated_p = p[0] * (1.0 - uv2.0) * (1.0 - uv2.1) + p[2] * (1.0 - uv2.0) * uv2.1 + p[3] * uv2.0 * uv2.1 + p[1] * uv2.0 * (1.0 - uv2.1); } else { // Need to go through triangulation :( // We get the indices in such an order that they form a triangle fan around idx[0]. // compute barycentric coordinates of our xy* point for all triangles in the fan: let ex: f32 = uv.0 - SPECTRUM_DATA_POINTS[idx[0] as usize].uv.0; let ey: f32 = uv.1 - SPECTRUM_DATA_POINTS[idx[0] as usize].uv.1; let mut e0x: f32 = SPECTRUM_DATA_POINTS[idx[1] as usize].uv.0 - SPECTRUM_DATA_POINTS[idx[0] as usize].uv.0; let mut e0y: f32 = SPECTRUM_DATA_POINTS[idx[1] as usize].uv.1 - SPECTRUM_DATA_POINTS[idx[0] as usize].uv.1; let mut uu: f32 = e0x * ey - ex * e0y; for i in 0..(num as usize - 1) { let (e1x, e1y): (f32, f32) = if i as i32 == (num - 2) { // Close the circle ( SPECTRUM_DATA_POINTS[idx[1] as usize].uv.0 - SPECTRUM_DATA_POINTS[idx[0] as usize].uv.0, SPECTRUM_DATA_POINTS[idx[1] as usize].uv.1 - SPECTRUM_DATA_POINTS[idx[0] as usize].uv.1, ) } else { ( SPECTRUM_DATA_POINTS[idx[i + 2] as usize].uv.0 - SPECTRUM_DATA_POINTS[idx[0] as usize].uv.0, SPECTRUM_DATA_POINTS[idx[i + 2] as usize].uv.1 - SPECTRUM_DATA_POINTS[idx[0] as usize].uv.1, ) }; let vv: f32 = ex * e1y - e1x * ey; let area: f32 = e0x * e1y - e1x * e0y; // Normalise let u: f32 = uu / area; let v: f32 = vv / area; let w: f32 = 1.0 - u - v; // Outside spectral locus (quantized version at least) or outside grid if u < 0.0 || v < 0.0 || w < 0.0 { uu = -vv; e0x = e1x; e0y = e1y; continue; } // This seems to be the triangle we've been looking for. interpolated_p = p[0] * w + p[i + 1] * v + p[if i as i32 == (num - 2) { 1 } else { i + 2 }] * u; break; } } // Now we have a spectrum which corresponds to the xy chromaticities of // the input. need to scale according to the input brightness X+Y+Z now: return interpolated_p * inv_norm; } /// Evaluate the spectrum for xyz at the given wavelengths. /// /// Works on 4 wavelengths at once via SIMD. #[inline] pub fn spectrum_xyz_to_p_4(lambdas: Vec4, xyz: (f32, f32, f32)) -> Vec4 { assert!(lambdas.min_element() >= SPECTRUM_SAMPLE_MIN); assert!(lambdas.max_element() <= SPECTRUM_SAMPLE_MAX); let inv_norm = xyz.0 + xyz.1 + xyz.2; let norm = { let norm = 1.0 / inv_norm; if norm < f32::MAX { norm } else { return Vec4::splat(0.0); } }; let xyy = (xyz.0 * norm, xyz.1 * norm, xyz.1); // Rotate to align with grid let uv = spectrum_xy_to_uv((xyy.0, xyy.1)); if uv.0 < 0.0 || uv.0 >= SPECTRUM_GRID_WIDTH as f32 || uv.1 < 0.0 || uv.1 >= SPECTRUM_GRID_HEIGHT as f32 { return Vec4::splat(0.0); } let uvi = (uv.0 as i32, uv.1 as i32); debug_assert!(uvi.0 < SPECTRUM_GRID_WIDTH); debug_assert!(uvi.1 < SPECTRUM_GRID_HEIGHT); let cell_idx: i32 = uvi.0 + SPECTRUM_GRID_WIDTH * uvi.1; debug_assert!(cell_idx >= 0); let cell = &SPECTRUM_GRID[cell_idx as usize]; let inside: bool = cell.inside; let idx = &cell.idx; let num: i32 = cell.num_points; // If the cell has no points, nothing we can do, so return 0.0 if num == 0 { return Vec4::splat(0.0); } // Normalize lambda to spectrum table index range. let sb: Vec4 = (lambdas - Vec4::splat(SPECTRUM_SAMPLE_MIN)) / (SPECTRUM_SAMPLE_MAX - SPECTRUM_SAMPLE_MIN) * (SPECTRUM_NUM_SAMPLES as f32 - 1.0); debug_assert!(sb.min_element() >= 0.0); debug_assert!(sb.max_element() <= SPECTRUM_NUM_SAMPLES as f32); // Get the spectral values for the vertices of the grid cell. // TODO: use integer SIMD intrinsics to make this part faster. let mut p = [Vec4::splat(0.0); 6]; let sb0: [i32; 4] = [sb[0] as i32, sb[1] as i32, sb[2] as i32, sb[3] as i32]; assert!(sb0[0].max(sb0[1]).max(sb0[2].max(sb0[3])) < SPECTRUM_NUM_SAMPLES); let sb1: [i32; 4] = [ (sb[0] as i32 + 1).min(SPECTRUM_NUM_SAMPLES - 1), (sb[1] as i32 + 1).min(SPECTRUM_NUM_SAMPLES - 1), (sb[2] as i32 + 1).min(SPECTRUM_NUM_SAMPLES - 1), (sb[3] as i32 + 1).min(SPECTRUM_NUM_SAMPLES - 1), ]; let sbf = sb - Vec4::new(sb0[0] as f32, sb0[1] as f32, sb0[2] as f32, sb0[3] as f32); for i in 0..(num as usize) { debug_assert!(idx[i] >= 0); let spectrum = &SPECTRUM_DATA_POINTS[idx[i] as usize].spectrum; let p0 = Vec4::new( spectrum[sb0[0] as usize], spectrum[sb0[1] as usize], spectrum[sb0[2] as usize], spectrum[sb0[3] as usize], ); let p1 = Vec4::new( spectrum[sb1[0] as usize], spectrum[sb1[1] as usize], spectrum[sb1[2] as usize], spectrum[sb1[3] as usize], ); p[i] = p0 * (Vec4::splat(1.0) - sbf) + p1 * sbf; } // Linearly interpolate the spectral power of the cell vertices. let mut interpolated_p = Vec4::splat(0.0); if inside { // Fast path for normal inner quads: let uv2 = (uv.0 - uvi.0 as f32, uv.1 - uvi.1 as f32); assert!(uv2.0 >= 0.0 && uv2.0 <= 1.0); assert!(uv2.1 >= 0.0 && uv2.1 <= 1.0); // The layout of the vertices in the quad is: // 2 3 // 0 1 interpolated_p = p[0] * ((1.0 - uv2.0) * (1.0 - uv2.1)) + p[2] * ((1.0 - uv2.0) * uv2.1) + p[3] * (uv2.0 * uv2.1) + p[1] * (uv2.0 * (1.0 - uv2.1)); } else { // Need to go through triangulation :( // We get the indices in such an order that they form a triangle fan around idx[0]. // compute barycentric coordinates of our xy* point for all triangles in the fan: let ex: f32 = uv.0 - SPECTRUM_DATA_POINTS[idx[0] as usize].uv.0; let ey: f32 = uv.1 - SPECTRUM_DATA_POINTS[idx[0] as usize].uv.1; let mut e0x: f32 = SPECTRUM_DATA_POINTS[idx[1] as usize].uv.0 - SPECTRUM_DATA_POINTS[idx[0] as usize].uv.0; let mut e0y: f32 = SPECTRUM_DATA_POINTS[idx[1] as usize].uv.1 - SPECTRUM_DATA_POINTS[idx[0] as usize].uv.1; let mut uu: f32 = e0x * ey - ex * e0y; for i in 0..(num as usize - 1) { let (e1x, e1y): (f32, f32) = if i as i32 == (num - 2) { // Close the circle ( SPECTRUM_DATA_POINTS[idx[1] as usize].uv.0 - SPECTRUM_DATA_POINTS[idx[0] as usize].uv.0, SPECTRUM_DATA_POINTS[idx[1] as usize].uv.1 - SPECTRUM_DATA_POINTS[idx[0] as usize].uv.1, ) } else { ( SPECTRUM_DATA_POINTS[idx[i + 2] as usize].uv.0 - SPECTRUM_DATA_POINTS[idx[0] as usize].uv.0, SPECTRUM_DATA_POINTS[idx[i + 2] as usize].uv.1 - SPECTRUM_DATA_POINTS[idx[0] as usize].uv.1, ) }; let vv: f32 = ex * e1y - e1x * ey; let area: f32 = e0x * e1y - e1x * e0y; // Normalise let u: f32 = uu / area; let v: f32 = vv / area; let w: f32 = 1.0 - u - v; // Outside spectral locus (quantized version at least) or outside grid if u < 0.0 || v < 0.0 || w < 0.0 { uu = -vv; e0x = e1x; e0y = e1y; continue; } // This seems to be the triangle we've been looking for. interpolated_p = p[0] * w + p[i + 1] * v + p[if i as i32 == (num - 2) { 1 } else { i + 2 }] * u; break; } } // Now we have a spectrum which corresponds to the xy chromaticities of // the input. need to scale according to the input brightness X+Y+Z now: return interpolated_p * inv_norm; } // apply a 3x2 matrix to a 2D color. #[inline(always)] fn spectrum_apply_3x2(matrix: &[f32; 6], src: (f32, f32)) -> (f32, f32) { ( matrix[0] * src.0 + matrix[1] * src.1 + matrix[2], matrix[3] * src.0 + matrix[4] * src.1 + matrix[5], ) } // Concrete conversion routines. // #[inline(always)] // fn spectrum_xy_to_xystar(xy: (f32, f32)) -> (f32, f32) { // spectrum_apply_3x2(&SPECTRUM_MAT_XY_TO_XYSTAR, xy) // } // #[inline(always)] // fn spectrum_xystar_to_xy(xystar: (f32, f32)) -> (f32, f32) { // spectrum_apply_3x2(&SPECTRUM_MAT_XYSTAR_TO_XY, xystar) // } #[inline(always)] fn spectrum_xy_to_uv(xy: (f32, f32)) -> (f32, f32) { spectrum_apply_3x2(&SPECTRUM_MAT_XY_TO_UV, xy) } // #[inline(always)] // fn spectrum_uv_to_xy(uv: (f32, f32)) -> (f32, f32) { // spectrum_apply_3x2(&SPECTRUM_MAT_UV_TO_XY, uv) // } // #[inline] // pub fn xyz_from_spectrum(spectrum: &[f32]) -> (f32, f32, f32) { // let mut xyz = (0.0, 0.0, 0.0); // for i in 0..(SPECTRUM_NUM_SAMPLES as usize) { // xyz.0 += spectrum[i] * CMF_X[i]; // xyz.1 += spectrum[i] * CMF_Y[i]; // xyz.2 += spectrum[i] * CMF_Z[i]; // } // xyz.0 *= SPECTRUM_BIN_SIZE; // xyz.1 *= SPECTRUM_BIN_SIZE; // xyz.2 *= SPECTRUM_BIN_SIZE; // return xyz; // }