diff --git a/src/main/scala/io/computenode/cyfra/foton/animation/AnimationFunctions.scala b/src/main/scala/io/computenode/cyfra/foton/animation/AnimationFunctions.scala index 1901a705..1e22b874 100644 --- a/src/main/scala/io/computenode/cyfra/foton/animation/AnimationFunctions.scala +++ b/src/main/scala/io/computenode/cyfra/foton/animation/AnimationFunctions.scala @@ -30,6 +30,24 @@ object AnimationFunctions: .otherwise: to + def colorChange(color1: Vec3[Float32], color2: Vec3[Float32], changeRate: Float32): AnimationInstant ?=> Vec3[Float32] = + inst ?=> + val time = inst.time.mod(changeRate * 2f) + val t = when(time < changeRate) { + time / changeRate + } otherwise { + (2f * changeRate - time) / changeRate + } + color1 * (1f - t) + color2 * t + + def orbit(center: Vec3[Float32], radius: Float32, duration: Milliseconds, at: Milliseconds = Milliseconds(0), + initialAngle: Float32, finalAngle: Float32): AnimationInstant ?=> Vec3[Float32] = + inst ?=> + val angle = smooth(initialAngle, finalAngle, duration, at) + val x = center.x + radius * cos(angle) + val z = center.z + radius * sin(angle) + (x, center.y, z) + // def freefall(from: Float32, to: Float32, g: Float32): Float32 => Vec3[Float32] = // t => // val distance = to - from diff --git a/src/main/scala/io/computenode/cyfra/foton/rt/shapes/Box.scala b/src/main/scala/io/computenode/cyfra/foton/rt/shapes/Box.scala index fa56c8cb..c6fbb657 100644 --- a/src/main/scala/io/computenode/cyfra/foton/rt/shapes/Box.scala +++ b/src/main/scala/io/computenode/cyfra/foton/rt/shapes/Box.scala @@ -38,7 +38,7 @@ case class Box( val tEnter = max(tMinX, tMinY, tMinZ) val tExit = min(tMaxX, tMaxY, tMaxZ) - when(tEnter < tExit || tExit < 0.0f) { + when(tEnter > tExit || tExit < 0.0f) { currentHit } otherwise { val hitDistance = when(tEnter > 0f)(tEnter).otherwise(tExit) diff --git a/src/main/scala/io/computenode/cyfra/foton/rt/shapes/Cylinder.scala b/src/main/scala/io/computenode/cyfra/foton/rt/shapes/Cylinder.scala new file mode 100644 index 00000000..3a367e04 --- /dev/null +++ b/src/main/scala/io/computenode/cyfra/foton/rt/shapes/Cylinder.scala @@ -0,0 +1,85 @@ +package io.computenode.cyfra.foton.rt.shapes + +import io.computenode.cyfra.dsl.Algebra.{*, given} +import io.computenode.cyfra.dsl.Control.* +import io.computenode.cyfra.dsl.Functions.* +import io.computenode.cyfra.dsl.Value.* +import io.computenode.cyfra.dsl.GStruct +import io.computenode.cyfra.dsl.{*, given} +import io.computenode.cyfra.foton.rt.Material +import io.computenode.cyfra.foton.rt.RtRenderer.{MinRayHitTime, RayHitInfo} + +import java.nio.file.Paths +import scala.collection.mutable +import scala.concurrent.ExecutionContext.Implicits +import scala.concurrent.duration.DurationInt +import scala.concurrent.{Await, ExecutionContext} +case class Cylinder( + center: Vec3[Float32], + radius: Float32, + height: Float32, + material: Material +) extends GStruct[Cylinder] with Shape: + def testRay( + rayPos: Vec3[Float32], + rayDir: Vec3[Float32], + currentHit: RayHitInfo, + ): RayHitInfo = + val capTop = center.y + height/2f + val capBottom = center.y - height/2f + + val ox = rayPos.x - center.x + val oz = rayPos.z - center.z + + val a = rayDir.x * rayDir.x + rayDir.z * rayDir.z + val b = 2f * (ox * rayDir.x + oz * rayDir.z) + val c = ox * ox + oz * oz - radius * radius + val discr = b * b - 4f * a * c + + val sideHit = when(discr >= 0f && !(a === 0f)) { + val sqrtD = sqrt(discr) + val t0 = (-b - sqrtD) / (2f * a) + val t1 = (-b + sqrtD) / (2f * a) + val t = when(t0 > 0f)(t0).elseWhen(t1 > 0f)(t1).otherwise(-1f) + when(t > 0f) { + val hitY = rayPos.y + rayDir.y * t + when(hitY >= capBottom && hitY <= capTop && t < currentHit.dist) { + val hitPoint = rayPos + rayDir * t + val normal = normalize((hitPoint.x - center.x, 0f, hitPoint.z - center.z)) + RayHitInfo(t, normal, material) + } otherwise currentHit + } otherwise currentHit + } otherwise currentHit + + val bottomHit = when(!(rayDir.y === 0f)) { + val t = (capBottom - rayPos.y) / rayDir.y + when(t > 0f && t < currentHit.dist) { + val hitPoint = rayPos + rayDir * t + val dx = hitPoint.x - center.x + val dz = hitPoint.z - center.z + val dist2 = dx * dx + dz * dz + when(dist2 <= radius * radius) { + RayHitInfo(t, (0f, -1f, 0f), material) + } otherwise currentHit + } otherwise currentHit + } otherwise currentHit + + val topHit = when(!(rayDir.y === 0f)) { + val t = (capTop - rayPos.y) / rayDir.y + when(t > 0f && t < currentHit.dist) { + val hitPoint = rayPos + rayDir * t + val dx = hitPoint.x - center.x + val dz = hitPoint.z - center.z + val dist2 = dx * dx + dz * dz + when(dist2 <= radius * radius) { + RayHitInfo(t, (0f, 1f, 0f), material) + } otherwise currentHit + } otherwise currentHit + } otherwise currentHit + + val bestHit = + when(sideHit.dist <= bottomHit.dist && sideHit.dist <= topHit.dist){sideHit} + .elseWhen(bottomHit.dist <= sideHit.dist && bottomHit.dist <= topHit.dist){bottomHit} + .otherwise(topHit) + + when(bestHit.dist < currentHit.dist)(bestHit) otherwise currentHit diff --git a/src/main/scala/io/computenode/cyfra/foton/rt/shapes/ShapeCollection.scala b/src/main/scala/io/computenode/cyfra/foton/rt/shapes/ShapeCollection.scala index 97c35038..419019d9 100644 --- a/src/main/scala/io/computenode/cyfra/foton/rt/shapes/ShapeCollection.scala +++ b/src/main/scala/io/computenode/cyfra/foton/rt/shapes/ShapeCollection.scala @@ -10,13 +10,13 @@ import io.computenode.cyfra.dsl.given import io.computenode.cyfra.dsl.{GSeq, GStruct} import io.computenode.cyfra.foton.rt.RtRenderer.RayHitInfo import izumi.reflect.Tag - import scala.util.chaining.* class ShapeCollection( val boxes: List[Box], val spheres: List[Sphere], val quads: List[Quad], + val cylinders: List[Cylinder], val planes: List[Plane] ) extends Shape: @@ -24,19 +24,22 @@ class ShapeCollection( shapes.collect { case box: Box => box }, shapes.collect { case sphere: Sphere => sphere }, shapes.collect { case quad: Quad => quad }, + shapes.collect { case cylinder: Cylinder => cylinder}, shapes.collect { case plane: Plane => plane } ) def addShape(shape: Shape): ShapeCollection = shape match case box: Box => - ShapeCollection(box :: boxes, spheres, quads, planes) + ShapeCollection(box :: boxes, spheres, quads, cylinders, planes) case sphere: Sphere => - ShapeCollection(boxes, sphere :: spheres, quads, planes) + ShapeCollection(boxes, sphere :: spheres, quads, cylinders,planes) case quad: Quad => - ShapeCollection(boxes, spheres, quad :: quads, planes) + ShapeCollection(boxes, spheres, quad :: quads, cylinders,planes) + case cylinder : Cylinder => + ShapeCollection(boxes, spheres, quads, cylinder :: cylinders,planes) case plane: Plane => - ShapeCollection(boxes, spheres, quads, plane :: planes) + ShapeCollection(boxes, spheres, quads, cylinders, plane :: planes) case _ => assert(false, "Unknown shape type: Broken sealed hierarchy") def testRay(rayPos: Vec3[Float32], rayDir: Vec3[Float32], noHit: RayHitInfo): RayHitInfo = @@ -50,3 +53,4 @@ class ShapeCollection( .pipe(testShapeType(spheres, _)) .pipe(testShapeType(boxes, _)) .pipe(testShapeType(planes, _)) + .pipe(testShapeType(cylinders, _)) \ No newline at end of file diff --git a/src/main/scala/io/computenode/cyfra/samples/foton/AnimatedConnectingForms.scala b/src/main/scala/io/computenode/cyfra/samples/foton/AnimatedConnectingForms.scala new file mode 100644 index 00000000..2ae2335d --- /dev/null +++ b/src/main/scala/io/computenode/cyfra/samples/foton/AnimatedConnectingForms.scala @@ -0,0 +1,55 @@ +package io.computenode.cyfra.samples.foton + +import io.computenode.cyfra +import io.computenode.cyfra.* +import io.computenode.cyfra.dsl.Algebra.{*, given} +import io.computenode.cyfra.dsl.Functions.* +import io.computenode.cyfra.dsl.GSeq +import io.computenode.cyfra.dsl.Value.* +import io.computenode.cyfra.foton.animation.AnimatedFunctionRenderer.Parameters +import io.computenode.cyfra.foton.animation.AnimationFunctions.* +import io.computenode.cyfra.foton.animation.{AnimatedFunction, AnimatedFunctionRenderer} +import io.computenode.cyfra.utility.Color.* +import io.computenode.cyfra.utility.Math3D.* + +import scala.concurrent.duration.DurationInt +import java.nio.file.Paths +import io.computenode.cyfra.dsl.Control.when + +object AnimatedConnectingForms: + @main + def AnimatedConnectingForms2() = + + def connectingForms(uv: Vec2[Float32])(using AnimationInstant): Int32 = + val p1 = smooth(from = 0.355f, to = 0.4f, duration = 10.seconds) + val p2 = smooth(from = 0.4f, to = 0.355f, duration = 10.seconds, at = 10.seconds) + val const1 = (p1, p1) + val const2 = (p2, p2) + GSeq.gen(uv, next = v => { + when(!(p1 === 0.4f)){ + val abs_v = (abs(v.x), abs(v.y)) + ((abs_v.x * abs_v.x) - (abs_v.y * abs_v.y), 2.0f * abs_v.x * abs_v.y) + const1 + }otherwise{ + val abs_v = (abs(v.x), abs(v.y)) + ((abs_v.x * abs_v.x) - (abs_v.y * abs_v.y), 2.0f * abs_v.x * abs_v.y) + const2 + } + + }).limit(1000).map(length).takeWhile(_ < 2.0f).count + + def connectingFormsColor(uv: Vec2[Float32])(using AnimationInstant): Vec4[Float32] = + val rotatedUv = rotate(uv, Math.PI.toFloat / 3.0f) + val recursionCount = connectingForms(rotatedUv) + val f = min(1f, recursionCount.asFloat / 100f) + val color = interpolate(InterpolationThemes.Red, f) + ( + color.r, + color.g, + color.b, + 1.0f + ) + + val animatedConnectingForms = AnimatedFunction.fromCoord(connectingFormsColor, 20.seconds) + + val renderer = AnimatedFunctionRenderer(Parameters(1024, 1024, 5)) + renderer.renderFramesToDir(animatedConnectingForms, Paths.get("connectingForms")) + diff --git a/src/main/scala/io/computenode/cyfra/samples/foton/AnimatedPlanetAroundSun.scala b/src/main/scala/io/computenode/cyfra/samples/foton/AnimatedPlanetAroundSun.scala new file mode 100644 index 00000000..0b8a6d69 --- /dev/null +++ b/src/main/scala/io/computenode/cyfra/samples/foton/AnimatedPlanetAroundSun.scala @@ -0,0 +1,79 @@ +package io.computenode.cyfra.samples.foton + +import io.computenode.cyfra.dsl.Algebra.{*, given} +import io.computenode.cyfra.dsl.Value.* +import io.computenode.cyfra.foton.animation.AnimationFunctions.smooth +import io.computenode.cyfra.utility.Color.hex +import io.computenode.cyfra.utility.Units.Milliseconds +import io.computenode.cyfra.foton.* +import io.computenode.cyfra.foton.rt.animation.{AnimatedScene, AnimationRtRenderer} +import io.computenode.cyfra.foton.rt.shapes.{Plane, Shape, Sphere} +import io.computenode.cyfra.foton.rt.{Camera, Material} +import scala.concurrent.duration.DurationInt + +import java.nio.file.Paths +import io.computenode.cyfra.foton.animation.AnimationFunctions.* +import io.computenode.cyfra.foton.rt.shapes.Cylinder +import io.computenode.cyfra.dsl.Control.when + +object AnimatedPlanetAroundSun: + @main + def planetRaytrace() = + + val alienPlanetMaterial = Material( + color = vec3(0.1f, 0.7f, 0.6f), + emissive = vec3(0.02f), + percentSpecular = 0.4f, + specularColor = vec3(0.1f, 0.085f, 0.04f), + roughness = 0.3f + ) + + val ringMaterial = Material( + color = vec3(0.6f, 0.6f, 0.7f), + emissive = vec3(0.1f), + percentSpecular = 0.8f, + specularColor = vec3(0.5f), + roughness = 0.1f, + refractionChance = 0.4f, + indexOfRefraction = 1.1f, + refractionRoughness = 0.1f + ) + + val sunMaterial1Color1 = vec3(1.0f, 0.5f, 0.0f) + val sunMaterial1Color2 = vec3(1f, 0f, 0.0f) + + val lightMaterial = Material( + color = (1f, 0.3f, 0.3f), + emissive = vec3(4f) + ) + + + val staticShapes: List[Shape] = List( + Sphere((0f, -140f, 10f), 50f, lightMaterial), + ) + + val scene = AnimatedScene( + shapes = staticShapes ::: List( + Sphere(orbit((0f, 1f, 14f), 8f, 90.seconds, 0.millis, 90f, 390f), 1f, alienPlanetMaterial), + Sphere((-1f, 0.5f, 14f),5f, Material(colorChange(sunMaterial1Color1, sunMaterial1Color2, 900f), emissive = vec3(0.6f, 0.1f, 0.1f))), + Cylinder(orbit((0f, 1f, 14f), 8f, 90.seconds, 0.millis, 90f, 390f),2f, 0f, ringMaterial) + ), + camera = Camera(position = (0f, 0f, -10f)), + duration = 1990.milliseconds + ) + + val parameters = AnimationRtRenderer.Parameters( + width = 640, + height = 480, + superFar = 300f, + pixelIterations = 10000, + iterations = 2, + bgColor = hex("#000000"), + framesPerSecond = 60 + ) + val renderer = AnimationRtRenderer(parameters) + renderer.renderFramesToDir(scene, Paths.get("raytracePlanetAndSun")) + +// Renderable with ffmpeg -framerate 30 -pattern_type sequence -start_number 01 -i frame%02d.png -s:v 1920x1080 -c:v libx264 -crf 17 -pix_fmt yuv420p output.mp4 + +// ffmpeg -t 3 -i output.mp4 -vf "fps=30,scale=720:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" -loop 0 output.gif diff --git a/src/main/scala/io/computenode/cyfra/samples/foton/AnimatedRaytraceEPFL.scala b/src/main/scala/io/computenode/cyfra/samples/foton/AnimatedRaytraceEPFL.scala new file mode 100644 index 00000000..c00f822b --- /dev/null +++ b/src/main/scala/io/computenode/cyfra/samples/foton/AnimatedRaytraceEPFL.scala @@ -0,0 +1,91 @@ +package io.computenode.cyfra.samples.foton + +import io.computenode.cyfra.dsl.Algebra.{*, given} +import io.computenode.cyfra.dsl.Value.* +import io.computenode.cyfra.foton.animation.AnimationFunctions.smooth +import io.computenode.cyfra.utility.Color.hex +import io.computenode.cyfra.utility.Units.Milliseconds +import io.computenode.cyfra.foton.* +import io.computenode.cyfra.foton.rt.animation.{AnimatedScene, AnimationRtRenderer} +import io.computenode.cyfra.foton.rt.shapes.{Plane, Shape, Sphere, Box} +import io.computenode.cyfra.foton.rt.{Camera, Material} +import scala.concurrent.duration.DurationInt + +import java.nio.file.Paths + +object BoxRaytrace: + @main + def raytraceEPFL() = + val lightMaterial = Material( + color = (1f, 0.3f, 0.3f), + emissive = vec3(40f) + ) + + val floorMaterial = Material( + color = vec3(0.5f), + emissive = vec3(0f), + roughness = 0.9f + ) + val wallMaterial = Material( + color = (0.4f, 0.0f, 0.0f), + emissive = vec3(0f), + percentSpecular = 0f, + specularColor = (1f, 0.3f, 0.3f) * 0.1f, + roughness = 1f + ) + + val x = 0f + val y = -1.5f + val z = 15f + + val staticShapes: List[Shape] = List( + + //E + Box((x + 0f, y + 0f, z + 0f), (x + 1f, y + 5f, z + 1f), wallMaterial), + Box((x + 1f, y + 4f, z + 0f), (x + 3f, y + 5f, z + 1f), wallMaterial), + Box((x + 1f, y + 2f, z + 0f), (x + 3f, y + 3f, z + 1f), wallMaterial), + Box((x + 1f, y + 0f, z + 0f), (x + 3f, y + 1f, z + 1f), wallMaterial), + + //P + Box((x + 4f, y + 0f, z + 0f), (x + 5f, y + 5f, z + 1f), wallMaterial), + Box((x + 5f, y + 0f, z + 0f), (x + 7f, y + 1f, z + 1f), wallMaterial), + Box((x + 7f, y + 0f, z + 0f), (x + 8f, y + 2f, z + 1f), wallMaterial), + Box((x + 5f, y + 2f, z + 0f), (x + 8f, y + 3f, z + 1f), wallMaterial), + + // F + Box((x + 9f, y + 0f, z + 0f), (x + 10f, y + 5f, z + 1f), wallMaterial), + Box((x + 10f, y + 0f, z + 0f), (x + 12f, y + 1f, z + 1f), wallMaterial), + Box((x + 10f, y + 2f, z + 0f), (x + 12f, y + 3f, z + 1f), wallMaterial), + + //L + Box((x + 13f, y + 0f, z + 0f), (x + 14f, y + 5f, z + 1f), wallMaterial), + Box((x + 14f, y + 4f, z + 0f), (x + 16f, y + 5f, z + 1f), wallMaterial), + + // Light + Sphere((0f, -140f, 10f), 50f, lightMaterial), + + // Floor + Plane((0f, 3.5f, 0f), (0f, 1f, 0f), floorMaterial), + ) + + val scene = AnimatedScene( + shapes = staticShapes, + camera = Camera(position = (smooth(from = -20f, to = 40f, duration = 6.seconds), 0f, -1f)), + duration = 6.seconds + ) + + val parameters = AnimationRtRenderer.Parameters( + width = 640, + height = 480, + superFar = 300f, + pixelIterations = 10000, + iterations = 2, + bgColor = hex("#ADD8E6"), + framesPerSecond = 30 + ) + val renderer = AnimationRtRenderer(parameters) + renderer.renderFramesToDir(scene, Paths.get("raytraceEPFL")) + +// Renderable with ffmpeg -framerate 30 -pattern_type sequence -start_number 01 -i frame%02d.png -s:v 1920x1080 -c:v libx264 -crf 17 -pix_fmt yuv420p output.mp4 + +// ffmpeg -t 3 -i output.mp4 -vf "fps=30,scale=720:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" -loop 0 output.gif diff --git a/src/main/scala/io/computenode/cyfra/samples/foton/AnimatedSpiral.scala b/src/main/scala/io/computenode/cyfra/samples/foton/AnimatedSpiral.scala new file mode 100644 index 00000000..2f1ba172 --- /dev/null +++ b/src/main/scala/io/computenode/cyfra/samples/foton/AnimatedSpiral.scala @@ -0,0 +1,40 @@ +package io.computenode.cyfra.samples.foton + +import io.computenode.cyfra +import io.computenode.cyfra.* +import io.computenode.cyfra.dsl.Algebra.{*, given} +import io.computenode.cyfra.dsl.Functions.* +import io.computenode.cyfra.dsl.GSeq +import io.computenode.cyfra.dsl.Value.* +import io.computenode.cyfra.foton.animation.AnimatedFunctionRenderer.Parameters +import io.computenode.cyfra.foton.animation.AnimationFunctions.* +import io.computenode.cyfra.foton.animation.{AnimatedFunction, AnimatedFunctionRenderer} +import io.computenode.cyfra.utility.Color.* +import io.computenode.cyfra.utility.Math3D.* + +import scala.concurrent.duration.DurationInt +import java.nio.file.Paths +import io.computenode.cyfra.dsl.Control.when + +object AnimatedSpiral: + @main + def Spiral() = + def spiral(uv: Vec2[Float32])(using a: AnimationInstant): Vec4[Float32] = + val centered = uv + val angle = atan2(centered.y, centered.x) + val radius = length(centered) + val f = abs(sin(10f * radius + 5f * angle + a.time)) + val color = interpolate(InterpolationThemes.Black, f) + val rotatedUv = rotate(uv, Math.PI.toFloat / 3.0f) + ( + color.r, + color.g, + color.b, + 1.0f + ) + + val animatedSpiral = AnimatedFunction.fromCoord(spiral, 2.seconds) + + val renderer = AnimatedFunctionRenderer(Parameters(1024, 1024, 80)) + renderer.renderFramesToDir(animatedSpiral, Paths.get("animatedSpiral")) + diff --git a/src/main/scala/io/computenode/cyfra/samples/foton/AnimatedWaveCircle.scala b/src/main/scala/io/computenode/cyfra/samples/foton/AnimatedWaveCircle.scala new file mode 100644 index 00000000..21860afa --- /dev/null +++ b/src/main/scala/io/computenode/cyfra/samples/foton/AnimatedWaveCircle.scala @@ -0,0 +1,43 @@ +package io.computenode.cyfra.samples.foton + +import io.computenode.cyfra +import io.computenode.cyfra.* +import io.computenode.cyfra.dsl.Algebra.{*, given} +import io.computenode.cyfra.dsl.Functions.* +import io.computenode.cyfra.dsl.GSeq +import io.computenode.cyfra.dsl.Value.* +import io.computenode.cyfra.foton.animation.AnimatedFunctionRenderer.Parameters +import io.computenode.cyfra.foton.animation.AnimationFunctions.* +import io.computenode.cyfra.foton.animation.{AnimatedFunction, AnimatedFunctionRenderer} +import io.computenode.cyfra.utility.Color.* +import io.computenode.cyfra.utility.Math3D.* + +import scala.concurrent.duration.DurationInt +import java.nio.file.Paths +import io.computenode.cyfra.dsl.Control.when + +//This pattern was adapted from https://www.shadertoy.com/view/XsXXDn by Silexars + +object AnimatedWaveCircle: + @main + def animatedWaveCircle() = + def waveColor(uv: Vec2[Float32])(using t: AnimationInstant): Vec4[Float32] = + val aspect = 840f / 473f + val p = (uv.x * aspect, uv.y) + var c = Array[Float32](0f, 0f, 0f) + val l = length(p) + var z = t.time + var uvCompute = p + + for i <- 0 until 3 do + z += 0.07f * (i + 1).toFloat + val multiplyFactor = (sin(z) + 1f) * abs(sin(l * 9f - 2f * z)) + uvCompute = (uvCompute.y +(p.x * (1f/l)) * multiplyFactor, uvCompute.x + (p.y * (1f/l)) * multiplyFactor) + val uvMod = (uvCompute.x.mod(1f), uvCompute.y.mod(1f)) + c(i) = 0.01f / length((uvMod._1 - 0.5f, uvMod._2 - 0.5f)) + + (c(0) / l, c(1) / l, c(2) / l, t.time) + + val animatedStar = AnimatedFunction.fromCoord(waveColor, 8.milliseconds) + val renderer = AnimatedFunctionRenderer(Parameters(840, 473, 30000)) + renderer.renderFramesToDir(animatedStar, Paths.get("animatedWaveCircle")) \ No newline at end of file diff --git a/src/main/scala/io/computenode/cyfra/utility/Color.scala b/src/main/scala/io/computenode/cyfra/utility/Color.scala index e8e99a4a..e41b26a6 100644 --- a/src/main/scala/io/computenode/cyfra/utility/Color.scala +++ b/src/main/scala/io/computenode/cyfra/utility/Color.scala @@ -43,6 +43,8 @@ object Color: object InterpolationThemes: val Blue: InterpolationTheme = ((8f, 22f, 104f) * (1 / 255f), (62f, 82f, 199f) * (1 / 255f), (221f, 233f, 255f) * (1 / 255f)) val Black: InterpolationTheme = ((255f, 255f, 255f) * (1 / 255f), (0f, 0f, 0f), (0f, 0f, 0f)) + val Red: InterpolationTheme = ((104f, 22f, 8f) * (1 / 255f),(199f, 82f, 62f) * (1 / 255f),(255f, 233f, 221f) * (1 / 255f)) + def interpolate(theme: InterpolationTheme, f: Float32): Vec3[Float32] = val (c1, c2, c3) = theme