/* Copyright (c) 2012 Jesper Öqvist <jesper@llbit.se>
*
* This file is part of Chunky.
*
* Chunky is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Chunky is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with Chunky. If not, see <http://www.gnu.org/licenses/>.
*/
package se.llbit.chunky.renderer;
import se.llbit.chunky.renderer.scene.Camera;
import se.llbit.chunky.renderer.scene.RayTracer;
import se.llbit.chunky.renderer.scene.Scene;
import se.llbit.log.Log;
import se.llbit.math.QuickMath;
import se.llbit.math.Ray;
import java.util.Random;
/**
* Performs rendering work.
*
* @author Jesper Öqvist <jesper@llbit.se>
*/
public class RenderWorker extends Thread {
/**
* Sleep interval (in ns)
*/
private static final int SLEEP_INTERVAL = 75000000;
private final int id;
private final AbstractRenderManager manager;
private final WorkerState state;
private final RayTracer previewRayTracer;
private final RayTracer rayTracer;
private long jobTime = 0;
/**
* Create a new render worker, slave to a given render manager.
*
* @param manager the parent render manager
* @param id the ID for this worker
* @param seed the random generator seed
*/
public RenderWorker(AbstractRenderManager manager, int id, long seed) {
super("3D Render Worker " + id);
this.manager = manager;
this.previewRayTracer = manager.getPreviewRayTracer();
this.rayTracer = manager.getRayTracer();
this.id = id;
state = new WorkerState();
state.random = new Random(seed);
state.ray = new Ray();
}
@Override public void run() {
try {
try {
while (!isInterrupted()) {
work(manager.getNextJob());
manager.jobDone();
}
} catch (InterruptedException ignored) {
// Interrupted.
}
} catch (Throwable e) {
Log.error("Render worker " + id +
" crashed with uncaught exception.", e);
}
}
/**
* Perform the rendering work for a single render job.
*
* @throws InterruptedException interrupted while sleeping
*/
private void work(int jobId) throws InterruptedException {
Scene scene = manager.getBufferedScene();
Random random = state.random;
Ray ray = state.ray;
int width = scene.canvasWidth();
int height = scene.canvasHeight();
double halfWidth = width / (2.0 * height);
double invHeight = 1.0 / height;
// Calculate pixel bounds for this job.
int xjobs = (width + (manager.tileWidth - 1)) / manager.tileWidth;
int x0 = manager.tileWidth * (jobId % xjobs);
int x1 = Math.min(x0 + manager.tileWidth, width);
int y0 = manager.tileWidth * (jobId / xjobs);
int y1 = Math.min(y0 + manager.tileWidth, height);
double[] samples = scene.getSampleBuffer();
final Camera cam = scene.camera();
long jobStart = System.nanoTime();
if (scene.getMode() != RenderMode.PREVIEW) {
for (int y = y0; y < y1; ++y) {
int offset = y * width * 3 + x0 * 3;
for (int x = x0; x < x1; ++x) {
double sr = 0;
double sg = 0;
double sb = 0;
for (int i = 0; i < RenderConstants.SPP_PER_PASS; ++i) {
double oy = random.nextDouble();
double ox = random.nextDouble();
cam.calcViewRay(ray, random, (-halfWidth + (x + ox) * invHeight),
(-.5 + (y + oy) * invHeight));
scene.rayTrace(rayTracer, state);
sr += ray.color.x;
sg += ray.color.y;
sb += ray.color.z;
}
double sinv = 1.0 / (scene.spp + RenderConstants.SPP_PER_PASS);
samples[offset + 0] = (samples[offset + 0] * scene.spp + sr) * sinv;
samples[offset + 1] = (samples[offset + 1] * scene.spp + sg) * sinv;
samples[offset + 2] = (samples[offset + 2] * scene.spp + sb) * sinv;
if (scene.shouldFinalizeBuffer()) {
scene.finalizePixel(x, y);
}
offset += 3;
}
}
} else {
// Preview rendering.
Ray target = new Ray(ray);
boolean hit = scene.traceTarget(target);
int tx = (int) QuickMath.floor(target.o.x + target.d.x * Ray.OFFSET);
int ty = (int) QuickMath.floor(target.o.y + target.d.y * Ray.OFFSET);
int tz = (int) QuickMath.floor(target.o.z + target.d.z * Ray.OFFSET);
for (int x = x0; x < x1; ++x)
for (int y = y0; y < y1; ++y) {
boolean firstFrame = scene.previewCount > 1;
if (firstFrame) {
if (((x + y) % 2) == 0) {
continue;
}
} else {
if (((x + y) % 2) != 0) {
scene.finalizePixel(x, y);
continue;
}
}
// Draw the crosshairs.
if (x == width / 2 && (y >= height / 2 - 5 && y <= height / 2 + 5) || y == height / 2 && (
x >= width / 2 - 5 && x <= width / 2 + 5)) {
samples[(y * width + x) * 3 + 0] = 0xFF;
samples[(y * width + x) * 3 + 1] = 0xFF;
samples[(y * width + x) * 3 + 2] = 0xFF;
scene.finalizePixel(x, y);
continue;
}
cam.calcViewRay(ray, random, (-halfWidth + (double) x * invHeight),
(-.5 + (double) y * invHeight));
scene.rayTrace(previewRayTracer, state);
// Target highlighting.
int rx = (int) QuickMath.floor(ray.o.x + ray.d.x * Ray.OFFSET);
int ry = (int) QuickMath.floor(ray.o.y + ray.d.y * Ray.OFFSET);
int rz = (int) QuickMath.floor(ray.o.z + ray.d.z * Ray.OFFSET);
if (hit && tx == rx && ty == ry && tz == rz) {
ray.color.x = 1 - ray.color.x;
ray.color.y = 1 - ray.color.y;
ray.color.z = 1 - ray.color.z;
ray.color.w = 1;
}
samples[(y * width + x) * 3 + 0] = ray.color.x;
samples[(y * width + x) * 3 + 1] = ray.color.y;
samples[(y * width + x) * 3 + 2] = ray.color.z;
scene.finalizePixel(x, y);
if (firstFrame) {
if (y % 2 == 0 && x < (width - 1)) {
// Copy the current pixel to the next one.
scene.copyPixel(y * width + x, 1);
} else if (y % 2 != 0 && x > 0) {
// Copy the next pixel to this pixel.
scene.copyPixel(y * width + x, -1);
}
}
}
}
jobTime += System.nanoTime() - jobStart;
if (jobTime > SLEEP_INTERVAL) {
if (manager.cpuLoad < 100) {
// sleep = jobTime * (1-utilization) / utilization
double load = (100.0 - manager.cpuLoad) / manager.cpuLoad;
sleep((long) ((jobTime / 1000000.0) * load));
}
jobTime = 0;
}
}
}