/* Copyright (c) 2017 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 org.junit.Test; import se.llbit.chunky.main.Chunky; import se.llbit.chunky.main.ChunkyOptions; import se.llbit.chunky.renderer.projection.ProjectionMode; import se.llbit.chunky.renderer.scene.Scene; import se.llbit.chunky.renderer.scene.Sky; import se.llbit.json.JsonObject; import se.llbit.math.Vector4; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; /** * Simple integration tests to verify that rendering * a blank scene works as it should. * The tests render using a small canvas size and with * only two samples per pixel. */ public class TestBlankRender { private static final int WIDTH = Math.max(10, Scene.MIN_CANVAS_WIDTH); private static final int HEIGHT = Math.max(10, Scene.MIN_CANVAS_HEIGHT); static class MockSceneProvider implements SceneProvider { private final Scene scene; private boolean change = true; public MockSceneProvider(Scene scene) { this.scene = scene; } @Override public synchronized ResetReason awaitSceneStateChange() throws InterruptedException { while (!change) { wait(); } change = false; return ResetReason.SCENE_LOADED; } @Override public synchronized boolean pollSceneStateChange() { return change; } @Override public synchronized void withSceneProtected(Consumer<Scene> fun) { fun.accept(scene); } @Override public synchronized void withEditSceneProtected(Consumer<Scene> fun) { // Won't be edited by the scene manager. } } private static void renderAndCheckSamples(Scene scene, double[] expected) throws InterruptedException { double[] samples = render(scene); int offset = 0; for (int i = 0; i < WIDTH * HEIGHT; ++i) { // Check each channel value: for (int cc = 0; cc < 3; ++cc) { if (samples[offset + cc] < expected[cc] - 0.005 || samples[offset + cc] > expected[cc] + 0.005) { assertEquals("Sampled pixel is outside expected value range.", expected[cc], samples[offset + cc], 0.005); fail("Sampled pixel is outside expected value range."); } } offset += 3; } } /** Renders a scene and returns the resulting sample buffer. */ private static double[] render(Scene scene) throws InterruptedException { // A single worker thread is used, with fixed PRNG seed. // This makes the path tracing results deterministic. ChunkyOptions options = ChunkyOptions.getDefaults(); options.renderThreads = 1; Chunky chunky = new Chunky(options); RenderContext context = new RenderContext(chunky); context.workerFactory = (renderManager, index, seed) -> new RenderWorker(renderManager, index, 0); RenderManager renderer = new RenderManager(context, true); renderer.setSceneProvider(new MockSceneProvider(scene)); renderer.start(); renderer.join(); return renderer.getBufferedScene().getSampleBuffer(); } /** Compares two sample buffers. */ private static void compareSamples(double[] expected, double[] actual, int size, double delta) throws InterruptedException { for (int i = 0; i < size; ++i) { if (actual[i] < expected[i] - delta || actual[i] > expected[i] + delta) { assertEquals("Sampled pixel is outside expected value range.", expected[i], actual[i], delta); fail("Sampled pixel is outside expected value range."); } } } /** * Render with a fully black sky. */ @Test public void testBlackRender() throws InterruptedException { final Scene scene = new Scene(); scene.setCanvasSize(WIDTH, HEIGHT); scene.setRenderMode(RenderMode.RENDERING); scene.setTargetSpp(2); scene.setName("foobar"); scene.sky().setSkyMode(Sky.SkyMode.BLACK); renderAndCheckSamples(scene, new double[] {0, 0, 0}); } /** * Render with a gray sky. */ @Test public void testGrayRender() throws InterruptedException { final Scene scene = new Scene(); scene.setCanvasSize(WIDTH, HEIGHT); scene.setRenderMode(RenderMode.RENDERING); scene.sky().setSkyMode(Sky.SkyMode.GRADIENT); List<Vector4> white = new ArrayList<>(); white.add(new Vector4(0.5, 0.5, 0.5, 0)); white.add(new Vector4(0.5, 0.5, 0.5, 1)); scene.sky().setGradient(white); scene.setTargetSpp(2); scene.setName("gray"); renderAndCheckSamples(scene, new double[] {0.5, 0.5, 0.5}); } /** * Test that render output is correct after JSON export/import. */ @Test public void testJsonRoundTrip1() throws InterruptedException { final Scene scene = new Scene(); scene.setCanvasSize(WIDTH, HEIGHT); scene.setRenderMode(RenderMode.RENDERING); scene.sky().setSkyMode(Sky.SkyMode.GRADIENT); List<Vector4> white = new ArrayList<>(); white.add(new Vector4(0.5, 1, 0.25, 0)); white.add(new Vector4(0.5, 1, 0.25, 1)); scene.sky().setGradient(white); scene.setTargetSpp(2); scene.setName("json1"); JsonObject json = scene.toJson(); scene.fromJson(json); scene.setRenderMode(RenderMode.RENDERING); // Un-pause after JSON import. renderAndCheckSamples(scene, new double[] {0.5, 1, 0.25}); } /** * Test that render output is correct after JSON export/import. */ @Test public void testJsonRoundTrip2() throws InterruptedException { final Scene scene = new Scene(); scene.setTargetSpp(2); scene.setName("json2"); scene.setCanvasSize(WIDTH, HEIGHT); scene.setRenderMode(RenderMode.RENDERING); scene.sky().setSkyMode(Sky.SkyMode.SIMULATED); scene.camera().setProjectionMode(ProjectionMode.PANORAMIC); scene.camera().setFoV(100); int size = 3 * WIDTH * HEIGHT; double[] samples1 = new double[size]; System.arraycopy(render(scene), 0, samples1, 0, size); JsonObject json = scene.toJson(); scene.fromJson(json); scene.setRenderMode(RenderMode.RENDERING); // Un-pause after JSON import. compareSamples(samples1, render(scene), size, 0.005); } }