Skip to content

Instantly share code, notes, and snippets.

@kishida
Created April 22, 2018 16:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kishida/2b2815b7ad271c15f7c7b6b682c3cb6a to your computer and use it in GitHub Desktop.
Save kishida/2b2815b7ad271c15f7c7b6b682c3cb6a to your computer and use it in GitHub Desktop.
SmallPT4j without ImageIO
/*
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