summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Math.scala14
-rw-r--r--src/Objects.scala151
-rw-r--r--src/Scene.scala152
-rw-r--r--src/main.scala40
-rw-r--r--src/test.scala3
5 files changed, 360 insertions, 0 deletions
diff --git a/src/Math.scala b/src/Math.scala
new file mode 100644
index 0000000..601e457
--- /dev/null
+++ b/src/Math.scala
@@ -0,0 +1,14 @@
+package net.iximeow.raytrace
+
+import Objects._
+
+object Raymath {
+ def angleBetween(a: Point, b: Point, c: Point): Double = {
+ val lineA = a - b
+ val lineB = c - b
+ Math.acos(lineA.dot(lineB) / (lineA.magnitude * lineB.magnitude))
+ }
+
+ def toDegrees(rad: Double): Double = rad * 360 / 2 / Math.PI
+ def toRadians(deg: Double): Double = deg / 360 * 2 * Math.PI
+}
diff --git a/src/Objects.scala b/src/Objects.scala
new file mode 100644
index 0000000..b4e6435
--- /dev/null
+++ b/src/Objects.scala
@@ -0,0 +1,151 @@
+package net.iximeow.raytrace
+
+import java.awt.image.BufferedImage
+
+object Objects {
+ /*
+ * 3d nah
+ case class Point(x: Double, y: Double, z: Double)
+ case class Plane(pitch: Double, roll: Double, altitude: Double)
+ case class BoundedPlane(pitch: Double, roll: Double, center: Point, h: Point, w: Point)
+ */
+
+ case class Point(x: Double, y: Double) {
+ def +(other: Point): Point = Point(x + other.x, y + other.y)
+ def -(other: Point): Point = Point(x - other.x, y - other.y)
+ def /(scale: Double): Point = Point(x / scale, y / scale)
+ def *(scale: Double): Point = Point(x * scale, y * scale)
+ def magnitude: Double = distTo(Point.Zero)
+ def distTo(other: Point) = Math.sqrt(sqDistTo(other))
+ def sqDistTo(other: Point) = (other.x - x) * (other.x - x) + (other.y - y) * (other.y - y)
+ def dot(other: Point): Double = x * other.x + y * other.y
+ }
+ object Point {
+ val Zero = Point(0, 0)
+ }
+ case class Line(m: Double, b: Double)
+ object Line {
+ def fromPoints(p1: Point, p2: Point): Line = {
+ val m = (p2.y - p1.y) / (p2.x - p1.x)
+ val b = p1.y - m*p1.x
+ Line(m, b)
+ }
+ }
+ /*
+ * Segments are defined for t in [0, 1]
+ */
+ case class Segment(x: Double, y: Double, initial: Point) {
+ def at(t: Double): Point = Point(x * t, y * t) + initial
+ def apply = at _
+ def length = Math.sqrt(x * x + y * y)
+ def intersect(other: Segment): Double = {
+ /*
+ * P1 = ai + t * a
+ * P2 = bi + u * b
+ * P1 == P2, so
+ * ai + t * a = bi + u * b
+ * ...
+ * ai.x + t * a.x = bi.x + u * b.x
+ * ai.y + t * a.y = bi.y + u * b.y
+ * t = (bi.x + u * b.x - ai.x) / a.x
+ * ai.y + a.y * (bi.x + u * b.x - ai.x) / a.x = bi.y + u * b.y
+ * ai.y + a.y * bi.x / a.x + u * b.x * a.y / a.x - ai.x * a.y / a.x = bi.y + u * b.y
+ * ai.y - bi.y + (a.y / a.x) (bi.x - ai.x) = u * b.y - u * b.x * a.y / a.x
+ * ai.y - bi.y + (a.y / a.x) (bi.x - ai.x) / (b.y - b.x * a.y / a.x) = u
+ *
+ * (a.x * (ai.y - bi.y) + a.y * (bi.x - ai.x)) / (b.y - b.x * a.y)
+ */
+ val u = (
+ x * (initial.y - other.initial.y) +
+ y * (other.initial.x - initial.x)
+ ) / (
+ other.y * x - other.x * y
+ )
+
+ u
+ }
+
+ def intersectChecked(other: Segment): Option[Point] = {
+ val u = intersect(other)
+ if (u >= 0 && u <= 1) {
+ //println("Intersection is at u=" + u)
+
+ Some(other.at(u))
+ } else {
+ None
+ }
+ }
+ def tFor(p: Point): Option[Double] = {
+ (x, y) match {
+ case (0, _) => Some((p.y - initial.y) / y)
+ case (_, 0) => Some((p.x - initial.x) / x)
+ case (_, _) => {
+ val xT = (p.x - initial.x) / x
+ val yT = (p.y - initial.y) / y
+ if (Math.abs(xT - yT) < 0.000001) {
+ Some(xT)
+ } else {
+ None
+ }
+ }
+ }
+ }
+ def rotate(angle: Double, about: Point = Point(0, 0)): Segment = {
+ val start = this.at(0)
+ val end = this.at(1)
+ val newStart = {
+ val offset = start - about
+ val m = offset.magnitude
+ val newAngle = Math.atan2(offset.y, offset.x) + angle
+ Point(Math.cos(newAngle) * m, Math.sin(newAngle) * m) + about
+ }
+ val newEnd = {
+ val offset = end - about
+ val m = offset.magnitude
+ val newAngle = Math.atan2(offset.y, offset.x) + angle
+ Point(Math.cos(newAngle) * m, Math.sin(newAngle) * m) + about
+ }
+ Segment.fromPoints(newStart, newEnd)
+ }
+ def renderTo(buf: BufferedImage, scale: Double = 1, xoff: Int = 0, yoff: Int = 0, color: Int = 0x808000): Unit = {
+ try {
+ for (i <- (0 to 100)) {
+ val point = this.at(i / 100.0)
+ buf.setRGB(Math.round((point.x * scale).toFloat) + xoff, Math.round((point.y * scale).toFloat) + yoff, color)
+ }
+ } catch {
+ case (x: ArrayIndexOutOfBoundsException) => { /* well, we're not properly rendering a region so uh just ignore the failure i guess lol */ }
+ }
+ }
+ def normal: Segment = {
+ val normalMag = Math.sqrt(x * x + y * y)
+ val finalNormMult = 1.5 / normalMag
+ Ray(-y * finalNormMult, x * finalNormMult, (at(0) + at(1)) / 2).toSegment
+ }
+ }
+ object Segment {
+ def fromPoints(p1: Point, p2: Point): Segment = {
+ val (x0, y0) = (p2.x - p1.x, p2.y - p1.y)
+ Segment(x0, y0, p1)
+ }
+ def fromPointWithAngle(p: Point, angle: Double): Segment = {
+ val (x0, y0) = (Math.cos(angle), Math.sin(angle))
+ Segment(x0, y0, p)
+ }
+ }
+ case class Ray(x: Double, y: Double, initial: Point) {
+ def toSegment: Segment = Segment(x, y, initial)
+ def endingAt(p: Point): Ray = {
+ val intersectAt = this.toSegment.tFor(p)
+ intersectAt.map(t =>
+ Ray(x * t, y * t, initial)
+ ).getOrElse(this)
+ }
+ def dot(other: Ray): Double = {
+ x * other.x + y * other.y
+ }
+ def mag: Double = {
+ Math.sqrt(x * x + y * y)
+ }
+ }
+}
diff --git a/src/Scene.scala b/src/Scene.scala
new file mode 100644
index 0000000..9b59f7b
--- /dev/null
+++ b/src/Scene.scala
@@ -0,0 +1,152 @@
+package net.iximeow.raytrace
+
+import Objects._
+
+import java.awt.image.BufferedImage
+import javax.imageio._
+import java.io.File
+
+case class Scene(walls: Seq[Segment]) {
+ val buffer = new BufferedImage(800, 600, BufferedImage.TYPE_INT_RGB)
+
+ def render(scale: Double = 1, xoff: Int = 0, yoff: Int = 0, color: Int = 0x808000, normals: Boolean = false): Unit = {
+ for (wall <- walls) {
+ wall.renderTo(buffer, scale, 400, 300, color = color)
+ }
+ for (wall <- walls) {
+ wall.normal.renderTo(buffer, scale, 400, 300, color = 0xc00000)
+ }
+ }
+
+ def save(path: String = "render.png"): Unit = {
+ ImageIO.write(buffer, "png", new File(path))
+ }
+
+ def cast(r: Ray, steps: Int): Seq[Segment] = {
+ (0 until steps).foldLeft(Seq.empty[Segment] -> r) { case (p: (Seq[Segment], Ray), i: Int) => {
+ val (prevRay, nextRay) = castSingle(p._2)
+ ((p._1 :+ prevRay.toSegment) -> nextRay): (Seq[Segment], Ray)
+ }}._1
+ }
+
+ def castSingle(r: Ray): (Ray, Ray) = {
+ val asSeg = r.toSegment
+
+ def reflect(firstIntersection: (Segment, Point)): (Ray, Ray) = {
+ val minAngle = {
+ val fromStart = Raymath.angleBetween(
+ r.initial,
+ firstIntersection._2,
+ firstIntersection._1.at(0)
+ )
+ val fromEnd = Raymath.angleBetween(
+ r.initial,
+ firstIntersection._2,
+ firstIntersection._1.at(1)
+ )
+
+ println("Fromstart: " + Raymath.toDegrees(fromStart))
+ println("Fromend: " + Raymath.toDegrees(fromEnd))
+
+ if (Math.abs(fromStart) < Math.PI / 2) {
+ fromStart
+ } else {
+ fromEnd
+ }
+
+ fromStart
+ }
+
+ val maxAngle = Math.PI - minAngle
+
+ val baseAngle = Math.atan2(firstIntersection._1.y, firstIntersection._1.x)
+ println("base angle: " + Raymath.toDegrees(baseAngle))
+
+ val reflectedAngle = baseAngle + minAngle
+
+ if (minAngle < 0 || minAngle > Math.PI * 2) {
+ println("lol")
+ (r.endingAt(firstIntersection._2), r.endingAt(firstIntersection._2)) //Ray(0, 0, firstIntersection._2))
+ } else {
+ val (x, y) = (
+ Math.cos(reflectedAngle) * 3,
+ Math.sin(reflectedAngle) * 3
+ )
+
+ // Sure hope this is right...
+ (r.endingAt(firstIntersection._2), Ray(x, y, firstIntersection._2))
+ }
+ }
+ val intersections: Seq[(Segment, Point)] = walls.flatMap(w => {
+ w.intersectChecked(asSeg)
+ .map(x => (w, x))
+ })
+ .filter { case (w: Segment, x: Point) => asSeg.tFor(x).map(_ > 0.0000001).getOrElse(false) }
+
+ def isBehind(start: Segment, wall: Segment): Boolean = {
+ val normal = Ray(-wall.y, wall.x, Point(0, 0))
+ val rebased = Ray(start.x, start.y, Point(0, 0))
+ val cosAngle = normal.dot(rebased) / (normal.mag * rebased.mag)
+ cosAngle > 0
+ }
+
+ val continuedIntersections = intersections
+ .filter(i => {
+ val otherT = i._1.tFor(i._2)
+ otherT.map(t => t >= 0 && t <= 1).getOrElse(true)
+ })
+
+ val stoppedIntersections = intersections
+ .filter(i => {
+ val otherT = i._1.tFor(i._2)
+ otherT.map(t => t >= 0 && t <= 1 && isBehind(asSeg, i._1)).getOrElse(false)
+ })
+
+ def fnMin(x: (Segment, Point), y: (Segment, Point)) = if (asSeg.tFor(x._2).get < asSeg.tFor(y._2).get) x else y
+ val firstStop: Option[(Segment, Point)] = stoppedIntersections.reduceOption(fnMin(_, _))
+ val firstReflect: Option[(Segment, Point)] = continuedIntersections.reduceOption(fnMin(_, _))
+
+ (firstStop, firstReflect) match {
+ case (None, None) =>
+ (r, Ray(r.x, r.y, r.toSegment.at(1)))
+ case (Some(stop), None) =>
+ (r.endingAt(stop._2), Ray(0, 0, r.initial))
+ case (None, Some(cont)) => reflect(cont)/* reflect */
+ case (Some(stop), Some(cont)) => {
+ if (fnMin(stop, cont) == stop) {
+ (r.endingAt(stop._2), Ray(0, 0, r.initial))
+ // stop
+ } else {
+ reflect(cont)
+ // reflect
+ }
+ }
+ }
+ }
+}
+
+object Scene {
+ def generateMirror(r: Double, segments: Int, arcSize: Double, at: Point, rotated: Double): Seq[Segment] = {
+ val sizePerSegment = arcSize / segments
+ val points = (0 to segments) map { i =>
+ val angle = i * sizePerSegment + rotated
+ at + Point(Math.cos(angle) * r, Math.sin(angle) * r)
+ }
+ points.sliding(2).map { case Seq(start, end) =>
+ Segment.fromPoints(start, end)
+ }.toSeq
+ }
+
+ def generateParabola(a: Double, b: Double, w: Double, w_i: Double, segments: Int, at: Point, rotated: Double): Seq[Segment] = {
+ val points = (-segments / 2 to segments / 2) map { i =>
+ val w_curr = (i / segments.toDouble) * w + w_i
+ at + Point(a * w_curr, b * w_curr * w_curr)
+ }
+ points.sliding(2).map { case Seq(start, end) =>
+ Segment.fromPoints(start, end)
+ }.toSeq
+ }
+
+ def rotate(walls: Seq[Segment], angle: Double) =
+ walls.map(_.rotate(angle))
+}
diff --git a/src/main.scala b/src/main.scala
new file mode 100644
index 0000000..8883cc7
--- /dev/null
+++ b/src/main.scala
@@ -0,0 +1,40 @@
+import net.iximeow.raytrace._
+import Objects._
+
+object main extends App {
+// val mirror = Scene(Scene.generateMirror(10, 60, Raymath.toRadians(180), Point(0, 0), 0))
+ val offset = Point(0.0, -9)
+ val mirror = Scene(Scene.generateParabola(-0.9, -0.0075, 16, 0, 42, Point(0, 14), 0) :+ Segment.fromPoints(
+ Point(-0.5, 0.5) + offset,
+ Point(0.5, -0.5) + offset
+ ))
+// val mirror = Scene(Seq(Segment(4, -4, Point(-2, 2))))
+ //val ray = mirror.cast(Ray(0, 2, Point(0.8, -1)), 4)
+ mirror.render(scale = 20, color = 0xc0c0c0, normals = true)
+ //for (segment <- ray) {
+ // segment.renderTo(mirror.buffer, 20, 400, 300)
+ //}
+/*
+ for (i <- -4 to 4 by 1) {
+ for (segment <- mirror.cast(Ray(i / 2.0, 2, Point(0.8, -1)), 4)) {
+ segment.renderTo(mirror.buffer, 20, 400, 300)
+ }
+ }
+*/
+ def rays(number: Int, spacing: Double, centerpoint: Point, direction: Point): Seq[Ray] = {
+ (0 until number).map { i =>
+ val x = (i.toDouble - number.toDouble / 2) * spacing
+ val y = 0
+ val dx = direction.x
+// * Math.cos((i.toDouble - number.toDouble) / (number.toDouble / 4))
+ val dy = direction.y
+ Ray(dx, dy, Point(x, y) + centerpoint)
+ }
+ }
+
+ rays(63, 0.165, Point(-0, -10), Point(0.0, 2))
+ .flatMap(x => mirror.cast(x, 30))
+ .map(_.renderTo(mirror.buffer, 20, 400, 300))
+ //println(ray)
+ mirror.save()
+}
diff --git a/src/test.scala b/src/test.scala
new file mode 100644
index 0000000..5780509
--- /dev/null
+++ b/src/test.scala
@@ -0,0 +1,3 @@
+object foo extends App {
+ println("hello world")
+}