├── .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 | [](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 |  | 
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 |
--------------------------------------------------------------------------------