├── render.png ├── .gitignore ├── LICENSE.md ├── README.md ├── png.cmake └── CMakeLists.txt /render.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/64/cmake-raytracer/HEAD/render.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | **/CMakeFiles 3 | **/*.cmake 4 | !png.cmake 5 | **/CMakeCache.txt 6 | **/Makefile 7 | **/*image.* 8 | worker-* 9 | *.sln 10 | .vscode/ 11 | *.vcxproj* 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2020 64 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CMake Ray Tracer 2 | 3 | A simple ray tracer written in pure CMake. Inspired by [raytracer.hpp](https://github.com/tcbrindle/raytracer.hpp). More information can be found at [my blog](https://64.github.io/cmake-raytracer). 4 | 5 | ![image](render.png) 6 | 7 | ## Usage 8 | 9 | The ray tracer writes its output to `stderr`, so you can use it with: 10 | 11 | ```shell 12 | cmake -Dimage_width=64 -Dimage_height=64 -Dnum_procs=4 -P CMakeLists.txt 2> image.ppm 13 | ``` 14 | 15 | Which renders using 4 CPU cores and writes the output to `image.ppm`. Then use an image viewer capable of opening PPM files (or [this](http://www.cs.rhodes.edu/welshc/COMP141_F16/ppmReader.html)) to view. 16 | 17 | **Alternatively, the ray tracer can directly write PNG files** (thanks to @benmcmorran for contributing this). The PNG encoder is also implemented in pure CMake, but either PowerShell or Python is required with this method since CMake is unable to write binary files directly. Set the `use_png` variable to set the name of the PNG file (without the extension). The example below will write output to `render.png`: 18 | 19 | ```shell 20 | cmake -Dimage_width=64 -Dimage_height=64 -Dnum_procs=4 -Duse_png=render -P CMakeLists.txt 21 | ``` 22 | 23 | `num_procs` controls the number of worker processes spawned. It is recommended to set this to a value no greater than the number of cores in your CPU, for maximum performance. If not provided, it will be automatically detected based on the number of available cores. 24 | 25 | To keep the code simple, the `image_width`, `image_height` and `num_procs` must be powers of 2, otherwise the image will not be fully formed. If not specified, these arguments default to the values shown above. 26 | 27 | ## Performance 28 | 29 | Using cmake 3.19.2 on Linux 5.4 on a i5-10210U (4 cores, 8 threads), running this command: 30 | 31 | ```shell 32 | for X in 1 2 4 8 16 32 64 128 256 512 ; do echo SIZE $X ; time cmake -Dimage_width=$X -Dimage_height=$X -Dnum_procs=8 -P CMakeLists.txt 2> image_size_${X}.ppm ; done 33 | ``` 34 | PowerShell alternative: 35 | ```powershell 36 | 1, 2, 4, 8, 16, 32, 64, 128, 256, 512 | foreach {Write-Host $_; (Measure-Command { cmake "-Dimage_width=$_" "-Dimage_height=$_" "-Dnum_procs=$Env:NUMBER_OF_PROCESSORS" -P CMakeLists.txt 2> image_size_$_.ppm }).TotalSeconds} 37 | ``` 38 | 39 | Figures reported by `time` command below (reformatted). As usual, the "real" time is the wall clock time. The others are summed on all processors. 40 | 41 | | size | real | user | sys | 42 | | ---: | ---: | ---: | --: | 43 | | 1 | 0,054s | 0,130s | 0,051s | 44 | | 2 | 0,035s | 0,098s | 0,041s | 45 | | 4 | 0,077s | 0,274s | 0,018s | 46 | | 8 | 0,106s | 0,460s | 0,023s | 47 | | 16 | 0,367s | 1,871s | 0,059s | 48 | | 32 | 1,296s | 7,617s | 0,132s | 49 | | 64 | 5,175s | 29,455s | 0,356s | 50 | | 128 | 21,093s | 2m06,299s | 1,566s | 51 | | 256 | 1m33,395s | 9m21,875s | 5,999s | 52 | | 512 | 7m23,094s | 45m36,327s | 32,588s | 53 | 54 | ## Contributing 55 | 56 | All contributions (issue, PRs) are welcome. This project is licensed under the MIT license. 57 | -------------------------------------------------------------------------------- /png.cmake: -------------------------------------------------------------------------------- 1 | set(CRC_LOOKUP_TABLE 2 | -771559539 -1526341861 1007455905 1259060791 -714134636 -1570235646 996231864 1281784366 3 | -589731905 -1411492055 852952723 1171273221 -608918618 -1397517520 901431946 1119744540 4 | -810156055 -1196241025 565944005 1455205971 -925352976 -1075901594 651582172 1372678730 5 | -1049724965 -1234614451 794826487 1483155041 -972835902 -1325104300 671994606 1594548856 6 | -378745019 -1637089325 123907689 1885708031 -301921444 -1727644726 1010288 1997036262 7 | -407419017 -1867483167 163128923 2126386893 -522550418 -1747078152 248832578 2043925204 8 | -186917087 -2082672713 450215437 1842515611 -206169288 -2068763730 498629140 1790921346 9 | -100641005 -1928894587 336475711 1661535913 -43150582 -1972722788 325317158 1684325040 10 | -1528910307 -740712821 1255198513 1037565863 -1548523004 -726377838 1304234792 985283518 11 | -1442503121 -587065671 1141589763 856455061 -1385635274 -630205792 1130791706 878818188 12 | -1184252295 -831615249 1466425173 543223747 -1107002784 -922531082 1342839628 655174618 13 | -1213057461 -1061878051 1505515367 784033777 -1327500718 -942095676 1590793086 701932520 14 | -1615819051 -390611389 1908338681 112844655 -1730327860 -270894502 1993550816 30677878 15 | -1855256857 -429115791 2137352139 140662621 -1777941762 -519966104 2013832146 252678980 16 | -2113429839 -184504793 1812594589 453955339 -2056627544 -227710402 1801730948 476252946 17 | -1931733373 -69523947 1657960367 366298937 -1951280486 -55123444 1707062198 314082080 18 | 1069182125 1220369467 -776729215 -1498202857 953657524 1339070498 -690370152 -1579222770 19 | 828499103 1181144073 -546339405 -1469532891 906764422 1091244048 -670940758 -1358597828 20 | 571309257 1426738271 -872210971 -1157354125 627095760 1382516806 -881927684 -1133909654 21 | 752284923 1540473965 -1025993257 -1243634367 733688034 1555824756 -977972786 -1296932520 22 | 81022053 1943239923 -354800311 -1646453281 62490748 1958656234 -306714288 -1699685946 23 | 168805463 2097738945 -469654149 -1828284947 224526414 2053451992 -479436446 -1804905996 24 | 425942017 1852075159 -143835859 -2140533317 504272920 1762240654 -268371660 -2029532766 25 | 397988915 1623188645 -105466593 -1900968567 282398762 1741824188 -19173114 -1982054000 26 | 1231433021 1046551979 -1486337007 -797999993 1309403428 957143474 -1610250232 -687687522 27 | 1203610895 817534361 -1447836637 -558566219 1087398166 936857984 -1361182662 -640077652 28 | 1422998873 601230799 -1159766923 -841454365 1404893504 616286678 -1112369044 -894064390 29 | 1510651243 755860989 -1274751929 -1023154991 1567060338 710951396 -1284960162 -999415608 30 | 1913130485 84884835 -1677300519 -352232369 1969605100 40040826 -1687443264 -328427434 31 | 2094237127 198489425 -1830951701 -438643587 2076066270 213479752 -1783619342 -491319196 32 | 1874795921 414723335 -2119074627 -155825109 1758648712 534112542 -2032355164 -237270990 33 | 1633981859 375629109 -1888815985 -127024103 1711886778 286155052 -2012794730 -16777216 34 | ) 35 | 36 | # Decimal, big-endian 37 | function(to_two_bytes) 38 | set(ONE_VALUE_ARGS NUMBER OUTPUT) 39 | cmake_parse_arguments(PARSE_ARGV 0 ARG "" "${ONE_VALUE_ARGS}" "") 40 | math(EXPR BYTE_1 "${ARG_NUMBER} / 256") 41 | math(EXPR BYTE_2 "${ARG_NUMBER} % 256") 42 | set(${ARG_OUTPUT} ${BYTE_1} ${BYTE_2} PARENT_SCOPE) 43 | endfunction() 44 | 45 | # Decimal, big-endian 46 | function(to_four_bytes) 47 | set(ONE_VALUE_ARGS NUMBER OUTPUT) 48 | cmake_parse_arguments(PARSE_ARGV 0 ARG "" "${ONE_VALUE_ARGS}" "") 49 | math(EXPR BYTE_1 "(${ARG_NUMBER} & 0xFF000000) >> 24") 50 | math(EXPR BYTE_2 "(${ARG_NUMBER} & 0x00FF0000) >> 16") 51 | math(EXPR BYTE_3 "(${ARG_NUMBER} & 0x0000FF00) >> 8") 52 | math(EXPR BYTE_4 "${ARG_NUMBER} & 0x000000FF") 53 | set(${ARG_OUTPUT} ${BYTE_1} ${BYTE_2} ${BYTE_3} ${BYTE_4} PARENT_SCOPE) 54 | endfunction() 55 | 56 | function(compute_adler32) 57 | cmake_parse_arguments(PARSE_ARGV 0 ARG "" OUTPUT DATA) 58 | set(ACCUMULATOR_1 1) 59 | set(ACCUMULATOR_2 0) 60 | foreach(ELEMENT IN LISTS ARG_DATA) 61 | math(EXPR ACCUMULATOR_1 "(${ELEMENT} + ${ACCUMULATOR_1}) % 65521") 62 | math(EXPR ACCUMULATOR_2 "(${ACCUMULATOR_1} + ${ACCUMULATOR_2}) % 65521") 63 | endforeach() 64 | to_two_bytes(NUMBER ${ACCUMULATOR_1} OUTPUT BYTES_1) 65 | to_two_bytes(NUMBER ${ACCUMULATOR_2} OUTPUT BYTES_2) 66 | set(${ARG_OUTPUT} ${BYTES_2} ${BYTES_1} PARENT_SCOPE) 67 | endfunction() 68 | 69 | function(compute_crc32) 70 | cmake_parse_arguments(PARSE_ARGV 0 ARG "" OUTPUT DATA) 71 | set(CRC 0) 72 | foreach(BYTE IN LISTS ARG_DATA) 73 | math(EXPR INDEX "(${CRC} & 0xFF) ^ ${BYTE}") 74 | list(GET CRC_LOOKUP_TABLE ${INDEX} TABLE_VALUE) 75 | math(EXPR CRC "${TABLE_VALUE} ^ ((${CRC} & 0xFFFFFFFF) >> 8)") 76 | endforeach() 77 | to_four_bytes(NUMBER ${CRC} OUTPUT CRC_BYTES) 78 | set(${ARG_OUTPUT} ${CRC_BYTES} PARENT_SCOPE) 79 | endfunction() 80 | 81 | function(encode_deflate_block) 82 | cmake_parse_arguments(PARSE_ARGV 0 ARG LAST_BLOCK OUTPUT DATA) 83 | 84 | # Start with a DEFLATE block header. From the least significant bit: 85 | # BFINAL = 1/0 (last block/not last block) 86 | # BTYPE = 00 (no compression) 87 | # Other bits in the byte are skipped when compression is disabled. 88 | if(ARG_LAST_BLOCK) 89 | set(RESULT 1) 90 | else() 91 | set(RESULT 0) 92 | endif() 93 | 94 | # Next is two bytes for the length of the block, followed by the one's 95 | # complement of the length. Even though the rest of the PNG uses big 96 | # endian numbers, deflate uses little endian lengths. 97 | list(LENGTH ARG_DATA LEN) 98 | to_two_bytes(NUMBER ${LEN} OUTPUT LEN_BYTES) 99 | list(REVERSE LEN_BYTES) 100 | list(APPEND RESULT ${LEN_BYTES}) 101 | foreach(LEN_BYTE IN LISTS LEN_BYTES) 102 | math(EXPR NLEN_BYTE "~${LEN_BYTE} & 0xFF") 103 | list(APPEND RESULT ${NLEN_BYTE}) 104 | endforeach() 105 | 106 | # And finally the actual block data itself. 107 | list(APPEND RESULT ${ARG_DATA}) 108 | set(${ARG_OUTPUT} ${RESULT} PARENT_SCOPE) 109 | endfunction() 110 | 111 | function(encode_zlib) 112 | cmake_parse_arguments(PARSE_ARGV 0 ARG "" OUTPUT DATA) 113 | 114 | # Start with a zlib stream header. 115 | # CM = 8 ("deflate" compression method) 116 | # CINFO = 0 (window size of 256, unused with compression disabled) 117 | # FCHECK = 29 (ensures header is a multiple of 31) 118 | # FDICT = 0 (no preset dictionary) 119 | # FLEVEL = 0 (compressor used fastest algorithm) 120 | set(RESULT 8 29) 121 | 122 | # Each DEFLATE block uses two bytes for length, so break up the data into 123 | # blocks that are at most 2^16-1 bytes long. 124 | list(LENGTH ARG_DATA LEN) 125 | math(EXPR LAST_INDEX "${LEN} - 1") 126 | foreach(START_INDEX RANGE 0 ${LAST_INDEX} 65535) 127 | list(SUBLIST ARG_DATA ${START_INDEX} 65535 BLOCK_DATA) 128 | math(EXPR NEXT_INDEX "${START_INDEX} + 65535") 129 | if(NEXT_INDEX GREATER LAST_INDEX) 130 | encode_deflate_block(LAST_BLOCK OUTPUT DEFLATE_BLOCK DATA ${BLOCK_DATA}) 131 | else() 132 | encode_deflate_block(OUTPUT DEFLATE_BLOCK DATA ${BLOCK_DATA}) 133 | endif() 134 | list(APPEND RESULT ${DEFLATE_BLOCK}) 135 | endforeach() 136 | 137 | # The zlib stream ends with an ADLER32 checksum of the uncompressed data. 138 | compute_adler32(OUTPUT CHECKSUM DATA ${ARG_DATA}) 139 | list(APPEND RESULT ${CHECKSUM}) 140 | set(${ARG_OUTPUT} ${RESULT} PARENT_SCOPE) 141 | endfunction() 142 | 143 | function(encode_png_block) 144 | set(ONE_VALUE_KEYWORDS OUTPUT NAME) 145 | cmake_parse_arguments(PARSE_ARGV 0 ARG "" "${ONE_VALUE_KEYWORDS}" DATA) 146 | 147 | # Get the ASCII bytes corresponding to block type name. 148 | set(RESULT) 149 | string(HEX ${ARG_NAME} HEX_NAME) 150 | foreach(INDEX RANGE 0 6 2) 151 | string(SUBSTRING ${HEX_NAME} ${INDEX} 2 HEX_BYTE) 152 | string(PREPEND HEX_BYTE "0x") 153 | math(EXPR HEX_BYTE ${HEX_BYTE}) 154 | list(APPEND RESULT ${HEX_BYTE}) 155 | endforeach() 156 | 157 | # Add the block data. 158 | list(APPEND RESULT ${ARG_DATA}) 159 | 160 | # The CRC-32 checksum includes the block type, but not the block length. 161 | compute_crc32(OUTPUT CHECKSUM DATA ${RESULT}) 162 | list(APPEND RESULT ${CHECKSUM}) 163 | 164 | # Length comes at the beginning of the block. 165 | list(LENGTH ARG_DATA LEN) 166 | to_four_bytes(NUMBER ${LEN} OUTPUT LEN_BYTES) 167 | list(PREPEND RESULT ${LEN_BYTES}) 168 | set(${ARG_OUTPUT} ${RESULT} PARENT_SCOPE) 169 | endfunction() 170 | 171 | function(encode_png) 172 | set(ONE_VALUE_KEYWORDS OUTPUT WIDTH HEIGHT) 173 | cmake_parse_arguments(PARSE_ARGV 0 ARG "" "${ONE_VALUE_KEYWORDS}" DATA) 174 | 175 | # Magic bytes at the beginning of the PNG format. 176 | set(RESULT 137 80 78 71 13 10 26 10) 177 | 178 | # The IHDR block starts with width and height. 179 | set(IHDR_DATA) 180 | to_four_bytes(NUMBER ${ARG_WIDTH} OUTPUT IHDR_DATA) 181 | to_four_bytes(NUMBER ${ARG_HEIGHT} OUTPUT HEIGHT_BYTES) 182 | list(APPEND IHDR_DATA ${HEIGHT_BYTES}) 183 | 184 | # Set all the other values to truecolor 8-bit with no interlacing. 185 | list(APPEND IHDR_DATA 8 2 0 0 0) 186 | encode_png_block(OUTPUT IHDR_BYTES NAME IHDR DATA ${IHDR_DATA}) 187 | list(APPEND RESULT ${IHDR_BYTES}) 188 | 189 | # Prefix every scanline with a filter type of 0 (no compression). 190 | list(LENGTH ARG_DATA DATA_LEN) 191 | math(EXPR DATA_LEN "${DATA_LEN} - 1") 192 | math(EXPR SCANLINE_LEN "${ARG_WIDTH} * 3") 193 | set(FILTERED_DATA) 194 | foreach(INDEX RANGE 0 ${DATA_LEN} ${SCANLINE_LEN}) 195 | list(SUBLIST ARG_DATA ${INDEX} ${SCANLINE_LEN} SCANLINE_DATA) 196 | list(APPEND FILTERED_DATA 0 ${SCANLINE_DATA}) 197 | endforeach() 198 | 199 | # zlib encode the image data before writing the IDAT block. 200 | encode_zlib(OUTPUT IDAT_DATA DATA ${FILTERED_DATA}) 201 | encode_png_block(OUTPUT IDAT_BYTES NAME IDAT DATA ${IDAT_DATA}) 202 | list(APPEND RESULT ${IDAT_BYTES}) 203 | 204 | # The final IEND block has no data. 205 | encode_png_block(OUTPUT IEND_BYTES NAME IEND) 206 | list(APPEND RESULT ${IEND_BYTES}) 207 | set(${ARG_OUTPUT} ${RESULT} PARENT_SCOPE) 208 | endfunction() 209 | 210 | # There's no way to write binary files in CMake, so we use Powershell or Python. 211 | # https://gitlab.kitware.com/cmake/cmake/-/issues/21878 212 | function(write_binary_file) 213 | cmake_parse_arguments(PARSE_ARGV 0 ARG "" FILENAME DATA) 214 | string(JOIN "\n" TEXT ${ARG_DATA}) 215 | file(WRITE ${ARG_FILENAME}.temp ${TEXT}) 216 | find_package(Python COMPONENTS Interpreter) 217 | if(Python_Interpreter_FOUND) 218 | execute_process(COMMAND ${Python_EXECUTABLE} -c "with open('${ARG_FILENAME}.temp', 'r') as f, open('${ARG_FILENAME}.png', 'wb') as o: [o.write(int(l).to_bytes()) for l in f]" 219 | RESULT_VARIABLE ret) 220 | elseif(WIN32) 221 | execute_process(COMMAND powershell -Command "(Get-Content '${ARG_FILENAME}.temp').ForEach({ [byte] $_ }) | Set-Content '${ARG_FILENAME}.png' -Encoding Byte" 222 | RESULT_VARIABLE ret) 223 | else() 224 | message(FATAL_ERROR "Python or Powershell is required to write PNG files.") 225 | endif() 226 | if(NOT ret EQUAL 0) 227 | message(FATAL_ERROR "Failed to write ${ARG_FILENAME}.png") 228 | endif() 229 | file(REMOVE ${ARG_FILENAME}.temp) 230 | endfunction() 231 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15 #[[ 2 | 3.7+ if(... [ LESS_EQUAL | GREATER_EQUAL ] ...) 3 | 3.15+ string(REPEAT ...) 4 | ]]) 5 | 6 | get_property(cmake_role GLOBAL PROPERTY CMAKE_ROLE) 7 | if(NOT cmake_role STREQUAL "SCRIPT") 8 | message(FATAL_ERROR "Please run in script mode, e.g: cmake -P CMakeLists.txt") 9 | endif() 10 | 11 | if(NOT DEFINED image_width) 12 | set(image_width 64) 13 | endif() 14 | if(NOT DEFINED image_height) 15 | set(image_height 64) 16 | endif() 17 | if(NOT DEFINED num_procs) 18 | cmake_host_system_information(RESULT num_procs QUERY NUMBER_OF_PHYSICAL_CORES) 19 | if(num_procs LESS 1) 20 | set(num_procs 2) 21 | endif() 22 | endif() 23 | 24 | include(png.cmake) 25 | 26 | if(NOT DEFINED worker_index) 27 | message(STATUS "Launching ray tracer with ${num_procs} processes, ${image_width}x${image_height} image...") 28 | 29 | set(exec_args) 30 | foreach(worker_index RANGE 1 ${num_procs}) 31 | list(APPEND exec_args 32 | COMMAND "${CMAKE_COMMAND}" 33 | -Dworker_index=${worker_index} 34 | -Dimage_width=${image_width} 35 | -Dimage_height=${image_height} 36 | -Dnum_procs=${num_procs} 37 | "-P" 38 | "${CMAKE_CURRENT_LIST_FILE}" 39 | ) 40 | endforeach() 41 | 42 | # Begin the worker processes 43 | execute_process( 44 | ${exec_args} 45 | WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" 46 | ) 47 | 48 | message(STATUS "Finished ray tracing, gathering results...") 49 | 50 | # Output PPM or PNG 51 | if(NOT DEFINED use_png) 52 | set(image_contents "P3 ${image_width} ${image_height}\n255\n\n") 53 | endif() 54 | 55 | foreach(worker_index RANGE 1 ${num_procs}) 56 | file(READ 57 | "${CMAKE_CURRENT_BINARY_DIR}/worker-${worker_index}.txt" 58 | file_contents 59 | ) 60 | if(NOT DEFINED use_png) 61 | set(image_contents "${image_contents}${file_contents}") 62 | else() 63 | string(APPEND image_data ${file_contents}) 64 | endif() 65 | endforeach() 66 | 67 | if(NOT DEFINED use_png) 68 | message("${image_contents}") 69 | else() 70 | # We're slightly abusing this command to replace whitespace from the 71 | # PPM format with semicolons to form a CMake list. 72 | separate_arguments(image_data UNIX_COMMAND "${image_data}") 73 | encode_png(OUTPUT png_bytes WIDTH ${image_width} HEIGHT ${image_height} DATA ${image_data}) 74 | write_binary_file(FILENAME ${use_png} DATA ${png_bytes}) 75 | message(STATUS "Wrote ${use_png}.png") 76 | endif() 77 | 78 | return() 79 | elseif(${worker_index} LESS_EQUAL 0 OR ${worker_index} GREATER ${num_procs}) 80 | message(FATAL_ERROR "worker index ${worker_index} out of bounds") 81 | else() 82 | # We're in a worker process 83 | math(EXPR image_min_y "(${worker_index} - 1) * ${image_height} / ${num_procs}") 84 | math(EXPR image_max_y "${worker_index} * ${image_height} / ${num_procs} - 1") 85 | 86 | math(EXPR image_max_x "${image_width} - 1") 87 | endif() 88 | 89 | set(scale "100000000") 90 | string(LENGTH ${scale} frac_digits) 91 | math(EXPR frac_digits "${frac_digits} - 1") 92 | 93 | function(add a b res) 94 | math(EXPR tmp "(${a}) + (${b})") 95 | set("${res}" "${tmp}" PARENT_SCOPE) 96 | endfunction() 97 | 98 | function(sub a b res) 99 | math(EXPR tmp "(${a}) - (${b})") 100 | set("${res}" "${tmp}" PARENT_SCOPE) 101 | endfunction() 102 | 103 | function(mul a b res) 104 | math(EXPR tmp "((${a}) * (${b})) / ${scale}") 105 | set("${res}" "${tmp}" PARENT_SCOPE) 106 | endfunction() 107 | 108 | function(div a b res) 109 | math(EXPR tmp "((${a}) * (${scale})) / ${b}") 110 | set("${res}" "${tmp}" PARENT_SCOPE) 111 | endfunction() 112 | 113 | function(fract x res) 114 | math(EXPR tmp "${x} % ${scale}") 115 | set(${res} ${tmp} PARENT_SCOPE) 116 | endfunction() 117 | 118 | # TODO: Is a regex quicker here? 119 | function(div_by_10 x res) 120 | string(LENGTH ${x} len) 121 | math(EXPR len "${len} - 1") 122 | string(SUBSTRING ${x} 0 "${len}" tmp) 123 | set("${res}" "${tmp}" PARENT_SCOPE) 124 | endfunction() 125 | 126 | function(div_by_2 x res) 127 | math(EXPR tmp "${x} >> 1") 128 | set("${res}" "${tmp}" PARENT_SCOPE) 129 | endfunction() 130 | 131 | function(mul_by_2 x res) 132 | math(EXPR tmp "${x} << 1") 133 | set("${res}" "${tmp}" PARENT_SCOPE) 134 | endfunction() 135 | 136 | function(truncate x res) 137 | math(EXPR tmp "${x} / ${scale}") 138 | set("${res}" "${tmp}" PARENT_SCOPE) 139 | endfunction() 140 | 141 | function(sqrt x res) 142 | if(${x} LESS 0) 143 | message(FATAL_ERROR "arg passed to square root ${x} was negative") 144 | endif() 145 | 146 | div_by_2(${x} guess) 147 | 148 | foreach(counter RANGE 5) 149 | if(${guess} EQUAL 0) 150 | set("${res}" 0 PARENT_SCOPE) 151 | return() 152 | endif() 153 | 154 | div(${x} ${guess} tmp) 155 | add(${tmp} ${guess} tmp) 156 | div_by_2(${tmp} guess) 157 | endforeach() 158 | 159 | set("${res}" "${guess}" PARENT_SCOPE) 160 | endfunction() 161 | 162 | function(vec3_add x y res) 163 | list(GET ${x} 0 x_0) 164 | list(GET ${x} 1 x_1) 165 | list(GET ${x} 2 x_2) 166 | list(GET ${y} 0 y_0) 167 | list(GET ${y} 1 y_1) 168 | list(GET ${y} 2 y_2) 169 | add(${x_0} ${y_0} z_0) 170 | add(${x_1} ${y_1} z_1) 171 | add(${x_2} ${y_2} z_2) 172 | set("${res}" ${z_0} ${z_1} ${z_2} PARENT_SCOPE) 173 | endfunction() 174 | 175 | function(vec3_sub x y res) 176 | list(GET ${x} 0 x_0) 177 | list(GET ${x} 1 x_1) 178 | list(GET ${x} 2 x_2) 179 | list(GET ${y} 0 y_0) 180 | list(GET ${y} 1 y_1) 181 | list(GET ${y} 2 y_2) 182 | sub(${x_0} ${y_0} z_0) 183 | sub(${x_1} ${y_1} z_1) 184 | sub(${x_2} ${y_2} z_2) 185 | set("${res}" ${z_0} ${z_1} ${z_2} PARENT_SCOPE) 186 | endfunction() 187 | 188 | function(vec3_mul x y res) 189 | list(GET ${x} 0 x_0) 190 | list(GET ${x} 1 x_1) 191 | list(GET ${x} 2 x_2) 192 | list(GET ${y} 0 y_0) 193 | list(GET ${y} 1 y_1) 194 | list(GET ${y} 2 y_2) 195 | mul(${x_0} ${y_0} z_0) 196 | mul(${x_1} ${y_1} z_1) 197 | mul(${x_2} ${y_2} z_2) 198 | set("${res}" ${z_0} ${z_1} ${z_2} PARENT_SCOPE) 199 | endfunction() 200 | 201 | function(vec3_mulf x y res) 202 | list(GET ${x} 0 x_0) 203 | list(GET ${x} 1 x_1) 204 | list(GET ${x} 2 x_2) 205 | mul(${x_0} ${y} z_0) 206 | mul(${x_1} ${y} z_1) 207 | mul(${x_2} ${y} z_2) 208 | set("${res}" ${z_0} ${z_1} ${z_2} PARENT_SCOPE) 209 | endfunction() 210 | 211 | function(vec3_divf x y res) 212 | list(GET ${x} 0 x_0) 213 | list(GET ${x} 1 x_1) 214 | list(GET ${x} 2 x_2) 215 | div(${x_0} ${y} z_0) 216 | div(${x_1} ${y} z_1) 217 | div(${x_2} ${y} z_2) 218 | set("${res}" ${z_0} ${z_1} ${z_2} PARENT_SCOPE) 219 | endfunction() 220 | 221 | function(vec3_dot x y res) 222 | list(GET ${x} 0 x_0) 223 | list(GET ${x} 1 x_1) 224 | list(GET ${x} 2 x_2) 225 | list(GET ${y} 0 y_0) 226 | list(GET ${y} 1 y_1) 227 | list(GET ${y} 2 y_2) 228 | mul(${x_0} ${y_0} z_0) 229 | mul(${x_1} ${y_1} z_1) 230 | mul(${x_2} ${y_2} z_2) 231 | add(${z_0} ${z_1} tmp) 232 | add(${tmp} ${z_2} tmp) 233 | set("${res}" ${tmp} PARENT_SCOPE) 234 | endfunction() 235 | 236 | function(vec3_truncate x res) 237 | list(GET ${x} 0 x_0) 238 | list(GET ${x} 1 x_1) 239 | list(GET ${x} 2 x_2) 240 | truncate(${x_0} z_0) 241 | truncate(${x_1} z_1) 242 | truncate(${x_2} z_2) 243 | set("${res}" ${z_0} ${z_1} ${z_2} PARENT_SCOPE) 244 | endfunction() 245 | 246 | function(vec3_sqrt x res) 247 | list(GET ${x} 0 x_0) 248 | list(GET ${x} 1 x_1) 249 | list(GET ${x} 2 x_2) 250 | sqrt(${x_0} z_0) 251 | sqrt(${x_1} z_1) 252 | sqrt(${x_2} z_2) 253 | set("${res}" ${z_0} ${z_1} ${z_2} PARENT_SCOPE) 254 | endfunction() 255 | 256 | function(vec3_normalize x res) 257 | vec3_dot(${x} ${x} x_2) 258 | rsqrt(${x_2} one_over_length) 259 | vec3_mulf(${x} ${one_over_length} tmp) 260 | set("${res}" ${tmp} PARENT_SCOPE) 261 | endfunction() 262 | 263 | # Convert a number to fixed point representation 264 | function(to_fp x res) 265 | # Basic idea: split into integer and fractional parts, 266 | # multiply both by scale and combine. 267 | string(REPLACE "." ";" both_parts "${x};0") 268 | list(GET both_parts 0 int_part) 269 | list(GET both_parts 1 frac_part) 270 | string(SUBSTRING ${int_part} 0 1 sign) 271 | 272 | string(SUBSTRING ${frac_part} 0 ${frac_digits} frac_part) 273 | string(LENGTH ${frac_part} frac_length) 274 | math(EXPR pad_length "${frac_digits} - ${frac_length}") 275 | string(REPEAT "0" "${pad_length}" padding) 276 | 277 | if(${sign} STREQUAL "-") 278 | math(EXPR tmp "${int_part} * ${scale} - ${frac_part}${padding}") 279 | else() 280 | math(EXPR tmp "${int_part} * ${scale} + ${frac_part}${padding}") 281 | endif() 282 | 283 | set("${res}" "${tmp}" PARENT_SCOPE) 284 | endfunction() 285 | 286 | # Converts from fixed point to normal representation 287 | # Doesn't really need to be fast as we only use it for debugging 288 | function(from_fp x res) 289 | math(EXPR int_part "(${x}) / ${scale}") 290 | if(${int_part} EQUAL 0) 291 | if(${x} GREATER_EQUAL 0) 292 | math(EXPR x "${x} + ${scale}") 293 | else() 294 | set(int_part "-0") 295 | math(EXPR x "${x} - ${scale}") 296 | endif() 297 | endif() 298 | 299 | # Can't just do x % scale, because this does not preserve leading zeroes 300 | string(LENGTH ${x} x_length) 301 | math(EXPR decimal_point_pos "${x_length} - ${frac_digits}") 302 | string(SUBSTRING "${x}" ${decimal_point_pos} ${frac_digits} fract_part) 303 | 304 | set("${res}" "${int_part}.${fract_part}" PARENT_SCOPE) 305 | endfunction() 306 | 307 | to_fp(1.5 three_halves) 308 | 309 | function(rsqrt x res) 310 | if(${x} LESS 0) 311 | message(FATAL_ERROR "arg to inverse square root ${x} was negative") 312 | endif() 313 | 314 | div_by_2(${x} x2) 315 | div(${scale} ${x} guess) # guess = 1/x 316 | 317 | foreach(counter RANGE 5) 318 | mul(${guess} ${guess} tmp) 319 | mul(${tmp} ${x2} tmp) 320 | sub(${three_halves} ${tmp} tmp) 321 | mul(${tmp} ${guess} guess) 322 | endforeach() 323 | 324 | set("${res}" "${guess}" PARENT_SCOPE) 325 | endfunction() 326 | 327 | function(vec3_to_fp x y z res) 328 | to_fp(${x} x) 329 | to_fp(${y} y) 330 | to_fp(${z} z) 331 | set("${res}" ${x} ${y} ${z} PARENT_SCOPE) 332 | endfunction() 333 | 334 | function(vec3_print v) 335 | list(GET ${v} 0 v_0) 336 | list(GET ${v} 1 v_1) 337 | list(GET ${v} 2 v_2) 338 | from_fp(${v_0} v_0) 339 | from_fp(${v_1} v_1) 340 | from_fp(${v_2} v_2) 341 | message("{ ${v_0}, ${v_1}, ${v_2} }") 342 | endfunction() 343 | 344 | function(print x) 345 | from_fp("${x}" tmp) 346 | message(${tmp}) 347 | endfunction() 348 | 349 | function(abs x res) 350 | if(${x} LESS 0) 351 | math(EXPR tmp "-${x}") 352 | set(${res} ${tmp} PARENT_SCOPE) 353 | else() 354 | set(${res} ${x} PARENT_SCOPE) 355 | endif() 356 | endfunction() 357 | 358 | function(clamp_0_1 x res) 359 | if(${x} GREATER ${scale}) 360 | set("${res}" ${scale} PARENT_SCOPE) 361 | elseif(${x} LESS 0) 362 | set("${res}" 0 PARENT_SCOPE) 363 | else() 364 | set("${res}" ${x} PARENT_SCOPE) 365 | endif() 366 | endfunction() 367 | 368 | function(vec3_clamp_0_1 x res) 369 | list(GET ${x} 0 x_0) 370 | list(GET ${x} 1 x_1) 371 | list(GET ${x} 2 x_2) 372 | clamp_0_1(${x_0} z_0) 373 | clamp_0_1(${x_1} z_1) 374 | clamp_0_1(${x_2} z_2) 375 | set("${res}" ${z_0} ${z_1} ${z_2} PARENT_SCOPE) 376 | endfunction() 377 | 378 | function(sphere_intersect ray_origin ray_dir hit_t hit_point hit_normal) 379 | vec3_sub(${ray_origin} sphere_center oc) 380 | vec3_dot(${ray_dir} ${ray_dir} a) 381 | vec3_dot(oc ${ray_dir} half_b) 382 | vec3_dot(oc oc oc_2) 383 | mul(${sphere_radius} ${sphere_radius} radius_2) 384 | sub(${oc_2} ${radius_2} c) 385 | 386 | mul(${half_b} ${half_b} half_b_2) 387 | mul(${a} ${c} ac) 388 | sub(${half_b_2} ${ac} discrim) 389 | 390 | if(${discrim} GREATER 0) 391 | sqrt(${discrim} root) 392 | sub(0 ${half_b} minus_half_b) 393 | 394 | sub(${minus_half_b} ${root} t) 395 | div(${t} ${a} t) 396 | if(${t} GREATER 0) 397 | # p = o + t * d 398 | vec3_mulf(${ray_dir} ${t} tv) 399 | vec3_add(${ray_origin} tv point) 400 | vec3_sub(point sphere_center normal) 401 | vec3_divf(normal ${sphere_radius} unit_normal) 402 | set(${hit_point} ${point} PARENT_SCOPE) 403 | set(${hit_normal} ${unit_normal} PARENT_SCOPE) 404 | set(${hit_t} ${t} PARENT_SCOPE) 405 | return() 406 | endif() 407 | 408 | add(${minus_half_b} ${root} t) 409 | div(${t} ${a} t) 410 | if (${t} GREATER 0) 411 | # p = o + t * d 412 | vec3_mulf(${ray_dir} ${t} tv) 413 | vec3_add(${ray_origin} tv point) 414 | vec3_sub(point sphere_center normal) 415 | vec3_divf(normal ${sphere_radius} unit_normal) 416 | set(${hit_point} ${point} PARENT_SCOPE) 417 | set(${hit_normal} ${unit_normal} PARENT_SCOPE) 418 | set(${hit_t} ${t} PARENT_SCOPE) 419 | return() 420 | endif() 421 | endif() 422 | 423 | set(${hit_t} -1 PARENT_SCOPE) 424 | endfunction() 425 | 426 | function(plane_intersect ray_origin ray_dir hit_t hit_point hit_normal) 427 | list(GET ${ray_dir} 1 ray_d_y) 428 | if(${ray_d_y} EQUAL 0) 429 | set(${hit_t} -1 PARENT_SCOPE) 430 | else() 431 | # t = (c - o.y) / d.y 432 | list(GET ${ray_origin} 1 ray_o_y) 433 | sub(${plane_y} ${ray_o_y} ray_y_dist) 434 | div(${ray_y_dist} ${ray_d_y} t) 435 | if(${t} GREATER 0 AND ${t} LESS 2000000000) 436 | vec3_mulf(${ray_dir} ${t} ray_scaled_d) 437 | vec3_add(${ray_origin} ray_scaled_d point) 438 | set(${hit_t} ${t} PARENT_SCOPE) 439 | set(${hit_point} ${point} PARENT_SCOPE) 440 | set(${hit_normal} 0 ${scale} 0 PARENT_SCOPE) 441 | else() 442 | set(${hit_t} -1 PARENT_SCOPE) 443 | endif() 444 | endif() 445 | endfunction() 446 | 447 | function(offset_origin ray_origin hit_norm out_origin) 448 | vec3_mulf(${hit_norm} ${ray_epsilon} scaled_norm) 449 | vec3_add(scaled_norm ${ray_origin} origin) 450 | set(${out_origin} ${origin} PARENT_SCOPE) 451 | endfunction() 452 | 453 | # Doesn't account for shadowing, this is faked 454 | function(light_contrib point norm light_pos light_col out_col) 455 | vec3_sub(${light_pos} ${point} l) 456 | vec3_normalize(l lnorm) 457 | vec3_dot(${norm} lnorm ndotl) 458 | 459 | if(${ndotl} LESS 0) 460 | set(${out_col} 0 0 0 PARENT_SCOPE) 461 | else() 462 | vec3_mulf(${light_col} ${ndotl} unscaled_out) 463 | vec3_dot(l l l2) 464 | vec3_divf(unscaled_out ${l2} out) 465 | set(${out_col} ${out} PARENT_SCOPE) 466 | endif() 467 | endfunction() 468 | 469 | # Ray dir must be normalized 470 | function(trace ray_origin ray_dir depth color) 471 | if(${depth} GREATER_EQUAL 3) 472 | return() 473 | else() 474 | math(EXPR depth "${depth} + 1") 475 | endif() 476 | 477 | sphere_intersect(${ray_origin} ${ray_dir} hit_t_1 hit_point_1 hit_normal_1) 478 | plane_intersect(${ray_origin} ${ray_dir} hit_t_2 hit_point_2 hit_normal_2) 479 | if(${hit_t_1} GREATER ${ray_epsilon}) 480 | # specular reflection 481 | offset_origin(hit_point_1 hit_normal_1 new_origin) 482 | 483 | # reflect 484 | vec3_dot(hit_normal_1 ${ray_dir} scalar) 485 | mul_by_2(${scalar} scalar) 486 | vec3_mulf(hit_normal_1 ${scalar} refl_a) 487 | vec3_sub(${ray_dir} refl_a new_dir) 488 | 489 | trace(new_origin new_dir ${depth} traced_col) 490 | 491 | set(col 0 0 0) 492 | light_contrib(hit_point_1 hit_normal_1 light1_pos light1_col out_col1) 493 | light_contrib(hit_point_1 hit_normal_1 light2_pos light2_col out_col2) 494 | vec3_add(col out_col1 col) 495 | vec3_add(col out_col2 col) 496 | vec3_add(col traced_col col) 497 | 498 | set(base_col ${sphere_color}) 499 | vec3_mul(base_col col col) 500 | 501 | elseif(${hit_t_2} GREATER ${ray_epsilon}) 502 | set(light_col 0 0 0) 503 | 504 | list(GET hit_point_2 0 hit_p_x) 505 | list(GET hit_point_2 2 hit_p_z) 506 | 507 | # Use equation of a circle to fake shadow 508 | sub(${hit_p_z} ${shadow_center} shadow_offset_z) 509 | mul(${hit_p_x} ${hit_p_x} shadow_offset_x_2) 510 | mul(${shadow_offset_z} ${shadow_offset_z} shadow_offset_z_2) 511 | add(${shadow_offset_x_2} ${shadow_offset_z_2} hit_dist_2) 512 | 513 | light_contrib(hit_point_2 hit_normal_2 light1_pos light1_col out_col1) 514 | light_contrib(hit_point_2 hit_normal_2 light2_pos light2_col out_col2) 515 | vec3_add(light_col out_col1 light_col) 516 | vec3_add(light_col out_col2 light_col) 517 | if((${hit_dist_2} LESS ${shadow_radius2}) AND (${depth} LESS 2)) 518 | vec3_mulf(light_col ${tenth} light_col) 519 | endif() 520 | 521 | # Calculate checkerboard pattern 522 | # TODO: Is there a better way? 523 | math(EXPR half "${scale} / 2") 524 | math(EXPR hit_p_x "${hit_p_x} % ${scale}") 525 | math(EXPR hit_p_z "${hit_p_z} % ${scale}") 526 | 527 | # CMake modulo yields negative values for negative arguments, fortunately it is easy to fix. 528 | if(${hit_p_x} LESS 0) 529 | add(${hit_p_x} ${scale} hit_p_x) 530 | endif() 531 | if(${hit_p_z} LESS 0) 532 | add(${hit_p_z} ${scale} hit_p_z) 533 | endif() 534 | 535 | if((${hit_p_x} GREATER ${half}) AND (${hit_p_z} GREATER ${half})) 536 | set(base_col ${plane_color_1}) 537 | elseif((${hit_p_x} LESS ${half}) AND (${hit_p_z} LESS ${half})) 538 | set(base_col ${plane_color_1}) 539 | else() 540 | set(base_col ${plane_color_2}) 541 | endif() 542 | 543 | vec3_mul(light_col base_col col) 544 | else() 545 | set(col 0 0 0) 546 | endif() 547 | 548 | set("${color}" ${col} PARENT_SCOPE) 549 | endfunction() 550 | 551 | to_fp(255.99 rgb_scaling) 552 | to_fp(0.5 half) 553 | to_fp(0.1 tenth) 554 | to_fp(${image_width} image_width_fp) 555 | to_fp(${image_height} image_height_fp) 556 | to_fp(0.01 ray_epsilon) 557 | 558 | to_fp(2 sphere_radius) 559 | vec3_to_fp(0.0 0.0 3.0 sphere_center) 560 | vec3_to_fp(0.3 0.3 0.3 sphere_color) 561 | 562 | to_fp(3.0 shadow_center) 563 | to_fp(4 shadow_radius2) 564 | 565 | to_fp(-2 plane_y) 566 | vec3_to_fp(0.6 0.6 0.6 plane_color_1) 567 | vec3_to_fp(0.1 0.1 0.1 plane_color_2) 568 | 569 | vec3_to_fp(-2 4 1 light1_pos) 570 | vec3_to_fp(20 3 3 light1_col) 571 | 572 | vec3_to_fp(2 4 1 light2_pos) 573 | vec3_to_fp(3 20 3 light2_col) 574 | 575 | file(REMOVE "${CMAKE_CURRENT_BINARY_DIR}/worker-${worker_index}.txt") 576 | 577 | foreach(y RANGE ${image_min_y} ${image_max_y}) 578 | set(row "") 579 | to_fp(${y} y_fp) 580 | div(${y_fp} ${image_height_fp} v) 581 | 582 | mul_by_2(${v} v2) 583 | sub(${scale} ${v2} ray_dir_y) 584 | 585 | foreach(x RANGE ${image_max_x}) 586 | to_fp(${x} x_fp) 587 | set(rgb 0 0 0) 588 | 589 | div(${x_fp} ${image_width_fp} u) 590 | 591 | mul_by_2(${u} u2) 592 | sub(${u2} ${scale} ray_dir_x) 593 | 594 | set(ray_dir ${ray_dir_x} ${ray_dir_y} ${scale}) 595 | set(d ${ray_dir}) 596 | 597 | set(o 0 0 0) 598 | trace(o d 0 rgb) 599 | 600 | vec3_clamp_0_1(rgb rgb) 601 | 602 | vec3_sqrt(rgb rgb) # approx gamma correction (1/2 \approx 1/2.2) 603 | vec3_mulf(rgb ${rgb_scaling} rgb) 604 | vec3_truncate(rgb rgb) # shitty tonemap 605 | 606 | list(GET rgb 0 r) 607 | list(GET rgb 1 g) 608 | list(GET rgb 2 b) 609 | set(row "${row} ${r} ${g} ${b}") 610 | endforeach() 611 | 612 | file(APPEND 613 | "${CMAKE_CURRENT_BINARY_DIR}/worker-${worker_index}.txt" 614 | "${row}\n" 615 | ) 616 | endforeach() 617 | --------------------------------------------------------------------------------