/* JWildfire - an image and animation processor written in Java Copyright (C) 1995-2017 Andreas Maschke This is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This software 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this software; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jwildfire.create.tina.base.solidrender; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import org.jwildfire.base.Prefs; import org.jwildfire.base.ThreadTools; import org.jwildfire.base.Tools; import org.jwildfire.base.mathlib.MathLib; import org.jwildfire.create.tina.base.Flame; import org.jwildfire.create.tina.base.raster.NormalsCalculator; import org.jwildfire.create.tina.random.AbstractRandomGenerator; import org.jwildfire.create.tina.random.MarsagliaRandomGenerator; import org.jwildfire.create.tina.render.FilterHolder; import org.jwildfire.create.tina.render.filter.FilterKernelType; import org.jwildfire.create.tina.render.image.AbstractImageRenderThread; @SuppressWarnings("serial") public class AOCalculator implements Serializable { private final float zBuf[][]; private final float nxBuf[][]; private final float nyBuf[][]; private final float nzBuf[][]; private final int rasterWidth, rasterHeight; private final double aoSearchRadius, aoBlurRadius, aoFalloff; private final int aoRadiusSamples, aoAzimuthSamples; private final AbstractRandomGenerator randGen; public AOCalculator(Flame flame, int rasterWidth, int rasterHeight, float zBuf[][], float nxBuf[][], float nyBuf[][], float nzBuf[][]) { this.rasterWidth = rasterWidth; this.rasterHeight = rasterHeight; this.zBuf = zBuf; this.nxBuf = nxBuf; this.nyBuf = nyBuf; this.nzBuf = nzBuf; aoSearchRadius = Tools.limitValue(flame.getSolidRenderSettings().getAoSearchRadius(), 0.25, 120.0); aoBlurRadius = Tools.limitValue(flame.getSolidRenderSettings().getAoBlurRadius(), 0.25, 10.0); aoFalloff = Tools.limitValue(flame.getSolidRenderSettings().getAoFalloff(), 0.0, 10.0); aoRadiusSamples = Tools.limitValue(flame.getSolidRenderSettings().getAoRadiusSamples(), 1, 128); aoAzimuthSamples = Tools.limitValue(flame.getSolidRenderSettings().getAoAzimuthSamples(), 1, 128); randGen = new MarsagliaRandomGenerator(); randGen.randomize(32851137); } public void refreshAO(float destBuf[][]) { double imgSize = MathLib.sqrt(rasterWidth * rasterWidth + rasterHeight * rasterHeight); double blurRadius = aoBlurRadius * imgSize / 500.0; boolean doSmooth = blurRadius >= 0.42; float[][] aoBuf = doSmooth ? new float[rasterWidth][rasterHeight] : destBuf; int threadCount = Prefs.getPrefs().getTinaRenderThreads(); if (threadCount < 1 || rasterHeight < 8 * threadCount) { threadCount = 1; } int rowsPerThread = rasterHeight / threadCount; { List<RefreshAOThread> threads = new ArrayList<>(); for (int i = 0; i < threadCount; i++) { int startRow = i * rowsPerThread; int endRow = i < threadCount - 1 ? startRow + rowsPerThread : rasterHeight; RefreshAOThread thread = new RefreshAOThread(startRow, endRow, aoBuf); threads.add(thread); if (threadCount > 1) { new Thread(thread).start(); } else { thread.run(); } } ThreadTools.waitForThreads(threadCount, threads); } if (doSmooth) { List<SmoothAOBufferThread> threads = new ArrayList<>(); for (int i = 0; i < threadCount; i++) { int startRow = i * rowsPerThread; int endRow = i < threadCount - 1 ? startRow + rowsPerThread : rasterHeight; SmoothAOBufferThread thread = new SmoothAOBufferThread(startRow, endRow, aoBuf, destBuf, FilterKernelType.GAUSSIAN, blurRadius, 0.1); threads.add(thread); if (threadCount > 1) { new Thread(thread).start(); } else { thread.run(); } } ThreadTools.waitForThreads(threadCount, threads); } } private double getZ(double x, double y) { return zBuf[getXCoord(x)][getYCoord(y)]; } private int getXCoord(double x) { int xi = Tools.FTOI(x); if (xi < 0) { xi = 0; } else if (xi >= rasterWidth) { xi = rasterWidth - 1; } return xi; } private int getYCoord(double y) { int yi = Tools.FTOI(y); if (yi < 0) { yi = 0; } else if (yi >= rasterHeight) { yi = rasterHeight - 1; } return yi; } private class SmoothAOBufferThread extends AbstractImageRenderThread { private final float[][] srcBuf, dstBuf; private final int startRow, endRow; private final double scale; private final double[][] f; private final int filterPixSize, fCenter; public SmoothAOBufferThread(int startRow, int endRow, float[][] srcBuf, float[][] dstBuf, FilterKernelType kernelType, double filterRadius, double scale) { this.startRow = startRow; this.endRow = endRow; this.srcBuf = srcBuf; this.dstBuf = dstBuf; this.scale = scale; FilterHolder filter = new FilterHolder(kernelType, 1, filterRadius); f = filter.getFilter(); filterPixSize = f.length; fCenter = filterPixSize / 2; } public void run() { setDone(false); try { for (int i = startRow; i < endRow; i++) { for (int j = 0; j < rasterWidth; j++) { double v = 0.0; for (int k = 0; k < filterPixSize; k++) { int px = j + k - fCenter; if (px >= 0 && px < rasterWidth) { for (int l = 0; l < filterPixSize; l++) { int py = i + l - fCenter; if (py >= 0 && py < rasterHeight) { v += srcBuf[px][py] * f[k][l]; } } } } dstBuf[j][i] = (float) (v * scale); } } } finally { setDone(true); } } } @SuppressWarnings("serial") private class RefreshAOThread extends AbstractImageRenderThread { private final float aoBuf[][]; private final int startRow, endRow; private final double imgSize, sphere_radius, radius_stepsize, azimuth_stepsize; private final double r_jitter, angleBias, ao_falloff, ao_intensity; private final int radius_samples, azimuth_samples; public RefreshAOThread(int startRow, int endRow, float aoBuf[][]) { this.startRow = startRow; this.endRow = endRow; this.aoBuf = aoBuf; imgSize = MathLib.sqrt(rasterWidth * rasterWidth + rasterHeight * rasterHeight); sphere_radius = aoSearchRadius * imgSize / 500.0; radius_samples = aoRadiusSamples; radius_stepsize = sphere_radius / (double) radius_samples; azimuth_samples = aoAzimuthSamples; azimuth_stepsize = MathLib.M_2PI / (double) azimuth_samples; r_jitter = radius_stepsize / 10.0; angleBias = 0.001; ao_falloff = aoFalloff; ao_intensity = 0.5; } @Override public void run() { setDone(false); try { for (int i = startRow; i < endRow; i++) { for (int j = 0; j < rasterWidth; j++) { double x0 = j; double y0 = i; double z0 = zBuf[j][i]; double nx = nxBuf[j][i]; double ny = nyBuf[j][i]; double nz = nzBuf[j][i]; if (z0 != NormalsCalculator.ZBUF_ZMIN && nx != NormalsCalculator.ZBUF_ZMIN) { double rndAngle = (0.5 - randGen.random()) * azimuth_stepsize / 4.0; double angle = rndAngle; for (int k = 0; k < azimuth_samples; k++, angle += azimuth_stepsize) { double dx = MathLib.cos(angle); double dy = MathLib.sin(angle); double r0 = radius_stepsize; double prevH = 0.0; for (int l = 0; l < radius_samples; l++, r0 += radius_stepsize) { double r = r0 + (0.5 - randGen.random()) * r_jitter; double x = x0 + r * dx + (0.5 - randGen.random()) * r_jitter; double y = y0 + r * dy + (0.5 - randGen.random()) * r_jitter; double z = getZ(x, y); double h = MathLib.atan2(z - z0, r) + angleBias; double Rx = x; double Ry = y; double RR = MathLib.sqrt(Rx * Rx + Ry * Ry); Rx /= RR; Ry /= RR; double tt = (nx * Rx + Ry * ny); double tx = -(Ry * nx * ny - Rx * ny * ny - Rx * nz * nz) / tt; double ty = (Ry * nx * nx + Ry * nz * nz - nx * Rx * ny) / tt; double tz = (-Ry * ny * nz - nx * Rx * nz) / tt; double ta = MathLib.atan2(tz, MathLib.sqrt(tx * tx + ty * ty)); double ao = MathLib.sin(h) - MathLib.sin(ta); if (ao > prevH) { prevH = ao; double dist = r / sphere_radius; aoBuf[j][i] += ao * MathLib.exp(-dist * dist * ao_falloff) * ao_intensity; } } } } } } } finally { setDone(true); } } } }