diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..709f8a6 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "frontend" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[workspace] +members = ["crates/*"] + +[dependencies] +clap = { version = "4.1.8", features = ["derive", "wrap_help"] } +gui = { path = "./crates/gui", optional = true } +implementations = { path = "./crates/implementations" } +indicatif = "0.17.3" +loader = { path = "./crates/loader" } +output = { path = "./crates/output" } +rand = { version = "0.8.3", features = [ "small_rng" ] } +rand_seeder = "0.2.2" +rayon = "1.5.1" +region = { path = "./crates/region" } +vulkano = { version = "0.28.0", optional = true } +vulkano-shaders = { version = "0.28.0", optional = true } +vulkano-win = { version = "0.28.0", optional = true } +winit = { version = "0.26.1", optional = true } + +[features] +f64 = ["implementations/f64"] +gui = ["dep:vulkano", "dep:vulkano-win", "dep:vulkano-shaders", "dep:winit", "dep:gui"] diff --git a/LICENSE b/LICENSE deleted file mode 100644 index f288702..0000000 --- a/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ - 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 -. diff --git a/crates/gui/Cargo.toml b/crates/gui/Cargo.toml new file mode 100644 index 0000000..4bab343 --- /dev/null +++ b/crates/gui/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "gui" +version = "0.1.0" +edition = "2021" + +[dependencies] +vulkano = { version = "0.28.0" } +vulkano-shaders = { version = "0.28.0" } +vulkano-win = { version = "0.28.0" } +winit = { version = "0.26.1" } +implementations = { path = "../implementations" } +indicatif = "0.17.3" diff --git a/frontend/src/gui.rs b/crates/gui/src/gui.rs similarity index 93% rename from frontend/src/gui.rs rename to crates/gui/src/gui.rs index bc505c0..2a04a22 100644 --- a/frontend/src/gui.rs +++ b/crates/gui/src/gui.rs @@ -1,4 +1,6 @@ -use crate::rendering::*; +use crate::rendering::CpuRendering; +use crate::rendering::RenderInfo; +use std::sync::atomic::AtomicBool; use std::sync::Arc; use vulkano::{ command_buffer::{AutoCommandBufferBuilder, CommandBufferUsage, PrimaryAutoCommandBuffer}, @@ -40,10 +42,11 @@ pub struct Gui { render_info: RenderInfo, combined_buffer: Arc, presentation_finished: Option>, + exit: Arc, } impl Gui { - pub fn new(instance: &Arc, width: u32, height: u32) -> Self { + pub fn new(instance: &Arc, width: u32, height: u32, exit: Arc) -> Self { let event_loop: EventLoop = EventLoop::with_user_event(); let surface = WindowBuilder::new() .build_vk_surface(&event_loop, instance.clone()) @@ -139,9 +142,7 @@ impl Gui { let cpu_rendering = CpuRendering::new(&physical_device, device.clone(), width, height); mod cs { - vulkano_shaders::shader! { - ty: "compute", - src: + vulkano_shaders::shader! {ty: "compute",src: "#version 460 layout(local_size_x = 32, local_size_y = 32) in; @@ -151,8 +152,8 @@ layout(set = 0, binding = 0, rgba32f) uniform readonly image2D cpu_input; layout(set = 0, binding = 1, rgba8) uniform writeonly image2D image_output; void main() { - vec4 data = sqrt(imageLoad(cpu_input, ivec2(gl_GlobalInvocationID.xy))); - imageStore(image_output, ivec2(gl_GlobalInvocationID.xy), data); + vec4 data = sqrt(imageLoad(cpu_input, ivec2(gl_GlobalInvocationID.xy))); + imageStore(image_output, ivec2(gl_GlobalInvocationID.xy), data); }"} } @@ -201,6 +202,7 @@ void main() { presentation_command_buffers, combined_buffer, presentation_finished: None, + exit, } } @@ -213,14 +215,14 @@ void main() { Event::DeviceEvent { event: winit::event::DeviceEvent::Key(key), .. - } => match key.virtual_keycode { - Some(code) => { + } => { + if let Some(code) = key.virtual_keycode { if code == winit::event::VirtualKeyCode::Escape { + self.exit.store(true, std::sync::atomic::Ordering::Relaxed); *control_flow = ControlFlow::Exit; } } - None => {} - }, + } Event::WindowEvent { event: WindowEvent::CloseRequested, .. @@ -246,9 +248,8 @@ void main() { } fn update(&mut self) { - match self.presentation_finished.as_mut() { - Some(future) => future.cleanup_finished(), - None => {} + if let Some(future) = self.presentation_finished.as_mut() { + future.cleanup_finished() } self.presentation_finished = Some(sync::now(self.device.clone()).boxed()); @@ -260,7 +261,7 @@ void main() { return; } Err(e) => { - panic!("Failed to acquire next image: {:?}", e) + panic!("Failed to acquire next image: {e:?}") } }; @@ -328,7 +329,7 @@ void main() { self.presentation_finished = Some(sync::now(self.device.clone()).boxed()); } Err(e) => { - println!("Failed to flush future: {:?}", e); + println!("Failed to flush future: {e:?}"); self.presentation_finished = Some(sync::now(self.device.clone()).boxed()); } } @@ -339,7 +340,7 @@ void main() { match self.swapchain.recreate().dimensions(dimensions).build() { Ok(r) => r, Err(SwapchainCreationError::UnsupportedDimensions) => return, - Err(e) => panic!("Failed to recreate swapchain: {:?}", e), + Err(e) => panic!("Failed to recreate swapchain: {e:?}"), }; let extent: [u32; 2] = self.surface.window().inner_size().into(); diff --git a/crates/gui/src/lib.rs b/crates/gui/src/lib.rs new file mode 100644 index 0000000..9abb406 --- /dev/null +++ b/crates/gui/src/lib.rs @@ -0,0 +1,184 @@ +mod gui; +mod rendering; + +use implementations::SamplerProgress; +use indicatif::ProgressBar; +use indicatif::ProgressStyle; + +use { + std::sync::{ + atomic::{AtomicBool, AtomicU64, Ordering}, + Arc, + }, + vulkano::{ + buffer::CpuAccessibleBuffer, + command_buffer::{AutoCommandBufferBuilder, CommandBufferUsage, PrimaryAutoCommandBuffer}, + device::{Device, Queue}, + image::StorageImage, + sync::{self, GpuFuture}, + }, + winit::event_loop::EventLoopProxy, +}; + +pub use crate::gui::{Gui, RenderEvent}; +pub use crate::rendering::Future; + +pub struct Data { + pub queue: Arc, + pub device: Arc, + pub to_sc: rendering::Future, + pub from_sc: rendering::Future, + pub command_buffers: [Arc; 2], + pub buffer: Arc>, + pub sc_index: Arc, + pub samples: Arc, + pub total_samples: u64, + pub rays_shot: Arc, + pub event_proxy: EventLoopProxy, + pub exit: Arc, + pub bar: ProgressBar, +} + +impl Data { + pub fn new( + queue: Arc, + device: Arc, + to_sc: rendering::Future, + from_sc: rendering::Future, + command_buffers: [Arc; 2], + buffer: Arc>, + sc_index: Arc, + samples: Arc, + total_samples: u64, + rays_shot: Arc, + exit: Arc, + event_proxy: EventLoopProxy, + ) -> Self { + Data { + queue, + device, + to_sc, + from_sc, + command_buffers, + buffer, + sc_index, + samples, + total_samples, + rays_shot, + event_proxy, + exit, + bar: ProgressBar::new(total_samples).with_style( + ProgressStyle::default_bar() + .template("[{elapsed_precise}] {bar:40.cyan/blue} {pos:>7}/{len:7} {msg}") + .unwrap(), + ), + } + } +} + +pub fn create_command_buffers( + device: Arc, + queue: Arc, + buffer: Arc>, + sc: [Arc; 2], +) -> [Arc; 2] { + let mut command_buffer_0 = None; + let mut command_buffer_1 = None; + for (i, sc_image) in sc.iter().enumerate() { + let mut builder = AutoCommandBufferBuilder::primary( + device.clone(), + queue.family(), + CommandBufferUsage::MultipleSubmit, + ) + .unwrap(); + + builder + .copy_buffer_to_image(buffer.clone(), sc_image.clone()) + .unwrap(); + if i == 0 { + command_buffer_0 = Some(builder.build().unwrap()); + } else { + command_buffer_1 = Some(builder.build().unwrap()); + } + } + + [ + Arc::new(command_buffer_0.unwrap()), + Arc::new(command_buffer_1.unwrap()), + ] +} + +pub fn sample_update(data: &mut Data, previous: &SamplerProgress, i: u64) -> bool { + if data.exit.load(Ordering::Relaxed) { + return true; + } + // update infomation about the rays shot and samples completed in the current render + data.samples.fetch_add(1, Ordering::Relaxed); + data.rays_shot + .fetch_add(previous.rays_shot, Ordering::Relaxed); + + // wait on from_sc future if is_some() + match &*data.from_sc.lock().unwrap() { + Some(future) => { + future.wait(None).unwrap(); + } + None => {} + } + match &*data.to_sc.lock().unwrap() { + Some(future) => { + future.wait(None).unwrap(); + } + None => {} + } + + { + // get access to CpuAccessibleBuffer + let mut buf = data.buffer.write().unwrap(); + buf.chunks_mut(4) + .zip(previous.current_image.chunks(3)) + .for_each(|(pres, acc)| { + pres[0] += (acc[0] as f32 - pres[0]) / i as f32; + pres[1] += (acc[1] as f32 - pres[1]) / i as f32; + pres[2] += (acc[2] as f32 - pres[2]) / i as f32; + pres[3] = 1.0; + }); + } + + // copy to cpu swapchain + let command_buffer = + data.command_buffers[data.sc_index.load(Ordering::Relaxed) as usize].clone(); + + // copy to swapchain and store op in to_sc future + { + let to_sc = &mut *data.to_sc.lock().unwrap(); + *to_sc = Some( + match to_sc.take() { + Some(future) => future + .then_execute(data.queue.clone(), command_buffer) + .unwrap() + .boxed_send_sync(), + None => sync::now(data.device.clone()) + .then_execute(data.queue.clone(), command_buffer) + .unwrap() + .boxed_send_sync(), + } + .then_signal_fence_and_flush() + .unwrap(), // change to match + ); + } + + // modify sc_index to !sc_index + data.sc_index + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |x| Some(!x)) + .unwrap(); + + data.bar.set_position(data.samples.load(Ordering::Relaxed)); + if data.samples.load(Ordering::Relaxed) == data.total_samples { + data.bar.abandon() + } + + // signal sample is ready to be presented + data.event_proxy + .send_event(RenderEvent::SampleCompleted) + .is_err() +} diff --git a/frontend/src/rendering.rs b/crates/gui/src/rendering.rs similarity index 100% rename from frontend/src/rendering.rs rename to crates/gui/src/rendering.rs diff --git a/implementations/Cargo.toml b/crates/implementations/Cargo.toml similarity index 51% rename from implementations/Cargo.toml rename to crates/implementations/Cargo.toml index 03f5fe1..d36d01a 100644 --- a/implementations/Cargo.toml +++ b/crates/implementations/Cargo.toml @@ -7,11 +7,21 @@ edition = "2021" [dependencies] image = "0.24.3" -proc = {path = "./proc"} +proc = { path = "./proc" } rand = { version = "0.8.3", features = [ "small_rng" ] } rayon = "1.5.1" -rt_core = {path = "../rt_core"} +rt_core = { path = "../rt_core" } +bumpalo = {version="3.12.0", features=["collections"]} +num_cpus = "1.15" +region = { path = "../region"} +statrs = "0.16.0" +clap = { version = "4.1.8", features = [ "derive" ] } + + [dev-dependencies] chrono = "0.4.19" -statrs = "0.16.0" \ No newline at end of file +statrs = "0.16.0" + +[features] +f64 = ["rt_core/f64"] diff --git a/implementations/proc/Cargo.toml b/crates/implementations/proc/Cargo.toml similarity index 100% rename from implementations/proc/Cargo.toml rename to crates/implementations/proc/Cargo.toml diff --git a/implementations/proc/src/lib.rs b/crates/implementations/proc/src/lib.rs similarity index 96% rename from implementations/proc/src/lib.rs rename to crates/implementations/proc/src/lib.rs index 2f4d3fe..5a3637e 100644 --- a/implementations/proc/src/lib.rs +++ b/crates/implementations/proc/src/lib.rs @@ -122,7 +122,7 @@ pub fn derive_primitive(tokens: TokenStream) -> TokenStream { let func_names_primitive = [ ( - quote!(get_int(&self, __one: &Ray) -> Option), + quote!(get_int(&self, __one: &Ray) -> Option>), quote!(get_int(__one)), ), ( @@ -177,7 +177,10 @@ pub fn derive_primitive(tokens: TokenStream) -> TokenStream { }); quote! { - impl #impl_generics Primitive #ty_generics for #enum_name #ty_generics #where_clause {#( #functions_primitive )*} + impl #impl_generics Primitive for #enum_name #ty_generics #where_clause { + type Material = M; + #( #functions_primitive )* + } impl #impl_generics AABound for #enum_name #ty_generics #where_clause { #( #functions_aabound )*} } .into() diff --git a/implementations/src/acceleration/aabb.rs b/crates/implementations/src/acceleration/aabb.rs similarity index 98% rename from implementations/src/acceleration/aabb.rs rename to crates/implementations/src/acceleration/aabb.rs index 1791967..073e43c 100644 --- a/implementations/src/acceleration/aabb.rs +++ b/crates/implementations/src/acceleration/aabb.rs @@ -1,5 +1,5 @@ use crate::utility::gamma; -use rt_core::{Float, Ray, Vec3}; +use rt_core::*; pub trait AABound { fn get_aabb(&self) -> AABB; diff --git a/implementations/src/acceleration/mod.rs b/crates/implementations/src/acceleration/mod.rs similarity index 81% rename from implementations/src/acceleration/mod.rs rename to crates/implementations/src/acceleration/mod.rs index 38b4f8e..71d0a2d 100644 --- a/implementations/src/acceleration/mod.rs +++ b/crates/implementations/src/acceleration/mod.rs @@ -4,7 +4,9 @@ use crate::{ utility::sort_by_indices, Axis, }; -use rt_core::{AccelerationStructure, Primitive, Ray, Scatter, SurfaceIntersection, Vec3}; +use region::RegionResSlice; + +use rt_core::*; use std::{collections::VecDeque, marker::PhantomData}; #[cfg(all(feature = "f64"))] @@ -25,7 +27,7 @@ pub struct PrimitiveInfo { } impl PrimitiveInfo { - fn new + AABound, M: Scatter>(index: usize, primitive: &P) -> PrimitiveInfo { + fn new(index: usize, primitive: &P) -> PrimitiveInfo { let aabb = primitive.get_aabb(); let min = aabb.min; let max = aabb.max; @@ -38,47 +40,55 @@ impl PrimitiveInfo { } } -pub struct Bvh, M: Scatter> { +pub struct Bvh> { split_type: SplitType, nodes: Vec, - pub primitives: Vec

, + sky: S, + pub primitives: RegionResSlice

, pub lights: Vec, phantom: PhantomData, } -impl Bvh +impl Bvh where - P: Primitive + AABound, + P: Primitive + AABound, M: Scatter, + S: NoHit, { - pub fn new(primitives: Vec

, split_type: SplitType) -> Self { + pub fn new( + mut primitives: region::RegionUniqSlice<'_, P>, + sky: S, + split_type: SplitType, + ) -> Self { let mut bvh = Self { split_type, nodes: Vec::new(), - primitives, + sky, + primitives: primitives.zero_slice(), lights: Vec::new(), phantom: PhantomData, }; - let mut primitives_info: Vec = bvh - .primitives + let mut primitives_info: Vec = primitives .iter() .enumerate() - .map(|(index, primitive)| PrimitiveInfo::new(index, primitive)) + .map(|(index, primitive)| PrimitiveInfo::new::(index, primitive)) .collect(); bvh.build_bvh(&mut Vec::new(), 0, &mut primitives_info); sort_by_indices( - &mut bvh.primitives, + &mut primitives, primitives_info.iter().map(|&info| info.index).collect(), ); - for (i, prim) in bvh.primitives.iter().enumerate() { + for (i, prim) in primitives.iter().enumerate() { if prim.material_is_light() { bvh.lights.push(i); } } + bvh.primitives = primitives.shared(); + bvh } pub fn number_nodes(&self) -> usize { @@ -177,11 +187,15 @@ where } } -impl AccelerationStructure for Bvh +impl AccelerationStructure for Bvh where - P: Primitive, + P: Primitive, M: Scatter, + S: NoHit, { + type Object = P; + type Material = M; + type Sky = S; fn get_intersection_candidates(&self, ray: &Ray) -> Vec<(usize, usize)> { let mut offset_len = Vec::new(); @@ -248,7 +262,7 @@ where intersection } - fn check_hit(&self, ray: &Ray) -> Option<(SurfaceIntersection, usize)> { + fn check_hit(&self, ray: &Ray) -> (SurfaceIntersection, usize) { let offset_lens = self.get_intersection_candidates(ray); let mut hit: Option<(SurfaceIntersection, usize)> = None; @@ -277,7 +291,30 @@ where } } } - hit + match hit { + None => (self.sky.get_si(ray), usize::MAX), + Some(hit) => hit, + } + } + fn get_pdf_from_index( + &self, + last_hit: &Hit, + light_hit: &Hit, + sampled_dir: Vec3, + index: usize, + ) -> Float { + let sky_samplable = self.sky.can_sample(); + let divisor = if sky_samplable { + self.lights.len() + 1 + } else { + self.lights.len() + } as Float; + + if index == usize::MAX { + self.sky.pdf(sampled_dir) / divisor + } else { + self.primitives[index].scattering_pdf(last_hit.point, sampled_dir, light_hit) / divisor + } } fn get_samplable(&self) -> &[usize] { &self.lights @@ -285,6 +322,9 @@ where fn get_object(&self, index: usize) -> Option<&P> { self.primitives.get(index) } + fn sky(&self) -> &S { + &self.sky + } } #[derive(Debug)] diff --git a/implementations/src/acceleration/split.rs b/crates/implementations/src/acceleration/split.rs similarity index 98% rename from implementations/src/acceleration/split.rs rename to crates/implementations/src/acceleration/split.rs index 7e443a9..bde3802 100644 --- a/implementations/src/acceleration/split.rs +++ b/crates/implementations/src/acceleration/split.rs @@ -1,5 +1,6 @@ use crate::{aabb::AABB, acceleration::PrimitiveInfo, Axis}; -use rt_core::Float; +use clap::ValueEnum; +use rt_core::*; const NUM_BUCKETS: usize = 12; const MAX_IN_NODE: usize = 255; @@ -30,6 +31,7 @@ macro_rules! partition { }}; } +#[derive(Debug, ValueEnum, Copy, Clone)] pub enum SplitType { Sah, Middle, diff --git a/implementations/src/cameras/mod.rs b/crates/implementations/src/camera.rs similarity index 96% rename from implementations/src/cameras/mod.rs rename to crates/implementations/src/camera.rs index b1a5c56..b9f31e7 100644 --- a/implementations/src/cameras/mod.rs +++ b/crates/implementations/src/camera.rs @@ -1,6 +1,8 @@ use crate::utility::random_float; -use rt_core::{Camera, Float, Ray, Vec3}; +use crate::Camera; +use rt_core::*; +#[derive(Debug)] pub struct SimpleCamera { pub viewport_width: Float, pub viewport_height: Float, diff --git a/crates/implementations/src/integrators/mis.rs b/crates/implementations/src/integrators/mis.rs new file mode 100644 index 0000000..ed4e46a --- /dev/null +++ b/crates/implementations/src/integrators/mis.rs @@ -0,0 +1,157 @@ +use crate::integrators::*; +use rt_core::*; + +pub struct MisIntegrator; + +impl Integrator for MisIntegrator { + fn get_colour, P: Primitive, M: Scatter>( + ray: &mut Ray, + bvh: &A, + ) -> (Vec3, u64) { + let (mut throughput, mut output) = (Vec3::one(), Vec3::zero()); + let mut ray_count = 0; + + let mut wo; + let mut hit; + let mut mat; + let (surface_intersection, _index) = bvh.check_hit(ray); + + (hit, mat) = (surface_intersection.hit, surface_intersection.material); + + wo = ray.direction; + + let emission = mat.get_emission(&hit, wo); + + let exit = mat.scatter_ray(&mut ray.clone(), &hit); + + output += emission; + + if exit { + return (output, ray_count); + } + + let mut depth = 1; + + while depth < MAX_DEPTH { + // light sampling + let sample_lights = sample_lights(bvh, &hit); + ray_count += 1; + if let Some((l_wi, le, l_pdf)) = sample_lights { + let m_pdf = mat.scattering_pdf(&hit, wo, l_wi); + let mis_weight = power_heuristic(l_pdf, m_pdf); + output += throughput * mat.eval(&hit, wo, l_wi) * mis_weight * le / l_pdf; + } + + // material sampling and bounce + let exit = mat.scatter_ray(ray, &hit); + if exit { + break; + } + let m_wi = ray.direction; + + let (intersection, index) = bvh.check_hit(ray); + + let m_pdf = mat.scattering_pdf(&hit, wo, m_wi); + let le = intersection.material.get_emission(&hit, m_wi); + throughput *= mat.eval_over_scattering_pdf(&hit, wo, m_wi); + if le != Vec3::zero() { + if (bvh.get_samplable().contains(&index) && !mat.is_delta()) + || (index == usize::MAX && bvh.sky().can_sample()) + { + let l_pdf = bvh.get_pdf_from_index(&hit, &intersection.hit, m_wi, index); + let mis_weight = power_heuristic(m_pdf, l_pdf); + output += throughput * le * mis_weight; + } else { + output += throughput * le; + } + } + + if intersection.material.is_light() { + break; + } + + if depth > RUSSIAN_ROULETTE_THRESHOLD { + let p = throughput.component_max(); + let mut rng = SmallRng::from_rng(thread_rng()).unwrap(); + if rng.gen::() > p { + break; + } + throughput /= p; + } + + wo = m_wi; + hit = intersection.hit; + mat = intersection.material; + + depth += 1; + } + if output.contains_nan() || !output.is_finite() { + return (Vec3::zero(), ray_count); + } + (output, ray_count) + } +} + +fn sample_lights, P: Primitive, M: Scatter>( + bvh: &A, + hit: &Hit, +) -> Option<(Vec3, Vec3, Float)> { + //l_wi, le, l_pdf + let sky = bvh.sky(); + let samplable_len = bvh.get_samplable().len(); + let sky_can_sample = sky.can_sample(); + + let sample_sky = |pdf_multiplier: Float| { + let l_wi = sky.sample(); + let ray = Ray::new(hit.point + 0.0001 * hit.normal, l_wi, 0.0); + + let (sa, index) = bvh.check_hit(&ray); + if index == usize::MAX { + let le = sa.material.get_emission(hit, l_wi); + let l_pdf = sky.pdf(l_wi); + return Some((l_wi, le, l_pdf * pdf_multiplier)); + } + None + }; + + let sample_light = |pdf_multiplier: Float, index: usize| { + let index = bvh.get_samplable()[index]; + let light = bvh.get_object(index).unwrap(); + + let l_wi = light.sample_visible_from_point(hit.point); + + if let Some(si) = + bvh.check_hit_index(&Ray::new(hit.point + 0.0001 * hit.normal, l_wi, 0.0), index) + { + let l_pdf = light.scattering_pdf(hit.point, l_wi, &si.hit); + if l_pdf > 0.0 { + let le = si.material.get_emission(&si.hit, l_wi); + return Some((l_wi, le, l_pdf * pdf_multiplier)); + } + } + None + }; + + match (samplable_len, sky_can_sample) { + (0, false) => None, + (0, true) => sample_sky(1.0), + (_, false) => { + let multipler = 1.0 / samplable_len as Float; + let light_index = SmallRng::from_rng(thread_rng()) + .unwrap() + .gen_range(0..samplable_len); + sample_light(multipler, light_index) + } + (_, true) => { + let multipler = 1.0 / (samplable_len + 1) as Float; + let light_index = SmallRng::from_rng(thread_rng()) + .unwrap() + .gen_range(0..=samplable_len); + if light_index == samplable_len { + sample_sky(multipler) + } else { + sample_light(multipler, light_index) + } + } + } +} diff --git a/crates/implementations/src/integrators/mod.rs b/crates/implementations/src/integrators/mod.rs new file mode 100644 index 0000000..72fc932 --- /dev/null +++ b/crates/implementations/src/integrators/mod.rs @@ -0,0 +1,79 @@ +use crate::rt_core::*; +use rand::rngs::SmallRng; +use rand::thread_rng; +use rand::Rng; +use rand::SeedableRng; + +const MAX_DEPTH: u32 = 50; +const RUSSIAN_ROULETTE_THRESHOLD: u32 = 3; + +pub mod mis; +pub use mis::*; + +pub trait Integrator { + fn get_colour, P: Primitive, M: Scatter>( + ray: &mut Ray, + bvh: &A, + ) -> (Vec3, u64); +} + +pub struct NaiveIntegrator; + +impl Integrator for NaiveIntegrator { + fn get_colour, P: Primitive, M: Scatter>( + ray: &mut Ray, + bvh: &A, + ) -> (Vec3, u64) { + let (mut throughput, mut output) = (Vec3::one(), Vec3::zero()); + let mut depth = 0; + let mut ray_count = 0; + + while depth < MAX_DEPTH { + let hit_info = bvh.check_hit(ray); + + ray_count += 1; + + let (surface_intersection, _index) = hit_info; + let (hit, mat) = (&surface_intersection.hit, &surface_intersection.material); + + let wo = ray.direction; + + let emission = mat.get_emission(hit, wo); + + let exit = mat.scatter_ray(ray, hit); + + if depth == 0 { + output += emission; + if exit { + break; + } + } + + if exit { + output += throughput * emission; + break; + } + + if !mat.is_delta() { + throughput *= mat.eval_over_scattering_pdf(hit, wo, ray.direction); + } else { + throughput *= mat.eval(hit, wo, ray.direction); + } + + if depth > RUSSIAN_ROULETTE_THRESHOLD { + let p = throughput.component_max(); + let mut rng = SmallRng::from_rng(thread_rng()).unwrap(); + if rng.gen::() > p { + break; + } + throughput /= p; + } + + depth += 1; + } + if output.contains_nan() || !output.is_finite() { + return (Vec3::zero(), ray_count); + } + (output, ray_count) + } +} diff --git a/implementations/src/lib.rs b/crates/implementations/src/lib.rs similarity index 63% rename from implementations/src/lib.rs rename to crates/implementations/src/lib.rs index 32d9e20..d8c243a 100644 --- a/implementations/src/lib.rs +++ b/crates/implementations/src/lib.rs @@ -1,26 +1,24 @@ mod acceleration; -mod cameras; +mod camera; +mod integrators; mod materials; mod primitives; mod samplers; +mod sky; +mod statistics; mod textures; mod utility; pub use acceleration::*; -pub use cameras::*; +pub use camera::*; pub use materials::*; pub use primitives::*; pub use proc::*; pub use samplers::*; +pub use sky::*; +pub use statistics::*; pub use textures::*; +pub use utility::*; pub use primitives::triangle::Triangle; - -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - let result = 2 + 2; - assert_eq!(result, 4); - } -} +pub use rt_core; diff --git a/crates/implementations/src/materials/emissive.rs b/crates/implementations/src/materials/emissive.rs new file mode 100644 index 0000000..5736af3 --- /dev/null +++ b/crates/implementations/src/materials/emissive.rs @@ -0,0 +1,39 @@ +use crate::{textures::Texture, utility::offset_ray}; +use rt_core::*; + +#[derive(Debug, Clone)] +pub struct Emit<'a, T: Texture> { + pub texture: &'a T, + pub strength: Float, +} + +impl<'a, T> Emit<'a, T> +where + T: Texture, +{ + pub fn new(texture: &'a T, strength: Float) -> Self { + Emit { texture, strength } + } +} + +impl<'a, T> Scatter for Emit<'a, T> +where + T: Texture, +{ + fn get_emission(&self, hit: &Hit, wo: Vec3) -> Vec3 { + let point = offset_ray(hit.point, hit.normal, hit.error, true); + self.strength * self.texture.colour_value(wo, point) + } + fn scattering_pdf(&self, _hit: &Hit, _wo: Vec3, _wi: Vec3) -> Float { + unreachable!() + } + fn is_light(&self) -> bool { + true + } + fn eval(&self, _hit: &Hit, _: Vec3, _: Vec3) -> Vec3 { + unreachable!() + } + fn scatter_ray(&self, _: &mut Ray, _: &Hit) -> bool { + true + } +} diff --git a/crates/implementations/src/materials/lambertian.rs b/crates/implementations/src/materials/lambertian.rs new file mode 100644 index 0000000..d4b2a9e --- /dev/null +++ b/crates/implementations/src/materials/lambertian.rs @@ -0,0 +1,51 @@ +use crate::{textures::Texture, utility::offset_ray}; +use rand::{rngs::SmallRng, thread_rng, SeedableRng}; +use rt_core::*; + +#[derive(Debug, Clone)] +pub struct Lambertian<'a, T: Texture> { + pub texture: &'a T, + pub albedo: Float, +} + +#[cfg(all(feature = "f64"))] +use std::f64::consts::PI; + +#[cfg(not(feature = "f64"))] +use std::f32::consts::PI; + +impl<'a, T> Lambertian<'a, T> +where + T: Texture, +{ + pub fn new(texture: &'a T, albedo: Float) -> Self { + Lambertian { texture, albedo } + } +} + +impl<'a, T> Scatter for Lambertian<'a, T> +where + T: Texture, +{ + fn scatter_ray(&self, ray: &mut Ray, hit: &Hit) -> bool { + let direction = crate::statistics::bxdfs::lambertian::sample( + ray.direction, // no negation since lambertian::sample doesn't use ray.direction + hit.normal, + &mut SmallRng::from_rng(thread_rng()).unwrap(), + ); + + let point = offset_ray(hit.point, hit.normal, hit.error, true); + *ray = Ray::new(point, direction, ray.time); + + false + } + fn scattering_pdf(&self, hit: &Hit, wo: Vec3, wi: Vec3) -> Float { + crate::statistics::bxdfs::lambertian::pdf(wo, wi, hit.normal) + } + fn eval(&self, hit: &Hit, wo: Vec3, wi: Vec3) -> Vec3 { + self.texture.colour_value(wo, hit.point) * self.albedo * hit.normal.dot(wi).max(0.0) / PI + } + fn eval_over_scattering_pdf(&self, hit: &Hit, wo: Vec3, _: Vec3) -> Vec3 { + self.texture.colour_value(wo, hit.point) * self.albedo + } +} diff --git a/crates/implementations/src/materials/mod.rs b/crates/implementations/src/materials/mod.rs new file mode 100644 index 0000000..00d3ee9 --- /dev/null +++ b/crates/implementations/src/materials/mod.rs @@ -0,0 +1,25 @@ +use proc::Scatter; +use rt_core::{Float, Hit, Ray, Scatter, Vec3}; + +pub mod emissive; +pub mod lambertian; +pub mod reflect; +pub mod refract; +pub mod trowbridge_reitz; + +pub use crate::{ + materials::{ + emissive::Emit, lambertian::Lambertian, reflect::Reflect, refract::Refract, + trowbridge_reitz::TrowbridgeReitz, + }, + textures::Texture, +}; + +#[derive(Scatter, Debug, Clone)] +pub enum AllMaterials<'a, T: Texture> { + Emit(Emit<'a, T>), + Lambertian(Lambertian<'a, T>), + TrowbridgeReitz(TrowbridgeReitz<'a, T>), + Reflect(Reflect<'a, T>), + Refract(Refract<'a, T>), +} diff --git a/implementations/src/materials/reflect.rs b/crates/implementations/src/materials/reflect.rs similarity index 62% rename from implementations/src/materials/reflect.rs rename to crates/implementations/src/materials/reflect.rs index b939426..b22052d 100644 --- a/implementations/src/materials/reflect.rs +++ b/crates/implementations/src/materials/reflect.rs @@ -2,33 +2,29 @@ use crate::{ textures::Texture, utility::{offset_ray, random_unit_vector}, }; -use rt_core::{Float, Hit, Ray, Scatter, Vec3}; -use std::sync::Arc; +use rt_core::*; -#[derive(Debug)] -pub struct Reflect { - pub texture: Arc, +#[derive(Debug, Clone)] +pub struct Reflect<'a, T: Texture> { + pub texture: &'a T, pub fuzz: Float, } -impl Reflect +impl<'a, T> Reflect<'a, T> where T: Texture, { - pub fn new(texture: &Arc, fuzz: Float) -> Self { - Reflect { - texture: texture.clone(), - fuzz, - } + pub fn new(texture: &'a T, fuzz: Float) -> Self { + Reflect { texture, fuzz } } } -impl Scatter for Reflect +impl<'a, T> Scatter for Reflect<'a, T> where T: Texture, { fn scatter_ray(&self, ray: &mut Ray, hit: &Hit) -> bool { - let mut direction = ray.direction; + let mut direction = -ray.direction; direction.reflect(hit.normal); let point = offset_ray(hit.point, hit.normal, hit.error, true); *ray = Ray::new( diff --git a/implementations/src/materials/refract.rs b/crates/implementations/src/materials/refract.rs similarity index 73% rename from implementations/src/materials/refract.rs rename to crates/implementations/src/materials/refract.rs index daecb08..0b30e99 100644 --- a/implementations/src/materials/refract.rs +++ b/crates/implementations/src/materials/refract.rs @@ -3,28 +3,24 @@ use crate::{ textures::Texture, utility::{offset_ray, random_float}, }; -use rt_core::{Float, Hit, Ray, Scatter, Vec3}; -use std::sync::Arc; +use rt_core::*; -#[derive(Debug)] -pub struct Refract { - pub texture: Arc, +#[derive(Debug, Clone)] +pub struct Refract<'a, T: Texture> { + pub texture: &'a T, pub eta: Float, } -impl Refract +impl<'a, T> Refract<'a, T> where T: Texture, { - pub fn new(texture: &Arc, eta: Float) -> Self { - Refract { - texture: texture.clone(), - eta, - } + pub fn new(texture: &'a T, eta: Float) -> Self { + Refract { texture, eta } } } -impl Scatter for Refract +impl<'a, T> Scatter for Refract<'a, T> where T: Texture, { @@ -34,14 +30,14 @@ where eta_fraction = self.eta; } - let cos_theta = ((-1.0 * ray.direction).dot(hit.normal)).min(1.0); + let cos_theta = ((-ray.direction).dot(hit.normal)).min(1.0); let sin_theta = (1.0 - cos_theta * cos_theta).sqrt(); let cannot_refract = eta_fraction * sin_theta > 1.0; let f0 = (1.0 - eta_fraction) / (1.0 + eta_fraction); let f0 = f0 * f0 * Vec3::one(); if cannot_refract || fresnel(cos_theta, f0).x > random_float() { - let ref_mat = Reflect::new(&self.texture.clone(), 0.0); + let ref_mat = Reflect::new(self.texture, 0.0); return ref_mat.scatter_ray(ray, hit); } diff --git a/crates/implementations/src/materials/trowbridge_reitz.rs b/crates/implementations/src/materials/trowbridge_reitz.rs new file mode 100644 index 0000000..df74abe --- /dev/null +++ b/crates/implementations/src/materials/trowbridge_reitz.rs @@ -0,0 +1,92 @@ +use crate::{materials::refract, statistics::bxdfs::*, textures::Texture, utility::offset_ray}; +use rand::{rngs::SmallRng, thread_rng, SeedableRng}; +use rt_core::*; + +#[derive(Debug, Clone)] +pub struct TrowbridgeReitz<'a, T: Texture> { + pub texture: &'a T, + pub alpha: Float, + pub ior: Vec3, + pub metallic: Float, +} + +impl<'a, T> TrowbridgeReitz<'a, T> +where + T: Texture, +{ + pub fn new(texture: &'a T, roughness: Float, ior: Vec3, metallic: Float) -> Self { + Self { + texture, + alpha: roughness * roughness, + ior, + metallic, + } + } + + fn fresnel(&self, hit: &Hit, wo: Vec3, wi: Vec3, h: Vec3) -> Vec3 { + let f0 = ((1.0 - self.ior) / (1.0 + self.ior)).abs(); + let f0 = f0 * f0; + let f0 = lerp(f0, self.texture.colour_value(wi, hit.point), self.metallic); + refract::fresnel(wo.dot(h), f0) + } +} + +impl<'a, T> Scatter for TrowbridgeReitz<'a, T> +where + T: Texture, +{ + fn scatter_ray(&self, ray: &mut Ray, hit: &Hit) -> bool { + let direction = trowbridge_reitz_vndf::isotropic::sample( + self.alpha, + -ray.direction, + hit.normal, + &mut SmallRng::from_rng(thread_rng()).unwrap(), + ); + + let point = offset_ray(hit.point, hit.normal, hit.error, true); + *ray = Ray::new(point, direction, ray.time); + + false + } + fn scattering_pdf(&self, hit: &Hit, wo: Vec3, wi: Vec3) -> Float { + let wo = -wo; + let a = trowbridge_reitz_vndf::isotropic::pdf(self.alpha, wo, wi, hit.normal); + if a == 0.0 { + INFINITY + } else { + a + } + } + fn eval(&self, hit: &Hit, wo: Vec3, wi: Vec3) -> Vec3 { + let wo = -wo; + let h = (wi + wo).normalised(); + + if wi.dot(hit.normal) < 0.0 || h.dot(wo) < 0.0 { + return Vec3::zero(); + } + + let f = self.fresnel(hit, wo, wi, h); + let g = trowbridge_reitz_vndf::isotropic::g2(self.alpha, hit.normal, h, wo, wi); + let d = trowbridge_reitz_vndf::isotropic::d(self.alpha, hit.normal.dot(h)); + + f * g * d / (4.0 * wo.dot(hit.normal).abs() * wi.dot(hit.normal)) + } + fn eval_over_scattering_pdf(&self, hit: &Hit, wo: Vec3, wi: Vec3) -> Vec3 { + let wo = -wo; + let h = (wi + wo).normalised(); + + if wo.dot(h) < 0.0 || wi.dot(hit.normal) < 0.0 { + return Vec3::zero(); + } + + let f = self.fresnel(hit, wo, wi, h); + + let g = trowbridge_reitz_vndf::isotropic::g2(self.alpha, hit.normal, h, wo, wi); + + f * g / trowbridge_reitz_vndf::isotropic::g1(self.alpha, hit.normal, h, wo) + } +} + +fn lerp(a: Vec3, b: Vec3, t: Float) -> Vec3 { + (1.0 - t) * a + t * b +} diff --git a/implementations/src/primitives/mod.rs b/crates/implementations/src/primitives/mod.rs similarity index 87% rename from implementations/src/primitives/mod.rs rename to crates/implementations/src/primitives/mod.rs index 0abc039..f9d7894 100644 --- a/implementations/src/primitives/mod.rs +++ b/crates/implementations/src/primitives/mod.rs @@ -6,16 +6,16 @@ use crate::{ }, }; use proc::Primitive; -use rt_core::{Float, Hit, Primitive, Ray, Scatter, SurfaceIntersection, Vec2, Vec3}; +use rt_core::*; pub mod sphere; pub mod triangle; -#[derive(Primitive, Debug)] -pub enum AllPrimitives { - Sphere(Sphere), - Triangle(Triangle), - MeshTriangle(MeshTriangle), +#[derive(Primitive, Debug, Clone)] +pub enum AllPrimitives<'a, M: Scatter> { + Sphere(Sphere<'a, M>), + Triangle(Triangle<'a, M>), + MeshTriangle(MeshTriangle<'a, M>), } #[derive(Clone, Debug)] diff --git a/implementations/src/primitives/sphere.rs b/crates/implementations/src/primitives/sphere.rs similarity index 88% rename from implementations/src/primitives/sphere.rs rename to crates/implementations/src/primitives/sphere.rs index 1e1be9c..3fee377 100644 --- a/implementations/src/primitives/sphere.rs +++ b/crates/implementations/src/primitives/sphere.rs @@ -2,34 +2,35 @@ use crate::{ aabb::{AABound, AABB}, utility::{coord::Coordinate, random_float}, }; -use rt_core::{Float, Hit, Primitive, Ray, Scatter, SurfaceIntersection, Vec2, Vec3, EPSILON, PI}; -use std::sync::Arc; -#[derive(Debug)] -pub struct Sphere { +use rt_core::*; + +#[derive(Debug, Clone)] +pub struct Sphere<'a, M: Scatter> { pub center: Vec3, pub radius: Float, - pub material: Arc, + pub material: &'a M, } -impl Sphere +impl<'a, M> Sphere<'a, M> where M: Scatter, { - pub fn new(center: Vec3, radius: Float, material: &Arc) -> Self { + pub fn new(center: Vec3, radius: Float, material: &'a M) -> Self { Sphere { center, radius, - material: material.clone(), + material, } } } #[allow(clippy::suspicious_operation_groupings)] -impl Primitive for Sphere +impl<'a, M> Primitive for Sphere<'a, M> where M: Scatter, { + type Material = M; fn get_int(&self, ray: &Ray) -> Option> { let dir = ray.direction; let center = self.center; @@ -96,7 +97,7 @@ where normal, self.get_uv(point), out, - &self.material, + self.material, )) } else { None @@ -144,12 +145,11 @@ where // get sphere point let coord_system = Coordinate::new_from_z((in_point - self.center).normalised()); - let mut vec = Vec3::new(sin_alpha * phi.cos(), sin_alpha * phi.sin(), cos_alpha); - coord_system.vec_to_coordinate(&mut vec); + let vec = Vec3::new(sin_alpha * phi.cos(), sin_alpha * phi.sin(), cos_alpha); + let vec = coord_system.to_coord(vec); self.center + self.radius * vec }; - (point - in_point).normalised() } fn scattering_pdf(&self, hit_point: Vec3, wi: Vec3, sampled_hit: &Hit) -> Float { @@ -172,7 +172,7 @@ where } } -impl AABound for Sphere { +impl<'a, M: Scatter> AABound for Sphere<'a, M> { fn get_aabb(&self) -> AABB { AABB::new( self.center - self.radius * Vec3::one(), diff --git a/implementations/src/primitives/triangle.rs b/crates/implementations/src/primitives/triangle.rs similarity index 78% rename from implementations/src/primitives/triangle.rs rename to crates/implementations/src/primitives/triangle.rs index 431d0cd..f82573b 100644 --- a/implementations/src/primitives/triangle.rs +++ b/crates/implementations/src/primitives/triangle.rs @@ -4,52 +4,52 @@ use crate::{ utility::{check_side, gamma}, }; use rand::{thread_rng, Rng}; -use rt_core::{Float, Hit, Primitive, Ray, Scatter, SurfaceIntersection, Vec2, Vec3}; +use rt_core::*; use std::sync::Arc; #[derive(Clone, Debug)] -pub struct Triangle { +pub struct Triangle<'a, M: Scatter> { pub points: [Vec3; 3], pub normals: [Vec3; 3], - pub material: Arc, + pub material: &'a M, } -impl Triangle +impl<'a, M> Triangle<'a, M> where M: Scatter, { - pub fn new(points: [Vec3; 3], normals: [Vec3; 3], material: &Arc) -> Self { + pub fn new(points: [Vec3; 3], normals: [Vec3; 3], material: &'a M) -> Self { Triangle { points, normals, - material: material.clone(), + material, } } } -#[derive(Debug)] -pub struct MeshTriangle { +#[derive(Debug, Clone)] +pub struct MeshTriangle<'a, M: Scatter> { pub point_indices: [usize; 3], pub normal_indices: [usize; 3], - pub material: Arc, + pub material: &'a M, pub mesh: Arc, } -impl MeshTriangle +impl<'a, M> MeshTriangle<'a, M> where M: Scatter, { pub fn new( point_indices: [usize; 3], normal_indices: [usize; 3], - material: &Arc, - mesh: &Arc, + material: &'a M, + mesh: Arc, ) -> Self { MeshTriangle { point_indices, normal_indices, - material: material.clone(), - mesh: mesh.clone(), + material, + mesh, } } } @@ -66,13 +66,13 @@ impl MeshData { } } -pub trait TriangleTrait { +pub trait TriangleTrait<'a, M: Scatter> { fn get_point(&self, index: usize) -> Vec3; fn get_normal(&self, index: usize) -> Vec3; - fn get_material(&self) -> &Arc; + fn get_material(&self) -> &'a M; } -impl TriangleTrait for Triangle +impl<'a, M> TriangleTrait<'a, M> for Triangle<'a, M> where M: Scatter, { @@ -82,30 +82,30 @@ where fn get_normal(&self, index: usize) -> Vec3 { self.normals[index] } - fn get_material(&self) -> &Arc { - &self.material + fn get_material(&self) -> &'a M { + self.material } } -impl TriangleTrait for MeshTriangle +impl<'a, M> TriangleTrait<'a, M> for MeshTriangle<'a, M> where M: Scatter, { fn get_point(&self, index: usize) -> Vec3 { - (*self.mesh).vertices[self.point_indices[index]] + self.mesh.vertices[self.point_indices[index]] } fn get_normal(&self, index: usize) -> Vec3 { - (*self.mesh).normals[self.normal_indices[index]] + self.mesh.normals[self.normal_indices[index]] } - fn get_material(&self) -> &Arc { - &self.material + fn get_material(&self) -> &'a M { + self.material } } -pub fn triangle_intersection, M: Scatter>( - triangle: &T, +pub fn triangle_intersection<'a, T: TriangleTrait<'a, M>, M: Scatter>( + triangle: &'a T, ray: &Ray, -) -> Option> { +) -> Option> { let mut p0t = triangle.get_point(0) - ray.origin; let mut p1t = triangle.get_point(1) - ray.origin; let mut p2t = triangle.get_point(2) - ray.origin; @@ -215,10 +215,11 @@ pub fn triangle_intersection, M: Scatter>( )) } -impl Primitive for Triangle +impl<'a, M> Primitive for Triangle<'a, M> where M: Scatter, { + type Material = M; fn get_int(&self, ray: &Ray) -> Option> { triangle_intersection(self, ray) } @@ -246,19 +247,20 @@ where } } -impl Primitive for MeshTriangle +impl<'a, M> Primitive for MeshTriangle<'a, M> where M: Scatter, { + type Material = M; fn get_int(&self, ray: &Ray) -> Option> { triangle_intersection(self, ray) } fn area(&self) -> Float { - 0.5 * ((*self.mesh).vertices[self.point_indices[1]] - - (*self.mesh).vertices[self.point_indices[0]]) + 0.5 * (self.mesh.vertices[self.point_indices[1]] + - self.mesh.vertices[self.point_indices[0]]) .cross( - (*self.mesh).vertices[self.point_indices[2]] - - (*self.mesh).vertices[self.point_indices[0]], + self.mesh.vertices[self.point_indices[2]] + - self.mesh.vertices[self.point_indices[0]], ) .mag() } @@ -267,9 +269,9 @@ where let uv = rng.gen::().sqrt(); let uv = (1.0 - uv, uv * rng.gen::().sqrt()); - let point = uv.0 * (*self.mesh).vertices[self.point_indices[0]] - + uv.1 * (*self.mesh).vertices[self.point_indices[1]] - + (1.0 - uv.0 - uv.1) * (*self.mesh).vertices[self.point_indices[2]]; + let point = uv.0 * self.mesh.vertices[self.point_indices[0]] + + uv.1 * self.mesh.vertices[self.point_indices[1]] + + (1.0 - uv.0 - uv.1) * self.mesh.vertices[self.point_indices[2]]; (point - in_point).normalised() } @@ -280,7 +282,7 @@ where self.material.is_light() } } -impl AABound for Triangle { +impl<'a, M: Scatter> AABound for Triangle<'a, M> { fn get_aabb(&self) -> AABB { AABB::new( self.points[0].min_by_component(self.points[1].min_by_component(self.points[2])), @@ -289,12 +291,12 @@ impl AABound for Triangle { } } -impl AABound for MeshTriangle { +impl<'a, M: Scatter> AABound for MeshTriangle<'a, M> { fn get_aabb(&self) -> AABB { let points = [ - (*self.mesh).vertices[self.point_indices[0]], - (*self.mesh).vertices[self.point_indices[1]], - (*self.mesh).vertices[self.point_indices[2]], + self.mesh.vertices[self.point_indices[0]], + self.mesh.vertices[self.point_indices[1]], + self.mesh.vertices[self.point_indices[2]], ]; AABB::new( diff --git a/rt_core/src/sampler.rs b/crates/implementations/src/samplers/mod.rs similarity index 63% rename from rt_core/src/sampler.rs rename to crates/implementations/src/samplers/mod.rs index 3e67281..c4e6953 100644 --- a/rt_core/src/sampler.rs +++ b/crates/implementations/src/samplers/mod.rs @@ -1,20 +1,22 @@ -use crate::{AccelerationStructure, Float, Primitive, Ray, Scatter, Vec3}; +use rt_core::*; -pub trait Sampler { - fn sample_image( +pub mod random_sampler; + +use clap::ValueEnum; + +pub trait Sampler: Sync { + fn sample_image( &self, _render_options: RenderOptions, _camera: &C, - _sky: &S, _acceleration_structure: &A, _update_function: Option<(&mut T, F)>, ) where - C: Camera + Send + Sync, - P: Primitive + Sync + Send + 'static, - M: Scatter + Send + Sync + 'static, - F: Fn(&mut T, &SamplerProgress, u64), - A: AccelerationStructure + Send + Sync, - S: NoHit + Send + Sync; + C: Camera, + P: Primitive, + M: Scatter, + F: Fn(&mut T, &SamplerProgress, u64) -> bool, + A: AccelerationStructure; } #[derive(Copy, Clone, Debug)] @@ -23,20 +25,22 @@ pub struct RenderOptions { pub render_method: RenderMethod, pub width: u64, pub height: u64, + pub gamma: Float, } impl Default for RenderOptions { fn default() -> Self { Self { - samples_per_pixel: 100, + samples_per_pixel: 128, render_method: RenderMethod::MIS, width: 1920, height: 1080, + gamma: 2.2, } } } -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, ValueEnum)] pub enum RenderMethod { Naive, MIS, @@ -58,10 +62,6 @@ impl SamplerProgress { } } -pub trait Camera { +pub trait Camera: Sync { fn get_ray(&self, u: Float, v: Float) -> Ray; } - -pub trait NoHit { - fn get_colour(&self, ray: &Ray) -> Vec3; -} diff --git a/implementations/src/samplers/random_sampler.rs b/crates/implementations/src/samplers/random_sampler.rs similarity index 65% rename from implementations/src/samplers/random_sampler.rs rename to crates/implementations/src/samplers/random_sampler.rs index 5c55e7a..a9aed17 100644 --- a/implementations/src/samplers/random_sampler.rs +++ b/crates/implementations/src/samplers/random_sampler.rs @@ -1,27 +1,24 @@ +use crate::integrators::*; +use crate::*; use rand::Rng; use rayon::prelude::*; -use rt_core::{ - AccelerationStructure, Camera, Float, NoHit, Primitive, Ray, RenderMethod, RenderOptions, - Sampler, SamplerProgress, Scatter, -}; +use rt_core::*; pub struct RandomSampler; impl Sampler for RandomSampler { - fn sample_image( + fn sample_image( &self, render_options: RenderOptions, camera: &C, - sky: &S, acceleration_structure: &A, mut presentation_update: Option<(&mut T, F)>, ) where - C: Camera + Send + Sync, - P: Primitive + Sync + Send + 'static, - M: Scatter + Send + Sync + 'static, - F: Fn(&mut T, &SamplerProgress, u64), - A: AccelerationStructure + Send + Sync, - S: NoHit + Send + Sync, + C: Camera, + P: Primitive, + M: Scatter, + F: Fn(&mut T, &SamplerProgress, u64) -> bool, + A: AccelerationStructure, { let channels = 3; let pixel_num = render_options.width * render_options.height; @@ -53,21 +50,22 @@ impl Sampler for RandomSampler { for chunk_pixel_i in 0..(chunk.len() / 3) { let pixel_i = chunk_pixel_i as u64 + pixel_chunk_size * chunk_i as u64; - let x = pixel_i as u64 % render_options.width; - let y = (pixel_i as u64 - x) / render_options.width; + let x = pixel_i % render_options.width; + let y = (pixel_i - x) / render_options.width; let u = (rng.gen_range(0.0..1.0) + x as Float) - / render_options.width as Float; + / (render_options.width - 1) as Float; let v = 1.0 - (rng.gen_range(0.0..1.0) + y as Float) - / render_options.height as Float; + / (render_options.height - 1) as Float; let mut ray = camera.get_ray(u, v); // remember to add le DOF let result = match render_options.render_method { - RenderMethod::Naive => { - Ray::get_colour_naive(&mut ray, sky, acceleration_structure) - } + RenderMethod::Naive => NaiveIntegrator::get_colour( + &mut ray, + acceleration_structure, + ), RenderMethod::MIS => { - Ray::get_colour(&mut ray, sky, acceleration_structure) + MisIntegrator::get_colour(&mut ray, acceleration_structure) } }; @@ -82,9 +80,10 @@ impl Sampler for RandomSampler { }); }); if i != 0 { - match presentation_update.as_mut() { - Some((ref mut data, f)) => f(data, previous, i), - None => (), + if let Some((ref mut data, f)) = presentation_update.as_mut() { + if f(data, previous, i) { + return; + } }; } } @@ -94,9 +93,8 @@ impl Sampler for RandomSampler { } else { (&accumulator_buffers.1, &mut accumulator_buffers.0) }; - match presentation_update.as_mut() { - Some((ref mut data, f)) => f(data, previous, render_options.samples_per_pixel), - None => (), + if let Some((ref mut data, f)) = presentation_update.as_mut() { + f(data, previous, render_options.samples_per_pixel); } } } diff --git a/crates/implementations/src/sky.rs b/crates/implementations/src/sky.rs new file mode 100644 index 0000000..1434630 --- /dev/null +++ b/crates/implementations/src/sky.rs @@ -0,0 +1,116 @@ +use crate::distributions::Distribution2D; +use crate::generate_values; +use crate::next_float; +use crate::random_float; +use rand::rngs::SmallRng; +use rand::thread_rng; +use rand::SeedableRng; +use rt_core::*; + +use crate::Texture; + +#[derive(Debug, Clone)] +pub struct Sky<'a, T: Texture, M: Scatter> { + texture: &'a T, + mat: &'a M, + pub distribution: Option, + sampler_res: (usize, usize), +} + +impl<'a, T: Texture, M: Scatter> Sky<'a, T, M> { + pub fn new(texture: &'a T, mat: &'a M, sampler_res: (usize, usize)) -> Self { + let values = generate_values(texture, sampler_res); + + let distribution = if sampler_res.0 | sampler_res.1 != 0 { + Some(Distribution2D::new(&values, sampler_res.0)) + } else { + None + }; + + Sky { + texture, + mat, + distribution, + sampler_res, + } + } +} + +impl<'a, T: Texture, M: Scatter> NoHit for Sky<'a, T, M> { + fn get_colour(&self, ray: &Ray) -> Vec3 { + self.texture.colour_value(ray.direction, ray.origin) + } + fn pdf(&self, wi: Vec3) -> Float { + let sin_theta = (1.0 - wi.z * wi.z).sqrt(); + if sin_theta <= 0.0 { + return 0.0; + } + let theta = wi.z.acos(); + let mut phi = (wi.y).atan2(wi.x); + + if phi < 0.0 { + phi += 2.0 * PI; + } + let u = phi / (2.0 * PI); + let v = theta / PI; + self.sampler_res.0 as Float + * self.sampler_res.1 as Float + * self.distribution.as_ref().unwrap().pdf(u, v) + / (sin_theta * TAU * PI) + } + fn can_sample(&self) -> bool { + self.sampler_res.0 | self.sampler_res.1 != 0 + } + fn sample(&self) -> Vec3 { + let uv = self + .distribution + .as_ref() + .unwrap() + .sample(&mut SmallRng::from_rng(thread_rng()).unwrap()); + + let u = next_float(uv.0 as Float + random_float()) / self.sampler_res.0 as Float; + let v = next_float(uv.1 as Float + random_float()) / self.sampler_res.1 as Float; + + let phi = u * 2.0 * PI; + let theta = v * PI; + + Vec3::from_spherical(theta.sin(), theta.cos(), phi.sin(), phi.cos()) + } + fn get_si(&self, _ray: &Ray) -> SurfaceIntersection { + SurfaceIntersection { + hit: Hit { + t: 0.0, + point: Vec3::zero(), + error: Vec3::zero(), + normal: Vec3::zero(), + uv: None, + out: false, + }, + material: self.mat, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + //use crate::spherical_sampling::test_spherical_pdf; + use crate::AllMaterials; + use crate::AllTextures; + use crate::Emit; + use crate::Lerp; + //use rand::rngs::ThreadRng; + + #[test] + fn sky_sampling() { + let tex = AllTextures::Lerp(Lerp::new(Vec3::zero(), Vec3::one())); + let mat = AllMaterials::Emit(Emit::new(&tex, 1.0)); + + let _sky = Sky::new(&tex, &mat, (60, 30)); + + /*let pdf = |outgoing: Vec3| sky.pdf(outgoing); + let sample = |_: &mut ThreadRng| sky.sample(); + test_spherical_pdf("lerp sky sampling", &pdf, &sample, false);*/ + todo!() + } +} diff --git a/crates/implementations/src/statistics/bxdfs/lambertian.rs b/crates/implementations/src/statistics/bxdfs/lambertian.rs new file mode 100644 index 0000000..0c82026 --- /dev/null +++ b/crates/implementations/src/statistics/bxdfs/lambertian.rs @@ -0,0 +1,49 @@ +use crate::coord::Coordinate; +use crate::statistics::*; +use rand::Rng; + +pub fn sample_local(_: Vec3, rng: &mut R) -> Vec3 { + let cos_theta = (1.0 - rng.gen::()).sqrt(); + let sin_theta = (1.0 - cos_theta * cos_theta).sqrt(); + let phi = 2.0 * PI * rng.gen::(); + Vec3::new(phi.cos() * sin_theta, phi.sin() * sin_theta, cos_theta) +} + +pub fn pdf_local(_: Vec3, outgoing: Vec3) -> Float { + outgoing.z.max(0.0) / PI +} + +pub fn sample(incoming: Vec3, normal: Vec3, rng: &mut R) -> Vec3 { + Coordinate::new_from_z(normal).to_coord(sample_local(incoming, rng)) +} + +pub fn pdf(_: Vec3, outgoing: Vec3, normal: Vec3) -> Float { + outgoing.dot(normal).max(0.0) / PI +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::statistics::spherical_sampling::*; + use rand::{rngs::ThreadRng, thread_rng}; + + #[test] + fn lambertian() { + let mut rng = thread_rng(); + let incoming = generate_wi(&mut rng); + let pdf = |outgoing: Vec3| pdf_local(incoming, outgoing); + let sample = |rng: &mut ThreadRng| sample_local(incoming, rng); + test_spherical_pdf("lambertian", &pdf, &sample, false); + } + + #[test] + fn non_local() { + let mut rng = thread_rng(); + let normal = random_unit_vector(&mut rng); + let to_local = Coordinate::new_from_z(normal); + let incoming = to_local.to_coord(generate_wi(&mut rng)); + let pdf = |outgoing: Vec3| pdf(incoming, outgoing, normal); + let sample = |rng: &mut ThreadRng| sample(incoming, normal, rng); + test_spherical_pdf("lambertian_nl", &pdf, &sample, false); + } +} diff --git a/crates/implementations/src/statistics/bxdfs/mod.rs b/crates/implementations/src/statistics/bxdfs/mod.rs new file mode 100644 index 0000000..bbb40db --- /dev/null +++ b/crates/implementations/src/statistics/bxdfs/mod.rs @@ -0,0 +1,13 @@ +pub mod lambertian; +pub mod trowbridge_reitz; +pub mod trowbridge_reitz_vndf; + +// All modules should adhere to the following: +// There should atleast provide the following functions +// sample(incoming, normal, ...) +// pdf(incoming, outgoing, normal, ...) +// If implementations of the above are provided in local space as well +// they must adhere to the same naming expect with _local and no normal parameter +// For the forementioned functions incoming will be pointing away from the surface +// outgoing will be pointing away from the surface and is the sampled direction +// Note that auxillary function do not have to adhere to the above diff --git a/crates/implementations/src/statistics/bxdfs/trowbridge_reitz.rs b/crates/implementations/src/statistics/bxdfs/trowbridge_reitz.rs new file mode 100644 index 0000000..9acb44a --- /dev/null +++ b/crates/implementations/src/statistics/bxdfs/trowbridge_reitz.rs @@ -0,0 +1,231 @@ +use crate::coord::Coordinate; +use crate::statistics::*; +use rand::Rng; + +pub fn sample_h(alpha: Float, rng: &mut R) -> Vec3 { + let r1: Float = rng.gen(); + let r2: Float = rng.gen(); + let cos_theta = ((1.0 - r1) / (r1 * (alpha * alpha - 1.0) + 1.0)).sqrt(); + let sin_theta = (1.0 - cos_theta * cos_theta).sqrt(); + let phi_s = (TAU * r2).max(0.0).min(TAU); + Vec3::new(phi_s.cos() * sin_theta, phi_s.sin() * sin_theta, cos_theta).normalised() +} + +pub fn d(alpha: Float, cos_theta: Float) -> Float { + if cos_theta <= 0.0 { + return 0.0; + } + let a_sq = alpha * alpha; + let tmp = cos_theta * cos_theta * (a_sq - 1.0) + 1.0; + a_sq / (PI * tmp * tmp) +} + +pub fn pdf_h(alpha: Float, h: Vec3) -> Float { + // technically the paper has an .abs() but it isn't needed since h.z < 0 => D(m) = 0 + d(alpha, h.z) * h.z +} + +pub fn sample_local(alpha: Float, incoming: Vec3, rng: &mut R) -> Vec3 { + let h = sample_h(alpha, rng); + incoming.reflected(h) +} + +pub fn pdf_local(alpha: Float, incoming: Vec3, outgoing: Vec3) -> Float { + let mut h = (outgoing + incoming).normalised(); + if h.z < 0.0 { + h = -h; + } + let d = d(alpha, h.z); + d * h.z.abs() / (4.0 * outgoing.dot(h).abs()) +} + +pub fn sample(alpha: Float, incoming: Vec3, normal: Vec3, rng: &mut R) -> Vec3 { + let coord = Coordinate::new_from_z(normal); + let local_h = sample_h(alpha, rng); + let h = coord.to_coord(local_h); + + incoming.reflected(h) +} + +pub fn pdf(alpha: Float, incoming: Vec3, outgoing: Vec3, normal: Vec3) -> Float { + let inverse = Coordinate::new_from_z(normal).create_inverse(); + let incoming = inverse.to_coord(incoming); + let outgoing = inverse.to_coord(outgoing); + let mut h = (outgoing + incoming).normalised(); + if h.z < 0.0 { + h = -h; + } + let d = d(alpha, h.z); + d * h.z.abs() / (4.0 * outgoing.dot(h).abs()) +} + +pub fn g2(alpha: Float, normal: Vec3, h: Vec3, incoming: Vec3, outgoing: Vec3) -> Float { + if incoming.dot(h) / incoming.dot(normal) <= 0.0 + || outgoing.dot(h) / outgoing.dot(normal) <= 0.0 + { + return 0.0; + } + + let alpha_sq = alpha * alpha; + let one_minus_alpha_sq = 1.0 - alpha_sq; + let cos_i = normal.dot(incoming); + let cos_i_sq = cos_i * cos_i; + let tmp_a = alpha_sq + one_minus_alpha_sq * cos_i_sq; + let cos_o = normal.dot(outgoing); + let cos_o_sq = cos_o * cos_o; + let tmp_b = alpha_sq + one_minus_alpha_sq * cos_o_sq; + 2.0 * cos_i * cos_o / (cos_o * tmp_a.sqrt() + cos_i * tmp_b.sqrt()) +} + +pub fn g1(alpha: Float, normal: Vec3, h: Vec3, v: Vec3) -> Float { + if v.dot(h) / v.dot(normal) <= 0.0 { + return 0.0; + } + let cos = normal.dot(v); + let cos_sq = cos * cos; + let alpha_sq = alpha * alpha; + let tmp = alpha_sq + (1.0 - alpha_sq) * cos_sq; + 2.0 * cos / (tmp.sqrt() + cos) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::statistics::spherical_sampling::*; + use rand::{rngs::ThreadRng, thread_rng, Rng}; + + #[test] + fn h() { + let mut rng = thread_rng(); + let alpha = rng.gen(); + let pdf = |outgoing: Vec3| pdf_h(alpha, outgoing); + let sample = |rng: &mut ThreadRng| sample_h(alpha, rng); + test_spherical_pdf("tr_h", &pdf, &sample, false); + } + + #[test] + fn tr() { + let mut rng = thread_rng(); + let incoming = -generate_wi(&mut rng); + let alpha = rng.gen(); + let pdf = |outgoing: Vec3| pdf_local(alpha, incoming, outgoing); + let sample = |rng: &mut ThreadRng| sample_local(alpha, incoming, rng); + test_spherical_pdf("tr", &pdf, &sample, false); + } + + #[test] + fn non_local() { + let mut rng = thread_rng(); + let normal = random_unit_vector(&mut rng); + let to_local = Coordinate::new_from_z(normal); + let incoming = to_local.to_coord(-generate_wi(&mut rng)); + let alpha = rng.gen(); + let pdf = |outgoing: Vec3| pdf(alpha, incoming, outgoing, normal); + let sample = |rng: &mut ThreadRng| sample(alpha, incoming, normal, rng); + test_spherical_pdf("tr_nl", &pdf, &sample, false); + } + + #[test] + fn g1_cos_test() { + let mut rng = thread_rng(); + let incoming = -generate_wi(&mut rng); + let cos_theta = incoming.z; + let alpha = rng.gen(); + let test = |h: Vec3| { + g1(alpha, Vec3::new(0.0, 0.0, 1.0), h, incoming) + * incoming.dot(h).max(0.0) + * d(alpha, h.z) + }; + + let integral = integrate_over_sphere(&test); + assert!((integral - cos_theta).abs() < 0.0001); + } + + #[test] + fn projected_area_test_local() { + let mut rng = thread_rng(); + let alpha = rng.gen(); + let test = |h: Vec3| d(alpha, h.z) * h.z; + let integral = integrate_over_sphere(&test); + assert!((integral - 1.0).abs() < 0.0001); + } + + #[test] + fn projected_area_test_non_local() { + let mut rng = thread_rng(); + let normal = random_unit_vector(&mut rng); + let alpha = rng.gen(); + let test = |h: Vec3| d(alpha, h.dot(normal)) * h.dot(normal); + let integral = integrate_over_sphere(&test); + assert!((integral - 1.0).abs() < 0.0001); + } + + #[test] + fn weak_furnace_test() { + let mut rng = thread_rng(); + let wo = -generate_wi(&mut rng); + + let alpha: Float = rng.gen(); + + let test = |wi: Vec3| { + let mut h = (wi + wo).normalised(); + if h.z < 0.0 { + h = -h; + } + let denom = 4.0 * wo.z.abs(); + if denom < 0.000000001 { + 0.0 + } else { + g1(alpha, Vec3::new(0.0, 0.0, 1.0), h, wo) * d(alpha, h.z) / denom + } + }; + + let integral = integrate_over_sphere(&test); + assert!((integral - 1.0).abs() < 0.0001); + } + + #[test] + fn g2_test() { + let mut rng = thread_rng(); + let a = -generate_wi(&mut rng); + let alpha = rng.gen(); + let test = |b: Vec3| { + let mut h = (a + b).normalised(); + if h.z < 0.0 { + h = -h; + } + let denom = 4.0 * a.z.abs(); + if denom < 0.000000001 { + 0.0 + } else { + g2(alpha, Vec3::new(0.0, 0.0, 1.0), h, a, b) * d(alpha, h.z) / denom + } + }; + + let integral = integrate_over_sphere(&test); + assert!(integral <= 1.0); + } + + #[test] + fn g2_test_non_local() { + let mut rng = thread_rng(); + let a = -generate_wi(&mut rng); + let normal = random_unit_vector(&mut rng); + let alpha = rng.gen(); + let test = |b: Vec3| { + let mut h = (a + b).normalised(); + if h.dot(normal) < 0.0 { + h = -h; + } + let denom = 4.0 * a.dot(-normal).abs(); + if denom < 0.000000001 { + 0.0 + } else { + g2(alpha, normal, h, a, b) * d(alpha, a.dot(-normal)) / denom + } + }; + + let integral = integrate_over_sphere(&test); + assert!(integral <= 1.0); + } +} diff --git a/crates/implementations/src/statistics/bxdfs/trowbridge_reitz_vndf.rs b/crates/implementations/src/statistics/bxdfs/trowbridge_reitz_vndf.rs new file mode 100644 index 0000000..583e36d --- /dev/null +++ b/crates/implementations/src/statistics/bxdfs/trowbridge_reitz_vndf.rs @@ -0,0 +1,219 @@ +use crate::coord::Coordinate; +use rand::Rng; +use rt_core::*; + +pub mod isotropic { + use super::*; + pub use crate::bxdfs::trowbridge_reitz::{d, g1, g2}; + + pub fn vndf(a: Float, h: Vec3, incoming: Vec3) -> Float { + if h.z < 0.0 { + return 0.0; + } + g1(a, Vec3::new(0.0, 0.0, 1.0), h, incoming) * incoming.dot(h).max(0.0) * d(a, h.z) + / incoming.z + } + + pub fn sample_vndf(a: Float, incoming: Vec3, rng: &mut R) -> Vec3 { + ansiotropic::sample_vndf(a, a, incoming, rng) + } + + pub fn sample_local(a: Float, incoming: Vec3, rng: &mut R) -> Vec3 { + let h = sample_vndf(a, incoming, rng); + incoming.reflected(h) + } + + pub fn pdf_local(alpha: Float, incoming: Vec3, outgoing: Vec3) -> Float { + let mut h = (outgoing + incoming).normalised(); + if h.z < 0.0 { + h = -h; + } + let vndf = vndf(alpha, h, incoming); + vndf / (4.0 * incoming.dot(h)) + } + + pub fn sample(a: Float, incoming: Vec3, normal: Vec3, rng: &mut R) -> Vec3 { + let coord = Coordinate::new_from_z(normal); + let inverse = coord.create_inverse(); + let h = coord.to_coord(sample_vndf(a, inverse.to_coord(incoming), rng)); + incoming.reflected(h) + } + + pub fn pdf(alpha: Float, incoming: Vec3, outgoing: Vec3, normal: Vec3) -> Float { + let inverse = Coordinate::new_from_z(normal).create_inverse(); + let incoming = inverse.to_coord(incoming); + let outgoing = inverse.to_coord(outgoing); + let mut h = (outgoing + incoming).normalised(); + if h.z < 0.0 { + h = -h; + } + let vndf = vndf(alpha, h, incoming); + vndf / (4.0 * incoming.dot(h)) + } +} +pub mod ansiotropic { + use super::*; + + pub fn d(a_x: Float, a_y: Float, h: Vec3) -> Float { + let tmp = h.x * h.x / (a_x * a_x) + h.y * h.y / (a_y * a_y) + h.z * h.z; + 1.0 / (PI * a_x * a_y * tmp * tmp) + } + + pub fn lambda(a_x: Float, a_y: Float, incoming: Vec3) -> Float { + let tmp = 1.0 + + (a_x * a_x * incoming.x * incoming.x + a_y * a_y * incoming.y * incoming.y) + / (incoming.z * incoming.z); + 0.5 * (tmp.sqrt() - 1.0) + } + + pub fn g1(a_x: Float, a_y: Float, incoming: Vec3) -> Float { + 1.0 / (1.0 + lambda(a_x, a_y, incoming)) + } + + pub fn vndf(a_x: Float, a_y: Float, h: Vec3, incoming: Vec3) -> Float { + if h.z < 0.0 { + return 0.0; + } + g1(a_x, a_y, incoming) * incoming.dot(h).max(0.0) * d(a_x, a_y, h) / incoming.z + } + + pub fn sample_vndf(a_x: Float, a_y: Float, incoming: Vec3, rng: &mut R) -> Vec3 { + let v_hemisphere = Vec3::new(a_x * incoming.x, a_y * incoming.y, incoming.z).normalised(); + + let len_sq = v_hemisphere.x * v_hemisphere.x + v_hemisphere.y * v_hemisphere.y; + + let basis_two = if len_sq > 0.0 { + Vec3::new(-v_hemisphere.y, v_hemisphere.x, 0.0) / len_sq.sqrt() + } else { + Vec3::new(1.0, 0.0, 0.0) + }; + let basis_three = v_hemisphere.cross(basis_two); + + let r = rng.gen::().sqrt(); + let phi = TAU * rng.gen::(); + let mut t = r * Vec2::new(phi.cos(), phi.sin()); + let s = 0.5 * (1.0 + v_hemisphere.z); + t.y = (1.0 - s) * (1.0 - t.x * t.x).sqrt() + s * t.y; + + let h_hemisphere = t.x * basis_two + + t.y * basis_three + + (1.0 - t.x * t.x - t.y * t.y).max(0.0).sqrt() * v_hemisphere; + + Vec3::new( + a_x * h_hemisphere.x, + a_y * h_hemisphere.y, + h_hemisphere.z.max(0.0), + ) + .normalised() + } + + pub fn sample_local(a_x: Float, a_y: Float, incoming: Vec3, rng: &mut R) -> Vec3 { + let h = sample_vndf(a_x, a_y, incoming, rng); + incoming.reflected(h) + } + + pub fn pdf_local(a_x: Float, a_y: Float, incoming: Vec3, outgoing: Vec3) -> Float { + let mut h = (outgoing + incoming).normalised(); + if h.z < 0.0 { + h = -h; + } + let vndf = vndf(a_x, a_y, h, incoming); + vndf / (4.0 * incoming.dot(h)) + } + + pub fn sample( + a_x: Float, + a_y: Float, + incoming: Vec3, + normal: Vec3, + rng: &mut R, + ) -> Vec3 { + let coord = Coordinate::new_from_z(normal); + let inverse = coord.create_inverse(); + let h = coord.to_coord(sample_vndf(a_x, a_y, inverse.to_coord(incoming), rng)); + incoming.reflected(h) + } + + pub fn pdf(a_x: Float, a_y: Float, incoming: Vec3, outgoing: Vec3, normal: Vec3) -> Float { + let inverse = Coordinate::new_from_z(normal).create_inverse(); + let incoming = inverse.to_coord(incoming); + let outgoing = inverse.to_coord(outgoing); + let mut h = (outgoing + incoming).normalised(); + if h.z < 0.0 { + h = -h; + } + let vndf = vndf(a_x, a_y, h, incoming); + vndf / (4.0 * incoming.dot(h)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::statistics::spherical_sampling::*; + use rand::{rngs::ThreadRng, thread_rng, Rng}; + + #[test] + fn isotropic_h() { + let mut rng = thread_rng(); + let incoming = -generate_wi(&mut rng); + let alpha = rng.gen(); + let pdf = |outgoing: Vec3| isotropic::vndf(alpha, outgoing, incoming); + let sample = |rng: &mut ThreadRng| isotropic::sample_vndf(alpha, incoming, rng); + test_spherical_pdf("iso_tr_vndf_h", &pdf, &sample, false); + } + + #[test] + fn isotropic() { + let mut rng = thread_rng(); + let incoming = -generate_wi(&mut rng); + let alpha = rng.gen(); + let pdf = |outgoing: Vec3| isotropic::pdf_local(alpha, incoming, outgoing); + let sample = |rng: &mut ThreadRng| isotropic::sample_local(alpha, incoming, rng); + test_spherical_pdf("iso_tr_vndf", &pdf, &sample, false); + } + + #[test] + fn isotropic_non_local() { + let mut rng = thread_rng(); + let normal = random_unit_vector(&mut rng); + let to_local = Coordinate::new_from_z(normal); + let incoming = to_local.to_coord(-generate_wi(&mut rng)); + let alpha = rng.gen(); + let pdf = |outgoing: Vec3| isotropic::pdf(alpha, incoming, outgoing, normal); + let sample = |rng: &mut ThreadRng| isotropic::sample(alpha, incoming, normal, rng); + test_spherical_pdf("iso_tr_vndf_nl", &pdf, &sample, false); + } + + #[test] + fn ansiotropic_h() { + let mut rng = thread_rng(); + let incoming = -generate_wi(&mut rng); + let (a_x, a_y) = (rng.gen(), rng.gen()); + let pdf = |outgoing: Vec3| ansiotropic::vndf(a_x, a_y, outgoing, incoming); + let sample = |rng: &mut ThreadRng| ansiotropic::sample_vndf(a_x, a_y, incoming, rng); + test_spherical_pdf("ansio_tr_vndf_h", &pdf, &sample, false); + } + + #[test] + fn ansiotropic() { + let mut rng = thread_rng(); + let incoming = -generate_wi(&mut rng); + let (a_x, a_y) = (rng.gen(), rng.gen()); + let pdf = |outgoing: Vec3| ansiotropic::pdf_local(a_x, a_y, incoming, outgoing); + let sample = |rng: &mut ThreadRng| ansiotropic::sample_local(a_x, a_y, incoming, rng); + test_spherical_pdf("ansio_tr_vndf", &pdf, &sample, false); + } + + #[test] + fn ansiotropic_non_local() { + let mut rng = thread_rng(); + let normal = random_unit_vector(&mut rng); + let to_local = Coordinate::new_from_z(normal); + let incoming = to_local.to_coord(-generate_wi(&mut rng)); + let (a_x, a_y) = (rng.gen(), rng.gen()); + let pdf = |outgoing: Vec3| ansiotropic::pdf(a_x, a_y, incoming, outgoing, normal); + let sample = |rng: &mut ThreadRng| ansiotropic::sample(a_x, a_y, incoming, normal, rng); + test_spherical_pdf("ansio_tr_vndf_nl", &pdf, &sample, false); + } +} diff --git a/crates/implementations/src/statistics/chi_squared.rs b/crates/implementations/src/statistics/chi_squared.rs new file mode 100644 index 0000000..d83d7ab --- /dev/null +++ b/crates/implementations/src/statistics/chi_squared.rs @@ -0,0 +1,76 @@ +use crate::statistics::*; +use rayon::prelude::*; +use statrs::function::gamma::gamma_ur; + +use std::cmp::Ordering::*; +pub fn chi2_probability(dof: f64, distance: f64) -> f64 { + match distance.partial_cmp(&0.0).unwrap() { + Less => panic!("distance < 0.0"), + Equal => 1.0, + Greater => { + if distance.is_infinite() { + 0.0 + } else { + gamma_ur(dof * 0.5, distance * 0.5) + } + } + } +} + +pub fn chi_squared( + freq_table: &[Float], + expected_freq_table: &[Float], + samples: usize, +) -> (usize, Float) { + assert_eq!(freq_table.len(), expected_freq_table.len()); + + let mut values = expected_freq_table + .into_par_iter() + .zip(freq_table.into_par_iter()) + .collect::>(); + + values.sort_by(|a, b| a.0.partial_cmp(b.0).unwrap()); + + let mut df = 0; + + let mut expected_pooled = 0.0; + let mut actual_pooled = 0.0; + + let mut chi_squared = 0.0; + + for (expected, actual) in values { + if *expected == 0.0 { + if *actual > (samples / 100_000) as Float { + chi_squared += INFINITY as Float; + } + } else if expected_pooled > 5.0 { + // prevent df = 0 when all values are less than 5 + let diff = actual_pooled - expected_pooled; + chi_squared += diff * diff / expected_pooled; + df += 1; + } else if *expected < 5.0 || (expected_pooled > 0.0 && expected_pooled < 5.0) { + expected_pooled += expected; + actual_pooled += actual; + } else { + let diff = actual - expected; + chi_squared += diff * diff / expected; + df += 1; + } + } + + if actual_pooled > 0.0 || expected_pooled > 0.0 { + let diff = actual_pooled - expected_pooled; + chi_squared += diff * diff / expected_pooled; + df += 1; + } + + df -= 1; + + (df, chi_squared) +} + +#[cfg(test)] +pub mod test {} + +#[cfg(test)] +pub use test::*; diff --git a/crates/implementations/src/statistics/distributions.rs b/crates/implementations/src/statistics/distributions.rs new file mode 100644 index 0000000..58f0bfc --- /dev/null +++ b/crates/implementations/src/statistics/distributions.rs @@ -0,0 +1,301 @@ +use rt_core::Float; + +use rand::Rng; + +#[derive(Debug, Clone, PartialEq)] +pub struct Distribution1D { + pub pdf: Vec, + pub cdf: Vec, +} + +impl Distribution1D { + pub fn new(values: &[Float]) -> Self { + if values.is_empty() { + panic!("Empty pdf passed to Distribution1D::from_pdf!"); + } + + let n = values.len(); + + let mut intervals = vec![0.0]; + + for i in 1..=n { + let last = intervals[i - 1]; + intervals.push(last + values[i - 1] as Float); + } + + let c = intervals[n]; + for (_, value) in intervals.iter_mut().enumerate() { + if c != 0.0 { + *value /= c as Float; + } + } + + let mut pdf = Vec::new(); + let mut last = 0.0; + for value in &intervals[1..] { + pdf.push(value - last); + last = *value; + } + + Self { + pdf, + cdf: intervals, + } + } + + pub fn sample_naive(&self, rng: &mut R) -> usize { + let threshold = rng.gen(); + + self.cdf.iter().position(|v| v >= &threshold).unwrap() - 1 + } + pub fn sample(&self, rng: &mut R) -> usize { + let num = rng.gen(); + + let pred = |i| self.cdf[i] <= num; + + { + let mut first = 0; + let mut len = self.cdf.len(); + while len > 0 { + let half = len >> 1; + let middle = first + half; + + if pred(middle) { + first = middle + 1; + len -= half + 1; + } else { + len = half; + } + } + (first - 1).clamp(0, self.cdf.len() - 2) + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Distribution2D { + pub x_distributions: Vec, + pub y_distribution: Distribution1D, + pub dim: (usize, usize), +} + +impl Distribution2D { + pub fn new(values: &[Float], width: usize) -> Self { + assert!(values.len() % width == 0 && !values.is_empty()); + let mut y_values = Vec::new(); + let mut x_distributions = Vec::new(); + for vec_x in values.chunks_exact(width) { + x_distributions.push(Distribution1D::new(vec_x)); + let row_sum: Float = vec_x.iter().sum(); + y_values.push(row_sum); + } + let y_distribution = Distribution1D::new(&y_values); + + Self { + x_distributions, + y_distribution, + dim: (width, values.len() / width), + } + } + pub fn sample(&self, rng: &mut R) -> (usize, usize) { + let v = self.y_distribution.sample(rng); + let u = self.x_distributions[v].sample(rng); + (u, v) + } + pub fn pdf(&self, u: Float, v: Float) -> Float { + let u = ((self.dim.0 as Float * u) as usize).clamp(0, self.dim.0 - 1); + let v = ((self.dim.1 as Float * v) as usize).clamp(0, self.dim.1 - 1); + + self.y_distribution.pdf[v] * self.x_distributions[v].pdf[u] + } + pub fn dim(&self) -> (usize, usize) { + self.dim + } +} + +#[cfg(test)] +mod tests { + use crate::statistics::{chi_squared::*, distributions::*, utility::*}; + use rand::thread_rng; + use rayon::prelude::*; + + macro_rules! random_1d { + ($len:expr) => {{ + const SAMPLES: usize = 100_000; + const SAMPLE_LEN: usize = 100; + const NUMBER_TESTS: usize = 10; + const CHI_SQUARED_THRESHOLD: Float = 0.01; + const BATCH_EXPONENT: usize = 6; + const BATCHES: usize = 2usize.pow(BATCH_EXPONENT as u32); + + let mut rng = thread_rng(); + + let values: Vec = (0..SAMPLE_LEN) + .into_iter() + .map(|_| rng.gen_range(0.0..100.0)) + .collect(); + + let cdf = Distribution1D::new(&values); + + let expected_values: Vec = cdf.pdf.iter().map(|v| v * SAMPLES as Float).collect(); + + let func = |samples| { + const SAMPLE_LEN_MINUS_ONE: usize = SAMPLE_LEN - 1; + let mut rng = thread_rng(); + let mut sampled_values = vec![0u64; SAMPLE_LEN]; + for _ in 0..samples { + match cdf.sample(&mut rng) { + index @ 0..=SAMPLE_LEN_MINUS_ONE => { + sampled_values[index] += 1; + } + _ => unreachable!(), + } + } + sampled_values + }; + + // Šidák correction + let threshold = 1.0 - (1.0 - CHI_SQUARED_THRESHOLD).powf(1.0 / NUMBER_TESTS as Float); + + for i in 0..NUMBER_TESTS { + let sampled_vecs: Vec> = (0..BATCHES) + .map(|_| { + distribute_samples_over_threads(SAMPLES as u64, &func) + .into_iter() + .map(|v| v as Float) + .collect() + }) + .collect(); + let sampled_values: Vec = (0..SAMPLE_LEN) + .into_par_iter() + .map(|i| { + recursively_binary_average((0..BATCHES).map(|j| sampled_vecs[j][i]).collect()) + }) + .collect(); + + let (df, chi_squared) = chi_squared(&sampled_values, &expected_values, SAMPLES); + + let p_value = chi2_probability(df as f64, chi_squared as f64); + if p_value < threshold as f64 { + panic!("LERP: recieved p value of {p_value} with {SAMPLES} samples on test {i}/{NUMBER_TESTS}") + } + }} + }; + } + + #[test] + fn random_1d_small() { + random_1d!(50) + } + + #[test] + fn random_1d_medium() { + random_1d!(500) + } + #[test] + fn random_1d_large() { + random_1d!(5000) + } + + macro_rules! random_2d { + ($x:expr, $y:expr) => { { + const SAMPLES: usize = 100_000; + const X_RES_MINUS_ONE: usize = $x - 1; + const Y_RES_MINUS_ONE: usize = $y - 1; + const SAMPLES_RES: (usize, usize) = (X_RES_MINUS_ONE + 1, Y_RES_MINUS_ONE + 1); + const SAMPLE_LEN: usize = SAMPLES_RES.0 * SAMPLES_RES.1; + const NUMBER_TESTS: usize = 10; + const CHI_SQUARED_THRESHOLD: Float = 0.01; + const BATCH_EXPONENT: usize = 6; + const BATCHES: usize = 2usize.pow(BATCH_EXPONENT as u32); + + //use rand_core::SeedableRng; + + //let mut rng = SmallRng::seed_from_u64(321321); + let mut rng = thread_rng(); + + let values: Vec = (0..SAMPLE_LEN) + .into_iter() + .map(|_| rng.gen_range(0.0..100.0)) + .collect(); + + let dist = Distribution2D::new(&values, SAMPLES_RES.0); + + let mut expected_values: Vec = Vec::new(); + for y in 0..SAMPLES_RES.1 { + for x in 0..SAMPLES_RES.0 { + expected_values.push( + dist.y_distribution.pdf[y] * dist.x_distributions[y].pdf[x] * SAMPLES as Float, + ); + } + } + + // dist.pdf.iter().map(|v| v * SAMPLES as Float).collect(); + + let func = |samples| { + const SAMPLE_LEN_MINUS_ONE: usize = SAMPLE_LEN - 1; + let mut rng = thread_rng(); + let mut sampled_values = vec![0u64; SAMPLE_LEN]; + for _ in 0..samples { + match dist.sample(&mut rng) { + indices @ (0..=X_RES_MINUS_ONE, 0..=Y_RES_MINUS_ONE) + if indices.0 + (X_RES_MINUS_ONE + 1) * indices.1 < SAMPLE_LEN => + { + sampled_values[indices.0 + indices.1 * (X_RES_MINUS_ONE + 1)] += 1; + } + _ => unreachable!(), + } + } + sampled_values + }; + + // Šidák correction + let threshold = 1.0 - (1.0 - CHI_SQUARED_THRESHOLD).powf(1.0 / NUMBER_TESTS as Float); + + for i in 0..NUMBER_TESTS { + let sampled_vecs: Vec> = (0..BATCHES) + .into_iter() + .map(|_| { + distribute_samples_over_threads(SAMPLES as u64, &func) + .into_iter() + .map(|v| v as Float) + .collect() + }) + .collect(); + let sampled_values: Vec = (0..SAMPLE_LEN) + .into_par_iter() + .map(|i| { + recursively_binary_average( + (0..BATCHES) + .into_iter() + .map(|j| sampled_vecs[j][i]) + .collect(), + ) + }) + .collect(); + + let (df, chi_squared) = chi_squared(&sampled_values, &expected_values, SAMPLES); + + let p_value = chi2_probability(df as f64, chi_squared as f64); + if p_value < threshold as f64 { + panic!("LERP: recieved p value of {p_value} with {SAMPLES} samples on test {i}/{NUMBER_TESTS}") + } + } + }}; + } + + #[test] + fn random_2d_small() { + random_2d!(3, 3) + } + + #[test] + fn random_2d_medium() { + random_2d!(30, 60) + } + + #[test] + fn random_2d_large() { + random_2d!(800, 1200) + } +} diff --git a/crates/implementations/src/statistics/integrators.rs b/crates/implementations/src/statistics/integrators.rs new file mode 100644 index 0000000..90bf5c1 --- /dev/null +++ b/crates/implementations/src/statistics/integrators.rs @@ -0,0 +1,76 @@ +use rt_core::{Float, Vec3}; +const MAX_DEPTH: usize = 6; +const EPSILON: Float = 0.000001; + +pub fn integrate_solid_angle( + pdf: &F, + theta_start: Float, + theta_end: Float, + phi_start: Float, + phi_end: Float, +) -> Float +where + F: Fn(Vec3) -> Float, +{ + let pdf = |phi: Float, a, b| { + adaptive_simpsons( + |theta| { + pdf(Vec3::new( + phi.cos() * theta.sin(), + phi.sin() * theta.sin(), + theta.cos(), + )) * theta.sin().abs() + }, + a, + b, + ) + }; + + adaptive_simpsons(|phi| pdf(phi, theta_start, theta_end), phi_start, phi_end) +} + +pub fn adaptive_simpsons(function: F, a: Float, b: Float) -> Float +where + F: Fn(Float) -> Float, +{ + fn aux( + function: &F, + a: Float, + b: Float, + c: Float, + fa: Float, + fb: Float, + fc: Float, + i: Float, + epsilon: Float, + depth: usize, + ) -> Float + where + F: Fn(Float) -> Float, + { + let d = 0.5 * (a + b); + let e = 0.5 * (b + c); + let fd = function(d); + let fe = function(e); + + let h = c - a; + let i0 = (1.0 / 12.0) * h * (fa + 4.0 * fd + fb); + let i1 = (1.0 / 12.0) * h * (fb + 4.0 * fe + fc); + let ip = i0 + i1; + + if depth >= MAX_DEPTH || (ip - i).abs() < 15.0 * epsilon { + return ip + (1.0 / 15.0) * (ip - i); + } + + aux(function, a, d, b, fa, fd, fb, i0, 0.5 * epsilon, depth + 1) + + aux(function, b, e, c, fb, fe, fc, i1, 0.5 * epsilon, depth + 1) + } + let c = b; + let b = 0.5 * (a + b); + + let fa = function(a); + let fb = function(b); + let fc = function(c); + let i = (c - a) * (1.0 / 6.0) * (fa + 4.0 * fb + fc); + aux(&function, a, b, c, fa, fb, fc, i, EPSILON, 0) +} diff --git a/crates/implementations/src/statistics/mod.rs b/crates/implementations/src/statistics/mod.rs new file mode 100644 index 0000000..ef4a189 --- /dev/null +++ b/crates/implementations/src/statistics/mod.rs @@ -0,0 +1,72 @@ +use rt_core::{self, *}; + +pub mod bxdfs; +pub mod chi_squared; +pub mod distributions; +pub mod integrators; +pub mod spherical_sampling; + +pub mod utility { + + use rayon::prelude::*; + use std::ops::*; + + pub fn distribute_samples_over_threads(samples: u64, f: &F) -> Vec + where + T: Add + Send, + F: Fn(u64) -> Vec + Sync, + Vec: FromIterator<::Output>, + { + let thread_count = num_cpus::get(); + let mut samples_per_thread = vec![samples / thread_count as u64; thread_count]; + let diff = ((samples / thread_count as u64) * thread_count as u64) - samples; + let last = samples_per_thread.len() - 1; + samples_per_thread[last] += diff; + + samples_per_thread + .into_par_iter() + .map(f) + .reduce_with(|a, b| { + a.into_iter() + .zip(b.into_iter()) + .map(|(a, b)| a + b) + .collect() + }) + .unwrap() + } + + use rt_core::Float; + + pub fn recursively_binary_average(mut values: Vec) -> T + where + ::Output: Mul, + T: Copy, + Vec: FromIterator<<::Output as Mul>::Output>, + { + let mut len = values.len(); + if len & (len - 1) != 0 && len != 0 { + panic!("values.len() is not a power of 2"); + } + while len != 1 { + len /= 2; + + let (a, b) = values.split_at(len); + + values = a + .iter() + .zip(b.iter()) + .map(|(&a, &b)| (a + b) * 0.5) + .collect(); + } + + values[0] + } + + #[cfg(test)] + #[test] + fn binary_average() { + assert!( + (recursively_binary_average::(vec![1.0, 3.0, 7.0, 1.0]) - 3.0).abs() < 0.0000001 + ); + } +} diff --git a/crates/implementations/src/statistics/spherical_sampling.rs b/crates/implementations/src/statistics/spherical_sampling.rs new file mode 100644 index 0000000..df8ba45 --- /dev/null +++ b/crates/implementations/src/statistics/spherical_sampling.rs @@ -0,0 +1,256 @@ +use crate::statistics::chi_squared::*; +use crate::statistics::integrators::integrate_solid_angle; +use crate::statistics::utility::*; +use crate::statistics::*; +use rand::rngs::ThreadRng; +use rand::thread_rng; +use rand::Rng; +use rayon::prelude::*; + +pub fn cosine_hemisphere_sampling(rng: &mut R) -> Vec3 { + let cos_theta = (1.0 - rng.gen::()).sqrt(); + let sin_theta = (1.0 - cos_theta * cos_theta).sqrt(); + let phi = 2.0 * PI * rng.gen::(); + Vec3::new(phi.cos() * sin_theta, phi.sin() * sin_theta, cos_theta) +} + +pub fn cosine_hemisphere_pdf(wo: Vec3) -> Float { + wo.z.max(0.0) / PI +} + +pub fn specular_sampling(n: Float, rng: &mut R) -> Vec3 { + let a = rng.gen::().powf(1.0 / (n + 1.0)); + let term = (1.0 - a * a).sqrt(); + let phi = 2.0 * PI * rng.gen::(); + Vec3::new(term * phi.cos(), term * phi.sin(), a) +} + +pub fn random_unit_vector(rng: &mut R) -> Vec3 { + let (mut x, mut y, mut z) = (1.0, 1.0, 1.0); + while x * x + y * y + z * z > 1.0 { + x = rng.gen_range(-1.0..1.0); + y = rng.gen_range(-1.0..1.0); + z = rng.gen_range(-1.0..1.0); + } + + Vec3::new(x, y, z).normalised() +} + +pub fn integrate_over_sphere Float>(function: &F) -> Float { + let mut values = Vec::new(); + + const THETA_RES: usize = 80; + const PHI_RES: usize = 160; + + let theta_step = PI / THETA_RES as Float; + let phi_step = TAU / PHI_RES as Float; + for phi_i in 0..PHI_RES { + for theta_i in 0..THETA_RES { + let theta_start = theta_i as Float * theta_step; + let phi_start = phi_i as Float * phi_step; + values.push(integrate_solid_angle( + function, + theta_start, + theta_start + theta_step, + phi_start, + phi_start + phi_step, + )); + } + } + + values.iter().sum::() +} + +pub fn test_spherical_pdf(name: &str, pdf: &P, sample: &S, hemisphere: bool) +where + P: Fn(Vec3) -> Float, + S: Fn(&mut ThreadRng) -> Vec3 + Send + Sync, +{ + const THETA_RES: usize = 80; + const PHI_RES: usize = 160; + const SAMPLES: usize = 100_000; + const SAMPLE_LEN: usize = THETA_RES * PHI_RES; + const NUMBER_TESTS: usize = 10; + const CHI_SQUARED_THRESHOLD: Float = 0.01; + const BATCH_EXPONENT: usize = 6; + const BATCHES: usize = 2usize.pow(BATCH_EXPONENT as u32); + + let mut expected_values = Vec::new(); + + let theta_step = if hemisphere { FRAC_PI_2 } else { PI } / THETA_RES as Float; + let phi_step = TAU / PHI_RES as Float; + for phi_i in 0..PHI_RES { + for theta_i in 0..THETA_RES { + let theta_start = theta_i as Float * theta_step; + let phi_start = phi_i as Float * phi_step; + expected_values.push(integrate_solid_angle( + pdf, + theta_start, + theta_start + theta_step, + phi_start, + phi_start + phi_step, + )); + } + } + + let pdf_sum = expected_values.iter().sum::(); + if (pdf_sum - 1.0).abs() > 0.001 { + panic!("reference pdf doesn't integrate to 1: {pdf_sum}"); + } + + let mut expected_values: Vec = expected_values + .into_iter() + .map(|v| v * SAMPLES as Float) + .collect(); + + let func = |samples| { + const SAMPLE_LEN_MINUS_ONE: usize = SAMPLE_LEN - 1; + let mut rng = thread_rng(); + let mut sampled_values = vec![0u64; SAMPLE_LEN]; + for _ in 0..samples { + let wo = sample(&mut rng); + + let sin_theta = (1.0 - wo.z * wo.z).sqrt(); + if sin_theta < 0.0 { + panic!("sin_theta ({sin_theta}) < 0.0"); + } + let theta = wo.z.acos(); + let mut phi = (wo.y).atan2(wo.x); + if phi < 0.0 { + phi += 2.0 * PI; + } + let theta_i = theta / theta_step; + let phi_i = phi / phi_step; + let index = (phi_i as usize * THETA_RES + theta_i as usize).min(SAMPLE_LEN_MINUS_ONE); + + sampled_values[index] += 1; + } + sampled_values + }; + + let threshold = 1.0 - (1.0 - CHI_SQUARED_THRESHOLD).powf(1.0 / NUMBER_TESTS as Float); + + for i in 0..NUMBER_TESTS { + let sampled_vecs: Vec> = (0..BATCHES) + .map(|_| { + distribute_samples_over_threads(SAMPLES as u64, &func) + .into_iter() + .map(|v| v as Float) + .collect() + }) + .collect(); + let mut sampled_values: Vec = (0..SAMPLE_LEN) + .into_par_iter() + .map(|i| { + recursively_binary_average::( + (0..BATCHES).map(|j| sampled_vecs[j][i]).collect(), + ) + }) + .collect(); + + let (df, chi_squared) = chi_squared(&sampled_values, &expected_values, SAMPLES); + + let p_value = chi2_probability(df as f64, chi_squared as f64); + if p_value < threshold as f64 { + let expected_abs_max = expected_values + .iter() + .max_by(|x, y| x.abs().partial_cmp(&y.abs()).unwrap()) + .unwrap() + .abs(); + + let sampled_abs_max = sampled_values + .iter() + .max_by(|x, y| x.abs().partial_cmp(&y.abs()).unwrap()) + .unwrap() + .abs(); + + let get_colour = |value: Float, max_abs_value: Float| -> Vec3 { + if value > 0.0 { + let t = value / max_abs_value; + + t * Vec3::new(1.0, 0.0, 0.0) + } else if value == 0.0 { + Vec3::new(0.0, 1.0, 0.0) + } else { + Vec3::new(0.0, 0.0, 1.0) + } + }; + + let transpose = |vec: &mut Vec| { + *vec = (0..(PHI_RES * THETA_RES)) + .map(|i| { + let y = i % PHI_RES; + let x = i / PHI_RES; + + vec[y * THETA_RES + x] + }) + .collect::>(); + }; + + transpose(&mut expected_values); + transpose(&mut sampled_values); + + let mut image = expected_values + .into_iter() + .map(|v| get_colour(v, expected_abs_max)) + .collect::>(); + image.extend( + (0..PHI_RES) + .map(|_| Vec3::new(0.12, 0.95, 0.95)) + .collect::>(), + ); + image.extend( + sampled_values + .into_iter() + .map(|v| get_colour(v, sampled_abs_max)) + .collect::>(), + ); + let image = image + .into_iter() + .flat_map(|v| [v.x, v.y, v.z]) + .map(|v| (v * 256.0).clamp(0.0, 255.0) as u8) + .collect::>(); + + image::save_buffer( + format!("{name}_failed_output_test_{i}.png"), + &image, + PHI_RES.try_into().unwrap(), + (THETA_RES * 2 + 1).try_into().unwrap(), + image::ColorType::Rgb8, + ) + .unwrap(); + + panic!("{name}: recieved p value of {p_value} with {SAMPLES} samples averaged over {BATCHES} batches on test {i}/{NUMBER_TESTS}") + } + } +} + +#[cfg(test)] +pub mod test { + use super::*; + use rand::Rng; + + pub fn to_vec(sin_theta: Float, cos_theta: Float, phi: Float) -> Vec3 { + Vec3::new(phi.cos() * sin_theta, phi.sin() * sin_theta, cos_theta) + } + + pub fn generate_wi(rng: &mut R) -> Vec3 { + let cos_theta: Float = rng.gen(); + let phi = TAU as Float * rng.gen::(); + + -to_vec((1.0 - cos_theta * cos_theta).sqrt(), cos_theta, phi) + } + + #[test] + fn cosine_hemisphere() { + test_spherical_pdf( + "cosine hemisphere sampling", + &cosine_hemisphere_pdf, + &cosine_hemisphere_sampling, + true, + ); + } +} + +#[cfg(test)] +pub use test::*; diff --git a/implementations/src/textures/mod.rs b/crates/implementations/src/textures/mod.rs similarity index 80% rename from implementations/src/textures/mod.rs rename to crates/implementations/src/textures/mod.rs index c99151d..3333599 100644 --- a/implementations/src/textures/mod.rs +++ b/crates/implementations/src/textures/mod.rs @@ -1,11 +1,12 @@ use image::{io::Reader, GenericImageView}; use proc::Texture; use rand::{rngs::SmallRng, thread_rng, Rng, SeedableRng}; -use rt_core::{Float, Vec2, Vec3, PI}; +use rt_core::*; +use std::path::Path; const PERLIN_RVECS: usize = 256; -pub trait Texture { +pub trait Texture: Sync { fn colour_value(&self, _: Vec3, _: Vec3) -> Vec3 { Vec3::new(1.0, 1.0, 1.0) } @@ -13,7 +14,7 @@ pub trait Texture { false } } -#[derive(Texture, Debug)] +#[derive(Texture, Debug, Clone)] pub enum AllTextures { CheckeredTexture(CheckeredTexture), SolidColour(SolidColour), @@ -22,17 +23,37 @@ pub enum AllTextures { Perlin(Box), } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct CheckeredTexture { - primary_colour: Vec3, - secondary_colour: Vec3, + colour_one: Vec3, + colour_two: Vec3, +} + +pub fn generate_values(texture: &T, sample_res: (usize, usize)) -> Vec { + let mut values = Vec::new(); + + let step = (1.0 / sample_res.0 as Float, 1.0 / sample_res.1 as Float); + for y in 0..sample_res.1 { + for x in 0..sample_res.0 { + let u = (x as Float + 0.5) * step.0; + let v = (y as Float + 0.5) * step.1; + let phi = u * 2.0 * PI; + let theta = v * PI; + let sin_theta = theta.sin(); + let direction = Vec3::new(phi.cos() * sin_theta, phi.sin() * sin_theta, theta.cos()); + let col = texture.colour_value(direction, Vec3::zero()); + values.push((0.2126 * col.x + 0.7152 * col.y + 0.0722 * col.z) * sin_theta); + } + } + + values } impl CheckeredTexture { - pub fn new(primary_colour: Vec3, secondary_colour: Vec3) -> Self { + pub fn new(colour_one: Vec3, colour_two: Vec3) -> Self { CheckeredTexture { - primary_colour, - secondary_colour, + colour_one, + colour_two, } } } @@ -41,9 +62,9 @@ impl Texture for CheckeredTexture { fn colour_value(&self, _: Vec3, point: Vec3) -> Vec3 { let sign = (10.0 * point.x).sin() * (10.0 * point.y).sin() * (10.0 * point.z).sin(); if sign > 0.0 { - self.primary_colour + self.colour_one } else { - self.secondary_colour + self.colour_two } } fn requires_uv(&self) -> bool { @@ -51,7 +72,7 @@ impl Texture for CheckeredTexture { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Perlin { ran_vecs: [Vec3; PERLIN_RVECS], perm_x: [u32; PERLIN_RVECS], @@ -119,12 +140,12 @@ impl Perlin { perm } - fn permute(perm: &mut [u32; 256]) { + fn permute(perm: &mut [u32; PERLIN_RVECS]) { let mut rng = rand::rngs::SmallRng::from_rng(rand::thread_rng()).unwrap(); - for i in (1..256).rev() { + for i in (1..PERLIN_RVECS).rev() { let target = rng.gen_range(0..i); - perm[0..256].swap(i, target); + perm[0..PERLIN_RVECS].swap(i, target); } } @@ -158,7 +179,7 @@ impl Texture for Box { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct SolidColour { pub colour: Vec3, } @@ -178,14 +199,17 @@ impl Texture for SolidColour { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct ImageTexture { pub data: Vec, pub dim: (usize, usize), } impl ImageTexture { - pub fn new(filepath: &str) -> Self { + pub fn new

(filepath: &P) -> Self + where + P: AsRef, + { // open image and get dimensions let img = match image::open(filepath) { @@ -214,7 +238,7 @@ impl ImageTexture { let image = img.to_rgb32f(); for col in image.into_raw().chunks(3) { data.push(Vec3::new( - *col.get(0).unwrap() as Float, + *col.first().unwrap() as Float, *col.get(1).unwrap() as Float, *col.get(2).unwrap() as Float, )); @@ -241,7 +265,7 @@ impl Texture for ImageTexture { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Lerp { pub colour_one: Vec3, pub colour_two: Vec3, @@ -258,7 +282,7 @@ impl Lerp { impl Texture for Lerp { fn colour_value(&self, direction: Vec3, _: Vec3) -> Vec3 { - let t = direction.y * 0.5 + 0.5; + let t = direction.z * 0.5 + 0.5; self.colour_one * t + self.colour_two * (1.0 - t) } fn requires_uv(&self) -> bool { diff --git a/crates/implementations/src/utility/coord.rs b/crates/implementations/src/utility/coord.rs new file mode 100644 index 0000000..6791a6d --- /dev/null +++ b/crates/implementations/src/utility/coord.rs @@ -0,0 +1,50 @@ +use rt_core::Vec3; + +pub struct Coordinate { + pub x: Vec3, + pub y: Vec3, + pub z: Vec3, +} + +impl Coordinate { + pub fn new_from_z(z: Vec3) -> Self { + let x = if z.x.abs() > z.y.abs() { + Vec3::new(-z.z, 0.0, z.x) / (z.x * z.x + z.z * z.z).sqrt() + } else { + Vec3::new(0.0, z.z, -z.y) / (z.y * z.y + z.z * z.z).sqrt() + }; + Coordinate { + x, + y: x.cross(z), + z, + } + } + pub fn create_inverse(&self) -> Self { + let x = Vec3::new(self.x.x, self.y.x, self.z.x); + let y = Vec3::new(self.x.y, self.y.y, self.z.y); + let z = Vec3::new(self.x.z, self.y.z, self.z.z); + Coordinate { x, y, z } + } + pub fn to_coord(&self, vec: Vec3) -> Vec3 { + vec.x * self.x + vec.y * self.y + vec.z * self.z + } +} + +#[cfg(test)] +mod tests { + use crate::random_unit_vector; + + use super::*; + + #[test] + fn inverse() { + let z = random_unit_vector(); + let to = Coordinate::new_from_z(z); + let from = to.create_inverse(); + let v = random_unit_vector(); + assert!( + (v - from.to_coord(to.to_coord(v))).mag_sq() < 0.000001 + && (v - to.to_coord(from.to_coord(v))).mag_sq() < 0.000001 + ); + } +} diff --git a/implementations/src/utility/mod.rs b/crates/implementations/src/utility/mod.rs similarity index 100% rename from implementations/src/utility/mod.rs rename to crates/implementations/src/utility/mod.rs diff --git a/crates/implementations/tests/sampling.rs b/crates/implementations/tests/sampling.rs new file mode 100644 index 0000000..2b5e07a --- /dev/null +++ b/crates/implementations/tests/sampling.rs @@ -0,0 +1,297 @@ +/*use rand::rngs::ThreadRng; +use rt_core::*; +use statistics::spherical_sampling::test_spherical_pdf; + +#[test] +fn sky_sampling() { + const SAMPLE_WIDTH: usize = 30; + const SAMPLE_HEIGHT: usize = 50; + + let tex = std::sync::Arc::new(implementations::AllTextures::Lerp( + implementations::Lerp::new(Vec3::zero(), Vec3::one()), + )); + + let sky = implementations::Sky::new(&tex, (SAMPLE_WIDTH, SAMPLE_HEIGHT)); + + let pdf = |outgoing: Vec3| sky.pdf(outgoing); + let sample = |_: &mut ThreadRng| sky.sample(); + test_spherical_pdf("lerp sky sampling", &pdf, &sample, false); +} + +use implementations::sphere::Sphere; +use implementations::*; +use rayon::prelude::{IntoParallelIterator, ParallelIterator}; + +use std::sync::Arc; + +type MaterialType = AllMaterials; +type PrimitiveType = AllPrimitives; +type BvhType = Bvh; + +pub fn furnace_test(sampler_res: (usize, usize)) -> (Sky, BvhType) { + let light_tex = AllTextures::SolidColour(SolidColour::new(Vec3::new(1.0, 1.0, 1.0))); + let light_mat = Arc::new(AllMaterials::Emit(Emit::new(&Arc::new(light_tex), 1.0))); + + let mat = AllTextures::SolidColour(SolidColour::new(Vec3::new(0.5, 0.5, 0.5))); + + let mat = Arc::new(AllMaterials::Lambertian(Lambertian::new( + &Arc::new(mat), + 0.5, + ))); + + let hidden_light_tex = AllTextures::SolidColour(SolidColour::new(Vec3::new(1.0, 0.0, 1.0))); + let hidden_light_mat = Arc::new(AllMaterials::Emit(Emit::new( + &Arc::new(hidden_light_tex), + 15.0, + ))); + + let primitives = vec![ + AllPrimitives::Sphere(Sphere::new(Vec3::zero(), 0.5, &mat)), + AllPrimitives::Sphere(Sphere::new(Vec3::new(0.0, 0.0, 0.0), 1000.0, &light_mat)), + AllPrimitives::Sphere(Sphere::new( + Vec3::new(0.0, 0.0, -5.0), + 0.45, + &hidden_light_mat, + )), // hidden light from above + ]; + + let sky_tex = AllTextures::Lerp(Lerp::new(Vec3::zero(), Vec3::new(0.5, 1.0, 0.2))); + + let sky = implementations::Sky::new(&std::sync::Arc::new(sky_tex), sampler_res); + + (sky, Bvh::new(primitives, split::SplitType::Sah)) +} + +pub fn get_test_scene(m: MaterialType, sampler_res: (usize, usize)) -> (Sky, BvhType) { + let light_tex = AllTextures::SolidColour(SolidColour::new(Vec3::new(1.0, 0.0, 0.5))); + let _light_mat = Arc::new(AllMaterials::Emit(Emit::new(&Arc::new(light_tex), 5.0))); + + let mat = Arc::new(m); + + let primitives = vec![ + AllPrimitives::Triangle(Triangle::new( + [ + Vec3::new(-0.5, -0.5, 0.0), + Vec3::new(0.0, 0.5, 0.0), + Vec3::new(0.5, -0.5, 0.0), + ], + [Vec3::new(0.0, 0.0, 1.0); 3], + &mat, + )), + //AllPrimitives::Sphere(Sphere::new(Vec3::new(0.0, 3.0, 1.0), 0.3, &light_mat)), + //AllPrimitives::Sphere(Sphere::new(Vec3::new(0.0, 2.9, 0.9), 0.2, &mat)), + //AllPrimitives::Sphere(Sphere::new(Vec3::new(0.0, 0.0, 0.0), 1000.0, &light_mat)), + ]; + + //let sky_tex = AllTextures::Lerp(Lerp::new(Vec3::zero(), Vec3::one())); + let sky_tex = AllTextures::SolidColour(implementations::SolidColour::new(rt_core::Vec3::new( + 1.0, 1.0, 1.0, //5.0, 0.0, 2.5, + ))); //rt_core::Vec3::zero())); + let sky = implementations::Sky::new(&std::sync::Arc::new(sky_tex), sampler_res); //Arc::new(sky_tex), sampler_res); + + (sky, Bvh::new(primitives, split::SplitType::Sah)) +} + +pub fn bxdf_testing(sampler_res: (usize, usize)) -> (Sky, BvhType) { + let mut primitives = Vec::new(); + + let triangle = |point_one, point_two, point_three, material| { + let normal = { + let a: Vec3 = point_two - point_one; + let b = point_three - point_one; + a.cross(b) + } + .normalised(); + + AllPrimitives::Triangle(Triangle::new( + [point_one, point_two, point_three], + [normal; 3], + material, + )) + }; + + let aarect = |point_one: &Vec2, point_two: &Vec2, axis_value: Float, axis: &Axis, material| { + let point_three = + Axis::point_from_2d(&Vec2::new(point_one.x, point_two.y), axis, axis_value); + let point_four = + Axis::point_from_2d(&Vec2::new(point_two.x, point_one.y), axis, axis_value); + let point_one = Axis::point_from_2d(point_one, axis, axis_value); + let point_two = Axis::point_from_2d(point_two, axis, axis_value); + vec![ + triangle(point_one, point_two, point_three, material), + triangle(point_one, point_two, point_four, material), + ] + }; + + let sphere = + |position, radius, material| AllPrimitives::Sphere(Sphere::new(position, radius, material)); + + let grey_tex = AllTextures::SolidColour(SolidColour::new(Vec3::new(0.5, 0.5, 0.5))); + + let diffuse = AllMaterials::Lambertian(Lambertian::new(&Arc::new(grey_tex), 0.5)); + + primitives.extend(aarect( + &Vec2::new(-500.0, -500.0), + &Vec2::new(500.0, 500.0), + -40.0, + &Axis::Z, + &Arc::new(diffuse), + )); + + let mat = Arc::new(AllMaterials::Emit(Emit::new( + &Arc::new(AllTextures::SolidColour(SolidColour::new(Vec3::new( + 0.0, 1.0, 0.0, + )))), + 5.5, + ))); + + let glowy = sphere(Vec3::new(0.0, -100.5, 0.0), 50.0, &mat); + + let glowy_two = sphere( + Vec3::new(0.0, 0.0, 300.0), + 50.0, + &Arc::new(AllMaterials::Emit(Emit::new( + &Arc::new(AllTextures::SolidColour(SolidColour::new(Vec3::new( + 1.0, 1.0, 1.0, + )))), + 10.5, + ))), + ); + + //let materials = vec![(testing_mat.clone(), "default")]; + + /*primitives.extend(crate::load_model::load_model_with_materials( + "../res/dragon.obj", + &materials, + ));*/ + + primitives.push(glowy); + primitives.push(glowy_two); + + let image = |filepath| Arc::new(AllTextures::ImageTexture(ImageTexture::new(&filepath))); + + let bvh = Bvh::new(primitives, split::SplitType::Sah); //create_bvh_with_info(primitives, bvh_type); + + ( + Sky::new(&image("../res/skymaps/lilienstein.webp"), sampler_res), + bvh, + ) +} + +#[test] +fn mis() { + let ray = Ray::new(Vec3::new(0.0, 0.0, 3.0), Vec3::new(0.0, 0.0, -1.0), 0.0); + + let (sky, bvh) = bxdf_testing((0, 0)); + + let samples = 2u64.pow(23); //5_000_000; + let mut ref_vals = Vec::new(); + for _ in 0..samples { + ref_vals.push(Ray::get_colour_naive(&mut ray.clone(), &sky, &bvh).0) + } + + let ref_val = statistics::utility::recursively_binary_average(ref_vals); + + println!("naive_sampling: {ref_val}"); + + let mut vals = Vec::new(); + for _ in 0..samples { + vals.push(Ray::get_colour(&mut ray.clone(), &sky, &bvh).0) + } + + let val = statistics::utility::recursively_binary_average(vals); + + println!("mis_sampling: {val}"); + + assert!((ref_val - val).mag() < 0.001) +} + +#[test] +fn mis_sky_sampling() { + let ray = Ray::new(Vec3::new(0.0, 0.0, 3.0), Vec3::new(0.0, 0.0, -1.0), 0.0); + + let (sky, bvh) = bxdf_testing((20, 10)); + + let samples = 5_000_000; + + let ref_val = (0..samples) + .into_par_iter() + .map(|_| Ray::get_colour_naive(&mut ray.clone(), &sky, &bvh).0) + .reduce_with(std::ops::Add::add) + .unwrap() + / samples as Float; + + let mut sum = Vec3::zero(); + for _ in 0..500 { + sum += (0..100_000) + .into_par_iter() + .map(|_| Ray::get_colour(&mut ray.clone(), &sky, &bvh).0) + .reduce_with(std::ops::Add::add) + .unwrap() / 100_000.0 as Float; + } + let val = sum / 500.0; + + println!("sky_sampling: {val}"); + + assert!((ref_val - val).mag() < 0.001) +} + +#[test] +fn naive_furnace_test() { + let ray = Ray::new(Vec3::new(0.0, 0.0, 3.0), Vec3::new(0.0, 0.0, -1.0), 0.0); + + let (sky, bvh) = furnace_test((0, 0)); + + let samples = 5_000_000; + + let ref_val = Vec3::new(0.25, 0.25, 0.25); + + let val = (0..samples) + .into_par_iter() + .map(|_| Ray::get_colour_naive(&mut ray.clone(), &sky, &bvh).0) + .reduce_with(std::ops::Add::add) + .unwrap() + / samples as Float; + + assert!((ref_val - val).mag() < 0.001) +} + +#[test] +fn mis_furnace_test() { + let ray = Ray::new(Vec3::new(0.0, 0.0, 3.0), Vec3::new(0.0, 0.0, -1.0), 0.0); + + let (sky, bvh) = furnace_test((0, 0)); + + let samples = 5_000_000; + + let ref_val = Vec3::new(0.25, 0.25, 0.25); + + let val = (0..samples) + .into_par_iter() + .map(|_| Ray::get_colour(&mut ray.clone(), &sky, &bvh).0) + .reduce_with(std::ops::Add::add) + .unwrap() + / samples as Float; + + assert!((ref_val - val).mag() < 0.001) +} + +#[test] +fn mis_sky_sampling_furnace_test() { + let ray = Ray::new(Vec3::new(0.0, 0.0, 3.0), Vec3::new(0.0, 0.0, -1.0), 0.0); + + let (sky, bvh) = furnace_test((10, 10)); + + let samples = 5_000_000; + + let ref_val = Vec3::new(0.25, 0.25, 0.25); + + let val = (0..samples) + .into_par_iter() + .map(|_| Ray::get_colour(&mut ray.clone(), &sky, &bvh).0) + .reduce_with(std::ops::Add::add) + .unwrap() + / samples as Float; + + assert!((ref_val - val).mag() < 0.001) +}*/ diff --git a/crates/loader/Cargo.toml b/crates/loader/Cargo.toml new file mode 100644 index 0000000..59ca43e --- /dev/null +++ b/crates/loader/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "loader" +version = "0.1.0" +edition = "2021" + +[dependencies] +log = "0.4" +nom = "7" +region = { path = "../region" } +implementations = { path = "../implementations" } +thiserror = "1.0" +wavefront_obj = "10.0.0" + +[features] +f64 = ["implementations/f64"] diff --git a/crates/loader/src/lib.rs b/crates/loader/src/lib.rs new file mode 100644 index 0000000..e5006f8 --- /dev/null +++ b/crates/loader/src/lib.rs @@ -0,0 +1,508 @@ +pub mod materials; +pub mod meshes; +pub mod misc; +pub mod obj; +pub mod parser; +pub mod primitives; +pub mod textures; + +use implementations::rt_core::{Float, NoHit, Primitive, Scatter, Vec2, Vec3}; +use implementations::*; +use region::{Region, RegionRes, RegionUniqSlice}; +use std::{collections::HashMap, fmt}; +use thiserror::Error; + +type TextureType = AllTextures; +type MaterialType<'a> = AllMaterials<'a, TextureType>; +type PrimitiveType<'a> = AllPrimitives<'a, MaterialType<'a>>; +type SkyType<'a> = Sky<'a, TextureType, AllMaterials<'a, TextureType>>; + +pub trait Load: Sized { + /// Take a set of properties and load an object from, optionally also + /// provide a resource name for this object. Such as a texture name + /// or material ID. + /// Also take a region if load needs to allocate + fn load(props: Properties, region: &mut Region) -> Result<(Option, Self), LoadErr>; +} + +#[derive(Default)] +pub struct Lookup { + texture: HashMap>, + scatter: HashMap>, +} + +impl fmt::Debug for Lookup { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + f.debug_struct("Lookup") + .field("texture", &format_args!("{:?}", self.texture.keys())) + .field("scatter", &format_args!("{:?}", self.scatter.keys())) + .finish() + } +} + +impl Lookup { + pub fn new() -> Self { + Default::default() + } + + pub fn texture_insert( + &mut self, + name: &str, + res: RegionRes, + ) -> Option> { + let key = name.into(); + let res = unsafe { std::mem::transmute(res) }; + self.texture + .insert(key, res) + .map(|o| unsafe { std::mem::transmute(o) }) + } + pub fn scatter_insert( + &mut self, + name: &str, + res: RegionRes, + ) -> Option> { + let key = name.into(); + let res = unsafe { std::mem::transmute(res) }; + self.scatter + .insert(key, res) + .map(|o| unsafe { std::mem::transmute(o) }) + } + + pub fn texture_lookup(&self, name: &str) -> Option> { + self.texture + .get(name) + .map(|o| unsafe { std::mem::transmute(o.clone()) }) + } + pub fn scatter_lookup(&self, name: &str) -> Option> { + self.scatter + .get(name) + .map(|o| unsafe { std::mem::transmute(o.clone()) }) + } +} + +#[derive(Debug)] +pub struct Properties<'a> { + lookup: &'a Lookup, + name: Option, + props: HashMap, + autocast: bool, +} + +#[derive(Debug)] +enum PropertiesValue { + Vec3(Vec3), + Vec2(Vec2), + Float(Float), + Text(String), +} + +impl<'a> From> for PropertiesValue { + fn from(u: parser::ObjectValue<'a>) -> Self { + use parser::ObjectValue::*; + match u { + Num1(a) => Self::Float(a), + Num2(a, b) => Self::Vec2(Vec2::new(a, b)), + Num3(a, b, c) => Self::Vec3(Vec3::new(a, b, c)), + Text(t) => Self::Text(t.into()), + } + } +} + +impl<'a> Properties<'a> { + pub fn new(lookup: &'a Lookup, object: &parser::Object) -> Self { + Self { + lookup, + name: object.name.map(Into::into), + props: object + .values + .iter() + .map(|(&k, &v)| (k.into(), v.into())) + .collect(), + autocast: true, + } + } + + pub fn auto_cast(&mut self, doit: bool) { + self.autocast = doit; + } + + pub fn texture(&self, name: &str) -> Option> { + self.lookup.texture_lookup(self.text(name)?) + } + pub fn scatter(&self, name: &str) -> Option> { + self.lookup.scatter_lookup(self.text(name)?) + } + pub fn lookup_material(&self, name: &str) -> Option> { + self.lookup.scatter_lookup(name) + } + pub fn vec3(&self, name: &str) -> Option { + match self.props.get(name) { + Some(PropertiesValue::Vec3(x)) => Some(*x), + Some(PropertiesValue::Float(x)) if self.autocast => Some(*x * Vec3::one()), + _ => None, + } + } + pub fn vec2(&self, name: &str) -> Option { + match self.props.get(name) { + Some(PropertiesValue::Vec2(x)) => Some(*x), + Some(PropertiesValue::Float(x)) if self.autocast => Some(*x * Vec2::one()), + _ => None, + } + } + pub fn float(&self, name: &str) -> Option { + match self.props.get(name) { + Some(PropertiesValue::Float(x)) => Some(*x), + _ => None, + } + } + pub fn text(&self, name: &str) -> Option<&str> { + match self.props.get(name) { + Some(PropertiesValue::Text(x)) => Some(x), + _ => None, + } + } + pub fn name(&mut self) -> Option { + self.name.take() + } + + pub fn default_texture(&self) -> RegionRes { + self.lookup + .texture_lookup("__DEFAULT_TEX") + .expect("default texture not loaded") + } + pub fn default_scatter(&self) -> RegionRes { + self.lookup + .scatter_lookup("__DEFAULT_MAT") + .expect("default material not loaded") + } +} + +#[derive(Error, Debug)] +pub enum LoadErr { + #[error("failed to load a file for the given reason")] + FileNotRead(std::path::PathBuf, std::io::Error), + #[error("failed to parse the scene config for the given reason")] + ParseError(parser::ParseError), + #[error("missing required type for object")] + MissingRequiredVariantType, + #[error("missing required value for object")] + MissingRequired(String), + #[error("missing required camera object")] + MissingCamera, + #[error("unknown error")] + Any(Box), +} + +pub fn load_file_full<'a, T, M, P, C, S>( + region: &'a mut Region, + file: &str, +) -> Result<(RegionUniqSlice<'a, P>, C, S), LoadErr> +where + T: Texture + Load, + M: Scatter + Load, + P: Primitive + Load + Clone, + C: Camera + Load, + S: NoHit + Load, + Vec

: Load, +{ + let scene_file = match std::fs::read_to_string(file) { + Ok(s) => s, + Err(e) => return Err(LoadErr::FileNotRead(file.into(), e)), + }; + log::debug!("Parsing scene file {}", file); + let scene_conf = match parser::from_str(&scene_file) { + Ok(c) => c, + Err(e) => return Err(LoadErr::ParseError(e)), + }; + + let mut lookup = Lookup::new(); + + log::info!("Loading textures..."); + let textures = load_textures::(&scene_conf, &lookup, region)?; + + region_insert_with_lookup(region, textures, |n, t| lookup.texture_insert(n, t)); + + log::info!("Loading materials..."); + let materials = load_materials::(&scene_conf, &lookup, region)?; + + region_insert_with_lookup(region, materials, |n, s| lookup.scatter_insert(n, s)); + + log::info!("Loading other objects..."); + let camera = load_scene_camera(&scene_conf, &lookup, region)?; + let sky = load_scene_sky(&scene_conf, &lookup, region)?; + + log::info!("Loading primitives..."); + let primitives = { + let mut primitives = load_primitives::

(&scene_conf, &lookup, region)?; + log::info!("Loading meshes..."); + primitives.extend(load_meshes::

(&scene_conf, &lookup, region)?); + region.alloc_slice(&primitives) + }; + + Ok((primitives, camera, sky)) +} + +pub fn load_str_full<'a, T, M, P, C, S>( + region: &'a mut Region, + data: &str, +) -> Result<(RegionUniqSlice<'a, PrimitiveType<'a>>, C, SkyType<'a>), LoadErr> +where + T: Texture + Load, + M: Scatter + Load, + C: Camera + Load, + S: NoHit + Load, + Vec

: Load, + SkyType<'a>: implementations::rt_core::NoHit, +{ + let scene_conf = match parser::from_str(data) { + Ok(c) => c, + Err(e) => return Err(LoadErr::ParseError(e)), + }; + + let mut lookup = Lookup::new(); + + log::info!("Loading textures..."); + let textures = load_textures::(&scene_conf, &lookup, region)?; + + region_insert_with_lookup(region, textures, |n, t| lookup.texture_insert(n, t)); + + log::info!("Loading materials..."); + let materials = load_materials::(&scene_conf, &lookup, region)?; + + region_insert_with_lookup(region, materials, |n, s| lookup.scatter_insert(n, s)); + + log::info!("Loading other objects..."); + let camera = load_scene_camera(&scene_conf, &lookup, region)?; + let sky = load_scene_sky::(&scene_conf, &lookup, region)?; + + log::info!("Loading primitives..."); + let primitives = { + let mut primitives = load_primitives::(&scene_conf, &lookup, region)?; + log::info!("Loading meshes..."); + primitives.extend(load_meshes::(&scene_conf, &lookup, region)?); + region.alloc_slice(&primitives) + }; + + Ok((primitives, camera, sky)) +} + +pub fn load_scene_camera( + objects: &[parser::Object], + lookup: &Lookup, + region: &mut Region, +) -> Result +where + C: Camera + Load, +{ + // Find a camera object + let props = Properties::new( + lookup, + objects + .iter() + .find(|o| o.kind.is_camera()) + .ok_or(LoadErr::MissingCamera)?, + ); + Ok(C::load(props, region)?.1) +} + +pub fn load_scene_sky( + objects: &[parser::Object], + lookup: &Lookup, + region: &mut Region, +) -> Result +where + S: NoHit + Load, + M: Scatter, +{ + // Find a sky object, if none warn and use a default + let obj = objects.iter().find(|o| o.kind.is_sky()); + let props = match obj { + Some(o) => Properties::new(lookup, o), + None => { + log::warn!("no sky object was provided in scene file, using default"); + Properties::new(lookup, &Default::default()) + } + }; + Ok(S::load(props, region)?.1) +} + +fn region_insert_with_lookup( + region: &mut Region, + items: Vec<(Option, T)>, + mut insert_fn: impl FnMut(&str, RegionRes) -> Option>, +) { + for (name, item) in items.into_iter() { + let uniq = region.alloc(item); + if let Some(name) = name { + if insert_fn(&name, uniq.shared()).is_some() { + log::warn!("Overwrote previous object of name: '{name}'"); + } + } + } +} + +fn load_textures( + objects: &[parser::Object], + lookup: &Lookup, + region: &mut Region, +) -> Result, T)>, LoadErr> { + let mut textures = Vec::new(); + for obj in objects.iter().filter(|o| o.kind.is_texture()) { + let props = Properties::new(lookup, obj); + textures.push(::load(props, region)?); + } + // Load default texture, assumes that T contains SolidColor + { + use parser::{Object, ObjectKind, ObjectValue}; + let def_obj = Object { + kind: ObjectKind::Texture, + name: Some("__DEFAULT_TEX"), + values: [ + ("type", ObjectValue::Text("solid")), + ("colour", ObjectValue::Num1(1.0)), + ] + .into(), + }; + let props = Properties::new(lookup, &def_obj); + textures.push(::load(props, region)?); + } + Ok(textures) +} + +fn load_materials( + objects: &[parser::Object], + lookup: &Lookup, + region: &mut Region, +) -> Result, S)>, LoadErr> { + let mut materials = Vec::new(); + for obj in objects.iter().filter(|o| o.kind.is_material()) { + let props = Properties::new(lookup, obj); + materials.push(::load(props, region)?); + } + // Load default material, assumes that S contains Lambertian + { + use parser::{Object, ObjectKind, ObjectValue}; + let def_obj = Object { + kind: ObjectKind::Material, + name: Some("__DEFAULT_MAT"), + values: [ + ("type", ObjectValue::Text("lambertian")), + ("texture", ObjectValue::Text("__DEFAULT_TEX")), + ("albedo", ObjectValue::Num1(0.25)), + ] + .into(), + }; + let props = Properties::new(lookup, &def_obj); + materials.push(::load(props, region)?); + } + Ok(materials) +} + +fn load_primitives( + objects: &[parser::Object], + lookup: &Lookup, + region: &mut Region, +) -> Result, LoadErr> { + let mut primitives = Vec::new(); + for obj in objects.iter().filter(|o| o.kind.is_primitive()) { + let props = Properties::new(lookup, obj); + primitives.push(

::load(props, region)?.1); + } + Ok(primitives) +} + +fn load_meshes( + objects: &[parser::Object], + lookup: &Lookup, + region: &mut Region, +) -> Result, LoadErr> +where + Vec

: Load, +{ + let mut primitives = Vec::new(); + for obj in objects.iter().filter(|o| o.kind.is_mesh()) { + let props = Properties::new(lookup, obj); + primitives.extend( as Load>::load(props, region)?.1); + } + Ok(primitives) +} + +#[cfg(test)] +mod tests { + use super::*; + + const DATA: &str = "camera ( + origin -5 3 -3 + lookat 0 0.5 0 + vup 0 1 0 + fov 34.0 + aperture 0.0 + focus_dis 10.0 +) + +texture sky ( + type solid + colour 0.0 +) + +sky ( + texture sky +) + +texture grey ( + type solid + colour 0.5 +) + +texture white ( + type solid + colour 1.0 +) + +material ground ( + type lambertian + texture grey + albedo 0.5 +) + +material light ( + type emissive + texture white + strength 1.5 +) + +primitive ( + type sphere + material ground + centre 0 -1000 0 + radius 1000 +) + +primitive ( + type sphere + material light + centre 0 0.5 0 + radius 0.5 +) + +primitive ( + type sphere + material ground + centre -0.45 0.15 -0.45 + radius 0.05 +)"; + + #[test] + fn scene() { + let mut region = Region::new(); + type Tex = AllTextures; + type Mat<'a> = AllMaterials<'a, Tex>; + type Prim<'a> = AllPrimitives<'a, Mat<'a>>; + type SkyType<'a> = Sky<'a, Tex, Mat<'a>>; + let stuff = + load_str_full::(&mut region, DATA).unwrap(); + + let (p, _, s) = stuff; + let _: Bvh = Bvh::new(p, s, split::SplitType::Sah); + } +} diff --git a/crates/loader/src/materials.rs b/crates/loader/src/materials.rs new file mode 100644 index 0000000..cf6dab4 --- /dev/null +++ b/crates/loader/src/materials.rs @@ -0,0 +1,136 @@ +use crate::Properties; +use crate::*; +use implementations::emissive::Emit; +use implementations::*; + +impl Load for AllMaterials<'_, T> { + fn load(props: Properties, region: &mut Region) -> Result<(Option, Self), LoadErr> { + let kind = match props.text("type") { + Some(k) => k, + None => return Err(LoadErr::MissingRequiredVariantType), + }; + + Ok(match kind { + "emissive" => { + let x = Emit::load(props, region)?; + (x.0, Self::Emit(x.1)) + } + "lambertian" => { + let x = Lambertian::load(props, region)?; + (x.0, Self::Lambertian(x.1)) + } + "reflect" => { + let x = Reflect::load(props, region)?; + (x.0, Self::Reflect(x.1)) + } + "refract" => { + let x = Refract::load(props, region)?; + (x.0, Self::Refract(x.1)) + } + "trowbridge_reitz" => { + let x = TrowbridgeReitz::load(props, region)?; + (x.0, Self::TrowbridgeReitz(x.1)) + } + o => { + return Err(LoadErr::MissingRequired(format!( + "required a known value for material type, found '{o}'" + ))) + } + }) + } +} + +impl Load for Lambertian<'_, T> { + fn load(mut props: Properties, _: &mut Region) -> Result<(Option, Self), LoadErr> { + let tex = props + .texture("texture") + .unwrap_or_else(|| props.default_texture()); + let albedo = props.float("albedo").unwrap_or(0.5); + + let name = props.name(); + + Ok((name, Self::new(unsafe { &*(&*tex as *const _) }, albedo))) + } +} + +impl Load for Emit<'_, T> { + fn load(mut props: Properties, _: &mut Region) -> Result<(Option, Self), LoadErr> { + let tex = props + .texture("texture") + .unwrap_or_else(|| props.default_texture()); + let strength = props.float("strength").unwrap_or(1.5); + + let name = props.name(); + + Ok((name, Self::new(unsafe { &*(&*tex as *const _) }, strength))) + } +} + +impl Load for Reflect<'_, T> { + fn load(mut props: Properties, _: &mut Region) -> Result<(Option, Self), LoadErr> { + let tex = props + .texture("texture") + .unwrap_or_else(|| props.default_texture()); + let fuzz = props.float("fuzz").unwrap_or(0.1); + + let name = props.name(); + + Ok((name, Self::new(unsafe { &*(&*tex as *const _) }, fuzz))) + } +} + +impl Load for Refract<'_, T> { + fn load(mut props: Properties, _: &mut Region) -> Result<(Option, Self), LoadErr> { + let tex = props + .texture("texture") + .unwrap_or_else(|| props.default_texture()); + let eta = props.float("eta").unwrap_or(1.5); + + let name = props.name(); + + Ok((name, Self::new(unsafe { &*(&*tex as *const _) }, eta))) + } +} + +impl Load for TrowbridgeReitz<'_, T> { + fn load(mut props: Properties, _: &mut Region) -> Result<(Option, Self), LoadErr> { + let tex = props + .texture("texture") + .unwrap_or_else(|| props.default_texture()); + let alpha = props.float("alpha").unwrap_or(0.5); + let ior = props.vec3("ior").unwrap_or(Vec3::one()); + let metallic = props.float("metallic").unwrap_or(0.0); + + let name = props.name(); + + Ok(( + name, + Self::new(unsafe { &*(&*tex as *const _) }, alpha, ior, metallic), + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn lambertian() { + let mut region = Region::new(); + let mut lookup = Lookup::new(); + let file = " +texture grey ( + type solid + colour 0.5 +) +material ground ( + type lambertian + texture grey + albedo 0.5 +)"; + let data = parser::from_str(file).unwrap(); + let textures = load_textures::(&data, &lookup, &mut region).unwrap(); + region_insert_with_lookup(&mut region, textures, |n, t| lookup.texture_insert(n, t)); + let _ = load_materials::>(&data, &lookup, &mut region).unwrap(); + } +} diff --git a/crates/loader/src/meshes.rs b/crates/loader/src/meshes.rs new file mode 100644 index 0000000..1f86f2c --- /dev/null +++ b/crates/loader/src/meshes.rs @@ -0,0 +1,119 @@ +use crate::obj::load_obj; +use crate::Properties; +use crate::*; +use implementations::triangle::MeshData; +use implementations::triangle::MeshTriangle; +use implementations::*; + +impl Load for Vec> { + fn load(props: Properties, region: &mut Region) -> Result<(Option, Self), LoadErr> { + let kind = match props.text("type") { + Some(k) => k, + None => return Err(LoadErr::MissingRequiredVariantType), + }; + match kind { + "mesh" => mesh(props, region), + "aacuboid" => cuboid(props, region), + o => { + return Err(LoadErr::MissingRequired(format!( + "required a known value for mesh type, found '{o}'" + ))) + } + } + } +} + +fn cuboid<'a, M: Scatter>( + props: Properties, + _: &mut Region, +) -> Result<(Option, Vec>), LoadErr> { + let mat: region::RegionRes = props + .scatter("material") + .unwrap_or_else(|| props.default_scatter()); + let point_one = match props.vec3("point_one") { + Some(c) => c, + None => { + return Err(LoadErr::MissingRequired( + "expected point_one on aacubiod, found nothing".to_string(), + )) + } + }; + let point_two = match props.vec3("point_two") { + Some(c) => c, + None => { + return Err(LoadErr::MissingRequired( + "expected point_two on aacubiod, found nothing".to_string(), + )) + } + }; + + let min = point_one.min_by_component(point_two); + let max = point_one.max_by_component(point_two); + + let points = vec![ + min, // 0 + Vec3::new(max.x, min.y, min.z), // 1 + Vec3::new(max.x, max.y, min.z), // 2 + Vec3::new(min.x, max.y, min.z), // 3 + Vec3::new(min.x, min.y, max.z), // 4 + Vec3::new(max.x, min.y, max.z), // 5 + max, // 6 + Vec3::new(min.x, max.y, max.z), // 7 + ]; + + let normals = vec![ + Vec3::x(), // 0 + -Vec3::x(), // 1 + Vec3::y(), // 2 + -Vec3::y(), // 3 + Vec3::z(), // 4 + -Vec3::z(), // 5 + ]; + + let mesh_data = std::sync::Arc::new(MeshData::new(points, normals)); + std::mem::forget(mesh_data.clone()); // prevent drop when primitives get moved to region + + macro_rules! mesh_tri { + ($p:expr, $normal:expr) => { + AllPrimitives::MeshTriangle(MeshTriangle::new( + $p, + [$normal; 3], + unsafe { &*(&*mat as *const _) }, + mesh_data.clone(), + )) + }; + } + + let triangles = vec![ + mesh_tri!([0, 1, 2], 5), + mesh_tri!([0, 2, 3], 5), + mesh_tri!([0, 1, 5], 3), + mesh_tri!([0, 5, 4], 3), + mesh_tri!([1, 2, 5], 0), + mesh_tri!([2, 5, 6], 0), + mesh_tri!([2, 3, 7], 2), + mesh_tri!([2, 6, 7], 2), + mesh_tri!([0, 3, 4], 1), + mesh_tri!([3, 4, 7], 1), + mesh_tri!([4, 5, 6], 4), + mesh_tri!([4, 6, 7], 4), + ]; + + Ok((None, triangles)) +} + +fn mesh<'a, M: Scatter>( + props: Properties, + _: &mut Region, +) -> Result<(Option, Vec>), LoadErr> { + let filepath = match props.text("obj") { + Some(c) => c.to_owned(), + None => { + return Err(LoadErr::MissingRequired( + "expected obj on mesh, found nothing".to_string(), + )) + } + }; + let prims = load_obj(&filepath, props); + Ok((None, prims)) +} diff --git a/crates/loader/src/misc.rs b/crates/loader/src/misc.rs new file mode 100644 index 0000000..18597da --- /dev/null +++ b/crates/loader/src/misc.rs @@ -0,0 +1,38 @@ +use crate::Properties; +use crate::*; + +use implementations::*; + +impl Load for SimpleCamera { + fn load(props: Properties, _: &mut Region) -> Result<(Option, Self), LoadErr> { + let origin = props.vec3("origin").unwrap_or(Vec3::new(3., 0., 0.)); + let lookat = props.vec3("lookat").unwrap_or(Vec3::zero()); + let vup = props.vec3("vup").unwrap_or(Vec3::new(0., 1., 0.)); + let fov = props.float("fov").unwrap_or(40.0); + let aperture = props.float("aperture").unwrap_or(0.0); + let focus = props.float("focus_dis").unwrap_or(10.0); + + let cam = Self::new(origin, lookat, vup, fov, 16.0 / 9.0, aperture, focus); + Ok((None, cam)) + } +} + +impl Load for Sky<'_, T, AllMaterials<'_, T>> { + fn load(props: Properties, region: &mut Region) -> Result<(Option, Self), LoadErr> { + let tex = props + .texture("texture") + .unwrap_or_else(|| props.default_texture()); + let res = props.vec2("sampler_res").unwrap_or(Vec2::new(100., 100.)); + + let mat = AllMaterials::Emit(Emit::new(unsafe { &*(&*tex as *const _) }, 1.0)); + + let mat = region.alloc(mat).shared(); + + let sky = Self::new( + unsafe { &*(&*tex as *const _) }, + unsafe { &*(&*mat as *const _) }, + (res.x as _, res.y as _), + ); + Ok((None, sky)) + } +} diff --git a/frontend/src/load_model.rs b/crates/loader/src/obj.rs similarity index 52% rename from frontend/src/load_model.rs rename to crates/loader/src/obj.rs index 82e8985..4f5659b 100644 --- a/frontend/src/load_model.rs +++ b/crates/loader/src/obj.rs @@ -1,17 +1,17 @@ +use crate::Float; +use crate::Properties; +use crate::Scatter; +use crate::Vec3; use implementations::{ triangle::{MeshData, MeshTriangle}, - AllMaterials, AllPrimitives, AllTextures, + AllPrimitives, }; -use rt_core::{Float, Vec3}; use std::sync::Arc; -pub fn load_model_with_materials( - filepath: &str, - materials: &[(Arc>, &str)], -) -> Vec>> { +pub fn load_obj<'a, M: Scatter>(filepath: &str, props: Properties) -> Vec> { let model = wavefront_obj::obj::parse(&std::fs::read_to_string(filepath).unwrap()).unwrap(); - let mut primitives: Vec>> = Vec::new(); + let mut primitives: Vec> = Vec::new(); for object in model.objects { let mesh_data: Arc = Arc::new(MeshData::new( @@ -34,26 +34,28 @@ pub fn load_model_with_materials( panic!("Please export obj file with vertex normals!"); } - let mat = get_material( - materials, - geometric_object - .material_name - .as_ref() - .unwrap_or(&"default".to_owned()), - ); + let mat: region::RegionRes = props + .lookup_material( + geometric_object + .material_name + .as_ref() + .unwrap_or(&"default".to_owned()), + ) + .unwrap_or_else(|| props.default_scatter()); - let triangle: AllPrimitives> = + let triangle: AllPrimitives<'a, M> = AllPrimitives::MeshTriangle(MeshTriangle::new( [i1.0, i2.0, i3.0], [i1.2.unwrap(), i2.2.unwrap(), i3.2.unwrap()], - &mat, - &mesh_data, + unsafe { &*(&*mat as *const _) }, + mesh_data.clone(), )); primitives.push(triangle) } } } + std::mem::forget(mesh_data); } primitives } @@ -61,21 +63,3 @@ pub fn load_model_with_materials( fn vertex_to_vec3(vertex: wavefront_obj::obj::Vertex) -> Vec3 { Vec3::new(vertex.x as Float, vertex.y as Float, vertex.z as Float) } - -fn get_material( - materials: &[(Arc>, &str)], - name: &str, -) -> Arc> { - let mat = materials.iter().find(|&v| v.1 == name); - - match mat { - Some(mat) => mat.0.clone(), - None => { - let mat = materials.iter().find(|&v| v.1 == "default"); - if let Some(mat) = mat { - return mat.0.clone(); - } - panic!("{} material not found", name); - } - } -} diff --git a/crates/loader/src/parser.rs b/crates/loader/src/parser.rs new file mode 100644 index 0000000..98cc1d9 --- /dev/null +++ b/crates/loader/src/parser.rs @@ -0,0 +1,197 @@ +use implementations::rt_core::Float; +use nom::Finish; +use std::collections::HashMap; +use thiserror::Error; + +/// What kind was parsed from the scene file. Variants match the initial keyword +/// used before the name of the object. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ObjectKind { + Camera, + Material, + Primitive, + Sky, + Texture, + Mesh, + Other, +} + +/// An unowned value for a key in the scene. This could be a collection of three +/// floats or a string referring to a filename or such. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum ObjectValue<'a> { + Num1(Float), + Num2(Float, Float), + Num3(Float, Float, Float), + Text(&'a str), +} + +/// An unowned object in the scene format. Contains the type of the object and +/// an optional name if provided. Each key-value pair is stored in a `HashMap`. +#[derive(Debug, Clone)] +pub struct Object<'a> { + pub kind: ObjectKind, + pub name: Option<&'a str>, + pub values: HashMap<&'a str, ObjectValue<'a>>, +} + +impl ObjectKind { + pub fn is_camera(&self) -> bool { + matches!(self, ObjectKind::Camera) + } + + pub fn is_material(&self) -> bool { + matches!(self, ObjectKind::Material) + } + + pub fn is_primitive(&self) -> bool { + matches!(self, ObjectKind::Primitive) + } + + pub fn is_sky(&self) -> bool { + matches!(self, ObjectKind::Sky) + } + + pub fn is_texture(&self) -> bool { + matches!(self, ObjectKind::Texture) + } + + pub fn is_mesh(&self) -> bool { + matches!(self, ObjectKind::Mesh) + } +} + +impl<'a> Object<'a> { + pub fn lookup(&self, key: &str) -> Option> { + self.values.get(key).cloned() + } +} + +impl<'a> Default for Object<'a> { + fn default() -> Self { + Self { + kind: ObjectKind::Other, + name: Option::default(), + values: HashMap::default(), + } + } +} + +/// Despite being listed as version 1, there is still more to be added +/// (probably). For example comments, and probably addition value types. +mod ver1 { + use super::{Object, ObjectKind, ObjectValue}; + + use implementations::rt_core::Float; + use nom::{ + branch::alt, + bytes::complete::tag, + character::complete::{ + alpha1, alphanumeric1, line_ending, multispace0, not_line_ending, space0, space1, + }, + combinator::{map, opt, recognize}, + error::{ParseError, VerboseError}, + multi::{many0, many0_count}, + number::complete::double, + sequence::{delimited, pair, preceded, terminated, tuple}, + IResult, + }; + use std::collections::HashMap; + + pub type Res = IResult>; + + pub fn ws<'a, F: 'a, O, E: ParseError<&'a str>>( + inner: F, + ) -> impl FnMut(&'a str) -> IResult<&'a str, O, E> + where + F: Fn(&'a str) -> IResult<&'a str, O, E>, + { + delimited(multispace0, inner, multispace0) + } + + pub fn identifier(i: &str) -> Res<&str, &str> { + recognize(pair( + alt((alpha1, tag("_"))), + many0_count(alt((alphanumeric1, tag("_")))), + ))(i) + } + + pub fn value(i: &str) -> Res<&str, ObjectValue> { + let f = |i| preceded(space0, double)(i); + alt(( + map(tuple((f, f, f)), |(a, b, c)| { + ObjectValue::Num3(a as Float, b as Float, c as Float) + }), + map(tuple((f, f)), |(a, b)| { + ObjectValue::Num2(a as Float, b as Float) + }), + map(f, |a| ObjectValue::Num1(a as Float)), + map(preceded(space0, not_line_ending), ObjectValue::Text), + ))(i) + } + + pub fn keyvalue(i: &str) -> Res<&str, (&str, ObjectValue)> { + tuple((delimited(space0, identifier, space1), value))(i) + } + + pub fn values(i: &str) -> Res<&str, HashMap<&str, ObjectValue>> { + delimited( + ws(tag("(")), + map(many0(terminated(keyvalue, line_ending)), |vec| { + vec.into_iter().collect() + }), + ws(tag(")")), + )(i) + } + + pub fn objectkind(i: &str) -> Res<&str, ObjectKind> { + alt(( + map(tag("camera"), |_| ObjectKind::Camera), + map(tag("material"), |_| ObjectKind::Material), + map(tag("primitive"), |_| ObjectKind::Primitive), + map(tag("sky"), |_| ObjectKind::Sky), + map(tag("texture"), |_| ObjectKind::Texture), + map(tag("mesh"), |_| ObjectKind::Mesh), + ))(i) + } + + pub fn object(i: &str) -> Res<&str, Object> { + map( + tuple((objectkind, opt(preceded(space1, identifier)), values)), + |(kind, name, values)| Object { kind, name, values }, + )(i) + } + + pub fn parse(i: &str) -> Res<&str, Vec> { + many0(ws(object))(i) + } +} + +fn by_version(i: &str) -> ver1::Res<&str, Vec> { + use nom::{bytes::complete::tag, combinator::opt}; + + match opt(ver1::ws(tag("#ver1")))(i) { + Ok((o, Some(_))) => ver1::parse(o), + Ok((o, _)) => ver1::parse(o), + Err(e) => Err(e), + } +} + +/// Possible errors that can occur when parsing the scene file. +#[derive(Error, Debug)] +pub enum ParseError { + /// No idea what went wrong, ask Milo probably + #[error("no idea what wrong, parsing fucked up")] + ParsingError, +} + +pub fn from_str(src: &str) -> Result, ParseError> { + match by_version(src).finish() { + Ok((o, _)) if !o.is_empty() => Err(ParseError::ParsingError), + Ok((_, o)) => Ok(o), + Err(e) => { + eprintln!("Error parsing scene file: {e:#?}"); + Err(ParseError::ParsingError) + } + } +} diff --git a/crates/loader/src/primitives.rs b/crates/loader/src/primitives.rs new file mode 100644 index 0000000..d213e9a --- /dev/null +++ b/crates/loader/src/primitives.rs @@ -0,0 +1,91 @@ +use crate::Properties; +use crate::*; +use implementations::sphere::Sphere; +use implementations::*; + +use rt_core::Scatter; + +impl Load for Sphere<'_, M> { + fn load(props: Properties, _: &mut Region) -> Result<(Option, Self), LoadErr> { + let mat: region::RegionRes = props + .scatter("material") + .unwrap_or_else(|| props.default_scatter()); + let radius = props.float("radius").unwrap_or(1.0); + let centre = match props.vec3("centre") { + Some(c) => c, + None => { + return Err(LoadErr::MissingRequired( + "expected centre on sphere, found nothing".to_string(), + )) + } + }; + + Ok(( + None, + Self::new(centre, radius, unsafe { &*(&*mat as *const _) }), + )) + } +} + +impl Load for AllPrimitives<'_, M> { + fn load(props: Properties, region: &mut Region) -> Result<(Option, Self), LoadErr> { + let kind = match props.text("type") { + Some(k) => k, + None => return Err(LoadErr::MissingRequiredVariantType), + }; + + Ok(match kind { + "sphere" => { + let x = Sphere::load(props, region)?; + (x.0, Self::Sphere(x.1)) + } + "triangle" => todo!(), + o => { + return Err(LoadErr::MissingRequired(format!( + "required a known value for primitive type, found '{o}'" + ))) + } + }) + } +} + +// TODO LOAD FOR TRIANGLE + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sphere() { + let mut region = Region::new(); + let mut lookup = Lookup::new(); + let file = " +texture grey ( + type solid + colour 0.5 +) +material ground ( + type lambertian + texture grey + albedo 0.5 +) +primitive ( + type sphere + material ground + centre 0 -1000 0 + radius 1000 +)"; + let data = parser::from_str(file).unwrap(); + let textures = load_textures::(&data, &lookup, &mut region).unwrap(); + + region_insert_with_lookup(&mut region, textures, |n, t| lookup.texture_insert(n, t)); + + let materials = + load_materials::>(&data, &lookup, &mut region).unwrap(); + + region_insert_with_lookup(&mut region, materials, |n, t| lookup.scatter_insert(n, t)); + + load_primitives::>>(&data, &lookup, &mut region) + .unwrap(); + } +} diff --git a/crates/loader/src/textures.rs b/crates/loader/src/textures.rs new file mode 100644 index 0000000..0aada14 --- /dev/null +++ b/crates/loader/src/textures.rs @@ -0,0 +1,118 @@ +use crate::*; + +use implementations::*; + +impl Load for AllTextures { + fn load(props: Properties, region: &mut Region) -> Result<(Option, Self), LoadErr> { + let kind = match props.text("type") { + Some(k) => k, + None => return Err(LoadErr::MissingRequiredVariantType), + }; + + Ok(match kind { + "checkered" => { + let x = CheckeredTexture::load(props, region)?; + (x.0, Self::CheckeredTexture(x.1)) + } + "solid" => { + let x = SolidColour::load(props, region)?; + (x.0, Self::SolidColour(x.1)) + } + "image" => { + let x = ImageTexture::load(props, region)?; + (x.0, Self::ImageTexture(x.1)) + } + "lerp" => { + let x = Lerp::load(props, region)?; + (x.0, Self::Lerp(x.1)) + } + "perlin" => { + let x = Perlin::load(props, region)?; + (x.0, Self::Perlin(Box::new(x.1))) + } + o => { + return Err(LoadErr::MissingRequired(format!( + "required a known value for texture type, found '{o}'" + ))) + } + }) + } +} + +impl Load for CheckeredTexture { + fn load(mut props: Properties, _: &mut Region) -> Result<(Option, Self), LoadErr> { + let primary = props.vec3("primary").unwrap_or(Vec3::one()); + let secondary = props.vec3("secondary").unwrap_or(Vec3::zero()); + let name = props.name(); + Ok((name, Self::new(primary, secondary))) + } +} + +impl Load for ImageTexture { + fn load(mut props: Properties, _: &mut Region) -> Result<(Option, Self), LoadErr> { + let name = props.name(); + let filename = match props.text("filename") { + Some(f) => f, + None => return Err(LoadErr::MissingRequired("filename".to_string())), + }; + Ok((name, Self::new(&filename))) + } +} + +impl Load for Perlin { + fn load(mut props: Properties, _: &mut Region) -> Result<(Option, Self), LoadErr> { + let name = props.name(); + Ok((name, Self::new())) + } +} + +impl Load for Lerp { + fn load(mut props: Properties, _: &mut Region) -> Result<(Option, Self), LoadErr> { + let primary = props.vec3("primary").unwrap_or(Vec3::one()); + let secondary = props.vec3("secondary").unwrap_or(Vec3::zero()); + let name = props.name(); + Ok((name, Self::new(primary, secondary))) + } +} + +impl Load for SolidColour { + fn load(mut props: Properties, _: &mut Region) -> Result<(Option, Self), LoadErr> { + let colour = props.vec3("colour").unwrap_or(0.5 * Vec3::one()); + let name = props.name(); + Ok((name, Self::new(colour))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn coloured_texture() { + let mut region = Region::new(); + let lookup = Lookup::new(); + let thing = "texture grey ( + type solid + colour 0.5 +)"; + let a = parser::from_str(thing).unwrap(); + let props = Properties::new(&lookup, &a[0]); + let b = ::load(props, &mut region).unwrap(); + println!("{b:?}"); + } + + #[test] + fn checkered_texture() { + let mut region = Region::new(); + let lookup = Lookup::new(); + let thing = "texture checkered ( + type checkered + primary 0.5 0.5 0.0 + secondary 0.0 +)"; + let a = parser::from_str(thing).unwrap(); + let props = Properties::new(&lookup, &a[0]); + let b = ::load(props, &mut region).unwrap(); + println!("{b:?}"); + } +} diff --git a/crates/output/Cargo.toml b/crates/output/Cargo.toml new file mode 100644 index 0000000..ee9146d --- /dev/null +++ b/crates/output/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "output" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +indicatif = "0.17.3" +log = { version = "^0.4.14", features = ["std"] } +chrono = "0.4.19" +image = "0.24" +fern = { version = "0.6", features = ["colored"] } +rt_core = { path = "../rt_core" } + + +[features] +f64 = ["rt_core/f64"] \ No newline at end of file diff --git a/crates/output/src/lib.rs b/crates/output/src/lib.rs new file mode 100644 index 0000000..bc3729a --- /dev/null +++ b/crates/output/src/lib.rs @@ -0,0 +1,136 @@ +use fern::colors::{Color, ColoredLevelConfig}; +use rt_core::Float; + +use std::process; +use std::time::Instant; + +use std::time::Duration; + +pub fn create_logger() { + let colors = ColoredLevelConfig::new() + .error(Color::Red) + .warn(Color::Yellow) + .info(Color::Cyan) + .debug(Color::Magenta); + + fern::Dispatch::new() + .format(move |out, message, record| { + out.finish(format_args!( + "{} {} [{}] {}", + chrono::Local::now().format("%H:%M:%S"), + colors.color(record.level()), + record.target(), + message + )) + }) + .level(log::LevelFilter::Info) + .level_for("winit", log::LevelFilter::Warn) + .chain(std::io::stderr()) + .apply() + .unwrap(); +} + +pub fn get_readable_duration(duration: Duration) -> String { + let days = duration.as_secs() / 86400; + + let days_string = match days { + 0 => "".to_string(), + 1 => format!("{days} day, "), + _ => format!("{days} days, "), + }; + + let hours = (duration.as_secs() - days * 86400) / 3600; + let hours_string = match hours { + 0 => "".to_string(), + 1 => format!("{hours} hour, "), + _ => format!("{hours} hours, "), + }; + + let minutes = (duration.as_secs() - days * 86400 - hours * 3600) / 60; + let minutes_string = match minutes { + 0 => "".to_string(), + 1 => format!("{minutes} minute, "), + _ => format!("{minutes} minutes, "), + }; + + let seconds = duration.as_secs() % 60; + let seconds_string = match seconds { + 0 => "~0 seconds".to_string(), + 1 => format!("{seconds} second"), + _ => format!("{seconds} seconds"), + }; + days_string + &hours_string + &minutes_string + &seconds_string +} + +pub fn rgba_to_rgb(data: &[Float]) -> Vec { + data.iter() + .enumerate() + .filter(|(i, _)| i % 4 != 0) + .map(|(_, v)| *v) + .collect::>() +} + +#[allow(clippy::unnecessary_cast)] +pub fn save_data_to_image( + filename: String, + width: u32, + height: u32, + image: Vec, + gamma: Float, +) { + let split = filename.split('.').collect::>(); + if split.len() != 2 { + println!("Invalid filename: {filename}"); + process::exit(0); + } + + let extension = split[1]; + + match extension { + // TODO HDR + "png" | "jpg" | "jpeg" | "tiff" | "ppm" | "bmp" => { + let data: Vec = image + .into_iter() + .map(|val| (val.powf(1.0 / gamma) * 255.999) as u8) + .collect(); + + image::save_buffer(&filename, &data, width, height, image::ColorType::Rgb8).unwrap(); + } + "exr" => { + // gamma is ignored because of exr + let data: Vec = image.into_iter().map(|val| (val as f32)).collect(); + + let image_buf: image::Rgb32FImage = + image::ImageBuffer::from_raw(width, height, data).unwrap(); + image_buf.save(&filename).unwrap(); + } + _ => { + log::error!("Unable to save file: (unknown filetype .{extension})"); + return; + } + }; + log::info!("Image {filename} saved"); +} + +pub fn print_final_statistics(start: Instant, ray_count: u64, samples: u64) { + let end = Instant::now(); + let duration = end.checked_duration_since(start).unwrap(); + + log::info!( + "Finished rendering:\n\tSamples:\t{samples}\n\tTime taken:\t{}\n\tRays shot:\t{ray_count} @ {:.2} Mray/s", + get_readable_duration(duration), + (ray_count as f64 / duration.as_secs_f64()) / 1000000.0, + ) +} + +pub fn print_render_start(width: u64, height: u64, gamma: f64, samples: Option) -> Instant { + match samples { + Some(samples) => log::info!( + "Render started:\n\tWidth:\t\t{width}\n\tHeight:\t\t{height}\n\tGamma:\t\t{gamma:.3}\n\tSamples:\t{samples}" + ), + None => { + log::info!("Render started:\n\tWidth:\t\t{width}\n\tHeight:\t\t{height}\n\tSamples:\t∞") + } + } + Instant::now() +} diff --git a/crates/region/Cargo.toml b/crates/region/Cargo.toml new file mode 100644 index 0000000..9de23a8 --- /dev/null +++ b/crates/region/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "region" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/crates/region/src/lib.rs b/crates/region/src/lib.rs new file mode 100644 index 0000000..b5323b4 --- /dev/null +++ b/crates/region/src/lib.rs @@ -0,0 +1,341 @@ +#![cfg_attr(not(test), no_std)] +#![feature(strict_provenance)] +#![feature(vec_into_raw_parts)] + +extern crate alloc; + +use alloc::alloc::handle_alloc_error; +use alloc::boxed::Box; +use core::alloc::Layout; +use core::cell::Cell; +use core::mem::{size_of, ManuallyDrop}; +use core::ptr::NonNull; + +pub mod wrappers; +pub use wrappers::*; + +const ALIGNMENT: usize = 32; +const BLOCK_SIZE: usize = 512 * ALIGNMENT; + +const DEFAULT_BLOCK_LAYOUT: Layout = + unsafe { Layout::from_size_align_unchecked(BLOCK_SIZE, ALIGNMENT) }; + +#[repr(C)] +struct Block { + pub head: Cell>, + pub bytes_left: usize, + pub next: Cell>>, + pub size: usize, +} + +fn align_up(current_pos: usize, alignment: usize) -> usize { + let modu = current_pos % alignment; + if modu == 0 { + 0 + } else { + alignment - modu + } +} + +impl Block { + pub fn new() -> Self { + let alloc_ptr = unsafe { alloc::alloc::alloc(DEFAULT_BLOCK_LAYOUT) }; + if alloc_ptr.is_null() { + handle_alloc_error(DEFAULT_BLOCK_LAYOUT); + } + + // unwrap should never fail due to the above + let head = unsafe { Cell::new(NonNull::new(alloc_ptr).unwrap_unchecked()) }; + + Block { + head, + bytes_left: BLOCK_SIZE, + next: Cell::new(None), + size: BLOCK_SIZE, + } + } + pub fn new_with_size(size: usize) -> Self { + let layout = Layout::from_size_align(size, ALIGNMENT) + .expect("size rounded up to the nearest align overflows!"); + + let alloc_ptr = unsafe { alloc::alloc::alloc(layout) }; + if alloc_ptr.is_null() { + handle_alloc_error(layout); + } + + // unwrap can never fail due to the above + let head = unsafe { Cell::new(NonNull::new(alloc_ptr).unwrap_unchecked()) }; + + Block { + head, + bytes_left: size, + next: Cell::new(None), + size, + } + } + pub fn new_block(&mut self, size: Option) -> NonNull { + if self.next.get().is_some() { + unreachable!() + } + + let block = Box::new(if let Some(size) = size { + Block::new_with_size(size) + } else { + Block::new() + }); + + let block = alloc::boxed::Box::::into_raw(block); + + // into_raw cannot return nullptr + let ptr = unsafe { NonNull::new(block.cast()).unwrap_unchecked() }; + self.next = Cell::new(Some(ptr)); + ptr + } + pub fn write_head(&self) -> *mut u8 { + self.head.get().as_ptr() + } + pub fn bytes_left(&self) -> usize { + self.bytes_left + } + pub fn offset_write_head(&mut self, offset: usize) { + *self.head.get_mut() = NonNull::new(unsafe { self.write_head().add(offset) }).unwrap(); + self.bytes_left -= offset; + } +} + +impl Drop for Block { + fn drop(&mut self) { + match self.next.get_mut() { + Some(block) => unsafe { + let _ = Box::from_raw(block.as_ptr()); + }, + None => {} + } + + let ptr = unsafe { self.write_head().sub(self.size - self.bytes_left) }; + let layout = Layout::from_size_align(self.size, ALIGNMENT).unwrap(); + unsafe { + alloc::alloc::dealloc(ptr, layout); + } + } +} + +#[repr(C)] +pub struct Region { + first: NonNull, + current: Cell>, +} + +unsafe impl Send for Region {} +unsafe impl Sync for Region {} + +impl Region { + pub fn new() -> ManuallyDrop { + let block = Box::::into_raw(Box::new(Block::new())); + + // into_raw cannot return nullptr + let first = unsafe { NonNull::new(block.cast()).unwrap_unchecked() }; + let current = Cell::new(first); + + ManuallyDrop::new(Self { first, current }) + } + #[inline(always)] + fn ptr_alloc(&mut self, data: T) -> *mut T { + self.generic_alloc( + data, + |data_ptr, data| unsafe { core::ptr::write(data_ptr, data) }, + size_of::(), + |block, data| Self::ptr_alloc(block, data), + ) + } + #[inline(always)] + fn ptr_slice_alloc(&mut self, data: &[T]) -> *mut [T] { + let ptr = self + .generic_alloc( + data, + |data_ptr, data| unsafe { + core::ptr::copy_nonoverlapping(data.as_ptr(), data_ptr as *mut T, data.len()) + }, + size_of::() * data.len(), + |block, data| Self::ptr_slice_alloc(block, data).cast(), + ) + .cast(); + + unsafe { core::slice::from_raw_parts_mut(ptr, data.len()) } + } + #[inline(always)] + fn generic_alloc *mut T>( + &mut self, + data: T, + write: F, + size: usize, + alloc: A, + ) -> *mut T { + let align = core::mem::align_of::(); + + let block = unsafe { self.current.get_mut().as_mut() }; + + // write head offset taking into consideration padding before cause of alignment + let write_offset = align_up(block.write_head().addr(), align); + let total_size = size + write_offset; + + if total_size > block.bytes_left() { + // check it is possible to fit data in block to avoid infinite allocation + if size > BLOCK_SIZE { + let ptr = block.new_block(Some(size)); + self.current = Cell::new(ptr); + + return alloc(self, data); + } + + // allocate new block in region + let ptr = block.new_block(None); + self.current = Cell::new(ptr); + return alloc(self, data); + } + + // offset ptr head to take into consideration alignment of allocated type + block.offset_write_head(write_offset); + + // get data ptr + let data_ptr = block.write_head() as *mut T; + + // write data + write(data_ptr, data); + + // offset write head since we have moved data to block + block.offset_write_head(size); + + data_ptr + } + pub fn alloc(&mut self, data: T) -> RegionUniq { + let data_ptr = self.ptr_alloc(data); + unsafe { RegionUniq(&mut *data_ptr) } + } + pub fn alloc_slice(&mut self, data: &[T]) -> RegionUniqSlice { + let data_ptr = self.ptr_slice_alloc(data); + unsafe { RegionUniqSlice(&mut *data_ptr) } + } +} + +impl Drop for Region { + fn drop(&mut self) { + unsafe { + let _ = Box::from_raw(self.first.as_ptr()); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn create_region() { + let mut region = Region::new(); + unsafe { core::mem::ManuallyDrop::<_>::drop(&mut region) } + } + + #[test] + fn single_alloc() { + let mut region = Region::new(); + + #[derive(Debug, PartialEq, Clone)] + enum TestEnum { + Val1(u32), + Val2, + Val3(f64), + } + #[derive(Debug, PartialEq, Clone)] + struct TestStruct { + a: f64, + b: [u32; 5], + c: Vec, + } + let vec = vec![TestEnum::Val1(37), TestEnum::Val2, TestEnum::Val3(-1.3)]; + let (vec_ptr, len, cap) = vec.into_raw_parts(); + + let data = TestStruct { + a: 2.713, + b: [3, 7, 123, 43124, 0], + c: unsafe { Vec::from_raw_parts(vec_ptr, len, cap) }, + }; + let data = region.alloc(data); + let tester = TestStruct { + a: 2.713, + b: [3, 7, 123, 43124, 0], + c: unsafe { Vec::from_raw_parts(vec_ptr, len, cap) }, + }; + + assert_eq!(tester.c.as_ptr(), data.c.as_ptr()); + assert_eq!(tester, *data); + + unsafe { core::mem::ManuallyDrop::<_>::drop(&mut region) } + } + + #[test] + fn multi_alloc() { + let mut region = Region::new(); + let mut data = Vec::new(); + + const NUM_ALLOC: usize = 4; + + for i in 0..NUM_ALLOC { + data.push(*region.alloc([i, i + 1, i + 2, i + 3, i + 4, i + 5, i + 6, i + 7])); + } + for (i, element) in data.into_iter().enumerate() { + assert_eq!( + [i, i + 1, i + 2, i + 3, i + 4, i + 5, i + 6, i + 7], + element + ) + } + unsafe { core::mem::ManuallyDrop::<_>::drop(&mut region) } + } + + #[test] + fn multiple_blocks() { + let mut region = Region::new(); + let mut data = Vec::new(); + + const NUM_ALLOC: usize = BLOCK_SIZE * 10; + + for i in 0..NUM_ALLOC { + data.push(*region.alloc([i, i + 1, i + 2, i + 3, i + 4, i + 5, i + 6, i + 7])); + } + for (i, element) in data.into_iter().enumerate() { + assert_eq!( + [i, i + 1, i + 2, i + 3, i + 4, i + 5, i + 6, i + 7], + element + ) + } + unsafe { core::mem::ManuallyDrop::<_>::drop(&mut region) } + } + + #[test] + fn slice_allocation() { + let mut region = Region::new(); + let data: Vec = vec![1, 2, 3, 4, 5]; + + let a = region.alloc_slice(&data); + assert_eq!([1, 2, 3, 4, 5], *a); + unsafe { core::mem::ManuallyDrop::<_>::drop(&mut region) } + } + + #[test] + fn large_allocation() { + let mut region = Region::new(); + let data = [4u8; 100_000]; // investige overflow with larger values + let a = region.alloc(data); + assert_eq!(data, *a); + unsafe { core::mem::ManuallyDrop::<_>::drop(&mut region) } + } + + #[test] + fn large_slice_allocation() { + let mut region = Region::new(); + let data = [4u8; 400_000]; + let a = region.alloc_slice(&data); + assert_eq!(data, *a); + unsafe { core::mem::ManuallyDrop::<_>::drop(&mut region) } + } +} diff --git a/crates/region/src/wrappers.rs b/crates/region/src/wrappers.rs new file mode 100644 index 0000000..d20d9d6 --- /dev/null +++ b/crates/region/src/wrappers.rs @@ -0,0 +1,93 @@ +use core::ops::{Deref, DerefMut}; +use core::ptr::NonNull; +pub struct RegionUniq<'a, T>(pub(crate) &'a mut T); + +impl<'a, T: Sync> RegionUniq<'a, T> { + pub fn shared(self) -> RegionRes { + // .as_mut_ptr() not in stable + unsafe { RegionRes(NonNull::new_unchecked(self.0 as *mut T)) } + } +} + +impl<'a, T> Deref for RegionUniq<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.0 + } +} + +impl<'a, T> DerefMut for RegionUniq<'a, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.0 + } +} + +pub struct RegionRes(pub(crate) NonNull); + +unsafe impl Sync for RegionRes {} + +impl Deref for RegionRes { + type Target = T; + + fn deref(&self) -> &Self::Target { + unsafe { &*self.0.as_ptr() } + } +} + +// investige how this compares to #[derive(Clone)] +impl core::clone::Clone for RegionRes { + fn clone(&self) -> Self { + Self(self.0) + } +} + +pub struct RegionUniqSlice<'a, T>(pub(crate) &'a mut [T]); + +impl<'a, T: Sync> RegionUniqSlice<'a, T> { + pub fn shared(self) -> RegionResSlice { + unsafe { RegionResSlice(NonNull::new_unchecked(self.0.as_mut_ptr()), self.0.len()) } + } + pub fn zero_slice(&self) -> RegionResSlice { + unsafe { RegionResSlice(NonNull::new_unchecked(self.0.as_ptr() as *mut _), 0) } + } +} + +impl<'a, T> Deref for RegionUniqSlice<'a, T> { + type Target = [T]; + + fn deref(&self) -> &Self::Target { + self.0 + } +} + +impl<'a, T> DerefMut for RegionUniqSlice<'a, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.0 + } +} + +pub struct RegionResSlice(pub(crate) NonNull, pub(crate) usize); + +unsafe impl Sync for RegionResSlice {} + +impl Deref for RegionResSlice { + type Target = [T]; + + fn deref(&self) -> &Self::Target { + unsafe { core::slice::from_raw_parts(self.0.as_ptr(), self.1) } + } +} + +impl DerefMut for RegionResSlice { + fn deref_mut(&mut self) -> &mut [T] { + unsafe { core::slice::from_raw_parts_mut(self.0.as_mut(), self.1) } + } +} + +// again investigate #[derive(Clone)] +impl core::clone::Clone for RegionResSlice { + fn clone(&self) -> Self { + Self(self.0, self.1) + } +} diff --git a/crates/rt_core/Cargo.toml b/crates/rt_core/Cargo.toml new file mode 100644 index 0000000..8224612 --- /dev/null +++ b/crates/rt_core/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "rt_core" +version = "0.1.0" +edition = "2021" + +[dependencies] +rand = { version = "0.8.3", features = [ "small_rng" ] } + +[features] +f64 = [] \ No newline at end of file diff --git a/crates/rt_core/src/acceleration.rs b/crates/rt_core/src/acceleration.rs new file mode 100644 index 0000000..6663745 --- /dev/null +++ b/crates/rt_core/src/acceleration.rs @@ -0,0 +1,32 @@ +use crate::*; + +pub trait AccelerationStructure: Sync { + type Object: Primitive; + type Material: Scatter; + type Sky: NoHit; + fn get_intersection_candidates(&self, ray: &Ray) -> Vec<(usize, usize)>; + + fn check_hit_index( + &self, + ray: &Ray, + object_index: usize, + ) -> Option>; + + fn check_hit(&self, ray: &Ray) -> (SurfaceIntersection, usize); + + fn get_samplable(&self) -> &[usize] { + unimplemented!() + } + + fn get_object(&self, _index: usize) -> Option<&Self::Object> { + unimplemented!() + } + fn get_pdf_from_index( + &self, + last_hit: &Hit, + light_hit: &Hit, + sampled_dir: Vec3, + index: usize, + ) -> Float; + fn sky(&self) -> &Self::Sky; +} diff --git a/rt_core/src/lib.rs b/crates/rt_core/src/lib.rs similarity index 60% rename from rt_core/src/lib.rs rename to crates/rt_core/src/lib.rs index 845fe88..f981836 100644 --- a/rt_core/src/lib.rs +++ b/crates/rt_core/src/lib.rs @@ -13,18 +13,25 @@ pub use sampler::*; pub use vec::*; #[cfg(all(feature = "f64"))] -pub type Float = f64; -#[cfg(all(feature = "f64"))] -pub use std::f64::consts::PI; -#[cfg(all(feature = "f64"))] -pub const EPSILON: Float = 5.58E-8; +pub mod f64_stuff { + pub type Float = f64; + pub use std::f64::consts::*; + pub use std::f64::*; + pub const EPSILON: Float = 5.58E-8; +} #[cfg(not(feature = "f64"))] -pub type Float = f32; -#[cfg(not(feature = "f64"))] -pub use std::f32::consts::PI; +pub mod f32_stuff { + pub type Float = f32; + pub use std::f32::consts::*; + pub use std::f32::*; + pub const EPSILON: Float = 3.0E-4; +} + #[cfg(not(feature = "f64"))] -pub const EPSILON: Float = 3.0E-4; +pub use f32_stuff::*; +#[cfg(all(feature = "f64"))] +pub use f64_stuff::*; #[inline] pub fn power_heuristic(pdf_a: Float, pdf_b: Float) -> Float { diff --git a/rt_core/src/material.rs b/crates/rt_core/src/material.rs similarity index 84% rename from rt_core/src/material.rs rename to crates/rt_core/src/material.rs index bcfe3d5..4e2f68b 100644 --- a/rt_core/src/material.rs +++ b/crates/rt_core/src/material.rs @@ -1,6 +1,7 @@ use crate::{Float, Hit, Ray, Vec3}; -pub trait Scatter { +// wo (and ray.direction in scatter_ray) points towards the surface and wi away by convention +pub trait Scatter: Sync { fn scatter_ray(&self, _ray: &mut Ray, _hit: &Hit) -> bool { true } diff --git a/rt_core/src/primitive.rs b/crates/rt_core/src/primitive.rs similarity index 76% rename from rt_core/src/primitive.rs rename to crates/rt_core/src/primitive.rs index 994ce93..14a9414 100644 --- a/rt_core/src/primitive.rs +++ b/crates/rt_core/src/primitive.rs @@ -1,5 +1,4 @@ use crate::{Float, Ray, Scatter, Vec2, Vec3}; -use std::sync::Arc; pub struct Hit { pub t: Float, @@ -10,12 +9,12 @@ pub struct Hit { pub out: bool, } -pub struct SurfaceIntersection { +pub struct SurfaceIntersection<'a, M: Scatter> { pub hit: Hit, - pub material: Arc, + pub material: &'a M, } -impl SurfaceIntersection +impl<'a, M> SurfaceIntersection<'a, M> where M: Scatter, { @@ -26,7 +25,7 @@ where normal: Vec3, uv: Option, out: bool, - material: &Arc, + material: &'a M, ) -> Self { SurfaceIntersection { hit: Hit { @@ -37,13 +36,15 @@ where uv, out, }, - material: material.clone(), + material, } } } -pub trait Primitive { - fn get_int(&self, _: &Ray) -> Option>; +pub trait Primitive: Sync { + type Material: Scatter; + + fn get_int(&self, _: &Ray) -> Option>; fn does_int(&self, ray: &Ray) -> bool { self.get_int(ray).is_some() } diff --git a/crates/rt_core/src/ray.rs b/crates/rt_core/src/ray.rs new file mode 100644 index 0000000..9170cd5 --- /dev/null +++ b/crates/rt_core/src/ray.rs @@ -0,0 +1,51 @@ +use crate::{Float, Vec3}; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Ray { + pub origin: Vec3, + pub direction: Vec3, + pub d_inverse: Vec3, + pub shear: Vec3, + pub time: Float, +} + +impl Ray { + pub fn new(origin: Vec3, mut direction: Vec3, time: Float) -> Self { + direction.normalise(); + + let max_axis = + if direction.x.abs() > direction.y.abs() && direction.x.abs() > direction.z.abs() { + 0 + } else if direction.y.abs() > direction.z.abs() { + 1 + } else { + 2 + }; + + let mut swaped_dir = direction; + match max_axis { + 0 => { + std::mem::swap(&mut swaped_dir.x, &mut swaped_dir.z); + } + 1 => { + std::mem::swap(&mut swaped_dir.x, &mut swaped_dir.z); + } + _ => {} + } + let shear_x = -swaped_dir.x / swaped_dir.z; + let shear_y = -swaped_dir.y / swaped_dir.z; + let shear_z = 1.0 / swaped_dir.z; + + Ray { + origin, + direction, + d_inverse: Vec3::new(1.0 / direction.x, 1.0 / direction.y, 1.0 / direction.z), + shear: Vec3::new(shear_x, shear_y, shear_z), + time, + } + } + + pub fn at(&self, t: Float) -> Vec3 { + self.origin + self.direction * t + } +} diff --git a/crates/rt_core/src/sampler.rs b/crates/rt_core/src/sampler.rs new file mode 100644 index 0000000..6754747 --- /dev/null +++ b/crates/rt_core/src/sampler.rs @@ -0,0 +1,17 @@ +use crate::{Float, Ray, Scatter, SurfaceIntersection, Vec3}; + +pub trait NoHit: Sync { + fn get_colour(&self, ray: &Ray) -> Vec3; + fn pdf(&self, _: Vec3) -> Float { + unimplemented!() + } + fn can_sample(&self) -> bool { + false + } + fn sample(&self) -> Vec3 { + unimplemented!() + } + fn get_si(&self, _: &Ray) -> SurfaceIntersection { + unimplemented!() + } +} diff --git a/rt_core/src/vec.rs b/crates/rt_core/src/vec.rs similarity index 89% rename from rt_core/src/vec.rs rename to crates/rt_core/src/vec.rs index e910269..d085665 100644 --- a/rt_core/src/vec.rs +++ b/crates/rt_core/src/vec.rs @@ -136,6 +136,31 @@ impl Vec3 { Vec3::new(0.0, 0.0, 0.0) } + #[inline] + pub fn x() -> Self { + Vec3::new(1.0, 0.0, 0.0) + } + + #[inline] + pub fn y() -> Self { + Vec3::new(0.0, 1.0, 0.0) + } + + #[inline] + pub fn z() -> Self { + Vec3::new(0.0, 0.0, 1.0) + } + + #[inline] + pub fn from_spherical( + sin_theta: Float, + cos_theta: Float, + sin_phi: Float, + cos_phi: Float, + ) -> Self { + Vec3::new(sin_theta * cos_phi, sin_theta * sin_phi, cos_theta) + } + #[inline] pub fn dot(&self, other: Self) -> Float { self.x * other.x + self.y * other.y + self.z * other.z @@ -173,14 +198,15 @@ impl Vec3 { pub fn abs(self) -> Self { Vec3::new(self.x.abs(), self.y.abs(), self.z.abs()) } + // note: self is pointing away from surface #[inline] pub fn reflect(&mut self, normal: Self) { - *self -= 2.0 * self.dot(normal) * normal + *self = self.reflected(normal) } #[inline] pub fn reflected(&self, normal: Self) -> Self { - *self - 2.0 * self.dot(normal) * normal + 2.0 * self.dot(normal) * normal - *self } #[inline] @@ -237,6 +263,16 @@ impl Vec2 { Vec2::new(0.0, 0.0) } + #[inline] + pub fn x() -> Self { + Vec2::new(1.0, 0.0) + } + + #[inline] + pub fn y() -> Self { + Vec2::new(0.0, 1.0) + } + #[inline] pub fn dot(&self, other: Self) -> Float { self.x * other.x + self.y * other.y @@ -323,3 +359,15 @@ impl std::fmt::Display for Vec3 { write!(f, "({}, {}, {})", self.x, self.y, self.z) } } + +impl From<[Float; 3]> for Vec3 { + fn from(vec: [Float; 3]) -> Self { + Vec3::new(vec[0], vec[1], vec[2]) + } +} + +impl From<[Float; 2]> for Vec2 { + fn from(vec: [Float; 2]) -> Self { + Vec2::new(vec[0], vec[1]) + } +} diff --git a/download_res.sh b/download_res.sh deleted file mode 100755 index c7ca526..0000000 --- a/download_res.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -mkdir frontend/res \ No newline at end of file diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml deleted file mode 100644 index 817eda1..0000000 --- a/frontend/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "frontend" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -chrono = "0.4.19" -image = "0.23.14" -implementations = {path = "../implementations"} -rand = { version = "0.8.3", features = [ "small_rng" ] } -rand_seeder = "0.2.2" -rt_core = {path = "../rt_core"} -vulkano = "0.28.0" -vulkano-win = "0.28.0" -vulkano-shaders = "0.28.0" -wavefront_obj = "10.0.0" -winit = "0.26.1" \ No newline at end of file diff --git a/frontend/src/generate.rs b/frontend/src/generate.rs deleted file mode 100644 index dece113..0000000 --- a/frontend/src/generate.rs +++ /dev/null @@ -1,294 +0,0 @@ -use crate::{scene::Scene, utility::create_bvh_with_info, *}; -use implementations::{ - random_sampler::RandomSampler, split::SplitType, AllMaterials, AllPrimitives, AllTextures, Bvh, -}; -use rand::{distributions::Alphanumeric, rngs::SmallRng, thread_rng, Rng, SeedableRng}; -use rand_seeder::Seeder; -use rt_core::Float; - -type MaterialType = AllMaterials; -type PrimitiveType = AllPrimitives; -type BvhType = Bvh; -pub type SceneType = Scene; - -pub fn get_seed(length: usize) -> String { - let mut rng = SmallRng::from_rng(thread_rng()).unwrap(); - std::iter::repeat(()) - .map(|()| rng.sample(Alphanumeric)) - .map(char::from) - .take(length) - .collect() -} - -pub fn classic(bvh_type: SplitType, aspect_ratio: Float, seed: Option) -> SceneType { - let mut primitives = Vec::new(); - - let ground = sphere!(0, -1000, 0, 1000, &diffuse!(0.5, 0.5, 0.5, 0.5)); - - let sphere_one = sphere!(0, 1, 0, 1, &refract!(&solid_colour!(colour!(1)), 1.5)); - - let sphere_two = sphere!(-4, 1, 0, 1, &diffuse!(0.4, 0.2, 0.1, 0.5)); - - let sphere_three = sphere!(4, 1, 0, 1, &reflect!(&solid_colour!(0.7, 0.6, 0.5), 0)); - - primitives.push(ground); - primitives.push(sphere_one); - primitives.push(sphere_two); - primitives.push(sphere_three); - let seed = match seed { - Some(seed) => seed, - None => get_seed(32), - }; - - println!("\tseed: {}", seed); - let mut rng: SmallRng = Seeder::from(seed).make_rng(); - - for a in -11..11 { - for b in -11..11 { - let center = position!( - a as Float + 0.9 * rng.gen::(), - 0.2, - b as Float + 0.9 * rng.gen::() - ); - - if (center - position!(4.0, 0.2, 0.0)).mag() > 0.9 { - let choose_material: Float = rng.gen(); - let colour = colour!(rng.gen::(), rng.gen::(), rng.gen::()); - - let sphere; - - if choose_material < 0.8 { - sphere = sphere!(center, 0.2, &diffuse!(&solid_colour!(colour), 0.5)); - } else if choose_material < 0.95 { - sphere = sphere!( - center, - 0.2, - &reflect!(&solid_colour!(colour), rng.gen::() / 2.0) - ); - } else { - sphere = sphere!(center, 0.2, &refract!(&solid_colour!(colour!(1)), 1.5)); - } - primitives.push(sphere); - } - } - } - - let sky = sky!(&texture_lerp!(colour!(0.5, 0.7, 1), colour!(1))); - - let camera = camera!( - position!(13, 2, -3), - position!(0, 0, 0), - position!(0, 1, 0), - 29, - aspect_ratio, - 0.1, - 10 - ); - - let bvh = create_bvh_with_info(primitives, bvh_type); - - scene!(camera, sky, random_sampler!(), bvh) -} - -pub fn furnace(bvh_type: SplitType, aspect_ratio: Float) -> SceneType { - let mut primitives = Vec::new(); - - let inner = &diffuse!(1, 1, 1, 0.9); - let emit = &emit!(&solid_colour!(colour!(1)), 1); - - primitives.push(sphere!(0, 0, 0, 0.5, inner)); - primitives.push(sphere!(0, 0, 0, 10, emit)); - - let sky = sky!(); - - let camera = camera!( - position!(3, 0, 0), - position!(0, 0, 0), - position!(0, 1, 0), - 40, - aspect_ratio, - 0, - 10 - ); - - let bvh = create_bvh_with_info(primitives, bvh_type); - scene!(camera, sky, random_sampler!(), bvh) -} - -pub fn overshadowed(bvh_type: SplitType, aspect_ratio: Float) -> SceneType { - let mut primitives = Vec::new(); - - let ground = sphere!(0, -1000, 0, 1000, &diffuse!(0.5, 0.5, 0.5, 0.5)); - - let glowy = sphere!(0, 0.5, 0, 0.5, &emit!(&solid_colour!(colour!(1)), 1.5)); - - let cube = aacuboid!( - -0.5, - 0.1, - -0.5, - -0.4, - 0.2, - -0.4, - &diffuse!(0.5, 0.5, 0.5, 0.5) - ); - - primitives.push(ground); - primitives.push(glowy); - primitives.extend(cube); - - let camera = camera!( - position!(-5, 3, -3), - position!(0, 0.5, 0), - position!(0, 1, 0), - 34, - aspect_ratio, - 0, - 10 - ); - - let bvh = create_bvh_with_info(primitives, bvh_type); - - scene!(camera, sky!(), random_sampler!(), bvh) -} - -/*pub fn scene_six(bvh_type: SplitType, aspect_ratio: Float) -> SceneType { - let mut primitives = Vec::new(); - - let ground = sphere!(0, -1000, 0, 1000, &diffuse!(0.5, 0.5, 0.5, 0.5)); - - let glowy = sphere!(5, 3.5, 5, 1.5, &emit!(&solid_colour!(colour!(1)), 5)); - - primitives.push(ground); - primitives.push(glowy); - primitives.extend(model!( - "res/dragon.obj", - &refract!(&solid_colour!(1, 1, 1), 1.52) - )); - - let camera = camera!( - position!(-20, 20, -25), - position!(0, 3.5, 0), - position!(0, 1, 0), - 34, - aspect_ratio, - 0, - 10 - ); - - let bvh = create_bvh_with_info(primitives, bvh_type); - - scene!(camera, sky!(), random_sampler!(), bvh) -}*/ - -pub fn coffee(bvh_type: SplitType, aspect_ratio: Float) -> SceneType { - let mut primitives = Vec::new(); - - let diffuse = &diffuse!(1, 1, 1, 0.9); - - let orange = &std::sync::Arc::new(implementations::AllMaterials::Phong( - implementations::Phong::new(&solid_colour!(0.592, 0.192, 0.0), 0.2, 0.8, 1250.0), - )); - - let floor = &std::sync::Arc::new(implementations::AllMaterials::Phong( - implementations::Phong::new(&solid_colour!(colour!(1)), 0.1, 0.9, 1250.0), - )); - - let black = &std::sync::Arc::new(implementations::AllMaterials::Phong( - implementations::Phong::new(&solid_colour!(colour!(0.006)), 0.1, 0.9, 1250.0), - )); - - let glass = &refract!(&solid_colour!(colour!(1)), 1.5); - - let left_light = &emit!(&solid_colour!(colour!(1)), 3.0 * 1.0); - let right_light = &emit!(&solid_colour!(colour!(1)), 1.0 * 1.0); - let left_reflection = &emit!(&solid_colour!(colour!(1)), 0.75 * 1.0); - - let metal = &reflect!(&solid_colour!(colour!(1)), 0); - - let materials = vec![ - (diffuse.clone(), "default"), - (orange.clone(), "Plastic_Orange"), - (glass.clone(), "Glass"), - (left_light.clone(), "Left_Light"), - (left_reflection.clone(), "Left_Reflection"), - (right_light.clone(), "Right_Light"), - (floor.clone(), "Floor"), - (black.clone(), "Plastic_Black"), - (metal.clone(), "Metal"), - ]; - primitives.extend(crate::load_model::load_model_with_materials( - "../../../render_scenes/coffee.obj", - &materials, - )); - - let sky = sky!(); - - let camera = camera!( - position!(0, 0.17 * 10.0, (2.0386 - 1.0) * 10.0), - position!(0, 0.150512 * 10.0, 0), - position!(0, 1, 0), - 40, - aspect_ratio, - 0, - 0.0075 * 10.0 - ); - - let bvh = create_bvh_with_info(primitives, bvh_type); - scene!(camera, sky, random_sampler!(), bvh) -} - -pub fn cornell(bvh_type: SplitType, aspect_ratio: Float) -> SceneType { - let mut primitives = Vec::new(); - - let red = &diffuse!(0.65, 0.05, 0.05, 0.0); - let white = &diffuse!(0.73, 0.73, 0.73, 0.0); - let green = &diffuse!(0.12, 0.45, 0.15, 0.0); - let light = &emit!(&solid_colour!(colour!(1)), 15); - - primitives.extend(aarect!(0.0, 0.0, 555.0, 555.0, 555.0, &axis!(X), green)); - primitives.extend(aarect!(0.0, 0.0, 555.0, 555.0, 0.0, &axis!(X), red)); - - primitives.extend(aarect!(0.0, 0.0, 555.0, 555.0, 555.0, &axis!(Y), white)); - primitives.extend(aarect!(0.0, 0.0, 555.0, 555.0, 0.0, &axis!(Y), white)); - - primitives.extend(aarect!(0.0, 0.0, 555.0, 555.0, 555.0, &axis!(Z), white)); - primitives.extend(aarect!(213.0, 227.0, 343.0, 332.0, 554.0, &axis!(Y), light)); - - primitives.extend(cuboid!( - 265.0, - 0.0, - 295.0, - 430.0, - 330.0, - 460.0, - rotation!(0, 15, 0, D), - white - )); - - primitives.extend(cuboid!( - 130.0, - 0.0, - 65.0, - 295.0, - 165.0, - 230.0, - rotation!(0, -18, 0, D), - white - )); - - let sky = sky!(); - - let camera = camera!( - position!(278, 278, -800), - position!(278, 278, 0), - position!(0, 1, 0), - 40, - aspect_ratio, - 0, - 10 - ); - - let bvh = create_bvh_with_info(primitives, bvh_type); - - scene!(camera, sky, random_sampler!(), bvh) -} diff --git a/frontend/src/macros.rs b/frontend/src/macros.rs deleted file mode 100644 index 3482c97..0000000 --- a/frontend/src/macros.rs +++ /dev/null @@ -1,804 +0,0 @@ -//------ -// MISC -//------ -#[macro_export] -macro_rules! position { - ($x:expr, $y:expr, $z:expr) => { - rt_core::vec::Vec3::new( - $x as rt_core::Float, - $y as rt_core::Float, - $z as rt_core::Float, - ) - }; - ($x:expr, $y:expr) => { - rt_core::vec::Vec2::new($x as rt_core::Float, $y as rt_core::Float) - }; -} - -#[macro_export] -macro_rules! colour { - ($r:expr,$g:expr,$b:expr) => { - rt_core::Vec3::new( - $r as rt_core::Float, - $g as rt_core::Float, - $b as rt_core::Float, - ) - }; - ($value:expr) => { - rt_core::Vec3::new( - $value as rt_core::Float, - $value as rt_core::Float, - $value as rt_core::Float, - ) - }; -} - -#[macro_export] -macro_rules! rotation { - ($x:expr, $y:expr, $z:expr, D) => { - rt_core::vec::Vec3::new( - $x as rt_core::Float * rt_core::PI / 180.0, - $y as rt_core::Float * rt_core::PI / 180.0, - $z as rt_core::Float * rt_core::PI / 180.0, - ) - }; - ($x:expr, $y:expr, $z:expr, R) => { - rt_core::vec::Vec3::new( - $x as rt_core::Float, - $y as rt_core::Float, - $z as rt_core::Float, - ) - }; -} - -#[macro_export] -macro_rules! axis { - (X) => { - implementations::Axis::X - }; - (Y) => { - implementations::Axis::Y - }; - (Z) => { - implementations::Axis::Z - }; -} - -//----- -// TEXTURES -//----- -#[macro_export] -macro_rules! solid_colour { - ($r:expr, $g:expr, $b:expr) => { - std::sync::Arc::new(implementations::AllTextures::SolidColour( - implementations::SolidColour::new(colour!($r, $g, $b)), - )) - }; - ($colour:expr) => { - std::sync::Arc::new(implementations::AllTextures::SolidColour( - implementations::SolidColour::new($colour), - )) - }; -} - -#[macro_export] -macro_rules! image { - ($filepath:expr) => { - std::sync::Arc::new(implementations::AllTextures::ImageTexture( - implementations::ImageTexture::new($filepath), - )) - }; -} - -#[macro_export] -macro_rules! checkered { - ($colour_one:expr, $colour_two:expr) => { - std::sync::Arc::new(implementations::AllTextures::CheckeredTexture( - rt_core::CheckeredTexture::new($colour_one, $colour_two), - )) - }; - ($r1:expr, $g1:expr, $b1:expr, $r2:expr, $g2:expr, $b2:expr) => { - std::sync::Arc::new(implementations::AllTextures::CheckeredTexture( - rt_core::CheckeredTexture::new(colour!($r1, $g1, $b1), colour!($r2, $g2, $b2)), - )) - }; -} - -#[macro_export] -macro_rules! texture_lerp { - ($colour_one:expr, $colour_two:expr) => { - std::sync::Arc::new(implementations::AllTextures::Lerp( - implementations::Lerp::new($colour_one, $colour_two), - )) - }; - ($r1:expr, $g1:expr, $b1:expr, $r2:expr, $g2:expr, $b2:expr) => { - std::sync::Arc::new(implementations::AllTextures::Lerp(rt_core::Lerp::new( - colour!($r1, $g1, $b1), - colour!($r2, $g2, $b2), - ))) - }; -} - -#[macro_export] -macro_rules! perlin { - () => { - std::sync::Arc::new(implementations::AllTextures::Perlin(Box::new( - rt_core::Perlin::new(), - ))) - }; -} - -//----- -// MATERIALS -//----- -#[macro_export] -macro_rules! diffuse { - ($r:expr,$g:expr,$b:expr, $absorption:expr) => { - std::sync::Arc::new(implementations::AllMaterials::Lambertian( - implementations::Lambertian::new( - &std::sync::Arc::new(implementations::AllTextures::SolidColour( - implementations::SolidColour::new(colour!($r, $g, $b)), - )), - $absorption as rt_core::Float, - ), - )) - }; - ($texture:expr,$absorption:expr) => { - std::sync::Arc::new(implementations::AllMaterials::Lambertian( - implementations::Lambertian::new($texture, $absorption as rt_core::Float), - )) - }; -} - -#[macro_export] -macro_rules! reflect { - ($r:expr,$g:expr,$b:expr, $fuzz:expr) => { - std::sync::Arc::new(implementations::AllMaterials::Reflect( - rt_core::Reflect::new( - &Arc::new(implementations::AllTextures::SolidColour( - rt_core::SolidColour::new(colour!($r, $g, $b)), - )), - $fuzz as rt_core::Float, - ), - )); - }; - ($texture:expr,$fuzz:expr) => { - std::sync::Arc::new(implementations::AllMaterials::Reflect( - implementations::Reflect::new($texture, $fuzz as rt_core::Float), - )) - }; -} - -#[macro_export] -macro_rules! refract { - ($r:expr,$g:expr,$b:expr, $eta:expr) => { - std::sync::Arc::new(implementations::AllMaterials::Refract( - rt_core::Refract::new( - &std::sync::Arc::new(implementations::AllTextures::SolidColour( - rt_core::SolidColour::new(colour!($r, $g, $b)), - )), - $eta as rt_core::Float, - ), - )) - }; - ($texture:expr,$eta:expr) => { - std::sync::Arc::new(implementations::AllMaterials::Refract( - implementations::Refract::new($texture, $eta as rt_core::Float), - )) - }; -} - -#[macro_export] -macro_rules! emit { - ($r:expr,$g:expr,$b:expr, $strength:expr) => { - std::sync::Arc::new(implementations::AllMaterials::Emit(rt_core::Emit::new( - &std::sync::Arc::new(Texture::SolidColour(SolidColour::new(colour!($r, $g, $b)))), - $strength as rt_core::Float, - ))); - }; - ($texture:expr,$strength:expr) => { - std::sync::Arc::new(implementations::AllMaterials::Emit( - implementations::Emit::new($texture, $strength as rt_core::Float), - )) - }; -} - -//----- -// PRIMITIVES -//----- -#[macro_export] -macro_rules! sphere { - ($x:expr, $y:expr, $z:expr, $radius:expr, $material:expr) => { - implementations::AllPrimitives::Sphere(implementations::sphere::Sphere::new( - position!($x, $y, $z), - $radius as Float, - $material, - )) - }; - ($position:expr, $radius:expr, $material:expr) => { - implementations::AllPrimitives::Sphere(implementations::sphere::Sphere::new( - $position, - $radius as rt_core::Float, - $material, - )) - }; -} - -#[macro_export] -macro_rules! aarect { - ($point_one:expr, $point_two:expr, $axis_value:expr, $axis:expr, $material:expr) => {{ - let point_three = implementations::Axis::point_from_2d( - rt_core::Vec2::new($point_one.x, $point_two.y), - $axis, - $axis_value, - ); - let point_four = implementations::Axis::point_from_2d( - rt_core::Vec2::new($point_two.x, $point_one.y), - $axis, - $axis_value, - ); - let point_one = implementations::Axis::point_from_2d($point_one, $axis, $axis_value); - let point_two = implementations::Axis::point_from_2d($point_two, $axis, $axis_value); - let mut vec = Vec::new(); - vec.push(triangle!(point_one, point_two, point_three, $material)); - vec.push(triangle!(point_one, point_two, point_four, $material)); - - vec - }}; - ($x1:expr, $y1:expr, $x2:expr, $y2:expr, $axis_value:expr, $axis:expr, $material:expr, $cp:expr) => {{ - let point_three = - implementations::Axis::point_from_2d(&rt_core::Vec2::new($x1, $y2), $axis, $axis_value); - let point_four = - implementations::Axis::point_from_2d(&rt_core::Vec2::new($x2, $y1), $axis, $axis_value); - let point_one = - implementations::Axis::point_from_2d(&rt_core::Vec2::new($x1, $y1), $axis, $axis_value); - let point_two = - implementations::Axis::point_from_2d(&rt_core::Vec2::new($x2, $y2), $axis, $axis_value); - let mut vec = Vec::new(); - let nr = 0.5 * (point_one + point_two) - $cp; - vec.push(triangle!(point_one, point_two, point_three, $material, nr)); - vec.push(triangle!(point_one, point_two, point_four, $material, nr)); - - vec - }}; - ($x1:expr, $y1:expr, $x2:expr, $y2:expr, $axis_value:expr, $axis:expr, $material:expr) => {{ - let point_three = - implementations::Axis::point_from_2d(&rt_core::Vec2::new($x1, $y2), $axis, $axis_value); - let point_four = - implementations::Axis::point_from_2d(&rt_core::Vec2::new($x2, $y1), $axis, $axis_value); - let point_one = - implementations::Axis::point_from_2d(&rt_core::Vec2::new($x1, $y1), $axis, $axis_value); - let point_two = - implementations::Axis::point_from_2d(&rt_core::Vec2::new($x2, $y2), $axis, $axis_value); - - let mut vec = Vec::new(); - vec.push(triangle!(point_one, point_two, point_three, $material)); - vec.push(triangle!(point_one, point_two, point_four, $material)); - - vec - }}; -} - -#[macro_export] -macro_rules! rect { - ($point_one:expr, $point_two:expr, $axis_value:expr, $axis:expr, $rotation:expr, $material:expr) => {{ - let center_point = 0.5 * ($point_one + $point_two); - let point_three = implementations::Axis::point_from_2d( - rt_core::Vec2::new($point_one.x, $point_two.y), - $axis, - $axis_value, - ); - let point_four = implementations::Axis::point_from_2d( - rt_core::Vec2::new($point_two.x, $point_one.y), - $axis, - $axis_value, - ); - let point_one = implementations::Axis::point_from_2d($point_one, $axis, $axis_value); - let point_two = implementations::Axis::point_from_2d($point_two, $axis, $axis_value); - - let sin_rot = rt_core::Vec3::new($rotation.x.sin(), $rotation.y.sin(), $rotation.z.sin()); - let cos_rot = rt_core::Vec3::new($rotation.x.cos(), $rotation.y.cos(), $rotation.z.cos()); - - let point_one = - $crate::utility::rotate_around_point(point_one, center_point, sin_rot, cos_rot); - let point_two = - $crate::utility::rotate_around_point(point_two, center_point, sin_rot, cos_rot); - let point_three = - $crate::utility::rotate_around_point(point_three, center_point, sin_rot, cos_rot); - let point_four = - $crate::utility::rotate_around_point(point_four, center_point, sin_rot, cos_rot); - - let vec = Vec::new(); - vec.push(triangle!(point_one, point_two, point_three, $material)); - vec.push(triangle!(point_one, point_two, point_four, $material)); - - vec - }}; - ($x1:expr, $y1:expr, $x2:expr, $y2:expr, $axis_value:expr, $axis:expr, $rotation:expr, $material:expr) => {{ - let mut point_three = - implementations::Axis::point_from_2d(&rt_core::Vec2::new($x1, $y2), $axis, $axis_value); - let mut point_four = - implementations::Axis::point_from_2d(&rt_core::Vec2::new($x2, $y1), $axis, $axis_value); - let mut point_one = - implementations::Axis::point_from_2d(&rt_core::Vec2::new($x1, $y1), $axis, $axis_value); - let mut point_two = - implementations::Axis::point_from_2d(&rt_core::Vec2::new($x2, $y2), $axis, $axis_value); - - let sin_rot = rt_core::Vec3::new($rotation.x.sin(), $rotation.y.sin(), $rotation.z.sin()); - let cos_rot = rt_core::Vec3::new($rotation.x.cos(), $rotation.y.cos(), $rotation.z.cos()); - - $crate::utility::rotate_around_point(&mut point_one, center_point, sin_rot, cos_rot); - - $crate::utility::rotate_around_point(&mut point_two, center_point, sin_rot, cos_rot); - - $crate::utility::rotate_around_point(&mut point_three, center_point, sin_rot, cos_rot); - - $crate::utility::rotate_around_point(&mut point_four, center_point, sin_rot, cos_rot); - - let mut vec = Vec::new(); - vec.push(triangle!(point_one, point_two, point_three, $material)); - vec.push(triangle!(point_one, point_two, point_four, $material)); - - vec - }}; - ($x1:expr, $y1:expr, $x2:expr, $y2:expr, $axis_value:expr, $axis:expr, $center_point:expr, $sin_rot:expr, $cos_rot:expr, $material:expr) => {{ - let mut point_three = - implementations::Axis::point_from_2d(&rt_core::Vec2::new($x1, $y2), $axis, $axis_value); - let mut point_four = - implementations::Axis::point_from_2d(&rt_core::Vec2::new($x2, $y1), $axis, $axis_value); - let mut point_one = - implementations::Axis::point_from_2d(&rt_core::Vec2::new($x1, $y1), $axis, $axis_value); - let mut point_two = - implementations::Axis::point_from_2d(&rt_core::Vec2::new($x2, $y2), $axis, $axis_value); - - $crate::utility::rotate_around_point(&mut point_one, $center_point, $sin_rot, $cos_rot); - - $crate::utility::rotate_around_point(&mut point_two, $center_point, $sin_rot, $cos_rot); - - $crate::utility::rotate_around_point(&mut point_three, $center_point, $sin_rot, $cos_rot); - - $crate::utility::rotate_around_point(&mut point_four, $center_point, $sin_rot, $cos_rot); - - let mut vec = Vec::new(); - vec.push(triangle!(point_one, point_two, point_three, $material)); - vec.push(triangle!(point_one, point_two, point_four, $material)); - - vec - }}; -} - -#[macro_export] -macro_rules! aacuboid { - ($point_one:expr, $point_two:expr, $material:expr) => {{ - let center_point = 0.5 * ($point_one + $point_two); - let mut vec = Vec::new(); - vec.extend(aarect!( - $point_one.x, - $point_one.y, - $point_two.x, - $point_two.y, - $point_one.z, - implementations::Axis::Z, - $material - center_point - )); - vec.extend(aarect!( - $point_one.x, - $point_one.y, - $point_two.x, - $point_two.y, - $point_two.z, - implementations::Axis::Z, - $material - center_point - )); - - vec.extend(aarect!( - $point_one.x, - $point_one.z, - $point_two.x, - $point_two.z, - $point_one.y, - implementations::Axis::Y, - $material - center_point - )); - vec.extend(aarect!( - $point_one.x, - $point_one.z, - $point_two.x, - $point_two.z, - $point_two.y, - implementations::Axis::Y, - $material - )); - - vec.extend(aarect!( - $point_one.y, - $point_one.z, - $point_two.y, - $point_two.z, - $point_one.x, - implementations::Axis::X, - $material - center_point - )); - vec.extend(aarect!( - $point_one.y, - $point_one.z, - $point_two.y, - $point_two.z, - $point_two.x, - implementations::Axis::X, - $material - center_point - )); - vec - }}; - ($x1:expr, $y1:expr, $z1:expr, $x2:expr, $y2:expr, $z2:expr, $material:expr) => {{ - let mut vec = Vec::new(); - let center_point = 0.5 * (rt_core::Vec3::new($x1, $y1, $z1) + rt_core::Vec3::new($x2, $y2, $z2)); - vec.extend(aarect!( - $x1, - $y1, - $x2, - $y2, - $z1, - &implementations::Axis::Z, - $material, - center_point - )); - vec.extend(aarect!( - $x1, - $y1, - $x2, - $y2, - $z2, - &implementations::Axis::Z, - $material, - center_point - )); - - vec.extend(aarect!( - $x1, - $z1, - $x2, - $z2, - $y1, - &implementations::Axis::Y, - $material, - center_point - )); - vec.extend(aarect!( - $x1, - $z1, - $x2, - $z2, - $y2, - &implementations::Axis::Y, - $material, - center_point - )); - - vec.extend(aarect!( - $y1, - $z1, - $y2, - $z2, - $x1, - &implementations::Axis::X, - $material, - center_point - )); - vec.extend(aarect!( - $y1, - $z1, - $y2, - $z2, - $x2, - &implementations::Axis::X, - $material, - center_point - )); - vec - }}; -} - -#[macro_export] -macro_rules! cuboid { - ($point_one:expr, $point_two:expr, $rotation:expr, $material:expr) => {{ - let center_point = 0.5 * ($point_one + $point_two); - let sin_rot = ($rotation.x.sin(), $rotation.y.sin(), $rotation.z.sin()); - let cos_rot = ($rotation.x.cos(), $rotation.y.cos(), $rotation.z.cos()); - - let mut vec = Vec::new(); - vec.extend(rect!( - $point_one.x, - $point_one.y, - $point_two.x, - $point_two.y, - $point_one.z, - implementations::Axis::Z, - $center_point, - $sin_rot, - $cos_rot, - $material - )); - vec.extend(rect!( - $point_one.x, - $point_one.y, - $point_two.x, - $point_two.y, - $point_two.z, - implementations::Axis::Z, - $center_point, - $sin_rot, - $cos_rot, - $material - )); - - vec.extend(rect!( - $point_one.x, - $point_one.z, - $point_two.x, - $point_two.z, - $point_one.y, - implementations::Axis::Y, - $center_point, - $sin_rot, - $cos_rot, - $material - )); - vec.extend(rect!( - $point_one.x, - $point_one.z, - $point_two.x, - $point_two.z, - $point_two.y, - implementations::Axis::Y, - $center_point, - $sin_rot, - $cos_rot, - $material - )); - - vec.extend(rect!( - $point_one.y, - $point_one.z, - $point_two.y, - $point_two.z, - $point_one.x, - implementations::Axis::X, - $center_point, - $sin_rot, - $cos_rot, - $material - )); - vec.extend(rect!( - $point_one.y, - $point_one.z, - $point_two.y, - $point_two.z, - $point_two.x, - implementations::Axis::X, - $center_point, - $sin_rot, - $cos_rot, - $material - )); - vec - }}; - ($x1:expr, $y1:expr, $z1:expr, $x2:expr, $y2:expr, $z2:expr, $rotation:expr, $material:expr) => {{ - let point_one = rt_core::Vec3::new($x1, $y1, $z1); - let point_two = rt_core::Vec3::new($x2, $y2, $z2); - let center_point = 0.5 * (point_one + point_two); - let sin_rot = rt_core::Vec3::new($rotation.x.sin(), $rotation.y.sin(), $rotation.z.sin()); - let cos_rot = rt_core::Vec3::new($rotation.x.cos(), $rotation.y.cos(), $rotation.z.cos()); - - let mut vec = Vec::new(); - vec.extend(rect!( - point_one.x, - point_one.y, - point_two.x, - point_two.y, - point_one.z, - &implementations::Axis::Z, - center_point, - sin_rot, - cos_rot, - $material - )); - vec.extend(rect!( - point_one.x, - point_one.y, - point_two.x, - point_two.y, - point_two.z, - &implementations::Axis::Z, - center_point, - sin_rot, - cos_rot, - $material - )); - - vec.extend(rect!( - point_one.x, - point_one.z, - point_two.x, - point_two.z, - point_one.y, - &implementations::Axis::Y, - center_point, - sin_rot, - cos_rot, - $material - )); - vec.extend(rect!( - point_one.x, - point_one.z, - point_two.x, - point_two.z, - point_two.y, - &implementations::Axis::Y, - center_point, - sin_rot, - cos_rot, - $material - )); - - vec.extend(rect!( - point_one.y, - point_one.z, - point_two.y, - point_two.z, - point_one.x, - &implementations::Axis::X, - center_point, - sin_rot, - cos_rot, - $material - )); - vec.extend(rect!( - point_one.y, - point_one.z, - point_two.y, - point_two.z, - point_two.x, - &implementations::Axis::X, - center_point, - sin_rot, - cos_rot, - $material - )); - vec - }}; -} - -#[macro_export] -macro_rules! model { - ($filepath:expr, $material:expr) => { - $crate::ray_tracing::load_model::load_model($filepath, $material) - }; -} - -// assumes orientation -#[macro_export] -macro_rules! triangle { - ($point_one:expr, $point_two:expr, $point_three:expr, $material:expr) => {{ - let normal = { - let a = $point_two - $point_one; - let b = $point_three - $point_one; - a.cross(b) - } - .normalised(); - - implementations::AllPrimitives::Triangle(implementations::Triangle::new( - [$point_one, $point_two, $point_three], - [normal; 3], - $material, - )) - }}; - ($point_one:expr, $point_two:expr, $point_three:expr, $material:expr, $nr:expr) => {{ - let mut normal = { - let a = $point_two - $point_one; - let b = $point_three - $point_one; - a.cross(b) - } - .normalised(); - - if normal.dot($nr) < 0.0 { - normal = -1.0 * normal; - } - - implementations::AllPrimitives::Triangle(implementations::Triangle::new( - [$point_one, $point_two, $point_three], - [normal; 3], - $material, - )) - }}; - ($point_one:expr, $point_two:expr, $point_three:expr, $normal_ray:expr, $material:expr) => {{ - let mut normal = { - let a = $point_two - $point_one; - let b = $point_three - $point_one; - a.cross(b) - } - .normalised(); - - if normal.dot($normal_ray) < 0.0 { - normal = -normal; - } - - implementations::AllPrimitives::Triangle(implementations::Triangle::new( - [$point_one, $point_two, $point_three], - [normal; 3], - $material, - )) - }}; - - ($p1x:expr, $p1y:expr, $p1z:expr, $p2x:expr, $p2y:expr, $p2z:expr, $p3x:expr, $p3y:expr, $p3z:expr, $material:expr) => {{ - let point_one = position!($p1x, $p1y, $p1z); - let point_two = position!($p2x, $p2y, $p2z); - let point_three = position!($p3x, $p3y, $p3z); - let normal = { - let a = point_two - point_one; - let b = point_three - point_one; - a.cross(b) - } - .normalized(); - - rt_core::AllPrimitives::Triangle(rt_core::Triangle::new_from_arc( - [point_one, point_two, point_three], - [normal; 3], - $material, - )) - }}; -} - -//----- -// OTHER STRUCTURES -//----- -#[macro_export] -macro_rules! camera { - ($origin:expr, $lookat:expr, $vup:expr, $fov:expr, $aspect_ratio:expr, $aperture:expr, $focus_dist:expr) => { - std::sync::Arc::new(implementations::SimpleCamera::new( - $origin, - $lookat, - $vup, - $fov as rt_core::Float, - $aspect_ratio as rt_core::Float, - $aperture as rt_core::Float, - $focus_dist as rt_core::Float, - )) - }; -} - -#[macro_export] -macro_rules! random_sampler { - () => { - std::sync::Arc::new(implementations::random_sampler::RandomSampler {}) - }; -} - -#[macro_export] -macro_rules! sky { - () => { - std::sync::Arc::new(implementations::Sky::new(None)) - }; - ($sky_texture:expr) => { - std::sync::Arc::new(implementations::Sky::new(Some($sky_texture))) - }; -} - -#[macro_export] -macro_rules! bvh { - ($primitives:expr, $split_type:expr) => { - std::sync::Arc::new(implementations::Bvh::new($primitives, $split_type)) - }; -} - -#[macro_export] -macro_rules! scene { - ($camera:expr, $sky:expr, $sampler:expr, $bvh:expr) => { - $crate::scene::Scene::new($camera, $sky, $sampler, $bvh) - }; -} diff --git a/frontend/src/main.rs b/frontend/src/main.rs deleted file mode 100644 index ccd1e3f..0000000 --- a/frontend/src/main.rs +++ /dev/null @@ -1,368 +0,0 @@ -use crate::{ - gui::{Gui, RenderEvent}, - parameters::line_break, - utility::{get_progress_output, print_final_statistics, print_render_start, save_u8_to_image}, -}; -use rt_core::{Float, SamplerProgress}; -use std::{ - env, - sync::{ - atomic::{AtomicBool, AtomicU64, Ordering}, - Arc, - }, -}; -use vulkano::{ - buffer::CpuAccessibleBuffer, - command_buffer::{AutoCommandBufferBuilder, CommandBufferUsage, PrimaryAutoCommandBuffer}, - device::{Device, Queue}, - image::StorageImage, - instance::Instance, - sync::{self, GpuFuture}, - Version, -}; -use winit::event_loop::EventLoopProxy; - -mod generate; -mod gui; -mod load_model; -mod macros; -mod parameters; -mod rendering; -mod scene; -mod utility; - -struct Data { - queue: Arc, - device: Arc, - to_sc: rendering::Future, - from_sc: rendering::Future, - command_buffers: [Arc; 2], - buffer: Arc>, - sc_index: Arc, - samples: Arc, - total_samples: u64, - rays_shot: Arc, - event_proxy: EventLoopProxy, -} - -impl Data { - pub fn new( - queue: Arc, - device: Arc, - to_sc: rendering::Future, - from_sc: rendering::Future, - command_buffers: [Arc; 2], - buffer: Arc>, - sc_index: Arc, - samples: Arc, - total_samples: u64, - rays_shot: Arc, - event_proxy: EventLoopProxy, - ) -> Self { - Data { - queue, - device, - to_sc, - from_sc, - command_buffers, - buffer, - sc_index, - samples, - total_samples, - rays_shot, - event_proxy, - } - } -} - -fn main() { - let args: Vec = env::args().collect(); - - if let Some((scene, parameters)) = parameters::process_args(args) { - let (render_options, filename) = (parameters.render_options, parameters.filename.clone()); - if !parameters.gui { - let start = print_render_start( - render_options.width, - render_options.height, - Some(render_options.samples_per_pixel), - ); - - let mut image = SamplerProgress::new(render_options.width * render_options.height, 3); - let progress_bar_output = - |sp: &mut SamplerProgress, previous: &SamplerProgress, i: u64| { - sp.samples_completed += 1; - sp.rays_shot += previous.rays_shot; - - sp.current_image - .iter_mut() - .zip(previous.current_image.iter()) - .for_each(|(pres, acc)| { - *pres += (acc - *pres) / i as Float; // since copies first buffer when i=1 - }); - - get_progress_output(sp.samples_completed, render_options.samples_per_pixel); - }; - - scene.generate_image_threaded(render_options, Some((&mut image, progress_bar_output))); - - let output = ℑ - - let ray_count = output.rays_shot; - - print_final_statistics(start, ray_count, None); - line_break(); - - let output: Vec = output - .current_image - .iter() - .map(|val| (val.sqrt() * 255.999) as u8) - .collect(); - - match filename { - Some(filename) => { - save_u8_to_image( - render_options.width, - render_options.height, - output, - filename, - false, - ); - } - None => {} - } - } else { - let required_extensions = vulkano_win::required_extensions(); - let instance = Instance::new(None, Version::V1_5, &required_extensions, None).unwrap(); - let gui = Gui::new( - &instance, - render_options.width as u32, - render_options.height as u32, - ); - - let event_loop_proxy: Option> = - gui.event_loop.as_ref().map(|el| el.create_proxy()); - let iter = [0.0f32, 0.0, 0.0, 0.0] - .repeat((render_options.width * render_options.height) as usize) - .into_iter(); - let buffer = CpuAccessibleBuffer::from_iter( - gui.device.clone(), - vulkano::buffer::BufferUsage::all(), - true, - iter, - ) - .unwrap(); - - let samples = Arc::new(AtomicU64::new(0)); - let ray_count = Arc::new(AtomicU64::new(0)); - - let command_buffers = create_command_buffers( - gui.device.clone(), - gui.queue.clone(), - buffer.clone(), - gui.cpu_rendering.cpu_swapchain.clone(), - ); - - let mut data = Data::new( - gui.queue.clone(), - gui.device.clone(), - gui.cpu_rendering.to_sc.clone(), - gui.cpu_rendering.from_sc.clone(), - command_buffers, - buffer.clone(), - gui.cpu_rendering.copy_to_first.clone(), - samples.clone(), - render_options.samples_per_pixel, - ray_count.clone(), - event_loop_proxy.unwrap(), - ); - - let image_copy_finished = data.to_sc.clone(); - - let start = print_render_start(render_options.width, render_options.height, None); - - let render_canceled = Arc::new(AtomicBool::new(true)); - - let moved_render_canceled = render_canceled.clone(); - let moved_filename = filename.clone(); - - std::thread::spawn(move || { - let ray_count = data.rays_shot.clone(); - let samples = data.samples.clone(); - let buffer = data.buffer.clone(); - let to_sc = data.to_sc.clone(); - - scene.generate_image_threaded( - render_options, - Some(( - &mut data, - |data: &mut Data, previous: &SamplerProgress, i: u64| { - sample_update(data, previous, i); - }, - )), - ); - - let ray_count = ray_count.load(Ordering::Relaxed); - let samples = samples.load(Ordering::Relaxed); - - print_final_statistics(start, ray_count, Some(samples)); - line_break(); - - moved_render_canceled.store(false, Ordering::Relaxed); - - save_file( - moved_filename, - render_options.width, - render_options.height, - &*buffer.read().unwrap(), - to_sc, - ); - }); - - gui.run(); - if render_canceled.load(Ordering::Relaxed) { - let ray_count = ray_count.load(Ordering::Relaxed); - let samples = samples.load(Ordering::Relaxed); - - print_final_statistics(start, ray_count, Some(samples)); - line_break(); - - save_file( - filename, - render_options.width, - render_options.height, - &*buffer.read().unwrap(), - image_copy_finished, - ); - } - } - } -} - -fn create_command_buffers( - device: Arc, - queue: Arc, - buffer: Arc>, - sc: [Arc; 2], -) -> [Arc; 2] { - let mut command_buffer_0 = None; - let mut command_buffer_1 = None; - for (i, sc_image) in sc.iter().enumerate() { - let mut builder = AutoCommandBufferBuilder::primary( - device.clone(), - queue.family(), - CommandBufferUsage::MultipleSubmit, - ) - .unwrap(); - - builder - .copy_buffer_to_image(buffer.clone(), sc_image.clone()) - .unwrap(); - if i == 0 { - command_buffer_0 = Some(builder.build().unwrap()); - } else { - command_buffer_1 = Some(builder.build().unwrap()); - } - } - - [ - Arc::new(command_buffer_0.unwrap()), - Arc::new(command_buffer_1.unwrap()), - ] -} - -fn sample_update(data: &mut Data, previous: &SamplerProgress, i: u64) { - // update infomation about the rays shot and samples completed in the current render - data.samples.fetch_add(1, Ordering::Relaxed); - data.rays_shot - .fetch_add(previous.rays_shot, Ordering::Relaxed); - - // wait on from_sc future if is_some() - match &*data.from_sc.lock().unwrap() { - Some(future) => { - future.wait(None).unwrap(); - } - None => {} - } - match &*data.to_sc.lock().unwrap() { - Some(future) => { - future.wait(None).unwrap(); - } - None => {} - } - - { - // get access to CpuAccessibleBuffer - let mut buf = data.buffer.write().unwrap(); - buf.chunks_mut(4) - .zip(previous.current_image.chunks(3)) - .for_each(|(pres, acc)| { - pres[0] += (acc[0] as f32 - pres[0]) / i as f32; - pres[1] += (acc[1] as f32 - pres[1]) / i as f32; - pres[2] += (acc[2] as f32 - pres[2]) / i as f32; - pres[3] = 1.0; - }); - } - - // copy to cpu swapchain - let command_buffer = - data.command_buffers[data.sc_index.load(Ordering::Relaxed) as usize].clone(); - - // copy to swapchain and store op in to_sc future - { - let to_sc = &mut *data.to_sc.lock().unwrap(); - *to_sc = Some( - match to_sc.take() { - Some(future) => future - .then_execute(data.queue.clone(), command_buffer) - .unwrap() - .boxed_send_sync(), - None => sync::now(data.device.clone()) - .then_execute(data.queue.clone(), command_buffer) - .unwrap() - .boxed_send_sync(), - } - .then_signal_fence_and_flush() - .unwrap(), // change to match - ); - } - - // modify sc_index to !sc_index - data.sc_index - .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |x| Some(!x)) - .unwrap(); - - get_progress_output(i, data.total_samples); - - // signal sample is ready to be presented - data.event_proxy - .send_event(RenderEvent::SampleCompleted) - .unwrap(); -} - -fn save_file( - filename: Option, - width: u64, - height: u64, - buffer: &[f32], - image_fence: rendering::Future, -) { - match filename { - Some(filename) => { - match &*image_fence.lock().unwrap() { - Some(future) => { - future.wait(None).unwrap(); - } - None => {} - } - - save_u8_to_image( - width, - height, - buffer.iter().map(|val| (val * 255.999) as u8).collect(), - filename, - true, - ) - } - None => {} - } -} diff --git a/frontend/src/parameters.rs b/frontend/src/parameters.rs deleted file mode 100644 index ec9f34b..0000000 --- a/frontend/src/parameters.rs +++ /dev/null @@ -1,417 +0,0 @@ -use crate::generate::SceneType; -use chrono::Local; -use implementations::split::SplitType; -use rt_core::{Float, RenderMethod, RenderOptions}; -use std::process; - -pub struct Parameters { - pub render_options: RenderOptions, - pub gui: bool, - pub filename: Option, -} - -impl Parameters { - pub fn new(render_options: RenderOptions, gui: bool, filename: Option) -> Self { - Self { - render_options, - gui, - filename, - } - } -} - -pub fn line_break() { - println!("--------------------------------"); -} - -macro_rules! scene { - ($scene_name:ident, $bvh_type:expr, $aspect_ratio:expr) => {{ - line_break(); - let time = Local::now(); - println!("{} - Scene Generation started", time.format("%X")); - crate::generate::$scene_name($bvh_type, $aspect_ratio) - }}; - ($scene_name:ident, $bvh_type:expr, $aspect_ratio:expr, $seed:expr) => {{ - line_break(); - let time = Local::now(); - println!("{} - Scene Generation started", time.format("%X")); - crate::generate::$scene_name($bvh_type, $aspect_ratio, $seed) - }}; -} - -pub fn process_args(args: Vec) -> Option<(SceneType, Parameters)> { - let mut scene_index = None; - let mut samples = None; - let mut width = None; - let mut height = None; - let mut filename = None; - let mut bvh_type = None; - let mut seed = None; - let mut render_method = None; - let mut gui = true; - - if args.len() == 1 { - println!("No arguments specified defaulting to help."); - display_help(); - process::exit(0); - } - - for arg_i in (0..(args.len() / 2)).map(|i| i * 2 + 1) { - if let Some(arg) = args.get(arg_i) { - match &arg[..] { - "-H" => { - display_help(); - process::exit(0); - } - "--help" => { - display_help(); - process::exit(0); - } - "-G" | "--gui" => match &args[arg_i + 1].to_lowercase()[..] { - "false" => { - gui = false; - } - "true" => {} - _ => { - println!("Invalid option {} for --gui", args[arg_i + 1]); - } - }, - "-L" | "--list" => { - get_list(); - } - "-I" | "--info" => { - get_info(&args, arg_i + 1); - } - "-S" | "--scene" => { - scene_index = Some(arg_i + 1); - } - "-N" | "--samples" => { - samples = Some(get_samples(&args, arg_i + 1)); - } - "-X" | "--width" => { - width = Some(get_dimension(&args, arg_i + 1)); - } - "-Y" | "--height" => { - height = Some(get_dimension(&args, arg_i + 1)); - } - "-B" | "--bvh" => { - bvh_type = Some(get_bvh_type(&args, arg_i + 1)); - } - "-O" | "--output" => { - filename = get_filename(&args, arg_i + 1); - } - "-R" | "--render_type" => { - render_method = get_render_method(&args, arg_i + 1); - } - "-J" | "--seed" => { - seed = Some(get_seed(&args, arg_i + 1)); - } - _ => {} - } - } - } - match scene_index { - Some(scene_index) => { - let mut render_options: RenderOptions = Default::default(); - render_options.samples_per_pixel = samples.unwrap_or(render_options.samples_per_pixel); - render_options.width = width.unwrap_or(render_options.width); - render_options.height = height.unwrap_or(render_options.height); - render_options.render_method = render_method.unwrap_or(render_options.render_method); - - let aspect_ratio = render_options.width as Float / render_options.height as Float; - let bvh_type = bvh_type.unwrap_or_default(); - let scene = get_scene(&args, scene_index, bvh_type, aspect_ratio, seed); - - let parameters = Parameters::new(render_options, gui, filename); - Some((scene, parameters)) - } - None => None, - } -} - -fn display_help() { - println!("Usage: cpu_raytracer [OPTION...]"); - println!("A headless CPU raytracer!\n"); - println!("Arguments:"); - println!("-H, --help"); - println!("\t Displays help."); - println!("-L, --list"); - println!("\t Lists all valid scenes."); - println!("-I [index], --info [index]"); - println!("\t Prints info for scene"); - println!("-S [index], --scene [index]"); - println!("\t Renders scene"); - println!("-N [samples], --samples [samples]"); - println!("\t Set samples per pixel. (Note 0 -> u64::MAX)"); - println!("-X [pixels], --width [pixels]"); - println!("\t Sets width of image"); - println!("-Y [pixels], --height [pixels]"); - println!("\t Sets height of image"); - println!("-B [split_type], --bvh [split_type]"); - println!("\t Sets split type for BVH."); - println!("\t supported split types: \"equal\", \"middle\""); - println!("-O [filename], --output [filename]"); - println!("\t Filename of output with supported file extension. If not specified no image will be output."); - println!("\t supported file extensions: \"png\", \"jpeg\""); - println!("-J [seed], --seed [seed]"); - println!("-G [true/false] --gui [true/false]"); - println!("\t Show render preview while rendering"); - println!("-R [render_method], --render_method [render_method]"); - println!("\t Possible options: mis, naive"); - println!("Seed for scene generation (if supported).") -} - -fn get_list() { - println!("-------------------"); - println!("1: Marbles"); - println!("-------------------"); - println!("Objects: 4-125"); - println!("Sky: Yes"); - println!("Motion Blur: Yes"); - println!("-------------------"); - println!("-------------------"); - println!("4: Overshadowed"); - println!("-------------------"); - println!("Objects: 3"); - println!("Sky: No"); - println!("Motion Blur: No"); - println!("-------------------"); - println!("-------------------"); - println!("6: Glass Dragon"); - println!("-------------------"); - println!("Objects: 3"); - println!("Sky: No"); - println!("Motion Blur: No"); - println!("-------------------"); - println!("-------------------"); - println!("8: Cornell Box"); - println!("-------------------"); - println!("Objects: 6"); - println!("Sky: No"); - println!("Motion Blur: No"); - println!("-------------------"); -} - -fn get_info(args: &[String], index: usize) { - match args.get(index) { - None => { - println!("Please specify a value for scene!"); - println!("Do -H or --help for more information."); - process::exit(0); - } - Some(string) => match &string.to_ascii_lowercase()[..] { - "marbles" => { - println!("Marbles"); - println!("Objects: 4-125"); - println!("Sky: Yes"); - println!("Motion Blur: Yes"); - } - "overshadowed" => { - println!("Overshadowed"); - println!("Objects: 3"); - println!("Sky: No"); - println!("Motion Blur: No"); - } - "dragon" => { - println!("Glass Dragon"); - println!("Objects: 3"); - println!("Sky: No"); - println!("Motion Blur: No"); - } - "cornell" => { - println!("Cornell Box"); - println!("Objects: 6"); - println!("Sky: No"); - println!("Motion Blur: No"); - } - "furnace" => { - println!("Furnace Test"); - println!("Objects: 6"); - println!("Sky: No"); - println!("Motion Blur: No"); - } - _ => { - println!("{} is not a valid scene index!", string); - println!("Please specify a valid for scene!"); - println!("Do -L or--list to view scenes or do -H or --help for more information."); - process::exit(0); - } - }, - } -} - -fn get_scene( - args: &[String], - index: usize, - bvh_type: SplitType, - aspect_ratio: Float, - seed: Option, -) -> SceneType { - match args.get(index) { - None => { - println!("Please specify a value for scene!"); - println!("Do -H or --help for more information."); - process::exit(0); - } - - Some(string) => match &string[..] { - "classic" => { - scene!(classic, bvh_type, aspect_ratio, seed) - } - "overshadowed" => { - scene!(overshadowed, bvh_type, aspect_ratio) - } - "cornell" => { - scene!(cornell, bvh_type, aspect_ratio) - } - "furnace" => { - scene!(furnace, bvh_type, aspect_ratio) - } - "coffee" => { - scene!(coffee, bvh_type, aspect_ratio) - } - _ => { - println!("{} is not a valid scene index!", string); - println!("Please specify a valid for scene!"); - println!("Do -L or--list to view scenes or do -H or --help for more information."); - process::exit(0); - } - }, - } -} - -fn get_seed(args: &[String], index: usize) -> String { - match args.get(index) { - Some(string) => string.to_string(), - None => { - println!("Please specify a value for seed!"); - println!("Do -H or --help for more information."); - process::exit(0); - } - } -} - -fn get_render_method(args: &[String], index: usize) -> Option { - Some(match args.get(index) { - Some(string) => match &string.to_ascii_lowercase()[..] { - "naive" => RenderMethod::Naive, - "mis" => RenderMethod::MIS, - _val => { - println!("Unsupported render method: {}", _val); - println!("Do -H or --help for more information."); - process::exit(0); - } - }, - None => { - println!("Please specify a valid render method!"); - println!("Do -H or --help for more information."); - process::exit(0); - } - }) -} - -fn get_filename(args: &[String], index: usize) -> Option { - Some(match args.get(index) { - Some(string) => { - let split_vec: Vec<&str> = string.split('.').collect(); - if split_vec.len() < 2 { - println!("Please specify a valid extension!"); - println!("Do -H or --help for more information."); - process::exit(0); - } - - match split_vec[split_vec.len() - 1] { - "jpeg" => string.to_string(), - "png" => string.to_string(), - _ => { - println!( - "Unsupported file extension: {}", - split_vec[split_vec.len() - 1] - ); - println!("Do -H or --help for more information."); - process::exit(0); - } - } - } - None => { - println!("Please specify a valid filename!"); - println!("Do -H or --help for more information."); - process::exit(0); - } - }) -} - -fn get_bvh_type(args: &[String], index: usize) -> SplitType { - match args.get(index) { - Some(string) => match &string.to_lowercase()[..] { - "equal" => SplitType::EqualCounts, - "middle" => SplitType::Middle, - "sah" => SplitType::Sah, - _ => { - println!("{} is not a valid value for BVH type!", string); - println!("Please specify a valid value for BVH type!"); - println!("Do -H or --help for more information."); - process::exit(0); - } - }, - None => { - println!("Please specify a value for BVH type!"); - println!("Do -H or --help for more information."); - process::exit(0); - } - } -} - -fn get_samples(args: &[String], index: usize) -> u64 { - match args.get(index) { - Some(string) => match string.parse::() { - Ok(parsed) => match parsed { - 0 => { - println!("Samples must be non zero positive integer."); - println!("Please specify a valid value for height!"); - println!("Do -H or --help for more information."); - process::exit(0); - } - _ => parsed, - }, - Err(_) => { - println!("{} is not a valid value for samples!", string); - println!("Please specify a valid value for height!"); - println!("Do -H or --help for more information."); - process::exit(0); - } - }, - None => { - println!("Please specify a valid value for samples!"); - println!("Do -H or --help for more information."); - process::exit(0); - } - } -} - -fn get_dimension(args: &[String], index: usize) -> u64 { - match args.get(index) { - Some(string) => match string.parse::() { - Ok(parsed) => match parsed { - 0 => { - println!("Height must be non zero positive integer."); - println!("Please specify a valid value for height!"); - println!("Do -H or --help for more information."); - process::exit(0); - } - _ => parsed, - }, - Err(_) => { - println!("{} is not a valid value for height!", string); - println!("Please specify a valid value for height!"); - println!("Do -H or --help for more information."); - process::exit(0); - } - }, - None => { - println!("Please specify a valid value for height!"); - println!("Do -H or --help for more information."); - process::exit(0); - } - } -} diff --git a/frontend/src/scene.rs b/frontend/src/scene.rs deleted file mode 100644 index 35d5793..0000000 --- a/frontend/src/scene.rs +++ /dev/null @@ -1,54 +0,0 @@ -use implementations::{SimpleCamera, Sky, Texture}; -use rt_core::{AccelerationStructure, Primitive, RenderOptions, Sampler, SamplerProgress, Scatter}; -use std::{marker::PhantomData, sync::Arc}; - -pub struct Scene< - P: Primitive, - M: Scatter, - S: Sampler, - A: AccelerationStructure, - T: Texture, -> { - pub acceleration_structure: Arc, - pub camera: Arc, - pub sampler: Arc, - pub sky: Arc>, - phantom_data: PhantomData<(M, P)>, -} - -impl Scene -where - P: Primitive + Send + Sync + 'static, - M: Scatter + Send + Sync + 'static, - S: Sampler, - A: AccelerationStructure + Send + Sync, - T: Texture + Send + Sync, -{ - pub fn new( - camera: Arc, - sky: Arc>, - sampler: Arc, - acceleration_structure: Arc, - ) -> Self { - Scene { - acceleration_structure, - camera, - sampler, - sky, - phantom_data: PhantomData, - } - } - pub fn generate_image_threaded( - &self, - render_options: RenderOptions, - presentation_update: Option<(&mut D, impl Fn(&mut D, &SamplerProgress, u64))>, - ) { - self.sampler.sample_image( - render_options, - &*self.camera, - &*self.sky, - &*self.acceleration_structure, - presentation_update, - ) - } -} diff --git a/frontend/src/utility.rs b/frontend/src/utility.rs deleted file mode 100644 index cdbc1dc..0000000 --- a/frontend/src/utility.rs +++ /dev/null @@ -1,226 +0,0 @@ -use chrono::Local; -use implementations::{aabb::AABound, split::SplitType, Axis, Bvh}; -use rt_core::{Float, Primitive, Scatter, Vec3}; -use std::{ - io::{stdout, Write}, - process, - sync::Arc, - time::{Duration, Instant}, -}; - -const BAR_LENGTH: u32 = 30; - -pub fn progress_bar(percentage: f64) { - print!("\r["); - for i in 1..=BAR_LENGTH { - if percentage >= i as f64 / BAR_LENGTH as f64 - && percentage < (i + 1) as f64 / BAR_LENGTH as f64 - && i != BAR_LENGTH - { - print!(">"); - } else if percentage >= i as f64 / BAR_LENGTH as f64 { - print!("="); - } else { - print!("-"); - } - } - print!("]"); -} - -pub fn get_readable_duration(duration: Duration) -> String { - let days = duration.as_secs() / 86400; - - let days_string = match days { - 0 => "".to_string(), - 1 => format!("{} day, ", days), - _ => format!("{} days, ", days), - }; - - let hours = (duration.as_secs() - days * 86400) / 3600; - let hours_string = match hours { - 0 => "".to_string(), - 1 => format!("{} hour, ", hours), - _ => format!("{} hours, ", hours), - }; - - let minutes = (duration.as_secs() - days * 86400 - hours * 3600) / 60; - let minutes_string = match minutes { - 0 => "".to_string(), - 1 => format!("{} minute, ", minutes), - _ => format!("{} minutes, ", minutes), - }; - - let seconds = duration.as_secs() % 60; - let seconds_string = match seconds { - 0 => "~0 seconds".to_string(), - 1 => format!("{} second", seconds), - _ => format!("{} seconds", seconds), - }; - days_string + &hours_string + &minutes_string + &seconds_string -} - -pub fn create_bvh_with_info + AABound, M: Scatter>( - primitives: Vec

, - bvh_type: SplitType, -) -> Arc> { - let time = Local::now(); - - println!("\n{} - Bvh construction started at", time.format("%X")); - - let start = Instant::now(); - let bvh = Arc::new(Bvh::new(primitives, bvh_type)); - let end = Instant::now(); - let duration = end.checked_duration_since(start).unwrap(); - - println!("\tBvh construction finished in: {}ms", duration.as_millis()); - println!("\tNumber of BVH nodes: {}\n", bvh.number_nodes()); - - bvh -} - -pub fn get_progress_output(samples_completed: u64, total_samples: u64) { - progress_bar(samples_completed as f64 / total_samples as f64); - - print!(" ({}/{}) samples", samples_completed, total_samples); - - stdout().flush().unwrap(); -} - -pub fn save_u8_to_image(width: u64, height: u64, image: Vec, filename: String, alpha: bool) { - let split = filename.split('.').collect::>(); - if split.len() != 2 { - println!("Invalid filename: {filename}"); - process::exit(0); - } - - let extension = split[1]; - - match extension { - "png" | "jpg" | "jpeg" | "exr" | "tiff" => { - image::save_buffer( - filename, - &image, - width.try_into().unwrap(), - height.try_into().unwrap(), - if alpha { - image::ColorType::Rgba8 - } else { - image::ColorType::Rgb8 - }, - ) - .unwrap(); - } - "ppm" => { - let mut data = format!("P3\n{} {}\n255\n", width, height) - .as_bytes() - .to_owned(); - - image.iter().enumerate().for_each(|(i, &v)| { - if i % 3 == 0 { - data.extend_from_slice(format!("{}\n", v).as_bytes()) - } else { - data.extend_from_slice(format!("{} ", v).as_bytes()) - } - }); - - let mut file = std::fs::File::create(filename).unwrap(); - file.write_all(&data).unwrap(); - } - _ => { - println!("Unknown filetype: .{extension}"); - } - } -} - -pub fn print_final_statistics(start: Instant, ray_count: u64, samples: Option) { - let end = Instant::now(); - let duration = end.checked_duration_since(start).unwrap(); - let time = Local::now(); - println!( - "\u{001b}[2K\r{} - Finised rendering image", - time.format("%X") - ); - println!("\tRender Time: {}", get_readable_duration(duration)); - println!("\tRays: {}", ray_count); - match samples { - Some(samples) => println!("\tSamples: {}", samples), - None => { - println!() - } - } - - println!( - "\tMrays/s: {:.2}", - (ray_count as f64 / duration.as_secs_f64()) / 1000000.0 - ); -} - -pub fn print_render_start(width: u64, height: u64, samples: Option) -> Instant { - let time = Local::now(); - println!("{} - Render started", time.format("%X")); - println!("\tWidth: {}", width); - println!("\tHeight: {}", height); - match samples { - Some(samples) => println!("\tSamples per pixel: {}\n", samples), - None => println!(), - } - Instant::now() -} - -pub fn rotate_around_point( - point: &mut Vec3, - center_point: Vec3, - sin_angles: Vec3, - cos_angles: Vec3, -) { - *point -= center_point; - - rotate_around_axis(point, Axis::X, sin_angles.x, cos_angles.x); - rotate_around_axis(point, Axis::Y, sin_angles.y, cos_angles.y); - rotate_around_axis(point, Axis::Z, sin_angles.z, cos_angles.z); - - *point += center_point; -} - -pub fn rotate_around_axis(point: &mut Vec3, axis: Axis, sin: Float, cos: Float) { - match axis { - Axis::X => { - let old_y = point.y; - point.y = cos * point.y - sin * point.z; - point.z = sin * old_y + cos * point.z; - } - Axis::Y => { - let old_x = point.x; - point.x = cos * point.x - sin * point.z; - point.z = sin * old_x + cos * point.z; - } - Axis::Z => { - let old_y = point.y; - point.y = cos * point.y - sin * point.x; - point.x = sin * old_y + cos * point.x; - } - } -} - -#[cfg(test)] -mod tests { - - use super::*; - use rt_core::PI; - - #[test] - fn rotation() { - let center_point = Vec3::new(1.0, 0.0, 0.0); - - let mut point = Vec3::new(2.0, 0.0, 0.0); - - let angles = Vec3::new(0.0, 45.0 * PI / 180.0, 0.0); - - let cos_angles = Vec3::new(angles.x.cos(), angles.y.cos(), angles.z.cos()); - let sin_angles = Vec3::new(angles.x.sin(), angles.y.sin(), angles.z.sin()); - - rotate_around_point(&mut point, center_point, sin_angles, cos_angles); - - assert!((point - Vec3::new(1.707107, 0.0, 0.7071069)).abs().mag() < 0.000001); - } -} diff --git a/implementations/src/materials/emissive.rs b/implementations/src/materials/emissive.rs deleted file mode 100644 index 58569de..0000000 --- a/implementations/src/materials/emissive.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::{textures::Texture, utility::offset_ray}; -use rt_core::{Float, Hit, Ray, Scatter, Vec3}; -use std::sync::Arc; - -#[derive(Debug)] -pub struct Emit { - pub texture: Arc, - pub strength: Float, -} - -impl Emit -where - T: Texture, -{ - pub fn new(texture: &Arc, strength: Float) -> Self { - Emit { - texture: texture.clone(), - strength, - } - } -} - -impl Scatter for Emit -where - T: Texture, -{ - fn get_emission(&self, hit: &Hit, _: Vec3) -> Vec3 { - let point = offset_ray(hit.point, hit.normal, hit.error, true); - self.strength * self.texture.colour_value(Vec3::zero(), point) - } - fn is_light(&self) -> bool { - true - } - fn eval(&self, _hit: &Hit, _: Vec3, _: Vec3) -> Vec3 { - unreachable!() - } - fn scatter_ray(&self, _: &mut Ray, _: &Hit) -> bool { - true - } -} diff --git a/implementations/src/materials/lambertian.rs b/implementations/src/materials/lambertian.rs deleted file mode 100644 index 3984488..0000000 --- a/implementations/src/materials/lambertian.rs +++ /dev/null @@ -1,59 +0,0 @@ -use crate::{ - textures::Texture, - utility::{coord::Coordinate, cosine_hemisphere_sampling, near_zero, offset_ray}, -}; -use rt_core::{Float, Hit, Ray, Scatter, Vec3}; -use std::sync::Arc; - -#[derive(Debug)] -pub struct Lambertian { - pub texture: Arc, - pub absorption: Float, -} - -#[cfg(all(feature = "f64"))] -use std::f64::consts::PI; - -#[cfg(not(feature = "f64"))] -use std::f32::consts::PI; - -impl Lambertian -where - T: Texture, -{ - pub fn new(texture: &Arc, absorption: Float) -> Self { - Lambertian { - texture: texture.clone(), - absorption, - } - } -} - -impl Scatter for Lambertian -where - T: Texture, -{ - fn scatter_ray(&self, ray: &mut Ray, hit: &Hit) -> bool { - let coordinate_system = Coordinate::new_from_z(hit.normal); - let mut direction = cosine_hemisphere_sampling(); - coordinate_system.vec_to_coordinate(&mut direction); - - if near_zero(direction) { - direction = hit.normal; - } - - let point = offset_ray(hit.point, hit.normal, hit.error, true); - *ray = Ray::new(point, direction, ray.time); - - false - } - fn scattering_pdf(&self, hit: &Hit, _: Vec3, wi: Vec3) -> Float { - hit.normal.dot(wi).max(0.0) / PI - } - fn eval(&self, hit: &Hit, wo: Vec3, wi: Vec3) -> Vec3 { - self.texture.colour_value(wo, hit.point) - * (1.0 - self.absorption) - * hit.normal.dot(wi).max(0.0) - / PI - } -} diff --git a/implementations/src/materials/mod.rs b/implementations/src/materials/mod.rs deleted file mode 100644 index 2839859..0000000 --- a/implementations/src/materials/mod.rs +++ /dev/null @@ -1,27 +0,0 @@ -use proc::Scatter; -use rt_core::{Float, Hit, Ray, Scatter, Vec3}; - -pub mod emissive; -pub mod lambertian; -pub mod phong; -pub mod reflect; -pub mod refract; -pub mod trowbridge_reitz; - -pub use crate::{ - materials::{ - emissive::Emit, lambertian::Lambertian, phong::Phong, reflect::Reflect, refract::Refract, - trowbridge_reitz::TrowbridgeReitz, - }, - textures::Texture, -}; - -#[derive(Scatter, Debug)] -pub enum AllMaterials { - Emit(Emit), - Lambertian(Lambertian), - Phong(Phong), - TrowbridgeReitz(TrowbridgeReitz), - Reflect(Reflect), - Refract(Refract), -} diff --git a/implementations/src/materials/phong.rs b/implementations/src/materials/phong.rs deleted file mode 100644 index 10c3183..0000000 --- a/implementations/src/materials/phong.rs +++ /dev/null @@ -1,83 +0,0 @@ -use crate::{ - textures::Texture, - utility::{random_float, specular_sampling, coord::Coordinate, cosine_hemisphere_sampling, near_zero, offset_ray}, -}; -use rt_core::{Float, Hit, Ray, Scatter, Vec3, PI}; -use std::sync::Arc; - -#[derive(Debug)] -pub struct Phong { - pub ks: Float, - pub kd: Float, - pub exponent: Float, - pub texture: Arc, -} - -impl Phong -where - T: Texture, -{ - pub fn new(texture: &Arc, kd: Float, ks: Float, exponent: Float) -> Self { - Self { ks, kd, exponent, texture: texture.clone()} - } -} - -impl Scatter for Phong -where - T: Texture, -{ - fn scatter_ray(&self, ray: &mut Ray, hit: &Hit) -> bool { - let rand = random_float(); - let direction = if rand > self.kd + self.ks { - return true; - } else if rand > self.kd { - // sample diffuse - let coordinate_system = Coordinate::new_from_z(hit.normal); - let mut direction = cosine_hemisphere_sampling(); - coordinate_system.vec_to_coordinate(&mut direction); - - if near_zero(direction) { - direction = hit.normal; - } - direction - } else { - // sample specular - let mut spec_dir = ray.direction; - spec_dir.reflect(hit.normal); - let coordinate_system = Coordinate::new_from_z(spec_dir); - let mut direction = specular_sampling(self.exponent); - coordinate_system.vec_to_coordinate(&mut direction); - - if direction.dot(hit.normal) < 0.0 { - return true; - } - if near_zero(direction) { - direction = spec_dir; - } - direction - }; - let point = offset_ray(hit.point, hit.normal, hit.error, true); - *ray = Ray::new(point, direction, ray.time); - false - } - fn scattering_pdf(&self, hit: &Hit, wo: Vec3, wi: Vec3) -> Float { - let mut spec_dir = wo; - spec_dir.reflect(hit.normal); - let cos_alpha = spec_dir.dot(wi).max(0.0); - - let diffuse = hit.normal.dot(wi).max(0.0) / PI; - let spec = (self.exponent + 1.0) * cos_alpha.powf(self.exponent) / (2.0 * PI); - self.kd * diffuse + self.ks * spec - } - fn eval(&self, hit: &Hit, wo: Vec3, wi: Vec3) -> Vec3 { - let mut spec_dir = wo; - spec_dir.reflect(hit.normal); - let cos_alpha = spec_dir.dot(wi).max(0.0); - - let diff = self.texture.colour_value(wo, hit.point)* hit.normal.dot(wi).max(0.0) - / PI; - let spec = Vec3::one() * (self.exponent + 2.0) * cos_alpha.powf(self.exponent) / (2.0 * PI); - - self.kd * diff + self.ks * spec - } -} \ No newline at end of file diff --git a/implementations/src/materials/trowbridge_reitz.rs b/implementations/src/materials/trowbridge_reitz.rs deleted file mode 100644 index af4da60..0000000 --- a/implementations/src/materials/trowbridge_reitz.rs +++ /dev/null @@ -1,144 +0,0 @@ -use crate::utility::coord::Coordinate; -use crate::{ - materials::refract, - textures::Texture, - utility::{offset_ray, random_float}, -}; -use rt_core::{Float, Hit, Ray, Scatter, Vec3}; -use std::{f32::INFINITY, sync::Arc}; - -#[derive(Debug)] -pub struct TrowbridgeReitz { - pub texture: Arc, - pub alpha: Float, - pub ior: Vec3, - pub metallic: Float, -} - -#[cfg(all(feature = "f64"))] -use std::f64::consts::PI; - -#[cfg(not(feature = "f64"))] -use std::f32::consts::PI; - -impl TrowbridgeReitz -where - T: Texture, -{ - pub fn new(texture: &Arc, roughness: Float, ior: Vec3, metallic: Float) -> Self { - Self { - texture: texture.clone(), - alpha: roughness * roughness, - ior, - metallic, - } - } - - fn fresnel(&self, hit: &Hit, wo: Vec3, wi: Vec3, h: Vec3) -> Vec3 { - let f0 = ((1.0 - self.ior) / (1.0 + self.ior)).abs(); - let f0 = f0 * f0; - let f0 = lerp(f0, self.texture.colour_value(wi, hit.point), self.metallic); - refract::fresnel(wo.dot(h), f0) - } - - fn geometry_partial_ggx(&self, h: Vec3, v: Vec3) -> Float { - 1.0 / (1.0 + self.lambda_ggx(h, v)) - } - - fn geometry_ggx(&self, h: Vec3, wo: Vec3, wi: Vec3) -> Float { - 1.0 / (1.0 + self.lambda_ggx(h, wo) + self.lambda_ggx(h, wi)) - } - - fn lambda_ggx(&self, h: Vec3, v: Vec3) -> Float { - let voh = v.dot(h); - let voh_sq = voh * voh; - let tan_sq = (1.0 - voh_sq) / voh_sq; - - ((1.0 + self.alpha * self.alpha * tan_sq).sqrt() - 1.0) * 0.5 - } - - fn distribution_ggx(&self, hit: &Hit, h: Vec3) -> Float { - let noh = hit.normal.dot(h); - let alpha_sq = self.alpha * self.alpha; - let noh_sq = noh * noh; - let den = noh_sq * (alpha_sq - 1.0) + 1.0; - alpha_sq / (PI * den * den) - } - - fn sample_h(&self, hit: &Hit, _: Vec3) -> Vec3 { - let coord = Coordinate::new_from_z(hit.normal); - - let r1 = random_float(); - let r2 = random_float(); - let cos_theta = ((1.0 - r1) / (r1 * (self.alpha * self.alpha - 1.0) + 1.0)).sqrt(); - let sin_theta = (1.0 - cos_theta * cos_theta).sqrt(); - let phi_s = (2.0 * PI * r2).max(0.0).min(2.0 * PI); - - let mut h = - Vec3::new(phi_s.cos() * sin_theta, phi_s.sin() * sin_theta, cos_theta).normalised(); - - coord.vec_to_coordinate(&mut h); - - h - } -} - -impl Scatter for TrowbridgeReitz -where - T: Texture, -{ - fn scatter_ray(&self, ray: &mut Ray, hit: &Hit) -> bool { - let wo = -ray.direction; - - let h = self.sample_h(hit, wo); - - let direction = ray.direction.reflected(h); - - let point = offset_ray(hit.point, hit.normal, hit.error, true); - *ray = Ray::new(point, direction, ray.time); - - false - } - fn scattering_pdf(&self, hit: &Hit, wo: Vec3, wi: Vec3) -> Float { - let wo = -wo; - let h = (wo + wi).normalised(); - let a = self.distribution_ggx(hit, h) * h.dot(hit.normal).abs() / (4.0 * wo.dot(h)); - if a == 0.0 { - INFINITY - } else { - a - } - } - fn eval(&self, hit: &Hit, wo: Vec3, wi: Vec3) -> Vec3 { - let wo = -wo; - let h = (wi + wo).normalised(); - - if wi.dot(hit.normal) < 0.0 || h.dot(wo) < 0.0 { - return Vec3::zero(); - } - - let spec_component = self.fresnel(hit, wo, wi, h) - * self.geometry_ggx(h, wo, wi) - * self.distribution_ggx(hit, h) - / (4.0 * wo.dot(hit.normal) * wi.dot(hit.normal)); - - spec_component * hit.normal.dot(wi).abs() - } - fn eval_over_scattering_pdf(&self, hit: &Hit, wo: Vec3, wi: Vec3) -> Vec3 { - let wo = -wo; - let h = (wi + wo).normalised(); - - if wo.dot(h) < 0.0 || wi.dot(hit.normal) < 0.0 { - return Vec3::zero(); - } - - self.distribution_ggx(hit, h); - - self.fresnel(hit, wo, wi, h) * self.geometry_ggx(h, wo, wi) - / self.geometry_partial_ggx(h, wo) - } -} - -fn lerp(a: Vec3, b: Vec3, t: Float) -> Vec3 { - (1.0 - t) * a + t * b -} diff --git a/implementations/src/samplers/mod.rs b/implementations/src/samplers/mod.rs deleted file mode 100644 index 162f4b8..0000000 --- a/implementations/src/samplers/mod.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::textures::Texture; -use rt_core::{NoHit, Ray, Vec3}; -use std::sync::Arc; - -pub mod random_sampler; - -pub struct Sky { - texture: Option>, -} - -impl Sky { - pub fn new(texture: Option<&Arc>) -> Self { - let texture = texture.cloned(); - Sky { texture } - } -} - -impl NoHit for Sky { - fn get_colour(&self, ray: &Ray) -> Vec3 { - match &self.texture { - Some(texture) => texture.colour_value(ray.direction, ray.origin), - None => Vec3::zero(), - } - } -} diff --git a/implementations/src/utility/coord.rs b/implementations/src/utility/coord.rs deleted file mode 100644 index 6663540..0000000 --- a/implementations/src/utility/coord.rs +++ /dev/null @@ -1,25 +0,0 @@ -use rt_core::Vec3; - -pub struct Coordinate { - pub x: Vec3, - pub y: Vec3, - pub z: Vec3, -} - -impl Coordinate { - pub fn new_from_z(z: Vec3) -> Self { - let x = if z.x.abs() > z.y.abs() { - Vec3::new(-z.z, 0.0, z.x) / (z.x * z.x + z.z * z.z).sqrt() - } else { - Vec3::new(0.0, z.z, -z.y) / (z.y * z.y + z.z * z.z).sqrt() - }; - Coordinate { - x, - y: x.cross(z), - z, - } - } - pub fn vec_to_coordinate(&self, vec: &mut Vec3) { - *vec = vec.x * self.x + vec.y * self.y + vec.z * self.z; - } -} diff --git a/implementations/tests/bxdf.rs b/implementations/tests/bxdf.rs deleted file mode 100644 index 309a81f..0000000 --- a/implementations/tests/bxdf.rs +++ /dev/null @@ -1,77 +0,0 @@ -use crate::common::*; -use rt_core::*; - -mod common; - -fn test_bxdf(bxdf: B, bxdf_name: String) -> bool -where - B: Scatter, -{ - let hit = Hit { - t: 0.0, - error: Vec3::zero(), - point: Vec3::zero(), - uv: None, - normal: Vec3::new(0.0, 0.0, 1.0), - out: true, - }; - - let sample = |wo: Vec3| { - let mut ray = Ray::new(Vec3::zero(), wo, 0.0); - bxdf.scatter_ray(&mut ray, &hit); - ray.direction - }; - - let pdf = |wo: Vec3, wi: Vec3| bxdf.scattering_pdf(&hit, wo, wi); - for i in 0..CHI_TESTS { - let wo = generate_wo(); - let freq_table = samped_frequency_distribution(&sample, wo, THETA_RES, PHI_RES, SAMPLES); - let expected_freq_table: Vec = - integrate_frequency_table(&pdf, wo, THETA_RES, PHI_RES) - .into_iter() - .map(|x| x * SAMPLES as Float) - .collect(); - if i == 0 { - dump_tables( - wo, - &freq_table - .iter() - .map(|&v| v as Float) - .collect::>(), - &expected_freq_table, - THETA_RES, - PHI_RES, - &bxdf_name, - ); - } - let (df, chi_squared) = chi_squared(freq_table, expected_freq_table, SAMPLES); - let p = chi2_probability(df as f64, chi_squared as f64); - let threshold = 1.0 - (1.0 - CHI2_THRESHOLD).powf(1.0 / CHI_TESTS as Float); - if p < threshold as f64 || p.is_infinite() { - return false; - } - } - true -} - -#[test] -fn trowbridge_reitz() { - let bxdf = implementations::trowbridge_reitz::TrowbridgeReitz::new( - &std::sync::Arc::new(implementations::SolidColour::new(Vec3::new(0.0, 0.0, 0.0))), - 0.3, - 3.2 * Vec3::one(), - 1.0, - ); - - assert!(test_bxdf(bxdf, "ggx".to_string())) -} - -#[test] -fn lambertian() { - let bxdf = implementations::lambertian::Lambertian::new( - &std::sync::Arc::new(implementations::SolidColour::new(Vec3::new(0.0, 0.0, 0.0))), - 0.3, - ); - - assert!(test_bxdf(bxdf, "lambertain".to_string())) -} diff --git a/implementations/tests/common/int.rs b/implementations/tests/common/int.rs deleted file mode 100644 index 5e114ee..0000000 --- a/implementations/tests/common/int.rs +++ /dev/null @@ -1,50 +0,0 @@ -use rt_core::Float; - -const MAX_DEPTH: usize = 6; -const EPSILON: Float = 0.000001; - -pub fn adaptive_simpsons(function: F, a: Float, b: Float) -> Float -where - F: Fn(Float) -> Float, -{ - fn aux( - function: &F, - a: Float, - b: Float, - c: Float, - fa: Float, - fb: Float, - fc: Float, - i: Float, - epsilon: Float, - depth: usize, - ) -> Float - where - F: Fn(Float) -> Float, - { - let d = 0.5 * (a + b); - let e = 0.5 * (b + c); - let fd = function(d); - let fe = function(e); - - let h = c - a; - let i0 = (1.0 / 12.0) * h * (fa + 4.0 * fd + fb); - let i1 = (1.0 / 12.0) * h * (fb + 4.0 * fe + fc); - let ip = i0 + i1; - - if depth >= MAX_DEPTH || (ip - i).abs() < 15.0 * epsilon { - return ip + (1.0 / 15.0) * (ip - i); - } - - aux(function, a, d, b, fa, fd, fb, i0, 0.5 * epsilon, depth + 1) - + aux(function, b, e, c, fb, fe, fc, i1, 0.5 * epsilon, depth + 1) - } - let c = b; - let b = 0.5 * (a + b); - - let fa = function(a); - let fb = function(b); - let fc = function(c); - let i = (c - a) * (1.0 / 6.0) * (fa + 4.0 * fb + fc); - aux(&function, a, b, c, fa, fb, fc, i, EPSILON, 0) -} diff --git a/implementations/tests/common/mod.rs b/implementations/tests/common/mod.rs deleted file mode 100644 index a641942..0000000 --- a/implementations/tests/common/mod.rs +++ /dev/null @@ -1,238 +0,0 @@ -use rand::{rngs::SmallRng, thread_rng, Rng, SeedableRng}; -use rayon::prelude::*; -use rt_core::{vec::*, Float}; -use statrs::function::gamma::*; -use std::{ - f64::{consts::*, INFINITY}, - {fs::File, io::Write}, -}; - -pub mod int; - -pub const SAMPLES: usize = 10_000_000; -pub const THETA_RES: usize = 80; -pub const PHI_RES: usize = 2 * THETA_RES; -pub const CHI2_THRESHOLD: Float = 0.01; -pub const CHI_TESTS: usize = 1; - -use int::*; - -fn chi_squared_term(a: Float, b: Float) -> Float { - if a < (SAMPLES / 100000) as Float && b == 0.0 { - return 0.0; - } - let val = a - b; - val * val / b -} - -pub fn to_vec(sin_theta: Float, cos_theta: Float, phi: Float) -> Vec3 { - Vec3::new(phi.cos() * sin_theta, phi.sin() * sin_theta, cos_theta) -} - -pub fn chi2_probability(dof: f64, distance: f64) -> f64 { - assert!( - (gamma_lr(dof * 0.5, distance * 0.5) + gamma_ur(dof * 0.5, distance * 0.5) - 1.0).abs() - < 0.0001 - ); - gamma_ur(dof * 0.5, distance * 0.5) -} - -pub fn integrate_frequency_table( - pdf: &F, - wo: Vec3, - theta_res: usize, - phi_res: usize, -) -> Vec -where - F: Fn(Vec3, Vec3) -> Float, -{ - let theta_step = PI as Float / theta_res as Float; - let phi_step = TAU as Float / phi_res as Float; - let pdf = |phi: Float, a, b| { - adaptive_simpsons( - |theta| { - pdf( - wo, - Vec3::new( - phi.cos() * theta.sin(), - phi.sin() * theta.sin(), - theta.cos(), - ), - ) * theta.sin() - }, - a, - b, - ) - }; - - let mut vec = Vec::new(); - for theta_i in 0..theta_res { - for phi_i in 0..phi_res { - let a = adaptive_simpsons( - |phi| { - pdf( - phi, - theta_i as Float * theta_step, - (theta_i + 1) as Float * theta_step, - ) - }, - phi_i as Float * phi_step, - (phi_i + 1) as Float * phi_step, - ); - vec.push(a); - } - } - vec -} - -pub fn samped_frequency_distribution( - function: &F, - wo: Vec3, - theta_res: usize, - phi_res: usize, - sample_count: usize, -) -> Vec -where - F: Fn(Vec3) -> Vec3, -{ - let mut freq = vec![0.0; theta_res * phi_res]; - let theta_step = PI as Float / theta_res as Float; - let phi_step = TAU as Float / phi_res as Float; - - for _ in 0..sample_count { - let sampled = function(wo); - let theta = sampled.z.acos(); - let mut phi = (sampled.y).atan2(sampled.x); - - if phi < 0.0 { - phi += TAU as Float; - } - - let theta_bin = ((theta / theta_step) as usize).max(0).min(theta_res - 1); - let phi_bin = ((phi / phi_step) as usize).max(0).min(phi_res - 1); - - freq[theta_bin * phi_res + phi_bin] += 1.0; - } - - freq -} - -pub fn random_float() -> Float { - let mut rng = SmallRng::from_rng(thread_rng()).unwrap(); - rng.gen() -} - -pub fn generate_wo() -> Vec3 { - let cos_theta = random_float(); - let phi = TAU as Float * random_float(); - - to_vec((1.0 - cos_theta * cos_theta).sqrt(), cos_theta, phi) -} - -pub fn chi_squared( - freq_table: Vec, - expected_freq_table: Vec, - samples: usize, -) -> (usize, Float) { - assert_eq!(freq_table.len(), expected_freq_table.len()); - - let mut values = expected_freq_table - .into_par_iter() - .zip(freq_table.into_par_iter()) - .collect::>(); - - values.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); - - let mut df = 0; - - let mut expected_pooled = 0.0; - let mut actual_pooled = 0.0; - - let mut chi_squared = 0.0; - - for (expected, actual) in values { - if expected == 0.0 { - if actual > (samples / 100_000) as Float { - chi_squared += INFINITY as Float; - } - } else if expected < 5.0 || (expected_pooled > 0.0 && expected_pooled < 5.0) { - expected_pooled += expected; - actual_pooled += actual; - } else { - let diff = actual - expected; - chi_squared += diff * diff / expected; - df += 1; - } - } - - if actual_pooled > 0.0 || expected_pooled > 0.0 { - let diff = actual_pooled - expected_pooled; - chi_squared += diff * diff / expected_pooled; - df += 1; - } - - df -= 1; - - (df, chi_squared) -} - -pub fn dump_tables( - wo: Vec3, - freq_table: &[Float], - expected_freq_table: &[Float], - theta_res: usize, - phi_res: usize, - bxdf_name: &str, -) { - let enumerate = |file: &mut File, func: fn(Float, Float) -> Float| { - (0..theta_res * phi_res).for_each(|index| { - file.write_all( - format!("{}", func(freq_table[index], expected_freq_table[index])).as_bytes(), - ) - .unwrap(); - - if index % phi_res + 1 != phi_res { - file.write_all(b", ").unwrap(); - } else if index / phi_res + 1 != theta_res { - file.write_all(b"; ").unwrap(); - } - }); - }; - - let time = chrono::Local::now(); - - let mut file = File::create(format!( - "chi_test_{bxdf_name}@{}.m", - time.format("%Y-%m-%d:%H:%M") - )) - .unwrap(); - - file.write_all(format!("% wo = {wo}\nfrequencies = [ ").as_bytes()) - .unwrap(); - enumerate(&mut file, |o, _| o); - - file.write_all(b" ];\nexpected_frequencies = [ ").unwrap(); - enumerate(&mut file, |_, e| e); - - file.write_all(b" ];\nchi_terms = [ ").unwrap(); - enumerate(&mut file, chi_squared_term); - - file.write_all( - b" ]; -colormap(jet); -clf; -subplot(3,1,1); -imagesc([0, 360], [0, 180], frequencies); -axis equal; -title('observed frequencies'); -subplot(3,1,2); -imagesc([0, 360], [0, 180], chi_terms); -axis equal; -title('chi terms'); -subplot(3,1,3); -imagesc([0, 360], [0, 180], expected_frequencies); -axis equal; -title('expected frequencies');", - ) - .unwrap(); -} diff --git a/rt_core/Cargo.toml b/rt_core/Cargo.toml deleted file mode 100644 index 835bd6f..0000000 --- a/rt_core/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "rt_core" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -rand = { version = "0.8.3", features = [ "small_rng" ] } \ No newline at end of file diff --git a/rt_core/src/acceleration.rs b/rt_core/src/acceleration.rs deleted file mode 100644 index a6e6d8c..0000000 --- a/rt_core/src/acceleration.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::{Primitive, Ray, Scatter, SurfaceIntersection}; - -pub trait AccelerationStructure -where - P: Primitive, - M: Scatter, -{ - fn get_intersection_candidates(&self, ray: &Ray) -> Vec<(usize, usize)>; - - fn check_hit_index(&self, ray: &Ray, object_index: usize) -> Option>; - - fn check_hit(&self, ray: &Ray) -> Option<(SurfaceIntersection, usize)>; - - fn get_samplable(&self) -> &[usize] { - unimplemented!() - } - - fn get_object(&self, _index: usize) -> Option<&P> { - unimplemented!() - } -} diff --git a/rt_core/src/ray.rs b/rt_core/src/ray.rs deleted file mode 100644 index 6d6d235..0000000 --- a/rt_core/src/ray.rs +++ /dev/null @@ -1,297 +0,0 @@ -use crate::{ - power_heuristic, AccelerationStructure, Float, Hit, NoHit, Primitive, Scatter, - SurfaceIntersection, Vec3, -}; -use rand::{prelude::SliceRandom, rngs::SmallRng, thread_rng, Rng, SeedableRng}; - -const RUSSIAN_ROULETTE_THRESHOLD: u32 = 3; -const MAX_DEPTH: u32 = 50; - -pub type Colour = Vec3; - -pub struct Ray { - pub origin: Vec3, - pub direction: Vec3, - pub d_inverse: Vec3, - pub shear: Vec3, - pub time: Float, -} - -impl Ray { - pub fn new(origin: Vec3, mut direction: Vec3, time: Float) -> Self { - direction.normalise(); - - let max_axis = - if direction.x.abs() > direction.y.abs() && direction.x.abs() > direction.z.abs() { - 0 - } else if direction.y.abs() > direction.z.abs() { - 1 - } else { - 2 - }; - - let mut swaped_dir = direction; - match max_axis { - 0 => { - std::mem::swap(&mut swaped_dir.x, &mut swaped_dir.z); - } - 1 => { - std::mem::swap(&mut swaped_dir.x, &mut swaped_dir.z); - } - _ => {} - } - let shear_x = -swaped_dir.x / swaped_dir.z; - let shear_y = -swaped_dir.y / swaped_dir.z; - let shear_z = 1.0 / swaped_dir.z; - - Ray { - origin, - direction, - d_inverse: Vec3::new(1.0 / direction.x, 1.0 / direction.y, 1.0 / direction.z), - shear: Vec3::new(shear_x, shear_y, shear_z), - time, - } - } - - pub fn at(&self, t: Float) -> Vec3 { - self.origin + self.direction * t - } - - fn sample_light, P: Primitive, M: Scatter>( - bvh: &A, - hit: &Hit, - mat: &M, - wo: Vec3, - ) -> Vec3 { - let light_index = match bvh - .get_samplable() - .choose(&mut SmallRng::from_rng(thread_rng()).unwrap()) - { - Some(&index) => index, - None => return Vec3::zero(), - }; - - let samplable = bvh.get_object(light_index).unwrap(); - - let sampled_wi = samplable.sample_visible_from_point(hit.point); - - if let Some(sampled_si) = bvh.check_hit_index( - &Ray::new(hit.point + 0.0001 * hit.normal, sampled_wi, 0.0), - light_index, - ) { - let sampled_hit = &sampled_si.hit; - - let sampled_pdf = samplable.scattering_pdf(hit.point, sampled_wi, sampled_hit); - - if sampled_pdf > 0.0 { - let li = sampled_si.material.get_emission(sampled_hit, sampled_wi); - - let f = mat.eval(hit, wo, sampled_wi); - - let num_lights = bvh.get_samplable().len() as Float; - - let scattering_pdf = mat.scattering_pdf(hit, wo, sampled_wi); - - let weight = power_heuristic(sampled_pdf, scattering_pdf); - - return li * f * num_lights * weight / sampled_pdf; - } - } - Vec3::zero() - } - - fn sample_light_mis, P: Primitive, M: Scatter>( - bvh: &A, - hit: &Hit, - mat: &M, - wo: Vec3, - index: usize, - wi: Vec3, - new_si: &SurfaceIntersection, - ) -> Vec3 { - let mut output = Vec3::zero(); - let samplable = bvh.get_object(index).unwrap(); - - let sampled_wi = samplable.sample_visible_from_point(hit.point); - - if let Some(sampled_si) = bvh.check_hit_index( - &Ray::new(hit.point + 0.0001 * hit.normal, sampled_wi, 0.0), - index, - ) { - let sampled_hit = &sampled_si.hit; - - let sampled_pdf = samplable.scattering_pdf(hit.point, sampled_wi, sampled_hit); - - if sampled_pdf > 0.0 { - let li = new_si.material.get_emission(sampled_hit, sampled_wi); - - let f = mat.eval(hit, wo, sampled_wi); - - let scattering_pdf = mat.scattering_pdf(hit, wo, sampled_wi); - - let weight = power_heuristic(sampled_pdf, scattering_pdf); - - output += li * f * weight / sampled_pdf; - } - } - - let scattering_pdf = mat.scattering_pdf(hit, wo, wi); - let li = new_si.material.get_emission(&new_si.hit, wi); - - let sampling_pdf = samplable.scattering_pdf(hit.point, wi, &new_si.hit); - - let weight = power_heuristic(scattering_pdf, sampling_pdf); - - let fp = mat.eval_over_scattering_pdf(hit, wo, wi); - - output += li * fp * weight; - - output - } - - pub fn get_colour, P: Primitive, M: Scatter, S: NoHit>( - ray: &mut Ray, - sky: &S, - bvh: &A, - ) -> (Colour, u64) { - let mut output; - let mut throughput = Colour::one(); - let mut ray_count = 1; - - if let Some((surface_intersection, _index)) = bvh.check_hit(ray) { - let (mut hit, mut mat) = (surface_intersection.hit, surface_intersection.material); - let mut wo = ray.direction; - output = mat.get_emission(&hit, wo); - - let mut exit = mat.scatter_ray(ray, &hit); - - if !exit { - for depth in 1..MAX_DEPTH { - if exit { - ray_count += depth as u64; - break; - } - - let wi = ray.direction; - - if let Some((new_si, new_index)) = bvh.check_hit(ray) { - if mat.is_delta() { - throughput *= mat.eval(&hit, wo, wi); - output += throughput * new_si.material.get_emission(&hit, wo); - } else if bvh.get_samplable().contains(&new_index) { - output += throughput - * Self::sample_light_mis( - bvh, &hit, &mat, wo, new_index, wi, &new_si, - ); - ray_count += 1; - throughput *= mat.eval_over_scattering_pdf(&hit, wo, wi); - } else { - output += throughput * Self::sample_light(bvh, &hit, &mat, wo); - ray_count += 1; - throughput *= mat.eval_over_scattering_pdf(&hit, wo, wi); - } - - mat = new_si.material; - hit = new_si.hit; - wo = wi; - } else { - if mat.is_delta() { - throughput *= mat.eval(&hit, wo, wi); - } else { - output += throughput * Self::sample_light(bvh, &hit, &mat, wo); - ray_count += 1; - throughput *= mat.eval_over_scattering_pdf(&hit, wo, wi); - } - - output += throughput * sky.get_colour(ray); - ray_count += depth as u64; - break; - } - - if depth > RUSSIAN_ROULETTE_THRESHOLD { - let p = throughput.component_max(); - let mut rng = SmallRng::from_rng(thread_rng()).unwrap(); - if rng.gen::() > p { - ray_count += depth as u64; - break; - } - throughput /= p; - } - - exit = mat.scatter_ray(ray, &hit); - } - } else { - output = mat.get_emission(&hit, wo); - } - } else { - output = sky.get_colour(ray); - } - (output, ray_count) - } - - pub fn get_colour_naive< - A: AccelerationStructure, - P: Primitive, - M: Scatter, - S: NoHit, - >( - ray: &mut Ray, - sky: &S, - bvh: &A, - ) -> (Colour, u64) { - let (mut throughput, mut output) = (Colour::one(), Colour::zero()); - let mut depth = 0; - let mut ray_count = 0; - - while depth < MAX_DEPTH { - let hit_info = bvh.check_hit(ray); - - ray_count += 1; - - if let Some((surface_intersection, _index)) = hit_info { - let (hit, mat) = (&surface_intersection.hit, &surface_intersection.material); - - let wo = ray.direction; - - let emission = mat.get_emission(hit, wo); - - let exit = mat.scatter_ray(ray, hit); - - if depth == 0 { - output += throughput * emission; - } - - if exit { - output += throughput * emission; - break; - } - - if !mat.is_delta() { - throughput *= mat.eval_over_scattering_pdf(hit, wo, ray.direction); - //mat.eval(hit, wo, ray.direction) - // / mat.scattering_pdf(hit, wo, ray.direction); - } else { - throughput *= mat.eval(hit, wo, ray.direction); - } - - if depth > RUSSIAN_ROULETTE_THRESHOLD { - let p = throughput.component_max(); - let mut rng = SmallRng::from_rng(thread_rng()).unwrap(); - if rng.gen::() > p { - break; - } - throughput /= p; - } - - depth += 1; - } else { - output += throughput * sky.get_colour(ray); - break; - } - } - if output.contains_nan() || !output.is_finite() { - return (Vec3::zero(), ray_count); - } - (output, ray_count) - } -} diff --git a/scenes/overshadowed.ssml b/scenes/overshadowed.ssml new file mode 100644 index 0000000..0d491c2 --- /dev/null +++ b/scenes/overshadowed.ssml @@ -0,0 +1,60 @@ +camera ( + origin -5 3 -3 + lookat 0 0.5 0 + vup 0 1 0 + fov 34.0 + aperture 0.0 + focus_dis 10.0 +) + +texture sky ( + type solid + colour 0.0 +) + +sky ( + texture sky +) + +texture grey ( + type solid + colour 0.5 +) + +texture white ( + type solid + colour 1.0 +) + +material ground ( + type lambertian + texture grey + albedo 0.5 +) + +material light ( + type emissive + texture white + strength 1.5 +) + +primitive ( + type sphere + material ground + centre 0 -1000 0 + radius 1000 +) + +primitive ( + type sphere + material light + centre 0 0.5 0 + radius 0.5 +) + +mesh ( + type aacuboid + material ground + point_one -0.5 0.1 -0.5 + point_two -0.4 0.2 -0.4 +) \ No newline at end of file diff --git a/scenes/rtweekend1.ssml b/scenes/rtweekend1.ssml new file mode 100644 index 0000000..ae69b28 --- /dev/null +++ b/scenes/rtweekend1.ssml @@ -0,0 +1,43 @@ +camera ( + origin 0 0 0 + lookat 0 1 0 + vup 0 0 1 + fov 121.28449291441745 + aperture 0.0 + focus_dis 1.0 +) + +texture sky ( + type lerp + primary 0.5 0.7 1.0 + secondary 1.0 +) + +sky ( + texture sky +) + +texture grey ( + type solid + colour 0.5 +) + +material ground ( + type lambertian + texture grey + albedo 1.0 +) + +primitive ( + type sphere + material ground + centre 0 1 -100.5 + radius 100 +) + +primitive ( + type sphere + material ground + centre 0 1 0 + radius 0.5 +) diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..31e6512 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,231 @@ +use crate::parameters::Parameters; +use crate::scene::Scene; +use implementations::rt_core::*; +use implementations::*; +use indicatif::ProgressBar; +use indicatif::ProgressStyle; +use output::*; + +#[cfg(feature = "gui")] +use { + gui::*, + std::sync::{atomic::*, Arc}, + vulkano::{buffer::CpuAccessibleBuffer, instance::Instance}, + winit::event_loop::EventLoopProxy, +}; + +mod parameters; +mod scene; + +#[cfg(feature = "gui")] +fn render_gui( + render_options: RenderOptions, + filename: Option, + scene: Scene, +) where + M: Scatter + 'static, + P: Primitive + 'static, + C: Camera + 'static, + S: NoHit + 'static, + A: AccelerationStructure + 'static, +{ + let required_extensions = vulkano_win::required_extensions(); + let instance = Instance::new( + None, + vulkano::instance::Version::V1_5, + &required_extensions, + None, + ) + .unwrap(); + let exit = Arc::new(AtomicBool::new(false)); + + let gui = Gui::new( + &instance, + render_options.width as u32, + render_options.height as u32, + exit.clone(), + ); + + let event_loop_proxy: Option> = + gui.event_loop.as_ref().map(|el| el.create_proxy()); + let iter = [0.0f32, 0.0, 0.0, 0.0] + .repeat((render_options.width * render_options.height) as usize) + .into_iter(); + let buffer = CpuAccessibleBuffer::from_iter( + gui.device.clone(), + vulkano::buffer::BufferUsage::all(), + true, + iter, + ) + .unwrap(); + + let samples = Arc::new(AtomicU64::new(0)); + let ray_count = Arc::new(AtomicU64::new(0)); + + let command_buffers = create_command_buffers( + gui.device.clone(), + gui.queue.clone(), + buffer.clone(), + gui.cpu_rendering.cpu_swapchain.clone(), + ); + + let mut data = Data::new( + gui.queue.clone(), + gui.device.clone(), + gui.cpu_rendering.to_sc.clone(), + gui.cpu_rendering.from_sc.clone(), + command_buffers, + buffer.clone(), + gui.cpu_rendering.copy_to_first.clone(), + samples.clone(), + render_options.samples_per_pixel, + ray_count.clone(), + exit, + event_loop_proxy.unwrap(), + ); + + let start = print_render_start( + render_options.width, + render_options.height, + render_options.gamma as f64, + Some(render_options.samples_per_pixel), + ); + + let render_canceled = Arc::new(AtomicBool::new(true)); + + let moved_render_canceled = render_canceled.clone(); + let moved_filename = filename.clone(); + + let handle = std::thread::spawn(move || { + let ray_count = data.rays_shot.clone(); + let samples = data.samples.clone(); + let buffer = data.buffer.clone(); + let to_sc = data.to_sc.clone(); + + scene.render( + render_options, + Some(( + &mut data, + |data: &mut Data, previous: &SamplerProgress, i: u64| -> bool { + sample_update(data, previous, i) + }, + )), + ); + + let ray_count = ray_count.load(Ordering::Relaxed); + let samples = samples.load(Ordering::Relaxed); + + print_final_statistics(start, ray_count, samples); + + moved_render_canceled.store(false, Ordering::Relaxed); + + if let Some(filename) = moved_filename { + match &*to_sc.lock().unwrap() { + Some(future) => { + future.wait(None).unwrap(); + } + None => {} + } + + save_data_to_image( + filename, + render_options.width as u32, + render_options.height as u32, + rgba_to_rgb(&*buffer.read().unwrap()), + render_options.gamma, + ); + } + }); + + gui.run(); + handle.join().unwrap(); +} + +fn render_tui( + render_options: RenderOptions, + filename: Option, + scene: Scene, +) where + M: Scatter, + P: Primitive, + C: Camera, + S: NoHit, + A: AccelerationStructure, +{ + let start = print_render_start( + render_options.width, + render_options.height, + render_options.gamma as f64, + Some(render_options.samples_per_pixel), + ); + + struct Progress { + pub sampler_progress: SamplerProgress, + pub bar: ProgressBar, + } + + let mut image = Progress { + sampler_progress: SamplerProgress::new(render_options.width * render_options.height, 3), + bar: ProgressBar::new(render_options.samples_per_pixel).with_style( + ProgressStyle::default_bar() + .template("[{elapsed_precise}] {bar:40.cyan/blue} {pos:>7}/{len:7} {msg}") + .unwrap(), + ), + }; + let progress_bar_output = |sp: &mut Progress, previous: &SamplerProgress, i: u64| -> bool { + sp.sampler_progress.samples_completed += 1; + sp.sampler_progress.rays_shot += previous.rays_shot; + + sp.sampler_progress + .current_image + .iter_mut() + .zip(previous.current_image.iter()) + .for_each(|(pres, acc)| { + *pres += (acc - *pres) / i as Float; // since copies first buffer when i=1 + }); + sp.bar.set_position(sp.sampler_progress.samples_completed); + if sp.sampler_progress.samples_completed == render_options.samples_per_pixel { + sp.bar.finish_and_clear() + } + false + }; + + scene.render(render_options, Some((&mut image, progress_bar_output))); + + let ray_count = image.sampler_progress.rays_shot; + + print_final_statistics(start, ray_count, image.sampler_progress.samples_completed); + + if let Some(filename) = filename { + save_data_to_image( + filename, + render_options.width as u32, + render_options.height as u32, + image.sampler_progress.current_image, + render_options.gamma, + ); + } +} + +fn main() { + create_logger(); + let (scene, parameters) = match parameters::process_args() { + Some(data) => data, + None => return, + }; + + let Parameters { + render_options, + gui, + filename, + } = parameters; + + if !gui { + render_tui(render_options, filename, scene); + } else { + #[cfg(feature = "gui")] + render_gui(render_options, filename, scene); + #[cfg(not(feature = "gui"))] + println!("feature: gui not enabled"); + } +} diff --git a/src/parameters.rs b/src/parameters.rs new file mode 100644 index 0000000..193bd98 --- /dev/null +++ b/src/parameters.rs @@ -0,0 +1,78 @@ +use crate::{scene::Scene, Float}; +use clap::Parser; + +use implementations::{split::SplitType, *}; +use region::Region; + +type MaterialType<'a> = AllMaterials<'a, AllTextures>; +type PrimitiveType<'a> = AllPrimitives<'a, MaterialType<'a>>; +type SkyType<'a> = Sky<'a, AllTextures, MaterialType<'a>>; +type BvhType<'a> = Bvh, MaterialType<'a>, SkyType<'a>>; +pub type SceneType<'a> = + Scene, PrimitiveType<'a>, SimpleCamera, SkyType<'a>, BvhType<'a>>; + +pub struct Parameters { + pub render_options: RenderOptions, + pub gui: bool, + pub filename: Option, +} + +#[derive(Parser, Debug)] +#[command(about, long_about=None)] +#[command(name = "Pathtracer")] +#[command(about = "An experimental pathtracer written in Rust")] +struct Cli { + #[arg(short, long, default_value_t = false)] + gui: bool, + #[arg(short, long, default_value_t = 128)] + samples: u64, + #[arg(short = 'x', long, default_value_t = 1920)] + width: u64, + #[arg(short = 'y', long, default_value_t = 1080)] + height: u64, + #[arg(short, long)] + filepath: String, + #[arg(short, long,value_enum, default_value_t = SplitType::Sah)] + bvh_type: SplitType, + #[arg(short, long,value_enum, default_value_t = RenderMethod::MIS)] + render_method: RenderMethod, + #[arg(short, long)] + output: Option, + #[arg(long, default_value_t = 2.2)] + gamma: Float, +} + +pub fn process_args() -> Option<(SceneType<'static>, Parameters)> { + let cli = Cli::parse(); + + let mut region = Region::new(); + let (primitives, camera, sky) = match loader::load_file_full::< + AllTextures, + MaterialType, + PrimitiveType, + SimpleCamera, + SkyType, + >(&mut region, &cli.filepath) + { + Ok(a) => a, + Err(e) => panic!("{e:?}"), + }; + + let bvh = Bvh::new(primitives, sky, cli.bvh_type); + + let scene = Scene::new(bvh, camera, region); + + let render_ops = RenderOptions { + width: cli.width, + height: cli.height, + samples_per_pixel: cli.samples, + render_method: cli.render_method, + gamma: cli.gamma, + }; + let params = Parameters { + render_options: render_ops, + gui: cli.gui, + filename: cli.output, + }; + Some((scene, params)) +} diff --git a/src/scene.rs b/src/scene.rs new file mode 100644 index 0000000..242facf --- /dev/null +++ b/src/scene.rs @@ -0,0 +1,153 @@ +use implementations::random_sampler::RandomSampler; +use implementations::rt_core::*; +use implementations::*; +use region::Region; +use std::mem::ManuallyDrop; + +pub struct Scene +where + M: Scatter, + P: Primitive, + C: Camera, + S: NoHit, + A: AccelerationStructure, +{ + acceleration: A, + camera: C, + _region: ManuallyDrop, +} + +impl Scene +where + M: Scatter, + P: Primitive, + C: Camera, + S: NoHit, + A: AccelerationStructure, +{ + pub fn new(acceleration: A, camera: C, region: ManuallyDrop) -> Self { + Self { + acceleration, + camera, + _region: region, + } + } + pub fn render( + &self, + opts: RenderOptions, + update: Option<(&mut T, impl Fn(&mut T, &SamplerProgress, u64) -> bool)>, + ) { + let sampler = RandomSampler {}; + sampler.sample_image(opts, &self.camera, &self.acceleration, update); + } +} + +unsafe impl Send for Scene +where + M: Scatter, + P: Primitive, + C: Camera, + S: NoHit, + A: AccelerationStructure, +{ +} + +/*#[cfg(test)] +mod tests { + use super::*; + use loader::load_str_full; + + const DATA: &str = "camera ( + origin -5 3 -3 + lookat 0 0.5 0 + vup 0 1 0 + fov 34.0 + aperture 0.0 + focus_dis 10.0 +) + +texture sky ( + type solid + colour 0.0 +) + +sky ( + texture sky +) + +texture grey ( + type solid + colour 0.5 +) + +texture white ( + type solid + colour 1.0 +) + +material ground ( + type lambertian + texture grey + albedo 0.5 +) + +material light ( + type emissive + texture white + strength 1.5 +) + +primitive ( + type sphere + material ground + centre 0 -1000 0 + radius 1000 +) + +primitive ( + type sphere + material light + centre 0 0.5 0 + radius 0.5 +) + +primitive ( + type sphere + material ground + centre -0.45 0.15 -0.45 + radius 0.05 +)"; + + #[test] + fn scene() { + let mut region = Region::new(); + type Tex = AllTextures; + type Mat<'a> = AllMaterials<'a, Tex>; + type Prim<'a> = AllPrimitives<'a, Mat<'a>>; + type SkyType<'a> = Sky<'a, Tex, Mat<'a>>; + type SceneType<'a> = Scene< + Mat<'a>, + Prim<'a>, + SimpleCamera, + SkyType<'a>, + Bvh, Mat<'a>, SkyType<'a>>, + >; + let stuff = + load_str_full::(&mut region, DATA).unwrap(); + + let (p, camera, sky) = stuff; + let bvh: Bvh = Bvh::new(p, sky.clone(), split::SplitType::Sah); + + let scene: SceneType<'static> = Scene::new(bvh, camera, sky, region); + + scene.render::<()>( + RenderOptions { + samples_per_pixel: 1, + render_method: RenderMethod::MIS, + width: 1920, + height: 1080, + }, + None as Option<(&mut (), fn(&mut (), &SamplerProgress, u64) -> bool)>, + ); + } +}*/