Package se.llbit.chunky.renderer.ui

Source Code of se.llbit.chunky.renderer.ui.RenderControls

/* Copyright (c) 2012-2014 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.ui;

import java.awt.Color;
import java.awt.Component;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.FileDialog;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
import javax.swing.DefaultComboBoxModel;
import javax.swing.DefaultListCellRenderer;
import javax.swing.GroupLayout;
import javax.swing.GroupLayout.Alignment;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JRadioButton;
import javax.swing.JSeparator;
import javax.swing.JSlider;
import javax.swing.JTabbedPane;
import javax.swing.JTextField;
import javax.swing.LayoutStyle.ComponentPlacement;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.AbstractDocument;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;

import org.apache.commons.math3.util.FastMath;
import org.apache.log4j.Logger;

import se.llbit.chunky.PersistentSettings;
import se.llbit.chunky.main.Chunky;
import se.llbit.chunky.renderer.Postprocess;
import se.llbit.chunky.renderer.RenderConstants;
import se.llbit.chunky.renderer.RenderContext;
import se.llbit.chunky.renderer.RenderManager;
import se.llbit.chunky.renderer.RenderStatusListener;
import se.llbit.chunky.renderer.scene.Camera;
import se.llbit.chunky.renderer.scene.CameraPreset;
import se.llbit.chunky.renderer.scene.ProjectionMode;
import se.llbit.chunky.renderer.scene.Scene;
import se.llbit.chunky.renderer.scene.SceneManager;
import se.llbit.chunky.renderer.scene.Sky;
import se.llbit.chunky.renderer.scene.Sky.SkyMode;
import se.llbit.chunky.renderer.scene.Sun;
import se.llbit.chunky.resources.Texture;
import se.llbit.chunky.ui.CenteredFileDialog;
import se.llbit.chunky.world.ChunkPosition;
import se.llbit.chunky.world.Icon;
import se.llbit.chunky.world.World;
import se.llbit.json.JsonMember;
import se.llbit.json.JsonObject;
import se.llbit.math.QuickMath;
import se.llbit.math.Vector3d;
import se.llbit.math.Vector4d;
import se.llbit.ui.Adjuster;

/**
* Render Controls dialog.
* @author Jesper Öqvist <jesper@llbit.se>
*/
@SuppressWarnings("serial")
public class RenderControls extends JDialog implements ViewListener,
  RenderStatusListener {

  private static final Logger logger =
      Logger.getLogger(RenderControls.class);

  private static final int[] dumpFrequencies = { 50, 100, 500, 1000, 5000 };

  private final RenderManager renderMan;
  private final SceneManager sceneMan;
  private final Chunk3DView view;
  private final Chunky chunky;

  /**
   * Number format for current locale.
   */
  private final NumberFormat numberFormat =
      NumberFormat.getInstance();

  private final JSlider skymapRotationSlider = new JSlider();
  private final JSlider lightProbeRotationSlider = new JSlider();
  private final JSlider skyboxRotationSlider = new JSlider();
  private final JButton loadSkymapBtn = new JButton();
  private final JButton loadLightProbeBtn = new JButton();
  private final JPanel simulatedSkyPanel = new JPanel();
  private final JPanel skymapPanel = new JPanel();
  private final JPanel lightProbePanel = new JPanel();
  private final JPanel skyGradientPanel = new JPanel();
  private final JPanel skyboxPanel = new JPanel();
  private final JComboBox canvasSizeCB = new JComboBox();
  private final JComboBox cameraPreset = new JComboBox();
  private final JComboBox customPreset = new JComboBox();
  private final JComboBox projectionMode = new JComboBox();
  private final JButton startRenderBtn = new JButton();
  private final JCheckBox enableEmitters = new JCheckBox();
  private final JCheckBox directLight = new JCheckBox();
  private final JButton saveSceneBtn = new JButton();
  private final JButton loadSceneBtn = new JButton();
  private final JButton openSceneDirBtn = new JButton();
  private final JButton saveFrameBtn = new JButton();
  private final JCheckBox stillWaterCB = new JCheckBox();
  private final JTextField sceneNameField = new JTextField();
  private final JLabel sceneNameLbl = new JLabel();
  private final JCheckBox biomeColorsCB = new JCheckBox();
  private final JButton stopRenderBtn = new JButton();
  private final JCheckBox clearWaterCB = new JCheckBox();
  private final JCheckBox atmosphereEnabled = new JCheckBox();
  private final JCheckBox volumetricFogEnabled = new JCheckBox();
  private final JCheckBox cloudsEnabled = new JCheckBox();
  private final RenderContext context;
  private final JButton showPreviewBtn = new JButton();
  private final JLabel renderTimeLbl = new JLabel();
  private final JLabel sppLbl = new JLabel();
  private final JProgressBar progressBar = new JProgressBar();
  private final JLabel progressLbl = new JLabel();
  private final JComboBox postprocessCB = new JComboBox();
  private final JComboBox skyModeCB = new JComboBox();
  private final JButton changeSunColorBtn = new JButton();
  private final JLabel etaLbl = new JLabel();
  private final JCheckBox waterWorldCB = new JCheckBox();
  private final JTextField waterHeightField = new JTextField();
  private final DecimalFormat decimalFormat = new DecimalFormat();
  private final JCheckBox saveDumpsCB = new JCheckBox();
  private final JComboBox dumpFrequencyCB = new JComboBox();
  private final JCheckBox saveSnapshotsCB = new JCheckBox("Save snapshot for each dump");
  private final JLabel dumpFrequencyLbl = new JLabel(" frames");
  private final JTextField sppTargetField = new JTextField();
  private final JTextField cameraX = new JTextField();
  private final JTextField cameraY = new JTextField();
  private final JTextField cameraZ = new JTextField();
  private final JTextField cameraYaw = new JTextField();
  private final JTextField cameraPitch = new JTextField();
  private final JTextField cameraRoll = new JTextField();
  private final JButton mergeDumpBtn = new JButton("Merge Render Dump");
  private final JCheckBox shutdownWhenDoneCB = new JCheckBox("Shutdown computer when render completes");
  private final JRadioButton v90Btn = new JRadioButton("90");
  private final JRadioButton v180Btn = new JRadioButton("180");

  private final JTabbedPane tabbedPane = new JTabbedPane();

  private final Adjuster skyHorizonOffset = new Adjuster(
      "Horizon offset",
      "Moves the horizon below the actual horizon",
      0.0, 1.0) {
    @Override
    public void valueChanged(double newValue) {
      renderMan.scene().sky().setHorizonOffset(newValue);
    }

    @Override
    public void update() {
      set(renderMan.scene().sky().getHorizonOffset());
    }
  };

  private final Adjuster numThreads = new Adjuster(
      "Render threads",
      "Number of rendering threads",
      RenderConstants.NUM_RENDER_THREADS_MIN,
      20) {
    {
      setClampMax(false);
    }
    @Override
    public void valueChanged(double newValue) {
      int value = (int) newValue;
      PersistentSettings.setNumThreads(value);
      renderMan.setNumThreads(value);
    }

    @Override
    public void update() {
      set(PersistentSettings.getNumThreads());
    }
  };

  private final Adjuster cpuLoad = new Adjuster(
      "CPU load",
      "CPU load percentage",
      1, 100) {
    @Override
    public void valueChanged(double newValue) {
      int value = (int) newValue;
      PersistentSettings.setCPULoad(value);
      renderMan.setCPULoad(value);
    }

    @Override
    public void update() {
      set(PersistentSettings.getCPULoad());
    }
  };

  private final Adjuster rayDepth = new Adjuster(
      "Ray depth",
      "Sets the recursive ray depth",
      1, 25) {
    @Override
    public void valueChanged(double newValue) {
      renderMan.scene().setRayDepth((int) newValue);
    }

    @Override
    public void update() {
      set(renderMan.scene().getRayDepth());
    }
  };
  private final Adjuster emitterIntensity = new Adjuster(
      "Emitter intensity",
      "Light intensity modifier for emitters",
      Scene.MIN_EMITTER_INTENSITY,
      Scene.MAX_EMITTER_INTENSITY) {
    {
      setLogarithmicMode();
    }
    @Override
    public void valueChanged(double newValue) {
      renderMan.scene().setEmitterIntensity(newValue);
    }

    @Override
    public void update() {
      set(renderMan.scene().getEmitterIntensity());
    }
  };
  private final Adjuster skyLight = new Adjuster(
      "Sky Light",
      "Sky light intensity modifier",
      Sky.MIN_INTENSITY,
      Sky.MAX_INTENSITY) {
    {
      setLogarithmicMode();
    }
    @Override
    public void valueChanged(double newValue) {
      renderMan.scene().sky().setSkyLight(newValue);
    }

    @Override
    public void update() {
      set(renderMan.scene().sky().getSkyLight());
    }
  };
  private final Adjuster sunIntensity = new Adjuster(
      "Sun Intensity",
      "Sunlight intensity modifier",
      Sun.MIN_INTENSITY,
      Sun.MAX_INTENSITY) {
    {
      setLogarithmicMode();
    }
    @Override
    public void valueChanged(double newValue) {
      renderMan.scene().sun().setIntensity(newValue);
    }

    @Override
    public void update() {
      set(renderMan.scene().sun().getIntensity());
    }
  };
  private final Adjuster sunAzimuth = new Adjuster(
      "Sun azimuth",
      "The angle towards the sun from north",
      0.0, 360.0) {
    @Override
    public void valueChanged(double newValue) {
      renderMan.scene().sun().setAzimuth(QuickMath.degToRad(newValue));
    }

    @Override
    public void update() {
      set(QuickMath.radToDeg(renderMan.scene().sun().getAzimuth()));
    }
  };
  private final Adjuster sunAltitude = new Adjuster(
      "Sun altitude",
      "Angle of the sun above the horizon",
      0.0, 90.0) {
    @Override
    public void valueChanged(double newValue) {
      renderMan.scene().sun().setAltitude(QuickMath.degToRad(newValue));
    }

    @Override
    public void update() {
      set(QuickMath.radToDeg(renderMan.scene().sun().getAltitude()));
    }
  };
  private final Adjuster fov = new Adjuster(
      "Field of View (zoom)",
      "Field of View",
      1.0,
      180.0) {
    {
      setClampMax(false);
    }
    @Override
    public void valueChanged(double newValue) {
      renderMan.scene().camera().setFoV(newValue);
    }

    @Override
    public void update() {
      Camera camera = renderMan.scene().camera();
      set(camera.getFoV(), camera.getMinFoV(), camera.getMaxFoV());
    }
  };
  private Adjuster dof;
  private final Adjuster subjectDistance = new Adjuster(
      "Subject Distance",
      "Distance to focal plane",
      Camera.MIN_SUBJECT_DISTANCE,
      Camera.MAX_SUBJECT_DISTANCE) {
    {
      setLogarithmicMode();
    }
    @Override
    public void valueChanged(double newValue) {
      renderMan.scene().camera().setSubjectDistance(newValue);
    }

    @Override
    public void update() {
      set(renderMan.scene().camera().getSubjectDistance());
    }
  };
  private final Adjuster exposure = new Adjuster(
      "exposure",
      "exposure",
      Scene.MIN_EXPOSURE,
      Scene.MAX_EXPOSURE) {
    {
      setLogarithmicMode();
    }
    @Override
    public void valueChanged(double newValue) {
      renderMan.scene().setExposure(newValue);
    }

    @Override
    public void update() {
      set(renderMan.scene().getExposure());
    }
  };
  private final Adjuster cloudSize = new Adjuster(
      "Cloud Size",
      "Cloud Size",
      1.0, 128.0) {
    {
      setLogarithmicMode();
    }
    @Override
    public void valueChanged(double newValue) {
      renderMan.scene().sky().setCloudSize(newValue);
    }

    @Override
    public void update() {
      set(renderMan.scene().sky().cloudSize());
    }
  };
  private final Adjuster cloudXOffset = new Adjuster(
      "Cloud X",
      "Cloud X Offset",
      1.0, 100.0) {
    @Override
    public void valueChanged(double newValue) {
      renderMan.scene().sky().setCloudXOffset(newValue);
    }

    @Override
    public void update() {
      set(renderMan.scene().sky().cloudXOffset());
    }
  };
  private final Adjuster cloudYOffset = new Adjuster(
      "Cloud Y",
      "Height of the cloud layer",
      -128.0, 512.0) {
    @Override
    public void valueChanged(double newValue) {
      renderMan.scene().sky().setCloudYOffset(newValue);
    }

    @Override
    public void update() {
      set(renderMan.scene().sky().cloudYOffset());
    }
  };
  private final Adjuster cloudZOffset = new Adjuster(
      "Cloud Z",
      "Cloud Z Offset",
      1.0, 100.0) {
    @Override
    public void valueChanged(double newValue) {
      renderMan.scene().sky().setCloudZOffset(newValue);
    }

    @Override
    public void update() {
      set(renderMan.scene().sky().cloudZOffset());
    }
  };

  private GradientEditor gradientEditor;

  /**
   * Create a new Render Controls dialog.
   * @param chunkyInstance
   * @param renderContext
   */
  public RenderControls(Chunky chunkyInstance, RenderContext renderContext) {

    super(chunkyInstance.getFrame());

    decimalFormat.setGroupingSize(3);
    decimalFormat.setGroupingUsed(true);

    context = renderContext;
    chunky = chunkyInstance;

    view = new Chunk3DView(this, chunkyInstance.getFrame());

    renderMan = new RenderManager(
        view.getCanvas(), renderContext, this);

    buildUI();

    renderMan.start();

    view.setRenderer(renderMan);

    sceneMan = new SceneManager(renderMan);
    sceneMan.start();
  }

  private void buildUI() {
    setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
    setModalityType(ModalityType.MODELESS);

    if (!ShutdownAlert.canShutdown()) {
      // disable the computer shutdown checkbox if we can't shutdown
      shutdownWhenDoneCB.setEnabled(false);
    }

    addWindowListener(new WindowListener() {
      @Override
      public void windowOpened(WindowEvent e) {
      }
      @Override
      public void windowIconified(WindowEvent e) {
      }
      @Override
      public void windowDeiconified(WindowEvent e) {
      }
      @Override
      public void windowDeactivated(WindowEvent e) {
      }
      @Override
      public void windowClosing(WindowEvent e) {
        sceneMan.interrupt();
        RenderControls.this.dispose();
      }
      @Override
      public void windowClosed(WindowEvent e) {
        // halt rendering
        renderMan.interrupt();

        // dispose of the 3D view
        view.setVisible(false);
        view.dispose();
      }
      @Override
      public void windowActivated(WindowEvent e) {
      }
    });

    updateTitle();

    addTab("General", Icon.wrench, buildGeneralPane());
    addTab("Lighting", Icon.colors, buildLightingPane());
    addTab("Sky", Icon.sky, buildSkyPane());
    addTab("Camera", Icon.camera, buildCameraPane());
    addTab("Post-processing", null, buildPostProcessingPane());
    addTab("Advanced", null, buildAdvancedPane());
    addTab("Help", Texture.unknown, buildHelpPane());

    JLabel sppTargetLbl = new JLabel("SPP Target: ");
    sppTargetLbl.setToolTipText("The render will be paused at this SPP count");

    JButton setDefaultBtn = new JButton("Set Default");
    setDefaultBtn.setToolTipText("Make the current SPP target the default");
    setDefaultBtn.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        PersistentSettings.setSppTargetDefault(renderMan.scene().getTargetSPP());
      }
    });

    sppTargetField.setColumns(10);
    sppTargetField.getDocument().addDocumentListener(sppTargetListener);

    updateSPPTargetField();

    JLabel renderLbl = new JLabel("Render: ");

    setViewVisible(false);
    showPreviewBtn.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        if (view.isViewVisible()) {
          view.hideView();
        } else {
          showPreviewWindow();
        }
      }
    });

    startRenderBtn.setText("START");
    startRenderBtn.setIcon(Icon.play.imageIcon());
    startRenderBtn.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        if (!renderMan.scene().pathTrace()) {
          renderMan.scene().startRender();
        } else {
          if (renderMan.scene().isPaused()) {
            renderMan.scene().resumeRender();
          } else {
            renderMan.scene().pauseRender();
          }
        }
        stopRenderBtn.setEnabled(true);
      }
    });

    stopRenderBtn.setText("RESET");
    stopRenderBtn.setIcon(Icon.stop.imageIcon());
    stopRenderBtn.setToolTipText("<html>Warning: this will discard the " +
        "current rendered image!<br>Make sure to save your image " +
        "before stopping the renderer!");
    stopRenderBtn.setEnabled(false);
    stopRenderBtn.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        renderMan.scene().haltRender();
      }
    });

    saveFrameBtn.setText("Save Current Frame");
    saveFrameBtn.addActionListener(saveFrameListener);

    sppLbl.setToolTipText("SPP = Samples Per Pixel, SPS = Samples Per Second");

    setRenderTime(0);
    setSamplesPerSecond(0);
    setSPP(0);
    setProgress("Progress:", 0, 0, 1);

    progressLbl.setText("Progress:");

    etaLbl.setText("ETA:");

    sceneNameLbl.setText("Scene name: ");
    sceneNameField.setColumns(15);
    AbstractDocument document = (AbstractDocument) sceneNameField.getDocument();
    document.setDocumentFilter(new SceneNameFilter());
    document.addDocumentListener(sceneNameListener);
    sceneNameField.addActionListener(sceneNameActionListener);
    updateSceneNameField();

    saveSceneBtn.setText("Save");
    saveSceneBtn.setIcon(Icon.disk.imageIcon());
    saveSceneBtn.addActionListener(saveSceneListener);

    JPanel panel = new JPanel();
    GroupLayout layout = new GroupLayout(panel);
    panel.setLayout(layout);
    layout.setHorizontalGroup(layout.createSequentialGroup()
      .addContainerGap()
      .addGroup(layout.createParallelGroup()
        .addGroup(layout.createSequentialGroup()
          .addComponent(sceneNameLbl)
          .addComponent(sceneNameField)
          .addPreferredGap(ComponentPlacement.RELATED)
          .addComponent(saveSceneBtn)
        )
        .addComponent(tabbedPane)
        .addGroup(layout.createSequentialGroup()
          .addComponent(sppTargetLbl)
          .addPreferredGap(ComponentPlacement.RELATED)
          .addComponent(sppTargetField, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE)
          .addPreferredGap(ComponentPlacement.RELATED)
          .addComponent(setDefaultBtn)
        )
        .addGroup(layout.createSequentialGroup()
          .addComponent(renderLbl)
          .addPreferredGap(ComponentPlacement.UNRELATED)
          .addComponent(startRenderBtn)
          .addPreferredGap(ComponentPlacement.UNRELATED)
          .addComponent(stopRenderBtn)
        )
        .addGroup(layout.createSequentialGroup()
          .addComponent(saveFrameBtn)
          .addPreferredGap(ComponentPlacement.UNRELATED, GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE)
          .addComponent(showPreviewBtn)
        )
        .addGroup(layout.createSequentialGroup()
          .addComponent(renderTimeLbl)
          .addPreferredGap(ComponentPlacement.UNRELATED, GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE)
          .addComponent(sppLbl)
        )
        .addGroup(layout.createSequentialGroup()
          .addComponent(progressLbl)
          .addPreferredGap(ComponentPlacement.UNRELATED, GroupLayout.PREFERRED_SIZE, Short.MAX_VALUE)
          .addComponent(etaLbl)
        )
        .addComponent(progressBar))
      .addContainerGap()
    );
    layout.setVerticalGroup(layout.createSequentialGroup()
      .addContainerGap()
      .addGroup(layout.createParallelGroup(Alignment.BASELINE)
        .addComponent(sceneNameLbl)
        .addComponent(sceneNameField)
        .addComponent(saveSceneBtn))
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addComponent(tabbedPane)
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addGroup(layout.createParallelGroup(Alignment.BASELINE)
        .addComponent(sppTargetLbl)
        .addComponent(sppTargetField)
        .addComponent(setDefaultBtn)
      )
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addGroup(layout.createParallelGroup(Alignment.BASELINE)
        .addComponent(renderLbl)
        .addComponent(startRenderBtn)
        .addComponent(stopRenderBtn)
      )
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addGroup(layout.createParallelGroup()
        .addComponent(saveFrameBtn)
        .addComponent(showPreviewBtn)
      )
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addGroup(layout.createParallelGroup()
        .addComponent(renderTimeLbl)
        .addComponent(sppLbl)
      )
      .addPreferredGap(ComponentPlacement.RELATED)
      .addGroup(layout.createParallelGroup()
        .addComponent(progressLbl)
        .addComponent(etaLbl)
      )
      .addComponent(progressBar)
      .addContainerGap()
    );
    setContentPane(panel);

    pack();

    setLocationRelativeTo(chunky.getFrame());

    setVisible(true);
  }

  /**
   * Add a tab and ensure that the icon is to the left of the text in the
   * tab label.
   *
   * @param title
   * @param icon
   * @param component
   */
  private void addTab(String title, Texture icon, Component component) {
    int index = tabbedPane.getTabCount();

    tabbedPane.add(title, component);

    if (icon != null) {
      JLabel lbl = new JLabel(title, icon.imageIcon(), SwingConstants.RIGHT);
      lbl.setIconTextGap(5);
      tabbedPane.setTabComponentAt(index, lbl);
    }
  }

  private JPanel buildAdvancedPane() {
    rayDepth.update();

    JSeparator sep1 = new JSeparator();
    JSeparator sep2 = new JSeparator();
    JSeparator sep3 = new JSeparator();

    numThreads.update();

    cpuLoad.update();

    JLabel waterWorldLbl = new JLabel(
        "Note: All chunks will be reloaded after changing the water world options!");
    JLabel waterHeightLbl = new JLabel("Water height: ");
    waterHeightField.setColumns(5);
    waterHeightField.setText("" + World.SEA_LEVEL);
    waterHeightField.setEnabled(renderMan.scene().getWaterHeight() != 0);
    waterHeightField.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        JTextField source = (JTextField) e.getSource();
        renderMan.scene().setWaterHeight(Integer.parseInt(source.getText()));
        sceneMan.reloadChunks();
        updateWaterHeight();
      }
    });

    waterWorldCB.setText("Water World Mode");
    waterWorldCB.setSelected(false);
    waterWorldCB.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        JCheckBox source = (JCheckBox) e.getSource();
        if (source.isSelected()) {
          renderMan.scene().setWaterHeight(
              Integer.parseInt(waterHeightField.getText()));
        } else {
          renderMan.scene().setWaterHeight(0);
        }
        sceneMan.reloadChunks();
        updateWaterHeight();
      }
    });

    mergeDumpBtn.setToolTipText(
        "Merge an existing render dump with the current render");
    mergeDumpBtn.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        CenteredFileDialog fileDialog =
            new CenteredFileDialog(null,
                "Select Render Dump", FileDialog.LOAD);
        fileDialog.setFilenameFilter(
            new FilenameFilter() {
              @Override
              public boolean accept(File dir, String name) {
                return name.toLowerCase().endsWith(".dump");
              }
            });
        fileDialog.setDirectory(PersistentSettings.
            getSceneDirectory().getAbsolutePath());
        fileDialog.setVisible(true);
        File selectedFile = fileDialog.getSelectedFile();
        if (selectedFile != null) {
          sceneMan.mergeRenderDump(selectedFile);
        }
      }
    });

    JPanel panel = new JPanel();
    GroupLayout layout = new GroupLayout(panel);
    panel.setLayout(layout);
    layout.setHorizontalGroup(layout.createSequentialGroup()
      .addContainerGap()
      .addGroup(layout.createParallelGroup()
        .addGroup(numThreads.horizontalGroup(layout))
        .addGroup(cpuLoad.horizontalGroup(layout))
        .addComponent(sep1)
        .addGroup(rayDepth.horizontalGroup(layout))
        .addComponent(sep2)
        .addComponent(waterWorldLbl)
        .addComponent(waterWorldCB)
        .addGroup(layout.createSequentialGroup()
          .addComponent(waterHeightLbl)
          .addGap(0, 0, Short.MAX_VALUE)
          .addComponent(waterHeightField, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE)
        )
        .addComponent(sep3)
        .addComponent(mergeDumpBtn)
        .addComponent(shutdownWhenDoneCB)
      )
      .addContainerGap()
    );
    layout.setVerticalGroup(layout.createSequentialGroup()
      .addContainerGap()
      .addGroup(numThreads.verticalGroup(layout))
      .addPreferredGap(ComponentPlacement.RELATED)
      .addGroup(cpuLoad.verticalGroup(layout))
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addComponent(sep1, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addGroup(rayDepth.verticalGroup(layout))
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addComponent(sep2, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addComponent(waterWorldLbl)
      .addPreferredGap(ComponentPlacement.RELATED)
      .addComponent(waterWorldCB)
      .addPreferredGap(ComponentPlacement.RELATED)
      .addGroup(layout.createParallelGroup()
        .addComponent(waterHeightLbl)
        .addComponent(waterHeightField, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE)
      )
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addComponent(sep3, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addComponent(mergeDumpBtn)
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addComponent(shutdownWhenDoneCB)
      .addContainerGap()
    );
    return panel;
  }

  private JPanel buildPostProcessingPane() {
    exposure.update();

    JLabel postprocessDescLbl = new JLabel("<html>Post processing affects rendering performance<br>when the preview window is visible");
    JLabel postprocessLbl = new JLabel("Post-processing mode:");
    for (Postprocess pp : Postprocess.values) {
      postprocessCB.addItem("" + pp);
    }
    updatePostprocessCB();
    postprocessCB.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        JComboBox source = (JComboBox) e.getSource();
        renderMan.scene().setPostprocess(
            Postprocess.get(source.getSelectedIndex()));
      }
    });

    JPanel panel = new JPanel();
    GroupLayout layout = new GroupLayout(panel);
    panel.setLayout(layout);
    layout.setHorizontalGroup(layout.createSequentialGroup()
      .addContainerGap()
      .addGroup(layout.createParallelGroup()
        .addGroup(exposure.horizontalGroup(layout))
        .addGroup(layout.createSequentialGroup()
          .addComponent(postprocessLbl)
          .addPreferredGap(ComponentPlacement.RELATED)
          .addComponent(postprocessCB)
        )
        .addComponent(postprocessDescLbl)
      )
      .addContainerGap()
    );
    layout.setVerticalGroup(layout.createSequentialGroup()
      .addContainerGap()
      .addGroup(exposure.verticalGroup(layout))
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addGroup(layout.createParallelGroup(Alignment.BASELINE)
        .addComponent(postprocessLbl)
        .addComponent(postprocessCB)
      )
      .addPreferredGap(ComponentPlacement.RELATED)
      .addComponent(postprocessDescLbl)
      .addContainerGap()
    );
    return panel;
  }

  private JPanel buildGeneralPane() {
    JLabel canvasSizeLbl = new JLabel("Canvas size:");
    JLabel canvasSizeAdvisory = new JLabel("Note: Actual image size may not be the same as the window size!");

    canvasSizeCB.setEditable(true);
    canvasSizeCB.addItem("400x400");
    canvasSizeCB.addItem("1024x768");
    canvasSizeCB.addItem("960x540");
    canvasSizeCB.addItem("1920x1080");
    canvasSizeCB.addActionListener(canvasSizeListener);
    final JTextField canvasSizeEditor = (JTextField) canvasSizeCB.getEditor().getEditorComponent();
    canvasSizeEditor.addFocusListener(new FocusListener() {
      @Override
      public void focusLost(FocusEvent e) {
      }
      @Override
      public void focusGained(FocusEvent e) {
        canvasSizeEditor.selectAll();
      }
    });

    updateCanvasSizeField();

    loadSceneBtn.setText("Load Scene");
    loadSceneBtn.setIcon(Icon.load.imageIcon());
    loadSceneBtn.addActionListener(loadSceneListener);

    JButton loadSelectedChunksBtn = new JButton("Load Selected Chunks");
    loadSelectedChunksBtn.setToolTipText("Load the chunks that are currently selected in the map view");
    loadSelectedChunksBtn.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        sceneMan.loadChunks(chunky.getWorld(), chunky.getSelectedChunks());
      }
    });

    JButton reloadChunksBtn = new JButton("Reload Chunks");
    reloadChunksBtn.setIcon(Icon.reload.imageIcon());
    reloadChunksBtn.setToolTipText("Reload all chunks in the scene");
    reloadChunksBtn.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        sceneMan.reloadChunks();
      }
    });

    openSceneDirBtn.setText("Open Scene Directory");
    openSceneDirBtn.setToolTipText("Open the directory where Chunky stores scene descriptions and renders");
    openSceneDirBtn.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent arg0) {
        try {
          if (Desktop.isDesktopSupported()) {
            Desktop.getDesktop().open(context.getSceneDirectory());
          }
        } catch (IOException e) {
          logger.warn("Failed to open scene directory", e);
        }
      }
    });
    openSceneDirBtn.setVisible(Desktop.isDesktopSupported());

    loadSceneBtn.setToolTipText("This replaces the current scene!");
    JButton setCanvasSizeBtn = new JButton("Set Canvas Size");
    setCanvasSizeBtn.addActionListener(canvasSizeListener);
    JButton halveCanvasSizeBtn = new JButton("Halve");
    halveCanvasSizeBtn.setToolTipText("Halve the canvas width and height");
    halveCanvasSizeBtn.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        int width = renderMan.scene().canvasWidth() / 2;
        int height = renderMan.scene().canvasHeight() / 2;
        setCanvasSize(width, height);
      }
    });
    JButton doubleCanvasSizeBtn = new JButton("Double");
    doubleCanvasSizeBtn.setToolTipText("Double the canvas width and height");
    doubleCanvasSizeBtn.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        int width = renderMan.scene().canvasWidth() * 2;
        int height = renderMan.scene().canvasHeight() * 2;
        setCanvasSize(width, height);
      }
    });

    JButton makeDefaultBtn = new JButton("Make Default");
    makeDefaultBtn.setToolTipText("Make the current canvas size the default");
    makeDefaultBtn.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent arg0) {
        PersistentSettings.set3DCanvasSize(
            renderMan.scene().canvasWidth(),
            renderMan.scene().canvasHeight());
      }
    });

    JSeparator sep1 = new JSeparator();
    JSeparator sep2 = new JSeparator();

    stillWaterCB.setText("still water");
    stillWaterCB.addActionListener(stillWaterListener);
    updateStillWater();

    clearWaterCB.setText("clear water");
    stillWaterCB.addActionListener(clearWaterListener);
    updateClearWater();

    biomeColorsCB.setText("enable biome colors");
    updateBiomeColorsCB();

    saveDumpsCB.setText("save dump once every ");
    saveDumpsCB.addActionListener(saveDumpsListener);
    updateSaveDumpsCheckBox();

    String[] frequencyStrings = new String[dumpFrequencies.length];
    for (int i = 0; i < dumpFrequencies.length; ++i) {
      frequencyStrings[i] = Integer.toString(dumpFrequencies[i]);
    }
    dumpFrequencyCB.setModel(new DefaultComboBoxModel(frequencyStrings));
    dumpFrequencyCB.setEditable(true);
    dumpFrequencyCB.addActionListener(dumpFrequencyListener);
    updateDumpFrequencyField();

    saveSnapshotsCB.addActionListener(saveSnapshotListener);
    updateSaveSnapshotCheckBox();

    JPanel panel = new JPanel();
    GroupLayout layout = new GroupLayout(panel);
    panel.setLayout(layout);
    layout.setHorizontalGroup(layout.createSequentialGroup()
      .addContainerGap()
      .addGroup(layout.createParallelGroup()
        .addGroup(layout.createSequentialGroup()
          .addComponent(loadSceneBtn)
          .addPreferredGap(ComponentPlacement.RELATED)
          .addComponent(openSceneDirBtn)
        )
        .addGroup(layout.createSequentialGroup()
          .addComponent(loadSelectedChunksBtn)
          .addPreferredGap(ComponentPlacement.RELATED)
          .addComponent(reloadChunksBtn)
        )
        .addComponent(sep1)
        .addGroup(layout.createSequentialGroup()
          .addComponent(canvasSizeLbl)
          .addPreferredGap(ComponentPlacement.RELATED)
          .addComponent(canvasSizeCB, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE)
          .addPreferredGap(ComponentPlacement.RELATED)
          .addComponent(setCanvasSizeBtn)
          .addPreferredGap(ComponentPlacement.RELATED)
          .addComponent(makeDefaultBtn)
        )
        .addGroup(layout.createSequentialGroup()
          .addComponent(halveCanvasSizeBtn)
          .addPreferredGap(ComponentPlacement.RELATED)
          .addComponent(doubleCanvasSizeBtn)
        )
        .addComponent(canvasSizeAdvisory)
        .addComponent(sep2)
        .addComponent(stillWaterCB)
        .addComponent(clearWaterCB)
        .addComponent(biomeColorsCB)
        .addGroup(layout.createSequentialGroup()
          .addComponent(saveDumpsCB)
          .addComponent(dumpFrequencyCB, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE)
          .addComponent(dumpFrequencyLbl)
          .addGap(0, 0, Short.MAX_VALUE)
        )
        .addComponent(saveSnapshotsCB)
      )
      .addContainerGap()
    );
    layout.setVerticalGroup(layout.createSequentialGroup()
      .addContainerGap()
      .addGroup(layout.createParallelGroup()
        .addComponent(loadSceneBtn)
        .addComponent(openSceneDirBtn)
      )
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addGroup(layout.createParallelGroup()
        .addComponent(loadSelectedChunksBtn)
        .addComponent(reloadChunksBtn)
      )
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addComponent(sep1, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addGroup(layout.createParallelGroup(Alignment.BASELINE)
        .addComponent(canvasSizeLbl)
        .addComponent(canvasSizeCB, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE)
        .addComponent(setCanvasSizeBtn)
        .addComponent(makeDefaultBtn)
      )
      .addPreferredGap(ComponentPlacement.RELATED)
      .addGroup(layout.createParallelGroup()
        .addComponent(halveCanvasSizeBtn)
        .addComponent(doubleCanvasSizeBtn)
      )
      .addPreferredGap(ComponentPlacement.RELATED)
      .addComponent(canvasSizeAdvisory)
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addComponent(sep2, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addComponent(stillWaterCB)
      .addComponent(clearWaterCB)
      .addComponent(biomeColorsCB)
      .addGroup(layout.createParallelGroup(Alignment.BASELINE)
        .addComponent(saveDumpsCB)
        .addComponent(dumpFrequencyCB)
        .addComponent(dumpFrequencyLbl)
      )
      .addComponent(saveSnapshotsCB)
      .addContainerGap()
    );
    return panel;
  }

  private JPanel buildLightingPane() {

    changeSunColorBtn.setText("Change Sun Color");
    changeSunColorBtn.setIcon(Icon.colors.imageIcon());
    changeSunColorBtn.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        ColorPicker picker = new ColorPicker(changeSunColorBtn, renderMan.scene().sun().getColor());
        picker.addColorListener(new ColorListener() {
          @Override
          public void onColorPicked(Vector3d color) {
            renderMan.scene().sun().setColor(color);
          }
        });
      }
    });

    directLight.setText("enable sunlight");
    directLight.setSelected(renderMan.scene().getDirectLight());
    directLight.addActionListener(directLightListener);

    enableEmitters.setText("enable emitters");
    enableEmitters.setSelected(renderMan.scene().getEmittersEnabled());
    enableEmitters.addActionListener(emittersListener);

    emitterIntensity.update();

    sunIntensity.update();

    skyLight.update();

    sunAzimuth.update();

    sunAltitude.update();

    JPanel panel = new JPanel();
    GroupLayout layout = new GroupLayout(panel);
    panel.setLayout(layout);
    layout.setHorizontalGroup(layout.createSequentialGroup()
      .addContainerGap()
      .addGroup(layout.createParallelGroup()
        .addComponent(directLight)
        .addComponent(enableEmitters)
        .addGroup(layout.createSequentialGroup()
          .addGroup(layout.createParallelGroup()
            .addComponent(skyLight.getLabel())
            .addComponent(emitterIntensity.getLabel())
            .addComponent(sunIntensity.getLabel())
            .addComponent(sunAzimuth.getLabel())
            .addComponent(sunAltitude.getLabel())
          )
          .addGroup(layout.createParallelGroup()
            .addComponent(skyLight.getSlider())
            .addComponent(emitterIntensity.getSlider())
            .addComponent(sunIntensity.getSlider())
            .addComponent(sunAzimuth.getSlider())
            .addComponent(sunAltitude.getSlider())
          )
          .addGroup(layout.createParallelGroup()
            .addComponent(skyLight.getField())
            .addComponent(emitterIntensity.getField())
            .addComponent(sunIntensity.getField())
            .addComponent(sunAzimuth.getField())
            .addComponent(sunAltitude.getField())
          )
        )
        .addComponent(changeSunColorBtn)
      )
      .addContainerGap()
    );
    layout.setVerticalGroup(layout.createSequentialGroup()
      .addContainerGap()
      .addGroup(skyLight.verticalGroup(layout))
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addComponent(enableEmitters)
      .addPreferredGap(ComponentPlacement.RELATED)
      .addGroup(emitterIntensity.verticalGroup(layout))
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addComponent(directLight)
      .addPreferredGap(ComponentPlacement.RELATED)
      .addGroup(sunIntensity.verticalGroup(layout))
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addGroup(sunAzimuth.verticalGroup(layout))
      .addPreferredGap(ComponentPlacement.RELATED)
      .addGroup(sunAltitude.verticalGroup(layout))
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addComponent(changeSunColorBtn)
      .addContainerGap()
    );
    return panel;
  }

  private JPanel buildSkyPane() {

    JLabel skyModeLbl = new JLabel("Sky Mode:");
    skyModeCB.setModel(new DefaultComboBoxModel(Sky.SkyMode.values()));
    skyModeCB.addActionListener(skyModeListener);
    updateSkyMode();

    JLabel skymapRotationLbl = new JLabel("Skymap rotation:");
    skymapRotationSlider.setMinimum(1);
    skymapRotationSlider.setMaximum(100);
    skymapRotationSlider.addChangeListener(skyRotationListener);
    skymapRotationSlider.setToolTipText("Controls the horizontal rotational offset for the skymap");
    JLabel lightProbeRotationLbl = new JLabel("Skymap rotation:");
    lightProbeRotationSlider.setMinimum(1);
    lightProbeRotationSlider.setMaximum(100);
    lightProbeRotationSlider.addChangeListener(skyRotationListener);
    lightProbeRotationSlider.setToolTipText("Controls the horizontal rotational offset for the skymap");
    JLabel skyboxRotationLbl = new JLabel("Skybox rotation:");
    skyboxRotationSlider.setMinimum(1);
    skyboxRotationSlider.setMaximum(100);
    skyboxRotationSlider.addChangeListener(skyRotationListener);
    skyboxRotationSlider.setToolTipText("Controls the horizontal rotational offset for the skymap");
    updateSkyRotation();

    skyHorizonOffset.update();
    cloudSize.update();
    cloudXOffset.update();
    cloudYOffset.update();
    cloudZOffset.update();

    JLabel verticalResolutionLbl = new JLabel("Vertical resolution (degrees):");
    ButtonGroup verticalResolution = new ButtonGroup();
    v90Btn.setSelected(true);
    v180Btn.setSelected(false);
    verticalResolution.add(v90Btn);
    verticalResolution.add(v180Btn);

    v90Btn.addActionListener(v90Listener);
    v180Btn.addActionListener(v180Listener);
    updateVerticalResolution();

    simulatedSkyPanel.setBorder(BorderFactory.createTitledBorder("Simulated Sky Settings"));
    GroupLayout simulatedSkyLayout = new GroupLayout(simulatedSkyPanel);
    simulatedSkyPanel.setLayout(simulatedSkyLayout);
    simulatedSkyLayout.setAutoCreateContainerGaps(true);
    simulatedSkyLayout.setAutoCreateGaps(true);
    simulatedSkyLayout.setHorizontalGroup(simulatedSkyLayout.createParallelGroup()
      .addGroup(skyHorizonOffset.horizontalGroup(simulatedSkyLayout))
    );
    simulatedSkyLayout.setVerticalGroup(simulatedSkyLayout.createSequentialGroup()
      .addGroup(skyHorizonOffset.verticalGroup(simulatedSkyLayout))
    );

    skymapPanel.setBorder(BorderFactory.createTitledBorder("Skymap Settings"));
    GroupLayout skymapLayout = new GroupLayout(skymapPanel);
    skymapPanel.setLayout(skymapLayout);
    skymapLayout.setAutoCreateContainerGaps(true);
    skymapLayout.setAutoCreateGaps(true);
    skymapLayout.setHorizontalGroup(skymapLayout.createParallelGroup()
      .addComponent(loadSkymapBtn)
      .addGroup(skymapLayout.createSequentialGroup()
        .addComponent(skymapRotationLbl)
        .addComponent(skymapRotationSlider)
      )
      .addGroup(skymapLayout.createSequentialGroup()
        .addComponent(verticalResolutionLbl)
        .addPreferredGap(ComponentPlacement.RELATED)
        .addComponent(v90Btn)
        .addPreferredGap(ComponentPlacement.RELATED)
        .addComponent(v180Btn)
      )
    );
    skymapLayout.setVerticalGroup(skymapLayout.createSequentialGroup()
      .addComponent(loadSkymapBtn)
      .addPreferredGap(ComponentPlacement.RELATED)
      .addGroup(skymapLayout.createParallelGroup(Alignment.BASELINE)
        .addComponent(verticalResolutionLbl)
        .addComponent(v90Btn)
        .addComponent(v180Btn)
      )
      .addPreferredGap(ComponentPlacement.RELATED)
      .addGroup(skymapLayout.createParallelGroup()
        .addComponent(skymapRotationLbl)
        .addComponent(skymapRotationSlider)
      )
    );

    loadSkymapBtn.setText("Load Skymap");
    loadSkymapBtn.setToolTipText("Use a panoramic skymap");
    loadSkymapBtn.addActionListener(new SkymapTextureLoader(renderMan));

    lightProbePanel.setBorder(BorderFactory.createTitledBorder("Spherical Skymap Settings"));
    GroupLayout lightProbeLayout = new GroupLayout(lightProbePanel);
    lightProbePanel.setLayout(lightProbeLayout);
    lightProbeLayout.setAutoCreateContainerGaps(true);
    lightProbeLayout.setAutoCreateGaps(true);
    lightProbeLayout.setHorizontalGroup(lightProbeLayout.createParallelGroup()
      .addComponent(loadLightProbeBtn)
      .addGroup(lightProbeLayout.createSequentialGroup()
        .addComponent(lightProbeRotationLbl)
        .addComponent(lightProbeRotationSlider)
      )
    );
    lightProbeLayout.setVerticalGroup(lightProbeLayout.createSequentialGroup()
      .addComponent(loadLightProbeBtn)
      .addPreferredGap(ComponentPlacement.RELATED)
      .addGroup(lightProbeLayout.createParallelGroup()
        .addComponent(lightProbeRotationLbl)
        .addComponent(lightProbeRotationSlider)
      )
    );

    loadLightProbeBtn.setText("Load Spherical Skymap");
    loadLightProbeBtn.setToolTipText("Select the spherical skymap to use");
    loadLightProbeBtn.addActionListener(new SkymapTextureLoader(renderMan));

    skyGradientPanel.setBorder(BorderFactory.createTitledBorder("Sky Gradient"));
    gradientEditor = new GradientEditor();
    gradientEditor.addGradientListener(gradientListener);
    updateSkyGradient();
    skyGradientPanel.add(gradientEditor);

    GroupLayout skyboxLayout = new GroupLayout(skyboxPanel);
    skyboxPanel.setLayout(skyboxLayout);
    skyboxPanel.setBorder(BorderFactory.createTitledBorder("Skybox"));

    JLabel skyboxLbl = new JLabel("Load skybox textures:");

    JButton loadUpTexture = new JButton("Up");
    loadUpTexture.setToolTipText("Load up texture");
    loadUpTexture.setIcon(Icon.skyboxUp.imageIcon());
    loadUpTexture.addActionListener(new SkyboxTextureLoader(renderMan, Sky.SKYBOX_UP));

    JButton loadDownTexture = new JButton("Down");
    loadDownTexture.setToolTipText("Load down texture");
    loadDownTexture.setIcon(Icon.skyboxDown.imageIcon());
    loadDownTexture.addActionListener(new SkyboxTextureLoader(renderMan, Sky.SKYBOX_DOWN));

    JButton loadFrontTexture = new JButton("Front");
    loadFrontTexture.setToolTipText("Load front (north) texture");
    loadFrontTexture.setIcon(Icon.skyboxFront.imageIcon());
    loadFrontTexture.addActionListener(new SkyboxTextureLoader(renderMan, Sky.SKYBOX_FRONT));

    JButton loadBackTexture = new JButton("Back");
    loadBackTexture.setToolTipText("Load back (south) texture");
    loadBackTexture.setIcon(Icon.skyboxBack.imageIcon());
    loadBackTexture.addActionListener(new SkyboxTextureLoader(renderMan, Sky.SKYBOX_BACK));

    JButton loadRightTexture = new JButton("Right");
    loadRightTexture.setToolTipText("Load right (east) texture");
    loadRightTexture.setIcon(Icon.skyboxRight.imageIcon());
    loadRightTexture.addActionListener(new SkyboxTextureLoader(renderMan, Sky.SKYBOX_RIGHT));

    JButton loadLeftTexture = new JButton("Left");
    loadLeftTexture.setToolTipText("Load left (west) texture");
    loadLeftTexture.setIcon(Icon.skyboxLeft.imageIcon());
    loadLeftTexture.addActionListener(new SkyboxTextureLoader(renderMan, Sky.SKYBOX_LEFT));

    skyboxLayout.setAutoCreateContainerGaps(true);
    skyboxLayout.setAutoCreateGaps(true);
    skyboxLayout.setHorizontalGroup(skyboxLayout.createParallelGroup()
      .addGroup(skyboxLayout.createSequentialGroup()
        .addComponent(skyboxLbl)
        .addGroup(skyboxLayout.createParallelGroup()
          .addComponent(loadUpTexture)
          .addComponent(loadFrontTexture)
          .addComponent(loadRightTexture)
        )
        .addGroup(skyboxLayout.createParallelGroup()
          .addComponent(loadDownTexture)
          .addComponent(loadBackTexture)
          .addComponent(loadLeftTexture)
        )
      )
      .addGroup(skyboxLayout.createSequentialGroup()
        .addComponent(skyboxRotationLbl)
        .addComponent(skyboxRotationSlider)
      )
    );
    skyboxLayout.setVerticalGroup(skyboxLayout.createSequentialGroup()
      .addComponent(skyboxLbl)
      .addGroup(skyboxLayout.createParallelGroup()
        .addComponent(loadUpTexture)
        .addComponent(loadDownTexture)
      )
      .addGroup(skyboxLayout.createParallelGroup()
        .addComponent(loadFrontTexture)
        .addComponent(loadBackTexture)
      )
      .addGroup(skyboxLayout.createParallelGroup()
        .addComponent(loadRightTexture)
        .addComponent(loadLeftTexture)
      )
      .addGroup(skyboxLayout.createParallelGroup()
        .addComponent(skyboxRotationLbl)
        .addComponent(skyboxRotationSlider)
      )
    );


    atmosphereEnabled.setText("enable atmosphere");
    atmosphereEnabled.addActionListener(atmosphereListener);
    updateAtmosphereCheckBox();

    volumetricFogEnabled.setText("enable volumetric fog");
    volumetricFogEnabled.addActionListener(volumetricFogListener);
    updateVolumetricFogCheckBox();

    cloudsEnabled.setText("enable clouds");
    cloudsEnabled.addActionListener(cloudsEnabledListener);
    updateCloudsEnabledCheckBox();

    JPanel panel = new JPanel();
    GroupLayout layout = new GroupLayout(panel);
    panel.setLayout(layout);
    layout.setHorizontalGroup(layout.createSequentialGroup()
      .addContainerGap()
      .addGroup(layout.createParallelGroup()
        .addGroup(layout.createSequentialGroup()
          .addComponent(skyModeLbl)
          .addPreferredGap(ComponentPlacement.RELATED)
          .addComponent(skyModeCB, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE)
        )
        .addComponent(simulatedSkyPanel)
        .addComponent(skymapPanel)
        .addComponent(lightProbePanel)
        .addComponent(skyGradientPanel)
        .addComponent(skyboxPanel)
        .addComponent(atmosphereEnabled)
        .addComponent(volumetricFogEnabled)
        .addComponent(cloudsEnabled)
        .addGroup(cloudSize.horizontalGroup(layout))
        .addGroup(cloudXOffset.horizontalGroup(layout))
        .addGroup(cloudYOffset.horizontalGroup(layout))
        .addGroup(cloudZOffset.horizontalGroup(layout))
      )
      .addContainerGap()
    );
    layout.setVerticalGroup(layout.createSequentialGroup()
      .addContainerGap()
      .addGroup(layout.createParallelGroup(Alignment.BASELINE)
        .addComponent(skyModeLbl)
        .addComponent(skyModeCB)
      )
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addComponent(simulatedSkyPanel)
      .addComponent(skymapPanel)
      .addComponent(lightProbePanel)
      .addComponent(skyGradientPanel)
      .addComponent(skyboxPanel)
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addComponent(atmosphereEnabled)
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addComponent(volumetricFogEnabled)
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addComponent(cloudsEnabled)
      .addPreferredGap(ComponentPlacement.RELATED)
      .addGroup(cloudSize.verticalGroup(layout))
      .addPreferredGap(ComponentPlacement.RELATED)
      .addGroup(cloudXOffset.verticalGroup(layout))
      .addPreferredGap(ComponentPlacement.RELATED)
      .addGroup(cloudYOffset.verticalGroup(layout))
      .addPreferredGap(ComponentPlacement.RELATED)
      .addGroup(cloudZOffset.verticalGroup(layout))
      .addContainerGap()
    );
    return panel;
  }

  private JPanel buildCameraPane() {
    JLabel projectionModeLbl = new JLabel("Projection");

    fov.update();

    dof = new DoFAdjuster(renderMan);
    dof.update();

    subjectDistance.update();

    JLabel presetLbl = new JLabel("Preset:");
    CameraPreset[] presets = {
      CameraPreset.NONE,
      CameraPreset.ISO_WEST_NORTH, CameraPreset.ISO_NORTH_EAST,
      CameraPreset.ISO_EAST_SOUTH, CameraPreset.ISO_SOUTH_WEST,
      CameraPreset.SKYBOX_RIGHT, CameraPreset.SKYBOX_LEFT,
      CameraPreset.SKYBOX_UP, CameraPreset.SKYBOX_DOWN,
      CameraPreset.SKYBOX_FRONT, CameraPreset.SKYBOX_BACK,
    };
    cameraPreset.setModel(new DefaultComboBoxModel(presets));
    cameraPreset.setMaximumRowCount(presets.length);
    final int presetHeight = cameraPreset.getPreferredSize().height;
    final int presetWidth = cameraPreset.getPreferredSize().width;
    cameraPreset.setRenderer(new DefaultListCellRenderer() {
      @Override
      public Component getListCellRendererComponent(JList list, Object value,
          int index, boolean isSelected, boolean cellHasFocus) {
        JLabel label = (JLabel) super.getListCellRendererComponent(
            list, value, index, isSelected, cellHasFocus);
        label.setPreferredSize(new Dimension(presetWidth, presetHeight));
        CameraPreset preset = (CameraPreset) value;
        label.setIcon(preset.getIcon());
        return label;
      }
    });
    cameraPreset.addActionListener(cameraPresetListener);

    JLabel customPresetLbl = new JLabel("Custom preset:");
    customPreset.setEditable(true);
    updateCustomPresets();
    JButton savePreset = new JButton("save");
    savePreset.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        String name = "";
        int selected = customPreset.getSelectedIndex();
        if (selected == -1) {
          // select name
          name = (String) customPreset.getEditor().getItem();
          name = (name==null) ? "" : name.trim();
          if (name.isEmpty()) {
            // auto-assign name
            int nextIndex = customPreset.getItemCount() + 1;
            outer: while (true) {
              name = "custom-" + (nextIndex++);
              for (int i = 0; i < customPreset.getItemCount(); ++i) {
                String item = (String) customPreset.getItemAt(i);
                if (name.equals(item)) {
                  continue outer;
                }
              }
              break;
            }
          } else {
            for (int i = 0; i < customPreset.getItemCount(); ++i) {
              String item = (String) customPreset.getItemAt(i);
              if (name.equals(item)) {
                selected = i;
                break;
              }
            }
          }
          if (selected == -1) {
            // add new preset
            selected =   customPreset.getItemCount();
            customPreset.addItem(name);

          }
          customPreset.setSelectedIndex(selected);
        } else {
          name = (String) customPreset.getSelectedItem();
        }
        renderMan.scene().saveCameraPreset(name);
      }
    });
    JButton loadPreset = new JButton("load");
    loadPreset.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        String name = "";
        int selected = customPreset.getSelectedIndex();
        if (selected == -1) {
          // select name
          name = (String) customPreset.getEditor().getItem();
          name = (name==null) ? "" : name.trim();
        } else {
          name = ((String) customPreset.getSelectedItem()).trim();
        }
        if (!name.isEmpty()) {
          renderMan.scene().loadCameraPreset(name);
        }
      }
    });
    JButton deletePreset = new JButton("delete");
    deletePreset.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        String name = "";
        int selected = customPreset.getSelectedIndex();
        if (selected == -1) {
          // select name
          name = (String) customPreset.getEditor().getItem();
          name = (name==null) ? "" : name.trim();
        } else {
          name = ((String) customPreset.getSelectedItem()).trim();
        }
        if (!name.isEmpty()) {
          renderMan.scene().deleteCameraPreset(name);
          if (selected != -1) {
            customPreset.removeItemAt(selected);
          } else {
            for (int i = 0; i < customPreset.getItemCount(); ++i) {
              if (name.equals(customPreset.getItemAt(i))) {
                customPreset.removeItemAt(i);
                break;
              }
            }
          }
        }
      }
    });

    ProjectionMode[] projectionModes = ProjectionMode.values();
    projectionMode.setModel(new DefaultComboBoxModel(projectionModes));
    projectionMode.addActionListener(projectionModeListener);
    updateProjectionMode();

    JButton autoFocusBtn = new JButton("Autofocus");
    autoFocusBtn.setToolTipText("Focuses on the object right in the center, under the crosshairs");
    autoFocusBtn.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        renderMan.scene().autoFocus();
        dof.update();
        subjectDistance.update();
      }
    });

    JButton cameraToPlayerBtn = new JButton("Camera to player");
    cameraToPlayerBtn.setToolTipText("Move camera to player position");
    cameraToPlayerBtn.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        renderMan.scene().moveCameraToPlayer();
      }
    });

    JLabel posLbl = new JLabel("Position:");
    cameraX.setColumns(10);
    cameraX.setHorizontalAlignment(JTextField.RIGHT);
    cameraX.addActionListener(cameraPositionListener);
    cameraY.setColumns(10);
    cameraY.setHorizontalAlignment(JTextField.RIGHT);
    cameraY.addActionListener(cameraPositionListener);
    cameraZ.setColumns(10);
    cameraZ.setHorizontalAlignment(JTextField.RIGHT);
    cameraZ.addActionListener(cameraPositionListener);
    updateCameraPosition();

    JLabel dirLbl = new JLabel("Direction:");
    cameraYaw.setColumns(10);
    cameraYaw.setHorizontalAlignment(JTextField.RIGHT);
    cameraYaw.addActionListener(cameraDirectionListener);
    cameraPitch.setColumns(10);
    cameraPitch.setHorizontalAlignment(JTextField.RIGHT);
    cameraPitch.addActionListener(cameraDirectionListener);
    cameraRoll.setColumns(10);
    cameraRoll.setHorizontalAlignment(JTextField.RIGHT);
    cameraRoll.addActionListener(cameraDirectionListener);
    updateCameraDirection();

    JButton centerCameraBtn = new JButton("Center camera");
    centerCameraBtn.setToolTipText("Center camera above loaded chunks");
    centerCameraBtn.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        renderMan.scene().moveCameraToCenter();
      }
    });

    JSeparator sep1 = new JSeparator();

    JPanel panel = new JPanel();
    GroupLayout layout = new GroupLayout(panel);
    panel.setLayout(layout);
    layout.setHorizontalGroup(layout.createSequentialGroup()
      .addContainerGap()
      .addGroup(layout.createParallelGroup()
        .addGroup(layout.createSequentialGroup()
          .addGroup(layout.createParallelGroup()
            .addComponent(posLbl)
            .addComponent(dirLbl)
          )
          .addPreferredGap(ComponentPlacement.RELATED)
          .addGroup(layout.createParallelGroup()
            .addComponent(cameraX, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE)
            .addComponent(cameraYaw, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE)
          )
          .addPreferredGap(ComponentPlacement.RELATED)
          .addGroup(layout.createParallelGroup()
            .addComponent(cameraY, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE)
            .addComponent(cameraPitch, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE)
          )
          .addPreferredGap(ComponentPlacement.RELATED)
          .addGroup(layout.createParallelGroup()
            .addComponent(cameraZ, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE)
            .addComponent(cameraRoll, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE)
          )
        )
        .addGroup(layout.createSequentialGroup()
          .addComponent(presetLbl)
          .addPreferredGap(ComponentPlacement.RELATED)
          .addComponent(cameraPreset)
        )
        .addGroup(layout.createSequentialGroup()
          .addComponent(customPresetLbl)
          .addPreferredGap(ComponentPlacement.RELATED)
          .addComponent(customPreset)
          .addPreferredGap(ComponentPlacement.RELATED)
          .addComponent(savePreset)
          .addPreferredGap(ComponentPlacement.RELATED)
          .addComponent(loadPreset)
          .addPreferredGap(ComponentPlacement.RELATED)
          .addComponent(deletePreset)
        )
        .addGroup(layout.createSequentialGroup()
          .addComponent(cameraToPlayerBtn)
          .addPreferredGap(ComponentPlacement.RELATED)
          .addComponent(centerCameraBtn)
        )
        .addComponent(sep1)
        .addGroup(layout.createSequentialGroup()
          .addGroup(layout.createParallelGroup()
            .addComponent(projectionModeLbl)
            .addComponent(fov.getLabel())
            .addComponent(dof.getLabel())
            .addComponent(subjectDistance.getLabel()))
          .addGroup(layout.createParallelGroup()
            .addComponent(projectionMode)
            .addComponent(fov.getSlider())
            .addComponent(dof.getSlider())
            .addComponent(subjectDistance.getSlider()))
          .addGroup(layout.createParallelGroup()
            .addComponent(fov.getField())
            .addComponent(dof.getField())
            .addComponent(subjectDistance.getField())))
        .addComponent(autoFocusBtn))
      .addContainerGap()
    );
    layout.setVerticalGroup(layout.createSequentialGroup()
      .addContainerGap()
      .addGroup(layout.createParallelGroup(Alignment.BASELINE)
        .addComponent(presetLbl)
        .addComponent(cameraPreset)
      )
      .addPreferredGap(ComponentPlacement.RELATED)
      .addGroup(layout.createParallelGroup(Alignment.BASELINE)
        .addComponent(customPresetLbl)
        .addComponent(customPreset)
        .addComponent(savePreset)
        .addComponent(loadPreset)
        .addComponent(deletePreset)
      )
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addGroup(layout.createParallelGroup(Alignment.BASELINE)
        .addComponent(posLbl)
        .addComponent(cameraX)
        .addComponent(cameraY)
        .addComponent(cameraZ)
      )
      .addPreferredGap(ComponentPlacement.RELATED)
      .addGroup(layout.createParallelGroup(Alignment.BASELINE)
        .addComponent(dirLbl)
        .addComponent(cameraYaw)
        .addComponent(cameraPitch)
        .addComponent(cameraRoll)
      )
      .addPreferredGap(ComponentPlacement.RELATED)
      .addGroup(layout.createParallelGroup()
        .addComponent(cameraToPlayerBtn)
        .addComponent(centerCameraBtn)
      )
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addComponent(sep1, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE)
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addGroup(layout.createParallelGroup(Alignment.BASELINE)
        .addComponent(projectionModeLbl)
        .addComponent(projectionMode)
      )
      .addPreferredGap(ComponentPlacement.RELATED)
      .addGroup(fov.verticalGroup(layout))
      .addGroup(dof.verticalGroup(layout))
      .addGroup(subjectDistance.verticalGroup(layout))
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addComponent(autoFocusBtn)
      .addContainerGap()
    );
    return panel;
  }

  private JPanel buildHelpPane() {
    JLabel helpLbl = new JLabel(
      "<html>Render Preview Controls:<br>" +
      "<b>W</b> move camera forward<br>" +
      "<b>S</b> move camera backward<br>" +
      "<b>A</b> strafe camera left<br>" +
      "<b>D</b> strafe camera right<br>" +
      "<b>R</b> move camera up<br>" +
      "<b>F</b> move camera down<br>" +
      "<b>U</b> toggle fullscreen mode<br>" +
      "<b>K</b> move camera forward x100<br>" +
      "<b>J</b> move camera backward x100<br>" +
      "<br>" +
      "Holding <b>SHIFT</b> makes the basic movement keys so move 1/10th of the normal distance."
    );

    JPanel panel = new JPanel();
    GroupLayout layout = new GroupLayout(panel);
    panel.setLayout(layout);
    layout.setHorizontalGroup(layout.createSequentialGroup()
      .addContainerGap()
      .addComponent(helpLbl)
      .addContainerGap()
    );
    layout.setVerticalGroup(layout.createSequentialGroup()
      .addContainerGap()
      .addComponent(helpLbl)
      .addContainerGap()
    );
    return panel;
  }

  protected void updateStillWater() {
    stillWaterCB.removeActionListener(stillWaterListener);
    stillWaterCB.setSelected(renderMan.scene().stillWaterEnabled());
    stillWaterCB.addActionListener(stillWaterListener);
  }

  protected void updateClearWater() {
    clearWaterCB.removeActionListener(clearWaterListener);
    clearWaterCB.setSelected(renderMan.scene().getClearWater());
    clearWaterCB.addActionListener(clearWaterListener);
  }

  protected void updateBiomeColorsCB() {
    biomeColorsCB.removeActionListener(biomeColorsCBListener);
    biomeColorsCB.addActionListener(biomeColorsCBListener);
    biomeColorsCB.setSelected(renderMan.scene().biomeColorsEnabled());
  }

  protected void updateAtmosphereCheckBox() {
    atmosphereEnabled.removeActionListener(atmosphereListener);
    atmosphereEnabled.setSelected(renderMan.scene().atmosphereEnabled());
    atmosphereEnabled.addActionListener(atmosphereListener);
  }

  protected void updateVerticalResolution() {
    v90Btn.removeActionListener(v90Listener);
    v180Btn.removeActionListener(v180Listener);
    boolean mirror = renderMan.scene().sky().isMirrored();
    v90Btn.setSelected(mirror);
    v180Btn.setSelected(!mirror);
    v90Btn.addActionListener(v90Listener);
    v180Btn.addActionListener(v180Listener);
  }

  protected void updateVolumetricFogCheckBox() {
    volumetricFogEnabled.removeActionListener(volumetricFogListener);
    volumetricFogEnabled.setSelected(renderMan.scene().volumetricFogEnabled());
    volumetricFogEnabled.addActionListener(volumetricFogListener);
  }

  protected void updateCloudsEnabledCheckBox() {
    cloudsEnabled.removeActionListener(cloudsEnabledListener);
    cloudsEnabled.setSelected(renderMan.scene().sky().cloudsEnabled());
    cloudsEnabled.addActionListener(cloudsEnabledListener);
  }

  private void updateTitle() {
    setTitle("Render Controls - " + renderMan.scene().name());
  }

  private final ActionListener dumpFrequencyListener = new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
      try {
        renderMan.scene().setDumpFrequency(getDumpFrequency());
      } catch (NumberFormatException e1) {
      }
      updateDumpFrequencyField();
    }
  };

  private final ActionListener saveDumpsListener = new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
      boolean enabled = saveDumpsCB.isSelected();
      if (enabled) {
        renderMan.scene().setDumpFrequency(getDumpFrequency());
      } else {
        renderMan.scene().setDumpFrequency(0);
      }
      dumpFrequencyCB.setEnabled(enabled);
      saveSnapshotsCB.setEnabled(enabled);
    }
  };

  private final ActionListener canvasSizeListener = new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
      String size = (String) canvasSizeCB.getSelectedItem();
      try {
        Pattern regex = Pattern.compile("([0-9]+)[xX.*]([0-9]+)");
        Matcher matcher = regex.matcher(size);
        if (matcher.matches()) {
          int width = Integer.parseInt(matcher.group(1));
          int height = Integer.parseInt(matcher.group(2));
          setCanvasSize(width, height);
        } else {
          logger.info("Failed to set canvas size: format must be WIDTHxHEIGHT!");
        }
      } catch (NumberFormatException e1) {
        logger.info("Failed to set canvas size: invalid dimensions!");
      }
    }
  };

  private final ActionListener sceneNameActionListener = new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
      JTextField source = (JTextField) e.getSource();
      renderMan.scene().setName(source.getText());
      updateTitle();
      sceneMan.saveScene();
    }
  };
  private final DocumentListener sceneNameListener = new DocumentListener() {
    @Override
    public void removeUpdate(DocumentEvent e) {
      updateName(e);
    }
    @Override
    public void insertUpdate(DocumentEvent e) {
      updateName(e);
    }
    @Override
    public void changedUpdate(DocumentEvent e) {
      updateName(e);
    }
    private void updateName(DocumentEvent e) {
      try {
        Document d = e.getDocument();
        renderMan.scene().setName(d.getText(0, d.getLength()));
        updateTitle();
      } catch (BadLocationException e1) {
        e1.printStackTrace();
      }
    }
  };

  private final DocumentListener sppTargetListener = new DocumentListener() {
    @Override
    public void removeUpdate(DocumentEvent e) {
      updateName(e);
    }
    @Override
    public void insertUpdate(DocumentEvent e) {
      updateName(e);
    }
    @Override
    public void changedUpdate(DocumentEvent e) {
      updateName(e);
    }
    private void updateName(DocumentEvent e) {
      try {
        Document d = e.getDocument();
        String value = d.getText(0, d.getLength());
        renderMan.scene().setTargetSPP(Integer.parseInt(value));
        updateTitle();
      } catch (NumberFormatException e1) {
      } catch (BadLocationException e1) {
        e1.printStackTrace();
      }
    }
  };

  private final GradientListener gradientListener = new GradientListener() {
    @Override
    public void gradientChanged(List<Vector4d> newGradient) {
      renderMan.scene().sky().setGradient(newGradient);
    }
    @Override
    public void stopSelected(int index) {
    }
    @Override
    public void stopModified(int index, Vector4d marker) {
    }
  };

  private final ActionListener saveSnapshotListener = new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
      JCheckBox source = (JCheckBox) e.getSource();
      renderMan.scene().setSaveSnapshots(source.isSelected());
    }
  };
  private final ActionListener saveSceneListener = new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
      renderMan.scene().setName(sceneNameField.getText());
      sceneMan.saveScene();
    }
  };
  private final ActionListener saveFrameListener = new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
      new Thread() {
        @Override
        public void run() {
          renderMan.saveSnapshot(RenderControls.this);
        }
      }.start();
    }
  };
  private final ActionListener loadSceneListener = new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
      new SceneSelector(RenderControls.this, context);
    }
  };
  private final ChangeListener skyRotationListener = new ChangeListener() {
    @Override
    public void stateChanged(ChangeEvent e) {
      JSlider source = (JSlider) e.getSource();
      double value = (double) (source.getValue() - source.getMinimum())
          / (source.getMaximum() - source.getMinimum());
      double rotation = value * 2 * Math.PI;
      renderMan.scene().sky().setRotation(rotation);
    }
  };
  private final ActionListener projectionModeListener = new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
      JComboBox source = (JComboBox) e.getSource();
      Object selected = source.getSelectedItem();
      if (selected != null && selected instanceof ProjectionMode) {
        renderMan.scene().camera().setProjectionMode(
            (ProjectionMode) selected);
        updateProjectionMode();
        fov.update();
      }
    }
  };
  private final ActionListener cameraPresetListener = new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
      JComboBox source = (JComboBox) e.getSource();
      Object selected = source.getSelectedItem();
      if (selected != null && selected instanceof CameraPreset) {
        CameraPreset preset = (CameraPreset) selected;
        preset.apply(renderMan.scene().camera());
        updateProjectionMode();
        fov.update();
        updateCameraDirection();
      }
    }
  };
  private final ActionListener skyModeListener = new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
      JComboBox source = (JComboBox) e.getSource();
      renderMan.scene().sky().setSkyMode((SkyMode) source.getSelectedItem());
      updateSkyMode();
      RenderControls.this.pack();
    }
  };
  private final ActionListener cameraPositionListener = new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
      Vector3d pos = new Vector3d(renderMan.scene().camera().getPosition());
      try {
        pos.x = numberFormat.parse(cameraX.getText()).doubleValue();
      } catch (NumberFormatException ex) {
      } catch (ParseException ex) {
      }
      try {
        pos.y = numberFormat.parse(cameraY.getText()).doubleValue();
      } catch (NumberFormatException ex) {
      } catch (ParseException ex) {
      }
      try {
        pos.z = numberFormat.parse(cameraZ.getText()).doubleValue();
      } catch (NumberFormatException ex) {
      } catch (ParseException ex) {
      }
      renderMan.scene().camera().setPosition(pos);
      updateCameraPosition();
    }
  };
  private final ActionListener cameraDirectionListener = new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
      double yaw = renderMan.scene().camera().getYaw();
      double pitch = renderMan.scene().camera().getPitch();
      double roll = renderMan.scene().camera().getRoll();
      try {
        double value = numberFormat.parse(cameraPitch.getText()).doubleValue();
        pitch = QuickMath.degToRad(value);
      } catch (NumberFormatException ex) {
      } catch (ParseException ex) {
      }
      try {
        double value = numberFormat.parse(cameraYaw.getText()).doubleValue();
        yaw = QuickMath.degToRad(value);
      } catch (NumberFormatException ex) {
      } catch (ParseException ex) {
      }
      try {
        double value = numberFormat.parse(cameraRoll.getText()).doubleValue();
        roll = QuickMath.degToRad(value);
      } catch (NumberFormatException ex) {
      } catch (ParseException ex) {
      }
      renderMan.scene().camera().setView(yaw, pitch, roll);
      updateCameraDirection();
    }
  };
  private final ActionListener stillWaterListener = new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
      renderMan.scene().setStillWater(stillWaterCB.isSelected());
    }
  };
  private final ActionListener clearWaterListener = new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
      renderMan.scene().setClearWater(clearWaterCB.isSelected());
    }
  };
  private final ActionListener atmosphereListener = new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
      JCheckBox source = (JCheckBox) e.getSource();
      renderMan.scene().setAtmosphereEnabled(source.isSelected());
    }
  };
  private final ActionListener volumetricFogListener = new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
      JCheckBox source = (JCheckBox) e.getSource();
      renderMan.scene().setVolumetricFogEnabled(source.isSelected());
    }
  };
  private final ActionListener cloudsEnabledListener = new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
      JCheckBox source = (JCheckBox) e.getSource();
      renderMan.scene().sky().setCloudsEnabled(source.isSelected());
    }
  };
  private final ActionListener v90Listener = new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
      if (v90Btn.isSelected()) {
        renderMan.scene().sky().setMirrored(true);
      }
    }
  };
  private final ActionListener v180Listener = new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
      if (v180Btn.isSelected()) {
        renderMan.scene().sky().setMirrored(false);
      }
    }
  };
  private final ActionListener biomeColorsCBListener = new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
      JCheckBox source = (JCheckBox) e.getSource();
      renderMan.scene().setBiomeColorsEnabled(source.isSelected());
    }
  };
  private final ActionListener emittersListener = new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
      renderMan.scene().setEmittersEnabled(enableEmitters.isSelected());
    }
  };
  private final ActionListener directLightListener = new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
      renderMan.scene().setDirectLight(directLight.isSelected());
    }
  };

  private int spp = 0;
  private int sps = 0;

  protected void updateWaterHeight() {
    int height = renderMan.scene().getWaterHeight();
    boolean waterWorld = height > 0;
    if (waterWorld) {
      waterHeightField.setText("" + height);
    }
    waterWorldCB.setSelected(height > 0);
    waterHeightField.setEnabled(height > 0);
  }

  protected void updateSkyRotation() {
    skymapRotationSlider.removeChangeListener(skyRotationListener);
    skymapRotationSlider.setValue((int) FastMath.round(
        100 * renderMan.scene().sky().getRotation() / (2 * Math.PI)));
    skymapRotationSlider.addChangeListener(skyRotationListener);
    skyboxRotationSlider.removeChangeListener(skyRotationListener);
    skyboxRotationSlider.setValue((int) FastMath.round(
        100 * renderMan.scene().sky().getRotation() / (2 * Math.PI)));
    skyboxRotationSlider.addChangeListener(skyRotationListener);
  }

  private void updateSkyGradient() {
    gradientEditor.removeGradientListener(gradientListener);
    gradientEditor.setGradient(renderMan.scene().sky().getGradient());
    gradientEditor.addGradientListener(gradientListener);
  }

  protected void updateProjectionMode() {
    projectionMode.removeActionListener(projectionModeListener);
    ProjectionMode mode = renderMan.scene().camera().getProjectionMode();
    projectionMode.setSelectedItem(mode);
    projectionMode.addActionListener(projectionModeListener);
  }

  protected void updateSkyMode() {
    skyModeCB.removeActionListener(skyModeListener);
    SkyMode mode = renderMan.scene().sky().getSkyMode();
    skyModeCB.setSelectedItem(mode);
    simulatedSkyPanel.setVisible(mode == SkyMode.SIMULATED);
    skymapPanel.setVisible(mode == SkyMode.SKYMAP_PANORAMIC);
    lightProbePanel.setVisible(mode == SkyMode.SKYMAP_SPHERICAL);
    skyGradientPanel.setVisible(mode == SkyMode.GRADIENT);
    skyboxPanel.setVisible(mode == SkyMode.SKYBOX);
    skyModeCB.addActionListener(skyModeListener);
  }

  protected void updateCanvasSizeField() {
    canvasSizeCB.removeActionListener(canvasSizeListener);
    canvasSizeCB.setSelectedItem("" + renderMan.scene().canvasWidth() +
        "x" + renderMan.scene().canvasHeight());
    canvasSizeCB.addActionListener(canvasSizeListener);
  }

  protected void updateSaveDumpsCheckBox() {
    saveDumpsCB.removeActionListener(saveDumpsListener);
    saveDumpsCB.setSelected(renderMan.scene().shouldSaveDumps());
    saveDumpsCB.addActionListener(saveDumpsListener);
  }

  protected void updateDumpFrequencyField() {
    dumpFrequencyCB.removeActionListener(dumpFrequencyListener);
    try {
      dumpFrequencyCB.setEnabled(renderMan.scene().shouldSaveDumps());
      saveSnapshotsCB.setEnabled(renderMan.scene().shouldSaveDumps());
      int frequency = renderMan.scene().getDumpFrequency();
      for (int i = 0; i < dumpFrequencies.length; ++i) {
        if (frequency == dumpFrequencies[i]) {
          dumpFrequencyCB.setSelectedIndex(i);
          return;
        }
      }
      dumpFrequencyCB.setSelectedItem(Integer.toString(frequency));
    } finally {
      dumpFrequencyCB.addActionListener(dumpFrequencyListener);
    }
  }

  protected void updateSaveSnapshotCheckBox() {
    saveSnapshotsCB.removeActionListener(saveSnapshotListener);
    try {
      saveSnapshotsCB.setSelected(renderMan.scene().shouldSaveSnapshots());
    } finally {
      saveSnapshotsCB.addActionListener(saveSnapshotListener);
    }
  }

  protected void updateSceneNameField() {
    sceneNameField.getDocument().removeDocumentListener(sceneNameListener);
    sceneNameField.removeActionListener(sceneNameActionListener);
    sceneNameField.setText(renderMan.scene().name());
    sceneNameField.getDocument().addDocumentListener(sceneNameListener);
    sceneNameField.addActionListener(sceneNameActionListener);
  }

  protected void updateSPPTargetField() {
    sppTargetField.getDocument().removeDocumentListener(sppTargetListener);
    sppTargetField.setText("" + renderMan.scene().getTargetSPP());
    sppTargetField.getDocument().addDocumentListener(sppTargetListener);
  }

  protected void updatePostprocessCB() {
    postprocessCB.setSelectedIndex(renderMan.scene().getPostprocess().ordinal());
  }

  protected void updateCustomPresets() {
    customPreset.removeAllItems();
    JsonObject presets = renderMan.scene().getCameraPresets();
    for (JsonMember member : presets.getMemberList()) {
      String name = member.getName().trim();
      if (!name.isEmpty()) {
        customPreset.addItem(name);
      }
    }
  }

  protected void updateCameraPosition() {
    cameraX.removeActionListener(cameraPositionListener);
    cameraY.removeActionListener(cameraPositionListener);
    cameraZ.removeActionListener(cameraPositionListener);

    Vector3d pos = renderMan.scene().camera().getPosition();
    cameraX.setText(decimalFormat.format(pos.x));
    cameraY.setText(decimalFormat.format(pos.y));
    cameraZ.setText(decimalFormat.format(pos.z));

    cameraX.addActionListener(cameraPositionListener);
    cameraY.addActionListener(cameraPositionListener);
    cameraZ.addActionListener(cameraPositionListener);
  }

  protected void updateCameraDirection() {
    cameraRoll.removeActionListener(cameraDirectionListener);
    cameraPitch.removeActionListener(cameraDirectionListener);
    cameraYaw.removeActionListener(cameraDirectionListener);

    double roll = QuickMath.radToDeg(renderMan.scene().camera().getRoll());
    double pitch = QuickMath.radToDeg(renderMan.scene().camera().getPitch());
    double yaw = QuickMath.radToDeg(renderMan.scene().camera().getYaw());

    cameraRoll.setText(decimalFormat.format(roll));
    cameraPitch.setText(decimalFormat.format(pitch));
    cameraYaw.setText(decimalFormat.format(yaw));

    cameraRoll.addActionListener(cameraDirectionListener);
    cameraPitch.addActionListener(cameraDirectionListener);
    cameraYaw.addActionListener(cameraDirectionListener);
  }

  /**
   * Load the scene with the given name
   * @param sceneName The name of the scene to load
   */
  public void loadScene(String sceneName) {
    sceneMan.loadScene(sceneName);
  }

  /**
   * Called when the current scene has been saved
   */
  @Override
  public void sceneSaved() {
    updateTitle();
  }

  @Override
  public void onStrafeLeft() {
    renderMan.scene().camera().strafeLeft(
        chunky.getShiftModifier() ? .1 : 1);
        updateCameraPosition();
  }

  @Override
  public void onStrafeRight() {
    renderMan.scene().camera().strafeRight(
        chunky.getShiftModifier() ? .1 : 1);
    updateCameraPosition();
  }

  @Override
  public void onMoveForward() {
    renderMan.scene().camera().moveForward(
        chunky.getShiftModifier() ? .1 : 1);
    updateCameraPosition();
  }

  @Override
  public void onMoveBackward() {
    renderMan.scene().camera().moveBackward(
        chunky.getShiftModifier() ? .1 : 1);
    updateCameraPosition();
  }

  @Override
  public void onMoveForwardFar() {
    renderMan.scene().camera().moveForward(100);
    updateCameraPosition();
  }

  @Override
  public void onMoveBackwardFar() {
    renderMan.scene().camera().moveBackward(100);
    updateCameraPosition();
  }

  @Override
  public void onMoveUp() {
    renderMan.scene().camera().moveUp(
        chunky.getShiftModifier() ? .1 : 1);
    updateCameraPosition();
  }

  @Override
  public void onMoveDown() {
    renderMan.scene().camera().moveDown(
        chunky.getShiftModifier() ? .1 : 1);
    updateCameraPosition();
  }

  @Override
  public void onMouseDragged(int dx, int dy) {
    renderMan.scene().camera().rotateView(
        - (Math.PI / 250) * dx,
        (Math.PI / 250) * dy);
    updateCameraDirection();
  }

  /**
   * Set the name of the current scene
   * @param sceneName
   */
  public void setSceneName(String sceneName) {
    renderMan.scene().setName(sceneName);
    sceneNameField.setText(renderMan.scene().name());
    updateTitle();
  }

  /**
   * Load the given chunks and center the camera.
   * @param world
   * @param chunks
   */
  public void loadFreshChunks(World world, Collection<ChunkPosition> chunks) {
    sceneMan.loadFreshChunks(world, chunks);
  }

  /**
   * Update the Show/Hide 3D view button.
   * @param visible
   */
  @Override
  public void setViewVisible(boolean visible) {
    if (visible) {
      showPreviewBtn.setText("Hide Preview Window");
      showPreviewBtn.setToolTipText("Hide the preview window");
    } else {
      showPreviewBtn.setText("Show Preview Window");
      showPreviewBtn.setToolTipText("Show the preview window");
    }
  }

  protected void setCanvasSize(int width, int height) {
    renderMan.scene().setCanvasSize(width, height);
    int canvasWidth = renderMan.scene().canvasWidth();
    int canvasHeight = renderMan.scene().canvasHeight();
    canvasSizeCB.setSelectedItem("" + canvasWidth + "x" + canvasHeight);
    view.setCanvasSize(canvasWidth, canvasHeight);
  }

  /**
   * Method to notify the render controls dialog that a scene has been loaded.
   * Causes canvas size to be updated. Can be called from outside EDT.
   */
  @Override
  public void sceneLoaded() {
    SwingUtilities.invokeLater(new Runnable() {
      @Override
      public void run() {
        updateAllSettings();

        showPreviewWindow();
      }
    });
  }

  protected void updateAllSettings() {
    skyHorizonOffset.update();
    dof.update();
    fov.update();
    subjectDistance.update();
    updateProjectionMode();
    updateSkyGradient();
    updateSkyMode();
    updateCanvasSizeField();
    emitterIntensity.update();
    skyLight.update();
    sunIntensity.update();
    sunAzimuth.update();
    sunAltitude.update();
    updateStillWater();
    updateClearWater();
    updateSkyRotation();
    updateVerticalResolution();
    updateBiomeColorsCB();
    updateAtmosphereCheckBox();
    updateVolumetricFogCheckBox();
    updateCloudsEnabledCheckBox();
    updateTitle();
    exposure.update();
    updateSaveDumpsCheckBox();
    updateSaveSnapshotCheckBox();
    updateDumpFrequencyField();
    updateSPPTargetField();
    updateSceneNameField();
    updatePostprocessCB();
    cloudSize.update();
    cloudXOffset.update();
    cloudYOffset.update();
    cloudZOffset.update();
    rayDepth.update();
    updateWaterHeight();
    updateCameraDirection();
    updateCameraPosition();
    updateCustomPresets();
    enableEmitters.setSelected(renderMan.scene().getEmittersEnabled());
    directLight.setSelected(renderMan.scene().getDirectLight());
    stopRenderBtn.setEnabled(true);
  }

  /**
   * Make sure the preview window is visible
   */
  public void showPreviewWindow() {
    view.showView(renderMan.scene().canvasWidth(),
        renderMan.scene().canvasHeight(),
        this);
  }

  /**
   * Update render time status label
   * @param time Total render time in milliseconds
   */
  @Override
  public void setRenderTime(long time) {
    if (renderTimeLbl == null)
      return;

    int seconds = (int) ((time / 1000) % 60);
    int minutes = (int) ((time / 60000) % 60);
    int hours = (int) (time / 3600000);
    renderTimeLbl.setText(String.format(
        "Render time: %d hours, %d minutes, %d seconds",
        hours, minutes, seconds));
  }

  /**
   * Update samples per second status label
   * @param sps Samples per second
   */
  @Override
  public void setSamplesPerSecond(int sps) {
    this.sps = sps;
    updateSPPLbl();
  }

  /**
   * Update SPP status label
   * @param spp Samples per pixel
   */
  @Override
  public void setSPP(int spp) {
    this.spp = spp;
    updateSPPLbl();
  }

  private void updateSPPLbl() {
    if (sppLbl != null) {
      sppLbl.setText(decimalFormat.format(spp) + " SPP, " +
          decimalFormat.format(sps) + " SPS");
    }
  }

  @Override
  public void setProgress(String task, int done, int start, int target) {
    if (progressBar != null && progressLbl != null && etaLbl != null) {
      progressLbl.setText(String.format("%s: %s of %s",
          task, decimalFormat.format(done), decimalFormat.format(target)));
      progressLbl.repaint();
      progressBar.setMinimum(start);
      progressBar.setMaximum(target);
      progressBar.setValue(Math.min(target, done));
      progressBar.repaint();
      etaLbl.setText("ETA: N/A");
    }
  }

  @Override
  public void setProgress(String task, int done, int start, int target, String eta) {
    if (progressBar != null && progressLbl != null && etaLbl != null) {
      setProgress(task, done, start, target);
      etaLbl.setText("ETA: " + eta);
    }
  }

  @Override
  public void taskAborted(String task) {
    // TODO add abort notice
    etaLbl.setText("ETA: N/A");
  }

  @Override
  public void taskFailed(String task) {
    // TODO add abort notice
    etaLbl.setText("ETA: N/A");
  }

  @Override
  public void onZoom(int diff) {
    Camera camera = renderMan.scene().camera();
    double value = renderMan.scene().camera().getFoV();
    double scale = camera.getMaxFoV() - camera.getMinFoV();
    double offset = value/scale;
    double newValue = scale*Math.exp(Math.log(offset) + 0.1*diff);
    if (!Double.isNaN(newValue) && !Double.isInfinite(newValue)) {
      renderMan.scene().camera().setFoV(newValue);
    }
    fov.update();
  }

  /**
   * @return The render context for this Render Controls dialog
   */
  public RenderContext getContext() {
    return context;
  }

  @Override
  public void renderStateChanged(boolean pathTrace, boolean paused) {
    if (pathTrace) {
      if (paused) {
        startRenderBtn.setText("RESUME");
        startRenderBtn.setIcon(Icon.play.imageIcon());
      } else {
        startRenderBtn.setText("PAUSE");
        startRenderBtn.setIcon(Icon.pause.imageIcon());
      }
      stopRenderBtn.setEnabled(true);
      stopRenderBtn.setForeground(Color.red);
    } else {
      startRenderBtn.setText("START");
      startRenderBtn.setIcon(Icon.play.imageIcon());
      stopRenderBtn.setEnabled(false);
      stopRenderBtn.setForeground(Color.black);
    }
  }

  @Override
  public void chunksLoaded() {
    SwingUtilities.invokeLater(new Runnable() {
      @Override
      public void run() {
        updateCameraPosition();
        showPreviewWindow();
      }
    });
  }

  @Override
  public void renderJobFinished(long time, int sps) {
    if (shutdownWhenDoneCB.isSelected()) {
      new ShutdownAlert(this);
    }
  }

  protected int getDumpFrequency() {
    int index = dumpFrequencyCB.getSelectedIndex();
    if (index != -1) {
      index = Math.max(0, index);
      index = Math.min(dumpFrequencies.length-1, index);
      return dumpFrequencies[index];
    } else {
      try {
        return Integer.valueOf((String) dumpFrequencyCB.getSelectedItem());
      } catch (NumberFormatException e) {
        return 0;
      }
    }
  }
  static class SkymapTextureLoader implements ActionListener {
    private final RenderManager renderMan;
    private static String defaultDirectory = System.getProperty("user.dir");
    public SkymapTextureLoader(RenderManager renderMan) {
      this.renderMan = renderMan;
    }
    @Override
    public void actionPerformed(ActionEvent e) {
      CenteredFileDialog fileDialog =
          new CenteredFileDialog(null, "Open Skymap", FileDialog.LOAD);
      String directory;
      synchronized (SkyboxTextureLoader.class) {
        directory = defaultDirectory;
      }
      fileDialog.setDirectory(directory);
      fileDialog.setFilenameFilter(
          new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
              return name.toLowerCase().endsWith(".png")
                  || name.toLowerCase().endsWith(".jpg")
                  || name.toLowerCase().endsWith(".hdr")
                  || name.toLowerCase().endsWith(".pfm");
            }
          });
      fileDialog.setVisible(true);
      File selectedFile = fileDialog.getSelectedFile();
      if (selectedFile != null) {
        synchronized (SkyboxTextureLoader.class) {
          File parent = selectedFile.getParentFile();
          if (parent != null) {
            defaultDirectory = parent.getAbsolutePath();
          }
        }
        renderMan.scene().sky().loadSkymap(selectedFile.getAbsolutePath());
      }
    }
  };

  static class SkyboxTextureLoader implements ActionListener {
    private final RenderManager renderMan;
    private final int textureIndex;
    private static String defaultDirectory = System.getProperty("user.dir");
    public SkyboxTextureLoader(RenderManager renderMan, int textureIndex) {
      this.renderMan = renderMan;
      this.textureIndex = textureIndex;
    }
    @Override
    public void actionPerformed(ActionEvent e) {
      CenteredFileDialog fileDialog =
          new CenteredFileDialog(null, "Open Skybox Texture", FileDialog.LOAD);
      String directory;
      synchronized (SkyboxTextureLoader.class) {
        directory = defaultDirectory;
      }
      fileDialog.setDirectory(directory);
      fileDialog.setFilenameFilter(
          new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
              return name.toLowerCase().endsWith(".png")
                  || name.toLowerCase().endsWith(".jpg")
                  || name.toLowerCase().endsWith(".hdr")
                  || name.toLowerCase().endsWith(".pfm");
            }
          });
      fileDialog.setVisible(true);
      File selectedFile = fileDialog.getSelectedFile();
      if (selectedFile != null) {
        synchronized (SkyboxTextureLoader.class) {
          File parent = selectedFile.getParentFile();
          if (parent != null) {
            defaultDirectory = parent.getAbsolutePath();
          }
        }
        renderMan.scene().sky().loadSkyboxTexture(selectedFile.getAbsolutePath(), textureIndex);
      }
    }
  };

  protected AtomicBoolean resetConfirmMutex = new AtomicBoolean(false);

  @Override
  public void renderResetPrevented() {
    SwingUtilities.invokeLater(new Runnable() {
      @Override
      public void run() {
        if (resetConfirmMutex.compareAndSet(false, true)) {
          new ConfirmResetPopup(RenderControls.this, new AcceptOrRejectListener() {
            @Override
            public void onAccept() {
              renderMan.scene().resetRender();
              resetConfirmMutex.set(false);
            }

            @Override
            public void onReject() {
              renderMan.revertPendingSceneChanges();
              updateAllSettings();
              resetConfirmMutex.set(false);
            }
          });
        }
      }
    });
  }
}
TOP

Related Classes of se.llbit.chunky.renderer.ui.RenderControls

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.