├── .ccls ├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── makefile.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── assets ├── demo_moving.gif └── demo_still.gif ├── demos ├── 01_spinning_cube.c ├── 02_3_diamonds.c ├── 02_spinning_coffin.c ├── 03_3_diamonds.c ├── 04_overlapping_objects.c ├── 05_moving_object.c ├── 06_moving_in_world.c ├── Makefile └── README.md ├── flake.lock ├── flake.nix ├── include ├── arg_parser.h ├── objects.h ├── renderer.h ├── screen.h ├── utils.h ├── vector.h └── xtrig.h ├── main.c ├── mesh_files ├── README.md ├── coffin.scl ├── cube.scl └── rhombus.scl ├── package.nix └── src ├── .ccls ├── arg_parser.c ├── objects.c ├── renderer.c ├── screen.c ├── utils.c ├── vector.c └── xtrig.c /.ccls: -------------------------------------------------------------------------------- 1 | gcc 2 | %c -std=gnu99 3 | -Iinclude 4 | -Wall 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | indent_style = tab 4 | indent_size = 4 5 | trim_trailing_whitespace = true 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Explicitly declare text files you want to always be normalized and converted 5 | # to native line endings on checkout. 6 | *.c text 7 | *.h text 8 | 9 | # Declare files that will always have CRLF line endings on checkout. 10 | *.sln text eol=crlf 11 | 12 | # Denote all files that are truly binary and should not be modified. 13 | *.png binary 14 | *.jpg binary 15 | -------------------------------------------------------------------------------- /.github/workflows/makefile.yml: -------------------------------------------------------------------------------- 1 | name: Makefile CI 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | paths-ignore: 7 | - 'README*' 8 | - 'assets/**' 9 | pull_request: 10 | branches: [ "master" ] 11 | paths-ignore: 12 | - 'README*' 13 | - 'assets/**' 14 | 15 | jobs: 16 | build: 17 | 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | 23 | - name: requirements 24 | run: sudo apt-get install libxrandr-dev 25 | 26 | - name: build 27 | run: make && cd demos && make 28 | 29 | # NOTE: running it in CI as ./cube -mi 5 doesn't work as CI redirects the stdout to a file 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Object files 5 | *.o 6 | *.ko 7 | *.obj 8 | *.elf 9 | 10 | # Linker output 11 | *.ilk 12 | *.map 13 | *.exp 14 | 15 | # Precompiled Headers 16 | *.gch 17 | *.pch 18 | 19 | # Libraries 20 | *.lib 21 | *.a 22 | *.la 23 | *.lo 24 | 25 | # Shared objects (inc. Windows DLLs) 26 | *.dll 27 | *.so 28 | *.so.* 29 | *.dylib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | *.i*86 36 | *.x86_64 37 | *.hex 38 | 39 | # Debug files 40 | *.dSYM/ 41 | *.su 42 | *.idb 43 | *.pdb 44 | 45 | # Kernel Module Compile Results 46 | *.mod* 47 | *.cmd 48 | .tmp_versions/ 49 | modules.order 50 | Module.symvers 51 | Mkfile.old 52 | dkms.conf 53 | 54 | # nix evaluation result 55 | result 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | This is a small 3D graphics engine for the terminal. 633 | Copyright (C) 2023 Leontios Mavropalias 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published by 637 | the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ############################################### 2 | # Definitions 3 | ############################################### 4 | CC = gcc 5 | EXEC = cube 6 | SRC_DIR = src 7 | INC_DIR = include 8 | # where to store the mesh (text) files - 9 | # set it from the command line if you want another location 10 | PREFIX = /usr 11 | CFG_DIR = $(PREFIX)/share/retrocube 12 | CFLAGS = -Wall -Wno-stringop-truncation -Wno-maybe-uninitialized -I$(INC_DIR)\ 13 | -std=gnu99 -O3 -DCFG_DIR=$(CFG_DIR) 14 | LDFLAGS = -lm 15 | SOURCES = $(wildcard $(SRC_DIR)/*.c) \ 16 | main.c 17 | OBJECTS = $(SOURCES:%.c=%.o) 18 | MKDIR = mkdir -p 19 | CP = cp -r 20 | RM = rm -rf 21 | 22 | 23 | ############################################### 24 | # Compilation 25 | ############################################### 26 | all: $(EXEC) 27 | 28 | $(EXEC): $(OBJECTS) cfg 29 | $(CC) $(OBJECTS) -o $(EXEC) $(LDFLAGS) 30 | 31 | %.o: %.c 32 | $(CC) $(CFLAGS) -c $^ -o $@ 33 | 34 | ############################################### 35 | # Commands (phony targets) 36 | ############################################### 37 | .PHONY: cfg 38 | cfg: 39 | # copy config files into CFG_DIR 40 | $(MKDIR) $(CFG_DIR) 41 | $(CP) mesh_files/*.scl $(CFG_DIR) 42 | 43 | .PHONY: clean 44 | clean: 45 | $(RM) $(OBJECTS) 46 | $(RM) $(EXEC) 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## :black_large_square: retrocube :white_large_square: 2 | 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | --- 5 | Render 3D meshes in ASCII on the command line. 6 | It runs on the standard C library. 7 | ``` 8 | +== 9 | ++====== 10 | +============ 11 | ================== 12 | ======================== 13 | +===========================.. 14 | ||+=====================....... 15 | ||+=================.......... 16 | +||||===========............... 17 | +||||||==+..................... 18 | +||||||........................ 19 | +||||||....................... 20 | +||||||~....................... 21 | +||||||....................~.% 22 | |||||...............~. 23 | ||||~..........~.. 24 | ||......... 25 | ||....~ 26 | 27 | ``` 28 | 29 | ### 1. This implementation 30 | 31 | In human language, the graphics are rendered more or less by the following algorithm: 32 | ``` 33 | rows <- terminal's height 34 | columns <- terminal's width 35 | // a face (surface) is a plane segment (x, y, z) restricted within 4 cube vertices 36 | initialise a cube (6 faces) 37 | for (r in rows): 38 | for (c in columns): 39 | z_rendered <- +inf 40 | have_intersection <- false 41 | pixel_to_draw <- (c, r) 42 | color_to_draw <- background 43 | for (surface in cube's faces): 44 | // from equation ax + by + cz + d = 0 45 | z <- surface.z(c, r) 46 | if (z < z_rendered) and ((c, r, z) in surface): 47 | z_rendered <- z 48 | color_to_draw <- surface.color 49 | draw(pixel_to_draw, color_to_draw) 50 | ``` 51 | 52 | ### 2. Requirements 53 | 54 | Currenctly there is no Windows support. You only need gcc and make: 55 | 1. **gcc** 56 | 2. **make** 57 | 58 | ### 3. Development and installation 59 | 60 | #### 3.1 Development 61 | 62 | ##### 3.1.1 Compiling the project 63 | 64 | The naming convention follows the one of [stb](https://github.com/nothings/stb). 65 | Source files are found in `src` and headers in `include`. 66 | 67 | When compiling the from project from a clean state, you need to specify where the mesh files 68 | (those that specify how shapes are rendereed) shall be stored. You can do this by setting the 69 | `PREFIX` variable to your directory of choice, e.g.: 70 | ``` 71 | make PREFIX=~/.config/retrocube 72 | ``` 73 | You can run the binary with (a list of command line arguments is provided in the next section): 74 | ``` 75 | ./cube 76 | ``` 77 | You can delete the binary and object files with: 78 | ``` 79 | make clean 80 | ``` 81 | ##### 3.1.2 Compiling the demos 82 | 83 | Several demos that showcase various usages of the libraries are found in the `demos` directory. 84 | These are compiled independently from their own file. To compile them you need to set the `PREFIX` 85 | once again: 86 | ``` 87 | cd demos 88 | make PREFIX=~/.config/retrocube 89 | # then you will see some binaries and run the binary of your choice 90 | ``` 91 | 92 | 93 | #### 3.2 General installation 94 | 95 | The `Makefile` includes an installation command. The binary will be installed at `/usr/bin/cube` as: 96 | ``` 97 | sudo make install 98 | ``` 99 | Similarly, you can uninstall it from `/usr/bin` as: 100 | ``` 101 | sudo make uninstall 102 | ``` 103 | 104 | #### 3.3 Installation as Nix package 105 | 106 | On Nix (with flakes enabled) you don't need to install it and you can directly run it with: 107 | ``` 108 | nix run github:leonmavr/retrocube 109 | ``` 110 | Credits for the Nix packaging @pmarreck and @Quantenzitrone. 111 | 112 | ### 4. Usage 113 | 114 | #### 4.1 Arguments 115 | 116 | By default the program runs forever so you can stop it with `Ctr+C`. Below are the command line arguments it accepts. 117 | 118 | 119 | | Short specifier | Long specifier | Argument type | Default | Description | 120 | |:--------------- |:--------------------------|:--------------|:--------|:--------------------------------------------------------------------------------------------| 121 | | `-sx` | `--speedx` | float | 0.7 |Rotational speed around the x axis (-1 to 1). If set, disables random rotations. | 122 | | `-sy` | `--speedy` | float | 0.4 |Rotational speed around the y axis (-1 to 1). If set, disables random rotations. | 123 | | `-sz` | `--speedz` | float | 0.6 |Rotational speed around the z axis (-1 to 1). If set, disables random rotations. | 124 | | `-f` | `--fps` | int | 40 |Throttle the fps at which the graphics can be rendered (lower it if high CPU usage or if flicker) | 125 | | `-r` | `--random` | no argument | On |Rotate the shape randomly and sinusoidally. | 126 | | `-cx` | `--cx` | int | 0 |x-coordinate of the shapes's center in pixels | 127 | | `-cy` | `--cy` | int | 0 |y-coordinate of the shapes's center in pixels | 128 | | `-cz` | `--cz` | int | 0 |z-coordinate of the shapes's center in pixels | 129 | | `-wi` | `--width` | int | 60 |Width of shape in pixels | 130 | | `-he` | `--height` | int | 60 |Height of shape in pixels | 131 | | `-de` | `--depth` | int | 60 |Depth of shape in pixels | 132 | | `-ff` | `--from-file` | string | `./mesh_files/cube.scl` |The filepath to the mesh file to render. See `mesh_files` directory. | 133 | | `-mi` | `--maximum-iterations` | int | Inf/ty |How many frames to run the program for | 134 | | `-up` | `--use-perspective` | no argument | Off |Whether or not to use pinhole camera's perspective transform on rendered pixels | 135 | | `-be` | `--bounce-every` | int | 0 |If non-zero (`-be N` or `--bounce-every N`), changes moving direction every N frames | 136 | | `-mx` | `--movex` | int | 2 |Move the object by this many pixels along x axis per frame if bounce (`-b`/`--bounce`) is enabled. | 137 | | `-my` | `--movey` | int | 1 |Move the object by this many pixels along y axis per frame if bounce (`-b`/`--bounce`) is enabled. | 138 | | `-mz` | `--movez` | int | 1 |Move the object by this many pixels along z axis per frame if bounce (`-b`/`--bounce`) is enabled. | 139 | 140 | Below are two examples of running the demo binary `./cube`: 141 | 142 | `./cube` | `./cube -b 100 -cz 300 -my -1 -mz 1 -up` 143 | :-------------------------:|:-------------------------: 144 | ![](https://github.com/leonmavr/retrocube/blob/master/assets/demo_still.gif?raw=true) | ![](https://raw.githubusercontent.com/leonmavr/retrocube/master/assets/demo_moving.gif) 145 | 146 | 147 | #### 4.2 Tips 148 | 149 | 1. If the CPU usage is too high (it was low on my ancient laptop), you can reduce the fps e.g. to 15 by: `./cube -f 15` or `./cube --fps 15`. 150 | 151 | 152 | ### 5. Contributing 153 | 154 | If you'd like to contribute, please follow the codiing guidelines (section 3.1) and make sure that it builds and runs. 155 | I'll be happy to merge new changes. 156 | 157 | Kudos to: 158 | * [@pmarreck](https://github.com/pmarreck) - Nix packaging 159 | * [@Quantenzitrone](https://github.com/Quantenzitrone) - Fixing and competing nix packaging, including the mesh text files properly, tidying up the make build 160 | * [@IchikaZou](https://github.com/IchikaZou) - porting to Gentoo 161 | * Anyone else who opened an issue for helping me make this project more robust. 162 | -------------------------------------------------------------------------------- /assets/demo_moving.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leonmavr/retrocube/d9cfe32d4c7bd62dcc2df8fefa629b867d8ba66a/assets/demo_moving.gif -------------------------------------------------------------------------------- /assets/demo_still.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leonmavr/retrocube/d9cfe32d4c7bd62dcc2df8fefa629b867d8ba66a/assets/demo_still.gif -------------------------------------------------------------------------------- /demos/01_spinning_cube.c: -------------------------------------------------------------------------------- 1 | #include "xtrig.h" 2 | #include "objects.h" 3 | #include "renderer.h" 4 | #include "arg_parser.h" 5 | #include "utils.h" // UT_MAX 6 | #include // sin, cos 7 | #include // for usleep 8 | #include // bool 9 | #include // exit 10 | #include // time 11 | #include // signal 12 | 13 | /* Callback that clears the screen and makes the cursor visible when the user hits Ctr+C */ 14 | static void interrupt_handler(int int_num) { 15 | if (int_num == SIGINT) { 16 | render_end(); 17 | exit(SIGINT); 18 | } 19 | } 20 | 21 | int main(int argc, char** argv) { 22 | arg_parse(argc, argv); 23 | // make sure we end gracefully if the user hits Ctr+C 24 | signal(SIGINT, interrupt_handler); 25 | // initialise objects to render 26 | mesh_t* shape = obj_mesh_from_file(g_mesh_file, g_cx, g_cy, g_cz, g_width, g_height, g_depth); 27 | ftrig_init_lut(); 28 | // do the actual rendering 29 | render_init(); 30 | for (size_t t = 0; t < g_max_iterations; ++t) { 31 | obj_mesh_rotate_to(shape, 0.05*t, 0.01*t, 0.025*t); 32 | render_write_shape(shape); 33 | render_flush(); 34 | #ifndef _WIN32 35 | // nanosleep does not work on Windows 36 | nanosleep((const struct timespec[]) {{0, (int)(1.0 / g_fps * 1e9)}}, NULL); 37 | #endif 38 | } 39 | obj_mesh_free(shape); 40 | 41 | render_end(); 42 | } 43 | -------------------------------------------------------------------------------- /demos/02_3_diamonds.c: -------------------------------------------------------------------------------- 1 | #include "objects.h" 2 | #include "renderer.h" 3 | #include "arg_parser.h" 4 | #include "xtrig.h" 5 | #include "utils.h" // UT_MAX 6 | #include // sin, cos 7 | #include // for usleep 8 | #include // bool 9 | #include // exit 10 | #include // time 11 | #include // signal 12 | #include // assert 13 | #include // sprintf 14 | 15 | // absolute path to mesh file 16 | char mesh_filepath[256]; 17 | 18 | /* Callback that clears the screen and makes the cursor visible when the user hits Ctr+C */ 19 | static void interrupt_handler(int int_num) { 20 | if (int_num == SIGINT) { 21 | render_end(); 22 | exit(SIGINT); 23 | } 24 | } 25 | 26 | int main(int argc, char** argv) { 27 | arg_parse(argc, argv); 28 | // make sure we end gracefully if the user hits Ctr+C 29 | signal(SIGINT, interrupt_handler); 30 | 31 | // path to directory where meshes are stored - stored in CFG_DIR prep. constant 32 | const char* mesh_dir = STRINGIFY(CFG_DIR); 33 | const char* mesh_filename = "rhombus.scl"; 34 | sprintf(mesh_filepath, "%s/%s", mesh_dir, mesh_filename); 35 | assert(access(mesh_filepath, F_OK) == 0); 36 | 37 | ftrig_init_lut(); 38 | mesh_t* obj1 = obj_mesh_from_file(mesh_filepath, -40, 0, 150, 60, 80, 60); 39 | mesh_t* obj2 = obj_mesh_from_file(mesh_filepath, 0, 0, 250, 60, 80, 60); 40 | mesh_t* obj3 = obj_mesh_from_file(mesh_filepath, 40, 0, 350, 60, 80, 60); 41 | render_use_perspective(0, 0, 75); 42 | // do the actual rendering 43 | render_init(); 44 | for (size_t t = 0; t < g_max_iterations; ++t) { 45 | obj_mesh_rotate_to(obj1, 1.0/60*t, 0*t, 1.0/100*t); 46 | obj_mesh_rotate_to(obj2, 1.0/60*t, 0*t, 1.0/100*t); 47 | obj_mesh_rotate_to(obj3, 1.0/60*t, 0*t, 1.0/100*t); 48 | render_write_shape(obj1); 49 | render_write_shape(obj2); 50 | render_write_shape(obj3); 51 | render_flush(); 52 | #ifndef _WIN32 53 | // nanosleep does not work on Windows 54 | nanosleep((const struct timespec[]) {{0, (int)(1.0 / g_fps * 1e9)}}, NULL); 55 | #endif 56 | } 57 | obj_mesh_free(obj1); 58 | obj_mesh_free(obj2); 59 | obj_mesh_free(obj3); 60 | 61 | render_end(); 62 | } 63 | -------------------------------------------------------------------------------- /demos/02_spinning_coffin.c: -------------------------------------------------------------------------------- 1 | #include "arg_parser.h" // CFG_DIR 2 | #include "objects.h" 3 | #include "renderer.h" 4 | #include "utils.h" // UT_MAX 5 | #include // sin, cos 6 | #include // for usleep 7 | #include // bool 8 | #include // exit 9 | #include // time 10 | #include // signal 11 | #include // assert 12 | #include // sprintf 13 | 14 | // absolute path to mesh file 15 | char mesh_filepath[256]; 16 | 17 | /* Callback that clears the screen and makes the cursor visible when the user hits Ctr+C */ 18 | static void interrupt_handler(int int_num) { 19 | if (int_num == SIGINT) { 20 | render_end(); 21 | exit(SIGINT); 22 | } 23 | } 24 | 25 | int main(int argc, char** argv) { 26 | // make sure we end gracefully if the user hits Ctr+C 27 | signal(SIGINT, interrupt_handler); 28 | 29 | // path to directory where meshes are stored - dir stored in CFG_DIR prep. constant 30 | const char* mesh_dir = STRINGIFY(CFG_DIR); 31 | // select file in CFG_DIR 32 | const char* mesh_filename = "coffin.scl"; 33 | sprintf(mesh_filepath, "%s/%s", mesh_dir, mesh_filename); 34 | assert(access(mesh_filepath, F_OK) == 0); 35 | 36 | mesh_t* obj = obj_mesh_from_file(mesh_filepath, 0, 0, 250, 60, 80, 60); 37 | // do the actual rendering 38 | render_init(); 39 | for (size_t t = 0; t < UINT_MAX; ++t) { 40 | obj_mesh_rotate_to(obj, 1.0/60*t, 1.0/100*t, 1.0/100*t); 41 | render_write_shape(obj); 42 | render_flush(); 43 | #ifndef _WIN32 44 | // nanosleep does not work on Windows 45 | nanosleep((const struct timespec[]) {{0, (int)(1.0 / g_fps * 1e9)}}, NULL); 46 | #endif 47 | } 48 | obj_mesh_free(obj); 49 | 50 | render_end(); 51 | } 52 | -------------------------------------------------------------------------------- /demos/03_3_diamonds.c: -------------------------------------------------------------------------------- 1 | #include "arg_parser.h" 2 | #include "objects.h" 3 | #include "renderer.h" 4 | #include "arg_parser.h" 5 | #include "xtrig.h" 6 | #include "utils.h" // UT_MAX 7 | #include // sin, cos 8 | #include // for usleep 9 | #include // bool 10 | #include // exit 11 | #include // time 12 | #include // signal 13 | #include // assert 14 | #include // sprintf 15 | 16 | // absolute path to mesh file 17 | char mesh_filepath[256]; 18 | 19 | /* Callback that clears the screen and makes the cursor visible when the user hits Ctr+C */ 20 | static void interrupt_handler(int int_num) { 21 | if (int_num == SIGINT) { 22 | render_end(); 23 | exit(SIGINT); 24 | } 25 | } 26 | 27 | int main(int argc, char** argv) { 28 | // change the variables below to configure the demo 29 | // width, height, depth for all three 30 | const unsigned w = 120, h = 150, d = 120; 31 | // focal length - the higher, the bigger the objects 32 | const unsigned focal_length = 300; 33 | 34 | // make sure we end gracefully if the user hits Ctr+C 35 | signal(SIGINT, interrupt_handler); 36 | 37 | // path to directory where meshes are stored - stored in CFG_DIR prep. constant 38 | const char* mesh_dir = STRINGIFY(CFG_DIR); 39 | // select file from mesh directory 40 | const char* mesh_filename = "rhombus.scl"; 41 | sprintf(mesh_filepath, "%s/%s", mesh_dir, mesh_filename); 42 | assert(access(mesh_filepath, F_OK) == 0); 43 | ftrig_init_lut(); 44 | 45 | // draw the same object in 3 different depths to showcase perspective 46 | mesh_t* obj1 = obj_mesh_from_file(mesh_filepath, -80, 0, 600, w, h, d); 47 | mesh_t* obj2 = obj_mesh_from_file(mesh_filepath, 0, 0, 500, w, h, d); 48 | mesh_t* obj3 = obj_mesh_from_file(mesh_filepath, 80, 0, 400, w, h, d); 49 | render_use_perspective(0, 0, focal_length); 50 | // do the actual rendering 51 | render_init(); 52 | for (size_t t = 0; t < UINT_MAX; ++t) { 53 | obj_mesh_rotate_to(obj1, 1.0/10*t, 0*t, 1.0/15*t); 54 | obj_mesh_rotate_to(obj2, 1.0/10*t, 0*t, 1.0/15*t); 55 | obj_mesh_rotate_to(obj3, 1.0/10*t, 0*t, 1.0/15*t); 56 | render_write_shape(obj1); 57 | render_write_shape(obj2); 58 | render_write_shape(obj3); 59 | render_flush(); 60 | #ifndef _WIN32 61 | // nanosleep does not work on Windows 62 | nanosleep((const struct timespec[]) {{0, (int)(1.0 / g_fps * 1e9)}}, NULL); 63 | #endif 64 | } 65 | obj_mesh_free(obj1); 66 | obj_mesh_free(obj2); 67 | obj_mesh_free(obj3); 68 | 69 | render_end(); 70 | } 71 | -------------------------------------------------------------------------------- /demos/04_overlapping_objects.c: -------------------------------------------------------------------------------- 1 | #include "objects.h" 2 | #include "renderer.h" 3 | #include "arg_parser.h" // args_parse, CFG_DIR 4 | #include "xtrig.h" 5 | #include // sin, cos 6 | #include // for usleep 7 | #include // bool 8 | #include // exit 9 | #include // time 10 | #include // signal 11 | #include // assert 12 | #include // sprintf 13 | 14 | // absolute path to mesh file 15 | static char cube_filepath[256]; 16 | static char rhombus_filepath[256]; 17 | 18 | /* Callback that clears the screen and makes the cursor visible when the user hits Ctr+C */ 19 | static void interrupt_handler(int int_num) { 20 | if (int_num == SIGINT) { 21 | render_end(); 22 | exit(SIGINT); 23 | } 24 | } 25 | 26 | int main(int argc, char** argv) { 27 | arg_parse(argc, argv); 28 | // make sure we end gracefully if the user hits Ctr+C 29 | signal(SIGINT, interrupt_handler); 30 | 31 | ftrig_init_lut(); 32 | render_init(); 33 | 34 | // path to directory where meshes are stored - dir stored in CFG_DIR prep. constant 35 | const char* mesh_dir = STRINGIFY(CFG_DIR); 36 | // select files in CFG_DIR 37 | const char* cube_fname = "cube.scl"; 38 | const char* rhombus_fname = "rhombus.scl"; 39 | // cube's dimensions 40 | const unsigned w = 50, h = 50, d = 50; 41 | sprintf(cube_filepath, "%s/%s", mesh_dir, cube_fname); 42 | sprintf(rhombus_filepath, "%s/%s", mesh_dir, rhombus_fname); 43 | mesh_t* obj1 = obj_mesh_from_file(cube_filepath, 0, 0, 0, w, h, d); 44 | mesh_t* obj2 = obj_mesh_from_file(rhombus_filepath, 0, 0, 0, 1.2*w, 1.35*h, 1.2*d); 45 | assert(access(cube_filepath, F_OK) == 0); 46 | assert(access(rhombus_filepath, F_OK) == 0); 47 | // spinning parameters in case random rotation was selected 48 | #ifndef _WIN32 49 | const float random_rot_speed_x = 0.002, random_rot_speed_y = 0.002, random_rot_speed_z = 0.002; 50 | const float amplitude_x = 4.25, amplitude_y = 4.25, amplitude_z = 4.25; 51 | #else 52 | // make it spin faster on Windows because terminal refresh functions are sluggish there 53 | const float random_rot_speed_x = 0.01, random_rot_speed_y = 0.01, random_rot_speed_z = 0.01; 54 | const float amplitude_x = 6.0, amplitude_y = 6.0, amplitude_z = 6.0; 55 | #endif 56 | for (size_t t = 0; t < UINT_MAX; ++t) { 57 | obj_mesh_rotate_to(obj1, 1.0/80*t, 1.0/40*t, 1.0/60*t); 58 | obj_mesh_rotate_to(obj2, amplitude_x*fsin(random_rot_speed_x*fsin(random_rot_speed_x*t) + 2*random_bias_x), 59 | amplitude_y*fsin(random_rot_speed_y*random_bias_y*t + 2*random_bias_y), 60 | amplitude_z*fsin(random_rot_speed_z*random_bias_z*t + 2*random_bias_z)); 61 | render_write_shape(obj1); 62 | render_write_shape(obj2); 63 | render_flush(); 64 | #ifndef _WIN32 65 | // nanosleep does not work on Windows 66 | nanosleep((const struct timespec[]) {{0, (int)(1.0 / 40 * 1e9)}}, NULL); 67 | #endif 68 | } 69 | obj_mesh_free(obj1); 70 | obj_mesh_free(obj2); 71 | render_end(); 72 | } 73 | -------------------------------------------------------------------------------- /demos/05_moving_object.c: -------------------------------------------------------------------------------- 1 | #include "objects.h" 2 | #include "renderer.h" 3 | #include "arg_parser.h" // args_parse, CFG_DIR 4 | #include "xtrig.h" 5 | #include // sin, cos 6 | #include // for usleep 7 | #include // bool 8 | #include // exit 9 | #include // time 10 | #include // signal 11 | #include // assert 12 | #include // sprintf 13 | 14 | /* Callback that clears the screen and makes the cursor visible when the user hits Ctr+C */ 15 | static void interrupt_handler(int int_num) { 16 | if (int_num == SIGINT) { 17 | render_end(); 18 | exit(SIGINT); 19 | } 20 | } 21 | 22 | int main(int argc, char** argv) { 23 | arg_parse(argc, argv); 24 | 25 | // make sure we end gracefully if the user hits Ctr+C 26 | signal(SIGINT, interrupt_handler); 27 | 28 | ftrig_init_lut(); 29 | render_init(); 30 | 31 | mesh_t* shape = obj_mesh_from_file(g_mesh_file, g_cx, g_cy, g_cz, g_width, g_height, g_depth); 32 | // spinning parameters in case random rotation was selected 33 | #ifndef _WIN32 34 | const float random_rot_speed_x = 0.002, random_rot_speed_y = 0.002, random_rot_speed_z = 0.002; 35 | const float amplitude_x = 4.25, amplitude_y = 4.25, amplitude_z = 4.25; 36 | #else 37 | // make it spin faster on Windows because terminal refresh functions are sluggish there 38 | const float random_rot_speed_x = 0.01, random_rot_speed_y = 0.01, random_rot_speed_z = 0.01; 39 | const float amplitude_x = 6.0, amplitude_y = 6.0, amplitude_z = 6.0; 40 | #endif 41 | for (size_t t = 0; t < g_max_iterations; ++t) { 42 | if (g_use_random_rotation) 43 | obj_mesh_rotate_to(shape, amplitude_x*fsin(random_rot_speed_x*fsin(random_rot_speed_x*t) + 2*random_bias_x), 44 | amplitude_y*fsin(random_rot_speed_y*random_bias_y*t + 2*random_bias_y), 45 | amplitude_z*fsin(random_rot_speed_z*random_bias_z*t + 2*random_bias_z)); 46 | else 47 | obj_mesh_rotate_to(shape, g_rot_speed_x/20*t, g_rot_speed_y/20*t, g_rot_speed_z/20*t); 48 | if ((t % 100) >= 50) 49 | obj_mesh_translate_by(shape, 1, 1, 1); 50 | else 51 | obj_mesh_translate_by(shape, -1, -1, -1); 52 | render_write_shape(shape); 53 | render_flush(); 54 | #ifndef _WIN32 55 | // nanosleep does not work on Windows 56 | nanosleep((const struct timespec[]) {{0, (int)(1.0 / g_fps * 1e9)}}, NULL); 57 | #endif 58 | } 59 | obj_mesh_free(shape); 60 | render_end(); 61 | } 62 | -------------------------------------------------------------------------------- /demos/06_moving_in_world.c: -------------------------------------------------------------------------------- 1 | #include "objects.h" 2 | #include "renderer.h" 3 | #include "arg_parser.h" // CFG_DIR 4 | #include "utils.h" // CFG_DIR 5 | #include // sin, cos 6 | #include // for usleep 7 | #include // bool 8 | #include // exit 9 | #include // time 10 | #include // signal 11 | #include // assert 12 | #include // sprintf 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | // absolute path to cube file 20 | char cube_filepath[256]; 21 | // absolute path to coffin file 22 | char coffin_filepath[256]; 23 | 24 | struct termios old_terminal_settings; 25 | struct termios new_terminal_settings; 26 | 27 | mesh_t** obj; 28 | 29 | /* Callback that clears the screen and makes the cursor visible when the user hits Ctr+C */ 30 | static void interrupt_handler(int int_num) { 31 | // restore the terminal settings to their prvious state 32 | if (tcsetattr(0, TCSANOW, &old_terminal_settings) < 0) 33 | perror("tcsetattr ICANON"); 34 | for (int i = 0; i < 5; ++i) 35 | obj_mesh_free(obj[i]); 36 | if (int_num == SIGINT) { 37 | render_end(); 38 | exit(SIGINT); 39 | } 40 | } 41 | 42 | /* Credits to @supirman from https://ubuntuforums.org */ 43 | static int is_key_pressed(void) 44 | { 45 | struct timeval tv; 46 | fd_set fds; 47 | tv.tv_sec = 0; 48 | tv.tv_usec = 0; 49 | 50 | FD_ZERO(&fds); 51 | FD_SET(STDIN_FILENO, &fds); 52 | 53 | select(STDIN_FILENO+1, &fds, NULL, NULL, &tv); 54 | return FD_ISSET(STDIN_FILENO, &fds); 55 | } 56 | 57 | static void set_terminal() { 58 | // Get the current terminal settings 59 | if (tcgetattr(0, &old_terminal_settings) < 0) 60 | perror("tcgetattr()"); 61 | memcpy(&new_terminal_settings, &old_terminal_settings, sizeof(struct termios)); 62 | // disable canonical mode processing in the line discipline driver, 63 | // disable echoing chracters 64 | new_terminal_settings.c_lflag &= ~ICANON; 65 | new_terminal_settings.c_lflag &= ~ECHO; 66 | // apply our new settings 67 | if (tcsetattr(0, TCSANOW, &new_terminal_settings) < 0) 68 | perror("tcsetattr ICANON"); 69 | } 70 | 71 | static int sign(int x) { 72 | return (x >= 0) - (x < 0); 73 | } 74 | 75 | int main(int argc, char** argv) { 76 | // width, height, depth of the four cubes 77 | const unsigned w = 100, h = 100, d = 100; 78 | // focal length - the higher, the bigger the objects 79 | const unsigned focal_length = 300; 80 | 81 | // make sure we end gracefully if the user hits Ctr+C 82 | signal(SIGINT, interrupt_handler); 83 | 84 | // path to directory where meshes are stored - stored in CFG_DIR prep. constant 85 | const char* mesh_dir = STRINGIFY(CFG_DIR); 86 | // select file from mesh directory 87 | const char* cube_filename = "cube.scl"; 88 | const char* coffin_filename = "coffin.scl"; 89 | sprintf(cube_filepath, "%s/%s", mesh_dir, cube_filename); 90 | sprintf(coffin_filepath, "%s/%s", mesh_dir, coffin_filename); 91 | assert(access(cube_filepath, F_OK) == 0); 92 | assert(access(coffin_filepath, F_OK) == 0); 93 | 94 | set_terminal(); 95 | /** 96 | * * V: viewer 97 | * __ C: coffin 98 | * _/ \_ *: cube 99 | * _/ \ v,^: movement directions 100 | * _/ \_ 101 | * _^ v_ 102 | * _/ \_ y ^ 103 | * _/ \_ | 104 | * _/ \_ | 105 | * * __ C __ * o------>x 106 | * \_ _/ \ 107 | * \_ __/ \ 108 | * \__ v/ v 109 | * ^_ __/ z 110 | * \_ _/ 111 | * \_/ 112 | * * 113 | * 114 | * 115 | * V(0,0,0) 116 | */ 117 | // create 1 coffin and 4 cubes at 12, 3, 6 and 9 o' clock 118 | int coffinx = 0, coffiny = 0, coffinz = 900; 119 | unsigned dist = 120; 120 | obj = malloc(sizeof(mesh_t*) * 5); 121 | // coffin 122 | obj[0] = obj_mesh_from_file(coffin_filepath, coffinx, coffiny+50, coffinz, w, 1.3*h, 0.8*d); 123 | // cubes at 3, 6, 9, 12 o'clock cubes 124 | obj[1] = obj_mesh_from_file(cube_filepath, dist, 0, coffinz, w, h, d); 125 | obj[2] = obj_mesh_from_file(cube_filepath, 0, 0, coffinz+200, w, h, d); 126 | obj[3] = obj_mesh_from_file(cube_filepath, -dist, 0, coffinz, w, h, d); 127 | obj[4] = obj_mesh_from_file(cube_filepath, 0, 0, coffinz-200, w, h, d); 128 | 129 | // do the actual rendering 130 | render_use_perspective(0, 0, focal_length); 131 | render_init(); 132 | 133 | unsigned rad = 2*dist; 134 | int sign_x = 1, sign_y = 1; 135 | for (size_t t = 0; t < UINT_MAX; ++t) { 136 | int dx[4] = {0}, dy[4] = {0}, dz[4] = {0}; 137 | // Check if a key is pressed. If it is, call getchar to fetch it. 138 | if (is_key_pressed()) { 139 | char ch = getchar(); 140 | if (ch == 'a') 141 | dx[0] += 10; 142 | else if (ch == 's') 143 | dz[0] += 10; 144 | else if (ch == 'd') 145 | dx[0] -= 10; 146 | else if (ch == 'w') 147 | dz[0] -= 10; 148 | } 149 | 150 | sign_x = (t % rad == 0) ? -sign_x: sign_x; 151 | sign_y = ((t + rad/2) % rad == 0) ? -sign_y: sign_y; 152 | // . 153 | // ^> v> 154 | // ^< v< 155 | // . 156 | dx[0] += sign_x; 157 | dz[0] += sign_y; 158 | // 1st cube at 1st quarter 159 | printf("%d, %d\n", sign_x, sign_y); 160 | if ((sign_x <= 0) && (sign_y >= 0)) { 161 | dx[1] = dx[0]; 162 | dz[1] = -dz[0]; 163 | dx[2] = -dx[0]; 164 | dz[2] = -dz[0]; 165 | dx[3] = -dx[0]; 166 | dz[3] = dz[0]; 167 | // 1st cube at 2nd quarter 168 | } else if ((sign_x <= 0) && (sign_y < 0)) { 169 | dx[1] = -dx[0]; 170 | dz[1] = dz[0]; 171 | dx[2] = -dx[0]; 172 | dz[2] = -dz[0]; 173 | dx[3] = dx[0]; 174 | dz[3] = -dz[0]; 175 | // 1st cube at 3rd quarter 176 | } else if ((sign_x <= 0) && (sign_y >= 0)) { 177 | dx[1] = dx[0]; 178 | dz[1] = -dz[0]; 179 | dx[2] = -dx[0]; 180 | dz[2] = -dz[0]; 181 | dx[3] = -dx[0]; 182 | dz[3] = dz[0]; 183 | } else { 184 | // . 185 | // ^> v> 186 | // ^< v< 187 | // . 188 | dx[1] = -dx[0]; 189 | dz[1] = dz[0]; 190 | dx[2] = -dx[0]; 191 | dz[2] = -dz[0]; 192 | dx[3] = dx[0]; 193 | dz[3] = -dz[0]; 194 | } 195 | for (int i = 0; i < 4; i++) 196 | obj_mesh_translate_by(obj[i+1], dx[i], dy[i], dz[i]); 197 | if ((obj[1]->center->x <= coffinx) && (obj[1]->center->z <= coffinz + rad)) 198 | printf("dz = %d, z = %d/%d\n", dz[0], obj[1]->center->z, coffinz); 199 | for (int i = 0; i < 5; ++i) { 200 | if (obj[i]->center->x != 0) 201 | obj_mesh_rotate_to(obj[i], atan(obj[i]->center->y/obj[i]->center->x), 0, 0); 202 | } 203 | 204 | obj_mesh_rotate_to(obj[1], 1.0/10*t, 0*t, 1.0/15*t); 205 | for (int i = 0; i < 5; ++i) 206 | render_write_shape(obj[i]); 207 | render_flush(); 208 | #ifndef _WIN32 209 | // nanosleep does not work on Windows 210 | nanosleep((const struct timespec[]) {{0, (int)(1.0 / 60 * 1e9)}}, NULL); 211 | #endif 212 | } 213 | for (int i = 0; i < 5; ++i) 214 | obj_mesh_free(obj[i]); 215 | 216 | render_end(); 217 | } 218 | -------------------------------------------------------------------------------- /demos/Makefile: -------------------------------------------------------------------------------- 1 | ############################################### 2 | # Definitions 3 | ############################################### 4 | CC = gcc 5 | SRC_DIR = ../src 6 | INC_DIR = ../include 7 | DEMO_DIR = . 8 | # where to store the mesh (text) files - 9 | # set it from the command line if you want another location 10 | PREFIX = /usr 11 | CFG_DIR = $(PREFIX)/share/retrocube 12 | CFLAGS = -Wall -Wno-stringop-truncation -Wno-maybe-uninitialized -I$(INC_DIR)\ 13 | -std=gnu99 -O3 -DCFG_DIR=$(CFG_DIR) 14 | LDFLAGS = -lm 15 | SOURCES = $(wildcard $(SRC_DIR)/*.c) 16 | OBJECTS = $(SOURCES:%.c=%.o) 17 | DEMOS = $(wildcard $(DEMO_DIR)/*.c) 18 | DEMO_OBJECTS = $(DEMOS:%.c=%.o) 19 | DEMO_EXECS = $(DEMOS:%.c=%) 20 | MKDIR = mkdir -p 21 | CP = cp -r 22 | RM = rm -rf 23 | # Usage: make print- 24 | print-% : ; @echo $* = $($*) 25 | 26 | ############################################### 27 | # Compilation 28 | ############################################### 29 | all: $(DEMO_EXECS) cfg 30 | 31 | 32 | $(OBJECTS): $(SOURCES) 33 | $(foreach file, $(wildcard $(SRC_DIR)/*.c), $(CC) $(CFLAGS) -c $(file) -o $(basename $(file)).o;) 34 | 35 | $(DEMO_OBJECTS): $(DEMOS) $(SOURCES) 36 | $(foreach file, $(wildcard $(DEMO_DIR)/*.c), $(CC) $(CFLAGS) -c $(file) -o $(basename $(file)).o;) 37 | 38 | $(DEMO_EXECS): $(OBJECTS) $(DEMO_OBJECTS) 39 | $(foreach demo_obj, $(DEMO_OBJECTS), $(CC) $(OBJECTS) $(demo_obj) -o $(basename $(demo_obj)) $(LDFLAGS);) 40 | 41 | 42 | ############################################### 43 | # Commands (phony targets) 44 | ############################################### 45 | .PHONY: cfg 46 | cfg: 47 | # copy config files into CFG_DIR 48 | $(MKDIR) $(CFG_DIR) 49 | $(CP) ../mesh_files/*.scl $(CFG_DIR) 50 | 51 | .PHONY: clean 52 | clean: 53 | $(RM) $(DEMO_OBJECTS) 54 | $(RM) $(DEMO_EXECS) 55 | $(RM) $(OBJECTS) 56 | -------------------------------------------------------------------------------- /demos/README.md: -------------------------------------------------------------------------------- 1 | # About 2 | This directory defines different setups to be tested 3 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1681202837, 9 | "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "cfacdce06f30d2b68473a46042957675eebb3401", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1685418690, 24 | "narHash": "sha256-DueCSjMXMQHzvYJ+3z1ycSvjcK5hcZYm8a02BnnTYj0=", 25 | "owner": "nixos", 26 | "repo": "nixpkgs", 27 | "rev": "8d245c250a50f3c8d052926211161d4bb5766922", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "nixos", 32 | "ref": "release-23.05", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = github:nixos/nixpkgs/release-23.05; 4 | flake-utils.url = github:numtide/flake-utils; 5 | }; 6 | outputs = { 7 | self, 8 | nixpkgs, 9 | flake-utils, 10 | }: 11 | flake-utils.lib.eachDefaultSystem (system: let 12 | pkgs = nixpkgs.legacyPackages.${system}; 13 | in { 14 | # usable with nix when flake are enabled with 15 | # `nix build` # to build the default package 16 | # `nix run` # to run the default executable (cube) in the default package 17 | packages = rec { 18 | retrocube = pkgs.callPackage ./package.nix {}; 19 | default = retrocube; 20 | }; 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /include/arg_parser.h: -------------------------------------------------------------------------------- 1 | #include // bool 2 | #include // UINT_MAX 3 | 4 | #ifndef CFG_DIR 5 | #define CFG_DIR "/usr/share/retrocube" 6 | #endif 7 | 8 | #define STRINGIFY(x) STRINGIFY2(x) 9 | #define STRINGIFY2(x) #x 10 | 11 | //// default command line arguments 12 | // rotation speed around x, y, z axes (-1 to 1) 13 | extern float g_rot_speed_x; 14 | extern float g_rot_speed_y; 15 | extern float g_rot_speed_z; 16 | // maximum fps at which to render the cube 17 | extern unsigned g_fps; 18 | extern bool g_use_random_rotation; 19 | // centre (x, y, z) of the cube 20 | extern int g_cx; 21 | extern int g_cy; 22 | extern int g_cz; 23 | // size of each dimension in "pixels" (rendered characters) 24 | extern unsigned g_width; 25 | extern unsigned g_height; 26 | extern unsigned g_depth; 27 | // how many frames to run the program for 28 | extern unsigned g_max_iterations; 29 | extern char g_mesh_file[256]; 30 | // defines the max and min values of random rotation bias 31 | extern float rand_min; 32 | // random rotation biases - the higher, the faster the rotation around x, y, 33 | extern float random_bias_x; 34 | extern float random_bias_y; 35 | extern float random_bias_z; 36 | extern bool render_from_file; 37 | // controls whether the cube bounces around the screen 38 | // 0 = keep the cube centered, N (!= 0) = change direction every N frames 39 | extern unsigned g_bounce_every; 40 | // how many pixels to move per frame along x, y, z axes 41 | extern int g_move_x; 42 | extern int g_move_y; 43 | extern int g_move_z; 44 | 45 | void arg_parse(int argc, char** argv); 46 | -------------------------------------------------------------------------------- /include/objects.h: -------------------------------------------------------------------------------- 1 | #ifndef OBJECTS_H 2 | #define OBJECTS_H 3 | 4 | #include "vector.h" 5 | #include // true/false 6 | #include // round 7 | #include // size_t 8 | 9 | enum connection_t { 10 | CONNECTION_RECT=0, 11 | CONNECTION_TRIANGLE, 12 | NUM_CONNECTIONS 13 | }; 14 | 15 | typedef char color_t; 16 | 17 | typedef struct mesh { 18 | vec3i_t** vertices; 19 | vec3i_t** vertices_backup; 20 | vec3i_t* center; 21 | // number of vertices 22 | size_t n_vertices; 23 | // number of surfaces 24 | size_t n_faces; 25 | struct bounding_box { 26 | // top left 27 | int x0, y0, z0; 28 | // bottop right 29 | int x1, y1, z1; 30 | unsigned width, height, depth; 31 | } bounding_box; 32 | /* 33 | * 2D array that defines the surfaces of the solid. 34 | * Its rows consist of the following data:a 35 | * -- 4 indexes 36 | * -- connection type (connection_t enum) 37 | * -- a character that indicates the color of the current surface 38 | * To define a rectangular surface, use: 39 | * {3, 4, 6, 7, CONNECTION_RECT, 'o'}, 40 | * For a triangular surface: 41 | * {3, 4, 6, -1, CONNECTION_TRIANGLE, 'o'}, 42 | * Last index in triangular surface is always ignored. The generated surface 43 | * will be spanned by vertices[3], [4], [6], [7] or [3], [4], [6] respectively 44 | * and painted with the 'o' character. 45 | */ 46 | int** connections; 47 | } mesh_t; 48 | 49 | typedef struct ray { 50 | // origin is the centre of perspective in pinhole camera model 51 | vec3i_t* orig; 52 | vec3i_t* end; 53 | } ray_t; 54 | 55 | // pinhole camera where objects are shot from 56 | typedef struct camera { 57 | // origin 58 | int x0; 59 | int y0; 60 | // focal length 61 | float focal_length; 62 | } camera_t; 63 | 64 | /* 65 | * plane in 3D assuming its equation is: 66 | * n_x*x + n_y*y + n_z*z + d = 0 (1) 67 | * or n.X + d = 0 (2) 68 | * , where n = (n_x, n_y, n_z) is the normal 69 | * , X = (x, y, y) a point on the plane 70 | * and d is the offset from the origin 71 | */ 72 | typedef struct plane { 73 | // d from eq. (2) 74 | int offset; 75 | // n from eq. (2) 76 | vec3i_t* normal; 77 | } plane_t; 78 | 79 | //------------------------------------------------------------------------------------------------------------- 80 | // Renderable objects 81 | //------------------------------------------------------------------------------------------------------------- 82 | /** 83 | * @brief Allocates and sets a 2D triangle 84 | * 85 | * @param p0 A triangle vertex 86 | * @param p1 A triangle vertex 87 | * @param p2 A triangle vertex 88 | * @param color Triangle's fill color 89 | * 90 | * @returns A pointer to the newly constructed mesh 91 | */ 92 | mesh_t* obj_triangle_new (vec3i_t* p0, vec3i_t* p1, vec3i_t* p2, color_t color); 93 | /** 94 | * @brief 95 | * 96 | * @param fpath File path to read vertex and connection info from 97 | * @param cx x-coordinate of the center of the mesh to be created 98 | * @param cy y-coordinate of the center of the mesh to be created 99 | * @param cz z-coordinate of the center of the mesh to be created 100 | * @param width Width of the mesh 101 | * @param height Height of the mesh 102 | * @param depth Depth of the mesh 103 | * 104 | * @returns A pointer to the mesh that has been constructed 105 | */ 106 | mesh_t* obj_mesh_from_file (const char* fpath, int cx, int cy, int cz, 107 | unsigned width, unsigned height, unsigned depth); 108 | void obj_mesh_rotate_to (mesh_t* mesh, float angle_x_rad, float angle_y_rad, float angle_z_rad); 109 | void obj_mesh_translate_by (mesh_t* mesh, float dx, float dy, float dz); 110 | void obj_mesh_free (mesh_t* mesh); 111 | 112 | //------------------------------------------------------------------------------------------------------------- 113 | // Ray 114 | //------------------------------------------------------------------------------------------------------------- 115 | // the pixel in the screen where the ray points to 116 | /** 117 | * @brief Allocates a ray structure containing an origin and end vector 118 | * 119 | * @return A pointer to the newly constructed ray 120 | */ 121 | ray_t* obj_ray_new (); 122 | /** 123 | * @brief Sets the destination (`end` member) of a ray 124 | * 125 | * @param[in/out] ray Pointer to the ray to modify 126 | * @param x0 x-coordinate of ray's origin 127 | * @param y0 y-coordinate of ray's origin 128 | * @param z0 z-coordinate of ray's origin 129 | * @param x1 x-coordinate of ray's destination 130 | * @param y1 y-coordinate of ray's destination 131 | * @param z1 z-coordinate of ray's destination 132 | */ 133 | void obj_ray_set (ray_t* ray, int x0, int y0, int z0, int x1, int y1, int z1); 134 | /** 135 | * @brief Sets the destination (`end` member) of a ray 136 | * 137 | * @param[in/out] ray Pointer to the ray to modify 138 | * @param x x-coordinate of ray's new destination 139 | * @param y y-coordinate of ray's new sestination 140 | * @param z z-coordinate of ray's new destination 141 | */ 142 | void obj_ray_send (ray_t* ray, int x, int y, int z); 143 | void obj_ray_free (ray_t* ray); 144 | 145 | //------------------------------------------------------------------------------------------------------------- 146 | // Camera 147 | //------------------------------------------------------------------------------------------------------------- 148 | camera_t* obj_camera_new (); 149 | void obj_camera_set (camera_t* camera, int cam_x0, int cam_y0, float focal_length); 150 | 151 | //------------------------------------------------------------------------------------------------------------- 152 | // Plane 153 | //------------------------------------------------------------------------------------------------------------- 154 | /** 155 | * @brief Allocates a plane and sets its normal vector and offset given three points 156 | * 157 | * @param p0 First point on the plane 158 | * @param p1 Second point on the plane 159 | * @param p2 Third point on the plane 160 | * 161 | * @return A pointer to the newly constructed plane 162 | */ 163 | plane_t* obj_plane_new (); 164 | /* recompute plane's normal and offset given 3 points */ 165 | void obj_plane_set (plane_t* plane, vec3i_t* p0, vec3i_t* p1, vec3i_t* p2); 166 | bool obj_is_point_in_triangle (vec3i_t* m, vec3i_t* a, vec3i_t* b, vec3i_t* c); 167 | bool obj_is_point_in_rect (vec3i_t* m, vec3i_t* a, vec3i_t* b, vec3i_t* c, vec3i_t* d); 168 | vec3i_t render__ray_plane_intersection (plane_t* plane, ray_t* ray); 169 | bool obj_ray_hits_rectangle (ray_t* ray, vec3i_t** points); 170 | bool obj_ray_hits_triangle (ray_t* ray, vec3i_t** points); 171 | void obj_plane_free (plane_t* plane); 172 | 173 | /* 174 | * Note for programmers: 175 | * 176 | * The table below uses a technique called X macro to expand its column. 177 | * The table itself is defined as a function is X. X does not need to be 178 | * defined yet, but it's defined later according to the expansion we want. 179 | * It maps the following information: 180 | * -> -> 181 | * 182 | * Connection types are defined as an enum in objects.h. 183 | * Intersection functions define whether the ray intersects a rectangle or 184 | * triangle. As a final note, all functions must take the same parameter 185 | * types since we later expand them as a function pointer table. 186 | */ 187 | #define CONN_TABLE \ 188 | X('R', CONNECTION_RECT, obj_ray_hits_rectangle) \ 189 | X('T', CONNECTION_TRIANGLE, obj_ray_hits_triangle) 190 | 191 | 192 | #endif /* OBJECTS_H */ 193 | -------------------------------------------------------------------------------- /include/renderer.h: -------------------------------------------------------------------------------- 1 | #ifndef RENDERER_H 2 | #define RENDERER_H 3 | #include "objects.h" 4 | #include "screen.h" 5 | #include 6 | 7 | extern int* g_z_buffer; 8 | // checks whether the ray hits each pixel 9 | extern plane_t* g_plane_test; 10 | // the 4 points that define the surface to render 11 | extern vec3i_t** g_surf_points; 12 | // checks whether the ray hits each pixel 13 | extern ray_t* g_ray_test; 14 | // camera where rays are shot from 15 | extern camera_t g_camera; 16 | extern color_t g_colors_refl[32]; 17 | extern bool g_use_perspective; 18 | extern bool g_use_reflectance; 19 | 20 | 21 | /** 22 | * @brief Use perspective transform (pinhole camera model) when rendering shapes. 23 | * After calling this function, call `render_init()` for the changes to 24 | * take place. 25 | * 26 | * @param center_x0 x-coordinate of the perspective center - aka the point 27 | * where rays are shot from 28 | * @param center_y0 y-coordinate of the perspective center - aka the point 29 | * where rays are shot from 30 | * @param focal_length Focal length of pinhole camera 31 | */ 32 | void render_use_perspective(int center_x0, int center_y0, float focal_length); 33 | 34 | /** 35 | * @brief Sets flag to change the surface color based on the hit angle 36 | */ 37 | void render_use_reflectance(); 38 | 39 | /** 40 | * @brief Initializes renderer by setting the point of persperctive and focal length 41 | * if projection is to be used 42 | */ 43 | void render_init(); 44 | 45 | /** 46 | * @brief Writes shape to screen buffer before it's rendered. 47 | * Once shapes have been written, they can be displayed with `screen_flush()` 48 | * (the latter is defined in screen.h) 49 | * 50 | * @param shape Pointer to the shape to write to the renderer. Note that it must be 51 | * initialised 52 | */ 53 | void render_write_shape(mesh_t* shape); 54 | 55 | /** 56 | * @brief Sets the depth (z) buffer to INT_MAX and flushes the screen, 57 | * drawing the pixels 58 | */ 59 | void render_flush(); 60 | 61 | /** 62 | * @brief Closes the renderer and deallocates its structures 63 | */ 64 | void render_end(); 65 | 66 | #endif /* RENDERER_H */ 67 | -------------------------------------------------------------------------------- /include/screen.h: -------------------------------------------------------------------------------- 1 | #ifndef DRAW_H 2 | #define DRAW_H 3 | 4 | #include "vector.h" 5 | #include "objects.h" 6 | #include // size_t 7 | 8 | extern int g_rows; 9 | extern int g_cols; 10 | // stores the pixels to be drawn on the screen 11 | extern color_t* g_screen_buffer; 12 | extern size_t g_buffer_size; 13 | 14 | /** 15 | * @brief Conver some pixel coordinates from (x, y) to an 1D index given 16 | * the rows and columns of the screen. This is done to index the 17 | * pixel and depth (z) buffers. 18 | * 19 | * @param x x-coordinate of pixel to index 20 | * @param y y-coordinate of pixel to index 21 | * 22 | * @retun the 1D buffer index corrsponding to coordinates (x,y) 23 | */ 24 | size_t screen_xy2ind(int x, int y); 25 | 26 | /** 27 | * @brief Initialises the screen buffer and prepares terminal for writing 28 | */ 29 | void screen_init(); 30 | /** 31 | * @brief Write pixel with coordinates (x, y) on the screen into the screen 32 | * buffer `g_screen_buffer`. Note that the origin (0, 0) is at the 33 | * center of the screen. 34 | * 35 | * @param x x-coordinate of pixel to write 36 | * @param y y-coordinate of pixel to write 37 | * @param c "color" of the pixel as an ASCII character 38 | */ 39 | void screen_write_pixel(int x, int y, color_t c); 40 | /** 41 | * @brief Draws whatever is stored in the screen buffer `g_screen_buffer` on 42 | * the screen. Then moves the cursor top left and empties the buffer. 43 | */ 44 | void screen_flush(); 45 | /** 46 | * @brief Clears the screen and restores the cursor. 47 | */ 48 | void screen_end(); 49 | 50 | 51 | #endif /* DRAW_H */ 52 | -------------------------------------------------------------------------------- /include/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef UTILS_H 2 | #define UTILS_H 3 | 4 | #include 5 | 6 | // TODO: 7 | // #define INLINE inline __attribute__((always_inline)) 8 | 9 | #define UT_SQRT_TWO 1.414213 10 | #define UT_HALF_SQRT_TWO 0.7071065 11 | // the golden ratio 12 | #define UT_PHI 1.6180 13 | 14 | // the min below is generic and avoids double evaluation by redefining `a`, `b` 15 | #define UT_MIN(a, b) ( \ 16 | { \ 17 | __typeof__ (a) _a = (a); \ 18 | __typeof__ (b) _b = (b); \ 19 | _a < _b ? _a : _b; \ 20 | } \ 21 | ) 22 | 23 | // the max below is generic and avoids double evaluation by redefining `a`, `b` 24 | #define UT_MAX(a, b) ( \ 25 | { \ 26 | __typeof__ (a) _a = (a); \ 27 | __typeof__ (b) _b = (b); \ 28 | _a > _b ? _a : _b; \ 29 | } \ 30 | ) 31 | 32 | #define UT_CLIP(val, min, max) ( \ 33 | { \ 34 | UT_MIN(UT_MAX(val, min), max); \ 35 | } \ 36 | ) 37 | 38 | #define UT_MATRIX_ROWS(mat_2D) (sizeof(mat_2D)/sizeof(mat_2D[0])) 39 | #define UT_MATRIX_COLS(mat_2D) (sizeof(mat_2D[0])/sizeof(mat_2D[0][0])) 40 | 41 | // float equality 42 | inline bool ut_float_equal(float a, float b) { 43 | return (-1e-4 < a - b) && (a - b < 1e-4); 44 | } 45 | 46 | 47 | /** 48 | * @brief Checks whether a null-terminated array of characters represents 49 | * a positive decimal number, e.g. 1.8999 or 1,002 50 | * 51 | * @param string A null-terminated array of chars 52 | * @return true if the given string is numerical 53 | */ 54 | bool ut_is_decimal(char* string); 55 | 56 | #endif /* UTILS_H */ 57 | -------------------------------------------------------------------------------- /include/vector.h: -------------------------------------------------------------------------------- 1 | #ifndef VECTOR_H 2 | #define VECTOR_H 3 | 4 | #include // bool 5 | 6 | typedef struct vec3i { 7 | int x, y, z; 8 | } vec3i_t; 9 | 10 | typedef struct vec3f { 11 | float x, y, z; 12 | } vec3f_t; 13 | 14 | // alias for floating vector type 15 | typedef vec3f_t vec3_t; 16 | 17 | // basic operations between floating vectors 18 | vec3_t* vec_vec3_new (); 19 | void vec_vec3_set (vec3_t* vec, float x, float y, float z); 20 | void vec_vec3_copy (vec3f_t* dest, vec3f_t* src); 21 | bool vec_vec3_are_equal (vec3_t* vec1, vec3_t* vec2); 22 | vec3_t vec_vec3_add (vec3_t* src1, vec3_t* src2); 23 | vec3_t vec_vec3_sub (vec3_t* src1, vec3_t* src2); 24 | vec3_t vec_vec3_mul_scalar (vec3_t* src, float scalar); 25 | float vec_vec3_dotprod (vec3_t* src1, vec3_t* src2); 26 | vec3_t vec_vec3_crossprod (vec3_t* src1, vec3_t* src2); 27 | /** 28 | * @brief Rotates a vector about a point 29 | * 30 | * @param[in/out] src Pointer to vector to rotate, we write to that 31 | * @param angle_x_rad Angle to rotate about x axis in radians 32 | * @param angle_y_rad Angle to rotate about y ayis in radians 33 | * @param angle_z_rad Angle to rotate about z azis in radians 34 | * @param x0 x-coordinate of point to rotate about 35 | * @param y0 y-coordinate of point to rotate about 36 | * @param z0 z-coordinate of point to rotate about 37 | */ 38 | void vec_vec3_rotate (vec3_t* src, float angle_x_rad, float angle_y_rad, float angle_z_rad, 39 | int x0, int y0, int z0); 40 | // basic operations between integral vectors 41 | vec3i_t* vec_vec3i_new (); 42 | void vec_vec3i_set (vec3i_t* vec, int x, int y, int z); 43 | void vec_vec3i_copy (vec3i_t* dest, vec3i_t* src); 44 | bool vec_vec3i_are_equal (vec3i_t* vec1, vec3i_t* vec2); 45 | vec3i_t vec_vec3i_add (vec3i_t* src1, vec3i_t* src2); 46 | /** 47 | * @brief Subtracts the second vector from the first 48 | * 49 | * @param src1 Pointer to subtractee vector 50 | * @param src2 Pointer to subtractor vector 51 | * 52 | * @return src1-src2 53 | */ 54 | vec3i_t vec_vec3i_sub (vec3i_t* src1, vec3i_t* src2); 55 | vec3i_t vec_vec3i_mul_scalar (vec3i_t* src, float scalar); 56 | int vec_vec3i_dotprod (vec3i_t* src1, vec3i_t* src2); 57 | vec3i_t vec_vec3i_crossprod (vec3i_t* src1, vec3i_t* src2); 58 | /** 59 | * @brief Rotates a vector about a point 60 | * 61 | * @param src[in/out] Pointer to vector to rotate, we write to that 62 | * @param angle_x_rad Angle to rotate about x axis in radians 63 | * @param angle_y_rad Angle to rotate about y ayis in radians 64 | * @param angle_z_rad Angle to rotate about z azis in radians 65 | * @param x0 x-coordinate of point to rotate about 66 | * @param y0 y-coordinate of point to rotate about 67 | * @param z0 z-coordinate of point to rotate about 68 | */ 69 | void vec_vec3i_rotate (vec3i_t* src, float angle_x_rad, float angle_y_rad, float angle_z_rad, 70 | int x0, int y0, int z0); 71 | 72 | #endif /* VECTOR_H */ 73 | -------------------------------------------------------------------------------- /include/xtrig.h: -------------------------------------------------------------------------------- 1 | #ifndef XTRIG_H 2 | #define XTRIG_H 3 | 4 | #include 5 | #include 6 | 7 | #define LUT_BIN_SIZE 0.00174533 // in radians 8 | #define HALF_PI (M_PI / 2.0) 9 | #define LUT_SIZE 901 // (int)(HALF_PI/ LUT_BIN_SIZE + 1) 10 | 11 | /** sine lookup table (LUT) with sampled values from 0 to pi/2 */ 12 | extern double sine_lut[LUT_SIZE]; 13 | 14 | /** Initialize lookup tables - must be called before fsin or fcos */ 15 | void ftrig_init_lut(); 16 | /** fast sine */ 17 | double fsin(double angle); 18 | /** fast cosine */ 19 | double fcos(double angle); 20 | 21 | #endif // XTRIG_H 22 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | #include "objects.h" 2 | #include "renderer.h" 3 | #include "arg_parser.h" 4 | #include "xtrig.h" 5 | #include "utils.h" // UT_MAX 6 | #include // sin, cos 7 | #include // for usleep 8 | #include // exit 9 | #include // time 10 | #include // signal 11 | 12 | /* Callback that clears the screen and makes the cursor visible when the user hits Ctr+C */ 13 | static void interrupt_handler(int int_num) { 14 | if (int_num == SIGINT) { 15 | render_end(); 16 | exit(SIGINT); 17 | } 18 | } 19 | 20 | int main(int argc, char** argv) { 21 | arg_parse(argc, argv); 22 | 23 | // make sure we end gracefully if the user hits Ctr+C 24 | signal(SIGINT, interrupt_handler); 25 | 26 | render_init(); 27 | ftrig_init_lut(); 28 | 29 | mesh_t* shape = obj_mesh_from_file(g_mesh_file, g_cx, g_cy, g_cz, g_width, g_height, g_depth); 30 | // spinning parameters in case random rotation was selected 31 | #ifndef _WIN32 32 | const float random_rot_speed_x = 0.002, random_rot_speed_y = 0.002, random_rot_speed_z = 0.002; 33 | const float amplitude_x = 4.25, amplitude_y = 4.25, amplitude_z = 4.25; 34 | #else 35 | // make it spin faster on Windows because terminal refresh functions are sluggish there 36 | const float random_rot_speed_x = 0.01, random_rot_speed_y = 0.01, random_rot_speed_z = 0.01; 37 | const float amplitude_x = 6.0, amplitude_y = 6.0, amplitude_z = 6.0; 38 | #endif 39 | for (size_t t = 0; t < g_max_iterations; ++t) { 40 | if (g_use_random_rotation) 41 | obj_mesh_rotate_to(shape, amplitude_x*fsin(random_rot_speed_x*fsin(random_rot_speed_x*t) + 2*random_bias_x), 42 | amplitude_y*fsin(random_rot_speed_y*random_bias_y*t + 2*random_bias_y), 43 | amplitude_z*fsin(random_rot_speed_z*random_bias_z*t + 2*random_bias_z)); 44 | else 45 | obj_mesh_rotate_to(shape, g_rot_speed_x/20*t, g_rot_speed_y/20*t, g_rot_speed_z/20*t); 46 | if (g_bounce_every != 0) { 47 | if ((t % (2*g_bounce_every)) >= g_bounce_every) 48 | obj_mesh_translate_by(shape, g_move_x, g_move_y, g_move_z); 49 | else 50 | obj_mesh_translate_by(shape, -g_move_x, -g_move_y, -g_move_z); 51 | } 52 | render_write_shape(shape); 53 | render_flush(); 54 | #ifndef _WIN32 55 | // nanosleep does not work on Windows 56 | nanosleep((const struct timespec[]) {{0, (int)(1.0 / g_fps * 1e9)}}, NULL); 57 | #endif 58 | } 59 | obj_mesh_free(shape); 60 | render_end(); 61 | 62 | return 0; 63 | } 64 | 65 | -------------------------------------------------------------------------------- /mesh_files/README.md: -------------------------------------------------------------------------------- 1 | # .scl file manual 2 | 3 | ### Introduction 4 | 5 | `.scl` is a file format I have developed for my project. It's inspired by Blender's `.obj` file format, however it only specifies vertices and connections. It's space-separated. 6 | 7 | ### Anatomy of an .scl file 8 | 9 | `.scl` files are parsed by the `obj_mesh_from_file` function declared in `objects.h`. The parser only takes into account two kinds of expressions in an `.scl` file: 10 | * v X X X 11 | * f Y Y Y Y T C 12 | where: 13 | * X is a float from -1.0 to 1.0 14 | * Y an integer 15 | * T a character - `R` or `T` 16 | * C a character 17 | 18 | `v` indicates that a vertex is to be defined. The next 3 numbers that follow (`X X X`) specify the location of the vertex. Each `X` can range from -1.0 to 1.0 and the first `X` specifies the location as a proportion of the width (-1.0 corresponds to -width/2, -0.25 -width/8, 0.5 to width/4, etc.). Likewise for the second and third `X`. 19 | 20 | When a vertex is defined, it's assigned a unique incremental index under the hood starting from zero. This is how it will be refererenced by the connections. 21 | 22 | `f` indicates that a connection is to be defined. In the end, it defines a surface. The first four integers (`Y`) reference the vertices is shall connect. For example, `0 2 4 1` connect the first, third, fifth and second vertices together. The next character indicates the connection type. Currecntly rectangular (`R`) and triangular (`T`) connections are supported. If `T` follows the vertices, only the first three are taken into account. In the previous example, `0 2 4 1 R` would define a rectangle with all four vertices and `0 2 4 1 T` would define a triangle with the `0, 2, 4`-th vertices. The number of vertex indexes must always be 4 no matter whether you want to draw a rectangle or triangle! The last entry can be any ASCII character. It specifies the filliing color of the surface to be rendered. 23 | Anything that doesn't start with `v` or `f` is considered a comment. Anything after `X X X` in `v`-prefixed lines is also a comment. Likewise for anything after `Y Y Y Y T C` in `f`-prefixed lines. 24 | -------------------------------------------------------------------------------- /mesh_files/coffin.scl: -------------------------------------------------------------------------------- 1 | 2,8 3,9 2 | X---X 15,23 16,24 3 | / \ X----X 4 | / /+-+ \ | | 5 | _/ | | \_ 14,22 | | 17,25 6 | / /+-+ +-+ \ X-----+ +-----X 7 | X | | X | | 8 | 1,7 \ +-+ +-+ / 4,10 X-----+ +-----X 9 | | / | | | 13,21 | | 18,26 10 | \ | | / | | 11 | | +-+ | | | 12 | \ / / | | 13 | | | | | 14 | \ / | | 15 | | | | | 16 | \ / X----X 17 | X---X 12,20 19,27 18 | 0,6 5,11 19 | 20 | ### Vertices 21 | # upper outline 22 | v -0.15 -0.5 0.3 # 0 23 | v -0.5 0.2 0.3 # 1 24 | v -0.15 0.5 0.3 # 2 25 | v 0.15 0.5 0.3 # 3 26 | v 0.5 0.2 0.3 # 4 27 | v 0.15 -0.5 0.3 # 5 28 | 29 | # lower outline 30 | v -0.15 -0.5 -0.3 # 6 31 | v -0.5 0.2 -0.3 # 7 32 | v -0.15 0.5 -0.3 # 8 33 | v 0.15 0.5 -0.3 # 9 34 | v 0.5 0.2 -0.3 # 10 35 | v 0.15 -0.5 -0.3 # 11 36 | 37 | # upper cross (z = 0.5) 38 | v -0.12 -0.4 0.5 # 12 39 | v -0.3 -0.1 0.5 # 13 40 | v -0.3 0.1 0.5 # 14 41 | v -0.12 0.3 0.5 # 15 42 | v 0.12 0.3 0.5 # 16 43 | v 0.3 0.1 0.5 # 17 44 | v 0.3 -0.1 0.5 # 18 45 | v 0.12 -0.4 0.5 # 19 46 | 47 | # lower cross (z = 0.3) 48 | v -0.12 -0.4 0.3 # 20 49 | v -0.3 -0.1 0.3 # 21 50 | v -0.3 0.1 0.3 # 22 51 | v -0.12 0.3 0.3 # 23 52 | v 0.12 0.3 0.3 # 24 53 | v 0.3 0.1 0.3 # 25 54 | v 0.3 -0.1 0.3 # 26 55 | v -0.12 -0.4 0.3 # 27 56 | 57 | 58 | ### Connections 59 | # upper outline 60 | f 0 1 2 0 T @ 61 | f 0 2 3 5 R @ 62 | f 3 5 4 0 T @ 63 | 64 | # sides 65 | f 0 1 7 6 R * 66 | f 1 2 8 7 R $ 67 | f 2 3 9 8 R % 68 | f 3 4 10 9 R ; 69 | f 4 5 11 10 R ^ 70 | f 0 5 11 6 R i 71 | 72 | # lower outline 73 | f 6 8 7 0 T # 74 | f 6 8 9 11 R # 75 | f 11 9 10 0 T # 76 | 77 | # cross 78 | f 12 15 16 19 R = 79 | f 13 14 17 18 R = 80 | f 12 20 23 15 R . 81 | f 15 23 24 16 R : 82 | f 16 24 27 19 R . 83 | f 12 19 27 20 R : 84 | f 13 18 26 21 R . 85 | f 13 14 22 21 R : 86 | f 14 17 25 22 R . 87 | f 17 18 26 25 R : 88 | -------------------------------------------------------------------------------- /mesh_files/cube.scl: -------------------------------------------------------------------------------- 1 | #-------------------------------------------------------------------------- 2 | # Cube 3 | #-------------------------------------------------------------------------- 4 | 5 | # p3 p2 6 | # +-------------------+ 7 | # | \ | \ 8 | # | \ | \ ^y 9 | # | \ p7 | \ | 10 | # | +-------------------+ p6 | 11 | # | | . | | 12 | # | |*(cx,xy,cz) | o-------> x 13 | # | | . | \ 14 | # | | . | \ 15 | # | | . | v z 16 | # p0 +---------|.........+ p1 | 17 | # \ | . | 18 | # \ | . | 19 | # \ | . | 20 | # \+-------------------+ 21 | # p4 p5 22 | # 23 | 24 | # Vertices 25 | v -0.3535 -0.3535 -0.3535 26 | v 0.3535 -0.3535 -0.3535 27 | v 0.3535 0.3535 -0.3535 28 | v -0.3535 0.3535 -0.3535 29 | v -0.3535 -0.3535 0.3535 30 | v 0.3535 -0.3535 0.3535 31 | v 0.3535 0.3535 0.3535 32 | v -0.3535 0.3535 0.3535 33 | 34 | # Surfaces 35 | f 0 1 2 3 R ~ 36 | f 0 4 7 3 R . 37 | f 4 5 6 7 R = 38 | f 5 1 2 6 R @ 39 | f 7 6 2 3 R ? 40 | f 0 4 5 1 R + 41 | -------------------------------------------------------------------------------- /mesh_files/rhombus.scl: -------------------------------------------------------------------------------- 1 | Rhombus 2 | ~~~~~~~ 3 | 4 | p5 center 5 | X X vertex 6 | / \ 7 | / \ y 8 | / \ ^ 9 | / \ | 10 | / p2 \ | 11 | / X \ | 12 | / .. .. \ o--------> x 13 | /.. .. \ \ 14 | p3 X. .X p1 \ 15 | \__ __/ \ 16 | \ \__ __/ / v 17 | \ X / z 18 | \ p0 / 19 | \ / 20 | \ / 21 | \ / 22 | \ / 23 | X 24 | p4 25 | 26 | Vertices: 27 | v 0 0 0.5 28 | v 0.5 0 0 29 | v 0 0 -0.5 30 | v -0.5 0 0 31 | v 0 -0.6180 0 32 | v 0 0.3819 0 33 | 34 | Connections: 35 | f 3, 4, 0, 0, T, ~ 36 | f 0, 4, 1, 0, T, . 37 | f 4, 2, 1, 0, T, = 38 | f 4, 2, 3, 0, T, @ 39 | f 3, 0, 5, 0, T, % 40 | f 0, 1, 5, 0, T, | 41 | f 1, 5, 2, 0, T, O 42 | f 3, 2, 5, 0, T, + 43 | -------------------------------------------------------------------------------- /package.nix: -------------------------------------------------------------------------------- 1 | # To build this with on nix, just run 2 | # nix build # with flakes 3 | # nix-build --expr "with import {}; callPackage ./package.nix {}" # without flakes 4 | { 5 | lib, 6 | # override this with fastStdenv for optimization/faster running times (8-12%) BUT... nondeterministic builds :( 7 | stdenv, 8 | ... 9 | }: 10 | stdenv.mkDerivation rec { 11 | pname = "retrocube"; 12 | version = "1.0"; 13 | # dont include nix and version control files in the source 14 | src = lib.cleanSourceWith { 15 | filter = name: _: let 16 | n = baseNameOf (toString name); 17 | in 18 | !(lib.hasSuffix ".nix" n) 19 | && !(lib.hasSuffix ".lock" n); 20 | src = lib.cleanSource ./.; 21 | }; 22 | # disable the buildPhase, because make will install the meshes while building, 23 | # which won't work in the buildPhase, so we will build in the installPhase 24 | dontBuild = true; 25 | # build and install retrocube 26 | installPhase = '' 27 | make PREFIX=$out 28 | mkdir $out/bin 29 | cp cube $out/bin 30 | ''; 31 | 32 | meta = with lib; { 33 | mainProgram = "cube"; 34 | license = licenses.mit; 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /src/.ccls: -------------------------------------------------------------------------------- 1 | gcc 2 | %c -std=gnu99 3 | -I../include 4 | -Wall 5 | -------------------------------------------------------------------------------- /src/arg_parser.c: -------------------------------------------------------------------------------- 1 | #include "arg_parser.h" 2 | #include "renderer.h" 3 | #include "utils.h" // UT_MAX 4 | #include // sin, cos 5 | #include // atof, atoi, random, exit 6 | #include // for usleep 7 | #include // assert 8 | #include // bool 9 | #include // strcmp 10 | #include // time 11 | #include // sprintf 12 | 13 | //// default command line arguments 14 | // rotation speed around x, y, z axes (-1 to 1) 15 | float g_rot_speed_x = 0.7; 16 | float g_rot_speed_y = 0.4; 17 | float g_rot_speed_z = 0.6; 18 | // maximum fps at which to render the cube 19 | unsigned g_fps = 40; 20 | bool g_use_random_rotation = true; 21 | // centre (x, y, z) of the cube 22 | int g_cx = 0; 23 | int g_cy = 0; 24 | int g_cz = 250; 25 | // size of each dimension in "pixels" (rendered characters) 26 | unsigned g_width = 60; 27 | unsigned g_height = 60; 28 | unsigned g_depth = 60; 29 | // how many frames to run the program for 30 | unsigned g_max_iterations = UINT_MAX; 31 | char g_mesh_file[256] = {'\0'}; 32 | // defines the max and min values of random rotation bias 33 | float rand_min = 0.75, rand_max = 2.25; 34 | // random rotation biases - the higher, the faster the rotation around x, y, 35 | float random_bias_x = 0; 36 | float random_bias_y = 0; 37 | float random_bias_z = 0; 38 | bool render_from_file = false; 39 | 40 | unsigned g_bounce_every = 0; 41 | // how many pixels to move per frame along x, y, z axes 42 | int g_move_x = 2; 43 | int g_move_y = 1; 44 | int g_move_z = 1; 45 | 46 | 47 | void arg_parse(int argc, char** argv) { 48 | // initialise pseudo randomness generator for random rotations 49 | srand(time(NULL)); 50 | rand_min = 0.75, rand_max = 2.25; 51 | random_bias_x = rand_min + (rand_max - rand_min)*rand()/(double)RAND_MAX; 52 | random_bias_y = rand_min + (rand_max - rand_min)*rand()/(double)RAND_MAX; 53 | random_bias_z = rand_min + (rand_max - rand_min)*rand()/(double)RAND_MAX; 54 | render_from_file = false; 55 | // parse command line arguments - if followed by an argument, e.g. -sx 0.9, increment `i` 56 | int i = 0; 57 | while (++i < argc) { 58 | if ((strcmp(argv[i], "--speedx") == 0) || (strcmp(argv[i], "-sx") == 0)) { 59 | g_rot_speed_x = atof(argv[++i]); 60 | g_use_random_rotation = false; 61 | } else if ((strcmp(argv[i], "--speedy") == 0) || (strcmp(argv[i], "-sy") == 0)) { 62 | g_rot_speed_y = atof(argv[++i]); 63 | g_use_random_rotation = false; 64 | } else if ((strcmp(argv[i], "--speedz") == 0) || (strcmp(argv[i], "-sz") == 0)) { 65 | g_rot_speed_z = atof(argv[++i]); 66 | g_use_random_rotation = false; 67 | } else if ((strcmp(argv[i], "--fps") == 0) || (strcmp(argv[i], "-f") == 0)) { 68 | g_fps = atoi(argv[++i]); 69 | } else if ((strcmp(argv[i], "--random") == 0) || (strcmp(argv[i], "-r") == 0)) { 70 | g_use_random_rotation = true; 71 | } else if ((strcmp(argv[i], "--cx") == 0) || (strcmp(argv[i], "-cx") == 0)) { 72 | g_cx = atoi(argv[++i]); 73 | } else if ((strcmp(argv[i], "--cy") == 0) || (strcmp(argv[i], "-cy") == 0)) { 74 | g_cy = atoi(argv[++i]); 75 | } else if ((strcmp(argv[i], "--cz") == 0) || (strcmp(argv[i], "-cz") == 0)) { 76 | g_cz = atoi(argv[++i]); 77 | } else if ((strcmp(argv[i], "--width") == 0) || (strcmp(argv[i], "-wi") == 0)) { 78 | g_width = atoi(argv[++i]); 79 | } else if ((strcmp(argv[i], "--height") == 0) || (strcmp(argv[i], "-he") == 0)) { 80 | g_height = atoi(argv[++i]); 81 | } else if ((strcmp(argv[i], "--depth") == 0) || (strcmp(argv[i], "-de") == 0)) { 82 | g_depth = atoi(argv[++i]); 83 | } else if ((strcmp(argv[i], "--max-iterations") == 0) || (strcmp(argv[i], "-mi") == 0)) { 84 | g_max_iterations = atoi(argv[++i]); 85 | } else if ((strcmp(argv[i], "--use-perspective") == 0) || (strcmp(argv[i], "-up") == 0)) { 86 | render_use_perspective(0, 0, -200); 87 | } else if ((strcmp(argv[i], "--use-reflection") == 0) || (strcmp(argv[i], "-ur") == 0)) { 88 | render_use_reflectance(); 89 | } else if ((strcmp(argv[i], "--from-file") == 0) || (strcmp(argv[i], "-ff") == 0)) { 90 | i++; 91 | strcpy(g_mesh_file, argv[i]); 92 | render_from_file = true; 93 | } else if ((strcmp(argv[i], "--bounce") == 0) || (strcmp(argv[i], "-b") == 0)) { 94 | g_bounce_every = atoi(argv[++i]); 95 | } else if ((strcmp(argv[i], "--movex") == 0) || (strcmp(argv[i], "-mx") == 0)) { 96 | g_move_x = atoi(argv[++i]); 97 | } else if ((strcmp(argv[i], "--movey") == 0) || (strcmp(argv[i], "-my") == 0)) { 98 | g_move_y = atoi(argv[++i]); 99 | } else if ((strcmp(argv[i], "--movez") == 0) || (strcmp(argv[i], "-mz") == 0)) { 100 | g_move_z = atoi(argv[++i]); 101 | } else { 102 | printf("Uknown option: %s\n", argv[i++]); 103 | } 104 | } 105 | assert(( -1.0001 < g_rot_speed_x) && (g_rot_speed_x < 1.0001) && 106 | (-1.0001 < g_rot_speed_y) && (g_rot_speed_y < 1.0001) && 107 | (-1.0001 < g_rot_speed_z) && (g_rot_speed_z < 1.0001)); 108 | // default file to render 109 | // define in preprocessor constant CFG_DIR 110 | if (!render_from_file) { 111 | const char* cfg_dir = STRINGIFY(CFG_DIR); 112 | const char* mesh_filename = "cube.scl"; 113 | // TODO: check boundaries 114 | sprintf(g_mesh_file, "%s/%s", cfg_dir, mesh_filename); 115 | } 116 | // we should have a valid filepath by now 117 | assert(access(g_mesh_file, F_OK) == 0); 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/objects.c: -------------------------------------------------------------------------------- 1 | #include "vector.h" 2 | #include "objects.h" 3 | #include "utils.h" 4 | #include "renderer.h" // g_plane_test 5 | #include // round, abs 6 | #include 7 | #include // bool 8 | #include // size_t 9 | #include // FILE, open, fclose, printf 10 | #include // isempty 11 | #include // strtok 12 | #include // assert 13 | 14 | 15 | // perpendicular 2D vector, i.e. rotated by 90 degrees ccw 16 | #define VEC_PERP(src) ( \ 17 | { \ 18 | __typeof__ (src) _ret; \ 19 | _ret.x = -src.y; \ 20 | _ret.y = src.x; \ 21 | _ret.z = 0; \ 22 | _ret; \ 23 | } \ 24 | ) 25 | 26 | #define VEC_PERP_DOT_PROD(a, b) a.x*b.y - a.y*b.x 27 | 28 | static char conn_letters[] = { 29 | #define X(a, b, c) a, 30 | CONN_TABLE 31 | #undef X 32 | }; 33 | 34 | static int conn_names[NUM_CONNECTIONS] = { 35 | #define X(a, b, c) b, 36 | CONN_TABLE 37 | #undef X 38 | }; 39 | 40 | //---------------------------------------------------------------------------------------------------------- 41 | // Static functions 42 | //---------------------------------------------------------------------------------------------------------- 43 | static inline bool obj__starts_with(const char* buffer, char first) { 44 | return buffer[0] == first; 45 | } 46 | 47 | static inline bool obj__line_is_comment(const char* buffer) { 48 | return buffer[0] == '#'; 49 | } 50 | 51 | bool obj__line_is_empty(const char *s) 52 | { 53 | while (*s) { 54 | if (!isspace(*s)) 55 | return false; 56 | s++; 57 | } 58 | return true; 59 | } 60 | 61 | static inline void obj__mesh_update_bbox(mesh_t* mesh) { 62 | const int w = mesh->bounding_box.width; 63 | const int h = mesh->bounding_box.height; 64 | const int d = mesh->bounding_box.depth; 65 | const int m = 2*sqrt(w*w + h*h + d*d); 66 | mesh->bounding_box.x0 = mesh->center->x - m/2; 67 | mesh->bounding_box.y0 = mesh->center->y - m/2; 68 | mesh->bounding_box.z0 = mesh->center->z - m/2; 69 | mesh->bounding_box.x1 = mesh->center->x + m/2; 70 | mesh->bounding_box.y1 = mesh->center->y + m/2; 71 | mesh->bounding_box.z1 = mesh->center->z + m/2; 72 | } 73 | //---------------------------------------------------------------------------------------------------------- 74 | // Renderable shapes 75 | //---------------------------------------------------------------------------------------------------------- 76 | mesh_t* obj_mesh_from_file(const char* fpath, int cx, int cy, int cz, unsigned width, unsigned height, unsigned depth) { 77 | FILE* file; 78 | file = fopen(fpath, "r"); 79 | if (file == NULL) { 80 | printf("Fatal error: Cannot open file %s\n. Exiting...", fpath); 81 | exit(1); 82 | } 83 | char buffer[128]; 84 | size_t n_verts = 0, n_surfs = 0; 85 | //// read numbers of vertices and surfaces 86 | while((fgets (buffer, 128, file))!= NULL) { 87 | if (obj__starts_with(buffer, 'v')) 88 | n_verts++; 89 | else if (obj__starts_with(buffer, 'f')) 90 | n_surfs++; 91 | } 92 | //// allocate data and prepare for reading 93 | // this is what we want to return 94 | mesh_t* new = malloc(sizeof(mesh_t)); 95 | new->bounding_box.width = width; 96 | new->bounding_box.height = height; 97 | new->bounding_box.depth = depth; 98 | new->center = vec_vec3i_new(); 99 | vec_vec3i_set(new->center, cx, cy, cz); 100 | new->n_vertices = n_verts; 101 | new->n_faces = n_surfs; 102 | new->vertices = (vec3i_t**) malloc(sizeof(vec3i_t*) * n_verts); 103 | new->vertices_backup = (vec3i_t**) malloc(sizeof(vec3i_t*) * n_verts); 104 | obj__mesh_update_bbox(new); 105 | // allocate 2D array that indicates how vertices are connected at each surface 106 | new->connections = malloc(new->n_faces * sizeof(int*)); 107 | for (int i = 0; i < new->n_faces; ++i) 108 | new->connections[i] = malloc(6 * sizeof(int)); 109 | 110 | //// set vertices and surfaces 111 | // go back to beginning of the file 112 | fseek(file, 0, SEEK_SET); 113 | size_t ivert = 0, isurf = 0; 114 | while((fgets (buffer, 128, file)) != NULL) { 115 | char* pch = strtok (buffer, " vf"); 116 | if (obj__starts_with(buffer, 'v')) { 117 | const float x = atof(pch); 118 | pch = strtok (NULL, " "); 119 | const float y = atof(pch); 120 | pch = strtok (NULL, " "); 121 | const float z = atof(pch); 122 | new->vertices[ivert] = vec_vec3i_new(); 123 | vec_vec3i_set(new->vertices[ivert++], round(width/2*x), round(height/2*y), round(depth/2*z)); 124 | } else if (obj__starts_with(buffer, 'f')) { 125 | assert(atoi(pch) <= new->n_vertices); 126 | new->connections[isurf][0] = atoi(pch); 127 | pch = strtok (NULL, " "); 128 | new->connections[isurf][1] = atoi(pch); 129 | pch = strtok (NULL, " "); 130 | new->connections[isurf][2] = atoi(pch); 131 | pch = strtok (NULL, " "); 132 | new->connections[isurf][3] = atoi(pch); 133 | pch = strtok (NULL, " "); 134 | for (int i = 0; i < NUM_CONNECTIONS; ++i) { 135 | if (*pch == conn_letters[i]) 136 | new->connections[isurf][4] = conn_names[i]; 137 | } 138 | pch = strtok (NULL, " "); 139 | new->connections[isurf][5] = *pch; 140 | isurf++; 141 | } 142 | } 143 | fclose(file); 144 | //// shift them to center and back them up 145 | for (int i = 0; i < new->n_vertices; ++i) { 146 | *new->vertices[i] = vec_vec3i_add(new->vertices[i], new->center); 147 | new->vertices_backup[i] = vec_vec3i_new(); 148 | vec_vec3i_set(new->vertices_backup[i], 0, 0, 0); 149 | vec_vec3i_copy(new->vertices_backup[i], new->vertices[i]); 150 | } 151 | return new; 152 | } 153 | 154 | mesh_t* obj_triangle_new(vec3i_t* p0, vec3i_t* p1, vec3i_t* p2, color_t color) { 155 | mesh_t* new = malloc(sizeof(mesh_t)); 156 | new->center = malloc(sizeof(vec3i_t)); 157 | new->center->x = (p0->x + p1->x + p2->x)/3; 158 | new->center->y = (p0->y + p1->y + p2->y)/3; 159 | new->center->z = (p0->z + p1->z + p2->z)/3; 160 | new->n_vertices = 3; 161 | new->n_faces = 1; 162 | new->vertices = (vec3i_t**) malloc(sizeof(vec3i_t*) * new->n_vertices); 163 | new->vertices_backup = (vec3i_t**) malloc(sizeof(vec3i_t*) * new->n_vertices); 164 | unsigned width = UT_MAX( UT_MAX(abs(p0->x - p1->x), abs(p0->x - p2->x)), 165 | UT_MAX(abs(p0->x - p1->x), abs(p1->x - p2->x))); 166 | unsigned height = UT_MAX(UT_MAX(abs(p0->y - p1->y), abs(p0->y - p2->y)), 167 | UT_MAX(abs(p0->y - p1->y), abs(p1->y - p2->y))); 168 | new->bounding_box.width = width; 169 | new->bounding_box.height = height; 170 | new->bounding_box.depth = 1; 171 | new->vertices[0] = vec_vec3i_new(); 172 | new->vertices[1] = vec_vec3i_new(); 173 | new->vertices[2] = vec_vec3i_new(); 174 | vec_vec3i_set(new->vertices[0], p0->x, p0->y, p0->z); 175 | vec_vec3i_set(new->vertices[1], p1->x, p1->y, p1->z); 176 | vec_vec3i_set(new->vertices[2], p2->x, p2->y, p2->z); 177 | obj__mesh_update_bbox(new); 178 | obj__mesh_update_bbox(new); 179 | obj__mesh_update_bbox(new); 180 | 181 | // allocate 2D array that indicates how vertices are connected at each surface 182 | new->connections = malloc(new->n_faces * sizeof(int*)); 183 | for (int i = 0; i < new->n_faces; ++i) 184 | new->connections[i] = malloc(6 * sizeof(int)); 185 | // define surfaces 186 | new->connections[0][0] = 0; 187 | new->connections[0][1] = 1; 188 | new->connections[0][2] = 2; 189 | new->connections[0][3] = 0; 190 | new->connections[0][4] = CONNECTION_TRIANGLE; 191 | new->connections[0][5] = color; 192 | 193 | // finish creating the vertices - shift the to the mesh's origin, back them up 194 | for (int i = 0; i < new->n_vertices; ++i) { 195 | *new->vertices[i] = vec_vec3i_add(new->vertices[i], new->center); 196 | new->vertices_backup[i] = vec_vec3i_new(); 197 | vec_vec3i_set(new->vertices_backup[i], 0, 0, 0); 198 | vec_vec3i_copy(new->vertices_backup[i], new->vertices[i]); 199 | } 200 | return new; 201 | } 202 | 203 | void obj_mesh_rotate_to (mesh_t* mesh, float angle_x_rad, float angle_y_rad, float angle_z_rad) { 204 | for (size_t i = 0; i < mesh->n_vertices; ++i) { 205 | // first, reset each vertex so no floating point error is accumulated 206 | vec_vec3i_copy(mesh->vertices[i], mesh->vertices_backup[i]); 207 | 208 | // point to rotate about 209 | int x0 = mesh->center->x, y0 = mesh->center->y, z0 = mesh->center->z; 210 | // rotate around x axis, then y, then z 211 | // We rotate as follows (* denotes matrix product, C the mesh's origin): 212 | // v = v - C, v = Rz*Ry*Rx*v, v = v + C 213 | vec_vec3i_rotate(mesh->vertices[i], angle_x_rad, angle_y_rad, angle_z_rad, x0, y0, z0); 214 | } 215 | } 216 | 217 | void obj_mesh_translate_by(mesh_t* mesh, float dx, float dy, float dz) { 218 | vec3i_t translation = {round(dx), round(dy), round(dz)}; 219 | *mesh->center = vec_vec3i_add(mesh->center, &translation); 220 | for (size_t i = 0; i < mesh->n_vertices; ++i) { 221 | *mesh->vertices[i] = vec_vec3i_add(mesh->vertices[i], &translation); 222 | *mesh->vertices_backup[i] = vec_vec3i_add(mesh->vertices_backup[i], &translation); 223 | } 224 | obj__mesh_update_bbox(mesh); 225 | } 226 | 227 | void obj_mesh_free(mesh_t* mesh) { 228 | // free the data of the vertices first 229 | for (size_t i = 0; i < mesh->n_vertices; ++i) { 230 | free(mesh->vertices[i]); 231 | free(mesh->vertices_backup[i]); 232 | } 233 | free(mesh->vertices); 234 | free(mesh->vertices_backup); 235 | for (int i = 0; i < mesh->n_faces; ++i) 236 | free(mesh->connections[i]); 237 | free(mesh->connections); 238 | free(mesh->center); 239 | free(mesh); 240 | } 241 | 242 | //---------------------------------------------------------------------------------------------------------- 243 | // Ray 244 | //---------------------------------------------------------------------------------------------------------- 245 | ray_t* obj_ray_new() { 246 | ray_t* new = malloc(sizeof(ray_t)); 247 | new->orig = malloc(sizeof(vec3i_t)); 248 | new->end = malloc(sizeof(vec3i_t)); 249 | return new; 250 | } 251 | 252 | void obj_ray_set(ray_t* ray, int x0, int y0, int z0, int x1, int y1, int z1) { 253 | ray->orig = vec_vec3i_new(); 254 | ray->end = vec_vec3i_new(); 255 | vec_vec3i_set(ray->orig, x0, y0, z0); 256 | vec_vec3i_set(ray->end, x1, y1, z1); 257 | 258 | } 259 | 260 | void obj_ray_send(ray_t* ray, int x, int y, int z) { 261 | ray->end->x = x; 262 | ray->end->y = y; 263 | ray->end->z = z; 264 | } 265 | 266 | void obj_ray_free(ray_t* ray) { 267 | free(ray->orig); 268 | free(ray->end); 269 | free(ray); 270 | } 271 | 272 | //------------------------------------------------------------------------------------------------------------- 273 | // Camera 274 | //------------------------------------------------------------------------------------------------------------- 275 | camera_t* obj_camera_new() { 276 | camera_t* new = malloc(sizeof(camera_t)); 277 | return new; 278 | } 279 | 280 | void obj_camera_set(camera_t* camera, int cam_x0, int cam_y0, float focal_length) { 281 | camera->x0 = cam_x0; 282 | camera->y0 = cam_y0; 283 | camera->focal_length = focal_length; 284 | } 285 | 286 | 287 | //---------------------------------------------------------------------------------------------------------- 288 | // Plane 289 | //---------------------------------------------------------------------------------------------------------- 290 | 291 | plane_t* obj_plane_new () { 292 | plane_t* new = malloc(sizeof(plane_t)); 293 | new->normal = malloc(sizeof(vec3_t)); 294 | return new; 295 | } 296 | 297 | 298 | 299 | void obj_plane_set(plane_t* plane, vec3i_t* p0, vec3i_t* p1, vec3i_t* p2) { 300 | /** 301 | * Determine the plane through 3 3D points p0, p1, p2 by determining: 302 | * 1. the normal vector 303 | * 2. the offset 304 | * 305 | * normal 306 | * ^ 307 | * / 308 | * +----------------/-----------+ 309 | * / *p0 / / 310 | * / <_ / / 311 | * / \__ / / 312 | * / \ / / 313 | * / *p1 / 314 | * / _/ / 315 | * / _/ / 316 | * / < / 317 | * / p2* / 318 | * / / 319 | * +----------------------------+ 320 | * If p0, p1, p2 are co-planar, then the normal through p1 is 321 | * perpendicular to both * p1p2 = p2 - p1 and p1p0 = p0 - p1. 322 | * Thererefore it's determined as the cross product of the two: 323 | * normal = p1p2 x p1p0 = (p2 - p1) x (p0 - p1) 324 | * 325 | * normal 326 | * ^ 327 | * / 328 | * +---------------/-----------+ 329 | * / / / 330 | * / / / 331 | * / *p1 / 332 | * / / / 333 | * / ___/ / 334 | * / / / 335 | * / * <_/ / 336 | * / x / 337 | * +---------------------------+ 338 | * If x = (x,y,z) is any point on the plane, then the normal 339 | * through p1 is perpendicular to p1x = x - p1 therefore their 340 | * dot product is zero: 341 | * n.(x - p1) = 0 => 342 | * n.x - n.p1 = 0 343 | * -n.p1 is the offset from the origin 344 | */ 345 | vec3i_t p1p2 = vec_vec3i_sub(p2, p1); 346 | vec3i_t p1p0 = vec_vec3i_sub(p0, p1); 347 | *plane->normal = vec_vec3i_crossprod(&p1p2, &p1p0); 348 | plane->offset = -vec_vec3i_dotprod(plane->normal, p1); 349 | } 350 | 351 | 352 | // Whether a point m is inside a triangle (a, b, c) 353 | bool obj_is_point_in_triangle(vec3i_t* m, vec3i_t* a, vec3i_t* b, vec3i_t* c) { 354 | /* 355 | * To test whether a point is inside a triangle, | a_perp(-a_y, a,x) 356 | * we use the concept of perpendicular (perp) | ^ <---- 357 | * vectors and perpendicular dot product. Perp | \ | 358 | * dot product (pdot) formulates whether vector b is| | | 359 | * clockwise (cw) or counterclockwise (ccw) of a. | \ 360 | * Given vector a(a_x, a_y), its perp vector a_perp | \ a(a_x, a_y) 361 | * is defined as the same vector rotated by 90 | \ ----> 362 | * degrees ccw: | | ---------/ 363 | * a_perp = (-a_y, a_x) | \-/ 364 | * | 365 | * The dot product (.) alone doesn't tell us whether| 366 | * b is (c)cw of a. We need the pdot for that. | ^ a_perp b cw from a 367 | * As shown in the sketch on the right half: | | angle(a, b) > 90 368 | * | \ a_perp . b < 0 369 | * a_perp . b < 0 when b is cw from a and the | | -----> 370 | * angle between a, b is obtuse and | |-----/ a 371 | * a_perp . b < 0 when b is cw from a and the | | 372 | * angle between a, b is acute. | | 373 | * | v b 374 | * Therefore a_perp . b < 0 when b is cw from a. | 375 | * Similarly, a_perp . b > 0 when b is ccw from a. | ^ a_perp b cw from a 376 | * . .| \ angle(a, b) < 90 377 | * . .| | -----> a_perp . b < 0 378 | * . .| -------/ a 379 | * . .| \ 380 | * . (cont'ed) .| \- 381 | * . .| \ 382 | * . .| > b 383 | * . .| 384 | * The scematic below shows that for point M to be | For M to be inside triangle (ABC), 385 | * inside triangle (ABC) the following condition | MB needs to be (c)cw from MA, MC 386 | * must be satisfied: | (c)cw from MB and MA (c)cw from MC 387 | * | A 388 | * (MB ccw from MA) => MA_perp . MB > 0 and | _+ 389 | * (MC ccw from MB) => MB_perp . MC > 0 and | / ^\_ 390 | * (MA ccw from MC) => MC_perp . MA > 0 and | _/ / \ 391 | * or | / | \_ 392 | * (MB cw from MA) => MA_perp . MB < 0 and | / / \ 393 | * (MC cw from MB) => MB_perp . MC < 0 and | _/ M*------ \_ 394 | * (MA cw from MC) => MC_perp . MA < 0 and | / --/ \----> 395 | * . .| / -/ ______/ + 396 | * . .| _--/______/ C 397 | * . .| 0) && 409 | (VEC_PERP_DOT_PROD(mb, mc) > 0) && 410 | (VEC_PERP_DOT_PROD(mc, ma) > 0)); 411 | return are_all_cw || are_all_ccw; 412 | } 413 | 414 | bool obj_is_point_in_rect(vec3i_t* m, vec3i_t* a, vec3i_t* b, vec3i_t* c, vec3i_t* d) { 415 | /* 416 | * The diagram below visualises the conditions for M to be inside rectangle ABCD: 417 | * 418 | * A (AM.AB).unit(AB) B AM.AB > 0 419 | * +---------->-----------------+ AM.AB < AB.AB 420 | * | . | 421 | * | . | 422 | * | . | 423 | * | . | AM.AD > 0 424 | * (AD.AM).unit(AD) v. . . . . *M | AM.AD < AD.AD 425 | * | | 426 | * | | 427 | * | | 428 | * +----------------------------+ 429 | * D C 430 | * 431 | */ 432 | vec3i_t ab = vec_vec3i_sub(a, b); 433 | vec3i_t ad = vec_vec3i_sub(a, d); 434 | vec3i_t am = vec_vec3i_sub(a, m); 435 | return (0 < vec_vec3i_dotprod(&am, &ab)) && 436 | (vec_vec3i_dotprod(&am, &ab) < vec_vec3i_dotprod(&ab, &ab)) && 437 | (0 < vec_vec3i_dotprod(&am, &ad)) && 438 | (vec_vec3i_dotprod(&am, &ad) < vec_vec3i_dotprod(&ad, &ad)); 439 | } 440 | 441 | vec3i_t render__ray_plane_intersection(plane_t* plane, ray_t* ray) { 442 | /* 443 | * The parametric line of a ray from from the origin O through 444 | * point B ('end' of the ray) is: 445 | * R(t) = O + t(B - O) = tB 446 | * This ray meets the plane for some t=t0 such that: 447 | * R(t0) = B*t0 448 | * Therefore R(t0) validates the equation of the plane. 449 | * For the plane we know the normal vector n and the offset 450 | * from the origin d. Any point X on the plane validates its 451 | * equation, which is: 452 | * n.X = d 453 | * Since R(t0) lies on the plane: 454 | * n.R(t0) = d => 455 | * n.B*t0 = d => 456 | * t0 = d/(n.B) 457 | * Finally, the ray meets the plane at point 458 | * R(t0) = (d/(n.B))*B 459 | * This is what this function returns. 460 | */ 461 | float t0 = (float)plane->offset / vec_vec3i_dotprod(plane->normal, ray->end); 462 | // only interested in intersections along the positive direction 463 | t0 = (t0 < 0.0) ? -t0 : t0; 464 | vec3i_t ray_at_intersection = vec_vec3i_mul_scalar(ray->end, t0); 465 | return ray_at_intersection; 466 | } 467 | 468 | bool obj_ray_hits_rectangle(ray_t* ray, vec3i_t** points) { 469 | // find the intersection between the ray and the plane segment 470 | // defined by p0, p1, p2, p3 and if the intersection is whithin 471 | // that segment, return true 472 | vec3i_t* p0 = points[0]; 473 | vec3i_t* p1 = points[1]; 474 | vec3i_t* p2 = points[2]; 475 | vec3i_t* p3 = points[3]; 476 | obj_plane_set(g_plane_test, p0, p1, p2); 477 | vec3i_t ray_plane_intersection = render__ray_plane_intersection(g_plane_test, ray); 478 | return obj_is_point_in_rect(&ray_plane_intersection, p0, p1, p2, p3); 479 | } 480 | 481 | bool obj_ray_hits_triangle(ray_t* ray, vec3i_t** points) { 482 | // Find the intersection between the ray and the triangle (p0, p1, p2). 483 | // Return whether the intersection is whithin that triangle 484 | vec3i_t* p0 = points[0]; 485 | vec3i_t* p1 = points[1]; 486 | vec3i_t* p2 = points[2]; 487 | obj_plane_set(g_plane_test, p0, p1, p2); 488 | vec3i_t ray_plane_intersection = render__ray_plane_intersection(g_plane_test, ray); 489 | return obj_is_point_in_triangle(&ray_plane_intersection, p0, p1, p2); 490 | } 491 | 492 | 493 | void obj_plane_free (plane_t* plane) { 494 | free(plane->normal); 495 | free(plane); 496 | } 497 | -------------------------------------------------------------------------------- /src/renderer.c: -------------------------------------------------------------------------------- 1 | #include "renderer.h" 2 | #include "screen.h" 3 | #include "objects.h" 4 | #include "vector.h" 5 | #include "utils.h" 6 | #include 7 | #include // malloc, free 8 | #include // memset 9 | #include // INT_MAX, INT_MIN 10 | 11 | 12 | #define VEC_MAGN_SQUARED(vec) vec->x*vec->x + vec->y*vec->y + vec->z*vec->z 13 | #define VEC_PERP_DOT_PROD(a, b) a.x*b.y - a.y*b.x 14 | 15 | 16 | bool g_use_perspective = false; 17 | bool g_use_reflectance = false; 18 | int* g_z_buffer; 19 | vec3i_t** g_surf_points; 20 | // defines a plane each time we're about to hit a pixel 21 | plane_t* g_plane_test; 22 | ray_t* g_ray_test; 23 | // camera where rays are shot from 24 | camera_t g_camera; 25 | // stores the colors of a surfaces after it reflects light - from brightest to darkest 26 | color_t g_colors_refl[32]; 27 | // expand the second column of `CONN_TABLE`, mapping connections 28 | // to intersection functions in an 1-1 manner 29 | bool (*func_table_intersection[NUM_CONNECTIONS])(ray_t* ray, vec3i_t** points) = { 30 | #define X(a, b, c) c, 31 | CONN_TABLE 32 | #undef X 33 | }; 34 | 35 | //------------------------------------------------------------------------------------ 36 | // Static functions 37 | //------------------------------------------------------------------------------------ 38 | static inline float render__cosine_squared(vec3i_t* vec1, vec3i_t* vec2) { 39 | const unsigned m1 = vec1->x*vec1->x + vec1->y*vec1->y + vec1->z*vec1->z; 40 | const unsigned m2 = vec2->x*vec2->x + vec2->y*vec2->y + vec2->z*vec2->z; 41 | return ((float)vec_vec3i_dotprod(vec1, vec2))*vec_vec3i_dotprod(vec1, vec2) / 42 | ((float)m1*m2); 43 | } 44 | 45 | 46 | /* find the z-coordinate on a plane given x and y */ 47 | static inline int plane_z_at_xy(plane_t* plane, int x, int y) { 48 | // solve for z in plane's eq/n: n.x*x + n.y*y + n.z*z + offset = 0 49 | vec3i_t coeffs = (vec3i_t) {plane->normal->x, plane->normal->y, plane->offset}; 50 | vec3i_t xyz = (vec3i_t) {x, y, 1}; 51 | return round(1.0/plane->normal->z*(-vec_vec3i_dotprod(&coeffs, &xyz))); 52 | } 53 | 54 | 55 | /* perspective trasnform to map world point (3D) to screen (2D) */ 56 | static inline vec3i_t render__persp_transform(vec3i_t* xyz) { 57 | // to avoid drawing inverted images 58 | int sign = (xyz->z > 0) ? -1 : 1; 59 | return (vec3i_t) {UT_CLIP(sign*xyz->x*g_camera.focal_length/(xyz->z + 1e-8), -g_cols/2, g_cols/2), 60 | UT_CLIP(sign*xyz->y*g_camera.focal_length/(xyz->z + 1e-8), -g_rows, g_rows), 61 | xyz->z}; 62 | } 63 | 64 | /** 65 | * @brief Returns a color based on the angle between the ray and plane, 66 | * simulating reflection 67 | * 68 | * @param[in] ray A pointer to ray 69 | * @param[in] plane A pointer to plane 70 | * @param[in] shape A pointer to shape 71 | * 72 | * @returns Reflected color 73 | */ 74 | static inline color_t render__reflect(ray_t* ray, plane_t* plane, mesh_t* shape) { 75 | const int z_refl = (g_use_perspective) ? g_camera.focal_length : -shape->center->z/2; 76 | vec3i_t camera_axis = {g_camera.x0, 77 | g_camera.y0, 78 | z_refl}; 79 | const vec3i_t plane_normal = *plane->normal; 80 | const int ray_angle_ccw = VEC_PERP_DOT_PROD(camera_axis, plane_normal); 81 | const int sign = (ray_angle_ccw > 0) ? 1 : -1; 82 | const float ray_plane_angle = sign*render__cosine_squared(&camera_axis, plane->normal); 83 | //----------------------------------------------------- 84 | // reflectance 85 | /* 86 | * 87 | * w_a = 2/n 88 | * <---------> 89 | * -1 -.66 -.33 0 .33 .66 1 90 | * +---------+---------+---------+---------+---------+---------+ 91 | * | | | | | | | 92 | * +---------+---------+---------+---------+---------+---------+ 93 | * | | | | | | 94 | * | | | | | | 95 | * | | | | | | 96 | * 0 v v v v v v 31 97 | * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 98 | * | | | | | | | | | | | | | | | | | | | | | 99 | * +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 100 | * <--------> 101 | * w_c = floor(32/n) 102 | * 103 | * i_angle = floor((angle + 1)/(2/n)) 104 | * w_c = floor(32/n) 105 | * i_color_start = (32 - (32 mod n))/2 + w_c/2 106 | * i_color = i_color_start + i_angle * wc 107 | */ 108 | const int n = 2*shape->n_faces; 109 | const float w_a = 2.0/n; 110 | const size_t w_c = 32/n; 111 | return g_colors_refl[(size_t)( 112 | (32 % n)/2 + w_c/2 + 113 | (size_t)((ray_plane_angle+1)/w_a)*w_c)]; 114 | } 115 | 116 | static void render_reset_zbuffer() { 117 | for (size_t i = 0; i < g_buffer_size; ++i) 118 | g_z_buffer[i] = INT_MAX; 119 | } 120 | 121 | //------------------------------------------------------------------------------------ 122 | // External functions 123 | //------------------------------------------------------------------------------------ 124 | void render_use_perspective(int center_x0, int center_y0, float focal_length) { 125 | g_use_perspective = true; 126 | obj_camera_set(&g_camera, center_x0, center_y0, focal_length); 127 | } 128 | 129 | void render_use_reflectance() { 130 | g_use_reflectance = true; 131 | } 132 | 133 | void render_init() { 134 | // initialize screen (pixel) buffer 135 | screen_init(); 136 | // z buffer that records the depth of each pixel 137 | g_z_buffer = malloc(sizeof(int) * g_buffer_size); 138 | render_reset_zbuffer(); 139 | g_plane_test = obj_plane_new(); 140 | g_surf_points = malloc(sizeof(vec3i_t*) * 4); 141 | g_ray_test = obj_ray_new(); 142 | obj_ray_set(g_ray_test, 0, 0, 0, 0, 0, 0); 143 | // reflection colors from brightest to darkest 144 | strncpy(g_colors_refl, "#OT&=@$x%><)(nc+:;qy\"/?|+.,-v^!`", 32); 145 | } 146 | 147 | 148 | void render_write_shape(mesh_t* shape) { 149 | /* 150 | * This function renders the given cube by the basic ray tracing principle. 151 | * 152 | * A ray is shot from the origin to every pixel on the screen row by row. 153 | * For each screen coordinate, there can zero to two intersections with the cube. 154 | * If there is one, render the (x, y) of the intersection (not the x,y of the screen!). 155 | * If there are two, render the (x, y) of the closer intersection. In the figure below, 156 | * z_hit are the z of the two intersections and z_rend is the closest one. 157 | * 158 | * The ray below intersects faces (p0, p1, p2, p3) and (p4, p5, p6, p7) 159 | * 160 | * O camera origin 161 | * \ 162 | * \ 163 | * V ray 164 | * p3 \ p2 o cube's centre a 165 | * +-------------------+ + cube's vertices 166 | * | \ \ | \ # ray-cube intersections 167 | * | \ # z_rend | \ (z_hit) 168 | * | \ p7 | \ 169 | * | +-------------------+ p6 ^ y 170 | * | | \ . | | 171 | * | | \ . | | 172 | * | | \ . | o-------> x 173 | * | | \ . | \ 174 | * | | \ . | \ 175 | * p0 +---------|......\..+ p1 | V z 176 | * \ | \ . | 177 | * \ | # . | 178 | * \ | \ . | 179 | * \+----------\--------+ 180 | * p4 \ p5 181 | * \ 182 | * V 183 | */ 184 | // whether we want to use the perspective transform or not 185 | vec3i_t ray_origin = (vec3i_t) {g_camera.x0, g_camera.y0, g_camera.focal_length}; 186 | vec_vec3i_copy(g_ray_test->orig, &ray_origin); 187 | // screen boundaries 188 | int xmin, xmax, ymin, ymax; 189 | if (g_use_perspective) { 190 | // clip rendering area to bounding box 191 | xmin = UT_MIN(shape->bounding_box.x0, shape->bounding_box.x1); 192 | ymin = UT_MIN(shape->bounding_box.y0, shape->bounding_box.y1); 193 | xmax = UT_MAX(shape->bounding_box.x0, shape->bounding_box.x1); 194 | ymax = UT_MAX(shape->bounding_box.y0, shape->bounding_box.y1); 195 | } else { 196 | // clip rendering area to screen clip to rows and columns 197 | xmin = UT_MAX(-g_cols/2+1, shape->bounding_box.x0); 198 | ymin = UT_MAX(-g_rows, shape->bounding_box.y0); 199 | xmax = UT_MIN(g_cols/2, shape->bounding_box.x1); 200 | ymax = UT_MIN(g_rows+1, shape->bounding_box.y1); 201 | } 202 | // downscale by subsampling if we use perspective 203 | unsigned step = (g_use_perspective) ? 204 | UT_MIN(abs(shape->bounding_box.z0), abs(shape->bounding_box.z1))/g_camera.focal_length : 205 | 1; 206 | step = (step < 1) ? 1 : step; 207 | 208 | for (int y = ymin; y <= ymax; y += step) { 209 | for (int x = xmin; x <= xmax; x += step) { 210 | // -y to avoid drawing inverted images 211 | size_t buffer_ind = screen_xy2ind(x, -y); 212 | // the final pixel and color to render 213 | vec3i_t rendered_point = (vec3i_t) {x, -y, g_z_buffer[buffer_ind]}; 214 | for (size_t isurf = 0; isurf < shape->n_faces; ++isurf) { 215 | // unpack surface info, hence define surface from shape->vertices 216 | const size_t ipoint0 = shape->connections[isurf][0]; 217 | const size_t ipoint1 = shape->connections[isurf][1]; 218 | const size_t ipoint2 = shape->connections[isurf][2]; 219 | const size_t ipoint3 = shape->connections[isurf][3]; 220 | const int connection_type = shape->connections[isurf][4]; 221 | const color_t surf_color = shape->connections[isurf][5]; 222 | g_surf_points[0] = shape->vertices[ipoint0]; 223 | g_surf_points[1] = shape->vertices[ipoint1]; 224 | g_surf_points[2] = shape->vertices[ipoint2]; 225 | g_surf_points[3] = shape->vertices[ipoint3]; 226 | 227 | // find intersections of ray and surface and set colour accordingly 228 | obj_plane_set(g_plane_test, g_surf_points[0], g_surf_points[1], g_surf_points[2]); 229 | // we keep the z to find the closest one to the origin and we draw 230 | // its x and y at the z the ray hits the current surface 231 | int z_hit = plane_z_at_xy(g_plane_test, x, y); 232 | obj_ray_send(g_ray_test, x, y, z_hit); 233 | vec3i_t persp_point; 234 | // if we use perspective, we index the depth buffer at the (x,y) 235 | // of the projected point, not the original one (`persp_point`) 236 | if (g_use_perspective) { 237 | persp_point = (vec3i_t) {x, -y, z_hit}; 238 | persp_point = render__persp_transform(&persp_point); 239 | buffer_ind = screen_xy2ind(persp_point.x, persp_point.y); 240 | } 241 | if ((*func_table_intersection[connection_type])(g_ray_test, g_surf_points) && 242 | (z_hit < g_z_buffer[buffer_ind])) { 243 | color_t rendered_color = surf_color; 244 | // modern compilers (gcc >= 4.0, clang >= 3.0) know how to optimize this: 245 | if (g_use_reflectance) 246 | rendered_color = render__reflect(g_ray_test, g_plane_test, shape); 247 | if (g_use_perspective) 248 | rendered_point = persp_point; 249 | g_z_buffer[buffer_ind] = z_hit; 250 | screen_write_pixel(rendered_point.x, rendered_point.y, rendered_color); 251 | } 252 | } /* for surfaces */ 253 | } /* for x */ 254 | } /* for y */ 255 | } 256 | 257 | void render_flush() { 258 | render_reset_zbuffer(); 259 | screen_flush(); 260 | } 261 | 262 | 263 | void render_end() { 264 | screen_end(); 265 | free(g_surf_points); 266 | obj_plane_free(g_plane_test); 267 | } 268 | -------------------------------------------------------------------------------- /src/screen.c: -------------------------------------------------------------------------------- 1 | #include "vector.h" 2 | #include "screen.h" 3 | #include "objects.h" 4 | #include "utils.h" 5 | #include 6 | #include 7 | #include // STDOUT_FILENO 8 | #include // exit 9 | #include // true/false 10 | #include // memset 11 | #include // size_t 12 | 13 | #ifndef _WIN32 14 | #define IOCTL_SIZE_INVALID 0 15 | //---------------------------------------------------------------------------------- 16 | // Linux POSIX terminal manipulation macros 17 | //---------------------------------------------------------------------------------- 18 | #define SCREEN_CLEAR() printf("\033[H\033[J") 19 | #define SCREEN_GOTO_TOPLEFT() printf("\033[0;0H") 20 | #define SCREEN_HIDE_CURSOR() printf("\e[?25l") 21 | #define SCREEN_SHOW_CURSOR() printf("\e[?25h") 22 | #else 23 | //---------------------------------------------------------------------------------- 24 | // Windows terminal manipulation macros 25 | //---------------------------------------------------------------------------------- 26 | // Credits to @oogabooga: 27 | // https://cboard.cprogramming.com/c-programming/161186-undefined-reference.html 28 | #define SCREEN_CLEAR() do { \ 29 | COORD top_left = {0, 0}; \ 30 | DWORD c_chars_written; \ 31 | CONSOLE_SCREEN_BUFFER_INFO csbi; \ 32 | GetConsoleScreenBufferInfo(g_cons_out, &csbi); \ 33 | DWORD dw_con_size = csbi.dwSize.X * csbi.dwSize.Y; \ 34 | FillConsoleOutputCharacter(g_cons_out, ' ', dw_con_size, \ 35 | top_left, &c_chars_written); \ 36 | FillConsoleOutputAttribute(g_cons_out, csbi.wAttributes, \ 37 | dw_con_size, top_left, &c_chars_written); \ 38 | SetConsoleCursorPosition(g_cons_out, top_left); \ 39 | } while(0) 40 | // Credits to @Jerry Coffin: https://stackoverflow.com/a/2732327 41 | #define SCREEN_GOTO_TOPLEFT() do { \ 42 | COORD pos = {0, 0}; \ 43 | HANDLE output = GetStdHandle(STD_OUTPUT_HANDLE); \ 44 | SetConsoleCursorPosition(output, pos); \ 45 | } while(0) 46 | #define SCREEN_HIDE_CURSOR() ; 47 | #define SCREEN_SHOW_CURSOR() ; 48 | #endif 49 | //---------------------------------------------------------------------------------- 50 | 51 | // rows, columns of the terminal 52 | int g_rows; 53 | int g_cols; 54 | // columns over rows for the terminal 55 | static float g_cols_over_rows; 56 | // screen resolution (pixels over pixels) 57 | static float g_screen_res; 58 | color_t* g_screen_buffer; 59 | size_t g_buffer_size; 60 | 61 | 62 | /** 63 | * @brief Attempt to get the screen info (size and resolution) in three ways: 64 | * 1.`ioctl` call - fails on some terminals 65 | * 2. (fallback) xrandr command 66 | * 3. (fallback) assume a common screen resolution, e.g. 1920/1080 67 | * Writes to global variables `g_screen_res` and `g_cols_over_rows`, 68 | * `g_rows`, `g_cols`, `g_min_rows`, `g_min_cols`, `g_max_rows`, 69 | * `g_max_cols` 70 | */ 71 | static void draw__get_screen_info() { 72 | //// 1st way - ioctl call 73 | struct winsize wsize; 74 | ioctl(STDOUT_FILENO, TIOCGWINSZ, &wsize); 75 | g_rows = wsize.ws_row; 76 | g_cols = wsize.ws_col; 77 | g_cols_over_rows = (float)g_cols/g_rows; 78 | if ((wsize.ws_xpixel != IOCTL_SIZE_INVALID) || (wsize.ws_ypixel != IOCTL_SIZE_INVALID)) { 79 | g_screen_res = (float)wsize.ws_xpixel/wsize.ws_ypixel; 80 | return; 81 | } 82 | 83 | //// 2nd way - xrandr command 84 | // Open the command for reading 85 | #if 1 86 | FILE *fp; 87 | char line[512]; 88 | fp = popen("echo `xrandr --current | grep \'*\' | uniq | awk \'{print $1}\' | cut -d \'x\' -f1` / `xrandr --current | grep \'*\' | uniq | awk \'{print $1}\' | cut -d \'x\' -f2` | bc -l", "r"); 89 | if (fp == NULL) { 90 | printf("Failed to run command\n" ); 91 | exit(1); 92 | } 93 | // parse the output - it should only be the resolution 94 | while (fgets(line, sizeof(line), fp) != NULL) { 95 | if (ut_is_decimal(line)) { 96 | g_screen_res = atof(line); 97 | pclose(fp); 98 | return; 99 | } 100 | } 101 | #endif 102 | //// 3rd way - assume a common resolution 103 | g_screen_res = 1920.0/1080.0; 104 | } 105 | 106 | void screen_init() { 107 | SCREEN_HIDE_CURSOR(); 108 | SCREEN_CLEAR(); 109 | // get terminal's size info 110 | draw__get_screen_info(); 111 | g_buffer_size = g_rows*g_cols; 112 | g_screen_buffer = malloc(sizeof(color_t) * g_buffer_size); 113 | } 114 | 115 | size_t screen_xy2ind(int x, int y) { 116 | x += g_cols/2; 117 | y += g_rows; 118 | const int y_scaled = round(y/(g_cols_over_rows/g_screen_res)); 119 | const int ind_buffer = round(y_scaled*g_cols + x); 120 | if ((ind_buffer >= g_buffer_size) || (ind_buffer < 0)) 121 | return 0; 122 | return ind_buffer; 123 | } 124 | 125 | void screen_write_pixel(int x, int y, color_t c) { 126 | /* Uses the following coordinate system: 127 | * 128 | * ^ y 129 | * | 130 | * | 131 | * | 132 | * o--------> x 133 | * \ 134 | * \ 135 | * v z 136 | */ 137 | size_t ind_buffer = screen_xy2ind(x, y); 138 | g_screen_buffer[ind_buffer] = c; 139 | } 140 | 141 | void screen_flush() { 142 | // render the screen buffer 143 | for (size_t i = 0; i < g_buffer_size; ++i) 144 | putchar(g_screen_buffer[i]); 145 | memset(g_screen_buffer, ' ', sizeof(color_t) * g_buffer_size); 146 | SCREEN_GOTO_TOPLEFT(); 147 | } 148 | 149 | void screen_end() { 150 | free(g_screen_buffer); 151 | SCREEN_CLEAR(); 152 | SCREEN_SHOW_CURSOR(); 153 | } 154 | 155 | -------------------------------------------------------------------------------- /src/utils.c: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | #include 3 | 4 | bool ut_is_decimal(char* string) { 5 | bool ret = false; 6 | for (char* s = string; *s != '\0'; ++s) { 7 | if (((*s >= '0') && (*s <= '9')) || 8 | (*s == '.') || (*s == ',') || 9 | (*s == '\n')) 10 | ret = true; 11 | else 12 | return false; 13 | } 14 | return ret; 15 | } 16 | -------------------------------------------------------------------------------- /src/vector.c: -------------------------------------------------------------------------------- 1 | #include "xtrig.h" 2 | #include "vector.h" 3 | #include // true/false 4 | #include // malloc 5 | #include // round 6 | 7 | // square root tolerance distance when comparing vectors 8 | #define SQRT_TOL 1e-2 9 | 10 | 11 | //----------------------------------------------------------------------------------- 12 | // Floating point vectors 13 | //----------------------------------------------------------------------------------- 14 | vec3_t* vec_vec3_new() { 15 | vec3_t* new = malloc(sizeof(vec3_t)); 16 | return new; 17 | } 18 | 19 | void vec_vec3_set(vec3_t* vec, float x, float y, float z) { 20 | vec->x = x; 21 | vec->y = y; 22 | vec->z = z; 23 | } 24 | 25 | void vec_vec3_copy(vec3f_t* dest, vec3f_t* src) { 26 | dest->x = src->x; 27 | dest->y = src->y; 28 | dest->z = src->z; 29 | } 30 | 31 | bool vec_vec3_are_equal(vec3_t* vec1, vec3_t* vec2) { 32 | return (vec1->x - vec2->x)*(vec1->x - vec2->x) + (vec1->y - vec2->y)*(vec1->y - vec2->y) + (vec1->z - vec2->z)*(vec1->z - vec2->z) < SQRT_TOL*SQRT_TOL; 33 | } 34 | 35 | vec3_t vec_vec3_add(vec3_t* src1, vec3_t* src2) { 36 | return (vec3_t) {src1->x + src2->x, src1->y + src2->y, src1->z + src2->z}; 37 | } 38 | 39 | vec3_t vec_vec3_sub(vec3_t* src1, vec3_t* src2) { 40 | return (vec3_t) {src1->x - src2->x, src1->y - src2->y, src1->z - src2->z}; 41 | } 42 | 43 | vec3_t vec_vec3_mul_scalar (vec3_t* src, float scalar) { 44 | return (vec3_t) {round(scalar*src->x), round(scalar*src->y), round(scalar*src->z)}; 45 | } 46 | 47 | float vec_vec3_dotprod(vec3_t* src1, vec3_t* src2) { 48 | return src1->x*src2->x + src1->y*src2->y + src1->z*src2->z; 49 | } 50 | 51 | vec3_t vec_vec3_crossprod(vec3_t* src1, vec3_t* src2) { 52 | return (vec3_t) { src1->y*src2->z - src1->z*src2->y, 53 | -src1->x*src2->z + src1->z*src2->x, 54 | src1->x*src2->y - src1->y*src2->x}; 55 | } 56 | 57 | void vec_vec3_rotate(vec3_t* src, float angle_x_rad, float angle_y_rad, float angle_z_rad, int x0, int y0, int z0) { 58 | // -(x0, y0, z0) 59 | // bring to zero so we can do the rotation 60 | src->x -= x0; 61 | src->y -= y0; 62 | src->z -= z0; 63 | 64 | float a = angle_x_rad, b = angle_y_rad, c = angle_z_rad; 65 | float ca = fcos(a), cb = fcos(b), cc = fcos(c); 66 | float sa = fsin(a), sb = fsin(b), sc = fsin(c); 67 | float matrix_rotx[3][3] = { 68 | {1, 0, 0 }, 69 | {0, ca, -sa}, 70 | {0, sa, ca }, 71 | }; 72 | float matrix_roty[3][3] = { 73 | {cb, 0, sb}, 74 | {0, 1, 0}, 75 | {-sb, 0, cb}, 76 | }; 77 | float matrix_rotz[3][3] = { 78 | {cc, -sc, 0}, 79 | {sc, cc, 0}, 80 | {0, 0, 1}, 81 | }; 82 | // 83 | // x, y, z store the previous coordinates as computed by the previous operation 84 | int x = src->x; 85 | int y = src->y; 86 | int z = src->z; 87 | src->x = matrix_rotx[0][0]*x + matrix_rotx[0][1]*y + matrix_rotx[0][2]*z; 88 | src->y = matrix_rotx[1][0]*x + matrix_rotx[1][1]*y + matrix_rotx[1][2]*z; 89 | src->z = matrix_rotx[2][0]*x + matrix_rotx[2][1]*y + matrix_rotx[2][2]*z; 90 | // Ry 91 | x = src->x; 92 | y = src->y; 93 | z = src->z; 94 | src->x = matrix_roty[0][0]*x + matrix_roty[0][1]*y + matrix_roty[0][2]*z; 95 | src->y = matrix_roty[1][0]*x + matrix_roty[1][1]*y + matrix_roty[1][2]*z; 96 | src->z = matrix_roty[2][0]*x + matrix_roty[2][1]*y + matrix_roty[2][2]*z; 97 | // Rz 98 | x = src->x; 99 | y = src->y; 100 | z = src->z; 101 | src->x = matrix_rotz[0][0]*x + matrix_rotz[0][1]*y + matrix_rotz[0][2]*z; 102 | src->y = matrix_rotz[1][0]*x + matrix_rotz[1][1]*y + matrix_rotz[1][2]*z; 103 | src->z = matrix_rotz[2][0]*x + matrix_rotz[2][1]*y + matrix_rotz[2][2]*z; 104 | 105 | // +(x0, y0, z0) 106 | // reset original offset 107 | src->x += x0; 108 | src->y += y0; 109 | src->z += z0; 110 | } 111 | 112 | //----------------------------------------------------------------------------------- 113 | // Integral vectors 114 | //----------------------------------------------------------------------------------- 115 | vec3i_t* vec_vec3i_new() { 116 | vec3i_t* new = malloc(sizeof(vec3i_t)); 117 | return new; 118 | } 119 | 120 | void vec_vec3i_set(vec3i_t* vec, int x, int y, int z) { 121 | vec->x = x; 122 | vec->y = y; 123 | vec->z = z; 124 | } 125 | 126 | void vec_vec3i_copy(vec3i_t* dest, vec3i_t* src) { 127 | dest->x = src->x; 128 | dest->y = src->y; 129 | dest->z = src->z; 130 | } 131 | 132 | bool vec_vec3i_are_equal(vec3i_t* vec1, vec3i_t* vec2) { 133 | return (vec1->x == vec2->x) && (vec1->y == vec2->y) && (vec1->z == vec2->z); 134 | } 135 | 136 | vec3i_t vec_vec3i_add(vec3i_t* src1, vec3i_t* src2) { 137 | return (vec3i_t) {src1->x + src2->x, src1->y + src2->y, src1->z + src2->z}; 138 | } 139 | 140 | vec3i_t vec_vec3i_sub(vec3i_t* src1, vec3i_t* src2) { 141 | return (vec3i_t) {src1->x - src2->x, src1->y - src2->y, src1->z - src2->z}; 142 | } 143 | 144 | vec3i_t vec_vec3i_mul_scalar (vec3i_t* src, float scalar) { 145 | return (vec3i_t) {round(scalar*src->x), round(scalar*src->y), round(scalar*src->z)}; 146 | } 147 | 148 | int vec_vec3i_dotprod(vec3i_t* src1, vec3i_t* src2) { 149 | return src1->x*src2->x + src1->y*src2->y + src1->z*src2->z; 150 | } 151 | 152 | vec3i_t vec_vec3i_crossprod(vec3i_t* src1, vec3i_t* src2) { 153 | return (vec3i_t) { src1->y*src2->z - src1->z*src2->y, 154 | -src1->x*src2->z + src1->z*src2->x, 155 | src1->x*src2->y - src1->y*src2->x}; 156 | } 157 | 158 | void vec_vec3i_rotate(vec3i_t* src, float angle_x_rad, float angle_y_rad, float angle_z_rad, int x0, int y0, int z0) { 159 | // -(x0, y0, z0) 160 | // bring vector to rotate to zero so we can do the rotation 161 | // contains input vector as float so we can do the operations precisely 162 | vec3_t rotated = (vec3_t) {src->x, src->y, src->z}; 163 | rotated.x -= x0; 164 | rotated.y -= y0; 165 | rotated.z -= z0; 166 | 167 | const float a = angle_x_rad, b = angle_y_rad, c = angle_z_rad; 168 | const float ca = fcos(a), cb = fcos(b), cc = fcos(c); 169 | const float sa = fsin(a), sb = fsin(b), sc = fsin(c); 170 | const float matrix_rotx[3][3] = { 171 | {1, 0, 0 }, 172 | {0, ca, -sa}, 173 | {0, sa, ca }, 174 | }; 175 | const float matrix_roty[3][3] = { 176 | {cb, 0, sb}, 177 | {0, 1, 0}, 178 | {-sb, 0, cb}, 179 | }; 180 | const float matrix_rotz[3][3] = { 181 | {cc, -sc, 0}, 182 | {sc, cc, 0}, 183 | {0, 0, 1}, 184 | }; 185 | // x, y, z store the previous coordinates as computed by the previous operation 186 | int x = rotated.x; 187 | int y = rotated.y; 188 | int z = rotated.z; 189 | rotated.x = matrix_rotx[0][0]*x + matrix_rotx[0][1]*y + matrix_rotx[0][2]*z; 190 | rotated.y = matrix_rotx[1][0]*x + matrix_rotx[1][1]*y + matrix_rotx[1][2]*z; 191 | rotated.z = matrix_rotx[2][0]*x + matrix_rotx[2][1]*y + matrix_rotx[2][2]*z; 192 | // Ry 193 | x = rotated.x; 194 | y = rotated.y; 195 | z = rotated.z; 196 | rotated.x = matrix_roty[0][0]*x + matrix_roty[0][1]*y + matrix_roty[0][2]*z; 197 | rotated.y = matrix_roty[1][0]*x + matrix_roty[1][1]*y + matrix_roty[1][2]*z; 198 | rotated.z = matrix_roty[2][0]*x + matrix_roty[2][1]*y + matrix_roty[2][2]*z; 199 | // Rz 200 | x = rotated.x; 201 | y = rotated.y; 202 | z = rotated.z; 203 | rotated.x = matrix_rotz[0][0]*x + matrix_rotz[0][1]*y + matrix_rotz[0][2]*z; 204 | rotated.y = matrix_rotz[1][0]*x + matrix_rotz[1][1]*y + matrix_rotz[1][2]*z; 205 | rotated.z = matrix_rotz[2][0]*x + matrix_rotz[2][1]*y + matrix_rotz[2][2]*z; 206 | 207 | // +(x0, y0, z0) 208 | // reset original offset 209 | rotated.x += x0; 210 | rotated.y += y0; 211 | rotated.z += z0; 212 | 213 | src->x = round(rotated.x); 214 | src->y = round(rotated.y); 215 | src->z = round(rotated.z); 216 | } 217 | -------------------------------------------------------------------------------- /src/xtrig.c: -------------------------------------------------------------------------------- 1 | #include "xtrig.h" 2 | 3 | double sine_lut[LUT_SIZE]; 4 | 5 | void ftrig_init_lut() { 6 | for (int i = 0; i < LUT_SIZE; i++) 7 | sine_lut[i] = sin(i * LUT_BIN_SIZE); 8 | } 9 | 10 | /** Get the index of an angle in the LUT */ 11 | static int get_lookup_index(double rad) { 12 | return (int) round(rad / LUT_BIN_SIZE); 13 | } 14 | 15 | double fsin(double rad) { 16 | // use the period and symmetry to compute based off [0, pi/2] 17 | rad = fmod(rad, 2*M_PI); 18 | if (rad < 0) rad += 2*M_PI; 19 | if (rad <= HALF_PI) { 20 | return sine_lut[get_lookup_index(rad)]; 21 | } else if (rad <= M_PI) { 22 | return sine_lut[get_lookup_index(M_PI - rad)]; 23 | } else if (rad <= 3*HALF_PI) { 24 | return -sine_lut[get_lookup_index(rad - M_PI)]; 25 | } else { 26 | return -sine_lut[get_lookup_index(2*M_PI - rad)]; 27 | } 28 | } 29 | 30 | double fcos(double rad) { 31 | return fsin(HALF_PI - rad); 32 | } 33 | 34 | --------------------------------------------------------------------------------