diff --git a/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeCoordinatesTest/renderings/Mitsuba/scene-001.mat b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeCoordinatesTest/renderings/Mitsuba/scene-001.mat
new file mode 100644
index 0000000000000000000000000000000000000000..91b97c1fb5bd7b3f3f3430d01789d7853c2c57e1
Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeCoordinatesTest/renderings/Mitsuba/scene-001.mat differ
diff --git a/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeCoordinatesTest/renderings/PBRT/scene-001.mat b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeCoordinatesTest/renderings/PBRT/scene-001.mat
new file mode 100644
index 0000000000000000000000000000000000000000..87847b68e8bb9a9c672bf3f862a1f794973df60a
Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeCoordinatesTest/renderings/PBRT/scene-001.mat differ
diff --git a/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereMatte.mat b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereMatte.mat
new file mode 100644
index 0000000000000000000000000000000000000000..147757056e2f6d9761765ad2b533fe88fa5fa65a
Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereMatte.mat differ
diff --git a/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereMetal.mat b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereMetal.mat
new file mode 100644
index 0000000000000000000000000000000000000000..ec74afcd8290b20e9f36097cba384fe64cb5c3d8
Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereMetal.mat differ
diff --git a/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereWard.mat b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereWard.mat
new file mode 100644
index 0000000000000000000000000000000000000000..ab258e33129912f2dad8db80ce287b2d46accd4b
Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereWard.mat differ
diff --git a/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereMatte.mat b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereMatte.mat
new file mode 100644
index 0000000000000000000000000000000000000000..ae0d5c3171187ac6cf9f51b15d0831f13912d524
Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereMatte.mat differ
diff --git a/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereMetal.mat b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereMetal.mat
new file mode 100644
index 0000000000000000000000000000000000000000..581b93e5b347f15f1ba7d0cdecaaea7ec0cc5a3d
Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereMetal.mat differ
diff --git a/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereWard.mat b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereWard.mat
new file mode 100644
index 0000000000000000000000000000000000000000..531b93f5db0a6607ae6846f8c1ee2beee5dc0aff
Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereWard.mat differ
diff --git a/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereMatte.mat b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereMatte.mat
new file mode 100644
index 0000000000000000000000000000000000000000..9581989dddc44c36f608e625b8f8d5526b23cebe
Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereMatte.mat differ
diff --git a/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereMetal.mat b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereMetal.mat
new file mode 100644
index 0000000000000000000000000000000000000000..68288e28e102fc195ba0b794eae61485c586d794
Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereMetal.mat differ
diff --git a/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereWard.mat b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereWard.mat
new file mode 100644
index 0000000000000000000000000000000000000000..de843d723570e18cb2e736e1044026f0fd93a5a0
Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereWard.mat differ
diff --git a/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereMatte.mat b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereMatte.mat
new file mode 100644
index 0000000000000000000000000000000000000000..db91b26ce2d86e0a604c259679c02dc950380ee6
Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereMatte.mat differ
diff --git a/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereMetal.mat b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereMetal.mat
new file mode 100644
index 0000000000000000000000000000000000000000..141ea0c82a44a3f8213304a0a8d7aa24bac68324
Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereMetal.mat differ
diff --git a/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereWard.mat b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereWard.mat
new file mode 100644
index 0000000000000000000000000000000000000000..23ffa17b5bac30b8dcf5c4c2d18e1d66ebce4afb
Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesA/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereWard.mat differ
diff --git a/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeCoordinatesTest/renderings/Mitsuba/scene-001.mat b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeCoordinatesTest/renderings/Mitsuba/scene-001.mat
new file mode 100644
index 0000000000000000000000000000000000000000..ed07771aae9652a555818dfe65c126dd304bffb9
Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeCoordinatesTest/renderings/Mitsuba/scene-001.mat differ
diff --git a/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeCoordinatesTest/renderings/PBRT/scene-001.mat b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeCoordinatesTest/renderings/PBRT/scene-001.mat
new file mode 100644
index 0000000000000000000000000000000000000000..b1d7f2875e428ab210b346360b25c78e8f62c9de
Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeCoordinatesTest/renderings/PBRT/scene-001.mat differ
diff --git a/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereMatte.mat b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereMatte.mat
new file mode 100644
index 0000000000000000000000000000000000000000..a5c6d16e3fb02ff1678e85120260cac738e1c849
Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereMatte.mat differ
diff --git a/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereMetal.mat b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereMetal.mat
new file mode 100644
index 0000000000000000000000000000000000000000..2c66232fd55136020ebebc16e966f274763d252d
Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereMetal.mat differ
diff --git a/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereWard.mat b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereWard.mat
new file mode 100644
index 0000000000000000000000000000000000000000..105f862a6f126f030fd43affa8dcc1bc11c33257
Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/Mitsuba/materialSphereWard.mat differ
diff --git a/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereMatte.mat b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereMatte.mat
new file mode 100644
index 0000000000000000000000000000000000000000..cc408994ce9c36cf910823a70ad931fbfe4f76d2
Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereMatte.mat differ
diff --git a/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereMetal.mat b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereMetal.mat
new file mode 100644
index 0000000000000000000000000000000000000000..38975361d6d9e56af6c9b38359ed3b0e90823f87
Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereMetal.mat differ
diff --git a/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereWard.mat b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereWard.mat
new file mode 100644
index 0000000000000000000000000000000000000000..9ab6a5cca1fca86ea126f3385918601ddc0aec25
Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereBumps/renderings/PBRT/materialSphereWard.mat differ
diff --git a/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereMatte.mat b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereMatte.mat
new file mode 100644
index 0000000000000000000000000000000000000000..acbf171682d78a79f713be6925cd5f03054c3c08
Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereMatte.mat differ
diff --git a/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereMetal.mat b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereMetal.mat
new file mode 100644
index 0000000000000000000000000000000000000000..840feacae1e34bf9beca74a98d148bad6b04cf91
Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereMetal.mat differ
diff --git a/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereWard.mat b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereWard.mat
new file mode 100644
index 0000000000000000000000000000000000000000..fe4e6ddd9e0b008a921695f01018e4eb70f90e27
Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/Mitsuba/materialSphereWard.mat differ
diff --git a/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereMatte.mat b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereMatte.mat
new file mode 100644
index 0000000000000000000000000000000000000000..066a22de2dc7bbf77a9559701a65ef4b03d8b956
Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereMatte.mat differ
diff --git a/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereMetal.mat b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereMetal.mat
new file mode 100644
index 0000000000000000000000000000000000000000..caca4383f23a79a1f9129a5b2dfd23034a1c8216
Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereMetal.mat differ
diff --git a/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereWard.mat b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereWard.mat
new file mode 100644
index 0000000000000000000000000000000000000000..ca18a50d8c7797877026ba535cd31e457364a78e
Binary files /dev/null and b/Test/Automated/Fixture/Comparison/RecipesB/rtbMakeMaterialSphereRemodeled/renderings/PBRT/materialSphereWard.mat differ
diff --git a/Test/Automated/RtbComparisonTests.m b/Test/Automated/RtbComparisonTests.m
new file mode 100644
index 0000000000000000000000000000000000000000..c15c8d7f5a4e99b0f97d0288eb30307d311ede12
--- /dev/null
+++ b/Test/Automated/RtbComparisonTests.m
@@ -0,0 +1,174 @@
+classdef RtbComparisonTests < matlab.unittest.TestCase
+    % Test functions that find and compare renderings.
+    
+    properties
+        folderA = fullfile(rtbRoot(), 'Test', 'Automated', 'Fixture', 'Comparison', 'RecipesA');
+        folderB = fullfile(rtbRoot(), 'Test', 'Automated', 'Fixture', 'Comparison', 'RecipesB');
+        scratchFolder = fullfile(tempdir(), 'RtbComparisonTests');
+    end
+    
+    methods (TestMethodSetup)
+        function cleanUpOutput(testCase)
+            if 7 == exist(testCase.scratchFolder, 'dir')
+                rmdir(testCase.scratchFolder, 's');
+            end
+        end
+    end
+    
+    
+    methods (Test)
+        
+        function testFindRenderingsSuccess(testCase)
+            expectedNames = {'rtbMakeCoordinatesTest', ...
+                'rtbMakeMaterialSphereBumps', ...
+                'rtbMakeMaterialSphereRemodeled'};
+            
+            renderingsA = rtbFindRenderings(testCase.folderA);
+            recipeNamesA = unique({renderingsA.recipeName});
+            testCase.assertEqual(recipeNamesA, expectedNames);
+            testCase.assertNumElements(renderingsA, 14);
+            
+            renderingsB = rtbFindRenderings(testCase.folderB);
+            recipeNamesB = unique({renderingsB.recipeName});
+            testCase.assertEqual(recipeNamesB, expectedNames);
+            testCase.assertNumElements(renderingsA, 14);
+        end
+        
+        function testFindRenderingsSubset(testCase)
+            renderings = rtbFindRenderings(testCase.folderA, ...
+                'filter', 'CoordinatesTest[^\.]+\.mat$');
+            testCase.assertNumElements(renderings, 2);
+            recipeNames = {renderings.recipeName};
+            testCase.assertEqual(strcmp(recipeNames, 'rtbMakeCoordinatesTest'), true(1,2));
+        end
+        
+        function testFindRenderingsNone(testCase)
+            renderings = rtbFindRenderings(testCase.folderA, ...
+                'filter', 'notagoodpattern');
+            testCase.assertEmpty(renderings);
+        end
+        
+        function fetchReferenceSuccess(testCase)
+            renderings = rtbFetchReferenceData('rtbMakeInterreflection', ...
+                'referenceRoot', testCase.scratchFolder);
+            testCase.assertNumElements(renderings, 6);
+            recipeNames = {renderings.recipeName};
+            testCase.assertEqual(strcmp(recipeNames, 'rtbMakeInterreflection'), true(1,6));
+        end
+        
+        function fetchReferenceNone(testCase)
+            renderings = rtbFetchReferenceData('nosuchrecipe', ...
+                'referenceRoot', testCase.scratchFolder);
+            testCase.assertNumElements(renderings, 0);
+        end
+        
+        function compareRenderingToSelf(testCase)
+            renderings = rtbFindRenderings(testCase.folderA);
+            comparison = rtbCompareRenderings(renderings(1), renderings(1));
+            testCase.assertEqual(comparison.corrcoef, 1, 'AbsTol', 1e-6);
+            testCase.assertEqual(comparison.relNormDiff.max, 0,'AbsTol', 1e-6);
+        end
+        
+        function compareRenderingToOther(testCase)
+            renderings = rtbFindRenderings(testCase.folderA);
+            comparison = rtbCompareRenderings(renderings(1), renderings(2));
+            testCase.assertLessThan(comparison.corrcoef, 1);
+            testCase.assertGreaterThan(comparison.relNormDiff.max, 0);
+        end
+        
+        function compareRenderingPlot(testCase)
+            renderings = rtbFindRenderings(testCase.folderA);
+            comparison = rtbCompareRenderings(renderings(1), renderings(1));
+            fig = rtbPlotRenderingComparison(comparison);
+            close(fig);
+        end
+        
+        function compareFolderToSelf(testCase)
+            comparisons = rtbCompareManyRecipes(testCase.folderA, testCase.folderA, ...
+                'fetchReferenceData', false);
+            testCase.assertNumElements(comparisons, 14);
+            testCase.assertEqual([comparisons.corrcoef], ones(1, 14), 'AbsTol', 1e-6);
+            relNormDiff = [comparisons.relNormDiff];
+            testCase.assertEqual([relNormDiff.max], zeros(1, 14) ,'AbsTol', 1e-6);
+        end
+        
+        function compareFolderToOther(testCase)
+            comparisons = rtbCompareManyRecipes(testCase.folderA, testCase.folderB, ...
+                'fetchReferenceData', false);
+            testCase.assertNumElements(comparisons, 14);
+            % real correlations can be unity, skip correlation test
+            relNormDiff = [comparisons.relNormDiff];
+            testCase.assertGreaterThan([relNormDiff.max], 0);
+        end
+        
+        function compareFolderToReference(testCase)
+            comparisons = rtbCompareManyRecipes(testCase.folderA, testCase.scratchFolder, ...
+                'fetchReferenceData', true);
+            testCase.assertNumElements(comparisons, 14);
+            % real correlations can be unity, skip correlation test
+            relNormDiff = [comparisons.relNormDiff];
+            testCase.assertGreaterThan([relNormDiff.max], 0);
+        end
+        
+        function compareFolderPlot(testCase)
+            comparisons = rtbCompareManyRecipes(testCase.folderA, testCase.folderA, ...
+                'fetchReferenceData', false);
+            fig = rtbPlotManyRecipeComparisons(comparisons);
+            close(fig);
+        end
+        
+        function epicComparisonPlots(testCase)
+            [comparisons, ~, figs] = rtbRunEpicComparison(testCase.folderA, testCase.folderA, ...
+                'plotSummary', true, ...
+                'plotImages', true);
+            testCase.assertNumElements(comparisons, 14);
+            testCase.assertNumElements(figs, 14 + 1);
+            close(figs);
+        end
+        
+        function epicComparisonNoPlots(testCase)
+            [comparisons, ~, figs] = rtbRunEpicComparison(testCase.folderA, testCase.folderA, ...
+                'plotSummary', false, ...
+                'plotImages', false);
+            testCase.assertNumElements(comparisons, 14);
+            testCase.assertEmpty(figs);
+        end
+        
+        function epicComparisonClosePlots(testCase)
+            [comparisons, ~, figs] = rtbRunEpicComparison(testCase.folderA, testCase.folderA, ...
+                'plotSummary', true, ...
+                'plotImages', true, ...
+                'closeSummary', true, ...
+                'closeImages', true);
+            testCase.assertNumElements(comparisons, 14);
+            testCase.assertEmpty(figs);
+        end
+        
+        function epicComparisonSavePlots(testCase)
+            [comparisons, ~, figs] = rtbRunEpicComparison(testCase.folderA, testCase.folderA, ...
+                'plotSummary', true, ...
+                'plotImages', true, ...
+                'closeSummary', true, ...
+                'closeImages', true, ...
+                'figureFolder', testCase.scratchFolder, ...
+                'summaryName', 'test-summary');
+            testCase.assertNumElements(comparisons, 14);
+            testCase.assertEmpty(figs);
+            
+            % summary should have been saved in scratch folder
+            summaryFig = fullfile(testCase.scratchFolder, 'test-summary.fig');
+            summaryPng = fullfile(testCase.scratchFolder, 'test-summary.png');
+            testCase.assertEqual(exist(summaryFig, 'file'), 2);
+            testCase.assertEqual(exist(summaryPng, 'file'), 2);
+            
+            % images should have been saved in the scratchFolder
+            for cc = 1:numel(comparisons)
+                identifier = comparisons(cc).renderingA.identifier;
+                imageFig = fullfile(testCase.scratchFolder, [identifier '.fig']);
+                imagePng = fullfile(testCase.scratchFolder, [identifier '.png']);
+                testCase.assertEqual(exist(imageFig, 'file'), 2);
+                testCase.assertEqual(exist(imagePng, 'file'), 2);
+            end
+        end
+    end
+end
diff --git a/Test/Automated/RtbExampleTests.m b/Test/Automated/RtbExampleTests.m
index 46bec743d2392c15bb348db96bcfa4116a5c30bd..df8f427ac08e555ff347184ad05fdb2dfb0790cd 100644
--- a/Test/Automated/RtbExampleTests.m
+++ b/Test/Automated/RtbExampleTests.m
@@ -3,6 +3,7 @@ classdef RtbExampleTests < matlab.unittest.TestCase
     
     properties
         outputRoot = fullfile(rtbWorkingFolder(), 'RtbExampleTests');
+        referenceRoot = fullfile(rtbWorkingFolder(), 'RtbReference');
     end
     
     methods (TestMethodSetup)
@@ -10,13 +11,17 @@ classdef RtbExampleTests < matlab.unittest.TestCase
             if 7 == exist(testCase.outputRoot, 'dir')
                 rmdir(testCase.outputRoot, 's');
             end
+            
+            if 7 == exist(testCase.referenceRoot, 'dir')
+                rmdir(testCase.referenceRoot, 's');
+            end
         end
     end
     
     methods (Test)
         
         function testNotAnExample(testCase)
-            results = rtbTestAllExampleScenes( ...
+            results = rtbRunEpicTest( ...
                 'outputRoot', testCase.outputRoot, ...
                 'makeFunctions', {'notAnExample.m'});
             testCase.assertFalse(results.isSuccess);
@@ -24,35 +29,79 @@ classdef RtbExampleTests < matlab.unittest.TestCase
         end
         
         function testCoordinatesTest(testCase)
-            results = rtbTestAllExampleScenes( ...
+            results = rtbRunEpicTest( ...
                 'outputRoot', testCase.outputRoot, ...
                 'makeFunctions', {'rtbMakeCoordinatesTest.m'});
             testCase.assertTrue(results.isSuccess);
             testCase.assertEmpty(results.error);
+            
+            % compare to reference rendering
+            comparisons = rtbCompareManyRecipes( ...
+                testCase.outputRoot, ...
+                testCase.referenceRoot, ...
+                'fetchReferenceData', true);
+            testCase.assertTrue(all([comparisons.isGoodComparison]));
+            testCase.assertTrue(all([comparisons.corrcoef] > 0.75));
+            relNormDiffs = [comparisons.relNormDiff];
+            testCase.assertTrue(all([relNormDiffs.max] < 2.5));
+            testCase.assertTrue(all([relNormDiffs.mean] < 2.5));
         end
         
         function testDragon(testCase)
-            results = rtbTestAllExampleScenes( ...
+            results = rtbRunEpicTest( ...
                 'outputRoot', testCase.outputRoot, ...
                 'makeFunctions', {'rtbMakeDragon.m'});
             testCase.assertTrue(results.isSuccess);
             testCase.assertEmpty(results.error);
+            
+            % compare to reference rendering
+            comparisons = rtbCompareManyRecipes( ...
+                testCase.outputRoot, ...
+                testCase.referenceRoot, ...
+                'fetchReferenceData', true);
+            testCase.assertTrue(all([comparisons.isGoodComparison]));
+            testCase.assertTrue(all([comparisons.corrcoef] > 0.75));
+            relNormDiffs = [comparisons.relNormDiff];
+            testCase.assertTrue(all([relNormDiffs.max] < 2.5));
+            testCase.assertTrue(all([relNormDiffs.mean] < 2.5));
         end
         
         function testMaterialSphereBumps(testCase)
-            results = rtbTestAllExampleScenes( ...
+            results = rtbRunEpicTest( ...
                 'outputRoot', testCase.outputRoot, ...
                 'makeFunctions', {'rtbMakeMaterialSphereBumps.m'});
             testCase.assertTrue(results.isSuccess);
             testCase.assertEmpty(results.error);
+            
+            % compare to reference rendering
+            comparisons = rtbCompareManyRecipes( ...
+                testCase.outputRoot, ...
+                testCase.referenceRoot, ...
+                'fetchReferenceData', true);
+            testCase.assertTrue(all([comparisons.isGoodComparison]));
+            testCase.assertTrue(all([comparisons.corrcoef] > 0.75));
+            relNormDiffs = [comparisons.relNormDiff];
+            testCase.assertTrue(all([relNormDiffs.max] < 2.5));
+            testCase.assertTrue(all([relNormDiffs.mean] < 2.5));
         end
         
         function testMaterialSphereRemodeled(testCase)
-            results = rtbTestAllExampleScenes( ...
+            results = rtbRunEpicTest( ...
                 'outputRoot', testCase.outputRoot, ...
                 'makeFunctions', {'rtbMakeMaterialSphereRemodeled.m'});
             testCase.assertTrue(results.isSuccess);
             testCase.assertEmpty(results.error);
+            
+            % compare to reference rendering
+            comparisons = rtbCompareManyRecipes( ...
+                testCase.outputRoot, ...
+                testCase.referenceRoot, ...
+                'fetchReferenceData', true);
+            testCase.assertTrue(all([comparisons.isGoodComparison]));
+            testCase.assertTrue(all([comparisons.corrcoef] > 0.75));
+            relNormDiffs = [comparisons.relNormDiff];
+            testCase.assertTrue(all([relNormDiffs.max] < 2.5));
+            testCase.assertTrue(all([relNormDiffs.mean] < 2.5));
         end
     end
 end
diff --git a/Test/Interactive/Comparison/rtbCompareManyRecipes.m b/Test/Interactive/Comparison/rtbCompareManyRecipes.m
new file mode 100644
index 0000000000000000000000000000000000000000..c620716a43bda6feec90449f4488064055c56aa9
--- /dev/null
+++ b/Test/Interactive/Comparison/rtbCompareManyRecipes.m
@@ -0,0 +1,121 @@
+function [comparisons, matchInfo] = rtbCompareManyRecipes(folderA, folderB, varargin)
+%% Compare paris of renderings across two folders.
+%
+% comparisons = rtbCompareManyRecipes(folderA, folderB) finds rendering
+% data files in the given folderA and folderB and attempts to match up
+% pairs of renderings that came from the same recipe and renderer.
+% For each pair, computes difference images and statistics.
+%
+% Returns a struct array of image comparisons, as returned from
+% rtbCompareRenderings().
+%
+% rtbCompareManyRecipes( ... 'fetchReferenceData', fetchReferenceData)
+% specify whether to use Remote Data Toolbox to fetch reference data for
+% comparison.  The default is true, fetch reference data when there is a
+% recipe in folderA that was not found in folderB, and cache the fetched
+% data in folderB.
+%
+%%% RenderToolbox4 Copyright (c) 2012-2017 The RenderToolbox Team.
+%%% About Us://github.com/RenderToolbox/RenderToolbox4/wiki/About-Us
+%%% RenderToolbox4 is released under the MIT License.  See LICENSE file.
+
+parser = inputParser();
+parser.KeepUnmatched = true;
+parser.addRequired('folderA', @ischar);
+parser.addRequired('folderB', @ischar);
+parser.addParameter('fetchReferenceData', true, @islogical);
+parser.parse(folderA, folderB, varargin{:});
+folderA = parser.Results.folderA;
+folderB = parser.Results.folderB;
+fetchReferenceData = parser.Results.fetchReferenceData;
+
+
+%% Identify renderings and recipes to compare.
+renderingsA = rtbFindRenderings(folderA, varargin{:});
+recipeNames = unique({renderingsA.recipeName});
+nRecipes = numel(recipeNames);
+
+renderingsB = rtbFindRenderings(folderB, varargin{:});
+
+%% Compare one recipe at a time, fetch data as necessary.
+comparisonsCell = cell(1, nRecipes);
+matchInfoCell = cell(1, nRecipes);
+for rr = 1:nRecipes
+    recipeName = recipeNames{rr};
+    fprintf('Comparing renderings for recipe <%s>.\n', recipeName);
+    
+    isRecipeA = strcmp({renderingsA.recipeName}, recipeName);
+    recipeRenderingsA = renderingsA(isRecipeA);
+    
+    % fetch missing recipe for B?
+    if isempty(renderingsB)
+        isRecipeB = false;
+    else
+        isRecipeB = strcmp({renderingsB.recipeName}, recipeName);
+    end
+    
+    if any(isRecipeB)
+        recipeRenderingsB = renderingsB(isRecipeB);
+    elseif fetchReferenceData
+        fprintf('  Fetching reference data to <%s>...\n', folderB);
+        recipeRenderingsB = rtbFetchReferenceData(recipeName, ...
+            'referenceRoot', folderB, ...
+            varargin{:});
+        if isempty(recipeRenderingsB)
+            fprintf('  ...could not fetch, skipping this recipe.\n');
+            continue;
+        else
+            fprintf('  ...OK.\n');
+        end
+    else
+        fprintf('  Skipping recipe not found in <%s>.\n', folderB);
+        continue;
+    end
+    
+    % match pairs of renderings for recipes A and B
+    info = matchRenderingPairs(recipeRenderingsA, recipeRenderingsB);
+    fprintf('  Found %d matched pairs of renderings.\n', info.nPairs);
+    
+    % run a comparison for each matched pair
+    pairsCell = cell(1, info.nPairs);
+    for pp = 1:info.nPairs
+        fprintf('    %s.\n', info.matchedA(pp).identifier);
+        pairsCell{pp} = rtbCompareRenderings(info.matchedA(pp), info.matchedB(pp), varargin{:});
+    end
+    comparisonsCell{rr} = [pairsCell{:}];
+    matchInfoCell{rr} = info;
+    
+    % report on unmatched renderings
+    if ~isempty(info.unmatchedA)
+        nUnmatched = info.unmatchedA;
+        fprintf('  %d renderings in A were not matched in B:\n', nUnmatched);
+        for uu = 1:nUnmatched
+            fprintf('    %s\n', info.info.unmatchedA(uu).identifier);
+        end
+    end
+    
+    if ~isempty(info.unmatchedB)
+        nUnmatched = info.unmatchedB;
+        fprintf('  %d renderings in B were not matched in A:\n', nUnmatched);
+        for uu = 1:nUnmatched
+            fprintf('    %s\n', info.info.unmatchedB(uu).identifier);
+        end
+    end
+end
+comparisons = [comparisonsCell{:}];
+matchInfo = [matchInfoCell{:}];
+
+
+%% For pairs of comparable renderings from two sets.
+function info = matchRenderingPairs(renderingsA, renderingsB)
+identifiersA = {renderingsA.identifier};
+identifiersB = {renderingsB.identifier};
+[~, indexA, indexB] = intersect(identifiersA, identifiersB, 'stable');
+[~, unmatchedIndexA] = setdiff(identifiersA, identifiersB);
+[~, unmatchedIndexB] = setdiff(identifiersB, identifiersA);
+
+info.nPairs = numel(indexA);
+info.matchedA = renderingsA(indexA);
+info.matchedB = renderingsB(indexB);
+info.unmatchedA = renderingsA(unmatchedIndexA);
+info.unmatchedB = renderingsB(unmatchedIndexB);
diff --git a/Test/Interactive/Comparison/rtbCompareRenderings.m b/Test/Interactive/Comparison/rtbCompareRenderings.m
new file mode 100644
index 0000000000000000000000000000000000000000..7ca36617f4c0c11634437b236dda6c28b3a8900c
--- /dev/null
+++ b/Test/Interactive/Comparison/rtbCompareRenderings.m
@@ -0,0 +1,194 @@
+function comparison = rtbCompareRenderings(renderingA, renderingB, varargin)
+%% Compare two renderings for difference images and statistics.
+%
+% comparison = rtbCompareRenderings(renderingA, renderingB) compares the
+% given renderingA against the given renderingB.  Each must be a rendering
+% record as returned from rtbRenderingRecord() or rtbFindRenderings().
+% Returns a comparison struct with many various fields including:
+%   - A -- the multispectral image from renderingA
+%   - B -- the multispectral image from renderingB
+%   - aMinusB -- the multispectral difference image A - B
+%   - bMinusA -- the multispectral difference image B - A
+%   - relNormDiff -- the min, mean, and max of per-pixel difference
+%   - corrcoef -- the overall correlation between A and B
+%   - error -- any error encountered during comparisons
+%
+% The relNormDiff is obtained by normalizing A and B each by its max value,
+% taking the absolute difference, and dividing the difference by the
+% normalized values of A.  Finally, values of relNormDiff that are less
+% than the given denominatorThreshold are set to nan.  This is to avoid
+% unfair comparisons when dividing by small values.
+%
+% rtbCompareRenderings( ... 'denominatorThreshold', denominatorThreshold)
+% specify the denominatorThreshold to use when computing the relNormDiff
+% image described above.  The default is 0.2.
+%
+%%% RenderToolbox4 Copyright (c) 2012-2017 The RenderToolbox Team.
+%%% About Us://github.com/RenderToolbox/RenderToolbox4/wiki/About-Us
+%%% RenderToolbox4 is released under the MIT License.  See LICENSE file.
+
+parser = inputParser();
+parser.KeepUnmatched = true;
+parser.addRequired('renderingA', @isstruct);
+parser.addRequired('renderingB', @isstruct);
+parser.addParameter('denominatorThreshold', 0.2, @isnumeric);
+parser.parse(renderingA, renderingB, varargin{:});
+renderingA = parser.Results.renderingA;
+renderingB = parser.Results.renderingB;
+denominatorThreshold = parser.Results.denominatorThreshold;
+
+
+%% Consistent struct fields for output.
+comparison = parser.Results;
+comparison.error = '';
+comparison.samplingA = [];
+comparison.samplingB = [];
+comparison.isGoodComparison = false;
+comparison.subpixelsA = [];
+comparison.subpixelsB = [];
+comparison.normA = [];
+comparison.normB = [];
+comparison.normDiff = [];
+comparison.absNormDiff = [];
+comparison.relNormDiff = [];
+comparison.corrcoef = [];
+comparison.A = [];
+comparison.B = [];
+comparison.aMinusB = [];
+comparison.bMinusA = [];
+
+
+%% Load multispectral images.
+dataA = load(renderingA.fileName);
+dataB = load(renderingB.fileName);
+
+% do we have images?
+hasImageA = isfield(dataA, 'multispectralImage');
+hasImageB = isfield(dataB, 'multispectralImage');
+if hasImageA
+    if hasImageB
+        % each has an image -- proceed
+    else
+        comparison.error = ...
+            'Data file A has a multispectralImage but B does not.';
+        disp(comparison.error);
+        return;
+    end
+else
+    if hasImageB
+        comparison.error = ...
+            'Data file B has a multispectralImage but A does not.';
+        disp(comparison.error);
+        return;
+    else
+        comparison.error = ...
+            'Neither data file A nor B has a multispectralImage.';
+        disp(comparison.error);
+        return;
+    end
+end
+multispectralA = dataA.multispectralImage;
+multispectralB = dataB.multispectralImage;
+
+
+%% Sanity check image dimensions.
+if ~isequal(size(multispectralA, 1), size(multispectralB, 1)) ...
+        || ~isequal(size(multispectralA, 2), size(multispectralB, 2))
+    
+    comparison.error = ...
+        sprintf('Image A[%s] is not the same size as image B[%s].', ...
+        num2str(size(multispectralA)), num2str(size(multispectralB)));
+    disp(comparison.error);
+    return;
+end
+
+
+%% Sanity check spectral sampling.
+hasSamplingA = isfield(dataA, 'S');
+hasSamplingB = isfield(dataB, 'S');
+if hasSamplingA
+    if hasSamplingB
+        % each has a sampling "S" -- proceed
+    else
+        comparison.error = ...
+            'Data file A has a spectral sampling "S" but B does not.';
+        disp(comparison.error);
+        return;
+    end
+else
+    if hasSamplingB
+        comparison.error = ...
+            'Data file B has a spectral sampling "S" but A does not.';
+        disp(comparison.error);
+        return;
+    else
+        comparison.error = ...
+            'Neither data file A nor B has spectral sampling "S".';
+        return;
+    end
+end
+comparison.samplingA = dataA.S;
+comparison.samplingB = dataB.S;
+
+% do spectral samplings agree?
+if ~isequal(dataA.S, dataB.S)
+    comparison.error = ...
+        sprintf('Spectral sampling A[%s] is not the same as B[%s].', ...
+        num2str(dataA.S), num2str(dataB.S));
+    disp(comparison.error);
+    % proceed with comparison, despite sampling mismatch
+end
+
+% match images based on sampling depths
+[multispectralA, multispectralB] = truncatePlanes( ...
+    multispectralA, multispectralB, dataA.S, dataB.S);
+
+
+%% Sanity Checks OK.
+comparison.isGoodComparison = true;
+
+
+%% Per-pixel difference stats.
+normA = multispectralA / max(multispectralA(:));
+normB = multispectralB / max(multispectralB(:));
+normDiff = normA - normB;
+absNormDiff = abs(normDiff);
+relNormDiff = absNormDiff ./ normA;
+relNormDiff(normA < denominatorThreshold) = nan;
+
+% summarize differnece stats
+comparison.subpixelsA = summarizeData(multispectralA);
+comparison.subpixelsB = summarizeData(multispectralB);
+comparison.normA = summarizeData(normA);
+comparison.normB = summarizeData(normB);
+comparison.normDiff = summarizeData(normDiff);
+comparison.absNormDiff = summarizeData(absNormDiff);
+comparison.relNormDiff = summarizeData(relNormDiff);
+
+
+%% Overall correlation.
+r = corrcoef(multispectralA(:), multispectralB(:));
+comparison.corrcoef = r(1, 2);
+
+
+%% Difference images.
+comparison.A = multispectralA;
+comparison.B = multispectralB;
+comparison.aMinusB = multispectralA - multispectralB;
+comparison.bMinusA = multispectralB - multispectralA;
+
+
+%% Truncate spectral planes if one image has more planes.
+function [truncA, truncB, truncSampling] = truncatePlanes(A, B, samplingA, samplingB)
+nPlanes = min(samplingA(3), samplingB(3));
+truncSampling = [samplingA(1:2) nPlanes];
+truncA = A(:,:,1:nPlanes);
+truncB = B(:,:,1:nPlanes);
+
+
+%% Summarize a distribuition of data with a struct of stats.
+function summary = summarizeData(data)
+finiteData = data(isfinite(data));
+summary.min = min(finiteData);
+summary.mean = mean(finiteData);
+summary.max = max(finiteData);
diff --git a/Test/Interactive/rtbFetchReferenceData.m b/Test/Interactive/Comparison/rtbFetchReferenceData.m
similarity index 89%
rename from Test/Interactive/rtbFetchReferenceData.m
rename to Test/Interactive/Comparison/rtbFetchReferenceData.m
index 8b400f66c862002c233f8a601277efbe7a9f58aa..adca9695739b4a1568e18a11029764114c8b1172 100644
--- a/Test/Interactive/rtbFetchReferenceData.m
+++ b/Test/Interactive/Comparison/rtbFetchReferenceData.m
@@ -30,6 +30,7 @@ function [renderings, referenceRoot, artifact] = rtbFetchReferenceData(recipeNam
 %%% RenderToolbox4 is released under the MIT License.  See LICENSE file.
 
 parser = inputParser();
+parser.KeepUnmatched = true;
 parser.addRequired('recipeName', @ischar);
 parser.addParameter('rdtConfig', 'render-toolbox');
 parser.addParameter('remotePath', 'reference-data', @ischar);
@@ -42,12 +43,17 @@ remotePath = parser.Results.remotePath;
 referenceVersion = parser.Results.referenceVersion;
 referenceRoot = parser.Results.referenceRoot;
 
-
 %% Get a whole recipe from the server.
 artifactPath = fullfile(remotePath, recipeName);
-[fileName, artifact] = rdtReadArtifact(rdtConfig, artifactPath, recipeName, ...
-    'version', referenceVersion, ...
-    'type', 'zip');
+try
+    [fileName, artifact] = rdtReadArtifact(rdtConfig, artifactPath, recipeName, ...
+        'version', referenceVersion, ...
+        'type', 'zip');
+catch err
+    renderings = [];
+    artifact = [];
+    return;
+end
 
 if isempty(fileName)
     renderings = [];
@@ -58,6 +64,9 @@ end
 
 %% Explode renderings it into the destination folder.
 destination = fullfile(referenceRoot, recipeName);
+if 7 ~= exist(destination, 'dir')
+    mkdir(destination);
+end
 unzip(fileName, destination);
 
 % scan for rendering records
diff --git a/Test/Interactive/rtbFindRenderings.m b/Test/Interactive/Comparison/rtbFindRenderings.m
similarity index 83%
rename from Test/Interactive/rtbFindRenderings.m
rename to Test/Interactive/Comparison/rtbFindRenderings.m
index cdde19cb50fced1a4ba722f4c9b6fd6ca2b2adaa..274c18179ec0dea76d42a3512a2f63af0842d140 100644
--- a/Test/Interactive/rtbFindRenderings.m
+++ b/Test/Interactive/Comparison/rtbFindRenderings.m
@@ -32,6 +32,7 @@ function renderings = rtbFindRenderings(rootFolder, varargin)
 %%% RenderToolbox4 is released under the MIT License.  See LICENSE file.
 
 parser = inputParser();
+parser.KeepUnmatched = true;
 parser.addRequired('rootFolder', @ischar);
 parser.addParameter('filter', '\.mat$', @ischar);
 parser.addParameter('renderingsFolderName', 'renderings', @ischar);
@@ -63,20 +64,31 @@ for ff = 1:nFiles
     renderingsFolderIndex = find(isRenderingsFolder, 1, 'last');
     
     % recipe name comes just before renderingsFolderName
-    recipeName = pathParts{renderingsFolderIndex - 1};
+    recipeNameIndex = renderingsFolderIndex - 1;
+    recipeName = pathParts{recipeNameIndex};
     
     % renderer name comes just after renderingsFolderName, if any
-    if nPathParts > renderingsFolderIndex
-        rendererName = pathParts{renderingsFolderIndex + 1};
+    rendererNameIndex = renderingsFolderIndex + 1;
+    if nPathParts >= rendererNameIndex
+        rendererName = pathParts{rendererNameIndex};
     else
         rendererName = '';
     end
     
+    % the path leading up to the recipeNamem, if any, is where it came from
+    sourceFolderIndices = 1:(recipeNameIndex-1);
+    if isempty(sourceFolderIndices)
+        sourceFolder = '';
+    else
+        sourceFolder = fullfile(pathParts{sourceFolderIndices});
+    end
+    
     renderingsCell{ff} = rtbRenderingRecord( ...
         'recipeName', recipeName, ...
         'rendererName', rendererName, ...
         'imageNumber', imageNumber, ...
         'imageName', imageName, ...
-        'fileName', files{ff});
+        'fileName', files{ff}, ...
+        'sourceFolder', sourceFolder);
 end
 renderings = [renderingsCell{:}];
diff --git a/Test/Interactive/Comparison/rtbPlotManyRecipeComparisons.m b/Test/Interactive/Comparison/rtbPlotManyRecipeComparisons.m
new file mode 100644
index 0000000000000000000000000000000000000000..72ef80c8d14779d9d8a13c8057b6acadc4073f32
--- /dev/null
+++ b/Test/Interactive/Comparison/rtbPlotManyRecipeComparisons.m
@@ -0,0 +1,134 @@
+function fig = rtbPlotManyRecipeComparisons(comparisons, varargin)
+%% Plot a many recipe comparisons from rtbCompareManyRecipes().
+%
+% fig = fig = rtbPlotManyRecipeComparisons(comparisons) makes a plot to
+% visualize the given struct array of comparison results, as produced by
+% rtbCompareManyRecipes().
+%
+%%% RenderToolbox4 Copyright (c) 2012-2017 The RenderToolbox Team.
+%%% About Us://github.com/RenderToolbox/RenderToolbox4/wiki/About-Us
+%%% RenderToolbox4 is released under the MIT License.  See LICENSE file.
+
+parser = inputParser();
+parser.KeepUnmatched = true;
+parser.addRequired('comparisons', @isstruct);
+parser.addParameter('fig', figure());
+parser.addParameter('figureWidth', 1000, @isnumeric);
+parser.addParameter('nRows', 25, @isnumeric);
+parser.addParameter('correlationMin', 0.8, @isnumeric);
+parser.addParameter('correlationStep', 0.05, @isnumeric);
+parser.addParameter('errorMax', 3, @isnumeric);
+parser.addParameter('errorStep', 0.5, @isnumeric);
+parser.parse(comparisons, varargin{:});
+comparisons = parser.Results.comparisons;
+fig = parser.Results.fig;
+figureWidth = parser.Results.figureWidth;
+nRows = parser.Results.nRows;
+correlationMin = parser.Results.correlationMin;
+correlationStep = parser.Results.correlationStep;
+errorMax = parser.Results.errorMax;
+errorStep = parser.Results.errorStep;
+
+
+%% Set up the figure.
+figureName = sprintf('Summary of %d rendering comparisons', ...
+    numel(comparisons));
+set(fig, ...
+    'Name', figureName, ...
+    'NumberTitle', 'off');
+
+position = get(fig, 'Position');
+if position(3) < figureWidth
+    position(3) = figureWidth;
+    set(fig, 'Position', position);
+end
+
+%% Summary of correlation coefficients.
+correlationTicks = correlationMin : correlationStep : 1;
+correlationTickLabels = num2cell(correlationTicks);
+correlationTickLabels{1} = '<=';
+correlation = [comparisons.corrcoef];
+correlation(correlation < correlationMin) = correlationMin;
+
+renderingsA = [comparisons.renderingA];
+names = {renderingsA.identifier};
+nLines = numel(names);
+ax(1) = subplot(1, 3, 2, ...
+    'Parent', fig, ...
+    'YTick', 1:nLines, ...
+    'YTickLabel', names, ...
+    'YGrid', 'on', ...
+    'XLim', [correlationTicks(1), correlationTicks(end)], ...
+    'XTick', correlationTicks, ...
+    'XTickLabel', correlationTickLabels);
+line(correlation, 1:nLines, ...
+    'Parent', ax(1), ...
+    'LineStyle', 'none', ...
+    'Marker', 'o', ...
+    'Color', [0 0 1])
+xlabel(ax(1), 'correlation');
+
+
+%% Overall title.
+name = sprintf('%s vs %s', ...
+    comparisons(1).renderingA.sourceFolder, ...
+    comparisons(1).renderingB.sourceFolder);
+title(ax(1), name, 'Interpreter', 'none');
+
+
+%% Summary of mean and max subpixel differences.
+errorTicks = 0 : errorStep : errorMax;
+errorTickLabels = num2cell(errorTicks);
+errorTickLabels{end} = '>=';
+
+relNormDiff = [comparisons.relNormDiff];
+maxes = [relNormDiff.max];
+means = [relNormDiff.mean];
+maxes(maxes > errorMax) = errorMax;
+means(means > errorMax) = errorMax;
+ax(2) = subplot(1, 3, 3, ...
+    'Parent', fig, ...
+    'YTick', 1:nLines, ...
+    'YTickLabel', 1:nLines, ...
+    'YAxisLocation', 'right', ...
+    'YGrid', 'on', ...
+    'XLim', [errorTicks(1), errorTicks(end)], ...
+    'XTick', errorTicks, ...
+    'XTickLabel', errorTickLabels);
+line(maxes, 1:nLines, ...
+    'Parent', ax(2), ...
+    'LineStyle', 'none', ...
+    'Marker', '+', ...
+    'Color', [1 0 0])
+line(means, 1:nLines, ...
+    'Parent', ax(2), ...
+    'LineStyle', 'none', ...
+    'Marker', 'o', ...
+    'Color', [0 0 0])
+legend(ax(2), 'max', 'mean', 'Location', 'northeast');
+xlabel(ax(2), 'relative diff');
+
+
+%% Let the user scroll both axes at the same time.
+scrollerData.axes = ax;
+scrollerData.nLinesAtATime = nRows;
+scroller = uicontrol( ...
+    'Parent', fig, ...
+    'Units', 'normalized', ...
+    'Position', [.95 0 .05 1], ...
+    'Callback', @scrollAxes, ...
+    'Min', 1, ...
+    'Max', max(2, nLines), ...
+    'Value', nLines, ...
+    'Style', 'slider', ...
+    'SliderStep', [1 2], ...
+    'UserData', scrollerData);
+scrollAxes(scroller, []);
+
+
+%% Scroll summary axes together.
+function scrollAxes(object, event)
+scrollerData = get(object, 'UserData');
+topLine = get(object, 'Value');
+yLimit = topLine + [-scrollerData.nLinesAtATime 1];
+set(scrollerData.axes, 'YLim', yLimit);
diff --git a/Test/Interactive/Comparison/rtbPlotRenderingComparison.m b/Test/Interactive/Comparison/rtbPlotRenderingComparison.m
new file mode 100644
index 0000000000000000000000000000000000000000..185bb1e0d2d19a6223f5ecdb180f4d8c78b38a75
--- /dev/null
+++ b/Test/Interactive/Comparison/rtbPlotRenderingComparison.m
@@ -0,0 +1,64 @@
+function fig = rtbPlotRenderingComparison(comparison, varargin)
+%% Plot a rendering comparison from rtbCompareRenderings().
+%
+% fig = rtbPlotRenderingComparison(comparison) makes a plot to visualize
+% the given struct of comparison results, as produced by
+% rtbCompareRenderings().
+%
+%%% RenderToolbox4 Copyright (c) 2012-2017 The RenderToolbox Team.
+%%% About Us://github.com/RenderToolbox/RenderToolbox4/wiki/About-Us
+%%% RenderToolbox4 is released under the MIT License.  See LICENSE file.
+
+parser = inputParser();
+parser.KeepUnmatched = true;
+parser.addRequired('comparison', @isstruct);
+parser.addParameter('isScale', true, @islogical);
+parser.addParameter('toneMapFactor', 0, @isnumeric);
+parser.addParameter('fig', figure());
+parser.parse(comparison, varargin{:});
+comparison = parser.Results.comparison;
+isScale = parser.Results.isScale;
+toneMapFactor = parser.Results.toneMapFactor;
+fig = parser.Results.fig;
+
+
+%% Compute RGB images for viewing.
+S = comparison.samplingA;
+rgbA = rtbMultispectralToSRGB(comparison.A, S, ...
+    'toneMapFactor', toneMapFactor, 'isScale', isScale);
+rgbB = rtbMultispectralToSRGB(comparison.B, S, ...
+    'toneMapFactor', toneMapFactor, 'isScale', isScale);
+rgbAminusB = rtbMultispectralToSRGB(comparison.aMinusB, S, ...
+    'toneMapFactor', toneMapFactor, 'isScale', isScale);
+rgbBminusA = rtbMultispectralToSRGB(comparison.bMinusA, S, ...
+    'toneMapFactor', toneMapFactor, 'isScale', isScale);
+
+
+%% Make the plot.
+name = sprintf('%s isScale %d toneMapFactor %.2f', ...
+    comparison.renderingA.identifier, ...
+    isScale, ...
+    toneMapFactor);
+set(fig, ...
+    'Name', name, ...
+    'NumberTitle', 'off');
+
+ax = subplot(2, 2, 2, 'Parent', fig);
+imshow(uint8(rgbA), 'Parent', ax);
+title(ax, 'A');
+xlabel(ax, comparison.renderingA.sourceFolder, ...
+    'Interpreter', 'none');
+
+ax = subplot(2, 2, 3, 'Parent', fig);
+imshow(uint8(rgbB), 'Parent', ax);
+title(ax, 'B');
+xlabel(ax, comparison.renderingB.sourceFolder, ...
+    'Interpreter', 'none');
+
+ax = subplot(2, 2, 1, 'Parent', fig);
+imshow(uint8(rgbAminusB), 'Parent', ax);
+title(ax, 'A - B');
+
+ax = subplot(2, 2, 4, 'Parent', fig);
+imshow(uint8(rgbBminusA), 'Parent', ax);
+title(ax, 'B - A');
diff --git a/Test/Interactive/rtbRenderingRecord.m b/Test/Interactive/Comparison/rtbRenderingRecord.m
similarity index 97%
rename from Test/Interactive/rtbRenderingRecord.m
rename to Test/Interactive/Comparison/rtbRenderingRecord.m
index ce806872ccb708c241933fe53fd713f0bd8e678d..db000e3dc45b2db96ad0e1eb4d93e7a6ee8e8863 100644
--- a/Test/Interactive/rtbRenderingRecord.m
+++ b/Test/Interactive/Comparison/rtbRenderingRecord.m
@@ -24,6 +24,7 @@ parser.addParameter('rendererName', '', @ischar);
 parser.addParameter('imageNumber', [], @isnumeric);
 parser.addParameter('imageName', '',@ischar);
 parser.addParameter('fileName', '', @ischar);
+parser.addParameter('sourceFolder', '', @ischar);
 parser.parse(varargin{:});
 
 % let the parser do most of the work
diff --git a/Test/Interactive/Comparison/rtbRunEpicComparison.m b/Test/Interactive/Comparison/rtbRunEpicComparison.m
new file mode 100644
index 0000000000000000000000000000000000000000..5ea42c274ae7a849ec309c9de81cfe21a8fb674c
--- /dev/null
+++ b/Test/Interactive/Comparison/rtbRunEpicComparison.m
@@ -0,0 +1,145 @@
+function [comparisons, matchInfo, figs] = rtbRunEpicComparison(folderA, folderB, varargin)
+%% Compare sets of renderings for similarity.
+%
+% comparisons = rtbRunEpicComparison(folderA, folderB) locates renderings
+% in folderA and folderB, compares pairs of renderings found between the
+% two folders, and plots a summary of the comparisons.  Returns a struct
+% array of comparison results, one for each pair of renderings.
+%
+% Also returns a struct array of info about how renderings were matched
+% between folderA and folderB, including renderings from each folder that
+% were unmatched.
+%
+% Also returns an array of figure handles for visualizations of the
+% comparison results.
+%
+% rtbRunEpicComparison( ... 'plotSummary', plotSummary) whether to create a
+% plot summarizing the overall comparison results.  The default is true,
+% make a summary plot.
+%
+% rtbRunEpicComparison( ... 'closeSummary', closeSummary) whether to create
+% a close the overalls summary plot when done.  This is useful when you
+% specify a figureFolder, where the summary plot can be saved to disk.  the
+% default is false, don't close the summary figure.
+%
+% rtbRunEpicComparison( ... 'plotImages', plotImages) whether to create a
+% plot showing images and difference images for each pair of renderings.
+% The default is false, don't show the images for each pair.
+%
+% rtbRunEpicComparison( ... 'closeImages', closeImages) whether to close
+% the image plot for each pair of renderings when done.  This is useful
+% when you specify a figureFolder, where the images can be saved to disk.
+% The default is true, do close the image figures.
+%
+% rtbRunEpicComparison( ... 'figureFolder', figureFolder) specify a folder
+% where generated plots should be saved to disk, as a .fig file and as a
+% .png file.  The default is '', don't save figures to disk.
+%
+%%% RenderToolbox4 Copyright (c) 2012-2017 The RenderToolbox Team.
+%%% About Us://github.com/RenderToolbox/RenderToolbox4/wiki/About-Us
+%%% RenderToolbox4 is released under the MIT License.  See LICENSE file.
+
+parser = inputParser();
+parser.addRequired('folderA', @ischar);
+parser.addRequired('folderB', @ischar);
+parser.addParameter('plotSummary', true, @islogical);
+parser.addParameter('closeSummary', false, @islogical);
+parser.addParameter('plotImages', true, @islogical);
+parser.addParameter('closeImages', false, @islogical);
+parser.addParameter('figureFolder', '', @ischar);
+parser.addParameter('summaryName', 'epic-summary', @ischar);
+parser.parse(folderA, folderB, varargin{:});
+folderA = parser.Results.folderA;
+folderB = parser.Results.folderB;
+plotSummary = parser.Results.plotSummary;
+closeSummary = parser.Results.closeSummary;
+plotImages = parser.Results.plotImages;
+closeImages = parser.Results.closeImages;
+figureFolder = parser.Results.figureFolder;
+summaryName = parser.Results.summaryName;
+
+figs = [];
+
+
+%% Run the grand comparison.
+[comparisons, matchInfo] = rtbCompareManyRecipes(folderA, folderB, ...
+    varargin{:});
+
+
+%% Sort the summary by size of error.
+goodComparisons = comparisons([comparisons.isGoodComparison]);
+relNormDiff = [goodComparisons.relNormDiff];
+errorStat = [relNormDiff.max];
+[~, order] = sort(errorStat);
+goodComparisons = goodComparisons(order);
+
+
+%% Plot the summary.
+if plotSummary
+    summaryFig = rtbPlotManyRecipeComparisons(goodComparisons, ...
+        varargin{:});
+    
+    if ~isempty(figureFolder);
+        imageFileName = fullfile(figureFolder, summaryName);
+        saveFigure(summaryFig, imageFileName);
+    end
+    
+    if closeSummary
+        close(summaryFig);
+    else
+        figs = [figs summaryFig];
+    end
+end
+
+
+%% Plot the detail images for each rendering.
+if plotImages
+    nComparisons = numel(goodComparisons);
+    imageFigs = cell(1, nComparisons);
+    for cc = 1:nComparisons
+        imageFig = rtbPlotRenderingComparison(goodComparisons(cc), ...
+            varargin{:});
+        
+        if ~isempty(figureFolder);
+            identifier = goodComparisons(cc).renderingA.identifier;
+            imageFileName = fullfile(figureFolder, identifier);
+            saveFigure(imageFig, imageFileName);
+        end
+        
+        if closeImages
+            close(imageFig);
+        else
+            imageFigs{cc} = imageFig;
+        end
+    end
+    figs = [figs imageFigs{:}];
+end
+
+
+%% Save a figure to file, watch out for things like uicontrols.
+function saveFigure(fig, fileName)
+
+% flush pending drawing commands
+drawnow();
+
+% hide uicontrols, which can't always be saved
+controls = findobj(fig, 'Type', 'uicontrol');
+set(controls, 'Visible', 'off');
+
+% make sure output location exists
+[filePath, fileName] = fileparts(fileName);
+if 7 ~= exist(filePath, 'dir')
+    mkdir(filePath);
+end
+
+% save a png and a figure
+figName = fullfile(filePath, [fileName '.fig']);
+saveas(fig, figName, 'fig');
+
+pngName = fullfile(filePath, [fileName '.png']);
+set(fig, 'PaperPositionMode', 'auto');
+saveas(fig, pngName, 'png');
+
+% restore uicontrols
+set(controls, 'Visible', 'on');
+
diff --git a/Test/Interactive/rtbCompareAllExampleScenes.m b/Test/Interactive/rtbCompareAllExampleScenes.m
deleted file mode 100644
index e0ec06ba9f59256a80f9b0c5fef489b34c1ec45c..0000000000000000000000000000000000000000
--- a/Test/Interactive/rtbCompareAllExampleScenes.m
+++ /dev/null
@@ -1,565 +0,0 @@
-function [matchInfo, unmatchedA, unmatchedB] = rtbCompareAllExampleScenes(workingFolderA, workingFolderB, varargin)
-%% Compare recipe renderings that were generated at different times.
-%
-% [matchInfo, unmatchedA, unmatchedB] = rtbCompareAllExampleScenes(workingFolderA, workingFolderB)
-% Finds 2 sets of rendering outputs: set A includes renderings located
-% under the given workingFolderA, set B includes renderings located
-% under workingFolderB.  Attempts to match up data files from both sets,
-% based on recipe names and renderer names.  Computes comparison statistics
-% and shows difference images for each matched pair.
-%
-% Data sets must use a particular folder structure, consistent with
-% rtbWorkingFolder().  For each rendering data file, the expected path is:
-%	workingFolder/recipeName/rendererName/renderings/fileName.mat
-%
-% where:
-%   - workingFolder is either workingFolderA or workingFolderB
-%	- recipeName must be the name of a recipe such as "MakeDragon"
-%	- rendererName must be the name of a renderer, like "PBRT" or "Mitsuba"
-%	- fileName must be the name of a multi-spectral data file, such as "Dragon-001"
-%
-% rtbCompareAllExampleScenes( ... 'filterExpression', filterExpression)
-% uses the given regular expression filterExpr'/home/ben/Desktop/testA'ession to select file paths.
-% Only data files whose paths match the expression will be compared.  The
-% default is to do no such filtering.
-%
-% rtbCompareAllExampleScenes( ... 'visualize', visualize) specifies the
-% level of visualization to do during comparinsons.  The options are:
-%   - 0 -- don't plot anything
-%   - 1 -- (default) plot a summary figure at the end
-%   - 2 -- plot a summary figure at the end and a detail figure for each comparison
-%
-% The summary figure will contain two plots:
-%
-% A "correlation" plot will show this correlation betweeen paired
-% multi-spectral images, with each image simply treated as a matrix of
-% numbers.
-%
-% A "relative diff" plot will show the relative differences between paired
-% pixel components, where raw diff is
-%   diff = |a-b|/a, where a/max(a) > .2
-% The diff is only calculated when the "a" value is not small, to avoid
-% unfair comparison due to large denominator.  For each pair of images, the
-% plot will show the mean and max of the diffs from all pixel components.
-%
-% rtbCompareAllExampleScenes( ... 'figureFolder', figureFolder) specifies
-% an output folder where to save figures used for visualization.  The
-% default is rtbWorkingFolder().
-%
-% This function is intended to help validate RenderToolbox installations
-% and detect bugs in the RenderToolbox code.  A potential use would
-% compare renderings produced locally with archived renderings located at
-% Amazon S3.  For example:
-%   % produce renderings locally
-%   rtbTestAllExampleScenes('my/local/renderings');
-%
-%   % download archived renderings to 'my/local/archive'
-%
-%   % summarize local vs archived renderings
-%   workingFolderA = 'my/local/renderings/data';
-%   workingFolderA = 'my/local/archive/data';
-%   visualize = 1;
-%   matchInfo = rtbCompareAllExampleScenes(workingFolderA, workingFolderB, 'visualize', visualize);
-%
-% Returns a struct array of info about each matched pair, including file
-% names and differneces between multispectral images (A minus B).
-%
-% Also returns a cell array of paths for files in set A that did not match
-% any of the files in set B.  Likewise, returns a cell array of paths for
-% files in set B that did not match any of the files in set A.
-%
-%%% RenderToolbox4 Copyright (c) 2012-2016 The RenderToolbox Team.
-%%% About Us://github.com/RenderToolbox/RenderToolbox4/wiki/About-Us
-%%% RenderToolbox4 is released under the MIT License.  See LICENSE file.
-
-parser = inputParser();
-parser.addRequired('workingFolderA', @ischar);
-parser.addRequired('workingFolderB', @ischar);
-parser.addParameter('filterExpression', '', @ischar);
-parser.addParameter('visualize', 1, @isnumeric);
-parser.addParameter('figureFolder', fullfile(rtbWorkingFolder(), 'comparisons'), @ischar);
-parser.parse(workingFolderA, workingFolderB, varargin{:});
-workingFolderA = parser.Results.workingFolderA;
-workingFolderB = parser.Results.workingFolderB;
-filterExpression = parser.Results.filterExpression;
-visualize = parser.Results.visualize;
-figureFolder = parser.Results.figureFolder;
-
-matchInfo = [];
-unmatchedA = {};
-unmatchedB = {};
-
-% find .mat files for sets A and B
-fileFilter = [filterExpression '[^\.]*\.mat$'];
-filesA = rtbFindFiles('root', workingFolderA, 'filter', fileFilter);
-filesB = rtbFindFiles('root', workingFolderB, 'filter', fileFilter);
-
-if isempty(filesA)
-    fprintf('Found no files for set A in: %s\n', workingFolderA);
-    return;
-end
-
-if isempty(filesB)
-    fprintf('Found no files for set B in: %s\n', workingFolderB);
-    return;
-end
-
-% parse out expected path parts for each file
-infoA = scanDataPaths(filesA);
-infoB = scanDataPaths(filesB);
-
-% report unmatched files
-matchTokensA = {infoA.matchToken};
-matchTokensB = {infoB.matchToken};
-[~, indexA, indexB] = intersect( ...
-    matchTokensA, matchTokensB, 'stable');
-[~, unmatchedIndex] = setdiff(matchTokensA, matchTokensB);
-unmatchedA = filesA(unmatchedIndex);
-[~, unmatchedIndex] = setdiff(matchTokensB, matchTokensA);
-unmatchedB = filesB(unmatchedIndex);
-
-if isempty(indexA) || isempty(indexB)
-    fprintf('Could not find any file matches.\n');
-    return;
-end
-
-% allocate an info struct for image comparisons
-filesA = {infoA.original};
-filesB = {infoB.original};
-matchInfo = struct( ...
-    'fileA', filesA(indexA), ...
-    'fileB', filesB(indexB), ...
-    'workingFolderA', workingFolderA, ...
-    'workingFolderB', workingFolderB, ...
-    'relativePathA', {infoA(indexA).relativePath}, ...
-    'relativePathB', {infoB(indexB).relativePath}, ...
-    'matchTokenA', matchTokensA(indexA), ...
-    'matchTokenB', matchTokensB(indexB), ...
-    'samplingA', [], ...
-    'samplingB', [], ...
-    'denominatorThreshold', 0.2, ...
-    'subpixelsA', [], ...
-    'subpixelsB', [], ...
-    'normA', [], ...
-    'normB', [], ...
-    'normDiff', [], ...
-    'absNormDiff', [], ...
-    'relNormDiff', [], ...
-    'corrcoef', nan, ...
-    'isGoodComparison', false, ...
-    'detailFigure', nan, ...
-    'error', '');
-
-% any comparisons to make?
-nMatches = numel(matchInfo);
-if nMatches > 0
-    fprintf('Found %d matched pairs of data files.\n', nMatches);
-    fprintf('Some of these might not contain images and would be skipped.\n');
-else
-    fprintf('Found no matched pairs.\n');
-    return;
-end
-fprintf('\n')
-
-nUnmatchedA = numel(unmatchedA);
-if nUnmatchedA > 0
-    fprintf('%d data files in set A had no match in set B:\n', nUnmatchedA);
-    for ii = 1:nUnmatchedA
-        fprintf('  %s\n', unmatchedA{ii});
-    end
-end
-fprintf('\n')
-
-nUnmatchedB = numel(unmatchedB);
-if nUnmatchedB > 0
-    fprintf('%d data files in set B had no match in set A:\n', nUnmatchedB);
-    for ii = 1:nUnmatchedB
-        fprintf('  %s\n', unmatchedB{ii});
-    end
-end
-fprintf('\n')
-
-% compare matched images!
-for ii = 1:nMatches
-    fprintf('%d of %d: %s\n', ii, nMatches, matchInfo(ii).matchTokenA);
-    
-    % load data
-    dataA = load(matchInfo(ii).fileA);
-    dataB = load(matchInfo(ii).fileB);
-    
-    % do we have images?
-    hasImageA = isfield(dataA, 'multispectralImage');
-    hasImageB = isfield(dataB, 'multispectralImage');
-    if hasImageA
-        if hasImageB
-            % each has an image -- proceed
-        else
-            matchInfo(ii).error = ...
-                'Data file A has a multispectralImage but B does not!';
-            disp(matchInfo(ii).error);
-            continue;
-        end
-    else
-        if hasImageB
-            matchInfo(ii).error = ...
-                'Data file B has a multispectralImage but A does not!';
-            disp(matchInfo(ii).error);
-            continue;
-        else
-            matchInfo(ii).error = ...
-                'Neither data file A nor B has a multispectralImage -- skipping.';
-            continue;
-        end
-    end
-    multispectralA = dataA.multispectralImage;
-    multispectralB = dataB.multispectralImage;
-    
-    % do image dimensions agree?
-    if ~isequal(size(multispectralA, 1), size(multispectralB, 1)) ...
-            || ~isequal(size(multispectralA, 2), size(multispectralB, 2))
-        matchInfo(ii).error = ...
-            sprintf('Image A[%s] is not the same size as image B[%s].', ...
-            num2str(size(multispectralA)), num2str(size(multispectralB)));
-        disp(matchInfo(ii).error);
-        continue;
-    end
-    
-    % do we have spectral sampling?
-    hasSamplingA = isfield(dataA, 'S');
-    hasSamplingB = isfield(dataB, 'S');
-    if hasSamplingA
-        if hasSamplingB
-            % each has a sampling "S" -- proceed
-        else
-            matchInfo(ii).error = ...
-                'Data file A has a spectral sampling "S" but B does not!';
-            disp(matchInfo(ii).error);
-            continue;
-        end
-    else
-        if hasSamplingB
-            matchInfo(ii).error = ...
-                'Data file B has a spectral sampling "S" but A does not!';
-            disp(matchInfo(ii).error);
-            continue;
-        else
-            matchInfo(ii).error = ...
-                'Neither data file A nor B has spectral sampling "S" -- skipping.';
-            continue;
-        end
-    end
-    matchInfo(ii).samplingA = dataA.S;
-    matchInfo(ii).samplingB = dataB.S;
-    
-    % do spectral samplings agree?
-    if ~isequal(dataA.S, dataB.S)
-        matchInfo(ii).error = ...
-            sprintf('Spectral sampling A[%s] is not the same as B[%s].', ...
-            num2str(dataA.S), num2str(dataB.S));
-        disp(matchInfo(ii).error);
-        % proceed with comparison, despite sampling mismatch
-    end
-    
-    % tolerate different sectral sampling depths
-    [A, B] = truncatePlanes(multispectralA, multispectralB, dataA.S, dataB.S);
-    
-    % comparison passes all sanity checks
-    matchInfo(ii).isGoodComparison = true;
-    
-    % compute per-pixel component difference stats
-    normA = A / max(A(:));
-    normB = B / max(B(:));
-    normDiff = normA - normB;
-    absNormDiff = abs(normDiff);
-    relNormDiff = absNormDiff ./ normA;
-    cutoff = matchInfo(ii).denominatorThreshold;
-    relNormDiff(normA < cutoff) = nan;
-    
-    % summarize differnece stats
-    matchInfo(ii).subpixelsA = summarizeData(A);
-    matchInfo(ii).subpixelsB = summarizeData(B);
-    matchInfo(ii).normA = summarizeData(normA);
-    matchInfo(ii).normB = summarizeData(normB);
-    matchInfo(ii).normDiff = summarizeData(normDiff);
-    matchInfo(ii).absNormDiff = summarizeData(absNormDiff);
-    matchInfo(ii).relNormDiff = summarizeData(relNormDiff);
-    
-    % compute correlation among pixel components
-    r = corrcoef(A(:), B(:));
-    matchInfo(ii).corrcoef = r(1, 2);
-    
-    % plot difference image?
-    if visualize > 1
-        f = showDifferenceImage(matchInfo(ii), A, B);
-        matchInfo(ii).detailFigure = f;
-        
-        % save detail figure to disk
-        drawnow();
-        [imagePath, imageName] = fileparts(matchInfo(ii).relativePathA);
-        imageCompPath = fullfile(figureFolder, imagePath);
-        if ~exist(imageCompPath, 'dir')
-            mkdir(imageCompPath);
-        end
-        figName = fullfile(imageCompPath, [imageName '.fig']);
-        saveas(f, figName, 'fig');
-        pngName = fullfile(imageCompPath, [imageName '.png']);
-        set(f, 'PaperPositionMode', 'auto');
-        saveas(f, pngName, 'png');
-        
-        close(f);
-    end
-end
-
-nComparisons = sum([matchInfo.isGoodComparison]);
-nSkipped = nMatches - nComparisons;
-fprintf('Compared %d pairs of data files.\n', nComparisons);
-fprintf('Skipped %d pairs of data files, which is not necessarily a problem.\n', nSkipped);
-
-% plot a grand summary?
-if visualize > 0
-    f = showDifferenceSummary(matchInfo);
-    
-    % save summary figure to disk
-    if ~exist(figureFolder, 'dir')
-        mkdir(figureFolder);
-    end
-    imageName = sprintf('%s-summary', mfilename());
-    figName = fullfile(figureFolder, [imageName '.fig']);
-    saveas(f, figName, 'fig');
-    
-    % some platforms can't pring uicontrols
-    controls = findobj(f, 'Type', 'uicontrol');
-    set(controls, 'Visible', 'off');
-    pngName = fullfile(figureFolder, [imageName '.png']);
-    saveas(f, pngName, 'png');
-    set(controls, 'Visible', 'on');
-end
-
-if visualize > 1
-    fprintf('\nSee comparison images saved in:\n  %s\n', figureFolder);
-end
-
-
-% Scan paths for expected parts:
-%   root/recipeName/subfolderName/rendererName/fileName.extension
-function info = scanDataPaths(paths)
-n = numel(paths);
-rootPath = cell(1, n);
-relativePath = cell(1, n);
-recipeName = cell(1, n);
-subfolderName = cell(1, n);
-rendererName = cell(1, n);
-fileName = cell(1, n);
-fileNumber = cell(1, n);
-hasNumber = false(1, n);
-matchToken = cell(1, n);
-for ii = 1:n
-    % break off the file name
-    [parentPath, baseName, extension] = fileparts(paths{ii});
-    fileName{ii} = [baseName extension];
-    if numel(baseName) >= 4
-        fileNumber{ii} = sscanf(baseName(end-3:end), '-%d');
-    end
-    hasNumber(ii) = ~isempty(fileNumber{ii});
-    
-    % break out subfolder names
-    scanResult = textscan(parentPath, '%s', 'Delimiter', filesep());
-    tokens = scanResult{1};
-    
-    % is there a renderer folder?
-    if any(strcmp(tokens{end}, {'PBRT', 'Mitsuba'}))
-        rendererName{ii} = tokens{end};
-        subfolderNameIndex = numel(tokens) - 1;
-    else
-        rendererName{ii} = '';
-        subfolderNameIndex = numel(tokens);
-    end
-    
-    % get the named subfolder name
-    subfolderName{ii} = tokens{subfolderNameIndex};
-    
-    % get the recipe name
-    recipeName{ii} = tokens{subfolderNameIndex-1};
-    
-    % get the root path
-    rootPath{ii} = fullfile(tokens{1:subfolderNameIndex-2});
-    
-    % build the rootless relative path
-    relativePath{ii} = fullfile(recipeName{ii}, subfolderName{ii}, ...
-        rendererName{ii}, fileName{ii});
-    
-    % build a token for matching across file sets
-    if strncmp(recipeName{ii}, 'rtb', 3)
-        nameBase = recipeName{ii}(4:end);
-    else
-        nameBase = recipeName{ii};
-    end
-    matchTokenBase = [nameBase '-' subfolderName{ii} '-' rendererName{ii} '-'];
-    if hasNumber(ii)
-        matchToken{ii} = [matchTokenBase sprintf('%03d', fileNumber{ii})];
-    else
-        matchToken{ii} = [matchTokenBase baseName];
-    end
-end
-
-info = struct( ...
-    'original', paths, ...
-    'fileName', fileName, ...
-    'fileNumber', fileNumber, ...
-    'hasNumber', hasNumber, ...
-    'rendererName', rendererName, ...
-    'recipeName', recipeName, ...
-    'subfolderName', subfolderName, ...
-    'rootPath', rootPath, ...
-    'relativePath', relativePath, ...
-    'matchToken', matchToken);
-
-
-% Show sRGB images and sRGB difference images
-function f = showDifferenceImage(info, A, B)
-
-% make SRGB images
-[A, B, S] = truncatePlanes(A, B, info.samplingA, info.samplingB);
-isScale = true;
-toneMapFactor = 0;
-imageA = rtbMultispectralToSRGB(A, S, 'toneMapFactor', toneMapFactor, 'isScale', isScale);
-imageB = rtbMultispectralToSRGB(B, S, 'toneMapFactor', toneMapFactor, 'isScale', isScale);
-imageAB = rtbMultispectralToSRGB(A-B, S, 'toneMapFactor', toneMapFactor, 'isScale', isScale);
-imageBA = rtbMultispectralToSRGB(B-A, S, 'toneMapFactor', toneMapFactor, 'isScale', isScale);
-
-% show images in a new figure
-name = sprintf('sRGB scaled: %s', info.matchTokenA);
-f = figure('Name', name, 'NumberTitle', 'off');
-
-ax = subplot(2, 2, 2, 'Parent', f);
-imshow(uint8(imageA), 'Parent', ax);
-title(ax, ['A: ' info.workingFolderA]);
-
-ax = subplot(2, 2, 3, 'Parent', f);
-imshow(uint8(imageB), 'Parent', ax);
-title(ax, ['B: ' info.workingFolderB]);
-
-ax = subplot(2, 2, 1, 'Parent', f);
-imshow(uint8(imageAB), 'Parent', ax);
-title(ax, 'Difference: A - B');
-
-ax = subplot(2, 2, 4, 'Parent', f);
-imshow(uint8(imageBA), 'Parent', ax);
-title(ax, 'Difference: B - A');
-
-
-% Truncate spectral planes if one image has more planes.
-function [truncA, truncB, truncSampling] = truncatePlanes(A, B, samplingA, samplingB)
-nPlanes = min(samplingA(3), samplingB(3));
-truncSampling = [samplingA(1:2) nPlanes];
-truncA = A(:,:,1:nPlanes);
-truncB = B(:,:,1:nPlanes);
-
-
-% Show a summary of all difference images.
-function f = showDifferenceSummary(info)
-figureName = sprintf('A: %s vs B: %s', ...
-    info(1).workingFolderA, info(1).workingFolderB);
-f = figure('Name', figureName, 'NumberTitle', 'off');
-
-% summarize only fair comparisions
-goodInfo = info([info.isGoodComparison]);
-
-% sort the summary by size of error
-diffSummary = [goodInfo.relNormDiff];
-errorStat = [diffSummary.max];
-[~, order] = sort(errorStat);
-goodInfo = goodInfo(order);
-
-% summarize data correlation coefficients
-minCorr = 0.85;
-peggedCorr = 0.8;
-corrTicks = [peggedCorr minCorr:0.05:1];
-corrTickLabels = num2cell(corrTicks);
-corrTickLabels{1} = sprintf('<%.2f', minCorr);
-corr = [goodInfo.corrcoef];
-corr(corr < minCorr) = peggedCorr;
-
-names = {goodInfo.matchTokenA};
-nLines = numel(names);
-ax(1) = subplot(1, 3, 2, ...
-    'Parent', f, ...
-    'YTick', 1:nLines, ...
-    'YTickLabel', names, ...
-    'YGrid', 'on', ...
-    'XLim', [corrTicks(1), corrTicks(end)], ...
-    'XTick', corrTicks, ...
-    'XTickLabel', corrTickLabels);
-line(corr, 1:nLines, ...
-    'Parent', ax(1), ...
-    'LineStyle', 'none', ...
-    'Marker', 'o', ...
-    'Color', [0 0 1])
-title(ax(1), 'correlation');
-
-% summarize mean and max subpixel differences
-maxDiff = 2.5;
-peggedDiff = 3;
-diffTicks = [0:0.5:maxDiff peggedDiff];
-diffTickLabels = num2cell(diffTicks);
-diffTickLabels{end} = sprintf('>%.2f', maxDiff);
-
-diffSummary = [goodInfo.relNormDiff];
-maxes = [diffSummary.max];
-means = [diffSummary.mean];
-maxes(maxes > maxDiff) = peggedDiff;
-means(means > maxDiff) = peggedDiff;
-ax(2) = subplot(1, 3, 3, ...
-    'Parent', f, ...
-    'YTick', 1:nLines, ...
-    'YTickLabel', 1:nLines, ...
-    'YAxisLocation', 'right', ...
-    'YGrid', 'on', ...
-    'XLim', [diffTicks(1), diffTicks(end)], ...
-    'XTick', diffTicks, ...
-    'XTickLabel', diffTickLabels);
-line(maxes, 1:nLines, ...
-    'Parent', ax(2), ...
-    'LineStyle', 'none', ...
-    'Marker', '+', ...
-    'Color', [1 0 0])
-line(means, 1:nLines, ...
-    'Parent', ax(2), ...
-    'LineStyle', 'none', ...
-    'Marker', 'o', ...
-    'Color', [0 0 0])
-legend(ax(2), 'max', 'mean', 'Location', 'northeast');
-title(ax(2), 'relative diff');
-
-% let the user scroll both axes at the same time
-nLinesAtATime = 25;
-scrollerData.axes = ax;
-scrollerData.nLinesAtATime = nLinesAtATime;
-scroller = uicontrol( ...
-    'Parent', f, ...
-    'Units', 'normalized', ...
-    'Position', [.95 0 .05 1], ...
-    'Callback', @scrollSummaryAxes, ...
-    'Min', 1, ...
-    'Max', max(2, nLines), ...
-    'Value', nLines, ...
-    'Style', 'slider', ...
-    'SliderStep', [1 2], ...
-    'UserData', scrollerData);
-scrollSummaryAxes(scroller, []);
-
-
-% Summarize a distribuition of data with a struct of stats.
-function summary = summarizeData(data)
-finiteData = data(isfinite(data));
-summary.min = min(finiteData);
-summary.mean = mean(finiteData);
-summary.max = max(finiteData);
-
-
-% Scroll summary axes together.
-function scrollSummaryAxes(object, event)
-scrollerData = get(object, 'UserData');
-topLine = get(object, 'Value');
-yLimit = topLine + [-scrollerData.nLinesAtATime 1];
-set(scrollerData.axes, 'YLim', yLimit);
diff --git a/Test/Interactive/rtbTestAllExampleScenes.m b/Test/Interactive/rtbRunEpicTest.m
similarity index 93%
rename from Test/Interactive/rtbTestAllExampleScenes.m
rename to Test/Interactive/rtbRunEpicTest.m
index a8329911f629f2589a9d48213a234e1daa6c3194..68ba5ea8997032c0ed9af5247eb92f5e11dadcc3 100644
--- a/Test/Interactive/rtbTestAllExampleScenes.m
+++ b/Test/Interactive/rtbRunEpicTest.m
@@ -1,7 +1,7 @@
-function results = rtbTestAllExampleScenes(varargin)
+function results = rtbRunEpicTest(varargin)
 %% Run all "rtbMake..." scripts in the ExampleScenes/ folder.
 %
-% results = rtbTestAllExampleScenes() renders example scenes by invoking
+% results = rtbRunEpicTest() renders example scenes by invoking
 % all of the "rtbMake..." executive sripts found within the ExampleScenes/
 % folder
 %
@@ -19,11 +19,11 @@ function results = rtbTestAllExampleScenes(varargin)
 % have a name that that includes the name of this m-file, plus the date and
 % time.
 %
-% rtbTestAllExampleScenes( ... 'outputRoot', outputRoot) specifies the
+% rtbRunEpicTest( ... 'outputRoot', outputRoot) specifies the
 % working folder where to put rendering outputs.  The default is from
 % rtbDefaultHints().
 %
-% rtbTestAllExampleScenes( ... 'makeFunctions', makeFunctions) specifies a
+% rtbRunEpicTest( ... 'makeFunctions', makeFunctions) specifies a
 % cell array of executive functions to run.  The default is to search the
 % ExampleScenes/ folder for m-files that begin with "rtbMake".
 %
diff --git a/Test/Interactive/rtbTestInstallation.m b/Test/Interactive/rtbTestInstallation.m
index 8243d8cb4ce61a41215a4b612f15c8a82ae795bf..ada27c2e7035d2605d8dd2a6ea5cb2ae05658f4a 100644
--- a/Test/Interactive/rtbTestInstallation.m
+++ b/Test/Interactive/rtbTestInstallation.m
@@ -77,7 +77,7 @@ end
 if doAll
     fprintf('\nTesting rendering with all example scripts.\n');
     fprintf('This might take a while.\n');
-    renderResults = rtbTestAllExampleScenes([], []);
+    renderResults = rtbRunEpicTest([], []);
     
 else
     testScenes = { ...
@@ -88,7 +88,7 @@ else
     
     fprintf('\nTesting rendering with %d example scripts.\n', numel(testScenes));
     fprintf('You should see several figures with rendered images.\n\n');
-    renderResults = rtbTestAllExampleScenes('makeFunctions', testScenes);
+    renderResults = rtbRunEpicTest('makeFunctions', testScenes);
     
 end
 
@@ -103,7 +103,7 @@ if ~isempty(referenceRoot)
     fprintf('\nComparing local renderings\n  %s\n', localRoot);
     fprintf('with reference renderings\n  %s\n', referenceRoot);
     fprintf('You should see several more figures.\n\n');
-    comparison = rtbCompareAllExampleScenes(localRoot, referenceRoot, '', 2);
+    comparison = rtbRunEpicComparison(localRoot, referenceRoot, '', 2);
 else
     fprintf('\nNo referenceRoot provided.  Local renderings\n');
     fprintf('will not be compared with reference renderings.\n');
diff --git a/Test/Interactive/rtbCheckNativeDependencies.m b/rtbCheckNativeDependencies.m
similarity index 100%
rename from Test/Interactive/rtbCheckNativeDependencies.m
rename to rtbCheckNativeDependencies.m