mirror of
				https://bitbucket.org/mfeemster/fractorium.git
				synced 2025-10-31 09:20:23 -04:00 
			
		
		
		
	 fcd060976c
			
		
	
	fcd060976c
	
	
	
		
			
			-Support 4k monitors, and in general, properly scale any monitor that is not HD. -Allow for a spatial filter of radius zero, which means do not use a spatial filter. -Add new variations: concentric, cpow3, helicoid, helix, rand_cubes, sphereblur. -Use a new method for computing elliptic which is more precise. Developed by Discord user Claude. -Remove the 8 variation per xform limitation on the GPU. -Allow for loading the last flame file on startup, rather than randoms. -Use two different default quality values in the interactive renderer, one each for CPU and GPU. -Creating linked xforms was using non-standard behavior. Make it match Apo and also support creating multiple linked xforms at once. --Bug fixes -No variations in an xform used to have the same behavior as a single linear variation with weight 1. While sensible, this breaks backward compatibility. No variations now sets the output point to zeroes. -Prevent crashing the program when adjusting a value on the main window while a final render is in progress. -The xaos table was inverted. --Code changes -Convert projects to Visual Studio 2017. -Change bad vals from +- 1e10 to +-1e20. -Reintroduce the symmetry tag in xforms for legacy support in programs that do not use color_speed. -Compiler will not let us use default values in templated member functions anymore.
		
			
				
	
	
		
			415 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			415 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| #include "FractoriumPch.h"
 | |
| #include "FractoriumEmberController.h"
 | |
| #include "Fractorium.h"
 | |
| #include "GLEmberController.h"
 | |
| 
 | |
| /// <summary>
 | |
| /// Constructor which initializes the non-templated members contained in this class.
 | |
| /// The renderer, other templated members and GUI setup will be done in the templated derived controller class.
 | |
| /// </summary>
 | |
| /// <param name="fractorium">Pointer to the main window.</param>
 | |
| FractoriumEmberControllerBase::FractoriumEmberControllerBase(Fractorium* fractorium)
 | |
| 	: m_PaletteList(PaletteList<float>::Instance())
 | |
| {
 | |
| 	Timing t;
 | |
| 	m_Fractorium = fractorium;
 | |
| 	m_Rand = QTIsaac<ISAAC_SIZE, ISAAC_INT>(ISAAC_INT(t.Tic()), ISAAC_INT(t.Tic() * 2), ISAAC_INT(t.Tic() * 3));//Ensure a different rand seed on each instance.
 | |
| 	m_RenderTimer = make_unique<QTimer>(m_Fractorium);
 | |
| 	m_RenderTimer->setInterval(0);
 | |
| 	m_Fractorium->connect(m_RenderTimer.get(), SIGNAL(timeout()), SLOT(IdleTimer()));
 | |
| 	m_RenderRestartTimer = make_unique<QTimer>(m_Fractorium);
 | |
| 	m_Fractorium->connect(m_RenderRestartTimer.get(), &QTimer::timeout, [&]() { m_Fractorium->StartRenderTimer(false); });//It's ok to pass false for the first shot because creating the controller will start the preview renders.
 | |
| }
 | |
| 
 | |
| /// <summary>
 | |
| /// Destructor which stops rendering and deletes the timers.
 | |
| /// All other memory is cleared automatically through the use of STL.
 | |
| /// </summary>
 | |
| FractoriumEmberControllerBase::~FractoriumEmberControllerBase()
 | |
| {
 | |
| 	StopRenderTimer(true);
 | |
| 	m_RenderTimer->stop();
 | |
| 	m_RenderRestartTimer->stop();
 | |
| }
 | |
| 
 | |
| /// <summary>
 | |
| /// Constructor which passes the main window parameter to the base, initializes the templated members contained in this class.
 | |
| /// Then sets up the parts of the GUI that require templated Widgets, such as the variations tree and the palette table.
 | |
| /// Note the renderer is not setup here automatically. Instead, it must be manually created by the caller later.
 | |
| /// </summary>
 | |
| /// <param name="fractorium">Pointer to the main window.</param>
 | |
| template <typename T>
 | |
| FractoriumEmberController<T>::FractoriumEmberController(Fractorium* fractorium)
 | |
| 	: FractoriumEmberControllerBase(fractorium),
 | |
| 	  m_VariationList(VariationList<T>::Instance())
 | |
| {
 | |
| 	size_t b = 0;
 | |
| 	m_GLController = make_unique<GLEmberController<T>>(fractorium, fractorium->ui.GLDisplay, this);
 | |
| 	m_LibraryPreviewRenderer = make_unique<TreePreviewRenderer<T>>(this, m_Fractorium->ui.LibraryTree, m_EmberFile);
 | |
| 	m_SequencePreviewRenderer = make_unique<TreePreviewRenderer<T>>(this, m_Fractorium->ui.SequenceTree, m_SequenceFile);
 | |
| 	m_PaletteList->Clear();
 | |
| 	m_Fractorium->ui.PaletteFilenameCombo->clear();
 | |
| 	//Initial combo change event to fill the palette table will be called automatically later.
 | |
| 	//Look hard for a palette.
 | |
| 	auto paths = GetDefaultPaths();
 | |
| 
 | |
| 	for (auto& path : paths)
 | |
| 		b |= InitPaletteList(path);
 | |
| 
 | |
| 	if (b)
 | |
| 	{
 | |
| 		m_SheepTools = make_unique<SheepTools<T, float>>(m_PaletteList->Name(0), new EmberNs::Renderer<T, float>());
 | |
| 	}
 | |
| 	else
 | |
| 	{
 | |
| 		QString allPaths;
 | |
| 
 | |
| 		for (auto& path : paths)
 | |
| 			allPaths += path + "\r\n";
 | |
| 
 | |
| 		allPaths = QString("No palettes found in paths:\r\n") + allPaths + "\r\nExiting.";
 | |
| 		std::runtime_error ex(allPaths.toStdString());
 | |
| 		throw ex;
 | |
| 	}
 | |
| 
 | |
| 	if (m_PaletteList->Size() >= 1)//Only add the user palette if the folder already had a palette, which means we'll be using this folder.
 | |
| 		if (m_PaletteList->AddEmptyPaletteFile((GetDefaultUserPath() + "/user-palettes.xml").toStdString()))
 | |
| 			m_Fractorium->ui.PaletteFilenameCombo->addItem("user-palettes.xml");
 | |
| 
 | |
| 	BackgroundChanged(QColor(0, 0, 0));//Default to black.
 | |
| 	ClearUndo();
 | |
| }
 | |
| 
 | |
| /// <summary>
 | |
| /// Empty destructor that does nothing.
 | |
| /// </summary>
 | |
| template <typename T>
 | |
| FractoriumEmberController<T>::~FractoriumEmberController() { }
 | |
| 
 | |
| /// <summary>
 | |
| /// Setters for embers, ember files and palettes which convert between float and double types.
 | |
| /// These are used to preserve the current ember/file when switching between renderers.
 | |
| /// Note that some precision will be lost when going from double to float.
 | |
| /// </summary>
 | |
| template <typename T> void FractoriumEmberController<T>::SetEmber(const Ember<float>& ember, bool verbatim, bool updatePointer) { SetEmberPrivate<float>(ember, verbatim, updatePointer); }
 | |
| template <typename T> void FractoriumEmberController<T>::CopyEmber(Ember<float>& ember, std::function<void(Ember<float>& ember)> perEmberOperation) { ember = m_Ember; perEmberOperation(ember); }
 | |
| template <typename T> void FractoriumEmberController<T>::SetEmberFile(const EmberFile<float>& emberFile, bool move) { move ? m_EmberFile = std::move(emberFile) : m_EmberFile = emberFile; }
 | |
| template <typename T> void FractoriumEmberController<T>::CopyEmberFile(EmberFile<float>& emberFile, bool sequence, std::function<void(Ember<float>& ember)> perEmberOperation)
 | |
| {
 | |
| 	if (sequence)
 | |
| 	{
 | |
| 		emberFile.m_Filename = m_SequenceFile.m_Filename;
 | |
| 		CopyCont(emberFile.m_Embers, m_SequenceFile.m_Embers, perEmberOperation);
 | |
| 	}
 | |
| 	else
 | |
| 	{
 | |
| 		emberFile.m_Filename = m_EmberFile.m_Filename;
 | |
| 		CopyCont(emberFile.m_Embers, m_EmberFile.m_Embers, perEmberOperation);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| template <typename T> void FractoriumEmberController<T>::SetTempPalette(const Palette<float>& palette) { m_TempPalette = palette; }
 | |
| template <typename T> void FractoriumEmberController<T>::CopyTempPalette(Palette<float>& palette) { palette = m_TempPalette; }
 | |
| #ifdef DO_DOUBLE
 | |
| template <typename T> void FractoriumEmberController<T>::SetEmber(const Ember<double>& ember, bool verbatim, bool updatePointer) { SetEmberPrivate<double>(ember, verbatim, updatePointer); }
 | |
| template <typename T> void FractoriumEmberController<T>::CopyEmber(Ember<double>& ember, std::function<void(Ember<double>& ember)> perEmberOperation) { ember = m_Ember; perEmberOperation(ember); }
 | |
| template <typename T> void FractoriumEmberController<T>::SetEmberFile(const EmberFile<double>& emberFile, bool move) { move ? m_EmberFile = std::move(emberFile) : m_EmberFile = emberFile; }
 | |
| template <typename T> void FractoriumEmberController<T>::CopyEmberFile(EmberFile<double>& emberFile, bool sequence, std::function<void(Ember<double>& ember)> perEmberOperation)
 | |
| {
 | |
| 	if (sequence)
 | |
| 	{
 | |
| 		emberFile.m_Filename = m_SequenceFile.m_Filename;
 | |
| 		CopyCont(emberFile.m_Embers, m_SequenceFile.m_Embers, perEmberOperation);
 | |
| 	}
 | |
| 	else
 | |
| 	{
 | |
| 		emberFile.m_Filename = m_EmberFile.m_Filename;
 | |
| 		CopyCont(emberFile.m_Embers, m_EmberFile.m_Embers, perEmberOperation);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| template <typename T> void FractoriumEmberController<T>::SetTempPalette(const Palette<double>& palette) { m_TempPalette = palette; }
 | |
| template <typename T> void FractoriumEmberController<T>::CopyTempPalette(Palette<double>& palette) { palette = m_TempPalette; }
 | |
| #endif
 | |
| template <typename T> Ember<T>* FractoriumEmberController<T>::CurrentEmber() { return &m_Ember; }
 | |
| 
 | |
| template <typename T>
 | |
| void FractoriumEmberController<T>::ConstrainDimensions(Ember<T>& ember)
 | |
| {
 | |
| 	ember.m_FinalRasW = std::min<int>(m_Fractorium->ui.GLDisplay->MaxTexSize(), int(ember.m_FinalRasW));
 | |
| 	ember.m_FinalRasH = std::min<int>(m_Fractorium->ui.GLDisplay->MaxTexSize(), int(ember.m_FinalRasH));
 | |
| }
 | |
| 
 | |
| /// <summary>
 | |
| /// Set the ember at the specified index from the currently opened file as the current Ember.
 | |
| /// Clears the undo state.
 | |
| /// Resets the rendering process.
 | |
| /// </summary>
 | |
| /// <param name="index">The index in the file from which to retrieve the ember</param>
 | |
| /// <param name="verbatim">If true, do not overwrite temporal samples, quality or supersample value, else overwrite.</param>
 | |
| template <typename T>
 | |
| void FractoriumEmberController<T>::SetEmber(size_t index, bool verbatim)
 | |
| {
 | |
| 	if (index < m_EmberFile.Size())
 | |
| 	{
 | |
| 		m_Fractorium->SelectLibraryItem(index);
 | |
| 		ClearUndo();
 | |
| 		SetEmber(*m_EmberFile.Get(index), verbatim, true);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| /// <summary>
 | |
| /// Wrapper to call a function, then optionally add the requested action to the rendering queue.
 | |
| /// </summary>
 | |
| /// <param name="func">The function to call</param>
 | |
| /// <param name="updateRender">True to update renderer, else false. Default: true.</param>
 | |
| /// <param name="action">The action to add to the rendering queue. Default: eProcessAction::FULL_RENDER.</param>
 | |
| template <typename T>
 | |
| void FractoriumEmberController<T>::Update(std::function<void(void)> func, bool updateRender, eProcessAction action)
 | |
| {
 | |
| 	func();
 | |
| 
 | |
| 	if (updateRender)
 | |
| 		UpdateRender(action);
 | |
| }
 | |
| 
 | |
| /// <summary>
 | |
| /// Wrapper to call a function on the current ember and optionally all other embers in the file.
 | |
| /// Then optionally add the requested action to the rendering queue.
 | |
| /// </summary>
 | |
| /// <param name="func">The function to call</param>
 | |
| /// <param name="updateRender">True to update renderer, else false. Default: true.</param>
 | |
| /// <param name="action">The action to add to the rendering queue. Default: eProcessAction::FULL_RENDER.</param>
 | |
| /// <param name="applyAll">True to apply the action to all embers in the file in addition to the curent one, false to apply the action only to the current one.</param>
 | |
| template <typename T>
 | |
| void FractoriumEmberController<T>::UpdateAll(std::function<void(Ember<T>& ember)> func, bool updateRender, eProcessAction action, bool applyAll)
 | |
| {
 | |
| 	func(m_Ember);
 | |
| 
 | |
| 	if (applyAll)
 | |
| 		for (auto& it : m_EmberFile.m_Embers)
 | |
| 			func(it);
 | |
| 
 | |
| 	if (updateRender)
 | |
| 		UpdateRender(action);
 | |
| }
 | |
| 
 | |
| /// <summary>
 | |
| /// Wrapper to call a function on the specified xforms, then optionally add the requested action to the rendering queue.
 | |
| /// If no xforms are selected via the checkboxes, and the update type is UPDATE_SELECTED, then the function will be called only on the currently selected xform.
 | |
| /// If the update type is UPDATE_CURRENT_AND_SELECTED, and the current is not among those selected, then the function will be called on the currently selected xform as well.
 | |
| /// </summary>
 | |
| /// <param name="func">The function to call</param>
 | |
| /// <param name="updateType">Whether to apply this update operation on the current, all or selected xforms. Default: eXformUpdate::UPDATE_CURRENT.</param>
 | |
| /// <param name="updateRender">True to update renderer, else false. Default: true.</param>
 | |
| /// <param name="action">The action to add to the rendering queue. Default: eProcessAction::FULL_RENDER.</param>
 | |
| /// <param name="index">The xform index to use when action is eXformUpdate::UPDATE_SPECIFIC. Default: 0.</param>
 | |
| template <typename T>
 | |
| void FractoriumEmberController<T>::UpdateXform(std::function<void(Xform<T>*)> func, eXformUpdate updateType, bool updateRender, eProcessAction action, size_t index)
 | |
| {
 | |
| 	int i = 0;
 | |
| 	auto current = CurrentXform();
 | |
| 	bool forceFinal = m_Fractorium->HaveFinal();
 | |
| 	bool isCurrentFinal = m_Ember.IsFinalXform(current);
 | |
| 	bool doFinal = updateType != eXformUpdate::UPDATE_SELECTED_EXCEPT_FINAL && updateType != eXformUpdate::UPDATE_ALL_EXCEPT_FINAL;
 | |
| 
 | |
| 	switch (updateType)
 | |
| 	{
 | |
| 		case eXformUpdate::UPDATE_SPECIFIC:
 | |
| 		{
 | |
| 			if (auto xform = m_Ember.GetTotalXform(index, forceFinal))
 | |
| 				func(xform);
 | |
| 		}
 | |
| 		break;
 | |
| 
 | |
| 		case eXformUpdate::UPDATE_CURRENT:
 | |
| 		{
 | |
| 			if (current)
 | |
| 				func(current);
 | |
| 		}
 | |
| 		break;
 | |
| 
 | |
| 		case eXformUpdate::UPDATE_CURRENT_AND_SELECTED:
 | |
| 		{
 | |
| 			bool currentDone = false;
 | |
| 
 | |
| 			while (auto xform = m_Ember.GetTotalXform(i, forceFinal))
 | |
| 			{
 | |
| 				if (i < m_Fractorium->m_XformSelections.size())
 | |
| 				{
 | |
| 					if (auto w = m_Fractorium->m_XformSelections[i])
 | |
| 					{
 | |
| 						if (w->isChecked())
 | |
| 						{
 | |
| 							func(xform);
 | |
| 
 | |
| 							if (xform == current)
 | |
| 								currentDone = true;
 | |
| 						}
 | |
| 					}
 | |
| 				}
 | |
| 
 | |
| 				i++;
 | |
| 			}
 | |
| 
 | |
| 			if (!currentDone)//Current was not among those selected, so apply to it.
 | |
| 				func(current);
 | |
| 		}
 | |
| 		break;
 | |
| 
 | |
| 		case eXformUpdate::UPDATE_SELECTED:
 | |
| 		case eXformUpdate::UPDATE_SELECTED_EXCEPT_FINAL:
 | |
| 		{
 | |
| 			bool anyUpdated = false;
 | |
| 
 | |
| 			while (auto xform = (doFinal ? m_Ember.GetTotalXform(i, forceFinal) : m_Ember.GetXform(i)))
 | |
| 			{
 | |
| 				if (i < m_Fractorium->m_XformSelections.size())
 | |
| 				{
 | |
| 					if (auto w = m_Fractorium->m_XformSelections[i])
 | |
| 					{
 | |
| 						if (w->isChecked())
 | |
| 						{
 | |
| 							func(xform);
 | |
| 							anyUpdated = true;
 | |
| 						}
 | |
| 					}
 | |
| 				}
 | |
| 
 | |
| 				i++;
 | |
| 			}
 | |
| 
 | |
| 			if (!anyUpdated)//None were selected, so just apply to the current.
 | |
| 				if (doFinal || !isCurrentFinal)//If do final, call func regardless. If not, only call if current is not final.
 | |
| 					if (current)
 | |
| 						func(current);
 | |
| 		}
 | |
| 		break;
 | |
| 
 | |
| 		case eXformUpdate::UPDATE_ALL:
 | |
| 		{
 | |
| 			while (auto xform = m_Ember.GetTotalXform(i++, forceFinal))
 | |
| 				func(xform);
 | |
| 		}
 | |
| 		break;
 | |
| 
 | |
| 		case eXformUpdate::UPDATE_ALL_EXCEPT_FINAL:
 | |
| 		default:
 | |
| 		{
 | |
| 			while (auto xform = m_Ember.GetXform(i++))
 | |
| 				func(xform);
 | |
| 		}
 | |
| 		break;
 | |
| 	}
 | |
| 
 | |
| 	if (updateRender)
 | |
| 		UpdateRender(action);
 | |
| }
 | |
| 
 | |
| /// <summary>
 | |
| /// Set the current ember, but use GUI values for the fields which make sense to
 | |
| /// keep the same between ember selection changes.
 | |
| /// Note the extra template parameter U allows for assigning ember of different types.
 | |
| /// Resets the rendering process.
 | |
| /// </summary>
 | |
| /// <param name="ember">The ember to set as the current</param>
 | |
| /// <param name="verbatim">If true, do not overwrite temporal samples, quality or supersample value, else overwrite.</param>
 | |
| /// <param name="updatePointer">If true, update the current ember pointer to the address of the one passed in.</param>
 | |
| template <typename T>
 | |
| template <typename U>
 | |
| void FractoriumEmberController<T>::SetEmberPrivate(const Ember<U>& ember, bool verbatim, bool updatePointer)
 | |
| {
 | |
| 	if (ember.m_Name != m_Ember.m_Name)
 | |
| 		m_LastSaveCurrent = "";
 | |
| 
 | |
| 	size_t w = m_Ember.m_FinalRasW;//Cache values for use below.
 | |
| 	size_t h = m_Ember.m_FinalRasH;
 | |
| 	m_Ember = ember;
 | |
| 
 | |
| 	if (updatePointer)
 | |
| 		m_EmberFilePointer = &ember;
 | |
| 
 | |
| 	if (!verbatim)
 | |
| 	{
 | |
| 		m_Ember.m_TemporalSamples = 1;//Change once animation is supported.
 | |
| 		m_Ember.m_Quality = m_Fractorium->m_QualitySpin->value();
 | |
| 		m_Ember.m_Supersample = m_Fractorium->m_SupersampleSpin->value();
 | |
| 	}
 | |
| 
 | |
| 	static EmberToXml<T> writer;//Save parameters of last full render just in case there is a crash.
 | |
| 	auto path = GetDefaultUserPath();
 | |
| 	QDir dir(path);
 | |
| 
 | |
| 	if (!dir.exists())
 | |
| 		dir.mkpath(".");
 | |
| 
 | |
| 	string filename = path.toStdString() + "/last.flame";
 | |
| 	writer.Save(filename, m_Ember, 0, true, true, false, true, true);
 | |
| 	m_GLController->ResetMouseState();
 | |
| 	FillXforms();//Must do this first because the palette setup in FillParamTablesAndPalette() uses the xforms combo.
 | |
| 	FillParamTablesAndPalette();
 | |
| 	FillCurvesControl();
 | |
| 	FillSummary();
 | |
| 
 | |
| 	//If a resize happened, this won't do anything because the new size is not reflected in the scroll area yet.
 | |
| 	//However, it will have been taken care of in SyncSizes() in that case, so it's ok.
 | |
| 	//This is for when a new ember with the same size was loaded. If it was larger than the scroll area, and was scrolled, re-center it.
 | |
| 	if (m_Ember.m_FinalRasW == w && m_Ember.m_FinalRasH == h)
 | |
| 		m_Fractorium->CenterScrollbars();
 | |
| }
 | |
| 
 | |
| /// <summary>
 | |
| /// Thin derivation to handle preview rendering multiple embers previews to a tree.
 | |
| /// </summary>
 | |
| /// <param name="start">The 0-based index to start rendering previews for</param>
 | |
| /// <param name="end">The 0-based index which is one beyond the last ember to render a preview for</param>
 | |
| template <typename T>
 | |
| void TreePreviewRenderer<T>::PreviewRenderFunc(uint start, uint end)
 | |
| {
 | |
| 	auto f = m_Controller->m_Fractorium;
 | |
| 	m_PreviewRenderer.EarlyClip(f->m_Settings->EarlyClip());
 | |
| 	m_PreviewRenderer.YAxisUp(f->m_Settings->YAxisUp());
 | |
| 	m_PreviewRenderer.ThreadCount(std::max(1u, Timing::ProcessorCount() - 1));//Leave one processor free so the GUI can breathe.
 | |
| 
 | |
| 	if (auto top = m_Tree->topLevelItem(0))
 | |
| 	{
 | |
| 		size_t i = start;
 | |
| 
 | |
| 		for (auto b = Advance(m_EmberFile.m_Embers.begin(), start); m_PreviewRun && i < end && b != m_EmberFile.m_Embers.end(); ++b, ++i)
 | |
| 		{
 | |
| 			m_PreviewEmber = *b;
 | |
| 			m_PreviewEmber.SyncSize();
 | |
| 			m_PreviewEmber.SetSizeAndAdjustScale(PREVIEW_SIZE, PREVIEW_SIZE, false, eScaleType::SCALE_WIDTH);
 | |
| 			m_PreviewEmber.m_TemporalSamples = 1;
 | |
| 			m_PreviewEmber.m_Quality = 25;
 | |
| 			m_PreviewEmber.m_Supersample = 1;
 | |
| 			m_PreviewRenderer.SetEmber(m_PreviewEmber);
 | |
| 
 | |
| 			if (m_PreviewRenderer.Run(m_PreviewFinalImage) == eRenderStatus::RENDER_OK)
 | |
| 			{
 | |
| 				if (auto treeItem = dynamic_cast<EmberTreeWidgetItemBase*>(top->child(int(i))))
 | |
| 				{
 | |
| 					//It is critical that Qt::BlockingQueuedConnection is passed because this is running on a different thread than the UI.
 | |
| 					//This ensures the events are processed in order as each preview is updated, and that control does not return here
 | |
| 					//until the update is complete.
 | |
| 					QMetaObject::invokeMethod(f, "SetLibraryTreeItemData", Qt::BlockingQueuedConnection,
 | |
| 											  Q_ARG(EmberTreeWidgetItemBase*, treeItem),
 | |
| 											  Q_ARG(vv4F&, m_PreviewFinalImage),
 | |
| 											  Q_ARG(uint, PREVIEW_SIZE),
 | |
| 											  Q_ARG(uint, PREVIEW_SIZE));
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| template class FractoriumEmberController<float>;
 | |
| template class PreviewRenderer<float>;
 | |
| template class TreePreviewRenderer<float>;
 | |
| 
 | |
| #ifdef DO_DOUBLE
 | |
| 	template class FractoriumEmberController<double>;
 | |
| 	template class PreviewRenderer<double>;
 | |
| 	template class TreePreviewRenderer<double>;
 | |
| #endif
 |