Sven Weidauer 2025-05-12 22:21:48 +02:00
parent 876586ee9d
commit 32de052a6c
5 changed files with 139 additions and 27 deletions

View file

@ -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

View file

@ -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)
}
}

View file

@ -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

View file

@ -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
}
}
}
fun reflect(
rayDirection: Vector,
normal: Vector
): Vector = rayDirection - 2 * (rayDirection dot normal) * normal

View file

@ -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<Thing>, val light: PointLight): Thing {
data class Scene(val things: List<Thing>, 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<Thing>, 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
}
}