From 9ac3ce01b8454ee1874e7d815a6215b888ed1c7a Mon Sep 17 00:00:00 2001 From: Sven Weidauer Date: Mon, 12 May 2025 20:23:24 +0200 Subject: [PATCH 1/3] Update to Kotlin 2.1 --- .idea/kotlinc.xml | 2 +- build.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index c224ad5..bb44937 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 0aea81a..3c449f6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - kotlin("jvm") version "2.0.21" + kotlin("jvm") version "2.1.0" } group = "org.example" From 876586ee9d25d3211a08961cef20a64d1f2f4bd4 Mon Sep 17 00:00:00 2001 From: Sven Weidauer Date: Mon, 12 May 2025 20:56:09 +0200 Subject: [PATCH 2/3] Multiple samples per pixel + focus effect --- src/main/kotlin/Ray.kt | 30 +++++++++++++++++++++++++----- src/main/kotlin/math/Vector.kt | 11 +++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/Ray.kt b/src/main/kotlin/Ray.kt index 8e2a2ea..7925ec8 100644 --- a/src/main/kotlin/Ray.kt +++ b/src/main/kotlin/Ray.kt @@ -42,14 +42,34 @@ fun main() { for (y in 0 until bmp.height) { for (x in 0 until bmp.width) { - val point = Point(x.toFloat(), y.toFloat(), 0f) - val ray = origin.rayTo(point) + var r = 0f + var g = 0f + var b = 0f - val color = scene.colorForRay(ray) - if (color != null) { - bmp.setPixel(x, y, color.toColor()) + repeat(samplesPerPixel) { + val point = Point(x.toFloat(), y.toFloat(), 0f) + Vector.randomInCircleXY() * blurriness + val ray = origin.rayTo(point) + + val color = scene.colorForRay(ray) + if (color != null) { + r += color.r + g += color.g + b += color.b + } } + + bmp.setPixel( + x, y, + MaterialColor( + r = r / samplesPerPixel, + g = g / samplesPerPixel, + b = b / samplesPerPixel + ).toColor() + ) } } File("test.tga").writeBitmap(bmp) } + +const val samplesPerPixel = 5 +const val blurriness = 1f \ No newline at end of file diff --git a/src/main/kotlin/math/Vector.kt b/src/main/kotlin/math/Vector.kt index dccb03a..d4d769d 100644 --- a/src/main/kotlin/math/Vector.kt +++ b/src/main/kotlin/math/Vector.kt @@ -1,6 +1,9 @@ package math +import kotlin.math.cos +import kotlin.math.sin import kotlin.math.sqrt +import kotlin.random.Random data class Vector(val x: Float, val y: Float, val z: Float) { val length: Float get() = sqrt(squaredLength) @@ -21,6 +24,14 @@ data class Vector(val x: Float, val y: Float, val z: Float) { ) infix fun dot(rhs: Vector): Float = x * rhs.x + y * rhs.y + z * rhs.z + + companion object { + fun randomInCircleXY(): Vector { + val r = sqrt(Random.Default.nextFloat()) + val theta = Random.Default.nextFloat() * 2 * Math.PI.toFloat() + return Vector(r * sin(theta), r * cos(theta), 0f) + } + } } operator fun Float.times(rhs: Vector): Vector = rhs * this From 32de052a6c936984fae6019c3657f45a446d6154 Mon Sep 17 00:00:00 2001 From: Sven Weidauer Date: Mon, 12 May 2025 22:21:48 +0200 Subject: [PATCH 3/3] Implement path tracing material See https://github.com/SebLague/Ray-Tracing/blob/main/Assets/Scripts/Shaders/RayTracer.shader#L328 --- src/main/kotlin/Ray.kt | 60 ++++++++----- src/main/kotlin/materials/MaterialColor.kt | 2 + .../kotlin/materials/PathTracedMaterial.kt | 85 +++++++++++++++++++ .../kotlin/materials/ReflectiveMaterial.kt | 12 ++- src/main/kotlin/things/Scene.kt | 7 +- 5 files changed, 139 insertions(+), 27 deletions(-) create mode 100644 src/main/kotlin/materials/PathTracedMaterial.kt diff --git a/src/main/kotlin/Ray.kt b/src/main/kotlin/Ray.kt index 7925ec8..7094e81 100644 --- a/src/main/kotlin/Ray.kt +++ b/src/main/kotlin/Ray.kt @@ -1,5 +1,6 @@ import lights.PointLight import materials.MaterialColor +import materials.PathTracedMaterial import materials.ReflectiveMaterial import materials.WhateverMaterial import materials.times @@ -15,19 +16,46 @@ fun main() { val bmp = Bitmap(1000, 1000) val origin = Point(500f, 500f, -500f) - val sphere = Sphere(Point(500f, 500f, 500f), radius = 500f, material = ReflectiveMaterial(reflectivity = 0.2f, MaterialColor(0f, 1.0f, 0f))) - val sphere2 = Sphere(Point(0f, 500f, 50f), radius = 50f, material = ReflectiveMaterial(reflectivity = 0.2f, MaterialColor(1.0f, 0.0f, 0.1f))) + val material1 = PathTracedMaterial( + MaterialColor(1f, 0f, 0f), + specularProbability = 0.1f, + specularColour = MaterialColor.White, + smoothness = 1.0f, + ) + val materialFloor = PathTracedMaterial( + MaterialColor(1f, 0f, 1f), + specularProbability = 0.1f, + specularColour = MaterialColor.White, + smoothness = 1.0f, + ) + + val lightMaterial = PathTracedMaterial( + color = MaterialColor.Black, + emissionColor = MaterialColor.White, + emissionStrength = 0.2f, + specularColour = MaterialColor.White + ) + + val material2 = ReflectiveMaterial(reflectivity = 0.2f, MaterialColor(0f, 1.0f, 0f)) + val material3 = ReflectiveMaterial(reflectivity = 0.2f, MaterialColor(1.0f, 0.0f, 0.1f)) + + val sphere = Sphere(Point(500f, 500f, 500f), radius = 500f, material = material1) + val sphere2 = Sphere(Point(0f, 500f, 50f), radius = 50f, material = material1) + val sphere3 = Sphere(Point(1000f, 1000f, -500f), radius = 100f, material = lightMaterial) + + val material4 = WhateverMaterial(MaterialColor(1f, 0.0f, 0.0f)) val plane = Plane( Point(0f, -1000f, 0f), normal = Vector(0f, 1f, 0f), - material = WhateverMaterial(MaterialColor(1f, 0.0f, 0.0f)) + material = materialFloor ) + val material5 = WhateverMaterial(MaterialColor(0.0f, 1f, 0.0f)) val leftPlane = Plane( Point(-1000f, 0f, 0f), Vector(1f, 0f, 0f), - material = WhateverMaterial(MaterialColor(0.0f, 1f, 0.0f)) + material = materialFloor ) val light = PointLight(Point(1000f, 1000f, -500f), color = 0.2f * MaterialColor(0.9f, 0.9f, 0.9f)) @@ -36,40 +64,30 @@ fun main() { sphere, sphere2, plane, - leftPlane - ), light = light) + leftPlane, + sphere3 + ), light = light, ambientLight = MaterialColor.White * 0.1f) for (y in 0 until bmp.height) { for (x in 0 until bmp.width) { - var r = 0f - var g = 0f - var b = 0f + var color = MaterialColor.Black repeat(samplesPerPixel) { val point = Point(x.toFloat(), y.toFloat(), 0f) + Vector.randomInCircleXY() * blurriness val ray = origin.rayTo(point) - val color = scene.colorForRay(ray) - if (color != null) { - r += color.r - g += color.g - b += color.b - } + color += scene.colorForRay(ray) } bmp.setPixel( x, y, - MaterialColor( - r = r / samplesPerPixel, - g = g / samplesPerPixel, - b = b / samplesPerPixel - ).toColor() + (color * (1.0f / samplesPerPixel)).toColor() ) } } File("test.tga").writeBitmap(bmp) } -const val samplesPerPixel = 5 +const val samplesPerPixel = 64 const val blurriness = 1f \ No newline at end of file diff --git a/src/main/kotlin/materials/MaterialColor.kt b/src/main/kotlin/materials/MaterialColor.kt index 0e192b3..ccc4063 100644 --- a/src/main/kotlin/materials/MaterialColor.kt +++ b/src/main/kotlin/materials/MaterialColor.kt @@ -11,9 +11,11 @@ data class MaterialColor(val r: Float, val g: Float, val b: Float) { operator fun times(rhs: MaterialColor) = MaterialColor(r * rhs.r, g * rhs.g, b * rhs.b) operator fun plus(rhs: MaterialColor) = MaterialColor(r + rhs.r, g + rhs.g, b + rhs.b) + operator fun times(rhs: Float) = MaterialColor(rhs * r, rhs * g, rhs * b) companion object { val Black: MaterialColor = MaterialColor(0f, 0f, 0f) + val White = MaterialColor(1f, 1f, 1f) } } diff --git a/src/main/kotlin/materials/PathTracedMaterial.kt b/src/main/kotlin/materials/PathTracedMaterial.kt new file mode 100644 index 0000000..17df071 --- /dev/null +++ b/src/main/kotlin/materials/PathTracedMaterial.kt @@ -0,0 +1,85 @@ +package materials + +import Hit +import math.Ray +import math.Vector +import things.Scene +import kotlin.math.cos +import kotlin.math.ln +import kotlin.math.log +import kotlin.math.sqrt +import kotlin.random.Random + +data class PathTracedMaterial( + val color: MaterialColor, + val emissionColor: MaterialColor = MaterialColor(0f, 0f, 0f), + val specularProbability: Float = 0f, + val smoothness: Float = 0.5f, + val emissionStrength: Float = 0f, + val specularColour: MaterialColor, +) : Material { + override fun shade(ray: Ray, hit: Hit, scene: Scene): MaterialColor { + // source: https://github.com/SebLague/Ray-Tracing/blob/main/Assets/Scripts/Shaders/RayTracer.shader#L328 + + var incomingLight = MaterialColor.Black + var rayColor = MaterialColor.White + + var hit: Hit? = hit + var ray = ray + + repeat(maxBounces) { + if (hit != null && hit.material is PathTracedMaterial) { + val isSpecular = hit.material.specularProbability >= Random.Default.nextFloat() + + val diffuseDir = (hit.normal + Random.Default.direction()).normalized() + val specularDir = reflect(ray.direction, hit.normal) + + val rayDir = if (isSpecular) { + lerp(diffuseDir, specularDir, hit.material.smoothness).normalized() + } else { + diffuseDir + } + + val emittedLight = hit.material.emissionColor * hit.material.emissionStrength; + incomingLight += emittedLight * rayColor; + rayColor *= if (isSpecular) hit.material.specularColour else hit.material.color + + val p = maxOf(rayColor.r, rayColor.g, rayColor.b) + if (Random.Default.nextFloat() >= p) { + return@repeat + } + + rayColor *= 1.0f / p; + + ray = Ray(hit.point, rayDir) + + } else if (hit != null) { + incomingLight += hit.material.shade(ray, hit, scene) * rayColor + return@repeat + } else { + incomingLight += scene.ambientLight * rayColor + return@repeat + } + + hit = scene.intersects(ray) + } + + return incomingLight + } + +} + +const val maxBounces = 4 + +fun Random.normalDistributedFloat(): Float { + val theta = nextFloat() * 2 * Math.PI.toFloat() + val rho = sqrt(-2 * ln(nextFloat())) + return rho * cos(theta) +} + +fun Random.direction(): Vector = + Vector(normalDistributedFloat(), normalDistributedFloat(), normalDistributedFloat()).normalized() + +fun lerp(a: Float, b: Float, c: Float): Float = a * (1 - c) + b * c +fun lerp(a: MaterialColor, b: MaterialColor, c: Float): MaterialColor = a * (1 - c) + b * c +fun lerp(a: Vector, b: Vector, c: Float): Vector = a * (1 - c) + b * c diff --git a/src/main/kotlin/materials/ReflectiveMaterial.kt b/src/main/kotlin/materials/ReflectiveMaterial.kt index 068d818..afa8b92 100644 --- a/src/main/kotlin/materials/ReflectiveMaterial.kt +++ b/src/main/kotlin/materials/ReflectiveMaterial.kt @@ -2,6 +2,7 @@ package materials import Hit import math.Ray +import math.Vector import things.Scene import math.times import kotlin.math.pow @@ -29,9 +30,14 @@ data class ReflectiveMaterial( color += intensity * MaterialColor(0.3f, 0f, 0f) } - val reflectionDirection = ray.direction - 2 * (ray.direction dot hit.normal) * hit.normal - val reflectedColor = scene.colorForRay(Ray(offsetPoint, reflectionDirection)) ?: MaterialColor.Black + val reflectionDirection = reflect(ray.direction, hit.normal) + val reflectedColor = scene.colorForRay(Ray(offsetPoint, reflectionDirection)) return (1.0f - reflectivity) * color + reflectivity * reflectedColor } -} \ No newline at end of file +} + +fun reflect( + rayDirection: Vector, + normal: Vector +): Vector = rayDirection - 2 * (rayDirection dot normal) * normal \ No newline at end of file diff --git a/src/main/kotlin/things/Scene.kt b/src/main/kotlin/things/Scene.kt index 974ce7c..3f5a0af 100644 --- a/src/main/kotlin/things/Scene.kt +++ b/src/main/kotlin/things/Scene.kt @@ -3,9 +3,10 @@ package things import Hit import materials.MaterialColor import lights.PointLight +import materials.Material import math.Ray -data class Scene(val things: List, val light: PointLight): Thing { +data class Scene(val things: List, val light: PointLight, val ambientLight: MaterialColor = MaterialColor.Black): Thing { override fun intersects(ray: Ray): Hit? { var closest: Hit? = null for (thing in things) { @@ -18,11 +19,11 @@ data class Scene(val things: List, val light: PointLight): Thing { return closest } - fun colorForRay(ray: Ray): MaterialColor? { + fun colorForRay(ray: Ray): MaterialColor { val hit = intersects(ray) if (hit != null) { return hit.material.shade(ray, hit, this) } - return null + return ambientLight } } \ No newline at end of file