├── .gitignore ├── .idea ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── misc.xml ├── uiDesigner.xml └── vcs.xml ├── Makefile ├── README.md ├── art └── screenshot.png ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main └── kotlin ├── Image.kt ├── Video.kt ├── geometry ├── Point3F.kt └── Utils.kt ├── materials ├── CheckeredMaterial.kt ├── Material.kt └── UniformMaterial.kt ├── objects ├── Object.kt ├── Plane.kt └── Sphere.kt └── scene ├── Camera.kt ├── Scene.kt ├── Screen.kt └── lights ├── Light.kt ├── LightDisc.kt └── LightPoint.kt /.gitignore: -------------------------------------------------------------------------------- 1 | output/* 2 | .DS_Store 3 | .idea/shelf 4 | /confluence/target 5 | /dependencies/repo 6 | /android.tests.dependencies 7 | /dependencies/android.tests.dependencies 8 | /dist 9 | /local 10 | /gh-pages 11 | /ideaSDK 12 | /clionSDK 13 | /android-studio/sdk 14 | out/ 15 | /tmp 16 | workspace.xml 17 | *.versionsBackup 18 | /idea/testData/debugger/tinyApp/classes* 19 | /jps-plugin/testData/kannotator 20 | /js/js.translator/testData/out/ 21 | /js/js.translator/testData/out-min/ 22 | /js/js.translator/testData/out-pir/ 23 | .gradle/ 24 | build/ 25 | !**/src/**/build 26 | !**/test/**/build 27 | *.iml 28 | !**/testData/**/*.iml 29 | .idea/libraries/Gradle*.xml 30 | .idea/libraries/Maven*.xml 31 | .idea/artifacts/PILL_*.xml 32 | .idea/artifacts/KotlinPlugin.xml 33 | .idea/modules 34 | .idea/runConfigurations/JPS_*.xml 35 | .idea/runConfigurations/PILL_*.xml 36 | .idea/libraries 37 | .idea/modules.xml 38 | .idea/gradle.xml 39 | .idea/compiler.xml 40 | .idea/inspectionProfiles/profiles_settings.xml 41 | .idea/.name 42 | .idea/artifacts/dist_auto_* 43 | .idea/artifacts/dist.xml 44 | .idea/artifacts/ideaPlugin.xml 45 | .idea/artifacts/kotlinc.xml 46 | .idea/artifacts/kotlin_compiler_jar.xml 47 | .idea/artifacts/kotlin_plugin_jar.xml 48 | .idea/artifacts/kotlin_jps_plugin_jar.xml 49 | .idea/artifacts/kotlin_daemon_client_jar.xml 50 | .idea/artifacts/kotlin_imports_dumper_compiler_plugin_jar.xml 51 | .idea/artifacts/kotlin_main_kts_jar.xml 52 | .idea/artifacts/kotlin_compiler_client_embeddable_jar.xml 53 | .idea/artifacts/kotlin_reflect_jar.xml 54 | .idea/artifacts/kotlin_stdlib_js_ir_* 55 | .idea/artifacts/kotlin_test_js_ir_* 56 | .idea/artifacts/kotlin_stdlib_wasm_* 57 | .idea/jarRepositories.xml 58 | kotlin-ultimate/ 59 | node_modules/ 60 | .rpt2_cache/ 61 | libraries/tools/kotlin-test-js-runner/lib/ 62 | libraries/tools/kotlin-source-map-loader/lib/ 63 | local.properties 64 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/uiDesigner.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | ffmpeg -framerate 30 -i $(in)/image_%d.png -c copy $(in)/muxed.mkv 3 | ffmpeg -i $(in)/muxed.mkv \ 4 | -c:v libx264 -crf 20 -profile:v main -pix_fmt yuv420p \ 5 | -c:a aac -ac 2 -b:a 128k \ 6 | -movflags faststart \ 7 | $(in)/final.mp4 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ray Tracer 2 | 3 | ```kotlin 4 | Scene( 5 | camera = Camera( 6 | position = Point3F(-1f, 1f, 1f), 7 | lookAt = Point3F(0f, 0.3f, -1f), 8 | planeNormal = Point3F.yUnit(), 9 | width = 2560, 10 | height = 1440 11 | ), 12 | lights = listOf( 13 | LightDisc(center = Point3F(10f, 10f, 10f), radius = 5f, towards = Point3F.origin()) 14 | ), 15 | objects = listOf( 16 | Sphere(Point3F(0.6f, 0.35f, -0.6f), 0.35f, UniformMaterial(Point3F(0.5f, 0f, 0.5f))), 17 | Sphere(Point3F(-0.8f, 0.2f, -0.7f), 0.2f, UniformMaterial(Point3F(0f, 0.6f, 0f))), 18 | Sphere(Point3F(0f, 0.3f, -1f), 0.3f, UniformMaterial(Point3F(0.5f, 0f, 0f))), 19 | Plane(Point3F.origin(), Point3F.yUnit(), CheckeredMaterial()) 20 | ) 21 | ).renderToFile("output/images/${System.currentTimeMillis()}.png") 22 | ``` 23 | 24 | ![image](art/screenshot.png) 25 | 26 | # Build Animation 27 | 28 | 1- Execute `Video.kt` 29 | 30 | 2- Mux images (`ffmpeg` required): `make in=output/animation` 31 | 32 | [![Watch the video](https://img.youtube.com/vi/klVCeTXNX2M/hqdefault.jpg)](https://youtu.be/klVCeTXNX2M) -------------------------------------------------------------------------------- /art/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omaraflak/RayTracer-Kotlin/ef8b4c8bc3a9514882917b1ec354419662998da4/art/screenshot.png -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.jetbrains.kotlin.jvm' version '1.3.21' 3 | } 4 | 5 | version '1.0-SNAPSHOT' 6 | 7 | repositories { 8 | mavenCentral() 9 | } 10 | 11 | dependencies { 12 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" 13 | implementation "io.reactivex.rxjava3:rxkotlin:3.0.0" 14 | } 15 | 16 | compileKotlin { 17 | kotlinOptions.jvmTarget = "1.8" 18 | } 19 | compileTestKotlin { 20 | kotlinOptions.jvmTarget = "1.8" 21 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omaraflak/RayTracer-Kotlin/ef8b4c8bc3a9514882917b1ec354419662998da4/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'raytrace' 2 | 3 | -------------------------------------------------------------------------------- /src/main/kotlin/Image.kt: -------------------------------------------------------------------------------- 1 | import geometry.Point3F 2 | import materials.CheckeredMaterial 3 | import materials.UniformMaterial 4 | import objects.Plane 5 | import objects.Sphere 6 | import scene.Camera 7 | import scene.Scene 8 | import scene.lights.LightPoint 9 | 10 | fun main() { 11 | createScene().renderToFile("output/images/${System.currentTimeMillis()}.png") { 12 | println("$it%") 13 | } 14 | } 15 | 16 | fun createScene(): Scene { 17 | return Scene( 18 | camera = Camera( 19 | position = Point3F(-1f, 1f, 1f), 20 | lookAt = Point3F(0f, 0.3f, -1f), 21 | planeNormal = Point3F.yUnit(), 22 | width = 1280, 23 | height = 720 24 | ), 25 | lights = listOf( 26 | LightPoint(Point3F(-5f, 5f, -1.5f)), 27 | LightPoint(Point3F(5f, 5f, 5f), diffuseIntensity = Point3F(0.5f)) 28 | ), 29 | objects = listOf( 30 | Sphere(Point3F(0.6f, 0.35f, -0.6f), 0.35f, UniformMaterial(Point3F(0.5f, 0f, 0.5f))), 31 | Sphere(Point3F(-0.8f, 0.2f, -0.7f), 0.2f, UniformMaterial(Point3F(0f, 0.6f, 0f))), 32 | Sphere(Point3F(0f, 0.3f, -1f), 0.3f, UniformMaterial(Point3F(0.5f, 0f, 0f))), 33 | Plane(Point3F.origin(), Point3F.yUnit(), CheckeredMaterial()) 34 | ) 35 | ) 36 | } -------------------------------------------------------------------------------- /src/main/kotlin/Video.kt: -------------------------------------------------------------------------------- 1 | import geometry.Point3F 2 | import io.reactivex.rxjava3.core.Observable 3 | import io.reactivex.rxjava3.schedulers.Schedulers 4 | import scene.Scene 5 | import java.io.File 6 | import javax.imageio.ImageIO 7 | 8 | fun main() { 9 | "output/animation".also { 10 | File(it).mkdirs() 11 | createAnimation(it) { current, total -> 12 | println("$current/$total") 13 | } 14 | } 15 | } 16 | 17 | fun createAnimation(folder: String, onProgress: ((Int, Int) -> Unit)? = null) { 18 | var processed = 0 19 | val origin = Point3F(0.6f, 0.35f, -0.6f) 20 | val axis = Point3F.yUnit() 21 | 22 | Observable.range(0, 360) 23 | .flatMap { 24 | val scene = createScene() 25 | scene.camera.updatePosition(scene.camera.position.rotate(it.toFloat(), axis, origin)) 26 | Observable.fromCallable { 27 | Pair(it, Scene.toBufferedImage(scene.render())) 28 | }.subscribeOn(Schedulers.computation()) 29 | } 30 | .observeOn(Schedulers.io()) 31 | .blockingSubscribe { 32 | val file = File("$folder/image_${it.first}.png") 33 | ImageIO.write(it.second, "png", file) 34 | onProgress?.invoke(++processed, 360) 35 | } 36 | } -------------------------------------------------------------------------------- /src/main/kotlin/geometry/Point3F.kt: -------------------------------------------------------------------------------- 1 | package geometry 2 | 3 | import kotlin.math.* 4 | 5 | class Point3F(var x: Float = 0f, var y: Float = 0f, var z: Float = 0f) { 6 | constructor(p: Point3F) : this() { 7 | set(p) 8 | } 9 | 10 | constructor(n: Float) : this() { 11 | set(n, n, n) 12 | } 13 | 14 | fun set(p: Point3F) { 15 | x = p.x 16 | y = p.y 17 | z = p.z 18 | } 19 | 20 | fun set(x: Float = this.x, y: Float = this.y, z: Float = this.z) { 21 | this.x = x 22 | this.y = y 23 | this.z = z 24 | } 25 | 26 | operator fun plus(p: Point3F): Point3F { 27 | return Point3F(x + p.x, y + p.y, z + p.z) 28 | } 29 | 30 | operator fun unaryMinus(): Point3F { 31 | return Point3F(-x, -y, -z) 32 | } 33 | 34 | operator fun minus(p: Point3F): Point3F { 35 | return this + (-p) 36 | } 37 | 38 | operator fun times(p: Point3F): Point3F { 39 | return Point3F(x * p.x, y * p.y, z * p.z) 40 | } 41 | 42 | operator fun times(f: Float): Point3F { 43 | return Point3F(x * f, y * f, z * f) 44 | } 45 | 46 | fun dot(p: Point3F): Float { 47 | return x * p.x + y * p.y + z * p.z 48 | } 49 | 50 | fun cross(p: Point3F): Point3F { 51 | return Point3F( 52 | y * p.z - z * p.y, 53 | z * p.x - x * p.z, 54 | x * p.y - y * p.x 55 | ) 56 | } 57 | 58 | fun reflection(p: Point3F): Point3F { 59 | return this - 2 * this.dot(p) * p 60 | } 61 | 62 | fun length(): Float { 63 | return sqrt(x * x + y * y + z * z) 64 | } 65 | 66 | fun normalized(): Point3F { 67 | return this * (1f / length()) 68 | } 69 | 70 | fun rotate(angle: Float, axis: Point3F): Point3F { 71 | val r = angle * PI / 180 72 | val cosR = cos(r) 73 | val sinR = sin(r) 74 | return Point3F( 75 | ((cosR + axis.x.pow(2) * (1 - cosR)) * x + (axis.x * axis.y * (1 - cosR) - axis.z * sinR) * y + (axis.x * axis.z * (1 - cosR) + axis.y * sinR) * z).toFloat(), 76 | ((axis.y * axis.x * (1 - cosR) + axis.z * sinR) * x + (cosR + axis.y.pow(2) * (1 - cosR)) * y + (axis.y * axis.z * (1 - cosR) - axis.x * sinR) * z).toFloat(), 77 | ((axis.z * axis.x * (1 - cosR) - axis.y * sinR) * x + (axis.z * axis.y * (1 - cosR) + axis.x * sinR) * y + (cosR + axis.z.pow(2) * (1 - cosR)) * z).toFloat() 78 | ) 79 | } 80 | 81 | fun rotate(angle: Float, axis: Point3F, origin: Point3F = origin()): Point3F { 82 | return (this - origin).rotate(angle, axis) + origin 83 | } 84 | 85 | override fun toString(): String { 86 | return "($x, $y, $z)" 87 | } 88 | 89 | companion object { 90 | fun origin(): Point3F = Point3F() 91 | fun ones(): Point3F = Point3F(1f, 1f, 1f) 92 | fun xUnit(): Point3F = Point3F(1f, 0f, 0f) 93 | fun yUnit(): Point3F = Point3F(0f, 1f, 0f) 94 | fun zUnit(): Point3F = Point3F(0f, 0f, 1f) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/kotlin/geometry/Utils.kt: -------------------------------------------------------------------------------- 1 | package geometry 2 | 3 | operator fun Float.times(p: Point3F): Point3F { 4 | return p.times(this) 5 | } 6 | -------------------------------------------------------------------------------- /src/main/kotlin/materials/CheckeredMaterial.kt: -------------------------------------------------------------------------------- 1 | package materials 2 | 3 | import geometry.Point3F 4 | import geometry.times 5 | import scene.lights.Light 6 | 7 | class CheckeredMaterial( 8 | private val uniformMaterial1: UniformMaterial = UniformMaterial(Point3F()), 9 | private val uniformMaterial2: UniformMaterial = UniformMaterial(Point3F(0.6f)), 10 | private val spaceNormal: Point3F = Point3F(0f, 1f, 0f), 11 | private val squareSize: Float = 0.2f 12 | ): Material { 13 | private val u = Point3F() 14 | private val v = Point3F() 15 | 16 | init { 17 | spaceNormal.set(spaceNormal.normalized()) 18 | u.set(Point3F(0f, spaceNormal.z, -spaceNormal.y).normalized()) 19 | v.set(u.cross(spaceNormal).normalized()) 20 | } 21 | 22 | private fun getMaterialOnSurface(point: Point3F): UniformMaterial { 23 | val projected = point - (point - this.spaceNormal).dot(this.spaceNormal) * this.spaceNormal 24 | val distance = (point - projected).length() 25 | projected.set(projected + distance * u) 26 | 27 | val tmp = (projected - this.spaceNormal) 28 | val a = tmp.dot(u) 29 | val b = tmp.dot(v) 30 | 31 | val i = (a / squareSize).run { if (this < 0) this - 1 else this }.toInt() 32 | val j = (b / squareSize).run { if (this < 0) this - 1 else this }.toInt() 33 | 34 | if ((i + j % 2) % 2 == 0) { 35 | return uniformMaterial1 36 | } 37 | return uniformMaterial2 38 | } 39 | 40 | override fun getColor(point: Point3F, normal: Point3F, toLight: Point3F, toCamera: Point3F, light: Light): Point3F { 41 | return getMaterialOnSurface(point).getColor(point, normal, toLight, toCamera, light) 42 | } 43 | 44 | override fun getReflectivity(point: Point3F): Float { 45 | return getMaterialOnSurface(point).getReflectivity(point) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/kotlin/materials/Material.kt: -------------------------------------------------------------------------------- 1 | package materials 2 | 3 | import geometry.Point3F 4 | import scene.lights.Light 5 | 6 | interface Material { 7 | fun getColor(point: Point3F, normal: Point3F, toLight: Point3F, toCamera: Point3F, light: Light): Point3F 8 | fun getReflectivity(point: Point3F): Float 9 | } -------------------------------------------------------------------------------- /src/main/kotlin/materials/UniformMaterial.kt: -------------------------------------------------------------------------------- 1 | package materials 2 | 3 | import geometry.Point3F 4 | import scene.lights.Light 5 | import kotlin.math.max 6 | import kotlin.math.pow 7 | 8 | class UniformMaterial( 9 | private val diffuse: Point3F, 10 | private val ambient: Point3F = diffuse * 0.1f, 11 | private val specular: Point3F = Point3F.ones(), 12 | private val shininess: Float = 100f, 13 | private val reflectivity: Float = 0.5f 14 | ): Material { 15 | private val color = Point3F() 16 | 17 | override fun getColor(point: Point3F, normal: Point3F, toLight: Point3F, toCamera: Point3F, light: Light): Point3F { 18 | color.set(0f, 0f, 0f) 19 | val diffuseFactor = toLight.dot(normal) 20 | if (diffuseFactor < 0) 21 | return color 22 | 23 | // ambient 24 | color.set(color + ambient * light.ambientIntensity) 25 | 26 | // diffuse 27 | color.set(color + diffuse * light.diffuseIntensity * max(0f, diffuseFactor)) 28 | 29 | // specular 30 | val h = (toCamera + toLight).normalized() 31 | color.set(color + specular * light.specularIntensity * max(0f, h.dot(normal).pow(shininess / 4f))) 32 | return color 33 | } 34 | 35 | override fun getReflectivity(point: Point3F): Float { 36 | return reflectivity 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/kotlin/objects/Object.kt: -------------------------------------------------------------------------------- 1 | package objects 2 | 3 | import geometry.Point3F 4 | import materials.Material 5 | import scene.Scene 6 | 7 | open class Object(val position: Point3F, val material: Material) { 8 | open fun intersect(origin: Point3F, direction: Point3F): Float? { 9 | throw NotImplementedError() 10 | } 11 | 12 | open fun getNormalVectorSurface(point: Point3F): Point3F { 13 | throw NotImplementedError() 14 | } 15 | 16 | fun getLightColor(origin: Point3F, direction: Point3F, distance: Float, scene: Scene): Point3F { 17 | val intersection = origin + direction * distance 18 | val toCameraUnit = -direction 19 | val normalToSurface = getNormalVectorSurface(intersection) 20 | var color = Point3F() 21 | 22 | scene.lights.forEach { light -> 23 | light.pointsVisibleTo(intersection).forEach { lightPoint -> 24 | val toLightUnit = (lightPoint - intersection).normalized() 25 | if (!scene.objects.any { it.intersect(intersection, toLightUnit) != null }) { 26 | color += material.getColor(intersection, normalToSurface, toLightUnit, toCameraUnit, light) 27 | } 28 | } 29 | } 30 | 31 | return color.apply { 32 | x = x.coerceIn(0f, 1f) 33 | y = y.coerceIn(0f, 1f) 34 | z = z.coerceIn(0f, 1f) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/kotlin/objects/Plane.kt: -------------------------------------------------------------------------------- 1 | package objects 2 | 3 | import geometry.Point3F 4 | import materials.Material 5 | import kotlin.math.absoluteValue 6 | 7 | class Plane( 8 | private val point: Point3F, 9 | private val normal: Point3F, 10 | material: Material 11 | ): Object(point, material) { 12 | override fun getNormalVectorSurface(point: Point3F): Point3F { 13 | return normal 14 | } 15 | 16 | override fun intersect(origin: Point3F, direction: Point3F): Float? { 17 | if (normal.dot(direction).absoluteValue < 1e-6) { 18 | return null 19 | } 20 | 21 | val t = normal.dot(point - origin) / normal.dot(direction) 22 | if (t > 1e-4) { 23 | return t 24 | } 25 | 26 | return null 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/kotlin/objects/Sphere.kt: -------------------------------------------------------------------------------- 1 | package objects 2 | 3 | import geometry.Point3F 4 | import materials.Material 5 | import kotlin.math.max 6 | import kotlin.math.min 7 | import kotlin.math.pow 8 | import kotlin.math.sqrt 9 | 10 | class Sphere( 11 | private val center: Point3F, 12 | private val radius: Float, 13 | material: Material 14 | ): Object(center, material) { 15 | override fun getNormalVectorSurface(point: Point3F): Point3F { 16 | return (point - center).normalized() 17 | } 18 | 19 | override fun intersect(origin: Point3F, direction: Point3F): Float? { 20 | val b = direction.dot((origin - center) * 2f) 21 | val c = (origin - center).length().pow(2) - radius.pow(2) 22 | val delta = b.pow(2) - 4 * c 23 | if (delta > 0) { 24 | val t1 = (-b + sqrt(delta)) / 2f 25 | val t2 = (-b - sqrt(delta)) / 2f 26 | val t = min(max(0f, t1), max(0f, t2)) 27 | if (t > 0) { 28 | return t 29 | } 30 | } 31 | return null 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/kotlin/scene/Camera.kt: -------------------------------------------------------------------------------- 1 | package scene 2 | 3 | import geometry.Point3F 4 | 5 | class Camera( 6 | val position: Point3F, 7 | private val lookAt: Point3F, 8 | private val planeNormal: Point3F, 9 | private val distanceToScreen: Float = 1f, 10 | private val width: Int, 11 | private val height: Int 12 | ) { 13 | lateinit var screen: Screen 14 | 15 | init { 16 | updateScreen() 17 | } 18 | 19 | fun updatePosition(newPosition: Point3F) { 20 | position.set(newPosition) 21 | updateScreen() 22 | } 23 | 24 | private fun updateScreen() { 25 | val diff = lookAt - position 26 | val tmp = diff.cross(planeNormal) 27 | 28 | val z = 1f 29 | val y = -z * (diff.x * tmp.z - tmp.x * diff.z) / (diff.x * tmp.y - tmp.x * diff.y) 30 | val x = (-tmp.y * y - tmp.z * z) / tmp.x 31 | val dirTop = Point3F(x, y, z).normalized() 32 | if (dirTop.dot(planeNormal) < 0) { 33 | dirTop.set(dirTop * -1f) 34 | } 35 | 36 | val ratio = width / height.toFloat() 37 | val screenCenter = position + (lookAt - position).normalized() * distanceToScreen 38 | val scaledLeft = dirTop.cross(lookAt - position).normalized() 39 | val scaledTop = dirTop * ( 1f / ratio) 40 | screen = Screen( 41 | topLeft = screenCenter + scaledLeft + scaledTop, 42 | topRight = screenCenter - scaledLeft + scaledTop, 43 | bottomLeft = screenCenter + scaledLeft - scaledTop, 44 | width = width, 45 | height = height 46 | ) 47 | } 48 | } -------------------------------------------------------------------------------- /src/main/kotlin/scene/Scene.kt: -------------------------------------------------------------------------------- 1 | package scene 2 | 3 | import geometry.Point3F 4 | import geometry.times 5 | import objects.Object 6 | import scene.lights.Light 7 | import java.awt.image.BufferedImage 8 | import java.io.File 9 | import javax.imageio.ImageIO 10 | 11 | class Scene( 12 | val camera: Camera, 13 | val lights: List, 14 | val objects: List 15 | ) { 16 | fun render(onProgress: ((Int) -> Unit)? = null): List> { 17 | val screen = camera.screen 18 | val image = List(screen.height) { 19 | List(screen.width) { 20 | Point3F() 21 | } 22 | } 23 | 24 | val xVector = (screen.topRight - screen.topLeft) * (1f / screen.width) 25 | val yVector = (screen.bottomLeft - screen.topLeft) * (1f / screen.height) 26 | 27 | for (y in 0 until screen.height) { 28 | for (x in 0 until screen.width) { 29 | val pixel = screen.topLeft + xVector * x.toFloat() + yVector * y.toFloat() 30 | val direction = (pixel - camera.position).normalized() 31 | val origin = Point3F(camera.position) 32 | image[y][x].set(traceRay(origin, direction)) 33 | } 34 | onProgress?.invoke((y + 1) * 100 / screen.height) 35 | } 36 | 37 | return image 38 | } 39 | 40 | private fun traceRay(origin: Point3F, direction: Point3F, maxDepth: Int = 3): Point3F { 41 | val color = Point3F() 42 | var reflection = 1f 43 | 44 | for (k in 0..maxDepth) { 45 | var nearestObject: Object? = null 46 | var minDistance = Float.MAX_VALUE 47 | objects.forEach { obj -> 48 | obj.intersect(origin, direction)?.let { 49 | if (it < minDistance) { 50 | minDistance = it 51 | nearestObject = obj 52 | } 53 | } 54 | } 55 | 56 | nearestObject?.let { 57 | val intersection = origin + minDistance * direction 58 | val normal = it.getNormalVectorSurface(intersection) 59 | color.set(color + reflection * it.getLightColor(origin, direction, minDistance, this)) 60 | origin.set(intersection + 1e-3f * normal) 61 | direction.set(direction.reflection(normal)) 62 | reflection *= it.material.getReflectivity(intersection) 63 | } ?: break 64 | } 65 | 66 | return color.apply { 67 | x = x.coerceIn(0f, 1f) 68 | y = y.coerceIn(0f, 1f) 69 | z = z.coerceIn(0f, 1f) 70 | } 71 | } 72 | 73 | fun renderToFile(filepath: String, onProgress: ((Int) -> Unit)? = null) { 74 | val image = render(onProgress) 75 | val buffer = toBufferedImage(image) 76 | val file = File(filepath) 77 | ImageIO.write(buffer, "png", file) 78 | } 79 | 80 | companion object { 81 | fun toBufferedImage(image: List>): BufferedImage { 82 | val height = image.size 83 | val width = image.first().size 84 | return BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR).also { 85 | image.forEachIndexed { y, list -> 86 | list.forEachIndexed { x, p -> 87 | val red = (p.x * 255).toInt() 88 | val green = (p.y * 255).toInt() 89 | val blue = (p.z * 255).toInt() 90 | val color = (red shl 16) or (green shl 8) or blue 91 | it.setRGB(x, y, color) 92 | } 93 | } 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/kotlin/scene/Screen.kt: -------------------------------------------------------------------------------- 1 | package scene 2 | 3 | import geometry.Point3F 4 | 5 | class Screen( 6 | val topLeft: Point3F, 7 | val bottomLeft: Point3F, 8 | val topRight: Point3F, 9 | val width: Int, 10 | val height: Int 11 | ) 12 | -------------------------------------------------------------------------------- /src/main/kotlin/scene/lights/Light.kt: -------------------------------------------------------------------------------- 1 | package scene.lights 2 | 3 | import geometry.Point3F 4 | 5 | open class Light( 6 | val position: Point3F, 7 | val diffuseIntensity: Point3F, 8 | val ambientIntensity: Point3F, 9 | val specularIntensity: Point3F 10 | ) { 11 | open fun pointsVisibleTo(point: Point3F): List { 12 | throw NotImplementedError() 13 | } 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/scene/lights/LightDisc.kt: -------------------------------------------------------------------------------- 1 | package scene.lights 2 | 3 | import geometry.Point3F 4 | 5 | class LightDisc( 6 | val center: Point3F, 7 | radius: Float, 8 | towards: Point3F, 9 | diffuseIntensity: Point3F = Point3F(2f), 10 | ambientIntensity: Point3F = Point3F(0.4f), 11 | specularIntensity: Point3F = Point3F.ones() 12 | ): Light(center, diffuseIntensity, ambientIntensity, specularIntensity) { 13 | private val positions = mutableListOf(center) 14 | 15 | init { 16 | val normal = (towards - center).normalized() 17 | val orthogonal = Point3F(0f, -normal.z, normal.y).normalized() 18 | for (i in 10 until 100 step 10) { 19 | for (j in 0 until 360 step 20) { 20 | positions.add(center + orthogonal.rotate(j.toFloat(), normal) * (i / 100f) * radius) 21 | } 22 | } 23 | diffuseIntensity.set(diffuseIntensity * (1f / positions.size)) 24 | ambientIntensity.set(ambientIntensity * (1f / positions.size)) 25 | specularIntensity.set(specularIntensity * (1f / positions.size)) 26 | } 27 | 28 | override fun pointsVisibleTo(point: Point3F): List { 29 | return positions 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/kotlin/scene/lights/LightPoint.kt: -------------------------------------------------------------------------------- 1 | package scene.lights 2 | 3 | import geometry.Point3F 4 | 5 | class LightPoint( 6 | position: Point3F, 7 | diffuseIntensity: Point3F = Point3F(1.5f), 8 | ambientIntensity: Point3F = Point3F(0.5f), 9 | specularIntensity: Point3F = Point3F.ones() 10 | ): Light(position, diffuseIntensity, ambientIntensity, specularIntensity) { 11 | override fun pointsVisibleTo(point: Point3F): List { 12 | return listOf(position) 13 | } 14 | } 15 | --------------------------------------------------------------------------------