diff --git a/.gitignore b/.gitignore index 99e4872..3c18e4f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ target/ !**/src/test/**/target/ image.ppm +image.png ### IntelliJ IDEA ### .idea/ diff --git a/src/main/java/App.java b/src/main/java/App.java new file mode 100644 index 0000000..6ad7484 --- /dev/null +++ b/src/main/java/App.java @@ -0,0 +1,23 @@ +import image.Image; +import image.ImageWriter; +import raytracer.RayTracer; +import raytracer.RayTracerConfig; +import raytracer.RayTracerImpl; +import raytracer.RayTracerLights; +import raytracer.scene.BallsScene; +import raytracer.scene.LightsScene; +import util.Dimension; +import java.nio.file.Paths; +import java.util.concurrent.TimeUnit; + +public class App { + public static void main(String[] args) throws Exception{ + ImageWriter writer = new ImageWriter(Math::sqrt); +// RayTracer rayTracer = new RayTracerImpl(new RayTracerConfig(new Dimension(400, 16d/9d), 50, 50), new BallsScene()); + RayTracer rayTracer = new RayTracerLights(new RayTracerConfig(new Dimension(400, 16d/9d), 50, 50), new LightsScene()); + long st = System.currentTimeMillis(); + Image image = rayTracer.render(); + System.out.printf("Processing time: %d seconds\n", TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis() - st)); + writer.writeToFile(Paths.get("image.png"), image); + } +} diff --git a/src/main/java/Main.java b/src/main/java/Main.java deleted file mode 100644 index 1d7e4da..0000000 --- a/src/main/java/Main.java +++ /dev/null @@ -1,123 +0,0 @@ -import math.Point; -import math.Ray; -import math.Vector; -import util.camera.BlurCamera; -import util.camera.Camera; -import util.camera.ClearCamera; -import util.collections.*; -import util.*; -import util.material.Dielectric; -import util.material.Lambertian; -import util.material.Material; -import util.material.Metal; - -import java.io.BufferedWriter; -import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Random; -import java.util.function.Function; - -import static util.Util.clamp; - -public class Main { - public static void main(String[] args) throws Exception{ - try (OutputStream stream = Files.newOutputStream(Paths.get("image.ppm")); - OutputStreamWriter outputStreamWriter = new OutputStreamWriter(stream); - BufferedWriter writer = new BufferedWriter(outputStreamWriter)){ - long start = System.currentTimeMillis(); - new Main(writer); - System.out.println("Processing time: " + (((double)(System.currentTimeMillis() - start))/1000)); - } - } - - public Main(BufferedWriter writer) throws IOException { - //Image - double aspectRatio = 16.0/9.0; - int width = 400; - int height = (int)(width / aspectRatio); - int samplesPerPixel = 100; - int depth = 50; - //World - HittableList world = new HittableArrayList(); - - Material ground = new Lambertian(new Color(0.5, 0.5, 0.5)); - Material left = new Lambertian(new Color(0, 0, 1)); - Material center = new Metal(new Color(0.8, 0.6, 0.2)); - Material right = new Dielectric(1.5); - - world.add(new Sphere(new Point(0, -1000, 0), 1000, ground)); - - world.add(new Sphere(new Point(-2, 1, 0), 1, left)); - world.add(new Sphere(new Point(0, 1, 0), 1, center)); - world.add(new Sphere(new Point(2, 1, 0), 1, right)); -// Util.fillScene(world); - //Camera - Point lookFrom = new Point(5, 5, 13); - Point lookAt = new Point(0, 1, 0); - Vector worldNormal = new Vector(0, 1, 0); - double fov = 20; - double aperture = 0.1; - double focusDist = new Vector(lookAt, lookFrom).length(); - - Camera camera = new ClearCamera(lookFrom, lookAt, worldNormal, fov, aspectRatio); -// Camera camera = new BlurCamera(lookFrom, lookAt, worldNormal, fov, aspectRatio, aperture, focusDist); - - Random random = new Random(); - //Rendering - writer.write("P3\n" + width + " " + height + "\n255\n"); - for(int j = height - 1; j > -1; j--){ - System.out.println("Lines remaining " + (j + 1)); - for(int i = 0; i < width; i++){ - Color pixelColor = new Color(); - for(int s = 0; s < samplesPerPixel; s++){ - double u = ((double) i + random.nextDouble())/(width - 1); - double v = ((double) j + random.nextDouble())/(height - 1); - Ray ray = camera.getRay(u, v); - Color color = rayColor(ray, world, depth).scale(1d/samplesPerPixel); -// Color color = Util.normalColor(ray, world).scale(1d/samplesPerPixel); - pixelColor.setRed(pixelColor.getRed() + color.getRed()) - .setGreen(pixelColor.getGreen() + color.getGreen()) - .setBlue(pixelColor.getBlue() + color.getBlue()); - } - writeColor(writer, pixelColor, Math::sqrt); - } - - } - writer.flush(); - } - - private Color rayColor(Ray r, Hittable world, int depth){ - if(depth <= 0){ - return new Color(0, 0, 0); - } - HitRecord record = new HitRecord(); - - if(world.hit(r, 0.00001, Double.POSITIVE_INFINITY, record)){ - Ray scattered = new Ray(); - Color attenuation = new Color(); - if(record.getMaterial().scatter(r, record, attenuation, scattered)){ - Color newColor = rayColor(scattered, world, depth - 1); - return attenuation.setRed(attenuation.getRed() * newColor.getRed()) - .setGreen(attenuation.getGreen() * newColor.getGreen()) - .setBlue(attenuation.getBlue() * newColor.getBlue()); - } - return new Color(0, 0, 0); - } - - Vector unitDirection = r.getDirection().unit(); - double t = 0.5 * (unitDirection.getY() + 1.0); - return new Color((1.0 - t) + (t * 0.5), (1.0 - t) + (t * 0.7), (1.0 - t) + (t)); - } - - - private void writeColor(BufferedWriter writer, Color color, Function gammaCorrectionFunction) throws IOException { - double r = gammaCorrectionFunction.apply(color.getRed()); - double g = gammaCorrectionFunction.apply(color.getGreen()); - double b = gammaCorrectionFunction.apply(color.getBlue()); - writer.write((int)(256 * clamp(r, 0d, 0.999)) + " " + (int)(256 * clamp(g, 0d, 0.999)) + " " + (int)(256 * clamp(b, 0d, 0.999)) + '\n'); - } - -} diff --git a/src/main/java/Perlin.java b/src/main/java/Perlin.java new file mode 100644 index 0000000..816bc27 --- /dev/null +++ b/src/main/java/Perlin.java @@ -0,0 +1,68 @@ +import math.Color; +import math.Point; +import math.Points; +import util.PerlinNoise; + +import javax.imageio.ImageIO; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.function.Function; + +import static util.Util.clamp; + +public class Perlin { + public static void main(String[] args) throws Exception{ + try (OutputStream stream = Files.newOutputStream(Paths.get("image.ppm")); + OutputStreamWriter outputStreamWriter = new OutputStreamWriter(stream); + BufferedWriter writer = new BufferedWriter(outputStreamWriter)){ + long start = System.currentTimeMillis(); + new Perlin(writer); + System.out.println("Processing time: " + (((double)(System.currentTimeMillis() - start))/1000)); + } + Convertor.main(args); + } + + Perlin(BufferedWriter writer) throws Exception{ + int width = 100; + int height = 100; + + PerlinNoise perlinNoise = new PerlinNoise(); + PerlinNoise r = new PerlinNoise(); + PerlinNoise g = new PerlinNoise(); + PerlinNoise b = new PerlinNoise(); + + writer.write("P3\n" + width + " " + height + "\n255\n"); + for(int j = 0; j < height; j++){ + System.out.print("Lines remaining " + (height - j) + "\r"); + for(int i = 0; i < width; i++){ + Color red = new Color(1, 0, 0).scale(15 * r.turbulence(Points.scale(new Point(i, 0, j), 0.1), 1)); + Color green = new Color(0, 1, 0).scale(10 * g.turbulence(Points.scale(new Point(i, 0, j), 0.1), 1)); + Color blue = new Color(0, 0, 1).scale(10 * b.turbulence(Points.scale(new Point(i, 0, j), 0.1), 1)); + + Color start = new Color(); + + Color mixed = new Color(start.getRed() + red.getRed() + green.getRed() + blue.getRed(), + start.getGreen() + red.getGreen() + green.getGreen() + blue.getGreen(), + start.getBlue() + red.getBlue() + green.getBlue() + blue.getBlue()); + mixed = mixed.scale((1d/4d)); + double scaling = perlinNoise.noise(Points.scale(new Point(i, 0, j), 0.1)); + Color pixelColor = new Color(1, 1, 1).scale(scaling); + writeColor(writer, mixed, x -> x); + } + } + writer.flush(); + } + + private void writeColor(BufferedWriter writer, Color color, Function gammaCorrectionFunction) throws IOException { + double r = gammaCorrectionFunction.apply(color.getRed()); + double g = gammaCorrectionFunction.apply(color.getGreen()); + double b = gammaCorrectionFunction.apply(color.getBlue()); + writer.write((int)(256 * clamp(r, 0d, 0.999)) + " " + (int)(256 * clamp(g, 0d, 0.999)) + " " + (int)(256 * clamp(b, 0d, 0.999)) + '\n'); + } + +} diff --git a/src/main/java/util/camera/BlurCamera.java b/src/main/java/camera/BlurCamera.java similarity index 97% rename from src/main/java/util/camera/BlurCamera.java rename to src/main/java/camera/BlurCamera.java index 26e1c91..81f117e 100644 --- a/src/main/java/util/camera/BlurCamera.java +++ b/src/main/java/camera/BlurCamera.java @@ -1,10 +1,9 @@ -package util.camera; +package camera; import math.Point; import math.Ray; import math.Vector; import math.Vectors; -import util.camera.Camera; public class BlurCamera implements Camera { private final Point origin; @@ -38,7 +37,7 @@ public Ray getRay(double s, double t){ Vector rd = Vectors.randomInUnitSphere().multiply(lensRadius); Vector offset = u.multiply(rd.getX()).add(v.multiply(rd.getY())); - Vector vector = horizontal.multiply(s).add(vertical.multiply(t)); + Vector vector = horizontal.multiply(s).add(vertical.multiply(1 - t)); return new Ray(origin.move(offset), new Vector(origin, lowerLeftCorner).add(vector).subtract(offset)); } diff --git a/src/main/java/util/camera/Camera.java b/src/main/java/camera/Camera.java similarity index 79% rename from src/main/java/util/camera/Camera.java rename to src/main/java/camera/Camera.java index 8203f23..90a3864 100644 --- a/src/main/java/util/camera/Camera.java +++ b/src/main/java/camera/Camera.java @@ -1,4 +1,4 @@ -package util.camera; +package camera; import math.Ray; diff --git a/src/main/java/util/camera/ClearCamera.java b/src/main/java/camera/ClearCamera.java similarity index 97% rename from src/main/java/util/camera/ClearCamera.java rename to src/main/java/camera/ClearCamera.java index 7bf5aed..f898c98 100644 --- a/src/main/java/util/camera/ClearCamera.java +++ b/src/main/java/camera/ClearCamera.java @@ -1,10 +1,11 @@ -package util.camera; +package camera; import math.Point; import math.Ray; import math.Vector; import math.Vectors; + public class ClearCamera implements Camera{ private final Point origin; private final Point lowerLeftCorner; @@ -18,7 +19,6 @@ public ClearCamera(Point lookFrom, Point lookAt, Vector worldNormal, double vert double viewportWidth = aspectRatio * viewportHeight; double focalLength = 1.0; - Vector w = new Vector(lookAt, lookFrom).unit(); Vector u = Vectors.cross(worldNormal, w).unit(); Vector v = Vectors.cross(w, u); @@ -32,7 +32,7 @@ public ClearCamera(Point lookFrom, Point lookAt, Vector worldNormal, double vert @Override public Ray getRay(double s, double t) { - Vector vector = horizontal.multiply(s).add(vertical.multiply(t)); + Vector vector = horizontal.multiply(s).add(vertical.multiply(1 - t)); return new Ray(origin, new Vector(origin, lowerLeftCorner).add(vector)); } @@ -51,4 +51,5 @@ public Vector getHorizontal() { public Vector getVertical() { return vertical; } + } diff --git a/src/main/java/camera/MotionBlurCamera.java b/src/main/java/camera/MotionBlurCamera.java new file mode 100644 index 0000000..5f97d7b --- /dev/null +++ b/src/main/java/camera/MotionBlurCamera.java @@ -0,0 +1,24 @@ +package camera; + +import math.Ray; + +public class MotionBlurCamera implements Camera{ + private final Camera camera; + private final double shutterOpeningMoment; + private final double shutterClosingMoment; + + public MotionBlurCamera(Camera camera, double shutterOpeningMoment, double shutterClosingMoment) { + if(shutterClosingMoment < shutterOpeningMoment){ + throw new IllegalArgumentException("shutterClosingMoment can`t be less then shutterOpeningMoment"); + } + this.camera = camera; + this.shutterOpeningMoment = shutterOpeningMoment; + this.shutterClosingMoment = shutterClosingMoment; + } + + @Override + public Ray getRay(double s, double t) { + double shutterOpenMoment = (Math.random() * (shutterClosingMoment - shutterOpeningMoment)) + shutterOpeningMoment; + return camera.getRay(s, t).setTimeMoment(shutterOpenMoment); + } +} diff --git a/src/main/java/image/ArrayImage.java b/src/main/java/image/ArrayImage.java new file mode 100644 index 0000000..f96564e --- /dev/null +++ b/src/main/java/image/ArrayImage.java @@ -0,0 +1,48 @@ +package image; + +import math.Color; +import util.Pair; + +import java.util.*; + +public class ArrayImage implements Image{ + private final int xLen; + private final int yLen; + private final Color[][] colors; + + public ArrayImage(int xLen, int yLen) { + this.xLen = xLen; + this.yLen = yLen; + colors = new Color[xLen][yLen]; + } + + public ArrayImage(Color[][] colors) { + this.colors = colors; + this.xLen = colors.length; + this.yLen = colors[0].length; + } + + @Override + public Pair getSize() { + return new Pair<>(xLen, yLen); + } + + @Override + public Color getColor(int x, int y) { + if(x >= xLen || y >= yLen){ + throw new IllegalArgumentException("Coordinate out of image size"); + } + return colors[x][y]; + } + + @Override + public Iterator iterator() { + List list = new ArrayList<>(); + for (int i = 0; i < xLen; i++) { + for (int j = 0; j < yLen; j++) { + list.add(new Pixel(i, j, colors[i][j])); + } + } + return list.iterator(); + } +} diff --git a/src/main/java/image/ColorImage.java b/src/main/java/image/ColorImage.java new file mode 100644 index 0000000..7c3bf81 --- /dev/null +++ b/src/main/java/image/ColorImage.java @@ -0,0 +1,45 @@ +package image; + +import math.Color; +import util.Pair; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class ColorImage implements Image{ + private final int x; + private final int y; + private final Color color; + + public ColorImage(int x, int y, Color color) { + this.x = x; + this.y = y; + this.color = color; + } + + public ColorImage(Color color) { + this(1, 1, color); + } + + @Override + public Pair getSize() { + return new Pair<>(x, y); + } + + @Override + public Color getColor(int x, int y) { + return color; + } + + @Override + public Iterator iterator() { + List pixels = new ArrayList<>(); + for (int i = 0; i < x; i++){ + for (int j = 0; j < y; j++) { + pixels.add(new Pixel(i, j, color)); + } + } + return pixels.iterator(); + } +} diff --git a/src/main/java/image/Image.java b/src/main/java/image/Image.java new file mode 100644 index 0000000..8c40a4c --- /dev/null +++ b/src/main/java/image/Image.java @@ -0,0 +1,9 @@ +package image; + +import math.Color; +import util.Pair; + +public interface Image extends Iterable{ + Pair getSize(); + Color getColor(int x, int y); +} diff --git a/src/main/java/image/ImageWriter.java b/src/main/java/image/ImageWriter.java new file mode 100644 index 0000000..cb6a4cc --- /dev/null +++ b/src/main/java/image/ImageWriter.java @@ -0,0 +1,48 @@ +package image; + +import math.Color; +import util.Pair; + + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.function.Function; + +public class ImageWriter { + private final Function gammaCorrection; + + public ImageWriter(Function gammaCorrection) { + this.gammaCorrection = gammaCorrection; + } + + public void writeToBuffer(BufferedImage imageBuf, Image image) { + for (Pixel px: image) { + Color now = px.getColor(); + Color corrected = new Color(gammaCorrection.apply(now.getRed()), gammaCorrection.apply(now.getGreen()), gammaCorrection.apply(now.getBlue())); + imageBuf.setRGB(px.getX(), px.getY(), corrected.toRGB()); + } + } + + public BufferedImage write(Image image) { + Pair size = image.getSize(); + BufferedImage bufferedImage = new BufferedImage(size.getFirst(), size.getSecond(), BufferedImage.TYPE_INT_RGB); + writeToBuffer(bufferedImage, image); + return bufferedImage; + } + + public void writeToFile(Path file, BufferedImage bufferedImage, OutputFormat format) throws IOException { + ImageIO.write(bufferedImage, format.type, file.toFile()); + } + + public void writeToFile(Path file, BufferedImage bufferedImage) throws IOException { + writeToFile(file, bufferedImage, OutputFormat.PNG); + } + + public void writeToFile(Path file, Image image) throws IOException{ + writeToFile(file, write(image)); + } + +} diff --git a/src/main/java/image/ListImage.java b/src/main/java/image/ListImage.java new file mode 100644 index 0000000..56b05d3 --- /dev/null +++ b/src/main/java/image/ListImage.java @@ -0,0 +1,43 @@ +package image; + +import math.Color; +import util.Pair; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +public class ListImage implements Image{ + private final int xLen; + private final int yLen; + private final List list = new LinkedList<>(); + + public ListImage(int xLen, int yLen) { + this.xLen = xLen; + this.yLen = yLen; + } + + @Override + public Iterator iterator() { + return list.iterator(); + } + + @Override + public Pair getSize() { + return new Pair(xLen, yLen); + } + + @Override + public Color getColor(int x, int y) { + if(x >= xLen || y >= yLen){ + throw new IllegalArgumentException("Coordinate out of image size"); + } + for (Pixel px: list) { + if(px.getX() == x && px.getY() == y){ + return px.getColor(); + } + } + throw new IllegalStateException("No color in this coordinate"); + } +} diff --git a/src/main/java/image/OutputFormat.java b/src/main/java/image/OutputFormat.java new file mode 100644 index 0000000..5e25a75 --- /dev/null +++ b/src/main/java/image/OutputFormat.java @@ -0,0 +1,12 @@ +package image; + +public enum OutputFormat { + PNG("png"), + JPEG("jpg"), + BMP("bmp"); + + public final String type; + OutputFormat(String type) { + this.type = type; + } +} diff --git a/src/main/java/image/Pixel.java b/src/main/java/image/Pixel.java new file mode 100644 index 0000000..7a79bfd --- /dev/null +++ b/src/main/java/image/Pixel.java @@ -0,0 +1,28 @@ +package image; + +import math.Color; +import math.Colors; + +public class Pixel { + private final int x; + private final int y; + private final Color color; + + public Pixel(int x, int y, Color color) { + this.x = x; + this.y = y; + this.color = new Colors.ImmutableColor(color); + } + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + public Color getColor() { + return color; + } +} diff --git a/src/main/java/util/material/Dielectric.java b/src/main/java/material/Dielectric.java similarity index 75% rename from src/main/java/util/material/Dielectric.java rename to src/main/java/material/Dielectric.java index b83e2b0..033f91f 100644 --- a/src/main/java/util/material/Dielectric.java +++ b/src/main/java/material/Dielectric.java @@ -1,23 +1,28 @@ -package util.material; +package material; import math.Ray; import math.Vector; import math.Vectors; -import util.Color; -import util.HitRecord; +import math.Color; +import math.HitRecord; public class Dielectric implements Material{ private final double refractionIndex; - + private final Color albedo; public Dielectric(double refractionIndex) { + this(refractionIndex, new Color(1, 1, 1)); + } + + @SuppressWarnings("I dont know is it right to add albedo") + public Dielectric(double refractionIndex, Color albedo) { this.refractionIndex = refractionIndex; + this.albedo = albedo; } @Override public boolean scatter(Ray rayIn, HitRecord record, Color attenuation, Ray scattered) { - attenuation.set(new Color(1, 1, 1)); + attenuation.set(albedo); double refractionRatio = record.isFrontFace() ? (1.0/refractionIndex) : refractionIndex; - Vector unitDirection = rayIn.getDirection().unit(); double cosTheta = Math.min(1d, Vectors.dot(unitDirection.negate(), record.getNormal())); @@ -30,8 +35,7 @@ public boolean scatter(Ray rayIn, HitRecord record, Color attenuation, Ray scatt }else{ direction = Vectors.refract(unitDirection, record.getNormal(), refractionRatio); } - scattered.setOrigin(record.getPoint()); - scattered.setDirection(direction); + scattered.setOrigin(record.getPoint()).setDirection(direction).setTimeMoment(rayIn.getTimeMoment()); return true; } diff --git a/src/main/java/material/DiffuseLight.java b/src/main/java/material/DiffuseLight.java new file mode 100644 index 0000000..c26247e --- /dev/null +++ b/src/main/java/material/DiffuseLight.java @@ -0,0 +1,28 @@ +package material; + +import math.*; +import texture.SolidColorTexture; +import texture.Texture; + +public class DiffuseLight implements Material{ + private final Texture texture; + + public DiffuseLight(Color color) { + this.texture = new SolidColorTexture(color); + } + + public DiffuseLight(Texture texture) { + this.texture = texture; + } + + @Override + public boolean scatter(Ray rayIn, HitRecord record, Color attenuation, Ray scattered) { + scattered.setOrigin(new Point()).setDirection(new Vector()); + return false; + } + + @Override + public Color emitted(double u, double v, Point p) { + return texture.value(u, v, p); + } +} diff --git a/src/main/java/util/material/Lambertian.java b/src/main/java/material/Lambertian.java similarity index 54% rename from src/main/java/util/material/Lambertian.java rename to src/main/java/material/Lambertian.java index fd98cbd..ce581a0 100644 --- a/src/main/java/util/material/Lambertian.java +++ b/src/main/java/material/Lambertian.java @@ -1,19 +1,24 @@ -package util.material; +package material; import math.Ray; import math.Vector; import math.Vectors; -import util.Color; -import util.HitRecord; +import math.Color; +import texture.SolidColorTexture; +import texture.Texture; +import math.HitRecord; /** * @see Lambertian reflection */ public class Lambertian implements Material{ - private final Color albedo; - + private final Texture albedo; public Lambertian(Color albedo) { - this.albedo = albedo; + this.albedo = new SolidColorTexture(albedo); + } + + public Lambertian(Texture texture) { + this.albedo = texture; } @Override @@ -22,9 +27,8 @@ public boolean scatter(Ray rayIn, HitRecord record, Color attenuation, Ray scatt if(scatteredDirection.nearZero()){ scatteredDirection = record.getNormal(); } - scattered.setDirection(scatteredDirection); - scattered.setOrigin(record.getPoint()); - attenuation.set(albedo); + scattered.setDirection(scatteredDirection).setOrigin(record.getPoint()).setTimeMoment(rayIn.getTimeMoment()); + attenuation.set(albedo.value(record.getU(), record.getV(), record.getPoint())); return true; } } diff --git a/src/main/java/material/Material.java b/src/main/java/material/Material.java new file mode 100644 index 0000000..c436b98 --- /dev/null +++ b/src/main/java/material/Material.java @@ -0,0 +1,14 @@ +package material; + +import math.Point; +import math.Ray; +import math.Color; +import math.HitRecord; + +public interface Material { + boolean scatter(Ray rayIn, HitRecord record, Color attenuation, Ray scattered); + + default Color emitted(double u, double v, Point p){ + return new Color(0, 0, 0); + } +} diff --git a/src/main/java/material/Metal.java b/src/main/java/material/Metal.java new file mode 100644 index 0000000..405d379 --- /dev/null +++ b/src/main/java/material/Metal.java @@ -0,0 +1,49 @@ +package material; + +import math.Ray; +import math.Vector; +import math.Vectors; +import math.Color; +import texture.SolidColorTexture; +import texture.Texture; +import math.HitRecord; + +public class Metal implements Material{ + private final Texture albedo; + //Для создания нечеткого отражения + private final double fuzz; + + public Metal(Color albedo) { + this.albedo = new SolidColorTexture(albedo); + fuzz = 0d; + } + + + public Metal(Color albedo, double fuzz) { + this.albedo = new SolidColorTexture(albedo); + this.fuzz = fuzz; + } + + public Metal(Texture texture){ + this.albedo = texture; + this.fuzz = 0d; + } + + public Metal(Texture texture, double fuzz){ + this.albedo = texture; + this.fuzz = fuzz; + } + + @Override + public boolean scatter(Ray rayIn, HitRecord record, Color attenuation, Ray scattered) { + Vector reflected = Vectors.reflect(rayIn.getDirection().unit(), record.getNormal()); + scattered.setOrigin(record.getPoint()) + .setDirection(reflected.add(Vectors.randomInUnitSphere().multiply(fuzz))) + .setTimeMoment(rayIn.getTimeMoment()); + + boolean scatter = Vectors.dot(scattered.getDirection(), record.getNormal()) > 0; + if (scatter) + attenuation.set(albedo.value(record.getU(), record.getV(), record.getPoint())); + return scatter; + } +} diff --git a/src/main/java/util/Color.java b/src/main/java/math/Color.java similarity index 73% rename from src/main/java/util/Color.java rename to src/main/java/math/Color.java index 5aab4b6..73dbd50 100644 --- a/src/main/java/util/Color.java +++ b/src/main/java/math/Color.java @@ -1,7 +1,9 @@ -package util; +package math; import java.util.Objects; +import static util.Util.clamp; + public class Color { //[0;1] private double red = 0d; @@ -15,9 +17,6 @@ public Color(double red, double green, double blue) { this.red = red; this.green = green; this.blue = blue; -// if(!(inRange(red) && inRange(green) && inRange(blue))){ -// throw new IllegalArgumentException("Color not in range: " + this); -// } } public Color(Color color){ @@ -32,6 +31,31 @@ public void set(Color color){ this.blue = color.getBlue(); } + public static Color getByRGB(int rgb){ + int r = (rgb >> 16) & 0xff; + int g = (rgb >> 8) & 0xff; + int b = (rgb) & 0xff; + return getByRGB(r, g, b); + } + + + public int toRGB(){ + int r = (int)(256 * clamp(red, 0d, 0.999)); + int g = (int)(256 * clamp(green, 0d, 0.999)); + int b = (int)(256 * clamp(blue, 0d, 0.999)); + + int ans = 0; + ans += (r << 16); + ans += (g << 8); + ans += b; + + return ans & 0xffffff; + } + + public static Color getByRGB(int r, int g, int b){ + return new Color((float)r/255, (float)g/255, (float)b/255); + } + public Color scale(double t){ return new Color(red * t, green * t, blue * t); } @@ -41,9 +65,6 @@ public double getRed() { } public Color setRed(double red) { -// if(!inRange(red)){ -// throw new IllegalArgumentException("Color not in range: " + this); -// } this.red = red; return this; } @@ -53,9 +74,6 @@ public double getGreen() { } public Color setGreen(double green) { -// if(!inRange(green)){ -// throw new IllegalArgumentException("Color not in range: " + this); -// } this.green = green; return this; } @@ -65,9 +83,6 @@ public double getBlue() { } public Color setBlue(double blue) { -// if(!inRange(blue)){ -// throw new IllegalArgumentException("Color not in range: " + this); -// } this.blue = blue; return this; } diff --git a/src/main/java/math/Colors.java b/src/main/java/math/Colors.java new file mode 100644 index 0000000..12aa9a8 --- /dev/null +++ b/src/main/java/math/Colors.java @@ -0,0 +1,47 @@ +package math; + +public class Colors { + public final static Color WHITE = new ImmutableColor(1, 1, 1); + public final static Color RED = new ImmutableColor(1, 0, 0); + public final static Color GREEN = new ImmutableColor(0, 1, 0); + public final static Color BLUE = new ImmutableColor(0, 0, 1); + public final static Color BLACK = new ImmutableColor(0, 0, 0); + public final static Color PURPLE = ImmutableColor.getByRGB(128,0,128); + + public static Color add(Color a, Color b){ + return new Color(a.getRed() + b.getRed(), a.getGreen() + b.getGreen(), a.getBlue() + b.getBlue()); + } + + public static Color multiply(Color a, Color b){ + return new Color(a.getRed() * b.getRed(), a.getGreen() * b.getGreen(), a.getBlue() * b.getBlue()); + } + + public static class ImmutableColor extends Color{ + + public ImmutableColor() { + } + + public ImmutableColor(double red, double green, double blue) { + super(red, green, blue); + } + + public ImmutableColor(Color color) { + super(color); + } + + @Override + public Color setRed(double red) { + return this; + } + + @Override + public Color setBlue(double blue) { + return this; + } + + @Override + public Color setGreen(double green) { + return this; + } + } +} diff --git a/src/main/java/util/HitRecord.java b/src/main/java/math/HitRecord.java similarity index 56% rename from src/main/java/util/HitRecord.java rename to src/main/java/math/HitRecord.java index c48880d..696dbce 100644 --- a/src/main/java/util/HitRecord.java +++ b/src/main/java/math/HitRecord.java @@ -1,32 +1,45 @@ -package util; +package math; -import math.Point; -import math.Ray; -import math.Vector; -import math.Vectors; -import util.material.Material; +import material.Material; +import util.Pair; public class HitRecord{ private Point point = new Point(); private Vector normal = new Vector(); private double t = 0d; + private double u; + private double v; private boolean frontFace = true; private Material material; + public HitRecord() { } + public HitRecord(HitRecord record){ + this.point = new Point(record.getPoint()); + this.normal = new Vector(record.getNormal()); + this.t = record.getT(); + this.u = record.getU(); + this.v = record.getV(); + this.frontFace = record.isFrontFace(); + this.material = record.getMaterial(); + } - public void set(HitRecord record){ + public HitRecord set(HitRecord record){ point = record.point; normal = record.normal; t = record.t; + u = record.u; + v = record.v; frontFace = record.frontFace; material = record.getMaterial(); + return this; } - public void setFaceNormal(Ray r, Vector outwardNormal){ + public HitRecord setFaceNormal(Ray r, Vector outwardNormal){ frontFace = Vectors.dot(r.getDirection(), outwardNormal) < 0; normal = frontFace ? outwardNormal : outwardNormal.negate(); + return this; } public Point getPoint() { @@ -73,4 +86,27 @@ public HitRecord setMaterial(Material material) { this.material = material; return this; } + + public HitRecord setU(double u) { + this.u = u; + return this; + } + + public HitRecord setV(double v) { + this.v = v; + return this; + } + public HitRecord setUV(Pair uv) { + this.u = uv.getFirst(); + this.v = uv.getSecond(); + return this; + } + + public double getU() { + return u; + } + + public double getV() { + return v; + } } diff --git a/src/main/java/math/Interval.java b/src/main/java/math/Interval.java new file mode 100644 index 0000000..31dd682 --- /dev/null +++ b/src/main/java/math/Interval.java @@ -0,0 +1,77 @@ +package math; + +import java.util.Objects; + +public class Interval { + private double min; + private double max; + + public Interval(double min, double max) { + this.min = min; + this.max = max; + } + + public Interval(Interval interval){ + this.min = interval.getMin(); + this.max = interval.getMax(); + } + + public Interval(Interval a, Interval b){ + this.min = Math.min(a.getMin(), b.getMin()); + this.max = Math.max(a.getMax(), b.getMax()); + } + + public boolean contains(double x){ + return min <= x && x <= max; + } + + public Interval expand(double delta){ + return new Interval(min - delta/2, max + delta/2); + } + + public Interval scale(double t){ + return new Interval(min * t, max * t); + } + + public double size(){ + return max - min; + } + + public Interval setMin(double min) { + this.min = min; + return this; + } + + public Interval setMax(double max) { + this.max = max; + return this; + } + + public double getMin() { + return min; + } + + public double getMax() { + return max; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Interval interval)) return false; + return Double.compare(interval.min, min) == 0 && Double.compare(interval.max, max) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(min, max); + } + + @Override + public String toString() { + return "Interval{" + + "min=" + min + + ", max=" + max + + '}'; + } +} diff --git a/src/main/java/math/Point.java b/src/main/java/math/Point.java index f159382..8ac8f4f 100644 --- a/src/main/java/math/Point.java +++ b/src/main/java/math/Point.java @@ -7,8 +7,6 @@ public class Point { private double y = 0d; private double z = 0d; - public static final Point ORIGIN_POINT = new Point(); - public Point() { } diff --git a/src/main/java/math/Points.java b/src/main/java/math/Points.java new file mode 100644 index 0000000..67d373e --- /dev/null +++ b/src/main/java/math/Points.java @@ -0,0 +1,10 @@ +package math; + +public class Points { + public static double[] toArray(Point point){ + return new double[]{point.getX(), point.getY(), point.getZ()}; + } + public static Point scale(Point point, double t){ + return new Point(point.getX() * t, point.getY() * t, point.getZ() * t); + } +} diff --git a/src/main/java/math/Ray.java b/src/main/java/math/Ray.java index 65c3c75..1ec1f17 100644 --- a/src/main/java/math/Ray.java +++ b/src/main/java/math/Ray.java @@ -3,7 +3,7 @@ public class Ray { private Point origin = new Point(); private Vector direction = new Vector(); - + private double timeMoment = 0; public Ray() { } @@ -12,6 +12,11 @@ public Ray(Point origin, Vector direction) { this.direction = direction; } + public Ray(Point origin, Vector direction, double timeMoment) { + this(origin, direction); + this.timeMoment = timeMoment; + } + public Point at(double t){ return origin.move(direction.multiply(t)); } @@ -20,15 +25,26 @@ public Point getOrigin() { return origin; } - public void setOrigin(Point origin) { + public Ray setOrigin(Point origin) { this.origin = origin; + return this; } public Vector getDirection() { return direction; } - public void setDirection(Vector direction) { + public Ray setDirection(Vector direction) { this.direction = direction; + return this; + } + + public double getTimeMoment() { + return timeMoment; + } + + public Ray setTimeMoment(double timeMoment) { + this.timeMoment = timeMoment; + return this; } } diff --git a/src/main/java/math/Vector.java b/src/main/java/math/Vector.java index fc905d6..679db8e 100644 --- a/src/main/java/math/Vector.java +++ b/src/main/java/math/Vector.java @@ -19,10 +19,14 @@ public Vector(double x, double y, double z) { this.z = z; } + public Vector(Point point){ + this(new Point(), point); + } + public Vector(Point point1, Point point2){ - this.x = -point1.getX() + point2.getX(); - this.y = -point1.getY() + point2.getY(); - this.z = -point1.getZ() + point2.getZ(); + this.x = point2.getX() - point1.getX(); + this.y = point2.getY() - point1.getY(); + this.z = point2.getZ() - point1.getZ(); } public Vector(Vector vector){ diff --git a/src/main/java/math/Vectors.java b/src/main/java/math/Vectors.java index ba435a3..55d80a1 100644 --- a/src/main/java/math/Vectors.java +++ b/src/main/java/math/Vectors.java @@ -55,7 +55,14 @@ public static Vector randomInUnitDisk(){ } } + public static double[] toArray(Vector vector){ + return new double[]{vector.getX(), vector.getY(), vector.getZ()}; + } + public static Vector randomUnitVector(){ return randomInUnitSphere().unit(); } + + private Vectors() { + } } diff --git a/src/main/java/objects/AABB.java b/src/main/java/objects/AABB.java new file mode 100644 index 0000000..c5c3f4a --- /dev/null +++ b/src/main/java/objects/AABB.java @@ -0,0 +1,85 @@ +package objects; + +import math.*; + +import static java.lang.Math.max; +import static java.lang.Math.min; + +public class AABB { + private final Interval xInterval; + private final Interval yInterval; + private final Interval zInterval; + + + public AABB() { + this(new Point(), new Point()); + } + + + + public AABB(Point min, Point max) { + xInterval = new Interval(Math.min(min.getX(), max.getX()), Math.max(min.getX(), max.getX())); + yInterval = new Interval(Math.min(min.getY(), max.getY()), Math.max(min.getY(), max.getY())); + zInterval = new Interval(Math.min(min.getZ(), max.getZ()), Math.max(min.getZ(), max.getZ())); + } + + public AABB(Interval xInterval, Interval yInterval, Interval zInterval) { + this.xInterval = xInterval; + this.yInterval = yInterval; + this.zInterval = zInterval; + } + + public boolean hit(Ray r, Interval tInterval){ + Interval[] axis = {xInterval, yInterval, zInterval}; + double[] vecAxis = Vectors.toArray(r.getDirection()); + double[] pointCo = Points.toArray(r.getOrigin()); + Interval tmpInterval = new Interval(tInterval); + for (int i = 0; i < 3; i++){ + double t0 = Math.min((axis[i].getMin() - pointCo[i])/ vecAxis[i], (axis[i].getMax() - pointCo[i])/vecAxis[i]); + double t1 = Math.max((axis[i].getMin() - pointCo[i])/vecAxis[i], (axis[i].getMax() - pointCo[i])/vecAxis[i]); + + tmpInterval.setMin(Math.max(t0, tmpInterval.getMin())); + tmpInterval.setMax(Math.min(t1, tmpInterval.getMax())); + + if (tmpInterval.getMax() <= tmpInterval.getMin()) + return false; + } + return true; + } + + public Interval getX() { + return xInterval; + } + + public Interval getY() { + return yInterval; + } + + public Interval getZ() { + return zInterval; + } + + public static AABB surroundingBox(AABB box0, AABB box1){ + Interval x = new Interval(box0.xInterval, box1.xInterval); + Interval y = new Interval(box0.yInterval, box1.yInterval); + Interval z = new Interval(box0.zInterval, box1.zInterval); + return new AABB(x, y, z); + } + + public AABB pad(){ + double delta = 0.1; + Interval newX = (xInterval.size() >= delta) ? xInterval : xInterval.expand(delta); + Interval newY = (yInterval.size() >= delta) ? yInterval : yInterval.expand(delta); + Interval newZ = (zInterval.size() >= delta) ? zInterval : zInterval.expand(delta); + return new AABB(newX, newY, newZ); + } + + @Override + public String toString() { + return "AABB{" + + "xInterval=" + xInterval + + ", yInterval=" + yInterval + + ", zInterval=" + zInterval + + '}'; + } +} diff --git a/src/main/java/objects/BVHNode.java b/src/main/java/objects/BVHNode.java new file mode 100644 index 0000000..0dc7dd5 --- /dev/null +++ b/src/main/java/objects/BVHNode.java @@ -0,0 +1,86 @@ +package objects; + +import math.Interval; +import math.Ray; +import objects.comparator.XComparator; +import objects.comparator.YComparator; +import objects.comparator.ZComparator; +import math.HitRecord; +import util.collections.BoundableList; + +import java.util.*; + +public class BVHNode implements Boundable { + private final Boundable left; + private final Boundable right; + private final AABB box; + + public BVHNode(BoundableList list) { + this(list, 0, list.size()); + } + + public BVHNode(BoundableList list, int begin, int end) { + if(list.isEmpty()){ + left = new Boundable() { + @Override + public AABB boundingBox() { + return new AABB(){ + @Override + public boolean hit(Ray r, Interval tInterval) { + return false; + } + }; + } + + @Override + public boolean hit(Ray r, Interval tInterval, HitRecord rec) { + return false; + } + }; + right = left; + box = AABB.surroundingBox(left.boundingBox(), right.boundingBox()); + return; + } + Random random = new Random(); + List> comparators = List.of(new XComparator(), new YComparator(), new ZComparator()); + Comparator comparator = comparators.get(random.nextInt(0, 3)); + int span = end - begin; + if(span == 1){ + left = list.get(begin); + right = left; + }else if(span == 2){ + left = list.get(begin); + right = list.get(begin + 1); + }else{ + list.subList(begin, end).sort(comparator); + int mid = begin + span / 2; + left = new BVHNode(list, begin, mid); + right = new BVHNode(list, mid, end); + } + box = AABB.surroundingBox(left.boundingBox(), right.boundingBox()); + } + + + @Override + public AABB boundingBox() { + return box; + } + + @Override + public boolean hit(Ray r, Interval tInterval, HitRecord rec) { + if(!box.hit(r, tInterval)) + return false; + boolean hitLeft = left.hit(r, tInterval, rec); + boolean hitRight = right.hit(r, new Interval(tInterval.getMin(), hitLeft ? rec.getT() : tInterval.getMax()), rec); + return hitLeft || hitRight; + } + + @Override + public String toString() { + return "BVHNode{" + + ", box=" + box + + "\nleft=" + left + + ", \nright=" + right + + '}'; + } +} diff --git a/src/main/java/objects/Boundable.java b/src/main/java/objects/Boundable.java new file mode 100644 index 0000000..c4e5487 --- /dev/null +++ b/src/main/java/objects/Boundable.java @@ -0,0 +1,5 @@ +package objects; + +public interface Boundable extends Hittable { + AABB boundingBox(); +} diff --git a/src/main/java/objects/Hittable.java b/src/main/java/objects/Hittable.java new file mode 100644 index 0000000..b7cf005 --- /dev/null +++ b/src/main/java/objects/Hittable.java @@ -0,0 +1,9 @@ +package objects; + +import math.Interval; +import math.Ray; +import math.HitRecord; + +public interface Hittable { + boolean hit(Ray r, Interval tInterval, HitRecord rec); +} diff --git a/src/main/java/objects/MovingSphere.java b/src/main/java/objects/MovingSphere.java new file mode 100644 index 0000000..4475cce --- /dev/null +++ b/src/main/java/objects/MovingSphere.java @@ -0,0 +1,69 @@ +package objects; + +import math.*; +import material.Material; + +public class MovingSphere implements Boundable { + private final Point centerStart; + private final Point centerFinish; + private final double timeStart; + private final double timeFinish; + private final double radius; + private final Material material; + private final AABB box; + public MovingSphere(Point centerStart, Point centerFinish, double radius, double timeStart, double timeFinish, Material material) { + this.centerStart = centerStart; + this.centerFinish = centerFinish; + this.timeStart = timeStart; + this.timeFinish = timeFinish; + this.radius = radius; + this.material = material; + + Vector rVector = new Vector(radius, radius, radius); + + AABB box0 = new AABB(centerStart.move(rVector.negate()), centerStart.move(rVector)); + AABB box1 = new AABB(centerFinish.move(rVector.negate()), centerFinish.move(rVector)); + box = AABB.surroundingBox(box0, box1); + } + + @Override + public boolean hit(Ray r, Interval tInterval, HitRecord rec) { + Vector oc = new Vector(center(r.getTimeMoment()), r.getOrigin()); + double A = r.getDirection().lengthSquared(); + double halfB = Vectors.dot(oc, r.getDirection()); + double C = oc.lengthSquared() - radius*radius; + double discriminant = halfB*halfB - A*C; + if(discriminant < 0d){ + return false; + } + double sqrtd = Math.sqrt(discriminant); + double root = (-halfB - sqrtd) / A; + if (!tInterval.contains(root)) { + root = (-halfB + sqrtd) / A; + if (!tInterval.contains(root)) + return false; + } + + rec.setT(root); + rec.setPoint(r.at(rec.getT())); + Vector outwardNormal = new Vector(center(r.getTimeMoment()), rec.getPoint()).divide(radius); + rec.setFaceNormal(r, outwardNormal); + rec.setMaterial(this.material); + return true; + } + + + @Override + public AABB boundingBox() { + return box; + } + + private Point center(double time){ + if(Math.abs(timeFinish - timeStart) <= 1e-10){ + return centerStart; + } + return centerStart.move(new Vector(centerStart, centerFinish).multiply((time - timeStart)/(timeFinish - timeStart))); + } + + +} diff --git a/src/main/java/objects/Plane.java b/src/main/java/objects/Plane.java new file mode 100644 index 0000000..718bdca --- /dev/null +++ b/src/main/java/objects/Plane.java @@ -0,0 +1,42 @@ +package objects; + +import material.Material; +import math.*; + +public class Plane implements Hittable{ + protected final Point p; + protected final Vector u; + protected final Vector v; + protected final Material material; + + protected final Vector normal; + protected final double D; + + public Plane(Point p, Vector u, Vector v, Material material) { + this.p = p; + this.u = u; + this.v = v; + this.material = material; + this.normal = Vectors.cross(u, v).unit(); + this.D = Vectors.dot(normal, new Vector(new Point(), p)); + } + public Plane(Point p, Point u, Point v, Material material) { + this(p, new Vector(u, p), new Vector(v, p), material); + } + + + + @Override + public boolean hit(Ray r, Interval tInterval, HitRecord rec) { + double denom = Vectors.dot(normal, r.getDirection()); + if (Math.abs(denom) <= 1e-8) + return false; + double t = (D - Vectors.dot(normal, new Vector(new Point(), r.getOrigin()))) / denom; + if (!tInterval.contains(t)) + return false; + + Point intersection = r.at(t); + rec.setT(t).setPoint(intersection).setMaterial(material).setFaceNormal(r, normal); + return true; + } +} diff --git a/src/main/java/objects/Quad.java b/src/main/java/objects/Quad.java new file mode 100644 index 0000000..41cf504 --- /dev/null +++ b/src/main/java/objects/Quad.java @@ -0,0 +1,72 @@ +package objects; + +import material.Material; +import math.*; + +@Deprecated +public class Quad implements Hittable, Boundable{ + private final Point Q; + private final Vector u; + private final Vector v; + private final Material material; + private final AABB box; + private final Vector normal; + private double D; + private final Vector w; + + public Quad(Point q, Vector u, Vector v, Material material) { + Q = q; + this.u = u; + this.v = v; + this.material = material; + + Vector n = Vectors.cross(u, v); + this.normal = n.unit(); + this.D = Vectors.dot(normal, new Vector(this.Q)); + this.w = n.divide(Vectors.dot(n, n)); + + this.box = new AABB(Q, Q.move(u).move(v)).pad(); + + } + + @Override + public AABB boundingBox() { + return box; + } + + @Override + public boolean hit(Ray r, Interval tInterval, HitRecord rec) { + double denom = Vectors.dot(normal, r.getDirection()); + if(Math.abs(denom) < 1e-8) + return false; + + double t = (D - Vectors.dot(normal, new Vector(r.getOrigin()))) / denom; + if(!tInterval.contains(t)){ + return false; + } + + Point intersection = r.at(t); + Vector hitVector = new Vector(Q, intersection); + double alpha = Vectors.dot(w, Vectors.cross(hitVector, v)); + double beta = Vectors.dot(w, Vectors.cross(u, hitVector)); + + if (!isInterior(alpha, beta, rec)) + return false; + + rec.setT(t) + .setPoint(intersection) + .setMaterial(material) + .setFaceNormal(r, normal); + + return true; + } + + private boolean isInterior(double a, double b, HitRecord rec) { + if ((a < 0) || (1 < a) || (b < 0) || (1 < b)) + return false; + + rec.setU(a); + rec.setV(b); + return true; + } +} diff --git a/src/main/java/objects/Quadrilateral.java b/src/main/java/objects/Quadrilateral.java new file mode 100644 index 0000000..c32d629 --- /dev/null +++ b/src/main/java/objects/Quadrilateral.java @@ -0,0 +1,40 @@ +package objects; + +import material.Material; +import math.*; + +public class Quadrilateral extends Plane implements Boundable{ + private final AABB box; + private final Vector w; + + public Quadrilateral(Point p, Vector u, Vector v, Material material) { + super(p, u, v, material); + this.box = new AABB(p, p.move(this.u).move(this.v)).pad(); + Vector n = Vectors.cross(u, v); + this.w = n.divide(Vectors.dot(n, n)); + } + + @Override + public AABB boundingBox() { + return box; + } + + @Override + public boolean hit(Ray r, Interval tInterval, HitRecord rec) { + HitRecord record = new HitRecord(); + if (!super.hit(r, tInterval, record)) + return false; + + Point intersect = r.at(record.getT()); + Vector hitVector = new Vector(p, intersect); + double alpha = Vectors.dot(w, Vectors.cross(hitVector, v)); + double beta = Vectors.dot(w, Vectors.cross(u, hitVector)); + + Interval testInterval = new Interval(0, 1); + if(!testInterval.contains(alpha) || !testInterval.contains(beta)) + return false; + + rec.set(record).setU(alpha).setV(beta); + return true; + } +} diff --git a/src/main/java/util/Sphere.java b/src/main/java/objects/Sphere.java similarity index 54% rename from src/main/java/util/Sphere.java rename to src/main/java/objects/Sphere.java index 3487d1c..8105568 100644 --- a/src/main/java/util/Sphere.java +++ b/src/main/java/objects/Sphere.java @@ -1,15 +1,13 @@ -package util; +package objects; -import math.Point; -import math.Ray; -import math.Vector; -import math.Vectors; -import util.material.Material; +import math.*; +import material.Material; +import util.Pair; -public class Sphere implements Hittable { +public class Sphere implements Boundable { private Point center; private double radius; - private Material material; + private final Material material; public Sphere(Point center, double radius, Material material) { this.center = center; @@ -18,7 +16,7 @@ public Sphere(Point center, double radius, Material material) { } @Override - public boolean hit(Ray r, double tMin, double tMax, HitRecord rec) { + public boolean hit(Ray r, Interval tInterval, HitRecord rec) { Vector oc = new Vector(center, r.getOrigin()); double A = r.getDirection().lengthSquared(); double halfB = Vectors.dot(oc, r.getDirection()); @@ -29,20 +27,31 @@ public boolean hit(Ray r, double tMin, double tMax, HitRecord rec) { } double sqrtd = Math.sqrt(discriminant); double root = (-halfB - sqrtd) / A; - if (root < tMin || tMax < root) { + if (!tInterval.contains(root)) { root = (-halfB + sqrtd) / A; - if (root < tMin || tMax < root) + if (!tInterval.contains(root)) return false; } - rec.setT(root); - rec.setPoint(r.at(rec.getT())); + rec.setT(root) + .setPoint(r.at(rec.getT())); Vector outwardNormal = new Vector(center, rec.getPoint()).divide(radius); - rec.setFaceNormal(r, outwardNormal); - rec.setMaterial(this.material); + rec.setFaceNormal(r, outwardNormal) + .setMaterial(this.material).setUV(getHitCoordinates(new Point().move(outwardNormal))); return true; } + @Override + public AABB boundingBox() { + return new AABB(center.move(new Vector(radius, radius, radius).negate()), center.move(new Vector(radius, radius, radius))); + } + + private Pair getHitCoordinates(Point point){ + double u = (Math.atan2(-point.getZ(), point.getX()) + Math.PI) / (2 * Math.PI); + double v = Math.acos(-point.getY())/Math.PI; + return new Pair<>(u, v); + } + public Point getCenter() { return center; } @@ -58,4 +67,6 @@ public double getRadius() { public void setRadius(double radius) { this.radius = radius; } + + } diff --git a/src/main/java/objects/comparator/BoxComparator.java b/src/main/java/objects/comparator/BoxComparator.java new file mode 100644 index 0000000..a00f149 --- /dev/null +++ b/src/main/java/objects/comparator/BoxComparator.java @@ -0,0 +1,17 @@ +package objects.comparator; + +import math.Interval; +import math.Points; +import objects.AABB; +import objects.Boundable; +import objects.Hittable; +import util.TriFunction; + +public class BoxComparator implements TriFunction { + @Override + public Integer apply(Boundable a, Boundable b, Integer axis) { + Interval[] aAxes = new Interval[]{a.boundingBox().getX(), a.boundingBox().getY(), a.boundingBox().getZ()}; + Interval[] bAxes = new Interval[]{b.boundingBox().getX(), b.boundingBox().getY(), b.boundingBox().getZ()}; + return Double.compare(aAxes[axis].getMin(), bAxes[axis].getMin()); + } +} diff --git a/src/main/java/objects/comparator/XComparator.java b/src/main/java/objects/comparator/XComparator.java new file mode 100644 index 0000000..314cf8f --- /dev/null +++ b/src/main/java/objects/comparator/XComparator.java @@ -0,0 +1,15 @@ +package objects.comparator; + +import objects.Boundable; +import objects.Hittable; + +import java.util.Comparator; +import java.util.function.BiFunction; + +public class XComparator implements Comparator { + BoxComparator boxComparator = new BoxComparator(); + @Override + public int compare(Boundable a, Boundable b) { + return boxComparator.apply(a, b, 0); + } +} diff --git a/src/main/java/objects/comparator/YComparator.java b/src/main/java/objects/comparator/YComparator.java new file mode 100644 index 0000000..9215b6b --- /dev/null +++ b/src/main/java/objects/comparator/YComparator.java @@ -0,0 +1,15 @@ +package objects.comparator; + +import objects.Boundable; +import objects.Hittable; + +import java.util.Comparator; +import java.util.function.BiFunction; + +public class YComparator implements Comparator { + BoxComparator boxComparator = new BoxComparator(); + @Override + public int compare(Boundable a, Boundable b) { + return boxComparator.apply(a, b, 1); + } +} diff --git a/src/main/java/objects/comparator/ZComparator.java b/src/main/java/objects/comparator/ZComparator.java new file mode 100644 index 0000000..4841378 --- /dev/null +++ b/src/main/java/objects/comparator/ZComparator.java @@ -0,0 +1,15 @@ +package objects.comparator; + +import objects.Boundable; +import objects.Hittable; + +import java.util.Comparator; +import java.util.function.BiFunction; + +public class ZComparator implements Comparator { + BoxComparator boxComparator = new BoxComparator(); + @Override + public int compare(Boundable a, Boundable b) { + return boxComparator.apply(a, b, 2); + } +} diff --git a/src/main/java/raytracer/RayTracer.java b/src/main/java/raytracer/RayTracer.java new file mode 100644 index 0000000..81b4ac8 --- /dev/null +++ b/src/main/java/raytracer/RayTracer.java @@ -0,0 +1,7 @@ +package raytracer; + +import image.Image; + +public interface RayTracer { + Image render(); +} diff --git a/src/main/java/raytracer/RayTracerConfig.java b/src/main/java/raytracer/RayTracerConfig.java new file mode 100644 index 0000000..651378c --- /dev/null +++ b/src/main/java/raytracer/RayTracerConfig.java @@ -0,0 +1,23 @@ +package raytracer; + +import util.Dimension; + +import java.util.Objects; + +public record RayTracerConfig(Dimension imageDimension, int depth, int samplesPerPixel) { + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof RayTracerConfig that)) return false; + return depth == that.depth && samplesPerPixel == that.samplesPerPixel && Objects.equals(imageDimension, that.imageDimension); + } + + public int getHeight() { + return imageDimension.getHeight(); + } + + public int getWidth() { + return imageDimension.getWidth(); + } +} diff --git a/src/main/java/raytracer/RayTracerImpl.java b/src/main/java/raytracer/RayTracerImpl.java new file mode 100644 index 0000000..127eb9b --- /dev/null +++ b/src/main/java/raytracer/RayTracerImpl.java @@ -0,0 +1,67 @@ +package raytracer; + +import camera.Camera; +import image.ArrayImage; +import image.Image; +import math.*; +import objects.Hittable; +import raytracer.scene.Scene; + +import java.util.Random; + +public class RayTracerImpl implements RayTracer{ + private final Camera camera; + private final Hittable world; + + private final RayTracerConfig config; + private final Color[][] colorMatrix; + + public RayTracerImpl (RayTracerConfig config, Scene scene){ + this.config = config; + this.camera = scene.getCamera(); + this.world = scene.getWorld(); + this.colorMatrix = new Color[config.imageDimension().getWidth()][config.imageDimension().getHeight()]; + } + @Override + public Image render() { + Random random = new Random(); + for (int j = 0; j < config.getHeight(); j++) { + System.out.print("Lines remaining " + (config.getHeight() - j) + "\r"); + for (int i = 0; i < config.getWidth(); i++) { + Color pixelColor = new Color(); + for (int s = 0; s < config.samplesPerPixel(); s++) { + double u = ((double) i + random.nextDouble())/(config.getWidth() - 1); + double v = ((double) j + random.nextDouble())/(config.getHeight() - 1); + Ray ray = camera.getRay(u, v); + Color color = rayColor(ray, config.depth()).scale(1d/config.samplesPerPixel()); + pixelColor = Colors.add(pixelColor, color); + } + colorMatrix[i][j] = pixelColor; + } + } + return new ArrayImage(colorMatrix); + } + + private Color rayColor(Ray r, int aDepth){ + if (aDepth <= 0){ + return new Color(0, 0, 0); + } + + HitRecord record = new HitRecord(); + if(world.hit(r, new Interval(0.00001, Double.POSITIVE_INFINITY), record)){ + Ray scattered = new Ray(); + Color attenuation = new Color(); + if(record.getMaterial().scatter(r, record, attenuation, scattered)){ + Color newColor = rayColor(scattered,aDepth - 1); + return attenuation.setRed(attenuation.getRed() * newColor.getRed()) + .setGreen(attenuation.getGreen() * newColor.getGreen()) + .setBlue(attenuation.getBlue() * newColor.getBlue()); + } + return new Color(0, 0, 0); + } + Vector unitDirection = r.getDirection().unit(); + double t = 0.5 * (unitDirection.getY() + 1.0); + return new Color((1.0 - t) + (t * 0.5), (1.0 - t) + (t * 0.7), (1.0 - t) + (t)); + } + +} diff --git a/src/main/java/raytracer/RayTracerLights.java b/src/main/java/raytracer/RayTracerLights.java new file mode 100644 index 0000000..b2dc4b2 --- /dev/null +++ b/src/main/java/raytracer/RayTracerLights.java @@ -0,0 +1,67 @@ +package raytracer; + +import camera.Camera; +import image.ArrayImage; +import image.Image; +import math.*; +import objects.Hittable; +import raytracer.scene.Scene; + +import java.util.Random; + +public class RayTracerLights implements RayTracer{ + private final Camera camera; + private final Hittable world; + + private final RayTracerConfig config; + private final Color[][] colorMatrix; + + public RayTracerLights (RayTracerConfig config, Scene scene){ + this.config = config; + this.camera = scene.getCamera(); + this.world = scene.getWorld(); + this.colorMatrix = new Color[config.imageDimension().getWidth()][config.imageDimension().getHeight()]; + } + @Override + public Image render() { + Random random = new Random(); + for (int j = 0; j < config.getHeight(); j++) { + System.out.print("Lines remaining " + (config.getHeight() - j) + "\r"); + for (int i = 0; i < config.getWidth(); i++) { + Color pixelColor = new Color(); + for (int s = 0; s < config.samplesPerPixel(); s++) { + double u = ((double) i + random.nextDouble())/(config.getWidth() - 1); + double v = ((double) j + random.nextDouble())/(config.getHeight() - 1); + Ray ray = camera.getRay(u, v); + Color color = rayColor(ray, config.depth()).scale(1d/config.samplesPerPixel()); + pixelColor = Colors.add(pixelColor, color); + } + colorMatrix[i][j] = pixelColor; + } + } + return new ArrayImage(colorMatrix); + } + + private Color rayColor(Ray r, int aDepth){ + HitRecord record = new HitRecord(); + if(aDepth <= 0){ + return new Color(0, 0, 0); + } + if(!world.hit(r, new Interval(0.00001, Double.POSITIVE_INFINITY), record)){ + return Colors.BLACK; + } + + Ray scattered = new Ray(); + Color attenuation = new Color(); + Color colorFromEmission = record.getMaterial().emitted(record.getU(), record.getV(), record.getPoint()); + + if(!record.getMaterial().scatter(r, record, attenuation, scattered)){ + return colorFromEmission; + } + + Color colorFromScatter = Colors.multiply(rayColor(scattered, aDepth - 1), attenuation); + + return Colors.add(colorFromEmission, colorFromScatter); + + } +} diff --git a/src/main/java/raytracer/scene/BallsScene.java b/src/main/java/raytracer/scene/BallsScene.java new file mode 100644 index 0000000..a2e4aac --- /dev/null +++ b/src/main/java/raytracer/scene/BallsScene.java @@ -0,0 +1,40 @@ +package raytracer.scene; + +import camera.Camera; +import camera.ClearCamera; +import camera.MotionBlurCamera; +import material.Lambertian; +import math.Color; +import math.Colors; +import math.Point; +import math.Vector; +import objects.BVHNode; +import objects.Hittable; +import objects.MovingSphere; +import objects.Sphere; +import texture.ImageTexture; +import texture.Texture; +import util.collections.BoundableList; +import util.collections.HittableList; +import util.collections.impl.BoundableArrayList; +import util.collections.impl.HittableArrayList; + +import java.nio.file.Path; +import java.nio.file.Paths; + +public class BallsScene extends Scene{ + @Override + public void init() { + Path earth = Paths.get("src", "main", "resources", "earthrealistic.jpg"); + Texture earthTexture = new ImageTexture(earth); + + + + boundableObjects.add(new MovingSphere(new Point(-1, 1, 0), new Point(1, 1, 0), 1, 2,5, new Lambertian(Colors.PURPLE))); + boundableObjects.add(new Sphere(new Point(0, -1000, 0), 1000, new Lambertian(new Color(0.8, 0.7, 0.8)))); + boundableObjects.add(new Sphere(new Point(0, 3, 0), 1, new Lambertian(earthTexture))); + + Camera camera = new ClearCamera(new Point(8, 4, 8), new Point(0, 2, 0), new Vector(0, 1, 0), 20, 16d/9d); + this.camera = new MotionBlurCamera(camera, 1, 2); + } +} diff --git a/src/main/java/raytracer/scene/LightsScene.java b/src/main/java/raytracer/scene/LightsScene.java new file mode 100644 index 0000000..9bd640d --- /dev/null +++ b/src/main/java/raytracer/scene/LightsScene.java @@ -0,0 +1,36 @@ +package raytracer.scene; + +import camera.Camera; +import camera.ClearCamera; +import camera.MotionBlurCamera; +import material.DiffuseLight; +import material.Lambertian; +import material.Material; +import math.Color; +import math.Colors; +import math.Point; +import math.Vector; +import objects.Quad; +import objects.Quadrilateral; +import objects.Sphere; +import texture.MarbleTexture; +import texture.PerlinTexture; +import texture.Texture; +import util.PerlinNoise; + +public class LightsScene extends Scene{ + @Override + public void init() { + Texture perlin = new MarbleTexture(); + boundableObjects.add(new Sphere(new Point(0, -1000, 0), 1000, new Lambertian(perlin))); + boundableObjects.add(new Sphere(new Point(0, 2, 0), 2, new Lambertian(perlin))); + + Material diffuseLight = new DiffuseLight(new Color(1, 0.2, 0.4).scale(5)); + boundableObjects.add(new Quadrilateral(new Point(3, 1, -2), new Vector(2, 0, 0), new Vector(0, 2, 0), diffuseLight)); +// boundableObjects.add(new Quadrilateral(new Point(3, 1, -2), new Vector(2, 0, 0), new Vector(0, 2, 0), new Lambertian(new Color(2, 0.4, 1)))); + + Camera camera = new ClearCamera(new Point(26, 3, 6), new Point(0, 2, 0), new Vector(0, 1, 0), 20, 16d/9d); +// Camera camera = new ClearCamera(new Point(26, 3, 26), new Point(0, 2, 0), new Vector(0, 1, 0), 20, 16d/9d); + this.camera = new MotionBlurCamera(camera, 1, 2); + } +} diff --git a/src/main/java/raytracer/scene/Scene.java b/src/main/java/raytracer/scene/Scene.java new file mode 100644 index 0000000..08556dc --- /dev/null +++ b/src/main/java/raytracer/scene/Scene.java @@ -0,0 +1,30 @@ +package raytracer.scene; + +import camera.Camera; +import objects.BVHNode; +import objects.Hittable; +import util.collections.BoundableList; +import util.collections.HittableList; +import util.collections.impl.BoundableArrayList; +import util.collections.impl.HittableArrayList; + +public abstract class Scene { + protected Camera camera; + protected final HittableList world = new HittableArrayList(); + protected final BoundableList boundableObjects = new BoundableArrayList(); + + public Scene() { + init(); + BVHNode bvhNode = new BVHNode(boundableObjects); + world.add(bvhNode); + } + + public abstract void init(); + + public Camera getCamera() { + return camera; + } + public Hittable getWorld() { + return world; + } +} diff --git a/src/main/java/texture/CheckerTexture.java b/src/main/java/texture/CheckerTexture.java new file mode 100644 index 0000000..38a9c89 --- /dev/null +++ b/src/main/java/texture/CheckerTexture.java @@ -0,0 +1,45 @@ +package texture; + +import math.Color; +import math.Point; + +import java.util.stream.IntStream; + +import static java.lang.Math.sin; + +public class CheckerTexture implements Texture{ + private final Texture odd; + private final Texture even; + private final double scale; + + public CheckerTexture(Texture odd, Texture even) { + this(1, odd, even); + } + + public CheckerTexture(Color odd, Color even) { + this(1, odd, even); + } + + public CheckerTexture(double scale, Texture odd, Texture even) { + this.scale = scale; + this.odd = odd; + this.even = even; + } + + public CheckerTexture(double scale, Color odd, Color even) { + this.scale = scale; + this.odd = new SolidColorTexture(odd); + this.even = new SolidColorTexture(even); + } + + @Override + public Color value(double u, double v, Point p) { + int x = (int) (scale * p.getX()); + int y = (int) (scale * p.getY()); + int z = (int) (scale * p.getZ()); + boolean isSumEven = (x + y + z) % 2 == 0; + + return isSumEven ? even.value(u, v, p) : odd.value(u, v, p); + } + +} diff --git a/src/main/java/texture/GradientTexture.java b/src/main/java/texture/GradientTexture.java new file mode 100644 index 0000000..dbcb7dd --- /dev/null +++ b/src/main/java/texture/GradientTexture.java @@ -0,0 +1,23 @@ +package texture; + +import math.Color; +import math.Colors; +import math.Point; + +public class GradientTexture implements Texture{ + private final Color begin; + private final Color end; + + public GradientTexture(Color begin, Color end) { + this.begin = begin; + this.end = end; + } + + @Override + public Color value(double u, double v, Point p) { + Color first = begin.scale(u); + Color second = end.scale((1 - u)); + + return Colors.add(first, second); + } +} diff --git a/src/main/java/texture/ImageTexture.java b/src/main/java/texture/ImageTexture.java new file mode 100644 index 0000000..5165da0 --- /dev/null +++ b/src/main/java/texture/ImageTexture.java @@ -0,0 +1,46 @@ +package texture; + +import math.Color; +import math.Point; +import util.Util; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.nio.file.Path; + +public class ImageTexture implements Texture{ + private BufferedImage bufferedImage; + private int width; + private int height; + + public ImageTexture(Path image){ + try{ + bufferedImage = ImageIO.read(image.toFile()); + width = bufferedImage.getWidth(); + height = bufferedImage.getHeight(); + } catch (IOException e) { + bufferedImage = null; + //TODO + throw new RuntimeException("Lolz no image found for this texture"); + } + + } + @Override + public Color value(double u, double v, Point p) { + if(bufferedImage == null){ + return new Color(0, 1, 1); + } + + double tu = Util.clamp(u, 0d, 1d); + double tv = 1d - Util.clamp(v, 0d, 1d); + + int i = (int)(tu * width); + int j = (int)(tv * height); + + if (i >= width) i = width-1; + if (j >= height) j = height-1; + + return Color.getByRGB(bufferedImage.getRGB(i, j)); + } +} diff --git a/src/main/java/texture/MarbleTexture.java b/src/main/java/texture/MarbleTexture.java new file mode 100644 index 0000000..8b3c24a --- /dev/null +++ b/src/main/java/texture/MarbleTexture.java @@ -0,0 +1,29 @@ +package texture; + +import math.Color; +import math.Point; + +import java.util.function.Function; + +public class MarbleTexture extends PerlinTexture { + private final Function coordinate; + public MarbleTexture() { + super(); + coordinate = Point::getZ; + } + + public MarbleTexture(Function coordinate, double scale, int depth) { + super(scale, depth); + this.coordinate = coordinate; + } + + public MarbleTexture(Color color, Function coordinate, double scale, int depth) { + super(color, scale, depth); + this.coordinate = coordinate; + } + + @Override + public Color value(double u, double v, Point p) { + return color.scale(0.5 * (1 + Math.sin(scale * coordinate.apply(p) + 10 * noise.turbulence(p)))); + } +} diff --git a/src/main/java/texture/PerlinTexture.java b/src/main/java/texture/PerlinTexture.java new file mode 100644 index 0000000..97d931e --- /dev/null +++ b/src/main/java/texture/PerlinTexture.java @@ -0,0 +1,35 @@ +package texture; + +import math.Color; +import math.Point; +import math.Points; +import util.PerlinNoise; + +import java.util.function.Function; +import java.util.function.Supplier; + +public class PerlinTexture implements Texture{ + protected final PerlinNoise noise = new PerlinNoise(); + protected final double scale; + protected final int depth; + protected final Color color; + public PerlinTexture() { + this(new Color(1, 1, 1), 4, 1); + } + + public PerlinTexture(double scale, int depth) { + this(new Color(1, 1, 1), scale, depth); + } + + public PerlinTexture(Color color, double scale, int depth){ + this.color = color; + this.scale = scale; + this.depth = depth; + } + + @Override + public Color value(double u, double v, Point p) { + return color.scale(noise.turbulence(Points.scale(p, scale), depth)); + } + +} diff --git a/src/main/java/texture/SinCheckerTexture.java b/src/main/java/texture/SinCheckerTexture.java new file mode 100644 index 0000000..e30a345 --- /dev/null +++ b/src/main/java/texture/SinCheckerTexture.java @@ -0,0 +1,25 @@ +package texture; + +import math.Color; +import math.Point; + +public class SinCheckerTexture implements Texture{ + private final Texture odd; + private final Texture even; + + public SinCheckerTexture(Texture odd, Texture even) { + this.odd = odd; + this.even = even; + } + + public SinCheckerTexture(Color odd, Color even){ + this.odd = new SolidColorTexture(odd); + this.even = new SolidColorTexture(even); + } + + @Override + public Color value(double u, double v, Point p) { + double sin = Math.sin(10 * p.getX()) * Math.sin(10 * p.getY()) * Math.sin(10 * p.getZ()); + return sin < 0 ? odd.value(u, v, p) : even.value(u, v, p); + } +} diff --git a/src/main/java/texture/SolidColorTexture.java b/src/main/java/texture/SolidColorTexture.java new file mode 100644 index 0000000..21c1ac0 --- /dev/null +++ b/src/main/java/texture/SolidColorTexture.java @@ -0,0 +1,17 @@ +package texture; + +import math.Color; +import math.Point; + +public class SolidColorTexture implements Texture{ + private final Color color; + + public SolidColorTexture(Color color) { + this.color = color; + } + + @Override + public Color value(double u, double v, Point p) { + return color; + } +} diff --git a/src/main/java/texture/Texture.java b/src/main/java/texture/Texture.java new file mode 100644 index 0000000..a4b247a --- /dev/null +++ b/src/main/java/texture/Texture.java @@ -0,0 +1,8 @@ +package texture; + +import math.Color; +import math.Point; + +public interface Texture { + Color value(double u, double v, Point p); +} diff --git a/src/main/java/util/Dimension.java b/src/main/java/util/Dimension.java new file mode 100644 index 0000000..bfcd1d2 --- /dev/null +++ b/src/main/java/util/Dimension.java @@ -0,0 +1,57 @@ +package util; + +import raytracer.RayTracerConfig; + +import java.util.Objects; + +public class Dimension { + private final int width; + private final int height; + private final double aspectRatio; + + public Dimension(int width, int height) { + this.height = height; + this.width = width; + this.aspectRatio = (double) width / (double)height; + } + + public Dimension(int width, double aspectRatio) { + this.width = width; + this.height = (int) (width / aspectRatio); + this.aspectRatio = aspectRatio; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public double getAspectRatio() { + return aspectRatio; + } + + @Override + public String toString() { + return "RayTracerConfig{" + + "width=" + width + + ", height=" + height + + ", aspectRatio=" + aspectRatio + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Dimension that)) return false; + return height == that.height && width == that.width; + } + + @Override + public int hashCode() { + return Objects.hash(height, width); + } + +} diff --git a/src/main/java/util/Hittable.java b/src/main/java/util/Hittable.java deleted file mode 100644 index 326823a..0000000 --- a/src/main/java/util/Hittable.java +++ /dev/null @@ -1,7 +0,0 @@ -package util; - -import math.Ray; - -public interface Hittable { - boolean hit(Ray r, double tMin, double tMax, HitRecord rec); -} diff --git a/src/main/java/util/Pair.java b/src/main/java/util/Pair.java new file mode 100644 index 0000000..dc9d854 --- /dev/null +++ b/src/main/java/util/Pair.java @@ -0,0 +1,54 @@ +package util; + +import java.util.Objects; + +public class Pair { + private F first; + private S second; + + public Pair() { + + } + + public Pair(F first, S second) { + this.first = first; + this.second = second; + } + + public F getFirst() { + return first; + } + + public void setFirst(F first) { + this.first = first; + } + + public S getSecond() { + return second; + } + + public void setSecond(S second) { + this.second = second; + } + + @Override + public String toString() { + return "Pair{" + + "first=" + first + + ", second=" + second + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Pair)) return false; + Pair pair = (Pair) o; + return Objects.equals(first, pair.first) && Objects.equals(second, pair.second); + } + + @Override + public int hashCode() { + return Objects.hash(first, second); + } +} diff --git a/src/main/java/util/PerlinNoise.java b/src/main/java/util/PerlinNoise.java new file mode 100644 index 0000000..0b2526f --- /dev/null +++ b/src/main/java/util/PerlinNoise.java @@ -0,0 +1,88 @@ +package util; + +import math.Point; +import math.Points; +import math.Vector; +import math.Vectors; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; + +import static java.lang.Math.floor; + +public class PerlinNoise { + private final static int pointCount = 256; + private final Vector[] randomVectors = new Vector[pointCount]; + private final List permX = new ArrayList<>(); + private final List permY = new ArrayList<>(); + private final List permZ = new ArrayList<>(); + + public PerlinNoise() { + for (int i = 0; i < pointCount; i++) { + randomVectors[i] = Vectors.getRandom(-1, 1).unit(); + permX.add(i); + permY.add(i); + permZ.add(i); + } + Collections.shuffle(permX); + Collections.shuffle(permY); + Collections.shuffle(permZ); + } + + public double turbulence(Point p){ + return turbulence(p, 4); + } + + public double turbulence(Point p, int depth){ + double accum = 0.0; + Point tmp = new Point(p); + double weight = 1.0; + + for (int i = 0; i < depth; i++) { + accum += (weight * noise(tmp)); + weight *= 0.5; + tmp = Points.scale(tmp, 2); + } + return Math.abs(accum); + } + + public double noise(Point p){ + double u = p.getX() - floor(p.getX()); + double v = p.getY() - floor(p.getY()); + double w = p.getZ() - floor(p.getZ()); + + int i = (int)floor(p.getX()); + int j = (int)floor(p.getY()); + int k = (int)floor(p.getZ()); + Vector[][][] c = new Vector[2][2][2]; + for (int di=0; di < 2; di++) + for (int dj=0; dj < 2; dj++) + for (int dk=0; dk < 2; dk++) + c[di][dj][dk] = randomVectors[ + permX.get((i + di) & 255) ^ + permY.get((j + dj) & 255) ^ + permZ.get((k + dk) & 255) + ]; + + return interpolation(c, u, v, w); + } + + private double interpolation(Vector[][][] c, double u, double v, double w){ + double accum = 0d; + double uu = u * u * (3 - 2 * u); + double vv = v * v * (3 - 2 * v); + double ww = w * w * (3 - 2 * w); + for (int i = 0; i < 2; i++) + for (int j = 0; j < 2; j++) + for (int k = 0; k < 2; k++) { + Vector weight = new Vector(u - i, v - j, w - k); + accum += (i * uu + (1 - i) * (1 - uu)) + * (j * vv + (1 - j) * (1 - vv)) + * (k * ww + (1 - k) * (1 - ww)) + * Vectors.dot(c[i][j][k], weight); + } + return accum; + } +} diff --git a/src/main/java/util/TriFunction.java b/src/main/java/util/TriFunction.java new file mode 100644 index 0000000..550d8a5 --- /dev/null +++ b/src/main/java/util/TriFunction.java @@ -0,0 +1,16 @@ +package util; + +import java.util.Objects; +import java.util.function.Function; + +@FunctionalInterface +public interface TriFunction { + + R apply(A a, B b, C c); + + default TriFunction andThen( + Function after) { + Objects.requireNonNull(after); + return (A a, B b, C c) -> after.apply(apply(a, b, c)); + } +} diff --git a/src/main/java/util/Util.java b/src/main/java/util/Util.java index b37d972..7960bf2 100644 --- a/src/main/java/util/Util.java +++ b/src/main/java/util/Util.java @@ -1,30 +1,31 @@ package util; -import math.Point; -import math.Ray; -import math.Vector; +import math.*; +import util.collections.BoundableList; import util.collections.HittableList; -import util.material.Dielectric; -import util.material.Lambertian; -import util.material.Material; -import util.material.Metal; +import material.Dielectric; +import material.Lambertian; +import material.Material; +import material.Metal; +import objects.Hittable; +import objects.Sphere; import static java.lang.Math.*; public class Util { - public static double clamp(double x, double min, double max){ - if(x < min) return min; - if(x > max) return max; + public static double clamp(double x, double min, double max) { + if (x < min) return min; + if (x > max) return max; return x; } /** * Represents normal of hit point, as a color */ - public static Color normalColor(Ray r, Hittable world){ + public static Color normalColor(Ray r, Hittable world) { HitRecord record = new HitRecord(); - if(world.hit(r, 0, Double.POSITIVE_INFINITY, record)){ + if (world.hit(r, new Interval(0, Double.POSITIVE_INFINITY), record)) { Vector normal = record.getNormal(); return new Color(0.5 * (normal.getX() + 1), 0.5 * (normal.getY() + 1), 0.5 * (normal.getZ() + 1)); } @@ -35,20 +36,20 @@ public static Color normalColor(Ray r, Hittable world){ return new Color((1.0 - t) + (t * 0.5), (1.0 - t) + (t * 0.7), (1.0 - t) + (t)); } - public static void fillScene(HittableList world){ - for(int x = -11; x < 11; x++){ - for(int z = -11; z < 11; z++){ - Point location = new Point(x + random()*0.9, 0.2, z + random()*0.9); - if(new Vector(new Point(4, 0.2, 0), location).length() > 0.9){ + public static void fillScene(BoundableList world) { + for (int x = -11; x < 11; x++) { + for (int z = -11; z < 11; z++) { + Point location = new Point(x + random() * 0.9, 0.5, z + random() * 0.9); + if (new Vector(new Point(0, 0.2, 0), location).length() > 3) { double rand = random(); - if(rand < 0.8){ + if (rand < 0.50) { Material material = new Lambertian(new Color(random(), random(), random())); world.add(new Sphere(location, 0.2, material)); - }else if(rand < 0.95){ - Material material = new Metal(new Color(random(), random(), random()), random()*0.3); + } else if (rand < 0.75) { + Material material = new Metal(new Color(random(), random(), random()), random() * 0.3); world.add(new Sphere(location, 0.2, material)); - }else{ - Material material = new Dielectric((Math.random()*(1.5)) + 1.5); + } else { + Material material = new Dielectric((Math.random() * (1.5)) + 1.5); world.add(new Sphere(location, 0.2, material)); } } diff --git a/src/main/java/util/collections/AbstractBoundableList.java b/src/main/java/util/collections/AbstractBoundableList.java new file mode 100644 index 0000000..0587df8 --- /dev/null +++ b/src/main/java/util/collections/AbstractBoundableList.java @@ -0,0 +1,130 @@ +package util.collections; + +import objects.Boundable; + +import java.util.*; + +public abstract class AbstractBoundableList implements BoundableList { + protected List list; + + public AbstractBoundableList(List list) { + this.list = list; + } + + + @Override + public int size() { + return list.size(); + } + + @Override + public boolean isEmpty() { + return list.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return list.contains(o); + } + + @Override + public Iterator iterator() { + return list.iterator(); + } + + @Override + public Object[] toArray() { + return list.toArray(); + } + + @Override + public T[] toArray(T[] a) { + return list.toArray(a); + } + + @Override + public boolean add(Boundable boundable) { + return list.add(boundable); + } + + @Override + public boolean remove(Object o) { + return list.remove(o); + } + + @Override + public boolean containsAll(Collection c) { + return new HashSet<>(list).containsAll(c); + } + + @Override + public boolean addAll(Collection c) { + return list.addAll(c); + } + + @Override + public boolean addAll(int index, Collection c) { + return list.addAll(index, c); + } + + @Override + public boolean removeAll(Collection c) { + return list.removeAll(c); + } + + @Override + public boolean retainAll(Collection c) { + return list.retainAll(c); + } + + @Override + public void clear() { + list.clear(); + } + + @Override + public Boundable get(int index) { + return list.get(index); + } + + @Override + public Boundable set(int index, Boundable element) { + return list.set(index, element); + } + + @Override + public void add(int index, Boundable element) { + list.add(index, element); + } + + @Override + public Boundable remove(int index) { + return list.remove(index); + } + + @Override + public int indexOf(Object o) { + return list.indexOf(o); + } + + @Override + public int lastIndexOf(Object o) { + return list.lastIndexOf(o); + } + + @Override + public ListIterator listIterator() { + return list.listIterator(); + } + + @Override + public ListIterator listIterator(int index) { + return list.listIterator(index); + } + + @Override + public List subList(int fromIndex, int toIndex) { + return list.subList(fromIndex, toIndex); + } + +} diff --git a/src/main/java/util/collections/AbstractHittableList.java b/src/main/java/util/collections/AbstractHittableList.java index 847e7ff..3078fb9 100644 --- a/src/main/java/util/collections/AbstractHittableList.java +++ b/src/main/java/util/collections/AbstractHittableList.java @@ -1,7 +1,6 @@ package util.collections; -import util.collections.HittableList; -import util.Hittable; +import objects.Hittable; import java.util.Collection; import java.util.Iterator; @@ -9,12 +8,17 @@ import java.util.ListIterator; public abstract class AbstractHittableList implements HittableList { - private List list; + protected List list; public AbstractHittableList(List list) { this.list = list; } + @Override + public List getList() { + return list; + } + @Override public int size() { return list.size(); diff --git a/src/main/java/util/collections/BoundableList.java b/src/main/java/util/collections/BoundableList.java new file mode 100644 index 0000000..6d8026d --- /dev/null +++ b/src/main/java/util/collections/BoundableList.java @@ -0,0 +1,9 @@ +package util.collections; + +import objects.Boundable; + +import java.util.List; + +public interface BoundableList extends List, Boundable { + HittableList getHittableList(); +} diff --git a/src/main/java/util/collections/HittableArrayList.java b/src/main/java/util/collections/HittableArrayList.java deleted file mode 100644 index b7411d7..0000000 --- a/src/main/java/util/collections/HittableArrayList.java +++ /dev/null @@ -1,30 +0,0 @@ -package util.collections; - -import util.HitRecord; -import util.Hittable; -import math.Ray; - -import java.util.ArrayList; - -public class HittableArrayList extends AbstractHittableList { - public HittableArrayList() { - super(new ArrayList<>()); - } - - @Override - public boolean hit(Ray r, double tMin, double tMax, HitRecord rec) { - HitRecord tempHit = new HitRecord(); - boolean hitAnything = false; - double closestSoFar = tMax; - - for (Hittable i: this) { - if(i.hit(r, tMin, closestSoFar, tempHit)){ - hitAnything = true; - closestSoFar = tempHit.getT(); - rec.set(tempHit); - } - } - return hitAnything; - } - -} diff --git a/src/main/java/util/collections/HittableList.java b/src/main/java/util/collections/HittableList.java index c4a4143..c3bacdd 100644 --- a/src/main/java/util/collections/HittableList.java +++ b/src/main/java/util/collections/HittableList.java @@ -1,8 +1,9 @@ package util.collections; -import util.Hittable; +import objects.Hittable; import java.util.List; public interface HittableList extends List, Hittable { + List getList(); } diff --git a/src/main/java/util/collections/impl/BoundableArrayList.java b/src/main/java/util/collections/impl/BoundableArrayList.java new file mode 100644 index 0000000..1122b9c --- /dev/null +++ b/src/main/java/util/collections/impl/BoundableArrayList.java @@ -0,0 +1,68 @@ +package util.collections.impl; + +import math.Interval; +import math.Ray; +import objects.AABB; +import math.HitRecord; +import objects.Boundable; +import objects.Hittable; +import util.collections.AbstractBoundableList; +import util.collections.HittableList; + +import java.util.ArrayList; + +public class BoundableArrayList extends AbstractBoundableList { + public BoundableArrayList() { + super(new ArrayList<>()); + } + + @Override + public AABB boundingBox() { + if(this.list.isEmpty()){ + throw new RuntimeException("Empty BoundingArrayList"); + } + AABB tmpBox = new AABB(); + boolean firstBox = true; + + for(Boundable i : this){ + if(firstBox){ + tmpBox = i.boundingBox(); + firstBox = false; + }else{ + tmpBox = (AABB.surroundingBox(i.boundingBox(), tmpBox)); + } + } + return tmpBox; + } + + @Override + public boolean hit(Ray r, Interval tInterval, HitRecord rec) { + HitRecord tempHit = new HitRecord(); + boolean hitAnything = false; + double closestSoFar = tInterval.getMax(); + + for (Hittable i: this) { + if(i.hit(r, new Interval(tInterval.getMin(), closestSoFar), tempHit)){ + hitAnything = true; + closestSoFar = tempHit.getT(); + rec.set(tempHit); + } + } + return hitAnything; + } + + + @Override + public HittableList getHittableList() { + HittableList list = new HittableArrayList(); + list.addAll(this); + return list; + } + + @Override + public String toString() { + return "BoundableArrayList{" + + "list=" + list + + '}'; + } +} diff --git a/src/main/java/util/collections/impl/HittableArrayList.java b/src/main/java/util/collections/impl/HittableArrayList.java new file mode 100644 index 0000000..56cea18 --- /dev/null +++ b/src/main/java/util/collections/impl/HittableArrayList.java @@ -0,0 +1,39 @@ +package util.collections.impl; + +import math.Interval; +import objects.AABB; +import math.HitRecord; +import objects.Hittable; +import math.Ray; +import util.collections.AbstractHittableList; + +import java.util.ArrayList; + +public class HittableArrayList extends AbstractHittableList { + public HittableArrayList() { + super(new ArrayList<>()); + } + + @Override + public boolean hit(Ray r, Interval tInterval, HitRecord rec) { + HitRecord tempHit = new HitRecord(); + boolean hitAnything = false; + double closestSoFar = tInterval.getMax(); + + for (Hittable i: this) { + if(i.hit(r, new Interval(tInterval.getMin(), closestSoFar), tempHit)){ + hitAnything = true; + closestSoFar = tempHit.getT(); + rec.set(tempHit); + } + } + return hitAnything; + } + + @Override + public String toString() { + return "HittableArrayList{" + + "list=" + list + + '}'; + } +} diff --git a/src/main/java/util/material/Material.java b/src/main/java/util/material/Material.java deleted file mode 100644 index f192717..0000000 --- a/src/main/java/util/material/Material.java +++ /dev/null @@ -1,9 +0,0 @@ -package util.material; - -import math.Ray; -import util.Color; -import util.HitRecord; - -public interface Material { - boolean scatter(Ray rayIn, HitRecord record, Color attenuation, Ray scattered); -} diff --git a/src/main/java/util/material/Metal.java b/src/main/java/util/material/Metal.java deleted file mode 100644 index 6a6bdde..0000000 --- a/src/main/java/util/material/Metal.java +++ /dev/null @@ -1,32 +0,0 @@ -package util.material; - -import math.Ray; -import math.Vector; -import math.Vectors; -import util.Color; -import util.HitRecord; - -public class Metal implements Material{ - private final Color albedo; - //Для создания нечеткого отражения - private final double fuzz; - - public Metal(Color albedo) { - this.albedo = albedo; - fuzz = 0d; - } - - public Metal(Color albedo, double fuzz) { - this.albedo = albedo; - this.fuzz = fuzz; - } - - @Override - public boolean scatter(Ray rayIn, HitRecord record, Color attenuation, Ray scattered) { - Vector reflected = Vectors.reflect(rayIn.getDirection().unit(), record.getNormal()); - scattered.setOrigin(record.getPoint()); - scattered.setDirection(reflected.add(Vectors.randomInUnitSphere().multiply(fuzz))); - attenuation.set(albedo); - return Vectors.dot(scattered.getDirection(), record.getNormal()) > 0; - } -} diff --git a/src/main/resources/earthmap.jpg b/src/main/resources/earthmap.jpg new file mode 100644 index 0000000..908c160 Binary files /dev/null and b/src/main/resources/earthmap.jpg differ diff --git a/src/main/resources/earthrealistic.jpg b/src/main/resources/earthrealistic.jpg new file mode 100644 index 0000000..485e40a Binary files /dev/null and b/src/main/resources/earthrealistic.jpg differ diff --git a/src/test/java/AABBTest.java b/src/test/java/AABBTest.java new file mode 100644 index 0000000..00b695b --- /dev/null +++ b/src/test/java/AABBTest.java @@ -0,0 +1,28 @@ +import math.Interval; +import math.Point; +import math.Vector; +import objects.AABB; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class AABBTest { + @Test + void testIntervals(){ + AABB aabb = new AABB(new Point(1, 2, 3), new Point(3, 4, 5)); + Interval x = aabb.getX(); + Interval y = aabb.getY(); + Interval z = aabb.getZ(); + assertEquals(x, new Interval(1, 3)); + assertEquals(y, new Interval(2, 4)); + assertEquals(z, new Interval(3, 5)); + + } + + @Test + void padTest(){ + AABB aabb = new AABB(new Point(0, 0, 0), new Point(0, 0, 0).move(new Vector(0, 0, 2)).move(new Vector(2, 0, 0))); + AABB padded = aabb.pad(); + assertNotEquals(padded.getY().size(), 0d); + } +} \ No newline at end of file diff --git a/src/test/java/ColorTest.java b/src/test/java/ColorTest.java new file mode 100644 index 0000000..c1ef0d9 --- /dev/null +++ b/src/test/java/ColorTest.java @@ -0,0 +1,31 @@ +import math.Color; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class ColorTest { + + @Test + void getByRGBInt() { + Color color = Color.getByRGB(0xff00ff); + assertEquals(color, new Color(1, 0, 1)); + } + + @Test + void getByRGB() { + Color color = Color.getByRGB(255, 0, 255); + assertEquals(color, new Color(1, 0, 1)); + } + + @Test + void scale() { + Color color = new Color(0.5, 0.4, 0.3); + assertEquals(color.scale(2), new Color(1, 0.8, 0.6)); + } + + @Test + void toRGBfromRGB(){ + Color color = new Color(1, 0, 1); + assertEquals(color, Color.getByRGB(color.toRGB())); + } +} \ No newline at end of file diff --git a/src/test/java/QuadrilateralTest.java b/src/test/java/QuadrilateralTest.java new file mode 100644 index 0000000..9cc3d80 --- /dev/null +++ b/src/test/java/QuadrilateralTest.java @@ -0,0 +1,22 @@ +import material.Lambertian; +import math.Colors; +import math.Point; +import math.Vector; +import objects.BVHNode; +import objects.Quadrilateral; +import org.junit.jupiter.api.Test; +import util.collections.impl.BoundableArrayList; + +import static org.junit.jupiter.api.Assertions.*; + +class QuadrilateralTest { + + @Test + void boundingBox() { + Quadrilateral quadrilateral = new Quadrilateral(new Point(0, 0, 0), new Vector(2, 0, 0), new Vector(0, 0, 2), new Lambertian(Colors.RED)); + BVHNode bvhNode = new BVHNode(new BoundableArrayList() {{ + add(quadrilateral); + }}); + System.out.println(bvhNode); + } +} \ No newline at end of file diff --git a/src/test/java/VectorsTest.java b/src/test/java/VectorsTest.java index c94f46f..f653913 100644 --- a/src/test/java/VectorsTest.java +++ b/src/test/java/VectorsTest.java @@ -34,4 +34,9 @@ void reflect() { Vector normal = new Vector(0, 1, 0); assertEquals(Vectors.reflect(vector, normal), new Vector(3, 3, 0)); } + + @Test + void refract() { + + } } \ No newline at end of file