From 2770febcc400f5a16aea55ba7f11d942f8ccc22c Mon Sep 17 00:00:00 2001 From: iximeow Date: Sun, 19 Feb 2017 13:12:52 -0800 Subject: initial commit --- src/Math.scala | 14 +++++ src/Objects.scala | 151 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/Scene.scala | 152 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.scala | 40 ++++++++++++++ src/test.scala | 3 ++ 5 files changed, 360 insertions(+) create mode 100644 src/Math.scala create mode 100644 src/Objects.scala create mode 100644 src/Scene.scala create mode 100644 src/main.scala create mode 100644 src/test.scala (limited to 'src') 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") +} -- cgit v1.1