Created
April 22, 2018 16:05
-
-
Save kishida/2b2815b7ad271c15f7c7b6b682c3cb6a to your computer and use it in GitHub Desktop.
SmallPT4j without ImageIO
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Copyright (c) 2017 Naoki Kishida (naokikishida@gmail.com / twitter: @kis) | |
This software is released under the MIT License. | |
( https://github.com/kishida/smallpt4j/blob/master/LICENSE.txt ) | |
This is based on the smallpt( http://www.kevinbeason.com/smallpt/ ) | |
that is released under the MIT License. | |
( https://github.com/kishida/smallpt4j/blob/master/smallpt_LICENSE.txt ) | |
*/ | |
// package naoki.smallpt; | |
import static java.lang.Math.*; | |
import java.awt.image.BufferedImage; | |
import java.io.BufferedWriter; | |
import java.io.File; | |
import java.io.IOException; | |
import java.io.PrintWriter; | |
import java.nio.file.Files; | |
import java.nio.file.Paths; | |
import java.time.Duration; | |
import java.time.Instant; | |
import java.util.Arrays; | |
import java.util.concurrent.ThreadLocalRandom; | |
import java.util.concurrent.atomic.AtomicInteger; | |
import java.util.stream.IntStream; | |
import javax.imageio.ImageIO; | |
public class SmallPT { | |
private static final int SAMPLES_DEFAULT = 40; | |
static final class Vec { // Usage: time ./smallpt 5000 xv image.ppm | |
double x, y, z; // position, also color (r,g,b) | |
public Vec(double x, double y, double z) { | |
this.x = x; | |
this.y = y; | |
this.z = z; | |
} | |
Vec() { | |
this(0, 0, 0); | |
} | |
Vec add(Vec b) { | |
return new Vec(x + b.x, y + b.y, z + b.z); | |
} | |
Vec sub(Vec b) { | |
return new Vec(x - b.x, y - b.y, z - b.z); | |
} | |
Vec mul(double b) { | |
return new Vec(x * b, y * b, z * b); | |
} | |
Vec vecmul(Vec b) { | |
return new Vec(x * b.x, y * b.y, z * b.z); | |
} | |
Vec normalize() { | |
double dist = sqrt(x * x + y * y + z * z); | |
x /= dist; | |
y /= dist; | |
z /= dist; | |
return this; | |
} | |
double dot(Vec b) { | |
return x * b.x + y * b.y + z * b.z; | |
} // cross: | |
Vec mod(Vec b) { | |
return new Vec(y * b.z - z * b.y, z * b.x - x * b.z, x * b.y - y * b.x); | |
} | |
} | |
static final class Ray { | |
final Vec obj, dist; | |
public Ray(Vec o, Vec d) { | |
this.obj = o; | |
this.dist = d; | |
} | |
}; | |
private static enum Reflection { | |
DIFFUSE, SPECULAR, REFRECTION | |
}; // material types, used in radiance()// material types, used in radiance()// material types, used in radiance()// material types, used in radiance() | |
static final class Sphere { | |
final double rad; // radius | |
final Vec pos, emission, color; // position, emission, color | |
final Reflection reflection; // reflection type (DIFFuse, SPECular, REFRactive) | |
public Sphere(double rad, Vec p, Vec e, Vec c, Reflection refl) { | |
this.rad = rad; | |
this.pos = p; | |
this.emission = e; | |
this.color = c; | |
this.reflection = refl; | |
} | |
double intersect(Ray r) { // returns distance, 0 if nohit | |
Vec op = pos.sub(r.obj); // Solve t^2*d.d + 2*t*(o-p).d + (o-p).(o-p)-R^2 = 0 | |
double t, | |
eps = 1e-4, | |
b = op.dot(r.dist), | |
det = b * b - op.dot(op) + rad * rad; | |
if (det < 0) { | |
return 0; | |
} else { | |
det = sqrt(det); | |
} | |
return (t = b - det) > eps ? t : ((t = b + det) > eps ? t : 0); | |
} | |
}; | |
static final Sphere spheres[] = {//Scene: radius, position, emission, color, material | |
new Sphere(1e5, new Vec(1e5 + 1, 40.8, 81.6), new Vec(), new Vec(.75, .25, .25), Reflection.DIFFUSE),//Left | |
new Sphere(1e5, new Vec(-1e5 + 99, 40.8, 81.6), new Vec(), new Vec(.25, .25, .75), Reflection.DIFFUSE),//Rght | |
new Sphere(1e5, new Vec(50, 40.8, 1e5), new Vec(), new Vec(.75, .75, .75), Reflection.DIFFUSE),//Back | |
new Sphere(1e5, new Vec(50, 40.8, -1e5 + 170), new Vec(), new Vec(), Reflection.DIFFUSE),//Frnt | |
new Sphere(1e5, new Vec(50, 1e5, 81.6), new Vec(), new Vec(.75, .75, .75), Reflection.DIFFUSE),//Botm | |
new Sphere(1e5, new Vec(50, -1e5 + 81.6, 81.6), new Vec(), new Vec(.75, .75, .75), Reflection.DIFFUSE),//Top | |
new Sphere(16.5, new Vec(27, 16.5, 47), new Vec(), new Vec(1, 1, 1).mul(.999), Reflection.SPECULAR),//Mirr | |
new Sphere(16.5, new Vec(73, 16.5, 78), new Vec(), new Vec(1, 1, 1).mul(.999), Reflection.REFRECTION),//Glas | |
new Sphere(600, new Vec(50, 681.6 - .27, 81.6), new Vec(12, 12, 12), new Vec(), Reflection.DIFFUSE) //Lite | |
}; | |
static double clamp(double x) { | |
return x < 0 ? 0 : x > 1 ? 1 : x; | |
} | |
static int toInt(double x) { | |
return min(255, (int) (pow(clamp(x), 1 / 2.2) * 255 + .5)); | |
} | |
private static final double INF = 1e20; | |
private static final Vec UNIT_X = new Vec(1, 0, 0); | |
private static final Vec UNIT_Y = new Vec(0, 1, 0); | |
static boolean intersect(Ray r, double[] t, int[] id) { | |
t[0] = INF; | |
for (int i = 0; i < spheres.length; ++i) { | |
double d = spheres[i].intersect(r); | |
if (d != 0 && (d < t[0])) { | |
t[0] = d; | |
id[0] = i; | |
} | |
} | |
return t[0] < INF; | |
} | |
private static double getRandom() { | |
return ThreadLocalRandom.current().nextDouble(); | |
} | |
static Vec radiance(Ray r, int depth) { | |
double[] t = {0}; // distance to intersection | |
int[] id = {0}; // id of intersected object | |
if (!intersect(r, t, id)) { | |
return new Vec(); // if miss, return black | |
} | |
Sphere obj = spheres[id[0]]; // the hit object | |
Vec x = r.obj.add(r.dist.mul(t[0])); | |
Vec n = x.sub(obj.pos).normalize(); | |
Vec nl = n.dot(r.dist) < 0 ? n : n.mul(-1); | |
Vec f = obj.color; | |
double p = max(f.x, max(f.y, f.z)); // max refl | |
depth++; | |
if (depth > 5) { | |
if (depth < 50 && getRandom() < p) { | |
f = f.mul(1 / p); | |
} else { | |
return obj.emission; //R.R. | |
} | |
} | |
if (null == obj.reflection) { | |
throw new IllegalStateException(); | |
} else switch (obj.reflection) { | |
case DIFFUSE: | |
double r1 = 2 * Math.PI * getRandom(), | |
r2 = getRandom(), | |
r2s = sqrt(r2); | |
Vec w = nl, | |
u = ((abs(w.x) > .1 ? UNIT_Y : UNIT_X).mod(w)).normalize(), | |
v = w.mod(u); | |
Vec d = (u.mul(cos(r1) * r2s).add(v.mul(sin(r1) * r2s)).add(w.mul(sqrt(1 - r2)))).normalize(); | |
return obj.emission.add(f.vecmul(radiance(new Ray(x, d), depth))); | |
case SPECULAR: | |
// Ideal SPECULAR reflection | |
return obj.emission.add(f.vecmul(radiance(new Ray(x, r.dist.sub(n.mul(2 * n.dot(r.dist)))), depth))); | |
case REFRECTION: | |
Ray reflectionRay = new Ray(x, r.dist.sub(n.mul(2 * n.dot(r.dist)))); // Ideal dielectric REFRACTION | |
boolean into = n.dot(nl) > 0; // Ray from outside going in? | |
double nc = 1, | |
nt = 1.5, | |
nnt = into ? nc / nt : nt / nc, | |
ddn = r.dist.dot(nl), | |
cos2t = 1 - nnt * nnt * (1 - ddn * ddn); | |
if (cos2t < 0) { // Total internal reflection | |
return obj.emission.add(f.vecmul(radiance(reflectionRay, depth))); | |
} | |
Vec tdir = (r.dist.mul(nnt).sub(n.mul((into ? 1 : -1) * (ddn * nnt + sqrt(cos2t))))).normalize(); | |
double a = nt - nc, | |
b = nt + nc, | |
R0 = a * a / (b * b), | |
c = 1 - (into ? -ddn : tdir.dot(n)); | |
double Re = R0 + (1 - R0) * c * c * c * c * c, | |
Tr = 1 - Re, | |
probability = .25 + .5 * Re, | |
RP = Re / probability, | |
TP = Tr / (1 - probability); | |
return obj.emission.add(f.vecmul(depth > 2 ? (getRandom() < probability // Russian roulette | |
? radiance(reflectionRay, depth).mul(RP) : radiance(new Ray(x, tdir), depth).mul(TP)) | |
: radiance(reflectionRay, depth).mul(Re).add(radiance(new Ray(x, tdir), depth).mul(Tr)))); | |
default: | |
throw new IllegalStateException(); | |
} | |
} | |
public static void main(String... argv) throws IOException { | |
int w = 1024, | |
h = 768, | |
samps = (argv.length > 0 ? Integer.parseInt(argv[0]) : SAMPLES_DEFAULT )/ 4; // # samples | |
System.out.printf("JVM:%s sample:%d%n", System.getProperty("java.version"), samps * 4); | |
Ray cam = new Ray(new Vec(50, 52, 295.6), new Vec(0, -0.042612, -1).normalize()); // cam pos, dir | |
Vec cx = new Vec(w * .5135 / h, 0, 0), | |
cy = (cx.mod(cam.dist)).normalize().mul(.5135); | |
Instant start = Instant.now(); | |
Vec[] c = new Vec[w * h]; | |
Arrays.fill(c, new Vec()); | |
AtomicInteger count = new AtomicInteger(); | |
IntStream.range(0, h).parallel().forEach(y -> { | |
//for (int y = 0; y < h; ++y) { | |
// System.out.printf("Rendering (%d spp) %5.2f%%%n", samps * 4, 100. * count.getAndIncrement() / (h - 1)); | |
for (int x = 0; x < w; x++) {// Loop cols | |
int i = (h - y - 1) * w + x; | |
for (int sy = 0; sy < 2; sy++) { // 2x2 subpixel rows | |
for (int sx = 0; sx < 2; sx++) { // 2x2 subpixel cols | |
Vec r = new Vec(); | |
for (int s = 0; s < samps; s++) { | |
double r1 = 2 * getRandom(), | |
dx = r1 < 1 ? sqrt(r1) - 1 : 1 - sqrt(2 - r1); | |
double r2 = 2 * getRandom(), | |
dy = r2 < 1 ? sqrt(r2) - 1 : 1 - sqrt(2 - r2); | |
Vec d = cx.mul(((sx + .5 + dx) / 2 + x) / w - .5) | |
.add(cy.mul(((sy + .5 + dy) / 2 + y) / h - .5)).add(cam.dist); | |
r = r.add(radiance(new Ray(cam.obj.add(d.mul(140)), d.normalize()), 0)); | |
} // Camera rays are pushed ^^^^^ forward to start in interior | |
r = r.mul(1. / samps); | |
c[i] = c[i].add(new Vec(clamp(r.x), clamp(r.y), clamp(r.z)).mul(.25)); | |
} | |
} | |
} | |
}); | |
Instant finish = Instant.now(); | |
System.out.printf("Samples:%d Type:%s Time:%s%n", | |
samps * 4, | |
"master", | |
Duration.between(start, finish)); | |
try(BufferedWriter bw = Files.newBufferedWriter(Paths.get("image.ppm")); | |
PrintWriter pw = new PrintWriter(bw)) { | |
pw.printf("P3\n%d %d\n%d\n", w, h, 255); | |
for (Vec v : c) { | |
pw.printf("%d %d %d ", toInt(v.x), toInt(v.y), toInt(v.z)); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment